Skip to content

Commit 7da2b4f

Browse files
committed
WIP: adding recent changes to tests
Signed-off-by: Henry Schreiner <[email protected]>
1 parent fb189d6 commit 7da2b4f

File tree

12 files changed

+401
-51
lines changed

12 files changed

+401
-51
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ ignore-words-list = [
6060
[tool.coverage.run]
6161
branch = true
6262
source_pkgs = ["packaging"]
63+
omit = ["src/packaging/project_table.py"]
6364

6465
[tool.coverage.report]
6566
show_missing = true

src/packaging/_pyproject.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ def ensure_str(self, value: str, key: str) -> str | None:
7272
self.config_error(msg, key=key, got_type=type(value))
7373
return None
7474

75-
def ensure_list(self, val: list[T], key: str) -> list[T] | None:
75+
def ensure_list(self, val: list[T] | None, key: str) -> list[T] | None:
7676
"""Ensure that a value is a list of strings."""
77+
if val is None:
78+
return None
7779
if not isinstance(val, list):
7880
msg = "Field {key} has an invalid type, expecting a list of strings"
7981
self.config_error(msg, key=key, got_type=type(val))

src/packaging/errors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import contextlib
44
import dataclasses
55
import sys
6-
from typing import TYPE_CHECKING, Any
6+
import typing
77

8-
if TYPE_CHECKING:
8+
if typing.TYPE_CHECKING:
99
from collections.abc import Generator
10+
from typing import Any
1011

1112
__all__ = ["ConfigurationError", "ConfigurationWarning", "ExceptionGroup"]
1213

src/packaging/metadata.py

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import email.policy
88
import keyword
99
import pathlib
10-
import sys
1110
import typing
1211
from typing import (
1312
Any,
@@ -29,28 +28,6 @@
2928
T = typing.TypeVar("T")
3029

3130

32-
if sys.version_info >= (3, 11): # pragma: no cover
33-
ExceptionGroup = ExceptionGroup
34-
else: # pragma: no cover
35-
36-
class ExceptionGroup(Exception): # type: ignore[no-redef]
37-
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
38-
39-
If :external:exc:`ExceptionGroup` is already defined by Python itself,
40-
that version is used instead.
41-
"""
42-
43-
message: str
44-
exceptions: list[Exception]
45-
46-
def __init__(self, message: str, exceptions: list[Exception]) -> None:
47-
self.message = message
48-
self.exceptions = exceptions
49-
50-
def __repr__(self) -> str:
51-
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
52-
53-
5431
class InvalidMetadata(ValueError):
5532
"""A metadata field contains invalid data."""
5633

@@ -746,6 +723,8 @@ def _process_import_names(self, value: list[str]) -> list[str]:
746723
name, semicolon, private = import_name.partition(";")
747724
name = name.rstrip()
748725
for identifier in name.split("."):
726+
if identifier == "":
727+
continue
749728
if not identifier.isidentifier():
750729
raise self._invalid_metadata(
751730
f"{name!r} is invalid for {{field}}; "

src/packaging/project.py

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import email.message
1010
import email.policy
1111
import email.utils
12+
import itertools
13+
import keyword
1214
import os
1315
import os.path
1416
import pathlib
@@ -23,7 +25,7 @@
2325

2426
if typing.TYPE_CHECKING: # pragma: no cover
2527
import sys
26-
from collections.abc import Mapping, Sequence
28+
from collections.abc import Generator, Mapping, Sequence
2729
from typing import Any
2830

2931
from .requirements import Requirement
@@ -55,6 +57,8 @@
5557
"dynamic",
5658
"entry-points",
5759
"gui-scripts",
60+
"import-names",
61+
"import-namespaces",
5862
"keywords",
5963
"license",
6064
"license-files",
@@ -68,6 +72,7 @@
6872
"version",
6973
}
7074
PRE_SPDX_METADATA_VERSIONS = {"2.1", "2.2", "2.3"}
75+
PRE_2_5_METADATA_VERSIONS = {"2.1", "2.2", "2.3", "2.4"}
7176

7277

7378
def extras_top_level(pyproject_table: Mapping[str, Any]) -> set[str]:
@@ -91,12 +96,55 @@ def extras_project(pyproject_table: Mapping[str, Any]) -> set[str]:
9196
return set(pyproject_table.get("project", [])) - KNOWN_PROJECT_FIELDS
9297

9398

99+
def _validate_import_names(
100+
names: list[str], key: str, *, errors: ErrorCollector
101+
) -> Generator[str, None, None]:
102+
"""
103+
Returns normalized names for comparisons.
104+
"""
105+
for fullname in names:
106+
name, simicolon, private = fullname.partition(";")
107+
if simicolon and private.lstrip() != "private":
108+
msg = "{key} contains an ending tag other than '; private', got {value!r}"
109+
errors.config_error(msg, key=key, value=fullname)
110+
name = name.rstrip()
111+
112+
for ident in name.split("."):
113+
if not ident.isidentifier():
114+
msg = "{key} contains {value!r}, which is not a valid identifier"
115+
errors.config_error(msg, key=key, value=fullname)
116+
117+
elif keyword.iskeyword(ident):
118+
msg = (
119+
"{key} contains a Python keyword,"
120+
" which is not a valid import name, got {value!r}"
121+
)
122+
errors.config_error(msg, key=key, value=fullname)
123+
124+
yield name
125+
126+
127+
def _validate_dotted_names(names: set[str], *, errors: ErrorCollector) -> None:
128+
"""
129+
Checks to make sure every name is accounted for. Takes the union of de-tagged names.
130+
"""
131+
132+
for name in names:
133+
for parent in itertools.accumulate(
134+
name.split(".")[:-1], lambda a, b: f"{a}.{b}"
135+
):
136+
if parent not in names:
137+
msg = "{key} is missing {value!r}, but submodules are present elsewhere"
138+
errors.config_error(msg, key="project.import-namespaces", value=parent)
139+
continue
140+
141+
94142
@dataclasses.dataclass
95143
class StandardMetadata:
96144
"""
97145
This class represents the standard metadata fields for a project. It can be
98146
used to read metadata from a pyproject.toml table, validate it, and write it
99-
to an RFC822 message or JSON.
147+
to an RFC822 message.
100148
"""
101149

102150
name: str
@@ -118,6 +166,8 @@ class StandardMetadata:
118166
keywords: list[str] = dataclasses.field(default_factory=list)
119167
scripts: dict[str, str] = dataclasses.field(default_factory=dict)
120168
gui_scripts: dict[str, str] = dataclasses.field(default_factory=dict)
169+
import_names: list[str] | None = None
170+
import_namespaces: list[str] | None = None
121171
dynamic: list[Dynamic] = dataclasses.field(default_factory=list)
122172
"""
123173
This field is used to track dynamic fields. You can't set a field not in this list.
@@ -273,14 +323,31 @@ def from_pyproject(
273323
project.get("gui-scripts", {}), "project.gui-scripts"
274324
)
275325
or {},
326+
import_names=pyproject.ensure_list(
327+
project.get("import-names", None), "project.import-names"
328+
),
329+
import_namespaces=pyproject.ensure_list(
330+
project.get("import-namespaces", None), "project.import-namespaces"
331+
),
276332
dynamic=dynamic,
277333
)
278334

279335
pyproject.finalize("Failed to parse pyproject.toml")
280336
assert self is not None
281337
return self
282338

283-
def validate_metdata(self, metadata_version: str) -> None:
339+
def validate_metadata(self, metadata_version: str) -> None:
340+
"""
341+
Validate metadata for consistency and correctness given a metadata
342+
version. This is called when making metadata. Checks:
343+
344+
- ``license`` is not an SPDX license expression if metadata_version
345+
>= 2.4 (warning)
346+
- License classifiers deprecated for metadata_version >= 2.4 (warning)
347+
- ``license`` is an SPDX license expression if metadata_version >= 2.4
348+
- ``license_files`` is supported only for metadata_version >= 2.4
349+
- ``import-name(paces)s`` is only supported on metadata_version >= 2.5
350+
"""
284351
errors = ErrorCollector()
285352

286353
if not self.version:
@@ -317,28 +384,37 @@ def validate_metdata(self, metadata_version: str) -> None:
317384
self.license_files is not None
318385
and metadata_version in PRE_SPDX_METADATA_VERSIONS
319386
):
320-
msg = "{key} is supported only when emitting metadata version >= 2.4"
387+
msg = "{key} is only supported when emitting metadata version >= 2.4"
321388
errors.config_error(msg, key="project.license-files")
322389

