openscad-parser 2.4.5__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.5
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
@@ -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.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
@@ -1350,7 +1356,7 @@ class ListCompIf(VectorElement):
1350
1356
  true_expr: VectorElement
1351
1357
 
1352
1358
  def __str__(self):
1353
- return f"if {self.condition} {self.true_expr}"
1359
+ return f"if ({self.condition}) {self.true_expr}"
1354
1360
 
1355
1361
  def build_scope(self, parent_scope: "Scope") -> None:
1356
1362
  self.scope = parent_scope
@@ -1379,7 +1385,7 @@ class ListCompIfElse(VectorElement):
1379
1385
  false_expr: VectorElement
1380
1386
 
1381
1387
  def __str__(self):
1382
- return f"if {self.condition} {self.true_expr} else {self.false_expr}"
1388
+ return f"if ({self.condition}) {self.true_expr} else {self.false_expr}"
1383
1389
 
1384
1390
  def build_scope(self, parent_scope: "Scope") -> None:
1385
1391
  self.scope = parent_scope
@@ -15,11 +15,17 @@ 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,
27
+ PositionalArgument,
28
+ NamedArgument,
23
29
  )
24
30
 
25
31
 
@@ -66,17 +72,33 @@ def _fmt_list_elem(elem, indent: int, w: int) -> str:
66
72
  pad = " " * indent
67
73
  inner_pad = " " * (indent + w)
68
74
  if isinstance(elem, ListCompFor):
69
- assigns_inline = ", ".join(str(a) for a in elem.assignments)
75
+ formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
70
76
  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)
77
+ assigns_inline = ", ".join(formatted)
78
+ if any("\n" in fa for fa in formatted) or len(f"for ({assigns_inline})") + indent > _MULTILINE_CHAR_LIMIT:
79
+ assign_lines = (",\n" + inner_pad).join(formatted)
73
80
  return f"for (\n{inner_pad}{assign_lines}\n{pad})\n{inner_pad}{body}"
74
81
  return f"for ({assigns_inline})\n{inner_pad}{body}"
75
82
  if isinstance(elem, ListCompCFor):
76
- inits = ", ".join(str(a) for a in elem.inits)
77
- incrs = ", ".join(str(a) for a in elem.incrs)
83
+ fmt_inits = [_fmt_assign(a, indent + w, w) for a in elem.inits]
84
+ fmt_incrs = [_fmt_assign(a, indent + w, w) for a in elem.incrs]
85
+ inits_str = ", ".join(fmt_inits)
86
+ incrs_str = ", ".join(fmt_incrs)
87
+ cond_str = str(elem.condition)
78
88
  body = _fmt_list_elem(elem.body, indent + w, w)
79
- return f"for ({inits}; {elem.condition}; {incrs})\n{inner_pad}{body}"
89
+ header = f"for ({inits_str}; {cond_str}; {incrs_str})"
90
+ any_multiline = (
91
+ any("\n" in fa for fa in fmt_inits) or
92
+ any("\n" in fa for fa in fmt_incrs) or
93
+ "\n" in cond_str
94
+ )
95
+ if any_multiline or len(header) + indent > _MULTILINE_CHAR_LIMIT:
96
+ return (
97
+ f"for (\n{inner_pad}{inits_str};\n"
98
+ f"{inner_pad}{cond_str};\n"
99
+ f"{inner_pad}{incrs_str}\n{pad})\n{inner_pad}{body}"
100
+ )
101
+ return f"{header}\n{inner_pad}{body}"
80
102
  if isinstance(elem, ListCompLet):
81
103
  formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
82
104
  body = _fmt_list_elem(elem.body, indent, w)
@@ -84,23 +106,40 @@ def _fmt_list_elem(elem, indent: int, w: int) -> str:
84
106
  assign_lines = (",\n" + inner_pad).join(formatted)
85
107
  return f"let(\n{inner_pad}{assign_lines}\n{pad})\n{pad}{body}"
86
108
  assigns = ", ".join(formatted)
87
- return f"let({assigns}) {body}"
109
+ return f"let({assigns})\n{pad}{body}"
88
110
  if isinstance(elem, LetOp):
89
111
  formatted = [_fmt_assign(a, indent + w, w) for a in elem.assignments]
90
- body = str(elem.body)
112
+ body = _fmt_expr(elem.body, indent, w)
91
113
  if len(formatted) > 1 or any("\n" in fa for fa in formatted):
92
114
  assign_lines = (",\n" + inner_pad).join(formatted)
93
115
  return f"let(\n{inner_pad}{assign_lines}\n{pad})\n{pad}{body}"
94
116
  assigns = ", ".join(formatted)
95
- 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
121
+ if isinstance(elem, ListComprehension):
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}"
96
135
  return str(elem)
97
136
 
98
137
 
99
- def _fmt_multiline_args(head: str, args: list, indent: int, w: int) -> str:
138
+ def _fmt_multiline_args(head: str, args: list, indent: int, w: int, fmt_fn=str) -> str:
100
139
  """Format `head(arg1, arg2, ...)` with each arg on its own line."""
101
140
  inner_pad = " " * (indent + w)
102
141
  pad = " " * indent
