|
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