openscad-parser 2.3.4__tar.gz → 2.4.1__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.3.4
3
+ Version: 2.4.1
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
@@ -14,6 +14,9 @@ Classifier: Operating System :: MacOS :: MacOS X
14
14
  Classifier: Operating System :: Microsoft :: Windows
15
15
  Classifier: Operating System :: POSIX
16
16
  Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
17
20
  Classifier: Topic :: Artistic Software
18
21
  Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
19
22
  Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
@@ -62,6 +65,8 @@ Features
62
65
  - AST tree can contain comment nodes (single-line and multi-line)
63
66
  - AST tree uses dataclasses and can be pickled/unpickled for caching/serialization
64
67
  - JSON and YAML serialization/deserialization of AST trees
68
+ - Pretty-printer that converts an AST back to formatted OpenSCAD source (``to_openscad()``)
69
+ - Command-line interface (``openscad-parser``) for JSON/YAML/formatted output
65
70
 
66
71
  Installation
67
72
  ------------
@@ -355,107 +360,129 @@ Parsing Library Files
355
360
  AST Node Types
356
361
  --------------
357
362
 
358
- The AST includes comprehensive node types for all OpenSCAD language constructs:
363
+ The AST includes comprehensive node types for all OpenSCAD language constructs.
364
+ All nodes inherit from ``ASTNode`` and carry ``position: Position`` and ``scope: Scope | None`` attributes.
359
365
 
360
366
  Base Classes
361
367
  ~~~~~~~~~~~~
362
368
 
363
- - ``ASTNode``: Base class for all AST nodes (includes ``position`` attribute)
369
+ - ``ASTNode(position: Position, scope: Scope | None)``: Base class for all AST nodes
364
370
  - ``Expression``: Base class for all expression nodes
365
- - ``Primary``: Base class for atomic value types
371
+ - ``Primary``: Base class for atomic value types (extends ``Expression``)
366
372
  - ``ModuleInstantiation``: Base class for module-related statements
373
+ - ``VectorElement``: Base class for list comprehension elements
367
374
 
368
375
  Literals
369
376
  ~~~~~~~~
370
377
 
371
- - ``Identifier``: Variable, function, or module names
372
- - ``StringLiteral``: String values
373
- - ``NumberLiteral``: Numeric values
374
- - ``BooleanLiteral``: true/false values
375
- - ``UndefinedLiteral``: undef value
376
- - ``RangeLiteral``: Range expressions [start:end:step]
378
+ - ``Identifier(name: str)``: Variable, function, or module names
379
+ - ``StringLiteral(val: str)``: String values
380
+ - ``NumberLiteral(val: float)``: Numeric values
381
+ - ``BooleanLiteral(val: bool)``: true/false values
382
+ - ``UndefinedLiteral``: The ``undef`` value (no additional fields)
383
+ - ``RangeLiteral(start: Expression, end: Expression, step: Expression)``: Range expressions ``[start:step:end]``
377
384
 
378
385
  Operators
379
386
  ~~~~~~~~~
380
387
 
388
+ All operators inherit from ``Expression`` and represent their respective operations with typed fields for operands. The AST preserves operator precedence and associativity as defined in OpenSCAD.
389
+
381
390
  Arithmetic:
382
- - ``AdditionOp``, ``SubtractionOp``, ``MultiplicationOp``, ``DivisionOp``
383
- - ``ModuloOp``, ``ExponentOp``, ``UnaryMinusOp``
391
+
392
+ - ``AdditionOp(left: Expression, right: Expression)``: represents ``left + right``
393
+ - ``SubtractionOp(left: Expression, right: Expression)``: represents ``left - right``
394
+ - ``MultiplicationOp(left: Expression, right: Expression)``: represents ``left * right``
395
+ - ``DivisionOp(left: Expression, right: Expression)``: represents ``left / right``
396
+ - ``ModuloOp(left: Expression, right: Expression)``: represents ``left % right``
397
+ - ``ExponentOp(left: Expression, right: Expression)``: represents ``left ^ right``
398
+ - ``UnaryMinusOp(expr: Expression)``: represents ``-expr``
384
399
 
385
400
  Logical:
386
- - ``LogicalAndOp``, ``LogicalOrOp``, ``LogicalNotOp``
401
+
402
+ - ``LogicalAndOp(left: Expression, right: Expression)``: represents ``left && right``
403
+ - ``LogicalOrOp(left: Expression, right: Expression)``: represents ``left || right``
404
+ - ``LogicalNotOp(expr: Expression)``: represents ``!expr``
387
405
 
