mls-parser 0.0.6__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,2 @@
1
+ include src/mls_parser/log.conf
2
+ include src/mls_parser/layout.peg
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.2
2
+ Name: mls-parser
3
+ Version: 0.0.6
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 Sheet Parser
47
+
48
+ Parses an *.mls file (Model Layout Sheet) 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.11+ 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 couple 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,49 @@
1
+ # Model Layout Sheet Parser
2
+
3
+ Parses an *.mls file (Model Layout Sheet) to yield an abstract syntax tree using python named tuples
4
+
5
+ ### Why you need this
6
+
7
+ You need to process an *.mls file to layout a model diagram
8
+
9
+ ### Installation
10
+
11
+ Create or use a python 3.11+ environment. Then
12
+
13
+ % pip install mls-parser
14
+
15
+ At this point you can invoke the parser via the command line or from your python script.
16
+
17
+ #### From your python script
18
+
19
+ You need this import statement at a minimum:
20
+
21
+ from mls_parser.parser import LayoutParser
22
+
23
+ You can then specify a path as shown:
24
+
25
+ result = LayoutParser.parse_file(file_input=path_to_file, debug=False)
26
+
27
+ In either case, `result` will be a list of parsed class model elements. You may find the header of the `visitor.py`
28
+ file helpful in interpreting these results.
29
+
30
+ #### From the command line
31
+
32
+ This is not the intended usage scenario, but may be helpful for testing or exploration. Since the parser
33
+ may generate some diagnostic info you may want to create a fresh working directory and cd into it
34
+ first. From there...
35
+
36
+ % mls elevator.mls
37
+
38
+ The .xcm extension is not necessary, but the file must contain xcm text. See this repository's wiki for
39
+ 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,
40
+ just browse through the code looking for the class_model.peg file, and let me know so I can fix it)
41
+
42
+ You can also specify a debug option like this:
43
+
44
+ % mls elevator.mls -D
45
+
46
+ This will create a scrall-diagnostics folder in your current working directory and deposite a couple of PDFs defining
47
+ the parse of both the class model grammar: `class_model_tree.pdf` and your supplied text: `class_model.pdf`.
48
+
49
+ You should also see a file named `mls_parser.log` in a diagnostics directory within your working directory
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75.2.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mls-parser"
7
+ version = "0.0.6"
8
+ description = "Flatland Model Layout Sheet Parser"
9
+ readme = "README.md"
10
+ authors = [{ name = "Leon Starr", email = "leon_starr@modelint.com" }]
11
+ license = { file = "LICENSE" }
12
+ classifiers = [
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python",
15
+ "Programming Language :: Python :: 3",
16
+ ]
17
+ keywords = ["action language", "executable uml", "class model", "mbse", "xuml", "xtuml", "platform independent", "sysml"]
18
+ dependencies = ["Arpeggio", 'tomli; python_version < "3.13"']
19
+ requires-python = ">=3.11"
20
+
21
+ [project.optional-dependencies]
22
+ build = ["build", "twine"]
23
+ dev = ["bump2version", "pytest"]
24
+
25
+ [project.scripts]
26
+ mls = "mls_parser.__main__:main"
27
+
28
+ [project.urls]
29
+ repository = "https://github.com/modelint/mls-parser"
30
+ documentation = "https://github.com/modelint/mls-parser/wiki"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ version = "0.0.6"
@@ -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}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}'
@@ -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 ternary_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
+ ternary_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': 'RIGHT', 'l': 'LEFT', 't': 'TOP', 'b': '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_ternary_node(cls, node, children):
285
+ """Ternary 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
+
@@ -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,94 @@
1
+ Metadata-Version: 2.2
2
+ Name: mls-parser
3
+ Version: 0.0.6
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 Sheet Parser
47
+
48
+ Parses an *.mls file (Model Layout Sheet) 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.11+ 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 couple 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,17 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ src/mls_parser/__init__.py
6
+ src/mls_parser/__main__.py
7
+ src/mls_parser/exceptions.py
8
+ src/mls_parser/layout.peg
9
+ src/mls_parser/layout_parser.py
10
+ src/mls_parser/layout_visitor.py
11
+ src/mls_parser/log.conf
12
+ src/mls_parser.egg-info/PKG-INFO
13
+ src/mls_parser.egg-info/SOURCES.txt
14
+ src/mls_parser.egg-info/dependency_links.txt
15
+ src/mls_parser.egg-info/entry_points.txt
16
+ src/mls_parser.egg-info/requires.txt
17
+ src/mls_parser.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mls = mls_parser.__main__:main
@@ -0,0 +1,12 @@
1
+ Arpeggio
2
+
3
+ [:python_version < "3.13"]
4
+ tomli
5
+
6
+ [build]
7
+ build
8
+ twine
9
+
10
+ [dev]
11
+ bump2version
12
+ pytest
@@ -0,0 +1 @@
1
+ mls_parser