390+
if (
391+
self.import_names is not None
392+
and metadata_version in PRE_2_5_METADATA_VERSIONS
393+
):
394+
msg = "{key} is only supported when emitting metadata version >= 2.5"
395+
errors.config_error(msg, key="project.import-names")
396+
397+
if (
398+
self.import_namespaces is not None
399+
and metadata_version in PRE_2_5_METADATA_VERSIONS
400+
):
401+
msg = "{key} is only supported when emitting metadata version >= 2.5"
402+
errors.config_error(msg, key="project.import-namespaces")
403+
323404
errors.finalize("Metadata validation failed")
324405

325406
def validate(self) -> None:
326407
"""
327-
Validate metadata for consistency and correctness. Will also produce
328-
warnings if ``warn`` is given. Respects ``all_errors``. This is called
329-
when loading a pyproject.toml, and when making metadata. Checks:
408+
Validate metadata for consistency and correctness. This is called
409+
when loading a pyproject.toml. Checks:
330410
331-
- ``metadata_version`` is a known version or None
332411
- ``name`` is a valid project name
333412
- ``license_files`` can't be used with classic ``license``
334413
- License classifiers can't be used with SPDX license
335414
- ``description`` is a single line (warning)
336-
- ``license`` is not an SPDX license expression if metadata_version
337-
>= 2.4 (warning)
338-
- License classifiers deprecated for metadata_version >= 2.4 (warning)
339-
- ``license`` is an SPDX license expression if metadata_version >= 2.4
340-
- ``license_files`` is supported only for metadata_version >= 2.4
341415
- ``project_url`` can't contain keys over 32 characters
416+
- ``import-name(space)s`` must be valid names, optionally with ``; private``
417+
- ``import-names`` and ``import-namespaces`` cannot overlap
342418
"""
343419
errors = ErrorCollector()
344420

