Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
9cbc689
Added packaging.utils.create_wheel_filename and create_sdist_filename…
e2thenegpii Mar 6, 2021
fd4d06c
fix: always normalize names
henryiii Mar 6, 2026
cf6539a
refactor: create -> compose
henryiii Mar 6, 2026
42f69b5
tests: add a parse and compose test
henryiii Mar 6, 2026
7bf745d
fix(types): support any iterable for the tag set
henryiii Mar 6, 2026
5e3529e
Add packaging.filenames
di Nov 25, 2024
edda633
chore: fix up typing and linting
henryiii Mar 9, 2026
16164e1
refactor: remove Filename
henryiii Mar 9, 2026
47fd9e2
refactor: use filenames for utils
henryiii Mar 10, 2026
ec4174e
refactor: remove compose_*
henryiii Mar 10, 2026
1a15199
docs: add filenames
henryiii Mar 10, 2026
c14bb7b
tests: fill out coverage
henryiii Mar 10, 2026
00d445d
fix: require sorted tags in strict mode
henryiii Mar 12, 2026
996bf1c
rename: SourceDistributionFilename
henryiii Mar 12, 2026
cd0c881
fix: include match_args
henryiii Apr 7, 2026
2df2e53
fix(filenames): port the validation changes from main
henryiii Sep 22, 2026
fc41060
docs(filenames): resolve NormalizedName cross-references
henryiii Sep 22, 2026
d956d9e
feat(filenames): parse PEP 817 variant labels in wheel filenames
henryiii Sep 22, 2026
29bf724
fix(filenames): validate project names in strict wheel parsing
henryiii Sep 22, 2026
b358322
fix(filenames): address review findings
henryiii Sep 22, 2026
f849b2f
refactor(filenames): share a base class and one error formatter
henryiii Sep 22, 2026
c89e0a2
docs: note on updating
henryiii Sep 22, 2026
9128884
refactor(filenames): keep name helpers in packaging.utils
henryiii Sep 22, 2026
8814c0b
fix(filenames): compare on normalized name and version
henryiii Sep 22, 2026
a0f2e73
feat(filenames): validate in the constructor and add __replace__
henryiii Sep 22, 2026
107c3f8
refactor(filenames): drop the underscore parameter of canonicalize_name
henryiii Sep 22, 2026
b4290cd
chore: metaclass
henryiii Sep 22, 2026
5df48eb
refactor(filenames): store only the normalized name and version
henryiii Sep 23, 2026
f8881b0
refactor(filenames): drop the base class and move helpers to functions
henryiii Sep 23, 2026
6a41b7d
refactor(filenames): simplify replace and merge overlapping tests
henryiii Sep 23, 2026
bdb15d5
refactor(filenames): move strict checks to validate helper functions
henryiii Sep 23, 2026
d2199bf
refactor(filenames): collect normalization errors into an ExceptionGroup
henryiii Sep 23, 2026
ee5f219
refactor(filenames): parse each filename once and share field checks
henryiii Sep 23, 2026
c6609ab
feat(filenames)!: require a valid tag set for WheelFilename
henryiii Sep 23, 2026
d0bd426
feat(filenames): add a stable pickle format
henryiii Sep 23, 2026
079c3b8
docs(filenames): describe properties and supported operations
henryiii Sep 23, 2026
734505c
docs(filenames): show how to mimic validate_order=True
henryiii Sep 23, 2026
f26dd04
refactor(filenames): inline single-purpose check helpers
henryiii Sep 23, 2026
366497f
refactor(filenames): inline _invalid and short check helpers
henryiii Sep 23, 2026
17ae3d4
refactor(filenames): inline _check_replace_keys
henryiii Sep 23, 2026
7670b81
fix(filenames): reject wheel tags that do not round-trip
henryiii Sep 23, 2026
a386075
refactor(filenames): use the filename string as pickle state
henryiii Sep 23, 2026
73f6f84
refactor(filenames): inline _check_normalized
henryiii Sep 23, 2026
3e25621
docs: touch up filenames
henryiii Sep 23, 2026
f3ff608
fix(filenames): collect invalid project names when validating
henryiii Sep 23, 2026
7a2547e
fix(filenames): validate build tags consistently and split name errors
henryiii Sep 23, 2026
14c0fdf
fix(filenames): do not re-parse the filename in __replace__ and copy
henryiii Sep 23, 2026
e9c6e1e
perf(filenames): parse into tuples and move parsers to utils
henryiii Sep 23, 2026
5885a4b
refactor(filenames): replace validate_*_filename with .validate() met…
henryiii Sep 23, 2026
3798403
refactor(filenames): merge wheel split and parse helpers
henryiii Sep 23, 2026
785a92a
feat(filenames): add original_filename and group sdist .zip error
henryiii Sep 24, 2026
dd7421d
perf(filenames): speed up validate()
henryiii Sep 24, 2026
0118666
feat(filenames)!: make WheelFilename build_tag and variant keyword-only
henryiii Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions docs/filenames.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
Filenames
=========

Tools to work with filenames for SDists and wheels.

.. versionadded:: 26.4

Working with filenames
----------------------

To parse a filename, use ``.from_filename`` on
:class:`~packaging.filenames.WheelFilename` or
:class:`~packaging.filenames.SourceDistributionFilename`:

.. doctest::

>>> from packaging.filenames import WheelFilename
>>> wheel_filename = WheelFilename.from_filename("foo-1.0-1-py3-none-any.whl")

Parsing normalizes filenames. To see if a filename was already normalized, call
``.validate()`` on the parsed filename. It collects all normalization problems
into an :external:exc:`ExceptionGroup`. Each problem has its own exception
class, so you can use ``except*`` to handle or ignore only some of them. The
classes are:

