diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d8170e8b..13665ebe 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog *unreleased* ~~~~~~~~~~~~ +* Added ``packaging.utils.create_wheel_filename()`` and ``create_sdist_filename()`` (:issue:`408`) * ``Marker.evaluate`` will now assume evaluation environment with empty ``extra``. Evaluating markers like ``"extra == 'xyz'"`` without passing any extra in the ``environment`` will no longer raise an exception. diff --git a/docs/utils.rst b/docs/utils.rst index 7a4c2f74..ea2d15cb 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -49,6 +49,33 @@ Reference >>> canonicalize_version('1.4.0.0.0') '1.4' +.. function:: create_wheel_filename(name, version, build, tags) + + 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 str name: The project name + :param ~packaging.version.Version version: The project version + :param Optional[(),(int,str)] build: An optional two-item tuple of an integer and string + :param set[~packaging.tags.Tag] tags: The set of tags that apply to the wheel + + .. doctest:: + + >>> from packaging.utils import create_wheel_filename + >>> from packaging.tags import Tag + >>> from packaging.version import Version + >>> version = Version("1.0") + >>> tags = {Tag("py3", "none", "any")} + >>> "foo_bar-1.0-py3-none-any.whl" == create_wheel_filename("foo-bar", version, None, tags) + True + .. function:: parse_wheel_filename(filename) This function takes the filename of a wheel file, and parses it, @@ -81,6 +108,20 @@ Reference >>> not build True +.. function:: create_sdist_filename(name, version) + + Combines the project name and a version to make a valid sdist filename. + + :param str name: The project name + :param ~packaging.version.Version version: The project version + + .. doctest:: + + >>> from packaging.utils import create_sdist_filename + >>> from packaging.version import Version + >>> "foo_bar-1.0.tar.gz" == create_sdist_filename("foo-bar", Version("1.0")) + True + .. function:: parse_sdist_filename(filename) This function takes the filename of a sdist file (as specified diff --git a/packaging/utils.py b/packaging/utils.py index 33c613b7..3c0e3744 100644 --- a/packaging/utils.py +++ b/packaging/utils.py @@ -3,7 +3,7 @@ # for complete details. import re -from typing import FrozenSet, NewType, Tuple, Union, cast +from typing import AbstractSet, FrozenSet, NewType, Optional, Tuple, Union, cast from .tags import Tag, parse_tag from .version import InvalidVersion, Version @@ -24,6 +24,7 @@ class InvalidSdistFilename(ValueError): """ +_distribution_regex = re.compile(r"[^\w\d.]+") _canonicalize_regex = re.compile(r"[-_.]+") # PEP 427: The build number must start with a digit. _build_tag_regex = re.compile(r"(\d+)(.*)") @@ -83,6 +84,30 @@ def canonicalize_version( return "".join(parts) +def _join_tag_attr(tags: AbstractSet[Tag], field: str) -> str: + return ".".join(sorted({getattr(tag, field) for tag in tags})) + + +def _compress_tag_set(tags: AbstractSet[Tag]) -> str: + return "-".join(_join_tag_attr(tags, x) for x in ("interpreter", "abi", "platform")) + + +def create_wheel_filename( + name: str, version: Version, build: Optional[BuildTag], tags: AbstractSet[Tag] +) -> str: + norm_name = _distribution_regex.sub("_", name) + 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]]: @@ -119,6 +144,10 @@ def parse_wheel_filename( return (name, version, build, tags) +def create_sdist_filename(name: str, version: Version) -> str: + return f"{_distribution_regex.sub('_', name)}-{version}.tar.gz" + + def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]: if filename.endswith(".tar.gz"): file_stem = filename[: -len(".tar.gz")] diff --git a/tests/test_utils.py b/tests/test_utils.py index a6c6711d..8b125e3b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,6 +10,8 @@ InvalidWheelFilename, canonicalize_name, canonicalize_version, + create_sdist_filename, + create_wheel_filename, parse_sdist_filename, parse_wheel_filename, ) @@ -94,6 +96,43 @@ def test_canonicalize_version_no_strip_trailing_zero(version): (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_create_wheel_filename(filename, name, version, build, tags): + assert create_wheel_filename(name, version, build, tags) == filename + + +@pytest.mark.parametrize( + ("filename", "name", "version", "build", "tags"), + [ + ( + "foo-1.0-py3-none-any.whl", + "foo", + 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")}, + ), ], ) def test_parse_wheel_filename(filename, name, version, build, tags): @@ -119,7 +158,17 @@ def test_parse_wheel_invalid_filename(filename): @pytest.mark.parametrize( ("filename", "name", "version"), - [("foo-1.0.tar.gz", "foo", Version("1.0")), ("foo-1.0.zip", "foo", Version("1.0"))], + [ + ("foo-1.0.tar.gz", "foo", Version("1.0")), + ("foo_bar-1.0.tar.gz", "foo-bar", Version("1.0")), + ], +) +def test_create_sdist_filename(filename, name, version): + assert create_sdist_filename(name, version) == filename + + +@pytest.mark.parametrize( + ("filename", "name", "version"), [("foo-1.0.tar.gz", "foo", Version("1.0"))] ) def test_parse_sdist_filename(filename, name, version): assert parse_sdist_filename(filename) == (name, version)