@@ -380,6 +456,23 @@ def validate(self) -> None:
380456
msg = "{key} names cannot be more than 32 characters long"
381457
errors.config_error(msg, key="project.urls", got=name)
382458

459+
import_names = set(
460+
_validate_import_names(
461+
self.import_names or [], "import-names", errors=errors
462+
)
463+
)
464+
import_namespaces = set(
465+
_validate_import_names(
466+
self.import_namespaces or [], "import-namespaces", errors=errors
467+
)
468+
)
469+
in_both = import_names & import_namespaces
470+
if in_both:
471+
msg = "{key} overlaps with 'project.import-namespaces': {in_both}"
472+
errors.config_error(msg, key="project.import-names", in_both=in_both)
473+
474+
_validate_dotted_names(import_names | import_namespaces, errors=errors)
475+
383476
errors.finalize("[project] table validation failed")
384477

385478
def metadata(
@@ -388,7 +481,7 @@ def metadata(
388481
"""
389482
Return an Message with the metadata.
390483
"""
391-
self.validate_metdata(metadata_version)
484+
self.validate_metadata(metadata_version)
392485

393486
assert self.version is not None
394487
message = packaging_metadata.RawMetadata(
@@ -452,9 +545,19 @@ def metadata(
452545
for requirement in requirements
453546
)
454547
if self.readme:
455-
if self.readme.content_type:
456-
message["description_content_type"] = self.readme.content_type
548+
assert self.readme.content_type # verified earlier
549+
message["description_content_type"] = self.readme.content_type
457550
message["description"] = self.readme.text
551+
552+
if self.import_names is not None:
553+
# Special case for empty import-names
554+
if not self.import_names:
555+
message["import_names"] = [""]
556+
else:
557+
message["import_names"] = self.import_names
558+
if self.import_namespaces is not None:
559+
message["import_namespaces"] = self.import_namespaces
560+
458561
# Core Metadata 2.2
459562
if metadata_version != "2.1":
460563
for field in dynamic_metadata:

src/packaging/project_table.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class LicenseTable(TypedDict, total=False):
5252
"dynamic",
5353
"entry-points",
5454
"gui-scripts",
55+
"import-names",
56+
"import-namespaces",
5557
"keywords",
5658
"license",
5759
"maintainers",
@@ -83,6 +85,8 @@ class LicenseTable(TypedDict, total=False):
8385
"keywords": List[str],
8486
"scripts": Dict[str, str],
8587
"gui-scripts": Dict[str, str],
88+
"import-names": List[str],
89+
"import-namespaces": List[str],
8690
"dynamic": List[Dynamic],
8791
},
8892
total=False,

tests/project/full-metadata/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ authors = [
1010
{ name = 'Example!' },
1111
]
1212
maintainers = [
13+
{ name = 'Emailless' },
1314
{ name = 'Other Example', email = '[email protected]' },
1415
]
1516
classifiers = [

tests/project/metadata-2.5/LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright © 2019 Filipe Laíns <[email protected]>
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a
4+
copy of this software and associated documentation files (the "Software"),
5+
to deal in the Software without restriction, including without limitation
6+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
7+
and/or sell copies of the Software, and to permit persons to whom the
8+
Software is furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice (including the next
11+
paragraph) shall be included in all copies or substantial portions of the
12+
Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
17+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20+
DEALINGS IN THE SOFTWARE.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
some readme 👋

tests/project/metadata-2.5/metadata25.py

Whitespace-only changes.

0 commit comments

Comments
 (0)