* :class:`~packaging.filenames.InvalidProjectName`: the name parses, but is
not a valid project name.
* :class:`~packaging.filenames.NonNormalizedName`: the name is valid, but not
normalized.
* :class:`~packaging.filenames.NonNormalizedVersion`: the version is valid,
but not normalized.
* :class:`~packaging.filenames.InvalidSdistFilename` (sdist only): the
extension is ``.zip``, not ``.tar.gz``.
* :class:`~packaging.filenames.NonNormalizedBuildTag` (wheel only): the build
tag is valid, but not normalized.
* :class:`~packaging.filenames.UnsortedWheelTags` (wheel only): the parts of
the compressed tag set are not in sorted order.
* :class:`~packaging.filenames.NonNormalizedTags` (wheel only): the tags are
sorted, but not normalized.

For example, to accept a non-normalized name and version, but reject all other
problems:

.. code-block:: python

from packaging.filenames import (
NonNormalizedName,
NonNormalizedVersion,
WheelFilename,
)

wheel = WheelFilename.from_filename("Foo-01.0-py3-none-any.whl")
try:
wheel.validate()
except* (NonNormalizedName, NonNormalizedVersion):
pass

On Python 3.10, catch the :class:`~packaging.errors.ExceptionGroup` backport
and process it directly (see :doc:`errors`).

Using filenames
---------------

Filenames have the following properties:

* ``name``: The normalized name.
* ``version``: The version as a :class:`~packaging.version.Version`.
* ``tags`` (wheel only): A frozenset of :class:`~packaging.tags.Tag`.
* ``build_tag`` (wheel only): An empty tuple, or a tuple of (build number,
build tag suffix). The suffix can be an empty string.
* ``variant`` (wheel only): The variant label, or ``None``.
* ``original_filename``: The filename given to ``.from_filename``, or ``None``
if the filename was constructed directly or with ``__replace__``.

There are also a few helper properties for wheels:

* ``compressed_tags``: The sorted, compressed wheel tags as a string.
* ``build_str``: The build tag as a string, or an empty string.

The immutable classes support most operations that you would expect:

* Use :func:`copy.replace` (Python 3.13+) or ``__replace__`` to replace parts
of the filename.
* Equality and hashing work. The number of trailing zeros in the version is
significant, so ``1.0`` and ``1.0.0`` are different. The original filename
is not compared, so a parsed filename can be equal to one that fails
``.validate()``.
* Pickling and unpickling are supported, and the pickle format is stable.
A pickle keeps the original filename, so ``.validate()`` gives the same
result after unpickling. A filename that is constructed or changed with
``__replace__`` has no original filename, and ``.validate()`` always
passes.
* Converting to a string (or ``.to_filename()``) produces a fully normalized
filename with the standard extension (``.whl`` or ``.tar.gz``).

Converting from older utils
---------------------------

The older :func:`packaging.utils.parse_wheel_filename` does not support
variant labels (:pep:`825`). To support them, replace the old form:

.. doctest::

>>> from packaging.utils import parse_wheel_filename
>>> name, version, build_tag, tags = parse_wheel_filename(
... "foo-1.0-1-py3-none-any.whl"
... )

with:

.. doctest::

>>> from packaging.filenames import WheelFilename
>>> wheel = WheelFilename.from_filename("foo-1.0-1-py3-none-any.whl")
>>> name, version, build_tag, tags = (
... wheel.name, wheel.version, wheel.build_tag, wheel.tags
... )
>>> wheel.variant is None
True

To replace ``validate_order=True``, call
:meth:`WheelFilename.validate() <packaging.filenames.WheelFilename.validate>`.
It reports unsorted tags with
:class:`~packaging.filenames.UnsortedWheelTags`. It also checks that the other
parts of the filename are normalized. To check only the tag order and
reject variant wheels, like ``parse_wheel_filename(..., validate_order=True)``,
this time showing Python 3.10+ compatible syntax:

.. testcode::

import sys

from packaging.filenames import (
InvalidWheelFilename,
UnsortedWheelTags,
WheelFilename,
)

if sys.version_info < (3, 11):
from packaging.errors import ExceptionGroup


def parse_ordered(filename: str) -> WheelFilename:
wheel = WheelFilename.from_filename(filename)
try:
wheel.validate()
except ExceptionGroup as group:
for error in group.exceptions:
if isinstance(error, UnsortedWheelTags):
raise error from None
if wheel.variant is not None:
msg = f"Invalid wheel filename (variant wheels are not supported): {filename!r}"
raise InvalidWheelFilename(msg)
return wheel

.. doctest::

>>> parse_ordered("Foo-1.0-py3.py2-none-any.whl")
Traceback (most recent call last):
...
packaging.filenames.UnsortedWheelTags: Invalid wheel filename (compressed tag set components must be in sorted order per PEP 425): 'Foo-1.0-py3.py2-none-any.whl'
>>> parse_ordered("foo-1.0-py3-none-any-x86_64_v3.whl")
Traceback (most recent call last):
...
packaging.filenames.InvalidWheelFilename: Invalid wheel filename (variant wheels are not supported): 'foo-1.0-py3-none-any-x86_64_v3.whl'
>>> str(parse_ordered("foo-1.0-py2.py3-none-any.whl"))
'foo-1.0-py2.py3-none-any.whl'

Reference
---------

.. automodule:: packaging.filenames
:members:
:inherited-members:
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The ``packaging`` library uses calendar-based versioning (``YY.N``).
direct_url
dependency_groups
errors
filenames
utils

.. toctree::
Expand Down
Loading
Loading