Mercurial > repos > rliterman > csp2
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 |