pythagoras 0.24.4__tar.gz → 0.24.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pythagoras-0.24.4 → pythagoras-0.24.7}/PKG-INFO +1 -1
- {pythagoras-0.24.4 → pythagoras-0.24.7}/pyproject.toml +1 -1
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_060_autonomous_code_portals/autonomous_decorators.py +31 -4
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_060_autonomous_code_portals/autonomous_portal_core_classes.py +94 -14
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_060_autonomous_code_portals/names_usage_analyzer.py +133 -4
- pythagoras-0.24.7/src/pythagoras/_070_protected_code_portals/basic_pre_validators.py +172 -0
- pythagoras-0.24.7/src/pythagoras/_070_protected_code_portals/fn_arg_names_checker.py +41 -0
- pythagoras-0.24.7/src/pythagoras/_070_protected_code_portals/list_flattener.py +50 -0
- pythagoras-0.24.7/src/pythagoras/_070_protected_code_portals/package_manager.py +150 -0
- pythagoras-0.24.7/src/pythagoras/_070_protected_code_portals/protected_decorators.py +97 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_070_protected_code_portals/protected_portal_core_classes.py +239 -4
- pythagoras-0.24.7/src/pythagoras/_070_protected_code_portals/system_utils.py +153 -0
- pythagoras-0.24.7/src/pythagoras/_070_protected_code_portals/validation_succesful_const.py +16 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_080_pure_code_portals/pure_core_classes.py +178 -25
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_080_pure_code_portals/pure_decorator.py +37 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_080_pure_code_portals/recursion_pre_validator.py +39 -0
- pythagoras-0.24.7/src/pythagoras/_090_swarming_portals/output_suppressor.py +47 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_090_swarming_portals/swarming_portals.py +165 -19
- pythagoras-0.24.7/src/pythagoras/_100_top_level_API/__init__.py +13 -0
- pythagoras-0.24.7/src/pythagoras/_800_signatures_and_converters/__init__.py +22 -0
- pythagoras-0.24.7/src/pythagoras/_800_signatures_and_converters/base_16_32_convertors.py +83 -0
- pythagoras-0.24.7/src/pythagoras/_800_signatures_and_converters/current_date_gmt_str.py +26 -0
- pythagoras-0.24.7/src/pythagoras/_800_signatures_and_converters/hash_signatures.py +69 -0
- pythagoras-0.24.7/src/pythagoras/_800_signatures_and_converters/node_signature.py +35 -0
- pythagoras-0.24.7/src/pythagoras/_800_signatures_and_converters/random_signatures.py +22 -0
- pythagoras-0.24.7/src/pythagoras/core/__init__.py +60 -0
- pythagoras-0.24.4/src/pythagoras/_070_protected_code_portals/basic_pre_validators.py +0 -57
- pythagoras-0.24.4/src/pythagoras/_070_protected_code_portals/fn_arg_names_checker.py +0 -39
- pythagoras-0.24.4/src/pythagoras/_070_protected_code_portals/list_flattener.py +0 -12
- pythagoras-0.24.4/src/pythagoras/_070_protected_code_portals/package_manager.py +0 -75
- pythagoras-0.24.4/src/pythagoras/_070_protected_code_portals/protected_decorators.py +0 -39
- pythagoras-0.24.4/src/pythagoras/_070_protected_code_portals/system_utils.py +0 -80
- pythagoras-0.24.4/src/pythagoras/_070_protected_code_portals/validation_succesful_const.py +0 -11
- pythagoras-0.24.4/src/pythagoras/_090_swarming_portals/output_suppressor.py +0 -18
- pythagoras-0.24.4/src/pythagoras/_100_top_level_API/__init__.py +0 -2
- pythagoras-0.24.4/src/pythagoras/_800_signatures_and_converters/__init__.py +0 -5
- pythagoras-0.24.4/src/pythagoras/_800_signatures_and_converters/base_16_32_convertors.py +0 -48
- pythagoras-0.24.4/src/pythagoras/_800_signatures_and_converters/current_date_gmt_str.py +0 -11
- pythagoras-0.24.4/src/pythagoras/_800_signatures_and_converters/hash_signatures.py +0 -33
- pythagoras-0.24.4/src/pythagoras/_800_signatures_and_converters/node_signature.py +0 -20
- pythagoras-0.24.4/src/pythagoras/_800_signatures_and_converters/random_signatures.py +0 -11
- pythagoras-0.24.4/src/pythagoras/core/__init__.py +0 -6
- {pythagoras-0.24.4 → pythagoras-0.24.7}/README.md +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/.DS_Store +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_010_basic_portals/__init__.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_010_basic_portals/basic_portal_core_classes.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_010_basic_portals/exceptions.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_010_basic_portals/long_infoname.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_010_basic_portals/portal_tester.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_010_basic_portals/post_init_metaclass.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_020_ordinary_code_portals/__init__.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_020_ordinary_code_portals/code_normalizer.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_020_ordinary_code_portals/function_processing.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_020_ordinary_code_portals/ordinary_decorator.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_020_ordinary_code_portals/ordinary_portal_core_classes.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_030_data_portals/__init__.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_030_data_portals/data_portal_core_classes.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_030_data_portals/ready_and_get.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_030_data_portals/storable_decorator.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_040_logging_code_portals/__init__.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_040_logging_code_portals/exception_processing_tracking.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_040_logging_code_portals/execution_environment_summary.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_040_logging_code_portals/kw_args.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_040_logging_code_portals/logging_decorator.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_040_logging_code_portals/logging_portal_core_classes.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_040_logging_code_portals/notebook_checker.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_040_logging_code_portals/output_capturer.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_040_logging_code_portals/uncaught_exceptions.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_050_safe_code_portals/__init__.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_050_safe_code_portals/safe_decorator.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_050_safe_code_portals/safe_portal_core_classes.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_060_autonomous_code_portals/__init__.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_070_protected_code_portals/__init__.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_080_pure_code_portals/__init__.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_090_swarming_portals/__init__.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_100_top_level_API/default_local_portal.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_100_top_level_API/top_level_API.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_900_project_stats_collector/__init__.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/_900_project_stats_collector/project_analyzer.py +0 -0
- {pythagoras-0.24.4 → pythagoras-0.24.7}/src/pythagoras/__init__.py +0 -0
|
@@ -43,11 +43,16 @@ from persidict import Joker, KEEP_CURRENT
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class autonomous(safe):
|
|
46
|
-
"""Decorator
|
|
46
|
+
"""Decorator that turns a regular function into an autonomous one.
|
|
47
47
|
|
|
48
|
-
An autonomous function is
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
An autonomous function is a self-contained function: it
|
|
49
|
+
can only use built-ins and any names it imports inside its own body. This
|
|
50
|
+
decorator wraps the target callable into an AutonomousFn and enforces both
|
|
51
|
+
static and runtime autonomy checks via the selected portal.
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- Only regular (non-async) functions are supported.
|
|
55
|
+
- Methods, closures, lambdas, and coroutines are not considered autonomous.
|
|
51
56
|
"""
|
|
52
57
|
_fixed_args: dict|None
|
|
53
58
|
|
|
@@ -56,6 +61,19 @@ class autonomous(safe):
|
|
|
56
61
|
, excessive_logging: bool|Joker = KEEP_CURRENT
|
|
57
62
|
, portal: AutonomousCodePortal | None = None
|
|
58
63
|
):
|
|
64
|
+
"""Initialize the decorator.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
fixed_kwargs: Keyword arguments to pre-bind (partially apply) to the
|
|
68
|
+
decorated function. These will be merged into every call.
|
|
69
|
+
excessive_logging: If True, enables verbose logging within the
|
|
70
|
+
selected portal. KEEP_CURRENT leaves the portal's setting as-is.
|
|
71
|
+
portal: Portal instance to use for autonomy and safety checks.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
AssertionError: If portal is not an AutonomousCodePortal or None, or
|
|
75
|
+
if fixed_kwargs is not a dict or None.
|
|
76
|
+
"""
|
|
59
77
|
assert isinstance(portal, AutonomousCodePortal) or portal is None
|
|
60
78
|
assert isinstance(fixed_kwargs, dict) or fixed_kwargs is None
|
|
61
79
|
safe.__init__(self=self
|
|
@@ -65,6 +83,15 @@ class autonomous(safe):
|
|
|
65
83
|
|
|
66
84
|
|
|
67
85
|
def __call__(self, fn: Callable|str) -> AutonomousFn:
|
|
86
|
+
"""Wrap the function with autonomy enforcement.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
fn: The function object to decorate.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
AutonomousFn: A wrapper that enforces autonomy at decoration and at
|
|
93
|
+
execution time, with any fixed keyword arguments pre-applied.
|
|
94
|
+
"""
|
|
68
95
|
wrapper = AutonomousFn(fn
|
|
69
96
|
,portal=self._portal
|
|
70
97
|
,fixed_kwargs=self._fixed_kwargs
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import builtins
|
|
4
|
-
from typing import Callable, Any
|
|
5
|
-
|
|
6
|
-
from persidict import PersiDict, Joker, KEEP_CURRENT
|
|
7
|
-
|
|
8
|
-
from .._010_basic_portals import PortalAwareClass
|
|
9
|
-
from .._010_basic_portals.basic_portal_core_classes import _visit_portal
|
|
10
4
|
from .._020_ordinary_code_portals.code_normalizer import _pythagoras_decorator_names
|
|
11
|
-
from .._030_data_portals import DataPortal
|
|
12
5
|
from .._040_logging_code_portals import KwArgs
|
|
13
6
|
|
|
14
7
|
from .._060_autonomous_code_portals.names_usage_analyzer import (
|
|
@@ -17,12 +10,27 @@ from .._060_autonomous_code_portals.names_usage_analyzer import (
|
|
|
17
10
|
from .._050_safe_code_portals.safe_portal_core_classes import *
|
|
18
11
|
|
|
19
12
|
class AutonomousCodePortal(SafeCodePortal):
|
|
20
|
-
|
|
13
|
+
"""Portal configured for enforcing autonomy constraints.
|
|
14
|
+
|
|
15
|
+
This portal behaves like SafeCodePortal but is specialized for autonomous
|
|
16
|
+
functions. It controls logging and consistency checks for operations related
|
|
17
|
+
to AutonomousFn instances.
|
|
18
|
+
"""
|
|
21
19
|
def __init__(self
|
|
22
20
|
, root_dict: PersiDict | str | None = None
|
|
23
21
|
, p_consistency_checks: float | Joker = KEEP_CURRENT
|
|
24
22
|
, excessive_logging: bool|Joker = KEEP_CURRENT
|
|
25
23
|
):
|
|
24
|
+
"""Create an autonomous code portal.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
root_dict: Persistence root backing the portal state. Can be a
|
|
28
|
+
PersiDict instance, a path string, or None for defaults.
|
|
29
|
+
p_consistency_checks: Probability [0..1] to run extra consistency
|
|
30
|
+
checks on operations. KEEP_CURRENT uses the existing setting.
|
|
31
|
+
excessive_logging: Whether to enable verbose logging. KEEP_CURRENT
|
|
32
|
+
preserves the existing portal setting.
|
|
33
|
+
"""
|
|
26
34
|
SafeCodePortal.__init__(self
|
|
27
35
|
, root_dict=root_dict
|
|
28
36
|
, p_consistency_checks=p_consistency_checks
|
|
@@ -30,10 +38,19 @@ class AutonomousCodePortal(SafeCodePortal):
|
|
|
30
38
|
|
|
31
39
|
|
|
32
40
|
class AutonomousFnCallSignature(SafeFnCallSignature):
|
|
33
|
-
"""A signature of a call to an autonomous function
|
|
41
|
+
"""A signature of a call to an autonomous function.
|
|
42
|
+
|
|
43
|
+
This extends SafeFnCallSignature to reference AutonomousFn instances.
|
|
44
|
+
"""
|
|
34
45
|
_fn_cache: AutonomousFn | None
|
|
35
46
|
|
|
36
47
|
def __init__(self, fn: AutonomousFn, arguments: dict):
|
|
48
|
+
"""Create a call signature for an autonomous function.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
fn: The autonomous function being called.
|
|
52
|
+
arguments: The call-time arguments mapping (already validated).
|
|
53
|
+
"""
|
|
37
54
|
assert isinstance(fn, AutonomousFn)
|
|
38
55
|
assert isinstance(arguments, dict)
|
|
39
56
|
super().__init__(fn, arguments)
|
|
@@ -45,7 +62,13 @@ class AutonomousFnCallSignature(SafeFnCallSignature):
|
|
|
45
62
|
|
|
46
63
|
|
|
47
64
|
class AutonomousFn(SafeFn):
|
|
65
|
+
"""A SafeFn wrapper that enforces function autonomy rules.
|
|
48
66
|
|
|
67
|
+
AutonomousFn performs static validation at construction time to ensure that
|
|
68
|
+
the wrapped function uses only built-ins or names imported inside its body,
|
|
69
|
+
has no yield statements, and does not reference nonlocal variables.
|
|
70
|
+
It also supports partial application via fixed keyword arguments.
|
|
71
|
+
"""
|
|
49
72
|
_fixed_kwargs_cache: KwArgs | None
|
|
50
73
|
_fixed_kwargs_packed: KwArgs | None
|
|
51
74
|
|
|
@@ -53,6 +76,20 @@ class AutonomousFn(SafeFn):
|
|
|
53
76
|
, fixed_kwargs: dict[str,Any]|None = None
|
|
54
77
|
, excessive_logging: bool|Joker = KEEP_CURRENT
|
|
55
78
|
, portal: AutonomousCodePortal|None = None):
|
|
79
|
+
"""Construct an AutonomousFn and validate autonomy constraints.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
fn: The function object, a string with the function's source code,
|
|
83
|
+
or an existing SafeFn to wrap. If an AutonomousFn is provided,
|
|
84
|
+
fixed_kwargs are merged.
|
|
85
|
+
fixed_kwargs: Keyword arguments to pre-bind (partially apply).
|
|
86
|
+
excessive_logging: Verbose logging flag or KEEP_CURRENT.
|
|
87
|
+
portal: AutonomousCodePortal to use; may be None to defer.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
AssertionError: If static analysis detects violations of autonomy
|
|
91
|
+
(nonlocal/global unbound names, missing imports, or yield usage).
|
|
92
|
+
"""
|
|
56
93
|
super().__init__(fn=fn
|
|
57
94
|
, portal = portal
|
|
58
95
|
, excessive_logging = excessive_logging)
|
|
@@ -105,6 +142,11 @@ class AutonomousFn(SafeFn):
|
|
|
105
142
|
|
|
106
143
|
@property
|
|
107
144
|
def fixed_kwargs(self) -> KwArgs:
|
|
145
|
+
"""KwArgs of pre-bound keyword arguments for this function.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
KwArgs: The fixed keyword arguments.
|
|
149
|
+
"""
|
|
108
150
|
if not hasattr(self, "_fixed_kwargs_cache"):
|
|
109
151
|
with self.portal:
|
|
110
152
|
self._fixed_kwargs_cache = self._fixed_kwargs_packed.unpack()
|
|
@@ -112,6 +154,19 @@ class AutonomousFn(SafeFn):
|
|
|
112
154
|
|
|
113
155
|
|
|
114
156
|
def execute(self, **kwargs) -> Any:
|
|
157
|
+
"""Execute the function within the portal, applying fixed kwargs.
|
|
158
|
+
|
|
159
|
+
Any kwargs provided here must not overlap with pre-bound fixed kwargs.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
**kwargs: Call-time keyword arguments.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Any: Result of the wrapped function call.
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
AssertionError: If provided kwargs overlap with fixed kwargs.
|
|
169
|
+
"""
|
|
115
170
|
with self.portal:
|
|
116
171
|
overlapping_keys = set(kwargs.keys()) & set(self.fixed_kwargs.keys())
|
|
117
172
|
assert len(overlapping_keys) == 0
|
|
@@ -120,16 +175,33 @@ class AutonomousFn(SafeFn):
|
|
|
120
175
|
|
|
121
176
|
|
|
122
177
|
def get_signature(self, arguments:dict) -> AutonomousFnCallSignature:
|
|
178
|
+
"""Build a call signature object for this function.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
arguments: Mapping of argument names to values for this call.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
AutonomousFnCallSignature: The signature representing this call.
|
|
185
|
+
"""
|
|
123
186
|
return AutonomousFnCallSignature(fn=self, arguments=arguments)
|
|
124
187
|
|
|
125
188
|
|
|
126
189
|
def fix_kwargs(self, **kwargs) -> AutonomousFn:
|
|
127
|
-
"""Create a new function
|
|
190
|
+
"""Create a new autonomous function with some kwargs pre-filled.
|
|
128
191
|
|
|
129
|
-
This is
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
192
|
+
This is partial application: it creates a function with fewer parameters
|
|
193
|
+
by fixing a subset of keyword arguments.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
**kwargs: Keyword arguments to fix for the new function.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
AutonomousFn: A new wrapper that will always apply the provided
|
|
200
|
+
keyword arguments in addition to already fixed ones.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
AssertionError: If any of the provided kwargs overlap with already
|
|
204
|
+
fixed kwargs.
|
|
133
205
|
"""
|
|
134
206
|
|
|
135
207
|
overlapping_keys = set(kwargs.keys()) & set(self.fixed_kwargs.keys())
|
|
@@ -140,6 +212,13 @@ class AutonomousFn(SafeFn):
|
|
|
140
212
|
|
|
141
213
|
|
|
142
214
|
def _first_visit_to_portal(self, portal: DataPortal) -> None:
|
|
215
|
+
"""Hook called on the first visit to a data portal.
|
|
216
|
+
|
|
217
|
+
Ensures that fixed kwargs are materialized (packed) within the portal.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
portal: The data portal being visited for the first time.
|
|
221
|
+
"""
|
|
143
222
|
super()._first_visit_to_portal(portal)
|
|
144
223
|
with portal:
|
|
145
224
|
_ = self.fixed_kwargs.pack()
|
|
@@ -160,6 +239,7 @@ class AutonomousFn(SafeFn):
|
|
|
160
239
|
|
|
161
240
|
@property
|
|
162
241
|
def portal(self) -> AutonomousCodePortal:
|
|
242
|
+
"""Return the autonomous portal associated with this function."""
|
|
163
243
|
return super().portal
|
|
164
244
|
|
|
165
245
|
|
|
@@ -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)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Basic pre-validation utilities for protected code portals.
|
|
2
|
+
|
|
3
|
+
This module contains small, composable validators used by protected portals
|
|
4
|
+
before executing user functions. Each public factory returns a
|
|
5
|
+
SimplePreValidatorFn configured with fixed arguments, so validators can be
|
|
6
|
+
attached declaratively to protected functions.
|
|
7
|
+
|
|
8
|
+
Execution context:
|
|
9
|
+
- When executed by a portal, validator functions run with two names injected
|
|
10
|
+
into their global namespace: `self` (the ValidatorFn instance) and `pth`
|
|
11
|
+
(the pythagoras package). This allows them to access portal services such as
|
|
12
|
+
system introspection and package management.
|
|
13
|
+
|
|
14
|
+
Conventions:
|
|
15
|
+
- Return ValidationSuccessFlag (VALIDATION_SUCCESSFUL) to indicate the check
|
|
16
|
+
passed; return None to indicate the check did not pass.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .._070_protected_code_portals import SimplePreValidatorFn
|
|
20
|
+
from .validation_succesful_const import ValidationSuccessFlag
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _at_least_X_CPU_cores_free_check(n: int) -> ValidationSuccessFlag | None:
|
|
24
|
+
"""Pass if at least ``n`` logical CPU cores are currently free.
|
|
25
|
+
|
|
26
|
+
This is a lightweight runtime check based on a heuristic estimation of
|
|
27
|
+
unused logical CPU capacity (see pth.get_unused_cpu_cores).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
n (int): Minimum number of free logical CPU cores required.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
ValidationSuccessFlag | None: VALIDATION_SUCCESSFUL if the estimated
|
|
34
|
+
number of free cores is >= n (within a small 0.1 tolerance);
|
|
35
|
+
otherwise None.
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
- The tolerance (0.1) helps account for fluctuations in the estimator.
|
|
39
|
+
- Uses instantaneous/short-horizon metrics; momentary spikes may affect
|
|
40
|
+
the outcome.
|
|
41
|
+
"""
|
|
42
|
+
cores = pth.get_unused_cpu_cores()
|
|
43
|
+
if cores >= n - 0.1:
|
|
44
|
+
return pth.VALIDATION_SUCCESSFUL
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def unused_cpu(cores: int) -> SimplePreValidatorFn:
|
|
48
|
+
"""Create a validator that requires at least the given free CPU cores.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
cores (int): Minimum number of free logical CPU cores required (> 0).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
SimplePreValidatorFn: A pre-validator that succeeds only when
|
|
55
|
+
the system has at least ``cores`` free logical CPU cores.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
TypeError: If ``cores`` is not an integer.
|
|
59
|
+
ValueError: If ``cores`` is not greater than 0.
|
|
60
|
+
"""
|
|
61
|
+
if not isinstance(cores, int):
|
|
62
|
+
raise TypeError("cores must be an int")
|
|
63
|
+
if cores <= 0:
|
|
64
|
+
raise ValueError("cores must be > 0")
|
|
65
|
+
return SimplePreValidatorFn(_at_least_X_CPU_cores_free_check).fix_kwargs(n=cores)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _at_least_X_G_RAM_free_check(x: int) -> ValidationSuccessFlag | None:
|
|
69
|
+
"""Pass if at least ``x`` GiB of RAM are currently available.
|
|
70
|
+
|
|
71
|
+
The check uses pth.get_unused_ram_mb() divided by 1024 to obtain gibibytes
|
|
72
|
+
(GiB) and compares with a small tolerance.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
x (int): Minimum amount of free RAM in GiB required.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
ValidationSuccessFlag | None: VALIDATION_SUCCESSFUL if the estimated
|
|
79
|
+
free RAM in GiB is >= x (within a 0.1 tolerance); otherwise None.
|
|
80
|
+
"""
|
|
81
|
+
ram = pth.get_unused_ram_mb() / 1024
|
|
82
|
+
if ram >= x - 0.1:
|
|
83
|
+
return pth.VALIDATION_SUCCESSFUL
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def unused_ram(Gb: int) -> SimplePreValidatorFn:
|
|
87
|
+
"""Create a validator that requires at least the given free RAM (GiB).
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
Gb (int): Minimum free memory required in GiB (> 0).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
SimplePreValidatorFn: A pre-validator that succeeds only when
|
|
94
|
+
the system has at least ``Gb`` GiB of RAM available.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
TypeError: If ``Gb`` is not an integer.
|
|
98
|
+
ValueError: If ``Gb`` is not greater than 0.
|
|
99
|
+
"""
|
|
100
|
+
if not isinstance(Gb, int):
|
|
101
|
+
raise TypeError("Gb must be an int")
|
|
102
|
+
if Gb <= 0:
|
|
103
|
+
raise ValueError("Gb must be > 0")
|
|
104
|
+
return SimplePreValidatorFn(_at_least_X_G_RAM_free_check).fix_kwargs(x=Gb)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _check_python_package_and_install_if_needed(
|
|
108
|
+
package_name: str) -> ValidationSuccessFlag | None:
|
|
109
|
+
"""Ensure a Python package is importable, attempting installation if not.
|
|
110
|
+
|
|
111
|
+
This validator tries to import the given package. If import fails, it will
|
|
112
|
+
throttle installation attempts to at most once every 10 minutes per
|
|
113
|
+
(node, package) pair and then invoke pth.install_package(package_name).
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
package_name (str): The importable package/module name to check.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
ValidationSuccessFlag | None: VALIDATION_SUCCESSFUL if the package is
|
|
120
|
+
already importable or was successfully installed; otherwise None.
|
|
121
|
+
|
|
122
|
+
Notes:
|
|
123
|
+
- The function relies on names injected by the portal: ``self`` (the
|
|
124
|
+
validator instance) and ``pth`` (pythagoras package).
|
|
125
|
+
- Throttling key is a tuple of (node_signature, package_name,
|
|
126
|
+
"installation_attempt") stored in portal._config_settings.
|
|
127
|
+
- Installation is performed synchronously and may take time.
|
|
128
|
+
"""
|
|
129
|
+
if not isinstance(package_name, str):
|
|
130
|
+
raise TypeError("package_name must be a str")
|
|
131
|
+
import importlib, time
|
|
132
|
+
try:
|
|
133
|
+
importlib.import_module(package_name)
|
|
134
|
+
return pth.VALIDATION_SUCCESSFUL
|
|
135
|
+
except:
|
|
136
|
+
portal = self.portal
|
|
137
|
+
address = (pth.get_node_signature()
|
|
138
|
+
, package_name
|
|
139
|
+
, "installation_attempt")
|
|
140
|
+
# allow installation retries every 10 minutes
|
|
141
|
+
if (not address in portal._config_settings
|
|
142
|
+
or portal._config_settings[address] < time.time() - 600):
|
|
143
|
+
portal._config_settings[address] = time.time()
|
|
144
|
+
pth.install_package(package_name)
|
|
145
|
+
return pth.VALIDATION_SUCCESSFUL
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def installed_packages(*args) -> list[SimplePreValidatorFn]:
|
|
149
|
+
"""Create validators ensuring each named package is available.
|
|
150
|
+
|
|
151
|
+
For each provided package name, this returns a SimplePreValidatorFn that
|
|
152
|
+
checks the package is importable and installs it if needed (with throttling).
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
*args: One or more package names as strings.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
list[SimplePreValidatorFn]: A list of pre-validators, one per package
|
|
159
|
+
name, preserving the original order.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
TypeError: If any of the provided arguments is not a string.
|
|
163
|
+
"""
|
|
164
|
+
validators = []
|
|
165
|
+
for package_name in args:
|
|
166
|
+
if not isinstance(package_name, str):
|
|
167
|
+
raise TypeError("All package names must be strings")
|
|
168
|
+
# TODO: check if the package is available on pypi.org
|
|
169
|
+
new_validator = SimplePreValidatorFn(_check_python_package_and_install_if_needed)
|
|
170
|
+
new_validator = new_validator.fix_kwargs(package_name=package_name)
|
|
171
|
+
validators.append(new_validator)
|
|
172
|
+
return validators
|