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.
Files changed (33) hide show
  1. pythagoras/_020_ordinary_code_portals/code_normalizer.py +37 -9
  2. pythagoras/_020_ordinary_code_portals/function_processing.py +58 -15
  3. pythagoras/_020_ordinary_code_portals/ordinary_decorator.py +14 -0
  4. pythagoras/_020_ordinary_code_portals/ordinary_portal_core_classes.py +196 -24
  5. pythagoras/_030_data_portals/data_portal_core_classes.py +74 -22
  6. pythagoras/_030_data_portals/ready_and_get.py +45 -4
  7. pythagoras/_030_data_portals/storable_decorator.py +18 -1
  8. pythagoras/_040_logging_code_portals/exception_processing_tracking.py +30 -2
  9. pythagoras/_040_logging_code_portals/execution_environment_summary.py +60 -24
  10. pythagoras/_040_logging_code_portals/kw_args.py +74 -12
  11. pythagoras/_040_logging_code_portals/logging_decorator.py +23 -1
  12. pythagoras/_040_logging_code_portals/logging_portal_core_classes.py +365 -12
  13. pythagoras/_040_logging_code_portals/notebook_checker.py +9 -1
  14. pythagoras/_040_logging_code_portals/uncaught_exceptions.py +40 -0
  15. pythagoras/_050_safe_code_portals/safe_decorator.py +27 -1
  16. pythagoras/_050_safe_code_portals/safe_portal_core_classes.py +87 -11
  17. pythagoras/_060_autonomous_code_portals/autonomous_decorators.py +31 -4
  18. pythagoras/_060_autonomous_code_portals/autonomous_portal_core_classes.py +94 -14
  19. pythagoras/_060_autonomous_code_portals/names_usage_analyzer.py +133 -4
  20. pythagoras/_070_protected_code_portals/list_flattener.py +45 -7
  21. pythagoras/_070_protected_code_portals/package_manager.py +99 -24
  22. pythagoras/_070_protected_code_portals/protected_portal_core_classes.py +70 -0
  23. pythagoras/_070_protected_code_portals/system_utils.py +85 -12
  24. pythagoras/_070_protected_code_portals/validation_succesful_const.py +12 -7
  25. pythagoras/_090_swarming_portals/swarming_portals.py +4 -6
  26. pythagoras/_800_signatures_and_converters/base_16_32_convertors.py +55 -20
  27. pythagoras/_800_signatures_and_converters/current_date_gmt_str.py +20 -5
  28. pythagoras/_800_signatures_and_converters/hash_signatures.py +46 -10
  29. pythagoras/_800_signatures_and_converters/node_signature.py +27 -12
  30. pythagoras/_800_signatures_and_converters/random_signatures.py +14 -3
  31. {pythagoras-0.24.3.dist-info → pythagoras-0.24.6.dist-info}/METADATA +1 -1
  32. {pythagoras-0.24.3.dist-info → pythagoras-0.24.6.dist-info}/RECORD +33 -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
- It returns an instance of NamesUsageAnalyzer class,
153
- which contains all the data needed to analyze
154
- names, used by the function.
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 with unlimited nesting levels."""
5
- assert isinstance(nested_list, list)
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
- for item in nested_list:
8
- if isinstance(item, list):
9
- flattened.extend(flatten_list(item))
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(item)
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 _install_uv_and_pip() -> None:
9
- global _uv_and_pip_installation_needed
10
- if not _uv_and_pip_installation_needed:
11
- return
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
- if package_name == "pip":
32
- assert use_uv
33
- elif package_name == "uv":
34
- assert not use_uv
35
- else:
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
- subprocess.run(command, check=True, stdout=subprocess.PIPE
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
- assert package_name not in ["pip", "uv"]
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
- subprocess.run(command, check=True, stdout=subprocess.PIPE
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
- except:
146
+ raise RuntimeError(
147
+ f"Package '{package_name}' still importable after uninstallation")
148
+ except ModuleNotFoundError:
72
149
  pass
73
- else:
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)