openscad-parser 2.6.1__tar.gz → 3.0.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.
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/PKG-INFO +52 -12
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/README.rst +51 -11
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/pyproject.toml +1 -1
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/src/openscad_parser/__init__.py +1 -1
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/src/openscad_parser/ast/__init__.py +95 -62
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/src/openscad_parser/ast/builder.py +9 -1
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/src/openscad_parser/ast/nodes.py +142 -71
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/src/openscad_parser/ast/pretty_print.py +282 -158
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/src/openscad_parser/ast/scope.py +58 -2
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/src/openscad_parser/ast/serialization.py +3 -0
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/src/openscad_parser/grammar.py +51 -2
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/src/openscad_parser/ast/source_map.py +0 -0
- {openscad_parser-2.6.1 → openscad_parser-3.0.1}/src/openscad_parser/cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openscad_parser
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.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
|
|
@@ -184,12 +184,39 @@ Use ``getASTfromLibraryFile()`` to find and parse library files using OpenSCAD's
|
|
|
184
184
|
The function searches for library files in this order:
|
|
185
185
|
|
|
186
186
|
1. Directory of the current file (if provided)
|
|
187
|
-
2. Directories in the ``OPENSCADPATH`` environment variable
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
2. Directories in the ``OPENSCADPATH`` environment variable, which add to the
|
|
188
|
+
directories below rather than replacing them, as in OpenSCAD
|
|
189
|
+
3. The platform's libraries folder:
|
|
190
|
+
|
|
191
|
+
- Windows: ``OpenSCAD\libraries`` in the Documents folder Windows reports
|
|
192
|
+
(under OneDrive when Known Folder Move is on)
|
|
190
193
|
- macOS: ``~/Documents/OpenSCAD/libraries``
|
|
191
194
|
- Linux: ``~/.local/share/OpenSCAD/libraries``
|
|
192
195
|
|
|
196
|
+
4. A ``libraries`` directory beside the installed package
|
|
197
|
+
|
|
198
|
+
``librarySearchDirs(currfile)`` returns this list, and a not-found error names
|
|
199
|
+
every directory in it.
|
|
200
|
+
|
|
201
|
+
Strict Commas
|
|
202
|
+
~~~~~~~~~~~~~
|
|
203
|
+
|
|
204
|
+
OpenSCAD accepts a trailing comma almost everywhere. OpenSCAD 2021.01 did not:
|
|
205
|
+
it rejected one after a call's last argument (``cube(1,)``) and after the last
|
|
206
|
+
assignment of a ``let``/``for``/``intersection_for`` (``let(x=1,)``), while
|
|
207
|
+
accepting it in list literals, list comprehensions and parameter lists. To
|
|
208
|
+
parse as 2021.01 did:
|
|
209
|
+
|
|
210
|
+
.. code-block:: python
|
|
211
|
+
|
|
212
|
+
from openscad_parser import strict_commas
|
|
213
|
+
|
|
214
|
+
with strict_commas():
|
|
215
|
+
ast = getASTfromFile("model.scad") # None if it uses a rejected comma
|
|
216
|
+
|
|
217
|
+
The mode nests, restores on exit, and every AST cache keys on it, so a strict
|
|
218
|
+
parse is never served a lenient tree.
|
|
219
|
+
|
|
193
220
|
Advanced AST Generation
|
|
194
221
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
195
222
|
|
|
@@ -432,6 +459,7 @@ Expressions
|
|
|
432
459
|
- ``AssertOp(arguments: list[Argument], body: Expression)``: assert clause ``assert(arguments) body``
|
|
433
460
|
- ``FunctionLiteral(parameters: list[ParameterDeclaration], body: Expression)``: Anonymous function expression ``function(parameters) body``
|
|
434
461
|
- ``PrimaryCall(left: Expression, arguments: list[Argument])``: Function calls ``left(arguments)``
|
|
462
|
+
- ``RenderExpression(arguments: list[Argument], children: list[ModuleInstantiation])``: Geometry measured as a value ``render(arguments) { children }``. Braces required; ``render`` stays usable as an ordinary name. Not part of upstream OpenSCAD
|
|
435
463
|
- ``PrimaryIndex(left: Expression, index: Expression)``: Array indexing ``left[index]``
|
|
436
464
|
- ``PrimaryMember(left: Expression, member: Identifier)``: Member access ``left.member``
|
|
437
465
|
|
|
@@ -535,7 +563,7 @@ Main Functions
|
|
|
535
563
|
|
|
536
564
|
``getASTfromLibraryFile(currfile: str, libfile: str, include_comments: bool = False, process_includes: bool = True)``
|
|
537
565
|
Find and parse an OpenSCAD library file using OpenSCAD's search path rules.
|
|
538
|
-
Searches
|
|
566
|
+
Searches the directories ``librarySearchDirs(currfile)`` returns.
|
|
539
567
|
|
|
540
568
|
:param currfile: Full path to the current OpenSCAD file (can be empty string)
|
|
541
569
|
:param libfile: Partial or full path to the library file to find
|
|
@@ -565,11 +593,16 @@ Main Functions
|
|
|
565
593
|
This function removes all cached AST trees from memory.
|
|
566
594
|
|
|
567
595
|
``build_scopes(ast: list[ASTNode]) -> Scope``
|
|
568
|
-
Build a scope tree over an AST
|
|
596
|
+
Build a scope tree over an AST, recording every node's scope in a ``ScopeTable``
|
|
597
|
+
hung on the root (``root.table``). Read a node's scope with ``root.scope_of(node)``.
|
|
569
598
|
|
|
570
599
|
:param ast: A list of top-level AST nodes (as returned by the ``getAST*`` functions)
|
|
571
600
|
:returns: The root ``Scope`` object
|
|
572
601
|
|
|
602
|
+
``build_scopes_into(ast: list[ASTNode], table: ScopeTable) -> Scope``
|
|
603
|
+
The same, recording into a ``ScopeTable`` the caller owns, so several roots (one per
|
|
604
|
+
``use``\ d file, say) can be read back through one table.
|
|
605
|
+
|
|
573
606
|
``Scope``
|
|
574
607
|
Represents a lexical scope with three independent namespaces (variables, functions,
|
|
575
608
|
modules), mirroring OpenSCAD's scoping rules.
|
|
@@ -1006,14 +1039,21 @@ names according to OpenSCAD's three-namespace scoping rules::
|
|
|
1006
1039
|
print(root_scope.lookup_variable("x")) # Assignment node
|
|
1007
1040
|
print(root_scope.lookup_module("box")) # ModuleDeclaration node
|
|
1008
1041
|
|
|
1009
|
-
#
|
|
1042
|
+
# Every node's enclosing scope, as this pass built it
|
|
1010
1043
|
box_decl = ast[1]
|
|
1011
1044
|
cube_call = box_decl.children[0]
|
|
1012
|
-
print(cube_call.
|
|
1013
|
-
|
|
1014
|
-
``build_scopes(ast)`` returns the root ``Scope`` object and
|
|
1015
|
-
|
|
1016
|
-
scopes automatically.
|
|
1045
|
+
print(root_scope.scope_of(cube_call).lookup_variable("size")) # ParameterDeclaration node
|
|
1046
|
+
|
|
1047
|
+
``build_scopes(ast)`` returns the root ``Scope`` object and records the scope of every node
|
|
1048
|
+
in the tree in ``root_scope.table``. Scopes form a parent chain so lookups fall through to
|
|
1049
|
+
enclosing scopes automatically.
|
|
1050
|
+
|
|
1051
|
+
A node's scope is not an attribute of the node. An included file is parsed once and its
|
|
1052
|
+
nodes are shared by every file that includes it, but ``include`` puts them in the
|
|
1053
|
+
*includer's* scope, so the same node sits in a different scope for each includer. Each
|
|
1054
|
+
``build_scopes()`` call therefore has its own table, and two files that include the same
|
|
1055
|
+
library can be scoped, and used, at the same time. (Before 3.0, ``node.scope`` held the
|
|
1056
|
+
scope of whichever file had built its scopes last.) Declarations (variables, functions, modules) inside a block are
|
|
1017
1057
|
hoisted to the top of that block's scope before child nodes are visited.
|
|
1018
1058
|
|
|
1019
1059
|
Testing
|
|
@@ -141,12 +141,39 @@ Use ``getASTfromLibraryFile()`` to find and parse library files using OpenSCAD's
|
|
|
141
141
|
The function searches for library files in this order:
|
|
142
142
|
|
|
143
143
|
1. Directory of the current file (if provided)
|
|
144
|
-
2. Directories in the ``OPENSCADPATH`` environment variable
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
2. Directories in the ``OPENSCADPATH`` environment variable, which add to the
|
|
145
|
+
directories below rather than replacing them, as in OpenSCAD
|
|
146
|
+
3. The platform's libraries folder:
|
|
147
|
+
|
|
148
|
+
- Windows: ``OpenSCAD\libraries`` in the Documents folder Windows reports
|
|
149
|
+
(under OneDrive when Known Folder Move is on)
|
|
147
150
|
- macOS: ``~/Documents/OpenSCAD/libraries``
|
|
148
151
|
- Linux: ``~/.local/share/OpenSCAD/libraries``
|
|
149
152
|
|
|
153
|
+
4. A ``libraries`` directory beside the installed package
|
|
154
|
+
|
|
155
|
+
``librarySearchDirs(currfile)`` returns this list, and a not-found error names
|
|
156
|
+
every directory in it.
|
|
157
|
+
|
|
158
|
+
Strict Commas
|
|
159
|
+
~~~~~~~~~~~~~
|
|
160
|
+
|
|
161
|
+
OpenSCAD accepts a trailing comma almost everywhere. OpenSCAD 2021.01 did not:
|
|
162
|
+
it rejected one after a call's last argument (``cube(1,)``) and after the last
|
|
163
|
+
assignment of a ``let``/``for``/``intersection_for`` (``let(x=1,)``), while
|
|
164
|
+
accepting it in list literals, list comprehensions and parameter lists. To
|
|
165
|
+
parse as 2021.01 did:
|
|
166
|
+
|
|
167
|
+
.. code-block:: python
|
|
168
|
+
|
|
169
|
+
from openscad_parser import strict_commas
|
|
170
|
+
|
|
171
|
+
with strict_commas():
|
|
172
|
+
ast = getASTfromFile("model.scad") # None if it uses a rejected comma
|
|
173
|
+
|
|
174
|
+
The mode nests, restores on exit, and every AST cache keys on it, so a strict
|
|
175
|
+
parse is never served a lenient tree.
|
|
176
|
+
|
|
150
177
|
Advanced AST Generation
|
|
151
178
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
152
179
|
|
|
@@ -389,6 +416,7 @@ Expressions
|
|
|
389
416
|
- ``AssertOp(arguments: list[Argument], body: Expression)``: assert clause ``assert(arguments) body``
|
|
390
417
|
- ``FunctionLiteral(parameters: list[ParameterDeclaration], body: Expression)``: Anonymous function expression ``function(parameters) body``
|
|
391
418
|
- ``PrimaryCall(left: Expression, arguments: list[Argument])``: Function calls ``left(arguments)``
|
|
419
|
+
- ``RenderExpression(arguments: list[Argument], children: list[ModuleInstantiation])``: Geometry measured as a value ``render(arguments) { children }``. Braces required; ``render`` stays usable as an ordinary name. Not part of upstream OpenSCAD
|
|
392
420
|
- ``PrimaryIndex(left: Expression, index: Expression)``: Array indexing ``left[index]``
|
|
393
421
|
- ``PrimaryMember(left: Expression, member: Identifier)``: Member access ``left.member``
|
|
394
422
|
|
|
@@ -492,7 +520,7 @@ Main Functions
|
|
|
492
520
|
|
|
493
521
|
``getASTfromLibraryFile(currfile: str, libfile: str, include_comments: bool = False, process_includes: bool = True)``
|
|
494
522
|
Find and parse an OpenSCAD library file using OpenSCAD's search path rules.
|
|
495
|
-
Searches
|
|
523
|
+
Searches the directories ``librarySearchDirs(currfile)`` returns.
|
|
496
524
|
|
|
497
525
|
:param currfile: Full path to the current OpenSCAD file (can be empty string)
|
|
498
526
|
:param libfile: Partial or full path to the library file to find
|
|
@@ -522,11 +550,16 @@ Main Functions
|
|
|
522
550
|
This function removes all cached AST trees from memory.
|
|
523
551
|
|
|
524
552
|
``build_scopes(ast: list[ASTNode]) -> Scope``
|
|
525
|
-
Build a scope tree over an AST
|
|
553
|
+
Build a scope tree over an AST, recording every node's scope in a ``ScopeTable``
|
|
554
|
+
hung on the root (``root.table``). Read a node's scope with ``root.scope_of(node)``.
|
|
526
555
|
|
|
527
556
|
:param ast: A list of top-level AST nodes (as returned by the ``getAST*`` functions)
|
|
528
557
|
:returns: The root ``Scope`` object
|
|
529
558
|
|
|
559
|
+
``build_scopes_into(ast: list[ASTNode], table: ScopeTable) -> Scope``
|
|
560
|
+
The same, recording into a ``ScopeTable`` the caller owns, so several roots (one per
|
|
561
|
+
``use``\ d file, say) can be read back through one table.
|
|
562
|
+
|
|
530
563
|
``Scope``
|
|
531
564
|
Represents a lexical scope with three independent namespaces (variables, functions,
|
|
532
565
|
modules), mirroring OpenSCAD's scoping rules.
|
|
@@ -963,14 +996,21 @@ names according to OpenSCAD's three-namespace scoping rules::
|
|
|
963
996
|
print(root_scope.lookup_variable("x")) # Assignment node
|
|
964
997
|
print(root_scope.lookup_module("box")) # ModuleDeclaration node
|
|
965
998
|
|
|
966
|
-
#
|
|
999
|
+
# Every node's enclosing scope, as this pass built it
|
|
967
1000
|
box_decl = ast[1]
|
|
968
1001
|
cube_call = box_decl.children[0]
|
|
969
|
-
print(cube_call.
|
|
970
|
-
|
|
971
|
-
``build_scopes(ast)`` returns the root ``Scope`` object and
|
|
972
|
-
|
|
973
|
-
scopes automatically.
|
|
1002
|
+
print(root_scope.scope_of(cube_call).lookup_variable("size")) # ParameterDeclaration node
|
|
1003
|
+
|
|
1004
|
+
``build_scopes(ast)`` returns the root ``Scope`` object and records the scope of every node
|
|
1005
|
+
in the tree in ``root_scope.table``. Scopes form a parent chain so lookups fall through to
|
|
1006
|
+
enclosing scopes automatically.
|
|
1007
|
+
|
|
1008
|
+
A node's scope is not an attribute of the node. An included file is parsed once and its
|
|
1009
|
+
nodes are shared by every file that includes it, but ``include`` puts them in the
|
|
1010
|
+
*includer's* scope, so the same node sits in a different scope for each includer. Each
|
|
1011
|
+
``build_scopes()`` call therefore has its own table, and two files that include the same
|
|
1012
|
+
library can be scoped, and used, at the same time. (Before 3.0, ``node.scope`` held the
|
|
1013
|
+
scope of whichever file had built its scopes last.) Declarations (variables, functions, modules) inside a block are
|
|
974
1014
|
hoisted to the top of that block's scope before child nodes are visited.
|
|
975
1015
|
|
|
976
1016
|
Testing
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
from __future__ import unicode_literals
|
|
6
6
|
|
|
7
7
|
from arpeggio import ParserPython
|
|
8
|
-
from .grammar import openscad_language, openscad_language_with_comments, comment, whitespace_only
|
|
8
|
+
from .grammar import openscad_language, openscad_language_with_comments, comment, whitespace_only, strict_commas
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
# --- The parser ---
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import functools
|
|
1
2
|
import hashlib
|
|
2
3
|
import json
|
|
3
4
|
import os
|
|
@@ -5,7 +6,8 @@ import pickle
|
|
|
5
6
|
import platform
|
|
6
7
|
from typing import Optional
|
|
7
8
|
from arpeggio import NoMatch
|
|
8
|
-
from openscad_parser import getOpenSCADParser
|
|
9
|
+
from openscad_parser import getOpenSCADParser, strict_commas
|
|
10
|
+
from openscad_parser.grammar import _STRICT_COMMAS
|
|
9
11
|
from .source_map import SourceMap, process_includes as process_includes_func
|
|
10
12
|
|
|
11
13
|
# Import all AST nodes from nodes
|
|
@@ -54,6 +56,7 @@ from .nodes import (
|
|
|
54
56
|
LessThanOrEqualOp,
|
|
55
57
|
FunctionLiteral,
|
|
56
58
|
PrimaryCall,
|
|
59
|
+
RenderExpression,
|
|
57
60
|
PrimaryIndex,
|
|
58
61
|
PrimaryMember,
|
|
59
62
|
VectorElement,
|
|
@@ -87,7 +90,7 @@ from .nodes import (
|
|
|
87
90
|
from .builder import ASTBuilderVisitor, Position
|
|
88
91
|
|
|
89
92
|
# Import scope classes
|
|
90
|
-
from .scope import Scope, build_scopes
|
|
93
|
+
from .scope import Scope, ScopeTable, build_scopes, build_scopes_into
|
|
91
94
|
|
|
92
95
|
# Import pretty-printer
|
|
93
96
|
from .pretty_print import to_openscad
|
|
@@ -105,57 +108,82 @@ from .serialization import (
|
|
|
105
108
|
|
|
106
109
|
# --- AST convenience functions ---
|
|
107
110
|
|
|
111
|
+
def _windows_documents_dir() -> str:
|
|
112
|
+
"""Where Windows says My Documents is -- the same SHGetFolderPathW(
|
|
113
|
+
CSIDL_PERSONAL, SHGFP_TYPE_CURRENT) call OpenSCAD makes -- rather than
|
|
114
|
+
assuming ~/Documents: OneDrive's Known Folder Move, on by default, puts
|
|
115
|
+
it at ~/OneDrive/Documents instead."""
|
|
116
|
+
try:
|
|
117
|
+
import ctypes
|
|
118
|
+
buf = ctypes.create_unicode_buffer(260)
|
|
119
|
+
if ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, buf) == 0: # CSIDL_PERSONAL, SHGFP_TYPE_CURRENT
|
|
120
|
+
return buf.value
|
|
121
|
+
except (AttributeError, OSError): # not actually on Windows
|
|
122
|
+
pass
|
|
123
|
+
return os.path.join(os.path.expanduser("~"), "Documents")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Libraries shipped beside this package -- OpenSCAD's resourcePath("libraries").
|
|
127
|
+
_BUNDLED_LIBRARY_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "libraries")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def librarySearchDirs(currfile: str) -> list[str]:
|
|
131
|
+
"""The directories `include`/`use` search, in order, as OpenSCAD's
|
|
132
|
+
parser_init() builds them: the including file's own directory, then
|
|
133
|
+
every OPENSCADPATH entry, then the user's libraries folder, then
|
|
134
|
+
libraries shipped beside this package.
|
|
135
|
+
|
|
136
|
+
OPENSCADPATH adds to the libraries folder rather than replacing it --
|
|
137
|
+
it used to replace it, so setting it for one library hid every other
|
|
138
|
+
one, a BOSL2 in the default folder included (openscad_cpp_parser #10).
|
|
139
|
+
"""
|
|
140
|
+
dirs = []
|
|
141
|
+
if currfile:
|
|
142
|
+
dirs.append(os.path.dirname(os.path.abspath(currfile)))
|
|
143
|
+
|
|
144
|
+
system = platform.system()
|
|
145
|
+
pathsep = ";" if system == "Windows" else ":"
|
|
146
|
+
for path in os.getenv("OPENSCADPATH", "").split(pathsep):
|
|
147
|
+
expanded_path = os.path.expandvars(path)
|
|
148
|
+
if expanded_path:
|
|
149
|
+
dirs.append(expanded_path)
|
|
150
|
+
|
|
151
|
+
if system == "Windows":
|
|
152
|
+
dirs.append(os.path.join(_windows_documents_dir(), "OpenSCAD", "libraries"))
|
|
153
|
+
elif system == "Darwin":
|
|
154
|
+
dirs.append(os.path.expanduser("~/Documents/OpenSCAD/libraries"))
|
|
155
|
+
elif system == "Linux":
|
|
156
|
+
dirs.append(os.path.expanduser("~/.local/share/OpenSCAD/libraries"))
|
|
157
|
+
# ponytail: only the folder beside the package, no ../share/openscad walk
|
|
158
|
+
dirs.append(_BUNDLED_LIBRARY_DIR)
|
|
159
|
+
return dirs
|
|
160
|
+
|
|
161
|
+
|
|
108
162
|
def findLibraryFile(currfile: str, libfile: str) -> Optional[str]:
|
|
109
|
-
"""Find a library file using OpenSCAD's search path rules
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
1. Directory of the current file (if currfile is provided)
|
|
113
|
-
2. Directories specified in OPENSCADPATH environment variable
|
|
114
|
-
3. Platform-specific default library directories
|
|
115
|
-
|
|
163
|
+
"""Find a library file using OpenSCAD's search path rules: the first
|
|
164
|
+
directory in librarySearchDirs(currfile) that holds it.
|
|
165
|
+
|
|
116
166
|
Args:
|
|
117
167
|
currfile: Full path to the current OpenSCAD file (can be empty string)
|
|
118
168
|
libfile: Partial or full path to the library file to find
|
|
119
|
-
|
|
169
|
+
|
|
120
170
|
Returns:
|
|
121
171
|
Full path to the found library file, or None if not found
|
|
122
172
|
"""
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# Add directory of current file if provided
|
|
126
|
-
if currfile:
|
|
127
|
-
dirs.append(os.path.dirname(os.path.abspath(currfile)))
|
|
128
|
-
|
|
129
|
-
# Determine path separator and default path based on platform
|
|
130
|
-
pathsep = ":"
|
|
131
|
-
dflt_path = ""
|
|
132
|
-
system = platform.system()
|
|
133
|
-
|
|
134
|
-
if system == "Windows": # pragma: no cover
|
|
135
|
-
dflt_path = os.path.join(os.path.expanduser("~"), "Documents", "OpenSCAD", "libraries")
|
|
136
|
-
pathsep = ";"
|
|
137
|
-
elif system == "Darwin": # pragma: no cover
|
|
138
|
-
dflt_path = os.path.expanduser("~/Documents/OpenSCAD/libraries")
|
|
139
|
-
elif system == "Linux": # pragma: no cover
|
|
140
|
-
dflt_path = os.path.expanduser("~/.local/share/OpenSCAD/libraries")
|
|
141
|
-
|
|
142
|
-
# Get OPENSCADPATH from environment or use default
|
|
143
|
-
env = os.getenv("OPENSCADPATH", dflt_path)
|
|
144
|
-
if env:
|
|
145
|
-
for path in env.split(pathsep):
|
|
146
|
-
expanded_path = os.path.expandvars(path)
|
|
147
|
-
if expanded_path:
|
|
148
|
-
dirs.append(expanded_path)
|
|
149
|
-
|
|
150
|
-
# Search for the file in each directory
|
|
151
|
-
for d in dirs:
|
|
173
|
+
for d in librarySearchDirs(currfile):
|
|
152
174
|
test_file = os.path.join(d, libfile)
|
|
153
175
|
if os.path.isfile(test_file):
|
|
154
176
|
return test_file
|
|
155
|
-
|
|
156
177
|
return None
|
|
157
178
|
|
|
158
179
|
|
|
180
|
+
def _not_found(what: str, filename: str, currfile: str) -> str:
|
|
181
|
+
"""A not-found message that lists every directory searched. Naming only
|
|
182
|
+
the includer read as "only there was searched"."""
|
|
183
|
+
return f"{what} '{filename}' not found. Searched:" + "".join(
|
|
184
|
+
f"\n {d}" for d in librarySearchDirs(currfile))
|
|
185
|
+
|
|
186
|
+
|
|
159
187
|
# Alias for backward compatibility (test_ast_convenience.py imports _find_library_file)
|
|
160
188
|
_find_library_file = findLibraryFile
|
|
161
189
|
|
|
@@ -257,14 +285,15 @@ def getASTfromString(code: str, include_comments: bool = False, origin: str = "<
|
|
|
257
285
|
|
|
258
286
|
|
|
259
287
|
# Module-level in-memory cache for per-file AST trees (no includes resolved)
|
|
260
|
-
# Key: tuple of (absolute file path (str), include_comments (bool))
|
|
288
|
+
# Key: tuple of (absolute file path (str), include_comments (bool), strict_commas (bool))
|
|
261
289
|
# Value: tuple of (AST nodes, modification timestamp)
|
|
262
|
-
_ast_cache: dict[tuple[str, bool], tuple[list[ASTNode] | None, float]] = {}
|
|
290
|
+
_ast_cache: dict[tuple[str, bool, bool], tuple[list[ASTNode] | None, float]] = {}
|
|
263
291
|
|
|
264
292
|
# Resolved (includes-expanded) cache
|
|
265
|
-
# Key: tuple of (absolute file path (str), include_comments (bool), process_includes (bool)
|
|
293
|
+
# Key: tuple of (absolute file path (str), include_comments (bool), process_includes (bool),
|
|
294
|
+
# strict_commas (bool))
|
|
266
295
|
# Value: tuple of (AST nodes, modification timestamp)
|
|
267
|
-
_resolved_cache: dict[tuple[str, bool, bool], tuple[list[ASTNode] | None, float]] = {}
|
|
296
|
+
_resolved_cache: dict[tuple[str, bool, bool, bool], tuple[list[ASTNode] | None, float]] = {}
|
|
268
297
|
|
|
269
298
|
|
|
270
299
|
def _get_disk_cache_dir() -> Optional[str]:
|
|
@@ -285,12 +314,26 @@ def _get_disk_cache_dir() -> Optional[str]:
|
|
|
285
314
|
return None
|
|
286
315
|
|
|
287
316
|
|
|
317
|
+
@functools.lru_cache(maxsize=None)
|
|
318
|
+
def _ast_format_tag() -> str:
|
|
319
|
+
"""Changes whenever the grammar or the AST classes do, so a pickled AST
|
|
320
|
+
from another version is never served: an older pickle lacks any field
|
|
321
|
+
added since (RangeLiteral.implicit_step, say) and reads its default."""
|
|
322
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
323
|
+
h = hashlib.sha256()
|
|
324
|
+
for path in (os.path.join(here, "..", "grammar.py"), os.path.join(here, "nodes.py"),
|
|
325
|
+
os.path.join(here, "builder.py"), os.path.join(here, "__init__.py")):
|
|
326
|
+
with open(path, "rb") as f:
|
|
327
|
+
h.update(f.read())
|
|
328
|
+
return h.hexdigest()[:16]
|
|
329
|
+
|
|
330
|
+
|
|
288
331
|
def _disk_cache_path(file_path: str, include_comments: bool) -> Optional[str]:
|
|
289
332
|
"""Get the disk cache file path for a given source file."""
|
|
290
333
|
cache_dir = _get_disk_cache_dir()
|
|
291
334
|
if not cache_dir:
|
|
292
335
|
return None # pragma: no cover
|
|
293
|
-
key = f"{file_path}:{include_comments}"
|
|
336
|
+
key = f"{file_path}:{include_comments}:{_STRICT_COMMAS.get()}:{_ast_format_tag()}"
|
|
294
337
|
h = hashlib.sha256(key.encode()).hexdigest()[:16]
|
|
295
338
|
return os.path.join(cache_dir, f"{h}.pickle")
|
|
296
339
|
|
|
@@ -429,7 +472,7 @@ def _parse_single_file(file_path: str, include_comments: bool = False) -> list[A
|
|
|
429
472
|
raise FileNotFoundError(f"File {file_path} not found")
|
|
430
473
|
|
|
431
474
|
current_mtime = os.path.getmtime(file_path)
|
|
432
|
-
cache_key = (file_path, include_comments)
|
|
475
|
+
cache_key = (file_path, include_comments, _STRICT_COMMAS.get())
|
|
433
476
|
|
|
434
477
|
# Check in-memory cache
|
|
435
478
|
if cache_key in _ast_cache:
|
|
@@ -475,10 +518,7 @@ def _resolve_includes(ast_nodes: list[ASTNode] | None, current_file: str,
|
|
|
475
518
|
filename = node.filepath.val
|
|
476
519
|
lib_file = findLibraryFile(current_file, filename)
|
|
477
520
|
if lib_file is None:
|
|
478
|
-
raise FileNotFoundError(
|
|
479
|
-
f"Included file '{filename}' not found. "
|
|
480
|
-
f"Searched relative to: {current_file if current_file else 'current directory'}"
|
|
481
|
-
)
|
|
521
|
+
raise FileNotFoundError(_not_found("Included file", filename, current_file))
|
|
482
522
|
lib_file = os.path.abspath(lib_file)
|
|
483
523
|
if lib_file in visited:
|
|
484
524
|
continue
|
|
@@ -550,7 +590,7 @@ def getASTfromFile(file: str, include_comments: bool = False, process_includes:
|
|
|
550
590
|
return _parse_single_file(file_path, include_comments)
|
|
551
591
|
|
|
552
592
|
# Check resolved cache (in-memory only since resolved ASTs depend on multiple files)
|
|
553
|
-
resolved_key = (file_path, include_comments, True)
|
|
593
|
+
resolved_key = (file_path, include_comments, True, _STRICT_COMMAS.get())
|
|
554
594
|
if resolved_key in _resolved_cache:
|
|
555
595
|
cached_ast, cached_mtime = _resolved_cache[resolved_key]
|
|
556
596
|
if cached_mtime == current_mtime:
|
|
@@ -574,13 +614,9 @@ def getASTfromLibraryFile(currfile: str, libfile: str, include_comments: bool =
|
|
|
574
614
|
Find and parse an OpenSCAD library file using OpenSCAD's search path rules,
|
|
575
615
|
and return both the AST and absolute path to the file.
|
|
576
616
|
|
|
577
|
-
This function searches
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
3. Platform-specific default library directories:
|
|
581
|
-
- Windows: ~/Documents/OpenSCAD/libraries
|
|
582
|
-
- macOS: ~/Documents/OpenSCAD/libraries
|
|
583
|
-
- Linux: ~/.local/share/OpenSCAD/libraries
|
|
617
|
+
This function searches the directories librarySearchDirs(currfile)
|
|
618
|
+
returns, in order: the current file's directory, OPENSCADPATH, the
|
|
619
|
+
platform's libraries folder, then libraries beside this package.
|
|
584
620
|
|
|
585
621
|
Once found, the file is parsed using getASTfromFile(), which includes
|
|
586
622
|
caching support and include processing.
|
|
@@ -624,10 +660,7 @@ def getASTfromLibraryFile(currfile: str, libfile: str, include_comments: bool =
|
|
|
624
660
|
found_file = findLibraryFile(currfile, libfile)
|
|
625
661
|
|
|
626
662
|
if found_file is None:
|
|
627
|
-
raise FileNotFoundError(
|
|
628
|
-
f"Library file '{libfile}' not found in search paths. "
|
|
629
|
-
f"Searched in: current file directory, OPENSCADPATH, and platform default paths."
|
|
630
|
-
)
|
|
663
|
+
raise FileNotFoundError(_not_found("Library file", libfile, currfile))
|
|
631
664
|
|
|
632
665
|
# Use getASTfromFile() which includes caching and include processing
|
|
633
666
|
ast = getASTfromFile(found_file, include_comments=include_comments, process_includes=process_includes)
|
|
@@ -984,6 +984,13 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
984
984
|
def visit_primary(self, node, children):
|
|
985
985
|
return children[0]
|
|
986
986
|
|
|
987
|
+
def visit_render_expr(self, node, children):
|
|
988
|
+
# Grammar: (KWD_RENDER, TOK_PAREN, arguments, TOK_ENDPAREN, statement_block)
|
|
989
|
+
arguments = children.get_rule("arguments") or []
|
|
990
|
+
block = children.get_rule("statement_block") or []
|
|
991
|
+
return RenderExpression(arguments=list(arguments), children=list(block),
|
|
992
|
+
position=self._get_node_position(node))
|
|
993
|
+
|
|
987
994
|
def visit_range_expr(self, node, children):
|
|
988
995
|
# OpenSCAD syntax: [start:end] or [start:step:end]
|
|
989
996
|
start = children[0]
|
|
@@ -993,7 +1000,8 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
993
1000
|
else:
|
|
994
1001
|
end = children[1]
|
|
995
1002
|
step = NumberLiteral(val=1.0, position=self._get_node_position(node))
|
|
996
|
-
return RangeLiteral(start=start, end=end, step=step,
|
|
1003
|
+
return RangeLiteral(start=start, end=end, step=step, implicit_step=len(children) <= 2,
|
|
1004
|
+
position=self._get_node_position(node))
|
|
997
1005
|
|
|
998
1006
|
def visit_vector_expr(self, node, children):
|
|
999
1007
|
elements = children if children else []
|