Repository navigation
remove and rollback don't PEP 503-normalise PyPI purl identifiers, so remove pkg:pypi/typing_extensions@4.7.1 exits 1 "No patch found" while get accepts the same identifier #1024
Description
[agent] Found by the scheduled Poetry bug-hunt routine (ledger #311).
Summary
#910 / #911 and #926 / #927 taught scan --package, socket.yml and get to compare PyPI names by their PEP 503 canonical form. remove and rollback were left out. Both match identifiers through patch_matches → purl_matches_identifier, which compares the purl text exactly. Every patch key on disk is canonical (pkg:pypi/typing-extensions@4.7.1), so an identifier spelled the way the project declares the package fails with No patch found matching identifier, exit 1, and nothing is unwound. That covers typing_extensions as written in pyproject.toml, Jinja2, Typing-Extensions and ruamel.yaml. This happens in all three modes (agent, hosted, vendored).
So get pkg:pypi/typing_extensions@4.7.1 patches the package, but remove pkg:pypi/typing_extensions@4.7.1 (the same string) can't undo it.
Impact
- A user, or a script, that removes or rolls back the patch it just applied by the same identifier gets exit 1 and keeps the patch: the lock stays hosted or vendored, and the agent-mode files stay patched.
- Poetry users meet this naturally. Poetry keeps the spelling from
pyproject.toml(typing_extensions = "…",Jinja2 = "…"), and the purl spec's PyPI rule (lowercase,_→-) isn't something users apply by hand. - It isn't Poetry-specific. The matcher is ecosystem-generic, so pip, uv, Pipenv, PDM and Hatch projects behave the same way. It's filed from the Poetry lane, where it was found.
Repro (Linux, Poetry 2.5.1, a local mock patch API with free patches for six@1.16.0 and typing-extensions@4.7.1)
mkdir app && cd app
cat > pyproject.toml <<'EOF'
[tool.poetry]
name = "demo-two"
version = "0.1.0"
description = ""
authors = ["x <x@example.com>"]
package-mode = false
[tool.poetry.dependencies]
python = "^3.9"
six = "1.16.0"
typing_extensions = "4.7.1"
EOF
poetry lock
socket-patch scan --mode hosted --ecosystems pypi --yes ... # both pinned to hosted wheels
socket-patch remove pkg:pypi/typing_extensions@4.7.1 --yes ... # exit 1
# Error: No patch found matching identifier: pkg:pypi/typing_extensions@4.7.1
socket-patch remove pkg:pypi/typing-extensions@4.7.1 --yes ... # exit 0, restored (control)Expected vs actual
- Expected: a PyPI purl identifier selects the patch whatever spelling of the name it uses, as
get(get <name>doesn't PEP 503-normalise PyPI names, soget typing_extensionsorget ruamel.yamlreports "No packages matching" (exit 0) for an installed, patchable package #926),scan --packageand socket.yml (socket.ymlignorePackages/packagesandscan --packagedon't PEP 503-normalise PyPI names, soignorePackages: ["typing_extensions"]is silently ignored and the package is patched anyway #910) now do. CLI_CONTRACT.md documents that PEP 503 rule forscan --package("PyPI names compare by their PEP 503 canonical form, sotyping_extensionsmatchespkg:pypi/typing-extensions"). The rollback / remove rows say a "base purl matches every release variant", and the purl spec requires PyPI names to be read canonically. - Actual: exit 1,
not_found(No patch found matching identifier), and nothing is restored.
Cells (main db83f01, Linux, Poetry 2.5.1; each cell run in a fresh copy)
| Mode | Command | pkg:pypi/typing-extensions@4.7.1 (control) |
pkg:pypi/typing_extensions@4.7.1 |
pkg:pypi/Typing-Extensions@4.7.1 |
|---|---|---|---|---|
| agent | remove |
✅ exit 0, file restored | ❌ exit 1, still patched | ❌ exit 1, still patched |
| agent | rollback |
✅ exit 0 | ❌ exit 1 | ❌ exit 1 |
| hosted | remove |
✅ exit 0, lock entry restored | ❌ exit 1, still hosted | ❌ exit 1 |
| hosted | rollback |
✅ exit 0 | ❌ exit 1 | ❌ exit 1 |
| vendored | remove |
✅ exit 0, vendoring reverted | ❌ exit 1, still vendored | ❌ exit 1 |
| vendored | rollback |
✅ exit 0 | ❌ exit 1 | ❌ exit 1 |
| hosted | get pkg:pypi/typing_extensions@4.7.1 |
— | ✅ pinned (asymmetry) | — |
The underscore case reproduced 3 times for hosted remove (two separate harness runs). OS doesn't matter: this is string matching. Not bisected.
Suspect code
crates/socket-patch-core/src/utils/purl.rs:421patch_matches→purl_matches_identifier: nocanonicalize_pypi_nameon thepkg:pypi/name before comparing.pypi_purl(same file) already canonicalizes the keys it builds.- Callers:
crates/socket-patch-cli/src/commands/remove.rs(the manifest, vendor ledger and hosted-pin filters) andcrates/socket-patch-cli/src/commands/rollback.rs(RollbackTarget::Identifier). - Move canonicalize_pypi_name and the PEP 508 name scanner out of crawlers and vendor into one PyPI name module #883 (one PyPI name module) is the natural home for the shared normaliser.
Activity
poetry: run 15 ledger (#1023, #1024 filed; #945 comment)
mikolalysenko commentedon Oct 7, 2026
[agent] Claiming this issue (shared root cause: remove/rollback identifier matching compares PyPI purl names without PEP 503 canonicalization). Branch: agent/fix-pypi-identifier-canonical-match. Claim-ID: 2026-10-07T14:21:35Z-a2b9e1
Generated by Claude Code
mikolalysenko commentedon Oct 7, 2026
[agent] Draft PR: #1025
Generated by Claude Code
Test PyPI name spellings in remove/rollback
Match PyPI names by PEP 503 form in remove