2121from collections .abc import Iterator
2222from typing import Any , Callable , Mapping , Optional , Sequence , TypeVar , Union
2323
24+ import packaging .utils
2425import pyproject_hooks
2526
2627from . import env
2728from ._exceptions import (
2829 BuildBackendException ,
2930 BuildException ,
3031 BuildSystemTableValidationError ,
32+ CircularBuildDependencyError ,
3133 FailedProcessError ,
34+ ProjectTableValidationError ,
3235 TypoWarning ,
36+ ProjectNameValidationError ,
3337)
34- from ._util import check_dependency , parse_wheel_filename
35-
38+ from ._util import check_dependency , parse_wheel_filename , project_name_from_path
3639
3740if sys .version_info >= (3 , 11 ):
3841 import tomllib
@@ -126,6 +129,23 @@ def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Mapping[str,
126129 return build_system_table
127130
128131
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+
129149def _wrap_subprocess_runner (runner : RunnerType , env : env .IsolatedEnv ) -> RunnerType :
130150 def _invoke_wrapped_runner (cmd : Sequence [str ], cwd : str | None , extra_environ : Mapping [str , str ] | None ) -> None :
131151 runner (cmd , cwd , {** (env .make_extra_environ () or {}), ** (extra_environ or {})})
@@ -168,10 +188,18 @@ def __init__(
168188 self ._runner = runner
169189
170190 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' )
173199 self ._backend = self ._build_system ['build-backend' ]
174200
201+ self ._check_dependencies_incomplete : dict [str , bool ] = {'wheel' : False , 'sdist' : False }
202+
175203 self ._hook = pyproject_hooks .BuildBackendHookCaller (
176204 self ._source_dir ,
177205 self ._backend ,
@@ -198,6 +226,15 @@ def source_dir(self) -> str:
198226 """Project source directory."""
199227 return self ._source_dir
200228
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+
201238 @property
202239 def python_executable (self ) -> str :
203240 """
@@ -214,7 +251,9 @@ def build_system_requires(self) -> set[str]:
214251 """
215252 return set (self ._build_system ['requires' ])
216253
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 ]:
218257 """
219258 Return the dependencies defined by the backend in addition to
220259 :attr:`build_system_requires` for a given distribution.
@@ -223,14 +262,26 @@ def get_requires_for_build(self, distribution: str, config_settings: ConfigSetti
223262 (``sdist`` or ``wheel``)
224263 :param config_settings: Config settings for the build backend
225264 """
226- self .log (f'Getting build dependencies for { distribution } ...' )
265+ if not finalize :
266+ self .log (f'Getting build dependencies for { distribution } ...' )
227267 hook_name = f'get_requires_for_build_{ distribution } '
228268 get_requires = getattr (self ._hook , hook_name )
229269
230270 with self ._handle_backend (hook_name ):
231271 return set (get_requires (config_settings ))
232272
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 , ...]]:
234285 """
235286 Return the dependencies which are not satisfied from the combined set of
236287 :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
240291 :param config_settings: Config settings for the build backend
241292 :returns: Set of variable-length unmet dependency tuples
242293 """
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
245307
246308 def prepare (
247309 self , distribution : str , output_directory : PathType , config_settings : ConfigSettingsType | None = None
@@ -286,7 +348,11 @@ def build(
286348 """
287349 self .log (f'Building { distribution } ...' )
288350 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
290356
291357 def metadata_path (self , output_directory : PathType ) -> str :
292358 """
@@ -301,13 +367,17 @@ def metadata_path(self, output_directory: PathType) -> str:
301367 # prepare_metadata hook
302368 metadata = self .prepare ('wheel' , output_directory )
303369 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' )
304373 return metadata
305374
306375 # fallback to build_wheel hook
307376 wheel = self .build ('wheel' , output_directory )
308377 match = parse_wheel_filename (os .path .basename (wheel ))
309378 if not match :
310379 raise ValueError ('Invalid wheel' )
380+ self ._update_project_name (match ['distribution' ], 'build_wheel' )
311381 distinfo = f"{ match ['distribution' ]} -{ match ['version' ]} .dist-info"
312382 member_prefix = f'{ distinfo } /'
313383 with zipfile .ZipFile (wheel ) as w :
@@ -352,6 +422,16 @@ def _handle_backend(self, hook: str) -> Iterator[None]:
352422 except Exception as exception :
353423 raise BuildBackendException (exception , exc_info = sys .exc_info ()) # noqa: B904 # use raise from
354424
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+
355435 @staticmethod
356436 def log (message : str ) -> None :
357437 """
@@ -373,9 +453,12 @@ def log(message: str) -> None:
373453 'BuildSystemTableValidationError' ,
374454 'BuildBackendException' ,
375455 'BuildException' ,
456+ 'CircularBuildDependencyError' ,
376457 'ConfigSettingsType' ,
377458 'FailedProcessError' ,
378459 'ProjectBuilder' ,
460+ 'ProjectNameValidationError' ,
461+ 'ProjectTableValidationError' ,
379462 'RunnerType' ,
380463 'TypoWarning' ,
381464 'check_dependency' ,
0 commit comments