toolfuncs 0.6.0__tar.gz → 0.7.0__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.
Files changed (25) hide show
  1. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/PKG-INFO +34 -16
  2. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/README.md +32 -14
  3. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/docs/contract.md +7 -7
  4. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/pyproject.toml +2 -2
  5. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/src/toolfuncs/__main__.py +5 -1
  6. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/src/toolfuncs/_import_path.py +8 -4
  7. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/tests/test_import_path.py +24 -0
  8. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/tests/test_sdk_cli.py +35 -1
  9. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/uv.lock +1 -1
  10. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/.github/workflows/release.yml +0 -0
  11. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/.gitignore +0 -0
  12. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/.python-version +0 -0
  13. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/src/toolfuncs/__init__.py +0 -0
  14. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/src/toolfuncs/_doctor.py +0 -0
  15. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/src/toolfuncs/_errors.py +0 -0
  16. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/src/toolfuncs/_setup.py +0 -0
  17. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/src/toolfuncs/_tools.py +0 -0
  18. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/src/toolfuncs/agent_hooks.py +0 -0
  19. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/src/toolfuncs/py.typed +0 -0
  20. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/src/toolfuncs/sdk.py +0 -0
  21. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/tests/__init__.py +0 -0
  22. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/tests/conftest.py +0 -0
  23. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/tests/test_agent_hooks.py +0 -0
  24. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/tests/test_doctor.py +0 -0
  25. {toolfuncs-0.6.0 → toolfuncs-0.7.0}/tests/test_tools.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: toolfuncs
3
- Version: 0.6.0
4
- Summary: Executable typed Python function tools discovered through PATH
3
+ Version: 0.7.0
4
+ Summary: Executable typed Python function tools invoked by path or discovered through PATH
5
5
  Project-URL: Repository, https://github.com/nimashoghi/toolfuncs
6
6
  Project-URL: Issues, https://github.com/nimashoghi/toolfuncs/issues
7
7
  Author-email: Nima Shoghi <nima@boltz.bio>
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
20
20
 
21
21
  `toolfuncs` turns typed Python functions into tools that have matching Python and command-line interfaces.
22
22
 
23
- A toolfunc is one executable Python script on `PATH`. Its lowercase kebab-case filename is its command name, and its Python import name is the same spelling with hyphens replaced by underscores. Its implementation may import any packages declared in its PEP 723 requirements, but packages and projects are not themselves discoverable toolfuncs.
23
+ A toolfunc is one executable Python script. Its lowercase kebab-case filename is its command name, and its Python import name is the same spelling with hyphens replaced by underscores. A known source can be called or imported directly from any path. Putting it on `PATH` additionally makes its filename a shell command and exposes it to discovery and dynamic Python lookup. Its implementation may import any packages declared in its PEP 723 requirements, but packages and projects are not themselves toolfuncs.
24
24
 
25
25
  ## Write a toolfunc
26
26
 
@@ -56,25 +56,44 @@ def current(city: str) -> dict[str, object]:
56
56
  return weather_client.current(city)
57
57
  ```
58
58
 
59
- Make it executable and place it in any existing `PATH` directory:
59
+ Make it executable:
60
60
 
61
61
  ```console
62
62
  chmod +x weather-report
63
- mv weather-report ~/.local/bin/
64
63
  ```
65
64
 
66
- That is the complete registration operation.
65
+ No registration operation is required. Move it to an existing `PATH` directory only when it should be globally discoverable:
66
+
67
+ ```console
68
+ mv weather-report ~/.local/bin/
69
+ ```
67
70
 
68
71
  ## Call it
69
72
 
70
- The executable is the CLI:
73
+ A known source can be run explicitly without adding it to `PATH`:
74
+
75
+ ```console
76
+ $ toolfuncs ./weather-report current London
77
+ {"city": "London", "temperature": 18}
78
+ ```
79
+
80
+ When the source is on `PATH`, its filename is also the CLI:
71
81
 
72
82
  ```console
73
83
  $ weather-report current London
74
84
  {"city": "London", "temperature": 18}
75
85
  ```
76
86
 
77
- The same file is dynamically importable in a Python process that has `toolfuncs` installed and the same `PATH`:
87
+ A known source is importable by path:
88
+
89
+ ```python
90
+ import toolfuncs as tools
91
+
92
+ weather_report = tools.import_path("./weather-report")
93
+ conditions = weather_report.current("London")
94
+ ```
95
+
96
+ When the source is on `PATH`, the same file is also dynamically importable by name in a Python process with that `PATH`:
78
97
 
79
98
  ```python
