Skip to content

fix(pypi): resolve name and version for PEP 658 metadata sidecars - #222

Merged
andrew merged 2 commits into
git-pkgs:mainfrom
wickedOne:pypi-fix
Aug 3, 2026
Merged

fix(pypi): resolve name and version for PEP 658 metadata sidecars#222
andrew merged 2 commits into
git-pkgs:mainfrom
wickedOne:pypi-fix

Conversation

@wickedOne

@wickedOne wickedOne commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

hi,

i was experiencing this while caching pypi dependencies

image

once again i left the fix to claude (opus), but please challenge me if you have any questions

Problem

PyPI packages sometimes appear in the package overview under a synthetic name like pkg:pypi/_hash_164a211cb654c0d4@0.

These are PEP 658/714 core-metadata sidecar files. When an index advertises data-core-metadata (simple API) or core-metadata (JSON API) for a file, pip fetches .metadata to resolve dependencies without downloading the full wheel. parseFilename only recognised .whl and four sdist extensions, so foo-1.2.0-py3-none-any.whl.metadata matched none of them and returned an empty name. handleDownload then fell back to a hash-derived identifier:

name = fmt.Sprintf("_hash_%s", hashPath(path))
version = "0"

Reproduced against the reported entry — hashPath of the request path for backports_asyncio_runner-1.2.0-py3-none-any.whl.metadata yields exactly 164a211cb654c0d4.

It looks intermittent because the sidecar fetch depends on the index advertising core metadata per-file and pip choosing metadata-only resolution. For backports_asyncio_runner 1.2.0, upstream advertises core metadata on the wheel but not the sdist.

Changes

  • Strip the .metadata suffix before parsing, so sidecars resolve to the distribution they describe. This is the actual fix.
  • Parse wheels per PEP 427 using the spec-guaranteed field positions instead of scanning for a python tag. The name and version fields have hyphens escaped to _, so parts[0]/parts[1] are authoritative. Fixes build-tagged wheels: foo-1.0-1-py3-none-any.whl previously parsed as name foo-1.0, version 1. Removes the now-unused isPythonTag.
  • Recognise more distribution formats — .tar.xz, .tar.Z, .tgz as sdist archives, and .egg/.exe/.msi as tagged formats. All previously hit the same hash fallback. The legacy bdists use the first-two-fields path deliberately: under the sdist heuristic, numpy-1.8.0-py2.7-macosx-10.9-x86_64.egg would parse as name numpy-1.8.0-py2.7-macosx, version 10.9-x86_64.

Testing

  • Extended TestPyPIParseFilename with sidecars, build-tagged wheels, the new extensions, and legacy bdists.
  • Added TestPyPIParseFilenameNoHashFallback, asserting the three real backports_asyncio_runner filenames all resolve to the same name/version — the property whose failure produces the bogus PURLs.
  • go build ./... && go vet ./... && go test ./... pass; gofmt clean.

Note

This stops new hash* entries from being created but does not clean up existing ones. The cache is keyed on (package PURL, version PURL, filename), so already-cached sidecars keep their old identity and remain visible in the overview until purged.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes PyPI filename parsing so PEP 658/714 core-metadata sidecar downloads (*.metadata) resolve to the correct package name/version instead of creating synthetic _hash_*@0 identifiers, improving cache key stability and package overview correctness.

Changes:

  • Strip the .metadata suffix before parsing distribution filenames (PEP 658/714 sidecars).
  • Rework wheel parsing to use spec-guaranteed field positions (PEP 427), correctly handling build-tagged wheels.
  • Expand recognized distribution extensions (more sdist archives + legacy bdists) and extend unit tests accordingly.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
internal/handler/pypi.go Updates parseFilename to handle .metadata sidecars and broaden filename parsing across more PyPI distribution formats.
internal/handler/pypi_test.go Extends TestPyPIParseFilename and adds a regression test to prevent _hash_* fallback for sidecars.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/handler/pypi_test.go Outdated
Comment on lines +50 to +53
// Legacy bdist formats
{"numpy-1.8.0-py2.7-macosx-10.9-x86_64.egg", "numpy", "1.8.0"},
{"pywin32-223.win32-py2.7.exe", "pywin32", "223.win32"},

Comment thread internal/handler/pypi.go Outdated
Comment on lines 473 to 482
for _, format := range taggedExtensions {
if !strings.HasSuffix(filename, format.ext) {
continue
}
parts := strings.Split(strings.TrimSuffix(filename, format.ext), "-")
if len(parts) < format.minParts {
return "", ""
}
return parts[0], parts[1]
}

@andrew andrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new .exe handling parses Windows installer filenames incorrectly. For foo-1.0.win32-py2.0.exe, parseFilename returns version 1.0.win32, but win32 is the platform and the version is 1.0. The pywin32 test currently codifies the same error. Please parse Windows installers separately or remove .exe support, and add a regression test using the documented filename form.

The bdist_wininst and bdist_msi layout joins the platform to the version
with a '.' rather than a '-', so treating .exe/.msi like a wheel folded
the platform into the version: foo-1.0.win32-py2.0.exe resolved to
version "1.0.win32". Eggs shared the problem, as setuptools' hyphen
escaping is not universal: aws-sdk-1.0.0-py3.11.egg resolved to name
"aws", version "sdk".

Give each format its own parser. Wheels keep the PEP 427
spec-guaranteed field positions, eggs locate the version relative to the
py{X.Y} interpreter field, and Windows installers strip the platform and
interpreter fields before splitting name from version.

A PEP 658 sidecar resolves to the same name and version as the
distribution it describes, so it is cached under that version. Browse and
compare took the first cached artifact without checking its extension,
handing openArchive plain text: a version pip had only fetched metadata
for reported hasCached and then 500'd.

Add firstBrowsableArtifact, replacing five duplicated selection loops,
and export PyPIMetadataSuffix so the suffix has a single definition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wickedOne

Copy link
Copy Markdown
Contributor Author

according to claude all issues have been addressed...

@wickedOne
wickedOne requested a review from andrew August 1, 2026 16:55
@andrew
andrew merged commit 63f0efd into git-pkgs:main Aug 3, 2026
5 checks passed
@wickedOne
wickedOne deleted the pypi-fix branch August 3, 2026 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants