pythagoras 0.24.4__py3-none-any.whl → 0.24.7__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 (27) hide show
  1. pythagoras/_060_autonomous_code_portals/autonomous_decorators.py +31 -4
  2. pythagoras/_060_autonomous_code_portals/autonomous_portal_core_classes.py +94 -14
  3. pythagoras/_060_autonomous_code_portals/names_usage_analyzer.py +133 -4
  4. pythagoras/_070_protected_code_portals/basic_pre_validators.py +130 -15
  5. pythagoras/_070_protected_code_portals/fn_arg_names_checker.py +20 -18
  6. pythagoras/_070_protected_code_portals/list_flattener.py +45 -7
  7. pythagoras/_070_protected_code_portals/package_manager.py +99 -24
  8. pythagoras/_070_protected_code_portals/protected_decorators.py +59 -1
  9. pythagoras/_070_protected_code_portals/protected_portal_core_classes.py +239 -4
  10. pythagoras/_070_protected_code_portals/system_utils.py +85 -12
  11. pythagoras/_070_protected_code_portals/validation_succesful_const.py +12 -7
  12. pythagoras/_080_pure_code_portals/pure_core_classes.py +178 -25
  13. pythagoras/_080_pure_code_portals/pure_decorator.py +37 -0
  14. pythagoras/_080_pure_code_portals/recursion_pre_validator.py +39 -0
  15. pythagoras/_090_swarming_portals/output_suppressor.py +32 -3
  16. pythagoras/_090_swarming_portals/swarming_portals.py +165 -19
  17. pythagoras/_100_top_level_API/__init__.py +11 -0
  18. pythagoras/_800_signatures_and_converters/__init__.py +17 -0
  19. pythagoras/_800_signatures_and_converters/base_16_32_convertors.py +55 -20
  20. pythagoras/_800_signatures_and_converters/current_date_gmt_str.py +20 -5
  21. pythagoras/_800_signatures_and_converters/hash_signatures.py +46 -10
  22. pythagoras/_800_signatures_and_converters/node_signature.py +27 -12
  23. pythagoras/_800_signatures_and_converters/random_signatures.py +14 -3
  24. pythagoras/core/__init__.py +54 -0
  25. {pythagoras-0.24.4.dist-info → pythagoras-0.24.7.dist-info}/METADATA +1 -1
  26. {pythagoras-0.24.4.dist-info → pythagoras-0.24.7.dist-info}/RECORD +27 -27
  27. {pythagoras-0.24.4.dist-info → pythagoras-0.24.7.dist-info}/WHEEL +0 -0
@@ -3,15 +3,21 @@ from typing import List, Set
3
3
 
4
4
 
5
5
  def check_if_fn_accepts_args(required_arg_names: List[str]|Set[str], fn: str) -> bool:
6
- """
7
- Analyzes the source code (string) `fn` of a Python function and determines
8
- if it can accept the arguments named in `required_arg_names`.
6
+ """Determine whether a function can accept specific keyword argument names.
7
+
8
+ Analyzes the source code of a single Python function and checks whether
9
+ all required names could be passed as keyword arguments.
9
10
 
10
- This will return True if:
11
- - The function has a **kwargs parameter, or
12
- - The function explicitly defines all of the names in `required_arg_names` as parameters.
11
+ Args:
12
+ required_arg_names: Iterable of parameter names that must be accepted.
13
+ fn: Source code string containing exactly one function definition.
13
14
 
