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.
@@ -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.11"
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
- Direct Python calls return ordinary Python objects and raise the original exceptions. `toolfuncs.load()` prepares the script's 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
+ `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.11"
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.11"
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
- Direct Python calls return ordinary Python objects and raise the original exceptions. `toolfuncs.load()` prepares the script's 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`.
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.11"
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.0"
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.11"
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 = "py311"
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.11"
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.0"
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.11"\n'
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.11"\n'
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'
@@ -27,7 +27,7 @@ def _script(
27
27
  body: str = "",
28
28
  *,
29
29
  dependencies: list[str] | None = None,
30
- requires_python: str = ">=3.11",
30
+ requires_python: str = ">=3.10",
31
31
  shebang: str = TOOL_SHEBANG,
32
32
  ) -> str:
33
33
  return (
@@ -1,6 +1,6 @@
1
1
  version = 1
2
2
  revision = 3
3
- requires-python = ">=3.11"
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.0"
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