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