Skip to content

Commit c113f2b

Browse files
committed
fix: validate names
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 4a57eff commit c113f2b

File tree

1 file changed

+61
-4
lines changed

1 file changed

+61
-4
lines changed

pyproject_metadata/__init__.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import email.message
3838
import email.policy
3939
import email.utils
40+
import itertools
41+
import keyword
4042
import os
4143
import os.path
4244
import pathlib
@@ -50,7 +52,7 @@
5052
from .pyproject import License, PyProjectReader, Readme
5153

5254
if typing.TYPE_CHECKING:
53-
from collections.abc import Mapping
55+
from collections.abc import Generator, Mapping
5456
from typing import Any
5557

5658
from packaging.requirements import Requirement
@@ -225,6 +227,46 @@ def _fold(
225227
return name + ": " + self.linesep.join(lines) + self.linesep # type: ignore[arg-type]
226228

227229

230+
def _validate_import_names(
231+
names: list[str], key: str, errors: ErrorCollector
232+
) -> Generator[str, None, None]:
233+
"""
234+
Returns normalized names for comparisons.
235+
"""
236+
for fullname in names:
237+
name, simicolon, private = fullname.partition(";")
238+
if simicolon and private.lstrip() != "private":
239+
msg = "{key} contains an ending tag other than '; private', got {value}"
240+
errors.config_error(msg, key=key, value=fullname)
241+
name = name.rstrip()
242+
243+
for ident in name.split("."):
244+
if not ident.isidentifier():
245+
msg = "{key} contains {value}, which is not a valid identifier"
246+
errors.config_error(msg, key=key, value=fullname)
247+
248+
elif keyword.iskeyword(ident):
249+
msg = "{key} contains a Python keyword, which is not a valid import name, got {value}"
250+
errors.config_error(msg, key=key, value=fullname)
251+
252+
yield name
253+
254+
255+
def _validate_dotted_names(names: frozenset[str], errors: ErrorCollector) -> None:
256+
"""
257+
Checks to make sure every name is accounted for. Takes the union of de-tagged names.
258+
"""
259+
260+
for name in names:
261+
for parent in itertools.accumulate(
262+
name.split(".")[:-1], lambda a, b: f"{a}.{b}"
263+
):
264+
if parent not in names:
265+
msg = "{key} is missing {value}, but children of this are present elsewhere"
266+
errors.config_error(msg, key="project.import-namespaces", value=parent)
267+
continue
268+
269+
228270
class RFC822Message(email.message.EmailMessage):
229271
"""
230272
This is :class:`email.message.EmailMessage` with two small changes: it defaults to
@@ -514,7 +556,9 @@ def validate(self, *, warn: bool = True) -> None: # noqa: C901
514556
- ``license`` is an SPDX license expression if metadata_version >= 2.4
515557
- ``license_files`` is supported only for metadata_version >= 2.4
516558
- ``project_url`` can't contain keys over 32 characters
517-
- ``import-names(paces)`` is only supported on metadata_version >= 2.5
559+
- ``import-name(paces)s`` is only supported on metadata_version >= 2.5
560+
- ``import-name(space)s`` must be valid names, optionally with ``; private``
561+
- ``import-names`` and ``import-namespaces`` cannot overlap
518562
"""
519563
errors = ErrorCollector(collect_errors=self.all_errors)
520564

@@ -586,14 +630,27 @@ def validate(self, *, warn: bool = True) -> None: # noqa: C901
586630
and self.auto_metadata_version in constants.PRE_2_5_METADATA_VERSIONS
587631
):
588632
msg = "{key} is only supported when emitting metadata version >= 2.5"
589-
errors.config_error(msg, key="project.import_names")
633+
errors.config_error(msg, key="project.import-names")
590634

591635
if (
592636
self.import_namespaces
593637
and self.auto_metadata_version in constants.PRE_2_5_METADATA_VERSIONS
594638
):
595639
msg = "{key} is only supported when emitting metadata version >= 2.5"
596-
errors.config_error(msg, key="project.import_namespaces")
640+
errors.config_error(msg, key="project.import-namespaces")
641+
642+
import_names = frozenset(
643+
_validate_import_names(self.import_names, "import-names")
644+
)
645+
import_namespaces = frozenset(
646+
_validate_import_names(self.import_namespaces, "import-namespaces")
647+
)
648+
in_both = import_names & import_namespaces
649+
if in_both:
650+
msg = "{key} overlaps with 'project.import-namespaces': {in_both}"
651+
errors.config_error(msg, key="project.import-names")
652+
653+
_validate_dotted_names(import_names | import_namespaces, errors)
597654

598655
errors.finalize("Metadata validation failed")
599656

0 commit comments

Comments
 (0)