pip 25.2__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.
Files changed (167) hide show
  1. pip/__init__.py +1 -1
  2. pip/_internal/__init__.py +0 -0
  3. pip/_internal/build_env.py +265 -8
  4. pip/_internal/cache.py +1 -1
  5. pip/_internal/cli/base_command.py +11 -0
  6. pip/_internal/cli/cmdoptions.py +200 -71
  7. pip/_internal/cli/index_command.py +20 -0
  8. pip/_internal/cli/main.py +11 -6
  9. pip/_internal/cli/main_parser.py +3 -1
  10. pip/_internal/cli/parser.py +96 -36
  11. pip/_internal/cli/progress_bars.py +4 -2
  12. pip/_internal/cli/req_command.py +126 -30
  13. pip/_internal/commands/cache.py +24 -0
  14. pip/_internal/commands/completion.py +2 -1
  15. pip/_internal/commands/download.py +12 -11
  16. pip/_internal/commands/index.py +13 -6
  17. pip/_internal/commands/install.py +55 -43
  18. pip/_internal/commands/list.py +14 -16
  19. pip/_internal/commands/lock.py +19 -14
  20. pip/_internal/commands/wheel.py +13 -23
  21. pip/_internal/configuration.py +1 -2
  22. pip/_internal/distributions/sdist.py +13 -14
  23. pip/_internal/exceptions.py +96 -6
  24. pip/_internal/index/collector.py +2 -3
  25. pip/_internal/index/package_finder.py +87 -21
  26. pip/_internal/locations/__init__.py +1 -2
  27. pip/_internal/locations/_sysconfig.py +4 -1
  28. pip/_internal/metadata/__init__.py +7 -2
  29. pip/_internal/metadata/importlib/_dists.py +8 -2
  30. pip/_internal/models/link.py +18 -14
  31. pip/_internal/models/release_control.py +92 -0
  32. pip/_internal/models/selection_prefs.py +6 -3
  33. pip/_internal/models/wheel.py +5 -66
  34. pip/_internal/network/auth.py +6 -2
  35. pip/_internal/network/cache.py +6 -11
  36. pip/_internal/network/download.py +4 -5
  37. pip/_internal/network/lazy_wheel.py +5 -3
  38. pip/_internal/network/session.py +14 -10
  39. pip/_internal/operations/build/wheel.py +4 -4
  40. pip/_internal/operations/build/wheel_editable.py +4 -4
  41. pip/_internal/operations/install/wheel.py +1 -2
  42. pip/_internal/operations/prepare.py +9 -4
  43. pip/_internal/pyproject.py +2 -61
  44. pip/_internal/req/__init__.py +1 -3
  45. pip/_internal/req/constructors.py +45 -39
  46. pip/_internal/req/pep723.py +41 -0
  47. pip/_internal/req/req_file.py +10 -2
  48. pip/_internal/req/req_install.py +32 -141
  49. pip/_internal/resolution/resolvelib/candidates.py +20 -11
  50. pip/_internal/resolution/resolvelib/factory.py +43 -1
  51. pip/_internal/resolution/resolvelib/provider.py +9 -0
  52. pip/_internal/resolution/resolvelib/reporter.py +21 -8
  53. pip/_internal/resolution/resolvelib/requirements.py +7 -3
  54. pip/_internal/resolution/resolvelib/resolver.py +2 -6
  55. pip/_internal/self_outdated_check.py +17 -16
  56. pip/_internal/utils/datetime.py +18 -0
  57. pip/_internal/utils/filesystem.py +52 -1
  58. pip/_internal/utils/logging.py +34 -2
  59. pip/_internal/utils/misc.py +18 -12
  60. pip/_internal/utils/pylock.py +116 -0
  61. pip/_internal/utils/unpacking.py +26 -1
  62. pip/_internal/vcs/versioncontrol.py +3 -1
  63. pip/_internal/wheel_builder.py +23 -96
  64. pip/_vendor/README.rst +180 -0
  65. pip/_vendor/cachecontrol/LICENSE.txt +13 -0
  66. pip/_vendor/cachecontrol/__init__.py +6 -3
  67. pip/_vendor/cachecontrol/adapter.py +0 -1
  68. pip/_vendor/cachecontrol/controller.py +1 -1
  69. pip/_vendor/cachecontrol/filewrapper.py +3 -1
  70. pip/_vendor/certifi/LICENSE +20 -0
  71. pip/_vendor/certifi/__init__.py +1 -1
  72. pip/_vendor/certifi/cacert.pem +62 -372
  73. pip/_vendor/dependency_groups/LICENSE.txt +9 -0
  74. pip/_vendor/distlib/LICENSE.txt +284 -0
  75. pip/_vendor/distro/LICENSE +202 -0
  76. pip/_vendor/idna/LICENSE.md +31 -0
  77. pip/_vendor/idna/codec.py +1 -1
  78. pip/_vendor/idna/core.py +1 -1
  79. pip/_vendor/idna/idnadata.py +72 -6
  80. pip/_vendor/idna/package_data.py +1 -1
  81. pip/_vendor/idna/uts46data.py +891 -731
  82. pip/_vendor/msgpack/COPYING +14 -0
  83. pip/_vendor/msgpack/__init__.py +2 -2
  84. pip/_vendor/packaging/LICENSE +3 -0
  85. pip/_vendor/packaging/LICENSE.APACHE +177 -0
  86. pip/_vendor/packaging/LICENSE.BSD +23 -0
  87. pip/_vendor/packaging/__init__.py +1 -1
  88. pip/_vendor/packaging/_elffile.py +0 -1
  89. pip/_vendor/packaging/_manylinux.py +36 -36
  90. pip/_vendor/packaging/_musllinux.py +1 -1
  91. pip/_vendor/packaging/_parser.py +22 -10
  92. pip/_vendor/packaging/_structures.py +8 -0
  93. pip/_vendor/packaging/_tokenizer.py +23 -25
  94. pip/_vendor/packaging/licenses/__init__.py +13 -11
  95. pip/_vendor/packaging/licenses/_spdx.py +41 -1
  96. pip/_vendor/packaging/markers.py +64 -38
  97. pip/_vendor/packaging/metadata.py +143 -27
  98. pip/_vendor/packaging/pylock.py +635 -0
  99. pip/_vendor/packaging/requirements.py +5 -10
  100. pip/_vendor/packaging/specifiers.py +219 -170
  101. pip/_vendor/packaging/tags.py +15 -20
  102. pip/_vendor/packaging/utils.py +19 -24
  103. pip/_vendor/packaging/version.py +315 -105
  104. pip/_vendor/pkg_resources/LICENSE +17 -0
  105. pip/_vendor/platformdirs/LICENSE +21 -0
  106. pip/_vendor/platformdirs/api.py +1 -1
  107. pip/_vendor/platformdirs/macos.py +10 -8
  108. pip/_vendor/platformdirs/version.py +16 -3
  109. pip/_vendor/platformdirs/windows.py +7 -1
  110. pip/_vendor/pygments/LICENSE +25 -0
  111. pip/_vendor/pyproject_hooks/LICENSE +21 -0
  112. pip/_vendor/requests/LICENSE +175 -0
  113. pip/_vendor/requests/__version__.py +2 -2
  114. pip/_vendor/requests/adapters.py +17 -40
  115. pip/_vendor/requests/sessions.py +1 -1
  116. pip/_vendor/resolvelib/LICENSE +13 -0
  117. pip/_vendor/resolvelib/__init__.py +1 -1
  118. pip/_vendor/resolvelib/resolvers/abstract.py +3 -3
  119. pip/_vendor/resolvelib/resolvers/resolution.py +5 -0
  120. pip/_vendor/rich/LICENSE +19 -0
  121. pip/_vendor/rich/style.py +7 -11
  122. pip/_vendor/tomli/LICENSE +21 -0
  123. pip/_vendor/tomli/__init__.py +1 -1
  124. pip/_vendor/tomli/_parser.py +28 -21
  125. pip/_vendor/tomli/_re.py +8 -5
  126. pip/_vendor/tomli_w/LICENSE +21 -0
  127. pip/_vendor/truststore/LICENSE +21 -0
  128. pip/_vendor/truststore/__init__.py +1 -1
  129. pip/_vendor/truststore/_api.py +14 -6
  130. pip/_vendor/truststore/_openssl.py +3 -1
  131. pip/_vendor/urllib3/LICENSE.txt +21 -0
  132. pip/_vendor/vendor.txt +11 -11
  133. {pip-25.2.dist-info → pip-26.0.dist-info}/METADATA +10 -11
  134. {pip-25.2.dist-info → pip-26.0.dist-info}/RECORD +158 -139
  135. {pip-25.2.dist-info → pip-26.0.dist-info}/WHEEL +1 -2
  136. pip-26.0.dist-info/entry_points.txt +4 -0
  137. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/AUTHORS.txt +27 -0
  138. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/idna/LICENSE.md +1 -1
  139. pip/_internal/models/pylock.py +0 -188
  140. pip/_internal/operations/build/metadata_legacy.py +0 -73
  141. pip/_internal/operations/build/wheel_legacy.py +0 -119
  142. pip/_internal/operations/install/editable_legacy.py +0 -48
  143. pip/_internal/utils/setuptools_build.py +0 -149
  144. pip-25.2.dist-info/entry_points.txt +0 -3
  145. pip-25.2.dist-info/licenses/src/pip/_vendor/tomli/LICENSE-HEADER +0 -3
  146. pip-25.2.dist-info/top_level.txt +0 -1
  147. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/LICENSE.txt +0 -0
  148. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/cachecontrol/LICENSE.txt +0 -0
  149. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/certifi/LICENSE +0 -0
  150. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/dependency_groups/LICENSE.txt +0 -0
  151. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/distlib/LICENSE.txt +0 -0
  152. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/distro/LICENSE +0 -0
  153. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/msgpack/COPYING +0 -0
  154. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE +0 -0
  155. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE.APACHE +0 -0
  156. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/packaging/LICENSE.BSD +0 -0
  157. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pkg_resources/LICENSE +0 -0
  158. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/platformdirs/LICENSE +0 -0
  159. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pygments/LICENSE +0 -0
  160. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/pyproject_hooks/LICENSE +0 -0
  161. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/requests/LICENSE +0 -0
  162. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/resolvelib/LICENSE +0 -0
  163. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/rich/LICENSE +0 -0
  164. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/tomli/LICENSE +0 -0
  165. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/tomli_w/LICENSE +0 -0
  166. {pip-25.2.dist-info → pip-26.0.dist-info}/licenses/src/pip/_vendor/truststore/LICENSE +0 -0
  167. {pip-25.2.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
- from .licenses import NormalizedLicenseExpression
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 interoptability.
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 potentional issue is that it's possible to have the
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 unparseable and add it
210
+ # thing we can, which is treat the field as unparsable and add it
200
211
  # to our list of unparsed fields.
201
- parts = [p.strip() for p in pair.split(",", 1)]
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 = parts
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 unparseable, and we can just add the whole thing to our
213
- # unparseable data and stop processing it.
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 name in frozenset(parsed.keys()):
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 = name.lower()
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 bin, encoding in email.header.decode_header(h):
403
+ for binary, _encoding in email.header.decode_header(h):
353
404
  try:
354
- bin.decode("utf8", "strict")
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((bin, encoding))
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 unparseable data and move on.
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 unparseable.
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)