toolfuncs 0.3.0__tar.gz → 0.3.2__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.
- toolfuncs-0.3.0/README.md → toolfuncs-0.3.2/PKG-INFO +43 -3
- toolfuncs-0.3.0/PKG-INFO → toolfuncs-0.3.2/README.md +25 -20
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/pyproject.toml +6 -4
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/src/toolfuncs/__init__.py +4 -1
- toolfuncs-0.3.2/src/toolfuncs/_import_path.py +258 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/src/toolfuncs/_tools.py +5 -1
- toolfuncs-0.3.2/tests/test_import_path.py +185 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/tests/test_sdk_cli.py +2 -2
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/tests/test_tools.py +1 -1
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/uv.lock +73 -2
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/.github/workflows/release.yml +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/.gitignore +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/.python-version +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/docs/contract.md +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/src/toolfuncs/__main__.py +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/src/toolfuncs/_errors.py +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/src/toolfuncs/_setup.py +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/src/toolfuncs/agent_hooks.py +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/src/toolfuncs/py.typed +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/src/toolfuncs/sdk.py +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/tests/__init__.py +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/tests/conftest.py +0 -0
- {toolfuncs-0.3.0 → toolfuncs-0.3.2}/tests/test_agent_hooks.py +0 -0
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: toolfuncs
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Executable typed Python function tools discovered through PATH
|
|
5
|
+
Project-URL: Repository, https://github.com/nimashoghi/toolfuncs
|
|
6
|
+
Project-URL: Issues, https://github.com/nimashoghi/toolfuncs/issues
|
|
7
|
+
Author-email: Nima Shoghi <nima@boltz.bio>
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: cyclopts<5,>=4.23.2
|
|
10
|
+
Requires-Dist: packaging>=24
|
|
11
|
+
Requires-Dist: pip>=25
|
|
12
|
+
Requires-Dist: pydantic-core<3,>=2
|
|
13
|
+
Requires-Dist: pydantic<3,>=2
|
|
14
|
+
Requires-Dist: tomli<3,>=2.2; python_version < '3.11'
|
|
15
|
+
Provides-Extra: hooks
|
|
16
|
+
Requires-Dist: typed-agent-hooks; extra == 'hooks'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
1
19
|
# toolfuncs
|
|
2
20
|
|
|
3
21
|
`toolfuncs` turns typed Python functions into tools that have matching Python and command-line interfaces.
|
|
@@ -11,7 +29,7 @@ Create an extensionless file named `weather`:
|
|
|
11
29
|
```python
|
|
12
30
|
#!/usr/bin/env toolfuncs
|
|
13
31
|
# /// script
|
|
14
|
-
# requires-python = ">=3.
|
|
32
|
+
# requires-python = ">=3.10"
|
|
15
33
|
# dependencies = ["weather-client"]
|
|
16
34
|
# [tool.toolfuncs]
|
|
17
35
|
# description = "Read current weather conditions."
|
|
@@ -62,7 +80,29 @@ import toolfuncs
|
|
|
62
80
|
weather = toolfuncs.load("weather")
|
|
63
81
|
```
|
|
64
82
|
|
|
65
|
-
|
|
83
|
+
`import_path()` is the narrower utility for a local script whose path is already
|
|
84
|
+
known:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
module = toolfuncs.import_path("scripts/prepare_data.py")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It accepts one ordinary `.py` file or extensionless script and derives the
|
|
91
|
+
module name from its filename. It does not require a shebang, executable bit,
|
|
92
|
+
`[tool.toolfuncs]` metadata, or `App`, and it does not import packages, projects,
|
|
93
|
+
distributions, or URLs. When an optional PEP 723 block exists, the default
|
|
94
|
+
installer installs all declared requirements together into the running Python
|
|
95
|
+
environment before import. A caller can replace that behavior with one callable
|
|
96
|
+
that accepts `list[str]` and returns `None`:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
module = toolfuncs.import_path(
|
|
100
|
+
"scripts/prepare_data.py",
|
|
101
|
+
dependency_installer=my_installer,
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Direct Python calls return ordinary Python objects and raise the original exceptions. `toolfuncs.load()` and `toolfuncs.import_path()` prepare declared requirements in the running Python environment before import. CLI calls use Cyclopts to parse annotated values and serialize successful results as strict JSON through `pydantic-core`.
|
|
66
106
|
|
|
67
107
|
## Source contract
|
|
68
108
|
|
|
@@ -96,7 +136,7 @@ A toolfunc remains one script even when most of its implementation lives in a pa
|
|
|
96
136
|
```python
|
|
97
137
|
#!/usr/bin/env toolfuncs
|
|
98
138
|
# /// script
|
|
99
|
-
# requires-python = ">=3.
|
|
139
|
+
# requires-python = ">=3.10"
|
|
100
140
|
# dependencies = ["my-large-package"]
|
|
101
141
|
# [tool.toolfuncs]
|
|
102
142
|
# description = "Run the package's agent operation."
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: toolfuncs
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: Executable typed Python function tools discovered through PATH
|
|
5
|
-
Project-URL: Repository, https://github.com/nimashoghi/toolfuncs
|
|
6
|
-
Project-URL: Issues, https://github.com/nimashoghi/toolfuncs/issues
|
|
7
|
-
Author-email: Nima Shoghi <nima@boltz.bio>
|
|
8
|
-
Requires-Python: >=3.11
|
|
9
|
-
Requires-Dist: cyclopts<5,>=4.23.2
|
|
10
|
-
Requires-Dist: packaging>=24
|
|
11
|
-
Requires-Dist: pip>=25
|
|
12
|
-
Requires-Dist: pydantic-core<3,>=2
|
|
13
|
-
Requires-Dist: pydantic<3,>=2
|
|
14
|
-
Provides-Extra: hooks
|
|
15
|
-
Requires-Dist: typed-agent-hooks; extra == 'hooks'
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
|
|
18
1
|
# toolfuncs
|
|
19
2
|
|
|
20
3
|
`toolfuncs` turns typed Python functions into tools that have matching Python and command-line interfaces.
|
|
@@ -28,7 +11,7 @@ Create an extensionless file named `weather`:
|
|
|
28
11
|
```python
|
|
29
12
|
#!/usr/bin/env toolfuncs
|
|
30
13
|
# /// script
|
|
31
|
-
# requires-python = ">=3.
|
|
14
|
+
# requires-python = ">=3.10"
|
|
32
15
|
# dependencies = ["weather-client"]
|
|
33
16
|
# [tool.toolfuncs]
|
|
34
17
|
# description = "Read current weather conditions."
|
|
@@ -79,7 +62,29 @@ import toolfuncs
|
|
|
79
62
|
weather = toolfuncs.load("weather")
|
|
80
63
|
```
|
|
81
64
|
|
|
82
|
-
|
|
65
|
+
`import_path()` is the narrower utility for a local script whose path is already
|
|
66
|
+
known:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
module = toolfuncs.import_path("scripts/prepare_data.py")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
It accepts one ordinary `.py` file or extensionless script and derives the
|
|
73
|
+
module name from its filename. It does not require a shebang, executable bit,
|
|
74
|
+
`[tool.toolfuncs]` metadata, or `App`, and it does not import packages, projects,
|
|
75
|
+
distributions, or URLs. When an optional PEP 723 block exists, the default
|
|
76
|
+
installer installs all declared requirements together into the running Python
|
|
77
|
+
environment before import. A caller can replace that behavior with one callable
|
|
78
|
+
that accepts `list[str]` and returns `None`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
module = toolfuncs.import_path(
|
|
82
|
+
"scripts/prepare_data.py",
|
|
83
|
+
dependency_installer=my_installer,
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Direct Python calls return ordinary Python objects and raise the original exceptions. `toolfuncs.load()` and `toolfuncs.import_path()` prepare declared requirements in the running Python environment before import. CLI calls use Cyclopts to parse annotated values and serialize successful results as strict JSON through `pydantic-core`.
|
|
83
88
|
|
|
84
89
|
## Source contract
|
|
85
90
|
|
|
@@ -113,7 +118,7 @@ A toolfunc remains one script even when most of its implementation lives in a pa
|
|
|
113
118
|
```python
|
|
114
119
|
#!/usr/bin/env toolfuncs
|
|
115
120
|
# /// script
|
|
116
|
-
# requires-python = ">=3.
|
|
121
|
+
# requires-python = ">=3.10"
|
|
117
122
|
# dependencies = ["my-large-package"]
|
|
118
123
|
# [tool.toolfuncs]
|
|
119
124
|
# description = "Run the package's agent operation."
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "toolfuncs"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "Executable typed Python function tools discovered through PATH"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "Nima Shoghi", email = "nima@boltz.bio" }
|
|
8
8
|
]
|
|
9
|
-
requires-python = ">=3.
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"cyclopts>=4.23.2,<5",
|
|
12
12
|
"packaging>=24",
|
|
13
13
|
"pip>=25",
|
|
14
14
|
"pydantic>=2,<3",
|
|
15
15
|
"pydantic-core>=2,<3",
|
|
16
|
+
"tomli>=2.2,<3; python_version < '3.11'",
|
|
16
17
|
]
|
|
17
18
|
|
|
18
19
|
[project.scripts]
|
|
@@ -37,12 +38,13 @@ dev = [
|
|
|
37
38
|
"pytest>=8",
|
|
38
39
|
"pytest-cov>=6",
|
|
39
40
|
"ruff>=0.12",
|
|
41
|
+
"tomli>=2.2,<3",
|
|
40
42
|
"typed-agent-hooks",
|
|
41
43
|
]
|
|
42
44
|
|
|
43
45
|
[tool.ruff]
|
|
44
46
|
line-length = 100
|
|
45
|
-
target-version = "
|
|
47
|
+
target-version = "py310"
|
|
46
48
|
|
|
47
49
|
[tool.ruff.lint]
|
|
48
50
|
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
@@ -52,5 +54,5 @@ addopts = "-ra"
|
|
|
52
54
|
testpaths = ["tests"]
|
|
53
55
|
|
|
54
56
|
[tool.basedpyright]
|
|
55
|
-
pythonVersion = "3.
|
|
57
|
+
pythonVersion = "3.10"
|
|
56
58
|
typeCheckingMode = "standard"
|
|
@@ -13,6 +13,7 @@ from ._errors import (
|
|
|
13
13
|
ToolfuncsError,
|
|
14
14
|
ToolNotFoundError,
|
|
15
15
|
)
|
|
16
|
+
from ._import_path import DependencyInstaller, import_path
|
|
16
17
|
from ._setup import SetupResult, setup
|
|
17
18
|
from ._tools import ALLOW_FLOATING_VCS_SHEBANG, TOOL_SHEBANG, Tool, discover, load
|
|
18
19
|
from .sdk import (
|
|
@@ -28,7 +29,7 @@ from .sdk import (
|
|
|
28
29
|
validators,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
|
-
__version__ = "0.3.
|
|
32
|
+
__version__ = "0.3.2"
|
|
32
33
|
|
|
33
34
|
__all__ = [
|
|
34
35
|
"ALLOW_FLOATING_VCS_SHEBANG",
|
|
@@ -36,6 +37,7 @@ __all__ = [
|
|
|
36
37
|
"UNSET",
|
|
37
38
|
"App",
|
|
38
39
|
"DependencyError",
|
|
40
|
+
"DependencyInstaller",
|
|
39
41
|
"Group",
|
|
40
42
|
"InvalidToolError",
|
|
41
43
|
"Parameter",
|
|
@@ -49,6 +51,7 @@ __all__ = [
|
|
|
49
51
|
"ToolNotFoundError",
|
|
50
52
|
"ToolfuncsError",
|
|
51
53
|
"discover",
|
|
54
|
+
"import_path",
|
|
52
55
|
"json_result_action",
|
|
53
56
|
"load",
|
|
54
57
|
"setup",
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Import one local Python script into the running interpreter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.abc
|
|
7
|
+
import importlib.machinery
|
|
8
|
+
import importlib.util
|
|
9
|
+
import keyword
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
import weakref
|
|
16
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from importlib.machinery import ModuleSpec
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from types import ModuleType
|
|
21
|
+
from typing import TypeAlias, cast
|
|
22
|
+
|
|
23
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
24
|
+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
25
|
+
from packaging.version import Version
|
|
26
|
+
|
|
27
|
+
from ._errors import DependencyError, ToolConflictError
|
|
28
|
+
|
|
29
|
+
if sys.version_info >= (3, 11):
|
|
30
|
+
import tomllib
|
|
31
|
+
else:
|
|
32
|
+
import tomli as tomllib
|
|
33
|
+
|
|
34
|
+
DependencyInstaller: TypeAlias = Callable[[list[str]], None]
|
|
35
|
+
|
|
36
|
+
_PEP723_BLOCK = re.compile(
|
|
37
|
+
r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)[ \t]*$[\r\n]"
|
|
38
|
+
r"(?P<content>(?:^#(?:| .*)$[\r\n]?)+)^# ///[ \t]*$"
|
|
39
|
+
)
|
|
40
|
+
_IMPORT_LOCK = threading.RLock()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class _ScriptDependencies:
|
|
45
|
+
requirements: tuple[str, ...]
|
|
46
|
+
requires_python: str | None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class _DirectImport:
|
|
51
|
+
name: str
|
|
52
|
+
source: Path
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_BY_SOURCE: dict[Path, ModuleType] = {}
|
|
56
|
+
_SOURCE_BY_NAME: dict[str, Path] = {}
|
|
57
|
+
_DIRECT_IMPORTS: weakref.WeakKeyDictionary[ModuleType, _DirectImport] = weakref.WeakKeyDictionary()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _DirectImportFinder(importlib.abc.MetaPathFinder):
|
|
61
|
+
"""Rediscover script-backed module specs for ``importlib.reload``."""
|
|
62
|
+
|
|
63
|
+
def find_spec(
|
|
64
|
+
self,
|
|
65
|
+
fullname: str,
|
|
66
|
+
path: Sequence[str] | None = None,
|
|
67
|
+
target: ModuleType | None = None,
|
|
68
|
+
) -> ModuleSpec | None:
|
|
69
|
+
del path
|
|
70
|
+
if target is None:
|
|
71
|
+
return None
|
|
72
|
+
with _IMPORT_LOCK:
|
|
73
|
+
direct_import = _DIRECT_IMPORTS.get(target)
|
|
74
|
+
if direct_import is None or direct_import.name != fullname:
|
|
75
|
+
return None
|
|
76
|
+
return _direct_import_spec(direct_import.name, direct_import.source)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_DIRECT_IMPORT_FINDER = _DirectImportFinder()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def import_path(
|
|
83
|
+
source: str | os.PathLike[str],
|
|
84
|
+
*,
|
|
85
|
+
dependency_installer: DependencyInstaller | None = None,
|
|
86
|
+
) -> ModuleType:
|
|
87
|
+
"""Import one local Python script.
|
|
88
|
+
|
|
89
|
+
``source`` may be an ordinary ``.py`` file or an extensionless script. Its
|
|
90
|
+
filename determines the module name. When the source contains PEP 723
|
|
91
|
+
metadata, its requirements are installed as one list before import. Pass a
|
|
92
|
+
custom ``dependency_installer`` to replace the default current-interpreter
|
|
93
|
+
pip installation.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
path = _source_path(source)
|
|
97
|
+
name = _module_name(path)
|
|
98
|
+
source_key = path.resolve(strict=True)
|
|
99
|
+
with _IMPORT_LOCK:
|
|
100
|
+
if imported := _BY_SOURCE.get(source_key):
|
|
101
|
+
return imported
|
|
102
|
+
if previous_source := _SOURCE_BY_NAME.get(name):
|
|
103
|
+
raise ToolConflictError(
|
|
104
|
+
f"script name {name!r} was already imported from {previous_source}, "
|
|
105
|
+
f"not {source_key}"
|
|
106
|
+
)
|
|
107
|
+
if name in sys.modules:
|
|
108
|
+
raise ToolConflictError(
|
|
109
|
+
f"cannot import {source_key} as {name!r}: that Python module name is already in use"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
dependencies = _script_dependencies(path)
|
|
113
|
+
_require_compatible_python(dependencies, source=path)
|
|
114
|
+
if dependencies.requirements:
|
|
115
|
+
installer = dependency_installer or _default_dependency_installer
|
|
116
|
+
installer(list(dependencies.requirements))
|
|
117
|
+
importlib.invalidate_caches()
|
|
118
|
+
|
|
119
|
+
spec = _direct_import_spec(name, path)
|
|
120
|
+
assert spec.loader is not None
|
|
121
|
+
imported = importlib.util.module_from_spec(spec)
|
|
122
|
+
sys.modules[name] = imported
|
|
123
|
+
try:
|
|
124
|
+
spec.loader.exec_module(imported)
|
|
125
|
+
except BaseException:
|
|
126
|
+
if sys.modules.get(name) is imported:
|
|
127
|
+
sys.modules.pop(name, None)
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
_BY_SOURCE[source_key] = imported
|
|
131
|
+
_SOURCE_BY_NAME[name] = source_key
|
|
132
|
+
_DIRECT_IMPORTS[imported] = _DirectImport(name, path)
|
|
133
|
+
if _DIRECT_IMPORT_FINDER not in sys.meta_path:
|
|
134
|
+
sys.meta_path.insert(0, _DIRECT_IMPORT_FINDER)
|
|
135
|
+
return imported
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _source_path(source: str | os.PathLike[str]) -> Path:
|
|
139
|
+
raw = os.fspath(source)
|
|
140
|
+
if isinstance(raw, bytes):
|
|
141
|
+
raise TypeError("source must be a string path, not bytes")
|
|
142
|
+
if not raw:
|
|
143
|
+
raise ValueError("source path must not be empty")
|
|
144
|
+
path = Path(os.path.abspath(Path(raw).expanduser()))
|
|
145
|
+
if not path.is_file():
|
|
146
|
+
raise FileNotFoundError(f"Python script does not exist: {path}")
|
|
147
|
+
return path
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _module_name(source: Path) -> str:
|
|
151
|
+
if source.suffix == ".py":
|
|
152
|
+
name = source.stem
|
|
153
|
+
elif not source.suffix:
|
|
154
|
+
name = source.name
|
|
155
|
+
else:
|
|
156
|
+
raise ValueError(f"Python script must be extensionless or end in '.py': {source}")
|
|
157
|
+
if not name.isidentifier() or keyword.iskeyword(name):
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"script filename does not define a valid Python module name: {source.name!r}"
|
|
160
|
+
)
|
|
161
|
+
return name
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _script_dependencies(source: Path) -> _ScriptDependencies:
|
|
165
|
+
try:
|
|
166
|
+
text = source.read_text(encoding="utf-8")
|
|
167
|
+
except (OSError, UnicodeError) as exc:
|
|
168
|
+
raise ValueError(f"could not read Python script {source}: {exc}") from exc
|
|
169
|
+
blocks = [match for match in _PEP723_BLOCK.finditer(text) if match.group("type") == "script"]
|
|
170
|
+
if not blocks:
|
|
171
|
+
return _ScriptDependencies((), None)
|
|
172
|
+
if len(blocks) > 1:
|
|
173
|
+
raise ValueError(f"Python script {source} contains more than one PEP 723 'script' block")
|
|
174
|
+
content = "\n".join(
|
|
175
|
+
line[2:] if line.startswith("# ") else line[1:]
|
|
176
|
+
for line in blocks[0].group("content").splitlines()
|
|
177
|
+
)
|
|
178
|
+
try:
|
|
179
|
+
table = tomllib.loads(content)
|
|
180
|
+
except tomllib.TOMLDecodeError as exc:
|
|
181
|
+
raise ValueError(f"Python script {source} has invalid PEP 723 metadata: {exc}") from exc
|
|
182
|
+
return _dependencies_from_table(table, source=source)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _dependencies_from_table(
|
|
186
|
+
table: Mapping[str, object],
|
|
187
|
+
*,
|
|
188
|
+
source: Path,
|
|
189
|
+
) -> _ScriptDependencies:
|
|
190
|
+
raw_dependencies = table.get("dependencies", [])
|
|
191
|
+
if not isinstance(raw_dependencies, list):
|
|
192
|
+
raise ValueError(f"{source}: PEP 723 'dependencies' must be a list of strings")
|
|
193
|
+
requirements: list[str] = []
|
|
194
|
+
for raw_dependency in cast(list[object], raw_dependencies):
|
|
195
|
+
if not isinstance(raw_dependency, str) or not raw_dependency.strip():
|
|
196
|
+
raise ValueError(f"{source}: PEP 723 dependencies must contain nonempty strings")
|
|
197
|
+
dependency = raw_dependency.strip()
|
|
198
|
+
try:
|
|
199
|
+
Requirement(dependency)
|
|
200
|
+
except InvalidRequirement as exc:
|
|
201
|
+
raise ValueError(f"{source}: invalid dependency {dependency!r}: {exc}") from exc
|
|
202
|
+
requirements.append(dependency)
|
|
203
|
+
|
|
204
|
+
requires_python: str | None = None
|
|
205
|
+
if (raw_requires_python := table.get("requires-python")) is not None:
|
|
206
|
+
if not isinstance(raw_requires_python, str) or not raw_requires_python.strip():
|
|
207
|
+
raise ValueError(f"{source}: PEP 723 'requires-python' must be a nonempty string")
|
|
208
|
+
requires_python = raw_requires_python.strip()
|
|
209
|
+
try:
|
|
210
|
+
SpecifierSet(requires_python)
|
|
211
|
+
except InvalidSpecifier as exc:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
f"{source}: invalid 'requires-python' value {requires_python!r}"
|
|
214
|
+
) from exc
|
|
215
|
+
return _ScriptDependencies(tuple(requirements), requires_python)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _require_compatible_python(metadata: _ScriptDependencies, *, source: Path) -> None:
|
|
219
|
+
if metadata.requires_python is None:
|
|
220
|
+
return
|
|
221
|
+
interpreter = Version(".".join(str(part) for part in sys.version_info[:3]))
|
|
222
|
+
if not SpecifierSet(metadata.requires_python).contains(interpreter, prereleases=True):
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"{source}: 'requires-python' {metadata.requires_python!r} does not admit "
|
|
225
|
+
f"the running interpreter {interpreter}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _default_dependency_installer(requirements: list[str]) -> None:
|
|
230
|
+
completed = subprocess.run(
|
|
231
|
+
(
|
|
232
|
+
sys.executable,
|
|
233
|
+
"-m",
|
|
234
|
+
"pip",
|
|
235
|
+
"--disable-pip-version-check",
|
|
236
|
+
"install",
|
|
237
|
+
"--quiet",
|
|
238
|
+
*requirements,
|
|
239
|
+
),
|
|
240
|
+
capture_output=True,
|
|
241
|
+
text=True,
|
|
242
|
+
check=False,
|
|
243
|
+
)
|
|
244
|
+
if completed.returncode:
|
|
245
|
+
raise DependencyError(
|
|
246
|
+
f"could not install script dependencies (pip exited {completed.returncode})"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _direct_import_spec(name: str, source: Path) -> ModuleSpec:
|
|
251
|
+
loader = importlib.machinery.SourceFileLoader(name, str(source))
|
|
252
|
+
spec = importlib.util.spec_from_loader(name, loader)
|
|
253
|
+
if spec is None:
|
|
254
|
+
raise ImportError(f"could not create an import specification for {source}")
|
|
255
|
+
return spec
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
__all__ = ["DependencyInstaller", "import_path"]
|
|
@@ -11,7 +11,6 @@ import re
|
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
13
|
import threading
|
|
14
|
-
import tomllib
|
|
15
14
|
from dataclasses import dataclass
|
|
16
15
|
from pathlib import Path
|
|
17
16
|
from types import ModuleType
|
|
@@ -21,6 +20,11 @@ from packaging.requirements import InvalidRequirement, Requirement
|
|
|
21
20
|
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
22
21
|
from packaging.version import Version
|
|
23
22
|
|
|
23
|
+
if sys.version_info >= (3, 11):
|
|
24
|
+
import tomllib
|
|
25
|
+
else:
|
|
26
|
+
import tomli as tomllib
|
|
27
|
+
|
|
24
28
|
from ._errors import DependencyError, InvalidToolError, ToolConflictError, ToolNotFoundError
|
|
25
29
|
|
|
26
30
|
TOOL_SHEBANG = "#!/usr/bin/env toolfuncs"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import toolfuncs._import_path as import_path_module
|
|
13
|
+
from toolfuncs import DependencyError, import_path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_import_path_imports_plain_python_file_without_installing(
|
|
17
|
+
tmp_path: Path,
|
|
18
|
+
unique_name: str,
|
|
19
|
+
) -> None:
|
|
20
|
+
source = tmp_path / f"{unique_name}.py"
|
|
21
|
+
source.write_text("VALUE = 41\n", encoding="utf-8")
|
|
22
|
+
calls: list[list[str]] = []
|
|
23
|
+
|
|
24
|
+
imported = import_path(source, dependency_installer=calls.append)
|
|
25
|
+
|
|
26
|
+
assert imported.__name__ == unique_name
|
|
27
|
+
assert imported.__file__ == str(source)
|
|
28
|
+
assert imported.VALUE == 41
|
|
29
|
+
assert calls == []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_import_path_accepts_extensionless_script(
|
|
33
|
+
tmp_path: Path,
|
|
34
|
+
unique_name: str,
|
|
35
|
+
) -> None:
|
|
36
|
+
source = tmp_path / unique_name
|
|
37
|
+
source.write_text("VALUE = 43\n", encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
imported = import_path(source)
|
|
40
|
+
|
|
41
|
+
assert imported.__name__ == unique_name
|
|
42
|
+
assert imported.VALUE == 43
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_pep723_dependencies_are_passed_as_one_list_before_import(
|
|
46
|
+
tmp_path: Path,
|
|
47
|
+
unique_name: str,
|
|
48
|
+
) -> None:
|
|
49
|
+
source = tmp_path / f"{unique_name}.py"
|
|
50
|
+
dependency_name = f"{unique_name}_dependency"
|
|
51
|
+
calls: list[list[str]] = []
|
|
52
|
+
|
|
53
|
+
def install(requirements: list[str]) -> None:
|
|
54
|
+
calls.append(requirements)
|
|
55
|
+
dependency = ModuleType(dependency_name)
|
|
56
|
+
cast(Any, dependency).VALUE = 47
|
|
57
|
+
sys.modules[dependency_name] = dependency
|
|
58
|
+
|
|
59
|
+
source.write_text(
|
|
60
|
+
"# /// script\n"
|
|
61
|
+
'# requires-python = ">=3.10"\n'
|
|
62
|
+
'# dependencies = ["demo-one>=1", "demo-two==2"]\n'
|
|
63
|
+
"# [tool.anything]\n"
|
|
64
|
+
'# ignored = "by import_path"\n'
|
|
65
|
+
"# ///\n"
|
|
66
|
+
f"import {dependency_name}\n"
|
|
67
|
+
f"VALUE = {dependency_name}.VALUE\n",
|
|
68
|
+
encoding="utf-8",
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
imported = import_path(source, dependency_installer=install)
|
|
72
|
+
|
|
73
|
+
assert imported.VALUE == 47
|
|
74
|
+
assert calls == [["demo-one>=1", "demo-two==2"]]
|
|
75
|
+
finally:
|
|
76
|
+
sys.modules.pop(dependency_name, None)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_default_installer_uses_current_interpreter_pip(
|
|
80
|
+
tmp_path: Path,
|
|
81
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
82
|
+
unique_name: str,
|
|
83
|
+
) -> None:
|
|
84
|
+
source = tmp_path / f"{unique_name}.py"
|
|
85
|
+
source.write_text(
|
|
86
|
+
'# /// script\n# dependencies = ["demo-package>=1"]\n# ///\nVALUE = 53\n',
|
|
87
|
+
encoding="utf-8",
|
|
88
|
+
)
|
|
89
|
+
calls: list[tuple[str, ...]] = []
|
|
90
|
+
|
|
91
|
+
def successful_install(
|
|
92
|
+
argv: tuple[str, ...],
|
|
93
|
+
**_: object,
|
|
94
|
+
) -> subprocess.CompletedProcess[str]:
|
|
95
|
+
calls.append(argv)
|
|
96
|
+
return subprocess.CompletedProcess(argv, 0, "", "")
|
|
97
|
+
|
|
98
|
+
monkeypatch.setattr(import_path_module.subprocess, "run", successful_install)
|
|
99
|
+
|
|
100
|
+
assert import_path(source).VALUE == 53
|
|
101
|
+
assert calls == [
|
|
102
|
+
(
|
|
103
|
+
sys.executable,
|
|
104
|
+
"-m",
|
|
105
|
+
"pip",
|
|
106
|
+
"--disable-pip-version-check",
|
|
107
|
+
"install",
|
|
108
|
+
"--quiet",
|
|
109
|
+
"demo-package>=1",
|
|
110
|
+
)
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_dependency_failure_prevents_import(
|
|
115
|
+
tmp_path: Path,
|
|
116
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
117
|
+
unique_name: str,
|
|
118
|
+
) -> None:
|
|
119
|
+
source = tmp_path / f"{unique_name}.py"
|
|
120
|
+
source.write_text(
|
|
121
|
+
'# /// script\n# dependencies = ["missing-package"]\n# ///\nVALUE = 59\n',
|
|
122
|
+
encoding="utf-8",
|
|
123
|
+
)
|
|
124
|
+
monkeypatch.setattr(
|
|
125
|
+
import_path_module.subprocess,
|
|
126
|
+
"run",
|
|
127
|
+
lambda argv, **_: subprocess.CompletedProcess(argv, 3, "", "failed"),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
with pytest.raises(DependencyError, match="pip exited 3"):
|
|
131
|
+
import_path(source)
|
|
132
|
+
assert unique_name not in sys.modules
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_incompatible_python_fails_before_custom_installer(
|
|
136
|
+
tmp_path: Path,
|
|
137
|
+
unique_name: str,
|
|
138
|
+
) -> None:
|
|
139
|
+
source = tmp_path / f"{unique_name}.py"
|
|
140
|
+
source.write_text(
|
|
141
|
+
'# /// script\n# requires-python = "<3"\n# dependencies = ["demo"]\n# ///\n',
|
|
142
|
+
encoding="utf-8",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def forbidden_install(_requirements: list[str]) -> None:
|
|
146
|
+
raise AssertionError("installer must not run")
|
|
147
|
+
|
|
148
|
+
with pytest.raises(ValueError, match="does not admit"):
|
|
149
|
+
import_path(source, dependency_installer=forbidden_install)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_import_path_caches_by_source_and_supports_reload(
|
|
153
|
+
tmp_path: Path,
|
|
154
|
+
unique_name: str,
|
|
155
|
+
) -> None:
|
|
156
|
+
source = tmp_path / f"{unique_name}.py"
|
|
157
|
+
source.write_text("COUNT = globals().get('COUNT', 0) + 1\n", encoding="utf-8")
|
|
158
|
+
|
|
159
|
+
imported = import_path(source)
|
|
160
|
+
again = import_path(source)
|
|
161
|
+
reloaded = importlib.reload(imported)
|
|
162
|
+
|
|
163
|
+
assert imported is again is reloaded
|
|
164
|
+
assert imported.COUNT == 2
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_import_failure_removes_partial_module(
|
|
168
|
+
tmp_path: Path,
|
|
169
|
+
unique_name: str,
|
|
170
|
+
) -> None:
|
|
171
|
+
source = tmp_path / f"{unique_name}.py"
|
|
172
|
+
source.write_text("raise RuntimeError('boom')\n", encoding="utf-8")
|
|
173
|
+
|
|
174
|
+
with pytest.raises(RuntimeError, match="boom"):
|
|
175
|
+
import_path(source)
|
|
176
|
+
|
|
177
|
+
assert unique_name not in sys.modules
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_import_path_rejects_non_script_suffix(tmp_path: Path, unique_name: str) -> None:
|
|
181
|
+
source = tmp_path / f"{unique_name}.txt"
|
|
182
|
+
source.write_text("VALUE = 1\n", encoding="utf-8")
|
|
183
|
+
|
|
184
|
+
with pytest.raises(ValueError, match=r"extensionless or end in '\.py'"):
|
|
185
|
+
import_path(source)
|
|
@@ -34,7 +34,7 @@ def _script(description: str, body: str = "") -> str:
|
|
|
34
34
|
return (
|
|
35
35
|
f"{TOOL_SHEBANG}\n"
|
|
36
36
|
"# /// script\n"
|
|
37
|
-
'# requires-python = ">=3.
|
|
37
|
+
'# requires-python = ">=3.10"\n'
|
|
38
38
|
"# dependencies = []\n"
|
|
39
39
|
"# [tool.toolfuncs]\n"
|
|
40
40
|
f"# description = {json.dumps(description)}\n"
|
|
@@ -87,7 +87,7 @@ def test_prepared_runtime_does_not_run_an_inner_installer(tmp_path: Path) -> Non
|
|
|
87
87
|
source,
|
|
88
88
|
f"{TOOL_SHEBANG}\n"
|
|
89
89
|
"# /// script\n"
|
|
90
|
-
'# requires-python = ">=3.
|
|
90
|
+
'# requires-python = ">=3.10"\n'
|
|
91
91
|
'# dependencies = ["package-that-does-not-exist==0"]\n'
|
|
92
92
|
"# [tool.toolfuncs]\n"
|
|
93
93
|
'# description = "Already prepared"\n'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
version = 1
|
|
2
2
|
revision = 3
|
|
3
|
-
requires-python = ">=3.
|
|
3
|
+
requires-python = ">=3.10"
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "annotated-types"
|
|
@@ -38,8 +38,10 @@ version = "1.6.0"
|
|
|
38
38
|
source = { registry = "https://pypi.org/simple" }
|
|
39
39
|
dependencies = [
|
|
40
40
|
{ name = "colorama", marker = "os_name == 'nt'" },
|
|
41
|
+
{ name = "importlib-metadata", marker = "python_full_version < '3.10.2'" },
|
|
41
42
|
{ name = "packaging" },
|
|
42
43
|
{ name = "pyproject-hooks" },
|
|
44
|
+
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
43
45
|
]
|
|
44
46
|
sdist = { url = "https://files.pythonhosted.org/packages/4d/b7/1db48a9ce2984842c8c886432ec8a2719613322e868a966ba82a28862f25/build-1.6.0.tar.gz", hash = "sha256:bd2c8afc603e7a2e0ce70e2ea85f0a6d02043bafbd307f5bada0f98669eca5af", size = 113825, upload-time = "2026-08-27T21:01:16.458Z" }
|
|
45
47
|
wheels = [
|
|
@@ -61,6 +63,20 @@ version = "7.15.4"
|
|
|
61
63
|
source = { registry = "https://pypi.org/simple" }
|
|
62
64
|
sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" }
|
|
63
65
|
wheels = [
|
|
66
|
+
{ url = "https://files.pythonhosted.org/packages/30/70/b052a519a584663a7bd052841a2debe11c8309ec49a7786340003f9c0a02/coverage-7.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264", size = 222245, upload-time = "2026-08-06T13:46:55.253Z" },
|
|
67
|
+
{ url = "https://files.pythonhosted.org/packages/67/39/892fa511aba3d1c3c8f49509a0ff5c71eab9f9f88d08e1a38da395821660/coverage-7.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d", size = 222762, upload-time = "2026-08-06T13:46:57.848Z" },
|
|
68
|
+
{ url = "https://files.pythonhosted.org/packages/9f/95/b2c724ce1e64bc23cb5b1d7eeffa9548dc3d811f7a6297b2d01607f4e062/coverage-7.15.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150", size = 249498, upload-time = "2026-08-06T13:46:59.012Z" },
|
|
69
|
+
{ url = "https://files.pythonhosted.org/packages/0b/4f/b1973f67a1382af65b572a31ed692f8e490a6ad707191eab59148376832a/coverage-7.15.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15", size = 251328, upload-time = "2026-08-06T13:47:00.764Z" },
|
|
70
|
+
{ url = "https://files.pythonhosted.org/packages/a2/09/03efa6722a132abcac91b32a60b64b240dd707c189c64eee697e48992c96/coverage-7.15.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0", size = 253194, upload-time = "2026-08-06T13:47:01.976Z" },
|
|
71
|
+
{ url = "https://files.pythonhosted.org/packages/45/63/8299201d9c80fb65551ce99c966cab83d706ec4066ac999bef08201346de/coverage-7.15.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d", size = 255106, upload-time = "2026-08-06T13:47:03.281Z" },
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/ee/16/26fd8a691eb8d9a230128685f6d23309d7402cb030aa553001788c8c50fc/coverage-7.15.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4", size = 250177, upload-time = "2026-08-06T13:47:04.713Z" },
|
|
73
|
+
{ url = "https://files.pythonhosted.org/packages/ad/ef/3c7556f33783a0a566e01443ca62bd8eb2cdfe22d271efdc02e08beb5654/coverage-7.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54", size = 251234, upload-time = "2026-08-06T13:47:06.104Z" },
|
|
74
|
+
{ url = "https://files.pythonhosted.org/packages/29/49/640a34043edac950738f36a3567832db5731d4cb2ed84b59cdb89c6bccbf/coverage-7.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20", size = 249237, upload-time = "2026-08-06T13:47:07.467Z" },
|
|
75
|
+
{ url = "https://files.pythonhosted.org/packages/48/f5/e80f212669dd1be954ff844f883ef11a437ef4fd0089c6e0effc7b66b15d/coverage-7.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325", size = 253050, upload-time = "2026-08-06T13:47:08.748Z" },
|
|
76
|
+
{ url = "https://files.pythonhosted.org/packages/c7/e9/e5da0fe39f7fde1bca9edc09c60921bb5fdba4cec7db5bbad41ddfd8c230/coverage-7.15.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88", size = 249508, upload-time = "2026-08-06T13:47:10.072Z" },
|
|
77
|
+
{ url = "https://files.pythonhosted.org/packages/7d/38/41bf25774a0c8bba6b467f917cb1c9a0a2605e02dc93aad489fc7050ed59/coverage-7.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04", size = 250110, upload-time = "2026-08-06T13:47:11.35Z" },
|
|
78
|
+
{ url = "https://files.pythonhosted.org/packages/89/6e/26f2e54b79acc29d179ee4272922625aedb69198c4eb61f7ff4f098f3c78/coverage-7.15.4-cp310-cp310-win32.whl", hash = "sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338", size = 224294, upload-time = "2026-08-06T13:47:12.753Z" },
|
|
79
|
+
{ url = "https://files.pythonhosted.org/packages/7b/06/9a318fc3ae040d4d6cb2d86101c6aa963fab20899a5c58666adf52cde0ca/coverage-7.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360", size = 224919, upload-time = "2026-08-06T13:47:14.17Z" },
|
|
64
80
|
{ url = "https://files.pythonhosted.org/packages/2a/66/edcec7d7a0b524aa8923e22925fde6fe50ce005a113dca13ae1581455c4c/coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490", size = 222367, upload-time = "2026-08-06T13:47:15.578Z" },
|
|
65
81
|
{ url = "https://files.pythonhosted.org/packages/e6/c6/ab8de429e2e8548faf58ec7e1674a4ce00414b4113942d3fe87109cf0f68/coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e", size = 222874, upload-time = "2026-08-06T13:47:16.961Z" },
|
|
66
82
|
{ url = "https://files.pythonhosted.org/packages/be/c4/3b7b49587e8a6b9af79b3eb468d443d6042b6d65b47aa26586846a0d6566/coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7", size = 253287, upload-time = "2026-08-06T13:47:18.291Z" },
|
|
@@ -183,6 +199,8 @@ dependencies = [
|
|
|
183
199
|
{ name = "docstring-parser" },
|
|
184
200
|
{ name = "rich" },
|
|
185
201
|
{ name = "rich-rst" },
|
|
202
|
+
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
203
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
|
186
204
|
]
|
|
187
205
|
sdist = { url = "https://files.pythonhosted.org/packages/ca/4e/4afd08d7dd836c436bbc5999f2743eda0ad4eac8946a8eb4b72241e3c555/cyclopts-4.23.3.tar.gz", hash = "sha256:4299ec47f5be853f9a114fcc534c84d42bbf19fefa303994597ecb7e5fd3082b", size = 197067, upload-time = "2026-08-26T18:16:35.688Z" }
|
|
188
206
|
wheels = [
|
|
@@ -198,6 +216,30 @@ wheels = [
|
|
|
198
216
|
{ url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
|
|
199
217
|
]
|
|
200
218
|
|
|
219
|
+
[[package]]
|
|
220
|
+
name = "exceptiongroup"
|
|
221
|
+
version = "1.3.1"
|
|
222
|
+
source = { registry = "https://pypi.org/simple" }
|
|
223
|
+
dependencies = [
|
|
224
|
+
{ name = "typing-extensions" },
|
|
225
|
+
]
|
|
226
|
+
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
227
|
+
wheels = [
|
|
228
|
+
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
[[package]]
|
|
232
|
+
name = "importlib-metadata"
|
|
233
|
+
version = "9.0.0"
|
|
234
|
+
source = { registry = "https://pypi.org/simple" }
|
|
235
|
+
dependencies = [
|
|
236
|
+
{ name = "zipp" },
|
|
237
|
+
]
|
|
238
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" }
|
|
239
|
+
wheels = [
|
|
240
|
+
{ url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" },
|
|
241
|
+
]
|
|
242
|
+
|
|
201
243
|
[[package]]
|
|
202
244
|
name = "iniconfig"
|
|
203
245
|
version = "2.3.0"
|
|
@@ -295,6 +337,20 @@ dependencies = [
|
|
|
295
337
|
]
|
|
296
338
|
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
|
297
339
|
wheels = [
|
|
340
|
+
{ url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
|
|
341
|
+
{ url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
|
|
342
|
+
{ url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
|
|
343
|
+
{ url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
|
|
344
|
+
{ url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
|
|
345
|
+
{ url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
|
|
346
|
+
{ url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
|
|
347
|
+
{ url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
|
|
348
|
+
{ url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
|
|
349
|
+
{ url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
|
|
350
|
+
{ url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
|
|
351
|
+
{ url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
|
|
352
|
+
{ url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
|
|
353
|
+
{ url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
|
|
298
354
|
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
|
|
299
355
|
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
|
|
300
356
|
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
|
|
@@ -412,10 +468,12 @@ version = "9.1.1"
|
|
|
412
468
|
source = { registry = "https://pypi.org/simple" }
|
|
413
469
|
dependencies = [
|
|
414
470
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
471
|
+
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
415
472
|
{ name = "iniconfig" },
|
|
416
473
|
{ name = "packaging" },
|
|
417
474
|
{ name = "pluggy" },
|
|
418
475
|
{ name = "pygments" },
|
|
476
|
+
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
419
477
|
]
|
|
420
478
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
|
421
479
|
wheels = [
|
|
@@ -543,7 +601,7 @@ wheels = [
|
|
|
543
601
|
|
|
544
602
|
[[package]]
|
|
545
603
|
name = "toolfuncs"
|
|
546
|
-
version = "0.3.
|
|
604
|
+
version = "0.3.2"
|
|
547
605
|
source = { editable = "." }
|
|
548
606
|
dependencies = [
|
|
549
607
|
{ name = "cyclopts" },
|
|
@@ -551,6 +609,7 @@ dependencies = [
|
|
|
551
609
|
{ name = "pip" },
|
|
552
610
|
{ name = "pydantic" },
|
|
553
611
|
{ name = "pydantic-core" },
|
|
612
|
+
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
554
613
|
]
|
|
555
614
|
|
|
556
615
|
[package.optional-dependencies]
|
|
@@ -565,6 +624,7 @@ dev = [
|
|
|
565
624
|
{ name = "pytest" },
|
|
566
625
|
{ name = "pytest-cov" },
|
|
567
626
|
{ name = "ruff" },
|
|
627
|
+
{ name = "tomli" },
|
|
568
628
|
{ name = "typed-agent-hooks" },
|
|
569
629
|
]
|
|
570
630
|
|
|
@@ -575,6 +635,7 @@ requires-dist = [
|
|
|
575
635
|
{ name = "pip", specifier = ">=25" },
|
|
576
636
|
{ name = "pydantic", specifier = ">=2,<3" },
|
|
577
637
|
{ name = "pydantic-core", specifier = ">=2,<3" },
|
|
638
|
+
{ name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.2,<3" },
|
|
578
639
|
{ name = "typed-agent-hooks", marker = "extra == 'hooks'" },
|
|
579
640
|
]
|
|
580
641
|
provides-extras = ["hooks"]
|
|
@@ -586,6 +647,7 @@ dev = [
|
|
|
586
647
|
{ name = "pytest", specifier = ">=8" },
|
|
587
648
|
{ name = "pytest-cov", specifier = ">=6" },
|
|
588
649
|
{ name = "ruff", specifier = ">=0.12" },
|
|
650
|
+
{ name = "tomli", specifier = ">=2.2,<3" },
|
|
589
651
|
{ name = "typed-agent-hooks" },
|
|
590
652
|
]
|
|
591
653
|
|
|
@@ -623,3 +685,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c
|
|
|
623
685
|
wheels = [
|
|
624
686
|
{ url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
|
|
625
687
|
]
|
|
688
|
+
|
|
689
|
+
[[package]]
|
|
690
|
+
name = "zipp"
|
|
691
|
+
version = "4.1.0"
|
|
692
|
+
source = { registry = "https://pypi.org/simple" }
|
|
693
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" }
|
|
694
|
+
wheels = [
|
|
695
|
+
{ url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" },
|
|
696
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|