Skip to content

Add packagename.Normalize as the PEP 503 entry point - #68

Merged
nodivbyzero merged 3 commits into
mainfrom
feat/packagename-normalize
Oct 19, 2026
Merged

nodivbyzero merged 3 commits into
mainfrom
feat/packagename-normalize

Conversation

@nodivbyzero

Copy link
Copy Markdown
Contributor

Summary

  • New packagename package with Normalize, the PEP 503 entry point for project-name normalization. Callers no longer have to use extras.Normalize for project names, which looked like a bug at the call site. Closes Add a PEP 503-named entry point for project-name normalization #25.
  • One implementation. The algorithm moves to internal/canonname. extras.Normalize and packagename.Normalize are thin wrappers, so they cannot drift apart.
  • Matches pypa/packaging's canonicalize_name, including non-ASCII. Lowercasing now follows Python's str.lower(): Ä → ä, İ → i + U+0307, and Σ → ς at the end of a word (checked on the original name, as upstream does, so ΑΣ.Β → ασ-β but ΑΣ_Β → ας-β). Invalid UTF-8 bytes pass through unchanged instead of becoming U+FFFD. Already-normalized names return without allocating, and there is still no regex.

Connected to: #25

Behavior change

extras.Normalize returns different output for non-ASCII and invalid UTF-8 input. Valid PEP 508 names are ASCII, so their results do not change. This is listed under Fixed in CHANGELOG.md.

Go 1.27.2 uses Unicode 17.0 and CPython 3.13 uses 15.1, so the final-sigma rule can differ for characters that were added or reclassified after 15.1.

Test Plan

  • go build ./..., go vet ./..., go test ./...
  • gofmt -l . prints nothing; golangci-lint config verify and golangci-lint run ./... report 0 issues
  • go list -deps ./... does not contain pgregory.net/rapid
  • New tests in internal/canonname: base table, Python-derived Unicode and final-sigma tables, invalid UTF-8, isNormalized, zero allocations on the fast path, and FuzzNormalize (60 s run, no failures)
  • packagename: pypa test_canonicalize_name conformance cases, plus a test that it agrees with extras.Normalize
  • Compared against upstream canonicalize_name (pinned 4eb0753) under CPython 3.13:
    • 23 / 23 edge cases identical
    • 200,000 / 200,000 random ASCII names identical
    • every code point, alone and in 4 final-sigma patterns (5.56M inputs): plain lowercasing identical; 520 final-sigma differences, all caused by Unicode version (516 characters unassigned in 15.1, 4 reclassified after it: ʕ U+0295 and U+1171E)

Comment thread CHANGELOG.md
{"ΑΣ\u0301", "ας\u0301"},
{"Α\u0301Σ", "α\u0301ς"},
{"ΑΣ\u0301Β", "ασ\u0301β"},
{"ΣΑΣ_ΣΑΣ", "σας-σας"},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚪ Low

Only ., :, ' and U+0301 exercise isCaseIgnorable; the other hand-listed Word_Break runes (U+00B7, U+0387, U+2019, U+FF0E, …) and the Other_Lowercase branch of isCased have no case, and FuzzNormalize's reference skips Σ. A typo in one of those runes would pass every test.
A few Python-checked cases would cover it, e.g. {"Α·Σ", "α·ς"}, {"ΑΣ·Β", "ασ·β"}, {"ΑΣ’Β", "ασ’β"}, {"ªΣ", "ªς"}, {"ʰΣ", "ʰσ"} (expected values derived by hand, so please confirm under CPython 3.13).

)

// TestNormalizeMatchesExtras checks that the PEP 503 and PEP 685 entry points
// agree, including on non-ASCII and invalid names. The transformation itself is covered by

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚪ Low

The comment says agreement is checked on invalid names, but inputs has no invalid UTF-8, İ or Σ (the only non-ASCII input is ÄBC_def). Either add "A\xff_B", "İstanbul" and "ΑΣ.Β", or drop "and invalid".


import "github.com/posit-dev/go-python-packaging/internal/canonname"

// Normalize canonicalizes a Python project name per PEP 503: lowercase the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚪ Low · tentative

"per PEP 503" isn't exact: PEP 503's formula is re.sub(r"[-_.]+", "-", name).lower() (collapse, then lowercase), while this follows canonicalize_name (lowercase, then collapse). They differ for a final sigma next to .: ΑΣ.Β gives ασ-β here and ας-β under the PEP 503 formula.
Consider naming canonicalize_name as the contract and noting the difference.

Comment thread internal/canonname/canonname.go Outdated

@jonyoder jonyoder left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. The inline notes are all low; the CHANGELOG wording is the one worth fixing before the next tag.

@nodivbyzero
nodivbyzero merged commit 8b71fc9 into main Oct 19, 2026
4 checks passed
@nodivbyzero
nodivbyzero deleted the feat/packagename-normalize branch October 19, 2026 14:28
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.

Add a PEP 503-named entry point for project-name normalization

2 participants