toolfuncs 0.3.1__tar.gz → 0.3.3__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,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: toolfuncs
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Executable typed Python function tools discovered through PATH
5
5
  Project-URL: Repository, https://github.com/nimashoghi/toolfuncs
6
6
  Project-URL: Issues, https://github.com/nimashoghi/toolfuncs/issues
@@ -80,7 +80,29 @@ import toolfuncs
80
80
  weather = toolfuncs.load("weather")
81
81
  ```
82
82
 
83
- 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`.
84
106
 
85
107
  ## Source contract
86
108
 
@@ -62,7 +62,29 @@ import toolfuncs
62
62
  weather = toolfuncs.load("weather")
63
63
  ```
64
64
 
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`.
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`.
66
88
 
67
89
  ## Source contract
68
90
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "toolfuncs"
3
- version = "0.3.1"
3
+ version = "0.3.3"
4
4
  description = "Executable typed Python function tools discovered through PATH"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -38,6 +38,7 @@ dev = [
38
38
  "pytest>=8",
39
39
  "pytest-cov>=6",
40
40
  "ruff>=0.12",
41
+ "tomli>=2.2,<3",
41
42
  "typed-agent-hooks",
42
43
  ]
43
44
 
@@ -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.1"
32
+ __version__ = "0.3.3"
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",
@@ -11,6 +11,7 @@ from typing import Any
11
11
 
12
12
  from pydantic_core import to_jsonable_python
13
13
 
14
+ from . import __version__
14
15
  from ._setup import setup
15
16
  from ._tools import (
16
17
  ALLOW_FLOATING_VCS_SHEBANG,
@@ -24,6 +25,7 @@ from .sdk import _run
24
25
 
25
26
  _HELP = f"""\
26
27
  Usage:
28
+ toolfuncs --version
27
29
  toolfuncs list
28
30
  toolfuncs setup [--bin-dir DIRECTORY]
29
31
 
@@ -42,6 +44,11 @@ def main(argv: Sequence[str] | None = None) -> int:
42
44
  if not tokens or tokens[0] in {"-h", "--help", "help"}:
43
45
  print(_HELP, end="")
44
46
  return 0
47
+ if tokens[0] in {"-V", "--version"}:
48
+ if len(tokens) != 1:
49
+ raise SystemExit("toolfuncs --version accepts no arguments")
50
+ print(__version__)
51
+ return 0
45
52
 
46
53
  command = tokens.pop(0)
47
54
  if command == "__run-source":
@@ -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"]
@@ -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)
@@ -25,6 +25,15 @@ from .conftest import run_python
25
25
  _INSTALL_HOOKS = setup_impl._install_hooks
26
26
 
27
27
 
28
+ @pytest.mark.parametrize("flag", ["--version", "-V"])
29
+ def test_cli_prints_installed_version(tmp_path: Path, flag: str) -> None:
30
+ completed = run_python(["-m", "toolfuncs", flag], cwd=tmp_path)
31
+
32
+ assert completed.returncode == 0
33
+ assert completed.stdout == "0.3.3\n"
34
+ assert completed.stderr == ""
35
+
36
+
28
37
  @pytest.fixture(autouse=True)
29
38
  def _avoid_installing_live_agent_hooks(monkeypatch: pytest.MonkeyPatch) -> None:
30
39
  monkeypatch.setattr(setup_impl, "_install_hooks", lambda _hook_wrapper: None)
@@ -601,7 +601,7 @@ wheels = [
601
601
 
602
602
  [[package]]
603
603
  name = "toolfuncs"
604
- version = "0.3.1"
604
+ version = "0.3.3"
605
605
  source = { editable = "." }
606
606
  dependencies = [
607
607
  { name = "cyclopts" },
@@ -624,6 +624,7 @@ dev = [
624
624
  { name = "pytest" },
625
625
  { name = "pytest-cov" },
626
626
  { name = "ruff" },
627
+ { name = "tomli" },
627
628
  { name = "typed-agent-hooks" },
628
629
  ]
629
630
 
@@ -646,6 +647,7 @@ dev = [
646
647
  { name = "pytest", specifier = ">=8" },
647
648
  { name = "pytest-cov", specifier = ">=6" },
648
649
  { name = "ruff", specifier = ">=0.12" },
650
+ { name = "tomli", specifier = ">=2.2,<3" },
649
651
  { name = "typed-agent-hooks" },
650
652
  ]
651
653
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes