|
40 | 40 | import os
|
41 | 41 | import os.path
|
42 | 42 | import pathlib
|
43 |
| -import re |
44 | 43 | import sys
|
45 | 44 | import typing
|
46 | 45 | import warnings
|
|
68 | 67 | import packaging.utils
|
69 | 68 | import packaging.version
|
70 | 69 |
|
71 |
| -__version__ = "0.9.0" |
| 70 | +if sys.version_info < (3, 12, 4): |
| 71 | + import re |
| 72 | + |
| 73 | + RE_EOL_STR = re.compile(r"[\r\n]+") |
| 74 | + RE_EOL_BYTES = re.compile(rb"[\r\n]+") |
| 75 | + |
| 76 | + |
| 77 | +__version__ = "0.9.1" |
72 | 78 |
|
73 | 79 | __all__ = [
|
74 | 80 | "ConfigurationError",
|
|
77 | 83 | "RFC822Policy",
|
78 | 84 | "Readme",
|
79 | 85 | "StandardMetadata",
|
80 |
| - "field_to_metadata", |
81 | 86 | "extras_build_system",
|
82 | 87 | "extras_project",
|
83 | 88 | "extras_top_level",
|
| 89 | + "field_to_metadata", |
84 | 90 | ]
|
85 | 91 |
|
86 | 92 |
|
@@ -187,6 +193,37 @@ def header_store_parse(self, name: str, value: str) -> tuple[str, str]:
|
187 | 193 | value = value.replace("\n", "\n" + " " * size)
|
188 | 194 | return (name, value)
|
189 | 195 |
|
| 196 | + if sys.version_info < (3, 12, 4): |
| 197 | + # Work around Python bug https://github.com/python/cpython/issues/117313 |
| 198 | + def _fold( |
| 199 | + self, name: str, value: Any, refold_binary: bool = False |
| 200 | + ) -> str: # pragma: no cover |
| 201 | + if hasattr(value, "name"): |
| 202 | + return value.fold(policy=self) # type: ignore[no-any-return] |
| 203 | + maxlen = self.max_line_length if self.max_line_length else sys.maxsize |
| 204 | + |
| 205 | + # this is from the library version, and it improperly breaks on chars like 0x0c, treating |
| 206 | + # them as 'form feed' etc. |
| 207 | + # we need to ensure that only CR/LF is used as end of line |
| 208 | + # lines = value.splitlines() |
| 209 | + |
| 210 | + # this is a workaround which splits only on CR/LF characters |
| 211 | + if isinstance(value, bytes): |
| 212 | + lines = RE_EOL_BYTES.split(value) |
| 213 | + else: |
| 214 | + lines = RE_EOL_STR.split(value) |
| 215 | + |
| 216 | + refold = self.refold_source == "all" or ( |
| 217 | + self.refold_source == "long" |
| 218 | + and ( |
| 219 | + (lines and len(lines[0]) + len(name) + 2 > maxlen) |
| 220 | + or any(len(x) > maxlen for x in lines[1:]) |
| 221 | + ) |
| 222 | + ) |
| 223 | + if refold or (refold_binary and email.policy._has_surrogates(value)): # type: ignore[attr-defined] |
| 224 | + return self.header_factory(name, "".join(lines)).fold(policy=self) # type: ignore[arg-type,no-any-return] |
| 225 | + return name + ": " + self.linesep.join(lines) + self.linesep # type: ignore[arg-type] |
| 226 | + |
190 | 227 |
|
191 | 228 | class RFC822Message(email.message.EmailMessage):
|
192 | 229 | """
|
@@ -474,11 +511,9 @@ def validate(self, *, warn: bool = True) -> None: # noqa: C901
|
474 | 511 | msg = "The metadata_version must be one of {versions} or None (default)"
|
475 | 512 | errors.config_error(msg, versions=constants.KNOWN_METADATA_VERSIONS)
|
476 | 513 |
|
477 |
| - # See https://packaging.python.org/en/latest/specifications/core-metadata/#name and |
478 |
| - # https://packaging.python.org/en/latest/specifications/name-normalization/#name-format |
479 |
| - if not re.match( |
480 |
| - r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", self.name, re.IGNORECASE |
481 |
| - ): |
| 514 | + try: |
| 515 | + packaging.utils.canonicalize_name(self.name, validate=True) |
| 516 | + except packaging.utils.InvalidName: |
482 | 517 | msg = (
|
483 | 518 | "Invalid project name {name!r}. A valid name consists only of ASCII letters and "
|
484 | 519 | "numbers, period, underscore and hyphen. It must start and end with a letter or number"
|
@@ -543,13 +578,15 @@ def _write_metadata( # noqa: C901
|
543 | 578 | """
|
544 | 579 | Write the metadata to the message. Handles JSON or Message.
|
545 | 580 | """
|
546 |
| - self.validate(warn=False) |
| 581 | + errors = ErrorCollector(collect_errors=self.all_errors) |
| 582 | + with errors.collect(): |
| 583 | + self.validate(warn=False) |
547 | 584 |
|
548 | 585 | smart_message["Metadata-Version"] = self.auto_metadata_version
|
549 | 586 | smart_message["Name"] = self.name
|
550 | 587 | if not self.version:
|
551 |
| - msg = "Missing version field" |
552 |
| - raise ConfigurationError(msg) |
| 588 | + msg = "Field {key} missing" |
| 589 | + errors.config_error(msg, key="project.version") |
553 | 590 | smart_message["Version"] = str(self.version)
|
554 | 591 | # skip 'Platform'
|
555 | 592 | # skip 'Supported-Platform'
|
@@ -604,13 +641,15 @@ def _write_metadata( # noqa: C901
|
604 | 641 | if self.auto_metadata_version != "2.1":
|
605 | 642 | for field in self.dynamic_metadata:
|
606 | 643 | if field.lower() in {"name", "version", "dynamic"}:
|
607 |
| - msg = f"Field cannot be set as dynamic metadata: {field}" |
608 |
| - raise ConfigurationError(msg) |
| 644 | + msg = f"Metadata field {field!r} cannot be declared dynamic" |
| 645 | + errors.config_error(msg) |
609 | 646 | if field.lower() not in constants.KNOWN_METADATA_FIELDS:
|
610 |
| - msg = f"Field is not known: {field}" |
611 |
| - raise ConfigurationError(msg) |
| 647 | + msg = f"Unknown metadata field {field!r} cannot be declared dynamic" |
| 648 | + errors.config_error(msg) |
612 | 649 | smart_message["Dynamic"] = field
|
613 | 650 |
|
| 651 | + errors.finalize("Failed to write metadata") |
| 652 | + |
614 | 653 |
|
615 | 654 | def _name_list(people: list[tuple[str, str | None]]) -> str | None:
|
616 | 655 | """
|
|
0 commit comments