Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 78 additions & 1 deletion src/packaging/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
from __future__ import annotations

import re
from typing import NewType, Tuple, Union, cast
from typing import TYPE_CHECKING, NewType, Tuple, Union, cast

from .tags import Tag, parse_tag
from .version import InvalidVersion, Version, _TrimmedRelease

if TYPE_CHECKING:
from collections.abc import Iterable

__all__ = [
"BuildTag",
"InvalidName",
Expand All @@ -18,6 +21,8 @@
"NormalizedName",
"canonicalize_name",
"canonicalize_version",
"compose_sdist_filename",
"compose_wheel_filename",
"is_normalized_name",
"parse_sdist_filename",
"parse_wheel_filename",
Expand Down Expand Up @@ -61,6 +66,7 @@ class InvalidSdistFilename(ValueError):
_normalized_regex = re.compile(r"[a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9]", re.ASCII)
# PEP 427: The build number must start with a digit.
_build_tag_regex = re.compile(r"(\d+)(.*)", re.ASCII)
_distribution_regex = re.compile(r"[^\w\d.]+", re.ASCII)


def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
Expand Down Expand Up @@ -154,6 +160,56 @@ def canonicalize_version(
return str(_TrimmedRelease(version) if strip_trailing_zero else version)


def _join_tag_attr(tags: Iterable[Tag], field: str) -> str:
return ".".join(sorted({getattr(tag, field) for tag in tags}))


def _compress_tag_set(tags: Iterable[Tag]) -> str:
return "-".join(_join_tag_attr(tags, x) for x in ("interpreter", "abi", "platform"))


def compose_wheel_filename(
name: str, version: Version, build: BuildTag | None, tags: Iterable[Tag]
) -> str:
"""
Combines a project name, version, build tag, and tag set
to make a properly formatted wheel filename.

The project name is normalized such that the non-alphanumeric
characters are replaced with ``_``. The version is an instance of
:class:`~packaging.version.Version`. The build tag can be None,
an empty tuple or a two-item tuple of an integer and a string.
The tags is set of tags that will be compressed into a wheel
tag string.

:param name: The project name
:param version: The project version
:param build: An optional two-item tuple of an integer and string
:param tags: The set of tags that apply to the wheel

>>> from packaging.utils import compose_wheel_filename
>>> from packaging.tags import Tag
>>> from packaging.version import Version
>>> version = Version("1.0")
>>> tags = {Tag("py3", "none", "any")}
>>> compose_wheel_filename("foo-bar", version, None, tags)
'foo_bar-1.0-py3-none-any.whl'

.. versionadded:: 26.1
"""
norm_name = canonicalize_name(name).replace("-", "_")
compressed_tag = _compress_tag_set(tags)

parts: tuple[str, ...]

if build:
parts = norm_name, str(version), "".join(map(str, build)), compressed_tag
else:
parts = norm_name, str(version), compressed_tag

return "-".join(parts) + ".whl"


def parse_wheel_filename(
filename: str,
) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:
Expand Down Expand Up @@ -229,6 +285,27 @@ def parse_wheel_filename(
return (name, version, build, tags)


def compose_sdist_filename(name: str, version: Version) -> str:
"""
Combines the project name and a version to make a valid sdist filename. The
project name is normalized as required so that any run of ``-._``
characters are replaced with ``_`` and characters are lower cased. The
version is an instance of :class:`~packaging.version.Version`.

:param name: The project name
:param version: The project version

>>> from packaging.utils import compose_sdist_filename
>>> from packaging.version import Version
>>> "foo_bar-1.0.tar.gz" == compose_sdist_filename("foo-bar", Version("1.0"))
True

.. versionadded:: 26.1
"""
norm_name = canonicalize_name(name).replace("-", "_")
return f"{norm_name}-{version}.tar.gz"


def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
"""
This function takes the filename of a sdist file (as specified
Expand Down
69 changes: 69 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@

from packaging.tags import Tag
from packaging.utils import (
BuildTag,
InvalidName,
InvalidSdistFilename,
InvalidWheelFilename,
canonicalize_name,
canonicalize_version,
compose_sdist_filename,
compose_wheel_filename,
is_normalized_name,
parse_sdist_filename,
parse_wheel_filename,
Expand Down Expand Up @@ -106,6 +109,52 @@ def test_canonicalize_version_no_strip_trailing_zero(version: str) -> None:
assert canonicalize_version(version, strip_trailing_zero=False) == version


@pytest.mark.parametrize(
("filename", "name", "version", "build", "tags"),
[
(
"foo-1.0-py3-none-any.whl",
"foo",
Version("1.0"),
(),
{Tag("py3", "none", "any")},
),
(
"some_package-1.0-py3-none-any.whl",
"some-PACKAGE",
Version("1.0"),
(),
{Tag("py3", "none", "any")},
),
(
"foo-1.0-1000-py3-none-any.whl",
"foo",
Version("1.0"),
(1000, ""),
{Tag("py3", "none", "any")},
),
(
"foo-1.0-1000abc-py3-none-any.whl",
"foo",
Version("1.0"),
(1000, "abc"),
{Tag("py3", "none", "any")},
),
(
"foo_bar-1.0-42-py2.py3-none-any.whl",
"foo-bar",
Version("1.0"),
(42, ""),
{Tag("py2", "none", "any"), Tag("py3", "none", "any")},
),
],
)
def test_compose_wheel_filename(
filename: str, name: str, version: Version, build: BuildTag | None, tags: set[Tag]
) -> None:
assert compose_wheel_filename(name, version, build, tags) == filename


@pytest.mark.parametrize(
("filename", "name", "version", "build", "tags"),
[
Expand Down Expand Up @@ -177,6 +226,26 @@ def test_parse_wheel_invalid_filename(filename: str) -> None:
parse_wheel_filename(filename)


def test_parse_and_create_filename() -> None:
filename = "numpy-1.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
sorted_f = "numpy-1.23.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"

name, version, build, tags = parse_wheel_filename(filename)
composed = compose_wheel_filename(name, version, build, tags)
assert sorted_f == composed


@pytest.mark.parametrize(
("filename", "name", "version"),
[
("foo-1.0.tar.gz", "foo", Version("1.0")),
("foo_bar-1.0.tar.gz", "foo-bar", Version("1.0")),
],
)
def test_compose_sdist_filename(filename: str, name: str, version: Version) -> None:
assert compose_sdist_filename(name, version) == filename


@pytest.mark.parametrize(
("filename", "name", "version"),
[("foo-1.0.tar.gz", "foo", Version("1.0")), ("foo-1.0.zip", "foo", Version("1.0"))],
Expand Down