@@ -104,6 +104,28 @@ def __str__(self) -> str:
104104 return f'Failed to validate `build-system` in pyproject.toml: { self .args [0 ]} '
105105
106106
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+
107129class TypoWarning (Warning ):
108130 """
109131 Warning raised when a potential typo is found
@@ -132,7 +154,10 @@ def _validate_source_directory(srcdir: PathType) -> None:
132154
133155
134156def 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 ,
136161) -> Iterator [Tuple [str , ...]]:
137162 """
138163 Verify that a dependency and all of its dependencies are met.
@@ -150,6 +175,12 @@ def check_dependency(
150175
151176 req = packaging .requirements .Requirement (req_string )
152177
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+
153184 if req .marker :
154185 extras = frozenset (('' ,)).union (parent_extras )
155186 # a requirement can have multiple extras but ``evaluate`` can
@@ -171,7 +202,7 @@ def check_dependency(
171202 elif dist .requires :
172203 for other_req_string in dist .requires :
173204 # 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 )
175206
176207
177208def _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
222253 return build_system_table
223254
224255
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+
225273class ProjectBuilder :
226274 """
227275 The PEP 517 consumer API.
@@ -268,6 +316,7 @@ def __init__(
268316 raise BuildException (f'Failed to parse { spec_file } : { e } ' )
269317
270318 self ._build_system = _parse_build_system_table (spec )
319+ self ._project_name = _parse_project_name (spec )
271320 self ._backend = self ._build_system ['build-backend' ]
272321 self ._scripts_dir = scripts_dir
273322 self ._hook_runner = runner
@@ -341,6 +390,15 @@ def get_requires_for_build(self, distribution: str, config_settings: Optional[Co
341390 with self ._handle_backend (hook_name ):
342391 return set (get_requires (config_settings ))
343392
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+
344402 def check_dependencies (
345403 self , distribution : str , config_settings : Optional [ConfigSettingsType ] = None
346404 ) -> Set [Tuple [str , ...]]:
@@ -353,8 +411,9 @@ def check_dependencies(
353411 :param config_settings: Config settings for the build backend
354412 :returns: Set of variable-length unmet dependency tuples
355413 """
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 )
358417
359418 def prepare (
360419 self , distribution : str , output_directory : PathType , config_settings : Optional [ConfigSettingsType ] = None
0 commit comments