388
406
  Comparison:
389
- - ``EqualityOp``, ``InequalityOp``
390
- - ``GreaterThanOp``, ``GreaterThanOrEqualOp``
391
- - ``LessThanOp``, ``LessThanOrEqualOp``
407
+
408
+ - ``EqualityOp(left: Expression, right: Expression)``: represents ``left == right``
409
+ - ``InequalityOp(left: Expression, right: Expression)``: represents ``left != right``
410
+ - ``GreaterThanOp(left: Expression, right: Expression)``: represents ``left > right``
411
+ - ``GreaterThanOrEqualOp(left: Expression, right: Expression)``: represents ``left >= right``
412
+ - ``LessThanOp(left: Expression, right: Expression)``: represents ``left < right``
413
+ - ``LessThanOrEqualOp(left: Expression, right: Expression)``: represents ``left <= right``
392
414
 
393
415
  Bitwise:
394
- - ``BitwiseAndOp``, ``BitwiseOrOp``, ``BitwiseNotOp``
395
- - ``BitwiseShiftLeftOp``, ``BitwiseShiftRightOp``
416
+
417
+ - ``BitwiseAndOp(left: Expression, right: Expression)``: represents ``left & right``
418
+ - ``BitwiseOrOp(left: Expression, right: Expression)``: represents ``left | right``
419
+ - ``BitwiseShiftLeftOp(left: Expression, right: Expression)``: represents ``left << right``
420
+ - ``BitwiseShiftRightOp(left: Expression, right: Expression)``: represents ``left >> right``
421
+ - ``BitwiseNotOp(expr: Expression)``: represents ``~expr``
396
422
 
397
423
  Other:
398
- - ``TernaryOp``: condition ? true_expr : false_expr
424
+
425
+ - ``TernaryOp(condition: Expression, true_expr: Expression, false_expr: Expression)``: Represents ``condition ? true_expr : false_expr``
399
426
 
400
427
  Expressions
401
428
  ~~~~~~~~~~~
402
429
 
403
- - ``LetOp``: let(assignments) body
404
- - ``EchoOp``: echo(arguments) body
405
- - ``AssertOp``: assert(arguments) body
406
- - ``FunctionLiteral``: function(parameters) body
407
- - ``PrimaryCall``: function calls
408
- - ``PrimaryIndex``: array indexing [index]
409
- - ``PrimaryMember``: member access .member
430
+ - ``LetOp(assignments: list[Assignment], body: Expression)``: let clause ``let(assignments) body``
431
+ - ``EchoOp(arguments: list[Argument], body: Expression)``: echo clause ``echo(arguments) body``
432
+ - ``AssertOp(arguments: list[Argument], body: Expression)``: assert clause ``assert(arguments) body``
433
+ - ``FunctionLiteral(parameters: list[ParameterDeclaration], body: Expression)``: Anonymous function expression ``function(parameters) body``
434
+ - ``PrimaryCall(left: Expression, arguments: list[Argument])``: Function calls ``left(arguments)``
435
+ - ``PrimaryIndex(left: Expression, index: Expression)``: Array indexing ``left[index]``
436
+ - ``PrimaryMember(left: Expression, member: Identifier)``: Member access ``left.member``
410
437
 
411
438
  List Comprehensions
412
439
  ~~~~~~~~~~~~~~~~~~~
413
440
 
414
- - ``ListComprehension``: Vector/list literals
415
- - ``ListCompFor``: for loops in list comprehensions
416
- - ``ListCompCFor``: C-style for loops
417
- - ``ListCompIf``, ``ListCompIfElse``: Conditionals
418
- - ``ListCompLet``: let expressions
419
- - ``ListCompEach``: each expressions
441
+ - ``ListComprehension(elements: list[VectorElement])``: Vector/list literals ``[elements]``
442
+ - ``ListCompFor(assignments: list[Assignment], body: VectorElement)``: for loops in list comprehensions ``for(assignments) body``
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``
446
+ - ``ListCompLet(assignments: list[Assignment], body: VectorElement)``: let expressions in list comprehensions ``let(assignments) body``
447
+ - ``ListCompEach(body: VectorElement)``: each expressions (flattens nested lists) ``each body``
420
448
 
421
449
  Module Instantiations
422
450
  ~~~~~~~~~~~~~~~~~~~~~
423
451
 
