54
54
_ExcInfoType = Union [Tuple [Type [BaseException ], BaseException , types .TracebackType ], Tuple [None , None , None ]]
55
55
56
56
57
+ _SDIST_NAME_REGEX = re .compile (r'(?P<distribution>.+)-(?P<version>.+)\.tar.gz' )
58
+
59
+
57
60
_WHEEL_NAME_REGEX = re .compile (
58
61
r'(?P<distribution>.+)-(?P<version>.+)'
59
62
r'(-(?P<build_tag>.+))?-(?P<python_tag>.+)'
@@ -104,6 +107,19 @@ def __str__(self) -> str:
104
107
return f'Failed to validate `build-system` in pyproject.toml: { self .args [0 ]} '
105
108
106
109
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
+
107
123
class TypoWarning (Warning ):
108
124
"""
109
125
Warning raised when a potential typo is found
@@ -131,8 +147,16 @@ def _validate_source_directory(srcdir: PathType) -> None:
131
147
raise BuildException (f'Source { srcdir } does not appear to be a Python project: no pyproject.toml or setup.py' )
132
148
133
149
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
+
134
155
def 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 ,
136
160
) -> Iterator [Tuple [str , ...]]:
137
161
"""
138
162
Verify that a dependency and all of its dependencies are met.
@@ -159,6 +183,12 @@ def check_dependency(
159
183
# dependency is satisfied.
160
184
return
161
185
186
+ # Front ends SHOULD check explicitly for requirement cycles, and
187
+ # terminate the build with an informative message if one is found.
188
+ # https://www.python.org/dev/peps/pep-0517/#build-requirements
189
+ if project_name is not None and _normalize (req .name ) == _normalize (project_name ):
190
+ raise CircularBuildSystemDependencyError ((project_name ,) + ancestral_req_strings + (req_string ,))
191
+
162
192
try :
163
193
dist = importlib_metadata .distribution (req .name ) # type: ignore[no-untyped-call]
164
194
except importlib_metadata .PackageNotFoundError :
@@ -171,7 +201,7 @@ def check_dependency(
171
201
elif dist .requires :
172
202
for other_req_string in dist .requires :
173
203
# 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 )
175
205
176
206
177
207
def _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
222
252
return build_system_table
223
253
224
254
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
+
225
272
class ProjectBuilder :
226
273
"""
227
274
The PEP 517 consumer API.
@@ -267,10 +314,14 @@ def __init__(
267
314
except TOMLDecodeError as e :
268
315
raise BuildException (f'Failed to parse { spec_file } : { e } ' )
269
316
317
+ self .project_name : Optional [str ] = _parse_project_name (spec )
270
318
self ._build_system = _parse_build_system_table (spec )
271
319
self ._backend = self ._build_system ['build-backend' ]
272
320
self ._scripts_dir = scripts_dir
273
321
self ._hook_runner = runner
322
+ self ._in_tree_build = False
323
+ if 'backend-path' in self ._build_system :
324
+ self ._in_tree_build = True
274
325
self ._hook = pep517 .wrappers .Pep517HookCaller (
275
326
self .srcdir ,
276
327
self ._backend ,
@@ -341,6 +392,17 @@ def get_requires_for_build(self, distribution: str, config_settings: Optional[Co
341
392
with self ._handle_backend (hook_name ):
342
393
return set (get_requires (config_settings ))
343
394
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
+
344
406
def check_dependencies (
345
407
self , distribution : str , config_settings : Optional [ConfigSettingsType ] = None
346
408
) -> Set [Tuple [str , ...]]:
@@ -353,8 +415,9 @@ def check_dependencies(
353
415
:param config_settings: Config settings for the build backend
354
416
:returns: Set of variable-length unmet dependency tuples
355
417
"""
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 )
358
421
359
422
def prepare (
360
423
self , distribution : str , output_directory : PathType , config_settings : Optional [ConfigSettingsType ] = None
@@ -399,7 +462,15 @@ def build(
399
462
"""
400
463
self .log (f'Building { distribution } ...' )
401
464
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
403
474
404
475
def metadata_path (self , output_directory : PathType ) -> str :
405
476
"""
@@ -413,13 +484,17 @@ def metadata_path(self, output_directory: PathType) -> str:
413
484
# prepare_metadata hook
414
485
metadata = self .prepare ('wheel' , output_directory )
415
486
if metadata is not None :
487
+ match = _WHEEL_NAME_REGEX .match (os .path .basename (metadata ))
488
+ if match :
489
+ self .project_name = match ['distribution' ]
416
490
return metadata
417
491
418
492
# fallback to build_wheel hook
419
493
wheel = self .build ('wheel' , output_directory )
420
494
match = _WHEEL_NAME_REGEX .match (os .path .basename (wheel ))
421
495
if not match :
422
496
raise ValueError ('Invalid wheel' )
497
+ self .project_name = match ['distribution' ]
423
498
distinfo = f"{ match ['distribution' ]} -{ match ['version' ]} .dist-info"
424
499
member_prefix = f'{ distinfo } /'
425
500
with zipfile .ZipFile (wheel ) as w :
0 commit comments