pythagoras 0.24.3__py3-none-any.whl → 0.24.6__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.
- pythagoras/_020_ordinary_code_portals/code_normalizer.py +37 -9
- pythagoras/_020_ordinary_code_portals/function_processing.py +58 -15
- pythagoras/_020_ordinary_code_portals/ordinary_decorator.py +14 -0
- pythagoras/_020_ordinary_code_portals/ordinary_portal_core_classes.py +196 -24
- pythagoras/_030_data_portals/data_portal_core_classes.py +74 -22
- pythagoras/_030_data_portals/ready_and_get.py +45 -4
- pythagoras/_030_data_portals/storable_decorator.py +18 -1
- pythagoras/_040_logging_code_portals/exception_processing_tracking.py +30 -2
- pythagoras/_040_logging_code_portals/execution_environment_summary.py +60 -24
- pythagoras/_040_logging_code_portals/kw_args.py +74 -12
- pythagoras/_040_logging_code_portals/logging_decorator.py +23 -1
- pythagoras/_040_logging_code_portals/logging_portal_core_classes.py +365 -12
- pythagoras/_040_logging_code_portals/notebook_checker.py +9 -1
- pythagoras/_040_logging_code_portals/uncaught_exceptions.py +40 -0
- pythagoras/_050_safe_code_portals/safe_decorator.py +27 -1
- pythagoras/_050_safe_code_portals/safe_portal_core_classes.py +87 -11
- pythagoras/_060_autonomous_code_portals/autonomous_decorators.py +31 -4
- pythagoras/_060_autonomous_code_portals/autonomous_portal_core_classes.py +94 -14
- pythagoras/_060_autonomous_code_portals/names_usage_analyzer.py +133 -4
- pythagoras/_070_protected_code_portals/list_flattener.py +45 -7
- pythagoras/_070_protected_code_portals/package_manager.py +99 -24
- pythagoras/_070_protected_code_portals/protected_portal_core_classes.py +70 -0
- pythagoras/_070_protected_code_portals/system_utils.py +85 -12
- pythagoras/_070_protected_code_portals/validation_succesful_const.py +12 -7
- pythagoras/_090_swarming_portals/swarming_portals.py +4 -6
- pythagoras/_800_signatures_and_converters/base_16_32_convertors.py +55 -20
- pythagoras/_800_signatures_and_converters/current_date_gmt_str.py +20 -5
- pythagoras/_800_signatures_and_converters/hash_signatures.py +46 -10
- pythagoras/_800_signatures_and_converters/node_signature.py +27 -12
- pythagoras/_800_signatures_and_converters/random_signatures.py +14 -3
- {pythagoras-0.24.3.dist-info → pythagoras-0.24.6.dist-info}/METADATA +1 -1
- {pythagoras-0.24.3.dist-info → pythagoras-0.24.6.dist-info}/RECORD +33 -33
- {pythagoras-0.24.3.dist-info → pythagoras-0.24.6.dist-info}/WHEEL +0 -0
|
@@ -4,7 +4,23 @@ from typing import Callable, Union
|
|
|
4
4
|
from .._020_ordinary_code_portals import get_normalized_function_source
|
|
5
5
|
|
|
6
6
|
class NamesUsedInFunction:
|
|
7
|
+
"""Container for name usage sets discovered in a function.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
function: Name of the top-level function being analyzed.
|
|
11
|
+
explicitly_global_unbound_deep: Names explicitly marked as global in the
|
|
12
|
+
function or its nested functions, which are not locally bound.
|
|
13
|
+
explicitly_nonlocal_unbound_deep: Names explicitly marked as nonlocal in
|
|
14
|
+
the function or its nested functions, which are not locally bound.
|
|
15
|
+
local: Names bound locally in the top-level function (including args).
|
|
16
|
+
imported: Names explicitly imported within the function body.
|
|
17
|
+
unclassified_deep: Names used in the function and/or nested functions
|
|
18
|
+
that are neither imported nor explicitly marked global/nonlocal.
|
|
19
|
+
accessible: All names currently considered accessible within function
|
|
20
|
+
scope during analysis; a union built as nodes are visited.
|
|
21
|
+
"""
|
|
7
22
|
def __init__(self):
|
|
23
|
+
"""Initialize all name sets to empty defaults."""
|
|
8
24
|
self.function = None # name of the function
|
|
9
25
|
self.explicitly_global_unbound_deep = set() # names, explicitly marked as global inside the function and/or called subfunctions, yet not bound to any object
|
|
10
26
|
self.explicitly_nonlocal_unbound_deep = set() # names, explicitly marked as nonlocal inside the function and/or called subfunctions, yet not bound to any object
|
|
@@ -21,12 +37,23 @@ class NamesUsageAnalyzer(ast.NodeVisitor):
|
|
|
21
37
|
"""
|
|
22
38
|
# TODO: add support for structural pattern matching
|
|
23
39
|
def __init__(self):
|
|
40
|
+
"""Initialize the analyzer state and counters."""
|
|
24
41
|
self.names = NamesUsedInFunction()
|
|
25
42
|
self.imported_packages_deep = set()
|
|
26
43
|
self.func_nesting_level = 0
|
|
27
44
|
self.n_yelds = 0
|
|
28
45
|
|
|
29
46
|
def visit_FunctionDef(self, node):
|
|
47
|
+
"""Handle a function definition.
|
|
48
|
+
|
|
49
|
+
- For the top-level function: record its name, parameters as locals,
|
|
50
|
+
and traverse its body.
|
|
51
|
+
- For nested functions: analyze them with a fresh analyzer and merge
|
|
52
|
+
relevant sets into the current analyzer, adjusting for accessibility.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
node: The ast.FunctionDef node.
|
|
56
|
+
"""
|
|
30
57
|
if self.func_nesting_level == 0:
|
|
31
58
|
self.names.function = node.name
|
|
32
59
|
self.func_nesting_level += 1
|
|
@@ -54,6 +81,15 @@ class NamesUsageAnalyzer(ast.NodeVisitor):
|
|
|
54
81
|
# self.n_yelds is not changing
|
|
55
82
|
|
|
56
83
|
def visit_Name(self, node):
|
|
84
|
+
"""Track variable usage and binding for a Name node.
|
|
85
|
+
|
|
86
|
+
- On load: if the name is not accessible, mark it unclassified and
|
|
87
|
+
accessible.
|
|
88
|
+
- On store: register it as a local and accessible.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
node: The ast.Name node.
|
|
92
|
+
"""
|
|
57
93
|
if isinstance(node.ctx, ast.Load):
|
|
58
94
|
if node.id not in self.names.accessible:
|
|
59
95
|
self.names.unclassified_deep |= {node.id}
|
|
@@ -65,23 +101,58 @@ class NamesUsageAnalyzer(ast.NodeVisitor):
|
|
|
65
101
|
self.generic_visit(node)
|
|
66
102
|
|
|
67
103
|
def visit_Attribute(self, node):
|
|
104
|
+
"""Visit an attribute access expression.
|
|
105
|
+
|
|
106
|
+
Currently no special handling is required; traversal continues.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
node: The ast.Attribute node.
|
|
110
|
+
"""
|
|
68
111
|
self.generic_visit(node)
|
|
69
112
|
|
|
70
113
|
def visit_Yield(self, node):
|
|
114
|
+
"""Record usage of a yield expression.
|
|
115
|
+
|
|
116
|
+
Increments the number of yields found, which disqualifies autonomy.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
node: The ast.Yield node.
|
|
120
|
+
"""
|
|
71
121
|
self.n_yelds += 1
|
|
72
122
|
self.generic_visit(node)
|
|
73
123
|
|
|
74
124
|
def visit_YieldFrom(self, node):
|
|
125
|
+
"""Record usage of a 'yield from' expression.
|
|
126
|
+
|
|
127
|
+
Increments the number of yields found, which disqualifies autonomy.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
node: The ast.YieldFrom node.
|
|
131
|
+
"""
|
|
75
132
|
self.n_yelds += 1
|
|
76
133
|
self.generic_visit(node)
|
|
77
134
|
|
|
78
135
|
def visit_Try(self, node):
|
|
136
|
+
"""Track names bound in exception handlers within try/except.
|
|
137
|
+
|
|
138
|
+
Exception handler names become local and accessible.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
node: The ast.Try node.
|
|
142
|
+
"""
|
|
79
143
|
for handler in node.handlers:
|
|
80
144
|
self.names.local |= {handler.name}
|
|
81
145
|
self.names.accessible |= {handler.name}
|
|
82
146
|
self.generic_visit(node)
|
|
83
147
|
|
|
84
148
|
def visit_comprehension(self, node):
|
|
149
|
+
"""Handle variable binding within a comprehension clause.
|
|
150
|
+
|
|
151
|
+
Targets in comprehension generators become local and accessible.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
node: The ast.comprehension node or a loop node with a similar API.
|
|
155
|
+
"""
|
|
85
156
|
if isinstance(node.target, (ast.Tuple, ast.List)):
|
|
86
157
|
all_targets =node.target.elts
|
|
87
158
|
else:
|
|
@@ -94,29 +165,59 @@ class NamesUsageAnalyzer(ast.NodeVisitor):
|
|
|
94
165
|
self.generic_visit(node)
|
|
95
166
|
|
|
96
167
|
def visit_For(self, node):
|
|
168
|
+
"""Handle a for-loop comprehension-like binding.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
node: The ast.For node.
|
|
172
|
+
"""
|
|
97
173
|
self.visit_comprehension(node)
|
|
98
174
|
|
|
99
175
|
def visit_ListComp(self, node):
|
|
176
|
+
"""Handle bindings within a list comprehension.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
node: The ast.ListComp node.
|
|
180
|
+
"""
|
|
100
181
|
for gen in node.generators:
|
|
101
182
|
self.visit_comprehension(gen)
|
|
102
183
|
self.generic_visit(node)
|
|
103
184
|
|
|
104
185
|
def visit_SetComp(self, node):
|
|
186
|
+
"""Handle bindings within a set comprehension.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
node: The ast.SetComp node.
|
|
190
|
+
"""
|
|
105
191
|
for gen in node.generators:
|
|
106
192
|
self.visit_comprehension(gen)
|
|
107
193
|
self.generic_visit(node)
|
|
108
194
|
|
|
109
195
|
def visit_DictComp(self, node):
|
|
196
|
+
"""Handle bindings within a dict comprehension.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
node: The ast.DictComp node.
|
|
200
|
+
"""
|
|
110
201
|
for gen in node.generators:
|
|
111
202
|
self.visit_comprehension(gen)
|
|
112
203
|
self.generic_visit(node)
|
|
113
204
|
|
|
114
205
|
def visit_GeneratorExp(self, node):
|
|
206
|
+
"""Handle bindings within a generator expression.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
node: The ast.GeneratorExp node.
|
|
210
|
+
"""
|
|
115
211
|
for gen in node.generators:
|
|
116
212
|
self.visit_comprehension(gen)
|
|
117
213
|
self.generic_visit(node)
|
|
118
214
|
|
|
119
215
|
def visit_Import(self, node):
|
|
216
|
+
"""Register imported names and top-level package usage.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
node: The ast.Import node.
|
|
220
|
+
"""
|
|
120
221
|
for alias in node.names:
|
|
121
222
|
name = alias.asname if alias.asname else alias.name
|
|
122
223
|
self.names.imported |= {name}
|
|
@@ -125,6 +226,11 @@ class NamesUsageAnalyzer(ast.NodeVisitor):
|
|
|
125
226
|
self.generic_visit(node)
|
|
126
227
|
|
|
127
228
|
def visit_ImportFrom(self, node):
|
|
229
|
+
"""Register names imported from a module and the module itself.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
node: The ast.ImportFrom node.
|
|
233
|
+
"""
|
|
128
234
|
self.imported_packages_deep |= {node.module.split('.')[-1]}
|
|
129
235
|
for alias in node.names:
|
|
130
236
|
name = alias.asname if alias.asname else alias.name
|
|
@@ -133,12 +239,22 @@ class NamesUsageAnalyzer(ast.NodeVisitor):
|
|
|
133
239
|
self.generic_visit(node)
|
|
134
240
|
|
|
135
241
|
def visit_Nonlocal(self, node):
|
|
242
|
+
"""Record names declared as nonlocal within the function.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
node: The ast.Nonlocal node.
|
|
246
|
+
"""
|
|
136
247
|
nonlocals = set(node.names)
|
|
137
248
|
self.names.explicitly_nonlocal_unbound_deep |= nonlocals
|
|
138
249
|
self.names.accessible |= nonlocals
|
|
139
250
|
self.generic_visit(node)
|
|
140
251
|
|
|
141
252
|
def visit_Global(self, node):
|
|
253
|
+
"""Record names declared as global within the function.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
node: The ast.Global node.
|
|
257
|
+
"""
|
|
142
258
|
globals = set(node.names)
|
|
143
259
|
self.names.explicitly_global_unbound_deep |= globals
|
|
144
260
|
self.names.accessible |= globals
|
|
@@ -147,11 +263,24 @@ class NamesUsageAnalyzer(ast.NodeVisitor):
|
|
|
147
263
|
def analyze_names_in_function(
|
|
148
264
|
a_func: Union[Callable,str]
|
|
149
265
|
):
|
|
150
|
-
"""Analyze names used in a function.
|
|
266
|
+
"""Analyze names used in a single conventional function.
|
|
267
|
+
|
|
268
|
+
The function source is normalized, decorators are skipped, and an AST is
|
|
269
|
+
parsed. Assertions ensure that exactly one top-level regular function
|
|
270
|
+
definition is present. The tree is visited with NamesUsageAnalyzer.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
a_func: A function object or its source string to analyze.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
dict: A mapping with keys:
|
|
277
|
+
- tree (ast.Module): The parsed AST module with a single function.
|
|
278
|
+
- analyzer (NamesUsageAnalyzer): The populated analyzer instance.
|
|
279
|
+
- normalized_source (str): The normalized source code.
|
|
151
280
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
281
|
+
Raises:
|
|
282
|
+
AssertionError: If the input is not a single regular function (e.g., a
|
|
283
|
+
lambda, async function, callable class, or multiple definitions).
|
|
155
284
|
"""
|
|
156
285
|
|
|
157
286
|
normalized_source = get_normalized_function_source(a_func)
|
|
@@ -1,12 +1,50 @@
|
|
|
1
|
+
from typing import List, Any
|
|
1
2
|
|
|
2
3
|
|
|
3
|
-
def flatten_list(nested_list):
|
|
4
|
-
"""Flatten a list
|
|
5
|
-
|
|
4
|
+
def flatten_list(nested_list: List[Any]) -> List[Any]:
|
|
5
|
+
"""Flatten a nested list into a single-level list.
|
|
6
|
+
|
|
7
|
+
This function flattens lists of arbitrary depth using an iterative
|
|
8
|
+
(non-recursive) algorithm. Only values of type ``list`` are treated as
|
|
9
|
+
containers to be expanded. Other iterable types (e.g., tuples, sets,
|
|
10
|
+
strings, generators) are considered atomic values and are not traversed.
|
|
11
|
+
|
|
12
|
+
Parameters:
|
|
13
|
+
- nested_list: list
|
|
14
|
+
A possibly nested Python list. Must be an instance of ``list``.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
- list
|
|
18
|
+
A new list containing all elements from ``nested_list`` in their
|
|
19
|
+
original left-to-right order, but with one level of nesting (flat).
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
- TypeError: If ``nested_list`` is not a ``list`` instance.
|
|
23
|
+
|
|
24
|
+
Notes:
|
|
25
|
+
- Preserves the order of elements.
|
|
26
|
+
- Supports unlimited nesting depth without recursion.
|
|
27
|
+
- Cyclic references (e.g., a list that contains itself, directly or
|
|
28
|
+
indirectly) will lead to an infinite loop.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
>>> flatten_list([1, [2, 3, [4]], 5])
|
|
32
|
+
[1, 2, 3, 4, 5]
|
|
33
|
+
>>> flatten_list([["a", ["b"]], "c"])
|
|
34
|
+
['a', 'b', 'c']
|
|
35
|
+
>>> flatten_list([(1, 2), [3, 4]])
|
|
36
|
+
[(1, 2), 3, 4]
|
|
37
|
+
"""
|
|
38
|
+
if not isinstance(nested_list, list):
|
|
39
|
+
raise TypeError(f"Expected list, got {type(nested_list).__name__}")
|
|
6
40
|
flattened = []
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
41
|
+
stack = [nested_list]
|
|
42
|
+
|
|
43
|
+
while stack:
|
|
44
|
+
current = stack.pop()
|
|
45
|
+
if isinstance(current, list):
|
|
46
|
+
stack.extend(reversed(current))
|
|
10
47
|
else:
|
|
11
|
-
flattened.append(
|
|
48
|
+
flattened.append(current)
|
|
49
|
+
|
|
12
50
|
return flattened
|
|
@@ -1,23 +1,55 @@
|
|
|
1
|
+
"""Utilities to install and uninstall Python packages at runtime.
|
|
2
|
+
|
|
3
|
+
This module provides a thin wrapper around pip and the uv tool to install
|
|
4
|
+
and uninstall packages from within the running Python process.
|
|
5
|
+
|
|
6
|
+
Key points:
|
|
7
|
+
- By default, uv is preferred as the installer frontend (uv pip ...). If uv
|
|
8
|
+
or pip is not available, the module will attempt to install the missing
|
|
9
|
+
tool as needed.
|
|
10
|
+
- For safety, uninstall_package refuses to operate on 'pip' or 'uv' directly.
|
|
11
|
+
- Calls are synchronous and raise on non-zero exit status.
|
|
12
|
+
"""
|
|
13
|
+
|
|
1
14
|
import subprocess
|
|
2
15
|
import importlib
|
|
3
16
|
import sys
|
|
17
|
+
from functools import lru_cache
|
|
4
18
|
from typing import Optional
|
|
5
19
|
|
|
6
|
-
_uv_and_pip_installation_needed:bool = True
|
|
7
20
|
|
|
8
|
-
def
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
def _run(command: list[str]) -> str:
|
|
22
|
+
"""Run command; raise RuntimeError on failure."""
|
|
23
|
+
try:
|
|
24
|
+
subprocess.run(command, check=True, stdout=subprocess.PIPE
|
|
25
|
+
, stderr=subprocess.STDOUT, text=True)
|
|
26
|
+
except subprocess.CalledProcessError as e:
|
|
27
|
+
raise RuntimeError(
|
|
28
|
+
f"Command failed: {' '.join(command)}\n{e.stdout}") from e
|
|
29
|
+
|
|
12
30
|
|
|
31
|
+
@lru_cache(maxsize=1) # ensure only one call to _install_uv_and_pip
|
|
32
|
+
def _install_uv_and_pip() -> None:
|
|
33
|
+
"""Ensure the 'uv' and 'pip' frontends are available.
|
|
34
|
+
|
|
35
|
+
Behavior:
|
|
36
|
+
- If this helper has already run in the current process and determined
|
|
37
|
+
the tools are present, it returns immediately.
|
|
38
|
+
- Tries to import 'uv'; if missing, installs it using system pip
|
|
39
|
+
(use_uv=False).
|
|
40
|
+
- Tries to import 'pip'; if missing, installs it using uv (use_uv=True).
|
|
41
|
+
|
|
42
|
+
This function is an internal helper and is called implicitly by
|
|
43
|
+
install_package() for any package other than 'pip' or 'uv'.
|
|
44
|
+
"""
|
|
13
45
|
try:
|
|
14
46
|
importlib.import_module("uv")
|
|
15
|
-
except:
|
|
47
|
+
except ModuleNotFoundError:
|
|
16
48
|
install_package("uv", use_uv=False)
|
|
17
49
|
|
|
18
50
|
try:
|
|
19
51
|
importlib.import_module("pip")
|
|
20
|
-
except:
|
|
52
|
+
except ModuleNotFoundError:
|
|
21
53
|
install_package("pip", use_uv=True)
|
|
22
54
|
|
|
23
55
|
|
|
@@ -26,13 +58,40 @@ def install_package(package_name:str
|
|
|
26
58
|
, version:Optional[str]=None
|
|
27
59
|
, use_uv:bool = True
|
|
28
60
|
) -> None:
|
|
29
|
-
"""Install package using pip.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
61
|
+
"""Install a Python package using uv (default) or pip.
|
|
62
|
+
|
|
63
|
+
Parameters:
|
|
64
|
+
- package_name: Name of the package to install. Special cases:
|
|
65
|
+
- 'pip': must be installed using uv (use_uv=True).
|
|
66
|
+
- 'uv' : must be installed using pip (use_uv=False).
|
|
67
|
+
- upgrade: If True, pass "--upgrade" to the installer.
|
|
68
|
+
- version: Optional version pin, e.g. "1.2.3". If provided, constructs
|
|
69
|
+
"package_name==version".
|
|
70
|
+
- use_uv: If True, run as `python -m uv pip install ...`; otherwise use pip.
|
|
71
|
+
|
|
72
|
+
Behavior:
|
|
73
|
+
- Ensures both uv and pip are available unless installing one of them.
|
|
74
|
+
- Runs the installer in a subprocess with check=True (raises on failure).
|
|
75
|
+
- Imports the package after installation to verify it is importable.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
- RuntimeError: if the installation command fails.
|
|
79
|
+
- ValueError: if package_name or version are invalid, or if attempting
|
|
80
|
+
to install pip with use_uv=False or uv with use_uv=True.
|
|
81
|
+
- ModuleNotFoundError: if the package cannot be imported after installation.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
if not package_name or not isinstance(package_name, str):
|
|
85
|
+
raise ValueError("package_name must be a non-empty string")
|
|
86
|
+
|
|
87
|
+
if version and not isinstance(version, str):
|
|
88
|
+
raise ValueError("version must be a string")
|
|
89
|
+
|
|
90
|
+
if package_name == "pip" and not use_uv:
|
|
91
|
+
raise ValueError("pip must be installed using uv (use_uv=True)")
|
|
92
|
+
elif package_name == "uv" and use_uv:
|
|
93
|
+
raise ValueError("uv must be installed using pip (use_uv=False)")
|
|
94
|
+
elif package_name not in ("pip", "uv"):
|
|
36
95
|
_install_uv_and_pip()
|
|
37
96
|
|
|
38
97
|
if use_uv:
|
|
@@ -46,30 +105,46 @@ def install_package(package_name:str
|
|
|
46
105
|
package_spec = f"{package_name}=={version}" if version else package_name
|
|
47
106
|
command += [package_spec]
|
|
48
107
|
|
|
49
|
-
|
|
50
|
-
, stderr=subprocess.STDOUT, text=True)
|
|
108
|
+
_run(command)
|
|
51
109
|
|
|
110
|
+
# Verify import. Note: assumes package name matches importable module name.
|
|
52
111
|
importlib.import_module(package_name)
|
|
53
112
|
|
|
54
113
|
|
|
55
114
|
def uninstall_package(package_name:str, use_uv:bool=True)->None:
|
|
56
|
-
"""Uninstall package using uv or pip.
|
|
115
|
+
"""Uninstall a Python package using uv (default) or pip.
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
- package_name: Name of the package to uninstall. Must not be 'pip' or 'uv'.
|
|
119
|
+
- use_uv: If True, run `python -m uv pip uninstall <name>`; otherwise use pip with "-y".
|
|
120
|
+
|
|
121
|
+
Behavior:
|
|
122
|
+
- Runs the uninstaller in a subprocess with check=True.
|
|
123
|
+
- Attempts to import and reload the package after uninstallation. If that
|
|
124
|
+
succeeds, raises an Exception to indicate the package still appears installed.
|
|
57
125
|
|
|
58
|
-
|
|
126
|
+
Raises:
|
|
127
|
+
- ValueError: if package_name is 'pip' or 'uv'.
|
|
128
|
+
- RuntimeError: if the uninstall command fails, or if post-uninstall
|
|
129
|
+
validation indicates the package is still importable.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
if package_name in ["pip", "uv"]:
|
|
133
|
+
raise ValueError(f"Cannot uninstall '{package_name}' "
|
|
134
|
+
"- it's a protected package")
|
|
59
135
|
|
|
60
136
|
if use_uv:
|
|
61
137
|
command = [sys.executable, "-m", "uv", "pip", "uninstall", package_name]
|
|
62
138
|
else:
|
|
63
139
|
command = [sys.executable, "-m", "pip", "uninstall", "-y", package_name]
|
|
64
140
|
|
|
65
|
-
|
|
66
|
-
, stderr=subprocess.STDOUT, text=True)
|
|
141
|
+
_run(command)
|
|
67
142
|
|
|
68
143
|
try:
|
|
69
144
|
package = importlib.import_module(package_name)
|
|
70
145
|
importlib.reload(package)
|
|
71
|
-
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
f"Package '{package_name}' still importable after uninstallation")
|
|
148
|
+
except ModuleNotFoundError:
|
|
72
149
|
pass
|
|
73
|
-
|
|
74
|
-
raise Exception(
|
|
75
|
-
f"Failed to validate package uninstallation for '{package_name}'. ")
|
|
150
|
+
|
|
@@ -30,6 +30,24 @@ from .._060_autonomous_code_portals import *
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class ProtectedCodePortal(AutonomousCodePortal):
|
|
33
|
+
"""Portal for protected code execution.
|
|
34
|
+
|
|
35
|
+
This portal specializes the AutonomousCodePortal to coordinate execution of
|
|
36
|
+
ProtectedFn instances. It carries configuration and storage
|
|
37
|
+
required by validators (e.g., retry throttling) and by protected function
|
|
38
|
+
orchestration.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
root_dict (PersiDict | str | None): Optional persistent dictionary or a
|
|
42
|
+
path/identifier to initialize the portal's storage. If None, a
|
|
43
|
+
default in-memory storage may be used.
|
|
44
|
+
p_consistency_checks (float | Joker): Probability or flag controlling
|
|
45
|
+
internal consistency checks performed by the portal. Use
|
|
46
|
+
KEEP_CURRENT to inherit the current setting.
|
|
47
|
+
excessive_logging (bool | Joker): Enables verbose logging of portal and
|
|
48
|
+
function operations. Use KEEP_CURRENT to inherit the current
|
|
49
|
+
setting.
|
|
50
|
+
"""
|
|
33
51
|
|
|
34
52
|
def __init__(self
|
|
35
53
|
, root_dict: PersiDict|str|None = None
|
|
@@ -42,6 +60,14 @@ class ProtectedCodePortal(AutonomousCodePortal):
|
|
|
42
60
|
|
|
43
61
|
|
|
44
62
|
class ProtectedFn(AutonomousFn):
|
|
63
|
+
"""Function wrapper that enforces pre/post validation around execution.
|
|
64
|
+
|
|
65
|
+
A ProtectedFn evaluates a sequence of pre-validators before executing the
|
|
66
|
+
underlying function and a sequence of post-validators after execution. If a
|
|
67
|
+
pre-validator returns a ProtectedFnCallSignature, that signature will be
|
|
68
|
+
executed first (allowing validators to perform prerequisite actions) before
|
|
69
|
+
re-attempting the validation/execution loop.
|
|
70
|
+
"""
|
|
45
71
|
|
|
46
72
|
_pre_validators_cache: list[ValidatorFn] | None
|
|
47
73
|
_post_validators_cache: list[ValidatorFn] | None
|
|
@@ -57,6 +83,25 @@ class ProtectedFn(AutonomousFn):
|
|
|
57
83
|
, excessive_logging: bool | Joker = KEEP_CURRENT
|
|
58
84
|
, fixed_kwargs: dict[str,Any] | None = None
|
|
59
85
|
, portal: ProtectedCodePortal | None = None):
|
|
86
|
+
"""Construct a ProtectedFn.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
fn (Callable | str): The underlying Python function or its source
|
|
90
|
+
code string.
|
|
91
|
+
pre_validators (list[ValidatorFn] | list[Callable] | ValidatorFn | Callable | None):
|
|
92
|
+
Pre-execution validators. Callables are wrapped into
|
|
93
|
+
PreValidatorFn. Lists can be nested and will
|
|
94
|
+
be flattened.
|
|
95
|
+
post_validators (list[ValidatorFn] | list[Callable] | ValidatorFn | Callable | None):
|
|
96
|
+
Post-execution validators. Callables are wrapped into
|
|
97
|
+
PostValidatorFn. Lists can be nested and will be flattened.
|
|
98
|
+
excessive_logging (bool | Joker): Enable verbose logging or inherit
|
|
99
|
+
current setting with KEEP_CURRENT.
|
|
100
|
+
fixed_kwargs (dict[str, Any] | None): Keyword arguments to be fixed
|
|
101
|
+
(bound) for every execution of the function.
|
|
102
|
+
portal (ProtectedCodePortal | None): Portal instance to bind the
|
|
103
|
+
function to.
|
|
104
|
+
"""
|
|
60
105
|
super().__init__(fn=fn
|
|
61
106
|
, portal = portal
|
|
62
107
|
, fixed_kwargs=fixed_kwargs
|
|
@@ -114,6 +159,11 @@ class ProtectedFn(AutonomousFn):
|
|
|
114
159
|
|
|
115
160
|
@property
|
|
116
161
|
def pre_validators(self) -> list[AutonomousFn]:
|
|
162
|
+
"""List of pre-validator functions for this protected function.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
list[AutonomousFn]: A cached list of PreValidatorFn instances.
|
|
166
|
+
"""
|
|
117
167
|
if not hasattr(self, "_pre_validators_cache"):
|
|
118
168
|
self._pre_validators_cache = [
|
|
119
169
|
addr.get() for addr in self._pre_validators_addrs]
|
|
@@ -122,6 +172,11 @@ class ProtectedFn(AutonomousFn):
|
|
|
122
172
|
|
|
123
173
|
@property
|
|
124
174
|
def post_validators(self) -> list[AutonomousFn]:
|
|
175
|
+
"""List of post-validator functions for this protected function.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
list[AutonomousFn]: A cached list of PostValidatorFn instances.
|
|
179
|
+
"""
|
|
125
180
|
if not hasattr(self, "_post_validators_cache"):
|
|
126
181
|
self._post_validators_cache = [
|
|
127
182
|
addr.get() for addr in self._post_validators_addrs]
|
|
@@ -131,6 +186,21 @@ class ProtectedFn(AutonomousFn):
|
|
|
131
186
|
def can_be_executed(self
|
|
132
187
|
, kw_args: KwArgs
|
|
133
188
|
) -> ProtectedFnCallSignature|ValidationSuccessFlag|None:
|
|
189
|
+
"""Run pre-validators to determine if execution can proceed.
|
|
190
|
+
|
|
191
|
+
The portal will shuffle the order of pre-validators. If any validator
|
|
192
|
+
returns a ProtectedFnCallSignature, that signature should be executed by
|
|
193
|
+
the caller prior to executing the protected function (this method simply
|
|
194
|
+
returns it). If any validator fails, None is returned. If all succeed,
|
|
195
|
+
VALIDATION_SUCCESSFUL is returned.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
kw_args (KwArgs): Arguments intended for the wrapped function.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
ProtectedFnCallSignature | ValidationSuccessFlag | None: Either a
|
|
202
|
+
signature to execute first, the success flag, or None on failure.
|
|
203
|
+
"""
|
|
134
204
|
with self.portal as portal:
|
|
135
205
|
kw_args = kw_args.pack()
|
|
136
206
|
pre_validators = copy(self.pre_validators)
|