424
- - ``ModularCall``: Module calls with arguments and children
425
- - ``ModularFor``: for loops
426
- - ``ModularCFor``: C-style for loops
427
- - ``ModularIntersectionFor``: intersection_for loops
428
- - ``ModularIntersectionCFor``: C-style intersection_for loops
429
- - ``ModularLet``: let statements
430
- - ``ModularEcho``: echo statements
431
- - ``ModularAssert``: assert statements
432
- - ``ModularIf``, ``ModularIfElse``: if/else statements
433
- - ``ModularModifierShowOnly``: ``!`` modifier
434
- - ``ModularModifierHighlight``: ``#`` modifier
435
- - ``ModularModifierBackground``: ``%`` modifier
436
- - ``ModularModifierDisable``: ``*`` modifier
452
+ - ``ModularCall(name: Identifier, arguments: list[Argument], children: list[ModuleInstantiation])``: Module calls ``name(arguments) { children }``
453
+ - ``ModularFor(assignments: list[Assignment], body: ModuleInstantiation)``: for loops in module bodies ``for(assignments) body``
454
+ - ``ModularIntersectionFor(assignments: list[Assignment], body: ModuleInstantiation)``: intersection_for loops ``intersection_for(assignments) body``
455
+ - ``ModularLet(assignments: list[Assignment], children: list[ModuleInstantiation])``: let statements in module bodies ``let(assignments) { children }``
456
+ - ``ModularEcho(arguments: list[Argument], children: list[ModuleInstantiation])``: echo statements in module bodies ``echo(arguments) { children }``
457
+ - ``ModularAssert(arguments: list[Argument], children: list[ModuleInstantiation])``: assert statements in module bodies ``assert(arguments) { children }``
458
+ - ``ModularIf(condition: Expression, true_branch: ModuleInstantiation)``: if statements in module bodies, with no else ``if(condition) true_branch``
459
+ - ``ModularIfElse(condition: Expression, true_branch: ModuleInstantiation, false_branch: ModuleInstantiation)``: if/else statements in module bodies ``if(condition) true_branch else false_branch``
460
+ - ``ModularModifierShowOnly(child: ModuleInstantiation)``: Show-Only modifier ``!child``
461
+ - ``ModularModifierHighlight(child: ModuleInstantiation)``: Highlight modifier ``#child``
462
+ - ``ModularModifierBackground(child: ModuleInstantiation)``: Background modifier ``%child``
463
+ - ``ModularModifierDisable(child: ModuleInstantiation)``: Disabler modifier ``*child``
437
464
 
438
465
  Declarations
439
466
  ~~~~~~~~~~~~
440
467
 
441
- - ``ModuleDeclaration``: module definitions
442
- - ``FunctionDeclaration``: function definitions
443
- - ``ParameterDeclaration``: function/module parameters
444
- - ``Assignment``: variable assignments
468
+ - ``ModuleDeclaration(name: Identifier, parameters: list[ParameterDeclaration], children: list[ModuleInstantiation | Assignment | FunctionDeclaration | ModuleDeclaration])``: Module definitions ``module name(parameters) { children }``
469
+ - ``FunctionDeclaration(name: Identifier, parameters: list[ParameterDeclaration], expr: Expression)``: Function definitions ``function name(parameters) = expr;``
470
+ - ``ParameterDeclaration(name: Identifier, default: Expression | None)``: Function/module parameter with optional default value ``name=default`` or ``name``
471
+ - ``Assignment(name: Identifier, expr: Expression)``: Variable assignments ``name = expr;``
445
472
 
446
473
  Statements
447
474
  ~~~~~~~~~~
448
475
 
449
- - ``UseStatement``: use <filepath>
450
- - ``IncludeStatement``: include <filepath>
451
- - ``PositionalArgument``: Function call positional arguments
452
- - ``NamedArgument``: Function call named arguments (name=value)
476
+ - ``UseStatement(filepath: StringLiteral)``: Represents ``use <filepath>``
477
+ - ``IncludeStatement(filepath: StringLiteral)``: Represents ``include <filepath>``
478
+ - ``PositionalArgument(expr: Expression)``: Function call positional arguments ``expr``
479
+ - ``NamedArgument(name: Identifier, expr: Expression)``: Function call named arguments ``name=expr``
453
480
 
454
481
  Comments
455
482
  ~~~~~~~~
456
483
 
457
- - ``CommentLine``: Single-line comments //
458
- - ``CommentSpan``: Multi-line comments ``/* */``
484
+ - ``CommentLine(text: str)``: Single-line comments ``// str``
485
+ - ``CommentSpan(text: str)``: Multi-line comments ``/* str */``
459
486
 
