CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
metadata.py184 linesDownload Raw Back to wheel
1"""2Tools for converting old- to new-style metadata.3"""4 5from __future__ import annotations6 7import functools8import itertools9import os.path10import re11import textwrap12from email.message import Message13from email.parser import Parser14from typing import Generator, Iterable, Iterator, Literal15 16from .vendored.packaging.requirements import Requirement17 18 19def _nonblank(str: str) -> bool | Literal[""]:20    return str and not str.startswith("#")21 22 23@functools.singledispatch24def yield_lines(iterable: Iterable[str]) -> Iterator[str]:25    r"""26    Yield valid lines of a string or iterable.27    >>> list(yield_lines(''))28    []29    >>> list(yield_lines(['foo', 'bar']))30    ['foo', 'bar']31    >>> list(yield_lines('foo\nbar'))32    ['foo', 'bar']33    >>> list(yield_lines('\nfoo\n#bar\nbaz #comment'))34    ['foo', 'baz #comment']35    >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n']))36    ['foo', 'bar', 'baz', 'bing']37    """38    return itertools.chain.from_iterable(map(yield_lines, iterable))39 40 41@yield_lines.register(str)42def _(text: str) -> Iterator[str]:43    return filter(_nonblank, map(str.strip, text.splitlines()))44 45 46def split_sections(47    s: str | Iterator[str],48) -> Generator[tuple[str | None, list[str]], None, None]:49    """Split a string or iterable thereof into (section, content) pairs50    Each ``section`` is a stripped version of the section header ("[section]")51    and each ``content`` is a list of stripped lines excluding blank lines and52    comment-only lines.  If there are any such lines before the first section53    header, they're returned in a first ``section`` of ``None``.54    """55    section = None56    content: list[str] = []57    for line in yield_lines(s):58        if line.startswith("["):59            if line.endswith("]"):60                if section or content:61                    yield section, content62                section = line[1:-1].strip()63                content = []64            else:65                raise ValueError("Invalid section heading", line)66        else:67            content.append(line)68 69    # wrap up last segment70    yield section, content71 72 73def safe_extra(extra: str) -> str:74    """Convert an arbitrary string to a standard 'extra' name75    Any runs of non-alphanumeric characters are replaced with a single '_',76    and the result is always lowercased.77    """78    return re.sub("[^A-Za-z0-9.-]+", "_", extra).lower()79 80 81def safe_name(name: str) -> str:82    """Convert an arbitrary string to a standard distribution name83    Any runs of non-alphanumeric/. characters are replaced with a single '-'.84    """85    return re.sub("[^A-Za-z0-9.]+", "-", name)86 87 88def requires_to_requires_dist(requirement: Requirement) -> str:89    """Return the version specifier for a requirement in PEP 345/566 fashion."""90    if requirement.url:91        return " @ " + requirement.url92 93    requires_dist: list[str] = []94    for spec in requirement.specifier:95        requires_dist.append(spec.operator + spec.version)96 97    if requires_dist:98        return " " + ",".join(sorted(requires_dist))99    else:100        return ""101 102 103def convert_requirements(requirements: list[str]) -> Iterator[str]:104    """Yield Requires-Dist: strings for parsed requirements strings."""105    for req in requirements:106        parsed_requirement = Requirement(req)107        spec = requires_to_requires_dist(parsed_requirement)108        extras = ",".join(sorted(safe_extra(e) for e in parsed_requirement.extras))109        if extras:110            extras = f"[{extras}]"111 112        yield safe_name(parsed_requirement.name) + extras + spec113 114 115def generate_requirements(116    extras_require: dict[str | None, list[str]],117) -> Iterator[tuple[str, str]]:118    """119    Convert requirements from a setup()-style dictionary to120    ('Requires-Dist', 'requirement') and ('Provides-Extra', 'extra') tuples.121 122    extras_require is a dictionary of {extra: [requirements]} as passed to setup(),123    using the empty extra {'': [requirements]} to hold install_requires.124    """125    for extra, depends in extras_require.items():126        condition = ""127        extra = extra or ""128        if ":" in extra:  # setuptools extra:condition syntax129            extra, condition = extra.split(":", 1)130 131        extra = safe_extra(extra)132        if extra:133            yield "Provides-Extra", extra134            if condition:135                condition = "(" + condition + ") and "136            condition += f"extra == '{extra}'"137 138        if condition:139            condition = " ; " + condition140 141        for new_req in convert_requirements(depends):142            canonical_req = str(Requirement(new_req + condition))143            yield "Requires-Dist", canonical_req144 145 146def pkginfo_to_metadata(egg_info_path: str, pkginfo_path: str) -> Message:147    """148    Convert .egg-info directory with PKG-INFO to the Metadata 2.1 format149    """150    with open(pkginfo_path, encoding="utf-8") as headers:151        pkg_info = Parser().parse(headers)152 153    pkg_info.replace_header("Metadata-Version", "2.1")154    # Those will be regenerated from `requires.txt`.155    del pkg_info["Provides-Extra"]156    del pkg_info["Requires-Dist"]157    requires_path = os.path.join(egg_info_path, "requires.txt")158    if os.path.exists(requires_path):159        with open(requires_path, encoding="utf-8") as requires_file:160            requires = requires_file.read()161 162        parsed_requirements = sorted(split_sections(requires), key=lambda x: x[0] or "")163        for extra, reqs in parsed_requirements:164            for key, value in generate_requirements({extra: reqs}):165                if (key, value) not in pkg_info.items():166                    pkg_info[key] = value167 168    description = pkg_info["Description"]169    if description:170        description_lines = pkg_info["Description"].splitlines()171        dedented_description = "\n".join(172            # if the first line of long_description is blank,173            # the first line here will be indented.174            (175                description_lines[0].lstrip(),176                textwrap.dedent("\n".join(description_lines[1:])),177                "\n",178            )179        )180        pkg_info.set_payload(dedented_description)181        del pkg_info["Description"]182 183    return pkg_info184 
Aluode/PerceptionLabPortable · CoolFace