• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

abravalheri / validate-pyproject / 4731292593422336

10 Nov 2025 09:29PM UTC coverage: 98.294% (+0.2%) from 98.101%
4731292593422336

push

cirrus-ci

web-flow
[pre-commit.ci] pre-commit autoupdate (#277)

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.3 → v0.14.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.3...v0.14.4)
- [github.com/scientific-python/cookie: 2025.10.20 → 2025.11.10](https://github.com/scientific-python/cookie/compare/2025.10.20...2025.11.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

459 of 472 branches covered (97.25%)

Branch coverage included in aggregate %.

1039 of 1052 relevant lines covered (98.76%)

9.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.47
/src/validate_pyproject/extra_validations.py
1
"""The purpose of this module is implement PEP 621 validations that are
2
difficult to express as a JSON Schema (or that are not supported by the current
3
JSON Schema library).
4
"""
5

6
import collections
10✔
7
import itertools
10✔
8
from inspect import cleandoc
10✔
9
from typing import Generator, Iterable, Mapping, TypeVar
10✔
10

11
from .error_reporting import ValidationError
10✔
12

13
T = TypeVar("T", bound=Mapping)
10✔
14

15

16
class RedefiningStaticFieldAsDynamic(ValidationError):
10✔
17
    _DESC = """According to PEP 621:
10✔
18

19
    Build back-ends MUST raise an error if the metadata specifies a field
20
    statically as well as being listed in dynamic.
21
    """
22
    __doc__ = _DESC
10✔
23
    _URL = (
10✔
24
        "https://packaging.python.org/en/latest/specifications/pyproject-toml/#dynamic"
25
    )
26

27

28
class IncludedDependencyGroupMustExist(ValidationError):
10✔
29
    _DESC = """An included dependency group must exist and must not be cyclic.
10✔
30
    """
31
    __doc__ = _DESC
10✔
32
    _URL = "https://peps.python.org/pep-0735/"
10✔
33

34

35
class ImportNameCollision(ValidationError):
10✔
36
    _DESC = """According to PEP 794:
10✔
37

38
    All import-names and import-namespaces items must be unique.
39
    """
40
    __doc__ = _DESC
10✔
41
    _URL = "https://peps.python.org/pep-0794/"
10✔
42

43

44
class ImportNameMissing(ValidationError):
10✔
45
    _DESC = """According to PEP 794:
10✔
46

47
    An import name must have all parents listed.
48
    """
49
    __doc__ = _DESC
10✔
50
    _URL = "https://peps.python.org/pep-0794/"
10✔
51

52

53
def validate_project_dynamic(pyproject: T) -> T:
10✔
54
    project_table = pyproject.get("project", {})
10✔
55
    dynamic = project_table.get("dynamic", [])
10✔
56

57
    for field in dynamic:
10✔
58
        if field in project_table:
10✔
59
            raise RedefiningStaticFieldAsDynamic(
10✔
60
                message=f"You cannot provide a value for `project.{field}` and "
61
                "list it under `project.dynamic` at the same time",
62
                value={
63
                    field: project_table[field],
64
                    "...": " # ...",
65
                    "dynamic": dynamic,
66
                },
67
                name=f"data.project.{field}",
68
                definition={
69
                    "description": cleandoc(RedefiningStaticFieldAsDynamic._DESC),
70
                    "see": RedefiningStaticFieldAsDynamic._URL,
71
                },
72
                rule="PEP 621",
73
            )
74

75
    return pyproject
10✔
76

77

78
def validate_include_depenency(pyproject: T) -> T:
10✔
79
    dependency_groups = pyproject.get("dependency-groups", {})
10✔
80
    for key, value in dependency_groups.items():
10✔
81
        for each in value:
10✔
82
            if (
10!
83
                isinstance(each, dict)
84
                and (include_group := each.get("include-group"))
85
                and include_group not in dependency_groups
86
            ):
87
                raise IncludedDependencyGroupMustExist(
×
88
                    message=f"The included dependency group {include_group} doesn't exist",
89
                    value=each,
90
                    name=f"data.dependency_groups.{key}",
91
                    definition={
92
                        "description": cleandoc(IncludedDependencyGroupMustExist._DESC),
93
                        "see": IncludedDependencyGroupMustExist._URL,
94
                    },
95
                    rule="PEP 735",
96
                )
97
    # TODO: check for `include-group` cycles (can be conditional to graphlib)
98
    return pyproject
10✔
99

100

101
def _remove_private(items: Iterable[str]) -> Generator[str, None, None]:
10✔
102
    for item in items:
10✔
103
        yield item.partition(";")[0].rstrip()
10✔
104

105

106
def validate_import_name_issues(pyproject: T) -> T:
10✔
107
    project = pyproject.get("project", {})
10✔
108
    import_names = collections.Counter(_remove_private(project.get("import-names", [])))
10✔
109
    import_namespaces = collections.Counter(
10✔
110
        _remove_private(project.get("import-namespaces", []))
111
    )
112

113
    duplicated = [k for k, v in (import_names + import_namespaces).items() if v > 1]
10✔
114

115
    if duplicated:
10✔
116
        raise ImportNameCollision(
10✔
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)
10✔
128
    for name in names:
10✔
129
        for parent in itertools.accumulate(
10!
130
            name.split(".")[:-1], lambda a, b: f"{a}.{b}"
131
        ):
132
            if parent not in names:
10✔
133
                raise ImportNameMissing(
10✔
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
10✔
145

146

147
EXTRA_VALIDATIONS = (
10✔
148
    validate_project_dynamic,
149
    validate_include_depenency,
150
    validate_import_name_issues,
151
)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc