Conversation
There was a problem hiding this comment.
Hey - I've found 3 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/poetry/console/commands/run.py" line_range="108-110" />
<code_context>
+ if isinstance(script, dict) and script.get("type") == "file":
+ return self.run_file_script(script, args)
+
for script_dir in self.env.script_dirs:
script_path = script_dir / args[0]
if WINDOWS:
</code_context>
<issue_to_address>
**issue (bug_risk):** On Windows, an installed extensionless file at `<scripts>/<name>` is selected before `<scripts>/<name>.cmd`, so `env.execute` attempts to run the extensionless file instead of using the executable wrapper. The command therefore fails even when a working `.cmd` wrapper exists.
**Triggers:** On Windows when both the extensionless installed file and its `.cmd` wrapper exist.
**Suggested fix:** Prefer the `.cmd` candidate on Windows, or skip the extensionless candidate there when a wrapper exists.
```suggestion
candidates = [script_dir / script_name]
if WINDOWS:
candidates.insert(0, script_dir / f"{script_name}.cmd")
```
</issue_to_address>
### Comment 2
<location path="src/poetry/console/commands/run.py" line_range="117-118" />
<code_context>
+
+ # If we reach this point, the script is not installed, so we fall back to
+ # the script file referenced in the project.
+ reference = script["reference"]
+ script_path = self.poetry.file.path.parent / reference
+
+ if not script_path.exists():
</code_context>
<issue_to_address>
**issue (bug_risk):** A file-script table without a `reference` key raises `KeyError` instead of reporting a command-not-found or malformed-script error. The editable builder explicitly tolerates this configuration and reports a missing reference, so `poetry run <name>` crashes for a configuration that installation accepts.
**Triggers:** When a `[tool.poetry.scripts]` entry has `type = "file"` but no `reference` field and no installed script is present.
**Suggested fix:** Read the reference with `.get()` and emit the same missing-reference error used by the builder before attempting execution.
</issue_to_address>
### Comment 3
<location path="src/poetry/console/commands/run.py" line_range="58-60" />
<code_context>
Otherwise (when an entry point script does not exist), ``sys.argv[0]`` is the
script name only, i.e. ``poetry run foo`` has ``sys.argv == ['foo']``.
"""
+ if isinstance(script, dict) and script.get("type") == "file":
+ return self.run_file_script(script, args)
+
for script_dir in self.env.script_dirs:
</code_context>
<issue_to_address>
**nitpick:** The `run_script` docstring now describes file-script fallback behavior incorrectly: it claims that an uninstalled script runs with `sys.argv[0]` set to only the script name, but `run_file_script` executes the referenced path directly, so `sys.argv[0]` is the full reference path.
**Triggers:** When a file script is run from its project reference because it is not installed.
**Suggested fix:** Document the distinct `sys.argv[0]` behavior for file scripts, or execute the fallback in a way that preserves the documented value.
```suggestion
Otherwise (when an entry point script does not exist), ``sys.argv[0]`` is the
script name only, i.e. ``poetry run foo`` has ``sys.argv == ['foo']``. For a
file script that is not installed, ``sys.argv[0]`` is the full reference path.
"""
```
</issue_to_address>Sourcery assessment
Approval pending. 2 findings to address first.
Blocking findings: src/poetry/console/commands/run.py:110, src/poetry/console/commands/run.py:118
| candidates = [script_dir / script_name] | ||
| if WINDOWS: | ||
| candidates.append(script_dir / f"{script_name}.cmd") |
There was a problem hiding this comment.
issue (bug_risk): On Windows, an installed extensionless file at <scripts>/<name> is selected before <scripts>/<name>.cmd, so env.execute attempts to run the extensionless file instead of using the executable wrapper. The command therefore fails even when a working .cmd wrapper exists.
Triggers: On Windows when both the extensionless installed file and its .cmd wrapper exist.
Suggested fix: Prefer the .cmd candidate on Windows, or skip the extensionless candidate there when a wrapper exists.
| candidates = [script_dir / script_name] | |
| if WINDOWS: | |
| candidates.append(script_dir / f"{script_name}.cmd") | |
| candidates = [script_dir / script_name] | |
| if WINDOWS: | |
| candidates.insert(0, script_dir / f"{script_name}.cmd") |
| reference = script["reference"] | ||
| script_path = self.poetry.file.path.parent / reference |
There was a problem hiding this comment.
issue (bug_risk): A file-script table without a reference key raises KeyError instead of reporting a command-not-found or malformed-script error. The editable builder explicitly tolerates this configuration and reports a missing reference, so poetry run <name> crashes for a configuration that installation accepts.
Triggers: When a [tool.poetry.scripts] entry has type = "file" but no reference field and no installed script is present.
Suggested fix: Read the reference with .get() and emit the same missing-reference error used by the builder before attempting execution.
| Otherwise (when an entry point script does not exist), ``sys.argv[0]`` is the | ||
| script name only, i.e. ``poetry run foo`` has ``sys.argv == ['foo']``. | ||
| """ |
There was a problem hiding this comment.
nitpick: The run_script docstring now describes file-script fallback behavior incorrectly: it claims that an uninstalled script runs with sys.argv[0] set to only the script name, but run_file_script executes the referenced path directly, so sys.argv[0] is the full reference path.
Triggers: When a file script is run from its project reference because it is not installed.
Suggested fix: Document the distinct sys.argv[0] behavior for file scripts, or execute the fallback in a way that preserves the documented value.
| Otherwise (when an entry point script does not exist), ``sys.argv[0]`` is the | |
| script name only, i.e. ``poetry run foo`` has ``sys.argv == ['foo']``. | |
| """ | |
| Otherwise (when an entry point script does not exist), ``sys.argv[0]`` is the | |
| script name only, i.e. ``poetry run foo`` has ``sys.argv == ['foo']``. For a | |
| file script that is not installed, ``sys.argv[0]`` is the full reference path. | |
| """ |
`poetry run <script>` raised `KeyError: 'callable'` for scripts declared as
tables in `[tool.poetry.scripts]`, e.g.
`my-script = { reference = "bin/my-script.sh", type = "file" }`, even though
the referenced file is copied to the environment's script directory when the
project is installed.
File scripts are now executed directly. They are looked up in the
environment's script directories first and fall back to the referenced file
in the project, with the usual "not installed as a script" warning. Scripts
declared with `type = "console"` (or with the legacy `callable` key) keep
being resolved to a `module:callable` entry point, which also fixes the same
`KeyError` for that table form.
33508f1 to
6e1ff6b
Compare
|
Here is the comparison as I see it. #11108 (closed by its author in favour of #11092) stops at detecting the file script: when the script is not installed it executes #11092 (open) fixes the same crash with a wider surface: besides What this PR does
Tests in Limitation I am not claiming to fix: on Windows, an extension-less script copied verbatim into the script directory cannot be started by So: if you would rather land #11092 and drop mine, that is fine with me; I would only ask that the "not installed / missing reference" handling ends up somewhere, since that is the part #11108 lacked. Otherwise this is the minimal change to Rebased onto |
Description
Fixes #11090.
poetry run <script>fails withKeyError: 'callable'when the script is declared as a table in[tool.poetry.scripts]:Poetry already copies
bin/my-script.shinto the environment's script directory as an executable when the project is installed, butpoetry run my-scriptthen tried to resolve the entry as amodule:callablepair, so file scripts could not be executed by name.Root cause
src/poetry/console/commands/run.pyunconditionally read thecallablekey for table entries:{ reference = ..., type = "file" }(andtype = "console") has nocallablekey, so the lookup raisedKeyError: 'callable'.Changes
run.pynow dispatches table entries withtype == "file"to a newRunCommand.run_file_script()method, which executes the script file directly. The script is looked up in the environment's script directories (<scripts>/<name>, or<scripts>/<name>.cmdon Windows) and falls back to the referenced file relative to the project root, printing the existing "not installed as a script" warning. If the reference does not exist, it reportsCommand not foundand returns 1.callable(legacy form) orreference(type = "console"), which fixes the sameKeyErrorfor those entries.Tests
Five tests added to
tests/console/commands/test_run.py:test_run_file_script_from_reference_when_not_installed– the referenced file is executed with the extra arguments when the script is not installed.test_run_file_script_from_the_environment– the installed file in the environment's script directory takes precedence.test_run_file_script_with_missing_reference–Command not found: missing-scriptand exit code 1.test_run_console_script_defined_as_table–{ reference = "pkg:main", type = "console" }is resolved to an entry point.test_run_file_script_uses_the_installed_file– end-to-end install withtmp_venv: the installed file is executed and its exit code is propagated (skipped on Windows because the fixture uses a bash shebang).The four non-skipped new tests fail on
mainwithKeyError: 'callable'inrun.pyand pass with this change.Verification
ruff check,ruff format --checkandmypyare clean for both files.Known limitation
File scripts are still not runnable on Windows: the editable builder copies the file without a
.cmdwrapper, and Windows cannot execute an extension-less script. This change does not make that worse (it was aKeyErrorbefore) and a<name>.cmdwrapper is used if one exists. Happy to address that separately if it should be in scope here.Disclosure: this change was prepared with an AI coding assistant (DeepSeek) under my direction; I reviewed the diff and the commands above before pushing.