mapFolding 0.8.5__py3-none-any.whl → 0.9.0__py3-none-any.whl
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.
- mapFolding/__init__.py +66 -18
- mapFolding/basecamp.py +32 -17
- mapFolding/beDRY.py +3 -3
- mapFolding/oeis.py +121 -25
- mapFolding/someAssemblyRequired/__init__.py +48 -27
- mapFolding/someAssemblyRequired/_theTypes.py +11 -15
- mapFolding/someAssemblyRequired/_tool_Make.py +40 -12
- mapFolding/someAssemblyRequired/_tool_Then.py +59 -25
- mapFolding/someAssemblyRequired/_toolboxAntecedents.py +151 -276
- mapFolding/someAssemblyRequired/_toolboxContainers.py +185 -51
- mapFolding/someAssemblyRequired/_toolboxPython.py +165 -44
- mapFolding/someAssemblyRequired/synthesizeNumbaJob.py +141 -20
- mapFolding/someAssemblyRequired/toolboxNumba.py +93 -52
- mapFolding/someAssemblyRequired/transformationTools.py +228 -138
- mapFolding/syntheticModules/numbaCount_doTheNeedful.py +0 -1
- mapFolding/theSSOT.py +147 -55
- mapFolding/toolboxFilesystem.py +1 -1
- mapfolding-0.9.0.dist-info/METADATA +177 -0
- mapfolding-0.9.0.dist-info/RECORD +46 -0
- tests/__init__.py +44 -0
- tests/conftest.py +75 -7
- tests/test_computations.py +90 -9
- tests/test_filesystem.py +32 -33
- tests/test_other.py +0 -1
- tests/test_tasks.py +2 -2
- mapFolding/noHomeYet.py +0 -32
- mapFolding/someAssemblyRequired/newInliner.py +0 -22
- mapfolding-0.8.5.dist-info/METADATA +0 -190
- mapfolding-0.8.5.dist-info/RECORD +0 -48
- {mapfolding-0.8.5.dist-info → mapfolding-0.9.0.dist-info}/WHEEL +0 -0
- {mapfolding-0.8.5.dist-info → mapfolding-0.9.0.dist-info}/entry_points.txt +0 -0
- {mapfolding-0.8.5.dist-info → mapfolding-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {mapfolding-0.8.5.dist-info → mapfolding-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -1,22 +1,55 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Container
|
|
2
|
+
AST Container Classes for Python Code Generation and Transformation
|
|
3
3
|
|
|
4
|
-
This module provides container classes
|
|
5
|
-
and
|
|
6
|
-
|
|
4
|
+
This module provides specialized container classes that organize AST nodes, imports,
|
|
5
|
+
and program structure for code generation and transformation. These classes form
|
|
6
|
+
the organizational backbone of the code generation system, enabling:
|
|
7
|
+
|
|
8
|
+
1. Tracking and managing imports with LedgerOfImports
|
|
9
|
+
2. Packaging function definitions with their dependencies via IngredientsFunction
|
|
10
|
+
3. Structuring complete modules with IngredientsModule
|
|
11
|
+
4. Configuring code synthesis with RecipeSynthesizeFlow
|
|
12
|
+
5. Organizing decomposed dataclass representations with ShatteredDataclass
|
|
13
|
+
|
|
14
|
+
Together, these container classes implement a component-based architecture for
|
|
15
|
+
programmatic generation of high-performance code. They maintain a clean separation
|
|
16
|
+
between structure and content, allowing transformations to be applied systematically
|
|
17
|
+
while preserving relationships between code elements.
|
|
18
|
+
|
|
19
|
+
The containers work in conjunction with transformation tools that manipulate the
|
|
20
|
+
contained AST nodes to implement specific optimizations and transformations.
|
|
7
21
|
"""
|
|
22
|
+
|
|
8
23
|
from collections import defaultdict
|
|
9
24
|
from collections.abc import Sequence
|
|
10
|
-
from mapFolding.someAssemblyRequired import
|
|
11
|
-
from mapFolding.theSSOT import
|
|
25
|
+
from mapFolding.someAssemblyRequired import ast_Identifier, Make, parseLogicalPath2astModule, str_nameDOTname
|
|
26
|
+
from mapFolding.theSSOT import The
|
|
12
27
|
from pathlib import Path, PurePosixPath
|
|
13
28
|
from Z0Z_tools import updateExtendPolishDictionaryLists
|
|
14
29
|
import ast
|
|
15
30
|
import dataclasses
|
|
16
31
|
|
|
32
|
+
# Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
|
|
17
33
|
class LedgerOfImports:
|
|
34
|
+
"""
|
|
35
|
+
Track and manage import statements for programmatically generated code.
|
|
36
|
+
|
|
37
|
+
LedgerOfImports acts as a registry for import statements, maintaining a clean
|
|
38
|
+
separation between the logical structure of imports and their textual representation.
|
|
39
|
+
It enables:
|
|
40
|
+
|
|
41
|
+
1. Tracking regular imports and import-from statements
|
|
42
|
+
2. Adding imports programmatically during code transformation
|
|
43
|
+
3. Merging imports from multiple sources
|
|
44
|
+
4. Removing unnecessary or conflicting imports
|
|
45
|
+
5. Generating optimized AST import nodes for the final code
|
|
46
|
+
|
|
47
|
+
This class forms the foundation of dependency management in generated code,
|
|
48
|
+
ensuring that all required libraries are available without duplication or
|
|
49
|
+
conflict.
|
|
50
|
+
"""
|
|
18
51
|
# TODO When resolving the ledger of imports, remove self-referential imports
|
|
19
|
-
# TODO TypeIgnore
|
|
52
|
+
# TODO add TypeIgnore tracking to the ledger of imports
|
|
20
53
|
|
|
21
54
|
def __init__(self, startWith: ast.AST | None = None) -> None:
|
|
22
55
|
self.dictionaryImportFrom: dict[str_nameDOTname, list[tuple[ast_Identifier, ast_Identifier | None]]] = defaultdict(list)
|
|
@@ -26,21 +59,54 @@ class LedgerOfImports:
|
|
|
26
59
|
|
|
27
60
|
def addAst(self, astImport____: ast.Import | ast.ImportFrom) -> None:
|
|
28
61
|
assert isinstance(astImport____, (ast.Import, ast.ImportFrom)), f"I received {type(astImport____) = }, but I can only accept {ast.Import} and {ast.ImportFrom}."
|
|
29
|
-
if
|
|
62
|
+
if isinstance(astImport____, ast.Import):
|
|
30
63
|
for alias in astImport____.names:
|
|
31
64
|
self.listImport.append(alias.name)
|
|
32
|
-
elif
|
|
65
|
+
elif isinstance(astImport____, ast.ImportFrom): # type: ignore
|
|
33
66
|
# TODO fix the mess created by `None` means '.'. I need a `str_nameDOTname` to replace '.'
|
|
34
67
|
if astImport____.module is None:
|
|
35
68
|
astImport____.module = '.'
|
|
36
69
|
for alias in astImport____.names:
|
|
37
70
|
self.dictionaryImportFrom[astImport____.module].append((alias.name, alias.asname))
|
|
38
71
|
|
|
39
|
-
def addImport_asStr(self,
|
|
40
|
-
self.listImport.append(
|
|
41
|
-
|
|
42
|
-
def addImportFrom_asStr(self,
|
|
43
|
-
|
|
72
|
+
def addImport_asStr(self, moduleWithLogicalPath: str_nameDOTname) -> None:
|
|
73
|
+
self.listImport.append(moduleWithLogicalPath)
|
|
74
|
+
|
|
75
|
+
# def addImportFrom_asStr(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier, asname: ast_Identifier | None = None) -> None:
|
|
76
|
+
# self.dictionaryImportFrom[moduleWithLogicalPath].append((name, asname))
|
|
77
|
+
|
|
78
|
+
def addImportFrom_asStr(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier, asname: ast_Identifier | None = None) -> None:
|
|
79
|
+
if moduleWithLogicalPath not in self.dictionaryImportFrom:
|
|
80
|
+
self.dictionaryImportFrom[moduleWithLogicalPath] = []
|
|
81
|
+
self.dictionaryImportFrom[moduleWithLogicalPath].append((name, asname))
|
|
82
|
+
|
|
83
|
+
def removeImportFromModule(self, moduleWithLogicalPath: str_nameDOTname) -> None:
|
|
84
|
+
"""Remove all imports from a specific module."""
|
|
85
|
+
self.removeImportFrom(moduleWithLogicalPath, None, None)
|
|
86
|
+
|
|
87
|
+
def removeImportFrom(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier | None, asname: ast_Identifier | None = None) -> None:
|
|
88
|
+
assert moduleWithLogicalPath is not None, SyntaxError(f"I received `{moduleWithLogicalPath = }`, but it must be the name of a module.")
|
|
89
|
+
if moduleWithLogicalPath in self.dictionaryImportFrom:
|
|
90
|
+
"""
|
|
91
|
+
name, asname Meaning
|
|
92
|
+
ast_Identifier, ast_Identifier : remove exact matches
|
|
93
|
+
ast_Identifier, None : remove exact matches
|
|
94
|
+
None, ast_Identifier : remove all matches for asname and if entry_asname is None remove name == ast_Identifier
|
|
95
|
+
None, None : remove all matches for the module
|
|
96
|
+
"""
|
|
97
|
+
if name is None and asname is None:
|
|
98
|
+
# Remove all entries for the module
|
|
99
|
+
self.dictionaryImportFrom.pop(moduleWithLogicalPath)
|
|
100
|
+
else:
|
|
101
|
+
if name is None:
|
|
102
|
+
self.dictionaryImportFrom[moduleWithLogicalPath] = [(entry_name, entry_asname) for entry_name, entry_asname in self.dictionaryImportFrom[moduleWithLogicalPath]
|
|
103
|
+
if not (entry_asname == asname) and not (entry_asname is None and entry_name == asname)]
|
|
104
|
+
else:
|
|
105
|
+
# Remove exact matches for the module
|
|
106
|
+
self.dictionaryImportFrom[moduleWithLogicalPath] = [(entry_name, entry_asname) for entry_name, entry_asname in self.dictionaryImportFrom[moduleWithLogicalPath]
|
|
107
|
+
if not (entry_name == name and entry_asname == asname)]
|
|
108
|
+
if not self.dictionaryImportFrom[moduleWithLogicalPath]:
|
|
109
|
+
self.dictionaryImportFrom.pop(moduleWithLogicalPath)
|
|
44
110
|
|
|
45
111
|
def exportListModuleIdentifiers(self) -> list[ast_Identifier]:
|
|
46
112
|
listModuleIdentifiers: list[ast_Identifier] = list(self.dictionaryImportFrom.keys())
|
|
@@ -49,13 +115,14 @@ class LedgerOfImports:
|
|
|
49
115
|
|
|
50
116
|
def makeList_ast(self) -> list[ast.ImportFrom | ast.Import]:
|
|
51
117
|
listImportFrom: list[ast.ImportFrom] = []
|
|
52
|
-
for
|
|
118
|
+
for moduleWithLogicalPath, listOfNameTuples in sorted(self.dictionaryImportFrom.items()):
|
|
53
119
|
listOfNameTuples = sorted(list(set(listOfNameTuples)), key=lambda nameTuple: nameTuple[0])
|
|
54
120
|
list_alias: list[ast.alias] = []
|
|
55
121
|
for name, asname in listOfNameTuples:
|
|
56
122
|
list_alias.append(Make.alias(name, asname))
|
|
57
|
-
|
|
58
|
-
|
|
123
|
+
if list_alias:
|
|
124
|
+
listImportFrom.append(Make.ImportFrom(moduleWithLogicalPath, list_alias))
|
|
125
|
+
list_astImport: list[ast.Import] = [Make.Import(moduleWithLogicalPath) for moduleWithLogicalPath in sorted(set(self.listImport))]
|
|
59
126
|
return listImportFrom + list_astImport
|
|
60
127
|
|
|
61
128
|
def update(self, *fromLedger: 'LedgerOfImports') -> None:
|
|
@@ -72,20 +139,55 @@ class LedgerOfImports:
|
|
|
72
139
|
if isinstance(nodeBuffalo, (ast.Import, ast.ImportFrom)):
|
|
73
140
|
self.addAst(nodeBuffalo)
|
|
74
141
|
|
|
142
|
+
# Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
|
|
75
143
|
@dataclasses.dataclass
|
|
76
144
|
class IngredientsFunction:
|
|
77
|
-
"""
|
|
145
|
+
"""
|
|
146
|
+
Package a function definition with its import dependencies for code generation.
|
|
147
|
+
|
|
148
|
+
IngredientsFunction encapsulates an AST function definition along with all the
|
|
149
|
+
imports required for that function to operate correctly. This creates a modular,
|
|
150
|
+
portable unit that can be:
|
|
151
|
+
|
|
152
|
+
1. Transformed independently (e.g., by applying Numba decorators)
|
|
153
|
+
2. Transplanted between modules while maintaining dependencies
|
|
154
|
+
3. Combined with other functions to form complete modules
|
|
155
|
+
4. Analyzed for optimization opportunities
|
|
156
|
+
|
|
157
|
+
This class forms the primary unit of function manipulation in the code generation
|
|
158
|
+
system, enabling targeted transformations while preserving function dependencies.
|
|
159
|
+
|
|
78
160
|
Parameters:
|
|
79
|
-
astFunctionDef:
|
|
161
|
+
astFunctionDef: The AST representation of the function definition
|
|
162
|
+
imports: Import statements needed by the function
|
|
163
|
+
type_ignores: Type ignore comments associated with the function
|
|
80
164
|
"""
|
|
81
165
|
astFunctionDef: ast.FunctionDef
|
|
82
166
|
imports: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
|
|
83
167
|
type_ignores: list[ast.TypeIgnore] = dataclasses.field(default_factory=list)
|
|
84
168
|
|
|
169
|
+
# Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
|
|
85
170
|
@dataclasses.dataclass
|
|
86
171
|
class IngredientsModule:
|
|
87
|
-
"""
|
|
88
|
-
|
|
172
|
+
"""
|
|
173
|
+
Assemble a complete Python module from its constituent AST components.
|
|
174
|
+
|
|
175
|
+
IngredientsModule provides a structured container for all elements needed to
|
|
176
|
+
generate a complete Python module, including:
|
|
177
|
+
|
|
178
|
+
1. Import statements aggregated from all module components
|
|
179
|
+
2. Prologue code that runs before function definitions
|
|
180
|
+
3. Function definitions with their dependencies
|
|
181
|
+
4. Epilogue code that runs after function definitions
|
|
182
|
+
5. Entry point code executed when the module runs as a script
|
|
183
|
+
6. Type ignores and other annotations
|
|
184
|
+
|
|
185
|
+
This class enables programmatic assembly of Python modules with a clear
|
|
186
|
+
separation between different structural elements, while maintaining the
|
|
187
|
+
proper ordering and relationships between components.
|
|
188
|
+
|
|
189
|
+
The modular design allows transformations to be applied to specific parts
|
|
190
|
+
of a module while preserving the overall structure.
|
|
89
191
|
|
|
90
192
|
Parameters:
|
|
91
193
|
ingredientsFunction (None): One or more `IngredientsFunction` that will appended to `listIngredientsFunctions`.
|
|
@@ -124,7 +226,7 @@ class IngredientsModule:
|
|
|
124
226
|
"""Append one or more statements to `prologue`."""
|
|
125
227
|
list_body: list[ast.stmt] = []
|
|
126
228
|
listTypeIgnore: list[ast.TypeIgnore] = []
|
|
127
|
-
if astModule is not None and
|
|
229
|
+
if astModule is not None and isinstance(astModule, ast.Module): # type: ignore
|
|
128
230
|
list_body.extend(astModule.body)
|
|
129
231
|
listTypeIgnore.extend(astModule.type_ignores)
|
|
130
232
|
if type_ignores is not None:
|
|
@@ -153,10 +255,21 @@ class IngredientsModule:
|
|
|
153
255
|
def appendIngredientsFunction(self, *ingredientsFunction: IngredientsFunction) -> None:
|
|
154
256
|
"""Append one or more `IngredientsFunction`."""
|
|
155
257
|
for allegedIngredientsFunction in ingredientsFunction:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
258
|
+
assert isinstance(allegedIngredientsFunction, IngredientsFunction), ValueError(f"I received `{type(allegedIngredientsFunction) = }`, but I can only accept `{IngredientsFunction}`.")
|
|
259
|
+
self.listIngredientsFunctions.append(allegedIngredientsFunction)
|
|
260
|
+
|
|
261
|
+
def removeImportFromModule(self, moduleWithLogicalPath: str_nameDOTname) -> None:
|
|
262
|
+
self.removeImportFrom(moduleWithLogicalPath, None, None)
|
|
263
|
+
"""Remove all imports from a specific module."""
|
|
264
|
+
|
|
265
|
+
def removeImportFrom(self, moduleWithLogicalPath: str_nameDOTname, name: ast_Identifier | None, asname: ast_Identifier | None = None) -> None:
|
|
266
|
+
"""
|
|
267
|
+
This method modifies all `LedgerOfImports` in this `IngredientsModule` and all `IngredientsFunction` in `listIngredientsFunctions`.
|
|
268
|
+
It is not a "blacklist", so the `import from` could be added after this modification.
|
|
269
|
+
"""
|
|
270
|
+
self.imports.removeImportFrom(moduleWithLogicalPath, name, asname)
|
|
271
|
+
for ingredientsFunction in self.listIngredientsFunctions:
|
|
272
|
+
ingredientsFunction.imports.removeImportFrom(moduleWithLogicalPath, name, asname)
|
|
160
273
|
|
|
161
274
|
@property
|
|
162
275
|
def list_astImportImportFrom(self) -> list[ast.Import | ast.ImportFrom]:
|
|
@@ -191,13 +304,33 @@ class IngredientsModule:
|
|
|
191
304
|
listTypeIgnore.extend(self.launcher.type_ignores)
|
|
192
305
|
return listTypeIgnore
|
|
193
306
|
|
|
307
|
+
# Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
|
|
194
308
|
@dataclasses.dataclass
|
|
195
309
|
class RecipeSynthesizeFlow:
|
|
196
|
-
"""
|
|
310
|
+
"""
|
|
311
|
+
Configure the generation of optimized Numba-accelerated code modules.
|
|
312
|
+
|
|
313
|
+
RecipeSynthesizeFlow defines the complete blueprint for transforming an original
|
|
314
|
+
Python algorithm into an optimized, accelerated implementation. It specifies:
|
|
315
|
+
|
|
316
|
+
1. Source code locations and identifiers
|
|
317
|
+
2. Target code locations and identifiers
|
|
318
|
+
3. Naming conventions for generated modules and functions
|
|
319
|
+
4. File system paths for output files
|
|
320
|
+
5. Import relationships between components
|
|
321
|
+
|
|
322
|
+
This configuration class serves as a single source of truth for the code generation
|
|
323
|
+
process, ensuring consistency across all generated artifacts while enabling
|
|
324
|
+
customization of the transformation pipeline.
|
|
325
|
+
|
|
326
|
+
The transformation process uses this configuration to extract functions from the
|
|
327
|
+
source module, transform them according to optimization rules, and output
|
|
328
|
+
properly structured optimized modules with all necessary imports.
|
|
329
|
+
"""
|
|
197
330
|
# ========================================
|
|
198
331
|
# Source
|
|
199
|
-
|
|
200
|
-
|
|
332
|
+
source_astModule: ast.Module = parseLogicalPath2astModule(The.logicalPathModuleSourceAlgorithm)
|
|
333
|
+
"""AST of the source algorithm module containing the original implementation."""
|
|
201
334
|
|
|
202
335
|
# Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
|
|
203
336
|
sourceCallableDispatcher: ast_Identifier = The.sourceCallableDispatcher
|
|
@@ -245,18 +378,15 @@ class RecipeSynthesizeFlow:
|
|
|
245
378
|
|
|
246
379
|
# ========================================
|
|
247
380
|
# Computed
|
|
248
|
-
#
|
|
249
|
-
""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
theLogicalPathModuleDispatcherSynthetic: str = '.'.join([The.packageName, The.moduleOfSyntheticModules, theModuleDispatcherSynthetic])
|
|
254
|
-
|
|
255
|
-
"""
|
|
381
|
+
# Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
|
|
382
|
+
# theFormatStrModuleSynthetic = "{packageFlow}Count"
|
|
383
|
+
# theFormatStrModuleForCallableSynthetic = theFormatStrModuleSynthetic + "_{callableTarget}"
|
|
384
|
+
# theModuleDispatcherSynthetic: ast_Identifier = theFormatStrModuleForCallableSynthetic.format(packageFlow=packageFlowSynthetic, callableTarget=The.sourceCallableDispatcher)
|
|
385
|
+
# theLogicalPathModuleDispatcherSynthetic: str = '.'.join([The.packageName, The.moduleOfSyntheticModules, theModuleDispatcherSynthetic])
|
|
256
386
|
# logicalPathModuleDispatcher: str = '.'.join([Z0Z_flowLogicalPathRoot, moduleDispatcher])
|
|
387
|
+
|
|
257
388
|
# ========================================
|
|
258
389
|
# Filesystem (names of physical objects)
|
|
259
|
-
# ========================================
|
|
260
390
|
pathPackage: PurePosixPath | None = PurePosixPath(The.pathPackage)
|
|
261
391
|
fileExtension: str = The.fileExtension
|
|
262
392
|
|
|
@@ -289,46 +419,50 @@ theLogicalPathModuleDispatcherSynthetic: str = '.'.join([The.packageName, The.mo
|
|
|
289
419
|
def pathFilenameSequential(self) -> PurePosixPath:
|
|
290
420
|
return self._makePathFilename(filenameStem=self.moduleSequential, logicalPathINFIX=self.logicalPathFlowRoot)
|
|
291
421
|
|
|
292
|
-
def __post_init__(self) -> None:
|
|
293
|
-
if ((self.concurrencyManagerIdentifier is not None and self.concurrencyManagerIdentifier != self.sourceConcurrencyManagerIdentifier) # `submit` # type: ignore
|
|
294
|
-
or ((self.concurrencyManagerIdentifier is None) != (self.concurrencyManagerNamespace is None))): # type: ignore
|
|
295
|
-
import warnings
|
|
296
|
-
warnings.warn(f"If your synthesized module is weird, check `{self.concurrencyManagerIdentifier=}` and `{self.concurrencyManagerNamespace=}`. (ChildProcessError? 'Yeah! Children shouldn't be processing stuff, man.')", category=ChildProcessError, stacklevel=2) # pyright: ignore[reportCallIssue, reportArgumentType] Y'all Pynatics need to be less shrill and focus on making code that doesn't need 8000 error categories.
|
|
297
|
-
|
|
298
|
-
# self.logicalPathModuleDispatcher!=logicalPathModuleDispatcherHARDCODED or
|
|
299
|
-
if self.callableDispatcher!=callableDispatcherHARDCODED:
|
|
300
|
-
print(f"fyi: `{self.callableDispatcher=}` but\n\t`{callableDispatcherHARDCODED=}`.")
|
|
301
|
-
|
|
302
422
|
dummyAssign = Make.Assign([Make.Name("dummyTarget")], Make.Constant(None))
|
|
303
423
|
dummySubscript = Make.Subscript(Make.Name("dummy"), Make.Name("slice"))
|
|
304
424
|
dummyTuple = Make.Tuple([Make.Name("dummyElement")])
|
|
305
425
|
|
|
426
|
+
# Consolidate settings classes through inheritance https://github.com/hunterhogan/mapFolding/issues/15
|
|
306
427
|
@dataclasses.dataclass
|
|
307
428
|
class ShatteredDataclass:
|
|
308
|
-
countingVariableAnnotation:
|
|
429
|
+
countingVariableAnnotation: ast.expr
|
|
309
430
|
"""Type annotation for the counting variable extracted from the dataclass."""
|
|
431
|
+
|
|
310
432
|
countingVariableName: ast.Name
|
|
311
433
|
"""AST name node representing the counting variable identifier."""
|
|
434
|
+
|
|
312
435
|
field2AnnAssign: dict[ast_Identifier, ast.AnnAssign] = dataclasses.field(default_factory=dict)
|
|
313
436
|
"""Maps field names to their corresponding AST call expressions."""
|
|
437
|
+
|
|
314
438
|
Z0Z_field2AnnAssign: dict[ast_Identifier, tuple[ast.AnnAssign, str]] = dataclasses.field(default_factory=dict)
|
|
439
|
+
|
|
315
440
|
fragments4AssignmentOrParameters: ast.Tuple = dummyTuple
|
|
316
441
|
"""AST tuple used as target for assignment to capture returned fragments."""
|
|
442
|
+
|
|
317
443
|
ledger: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
|
|
318
444
|
"""Import records for the dataclass and its constituent parts."""
|
|
445
|
+
|
|
319
446
|
list_argAnnotated4ArgumentsSpecification: list[ast.arg] = dataclasses.field(default_factory=list)
|
|
320
447
|
"""Function argument nodes with annotations for parameter specification."""
|
|
448
|
+
|
|
321
449
|
list_keyword_field__field4init: list[ast.keyword] = dataclasses.field(default_factory=list)
|
|
322
450
|
"""Keyword arguments for dataclass initialization with field=field format."""
|
|
323
|
-
|
|
451
|
+
|
|
452
|
+
listAnnotations: list[ast.expr] = dataclasses.field(default_factory=list)
|
|
324
453
|
"""Type annotations for each dataclass field."""
|
|
454
|
+
|
|
325
455
|
listName4Parameters: list[ast.Name] = dataclasses.field(default_factory=list)
|
|
326
456
|
"""Name nodes for each dataclass field used as function parameters."""
|
|
457
|
+
|
|
327
458
|
listUnpack: list[ast.AnnAssign] = dataclasses.field(default_factory=list)
|
|
328
459
|
"""Annotated assignment statements to extract fields from dataclass."""
|
|
329
|
-
|
|
460
|
+
|
|
461
|
+
map_stateDOTfield2Name: dict[ast.AST, ast.Name] = dataclasses.field(default_factory=dict)
|
|
330
462
|
"""Maps AST expressions to Name nodes for find-replace operations."""
|
|
463
|
+
|
|
331
464
|
repack: ast.Assign = dummyAssign
|
|
332
465
|
"""AST assignment statement that reconstructs the original dataclass instance."""
|
|
466
|
+
|
|
333
467
|
signatureReturnAnnotation: ast.Subscript = dummySubscript
|
|
334
468
|
"""tuple-based return type annotation for function definitions."""
|
|
@@ -1,62 +1,183 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Core AST Traversal and Transformation Utilities for Python Code Manipulation
|
|
3
|
+
|
|
4
|
+
This module provides the foundation for traversing and modifying Python Abstract
|
|
5
|
+
Syntax Trees (ASTs). It contains two primary classes:
|
|
6
|
+
|
|
7
|
+
1. NodeTourist: Implements the visitor pattern to traverse an AST and extract information
|
|
8
|
+
from nodes that match specific predicates without modifying the AST.
|
|
9
|
+
|
|
10
|
+
2. NodeChanger: Extends ast.NodeTransformer to selectively transform AST nodes that
|
|
11
|
+
match specific predicates, enabling targeted code modifications.
|
|
12
|
+
|
|
13
|
+
The module also provides utilities for importing modules, loading callables from files,
|
|
14
|
+
and parsing Python code into AST structures, creating a complete workflow for code
|
|
15
|
+
analysis and transformation.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from collections.abc import Callable
|
|
2
19
|
from inspect import getsource as inspect_getsource
|
|
3
|
-
from mapFolding.someAssemblyRequired import ast_Identifier, str_nameDOTname
|
|
20
|
+
from mapFolding.someAssemblyRequired import ast_Identifier, str_nameDOTname
|
|
4
21
|
from os import PathLike
|
|
5
22
|
from pathlib import Path, PurePath
|
|
6
23
|
from types import ModuleType
|
|
7
|
-
from typing import Any,
|
|
24
|
+
from typing import Any, Literal
|
|
8
25
|
import ast
|
|
9
26
|
import importlib
|
|
10
27
|
import importlib.util
|
|
11
28
|
|
|
12
29
|
# TODO Identify the logic that narrows the type and can help the user during static type checking.
|
|
13
30
|
|
|
14
|
-
class NodeTourist(ast.NodeVisitor
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
class NodeTourist(ast.NodeVisitor):
|
|
32
|
+
"""
|
|
33
|
+
Visit and extract information from AST nodes that match a predicate.
|
|
34
|
+
|
|
35
|
+
NodeTourist implements the visitor pattern to traverse an AST, applying
|
|
36
|
+
a predicate function to each node and capturing nodes or their attributes
|
|
37
|
+
when they match. Unlike NodeChanger, it doesn't modify the AST but collects
|
|
38
|
+
information during traversal.
|
|
39
|
+
|
|
40
|
+
This class is particularly useful for analyzing AST structures, extracting
|
|
41
|
+
specific nodes or node properties, and gathering information about code patterns.
|
|
42
|
+
"""
|
|
43
|
+
def __init__(self, findThis: Callable[..., Any], doThat: Callable[..., Any]) -> None:
|
|
44
|
+
self.findThis = findThis
|
|
45
|
+
self.doThat = doThat
|
|
46
|
+
self.nodeCaptured: Any | None = None
|
|
47
|
+
|
|
48
|
+
def visit(self, node: ast.AST) -> None:
|
|
49
|
+
if self.findThis(node):
|
|
50
|
+
nodeActionReturn = self.doThat(node)
|
|
51
|
+
if nodeActionReturn is not None:
|
|
52
|
+
self.nodeCaptured = nodeActionReturn
|
|
53
|
+
self.generic_visit(node)
|
|
54
|
+
|
|
55
|
+
def captureLastMatch(self, node: ast.AST) -> Any | None:
|
|
56
|
+
self.nodeCaptured = None
|
|
57
|
+
self.visit(node)
|
|
58
|
+
return self.nodeCaptured
|
|
59
|
+
|
|
60
|
+
class NodeChanger(ast.NodeTransformer):
|
|
61
|
+
"""
|
|
62
|
+
Transform AST nodes that match a predicate by applying a transformation function.
|
|
63
|
+
|
|
64
|
+
NodeChanger is an AST node transformer that applies a targeted transformation
|
|
65
|
+
to nodes matching a specific predicate. It traverses the AST and only modifies
|
|
66
|
+
nodes that satisfy the predicate condition, leaving other nodes unchanged.
|
|
67
|
+
|
|
68
|
+
This class extends ast.NodeTransformer and implements the visitor pattern
|
|
69
|
+
to systematically process and transform an AST tree.
|
|
70
|
+
"""
|
|
71
|
+
def __init__(self, findThis: Callable[..., Any], doThat: Callable[..., Any]) -> None:
|
|
72
|
+
self.findThis = findThis
|
|
73
|
+
self.doThat = doThat
|
|
74
|
+
|
|
75
|
+
def visit(self, node: ast.AST) -> ast.AST:
|
|
76
|
+
if self.findThis(node):
|
|
77
|
+
return self.doThat(node)
|
|
78
|
+
return super().visit(node)
|
|
41
79
|
|
|
42
80
|
def importLogicalPath2Callable(logicalPathModule: str_nameDOTname, identifier: ast_Identifier, packageIdentifierIfRelative: ast_Identifier | None = None) -> Callable[..., Any]:
|
|
43
|
-
|
|
44
|
-
|
|
81
|
+
"""
|
|
82
|
+
Import a callable object (function or class) from a module based on its logical path.
|
|
83
|
+
|
|
84
|
+
This function imports a module using `importlib.import_module()` and then retrieves
|
|
85
|
+
a specific attribute (function, class, or other object) from that module.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
logicalPathModule : str
|
|
90
|
+
The logical path to the module, using dot notation (e.g., 'package.subpackage.module').
|
|
91
|
+
identifier : str
|
|
92
|
+
The name of the callable object to retrieve from the module.
|
|
93
|
+
packageIdentifierIfRelative : str, optional
|
|
94
|
+
The package name to use as the anchor point if `logicalPathModule` is a relative import.
|
|
95
|
+
If None, absolute import is assumed.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
Callable[..., Any]
|
|
100
|
+
The callable object (function, class, etc.) retrieved from the module.
|
|
101
|
+
"""
|
|
102
|
+
moduleImported: ModuleType = importlib.import_module(logicalPathModule, packageIdentifierIfRelative)
|
|
103
|
+
return getattr(moduleImported, identifier)
|
|
45
104
|
|
|
46
105
|
def importPathFilename2Callable(pathFilename: PathLike[Any] | PurePath, identifier: ast_Identifier, moduleIdentifier: ast_Identifier | None = None) -> Callable[..., Any]:
|
|
47
|
-
|
|
106
|
+
"""
|
|
107
|
+
Load a callable (function, class, etc.) from a Python file.
|
|
108
|
+
This function imports a specified Python file as a module, extracts a callable object
|
|
109
|
+
from it by name, and returns that callable.
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
pathFilename : Union[PathLike[Any], PurePath]
|
|
113
|
+
Path to the Python file to import.
|
|
114
|
+
identifier : str
|
|
115
|
+
Name of the callable to extract from the imported module.
|
|
116
|
+
moduleIdentifier : Optional[str]
|
|
117
|
+
Name to use for the imported module. If None, the filename stem is used.
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
Callable[..., Any]
|
|
121
|
+
The callable object extracted from the imported module.
|
|
122
|
+
Raises
|
|
123
|
+
------
|
|
124
|
+
ImportError
|
|
125
|
+
If the file cannot be imported or the importlib specification is invalid.
|
|
126
|
+
AttributeError
|
|
127
|
+
If the identifier does not exist in the imported module.
|
|
128
|
+
"""
|
|
129
|
+
pathFilename = Path(pathFilename)
|
|
130
|
+
|
|
131
|
+
importlibSpecification = importlib.util.spec_from_file_location(moduleIdentifier or pathFilename.stem, pathFilename)
|
|
132
|
+
if importlibSpecification is None or importlibSpecification.loader is None: raise ImportError(f"I received\n\t`{pathFilename = }`,\n\t`{identifier = }`, and\n\t`{moduleIdentifier = }`.\n\tAfter loading, \n\t`importlibSpecification` {'is `None`' if importlibSpecification is None else 'has a value'} and\n\t`importlibSpecification.loader` is unknown.")
|
|
133
|
+
|
|
134
|
+
moduleImported_jk_hahaha: ModuleType = importlib.util.module_from_spec(importlibSpecification)
|
|
135
|
+
importlibSpecification.loader.exec_module(moduleImported_jk_hahaha)
|
|
136
|
+
return getattr(moduleImported_jk_hahaha, identifier)
|
|
137
|
+
|
|
138
|
+
def parseLogicalPath2astModule(logicalPathModule: str_nameDOTname, packageIdentifierIfRelative: ast_Identifier|None=None, mode: Literal['exec'] = 'exec') -> ast.Module:
|
|
139
|
+
"""
|
|
140
|
+
Parse a logical Python module path into an AST Module.
|
|
141
|
+
|
|
142
|
+
This function imports a module using its logical path (e.g., 'package.subpackage.module')
|
|
143
|
+
and converts its source code into an Abstract Syntax Tree (AST) Module object.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
logicalPathModule : str
|
|
148
|
+
The logical path to the module using dot notation (e.g., 'package.module').
|
|
149
|
+
packageIdentifierIfRelative : ast.Identifier or None, optional
|
|
150
|
+
The package identifier to use if the module path is relative, defaults to None.
|
|
151
|
+
mode : Literal['exec'], optional
|
|
152
|
+
The parsing mode to use, defaults to 'exec'.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
ast.Module
|
|
157
|
+
An AST Module object representing the parsed source code of the imported module.
|
|
158
|
+
"""
|
|
159
|
+
moduleImported: ModuleType = importlib.import_module(logicalPathModule, packageIdentifierIfRelative)
|
|
160
|
+
sourcePython: str = inspect_getsource(moduleImported)
|
|
161
|
+
return ast.parse(sourcePython, mode=mode)
|
|
48
162
|
|
|
49
|
-
|
|
50
|
-
|
|
163
|
+
def parsePathFilename2astModule(pathFilename: PathLike[Any] | PurePath, mode: Literal['exec'] = 'exec') -> ast.Module:
|
|
164
|
+
"""
|
|
165
|
+
Parse a file from a given path into an ast.Module.
|
|
51
166
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return getattr(moduleImported_jk_hahaha, identifier)
|
|
167
|
+
This function reads the content of a file specified by `pathFilename` and parses it into an
|
|
168
|
+
Abstract Syntax Tree (AST) Module using Python's ast module.
|
|
55
169
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
pathFilename : PathLike[Any] | PurePath
|
|
173
|
+
The path to the file to be parsed. Can be a string path, PathLike object, or PurePath object.
|
|
174
|
+
mode : Literal['exec'], optional
|
|
175
|
+
The mode parameter for ast.parse. Default is 'exec'.
|
|
176
|
+
Options are 'exec', 'eval', or 'single'. See ast.parse documentation for details.
|
|
60
177
|
|
|
61
|
-
|
|
62
|
-
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
ast.Module
|
|
181
|
+
The parsed abstract syntax tree module.
|
|
182
|
+
"""
|
|
183
|
+
return ast.parse(Path(pathFilename).read_text(), mode=mode)
|