1# This file is dual licensed under the terms of the Apache License, Version 2# 2.0, and the BSD License. See the LICENSE file in the root of this repository 3# for complete details. 4from __future__ import absolute_import, division, print_function 5 6import re 7 8from ._typing import TYPE_CHECKING, cast 9from .tags import Tag, parse_tag 10from .version import InvalidVersion, Version 11 12if TYPE_CHECKING: # pragma: no cover 13 from typing import FrozenSet, NewType, Tuple, Union 14 15 BuildTag = Union[Tuple[()], Tuple[int, str]] 16 NormalizedName = NewType("NormalizedName", str) 17else: 18 BuildTag = tuple 19 NormalizedName = str 20 21 22class InvalidWheelFilename(ValueError): 23 """ 24 An invalid wheel filename was found, users should refer to PEP 427. 25 """ 26 27 28class InvalidSdistFilename(ValueError): 29 """ 30 An invalid sdist filename was found, users should refer to the packaging user guide. 31 """ 32 33 34_canonicalize_regex = re.compile(r"[-_.]+") 35# PEP 427: The build number must start with a digit. 36_build_tag_regex = re.compile(r"(\d+)(.*)") 37 38 39def canonicalize_name(name): 40 # type: (str) -> NormalizedName 41 # This is taken from PEP 503. 42 value = _canonicalize_regex.sub("-", name).lower() 43 return cast(NormalizedName, value) 44 45 46def canonicalize_version(version): 47 # type: (Union[Version, str]) -> Union[Version, str] 48 """ 49 This is very similar to Version.__str__, but has one subtle difference 50 with the way it handles the release segment. 51 """ 52 if not isinstance(version, Version): 53 try: 54 version = Version(version) 55 except InvalidVersion: 56 # Legacy versions cannot be normalized 57 return version 58 59 parts = [] 60 61 # Epoch 62 if version.epoch != 0: 63 parts.append("{0}!".format(version.epoch)) 64 65 # Release segment 66 # NB: This strips trailing '.0's to normalize 67 parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in version.release))) 68 69 # Pre-release 70 if version.pre is not None: 71 parts.append("".join(str(x) for x in version.pre)) 72 73 # Post-release 74 if version.post is not None: 75 parts.append(".post{0}".format(version.post)) 76 77 # Development release 78 if version.dev is not None: 79 parts.append(".dev{0}".format(version.dev)) 80 81 # Local version segment 82 if version.local is not None: 83 parts.append("+{0}".format(version.local)) 84 85 return "".join(parts) 86 87 88def parse_wheel_filename(filename): 89 # type: (str) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]] 90 if not filename.endswith(".whl"): 91 raise InvalidWheelFilename( 92 "Invalid wheel filename (extension must be '.whl'): {0}".format(filename) 93 ) 94 95 filename = filename[:-4] 96 dashes = filename.count("-") 97 if dashes not in (4, 5): 98 raise InvalidWheelFilename( 99 "Invalid wheel filename (wrong number of parts): {0}".format(filename) 100 ) 101 102 parts = filename.split("-", dashes - 2) 103 name_part = parts[0] 104 # See PEP 427 for the rules on escaping the project name 105 if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: 106 raise InvalidWheelFilename("Invalid project name: {0}".format(filename)) 107 name = canonicalize_name(name_part) 108 version = Version(parts[1]) 109 if dashes == 5: 110 build_part = parts[2] 111 build_match = _build_tag_regex.match(build_part) 112 if build_match is None: 113 raise InvalidWheelFilename( 114 "Invalid build number: {0} in '{1}'".format(build_part, filename) 115 ) 116 build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) 117 else: 118 build = () 119 tags = parse_tag(parts[-1]) 120 return (name, version, build, tags) 121 122 123def parse_sdist_filename(filename): 124 # type: (str) -> Tuple[NormalizedName, Version] 125 if not filename.endswith(".tar.gz"): 126 raise InvalidSdistFilename( 127 "Invalid sdist filename (extension must be '.tar.gz'): {0}".format(filename) 128 ) 129 130 # We are requiring a PEP 440 version, which cannot contain dashes, 131 # so we split on the last dash. 132 name_part, sep, version_part = filename[:-7].rpartition("-") 133 if not sep: 134 raise InvalidSdistFilename("Invalid sdist filename: {0}".format(filename)) 135 136 name = canonicalize_name(name_part) 137 version = Version(version_part) 138 return (name, version) 139