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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscad_parser
3
- Version: 2.4.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.0]);
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.0]);
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::
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_parser"
7
- version = "2.4.4"
7
+ version = "2.4.6"
8
8
  description = "A PEG parser to read OpenSCAD language source code, with optional AST tree generation."
9
9
  readme = "README.rst"
10
10
  authors = [
@@ -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
- assigns_inline = ", ".join(str(a) for a in elem.assignments)
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
- if len(f"for ({assigns_inline})") + indent > _MULTILINE_CHAR_LIMIT:
71
- assign_lines = (",\n" + inner_pad).join(str(a) for a in elem.assignments)
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
- inits = ", ".join(str(a) for a in elem.inits)
76
- incrs = ", ".join(str(a) for a in elem.incrs)
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
- return f"for ({inits}; {elem.condition}; {incrs})\n{inner_pad}{body}"
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(str(a) for a in args)
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}? {_fmt_expr(expr.true_expr, indent + 2, w)}\n"
98
- f"{pad2}: {_fmt_expr(expr.false_expr, indent + 2, w)}"
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
- if len(expr.assignments) <= 1:
109
- assigns = ", ".join(str(a) for a in expr.assignments)
110
- return f"let({assigns})\n{pad}{_fmt_expr(expr.body, indent, w)}"
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(str(expr.left), expr.arguments, indent, w)
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
- inline = str(expr)
123
- if len(inline) + indent > _MULTILINE_CHAR_LIMIT:
124
- inner_pad = " " * (indent + w)
125
- pad = " " * indent
126
- items = (",\n" + inner_pad).join(_fmt_list_elem(e, indent + w, w) for e in expr.elements)
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(head, node.arguments, indent, w)
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
- assigns = _join_str(_as_list(node.assignments))
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):