5454_ExcInfoType = Union [Tuple [Type [BaseException ], BaseException , types .TracebackType ], Tuple [None , None , None ]]
5555
5656
57+ _SDIST_NAME_REGEX = re .compile (r'(?P<distribution>.+)-(?P<version>.+)\.tar.gz' )
58+
59+
5760_WHEEL_NAME_REGEX = re .compile (
5861 r'(?P<distribution>.+)-(?P<version>.+)'
5962 r'(-(?P<build_tag>.+))?-(?P<python_tag>.+)'
@@ -104,6 +107,19 @@ def __str__(self) -> str:
104107 return f'Failed to validate `build-system` in pyproject.toml: { self .args [0 ]} '
105108
106109
110+ class CircularBuildSystemDependencyError (BuildException ):
111+ """
112+ Exception raised when a ``[build-system]`` requirement in pyproject.toml is circular.
113+ """
114+
115+ def __str__ (self ) -> str :
116+ cycle_deps = self .args [0 ]
117+ cycle_err_str = f'`{ cycle_deps [0 ]} `'
118+ for dep in cycle_deps [1 :]:
119+ cycle_err_str += f' -> `{ dep } `'
120+ return f'Failed to validate `build-system` in pyproject.toml, dependency cycle detected: { cycle_err_str } '
121+
122+
107123class TypoWarning (Warning ):
108124 """
109125 Warning raised when a potential typo is found
@@ -131,8 +147,16 @@ def _validate_source_directory(srcdir: PathType) -> None:
131147 raise BuildException (f'Source { srcdir } does not appear to be a Python project: no pyproject.toml or setup.py' )
132148
133149
150+ # https://www.python.org/dev/peps/pep-0503/#normalized-names
151+ def _normalize (name : str ) -> str :
152+ return re .sub (r'[-_.]+' , '-' , name ).lower ()
153+
154+
134155def check_dependency (
135- req_string : str , ancestral_req_strings : Tuple [str , ...] = (), parent_extras : AbstractSet [str ] = frozenset ()
156+ req_string : str ,
157+ ancestral_req_strings : Tuple [str , ...] = (),
158+ parent_extras : AbstractSet [str ] = frozenset (),
159+ project_name : Optional [str ] = None ,
136160) -> Iterator [Tuple [str , ...]]:
137161 """
138162 Verify that a dependency and all of its dependencies are met.
@@ -150,6 +174,12 @@ def check_dependency(
150174
151175 req = packaging .requirements .Requirement (req_string )
152176
177+ # Front ends SHOULD check explicitly for requirement cycles, and
178+ # terminate the build with an informative message if one is found.
179+ # https://www.python.org/dev/peps/pep-0517/#build-requirements
180+ if project_name is not None and _normalize (req .name ) == _normalize (project_name ):
181+ raise CircularBuildSystemDependencyError ((project_name ,) + ancestral_req_strings + (req_string ,))
182+
153183 if req .marker :
154184 extras = frozenset (('' ,)).union (parent_extras )
155185 # a requirement can have multiple extras but ``evaluate`` can
@@ -171,7 +201,7 @@ def check_dependency(
171201 elif dist .requires :
172202 for other_req_string in dist .requires :
173203 # yields transitive dependencies that are not satisfied.
174- yield from check_dependency (other_req_string , ancestral_req_strings + (req_string ,), req .extras )
204+ yield from check_dependency (other_req_string , ancestral_req_strings + (req_string ,), req .extras , project_name )
175205
176206
177207def _find_typo (dictionary : Mapping [str , str ], expected : str ) -> None :
@@ -222,6 +252,23 @@ def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Dict[str, An
222252 return build_system_table
223253
224254
255+ def _parse_project_name (pyproject_toml : Mapping [str , Any ]) -> Optional [str ]:
256+ if 'project' not in pyproject_toml :
257+ return None
258+
259+ project_table = dict (pyproject_toml ['project' ])
260+
261+ # If [project] is present, it must have a ``name`` field (per PEP 621)
262+ if 'name' not in project_table :
263+ return None
264+
265+ project_name = project_table ['name' ]
266+ if not isinstance (project_name , str ):
267+ return None
268+
269+ return project_name
270+
271+
225272class ProjectBuilder :
226273 """
227274 The PEP 517 consumer API.
@@ -267,10 +314,14 @@ def __init__(
267314 except TOMLDecodeError as e :
268315 raise BuildException (f'Failed to parse { spec_file } : { e } ' )
269316
317+ self .project_name : Optional [str ] = _parse_project_name (spec )
270318 self ._build_system = _parse_build_system_table (spec )
271319 self ._backend = self ._build_system ['build-backend' ]
272320 self ._scripts_dir = scripts_dir
273321 self ._hook_runner = runner
322+ self ._in_tree_build = False
323+ if 'backend-path' in self ._build_system :
324+ self ._in_tree_build = True
274325 self ._hook = pep517 .wrappers .Pep517HookCaller (
275326 self .srcdir ,
276327 self ._backend ,
@@ -341,6 +392,17 @@ def get_requires_for_build(self, distribution: str, config_settings: Optional[Co
341392 with self ._handle_backend (hook_name ):
342393 return set (get_requires (config_settings ))
343394
395+ def check_build_dependencies (self ) -> Set [Tuple [str , ...]]:
396+ """
397+ Return the dependencies which are not satisfied from
398+ :attr:`build_system_requires`
399+
400+ :returns: Set of variable-length unmet dependency tuples
401+ """
402+ if self ._in_tree_build :
403+ return set ()
404+ return {u for d in self .build_system_requires for u in check_dependency (d , project_name = self .project_name )}
405+
344406 def check_dependencies (
345407 self , distribution : str , config_settings : Optional [ConfigSettingsType ] = None
346408 ) -> Set [Tuple [str , ...]]:
@@ -353,8 +415,9 @@ def check_dependencies(
353415 :param config_settings: Config settings for the build backend
354416 :returns: Set of variable-length unmet dependency tuples
355417 """
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 )}
418+ build_system_dependencies = self .check_build_dependencies ()
419+ dependencies = {u for d in self .get_requires_for_build (distribution , config_settings ) for u in check_dependency (d )}
420+ return dependencies .union (build_system_dependencies )
358421
359422 def prepare (
360423 self , distribution : str , output_directory : PathType , config_settings : Optional [ConfigSettingsType ] = None
@@ -399,7 +462,15 @@ def build(
399462 """
400463 self .log (f'Building { distribution } ...' )
401464 kwargs = {} if metadata_directory is None else {'metadata_directory' : metadata_directory }
402- return self ._call_backend (f'build_{ distribution } ' , output_directory , config_settings , ** kwargs )
465+ basename = self ._call_backend (f'build_{ distribution } ' , output_directory , config_settings , ** kwargs )
466+ match = None
467+ if distribution == 'wheel' :
468+ match = _WHEEL_NAME_REGEX .match (os .path .basename (basename ))
469+ elif distribution == 'sdist' :
470+ match = _SDIST_NAME_REGEX .match (os .path .basename (basename ))
471+ if match :
472+ self .project_name = match ['distribution' ]
473+ return basename
403474
404475 def metadata_path (self , output_directory : PathType ) -> str :
405476 """
@@ -413,13 +484,17 @@ def metadata_path(self, output_directory: PathType) -> str:
413484 # prepare_metadata hook
414485 metadata = self .prepare ('wheel' , output_directory )
415486 if metadata is not None :
487+ match = _WHEEL_NAME_REGEX .match (os .path .basename (metadata ))
488+ if match :
489+ self .project_name = match ['distribution' ]
416490 return metadata
417491
418492 # fallback to build_wheel hook
419493 wheel = self .build ('wheel' , output_directory )
420494 match = _WHEEL_NAME_REGEX .match (os .path .basename (wheel ))
421495 if not match :
422496 raise ValueError ('Invalid wheel' )
497+ self .project_name = match ['distribution' ]
423498 distinfo = f"{ match ['distribution' ]} -{ match ['version' ]} .dist-info"
424499 member_prefix = f'{ distinfo } /'
425500 with zipfile .ZipFile (wheel ) as w :
0 commit comments