From 50535ccdabf1eff81504a0a04a9cb5792b063d6e Mon Sep 17 00:00:00 2001 From: William Tan <1284324+Ninja3047@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:03:01 -0400 Subject: [PATCH 1/3] fix(modern-python): suggest exact `uv run python` so the advice works outside projects The python/python3 shim suggested `uv run $cmd ...`, echoing back whichever name was invoked. For `python3` that advice is self-defeating on machines with no uv-managed interpreters: uv resolves the `python3` command through an ordinary PATH lookup, which hits the shim again and fails with the same suggestion. uv special-cases the exact command name `python` (uv >= 0.4.0) and executes its resolved interpreter directly, so always suggesting `uv run python ...` works everywhere. Reproduced on stock Debian + uv 0.11.27 (apt python3, zero managed pythons, no project): `uv run python3 script.py` fails via the shim while `uv run python script.py` succeeds, across script/-c/-m/REPL forms. Reported in #195. Co-Authored-By: Claude Fable 5 --- .claude-plugin/marketplace.json | 2 +- .../modern-python/.claude-plugin/plugin.json | 2 +- plugins/modern-python/README.md | 2 +- plugins/modern-python/hooks/setup-shims.sh | 7 +++++-- plugins/modern-python/hooks/shims/python | 9 +++++++-- .../hooks/shims/python-shim.bats | 19 ++++++++++++++++++- 6 files changed, 33 insertions(+), 8 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c3dbab76..4313f54a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -262,7 +262,7 @@ }, { "name": "modern-python", - "version": "1.5.2", + "version": "1.5.3", "description": "Modern Python best practices. Use when creating new Python projects, and writing Python scripts, or migrating existing projects from legacy tools.", "author": { "name": "William Tan", diff --git a/plugins/modern-python/.claude-plugin/plugin.json b/plugins/modern-python/.claude-plugin/plugin.json index 38b9190f..0fe0df73 100644 --- a/plugins/modern-python/.claude-plugin/plugin.json +++ b/plugins/modern-python/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "modern-python", - "version": "1.5.2", + "version": "1.5.3", "description": "Modern Python best practices. Use when creating new Python projects, and writing Python scripts, or migrating existing projects from legacy tools.", "author": { "name": "William Tan", diff --git a/plugins/modern-python/README.md b/plugins/modern-python/README.md index 96f42505..c4ac9a3f 100644 --- a/plugins/modern-python/README.md +++ b/plugins/modern-python/README.md @@ -37,7 +37,7 @@ Modern Python tooling and best practices using uv, ruff, ty, and pytest. Based o ## Hook: Legacy Command Interception -This plugin includes a `SessionStart` hook that prepends PATH shims for `python`, `pip`, `pipx`, and `uv`. When Claude runs a bare `python`, `pip`, or `pipx` command, the shell resolves to the shim, which prints an error with the correct `uv` alternative and exits non-zero. `uv run` is unaffected because it prepends its managed virtualenv's `bin/` to PATH, shadowing the shims. +This plugin includes a `SessionStart` hook that prepends PATH shims for `python`, `pip`, `pipx`, and `uv`. When Claude runs a bare `python`, `pip`, or `pipx` command, the shell resolves to the shim, which prints an error with the correct `uv` alternative and exits non-zero. The suggested alternative always uses the exact command name `python` (never `python3`): uv special-cases `uv run python ...` (uv >= 0.4.0) and executes its resolved interpreter directly instead of looking the command up on PATH, so the suggestion succeeds everywhere — including outside a project, where `uv run python3` would resolve back to the shim and fail. | Intercepted Command | Suggested Alternative | |---------------------|----------------------| diff --git a/plugins/modern-python/hooks/setup-shims.sh b/plugins/modern-python/hooks/setup-shims.sh index 5e3d590c..7f4acf04 100755 --- a/plugins/modern-python/hooks/setup-shims.sh +++ b/plugins/modern-python/hooks/setup-shims.sh @@ -4,8 +4,11 @@ set -euo pipefail # SessionStart hook: prepend shims directory to PATH so that bare # python/pip/pipx/uv-pip invocations are intercepted with uv suggestions. # -# `uv run` is unaffected because it prepends its managed virtualenv's -# bin/ to PATH, shadowing the shims. +# The suggested `uv run python ...` commands are unaffected by the shims: +# uv special-cases the `python` command and executes its resolved +# interpreter directly instead of a PATH lookup, so the suggestion works +# even outside a project (where `uv run python3` would resolve back to +# the shim). # Guard: only activate when uv is available command -v uv &>/dev/null || exit 0 diff --git a/plugins/modern-python/hooks/shims/python b/plugins/modern-python/hooks/shims/python index 695a54a2..7e7d5c5f 100755 --- a/plugins/modern-python/hooks/shims/python +++ b/plugins/modern-python/hooks/shims/python @@ -5,6 +5,11 @@ set -euo pipefail # suggests the uv equivalent. Works for both names via $0. cmd="$(basename "$0")" +# Suggestions always use the exact name `python`, never `python3`: uv +# special-cases the `python` command (uv >= 0.4.0) and executes its resolved +# interpreter directly instead of a PATH lookup, so the suggested command +# works even outside a project, where `uv run python3` would resolve back +# to this shim. case "${1:-}" in -m) case "${2:-}" in @@ -14,12 +19,12 @@ case "${1:-}" in echo " uv remove # remove a dependency" >&2 ;; *) - echo "ERROR: Use \`uv run $cmd -m ${2:-}\` instead of \`$cmd -m ${2:-}\`" >&2 + echo "ERROR: Use \`uv run python -m ${2:-}\` instead of \`$cmd -m ${2:-}\`" >&2 ;; esac ;; *) - echo "ERROR: Use \`uv run $cmd ${*:-}\` instead of \`$cmd ${*:-}\`" >&2 + echo "ERROR: Use \`uv run python ${*:-}\` instead of \`$cmd ${*:-}\`" >&2 ;; esac diff --git a/plugins/modern-python/hooks/shims/python-shim.bats b/plugins/modern-python/hooks/shims/python-shim.bats index b147ecf7..88435a2a 100644 --- a/plugins/modern-python/hooks/shims/python-shim.bats +++ b/plugins/modern-python/hooks/shims/python-shim.bats @@ -43,7 +43,24 @@ SHIM="${BATS_TEST_DIRNAME}/python" @test "works when invoked as python3 via symlink" { run "${BATS_TEST_DIRNAME}/python3" [[ $status -ne 0 ]] - [[ "$output" == *"uv run python3"* ]] + [[ "$output" == *'instead of `python3'* ]] +} + +# `uv run python3` resolves the command via PATH and hits this shim again +# outside a project, so the suggestion must use the exact name `python`, +# which uv executes directly via its resolved interpreter. +@test "suggests exact 'uv run python', not python3, when invoked as python3" { + run "${BATS_TEST_DIRNAME}/python3" script.py + [[ $status -ne 0 ]] + [[ "$output" == *'Use `uv run python script.py`'* ]] + [[ "$output" != *"uv run python3"* ]] +} + +@test "suggests exact 'uv run python -m', not python3, for modules" { + run "${BATS_TEST_DIRNAME}/python3" -m http.server + [[ $status -ne 0 ]] + [[ "$output" == *'Use `uv run python -m http.server`'* ]] + [[ "$output" != *"uv run python3"* ]] } @test "python3 -m pip suggests uv add" { From b6c8fb9e1ff4374fe3a4bccfed9c2b7f001dc51d Mon Sep 17 00:00:00 2001 From: William Tan <1284324+Ninja3047@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:05:08 -0400 Subject: [PATCH 2/3] fix(modern-python): satisfy shellcheck SC2016 in new bats assertions Escaped backticks in double quotes instead of literal backticks in single quotes, which shellcheck flags as a possible unintended non-expansion. Co-Authored-By: Claude Fable 5 --- plugins/modern-python/hooks/shims/python-shim.bats | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modern-python/hooks/shims/python-shim.bats b/plugins/modern-python/hooks/shims/python-shim.bats index 88435a2a..62aba71d 100644 --- a/plugins/modern-python/hooks/shims/python-shim.bats +++ b/plugins/modern-python/hooks/shims/python-shim.bats @@ -52,14 +52,14 @@ SHIM="${BATS_TEST_DIRNAME}/python" @test "suggests exact 'uv run python', not python3, when invoked as python3" { run "${BATS_TEST_DIRNAME}/python3" script.py [[ $status -ne 0 ]] - [[ "$output" == *'Use `uv run python script.py`'* ]] + [[ "$output" == *"Use \`uv run python script.py\`"* ]] [[ "$output" != *"uv run python3"* ]] } @test "suggests exact 'uv run python -m', not python3, for modules" { run "${BATS_TEST_DIRNAME}/python3" -m http.server [[ $status -ne 0 ]] - [[ "$output" == *'Use `uv run python -m http.server`'* ]] + [[ "$output" == *"Use \`uv run python -m http.server\`"* ]] [[ "$output" != *"uv run python3"* ]] } From 214ede818daaae0fde3c36af468f5ebef7ba052a Mon Sep 17 00:00:00 2001 From: William Tan <1284324+Ninja3047@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:36:26 -0400 Subject: [PATCH 3/3] fix(modern-python): requote shim suggestions and carry all arguments through Review findings on #196: the -m branch interpolated only the module name, so `python -m http.server 8000` suggested a command missing the port, and `${*}` flattened arguments without quoting, so `python -c 'print(1+1)'` suggested a command that is a bash syntax error if run verbatim (plus a trailing space inside the backticks for bare invocations). Both branches now build the suggestion from %q-requoted arguments, with regression tests for each case. Also consolidates the exact-`python` rationale into a single canonical copy in the shim's header comment; README, setup-shims.sh, and the bats file now point there instead of paraphrasing it. Co-Authored-By: Claude Fable 5 --- plugins/modern-python/README.md | 2 +- plugins/modern-python/hooks/setup-shims.sh | 7 ++--- plugins/modern-python/hooks/shims/python | 18 +++++++++++-- .../hooks/shims/python-shim.bats | 27 ++++++++++++++++--- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/plugins/modern-python/README.md b/plugins/modern-python/README.md index c4ac9a3f..bedbab31 100644 --- a/plugins/modern-python/README.md +++ b/plugins/modern-python/README.md @@ -37,7 +37,7 @@ Modern Python tooling and best practices using uv, ruff, ty, and pytest. Based o ## Hook: Legacy Command Interception -This plugin includes a `SessionStart` hook that prepends PATH shims for `python`, `pip`, `pipx`, and `uv`. When Claude runs a bare `python`, `pip`, or `pipx` command, the shell resolves to the shim, which prints an error with the correct `uv` alternative and exits non-zero. The suggested alternative always uses the exact command name `python` (never `python3`): uv special-cases `uv run python ...` (uv >= 0.4.0) and executes its resolved interpreter directly instead of looking the command up on PATH, so the suggestion succeeds everywhere — including outside a project, where `uv run python3` would resolve back to the shim and fail. +This plugin includes a `SessionStart` hook that prepends PATH shims for `python`, `pip`, `pipx`, and `uv`. When Claude runs a bare `python`, `pip`, or `pipx` command, the shell resolves to the shim, which prints an error with the correct `uv` alternative and exits non-zero. The suggested alternative always uses the exact command name `python` (never `python3`) so it also works outside a project; see the header comment in [`hooks/shims/python`](hooks/shims/python) for the full rationale. | Intercepted Command | Suggested Alternative | |---------------------|----------------------| diff --git a/plugins/modern-python/hooks/setup-shims.sh b/plugins/modern-python/hooks/setup-shims.sh index 7f4acf04..113d9508 100755 --- a/plugins/modern-python/hooks/setup-shims.sh +++ b/plugins/modern-python/hooks/setup-shims.sh @@ -4,11 +4,8 @@ set -euo pipefail # SessionStart hook: prepend shims directory to PATH so that bare # python/pip/pipx/uv-pip invocations are intercepted with uv suggestions. # -# The suggested `uv run python ...` commands are unaffected by the shims: -# uv special-cases the `python` command and executes its resolved -# interpreter directly instead of a PATH lookup, so the suggestion works -# even outside a project (where `uv run python3` would resolve back to -# the shim). +# The suggested `uv run python ...` commands are unaffected by the shims; +# see the header comment in shims/python for why. # Guard: only activate when uv is available command -v uv &>/dev/null || exit 0 diff --git a/plugins/modern-python/hooks/shims/python b/plugins/modern-python/hooks/shims/python index 7e7d5c5f..17449b3d 100755 --- a/plugins/modern-python/hooks/shims/python +++ b/plugins/modern-python/hooks/shims/python @@ -5,11 +5,22 @@ set -euo pipefail # suggests the uv equivalent. Works for both names via $0. cmd="$(basename "$0")" +# Canonical rationale for the suggestion's shape (the README, +# setup-shims.sh, and python-shim.bats point here): +# # Suggestions always use the exact name `python`, never `python3`: uv # special-cases the `python` command (uv >= 0.4.0) and executes its resolved # interpreter directly instead of a PATH lookup, so the suggested command # works even outside a project, where `uv run python3` would resolve back # to this shim. +# +# Arguments are requoted with %q so the suggestion stays runnable when they +# contain spaces or shell metacharacters (e.g. -c 'print(1+1)'). +args="" +if (($#)); then + args="$(printf ' %q' "$@")" +fi + case "${1:-}" in -m) case "${2:-}" in @@ -19,12 +30,15 @@ case "${1:-}" in echo " uv remove # remove a dependency" >&2 ;; *) - echo "ERROR: Use \`uv run python -m ${2:-}\` instead of \`$cmd -m ${2:-}\`" >&2 + if (($# < 2)); then + args=" -m " + fi + echo "ERROR: Use \`uv run python$args\` instead of \`$cmd$args\`" >&2 ;; esac ;; *) - echo "ERROR: Use \`uv run python ${*:-}\` instead of \`$cmd ${*:-}\`" >&2 + echo "ERROR: Use \`uv run python$args\` instead of \`$cmd$args\`" >&2 ;; esac diff --git a/plugins/modern-python/hooks/shims/python-shim.bats b/plugins/modern-python/hooks/shims/python-shim.bats index 62aba71d..49a14ec3 100644 --- a/plugins/modern-python/hooks/shims/python-shim.bats +++ b/plugins/modern-python/hooks/shims/python-shim.bats @@ -46,9 +46,8 @@ SHIM="${BATS_TEST_DIRNAME}/python" [[ "$output" == *'instead of `python3'* ]] } -# `uv run python3` resolves the command via PATH and hits this shim again -# outside a project, so the suggestion must use the exact name `python`, -# which uv executes directly via its resolved interpreter. +# The suggestion must use the exact name `python`, never `python3`; see +# the header comment in ./python for the full rationale. @test "suggests exact 'uv run python', not python3, when invoked as python3" { run "${BATS_TEST_DIRNAME}/python3" script.py [[ $status -ne 0 ]] @@ -63,6 +62,28 @@ SHIM="${BATS_TEST_DIRNAME}/python" [[ "$output" != *"uv run python3"* ]] } +@test "-m suggestion preserves arguments after the module" { + run "${BATS_TEST_DIRNAME}/python3" -m http.server 8000 + [[ $status -ne 0 ]] + [[ "$output" == *"Use \`uv run python -m http.server 8000\` instead of \`python3 -m http.server 8000\`"* ]] +} + +# %q output can differ across bash versions, so build the expectation with +# the same requoting the shim uses, after checking it actually escapes. +@test "suggestion requotes -c code so it stays copy-paste runnable" { + run "$SHIM" -c 'print(1+1)' + [[ $status -ne 0 ]] + quoted="$(printf '%q' 'print(1+1)')" + [[ "$quoted" != 'print(1+1)' ]] + [[ "$output" == *"Use \`uv run python -c $quoted\`"* ]] +} + +@test "bare invocation suggests uv run python without trailing space" { + run "$SHIM" + [[ $status -ne 0 ]] + [[ "$output" == *"Use \`uv run python\` instead of \`python\`"* ]] +} + @test "python3 -m pip suggests uv add" { run "${BATS_TEST_DIRNAME}/python3" -m pip install foo [[ $status -ne 0 ]]