mls-parser 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mls_parser/__init__.py ADDED
@@ -0,0 +1 @@
1
+ version = "0.0.2"
mls_parser/__main__.py ADDED
@@ -0,0 +1,67 @@
1
+ """
2
+ __main__.py
3
+
4
+ Layout Model Sheet Parser
5
+ """
6
+
7
+ # System
8
+ import logging
9
+ import logging.config
10
+ import sys
11
+ import argparse
12
+ from pathlib import Path
13
+
14
+ # Parser
15
+ from mls_parser import version
16
+ from mls_parser.layout_parser import LayoutParser
17
+
18
+ _logpath = Path("mls_parser.log")
19
+
20
+ def get_logger():
21
+ """Initiate the logger"""
22
+ log_conf_path = Path(__file__).parent / 'log.conf' # Logging configuration is in this file
23
+ logging.config.fileConfig(fname=log_conf_path, disable_existing_loggers=False)
24
+ return logging.getLogger(__name__) # Create a logger for this module
25
+
26
+ # Configure the expected parameters and actions for the argparse module
27
+ def parse(cl_input):
28
+ """
29
+ The command line interface is for diagnostic purposes
30
+
31
+ :param cl_input:
32
+ :return:
33
+ """
34
+ parser = argparse.ArgumentParser(description='Model layout parser')
35
+ parser.add_argument('cmfile', nargs='?', action='store',
36
+ help='Model layout file name with .mls extension')
37
+ parser.add_argument('-D', '--debug', action='store_true',
38
+ help='Debug mode'),
39
+ parser.add_argument('-V', '--version', action='store_true',
40
+ help='Print the current version of parser')
41
+ return parser.parse_args(cl_input)
42
+
43
+
44
+ def main():
45
+ # Start logging
46
+ logger = get_logger()
47
+ logger.info(f'Model layout parser version: {version}')
48
+
49
+ # Parse the command line args
50
+ args = parse(sys.argv[1:])
51
+
52
+ if args.version:
53
+ # Just print the version and quit
54
+ print(f'Model layout parser version: {version}')
55
+ sys.exit(0)
56
+
57
+ if args.cmfile:
58
+ fpath = Path(args.cmfile)
59
+ d = args.debug
60
+ result = LayoutParser.parse_file(file_input=fpath, debug=d)
61
+
62
+ logger.info("No problemo") # We didn't die on an exception, basically
63
+ print("\nNo problemo")
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
@@ -0,0 +1,100 @@
1
+ """
2
+ exceptions.py – Model layout parser specific exceptions
3
+ """
4
+
5
+ # Every error should have the same format
6
+ # with a standard prefix and postfix defined here
7
+ pre = "\nModel layout parser: ["
8
+ post = "]"
9
+
10
+
11
+ class MLSException(Exception):
12
+ pass
13
+
14
+ class MLSUserInputException(MLSException):
15
+ pass
16
+
17
+ class MLSIOException(MLSException):
18
+ pass
19
+
20
+ class LayoutParseError(MLSUserInputException):
21
+ def __init__(self, model_file, e):
22
+ self.model_file = model_file
23
+ self.e = e
24
+
25
+ def __str__(self):
26
+ return f'{pre}Parse error in layout "{self.model_file}"\n\t{self.e}"{post}'
27
+
28
+ class LayoutInputFileOpen(MLSIOException):
29
+ def __init__(self, path):
30
+ self.path = path
31
+
32
+ def __str__(self):
33
+ return f'{pre}Parser cannot open this layout file: "{self.path}"{post}'
34
+
35
+ class LayoutInputFileEmpty(MLSIOException):
36
+ def __init__(self, path):
37
+ self.path = path
38
+
39
+ def __str__(self):
40
+ return f'{pre}For some reason, nothing was read from the layout file: "{self.path}"{post}'
41
+
42
+ class LayoutGrammarFileOpen(MLSIOException):
43
+ def __init__(self, path):
44
+ self.path = path
45
+
46
+ def __str__(self):
47
+ return f'{pre}Parser cannot open this layout grammar file: "{self.path}"{post}'
48
+
49
+ class MultipleFloatsInSameStraightConnector(MLSException):
50
+ def __init__(self, name):
51
+ self.name = name
52
+
53
+ def __str__(self):
54
+ return f'{pre}Straight connector "{self.name}" has two floating anchors (*). Specify one.{post}'
55
+
56
+ class MultipleFloatsInSameBranch(MLSException):
57
+ def __init__(self, branch):
58
+ self.branch = branch
59
+
60
+ def __str__(self):
61
+ return f'{pre}There may be at most one floating anchor (*) per branch: "{self.branch}"{post}'
62
+
63
+ class ConflictingGraftFloat(MLSException):
64
+ def __init__(self, stem):
65
+ self.stem = stem
66
+
67
+ def __str__(self):
68
+ return f'{pre}A floating anchor(*) may not graft (>, >>): "{self.stem}"{post}'
69
+
70
+ class MultipleGraftsInSameBranch(MLSException):
71
+ def __init__(self, branch):
72
+ self.branch = branch
73
+
74
+ def __str__(self):
75
+ return f'{pre}There may be at most one graft (>, >>) per branch: "{self.branch}"{post}'
76
+
77
+ class TrunkLeafGraftConflict(MLSException):
78
+ def __str__(self):
79
+ return f'{pre}Leaf may not graft locally (>) if Trunk is grafting (>) {post}'
80
+
81
+ class ExternalLocalGraftConflict(MLSException):
82
+ def __init__(self, branch):
83
+ self.branch = branch
84
+
85
+ def __str__(self):
86
+ return f'{pre}Branch has local (>) graft with conflicting external graft (>>) in preceding branch: "{self.branch}"{post}'
87
+
88
+ class ExternalGraftOnLastBranch(MLSException):
89
+ def __init__(self, branch):
90
+ self.branch = branch
91
+
92
+ def __str__(self):
93
+ return f'{pre}Last branch in tree layout has a superfluous external (>>) graft: "{self.branch}"{post}'
94
+
95
+ class GraftRutBranchConflict(MLSException):
96
+ def __init__(self, branch):
97
+ self.branch = branch
98
+
99
+ def __str__(self):
100
+ return f'{pre}A rut branch, with (: Ln[R+/-n]), may not include a local graft(>): "{self.branch}"{post}'
mls_parser/layout.peg ADDED
@@ -0,0 +1,109 @@
1
+ // Flatland Model Layout Arpeggio Clean Peg Grammar
2
+
3
+ // This grammar expresses the geometric layout of nodes and connectors and where
4
+ // connectors attach to node faces on a diagram. It does not specify any meaning
5
+ // associated with the nodes and connectors. Node and connector names are ascribed
6
+ // in their model files, but no other information in the model files is referenced
7
+ // in this grammar.
8
+
9
+ // You can draw unconnected nodes, connected nodes or a blank sheet
10
+ diagram_layout = EOL* layout_spec ((node_block connector_block) / (node_block))? EOF
11
+
12
+ // Layout spec
13
+ layout_spec = (diagram notation color? presentation sheet padding? orientation frame? frame_presentation?)# // Each of these in any order
14
+
15
+ // Diagram
16
+ diagram = "diagram" SP+ name EOL* // The type of diagram to draw
17
+ notation = "notation" SP+ name EOL* // The notation to use in the diagram
18
+ color = "color" SP+ name EOL* // The type of diagram to draw
19
+ presentation = "presentation" SP+ name EOL* // The presentation style for drawing
20
+ sheet = "sheet" SP+ name EOL* // The sheet size name
21
+ padding = "padding" (tpad? bpad? lpad? rpad?)# EOL*
22
+ tpad = SP+ 't'number
23
+ bpad = SP+ 'b'number
24
+ lpad = SP+ 'l'number
25
+ rpad = SP+ 'r'number
26
+ orientation = "orientation" SP+ ("portrait" / "landscape") EOL* // The sheet orientation
27
+ frame = "frame" SP+ name EOL* // The frame name
28
+ frame_presentation = "frame_presentation" SP+ name EOL* // The presentation style for the frame
29
+
30
+ // Node
31
+ node_block = nodes_header node_spec+
32
+ nodes_header = "nodes" EOL*
33
+ node_spec = INDENT node_name wrap? (SP+ node_width_expansion)? (SP+ node_height_expansion)? SP+ node_placement (SP+ color_tag)? EOL*
34
+ node_name = name
35
+ node_width_expansion = number '%' // Increase the width of the node
36
+ node_height_expansion = comp_height_expansion (SP+ comp_height_expansion)* // A node's height is the sum of its expanded compartments
37
+ comp_height_expansion = '[C' number ']' number '%' // Increase the height of a single compartment
38
+ node_placement = grid_place (SP+ ':' SP+ grid_place)* // One or more such locations
39
+ grid_place = node_loc (SP+ node_align)? // Location in grid where a node will appear
40
+ node_loc = span ',' span // Row and column
41
+ span = number '-' number / number
42
+ color_tag = '<' name '>'
43
+
44
+ // Connector
45
+ connector_block = connectors_header connector_layout+ // All
46
+ connectors_header = "connectors" EOL* // starts section where connector layout info is specified
47
+ connector_layout = INDENT cname_place? ( binary_layout / tree_layout / unary_layout ) EOL*
48
+ cname_place = dir? name wrap? bend? notch? csep // Side of connector axis and name of connector (since it is probably short)
49
+ bend = '.' number // Bend in connector where 1 is at the tstem side increasing to the pstem side
50
+
51
+ // Binary connector
52
+ binary_layout = tstem csep pstem tertiary_node? (csep paths)? // All layout info for a binary connector
53
+ tstem = stem_side // one stem in a binary connector (t and p are arbitary names)
54
+ pstem = stem_side // the opposite stem in a binary connector
55
+ tertiary_node = ',' SP+ node_face
56
+ paths = path (SP+ path)*
57
+ sname_place = dir wrap // Side of stem axis and number of text lines after wrapping in stem name text block
58
+ stem_side = (sname_place SP+)? node_face // Either the tstem or pstem layout
59
+
60
+ // Tree layout
61
+ tree_layout = trunk_face (SP+ branch)+ // All layout info for a tree connector
62
+ trunk_face = node_face ('>')? // May or may not graft the trunk branch with > symbol
63
+ branch = '{ ' leaf_faces (csep path)? ' }' // A branch is attached to one or more leaf faces
64
+ leaf_faces = leaf_face (', ' leaf_face)*
65
+ leaf_face = node_face ('>>' / '>' )? // graft its own branch or the next one
66
+ // Within a branch, one leaf may either graft that branch > or graft the subsequent branch >>
67
+ // See patterns 3 and 2 respectively in tech note tn.2 in the documentation folder for examples
68
+ // There are three ways to specify the path of a branch:
69
+ // 1) Interpolated - Compute position between opposing node faces (no specification)
70
+ // 2) Grafted - Line up branch with some face placement (a > on the trunk or some leaf face or a >> on a leaf face)
71
+ // 3) Rut - Run branch down a rut in a lane as specified by a path (path)
72
+ // A branch that is neither interpolated nor grafted has a path specified where it runs
73
+
74
+ // Unary connector
75
+ unary_layout = stem_side
76
+
77
+ // Face attachment
78
+ node_ref = name ('.' number)? // Optional additional placement of the same node
79
+ face = "t" / "b" / "l" / "r" // top, bottom, left or right node face
80
+ dir = "+" / "-" // direction of increasing coord values, up and to the right is positive
81
+ csep = SP+ ':' SP+ // argument separator
82
+ anchor = notch / '*'
83
+ node_face = face anchor? "|" node_ref // Where a stem attaches to a node face
84
+
85
+ // Alignment
86
+ valign = ">" ("top" / "bottom")
87
+ halign = ">" ("right" / "left")
88
+ node_align = valign SP? halign / halign SP? valign / valign / halign
89
+ // A notch is based on a system where 0 means 'centered' and deviations from the center are proportional
90
+ // increments, each the same size, distant from the center in the positive or negative direction
91
+ // Positive is always in the increasing coordinate direction, up or to the right
92
+ notch = '0' / ('+' / '-') number // A unit of alignment, center, or a positive or negative integer
93
+ path = 'L' number ('R' notch)? // Lane and rut, assume rut 0 if R not specified
94
+
95
+ // Elements
96
+ wrap = '/' number // Number of lines to wrap an associated string
97
+ number = r'[1-9][0-9]*' // Always a positive integer
98
+
99
+ // Model element names
100
+ // A name is one or more words separated by a delimiter
101
+ delim = r'[ _]' // Delimiter to separate words in a name
102
+ word = r'[A-Za-z][A-Za-z0-9]*' // String of alpahnumeric text with no whitespace starting with alpha char
103
+ name = word (delim word)* // Sequence of delimited words forming a name
104
+
105
+ // Whitespace and comments
106
+ INDENT = " " // For clarity
107
+ EOL = SP* COMMENT? '\n' // end of line: Comments, blank lines, whitespace we can omit from the parser result
108
+ COMMENT = '//' r'.*' // Comment slashes don't work if included in the regular expression for some reason
109
+ SP = " "
@@ -0,0 +1,118 @@
1
+ """ layout_parser.py – Parse layout file """
2
+
3
+ from mls_parser.exceptions import LayoutGrammarFileOpen, LayoutInputFileOpen, LayoutInputFileEmpty, LayoutParseError
4
+ from mls_parser.layout_visitor import LayoutVisitor
5
+ from arpeggio import visit_parse_tree, NoMatch
6
+ from arpeggio.cleanpeg import ParserPEG
7
+ import os
8
+ from pathlib import Path
9
+
10
+
11
+ class LayoutParser:
12
+ """
13
+ Parses a model layout file using the arpeggio parser generator
14
+
15
+ Attributes
16
+
17
+ - grammar_file -- (class based) Name of the system file defining the Executable UML grammar
18
+ - root_rule_name -- (class based) Name of the top level grammar element found in grammar file
19
+ - debug -- debug flag (used to set arpeggio parser mode)
20
+ - model_grammar -- The model grammar text read from the system grammar file
21
+ - model_text -- The input model text read from the user supplied text file
22
+ """
23
+
24
+ debug = False # by default
25
+ mls_grammar = None # We haven't read it in yet
26
+ model_text = None # User will provide this in a file
27
+ model_file = None # The user supplied xcm file path
28
+
29
+ root_rule_name = 'diagram_layout' # The required name of the highest level parse element
30
+
31
+ # Useful paths within the project
32
+ src_path = Path(__file__).parent.parent # Path to src folder
33
+ module_path = src_path / 'mls_parser'
34
+ grammar_path = module_path # The grammar files are all here
35
+ cwd = Path.cwd()
36
+ diagnostics_path = cwd / 'diagnostics' # All parser diagnostic output goes here
37
+
38
+ # Files
39
+ grammar_file = grammar_path / "layout.peg" # We parse using this peg grammar
40
+ grammar_model_pdf = diagnostics_path / "layout.pdf"
41
+ parse_tree_pdf = diagnostics_path / "layout_parse_tree.pdf"
42
+ parse_tree_dot = cwd / f"{root_rule_name}_parse_tree.dot"
43
+ parser_model_dot = cwd / f"{root_rule_name}_peg_parser_model.dot"
44
+
45
+ pg_tree_dot = cwd / "peggrammar_parse_tree.dot"
46
+ pg_model_dot = cwd / "peggrammar_parser_model.dot"
47
+ pg_tree_pdf = diagnostics_path / "peggrammar_parse_tree.pdf"
48
+ pg_model_pdf = diagnostics_path / "peggrammar_parser_model.pdf"
49
+
50
+ @classmethod
51
+ def parse_file(cls, file_input: Path, debug=False):
52
+ """
53
+
54
+ :param file_input: class model file to read
55
+ :param debug: Run parser in debug mode
56
+ """
57
+ cls.model_file = file_input
58
+ cls.debug = debug
59
+ if debug:
60
+ # If there is no diagnostics directory, create one in the current working directory
61
+ cls.diagnostics_path.mkdir(parents=False, exist_ok=True)
62
+
63
+ # Read the class model file
64
+ try:
65
+ cls.model_text = open(file_input, 'r').read() + '\n'
66
+ # At least one newline at end simplifies grammar rules
67
+ except OSError as e:
68
+ raise LayoutInputFileOpen(file_input)
69
+
70
+ if not cls.model_text:
71
+ raise LayoutInputFileEmpty(file_input)
72
+
73
+ return cls.parse()
74
+
75
+ @classmethod
76
+ # def parse(cls) -> Subsystem_a:
77
+ def parse(cls):
78
+ """
79
+ Parse the model file and return the content
80
+ :return: The abstract syntax tree content of interest
81
+ """
82
+ # Read the grammar file
83
+ try:
84
+ cls.mls_grammar = open(LayoutParser.grammar_file, 'r').read()
85
+ except OSError as e:
86
+ raise LayoutGrammarFileOpen(LayoutParser.grammar_file)
87
+
88
+ # Create an arpeggio parser for our model grammar that does not eliminate whitespace
89
+ # We interpret newlines and indents in our grammar, so whitespace must be preserved
90
+ parser = ParserPEG(cls.mls_grammar, LayoutParser.root_rule_name, skipws=False, debug=cls.debug)
91
+ if cls.debug:
92
+ # Transform dot files into pdfs
93
+ # os.system(f'dot -Tpdf {cls.pg_tree_dot} -o {cls.pg_tree_pdf}')
94
+ # os.system(f'dot -Tpdf {cls.pg_model_dot} -o {cls.pg_model_pdf}')
95
+ os.system(f'dot -Tpdf {cls.parser_model_dot} -o {cls.grammar_model_pdf}')
96
+ cls.parser_model_dot.unlink(True)
97
+ cls.pg_tree_dot.unlink(True)
98
+ cls.pg_model_dot.unlink(True)
99
+
100
+ # Now create an abstract syntax tree from our model text
101
+ try:
102
+ parse_tree = parser.parse(cls.model_text)
103
+ except NoMatch as e:
104
+ raise LayoutParseError(cls.model_file.name, e) from None
105
+
106
+ # Transform that into a result that is better organized with grammar artifacts filtered out
107
+ result = visit_parse_tree(parse_tree, LayoutVisitor(debug=cls.debug))
108
+
109
+ if cls.debug:
110
+ # Transform dot files into pdfs
111
+ os.system(f'dot -Tpdf {cls.parse_tree_dot} -o {cls.parse_tree_pdf}')
112
+ # Delete dot files since we are only interested in the generated PDFs
113
+ # Comment this part out if you want to retain the dot files
114
+ cls.parse_tree_dot.unlink(True)
115
+
116
+ return result
117
+
118
+
@@ -0,0 +1,510 @@
1
+ """ layout_visitor.py """
2
+ from collections import namedtuple
3
+ from enum import Enum
4
+ from arpeggio import PTNodeVisitor
5
+ from mls_parser.exceptions import (ConflictingGraftFloat, MultipleGraftsInSameBranch, ExternalLocalGraftConflict,
6
+ MultipleFloatsInSameBranch, TrunkLeafGraftConflict, GraftRutBranchConflict,
7
+ ExternalGraftOnLastBranch)
8
+
9
+
10
+ DiagramLayout = namedtuple('DiagramLayout', 'layout_spec node_placement connector_placement')
11
+ LayoutSpec = namedtuple('LayoutSpec', 'dtype pres notation color sheet orientation frame frame_presentation padding')
12
+
13
+ class NodeFace(Enum):
14
+ """
15
+ Values are multiplied by absolute distance to get an x or y coordinate.
16
+ """
17
+ TOP = 0
18
+ BOTTOM = 1
19
+ RIGHT = 2
20
+ LEFT = 3
21
+
22
+
23
+ face_map = {'r': NodeFace.RIGHT, 'l': NodeFace.LEFT, 't': NodeFace.TOP, 'b': NodeFace.BOTTOM}
24
+
25
+
26
+ class LayoutVisitor(PTNodeVisitor):
27
+ """
28
+ Organized in the same categories commented in the clean peg grammar file.
29
+
30
+ Some conventions:
31
+
32
+ - Comment each visit with parsing semantics
33
+ - Descriptive named variables if processing is required
34
+ - Use *node.rule_name* in case the rule name changes
35
+ - Combine values into dictionaries for stability, ease of interpretation and to avoid mistakes
36
+ - Assigining result to a variable that is returned for ease of debugging
37
+ """
38
+
39
+ # Root
40
+ @classmethod
41
+ def visit_diagram_layout(cls, node, children) -> DiagramLayout:
42
+ """
43
+ EOL* layout_spec ((node_block connector_block) / (node_block))? EOF
44
+
45
+ Root Rule
46
+ """
47
+ # Organize the input into a layout spec, a node dictionary, and an optional connector block
48
+ lspec = children.results['layout_spec'][0]
49
+ node_pdict = {}
50
+ for n in children.results['node_block'][0]:
51
+ dup_num = n.get('duplicate')
52
+ key = n['node_name'] if not dup_num else f"{n['node_name']}_{dup_num}"
53
+ node_pdict[key] = n
54
+
55
+ if 'connector_block' in children.results:
56
+ rc = children.results['connector_block'][0]
57
+ else:
58
+ rc = None
59
+ return DiagramLayout(layout_spec=lspec, node_placement=node_pdict, connector_placement=rc)
60
+
61
+ @classmethod
62
+ def visit_layout_spec(cls, node, children) -> LayoutSpec:
63
+ """
64
+ (diagram notation color? presentation sheet padding? orientation frame? frame_presentation?)#
65
+
66
+ Layout specification
67
+ """
68
+ ld = children.results
69
+ frame = ld.get('frame')
70
+ color = ld.get('color', ['white'])
71
+ frame_presentation = ld.get('frame_presentation')
72
+ padding = ld.get('padding')
73
+ lspec = LayoutSpec(dtype=ld['diagram'][0], notation=ld['notation'][0], pres=ld['presentation'][0],
74
+ orientation=ld['orientation'][0], sheet=ld['sheet'][0],
75
+ color=color[0],
76
+ frame=None if not frame else frame[0],
77
+ # frame_presentation not relevant if no frame
78
+ frame_presentation=None if not frame else frame_presentation[0],
79
+ padding=None if not padding else padding[0])
80
+ return lspec
81
+
82
+ # Diagram
83
+ @classmethod
84
+ def visit_diagram(cls, node, children):
85
+ """Keyword argument"""
86
+ return children[0]
87
+
88
+ @classmethod
89
+ def visit_notation(cls, node, children):
90
+ """Keyword argument"""
91
+ return children[0]
92
+
93
+ @classmethod
94
+ def visit_color(cls, node, children):
95
+ """Keyword argument"""
96
+ return children[0]
97
+
98
+ @classmethod
99
+ def visit_presentation(cls, node, children):
100
+ """Keyword argument"""
101
+ return children[0]
102
+
103
+ @classmethod
104
+ def visit_sheet(cls, node, children):
105
+ """Keyword argument"""
106
+ return children[0]
107
+
108
+ @classmethod
109
+ def visit_padding(cls, node, children):
110
+ """Keyword argument"""
111
+ d = {k: v for c in children for k, v in c.items()}
112
+ return d
113
+
114
+ @classmethod
115
+ def visit_tpad(cls, node, children):
116
+ return {'top': children[0]}
117
+
118
+ @classmethod
119
+ def visit_bpad(cls, node, children):
120
+ return {'bottom': children[0]}
121
+
122
+ @classmethod
123
+ def visit_lpad(cls, node, children):
124
+ return {'left': children[0]}
125
+
126
+ @classmethod
127
+ def visit_rpad(cls, node, children):
128
+ return {'right': children[0]}
129
+
130
+ @classmethod
131
+ def visit_orientation(cls, node, children):
132
+ """Keyword argument"""
133
+ return children[0]
134
+
135
+ @classmethod
136
+ def visit_frame(cls, node, children):
137
+ """Keyword argument"""
138
+ return children[0]
139
+
140
+ @classmethod
141
+ def visit_frame_presentation(cls, node, children):
142
+ """Keyword argument"""
143
+ return children[0]
144
+
145
+
146
+ # Node
147
+ @classmethod
148
+ def visit_node_block(cls, node, children):
149
+ """ nodes_header node_spec+ """
150
+ return children
151
+
152
+ @classmethod
153
+ def visit_node_spec(cls, node, children):
154
+ """
155
+ INDENT node_name wrap? (SP+ node_width_expansion)? (SP+ node_height_expansion)?
156
+ SP+ node_placement (SP+ color_tag)? EOL*
157
+ """
158
+ ditems = {k: v for c in children for k, v in c.items()}
159
+ return ditems
160
+
161
+ @classmethod
162
+ def visit_node_name(cls, node, children):
163
+ """ name """
164
+ return {node.rule_name: ''.join(children)}
165
+
166
+ @classmethod
167
+ def visit_node_width_expansion(cls, node, children):
168
+ """ number '%' """
169
+ # Convert percentage to ratio ensuring ratio is positive
170
+ user_percent = children[0]
171
+ ratio = 0 if user_percent < 0 else round(user_percent / 100, 2)
172
+ return {node.rule_name: ratio}
173
+
174
+ @classmethod
175
+ def visit_node_height_expansion(cls, node, children):
176
+ """ number '%' """
177
+ d = {k: v for k, v in children}
178
+ return {node.rule_name: d}
179
+
180
+ @classmethod
181
+ def visit_comp_height_expansion(cls, node, children):
182
+ """ '[C' number ']' number '%' """
183
+ # Convert percentage to ratio ensuring ratio is positive
184
+ user_percent = children[1]
185
+ ratio = 0 if user_percent < 0 else round(user_percent / 100, 2)
186
+ return [children[0], ratio]
187
+
188
+ @classmethod
189
+ def visit_node_placement(cls, node, children):
190
+ """ grid_place (SP+ ':' SP+ grid_place)* """
191
+ return {'placements': children}
192
+
193
+ @classmethod
194
+ def visit_grid_place(cls, node, children):
195
+ """ node_loc (SP+ node_align)? """
196
+ d = {k: v for c in children for k, v in c.items()}
197
+ return d
198
+
199
+ @classmethod
200
+ def visit_node_loc(cls, node, children):
201
+ """
202
+ span ',' span
203
+
204
+ row and column
205
+ """
206
+ return {node.rule_name: children}
207
+
208
+ @classmethod
209
+ def visit_span(cls, node, children):
210
+ """
211
+ number '-' number / number
212
+
213
+ for example:
214
+ 3-5 is returned as span [3,5]
215
+ 3 is returned as span [3]
216
+ """
217
+ return children
218
+
219
+ @classmethod
220
+ def visit_color_tag(cls, node, children):
221
+ """ '<' name '>' """
222
+ return {node.rule_name: children[0]}
223
+
224
+ # Connector
225
+ @classmethod
226
+ def visit_connector_block(cls, node, children):
227
+ return children
228
+
229
+ @classmethod
230
+ def visit_connector_layout(cls, node, children):
231
+ """All layout info for the connector"""
232
+ # Combine all child dictionaries
233
+ items = {k: v for d in children for k, v in d.items()}
234
+ items['bend'] = items.get('bend', 1) # No bend supplied, assume 1
235
+ return items
236
+
237
+ @classmethod
238
+ def visit_cname_place(cls, node, children):
239
+ """Name of connector and the side of the connector axis where it is placed"""
240
+ # If a value is supplied it will be a single time list, so extract with [0]
241
+ # If no value is supplied for an optional item, default must also be a single item list [default_value]
242
+ w = children.results.get('wrap')
243
+ wrap_value = 1 if not w else w[0]['wrap']
244
+ cplace = {'cname': children.results['name'][0], # Required component
245
+ 'dir': children.results.get('dir', [1])[0], # many optional components with default values
246
+ 'bend': children.results.get('bend', [1])[0],
247
+ 'notch': children.results.get('notch', [0])[0],
248
+ 'wrap': wrap_value,
249
+ }
250
+ return cplace
251
+
252
+ @classmethod
253
+ def visit_bend(cls, node, children):
254
+ """Number of bend where cname appears"""
255
+ # return {node.rule_name: int(children[0])}
256
+ bend = int(children[0])
257
+ return bend
258
+
259
+ # Binary connector
260
+ @classmethod
261
+ def visit_binary_layout(cls, node, children):
262
+ """All layout info for the binary connector"""
263
+ # Combine all child dictionaries
264
+ items = {k: v for d in children for k, v in d.items()}
265
+ return items
266
+
267
+ @classmethod
268
+ def visit_tstem(cls, node, children):
269
+ """T stem layout info"""
270
+ items = {k: v for d in children for k, v in d.items()}
271
+ items['anchor'] = items.get('anchor', 0)
272
+ tstem = {node.rule_name: items}
273
+ return tstem
274
+
275
+ @classmethod
276
+ def visit_pstem(cls, node, children):
277
+ """P stem layout info"""
278
+ items = {k: v for d in children for k, v in d.items()}
279
+ items['anchor'] = items.get('anchor', 0)
280
+ pstem = {node.rule_name: items}
281
+ return pstem
282
+
283
+ @classmethod
284
+ def visit_tertiary_node(cls, node, children):
285
+ """Tertiary node face and anchor"""
286
+ return {node.rule_name: children[0]}
287
+
288
+ @classmethod
289
+ def visit_paths(cls, node, children):
290
+ """A sequence of one or more paths since a binary connector may bend multiple times"""
291
+ paths = {node.rule_name: [p['path'] for p in children]}
292
+ return paths
293
+
294
+ @classmethod
295
+ def visit_sname_place(cls, node, children):
296
+ """Side of stem axis and number of lines in text block"""
297
+ d = {'stem_dir': children[0]} # initialize d
298
+ d.update(children[1]) # Add wrap key
299
+ return d
300
+
301
+ # Tree connector
302
+ @classmethod
303
+ def visit_tree_layout(cls, node, children):
304
+ """All layout info for the tree connector"""
305
+ tlayout = children[0]
306
+ # If the trunk is grafting (>), there can be no other leaf stem grafting locally (>)
307
+ tlayout['branches'] = [c['branch'] for c in children[1:]]
308
+ tgraft = tlayout['trunk_face']['graft']
309
+ tleaves = tlayout['branches'][0]['leaf_faces']
310
+ if tgraft and [tleaves[n]['graft'] for n in tleaves if tleaves[n]['graft'] == 'local']:
311
+ raise TrunkLeafGraftConflict() # In the first branch (trunk branch) both trunk and some leaf are grafting
312
+ # For all offshoot (non-trunk) branches, there can be no local graft (>) if the preceding branch
313
+ # is grafting externally (>>). In other words, no more than one graft per branch.
314
+ for b, next_b in zip(tlayout['branches'], tlayout['branches'][1:]):
315
+ lf = b['leaf_faces']
316
+ external_graft = [lf[n]['graft'] for n in lf if lf[n]['graft'] == 'next']
317
+ if external_graft:
318
+ next_lf = next_b['leaf_faces']
319
+ if [next_lf[n]['graft'] for n in next_lf if next_lf[n]['graft'] == 'local']:
320
+ # External graft conflicts with local branch
321
+ raise ExternalLocalGraftConflict(set(lf.keys()))
322
+ # Check for dangling external graft in last branch
323
+ last_lf = tlayout['branches'][-1]['leaf_faces']
324
+ external_graft = [last_lf[n]['graft'] for n in last_lf if last_lf[n]['graft'] == 'next']
325
+ if external_graft:
326
+ raise ExternalGraftOnLastBranch(branch=set(last_lf.keys()))
327
+ return tlayout
328
+
329
+ @classmethod
330
+ def visit_trunk_face(cls, node, children):
331
+ """A single trunk node at the top of the tree layout. It may or may not graft its branch."""
332
+ face = children[0] # Face, node and optional notch
333
+ graft = False if len(children) == 1 else True
334
+ if 'anchor' not in face.keys():
335
+ face['anchor'] = 0 # A Trunk face is never grafted, so an unspecified anchor is 0
336
+ tface = {'trunk_face': {'node_ref': face.pop('node_ref'), **face, 'graft': graft}}
337
+ return tface
338
+
339
+ @classmethod
340
+ def visit_branch(cls, node, children):
341
+ """A tree connector branch"""
342
+ branch = {k: v for d in children for k, v in d.items()}
343
+ # Verify that this is either an interpolated, rut or graft branch and not an illegal mix
344
+ # If a path is specified it is a rut branch or if there is a local graft it is a grafted branch
345
+ # If both path and local graft are present in the same branch it is illegal
346
+ if branch.get('path', None): # Path specified, so there should be no local grafts in this branch
347
+ lf = branch['leaf_faces']
348
+ local_graft = [lf[n]['graft'] for n in lf if lf[n]['graft'] == 'local']
349
+ if local_graft:
350
+ raise GraftRutBranchConflict(branch=set(lf.keys()))
351
+ # Return dictionary of leaf faces and an optional path keyed to the local rule
352
+ return {node.rule_name: branch}
353
+
354
+ @classmethod
355
+ def visit_leaf_faces(cls, node, children):
356
+ """Combine into dictionary of each leaf face indexed by node name"""
357
+ lfaces = {k: v for d in children for k, v in d.items()}
358
+ if len([lfaces[n]['graft'] for n in lfaces if lfaces[n]['graft']]) > 1:
359
+ raise MultipleGraftsInSameBranch(branch=set(lfaces.keys()))
360
+ if len([lfaces[n]['anchor'] for n in lfaces if lfaces[n]['anchor'] == 'float']) > 1:
361
+ raise MultipleFloatsInSameBranch(branch=set(lfaces.keys()))
362
+ return {node.rule_name: lfaces}
363
+
364
+ @classmethod
365
+ def visit_leaf_face(cls, node, children):
366
+ """Branch face that may be a graft to its branch (local) or the (next) branch"""
367
+ lface = children[0]
368
+ graft = None
369
+ if 'anchor' not in lface.keys():
370
+ lface['anchor'] = 0 # If not float or a number, it must be zero in a tree layout
371
+ if len(children) == 2:
372
+ graft = 'local' if children[1] == '>' else 'next'
373
+ if lface['anchor'] == 'float' and graft:
374
+ raise ConflictingGraftFloat(stem=lface['name'])
375
+ lface['graft'] = graft
376
+ node_ref = lface.pop('node_ref')
377
+ # name = node_ref[0] if len(node_ref) == 1 else f"{node_ref[0]}_{node_ref[1]}"
378
+ return {node_ref: lface} # Single element dictionary indexed by the node name
379
+
380
+ # Unary connector
381
+ @classmethod
382
+ def visit_unary_layout(cls, node, children):
383
+ """Unary layout which is just a single stem"""
384
+ return {'ustem': children[0]}
385
+
386
+ # Face attachment
387
+ @classmethod
388
+ def visit_node_ref(cls, node, children):
389
+ """name number?"""
390
+ return children[0] if len(children) < 2 else f"{children[0]}_{children[1]}"
391
+
392
+ @classmethod
393
+ def visit_face(cls, node, children):
394
+ """Face character"""
395
+ return face_map[node.value]
396
+
397
+ @classmethod
398
+ def visit_dir(cls, node, children):
399
+ """Pos-neg direction"""
400
+ return 1 if node.value == '+' else -1
401
+
402
+ @classmethod
403
+ def visit_anchor(cls, node, children):
404
+ """Anchor position"""
405
+ anchor = 'float' if children[0] == '*' else children[0]
406
+ return anchor
407
+
408
+ @classmethod
409
+ def visit_node_face(cls, node, children):
410
+ """Where connector attaches to node face"""
411
+ nface = {k: v[0] for k, v in children.results.items()}
412
+ return nface
413
+
414
+ # Alignment
415
+ @classmethod
416
+ def visit_valign(cls, node, children):
417
+ """Vertical alignment of noce in its cell"""
418
+ return {node.rule_name: children[0].upper()}
419
+
420
+ @classmethod
421
+ def visit_halign(cls, node, children):
422
+ """Horizontal alignment of noce in its cell"""
423
+ return {node.rule_name: children[0].upper()}
424
+
425
+ @classmethod
426
+ def visit_node_align(cls, node, children):
427
+ """Vertical and/or horizontal alignment of node in its cell"""
428
+ if len(children) == 2:
429
+ # Merge the two dictionaries
430
+ return {**children[0], **children[1]}
431
+ else:
432
+ return children[0]
433
+
434
+ @classmethod
435
+ def visit_notch(cls, node, children):
436
+ """The digit 0 or a positive or negative number of notches"""
437
+ if children[0] == '0':
438
+ return 0
439
+ else:
440
+ scale = -1 if children[0] == '-' else 1
441
+ return int(children[1]) * scale
442
+
443
+ @classmethod
444
+ def visit_path(cls, node, children):
445
+ """
446
+ 'L' number ('R' notch)?
447
+
448
+ Lane and rut, assume rut 0 if R not specified
449
+ """
450
+ # Rut is zero by default
451
+ path = {node.rule_name: {'lane': children[0], 'rut': children.results.get('notch', [0])[0]}}
452
+ return path # { path: { lane: <lane_num>, rut: <rut_displacement> }
453
+
454
+ # Elements
455
+ @classmethod
456
+ def visit_wrap(cls, node, children):
457
+ """
458
+ '/' number
459
+
460
+ Number of lines to wrap an associated string
461
+ """
462
+ return {node.rule_name: int(children[0])}
463
+
464
+ @classmethod
465
+ def visit_number(cls, node, children):
466
+ """
467
+ r'[1-9][0-9]*'
468
+
469
+ Natural nummber
470
+ """
471
+ return int(node.value)
472
+
473
+ @classmethod
474
+ def visit_name(cls, node, children):
475
+ """
476
+ word (delim word)*
477
+
478
+ Sequence of delimited words forming a name
479
+ """
480
+ name = ''.join(children)
481
+ return name
482
+
483
+ # Discarded whitespace and comments
484
+ @classmethod
485
+ def visit_LINEWRAP(cls, node, children):
486
+ """
487
+ EOL SP*
488
+
489
+ end of line followed by optional INDENT on next line
490
+ """
491
+ return None
492
+
493
+ @classmethod
494
+ def visit_EOL(cls, node, children):
495
+ """
496
+ SP* COMMENT? '\n'
497
+
498
+ end of line: Spaces, Comments, blank lines, whitespace we can omit from the parser result
499
+ """
500
+ return None
501
+
502
+ @classmethod
503
+ def visit_SP(cls, node, children):
504
+ """
505
+ ' '
506
+
507
+ Single space character (SP)
508
+ """
509
+ return None
510
+
mls_parser/log.conf ADDED
@@ -0,0 +1,42 @@
1
+ [loggers]
2
+ keys=root,mlsParserLogger
3
+
4
+ [handlers]
5
+ keys=fileHandler, consoleHandler, consoleHandlerUser
6
+
7
+ [formatters]
8
+ keys=mlsParserFormatter, mlsParserFormatterUser
9
+
10
+ [logger_root]
11
+ level=DEBUG
12
+ handlers=fileHandler, consoleHandlerUser
13
+
14
+ [logger_mlsParserLogger]
15
+ level=DEBUG
16
+ handlers=fileHandler, consoleHandlerUser
17
+ qualname=mlsParserLogger
18
+ propagate=0
19
+
20
+ [handler_fileHandler]
21
+ class=FileHandler
22
+ level=DEBUG
23
+ formatter=mlsParserFormatter
24
+ args=('mls_parser.log', 'w')
25
+
26
+ [handler_consoleHandlerUser]
27
+ class=StreamHandler
28
+ level=WARNING
29
+ formatter=mlsParserFormatterUser
30
+ args=(sys.stderr,)
31
+
32
+ [handler_consoleHandler]
33
+ class=StreamHandler
34
+ level=WARNING
35
+ formatter=mlsParserFormatter
36
+ args=(sys.stderr,)
37
+
38
+ [formatter_mlsParserFormatter]
39
+ format=mlsParser parser: %(name)s - %(levelname)s - %(message)s
40
+
41
+ [formatter_mlsParserFormatterUser]
42
+ format=mlsParser parser: %(levelname)s - %(message)s
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019-2023 Leon Starr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.1
2
+ Name: mls-parser
3
+ Version: 0.0.2
4
+ Summary: Flatland Model Layout Sheet Parser
5
+ Author-email: Leon Starr <leon_starr@modelint.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2019-2023 Leon Starr
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: repository, https://github.com/modelint/mls-parser
29
+ Project-URL: documentation, https://github.com/modelint/mls-parser/wiki
30
+ Keywords: action language,executable uml,class model,mbse,xuml,xtuml,platform independent,sysml
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python
33
+ Classifier: Programming Language :: Python :: 3
34
+ Requires-Python: >=3.11
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: Arpeggio
38
+ Requires-Dist: tomli; python_version < "3.13"
39
+ Provides-Extra: build
40
+ Requires-Dist: build; extra == "build"
41
+ Requires-Dist: twine; extra == "build"
42
+ Provides-Extra: dev
43
+ Requires-Dist: bump2version; extra == "dev"
44
+ Requires-Dist: pytest; extra == "dev"
45
+
46
+ # Model Layout Parser
47
+
48
+ Parses an *.mls file (Model Layout Style) to yield an abstract syntax tree using python named tuples
49
+
50
+ ### Why you need this
51
+
52
+ You need to process an *.mls file to layout a model diagram
53
+
54
+ ### Installation
55
+
56
+ Create or use a python 3.13+ environment. Then
57
+
58
+ % pip install mls-parser
59
+
60
+ At this point you can invoke the parser via the command line or from your python script.
61
+
62
+ #### From your python script
63
+
64
+ You need this import statement at a minimum:
65
+
66
+ from mls-parser.parser import LayoutParser
67
+
68
+ You can then specify a path as shown:
69
+
70
+ result = LayoutParser.parse_file(file_input=path_to_file, debug=False)
71
+
72
+ In either case, `result` will be a list of parsed class model elements. You may find the header of the `visitor.py`
73
+ file helpful in interpreting these results.
74
+
75
+ #### From the command line
76
+
77
+ This is not the intended usage scenario, but may be helpful for testing or exploration. Since the parser
78
+ may generate some diagnostic info you may want to create a fresh working directory and cd into it
79
+ first. From there...
80
+
81
+ % mls elevator.mls
82
+
83
+ The .xcm extension is not necessary, but the file must contain xcm text. See this repository's wiki for
84
+ more about the mls language. The grammar is defined in the [layout.peg](https://github.com/modelint/mls-parser/blob/main/src/mls_parser/layout.peg) file. (if the link breaks after I do some update to the code,
85
+ just browse through the code looking for the class_model.peg file, and let me know so I can fix it)
86
+
87
+ You can also specify a debug option like this:
88
+
89
+ % mls elevator.mls -D
90
+
91
+ This will create a scrall-diagnostics folder in your current working directory and deposite a coupel of PDFs defining
92
+ the parse of both the class model grammar: `class_model_tree.pdf` and your supplied text: `class_model.pdf`.
93
+
94
+ You should also see a file named `mls-parser.log` in a diagnostics directory within your working directory
@@ -0,0 +1,13 @@
1
+ mls_parser/__init__.py,sha256=zWVHf3DqIS6sYe780DU38lmTH_EXQUcBK32nOsBO7ao,18
2
+ mls_parser/__main__.py,sha256=NkowelWxc7cc6HRmcRRcWMdMShy0nKC4uO4Rm9EuB-g,1877
3
+ mls_parser/exceptions.py,sha256=hJx2OZPTEFOS4RqZJfH0meqV4ZzH7iBP7O9fXLV_McI,3059
4
+ mls_parser/layout.peg,sha256=BBAeHYR5hBgThUtCej5FmiphI3DlV_1kGJh4NgvnFLo,6019
5
+ mls_parser/layout_parser.py,sha256=fahdd8YeyJybJXZzz-lDoMi202Sv5UFJa0oGGmMiYcA,4854
6
+ mls_parser/layout_visitor.py,sha256=krXYZHIs0UkwJ446fYEmyLs448Iq5nPmOa2QRIh0rHI,18008
7
+ mls_parser/log.conf,sha256=EL_8Pn8A7gz1IV_AODmgtQdBsOREtmhaoomIuAxMSLk,867
8
+ mls_parser-0.0.2.dist-info/LICENSE,sha256=kL0xVrwl2i3Pk9mQXAVAPANCTaLGGOsoXgvqW7TBs20,1072
9
+ mls_parser-0.0.2.dist-info/METADATA,sha256=pEBz1x1dFI3geJSkm09J8nojcG0QykMgsJxsVf7B3Dk,4026
10
+ mls_parser-0.0.2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
11
+ mls_parser-0.0.2.dist-info/entry_points.txt,sha256=HsIgu1_OJw84-8oMi7zGKznq7oQgpviMC_HL46vsjVo,49
12
+ mls_parser-0.0.2.dist-info/top_level.txt,sha256=s_pWxPSrMvumqwFDqPVWrajhZO2giOrZg9acHgn6h8Q,11
13
+ mls_parser-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.2.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mls = mls_parser.__main__:main
@@ -0,0 +1 @@
1
+ mls_parser