80
99
  import toolfuncs as tools
@@ -82,8 +101,7 @@ import toolfuncs as tools
82
101
  conditions = tools.weather_report.current("London")
83
102
  ```
84
103
 
85
- `import_path()` is the narrower utility for a local script whose path is already
86
- known:
104
+ `import_path()` also works for an ordinary local Python script whose path is already known:
87
105
 
88
106
  ```python
89
107
  import toolfuncs as tools
@@ -91,8 +109,7 @@ import toolfuncs as tools
91
109
  module = tools.import_path("scripts/prepare_data.py")
92
110
  ```
93
111
 
94
- It accepts one ordinary `.py` file or extensionless script and derives the
95
- module name from its filename. It does not require a shebang, executable bit,
112
+ It accepts one ordinary `.py` file or extensionless script and derives the module name from its filename. An extensionless lowercase kebab-case filename is mapped to its snake-case Python name. It does not require a shebang, executable bit,
96
113
  `[tool.toolfuncs]` metadata, or `App`, and it does not import packages, projects,
97
114
  distributions, or URLs. When an optional PEP 723 block exists, the default
98
115
  installer installs all declared requirements together into the running Python
@@ -110,9 +127,9 @@ Direct Python calls return ordinary Python objects and raise the original except
110
127
 
111
128
  ## Source contract
112
129
 
113
- A discoverable toolfunc must:
130
+ A toolfunc source must:
114
131
 
115
- 1. be one executable file in a directory on `PATH`;
132
+ 1. be one executable file;
116
133
  2. have an extensionless lowercase ASCII kebab-case filename;
117
134
  3. start with one of the two exact toolfuncs shebangs described below;
118
135
  4. contain exactly one PEP 723 `script` block with `dependencies`, a one-line description, and required `cli_name` and `python_name` documentation that exactly matches the filename-derived identities;
@@ -215,12 +232,13 @@ Setup does not discover, copy, register, link, synchronize, or remove tools. The
215
232
  ## Management commands
216
233
 
217
234
  ```console
235
+ toolfuncs SOURCE [ARGS...]
218
236
  toolfuncs doctor SOURCE
219
237
  toolfuncs list
220
238
  toolfuncs setup [--bin-dir DIRECTORY]
221
239
  ```
222
240
 
223
- The source-path argument supplied automatically by `#!/usr/bin/env toolfuncs` is runtime plumbing rather than another user-facing command. Tools are invoked directly by their own names.
241
+ `toolfuncs SOURCE [ARGS...]` runs a known source directly. This is the explicit-source interface, not a name dispatcher: `toolfuncs weather-report` resolves `weather-report` as a source path rather than searching `PATH` for that tool name. A toolfunc on `PATH` can instead be invoked directly by its own filename.
224
242
 
225
243
  ## Agent integration
226
244
 
@@ -245,7 +263,7 @@ uv run ruff check .
245
263
  uv run basedpyright
246
264
  ```
247
265
 
248
- The exact 0.4 guarantees and non-goals are recorded in [the contract](docs/contract.md).
266
+ The exact guarantees and non-goals are recorded in [the contract](docs/contract.md).
249
267
 
250
268
  ## Releasing
251
269
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  `toolfuncs` turns typed Python functions into tools that have matching Python and command-line interfaces.
4
4
 
5
- A toolfunc is one executable Python script on `PATH`. Its lowercase kebab-case filename is its command name, and its Python import name is the same spelling with hyphens replaced by underscores. Its implementation may import any packages declared in its PEP 723 requirements, but packages and projects are not themselves discoverable toolfuncs.
5
+ A toolfunc is one executable Python script. Its lowercase kebab-case filename is its command name, and its Python import name is the same spelling with hyphens replaced by underscores. A known source can be called or imported directly from any path. Putting it on `PATH` additionally makes its filename a shell command and exposes it to discovery and dynamic Python lookup. Its implementation may import any packages declared in its PEP 723 requirements, but packages and projects are not themselves toolfuncs.
6
6
 
7
7
  ## Write a toolfunc
8
8
 
@@ -38,25 +38,44 @@ def current(city: str) -> dict[str, object]:
38
38
  return weather_client.current(city)
39
39
  ```
