Skip to content

Commit

Permalink
Add functions for parsing wheel and sdist filenames (#387)
Browse files Browse the repository at this point in the history
  • Loading branch information
pfmoore committed Jan 20, 2021
1 parent 1b26f84 commit da68002
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 2 deletions.
42 changes: 42 additions & 0 deletions docs/utils.rst
Expand Up @@ -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
<Version('1.0')>
>>> 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
<Version('1.0')>

.. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name
1 change: 1 addition & 0 deletions noxfile.py
Expand Up @@ -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)
Expand Down
69 changes: 68 additions & 1 deletion packaging/utils.py
Expand Up @@ -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"[-_.]+")


Expand Down Expand Up @@ -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)
61 changes: 60 additions & 1 deletion tests/test_utils.py
Expand Up @@ -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


Expand Down Expand Up @@ -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)

0 comments on commit da68002

Please sign in to comment.