A PEG parser to read OpenSCAD language source code, with optional AST tree generation.
Project description
A PEG parser for the OpenSCAD language that can parse OpenSCAD source code and optionally generate an Abstract Syntax Tree (AST) for programmatic analysis and manipulation.
Features
Full OpenSCAD language support including: - Module and function definitions - Expressions with proper operator precedence - Control structures (if/else, for loops, let, assert, echo) - List comprehensions - Module modifiers (!, #, %, *) - Use and include statements
Parse tree generation using Arpeggio PEG parser
AST generation with comprehensive node types
Source position tracking for all AST nodes
AST tree can contain comment nodes (single-line and multi-line)
AST tree uses dataclasses and can be pickled/unpickled for caching/serialization
Installation
Install from PyPI:
pip install openscad-parser
Or install from source:
git clone https://github.com/belfryscad/openscad_parser.git cd openscad_parser pip install -e .
Basic Usage
Parsing OpenSCAD Code
To parse OpenSCAD code, first create a parser instance, then parse your code:
from openscad_parser import getOpenSCADParser
# Create a parser instance
parser = getOpenSCADParser(reduce_tree=False)
# Parse OpenSCAD code
code = """
module test(x, y=10) {
cube([x, y, 5]);
translate([0, 0, y]) sphere(5);
}
"""
parse_tree = parser.parse(code)
The parser returns an Arpeggio parse tree that represents the structure of your OpenSCAD code.
Parser Options
The getOpenSCADParser() function accepts several options:
parser = getOpenSCADParser(
reduce_tree=False, # Keep full parse tree (default: False)
debug=False # Enable debug output (default: False)
)
reduce_tree: If True, reduces the parse tree by removing single-child nodes. Set to False when generating ASTs.
debug: If True, enables verbose debug output during parsing.
AST Generation
The parser can convert the parse tree into an Abstract Syntax Tree (AST) with typed nodes for easier programmatic manipulation.
Convenience Functions
The easiest way to generate ASTs is using the convenience functions that handle parser creation automatically:
Parsing from a String
Use getASTfromString() to parse OpenSCAD code from a string:
from openscad_parser.ast import getASTfromString code = "x = 10 + 5;" ast = getASTfromString(code) # ast is a list of top-level statements assignment = ast[0] print(assignment.name.name) # "x" print(assignment.expr) # AdditionOp(left=NumberLiteral(10), right=NumberLiteral(5))
Parsing from a File
Use getASTfromFile() to parse an OpenSCAD file. This function includes automatic caching - files are only re-parsed if their modification timestamp changes:
from openscad_parser.ast import getASTfromFile
# Parse a file (cached automatically)
ast = getASTfromFile("my_model.scad")
# Subsequent calls return cached AST if file hasn't changed
ast2 = getASTfromFile("my_model.scad") # Returns cached version
The cache is automatically invalidated when the file is modified, ensuring you always get up-to-date results.
Parsing Library Files
Use getASTfromLibraryFile() to find and parse library files using OpenSCAD’s search path rules. This is useful for resolving use and include statements:
from openscad_parser.ast import getASTfromLibraryFile
# From a file that includes a library
# Searches: current file directory, OPENSCADPATH, platform defaults
# Returns: (AST, absolute_path) tuple
ast, path = getASTfromLibraryFile("/path/to/main.scad", "utils/math.scad")
# Or without current file context
ast, path = getASTfromLibraryFile("", "MCAD/boxes.scad")
The function searches for library files in this order:
Directory of the current file (if provided)
Directories in the OPENSCADPATH environment variable
Platform-specific default library directories: - Windows: ~/Documents/OpenSCAD/libraries - macOS: ~/Documents/OpenSCAD/libraries - Linux: ~/.local/share/OpenSCAD/libraries
Advanced AST Generation
For more control, you can use parse_ast() directly with a custom parser instance:
from openscad_parser import getOpenSCADParser from openscad_parser.ast import parse_ast # Create a parser instance parser = getOpenSCADParser(reduce_tree=False) # Parse and generate AST code = "x = 10 + 5;" ast = parse_ast(parser, code) # ast is a list of top-level statements assignment = ast[0] print(assignment.name.name) # "x" print(assignment.expr) # AdditionOp(left=NumberLiteral(10), right=NumberLiteral(5))
The parse_ast() function is the lower-level API for AST generation. It takes:
parser: An Arpeggio parser instance (from getOpenSCADParser())
code: The OpenSCAD code string to parse
file: Optional file path for source location tracking
Working with AST Nodes
All AST nodes inherit from ASTNode and have a position attribute for source location tracking:
from openscad_parser.ast import (
getASTfromString, Assignment, Identifier, NumberLiteral, AdditionOp
)
code = "result = 10 + 20;"
ast = getASTfromString(code)
assignment = ast[0]
# Check node types
assert isinstance(assignment, Assignment)
assert isinstance(assignment.name, Identifier)
assert isinstance(assignment.expr, AdditionOp)
# Access node properties
print(assignment.name.name) # "result"
print(assignment.expr.left.val) # 10
print(assignment.expr.right.val) # 20
# Access source position
print(assignment.position.line) # Line number (1-indexed)
print(assignment.position.char) # Column number (1-indexed)
Examples
Parsing a Simple Assignment
from openscad_parser import getOpenSCADParser from openscad_parser.ast import parse_ast, Assignment, Identifier parser = getOpenSCADParser(reduce_tree=False) code = "x = 42;" ast = parse_ast(parser, code) assignment = ast[0] assert isinstance(assignment, Assignment) assert assignment.name.name == "x" assert assignment.expr.val == 42
Parsing a Module Definition
From a file:
from openscad_parser.ast import getASTfromFile, ModuleDeclaration, ModularCall
ast = getASTfromFile("box.scad")
module = ast[0]
assert isinstance(module, ModuleDeclaration)
assert module.name.name == "box"
assert len(module.parameters) == 1
assert len(module.children) == 1
assert isinstance(module.children[0], ModularCall)
assert module.children[0].name.name == "cube"
Or from a string:
from openscad_parser.ast import getASTfromString, ModuleDeclaration, ModularCall
code = """
module box(size) {
cube(size);
}
"""
ast = getASTfromString(code)
module = ast[0]
assert isinstance(module, ModuleDeclaration)
assert module.name.name == "box"
assert len(module.parameters) == 1
assert len(module.children) == 1
assert isinstance(module.children[0], ModularCall)
assert module.children[0].name.name == "cube"
Parsing Expressions
from openscad_parser.ast import (
getASTfromString, Assignment, AdditionOp, MultiplicationOp, NumberLiteral
)
code = "result = (10 + 5) * 2;"
ast = getASTfromString(code)
assignment = ast[0]
# The expression tree preserves operator precedence
mult_op = assignment.expr
assert isinstance(mult_op, MultiplicationOp)
assert isinstance(mult_op.left, AdditionOp)
assert mult_op.left.left.val == 10
assert mult_op.left.right.val == 5
assert mult_op.right.val == 2
Parsing Function Calls
from openscad_parser.ast import (
getASTfromString, PrimaryCall, PositionalArgument, NamedArgument
)
code = "x = foo(1, b=2);"
ast = getASTfromString(code)
assignment = ast[0]
call = assignment.expr
assert isinstance(call, PrimaryCall)
assert call.left.name == "foo"
assert len(call.arguments) == 2
assert isinstance(call.arguments[0], PositionalArgument)
assert isinstance(call.arguments[1], NamedArgument)
assert call.arguments[1].name.name == "b"
Parsing Library Files
from openscad_parser.ast import getASTfromLibraryFile, ModuleDeclaration
# Parse a library file using OpenSCAD's search path
# Searches: current file dir, OPENSCADPATH, platform defaults
# Returns: (AST, absolute_path) tuple
ast, path = getASTfromLibraryFile("/path/to/main.scad", "utils/math.scad")
# Or without current file context
ast, path = getASTfromLibraryFile("", "MCAD/boxes.scad")
AST Node Types
The AST includes comprehensive node types for all OpenSCAD language constructs:
Base Classes
ASTNode: Base class for all AST nodes (includes position attribute)
Expression: Base class for all expression nodes
Primary: Base class for atomic value types
ModuleInstantiation: Base class for module-related statements
Literals
Identifier: Variable, function, or module names
StringLiteral: String values
NumberLiteral: Numeric values
BooleanLiteral: true/false values
UndefinedLiteral: undef value
RangeLiteral: Range expressions [start:end:step]
Operators
Arithmetic: - AdditionOp, SubtractionOp, MultiplicationOp, DivisionOp - ModuloOp, ExponentOp, UnaryMinusOp
Logical: - LogicalAndOp, LogicalOrOp, LogicalNotOp
Comparison: - EqualityOp, InequalityOp - GreaterThanOp, GreaterThanOrEqualOp - LessThanOp, LessThanOrEqualOp
Bitwise: - BitwiseAndOp, BitwiseOrOp, BitwiseNotOp - BitwiseShiftLeftOp, BitwiseShiftRightOp
Other: - TernaryOp: condition ? true_expr : false_expr
Expressions
LetOp: let(assignments) body
EchoOp: echo(arguments) body
AssertOp: assert(arguments) body
FunctionLiteral: function(parameters) body
PrimaryCall: function calls
PrimaryIndex: array indexing [index]
PrimaryMember: member access .member
List Comprehensions
ListComprehension: Vector/list literals
ListCompFor: for loops in list comprehensions
ListCompCStyleFor: C-style for loops
ListCompIf, ListCompIfElse: Conditionals
ListCompLet: let expressions
ListCompEach: each expressions
Module Instantiations
ModularCall: Module calls with arguments and children
ModularFor: for loops
ModularCLikeFor: C-style for loops
ModularIntersectionFor: intersection_for loops
ModularLet: let statements
ModularEcho: echo statements
ModularAssert: assert statements
ModularIf, ModularIfElse: if/else statements
ModularModifierShowOnly: ! modifier
ModularModifierHighlight: # modifier
ModularModifierBackground: % modifier
ModularModifierDisable: * modifier
Declarations
ModuleDeclaration: module definitions
FunctionDeclaration: function definitions
ParameterDeclaration: function/module parameters
Assignment: variable assignments
Statements
UseStatement: use <filepath>
IncludeStatement: include <filepath>
PositionalArgument: Function call positional arguments
NamedArgument: Function call named arguments (name=value)
API Reference
Main Functions
- getOpenSCADParser(reduce_tree=False, debug=False)
Create an Arpeggio parser instance for OpenSCAD code.
- param reduce_tree:
If True, reduces single-child nodes in parse tree
- param debug:
If True, enables debug output
- returns:
ParserPython instance
- getASTfromString(code: str)
Parse OpenSCAD code from a string and return its AST.
- param code:
The OpenSCAD source code to be parsed
- returns:
AST node or list of AST nodes (for top-level statements)
- rtype:
ASTNode | list[ASTNode] | None
- getASTfromFile(file: str)
Parse an OpenSCAD source file and return its AST. Includes automatic caching that invalidates when the file’s modification timestamp changes.
- param file:
The OpenSCAD source file to be parsed
- returns:
List of AST nodes (for top-level statements)
- rtype:
list[ASTNode] | None
- raises FileNotFoundError:
If the specified file does not exist
- raises Exception:
If there is an error while reading the file
- getASTfromLibraryFile(currfile: str, libfile: str)
Find and parse an OpenSCAD library file using OpenSCAD’s search path rules. Searches in: current file directory, OPENSCADPATH, and platform default paths.
- param currfile:
Full path to the current OpenSCAD file (can be empty string)
- param libfile:
Partial or full path to the library file to find
- returns:
Tuple of (AST nodes list, absolute file path). The AST list is None if empty or not valid.
- rtype:
tuple[list[ASTNode] | None, str]
- raises FileNotFoundError:
If the library file cannot be found
- raises Exception:
If there is an error while reading or parsing the file
- parse_ast(parser, code, file="")
Parse OpenSCAD code and generate an AST (lower-level API).
- param parser:
Arpeggio parser instance from getOpenSCADParser()
- param code:
OpenSCAD code string to parse
- param file:
Optional file path for source location tracking
- returns:
AST node or list of AST nodes (for top-level statements)
- clear_ast_cache()
Clear the in-memory AST cache, forcing all subsequent calls to getASTfromFile() to re-parse files.
This function removes all cached AST trees from memory.
AST Node Classes
All AST node classes are located in openscad_parser.ast. Each node class:
Inherits from ASTNode (or a subclass like Expression)
Has a position attribute of type Position for source location
Implements __str__() for string representation
Is a dataclass with typed fields
Import commonly used classes:
from openscad_parser.ast import (
# Base classes
ASTNode, Expression, Primary,
# Literals
Identifier, StringLiteral, NumberLiteral, BooleanLiteral,
# Operators
AdditionOp, SubtractionOp, MultiplicationOp, DivisionOp,
LogicalAndOp, LogicalOrOp, EqualityOp, InequalityOp,
# Expressions
PrimaryCall, PrimaryIndex, PrimaryMember,
LetOp, EchoOp, AssertOp, TernaryOp,
# Modules
ModuleDeclaration, ModularCall, ModularFor,
# Functions
FunctionDeclaration,
# Statements
Assignment, UseStatement, IncludeStatement,
PositionalArgument, NamedArgument, ParameterDeclaration
)
Source Position Tracking
All AST nodes include source position information:
from openscad_parser.ast import getASTfromFile, Position
ast = getASTfromFile("example.scad")
assignment = ast[0]
position = assignment.position
print(position.file) # "example.scad"
print(position.line) # 1 (1-indexed)
print(position.char) # 1 (1-indexed, column number)
print(position.position) # 0 (0-indexed character position)
The Position class provides lazy evaluation of line/column numbers from character positions.
Error Handling
The parser will raise SyntaxError exceptions for invalid OpenSCAD syntax:
from openscad_parser.ast import getASTfromString
try:
code = "x = ;" # Invalid syntax
ast = getASTfromString(code)
except SyntaxError as e:
print(f"Parse error: {e}")
File operations will raise FileNotFoundError for missing files:
from openscad_parser.ast import getASTfromFile, getASTfromLibraryFile
try:
ast = getASTfromFile("nonexistent.scad")
except FileNotFoundError as e:
print(f"File not found: {e}")
try:
ast, path = getASTfromLibraryFile("main.scad", "missing_lib.scad")
except FileNotFoundError as e:
print(f"Library file not found: {e}")
Advanced Usage
File Caching
The getASTfromFile() function automatically caches parsed ASTs in memory:
from openscad_parser.ast import getASTfromFile
# First call parses and caches
ast1 = getASTfromFile("model.scad")
# Second call returns cached AST (same object)
ast2 = getASTfromFile("model.scad")
assert ast1 is ast2 # True - same cached object
# After file modification, cache is invalidated and file is re-parsed
# (modify model.scad here)
ast3 = getASTfromFile("model.scad")
assert ast1 is not ast3 # True - new parse after modification
Cache entries are automatically invalidated when a file’s modification timestamp changes. To manually clear the cache:
from openscad_parser.ast import clear_ast_cache clear_ast_cache() # Clear all cached ASTs
Reusing Parser Instances
Parser instances can be reused for parsing multiple code snippets:
parser = getOpenSCADParser(reduce_tree=False) # Parse multiple files ast1 = parse_ast(parser, code1, file="file1.scad") ast2 = parse_ast(parser, code2, file="file2.scad")
Note: For some use cases (like testing), you may need to create fresh parser instances to avoid memoization issues.
Traversing the AST
The AST is a tree structure that can be traversed recursively:
def visit_node(node):
"""Recursively visit AST nodes."""
if isinstance(node, Assignment):
print(f"Assignment: {node.name.name}")
visit_node(node.expr)
elif isinstance(node, AdditionOp):
print("Addition operation")
visit_node(node.left)
visit_node(node.right)
elif isinstance(node, NumberLiteral):
print(f"Number: {node.val}")
# ... handle other node types
from openscad_parser.ast import getASTfromString
code = "x = 10; y = 20;"
ast = getASTfromString(code)
for node in ast:
visit_node(node)
Testing
The project includes a comprehensive test suite. Run tests with:
pytest tests/
Development
Contributions are welcome! The project uses:
License
MIT License - see LICENSE file for details.
Links
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openscad_parser-1.0.1.tar.gz.
File metadata
- Download URL: openscad_parser-1.0.1.tar.gz
- Upload date:
- Size: 29.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.18
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f450be2b5019ca17775e24975e15936c04bfa062604dac9e7156522fbe168b6b
|
|
| MD5 |
a02f0dd80eea7b479d0bd18d324a7685
|
|
| BLAKE2b-256 |
916efc1232b613ca2ed1723dc561efe90dbd25a3511cdb0ef823fb38efe62527
|
File details
Details for the file openscad_parser-1.0.1-py3-none-any.whl.
File metadata
- Download URL: openscad_parser-1.0.1-py3-none-any.whl
- Upload date:
- Size: 30.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.18
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4b9e34d5bf0b675c457a57623911dd4cc2e9c13c36a23da5f5c9ecb252eed511
|
|
| MD5 |
b3578803ee4cd720c898f4919715f3e8
|
|
| BLAKE2b-256 |
c11c0e8b560f5fedd99134a829271ae96b0c38a7d32e0dbd8b072407061689b9
|
Comments
CommentLine: Single-line comments //
CommentSpan: Multi-line comments /* */
All AST node classes are fully documented with docstrings that include: - Description of what the node represents - OpenSCAD code examples - Field/attribute descriptions - Usage notes