103
- arg_lines = (",\n" + inner_pad).join(str(a) for a in args)
142
+ arg_lines = (",\n" + inner_pad).join(fmt_fn(a) for a in args)
104
143
  return f"{head}(\n{inner_pad}{arg_lines}\n{pad})"
105
144
 
106
145
 
@@ -109,21 +148,74 @@ def _fmt_assign(assign, indent: int, w: int) -> str:
109
148
  return f"{assign.name} = {_fmt_expr(assign.expr, indent, w)}"
110
149
 
111
150
 
151
+ def _fmt_argument(arg, indent: int, w: int) -> str:
152
+ """Format a call argument, routing its expression through _fmt_expr."""
153
+ if isinstance(arg, PositionalArgument):
154
+ return _fmt_expr(arg.expr, indent, w)
155
+ if isinstance(arg, NamedArgument):
156
+ return f"{arg.name}={_fmt_expr(arg.expr, indent, w)}"
157
+ return str(arg)
158
+
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
+
112
190
  def _fmt_expr(expr, indent: int, w: int) -> str:
113
191
  """Format an expression with indent-aware layout for ternary, assert, and echo."""
114
192
  pad = " " * indent
115
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)
116
197
  pad2 = " " * (indent + 2)
198
+ def _fmt_branch(branch):
199
+ # Nested ternary: 2 spaces per nesting level
200
+ if isinstance(branch, TernaryOp):
201
+ return _fmt_expr(branch, indent + 2, w)
202
+ # All other expressions: their visual start is 2 chars into "? "/":", "
203
+ # so block content and closing delimiters align to indent + 4
204
+ return _fmt_expr(branch, indent + 4, w)
117
205
  return (
118
206
  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)}"
207
+ f"{pad2}? {_fmt_branch(expr.true_expr)}\n"
208
+ f"{pad2}: {_fmt_branch(expr.false_expr)}"
121
209
  )
122
210
  if isinstance(expr, AssertOp):
123
211
  args = ", ".join(str(a) for a in expr.arguments)
212
+ if isinstance(expr.body, UndefinedLiteral):
213
+ return f"assert({args})"
124
214
  return f"assert({args})\n{pad}{_fmt_expr(expr.body, indent, w)}"
125
215
  if isinstance(expr, EchoOp):
126
216
  args = ", ".join(str(a) for a in expr.arguments)
217
+ if isinstance(expr.body, UndefinedLiteral):
218
+ return f"echo({args})"
127
219
  return f"echo({args})\n{pad}{_fmt_expr(expr.body, indent, w)}"
128
220
  if isinstance(expr, LetOp):
129
221
  inner_pad = " " * (indent + w)
@@ -135,22 +227,19 @@ def _fmt_expr(expr, indent: int, w: int) -> str:
135
227
  f"{pad}{_fmt_expr(expr.body, indent, w)}"
136
228
  )
137
229
  assigns = ", ".join(formatted)
138
- 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)}"
139
231
  if isinstance(expr, PrimaryCall):
140
232
  inline = str(expr)
141
233
  if len(inline) + indent > _MULTILINE_CHAR_LIMIT:
142
- return _fmt_multiline_args(str(expr.left), expr.arguments, indent, w)
234
+ return _fmt_multiline_args(
235
+ str(expr.left), expr.arguments, indent, w,
236
+ fmt_fn=lambda a: _fmt_argument(a, indent + w, w),
237
+ )
143
238
  if isinstance(expr, ListComprehension):
144
239
  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]
240
+ formatted_elems = [_fmt_list_elem(e, indent + w, w) for e in expr.elements]
241
+ any_multiline = any("\n" in fe for fe in formatted_elems)
242
+ if len(str(expr)) + indent > _MULTILINE_CHAR_LIMIT or any_multiline:
154
243
  items = (",\n" + inner_pad).join(formatted_elems)
155
244
  return f"[\n{inner_pad}{items}\n{pad}]"
156
245
  return str(expr)
@@ -241,7 +330,10 @@ def _fmt_inst(node: ModuleInstantiation, indent: int, w: int, prefix: str = "")
241
330
  head = f"{pad}{prefix}{node.name}"
242
331
  inline = f"{head}({_join_str(node.arguments)})"
243
332
  if len(inline) > _MULTILINE_CHAR_LIMIT:
244
- call = _fmt_multiline_args(head, node.arguments, indent, w)
333
+ call = _fmt_multiline_args(
334
+ head, node.arguments, indent, w,
335
+ fmt_fn=lambda a: _fmt_argument(a, indent + w, w),
336
+ )
245
337
  else:
246
338
  call = inline
247
339
  return call + _fmt_child(node.children, indent, w)
@@ -265,12 +357,24 @@ def _fmt_inst(node: ModuleInstantiation, indent: int, w: int, prefix: str = "")
265
357
  return f"{pad}{prefix}let ({assigns})" + _fmt_child(node.children, indent, w)
266
358
 
267
359
  if isinstance(node, ModularEcho):
268
- args = _join_str(node.arguments)
269
- 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)
270
368
 
271
369
  if isinstance(node, ModularAssert):
272
- args = _join_str(node.arguments)
273
- 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)
274
378
 
275
379
  if isinstance(node, ModularIf):
276
380
  header = f"{pad}{prefix}if ({node.condition})"