40
40
 
41
- Make it executable and place it in any existing `PATH` directory:
41
+ Make it executable:
42
42
 
43
43
  ```console
44
44
  chmod +x weather-report
45
- mv weather-report ~/.local/bin/
46
45
  ```
47
46
 
48
- That is the complete registration operation.
47
+ No registration operation is required. Move it to an existing `PATH` directory only when it should be globally discoverable:
48
+
49
+ ```console
50
+ mv weather-report ~/.local/bin/
51
+ ```
49
52
 
50
53
  ## Call it
51
54
 
52
- The executable is the CLI:
55
+ A known source can be run explicitly without adding it to `PATH`:
56
+
57
+ ```console
58
+ $ toolfuncs ./weather-report current London
59
+ {"city": "London", "temperature": 18}
60
+ ```
61
+
62
+ When the source is on `PATH`, its filename is also the CLI:
53
63
 
54
64
  ```console
55
65
  $ weather-report current London
56
66
  {"city": "London", "temperature": 18}
57
67
  ```
58
68
 
59
- The same file is dynamically importable in a Python process that has `toolfuncs` installed and the same `PATH`:
69
+ A known source is importable by path:
70
+
71
+ ```python
72
+ import toolfuncs as tools
73
+
74
+ weather_report = tools.import_path("./weather-report")
75
+ conditions = weather_report.current("London")
76
+ ```
77
+
78
+ When the source is on `PATH`, the same file is also dynamically importable by name in a Python process with that `PATH`:
60
79
 
61
80
  ```python
62
81
  import toolfuncs as tools
@@ -64,8 +83,7 @@ import toolfuncs as tools
64
83
  conditions = tools.weather_report.current("London")
65
84
  ```
66
85
 
67
- `import_path()` is the narrower utility for a local script whose path is already
68
- known:
86
+ `import_path()` also works for an ordinary local Python script whose path is already known:
69
87
 
70
88
  ```python
71
89
  import toolfuncs as tools
@@ -73,8 +91,7 @@ import toolfuncs as tools
73
91
  module = tools.import_path("scripts/prepare_data.py")
74
92
  ```
75
93
 
76
- It accepts one ordinary `.py` file or extensionless script and derives the
77
- module name from its filename. It does not require a shebang, executable bit,
94
+ It accepts one ordinary `.py` file or extensionless script and derives the module name from its filename. An extensionless lowercase kebab-case filename is mapped to its snake-case Python name. It does not require a shebang, executable bit,
78
95
  `[tool.toolfuncs]` metadata, or `App`, and it does not import packages, projects,
79
96
  distributions, or URLs. When an optional PEP 723 block exists, the default
80
97
  installer installs all declared requirements together into the running Python
@@ -92,9 +109,9 @@ Direct Python calls return ordinary Python objects and raise the original except
92
109
 
93
110
  ## Source contract
94
111
 
95
- A discoverable toolfunc must:
112
+ A toolfunc source must:
96
113
 
97
- 1. be one executable file in a directory on `PATH`;
114
+ 1. be one executable file;
98
115
  2. have an extensionless lowercase ASCII kebab-case filename;
99
116
  3. start with one of the two exact toolfuncs shebangs described below;
100
117
  4. contain exactly one PEP 723 `script` block with `dependencies`, a one-line description, and required `cli_name` and `python_name` documentation that exactly matches the filename-derived identities;
@@ -197,12 +214,13 @@ Setup does not discover, copy, register, link, synchronize, or remove tools. The
197
214
  ## Management commands
198
215
 
199
216
  ```console
217
+ toolfuncs SOURCE [ARGS...]
200
218
  toolfuncs doctor SOURCE
201
219
  toolfuncs list
202
220
  toolfuncs setup [--bin-dir DIRECTORY]
203
221
  ```
204
222
 
205
- The source-path argument supplied automatically by `#!/usr/bin/env toolfuncs` is runtime plumbing rather than another user-facing command. Tools are invoked directly by their own names.
223
+ `toolfuncs SOURCE [ARGS...]` runs a known source directly. This is the explicit-source interface, not a name dispatcher: `toolfuncs weather-report` resolves `weather-report` as a source path rather than searching `PATH` for that tool name. A toolfunc on `PATH` can instead be invoked directly by its own filename.
206
224
 