460
487
  All AST node classes are fully documented with docstrings that include:
461
488
  - Description of what the node represents
@@ -552,6 +579,13 @@ Main Functions
552
579
  - ``scope.lookup_module(name)`` — search this scope and its parents
553
580
  - ``scope.parent`` — the enclosing scope (``None`` for root)
554
581
 
582
+ ``to_openscad(nodes: list[ASTNode], indent_width: int = 4)``
583
+ Convert a list of AST nodes to formatted OpenSCAD source code.
584
+
585
+ :param nodes: Top-level AST nodes as returned by the ``getAST*`` functions.
586
+ :param indent_width: Spaces per indentation level (default: 4).
587
+ :returns: Formatted OpenSCAD source as a string.
588
+
555
589
  Serialization Functions
556
590
  ~~~~~~~~~~~~~~~~~~~~~~~
557
591
 
@@ -759,6 +793,100 @@ They are also available from ``openscad_parser.ast.serialization``::
759
793
  ast_from_yaml,
760
794
  )
761
795
 
796
+ Pretty-Printing
797
+ ---------------
798
+
799
+ The ``to_openscad()`` function converts an AST back to formatted OpenSCAD source code::
800
+
801
+ from openscad_parser.ast import getASTfromString, to_openscad
802
+
803
+ code = "module box(w,h){cube([w,h,1]);}"
804
+ ast = getASTfromString(code)
805
+
806
+ formatted = to_openscad(ast)
807
+ # module box(w, h) {
808
+ # cube([w, h, 1.0]);
809
+ # }
810
+ print(formatted)
811
+
812
+ The pretty-printer normalises whitespace and indentation while preserving the logical
813
+ structure of the code. It supports all AST node types including modules, functions,
814
+ control structures, modifiers, list comprehensions, and comments.
815
+
816
+ ``to_openscad(nodes, indent_width=4)``
817
+ Convert a list of AST nodes to formatted OpenSCAD source.
818
+
819
+ :param nodes: Top-level AST nodes (as returned by ``getAST*`` functions).
820
+ :param indent_width: Spaces per indentation level (default: 4).
821
+ :returns: Formatted OpenSCAD source code as a string.
822
+
823
+ - Blank lines are inserted before and after module/function declarations.
824
+ - Single-child module instantiations are formatted inline; multiple children use a block.
825
+ - Comments are preserved when the AST was parsed with ``include_comments=True``.
826
+
827
+ Controlling indentation::
828
+
829
+ from openscad_parser.ast import getASTfromString, to_openscad
830
+
831
+ ast = getASTfromString("module m() { cube(1); }")
832
+ print(to_openscad(ast, indent_width=2))
833
+ # module m() {
834
+ # cube(1.0);
835
+ # }
836
+
837
+ Command-Line Interface
838
+ -----------------------
839
+
840
+ The ``openscad-parser`` CLI is installed alongside the package::
841
+
842
+ pip install openscad-parser
843
+
844
+ Usage::
845
+
846
+ openscad-parser [OPTIONS] [FILE]
847
+
848
+ Read from a file or ``-`` for stdin. Default output is JSON.
849
+
850
+ **Options:**
851
+
852
+ ``--json``
853
+ Output AST as JSON (default).
854
+
855
+ ``--yaml``
856
+ Output AST as YAML (requires ``pip install openscad_parser[yaml]``).
857
+
858
+ ``--format``
859
+ Output reformatted OpenSCAD source code.
860
+
861
+ ``--indent N``
862
+ Indentation width in spaces (default: 4). Applies to ``--format`` and ``--json``.
863
+
864
+ ``--include-comments``
865
+ Include comment nodes in the output.
866
+
867
+ ``--no-includes``
868
+ Do not expand ``include <...>`` statements; keep ``IncludeStatement`` nodes instead.
869
+
870
+ **Examples:**
871
+
872
+ Dump AST as JSON::
873
+
874
+ openscad-parser model.scad
875
+ openscad-parser - < model.scad # stdin
876
+
877
+ Reformat OpenSCAD source::
878
+
879
+ openscad-parser --format model.scad
880
+ openscad-parser --format --indent 2 model.scad
881
+
882
+ Output YAML::
883
+
884
+ openscad-parser --yaml model.scad
885
+
886
+ Include comments in the AST::
887
+
888
+ openscad-parser --include-comments --json model.scad
889
+
762
890
  Error Handling
763
891
  --------------
764
892
 
