21
21
from collections .abc import Iterator
22
22
from typing import Any , Callable , Mapping , Optional , Sequence , TypeVar , Union
23
23
24
+ import packaging .utils
24
25
import pyproject_hooks
25
26
26
27
from . import env
27
28
from ._exceptions import (
28
29
BuildBackendException ,
29
30
BuildException ,
30
31
BuildSystemTableValidationError ,
32
+ CircularBuildDependencyError ,
31
33
FailedProcessError ,
34
+ ProjectTableValidationError ,
32
35
TypoWarning ,
36
+ ProjectNameValidationError ,
33
37
)
34
- from ._util import check_dependency , parse_wheel_filename
35
-
38
+ from ._util import check_dependency , parse_wheel_filename , project_name_from_path
36
39
37
40
if sys .version_info >= (3 , 11 ):
38
41
import tomllib
@@ -126,6 +129,23 @@ def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Mapping[str,
126
129
return build_system_table
127
130
128
131
132
+ def _parse_project_name (pyproject_toml : Mapping [str , Any ]) -> str | None :
133
+ if 'project' not in pyproject_toml :
134
+ return None
135
+
136
+ project_table = dict (pyproject_toml ['project' ])
137
+
138
+ # If [project] is present, it must have a ``name`` field (per PEP 621)
139
+ if 'name' not in project_table :
140
+ raise ProjectTableValidationError ('`project` must have a `name` field' )
141
+
142
+ project_name = project_table ['name' ]
143
+ if not isinstance (project_name , str ):
144
+ raise ProjectTableValidationError ('`name` field in `project` must be a string' )
145
+
146
+ return project_name
147
+
148
+
129
149
def _wrap_subprocess_runner (runner : RunnerType , env : env .IsolatedEnv ) -> RunnerType :
130
150
def _invoke_wrapped_runner (cmd : Sequence [str ], cwd : str | None , extra_environ : Mapping [str , str ] | None ) -> None :
131
151
runner (cmd , cwd , {** (env .make_extra_environ () or {}), ** (extra_environ or {})})
@@ -168,10 +188,18 @@ def __init__(
168
188
self ._runner = runner
169
189
170
190
pyproject_toml_path = os .path .join (source_dir , 'pyproject.toml' )
171
- self ._build_system = _parse_build_system_table (_read_pyproject_toml (pyproject_toml_path ))
172
-
191
+ pyproject_toml = _read_pyproject_toml (pyproject_toml_path )
192
+ self ._build_system = _parse_build_system_table (pyproject_toml )
193
+
194
+ self ._project_name : str | None = None
195
+ self ._project_name_source : str | None = None
196
+ project_name = _parse_project_name (pyproject_toml )
197
+ if project_name :
198
+ self ._update_project_name (project_name , 'pyproject.toml [project] table' )
173
199
self ._backend = self ._build_system ['build-backend' ]
174
200
201
+ self ._check_dependencies_incomplete : dict [str , bool ] = {'wheel' : False , 'sdist' : False }
202
+
175
203
self ._hook = pyproject_hooks .BuildBackendHookCaller (
176
204
self ._source_dir ,
177
205
self ._backend ,
@@ -198,6 +226,15 @@ def source_dir(self) -> str:
198
226
"""Project source directory."""
199
227
return self ._source_dir
200
228
229
+ @property
230
+ def project_name (self ) -> str | None :
231
+ """
232
+ The canonicalized project name.
233
+ """
234
+ if self ._project_name is not None :
235
+ return packaging .utils .canonicalize_name (self ._project_name )
236
+ return None
237
+
201
238
@property
202
239
def python_executable (self ) -> str :
203
240
"""
@@ -214,7 +251,9 @@ def build_system_requires(self) -> set[str]:
214
251
"""
215
252
return set (self ._build_system ['requires' ])
216
253
217
- def get_requires_for_build (self , distribution : str , config_settings : ConfigSettingsType | None = None ) -> set [str ]:
254
+ def get_requires_for_build (
255
+ self , distribution : str , config_settings : ConfigSettingsType | None = None , finalize : bool = False
256
+ ) -> set [str ]:
218
257
"""
219
258
Return the dependencies defined by the backend in addition to
220
259
:attr:`build_system_requires` for a given distribution.
@@ -223,14 +262,26 @@ def get_requires_for_build(self, distribution: str, config_settings: ConfigSetti
223
262
(``sdist`` or ``wheel``)
224
263
:param config_settings: Config settings for the build backend
225
264
"""
226
- self .log (f'Getting build dependencies for { distribution } ...' )
265
+ if not finalize :
266
+ self .log (f'Getting build dependencies for { distribution } ...' )
227
267
hook_name = f'get_requires_for_build_{ distribution } '
228
268
get_requires = getattr (self ._hook , hook_name )
229
269
230
270
with self ._handle_backend (hook_name ):
231
271
return set (get_requires (config_settings ))
232
272
233
- def check_dependencies (self , distribution : str , config_settings : ConfigSettingsType | None = None ) -> set [tuple [str , ...]]:
273
+ def check_build_system_dependencies (self ) -> set [tuple [str , ...]]:
274
+ """
275
+ Return the dependencies which are not satisfied from
276
+ :attr:`build_system_requires`
277
+
278
+ :returns: Set of variable-length unmet dependency tuples
279
+ """
280
+ return {u for d in self .build_system_requires for u in check_dependency (d , project_name = self ._project_name )}
281
+
282
+ def check_dependencies (
283
+ self , distribution : str , config_settings : ConfigSettingsType | None = None , finalize : bool = False
284
+ ) -> set [tuple [str , ...]]:
234
285
"""
235
286
Return the dependencies which are not satisfied from the combined set of
236
287
:attr:`build_system_requires` and :meth:`get_requires_for_build` for a given
@@ -240,8 +291,19 @@ def check_dependencies(self, distribution: str, config_settings: ConfigSettingsT
240
291
:param config_settings: Config settings for the build backend
241
292
:returns: Set of variable-length unmet dependency tuples
242
293
"""
243
- dependencies = self .get_requires_for_build (distribution , config_settings ).union (self .build_system_requires )
244
- return {u for d in dependencies for u in check_dependency (d )}
294
+ if self ._project_name is None :
295
+ self ._check_dependencies_incomplete [distribution ] = True
296
+ build_system_dependencies = self .check_build_system_dependencies ()
297
+ requires_for_build = self .get_requires_for_build (distribution , config_settings , finalize = finalize )
298
+ dependencies = {
299
+ u for d in requires_for_build for u in check_dependency (d , project_name = self ._project_name , backend = self ._backend )
300
+ }
301
+ return dependencies .union (build_system_dependencies )
302
+
303
+ def finalize_check_dependencies (self , distribution : str , config_settings : ConfigSettingsType | None = None ) -> None :
304
+ if self ._check_dependencies_incomplete [distribution ] and self ._project_name is not None :
305
+ self .check_dependencies (distribution , config_settings , finalize = True )
306
+ self ._check_dependencies_incomplete [distribution ] = False
245
307
246
308
def prepare (
247
309
self , distribution : str , output_directory : PathType , config_settings : ConfigSettingsType | None = None
@@ -286,7 +348,11 @@ def build(
286
348
"""
287
349
self .log (f'Building { distribution } ...' )
288
350
kwargs = {} if metadata_directory is None else {'metadata_directory' : metadata_directory }
289
- return self ._call_backend (f'build_{ distribution } ' , output_directory , config_settings , ** kwargs )
351
+ basename = self ._call_backend (f'build_{ distribution } ' , output_directory , config_settings , ** kwargs )
352
+ project_name = project_name_from_path (basename , distribution )
353
+ if project_name :
354
+ self ._update_project_name (project_name , f'build_{ distribution } ' )
355
+ return basename
290
356
291
357
def metadata_path (self , output_directory : PathType ) -> str :
292
358
"""
@@ -301,13 +367,17 @@ def metadata_path(self, output_directory: PathType) -> str:
301
367
# prepare_metadata hook
302
368
metadata = self .prepare ('wheel' , output_directory )
303
369
if metadata is not None :
370
+ project_name = project_name_from_path (metadata , 'distinfo' )
371
+ if project_name :
372
+ self ._update_project_name (project_name , 'prepare_metadata_for_build_wheel' )
304
373
return metadata
305
374
306
375
# fallback to build_wheel hook
307
376
wheel = self .build ('wheel' , output_directory )
308
377
match = parse_wheel_filename (os .path .basename (wheel ))
309
378
if not match :
310
379
raise ValueError ('Invalid wheel' )
380
+ self ._update_project_name (match ['distribution' ], 'build_wheel' )
311
381
distinfo = f"{ match ['distribution' ]} -{ match ['version' ]} .dist-info"
312
382
member_prefix = f'{ distinfo } /'
313
383
with zipfile .ZipFile (wheel ) as w :
@@ -352,6 +422,16 @@ def _handle_backend(self, hook: str) -> Iterator[None]:
352
422
except Exception as exception :
353
423
raise BuildBackendException (exception , exc_info = sys .exc_info ()) # noqa: B904 # use raise from
354
424
425
+ def _update_project_name (self , name : str , source : str ) -> None :
426
+ if (
427
+ self ._project_name is not None
428
+ and self ._project_name_source is not None
429
+ and packaging .utils .canonicalize_name (self ._project_name ) != packaging .utils .canonicalize_name (name )
430
+ ):
431
+ raise ProjectNameValidationError (self ._project_name , self ._project_name_source , name , source )
432
+ self ._project_name = name
433
+ self ._project_name_source = source
434
+
355
435
@staticmethod
356
436
def log (message : str ) -> None :
357
437
"""
@@ -373,9 +453,11 @@ def log(message: str) -> None:
373
453
'BuildSystemTableValidationError' ,
374
454
'BuildBackendException' ,
375
455
'BuildException' ,
456
+ 'CircularBuildDependencyError' ,
376
457
'ConfigSettingsType' ,
377
458
'FailedProcessError' ,
378
459
'ProjectBuilder' ,
460
+ 'ProjectTableValidationError' ,
379
461
'RunnerType' ,
380
462
'TypoWarning' ,
381
463
'check_dependency' ,
0 commit comments