openscad-parser 2.4.5__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.5
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.5"
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
@@ -20,6 +20,8 @@ from .nodes import (
20
20
  ListCompFor,
21
21
  ListCompCFor,
22
22
  ListCompLet,
23
+ PositionalArgument,
24
+ NamedArgument,
23
25
  )
24
26
 
25
27
 
@@ -66,17 +68,33 @@ def _fmt_list_elem(elem, indent: int, w: int) -> str:
66
68
  pad = " " * indent
67
69
  inner_pad = " " * (indent + w)
68
70
  if isinstance(elem, ListCompFor):
69
- assigns_inline = ", ".join(str(a) for a in elem.assignments)
71
+ formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
70
72
  body = _fmt_list_elem(elem.body, indent + w, w)
71
- if len(f"for ({assigns_inline})") + indent > _MULTILINE_CHAR_LIMIT:
72
- 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)
73
76
  return f"for (\n{inner_pad}{assign_lines}\n{pad})\n{inner_pad}{body}"
74
77
  return f"for ({assigns_inline})\n{inner_pad}{body}"
75
78
  if isinstance(elem, ListCompCFor):
76
- inits = ", ".join(str(a) for a in elem.inits)
77
- 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)
78
84
  body = _fmt_list_elem(elem.body, indent + w, w)
79
- 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}"
80
98
  if isinstance(elem, ListCompLet):
81
99
  formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
82
100
  body = _fmt_list_elem(elem.body, indent, w)
@@ -84,7 +102,7 @@ def _fmt_list_elem(elem, indent: int, w: int) -> str:
84
102
  assign_lines = (",\n" + inner_pad).join(formatted)
85
103
  return f"let(\n{inner_pad}{assign_lines}\n{pad})\n{pad}{body}"
86
104
  assigns = ", ".join(formatted)
87
- return f"let({assigns}) {body}"
105
+ return f"let({assigns})\n{pad}{body}"
88
106
  if isinstance(elem, LetOp):
89
107
  formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
90
108
  body = str(elem.body)
@@ -93,14 +111,16 @@ def _fmt_list_elem(elem, indent: int, w: int) -> str:
93
111
  return f"let(\n{inner_pad}{assign_lines}\n{pad})\n{pad}{body}"
94
112
  assigns = ", ".join(formatted)
95
113
  return f"let({assigns}) {body}"
114
+ if isinstance(elem, ListComprehension):
115
+ return _fmt_expr(elem, indent, w)
96
116
  return str(elem)
97
117
 
98
118
 
99
- 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:
100
120
  """Format `head(arg1, arg2, ...)` with each arg on its own line."""
101
121
  inner_pad = " " * (indent + w)
102
122
  pad = " " * indent
103
- 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)
104
124
  return f"{head}(\n{inner_pad}{arg_lines}\n{pad})"
105
125
 
106
126
 
@@ -109,15 +129,31 @@ def _fmt_assign(assign, indent: int, w: int) -> str:
109
129
  return f"{assign.name} = {_fmt_expr(assign.expr, indent, w)}"
110
130
 
111
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
+
112
141
  def _fmt_expr(expr, indent: int, w: int) -> str:
113
142
  """Format an expression with indent-aware layout for ternary, assert, and echo."""
114
143
  pad = " " * indent
115
144
  if isinstance(expr, TernaryOp):
116
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)
117
153
  return (
118
154
  f"{expr.condition}\n"
119
- f"{pad2}? {_fmt_expr(expr.true_expr, indent + 2, w)}\n"
120
- 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)}"
121
157
  )
122
158
  if isinstance(expr, AssertOp):
123
159
  args = ", ".join(str(a) for a in expr.arguments)
@@ -139,18 +175,15 @@ def _fmt_expr(expr, indent: int, w: int) -> str:
139
175
  if isinstance(expr, PrimaryCall):
140
176
  inline = str(expr)
141
177
  if len(inline) + indent > _MULTILINE_CHAR_LIMIT:
142
- 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
+ )
143
182
  if isinstance(expr, ListComprehension):
144
183
  inner_pad = " " * (indent + w)
145
- let_multiline = any(
146
- isinstance(e, (LetOp, ListCompLet)) and (
147
- len(e.assignments) > 1 or
148
- any("\n" in _fmt_assign(a, indent + w + w, w) for a in e.assignments)
149
- )
150
- for e in expr.elements
151
- )
152
- if len(str(expr)) + indent > _MULTILINE_CHAR_LIMIT or let_multiline:
153
- formatted_elems = [_fmt_list_elem(e, indent + w, w) for e in expr.elements]
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:
154
187
  items = (",\n" + inner_pad).join(formatted_elems)
155
188
  return f"[\n{inner_pad}{items}\n{pad}]"
156
189
  return str(expr)
@@ -241,7 +274,10 @@ def _fmt_inst(node: ModuleInstantiation, indent: int, w: int, prefix: str = "")
241
274
  head = f"{pad}{prefix}{node.name}"
242
275
  inline = f"{head}({_join_str(node.arguments)})"
243
276
  if len(inline) > _MULTILINE_CHAR_LIMIT:
244
- 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
+ )
245
281
  else:
246
282
  call = inline
247
283
  return call + _fmt_child(node.children, indent, w)