gmake_python_helpers

Helpers for using Python venv/pip-tools/black/pylama/... with GNU make
git clone https://ccx.te2000.cz/git/gmake_python_helpers
Log | Files | Refs | README

utils.py (5050B)


      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.
      4 
      5 from __future__ import annotations
      6 
      7 import functools
      8 import re
      9 from typing import NewType, Tuple, Union, cast
     10 
     11 from .tags import Tag, parse_tag
     12 from .version import InvalidVersion, Version, _TrimmedRelease
     13 
     14 BuildTag = Union[Tuple[()], Tuple[int, str]]
     15 NormalizedName = NewType("NormalizedName", str)
     16 
     17 
     18 class InvalidName(ValueError):
     19     """
     20     An invalid distribution name; users should refer to the packaging user guide.
     21     """
     22 
     23 
     24 class InvalidWheelFilename(ValueError):
     25     """
     26     An invalid wheel filename was found, users should refer to PEP 427.
     27     """
     28 
     29 
     30 class InvalidSdistFilename(ValueError):
     31     """
     32     An invalid sdist filename was found, users should refer to the packaging user guide.
     33     """
     34 
     35 
     36 # Core metadata spec for `Name`
     37 _validate_regex = re.compile(
     38     r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE
     39 )
     40 _canonicalize_regex = re.compile(r"[-_.]+")
     41 _normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$")
     42 # PEP 427: The build number must start with a digit.
     43 _build_tag_regex = re.compile(r"(\d+)(.*)")
     44 
     45 
     46 def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
     47     if validate and not _validate_regex.match(name):
     48         raise InvalidName(f"name is invalid: {name!r}")
     49     # This is taken from PEP 503.
     50     value = _canonicalize_regex.sub("-", name).lower()
     51     return cast(NormalizedName, value)
     52 
     53 
     54 def is_normalized_name(name: str) -> bool:
     55     return _normalized_regex.match(name) is not None
     56 
     57 
     58 @functools.singledispatch
     59 def canonicalize_version(
     60     version: Version | str, *, strip_trailing_zero: bool = True
     61 ) -> str:
     62     """
     63     Return a canonical form of a version as a string.
     64 
     65     >>> canonicalize_version('1.0.1')
     66     '1.0.1'
     67 
     68     Per PEP 625, versions may have multiple canonical forms, differing
     69     only by trailing zeros.
     70 
     71     >>> canonicalize_version('1.0.0')
     72     '1'
     73     >>> canonicalize_version('1.0.0', strip_trailing_zero=False)
     74     '1.0.0'
     75 
     76     Invalid versions are returned unaltered.
     77 
     78     >>> canonicalize_version('foo bar baz')
     79     'foo bar baz'
     80     """
     81     return str(_TrimmedRelease(str(version)) if strip_trailing_zero else version)
     82 
     83 
     84 @canonicalize_version.register
     85 def _(version: str, *, strip_trailing_zero: bool = True) -> str:
     86     try:
     87         parsed = Version(version)
     88     except InvalidVersion:
     89         # Legacy versions cannot be normalized
     90         return version
     91     return canonicalize_version(parsed, strip_trailing_zero=strip_trailing_zero)
     92 
     93 
     94 def parse_wheel_filename(
     95     filename: str,
     96 ) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:
     97     if not filename.endswith(".whl"):
     98         raise InvalidWheelFilename(
     99             f"Invalid wheel filename (extension must be '.whl'): {filename!r}"
    100         )
    101 
    102     filename = filename[:-4]
    103     dashes = filename.count("-")
    104     if dashes not in (4, 5):
    105         raise InvalidWheelFilename(
    106             f"Invalid wheel filename (wrong number of parts): {filename!r}"
    107         )
    108 
    109     parts = filename.split("-", dashes - 2)
    110     name_part = parts[0]
    111     # See PEP 427 for the rules on escaping the project name.
    112     if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None:
    113         raise InvalidWheelFilename(f"Invalid project name: {filename!r}")
    114     name = canonicalize_name(name_part)
    115 
    116     try:
    117         version = Version(parts[1])
    118     except InvalidVersion as e:
    119         raise InvalidWheelFilename(
    120             f"Invalid wheel filename (invalid version): {filename!r}"
    121         ) from e
    122 
    123     if dashes == 5:
    124         build_part = parts[2]
    125         build_match = _build_tag_regex.match(build_part)
    126         if build_match is None:
    127             raise InvalidWheelFilename(
    128                 f"Invalid build number: {build_part} in {filename!r}"
    129             )
    130         build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2)))
    131     else:
    132         build = ()
    133     tags = parse_tag(parts[-1])
    134     return (name, version, build, tags)
    135 
    136 
    137 def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
    138     if filename.endswith(".tar.gz"):
    139         file_stem = filename[: -len(".tar.gz")]
    140     elif filename.endswith(".zip"):
    141         file_stem = filename[: -len(".zip")]
    142     else:
    143         raise InvalidSdistFilename(
    144             f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):"
    145             f" {filename!r}"
    146         )
    147 
    148     # We are requiring a PEP 440 version, which cannot contain dashes,
    149     # so we split on the last dash.
    150     name_part, sep, version_part = file_stem.rpartition("-")
    151     if not sep:
    152         raise InvalidSdistFilename(f"Invalid sdist filename: {filename!r}")
    153 
    154     name = canonicalize_name(name_part)
    155 
    156     try:
    157         version = Version(version_part)
    158     except InvalidVersion as e:
    159         raise InvalidSdistFilename(
    160             f"Invalid sdist filename (invalid version): {filename!r}"
    161         ) from e
    162 
    163     return (name, version)