toolfuncs 0.3.2__tar.gz → 0.3.4__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.2 → toolfuncs-0.3.4}/PKG-INFO +1 -1
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/pyproject.toml +1 -1
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/src/toolfuncs/__init__.py +1 -1
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/src/toolfuncs/__main__.py +27 -3
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/tests/test_sdk_cli.py +56 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/uv.lock +1 -1
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/.github/workflows/release.yml +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/.gitignore +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/.python-version +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/README.md +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/docs/contract.md +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/src/toolfuncs/_errors.py +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/src/toolfuncs/_import_path.py +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/src/toolfuncs/_setup.py +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/src/toolfuncs/_tools.py +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/src/toolfuncs/agent_hooks.py +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/src/toolfuncs/py.typed +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/src/toolfuncs/sdk.py +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/tests/__init__.py +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/tests/conftest.py +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/tests/test_agent_hooks.py +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/tests/test_import_path.py +0 -0
- {toolfuncs-0.3.2 → toolfuncs-0.3.4}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: toolfuncs
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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
|
|
@@ -4,13 +4,15 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import sys
|
|
7
|
-
from collections.abc import Sequence
|
|
7
|
+
from collections.abc import Iterator, Sequence
|
|
8
|
+
from contextlib import contextmanager
|
|
8
9
|
from dataclasses import asdict
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
from pydantic_core import to_jsonable_python
|
|
13
14
|
|
|
15
|
+
from . import __version__
|
|
14
16
|
from ._setup import setup
|
|
15
17
|
from ._tools import (
|
|
16
18
|
ALLOW_FLOATING_VCS_SHEBANG,
|
|
@@ -24,6 +26,7 @@ from .sdk import _run
|
|
|
24
26
|
|
|
25
27
|
_HELP = f"""\
|
|
26
28
|
Usage:
|
|
29
|
+
toolfuncs --version
|
|
27
30
|
toolfuncs list
|
|
28
31
|
toolfuncs setup [--bin-dir DIRECTORY]
|
|
29
32
|
|
|
@@ -35,6 +38,18 @@ Floating Git revisions require the explicit shebang:
|
|
|
35
38
|
"""
|
|
36
39
|
|
|
37
40
|
|
|
41
|
+
@contextmanager
|
|
42
|
+
def _tool_program_name(source: Path) -> Iterator[None]:
|
|
43
|
+
"""Expose the source executable as the invoked app's program name."""
|
|
44
|
+
|
|
45
|
+
previous = sys.argv[0]
|
|
46
|
+
sys.argv[0] = source.name
|
|
47
|
+
try:
|
|
48
|
+
yield
|
|
49
|
+
finally:
|
|
50
|
+
sys.argv[0] = previous
|
|
51
|
+
|
|
52
|
+
|
|
38
53
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
39
54
|
"""Run a source supplied by its shebang or dispatch a management command."""
|
|
40
55
|
|
|
@@ -42,13 +57,20 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
42
57
|
if not tokens or tokens[0] in {"-h", "--help", "help"}:
|
|
43
58
|
print(_HELP, end="")
|
|
44
59
|
return 0
|
|
60
|
+
if tokens[0] in {"-V", "--version"}:
|
|
61
|
+
if len(tokens) != 1:
|
|
62
|
+
raise SystemExit("toolfuncs --version accepts no arguments")
|
|
63
|
+
print(__version__)
|
|
64
|
+
return 0
|
|
45
65
|
|
|
46
66
|
command = tokens.pop(0)
|
|
47
67
|
if command == "__run-source":
|
|
48
68
|
if not tokens:
|
|
49
69
|
raise SystemExit("toolfuncs internal error: missing prepared source")
|
|
50
70
|
source = Path(tokens.pop(0))
|
|
51
|
-
|
|
71
|
+
module = _load_prepared_source(source)
|
|
72
|
+
with _tool_program_name(source):
|
|
73
|
+
result = _run(module, tokens)
|
|
52
74
|
return result if isinstance(result, int) else 0
|
|
53
75
|
if command == "__validate-source":
|
|
54
76
|
if len(tokens) != 1:
|
|
@@ -68,7 +90,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
68
90
|
source = Path(command)
|
|
69
91
|
if not source.is_file():
|
|
70
92
|
raise SystemExit(f"unknown toolfuncs command or missing source: {command}")
|
|
71
|
-
|
|
93
|
+
module = _load_source(source)
|
|
94
|
+
with _tool_program_name(source):
|
|
95
|
+
result = _run(module, tokens)
|
|
72
96
|
return result if isinstance(result, int) else 0
|
|
73
97
|
|
|
74
98
|
|
|
@@ -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.4\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)
|
|
@@ -81,6 +90,43 @@ def test_runtime_requires_a_module_level_app(tmp_path: Path) -> None:
|
|
|
81
90
|
assert "must define 'app'" in completed.stderr
|
|
82
91
|
|
|
83
92
|
|
|
93
|
+
@pytest.mark.parametrize("prepared", [False, True])
|
|
94
|
+
@pytest.mark.parametrize(
|
|
95
|
+
("app_definition", "expected_name"),
|
|
96
|
+
[
|
|
97
|
+
("app = toolfuncs.App()", "named_tool"),
|
|
98
|
+
("app = toolfuncs.App(name='explicit')", "explicit"),
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
def test_runtime_uses_source_name_unless_app_name_is_explicit(
|
|
102
|
+
tmp_path: Path,
|
|
103
|
+
prepared: bool,
|
|
104
|
+
app_definition: str,
|
|
105
|
+
expected_name: str,
|
|
106
|
+
) -> None:
|
|
107
|
+
source = tmp_path / "named_tool"
|
|
108
|
+
_write_executable(
|
|
109
|
+
source,
|
|
110
|
+
_script(
|
|
111
|
+
"Named tool",
|
|
112
|
+
"import toolfuncs\n\n"
|
|
113
|
+
f"{app_definition}\n\n"
|
|
114
|
+
"@app.command\n"
|
|
115
|
+
"def ping() -> str:\n"
|
|
116
|
+
" return 'pong'\n",
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
runtime_prefix = ["__run-source"] if prepared else []
|
|
120
|
+
|
|
121
|
+
completed = run_python(
|
|
122
|
+
["-m", "toolfuncs", *runtime_prefix, str(source), "--help"],
|
|
123
|
+
cwd=tmp_path,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
assert completed.returncode == 0, completed.stderr
|
|
127
|
+
assert completed.stdout.splitlines()[0] == f"Usage: {expected_name} COMMAND"
|
|
128
|
+
|
|
129
|
+
|
|
84
130
|
def test_prepared_runtime_does_not_run_an_inner_installer(tmp_path: Path) -> None:
|
|
85
131
|
source = tmp_path / "prepared"
|
|
86
132
|
_write_executable(
|
|
@@ -162,6 +208,14 @@ def test_direct_command_dynamic_import_and_static_listing(tmp_path: Path) -> Non
|
|
|
162
208
|
text=True,
|
|
163
209
|
check=False,
|
|
164
210
|
)
|
|
211
|
+
helped = subprocess.run(
|
|
212
|
+
[str(source), "--help"],
|
|
213
|
+
cwd=tmp_path,
|
|
214
|
+
env={**os.environ, **environment},
|
|
215
|
+
capture_output=True,
|
|
216
|
+
text=True,
|
|
217
|
+
check=False,
|
|
218
|
+
)
|
|
165
219
|
imported = run_python(
|
|
166
220
|
["-c", "from toolfuncs import greet; print(greet.hello('Ada')['message'])"],
|
|
167
221
|
cwd=tmp_path,
|
|
@@ -171,6 +225,8 @@ def test_direct_command_dynamic_import_and_static_listing(tmp_path: Path) -> Non
|
|
|
171
225
|
|
|
172
226
|
assert direct.returncode == 0, direct.stderr
|
|
173
227
|
assert json.loads(direct.stdout) == {"message": "Hello, Ada!"}
|
|
228
|
+
assert helped.returncode == 0, helped.stderr
|
|
229
|
+
assert helped.stdout.splitlines()[0] == "Usage: greet COMMAND"
|
|
174
230
|
assert imported.returncode == 0, imported.stderr
|
|
175
231
|
assert imported.stdout == "Hello, Ada.\n"
|
|
176
232
|
record = next(item for item in json.loads(listed.stdout) if item["name"] == "greet")
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|