207
225
  ## Agent integration
208
226
 
@@ -227,7 +245,7 @@ uv run ruff check .
227
245
  uv run basedpyright
228
246
  ```
229
247
 
230
- The exact 0.4 guarantees and non-goals are recorded in [the contract](docs/contract.md).
248
+ The exact guarantees and non-goals are recorded in [the contract](docs/contract.md).
231
249
 
232
250
  ## Releasing
233
251
 
@@ -1,4 +1,4 @@
1
- # toolfuncs 0.6 contract
1
+ # toolfuncs 0.7 contract
2
2
 
3
3
  ## Purpose
4
4
 
@@ -6,9 +6,9 @@ Toolfuncs projects one explicitly registered set of typed Python functions into
6
6
 
7
7
  ## Tool identity and source
8
8
 
9
- A toolfunc is one executable regular file or symlink in a directory on `PATH`. Its first line is exactly `#!/usr/bin/env toolfuncs`, or the explicit floating-VCS form `#!/usr/bin/env -S toolfuncs --allow-floating-vcs`.
9
+ A toolfunc is one executable regular file or symlink. Its first line is exactly `#!/usr/bin/env toolfuncs`, or the explicit floating-VCS form `#!/usr/bin/env -S toolfuncs --allow-floating-vcs`.
10
10
 
11
- The PATH-visible extensionless filename is the CLI command and must use lowercase ASCII kebab-case: a letter followed by letters or digits, with optional single hyphens separating further nonempty letter-or-digit segments. The Python name is the CLI name with every hyphen replaced by an underscore. A Python name that conflicts with `toolfuncs.sdk` or `toolfuncs.import_path` is invalid.
11
+ The extensionless filename is the CLI command and must use lowercase ASCII kebab-case: a letter followed by letters or digits, with optional single hyphens separating further nonempty letter-or-digit segments. The Python name is the CLI name with every hyphen replaced by an underscore. A Python name that conflicts with `toolfuncs.sdk` or `toolfuncs.import_path` is invalid.
12
12
 
13
13
  The source contains exactly one PEP 723 `script` block. `dependencies` is a list of valid Python requirement strings and `requires-python` is optional. `[tool.toolfuncs]` contains exactly three required documentation fields:
14
14
 
@@ -40,7 +40,7 @@ Dynamic attribute lookup maps the requested snake-case Python name to its kebab-
40
40
 
41
41
  The package root publicly exposes only dynamic tools and `import_path`. There is no public `load`, discovery, setup, record, error, or builder API at the root. `import toolfuncs.NAME` is not promised because discovered tools are dynamic package attributes rather than installed submodules.
42
42
 
43
- `import_path` remains separate. It imports one already-known local `.py` file or extensionless script, derives its module name from that source filename, optionally prepares ordinary PEP 723 dependencies, and does not require the toolfuncs shebang, identity metadata, executable permission, or a module-level app.
43
+ `import_path` remains separate. It imports one already-known local `.py` file or extensionless script, derives its module name from that source filename, maps a valid extensionless kebab-case filename to snake case, optionally prepares ordinary PEP 723 dependencies, and does not require the toolfuncs shebang, identity metadata, executable permission, or a module-level app.
44
44
 
45
45
  ## Dependencies
46
46
 
