Skip to content

Commit 7598ddb

Browse files
authored
feat: PEP 794 support (#271)
1 parent de932db commit 7598ddb

15 files changed

+158
-4
lines changed

src/validate_pyproject/extra_validations.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
JSON Schema library).
44
"""
55

6+
import collections
7+
import itertools
68
from inspect import cleandoc
7-
from typing import Mapping, TypeVar
9+
from typing import Generator, Iterable, Mapping, TypeVar
810

911
from .error_reporting import ValidationError
1012

@@ -30,6 +32,24 @@ class IncludedDependencyGroupMustExist(ValidationError):
3032
_URL = "https://peps.python.org/pep-0735/"
3133

3234

35+
class ImportNameCollision(ValidationError):
36+
_DESC = """According to PEP 794:
37+
38+
All import-names and import-namespaces items must be unique.
39+
"""
40+
__doc__ = _DESC
41+
_URL = "https://peps.python.org/pep-0794/"
42+
43+
44+
class ImportNameMissing(ValidationError):
45+
_DESC = """According to PEP 794:
46+
47+
An import name must have all parents listed.
48+
"""
49+
__doc__ = _DESC
50+
_URL = "https://peps.python.org/pep-0794/"
51+
52+
3353
def validate_project_dynamic(pyproject: T) -> T:
3454
project_table = pyproject.get("project", {})
3555
dynamic = project_table.get("dynamic", [])
@@ -78,4 +98,54 @@ def validate_include_depenency(pyproject: T) -> T:
7898
return pyproject
7999

80100

81-
EXTRA_VALIDATIONS = (validate_project_dynamic, validate_include_depenency)
101+
def _remove_private(items: Iterable[str]) -> Generator[str, None, None]:
102+
for item in items:
103+
yield item.partition(";")[0].rstrip()
104+
105+
106+
def validate_import_name_issues(pyproject: T) -> T:
107+
project = pyproject.get("project", {})
108+
import_names = collections.Counter(_remove_private(project.get("import-names", [])))
109+
import_namespaces = collections.Counter(
110+
_remove_private(project.get("import-namespaces", []))
111+
)
112+
113+
duplicated = [k for k, v in (import_names + import_namespaces).items() if v > 1]
114+
115+
if duplicated:
116+
raise ImportNameCollision(
117+
message="Duplicated names are not allowed in import-names/import-namespaces",
118+
value=duplicated,
119+
name="data.project.importnames(paces)",
120+
definition={
121+
"description": cleandoc(ImportNameCollision._DESC),
122+
"see": ImportNameCollision._URL,
123+
},
124+
rule="PEP 794",
125+
)
126+
127+
names = frozenset(import_names + import_namespaces)
128+
for name in names:
129+
for parent in itertools.accumulate(
130+
name.split(".")[:-1], lambda a, b: f"{a}.{b}"
131+
):
132+
if parent not in names:
133+
raise ImportNameMissing(
134+
message="All parents of an import name must also be listed in import-namespace/import-names",
135+
value=name,
136+
name="data.project.importnames(paces)",
137+
definition={
138+
"description": cleandoc(ImportNameMissing._DESC),
139+
"see": ImportNameMissing._URL,
140+
},
141+
rule="PEP 794",
142+
)
143+
144+
return pyproject
145+
146+
147+
EXTRA_VALIDATIONS = (
148+
validate_project_dynamic,
149+
validate_include_depenency,
150+
validate_import_name_issues,
151+
)

src/validate_pyproject/formats.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
import builtins
11+
import keyword
1112
import logging
1213
import os
1314
import re
@@ -400,3 +401,27 @@ def SPDX(value: str) -> bool:
400401

401402
def SPDX(value: str) -> bool:
402403
return True
404+
405+
406+
VALID_IMPORT_NAME = re.compile(
407+
r"""
408+
^ # start of string
409+
[A-Za-z_][A-Za-z_0-9]+ # a valid Python identifier
410+
(?:\.[A-Za-z_][A-Za-z_0-9]*)* # optionally followed by .identifier's
411+
(?:\s*;\s*private)? # optionally followed by ; private
412+
$ # end of string
413+
""",
414+
re.VERBOSE,
415+
)
416+
417+
418+
def import_name(value: str) -> bool:
419+
"""This is a valid import name. It has to be series of python identifiers
420+
(not keywords), separated by dots, optionally followed by a semicolon and
421+
the keyword "private".
422+
"""
423+
if VALID_IMPORT_NAME.match(value) is None:
424+
return False
425+
426+
idents, _, _ = value.partition(";")
427+
return all(not keyword.iskeyword(ident) for ident in idents.rstrip().split("."))

src/validate_pyproject/project_metadata.schema.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,22 @@
233233
}
234234
}
235235
},
236+
"import-names": {
237+
"description": "Lists import names which a project, when installed, would exclusively provide.",
238+
"type": "array",
239+
"items": {
240+
"type": "string",
241+
"format": "import-name"
242+
}
243+
},
244+
"import-namespaces": {
245+
"description": "Lists import names that, when installed, would be provided by the project, but not exclusively.",
246+
"type": "array",
247+
"items": {
248+
"type": "string",
249+
"format": "import-name"
250+
}
251+
},
236252
"dynamic": {
237253
"type": "array",
238254
"$$description": [
@@ -256,7 +272,9 @@
256272
"gui-scripts",
257273
"entry-points",
258274
"dependencies",
259-
"optional-dependencies"
275+
"optional-dependencies",
276+
"import-names",
277+
"import-namespaces"
260278
]
261279
}
262280
}

tests/examples/pdm/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ requires-python = ">=3.9"
1111
readme = "README.md"
1212
keywords = ["packaging", "PEP 517", "build"]
1313
dynamic = ["version"]
14+
import-names = ["pdm.backend"]
15+
import-namespaces = ["pdm"]
1416

1517
classifiers = [
1618
"Development Status :: 5 - Production/Stable",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[project]
2+
name = "hi"
3+
version = "1.0.0"
4+
import-names = ["one", "one.two", "two; private"]
5+
import-namespaces = ["_other ; private"]

tests/examples/trampolim/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ classifiers = [
2323
'Development Status :: 4 - Beta',
2424
'Programming Language :: Python'
2525
]
26-
26+
import-namespaces = ["trampolim"]
2727
dependencies = [
2828
'tomli>=1.0.0',
2929
'packaging',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Duplicated names are not allowed in import-names/import-namespaces
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[project]
2+
name = "hi"
3+
version = "1.0.0"
4+
import-names = ["one"]
5+
import-namespaces = ["one; private"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
project.import-namespaces[0]` must be import-name
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[project]
2+
name = "hi"
3+
version = "1.0.0"
4+
import-namespaces = ["class"]

0 commit comments

Comments
 (0)