@@ -22,6 +22,8 @@ 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()``)
26
+ - Command-line interface (``openscad-parser``) for JSON/YAML/formatted output
25
27
 
26
28
  Installation
27
29
  ------------
@@ -315,107 +317,129 @@ Parsing Library Files
315
317
  AST Node Types
316
318
  --------------
317
319
 
318
- The AST includes comprehensive node types for all OpenSCAD language constructs:
320
+ The AST includes comprehensive node types for all OpenSCAD language constructs.
321
+ All nodes inherit from ``ASTNode`` and carry ``position: Position`` and ``scope: Scope | None`` attributes.
319
322
 
320
323
  Base Classes
321
324
  ~~~~~~~~~~~~
322
325
 
323
- - ``ASTNode``: Base class for all AST nodes (includes ``position`` attribute)
326
+ - ``ASTNode(position: Position, scope: Scope | None)``: Base class for all AST nodes
324
327
  - ``Expression``: Base class for all expression nodes
325
- - ``Primary``: Base class for atomic value types
328
+ - ``Primary``: Base class for atomic value types (extends ``Expression``)
326
329
  - ``ModuleInstantiation``: Base class for module-related statements
330
+ - ``VectorElement``: Base class for list comprehension elements
327
331
 
328
332
  Literals
329
333
  ~~~~~~~~
330
334
 
331
- - ``Identifier``: Variable, function, or module names
332
- - ``StringLiteral``: String values
333
- - ``NumberLiteral``: Numeric values
334
- - ``BooleanLiteral``: true/false values
335
- - ``UndefinedLiteral``: undef value
336
- - ``RangeLiteral``: Range expressions [start:end:step]
335
+ - ``Identifier(name: str)``: Variable, function, or module names
336
+ - ``StringLiteral(val: str)``: String values
337
+ - ``NumberLiteral(val: float)``: Numeric values
338
+ - ``BooleanLiteral(val: bool)``: true/false values
339
+ - ``UndefinedLiteral``: The ``undef`` value (no additional fields)
340
+ - ``RangeLiteral(start: Expression, end: Expression, step: Expression)``: Range expressions ``[start:step:end]``
337
341
 
338
342
  Operators
339
343
  ~~~~~~~~~
340
344
 
345
+ All operators inherit from ``Expression`` and represent their respective operations with typed fields for operands. The AST preserves operator precedence and associativity as defined in OpenSCAD.
346
+
341
347
  Arithmetic:
342
- - ``AdditionOp``, ``SubtractionOp``, ``MultiplicationOp``, ``DivisionOp``
343
- - ``ModuloOp``, ``ExponentOp``, ``UnaryMinusOp``
348
+
349
+ - ``AdditionOp(left: Expression, right: Expression)``: represents ``left + right``
350
+ - ``SubtractionOp(left: Expression, right: Expression)``: represents ``left - right``
351
+ - ``MultiplicationOp(left: Expression, right: Expression)``: represents ``left * right``
352
+ - ``DivisionOp(left: Expression, right: Expression)``: represents ``left / right``
353
+ - ``ModuloOp(left: Expression, right: Expression)``: represents ``left % right``
354
+ - ``ExponentOp(left: Expression, right: Expression)``: represents ``left ^ right``
355
+ - ``UnaryMinusOp(expr: Expression)``: represents ``-expr``
344
356
 
345
357
  Logical:
346
- - ``LogicalAndOp``, ``LogicalOrOp``, ``LogicalNotOp``
358
+
359
+ - ``LogicalAndOp(left: Expression, right: Expression)``: represents ``left && right``
360
+ - ``LogicalOrOp(left: Expression, right: Expression)``: represents ``left || right``
361
+ - ``LogicalNotOp(expr: Expression)``: represents ``!expr``
347
362
 
348
363
  Comparison:
349
- - ``EqualityOp``, ``InequalityOp``
350
- - ``GreaterThanOp``, ``GreaterThanOrEqualOp``
351
- - ``LessThanOp``, ``LessThanOrEqualOp``
364
+
365
+ - ``EqualityOp(left: Expression, right: Expression)``: represents ``left == right``
366
+ - ``InequalityOp(left: Expression, right: Expression)``: represents ``left != right``
367
+ - ``GreaterThanOp(left: Expression, right: Expression)``: represents ``left > right``
368
+ - ``GreaterThanOrEqualOp(left: Expression, right: Expression)``: represents ``left >= right``
369
+ - ``LessThanOp(left: Expression, right: Expression)``: represents ``left < right``
370
+ - ``LessThanOrEqualOp(left: Expression, right: Expression)``: represents ``left <= right``
352
371
 
