openscad-parser 2.4.4__tar.gz → 2.4.6__tar.gz
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.
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/PKG-INFO +11 -5
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/README.rst +10 -4
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/pyproject.toml +1 -1
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/src/openscad_parser/ast/nodes.py +2 -2
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/src/openscad_parser/ast/pretty_print.py +93 -23
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/src/openscad_parser/__init__.py +0 -0
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/src/openscad_parser/ast/__init__.py +0 -0
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/src/openscad_parser/ast/builder.py +0 -0
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/src/openscad_parser/ast/scope.py +0 -0
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/src/openscad_parser/ast/serialization.py +0 -0
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/src/openscad_parser/ast/source_map.py +0 -0
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/src/openscad_parser/cli.py +0 -0
- {openscad_parser-2.4.4 → openscad_parser-2.4.6}/src/openscad_parser/grammar.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openscad_parser
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.6
|
|
4
4
|
Summary: A PEG parser to read OpenSCAD language source code, with optional AST tree generation.
|
|
5
5
|
Keywords: openscad,openscad parser,parser
|
|
6
6
|
Author: Revar Desmera
|
|
@@ -65,7 +65,7 @@ Features
|
|
|
65
65
|
- AST tree can contain comment nodes (single-line and multi-line)
|
|
66
66
|
- AST tree uses dataclasses and can be pickled/unpickled for caching/serialization
|
|
67
67
|
- JSON and YAML serialization/deserialization of AST trees
|
|
68
|
-
- Pretty-printer that converts an AST back to formatted OpenSCAD source (``to_openscad()``)
|
|
68
|
+
- Pretty-printer that converts an AST back to formatted OpenSCAD source (``to_openscad()``) with correct operator-precedence parenthesization
|
|
69
69
|
- Command-line interface (``openscad-parser``) for JSON/YAML/formatted output
|
|
70
70
|
|
|
71
71
|
Installation
|
|
@@ -441,8 +441,8 @@ List Comprehensions
|
|
|
441
441
|
- ``ListComprehension(elements: list[VectorElement])``: Vector/list literals ``[elements]``
|
|
442
442
|
- ``ListCompFor(assignments: list[Assignment], body: VectorElement)``: for loops in list comprehensions ``for(assignments) body``
|
|
443
443
|
- ``ListCompCFor(inits: list[Assignment], condition: Expression, incrs: list[Assignment], body: VectorElement)``: C-style for loops in list comprehensions ``for(inits; condition; incrs) body``
|
|
444
|
-
- ``ListCompIf(condition: Expression, true_expr: VectorElement)``: Conditional inclusion without else ``if(condition) true_expr``
|
|
445
|
-
- ``ListCompIfElse(condition: Expression, true_expr: VectorElement, false_expr: VectorElement)``: Conditional inclusion with else ``if(condition) true_expr else false_expr``
|
|
444
|
+
- ``ListCompIf(condition: Expression, true_expr: VectorElement)``: Conditional inclusion without else ``if (condition) true_expr``
|
|
445
|
+
- ``ListCompIfElse(condition: Expression, true_expr: VectorElement, false_expr: VectorElement)``: Conditional inclusion with else ``if (condition) true_expr else false_expr``
|
|
446
446
|
- ``ListCompLet(assignments: list[Assignment], body: VectorElement)``: let expressions in list comprehensions ``let(assignments) body``
|
|
447
447
|
- ``ListCompEach(body: VectorElement)``: each expressions (flattens nested lists) ``each body``
|
|
448
448
|
|
|
@@ -805,7 +805,7 @@ The ``to_openscad()`` function converts an AST back to formatted OpenSCAD source
|
|
|
805
805
|
|
|
806
806
|
formatted = to_openscad(ast)
|
|
807
807
|
# module box(w, h) {
|
|
808
|
-
# cube([w, h, 1
|
|
808
|
+
# cube([w, h, 1]);
|
|
809
809
|
# }
|
|
810
810
|
print(formatted)
|
|
811
811
|
|
|
@@ -822,6 +822,12 @@ control structures, modifiers, list comprehensions, and comments.
|
|
|
822
822
|
|
|
823
823
|
- Blank lines are inserted before and after module/function declarations.
|
|
824
824
|
- Single-child module instantiations are formatted inline; multiple children use a block.
|
|
825
|
+
- Operator precedence is preserved — parentheses are re-inserted exactly where needed.
|
|
826
|
+
- Boolean literals are always written as ``true`` / ``false``.
|
|
827
|
+
- Ternary expressions are formatted across three lines (``condition``, ``? true``, ``: false``); block-formatted branches (``let``, ``assert``, ``echo``, list comprehensions, long calls) align their closing delimiter with their visual keyword column.
|
|
828
|
+
- ``let()``, ``assert()``, and ``echo()`` expressions place their body on the next line.
|
|
829
|
+
- List comprehensions containing ``for`` loops always expand to block form; ``if (condition)`` elements include parentheses around the condition.
|
|
830
|
+
- Long argument/parameter lists (> 100 characters) are formatted one argument per line, with each argument expression individually reformatted (ternaries, let, list comprehensions, nested long calls).
|
|
825
831
|
- Comments are preserved when the AST was parsed with ``include_comments=True``.
|
|
826
832
|
|
|
827
833
|
Controlling indentation::
|
|
@@ -22,7 +22,7 @@ Features
|
|
|
22
22
|
- AST tree can contain comment nodes (single-line and multi-line)
|
|
23
23
|
- AST tree uses dataclasses and can be pickled/unpickled for caching/serialization
|
|
24
24
|
- JSON and YAML serialization/deserialization of AST trees
|
|
25
|
-
- Pretty-printer that converts an AST back to formatted OpenSCAD source (``to_openscad()``)
|
|
25
|
+
- Pretty-printer that converts an AST back to formatted OpenSCAD source (``to_openscad()``) with correct operator-precedence parenthesization
|
|
26
26
|
- Command-line interface (``openscad-parser``) for JSON/YAML/formatted output
|
|
27
27
|
|
|
28
28
|
Installation
|
|
@@ -398,8 +398,8 @@ List Comprehensions
|
|
|
398
398
|
- ``ListComprehension(elements: list[VectorElement])``: Vector/list literals ``[elements]``
|
|
399
399
|
- ``ListCompFor(assignments: list[Assignment], body: VectorElement)``: for loops in list comprehensions ``for(assignments) body``
|
|
400
400
|
- ``ListCompCFor(inits: list[Assignment], condition: Expression, incrs: list[Assignment], body: VectorElement)``: C-style for loops in list comprehensions ``for(inits; condition; incrs) body``
|
|
401
|
-
- ``ListCompIf(condition: Expression, true_expr: VectorElement)``: Conditional inclusion without else ``if(condition) true_expr``
|
|
402
|
-
- ``ListCompIfElse(condition: Expression, true_expr: VectorElement, false_expr: VectorElement)``: Conditional inclusion with else ``if(condition) true_expr else false_expr``
|
|
401
|
+
- ``ListCompIf(condition: Expression, true_expr: VectorElement)``: Conditional inclusion without else ``if (condition) true_expr``
|
|
402
|
+
- ``ListCompIfElse(condition: Expression, true_expr: VectorElement, false_expr: VectorElement)``: Conditional inclusion with else ``if (condition) true_expr else false_expr``
|
|
403
403
|
- ``ListCompLet(assignments: list[Assignment], body: VectorElement)``: let expressions in list comprehensions ``let(assignments) body``
|
|
404
404
|
- ``ListCompEach(body: VectorElement)``: each expressions (flattens nested lists) ``each body``
|
|
405
405
|
|
|
@@ -762,7 +762,7 @@ The ``to_openscad()`` function converts an AST back to formatted OpenSCAD source
|
|
|
762
762
|
|
|
763
763
|
formatted = to_openscad(ast)
|
|
764
764
|
# module box(w, h) {
|
|
765
|
-
# cube([w, h, 1
|
|
765
|
+
# cube([w, h, 1]);
|
|
766
766
|
# }
|
|
767
767
|
print(formatted)
|
|
768
768
|
|
|
@@ -779,6 +779,12 @@ control structures, modifiers, list comprehensions, and comments.
|
|
|
779
779
|
|
|
780
780
|
- Blank lines are inserted before and after module/function declarations.
|
|
781
781
|
- Single-child module instantiations are formatted inline; multiple children use a block.
|
|
782
|
+
- Operator precedence is preserved — parentheses are re-inserted exactly where needed.
|
|
783
|
+
- Boolean literals are always written as ``true`` / ``false``.
|
|
784
|
+
- Ternary expressions are formatted across three lines (``condition``, ``? true``, ``: false``); block-formatted branches (``let``, ``assert``, ``echo``, list comprehensions, long calls) align their closing delimiter with their visual keyword column.
|
|
785
|
+
- ``let()``, ``assert()``, and ``echo()`` expressions place their body on the next line.
|
|
786
|
+
- List comprehensions containing ``for`` loops always expand to block form; ``if (condition)`` elements include parentheses around the condition.
|
|
787
|
+
- Long argument/parameter lists (> 100 characters) are formatted one argument per line, with each argument expression individually reformatted (ternaries, let, list comprehensions, nested long calls).
|
|
782
788
|
- Comments are preserved when the AST was parsed with ``include_comments=True``.
|
|
783
789
|
|
|
784
790
|
Controlling indentation::
|
|
@@ -1350,7 +1350,7 @@ class ListCompIf(VectorElement):
|
|
|
1350
1350
|
true_expr: VectorElement
|
|
1351
1351
|
|
|
1352
1352
|
def __str__(self):
|
|
1353
|
-
return f"if {self.condition} {self.true_expr}"
|
|
1353
|
+
return f"if ({self.condition}) {self.true_expr}"
|
|
1354
1354
|
|
|
1355
1355
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
1356
1356
|
self.scope = parent_scope
|
|
@@ -1379,7 +1379,7 @@ class ListCompIfElse(VectorElement):
|
|
|
1379
1379
|
false_expr: VectorElement
|
|
1380
1380
|
|
|
1381
1381
|
def __str__(self):
|
|
1382
|
-
return f"if {self.condition} {self.true_expr} else {self.false_expr}"
|
|
1382
|
+
return f"if ({self.condition}) {self.true_expr} else {self.false_expr}"
|
|
1383
1383
|
|
|
1384
1384
|
def build_scope(self, parent_scope: "Scope") -> None:
|
|
1385
1385
|
self.scope = parent_scope
|
|
@@ -19,6 +19,9 @@ from .nodes import (
|
|
|
19
19
|
ListComprehension,
|
|
20
20
|
ListCompFor,
|
|
21
21
|
ListCompCFor,
|
|
22
|
+
ListCompLet,
|
|
23
|
+
PositionalArgument,
|
|
24
|
+
NamedArgument,
|
|
22
25
|
)
|
|
23
26
|
|
|
24
27
|
|
|
@@ -65,37 +68,92 @@ def _fmt_list_elem(elem, indent: int, w: int) -> str:
|
|
|
65
68
|
pad = " " * indent
|
|
66
69
|
inner_pad = " " * (indent + w)
|
|
67
70
|
if isinstance(elem, ListCompFor):
|
|
68
|
-
|
|
71
|
+
formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
|
|
69
72
|
body = _fmt_list_elem(elem.body, indent + w, w)
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
assigns_inline = ", ".join(formatted)
|
|
74
|
+
if any("\n" in fa for fa in formatted) or len(f"for ({assigns_inline})") + indent > _MULTILINE_CHAR_LIMIT:
|
|
75
|
+
assign_lines = (",\n" + inner_pad).join(formatted)
|
|
72
76
|
return f"for (\n{inner_pad}{assign_lines}\n{pad})\n{inner_pad}{body}"
|
|
73
77
|
return f"for ({assigns_inline})\n{inner_pad}{body}"
|
|
74
78
|
if isinstance(elem, ListCompCFor):
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
fmt_inits = [_fmt_assign(a, indent + w, w) for a in elem.inits]
|
|
80
|
+
fmt_incrs = [_fmt_assign(a, indent + w, w) for a in elem.incrs]
|
|
81
|
+
inits_str = ", ".join(fmt_inits)
|
|
82
|
+
incrs_str = ", ".join(fmt_incrs)
|
|
83
|
+
cond_str = str(elem.condition)
|
|
77
84
|
body = _fmt_list_elem(elem.body, indent + w, w)
|
|
78
|
-
|
|
85
|
+
header = f"for ({inits_str}; {cond_str}; {incrs_str})"
|
|
86
|
+
any_multiline = (
|
|
87
|
+
any("\n" in fa for fa in fmt_inits) or
|
|
88
|
+
any("\n" in fa for fa in fmt_incrs) or
|
|
89
|
+
"\n" in cond_str
|
|
90
|
+
)
|
|
91
|
+
if any_multiline or len(header) + indent > _MULTILINE_CHAR_LIMIT:
|
|
92
|
+
return (
|
|
93
|
+
f"for (\n{inner_pad}{inits_str};\n"
|
|
94
|
+
f"{inner_pad}{cond_str};\n"
|
|
95
|
+
f"{inner_pad}{incrs_str}\n{pad})\n{inner_pad}{body}"
|
|
96
|
+
)
|
|
97
|
+
return f"{header}\n{inner_pad}{body}"
|
|
98
|
+
if isinstance(elem, ListCompLet):
|
|
99
|
+
formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
|
|
100
|
+
body = _fmt_list_elem(elem.body, indent, w)
|
|
101
|
+
if len(formatted) > 1 or any("\n" in fa for fa in formatted):
|
|
102
|
+
assign_lines = (",\n" + inner_pad).join(formatted)
|
|
103
|
+
return f"let(\n{inner_pad}{assign_lines}\n{pad})\n{pad}{body}"
|
|
104
|
+
assigns = ", ".join(formatted)
|
|
105
|
+
return f"let({assigns})\n{pad}{body}"
|
|
106
|
+
if isinstance(elem, LetOp):
|
|
107
|
+
formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
|
|
108
|
+
body = str(elem.body)
|
|
109
|
+
if len(formatted) > 1 or any("\n" in fa for fa in formatted):
|
|
110
|
+
assign_lines = (",\n" + inner_pad).join(formatted)
|
|
111
|
+
return f"let(\n{inner_pad}{assign_lines}\n{pad})\n{pad}{body}"
|
|
112
|
+
assigns = ", ".join(formatted)
|
|
113
|
+
return f"let({assigns}) {body}"
|
|
114
|
+
if isinstance(elem, ListComprehension):
|
|
115
|
+
return _fmt_expr(elem, indent, w)
|
|
79
116
|
return str(elem)
|
|
80
117
|
|
|
81
118
|
|
|
82
|
-
def _fmt_multiline_args(head: str, args: list, indent: int, w: int) -> str:
|
|
119
|
+
def _fmt_multiline_args(head: str, args: list, indent: int, w: int, fmt_fn=str) -> str:
|
|
83
120
|
"""Format `head(arg1, arg2, ...)` with each arg on its own line."""
|
|
84
121
|
inner_pad = " " * (indent + w)
|
|
85
122
|
pad = " " * indent
|
|
86
|
-
arg_lines = (",\n" + inner_pad).join(
|
|
123
|
+
arg_lines = (",\n" + inner_pad).join(fmt_fn(a) for a in args)
|
|
87
124
|
return f"{head}(\n{inner_pad}{arg_lines}\n{pad})"
|
|
88
125
|
|
|
89
126
|
|
|
127
|
+
def _fmt_assign(assign, indent: int, w: int) -> str:
|
|
128
|
+
"""Format an Assignment node, routing its expression through _fmt_expr."""
|
|
129
|
+
return f"{assign.name} = {_fmt_expr(assign.expr, indent, w)}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _fmt_argument(arg, indent: int, w: int) -> str:
|
|
133
|
+
"""Format a call argument, routing its expression through _fmt_expr."""
|
|
134
|
+
if isinstance(arg, PositionalArgument):
|
|
135
|
+
return _fmt_expr(arg.expr, indent, w)
|
|
136
|
+
if isinstance(arg, NamedArgument):
|
|
137
|
+
return f"{arg.name}={_fmt_expr(arg.expr, indent, w)}"
|
|
138
|
+
return str(arg)
|
|
139
|
+
|
|
140
|
+
|
|
90
141
|
def _fmt_expr(expr, indent: int, w: int) -> str:
|
|
91
142
|
"""Format an expression with indent-aware layout for ternary, assert, and echo."""
|
|
92
143
|
pad = " " * indent
|
|
93
144
|
if isinstance(expr, TernaryOp):
|
|
94
145
|
pad2 = " " * (indent + 2)
|
|
146
|
+
def _fmt_branch(branch):
|
|
147
|
+
# Nested ternary: 2 spaces per nesting level
|
|
148
|
+
if isinstance(branch, TernaryOp):
|
|
149
|
+
return _fmt_expr(branch, indent + 2, w)
|
|
150
|
+
# All other expressions: their visual start is 2 chars into "? "/":", "
|
|
151
|
+
# so block content and closing delimiters align to indent + 4
|
|
152
|
+
return _fmt_expr(branch, indent + 4, w)
|
|
95
153
|
return (
|
|
96
154
|
f"{expr.condition}\n"
|
|
97
|
-
f"{pad2}? {
|
|
98
|
-
f"{pad2}: {
|
|
155
|
+
f"{pad2}? {_fmt_branch(expr.true_expr)}\n"
|
|
156
|
+
f"{pad2}: {_fmt_branch(expr.false_expr)}"
|
|
99
157
|
)
|
|
100
158
|
if isinstance(expr, AssertOp):
|
|
101
159
|
args = ", ".join(str(a) for a in expr.arguments)
|
|
@@ -105,25 +163,28 @@ def _fmt_expr(expr, indent: int, w: int) -> str:
|
|
|
105
163
|
return f"echo({args})\n{pad}{_fmt_expr(expr.body, indent, w)}"
|
|
106
164
|
if isinstance(expr, LetOp):
|
|
107
165
|
inner_pad = " " * (indent + w)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
else:
|
|
112
|
-
assign_lines = (",\n" + inner_pad).join(str(a) for a in expr.assignments)
|
|
166
|
+
formatted = [_fmt_assign(a, indent + w, w) for a in expr.assignments]
|
|
167
|
+
if len(formatted) > 1 or any("\n" in fa for fa in formatted):
|
|
168
|
+
assign_lines = (",\n" + inner_pad).join(formatted)
|
|
113
169
|
return (
|
|
114
170
|
f"let(\n{inner_pad}{assign_lines}\n{pad})\n"
|
|
115
171
|
f"{pad}{_fmt_expr(expr.body, indent, w)}"
|
|
116
172
|
)
|
|
173
|
+
assigns = ", ".join(formatted)
|
|
174
|
+
return f"let({assigns})\n{pad}{_fmt_expr(expr.body, indent, w)}"
|
|
117
175
|
if isinstance(expr, PrimaryCall):
|
|
118
176
|
inline = str(expr)
|
|
119
177
|
if len(inline) + indent > _MULTILINE_CHAR_LIMIT:
|
|
120
|
-
return _fmt_multiline_args(
|
|
178
|
+
return _fmt_multiline_args(
|
|
179
|
+
str(expr.left), expr.arguments, indent, w,
|
|
180
|
+
fmt_fn=lambda a: _fmt_argument(a, indent + w, w),
|
|
181
|
+
)
|
|
121
182
|
if isinstance(expr, ListComprehension):
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
items = (",\n" + inner_pad).join(
|
|
183
|
+
inner_pad = " " * (indent + w)
|
|
184
|
+
formatted_elems = [_fmt_list_elem(e, indent + w, w) for e in expr.elements]
|
|
185
|
+
any_multiline = any("\n" in fe for fe in formatted_elems)
|
|
186
|
+
if len(str(expr)) + indent > _MULTILINE_CHAR_LIMIT or any_multiline:
|
|
187
|
+
items = (",\n" + inner_pad).join(formatted_elems)
|
|
127
188
|
return f"[\n{inner_pad}{items}\n{pad}]"
|
|
128
189
|
return str(expr)
|
|
129
190
|
|
|
@@ -213,7 +274,10 @@ def _fmt_inst(node: ModuleInstantiation, indent: int, w: int, prefix: str = "")
|
|
|
213
274
|
head = f"{pad}{prefix}{node.name}"
|
|
214
275
|
inline = f"{head}({_join_str(node.arguments)})"
|
|
215
276
|
if len(inline) > _MULTILINE_CHAR_LIMIT:
|
|
216
|
-
call = _fmt_multiline_args(
|
|
277
|
+
call = _fmt_multiline_args(
|
|
278
|
+
head, node.arguments, indent, w,
|
|
279
|
+
fmt_fn=lambda a: _fmt_argument(a, indent + w, w),
|
|
280
|
+
)
|
|
217
281
|
else:
|
|
218
282
|
call = inline
|
|
219
283
|
return call + _fmt_child(node.children, indent, w)
|
|
@@ -227,7 +291,13 @@ def _fmt_inst(node: ModuleInstantiation, indent: int, w: int, prefix: str = "")
|
|
|
227
291
|
return f"{pad}{prefix}intersection_for ({assigns})" + _fmt_child(node.body, indent, w)
|
|
228
292
|
|
|
229
293
|
if isinstance(node, ModularLet):
|
|
230
|
-
|
|
294
|
+
inner_pad = " " * (indent + w)
|
|
295
|
+
formatted = [_fmt_assign(a, indent + w, w) for a in _as_list(node.assignments)]
|
|
296
|
+
if len(formatted) > 1 or any("\n" in fa for fa in formatted):
|
|
297
|
+
assign_lines = (",\n" + inner_pad).join(formatted)
|
|
298
|
+
tail = _fmt_child(node.children, indent, w)
|
|
299
|
+
return f"{pad}{prefix}let (\n{inner_pad}{assign_lines}\n{pad}){tail}"
|
|
300
|
+
assigns = ", ".join(formatted)
|
|
231
301
|
return f"{pad}{prefix}let ({assigns})" + _fmt_child(node.children, indent, w)
|
|
232
302
|
|
|
233
303
|
if isinstance(node, ModularEcho):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|