mapFolding 0.7.1__py3-none-any.whl → 0.8.1__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 +33 -4
- mapFolding/basecamp.py +14 -0
- mapFolding/beDRY.py +93 -82
- mapFolding/filesystem.py +124 -90
- mapFolding/noHomeYet.py +14 -2
- mapFolding/oeis.py +18 -3
- mapFolding/reference/flattened.py +46 -45
- mapFolding/reference/hunterNumba.py +4 -4
- mapFolding/reference/irvineJavaPort.py +1 -1
- mapFolding/reference/lunnanNumpy.py +3 -4
- mapFolding/reference/lunnanWhile.py +5 -7
- mapFolding/reference/rotatedEntryPoint.py +2 -3
- mapFolding/someAssemblyRequired/__init__.py +33 -3
- mapFolding/someAssemblyRequired/getLLVMforNoReason.py +32 -15
- mapFolding/someAssemblyRequired/ingredientsNumba.py +108 -2
- mapFolding/someAssemblyRequired/synthesizeNumbaFlow.py +196 -0
- mapFolding/someAssemblyRequired/{synthesizeNumbaJob.py → synthesizeNumbaJobVESTIGIAL.py} +19 -23
- mapFolding/someAssemblyRequired/transformDataStructures.py +162 -0
- mapFolding/someAssemblyRequired/transformationTools.py +607 -252
- mapFolding/syntheticModules/numbaCount_doTheNeedful.py +197 -12
- mapFolding/theDao.py +37 -16
- mapFolding/theSSOT.py +47 -44
- {mapfolding-0.7.1.dist-info → mapfolding-0.8.1.dist-info}/METADATA +51 -46
- mapfolding-0.8.1.dist-info/RECORD +39 -0
- {mapfolding-0.7.1.dist-info → mapfolding-0.8.1.dist-info}/WHEEL +1 -1
- tests/conftest.py +2 -3
- tests/test_filesystem.py +0 -2
- tests/test_other.py +2 -3
- tests/test_tasks.py +0 -4
- mapFolding/reference/lunnan.py +0 -153
- mapFolding/someAssemblyRequired/Z0Z_workbench.py +0 -33
- mapFolding/someAssemblyRequired/synthesizeCountingFunctions.py +0 -7
- mapFolding/someAssemblyRequired/synthesizeDataConverters.py +0 -135
- mapFolding/someAssemblyRequired/synthesizeNumba.py +0 -91
- mapFolding/someAssemblyRequired/synthesizeNumbaModules.py +0 -91
- mapFolding/someAssemblyRequired/whatWillBe.py +0 -357
- mapFolding/syntheticModules/dataNamespaceFlattened.py +0 -30
- mapFolding/syntheticModules/multiprocessingCount_doTheNeedful.py +0 -216
- mapFolding/syntheticModules/numbaCount.py +0 -90
- mapFolding/syntheticModules/numbaCountExample.py +0 -158
- mapFolding/syntheticModules/numbaCountSequential.py +0 -111
- mapFolding/syntheticModules/numba_doTheNeedful.py +0 -12
- mapFolding/syntheticModules/numba_doTheNeedfulExample.py +0 -13
- mapfolding-0.7.1.dist-info/RECORD +0 -51
- /mapFolding/{syntheticModules → reference}/__init__.py +0 -0
- {mapfolding-0.7.1.dist-info → mapfolding-0.8.1.dist-info}/entry_points.txt +0 -0
- {mapfolding-0.7.1.dist-info → mapfolding-0.8.1.dist-info/licenses}/LICENSE +0 -0
- {mapfolding-0.7.1.dist-info → mapfolding-0.8.1.dist-info}/top_level.txt +0 -0
|
@@ -1,15 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools for transforming Python code through abstract syntax tree (AST) manipulation.
|
|
3
|
+
|
|
4
|
+
This module provides a comprehensive set of utilities for programmatically analyzing,
|
|
5
|
+
transforming, and generating Python code through AST manipulation. It implements
|
|
6
|
+
a highly flexible framework that enables:
|
|
7
|
+
|
|
8
|
+
1. Precise identification of code patterns through composable predicates
|
|
9
|
+
2. Targeted modification of code structures while preserving semantics
|
|
10
|
+
3. Code generation with proper syntax and import management
|
|
11
|
+
4. Analysis of code dependencies and relationships
|
|
12
|
+
5. Clean transformation of one algorithmic implementation to another
|
|
13
|
+
|
|
14
|
+
The utilities are organized into several key components:
|
|
15
|
+
- Predicate factories (ifThis): Create composable functions for matching AST patterns
|
|
16
|
+
- Node transformers: Modify AST structures in targeted ways
|
|
17
|
+
- Code generation helpers (Make): Create well-formed AST nodes programmatically
|
|
18
|
+
- Import tracking: Maintain proper imports during code transformation
|
|
19
|
+
- Analysis tools: Extract and organize code information
|
|
20
|
+
|
|
21
|
+
While these tools were developed to transform the baseline algorithm into optimized formats,
|
|
22
|
+
they are designed as general-purpose utilities applicable to a wide range of code
|
|
23
|
+
transformation scenarios beyond the scope of this package.
|
|
24
|
+
"""
|
|
25
|
+
from autoflake import fix_code as autoflake_fix_code
|
|
26
|
+
from collections import defaultdict
|
|
1
27
|
from collections.abc import Callable, Container, Sequence
|
|
2
|
-
from
|
|
3
|
-
from
|
|
28
|
+
from copy import deepcopy
|
|
29
|
+
from inspect import getsource as inspect_getsource
|
|
30
|
+
from mapFolding.filesystem import writeStringToHere
|
|
31
|
+
from mapFolding.theSSOT import (
|
|
32
|
+
getSourceAlgorithm,
|
|
33
|
+
raiseIfNoneGitHubIssueNumber3,
|
|
34
|
+
theDataclassIdentifier,
|
|
35
|
+
theDataclassInstance,
|
|
36
|
+
theDataclassInstanceTaskDistribution,
|
|
37
|
+
theDispatcherCallable,
|
|
38
|
+
theFileExtension,
|
|
39
|
+
theFormatStrModuleForCallableSynthetic,
|
|
40
|
+
theFormatStrModuleSynthetic,
|
|
41
|
+
theLogicalPathModuleDataclass,
|
|
42
|
+
theLogicalPathModuleDispatcherSynthetic,
|
|
43
|
+
theModuleDispatcherSynthetic,
|
|
44
|
+
theModuleOfSyntheticModules,
|
|
45
|
+
thePackageName,
|
|
46
|
+
thePathPackage,
|
|
47
|
+
theSourceInitializeCallable,
|
|
48
|
+
theSourceParallelCallable,
|
|
49
|
+
theSourceSequentialCallable,
|
|
50
|
+
)
|
|
51
|
+
from os import PathLike
|
|
52
|
+
from pathlib import Path, PurePath, PurePosixPath
|
|
53
|
+
from types import ModuleType
|
|
54
|
+
from typing import Any, cast, Generic, TypeAlias, TypeGuard, TypeVar
|
|
55
|
+
from Z0Z_tools import updateExtendPolishDictionaryLists
|
|
4
56
|
import ast
|
|
5
|
-
|
|
6
|
-
|
|
57
|
+
import dataclasses
|
|
58
|
+
|
|
7
59
|
"""
|
|
8
60
|
Semiotic notes:
|
|
9
61
|
In the `ast` package, some things that look and feel like a "name" are not `ast.Name` type. The following semiotics are a balance between technical precision and practical usage.
|
|
10
62
|
|
|
11
63
|
astName: always means `ast.Name`.
|
|
12
64
|
Name: uppercase, _should_ be interchangeable with astName, even in camelCase.
|
|
65
|
+
Hunter: ^^ did you do that ^^ ? Are you sure? You just fixed some that should have been "_name" because it confused you.
|
|
13
66
|
name: lowercase, never means `ast.Name`. In camelCase, I _should_ avoid using it in such a way that it could be confused with "Name", uppercase.
|
|
14
67
|
_Identifier: very strongly correlates with the private `ast._Identifier`, which is a TypeAlias for `str`.
|
|
15
68
|
identifier: lowercase, a general term that includes the above and other Python identifiers.
|
|
@@ -17,316 +70,332 @@ Identifier: uppercase, without the leading underscore should only appear in came
|
|
|
17
70
|
namespace: lowercase, in dotted-names, such as `pathlib.Path` or `collections.abc`, "namespace" is the part before the dot.
|
|
18
71
|
Namespace: uppercase, should only appear in camelCase and means "namespace", lowercase.
|
|
19
72
|
"""
|
|
20
|
-
# TODO consider semiotic usefulness of "namespace" or variations such as "namespaceName", "namespacePath", and "namespace_Identifier"
|
|
21
73
|
|
|
22
|
-
#
|
|
74
|
+
# Would `LibCST` be better than `ast` in some cases? https://github.com/hunterhogan/mapFolding/issues/7
|
|
23
75
|
|
|
24
|
-
|
|
76
|
+
ast_expr_Slice: TypeAlias = ast.expr
|
|
25
77
|
ast_Identifier: TypeAlias = str
|
|
26
|
-
|
|
27
|
-
|
|
78
|
+
astClassHasAttributeDOTname: TypeAlias = ast.FunctionDef | ast.ClassDef | ast.AsyncFunctionDef
|
|
79
|
+
astMosDef = TypeVar('astMosDef', bound=astClassHasAttributeDOTname)
|
|
28
80
|
list_ast_type_paramORintORNone: TypeAlias = Any
|
|
81
|
+
nodeType = TypeVar('nodeType', bound=ast.AST)
|
|
82
|
+
strDotStrCuzPyStoopid: TypeAlias = str
|
|
29
83
|
strORintORNone: TypeAlias = Any
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# NOTE: the new "Recipe" concept will allow me to remove this
|
|
33
|
-
class YouOughtaKnow(NamedTuple):
|
|
34
|
-
callableSynthesized: str
|
|
35
|
-
pathFilenameForMe: Path
|
|
36
|
-
astForCompetentProgrammers: ast.ImportFrom
|
|
37
|
-
|
|
38
|
-
# listAsNode
|
|
84
|
+
strORlist_ast_type_paramORintORNone: TypeAlias = Any
|
|
39
85
|
|
|
40
|
-
class NodeCollector(ast.NodeVisitor):
|
|
41
|
-
|
|
42
|
-
def __init__(self,
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
86
|
+
class NodeCollector(Generic[nodeType], ast.NodeVisitor):
|
|
87
|
+
"""A node visitor that collects data via one or more actions when a predicate is met."""
|
|
88
|
+
def __init__(self, findThis: Callable[[ast.AST], TypeGuard[nodeType] | bool], doThat: list[Callable[[nodeType], Any]]) -> None:
|
|
89
|
+
self.findThis = findThis
|
|
90
|
+
self.doThat = doThat
|
|
45
91
|
|
|
46
92
|
def visit(self, node: ast.AST) -> None:
|
|
47
|
-
if self.
|
|
48
|
-
for action in self.
|
|
49
|
-
action(node)
|
|
93
|
+
if self.findThis(node):
|
|
94
|
+
for action in self.doThat:
|
|
95
|
+
action(cast(nodeType, node))
|
|
50
96
|
self.generic_visit(node)
|
|
51
97
|
|
|
52
|
-
class NodeReplacer(ast.NodeTransformer):
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
None from the replacement builder indicates that the node should be removed.
|
|
58
|
-
|
|
59
|
-
Attributes:
|
|
60
|
-
findMe: A function that finds all locations that match a one or more conditions.
|
|
61
|
-
doThis: A function that does work at each location, such as make a new node, collect information or delete the node.
|
|
62
|
-
|
|
63
|
-
Methods:
|
|
64
|
-
visit(node: ast.AST) -> Optional[ast.AST]:
|
|
65
|
-
Visits each node in the AST, replacing or removing it based on the predicate.
|
|
66
|
-
"""
|
|
67
|
-
def __init__(self
|
|
68
|
-
, findMe: Callable[[ast.AST], bool]
|
|
69
|
-
, doThis: Callable[[ast.AST], ast.AST | Sequence[ast.AST] | None]
|
|
70
|
-
) -> None:
|
|
71
|
-
self.findMe = findMe
|
|
72
|
-
self.doThis = doThis
|
|
98
|
+
class NodeReplacer(Generic[nodeType], ast.NodeTransformer):
|
|
99
|
+
"""A node transformer that replaces or removes AST nodes based on a condition."""
|
|
100
|
+
def __init__(self, findThis: Callable[[ast.AST], TypeGuard[nodeType] | bool], doThat: Callable[[nodeType], ast.AST | Sequence[ast.AST] | None]) -> None:
|
|
101
|
+
self.findThis = findThis
|
|
102
|
+
self.doThat = doThat
|
|
73
103
|
|
|
74
104
|
def visit(self, node: ast.AST) -> ast.AST | Sequence[ast.AST] | None:
|
|
75
|
-
if self.
|
|
76
|
-
return self.
|
|
105
|
+
if self.findThis(node):
|
|
106
|
+
return self.doThat(cast(nodeType, node))
|
|
77
107
|
return super().visit(node)
|
|
78
108
|
|
|
79
|
-
def descendantContainsMatchingNode(node: ast.AST, predicateFunction: Callable[[ast.AST], bool]) -> bool:
|
|
80
|
-
""" Return True if any descendant of the node (or the node itself) matches the predicateFunction. """
|
|
81
|
-
matchFound = False
|
|
82
|
-
|
|
83
|
-
class DescendantFinder(ast.NodeVisitor):
|
|
84
|
-
def generic_visit(self, node: ast.AST) -> None:
|
|
85
|
-
nonlocal matchFound
|
|
86
|
-
if predicateFunction(node):
|
|
87
|
-
matchFound = True
|
|
88
|
-
else:
|
|
89
|
-
super().generic_visit(node)
|
|
90
|
-
|
|
91
|
-
DescendantFinder().visit(node)
|
|
92
|
-
return matchFound
|
|
93
|
-
|
|
94
|
-
def executeActionUnlessDescendantMatches(exclusionPredicate: Callable[[ast.AST], bool], actionFunction: Callable[[ast.AST], None]) -> Callable[[ast.AST], None]:
|
|
95
|
-
"""
|
|
96
|
-
Return a new action that will execute actionFunction only if no descendant (or the node itself)
|
|
97
|
-
matches exclusionPredicate.
|
|
98
|
-
"""
|
|
99
|
-
def wrappedAction(node: ast.AST) -> None:
|
|
100
|
-
if not descendantContainsMatchingNode(node, exclusionPredicate):
|
|
101
|
-
actionFunction(node)
|
|
102
|
-
return wrappedAction
|
|
103
|
-
|
|
104
109
|
class ifThis:
|
|
105
110
|
@staticmethod
|
|
106
|
-
def
|
|
107
|
-
return lambda nodeTarget: any(predicate(nodeTarget) for predicate in somePredicates)
|
|
108
|
-
|
|
109
|
-
@staticmethod
|
|
110
|
-
def ast_IdentifierIsIn(container: Container[ast_Identifier]) -> Callable[[ast_Identifier], bool]:
|
|
111
|
+
def ast_IdentifierIsIn(container: Container[ast_Identifier]) -> Callable[[ast_Identifier], TypeGuard[ast_Identifier] | bool]:
|
|
111
112
|
return lambda node: node in container
|
|
112
|
-
|
|
113
|
-
# TODO is this only useable if namespace is not `None`? Yes, but use "" for namespace if necessary.
|
|
114
113
|
@staticmethod
|
|
115
|
-
def CallDoesNotCallItself(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
def CallDoesNotCallItself(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Call] | bool]:
|
|
115
|
+
"""If `namespace` is not applicable to your case, then call with `namespace=""`."""
|
|
116
|
+
return lambda node: ifThis.matchesMeButNotAnyDescendant(ifThis.CallReallyIs(namespace, identifier))(node)
|
|
118
117
|
@staticmethod
|
|
119
|
-
def CallDoesNotCallItselfAndNameDOTidIsIn(container: Container[ast_Identifier]) -> Callable[[ast.AST], bool]:
|
|
120
|
-
return lambda
|
|
121
|
-
|
|
118
|
+
def CallDoesNotCallItselfAndNameDOTidIsIn(container: Container[ast_Identifier]) -> Callable[[ast.AST], TypeGuard[ast.Call] | bool]:
|
|
119
|
+
return lambda node: ifThis.isCall(node) and ifThis.isName(node.func) and ifThis.ast_IdentifierIsIn(container)(node.func.id) and ifThis.CallDoesNotCallItself("", node.func.id)(node)
|
|
122
120
|
@staticmethod
|
|
123
|
-
def CallReallyIs(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
124
|
-
return ifThis.
|
|
125
|
-
|
|
121
|
+
def CallReallyIs(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Call] | bool]:
|
|
122
|
+
return ifThis.isAnyOf(ifThis.isCall_Identifier(identifier), ifThis.isCallNamespace_Identifier(namespace, identifier))
|
|
126
123
|
@staticmethod
|
|
127
124
|
def is_keyword(node: ast.AST) -> TypeGuard[ast.keyword]:
|
|
128
125
|
return isinstance(node, ast.keyword)
|
|
129
|
-
|
|
130
126
|
@staticmethod
|
|
131
|
-
def is_keywordAndValueIsConstant(
|
|
132
|
-
return ifThis.is_keyword(
|
|
133
|
-
|
|
127
|
+
def is_keywordAndValueIsConstant(node: ast.AST) -> TypeGuard[ast.keyword]:
|
|
128
|
+
return ifThis.is_keyword(node) and ifThis.isConstant(node.value)
|
|
134
129
|
@staticmethod
|
|
135
|
-
def is_keyword_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
136
|
-
|
|
130
|
+
def is_keyword_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.keyword] | bool]:
|
|
131
|
+
def workhorse(node: ast.AST) -> TypeGuard[ast.keyword] | bool:
|
|
132
|
+
return ifThis.is_keyword(node) and node.arg == identifier
|
|
133
|
+
return workhorse
|
|
137
134
|
@staticmethod
|
|
138
|
-
def is_keyword_IdentifierEqualsConstantValue(identifier: ast_Identifier, ConstantValue: Any) -> Callable[[ast.AST], bool]:
|
|
139
|
-
return lambda
|
|
140
|
-
|
|
135
|
+
def is_keyword_IdentifierEqualsConstantValue(identifier: ast_Identifier, ConstantValue: Any) -> Callable[[ast.AST], TypeGuard[ast.keyword] | bool]:
|
|
136
|
+
return lambda node: ifThis.is_keyword_Identifier(identifier)(node) and ifThis.is_keywordAndValueIsConstant(node) and ifThis.isConstantEquals(ConstantValue)(node.value)
|
|
137
|
+
@staticmethod
|
|
138
|
+
def isAllOf(*thesePredicates: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
|
|
139
|
+
return lambda node: all(predicate(node) for predicate in thesePredicates)
|
|
141
140
|
@staticmethod
|
|
142
141
|
def isAnnAssign(node: ast.AST) -> TypeGuard[ast.AnnAssign]:
|
|
143
142
|
return isinstance(node, ast.AnnAssign)
|
|
144
|
-
|
|
145
143
|
@staticmethod
|
|
146
144
|
def isAnnAssignAndAnnotationIsName(node: ast.AST) -> TypeGuard[ast.AnnAssign]:
|
|
147
145
|
return ifThis.isAnnAssign(node) and ifThis.isName(node.annotation)
|
|
148
|
-
|
|
149
146
|
@staticmethod
|
|
150
|
-
def isAnnAssignAndTargetIsName(
|
|
151
|
-
return ifThis.isAnnAssign(
|
|
152
|
-
|
|
147
|
+
def isAnnAssignAndTargetIsName(node: ast.AST) -> TypeGuard[ast.AnnAssign]:
|
|
148
|
+
return ifThis.isAnnAssign(node) and ifThis.isName(node.target)
|
|
153
149
|
@staticmethod
|
|
154
|
-
def isAnnAssignTo(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
155
|
-
return lambda
|
|
156
|
-
|
|
150
|
+
def isAnnAssignTo(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.AnnAssign] | bool]:
|
|
151
|
+
return lambda node: ifThis.isAnnAssign(node) and ifThis.NameReallyIs_Identifier(identifier)(node.target)
|
|
157
152
|
@staticmethod
|
|
158
153
|
def isAnyAssignmentTo(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
159
|
-
return ifThis.
|
|
160
|
-
|
|
154
|
+
return ifThis.isAnyOf(ifThis.isAssignOnlyTo(identifier), ifThis.isAnnAssignTo(identifier), ifThis.isAugAssignTo(identifier))
|
|
155
|
+
@staticmethod
|
|
156
|
+
def isAnyCompare(node: ast.AST) -> TypeGuard[ast.Compare] | TypeGuard[ast.BoolOp]:
|
|
157
|
+
return ifThis.isCompare(node) or ifThis.isBoolOp(node)
|
|
158
|
+
@staticmethod
|
|
159
|
+
def isAnyOf(*thesePredicates: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
|
|
160
|
+
return lambda node: any(predicate(node) for predicate in thesePredicates)
|
|
161
161
|
@staticmethod
|
|
162
162
|
def isAssign(node: ast.AST) -> TypeGuard[ast.Assign]:
|
|
163
163
|
return isinstance(node, ast.Assign)
|
|
164
|
-
|
|
165
164
|
@staticmethod
|
|
166
|
-
def
|
|
167
|
-
return lambda
|
|
168
|
-
|
|
165
|
+
def isAssignAndValueIsCall_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Assign] | bool]:
|
|
166
|
+
return lambda node: ifThis.isAssign(node) and ifThis.isCall_Identifier(identifier)(node.value)
|
|
167
|
+
@staticmethod
|
|
168
|
+
def isAssignAndValueIsCallNamespace_Identifier(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Assign] | bool]:
|
|
169
|
+
return ifThis.isAssignAndValueIs(ifThis.isCallNamespace_Identifier(namespace, identifier))
|
|
170
|
+
@staticmethod
|
|
171
|
+
def isAssignOnlyTo(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Assign] | bool]:
|
|
172
|
+
return lambda node: ifThis.isAssign(node) and ifThis.NameReallyIs_Identifier(identifier)(node.targets[0])
|
|
173
|
+
@staticmethod
|
|
174
|
+
def isAssignAndTargets0Is(targets0Predicate: Callable[[ast.AST], bool]) -> Callable[[ast.AST], TypeGuard[ast.Assign] | bool]:
|
|
175
|
+
"""node is Assign and node.targets[0] matches `targets0Predicate`."""
|
|
176
|
+
return lambda node: ifThis.isAssign(node) and targets0Predicate(node.targets[0])
|
|
177
|
+
@staticmethod
|
|
178
|
+
def isAssignAndValueIs(valuePredicate: Callable[[ast.AST], bool]) -> Callable[[ast.AST], TypeGuard[ast.Assign] | bool]:
|
|
179
|
+
"""node is ast.Assign and node.value matches `valuePredicate`.
|
|
180
|
+
Parameters:
|
|
181
|
+
valuePredicate: Function that evaluates the value of the assignment
|
|
182
|
+
Returns:
|
|
183
|
+
predicate: matches assignments with values meeting the criteria
|
|
184
|
+
"""
|
|
185
|
+
return lambda node: ifThis.isAssign(node) and valuePredicate(node.value)
|
|
169
186
|
@staticmethod
|
|
170
187
|
def isAttribute(node: ast.AST) -> TypeGuard[ast.Attribute]:
|
|
171
188
|
return isinstance(node, ast.Attribute)
|
|
172
|
-
|
|
173
189
|
@staticmethod
|
|
174
190
|
def isAugAssign(node: ast.AST) -> TypeGuard[ast.AugAssign]:
|
|
175
191
|
return isinstance(node, ast.AugAssign)
|
|
176
|
-
|
|
177
192
|
@staticmethod
|
|
178
|
-
def isAugAssignTo(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
179
|
-
return lambda
|
|
180
|
-
|
|
193
|
+
def isAugAssignTo(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.AugAssign] | bool]:
|
|
194
|
+
return lambda node: ifThis.isAugAssign(node) and ifThis.NameReallyIs_Identifier(identifier)(node.target)
|
|
195
|
+
@staticmethod
|
|
196
|
+
def isBoolOp(node: ast.AST) -> TypeGuard[ast.BoolOp]:
|
|
197
|
+
return isinstance(node, ast.BoolOp)
|
|
181
198
|
@staticmethod
|
|
182
199
|
def isCall(node: ast.AST) -> TypeGuard[ast.Call]:
|
|
183
200
|
return isinstance(node, ast.Call)
|
|
184
|
-
|
|
185
201
|
@staticmethod
|
|
186
|
-
def isCall_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
187
|
-
return lambda
|
|
188
|
-
|
|
189
|
-
# TODO what happens if `None` is passed as the namespace?
|
|
202
|
+
def isCall_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Call] | bool]:
|
|
203
|
+
return lambda node: ifThis.isCall(node) and ifThis.isName_Identifier(identifier)(node.func)
|
|
190
204
|
@staticmethod
|
|
191
|
-
def isCallNamespace_Identifier(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
192
|
-
return lambda node: ifThis.isCall(node) and ifThis.
|
|
193
|
-
|
|
205
|
+
def isCallNamespace_Identifier(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Call] | bool]:
|
|
206
|
+
return lambda node: ifThis.isCall(node) and ifThis.is_nameDOTnameNamespace_Identifier(namespace, identifier)(node.func)
|
|
194
207
|
@staticmethod
|
|
195
208
|
def isCallToName(node: ast.AST) -> TypeGuard[ast.Call]:
|
|
196
209
|
return ifThis.isCall(node) and ifThis.isName(node.func)
|
|
197
|
-
|
|
198
210
|
@staticmethod
|
|
199
211
|
def isClassDef(node: ast.AST) -> TypeGuard[ast.ClassDef]:
|
|
200
212
|
return isinstance(node, ast.ClassDef)
|
|
201
|
-
|
|
202
213
|
@staticmethod
|
|
203
|
-
def isClassDef_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
214
|
+
def isClassDef_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.ClassDef] | bool]:
|
|
204
215
|
return lambda node: ifThis.isClassDef(node) and node.name == identifier
|
|
205
|
-
|
|
216
|
+
@staticmethod
|
|
217
|
+
def isCompare(node: ast.AST) -> TypeGuard[ast.Compare]:
|
|
218
|
+
return isinstance(node, ast.Compare)
|
|
206
219
|
@staticmethod
|
|
207
220
|
def isConstant(node: ast.AST) -> TypeGuard[ast.Constant]:
|
|
208
221
|
return isinstance(node, ast.Constant)
|
|
209
|
-
|
|
210
222
|
@staticmethod
|
|
211
|
-
def isConstantEquals(value: Any) -> Callable[[ast.AST], bool]:
|
|
223
|
+
def isConstantEquals(value: Any) -> Callable[[ast.AST], TypeGuard[ast.Constant] | bool]:
|
|
212
224
|
return lambda node: ifThis.isConstant(node) and node.value == value
|
|
213
|
-
|
|
225
|
+
@staticmethod
|
|
226
|
+
def isExpr(node: ast.AST) -> TypeGuard[ast.Expr]:
|
|
227
|
+
return isinstance(node, ast.Expr)
|
|
214
228
|
@staticmethod
|
|
215
229
|
def isFunctionDef(node: ast.AST) -> TypeGuard[ast.FunctionDef]:
|
|
216
230
|
return isinstance(node, ast.FunctionDef)
|
|
217
|
-
|
|
218
231
|
@staticmethod
|
|
219
|
-
def isFunctionDef_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
232
|
+
def isFunctionDef_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.FunctionDef] | bool]:
|
|
220
233
|
return lambda node: ifThis.isFunctionDef(node) and node.name == identifier
|
|
221
|
-
|
|
222
234
|
@staticmethod
|
|
223
235
|
def isImport(node: ast.AST) -> TypeGuard[ast.Import]:
|
|
224
236
|
return isinstance(node, ast.Import)
|
|
225
|
-
|
|
226
237
|
@staticmethod
|
|
227
238
|
def isName(node: ast.AST) -> TypeGuard[ast.Name]:
|
|
239
|
+
"""TODO
|
|
240
|
+
ast.Name()
|
|
241
|
+
ast.Attribute()
|
|
242
|
+
ast.Subscript()
|
|
243
|
+
ast.Starred()
|
|
244
|
+
"""
|
|
228
245
|
return isinstance(node, ast.Name)
|
|
229
|
-
|
|
230
246
|
@staticmethod
|
|
231
|
-
def isName_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
247
|
+
def isName_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Name] | bool]:
|
|
232
248
|
return lambda node: ifThis.isName(node) and node.id == identifier
|
|
233
|
-
|
|
234
249
|
@staticmethod
|
|
235
|
-
def
|
|
250
|
+
def is_nameDOTname(node: ast.AST) -> TypeGuard[ast.Attribute]:
|
|
236
251
|
return ifThis.isAttribute(node) and ifThis.isName(node.value)
|
|
237
|
-
|
|
238
252
|
@staticmethod
|
|
239
|
-
def
|
|
240
|
-
return lambda node: ifThis.
|
|
241
|
-
|
|
253
|
+
def is_nameDOTnameNamespace(namespace: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Attribute] | bool]:
|
|
254
|
+
return lambda node: ifThis.is_nameDOTname(node) and ifThis.isName_Identifier(namespace)(node.value)
|
|
255
|
+
@staticmethod
|
|
256
|
+
def is_nameDOTnameNamespace_Identifier(namespace: ast_Identifier, identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Attribute] | bool]:
|
|
257
|
+
return lambda node: ifThis.is_nameDOTname(node) and ifThis.isName_Identifier(namespace)(node.value) and node.attr == identifier
|
|
258
|
+
@staticmethod
|
|
259
|
+
def NameReallyIs_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
260
|
+
# The following logic is incomplete.
|
|
261
|
+
return ifThis.isAnyOf(ifThis.isName_Identifier(identifier), ifThis.isSubscriptIsName_Identifier(identifier))
|
|
262
|
+
@staticmethod
|
|
263
|
+
def isReturn(node: ast.AST) -> TypeGuard[ast.Return]:
|
|
264
|
+
return isinstance(node, ast.Return)
|
|
265
|
+
@staticmethod
|
|
266
|
+
def isReturnAnyCompare(node: ast.AST) -> TypeGuard[ast.Return]:
|
|
267
|
+
return ifThis.isReturn(node) and node.value is not None and ifThis.isAnyCompare(node.value)
|
|
268
|
+
@staticmethod
|
|
269
|
+
def isReturnUnaryOp(node: ast.AST) -> TypeGuard[ast.Return]:
|
|
270
|
+
return ifThis.isReturn(node) and node.value is not None and ifThis.isUnaryOp(node.value)
|
|
242
271
|
@staticmethod
|
|
243
272
|
def isSubscript(node: ast.AST) -> TypeGuard[ast.Subscript]:
|
|
244
273
|
return isinstance(node, ast.Subscript)
|
|
245
|
-
|
|
246
274
|
@staticmethod
|
|
247
|
-
def isSubscript_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST],
|
|
275
|
+
def isSubscript_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Subscript]]:
|
|
276
|
+
"""node is `ast.Subscript` and the top-level `ast.Name` is `identifier`
|
|
277
|
+
Parameters:
|
|
278
|
+
identifier: The identifier to look for in the value chain
|
|
279
|
+
Returns:
|
|
280
|
+
predicate: function that checks if a node matches the criteria
|
|
281
|
+
"""
|
|
282
|
+
def predicate(node: ast.AST) -> TypeGuard[ast.Subscript]:
|
|
283
|
+
if not ifThis.isSubscript(node):
|
|
284
|
+
return False
|
|
285
|
+
def checkNodeDOTvalue(nodeDOTvalue: ast.AST) -> bool:
|
|
286
|
+
if ifThis.isName(nodeDOTvalue):
|
|
287
|
+
if nodeDOTvalue.id == identifier:
|
|
288
|
+
return True
|
|
289
|
+
elif hasattr(nodeDOTvalue, "value"):
|
|
290
|
+
return checkNodeDOTvalue(nodeDOTvalue.value) # type: ignore
|
|
291
|
+
return False
|
|
292
|
+
return checkNodeDOTvalue(node.value)
|
|
293
|
+
return predicate
|
|
294
|
+
@staticmethod
|
|
295
|
+
def isSubscriptIsName_Identifier(identifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Subscript] | bool]:
|
|
248
296
|
return lambda node: ifThis.isSubscript(node) and ifThis.isName_Identifier(identifier)(node.value)
|
|
249
|
-
|
|
250
297
|
@staticmethod
|
|
251
|
-
def isSubscript_Identifier_Identifier(identifier: ast_Identifier, sliceIdentifier: ast_Identifier) -> Callable[[ast.AST], bool]:
|
|
252
|
-
return lambda node: ifThis.isSubscript(node) and ifThis.isName_Identifier(identifier)(node.value) and ifThis.isName_Identifier(sliceIdentifier)(node.slice)
|
|
253
|
-
|
|
298
|
+
def isSubscript_Identifier_Identifier(identifier: ast_Identifier, sliceIdentifier: ast_Identifier) -> Callable[[ast.AST], TypeGuard[ast.Subscript] | bool]:
|
|
299
|
+
return lambda node: ifThis.isSubscript(node) and ifThis.isName_Identifier(identifier)(node.value) and ifThis.isName_Identifier(sliceIdentifier)(node.slice)
|
|
300
|
+
@staticmethod
|
|
301
|
+
def isUnaryOp(node: ast.AST) -> TypeGuard[ast.UnaryOp]:
|
|
302
|
+
return isinstance(node, ast.UnaryOp)
|
|
303
|
+
# TODO Does this work?
|
|
304
|
+
@staticmethod
|
|
305
|
+
def matchesAtLeast1Descendant(predicate: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
|
|
306
|
+
"""Create a predicate that returns True if any descendant of the node matches the given predicate."""
|
|
307
|
+
return lambda node: not ifThis.matchesNoDescendant(predicate)(node)
|
|
308
|
+
# TODO Does this work?
|
|
309
|
+
@staticmethod
|
|
310
|
+
def matchesMeAndMyDescendantsExactlyNTimes(predicate: Callable[[ast.AST], bool], nTimes: int) -> Callable[[ast.AST], bool]:
|
|
311
|
+
"""Create a predicate that returns True if exactly 'count' nodes in the tree match the predicate."""
|
|
312
|
+
def countMatchingNodes(node: ast.AST) -> bool:
|
|
313
|
+
matches = sum(1 for descendant in ast.walk(node) if predicate(descendant))
|
|
314
|
+
return matches == nTimes
|
|
315
|
+
return countMatchingNodes
|
|
316
|
+
@staticmethod
|
|
317
|
+
def matchesMeButNotAnyDescendant(predicate: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
|
|
318
|
+
"""Create a predicate that returns True if the node matches but none of its descendants match the predicate."""
|
|
319
|
+
return lambda node: predicate(node) and ifThis.matchesNoDescendant(predicate)(node)
|
|
320
|
+
@staticmethod
|
|
321
|
+
def matchesNoDescendant(predicate: Callable[[ast.AST], bool]) -> Callable[[ast.AST], bool]:
|
|
322
|
+
"""Create a predicate that returns True if no descendant of the node matches the given predicate."""
|
|
323
|
+
def checkNoMatchingDescendant(node: ast.AST) -> bool:
|
|
324
|
+
for descendant in ast.walk(node):
|
|
325
|
+
if descendant is not node and predicate(descendant):
|
|
326
|
+
return False
|
|
327
|
+
return True
|
|
328
|
+
return checkNoMatchingDescendant
|
|
329
|
+
@staticmethod
|
|
330
|
+
def onlyReturnAnyCompare(astFunctionDef: ast.AST) -> TypeGuard[ast.FunctionDef]:
|
|
331
|
+
return ifThis.isFunctionDef(astFunctionDef) and len(astFunctionDef.body) == 1 and ifThis.isReturnAnyCompare(astFunctionDef.body[0])
|
|
254
332
|
@staticmethod
|
|
255
|
-
def
|
|
256
|
-
return ifThis.
|
|
333
|
+
def onlyReturnUnaryOp(astFunctionDef: ast.AST) -> TypeGuard[ast.FunctionDef]:
|
|
334
|
+
return ifThis.isFunctionDef(astFunctionDef) and len(astFunctionDef.body) == 1 and ifThis.isReturnUnaryOp(astFunctionDef.body[0])
|
|
257
335
|
|
|
258
336
|
class Make:
|
|
259
337
|
@staticmethod
|
|
260
|
-
def
|
|
261
|
-
"""
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return dictionaryKeywords
|
|
267
|
-
|
|
338
|
+
def ast_arg(identifier: ast_Identifier, annotation: ast.expr | None = None, **keywordArguments: strORintORNone) -> ast.arg:
|
|
339
|
+
"""keywordArguments: type_comment:str|None, lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
|
|
340
|
+
return ast.arg(identifier, annotation, **keywordArguments)
|
|
341
|
+
@staticmethod
|
|
342
|
+
def ast_keyword(keywordArgument: ast_Identifier, value: ast.expr, **keywordArguments: int) -> ast.keyword:
|
|
343
|
+
return ast.keyword(arg=keywordArgument, value=value, **keywordArguments)
|
|
268
344
|
@staticmethod
|
|
269
345
|
def astAlias(name: ast_Identifier, asname: ast_Identifier | None = None) -> ast.alias:
|
|
270
|
-
return ast.alias(name
|
|
271
|
-
|
|
346
|
+
return ast.alias(name, asname)
|
|
272
347
|
@staticmethod
|
|
273
348
|
def astAnnAssign(target: ast.Name | ast.Attribute | ast.Subscript, annotation: ast.expr, value: ast.expr | None = None, **keywordArguments: int) -> ast.AnnAssign:
|
|
274
|
-
"""
|
|
349
|
+
"""`simple: int`: uses a clever int-from-boolean to assign the correct value to the `simple` attribute. So, don't add it as a parameter."""
|
|
275
350
|
return ast.AnnAssign(target, annotation, value, simple=int(isinstance(target, ast.Name)), **keywordArguments)
|
|
276
|
-
|
|
277
351
|
@staticmethod
|
|
278
352
|
def astAssign(listTargets: Any, value: ast.expr, **keywordArguments: strORintORNone) -> ast.Assign:
|
|
279
353
|
"""keywordArguments: type_comment:str|None, lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
|
|
280
354
|
return ast.Assign(targets=listTargets, value=value, **keywordArguments)
|
|
281
|
-
|
|
282
|
-
@staticmethod
|
|
283
|
-
def astArg(identifier: ast_Identifier, annotation: ast.expr | None = None, **keywordArguments: strORintORNone) -> ast.arg:
|
|
284
|
-
"""keywordArguments: type_comment:str|None, lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
|
|
285
|
-
return ast.arg(identifier, annotation, **keywordArguments)
|
|
286
|
-
|
|
287
355
|
@staticmethod
|
|
288
356
|
def astArgumentsSpecification(posonlyargs: list[ast.arg]=[], args: list[ast.arg]=[], vararg: ast.arg|None=None, kwonlyargs: list[ast.arg]=[], kw_defaults: list[ast.expr|None]=[None], kwarg: ast.arg|None=None, defaults: list[ast.expr]=[]) -> ast.arguments:
|
|
289
|
-
return ast.arguments(posonlyargs
|
|
290
|
-
|
|
357
|
+
return ast.arguments(posonlyargs, args, vararg, kwonlyargs, kw_defaults, kwarg, defaults)
|
|
291
358
|
@staticmethod
|
|
292
|
-
def
|
|
293
|
-
|
|
294
|
-
|
|
359
|
+
def astAttribute(value: ast.expr, attribute: ast_Identifier, context: ast.expr_context = ast.Load(), **keywordArguments: int) -> ast.Attribute:
|
|
360
|
+
"""
|
|
361
|
+
Parameters:
|
|
362
|
+
value: the part before the dot (hint `ast.Name` for nameDOTname)
|
|
363
|
+
attribute: the `str` after the dot
|
|
364
|
+
context (ast.Load()): Load/Store/Del"""
|
|
365
|
+
return ast.Attribute(value, attribute, context, **keywordArguments)
|
|
366
|
+
@staticmethod
|
|
367
|
+
def astCall(caller: ast.Name | ast.Attribute, listArguments: Sequence[ast.expr] | None = None, list_astKeywords: Sequence[ast.keyword] | None = None) -> ast.Call:
|
|
368
|
+
return ast.Call(func=caller, args=list(listArguments) if listArguments else [], keywords=list(list_astKeywords) if list_astKeywords else [])
|
|
295
369
|
@staticmethod
|
|
296
370
|
def astClassDef(name: ast_Identifier, listBases: list[ast.expr]=[], list_keyword: list[ast.keyword]=[], body: list[ast.stmt]=[], decorator_list: list[ast.expr]=[], **keywordArguments: list_ast_type_paramORintORNone) -> ast.ClassDef:
|
|
297
371
|
"""keywordArguments: type_params:list[ast.type_param], lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
|
|
298
372
|
return ast.ClassDef(name=name, bases=listBases, keywords=list_keyword, body=body, decorator_list=decorator_list, **keywordArguments)
|
|
299
|
-
|
|
373
|
+
@staticmethod
|
|
374
|
+
def astConstant(value: Any, **keywordArguments: strORintORNone) -> ast.Constant:
|
|
375
|
+
"""value: str|int|float|bool|None|bytes|bytearray|memoryview|complex|list|tuple|dict|set, or any other type that can be represented as a constant in Python.
|
|
376
|
+
keywordArguments: kind:str, lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
|
|
377
|
+
return ast.Constant(value, **keywordArguments)
|
|
300
378
|
@staticmethod
|
|
301
379
|
def astFunctionDef(name: ast_Identifier, argumentsSpecification: ast.arguments=ast.arguments(), body: list[ast.stmt]=[], decorator_list: list[ast.expr]=[], returns: ast.expr|None=None, **keywordArguments: strORlist_ast_type_paramORintORNone) -> ast.FunctionDef:
|
|
302
380
|
"""keywordArguments: type_comment:str|None, type_params:list[ast.type_param], lineno:int, col_offset:int, end_lineno:int|None, end_col_offset:int|None"""
|
|
303
381
|
return ast.FunctionDef(name=name, args=argumentsSpecification, body=body, decorator_list=decorator_list, returns=returns, **keywordArguments)
|
|
304
|
-
|
|
305
382
|
@staticmethod
|
|
306
383
|
def astImport(moduleName: ast_Identifier, asname: ast_Identifier | None = None, **keywordArguments: int) -> ast.Import:
|
|
307
384
|
return ast.Import(names=[Make.astAlias(moduleName, asname)], **keywordArguments)
|
|
308
|
-
|
|
309
385
|
@staticmethod
|
|
310
386
|
def astImportFrom(moduleName: ast_Identifier, list_astAlias: list[ast.alias], **keywordArguments: int) -> ast.ImportFrom:
|
|
311
387
|
return ast.ImportFrom(module=moduleName, names=list_astAlias, level=0, **keywordArguments)
|
|
312
|
-
|
|
313
|
-
@staticmethod
|
|
314
|
-
def astKeyword(keywordArgument: ast_Identifier, value: ast.expr, **keywordArguments: int) -> ast.keyword:
|
|
315
|
-
return ast.keyword(arg=keywordArgument, value=value, **keywordArguments)
|
|
316
|
-
|
|
317
388
|
@staticmethod
|
|
318
389
|
def astModule(body: list[ast.stmt], type_ignores: list[ast.TypeIgnore] = []) -> ast.Module:
|
|
319
390
|
return ast.Module(body, type_ignores)
|
|
320
|
-
|
|
321
391
|
@staticmethod
|
|
322
|
-
def astName(identifier: ast_Identifier) -> ast.Name:
|
|
323
|
-
return ast.Name(
|
|
324
|
-
|
|
392
|
+
def astName(identifier: ast_Identifier, context: ast.expr_context = ast.Load(), **keywordArguments: int) -> ast.Name:
|
|
393
|
+
return ast.Name(identifier, context, **keywordArguments)
|
|
325
394
|
@staticmethod
|
|
326
395
|
def itDOTname(nameChain: ast.Name | ast.Attribute, dotName: str) -> ast.Attribute:
|
|
327
396
|
return ast.Attribute(value=nameChain, attr=dotName, ctx=ast.Load())
|
|
328
|
-
|
|
329
397
|
@staticmethod
|
|
398
|
+
# TODO rewrite with all parameters
|
|
330
399
|
def nameDOTname(identifier: ast_Identifier, *dotName: str) -> ast.Name | ast.Attribute:
|
|
331
400
|
nameDOTname: ast.Name | ast.Attribute = Make.astName(identifier)
|
|
332
401
|
if not dotName:
|
|
@@ -334,81 +403,234 @@ class Make:
|
|
|
334
403
|
for suffix in dotName:
|
|
335
404
|
nameDOTname = Make.itDOTname(nameDOTname, suffix)
|
|
336
405
|
return nameDOTname
|
|
337
|
-
|
|
338
406
|
@staticmethod
|
|
339
407
|
def astReturn(value: ast.expr | None = None, **keywordArguments: int) -> ast.Return:
|
|
340
|
-
return ast.Return(value
|
|
341
|
-
|
|
408
|
+
return ast.Return(value, **keywordArguments)
|
|
409
|
+
@staticmethod
|
|
410
|
+
def astSubscript(value: ast.expr, slice: ast_expr_Slice, context: ast.expr_context = ast.Load(), **keywordArguments: int) -> ast.Subscript:
|
|
411
|
+
return ast.Subscript(value, slice, ctx=context, **keywordArguments)
|
|
342
412
|
@staticmethod
|
|
343
|
-
def astTuple(elements: Sequence[ast.expr], context: ast.expr_context
|
|
413
|
+
def astTuple(elements: Sequence[ast.expr], context: ast.expr_context = ast.Load(), **keywordArguments: int) -> ast.Tuple:
|
|
344
414
|
"""context: Load/Store/Del"""
|
|
345
|
-
context = context or ast.Load()
|
|
346
415
|
return ast.Tuple(elts=list(elements), ctx=context, **keywordArguments)
|
|
347
416
|
|
|
417
|
+
class LedgerOfImports:
|
|
418
|
+
# TODO When resolving the ledger of imports, remove self-referential imports
|
|
419
|
+
|
|
420
|
+
def __init__(self, startWith: ast.AST | None = None) -> None:
|
|
421
|
+
self.dictionaryImportFrom: dict[str, list[tuple[str, str | None]]] = defaultdict(list)
|
|
422
|
+
self.listImport: list[str] = []
|
|
423
|
+
|
|
424
|
+
if startWith:
|
|
425
|
+
self.walkThis(startWith)
|
|
426
|
+
|
|
427
|
+
def addAst(self, astImport_: ast.Import | ast.ImportFrom) -> None:
|
|
428
|
+
assert isinstance(astImport_, (ast.Import, ast.ImportFrom)), f"Expected ast.Import or ast.ImportFrom, got {type(astImport_)}"
|
|
429
|
+
if isinstance(astImport_, ast.Import):
|
|
430
|
+
for alias in astImport_.names:
|
|
431
|
+
self.listImport.append(alias.name)
|
|
432
|
+
else:
|
|
433
|
+
if astImport_.module is not None:
|
|
434
|
+
for alias in astImport_.names:
|
|
435
|
+
self.dictionaryImportFrom[astImport_.module].append((alias.name, alias.asname))
|
|
436
|
+
|
|
437
|
+
def addImportStr(self, module: str) -> None:
|
|
438
|
+
self.listImport.append(module)
|
|
439
|
+
|
|
440
|
+
def addImportFromStr(self, module: str, name: str, asname: str | None = None) -> None:
|
|
441
|
+
self.dictionaryImportFrom[module].append((name, asname))
|
|
442
|
+
|
|
443
|
+
def exportListModuleNames(self) -> list[str]:
|
|
444
|
+
listModuleNames: list[str] = list(self.dictionaryImportFrom.keys())
|
|
445
|
+
listModuleNames.extend(self.listImport)
|
|
446
|
+
return sorted(set(listModuleNames))
|
|
447
|
+
|
|
448
|
+
def makeListAst(self) -> list[ast.ImportFrom | ast.Import]:
|
|
449
|
+
listAstImportFrom: list[ast.ImportFrom] = []
|
|
450
|
+
|
|
451
|
+
for module, listOfNameTuples in sorted(self.dictionaryImportFrom.items()):
|
|
452
|
+
listOfNameTuples = sorted(list(set(listOfNameTuples)), key=lambda nameTuple: nameTuple[0])
|
|
453
|
+
listAlias: list[ast.alias] = []
|
|
454
|
+
for name, asname in listOfNameTuples:
|
|
455
|
+
listAlias.append(Make.astAlias(name, asname))
|
|
456
|
+
listAstImportFrom.append(Make.astImportFrom(module, listAlias))
|
|
457
|
+
|
|
458
|
+
listAstImport: list[ast.Import] = [Make.astImport(name) for name in sorted(set(self.listImport))]
|
|
459
|
+
return listAstImportFrom + listAstImport
|
|
460
|
+
|
|
461
|
+
def update(self, *fromLedger: 'LedgerOfImports') -> None:
|
|
462
|
+
"""Update this ledger with imports from one or more other ledgers.
|
|
463
|
+
Parameters:
|
|
464
|
+
*fromLedger: One or more other `LedgerOfImports` objects from which to merge.
|
|
465
|
+
"""
|
|
466
|
+
self.dictionaryImportFrom = updateExtendPolishDictionaryLists(self.dictionaryImportFrom, *(ledger.dictionaryImportFrom for ledger in fromLedger), destroyDuplicates=True, reorderLists=True)
|
|
467
|
+
|
|
468
|
+
for ledger in fromLedger:
|
|
469
|
+
self.listImport.extend(ledger.listImport)
|
|
470
|
+
|
|
471
|
+
def walkThis(self, walkThis: ast.AST) -> None:
|
|
472
|
+
for smurf in ast.walk(walkThis):
|
|
473
|
+
if isinstance(smurf, (ast.Import, ast.ImportFrom)):
|
|
474
|
+
self.addAst(smurf)
|
|
475
|
+
|
|
348
476
|
class Then:
|
|
349
477
|
@staticmethod
|
|
350
|
-
def
|
|
351
|
-
return lambda
|
|
478
|
+
def append_targetTo(listName: list[ast.AST]) -> Callable[[ast.AnnAssign], None]:
|
|
479
|
+
return lambda node: listName.append(node.target)
|
|
352
480
|
@staticmethod
|
|
353
|
-
def
|
|
354
|
-
return lambda
|
|
481
|
+
def appendTo(listOfAny: list[Any]) -> Callable[[ast.AST], None]:
|
|
482
|
+
return lambda node: listOfAny.append(node)
|
|
355
483
|
@staticmethod
|
|
356
|
-
def
|
|
357
|
-
return lambda
|
|
484
|
+
def insertThisAbove(list_astAST: Sequence[ast.AST]) -> Callable[[ast.AST], Sequence[ast.AST]]:
|
|
485
|
+
return lambda aboveMe: [*list_astAST, aboveMe]
|
|
358
486
|
@staticmethod
|
|
359
|
-
def
|
|
487
|
+
def insertThisBelow(list_astAST: Sequence[ast.AST]) -> Callable[[ast.AST], Sequence[ast.AST]]:
|
|
488
|
+
return lambda belowMe: [belowMe, *list_astAST]
|
|
489
|
+
@staticmethod
|
|
490
|
+
def removeThis(_node: ast.AST) -> None:
|
|
360
491
|
return None
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
class
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
492
|
+
@staticmethod
|
|
493
|
+
def replaceWith(astAST: ast.AST) -> Callable[[ast.AST], ast.AST]:
|
|
494
|
+
return lambda _replaceMe: astAST
|
|
495
|
+
@staticmethod
|
|
496
|
+
def updateThis(dictionaryOf_astMosDef: dict[ast_Identifier, astMosDef]) -> Callable[[astMosDef], astMosDef]:
|
|
497
|
+
return lambda node: dictionaryOf_astMosDef.setdefault(node.name, node)
|
|
498
|
+
@staticmethod
|
|
499
|
+
def Z0Z_ledger(logicalPath: strDotStrCuzPyStoopid, ledger: LedgerOfImports) -> Callable[[ast.AnnAssign], None]:
|
|
500
|
+
return lambda node: ledger.addImportFromStr(logicalPath, node.annotation.id) # type: ignore
|
|
501
|
+
@staticmethod
|
|
502
|
+
def Z0Z_appendKeywordMirroredTo(list_keyword: list[ast.keyword]) -> Callable[[ast.AnnAssign], None]:
|
|
503
|
+
return lambda node: list_keyword.append(Make.ast_keyword(node.target.id, node.target)) # type: ignore
|
|
504
|
+
@staticmethod
|
|
505
|
+
def Z0Z_appendAnnAssignOf_nameDOTnameTo(identifier: ast_Identifier, list_nameDOTname: list[ast.AnnAssign]) -> Callable[[ast.AnnAssign], None]:
|
|
506
|
+
return lambda node: list_nameDOTname.append(Make.astAnnAssign(node.target, node.annotation, Make.nameDOTname(identifier, node.target.id))) # type: ignore
|
|
507
|
+
|
|
508
|
+
@dataclasses.dataclass
|
|
509
|
+
class IngredientsFunction:
|
|
510
|
+
"""Everything necessary to integrate a function into a module should be here."""
|
|
511
|
+
astFunctionDef: ast.FunctionDef # hint `Make.astFunctionDef`
|
|
512
|
+
imports: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
|
|
513
|
+
|
|
514
|
+
@dataclasses.dataclass
|
|
515
|
+
class IngredientsModule:
|
|
516
|
+
"""Everything necessary to create one _logical_ `ast.Module` should be here.
|
|
517
|
+
Extrinsic qualities should _probably_ be handled externally."""
|
|
518
|
+
ingredientsFunction: dataclasses.InitVar[Sequence[IngredientsFunction] | IngredientsFunction | None] = None
|
|
519
|
+
|
|
520
|
+
# init var with an existing module? method to deconstruct an existing module?
|
|
521
|
+
|
|
522
|
+
# `body` attribute of `ast.Module`
|
|
523
|
+
imports: LedgerOfImports = dataclasses.field(default_factory=LedgerOfImports)
|
|
524
|
+
prologue: list[ast.stmt] = dataclasses.field(default_factory=list)
|
|
525
|
+
functions: list[ast.FunctionDef | ast.stmt] = dataclasses.field(default_factory=list)
|
|
526
|
+
epilogue: list[ast.stmt] = dataclasses.field(default_factory=list)
|
|
527
|
+
launcher: list[ast.stmt] = dataclasses.field(default_factory=list)
|
|
528
|
+
|
|
529
|
+
# parameter for `ast.Module` constructor
|
|
530
|
+
type_ignores: list[ast.TypeIgnore] = dataclasses.field(default_factory=list)
|
|
531
|
+
|
|
532
|
+
def __post_init__(self, ingredientsFunction: Sequence[IngredientsFunction] | IngredientsFunction | None = None) -> None:
|
|
533
|
+
if ingredientsFunction is not None:
|
|
534
|
+
if isinstance(ingredientsFunction, IngredientsFunction):
|
|
535
|
+
self.addIngredientsFunction(ingredientsFunction)
|
|
536
|
+
else:
|
|
537
|
+
self.addIngredientsFunction(*ingredientsFunction)
|
|
538
|
+
|
|
539
|
+
def addIngredientsFunction(self, *ingredientsFunction: IngredientsFunction) -> None:
|
|
540
|
+
"""Add one or more `IngredientsFunction`."""
|
|
541
|
+
listLedgers: list[LedgerOfImports] = []
|
|
542
|
+
for definition in ingredientsFunction:
|
|
543
|
+
self.functions.append(definition.astFunctionDef)
|
|
544
|
+
listLedgers.append(definition.imports)
|
|
545
|
+
self.imports.update(*listLedgers)
|
|
546
|
+
|
|
547
|
+
def _makeModuleBody(self) -> list[ast.stmt]:
|
|
548
|
+
body: list[ast.stmt] = []
|
|
549
|
+
body.extend(self.imports.makeListAst())
|
|
550
|
+
body.extend(self.prologue)
|
|
551
|
+
body.extend(self.functions)
|
|
552
|
+
body.extend(self.epilogue)
|
|
553
|
+
body.extend(self.launcher)
|
|
554
|
+
# TODO `launcher`, if it exists, must start with `if __name__ == '__main__':` and be indented
|
|
555
|
+
return body
|
|
556
|
+
|
|
557
|
+
def export(self) -> ast.Module:
|
|
558
|
+
"""Create a new `ast.Module` from the ingredients."""
|
|
559
|
+
return Make.astModule(self._makeModuleBody(), self.type_ignores)
|
|
560
|
+
|
|
561
|
+
@dataclasses.dataclass
|
|
562
|
+
class RecipeSynthesizeFlow:
|
|
563
|
+
"""Settings for synthesizing flow."""
|
|
564
|
+
# ========================================
|
|
565
|
+
# Source
|
|
566
|
+
sourceAlgorithm: ModuleType = getSourceAlgorithm()
|
|
567
|
+
sourcePython: str = inspect_getsource(sourceAlgorithm)
|
|
568
|
+
source_astModule: ast.Module = ast.parse(sourcePython)
|
|
569
|
+
|
|
570
|
+
# Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
|
|
571
|
+
sourceDispatcherCallable: str = theDispatcherCallable
|
|
572
|
+
sourceInitializeCallable: str = theSourceInitializeCallable
|
|
573
|
+
sourceParallelCallable: str = theSourceParallelCallable
|
|
574
|
+
sourceSequentialCallable: str = theSourceSequentialCallable
|
|
575
|
+
|
|
576
|
+
sourceDataclassIdentifier: str = theDataclassIdentifier
|
|
577
|
+
sourceDataclassInstance: str = theDataclassInstance
|
|
578
|
+
sourceDataclassInstanceTaskDistribution: str = theDataclassInstanceTaskDistribution
|
|
579
|
+
sourcePathModuleDataclass: str = theLogicalPathModuleDataclass
|
|
580
|
+
|
|
581
|
+
# ========================================
|
|
582
|
+
# Filesystem
|
|
583
|
+
pathPackage: PurePosixPath | None = PurePosixPath(thePathPackage)
|
|
584
|
+
fileExtension: str = theFileExtension
|
|
585
|
+
|
|
586
|
+
# ========================================
|
|
587
|
+
# Logical identifiers
|
|
588
|
+
# meta
|
|
589
|
+
formatStrModuleSynthetic: str = theFormatStrModuleSynthetic
|
|
590
|
+
formatStrModuleForCallableSynthetic: str = theFormatStrModuleForCallableSynthetic
|
|
591
|
+
|
|
592
|
+
# Package
|
|
593
|
+
packageName: ast_Identifier | None = thePackageName
|
|
594
|
+
|
|
595
|
+
# Module
|
|
596
|
+
# Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
|
|
597
|
+
Z0Z_flowLogicalPathRoot: str = theModuleOfSyntheticModules
|
|
598
|
+
moduleDispatcher: str = theModuleDispatcherSynthetic
|
|
599
|
+
logicalPathModuleDataclass: str = sourcePathModuleDataclass
|
|
600
|
+
# Figure out dynamic flow control to synthesized modules https://github.com/hunterhogan/mapFolding/issues/4
|
|
601
|
+
# `theLogicalPathModuleDispatcherSynthetic` is a problem. It is defined in theSSOT, but it can also be calculated.
|
|
602
|
+
logicalPathModuleDispatcher: str = theLogicalPathModuleDispatcherSynthetic
|
|
603
|
+
|
|
604
|
+
# Function
|
|
605
|
+
dispatcherCallable: str = sourceDispatcherCallable
|
|
606
|
+
initializeCallable: str = sourceInitializeCallable
|
|
607
|
+
parallelCallable: str = sourceParallelCallable
|
|
608
|
+
sequentialCallable: str = sourceSequentialCallable
|
|
609
|
+
|
|
610
|
+
dataclassIdentifier: str = sourceDataclassIdentifier
|
|
611
|
+
|
|
612
|
+
# Variable
|
|
613
|
+
dataclassInstance: str = sourceDataclassInstance
|
|
614
|
+
|
|
615
|
+
def _makePathFilename(self, filenameStem: str,
|
|
616
|
+
pathRoot: PurePosixPath | None = None,
|
|
617
|
+
logicalPathINFIX: strDotStrCuzPyStoopid | None = None,
|
|
618
|
+
fileExtension: str | None = None,
|
|
619
|
+
) -> PurePosixPath:
|
|
620
|
+
"""filenameStem: (hint: the name of the logical module)"""
|
|
621
|
+
if pathRoot is None:
|
|
622
|
+
pathRoot = self.pathPackage or PurePosixPath(Path.cwd())
|
|
623
|
+
if logicalPathINFIX:
|
|
624
|
+
whyIsThisStillAThing: list[str] = logicalPathINFIX.split('.')
|
|
625
|
+
pathRoot = pathRoot.joinpath(*whyIsThisStillAThing)
|
|
626
|
+
if fileExtension is None:
|
|
627
|
+
fileExtension = self.fileExtension
|
|
628
|
+
filename: str = filenameStem + fileExtension
|
|
629
|
+
return pathRoot.joinpath(filename)
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def pathFilenameDispatcher(self) -> PurePosixPath:
|
|
633
|
+
return self._makePathFilename(filenameStem=self.moduleDispatcher, logicalPathINFIX=self.Z0Z_flowLogicalPathRoot)
|
|
412
634
|
|
|
413
635
|
def extractClassDef(identifier: ast_Identifier, module: ast.Module) -> ast.ClassDef | None:
|
|
414
636
|
sherpa: list[ast.ClassDef] = []
|
|
@@ -423,3 +645,136 @@ def extractFunctionDef(identifier: ast_Identifier, module: ast.Module) -> ast.Fu
|
|
|
423
645
|
extractor.visit(module)
|
|
424
646
|
astClassDef = sherpa[0] if sherpa else None
|
|
425
647
|
return astClassDef
|
|
648
|
+
|
|
649
|
+
def makeDictionaryFunctionDef(module: ast.Module) -> dict[ast_Identifier, ast.FunctionDef]:
|
|
650
|
+
dictionaryFunctionDef: dict[ast_Identifier, ast.FunctionDef] = {}
|
|
651
|
+
NodeCollector(ifThis.isFunctionDef, [Then.updateThis(dictionaryFunctionDef)]).visit(module)
|
|
652
|
+
return dictionaryFunctionDef
|
|
653
|
+
|
|
654
|
+
def makeDictionaryReplacementStatements(module: ast.Module) -> dict[ast_Identifier, ast.stmt | list[ast.stmt]]:
|
|
655
|
+
"""Return a dictionary of function names and their replacement statements."""
|
|
656
|
+
dictionaryFunctionDef: dict[ast_Identifier, ast.FunctionDef] = makeDictionaryFunctionDef(module)
|
|
657
|
+
dictionaryReplacementStatements: dict[ast_Identifier, ast.stmt | list[ast.stmt]] = {}
|
|
658
|
+
for name, astFunctionDef in dictionaryFunctionDef.items():
|
|
659
|
+
if ifThis.onlyReturnAnyCompare(astFunctionDef):
|
|
660
|
+
dictionaryReplacementStatements[name] = astFunctionDef.body[0].value # type: ignore
|
|
661
|
+
elif ifThis.onlyReturnUnaryOp(astFunctionDef):
|
|
662
|
+
dictionaryReplacementStatements[name] = astFunctionDef.body[0].value # type: ignore
|
|
663
|
+
else:
|
|
664
|
+
dictionaryReplacementStatements[name] = astFunctionDef.body[0:-1]
|
|
665
|
+
return dictionaryReplacementStatements
|
|
666
|
+
|
|
667
|
+
def Z0Z_descendantContainsMatchingNode(node: ast.AST, predicateFunction: Callable[[ast.AST], bool]) -> bool:
|
|
668
|
+
"""Return True if any descendant of the node (or the node itself) matches the predicateFunction."""
|
|
669
|
+
matchFound = False
|
|
670
|
+
|
|
671
|
+
class DescendantFinder(ast.NodeVisitor):
|
|
672
|
+
def generic_visit(self, node: ast.AST) -> None:
|
|
673
|
+
nonlocal matchFound
|
|
674
|
+
if predicateFunction(node):
|
|
675
|
+
matchFound = True
|
|
676
|
+
else:
|
|
677
|
+
super().generic_visit(node)
|
|
678
|
+
|
|
679
|
+
DescendantFinder().visit(node)
|
|
680
|
+
return matchFound
|
|
681
|
+
|
|
682
|
+
def Z0Z_executeActionUnlessDescendantMatches(exclusionPredicate: Callable[[ast.AST], bool], actionFunction: Callable[[ast.AST], None]) -> Callable[[ast.AST], None]:
|
|
683
|
+
"""Return a new action that will execute actionFunction only if no descendant (or the node itself) matches exclusionPredicate."""
|
|
684
|
+
def wrappedAction(node: ast.AST) -> None:
|
|
685
|
+
if not Z0Z_descendantContainsMatchingNode(node, exclusionPredicate):
|
|
686
|
+
actionFunction(node)
|
|
687
|
+
return wrappedAction
|
|
688
|
+
|
|
689
|
+
def inlineThisFunctionWithTheseValues(astFunctionDef: ast.FunctionDef, dictionaryReplacementStatements: dict[str, ast.stmt | list[ast.stmt]]) -> ast.FunctionDef:
|
|
690
|
+
class FunctionInliner(ast.NodeTransformer):
|
|
691
|
+
def __init__(self, dictionaryReplacementStatements: dict[str, ast.stmt | list[ast.stmt]]) -> None:
|
|
692
|
+
self.dictionaryReplacementStatements = dictionaryReplacementStatements
|
|
693
|
+
|
|
694
|
+
def generic_visit(self, node: ast.AST) -> ast.AST:
|
|
695
|
+
"""Visit all nodes and replace them if necessary."""
|
|
696
|
+
return super().generic_visit(node)
|
|
697
|
+
|
|
698
|
+
def visit_Expr(self, node: ast.Expr) -> ast.AST | list[ast.stmt]:
|
|
699
|
+
if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryReplacementStatements)(node.value):
|
|
700
|
+
return self.dictionaryReplacementStatements[node.value.func.id] # type: ignore[attr-defined]
|
|
701
|
+
return node
|
|
702
|
+
|
|
703
|
+
def visit_Assign(self, node: ast.Assign) -> ast.AST | list[ast.stmt]:
|
|
704
|
+
if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryReplacementStatements)(node.value):
|
|
705
|
+
return self.dictionaryReplacementStatements[node.value.func.id] # type: ignore[attr-defined]
|
|
706
|
+
return node
|
|
707
|
+
|
|
708
|
+
def visit_Call(self, node: ast.Call) -> ast.AST | list[ast.stmt]:
|
|
709
|
+
if ifThis.CallDoesNotCallItselfAndNameDOTidIsIn(self.dictionaryReplacementStatements)(node):
|
|
710
|
+
replacement = self.dictionaryReplacementStatements[node.func.id] # type: ignore[attr-defined]
|
|
711
|
+
if not isinstance(replacement, list):
|
|
712
|
+
return replacement
|
|
713
|
+
return node
|
|
714
|
+
|
|
715
|
+
keepGoing = True
|
|
716
|
+
ImaInlineFunction = deepcopy(astFunctionDef)
|
|
717
|
+
while keepGoing:
|
|
718
|
+
ImaInlineFunction = deepcopy(astFunctionDef)
|
|
719
|
+
FunctionInliner(deepcopy(dictionaryReplacementStatements)).visit(ImaInlineFunction)
|
|
720
|
+
if ast.unparse(ImaInlineFunction) == ast.unparse(astFunctionDef):
|
|
721
|
+
keepGoing = False
|
|
722
|
+
else:
|
|
723
|
+
astFunctionDef = deepcopy(ImaInlineFunction)
|
|
724
|
+
ast.fix_missing_locations(astFunctionDef)
|
|
725
|
+
return ImaInlineFunction
|
|
726
|
+
|
|
727
|
+
def Z0Z_replaceMatchingASTnodes(astTree: ast.AST, mappingFindReplaceNodes: dict[ast.AST, ast.AST]) -> ast.AST:
|
|
728
|
+
class TargetedNodeReplacer(ast.NodeTransformer):
|
|
729
|
+
def __init__(self, mappingFindReplaceNodes: dict[ast.AST, ast.AST]) -> None:
|
|
730
|
+
self.mappingFindReplaceNodes = mappingFindReplaceNodes
|
|
731
|
+
|
|
732
|
+
def visit(self, node: ast.AST) -> ast.AST:
|
|
733
|
+
for nodeFind, nodeReplace in self.mappingFindReplaceNodes.items():
|
|
734
|
+
if self.nodesMatchStructurally(node, nodeFind):
|
|
735
|
+
return nodeReplace
|
|
736
|
+
return self.generic_visit(node)
|
|
737
|
+
|
|
738
|
+
def nodesMatchStructurally(self, nodeSubject: ast.AST | list[Any] | Any, nodePattern: ast.AST | list[Any] | Any) -> bool:
|
|
739
|
+
if nodeSubject is None or nodePattern is None:
|
|
740
|
+
return nodeSubject is None and nodePattern is None
|
|
741
|
+
|
|
742
|
+
if type(nodeSubject) != type(nodePattern):
|
|
743
|
+
return False
|
|
744
|
+
|
|
745
|
+
if isinstance(nodeSubject, ast.AST):
|
|
746
|
+
for field, fieldValueSubject in ast.iter_fields(nodeSubject):
|
|
747
|
+
if field in ('lineno', 'col_offset', 'end_lineno', 'end_col_offset', 'ctx'):
|
|
748
|
+
continue
|
|
749
|
+
attrPattern = getattr(nodePattern, field, None)
|
|
750
|
+
if not self.nodesMatchStructurally(fieldValueSubject, attrPattern):
|
|
751
|
+
return False
|
|
752
|
+
return True
|
|
753
|
+
|
|
754
|
+
if isinstance(nodeSubject, list) and isinstance(nodePattern, list):
|
|
755
|
+
nodeSubjectList: list[Any] = nodeSubject
|
|
756
|
+
nodePatternList: list[Any] = nodePattern
|
|
757
|
+
return len(nodeSubjectList) == len(nodePatternList) and all(
|
|
758
|
+
self.nodesMatchStructurally(elementSubject, elementPattern)
|
|
759
|
+
for elementSubject, elementPattern in zip(nodeSubjectList, nodePatternList)
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
return nodeSubject == nodePattern
|
|
763
|
+
|
|
764
|
+
astTreeCurrent, astTreePrevious = None, astTree
|
|
765
|
+
while astTreeCurrent is None or ast.unparse(astTreeCurrent) != ast.unparse(astTreePrevious):
|
|
766
|
+
astTreePrevious = astTreeCurrent if astTreeCurrent else astTree
|
|
767
|
+
astTreeCurrent = TargetedNodeReplacer(mappingFindReplaceNodes).visit(astTreePrevious)
|
|
768
|
+
|
|
769
|
+
return astTreeCurrent
|
|
770
|
+
|
|
771
|
+
def write_astModule(ingredients: IngredientsModule, pathFilename: str | PathLike[Any] | PurePath, packageName: ast_Identifier | None = None) -> None:
|
|
772
|
+
astModule = ingredients.export()
|
|
773
|
+
ast.fix_missing_locations(astModule)
|
|
774
|
+
pythonSource: str = ast.unparse(astModule)
|
|
775
|
+
if not pythonSource: raise raiseIfNoneGitHubIssueNumber3
|
|
776
|
+
autoflake_additional_imports: list[str] = ingredients.imports.exportListModuleNames()
|
|
777
|
+
if packageName:
|
|
778
|
+
autoflake_additional_imports.append(packageName)
|
|
779
|
+
pythonSource = autoflake_fix_code(pythonSource, autoflake_additional_imports, expand_star_imports=False, remove_all_unused_imports=False, remove_duplicate_keys = False, remove_unused_variables = False)
|
|
780
|
+
writeStringToHere(pythonSource, pathFilename)
|