353
372
  Bitwise:
354
- - ``BitwiseAndOp``, ``BitwiseOrOp``, ``BitwiseNotOp``
355
- - ``BitwiseShiftLeftOp``, ``BitwiseShiftRightOp``
373
+
374
+ - ``BitwiseAndOp(left: Expression, right: Expression)``: represents ``left & right``
375
+ - ``BitwiseOrOp(left: Expression, right: Expression)``: represents ``left | right``
376
+ - ``BitwiseShiftLeftOp(left: Expression, right: Expression)``: represents ``left << right``
377
+ - ``BitwiseShiftRightOp(left: Expression, right: Expression)``: represents ``left >> right``
378
+ - ``BitwiseNotOp(expr: Expression)``: represents ``~expr``
356
379
 
357
380
  Other:
358
- - ``TernaryOp``: condition ? true_expr : false_expr
381
+
382
+ - ``TernaryOp(condition: Expression, true_expr: Expression, false_expr: Expression)``: Represents ``condition ? true_expr : false_expr``
359
383
 
360
384
  Expressions
361
385
  ~~~~~~~~~~~
362
386
 
363
- - ``LetOp``: let(assignments) body
364
- - ``EchoOp``: echo(arguments) body
365
- - ``AssertOp``: assert(arguments) body
366
- - ``FunctionLiteral``: function(parameters) body
367
- - ``PrimaryCall``: function calls
368
- - ``PrimaryIndex``: array indexing [index]
369
- - ``PrimaryMember``: member access .member
387
+ - ``LetOp(assignments: list[Assignment], body: Expression)``: let clause ``let(assignments) body``
388
+ - ``EchoOp(arguments: list[Argument], body: Expression)``: echo clause ``echo(arguments) body``
389
+ - ``AssertOp(arguments: list[Argument], body: Expression)``: assert clause ``assert(arguments) body``
390
+ - ``FunctionLiteral(parameters: list[ParameterDeclaration], body: Expression)``: Anonymous function expression ``function(parameters) body``
391
+ - ``PrimaryCall(left: Expression, arguments: list[Argument])``: Function calls ``left(arguments)``
392
+ - ``PrimaryIndex(left: Expression, index: Expression)``: Array indexing ``left[index]``
393
+ - ``PrimaryMember(left: Expression, member: Identifier)``: Member access ``left.member``
370
394
 
371
395
  List Comprehensions
372
396
  ~~~~~~~~~~~~~~~~~~~
373
397
 
374
- - ``ListComprehension``: Vector/list literals
375
- - ``ListCompFor``: for loops in list comprehensions
376
- - ``ListCompCFor``: C-style for loops
377
- - ``ListCompIf``, ``ListCompIfElse``: Conditionals
378
- - ``ListCompLet``: let expressions
379
- - ``ListCompEach``: each expressions
398
+ - ``ListComprehension(elements: list[VectorElement])``: Vector/list literals ``[elements]``
399
+ - ``ListCompFor(assignments: list[Assignment], body: VectorElement)``: for loops in list comprehensions ``for(assignments) body``
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``
403
+ - ``ListCompLet(assignments: list[Assignment], body: VectorElement)``: let expressions in list comprehensions ``let(assignments) body``
404
+ - ``ListCompEach(body: VectorElement)``: each expressions (flattens nested lists) ``each body``
380
405
 
381
406
  Module Instantiations
382
407
  ~~~~~~~~~~~~~~~~~~~~~
383
408
 
