openscad-parser 2.4.6__tar.gz → 2.4.7__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.6
3
+ Version: 2.4.7
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
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_parser"
7
- version = "2.4.6"
7
+ version = "2.4.7"
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 = [
@@ -624,7 +624,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
624
624
  arguments = children[0] if len(children) > 0 else []
625
625
  body = children[1] if len(children) > 1 else None
626
626
  if body is None:
627
- raise ValueError("assert_expr should have an Expression body")
627
+ body = UndefinedLiteral(position=self._get_node_position(node))
628
628
  return AssertOp(arguments=arguments, body=body, position=self._get_node_position(node))
629
629
 
630
630
  def visit_echo_expr(self, node, children):
@@ -637,7 +637,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
637
637
  arguments = children[0] if len(children) > 0 else []
638
638
  body = children[1] if len(children) > 1 else None
639
639
  if body is None:
640
- raise ValueError("echo_expr should have an Expression body")
640
+ body = UndefinedLiteral(position=self._get_node_position(node))
641
641
  return EchoOp(arguments=arguments, body=body, position=self._get_node_position(node))
642
642
 
643
643
  def visit_ternary_expr(self, node, children):
@@ -430,7 +430,10 @@ class EchoOp(Expression):
430
430
  body: Expression
431
431
 
432
432
  def __str__(self):
433
- return f"echo({', '.join(str(arg) for arg in self.arguments)}) {self.body}"
433
+ args = ', '.join(str(arg) for arg in self.arguments)
434
+ if isinstance(self.body, UndefinedLiteral):
435
+ return f"echo({args})"
436
+ return f"echo({args}) {self.body}"
434
437
 
435
438
  def build_scope(self, parent_scope: "Scope") -> None:
436
439
  self.scope = parent_scope
@@ -458,7 +461,10 @@ class AssertOp(Expression):
458
461
  body: Expression
459
462
 
460
463
  def __str__(self):
461
- return f"assert({', '.join(str(arg) for arg in self.arguments)}) {self.body}"
464
+ args = ', '.join(str(arg) for arg in self.arguments)
465
+ if isinstance(self.body, UndefinedLiteral):
466
+ return f"assert({args})"
467
+ return f"assert({args}) {self.body}"
462
468
 
463
469
  def build_scope(self, parent_scope: "Scope") -> None:
464
470
  self.scope = parent_scope
@@ -15,11 +15,15 @@ from .nodes import (
15
15
  EchoOp,
16
16
  AssertOp,
17
17
  LetOp,
18
+ UndefinedLiteral,
18
19
  PrimaryCall,
19
20
  ListComprehension,
20
21
  ListCompFor,
21
22
  ListCompCFor,
22
23
  ListCompLet,
24
+ ListCompIf,
25
+ ListCompIfElse,
26
+ ListCompEach,
23
27
  PositionalArgument,
24
28
  NamedArgument,
25
29
  )
@@ -105,14 +109,29 @@ def _fmt_list_elem(elem, indent: int, w: int) -> str:
105
109
  return f"let({assigns})\n{pad}{body}"
106
110
  if isinstance(elem, LetOp):
107
111
  formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
108
- body = str(elem.body)
112
+ body = _fmt_expr(elem.body, indent, w)
109
113
  if len(formatted) > 1 or any("\n" in fa for fa in formatted):
110
114
  assign_lines = (",\n" + inner_pad).join(formatted)
111
115
  return f"let(\n{inner_pad}{assign_lines}\n{pad})\n{pad}{body}"
112
116
  assigns = ", ".join(formatted)
113
- return f"let({assigns}) {body}"
117
+ inline = f"let({assigns}) {body}"
118
+ if "\n" in body or len(inline) + indent > _MULTILINE_CHAR_LIMIT:
119
+ return f"let({assigns})\n{pad}{body}"
120
+ return inline
114
121
  if isinstance(elem, ListComprehension):
115
122
  return _fmt_expr(elem, indent, w)
123
+ if isinstance(elem, ListCompIf):
124
+ cond = str(elem.condition)
125
+ body = _fmt_list_elem(elem.true_expr, indent + w, w)
126
+ return f"if ({cond})\n{inner_pad}{body}"
127
+ if isinstance(elem, ListCompIfElse):
128
+ cond = str(elem.condition)
129
+ true_body = _fmt_list_elem(elem.true_expr, indent + w, w)
130
+ false_body = _fmt_list_elem(elem.false_expr, indent + w, w)
131
+ return f"if ({cond})\n{inner_pad}{true_body}\n{pad}else\n{inner_pad}{false_body}"
132
+ if isinstance(elem, ListCompEach):
133
+ body = _fmt_list_elem(elem.body, indent, w)
134
+ return f"each {body}"
116
135
  return str(elem)
117
136
 
118
137
 
@@ -138,10 +157,43 @@ def _fmt_argument(arg, indent: int, w: int) -> str:
138
157
  return str(arg)
139
158
 
140
159
 
160
+ def _fmt_ternary_chain(expr: TernaryOp, indent: int, w: int) -> str:
161
+ """Format a right-chain of ternaries with flat ? / : alignment.
162
+
163
+ All ': ' connectors stay at the same indent column as the conditions;
164
+ each true branch is on its own line at indent+w. The '?' moves to
165
+ the end of the condition line rather than the beginning of the true-branch.
166
+
167
+ cond1?
168
+ true1
169
+ : cond2?
170
+ true2
171
+ : final_else
172
+ """
173
+ pad = " " * indent
174
+ inner_pad = " " * (indent + w)
175
+ parts = []
176
+ node: TernaryOp = expr
177
+ while isinstance(node, TernaryOp):
178
+ parts.append((node.condition, node.true_expr))
179
+ node = node.false_expr
180
+ final = node
181
+ lines = []
182
+ for i, (cond, true_expr) in enumerate(parts):
183
+ true_str = _fmt_expr(true_expr, indent + w, w)
184
+ prefix = "" if i == 0 else f"{pad}: "
185
+ lines.append(f"{prefix}{cond} ?\n{inner_pad}{true_str}")
186
+ lines.append(f"{pad}: {_fmt_expr(final, indent + w, w)}")
187
+ return "\n".join(lines)
188
+
189
+
141
190
  def _fmt_expr(expr, indent: int, w: int) -> str:
142
191
  """Format an expression with indent-aware layout for ternary, assert, and echo."""
143
192
  pad = " " * indent
144
193
  if isinstance(expr, TernaryOp):
194
+ # Right-chain of ternaries → flat cascade format
195
+ if isinstance(expr.false_expr, TernaryOp):
196
+ return _fmt_ternary_chain(expr, indent, w)
145
197
  pad2 = " " * (indent + 2)
146
198
  def _fmt_branch(branch):
147
199
  # Nested ternary: 2 spaces per nesting level
@@ -157,9 +209,13 @@ def _fmt_expr(expr, indent: int, w: int) -> str:
157
209
  )