@@ -68,7 +68,7 @@ The command emits one strict-JSON tagged union. `DoctorSuccess` has `status: "su
68
68
 
69
69
  ## CLI execution
70
70
 
71
- When the operating system executes a tool, `/usr/bin/env` resolves the managed `toolfuncs` wrapper on `PATH` and passes it the PATH-visible source path followed by the user's arguments. The wrapper validates dependency policy, starts one uv-prepared environment, and invokes the runtime. Toolfuncs validates identity metadata, imports the source under `python_name`, obtains its module-level app, preserves the exact CLI filename as the Cyclopts program name, and invokes the app with the remaining arguments.
71
+ `toolfuncs SOURCE [ARGS...]` runs a known source without requiring that source to be on `PATH`. It resolves `SOURCE` as a filesystem path and does not search `PATH` for a tool name. When the operating system executes a tool that is on `PATH`, `/usr/bin/env` resolves the managed `toolfuncs` wrapper and passes it the source path followed by the user's arguments. In either form, the wrapper validates dependency policy, starts one uv-prepared environment, and invokes the runtime. Toolfuncs validates identity metadata, imports the source under `python_name`, obtains its module-level app, preserves the exact CLI filename as the Cyclopts program name, and invokes the app with the remaining arguments.
72
72
 
73
73
  The source is imported rather than executed as `__main__`. A main guard is neither required nor used. Plain `python SOURCE ...` does not invoke the CLI and does not prepare PEP 723 dependencies.
74
74
 
@@ -80,8 +80,8 @@ Successful default CLI operations emit one strict JSON value. CLI parsing, valid
80
80
 
81
81
  Setup does not discover, install, copy, link, register, synchronize, or remove tool sources. Adding and removing tools are ordinary filesystem operations in directories already on `PATH`.
82
82
 
83
- The public management commands are `toolfuncs doctor SOURCE`, `toolfuncs list`, and `toolfuncs setup [--bin-dir DIRECTORY]`. Tools are invoked directly by their own CLI names. There is no universal name dispatcher, `run`, `exec`, `describe`, or `sync` command.
83
+ The public management commands are `toolfuncs doctor SOURCE`, `toolfuncs list`, and `toolfuncs setup [--bin-dir DIRECTORY]`. The explicit-source execution form is `toolfuncs SOURCE [ARGS...]`; it is not a universal name dispatcher. Tools on `PATH` are invoked directly by their own CLI names. There is no `run`, `exec`, `describe`, or `sync` command.
84
84
 
85
85
  ## Compatibility
86
86
 
87
- Version 0.6 retains the 0.4 runtime and identity contract and adds the authoring doctor. It intentionally does not accept the former underscore executable names, missing identity declarations, root-level builder exports, public `load`, or public discovery records. All controlled tools must migrate with the runtime.
87
+ Version 0.7 adds public explicit-source CLI execution and maps extensionless kebab-case names to snake case in `import_path`. It retains the 0.6 runtime, identity, discovery, and authoring contracts. It intentionally does not accept the former underscore executable names, missing identity declarations, root-level builder exports, public `load`, or public discovery records. All controlled tools must migrate with the runtime.
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "toolfuncs"
3
- version = "0.6.0"
4
- description = "Executable typed Python function tools discovered through PATH"
3
+ version = "0.7.0"
4
+ description = "Executable typed Python function tools invoked by path or discovered through PATH"
5
5
  readme = "README.md"
6
6
  authors = [
7
7
  { name = "Nima Shoghi", email = "nima@boltz.bio" }
@@ -29,13 +29,17 @@ from .sdk import _run
29
29
  _HELP = f"""\
30
30
  Usage:
31
31
  toolfuncs --version
32
+ toolfuncs SOURCE [ARGS...]
32
33
  toolfuncs doctor SOURCE
33
34
  toolfuncs list
34
35
  toolfuncs setup [--bin-dir DIRECTORY]
35
36
 
36
- A toolfunc is an executable Python script on PATH whose first line is exactly:
37
+ A toolfunc is an executable Python script whose first line is exactly:
37
38
  {TOOL_SHEBANG}
38
39
 
40
+ Run a known source with `toolfuncs SOURCE [ARGS...]`. Put it on PATH to invoke
41
+ it by filename and make it available to discovery and dynamic Python lookup.
42
+
39
43
  Floating Git revisions require the explicit shebang:
40
44
  {ALLOW_FLOATING_VCS_SHEBANG}
41
45
  """
@@ -37,6 +37,7 @@ _PEP723_BLOCK = re.compile(
37
37
  r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)[ \t]*$[\r\n]"
38
38
  r"(?P<content>(?:^#(?:| .*)$[\r\n]?)+)^# ///[ \t]*$"
39
39
  )
40
+ _EXTENSIONLESS_MODULE_NAME = re.compile(r"[a-z][a-z0-9]*(?:-[a-z0-9]+)*\Z")
40
41
  _IMPORT_LOCK = threading.RLock()
41
42
 
42
43
 
@@ -87,10 +88,11 @@ def import_path(
87
88
  """Import one local Python script.
88
89
 
89
90
  ``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.
91
+ filename determines the module name; valid extensionless kebab-case names
92
+ map to snake case. When the source contains PEP 723 metadata, its
93
+ requirements are installed as one list before import. Pass a custom
94
+ ``dependency_installer`` to replace the default current-interpreter pip
95
+ installation.
94
96
  """
95
97
 
96
98
  path = _source_path(source)
@@ -152,6 +154,8 @@ def _module_name(source: Path) -> str:
152
154
  name = source.stem
153
155
  elif not source.suffix:
154
156
  name = source.name
157
+ if _EXTENSIONLESS_MODULE_NAME.fullmatch(name):
158
+ name = name.replace("-", "_")
155
159
  else:
156
160
  raise ValueError(f"Python script must be extensionless or end in '.py': {source}")
157
161
  if not name.isidentifier() or keyword.iskeyword(name):
@@ -43,6 +43,30 @@ def test_import_path_accepts_extensionless_script(
43
43
  assert imported.VALUE == 43
44
44
 
45
45
 
46
+ def test_import_path_maps_extensionless_kebab_case_to_snake_case(
47
+ tmp_path: Path,
48
+ unique_name: str,
49
+ ) -> None:
50
+ source = tmp_path / f"{unique_name}-job"
51
+ source.write_text("VALUE = 44\n", encoding="utf-8")
52
+
53
+ imported = import_path(source)
54
+
55
+ assert imported.__name__ == f"{unique_name}_job"
56
+ assert imported.VALUE == 44
57
+
58
+
59
+ def test_import_path_rejects_malformed_extensionless_kebab_case(
60
+ tmp_path: Path,
61
+ unique_name: str,
62
+ ) -> None:
63
+ source = tmp_path / f"{unique_name}--job"
64
+ source.write_text("VALUE = 45\n", encoding="utf-8")
65
+
66
+ with pytest.raises(ValueError, match="valid Python module name"):
67
+ import_path(source)
68
+
69
+
46
70
  def test_pep723_dependencies_are_passed_as_one_list_before_import(
47
71
  tmp_path: Path,
48
72
  unique_name: str,
@@ -34,7 +34,16 @@ def test_cli_prints_installed_version(tmp_path: Path, flag: str) -> None:
34
34
  completed = run_python(["-m", "toolfuncs", flag], cwd=tmp_path)
35
35
 
36
36
  assert completed.returncode == 0
37
- assert completed.stdout == "0.6.0\n"
37
+ assert completed.stdout == "0.7.0\n"
38
+ assert completed.stderr == ""
39
+
40
+
41
+ def test_cli_help_advertises_explicit_source_execution(tmp_path: Path) -> None:
42
+ completed = run_python(["-m", "toolfuncs", "--help"], cwd=tmp_path)
43
+
44
+ assert completed.returncode == 0
45
+ assert "toolfuncs SOURCE [ARGS...]" in completed.stdout
46
+ assert "Run a known source" in completed.stdout
38
47
  assert completed.stderr == ""
39
48
 
40
49
 
@@ -106,6 +115,31 @@ def test_runtime_requires_a_module_level_app(tmp_path: Path) -> None:
106
115
  assert "must define 'app'" in completed.stderr
107
116
 
108
117
 
118
+ def test_explicit_source_runs_without_being_on_path(tmp_path: Path) -> None:
119
+ source = tmp_path / "local-tool"
120
+ _write_executable(
121
+ source,
122
+ _script(
123
+ "local-tool",
124
+ "Local tool",
125
+ "import toolfuncs.sdk as toolsdk\n\n"
126
+ "app = toolsdk.App()\n\n"
127
+ "@app.command\n"
128
+ "def ping() -> str:\n"
129
+ " return 'pong'\n",
130
+ ),
131
+ )
132
+
133
+ completed = run_python(
134
+ ["-m", "toolfuncs", str(source), "ping"],
135
+ cwd=tmp_path,
136
+ env={"PATH": os.defpath},
137
+ )
138
+
139
+ assert completed.returncode == 0, completed.stderr
140
+ assert json.loads(completed.stdout) == "pong"
141
+
142
+
109
143
  @pytest.mark.parametrize("prepared", [False, True])
110
144
  @pytest.mark.parametrize(
111
145
  ("app_definition", "expected_name"),
@@ -601,7 +601,7 @@ wheels = [
601
601
 
602
602
  [[package]]
603
603
  name = "toolfuncs"
604
- version = "0.6.0"
604
+ version = "0.7.0"
605
605
  source = { editable = "." }
606
606
  dependencies = [
607
607
  { name = "cyclopts" },
File without changes
File without changes
File without changes
File without changes
File without changes