384
- - ``ModularCall``: Module calls with arguments and children
385
- - ``ModularFor``: for loops
386
- - ``ModularCFor``: C-style for loops
387
- - ``ModularIntersectionFor``: intersection_for loops
388
- - ``ModularIntersectionCFor``: C-style intersection_for loops
389
- - ``ModularLet``: let statements
390
- - ``ModularEcho``: echo statements
391
- - ``ModularAssert``: assert statements
392
- - ``ModularIf``, ``ModularIfElse``: if/else statements
393
- - ``ModularModifierShowOnly``: ``!`` modifier
394
- - ``ModularModifierHighlight``: ``#`` modifier
395
- - ``ModularModifierBackground``: ``%`` modifier
396
- - ``ModularModifierDisable``: ``*`` modifier
409
+ - ``ModularCall(name: Identifier, arguments: list[Argument], children: list[ModuleInstantiation])``: Module calls ``name(arguments) { children }``
410
+ - ``ModularFor(assignments: list[Assignment], body: ModuleInstantiation)``: for loops in module bodies ``for(assignments) body``
411
+ - ``ModularIntersectionFor(assignments: list[Assignment], body: ModuleInstantiation)``: intersection_for loops ``intersection_for(assignments) body``
412
+ - ``ModularLet(assignments: list[Assignment], children: list[ModuleInstantiation])``: let statements in module bodies ``let(assignments) { children }``
413
+ - ``ModularEcho(arguments: list[Argument], children: list[ModuleInstantiation])``: echo statements in module bodies ``echo(arguments) { children }``
414
+ - ``ModularAssert(arguments: list[Argument], children: list[ModuleInstantiation])``: assert statements in module bodies ``assert(arguments) { children }``
415
+ - ``ModularIf(condition: Expression, true_branch: ModuleInstantiation)``: if statements in module bodies, with no else ``if(condition) true_branch``
416
+ - ``ModularIfElse(condition: Expression, true_branch: ModuleInstantiation, false_branch: ModuleInstantiation)``: if/else statements in module bodies ``if(condition) true_branch else false_branch``
417
+ - ``ModularModifierShowOnly(child: ModuleInstantiation)``: Show-Only modifier ``!child``
418
+ - ``ModularModifierHighlight(child: ModuleInstantiation)``: Highlight modifier ``#child``
419
+ - ``ModularModifierBackground(child: ModuleInstantiation)``: Background modifier ``%child``
420
+ - ``ModularModifierDisable(child: ModuleInstantiation)``: Disabler modifier ``*child``
397
421
 
398
422
  Declarations
399
423
  ~~~~~~~~~~~~
400
424
 
401
- - ``ModuleDeclaration``: module definitions
402
- - ``FunctionDeclaration``: function definitions
403
- - ``ParameterDeclaration``: function/module parameters
404
- - ``Assignment``: variable assignments
425
+ - ``ModuleDeclaration(name: Identifier, parameters: list[ParameterDeclaration], children: list[ModuleInstantiation | Assignment | FunctionDeclaration | ModuleDeclaration])``: Module definitions ``module name(parameters) { children }``
426
+ - ``FunctionDeclaration(name: Identifier, parameters: list[ParameterDeclaration], expr: Expression)``: Function definitions ``function name(parameters) = expr;``
427
+ - ``ParameterDeclaration(name: Identifier, default: Expression | None)``: Function/module parameter with optional default value ``name=default`` or ``name``
428
+ - ``Assignment(name: Identifier, expr: Expression)``: Variable assignments ``name = expr;``
405
429
 
406
430
  Statements
407
431
  ~~~~~~~~~~
408
432
 
409
- - ``UseStatement``: use <filepath>
410
- - ``IncludeStatement``: include <filepath>
411
- - ``PositionalArgument``: Function call positional arguments
412
- - ``NamedArgument``: Function call named arguments (name=value)
433
+ - ``UseStatement(filepath: StringLiteral)``: Represents ``use <filepath>``
434
+ - ``IncludeStatement(filepath: StringLiteral)``: Represents ``include <filepath>``
435
+ - ``PositionalArgument(expr: Expression)``: Function call positional arguments ``expr``
436
+ - ``NamedArgument(name: Identifier, expr: Expression)``: Function call named arguments ``name=expr``
413
437
 
414
438
  Comments
415
439
  ~~~~~~~~
416
440
 
417
- - ``CommentLine``: Single-line comments //
418
- - ``CommentSpan``: Multi-line comments ``/* */``
441
+ - ``CommentLine(text: str)``: Single-line comments ``// str``
442
+ - ``CommentSpan(text: str)``: Multi-line comments ``/* str */``
419
443
 
420
444
  All AST node classes are fully documented with docstrings that include:
421
445
  - Description of what the node represents
@@ -512,6 +536,13 @@ Main Functions
512
536
  - ``scope.lookup_module(name)`` — search this scope and its parents
513
537
  - ``scope.parent`` — the enclosing scope (``None`` for root)
514
538
 
539
+ ``to_openscad(nodes: list[ASTNode], indent_width: int = 4)``
540
+ Convert a list of AST nodes to formatted OpenSCAD source code.
541
+
542
+ :param nodes: Top-level AST nodes as returned by the ``getAST*`` functions.
543
+ :param indent_width: Spaces per indentation level (default: 4).
544
+ :returns: Formatted OpenSCAD source as a string.
545
+
515
546
  Serialization Functions