158
210
  if isinstance(expr, AssertOp):
159
211
  args = ", ".join(str(a) for a in expr.arguments)
212
+ if isinstance(expr.body, UndefinedLiteral):
213
+ return f"assert({args})"
160
214
  return f"assert({args})\n{pad}{_fmt_expr(expr.body, indent, w)}"
161
215
  if isinstance(expr, EchoOp):
162
216
  args = ", ".join(str(a) for a in expr.arguments)
217
+ if isinstance(expr.body, UndefinedLiteral):
218
+ return f"echo({args})"
163
219
  return f"echo({args})\n{pad}{_fmt_expr(expr.body, indent, w)}"
164
220
  if isinstance(expr, LetOp):
165
221
  inner_pad = " " * (indent + w)
@@ -171,7 +227,7 @@ def _fmt_expr(expr, indent: int, w: int) -> str:
171
227
  f"{pad}{_fmt_expr(expr.body, indent, w)}"
172
228
  )
173
229
  assigns = ", ".join(formatted)
174
- return f"let({assigns})\n{pad}{_fmt_expr(expr.body, indent, w)}"
230
+ return f"let({assigns})\n{inner_pad}{_fmt_expr(expr.body, indent + w, w)}"
175
231
  if isinstance(expr, PrimaryCall):
176
232
  inline = str(expr)
177
233
  if len(inline) + indent > _MULTILINE_CHAR_LIMIT:
@@ -301,12 +357,24 @@ def _fmt_inst(node: ModuleInstantiation, indent: int, w: int, prefix: str = "")
301
357
  return f"{pad}{prefix}let ({assigns})" + _fmt_child(node.children, indent, w)
302
358
 
303
359
  if isinstance(node, ModularEcho):
304
- args = _join_str(node.arguments)
305
- return f"{pad}{prefix}echo({args})" + _fmt_child(node.children, indent, w)
360
+ head = f"{pad}{prefix}echo"
361
+ inline = f"{head}({_join_str(node.arguments)})"
362
+ if len(inline) > _MULTILINE_CHAR_LIMIT:
363
+ call = _fmt_multiline_args(head, node.arguments, indent, w,
364
+ fmt_fn=lambda a: _fmt_argument(a, indent + w, w))
365
+ else:
366
+ call = inline
367
+ return call + _fmt_child(node.children, indent, w)
306
368
 
307
369
  if isinstance(node, ModularAssert):
308
- args = _join_str(node.arguments)
309
- return f"{pad}{prefix}assert({args})" + _fmt_child(node.children, indent, w)
370
+ head = f"{pad}{prefix}assert"
371
+ inline = f"{head}({_join_str(node.arguments)})"
372
+ if len(inline) > _MULTILINE_CHAR_LIMIT:
373
+ call = _fmt_multiline_args(head, node.arguments, indent, w,
374
+ fmt_fn=lambda a: _fmt_argument(a, indent + w, w))
375
+ else:
376
+ call = inline
377
+ return call + _fmt_child(node.children, indent, w)
310
378
 
311
379
  if isinstance(node, ModularIf):
312
380
  header = f"{pad}{prefix}if ({node.condition})"