comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/site-packages/wheel/metadata.py @ 69:33d812a61356

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 17:55:14 -0400
parents
children
comparison
equal deleted inserted replaced
67:0e9998148a16 69:33d812a61356
1 """
2 Tools for converting old- to new-style metadata.
3 """
4
5 from __future__ import annotations
6
7 import functools
8 import itertools
9 import os.path
10 import re
11 import textwrap
12 from email.message import Message
13 from email.parser import Parser
14 from typing import Generator, Iterable, Iterator, Literal
15
16 from .vendored.packaging.requirements import Requirement
17
18
19 def _nonblank(str: str) -> bool | Literal[""]:
20 return str and not str.startswith("#")
21
22
23 @functools.singledispatch
24 def 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)
42 def _(text: str) -> Iterator[str]:
43 return filter(_nonblank, map(str.strip, text.splitlines()))
44
45
46 def 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) pairs
50 Each ``section`` is a stripped version of the section header ("[section]")
51 and each ``content`` is a list of stripped lines excluding blank lines and
52 comment-only lines. If there are any such lines before the first section
53 header, they're returned in a first ``section`` of ``None``.
54 """
55 section = None
56 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, content
62 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 segment
70 yield section, content
71
72
73 def safe_extra(extra: str) -> str:
74 """Convert an arbitrary string to a standard 'extra' name
75 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
81 def safe_name(name: str) -> str:
82 """Convert an arbitrary string to a standard distribution name
83 Any runs of non-alphanumeric/. characters are replaced with a single '-'.
84 """
85 return re.sub("[^A-Za-z0-9.]+", "-", name)
86
87
88 def 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.url
92
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
103 def 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 + spec
113
114
115 def generate_requirements(
116 extras_require: dict[str | None, list[str]],
117 ) -> Iterator[tuple[str, str]]:
118 """
119 Convert requirements from a setup()-style dictionary to
120 ('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 syntax
129 extra, condition = extra.split(":", 1)
130
131 extra = safe_extra(extra)
132 if extra:
133 yield "Provides-Extra", extra
134 if condition:
135 condition = "(" + condition + ") and "
136 condition += f"extra == '{extra}'"
137
138 if condition:
139 condition = " ; " + condition
140
141 for new_req in convert_requirements(depends):
142 canonical_req = str(Requirement(new_req + condition))
143 yield "Requires-Dist", canonical_req
144
145
146 def 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 format
149 """
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] = value
167
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_info