516
547
  ~~~~~~~~~~~~~~~~~~~~~~~
517
548
 
@@ -719,6 +750,100 @@ They are also available from ``openscad_parser.ast.serialization``::
719
750
  ast_from_yaml,
720
751
  )
721
752
 
753
+ Pretty-Printing
754
+ ---------------
755
+
756
+ The ``to_openscad()`` function converts an AST back to formatted OpenSCAD source code::
757
+
758
+ from openscad_parser.ast import getASTfromString, to_openscad
759
+
760
+ code = "module box(w,h){cube([w,h,1]);}"
761
+ ast = getASTfromString(code)
762
+
763
+ formatted = to_openscad(ast)
764
+ # module box(w, h) {
765
+ # cube([w, h, 1.0]);
766
+ # }
767
+ print(formatted)
768
+
769
+ The pretty-printer normalises whitespace and indentation while preserving the logical
770
+ structure of the code. It supports all AST node types including modules, functions,
771
+ control structures, modifiers, list comprehensions, and comments.
772
+
773
+ ``to_openscad(nodes, indent_width=4)``
774
+ Convert a list of AST nodes to formatted OpenSCAD source.
775
+
776
+ :param nodes: Top-level AST nodes (as returned by ``getAST*`` functions).
777
+ :param indent_width: Spaces per indentation level (default: 4).
778
+ :returns: Formatted OpenSCAD source code as a string.
779
+
780
+ - Blank lines are inserted before and after module/function declarations.
781
+ - Single-child module instantiations are formatted inline; multiple children use a block.
782
+ - Comments are preserved when the AST was parsed with ``include_comments=True``.
783
+
784
+ Controlling indentation::
785
+
786
+ from openscad_parser.ast import getASTfromString, to_openscad
787
+
788
+ ast = getASTfromString("module m() { cube(1); }")
789
+ print(to_openscad(ast, indent_width=2))
790
+ # module m() {
791
+ # cube(1.0);
792
+ # }
793
+
794
+ Command-Line Interface
795
+ -----------------------
796
+
797
+ The ``openscad-parser`` CLI is installed alongside the package::
798
+
799
+ pip install openscad-parser
800
+
801
+ Usage::
802
+
803
+ openscad-parser [OPTIONS] [FILE]
804
+
805
+ Read from a file or ``-`` for stdin. Default output is JSON.
806
+
807
+ **Options:**
808
+
809
+ ``--json``
810
+ Output AST as JSON (default).
811
+
812
+ ``--yaml``
813
+ Output AST as YAML (requires ``pip install openscad_parser[yaml]``).
814
+
815
+ ``--format``
816
+ Output reformatted OpenSCAD source code.
817
+
818
+ ``--indent N``
819
+ Indentation width in spaces (default: 4). Applies to ``--format`` and ``--json``.
820
+
821
+ ``--include-comments``
822
+ Include comment nodes in the output.
823
+
824
+ ``--no-includes``
825
+ Do not expand ``include <...>`` statements; keep ``IncludeStatement`` nodes instead.
826
+
827
+ **Examples:**
828
+
829
+ Dump AST as JSON::
830
+
831
+ openscad-parser model.scad
832
+ openscad-parser - < model.scad # stdin
833
+
834
+ Reformat OpenSCAD source::
835
+
836
+ openscad-parser --format model.scad
837
+ openscad-parser --format --indent 2 model.scad
838
+
839
+ Output YAML::
840
+
841
+ openscad-parser --yaml model.scad
842
+
843
+ Include comments in the AST::
844
+
845
+ openscad-parser --include-comments --json model.scad
846
+
722
847
  Error Handling
723
848
  --------------
724
849
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "openscad_parser"
7
- version = "2.3.4"
7
+ version = "2.4.1"
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 = [
@@ -24,6 +24,9 @@ classifiers = [
24
24
  "Operating System :: Microsoft :: Windows",
25
25
  "Operating System :: POSIX",
26
26
  "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
27
30
  "Topic :: Artistic Software",
28
31
  "Topic :: Multimedia :: Graphics :: 3D Modeling",
29
32
  "Topic :: Multimedia :: Graphics :: 3D Rendering",
@@ -35,6 +38,9 @@ dependencies = [
35
38
  "arpeggio>=2.0.3",
36
39
  ]
37
40
 
41
+ [project.scripts]
42
+ openscad-parser = "openscad_parser.cli:main"
43
+
38
44
  [project.optional-dependencies]
39
45
  dev = [
40
46
  "pytest>=7.0.0",