annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/site-packages/wheel/metadata.py @ 68:5028fdace37b

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