pip 25.3__py3-none-any.whl → 26.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pip/__init__.py +1 -1
- pip/_internal/build_env.py +194 -5
- pip/_internal/cli/base_command.py +11 -0
- pip/_internal/cli/cmdoptions.py +157 -0
- pip/_internal/cli/index_command.py +20 -0
- pip/_internal/cli/main.py +11 -6
- pip/_internal/cli/main_parser.py +3 -1
- pip/_internal/cli/parser.py +93 -33
- pip/_internal/cli/progress_bars.py +4 -2
- pip/_internal/cli/req_command.py +99 -23
- pip/_internal/commands/cache.py +24 -0
- pip/_internal/commands/completion.py +2 -1
- pip/_internal/commands/download.py +8 -4
- pip/_internal/commands/index.py +13 -6
- pip/_internal/commands/install.py +36 -29
- pip/_internal/commands/list.py +14 -16
- pip/_internal/commands/lock.py +16 -8
- pip/_internal/commands/wheel.py +8 -13
- pip/_internal/exceptions.py +76 -3
- pip/_internal/index/collector.py +2 -3
- pip/_internal/index/package_finder.py +84 -18
- pip/_internal/locations/__init__.py +1 -2
- pip/_internal/locations/_sysconfig.py +4 -1
- pip/_internal/models/link.py +18 -14
- pip/_internal/models/release_control.py +92 -0
- pip/_internal/models/selection_prefs.py +6 -3
- pip/_internal/network/auth.py +6 -2
- pip/_internal/network/download.py +4 -5
- pip/_internal/network/session.py +14 -10
- pip/_internal/operations/install/wheel.py +1 -2
- pip/_internal/operations/prepare.py +2 -3
- pip/_internal/req/constructors.py +3 -1
- pip/_internal/req/pep723.py +41 -0
- pip/_internal/req/req_file.py +10 -1
- pip/_internal/resolution/resolvelib/factory.py +12 -1
- pip/_internal/resolution/resolvelib/requirements.py +7 -3
- pip/_internal/self_outdated_check.py +6 -13
- pip/_internal/utils/datetime.py +18 -0
- pip/_internal/utils/filesystem.py +40 -1
- pip/_internal/utils/logging.py +34 -2
- pip/_internal/utils/misc.py +18 -12
- pip/_internal/utils/pylock.py +116 -0
- pip/_internal/utils/unpacking.py +1 -1
- pip/_internal/vcs/versioncontrol.py +3 -1
- pip/_vendor/cachecontrol/__init__.py +6 -3
- pip/_vendor/cachecontrol/adapter.py +0 -1
- pip/_vendor/cachecontrol/controller.py +1 -1
- pip/_vendor/cachecontrol/filewrapper.py +3 -1
- pip/_vendor/certifi/__init__.py +1 -1
- pip/_vendor/certifi/cacert.pem +0 -332
- pip/_vendor/idna/LICENSE.md +1 -1
- pip/_vendor/idna/codec.py +1 -1
- pip/_vendor/idna/core.py +1 -1
- pip/_vendor/idna/idnadata.py +72 -6
- pip/_vendor/idna/package_data.py +1 -1
- pip/_vendor/idna/uts46data.py +891 -731
- pip/_vendor/packaging/__init__.py +1 -1
- pip/_vendor/packaging/_elffile.py +0 -1
- pip/_vendor/packaging/_manylinux.py +36 -36
- pip/_vendor/packaging/_musllinux.py +1 -1
- pip/_vendor/packaging/_parser.py +22 -10
- pip/_vendor/packaging/_structures.py +8 -0
- pip/_vendor/packaging/_tokenizer.py +23 -25
- pip/_vendor/packaging/licenses/__init__.py +13 -11
- pip/_vendor/packaging/licenses/_spdx.py +41 -1
- pip/_vendor/packaging/markers.py +64 -38
- pip/_vendor/packaging/metadata.py +143 -27
- pip/_vendor/packaging/pylock.py +635 -0
- pip/_vendor/packaging/requirements.py +5 -10
- pip/_vendor/packaging/specifiers.py +219 -170
- pip/_vendor/packaging/tags.py +15 -20
- pip/_vendor/packaging/utils.py +19 -24
- pip/_vendor/packaging/version.py +315 -105
- pip/_vendor/platformdirs/version.py +2 -2
- pip/_vendor/platformdirs/windows.py +7 -1
- pip/_vendor/vendor.txt +5 -5
- {pip-25.3.dist-info → pip-26.0.dist-info}/METADATA +2 -2
- {pip-25.3.dist-info → pip-26.0.dist-info}/RECORD +103 -100
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/AUTHORS.txt +18 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/idna/LICENSE.md +1 -1
- pip/_internal/models/pylock.py +0 -188
- {pip-25.3.dist-info → pip-26.0.dist-info}/WHEEL +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/entry_points.txt +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/LICENSE.txt +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/cachecontrol/LICENSE.txt +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/certifi/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/dependency_groups/LICENSE.txt +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/distlib/LICENSE.txt +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/distro/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/msgpack/COPYING +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE.APACHE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE.BSD +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pkg_resources/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/platformdirs/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pygments/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pyproject_hooks/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/requests/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/resolvelib/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/rich/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/tomli/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/tomli_w/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/truststore/LICENSE +0 -0
- {pip-25.3.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/urllib3/LICENSE.txt +0 -0
|
@@ -5,6 +5,7 @@ import email.header
|
|
|
5
5
|
import email.message
|
|
6
6
|
import email.parser
|
|
7
7
|
import email.policy
|
|
8
|
+
import keyword
|
|
8
9
|
import pathlib
|
|
9
10
|
import sys
|
|
10
11
|
import typing
|
|
@@ -19,13 +20,15 @@ from typing import (
|
|
|
19
20
|
|
|
20
21
|
from . import licenses, requirements, specifiers, utils
|
|
21
22
|
from . import version as version_module
|
|
22
|
-
|
|
23
|
+
|
|
24
|
+
if typing.TYPE_CHECKING:
|
|
25
|
+
from .licenses import NormalizedLicenseExpression
|
|
23
26
|
|
|
24
27
|
T = typing.TypeVar("T")
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
if sys.version_info >= (3, 11): # pragma: no cover
|
|
28
|
-
ExceptionGroup = ExceptionGroup
|
|
31
|
+
ExceptionGroup = ExceptionGroup # noqa: F821
|
|
29
32
|
else: # pragma: no cover
|
|
30
33
|
|
|
31
34
|
class ExceptionGroup(Exception):
|
|
@@ -126,13 +129,19 @@ class RawMetadata(TypedDict, total=False):
|
|
|
126
129
|
|
|
127
130
|
# Metadata 2.3 - PEP 685
|
|
128
131
|
# No new fields were added in PEP 685, just some edge case were
|
|
129
|
-
# tightened up to provide better
|
|
132
|
+
# tightened up to provide better interoperability.
|
|
130
133
|
|
|
131
134
|
# Metadata 2.4 - PEP 639
|
|
132
135
|
license_expression: str
|
|
133
136
|
license_files: list[str]
|
|
134
137
|
|
|
138
|
+
# Metadata 2.5 - PEP 794
|
|
139
|
+
import_names: list[str]
|
|
140
|
+
import_namespaces: list[str]
|
|
141
|
+
|
|
135
142
|
|
|
143
|
+
# 'keywords' is special as it's a string in the core metadata spec, but we
|
|
144
|
+
# represent it as a list.
|
|
136
145
|
_STRING_FIELDS = {
|
|
137
146
|
"author",
|
|
138
147
|
"author_email",
|
|
@@ -165,6 +174,8 @@ _LIST_FIELDS = {
|
|
|
165
174
|
"requires_dist",
|
|
166
175
|
"requires_external",
|
|
167
176
|
"supported_platforms",
|
|
177
|
+
"import_names",
|
|
178
|
+
"import_namespaces",
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
_DICT_FIELDS = {
|
|
@@ -193,24 +204,23 @@ def _parse_project_urls(data: list[str]) -> dict[str, str]:
|
|
|
193
204
|
# be the missing value, then they'd have multiple '' values that
|
|
194
205
|
# overwrite each other in a accumulating dict.
|
|
195
206
|
#
|
|
196
|
-
# The other
|
|
207
|
+
# The other potential issue is that it's possible to have the
|
|
197
208
|
# same label multiple times in the metadata, with no solid "right"
|
|
198
209
|
# answer with what to do in that case. As such, we'll do the only
|
|
199
|
-
# thing we can, which is treat the field as
|
|
210
|
+
# thing we can, which is treat the field as unparsable and add it
|
|
200
211
|
# to our list of unparsed fields.
|
|
201
|
-
|
|
202
|
-
parts.extend([""] * (max(0, 2 - len(parts)))) # Ensure 2 items
|
|
203
|
-
|
|
212
|
+
#
|
|
204
213
|
# TODO: The spec doesn't say anything about if the keys should be
|
|
205
214
|
# considered case sensitive or not... logically they should
|
|
206
215
|
# be case-preserving and case-insensitive, but doing that
|
|
207
216
|
# would open up more cases where we might have duplicate
|
|
208
217
|
# entries.
|
|
209
|
-
label, url =
|
|
218
|
+
label, _, url = (s.strip() for s in pair.partition(","))
|
|
219
|
+
|
|
210
220
|
if label in urls:
|
|
211
221
|
# The label already exists in our set of urls, so this field
|
|
212
|
-
# is
|
|
213
|
-
#
|
|
222
|
+
# is unparsable, and we can just add the whole thing to our
|
|
223
|
+
# unparsable data and stop processing it.
|
|
214
224
|
raise KeyError("duplicate labels in project urls")
|
|
215
225
|
urls[label] = url
|
|
216
226
|
|
|
@@ -257,6 +267,8 @@ _EMAIL_TO_RAW_MAPPING = {
|
|
|
257
267
|
"download-url": "download_url",
|
|
258
268
|
"dynamic": "dynamic",
|
|
259
269
|
"home-page": "home_page",
|
|
270
|
+
"import-name": "import_names",
|
|
271
|
+
"import-namespace": "import_namespaces",
|
|
260
272
|
"keywords": "keywords",
|
|
261
273
|
"license": "license",
|
|
262
274
|
"license-expression": "license_expression",
|
|
@@ -283,6 +295,45 @@ _EMAIL_TO_RAW_MAPPING = {
|
|
|
283
295
|
_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
|
|
284
296
|
|
|
285
297
|
|
|
298
|
+
# This class is for writing RFC822 messages
|
|
299
|
+
class RFC822Policy(email.policy.EmailPolicy):
|
|
300
|
+
"""
|
|
301
|
+
This is :class:`email.policy.EmailPolicy`, but with a simple ``header_store_parse``
|
|
302
|
+
implementation that handles multi-line values, and some nice defaults.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
utf8 = True
|
|
306
|
+
mangle_from_ = False
|
|
307
|
+
max_line_length = 0
|
|
308
|
+
|
|
309
|
+
def header_store_parse(self, name: str, value: str) -> tuple[str, str]:
|
|
310
|
+
size = len(name) + 2
|
|
311
|
+
value = value.replace("\n", "\n" + " " * size)
|
|
312
|
+
return (name, value)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# This class is for writing RFC822 messages
|
|
316
|
+
class RFC822Message(email.message.EmailMessage):
|
|
317
|
+
"""
|
|
318
|
+
This is :class:`email.message.EmailMessage` with two small changes: it defaults to
|
|
319
|
+
our `RFC822Policy`, and it correctly writes unicode when being called
|
|
320
|
+
with `bytes()`.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def __init__(self) -> None:
|
|
324
|
+
super().__init__(policy=RFC822Policy())
|
|
325
|
+
|
|
326
|
+
def as_bytes(
|
|
327
|
+
self, unixfrom: bool = False, policy: email.policy.Policy | None = None
|
|
328
|
+
) -> bytes:
|
|
329
|
+
"""
|
|
330
|
+
Return the bytes representation of the message.
|
|
331
|
+
|
|
332
|
+
This handles unicode encoding.
|
|
333
|
+
"""
|
|
334
|
+
return self.as_string(unixfrom, policy=policy).encode("utf-8")
|
|
335
|
+
|
|
336
|
+
|
|
286
337
|
def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|
287
338
|
"""Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``).
|
|
288
339
|
|
|
@@ -310,10 +361,10 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|
|
310
361
|
# We have to wrap parsed.keys() in a set, because in the case of multiple
|
|
311
362
|
# values for a key (a list), the key will appear multiple times in the
|
|
312
363
|
# list of keys, but we're avoiding that by using get_all().
|
|
313
|
-
for
|
|
364
|
+
for name_with_case in frozenset(parsed.keys()):
|
|
314
365
|
# Header names in RFC are case insensitive, so we'll normalize to all
|
|
315
366
|
# lower case to make comparisons easier.
|
|
316
|
-
name =
|
|
367
|
+
name = name_with_case.lower()
|
|
317
368
|
|
|
318
369
|
# We use get_all() here, even for fields that aren't multiple use,
|
|
319
370
|
# because otherwise someone could have e.g. two Name fields, and we
|
|
@@ -349,16 +400,16 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|
|
349
400
|
# can be independently encoded, so we'll need to check each
|
|
350
401
|
# of them.
|
|
351
402
|
chunks: list[tuple[bytes, str | None]] = []
|
|
352
|
-
for
|
|
403
|
+
for binary, _encoding in email.header.decode_header(h):
|
|
353
404
|
try:
|
|
354
|
-
|
|
405
|
+
binary.decode("utf8", "strict")
|
|
355
406
|
except UnicodeDecodeError:
|
|
356
407
|
# Enable mojibake.
|
|
357
408
|
encoding = "latin1"
|
|
358
409
|
valid_encoding = False
|
|
359
410
|
else:
|
|
360
411
|
encoding = "utf8"
|
|
361
|
-
chunks.append((
|
|
412
|
+
chunks.append((binary, encoding))
|
|
362
413
|
|
|
363
414
|
# Turn our chunks back into a Header object, then let that
|
|
364
415
|
# Header object do the right thing to turn them into a
|
|
@@ -397,6 +448,11 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|
|
397
448
|
# of unparsed stuff.
|
|
398
449
|
if raw_name in _STRING_FIELDS and len(value) == 1:
|
|
399
450
|
raw[raw_name] = value[0]
|
|
451
|
+
# If this is import_names, we need to special case the empty field
|
|
452
|
+
# case, which converts to an empty list instead of None. We can't let
|
|
453
|
+
# the empty case slip through, as it will fail validation.
|
|
454
|
+
elif raw_name == "import_names" and value == [""]:
|
|
455
|
+
raw[raw_name] = []
|
|
400
456
|
# If this is one of our list of string fields, then we can just assign
|
|
401
457
|
# the value, since email *only* has strings, and our get_all() call
|
|
402
458
|
# above ensures that this is a list.
|
|
@@ -424,7 +480,7 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|
|
424
480
|
except KeyError:
|
|
425
481
|
unparsed[name] = value
|
|
426
482
|
# Nothing that we've done has managed to parse this, so it'll just
|
|
427
|
-
# throw it in our
|
|
483
|
+
# throw it in our unparsable data and move on.
|
|
428
484
|
else:
|
|
429
485
|
unparsed[name] = value
|
|
430
486
|
|
|
@@ -441,9 +497,9 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|
|
441
497
|
else:
|
|
442
498
|
if payload:
|
|
443
499
|
# Check to see if we've already got a description, if so then both
|
|
444
|
-
# it, and this body move to
|
|
500
|
+
# it, and this body move to unparsable.
|
|
445
501
|
if "description" in raw:
|
|
446
|
-
description_header = cast(str, raw.pop("description"))
|
|
502
|
+
description_header = cast("str", raw.pop("description"))
|
|
447
503
|
unparsed.setdefault("description", []).extend(
|
|
448
504
|
[description_header, payload]
|
|
449
505
|
)
|
|
@@ -456,15 +512,15 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
|
|
|
456
512
|
# literal key names, but we're computing our key names on purpose, but the
|
|
457
513
|
# way this function is implemented, our `TypedDict` can only have valid key
|
|
458
514
|
# names.
|
|
459
|
-
return cast(RawMetadata, raw), unparsed
|
|
515
|
+
return cast("RawMetadata", raw), unparsed
|
|
460
516
|
|
|
461
517
|
|
|
462
518
|
_NOT_FOUND = object()
|
|
463
519
|
|
|
464
520
|
|
|
465
521
|
# Keep the two values in sync.
|
|
466
|
-
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
|
|
467
|
-
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
|
|
522
|
+
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
|
|
523
|
+
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
|
|
468
524
|
|
|
469
525
|
_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])
|
|
470
526
|
|
|
@@ -519,7 +575,7 @@ class _Validator(Generic[T]):
|
|
|
519
575
|
except KeyError:
|
|
520
576
|
pass
|
|
521
577
|
|
|
522
|
-
return cast(T, value)
|
|
578
|
+
return cast("T", value)
|
|
523
579
|
|
|
524
580
|
def _invalid_metadata(
|
|
525
581
|
self, msg: str, cause: Exception | None = None
|
|
@@ -534,7 +590,7 @@ class _Validator(Generic[T]):
|
|
|
534
590
|
# Implicitly makes Metadata-Version required.
|
|
535
591
|
if value not in _VALID_METADATA_VERSIONS:
|
|
536
592
|
raise self._invalid_metadata(f"{value!r} is not a valid metadata version")
|
|
537
|
-
return cast(_MetadataVersion, value)
|
|
593
|
+
return cast("_MetadataVersion", value)
|
|
538
594
|
|
|
539
595
|
def _process_name(self, value: str) -> str:
|
|
540
596
|
if not value:
|
|
@@ -647,9 +703,7 @@ class _Validator(Generic[T]):
|
|
|
647
703
|
else:
|
|
648
704
|
return reqs
|
|
649
705
|
|
|
650
|
-
def _process_license_expression(
|
|
651
|
-
self, value: str
|
|
652
|
-
) -> NormalizedLicenseExpression | None:
|
|
706
|
+
def _process_license_expression(self, value: str) -> NormalizedLicenseExpression:
|
|
653
707
|
try:
|
|
654
708
|
return licenses.canonicalize_license_expression(value)
|
|
655
709
|
except ValueError as exc:
|
|
@@ -683,6 +737,30 @@ class _Validator(Generic[T]):
|
|
|
683
737
|
paths.append(path)
|
|
684
738
|
return paths
|
|
685
739
|
|
|
740
|
+
def _process_import_names(self, value: list[str]) -> list[str]:
|
|
741
|
+
for import_name in value:
|
|
742
|
+
name, semicolon, private = import_name.partition(";")
|
|
743
|
+
name = name.rstrip()
|
|
744
|
+
for identifier in name.split("."):
|
|
745
|
+
if not identifier.isidentifier():
|
|
746
|
+
raise self._invalid_metadata(
|
|
747
|
+
f"{name!r} is invalid for {{field}}; "
|
|
748
|
+
f"{identifier!r} is not a valid identifier"
|
|
749
|
+
)
|
|
750
|
+
elif keyword.iskeyword(identifier):
|
|
751
|
+
raise self._invalid_metadata(
|
|
752
|
+
f"{name!r} is invalid for {{field}}; "
|
|
753
|
+
f"{identifier!r} is a keyword"
|
|
754
|
+
)
|
|
755
|
+
if semicolon and private.lstrip() != "private":
|
|
756
|
+
raise self._invalid_metadata(
|
|
757
|
+
f"{import_name!r} is invalid for {{field}}; "
|
|
758
|
+
"the only valid option is 'private'"
|
|
759
|
+
)
|
|
760
|
+
return value
|
|
761
|
+
|
|
762
|
+
_process_import_namespaces = _process_import_names
|
|
763
|
+
|
|
686
764
|
|
|
687
765
|
class Metadata:
|
|
688
766
|
"""Representation of distribution metadata.
|
|
@@ -854,9 +932,47 @@ class Metadata:
|
|
|
854
932
|
""":external:ref:`core-metadata-provides-dist`"""
|
|
855
933
|
obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2")
|
|
856
934
|
""":external:ref:`core-metadata-obsoletes-dist`"""
|
|
935
|
+
import_names: _Validator[list[str] | None] = _Validator(added="2.5")
|
|
936
|
+
""":external:ref:`core-metadata-import-name`"""
|
|
937
|
+
import_namespaces: _Validator[list[str] | None] = _Validator(added="2.5")
|
|
938
|
+
""":external:ref:`core-metadata-import-namespace`"""
|
|
857
939
|
requires: _Validator[list[str] | None] = _Validator(added="1.1")
|
|
858
940
|
"""``Requires`` (deprecated)"""
|
|
859
941
|
provides: _Validator[list[str] | None] = _Validator(added="1.1")
|
|
860
942
|
"""``Provides`` (deprecated)"""
|
|
861
943
|
obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
|
|
862
944
|
"""``Obsoletes`` (deprecated)"""
|
|
945
|
+
|
|
946
|
+
def as_rfc822(self) -> RFC822Message:
|
|
947
|
+
"""
|
|
948
|
+
Return an RFC822 message with the metadata.
|
|
949
|
+
"""
|
|
950
|
+
message = RFC822Message()
|
|
951
|
+
self._write_metadata(message)
|
|
952
|
+
return message
|
|
953
|
+
|
|
954
|
+
def _write_metadata(self, message: RFC822Message) -> None:
|
|
955
|
+
"""
|
|
956
|
+
Return an RFC822 message with the metadata.
|
|
957
|
+
"""
|
|
958
|
+
for name, validator in self.__class__.__dict__.items():
|
|
959
|
+
if isinstance(validator, _Validator) and name != "description":
|
|
960
|
+
value = getattr(self, name)
|
|
961
|
+
email_name = _RAW_TO_EMAIL_MAPPING[name]
|
|
962
|
+
if value is not None:
|
|
963
|
+
if email_name == "project-url":
|
|
964
|
+
for label, url in value.items():
|
|
965
|
+
message[email_name] = f"{label}, {url}"
|
|
966
|
+
elif email_name == "keywords":
|
|
967
|
+
message[email_name] = ",".join(value)
|
|
968
|
+
elif email_name == "import-name" and value == []:
|
|
969
|
+
message[email_name] = ""
|
|
970
|
+
elif isinstance(value, list):
|
|
971
|
+
for item in value:
|
|
972
|
+
message[email_name] = str(item)
|
|
973
|
+
else:
|
|
974
|
+
message[email_name] = str(value)
|
|
975
|
+
|
|
976
|
+
# The description is a special case because it is in the body of the message.
|
|
977
|
+
if self.description is not None:
|
|
978
|
+
message.set_payload(self.description)
|