@@ -104,6 +104,28 @@ def __str__(self) -> str:
104
104
return f'Failed to validate `build-system` in pyproject.toml: { self .args [0 ]} '
105
105
106
106
107
+ class CircularBuildSystemDependencyError (BuildException ):
108
+ """
109
+ Exception raised when a ``[build-system]`` requirement in pyproject.toml is circular.
110
+ """
111
+
112
+ def __str__ (self ) -> str :
113
+ cycle_deps = self .args [0 ]
114
+ cycle_err_str = f'`{ cycle_deps [0 ]} `'
115
+ for dep in cycle_deps [1 :]:
116
+ cycle_err_str += f' -> `{ dep } `'
117
+ return f'Failed to validate `build-system` in pyproject.toml, dependency cycle detected: { cycle_err_str } '
118
+
119
+
120
+ class ProjectTableValidationError (BuildException ):
121
+ """
122
+ Exception raised when the ``[project]`` table in pyproject.toml is invalid.
123
+ """
124
+
125
+ def __str__ (self ) -> str :
126
+ return f'Failed to validate `project` in pyproject.toml: { self .args [0 ]} '
127
+
128
+
107
129
class TypoWarning (Warning ):
108
130
"""
109
131
Warning raised when a potential typo is found
@@ -132,7 +154,10 @@ def _validate_source_directory(srcdir: PathType) -> None:
132
154
133
155
134
156
def check_dependency (
135
- req_string : str , ancestral_req_strings : Tuple [str , ...] = (), parent_extras : AbstractSet [str ] = frozenset ()
157
+ req_string : str ,
158
+ ancestral_req_strings : Tuple [str , ...] = (),
159
+ parent_extras : AbstractSet [str ] = frozenset (),
160
+ project_name : Optional [str ] = None ,
136
161
) -> Iterator [Tuple [str , ...]]:
137
162
"""
138
163
Verify that a dependency and all of its dependencies are met.
@@ -150,6 +175,12 @@ def check_dependency(
150
175
151
176
req = packaging .requirements .Requirement (req_string )
152
177
178
+ # Front ends SHOULD check explicitly for requirement cycles, and
179
+ # terminate the build with an informative message if one is found.
180
+ # https://www.python.org/dev/peps/pep-0517/#build-requirements
181
+ if project_name is not None and req .name == project_name :
182
+ raise CircularBuildSystemDependencyError (ancestral_req_strings + (req_string ,))
183
+
153
184
if req .marker :
154
185
extras = frozenset (('' ,)).union (parent_extras )
155
186
# a requirement can have multiple extras but ``evaluate`` can
@@ -171,7 +202,7 @@ def check_dependency(
171
202
elif dist .requires :
172
203
for other_req_string in dist .requires :
173
204
# yields transitive dependencies that are not satisfied.
174
- yield from check_dependency (other_req_string , ancestral_req_strings + (req_string ,), req .extras )
205
+ yield from check_dependency (other_req_string , ancestral_req_strings + (req_string ,), req .extras , project_name )
175
206
176
207
177
208
def _find_typo (dictionary : Mapping [str , str ], expected : str ) -> None :
@@ -222,6 +253,23 @@ def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Dict[str, An
222
253
return build_system_table
223
254
224
255
256
+ def _parse_project_name (pyproject_toml : Mapping [str , Any ]) -> Optional [str ]:
257
+ if 'project' not in pyproject_toml :
258
+ return None
259
+
260
+ project_table = dict (pyproject_toml ['project' ])
261
+
262
+ # If [project] is present, it must have a ``name`` field (per PEP 621)
263
+ if 'name' not in project_table :
264
+ raise ProjectTableValidationError ('`name` is a required property' )
265
+
266
+ project_name = project_table ['name' ]
267
+ if not isinstance (project_name , str ):
268
+ raise ProjectTableValidationError ('`name` must be a string' )
269
+
270
+ return project_name
271
+
272
+
225
273
class ProjectBuilder :
226
274
"""
227
275
The PEP 517 consumer API.
@@ -268,6 +316,7 @@ def __init__(
268
316
raise BuildException (f'Failed to parse { spec_file } : { e } ' )
269
317
270
318
self ._build_system = _parse_build_system_table (spec )
319
+ self ._project_name = _parse_project_name (spec )
271
320
self ._backend = self ._build_system ['build-backend' ]
272
321
self ._scripts_dir = scripts_dir
273
322
self ._hook_runner = runner
@@ -341,6 +390,15 @@ def get_requires_for_build(self, distribution: str, config_settings: Optional[Co
341
390
with self ._handle_backend (hook_name ):
342
391
return set (get_requires (config_settings ))
343
392
393
+ def check_build_dependencies (self ) -> Set [Tuple [str , ...]]:
394
+ """
395
+ Return the dependencies which are not satisfied from
396
+ :attr:`build_system_requires`
397
+
398
+ :returns: Set of variable-length unmet dependency tuples
399
+ """
400
+ return {u for d in self .build_system_requires for u in check_dependency (d , project_name = self ._project_name )}
401
+
344
402
def check_dependencies (
345
403
self , distribution : str , config_settings : Optional [ConfigSettingsType ] = None
346
404
) -> Set [Tuple [str , ...]]:
@@ -353,8 +411,9 @@ def check_dependencies(
353
411
:param config_settings: Config settings for the build backend
354
412
:returns: Set of variable-length unmet dependency tuples
355
413
"""
356
- dependencies = self .get_requires_for_build (distribution , config_settings ).union (self .build_system_requires )
357
- return {u for d in dependencies for u in check_dependency (d )}
414
+ build_system_dependencies = self .check_build_dependencies ()
415
+ dependencies = {u for d in self .get_requires_for_build (distribution , config_settings ) for u in check_dependency (d )}
416
+ return dependencies .union (build_system_dependencies )
358
417
359
418
def prepare (
360
419
self , distribution : str , output_directory : PathType , config_settings : Optional [ConfigSettingsType ] = None
0 commit comments