diff --git a/docs/utils.rst b/docs/utils.rst index 134094aa..c4487abe 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -39,3 +39,45 @@ Reference >>> from packaging.utils import canonicalize_version >>> canonicalize_version('1.4.0.0.0') '1.4' + +.. function:: parse_wheel_filename(filename) + + This function takes the filename of a wheel file, and parses it, + returning a tuple of name, version, build number and tags. The + build number will be ``None`` if there is no build number in the + wheel filename. + + :param str filename: The name of the wheel file. + + .. doctest:: + + >>> from packaging.utils import parse_wheel_filename + >>> from packaging.tags import Tag + >>> name, ver, build, tags = parse_wheel_filename("foo-1.0-py3-none-any.whl") + >>> name + 'foo' + >>> ver + + >>> tags == {Tag("py3", "none", "any")} + True + >>> build is None + True + +.. function:: parse_sdist_filename(filename) + + This function takes the filename of a sdist file (as specified + in the `Source distribution format`_ documentation), and parses + it, returning a tuple of name and version. + + :param str filename: The name of the sdist file. + + .. doctest:: + + >>> from packaging.utils import parse_sdist_filename + >>> name, ver = parse_sdist_filename("foo-1.0.tar.gz") + >>> name + 'foo' + >>> ver + + +.. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name diff --git a/noxfile.py b/noxfile.py index a77643cb..732abb6b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -62,6 +62,7 @@ def lint(session): def docs(session): shutil.rmtree("docs/_build", ignore_errors=True) session.install("furo") + session.install("-e", ".") variants = [ # (builder, dest) diff --git a/packaging/utils.py b/packaging/utils.py index 92c7b00b..ab8e82a5 100644 --- a/packaging/utils.py +++ b/packaging/utils.py @@ -6,15 +6,29 @@ import re from ._typing import TYPE_CHECKING, cast +from .tags import Tag, parse_tag from .version import InvalidVersion, Version if TYPE_CHECKING: # pragma: no cover - from typing import NewType, Union + from typing import FrozenSet, NewType, Optional, Tuple, Union NormalizedName = NewType("NormalizedName", str) else: NormalizedName = str + +class InvalidWheelFilename(ValueError): + """ + An invalid wheel filename was found, users should refer to PEP 427. + """ + + +class InvalidSdistFilename(ValueError): + """ + An invalid sdist filename was found, users should refer to the packaging user guide. + """ + + _canonicalize_regex = re.compile(r"[-_.]+") @@ -65,3 +79,56 @@ def canonicalize_version(version): parts.append("+{0}".format(version.local)) return "".join(parts) + + +def parse_wheel_filename(filename): + # type: (str) -> Tuple[NormalizedName, Version, Optional[str], FrozenSet[Tag]] + if not filename.endswith(".whl"): + raise InvalidWheelFilename( + "Invalid wheel filename (extension must be '.whl'): {0}".format(filename) + ) + + filename = filename[:-4] + dashes = filename.count("-") + if dashes not in (4, 5): + raise InvalidWheelFilename( + "Invalid wheel filename (wrong number of parts): {0}".format(filename) + ) + + parts = filename.split("-", dashes - 2) + name_part = parts[0] + # See PEP 427 for the rules on escaping the project name + if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: + raise InvalidWheelFilename("Invalid project name: {0}".format(filename)) + name = canonicalize_name(name_part) + version = Version(parts[1]) + if dashes == 5: + # Build number must start with a digit + build_part = parts[2] + if not build_part[0].isdigit(): + raise InvalidWheelFilename( + "Invalid build number: {0} in '{1}'".format(build_part, filename) + ) + build = build_part # type: Optional[str] + else: + build = None + tags = parse_tag(parts[-1]) + return (name, version, build, tags) + + +def parse_sdist_filename(filename): + # type: (str) -> Tuple[NormalizedName, Version] + if not filename.endswith(".tar.gz"): + raise InvalidSdistFilename( + "Invalid sdist filename (extension must be '.tar.gz'): {0}".format(filename) + ) + + # We are requiring a PEP 440 version, which cannot contain dashes, + # so we split on the last dash. + name_part, sep, version_part = filename[:-7].rpartition("-") + if not sep: + raise InvalidSdistFilename("Invalid sdist filename: {0}".format(filename)) + + name = canonicalize_name(name_part) + version = Version(version_part) + return (name, version) diff --git a/tests/test_utils.py b/tests/test_utils.py index a8ea6bdb..a5e0d090 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,7 +5,15 @@ import pytest -from packaging.utils import canonicalize_name, canonicalize_version +from packaging.tags import Tag +from packaging.utils import ( + InvalidSdistFilename, + InvalidWheelFilename, + canonicalize_name, + canonicalize_version, + parse_sdist_filename, + parse_wheel_filename, +) from packaging.version import Version @@ -47,3 +55,54 @@ def test_canonicalize_name(name, expected): ) def test_canonicalize_version(version, expected): assert canonicalize_version(version) == expected + + +@pytest.mark.parametrize( + ("filename", "name", "version", "build", "tags"), + [ + ( + "foo-1.0-py3-none-any.whl", + "foo", + Version("1.0"), + None, + {Tag("py3", "none", "any")}, + ), + ( + "foo-1.0-1000-py3-none-any.whl", + "foo", + Version("1.0"), + "1000", + {Tag("py3", "none", "any")}, + ), + ], +) +def test_parse_wheel_filename(filename, name, version, build, tags): + assert parse_wheel_filename(filename) == (name, version, build, tags) + + +@pytest.mark.parametrize( + ("filename"), + [ + ("foo-1.0.wheel"), + ("foo__bar-1.0-py3-none-any.whl"), + ("foo#bar-1.0-py3-none-any.whl"), + ("foo-1.0-abc-py3-none-any.whl"), + ("foo-1.0-200-py3-none-any-junk.whl"), + ], +) +def test_parse_wheel_invalid_filename(filename): + with pytest.raises(InvalidWheelFilename): + parse_wheel_filename(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) + + +@pytest.mark.parametrize(("filename"), [("foo-1.0.zip"), ("foo1.0.tar.gz")]) +def test_parse_sdist_invalid_filename(filename): + with pytest.raises(InvalidSdistFilename): + parse_sdist_filename(filename)