14
- Otherwise, returns False.
15
+ Returns:
16
+ True if the function has **kwargs or explicitly defines all required names
17
+ as keyword-acceptable parameters; otherwise False.
18
+
19
+ Raises:
20
+ ValueError: If no function definition is found or if multiple functions are present.
15
21
  """
16
22
 
17
23
  tree = ast.parse(fn)
@@ -19,6 +25,8 @@ def check_if_fn_accepts_args(required_arg_names: List[str]|Set[str], fn: str) ->
19
25
  func_def_nodes = [node for node in tree.body if isinstance(node, ast.FunctionDef)]
20
26
  if not func_def_nodes:
21
27
  raise ValueError("No function definition found in the provided source code.")
28
+ if not len(func_def_nodes) == 1:
29
+ raise ValueError("Multiple function definitions found in the provided source code.")
22
30
  func_def = func_def_nodes[0]
23
31
  args = func_def.args
24
32
 
@@ -26,14 +34,8 @@ def check_if_fn_accepts_args(required_arg_names: List[str]|Set[str], fn: str) ->
26
34
  if args.kwarg is not None:
27
35
  return True
28
36
 
29
- # Collect all explicitly named parameters
30
- param_names = set()
31
- for arg in args.args:
32
- param_names.add(arg.arg)
33
- for kwarg in args.kwonlyargs:
34
- param_names.add(kwarg.arg)
35
-
36
- for name in required_arg_names:
37
- if name not in param_names:
38
- return False
39
- return True
37
+ # Collect all explicitly named parameters (excluding positional-only)
38
+ # Note: args.posonlyargs are NOT included as they cannot be passed by keyword
39
+ param_names = {arg.arg for arg in args.args + args.kwonlyargs}
40
+
41
+ return set(required_arg_names).issubset(param_names)
@@ -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
+
@@ -1,4 +1,14 @@
1
- """Support for work with protected functions."""
1
+ """Decorators for building protected functions.
2
+
3
+ This module provides the protected decorator which wraps callables
4
+ into ProtectedFn objects. A ProtectedFn coordinates pre- and post-execution
5
+ validation using ValidatorFn instances and executes within a
6
+ ProtectedCodePortal context.
7
+
8
+ The decorator is a thin, declarative layer over the underlying core classes,
9
+ allowing you to attach validators at definition
10
+ time while keeping function logic clean and focused.
11
+ """
2
12
 
3
13
  from typing import Callable, Any
4
14
 
@@ -8,6 +18,27 @@ from .protected_portal_core_classes import *
8
18
  from persidict import Joker, KEEP_CURRENT
9
19
 
10
20
  class protected(autonomous):
21
+ """Decorator for protected functions with pre/post validation.
22
+
23
+ This decorator wraps a target callable into a ProtectedFn that enforces
24
+ a sequence of pre- and post-execution validators. It builds on the
25
+ autonomous decorator, adding validator support to it.
26
+
27
+ Typical usage:
28
+ @protected(pre_validators=[...], post_validators=[...])
29
+ def fn(...):
30
+ ...
31
+
32
+ See Also:
33
+ ProtectedFn: The runtime wrapper that performs validation and execution.
34
+ ProtectedCodePortal: Portal coordinating protected function execution.
35
+
36
+ Attributes:
37
+ _pre_validators (list[ValidatorFn] | None): Validators executed before
38
+ the target function.
39
+ _post_validators (list[ValidatorFn] | None): Validators executed after
40
+ the target function.
41
+ """
11
42
 
12
43
  _pre_validators: list[ValidatorFn] | None
13
44
  _post_validators: list[ValidatorFn] | None
@@ -19,6 +50,24 @@ class protected(autonomous):
19
50
  , excessive_logging: bool|Joker = KEEP_CURRENT
20
51
  , portal: ProtectedCodePortal | None = None
21
52
  ):
53
+ """Initialize the protected decorator.
54
+
55
+ Args:
56
+ pre_validators (list[ValidatorFn] | None): Pre-execution validators
57
+ to apply. Each item is either a ValidatorFn or a callable that
58
+ can be wrapped into a PreValidatorFn by ProtectedFn.
59
+ post_validators (list[ValidatorFn] | None): Post-execution validators
60
+ to apply. Each item is either a ValidatorFn or a callable that
61
+ can be wrapped into a PostValidatorFn by ProtectedFn.
62
+ fixed_kwargs (dict[str, Any] | None): Keyword arguments to pre-bind
63
+ to the wrapped function for every call.
64
+ excessive_logging (bool | Joker): Enables verbose logging for the
65
+ wrapped function and its validators. Use KEEP_CURRENT to inherit
66
+ the current setting from the portal/context.
67
+ portal (ProtectedCodePortal | None): Optional portal instance to
68
+ bind the wrapped function to. If None, a suitable portal will be
69
+ inferred when fuction is called.
70
+ """
22
71
  assert isinstance(portal, ProtectedCodePortal) or portal is None
23
72
  assert isinstance(fixed_kwargs, dict) or fixed_kwargs is None
24
73
  autonomous.__init__(self=self
@@ -30,6 +79,15 @@ class protected(autonomous):
30
79
 
31
80
 
32
81
  def __call__(self, fn: Callable|str) -> ProtectedFn:
82
+ """Wrap the given function into a ProtectedFn.
83
+
84
+ Args:
85
+ fn (Callable | str): The target function or its source code string.
86
+
87
+ Returns:
88
+ ProtectedFn: A wrapper that performs pre/post validation and then
89
+ executes the function.
90
+ """
33
91
  wrapper = ProtectedFn(fn
34
92
  , portal=self._portal
35
93
  , pre_validators=self._pre_validators