@@ -58,6 +58,28 @@ class SourceBuildResult:
5858 source_type : SourceType
5959
6060
61+ # Valid failure types for test mode error recording
62+ FailureType = typing .Literal ["resolution" , "bootstrap" , "hook" , "dependency_extraction" ]
63+
64+
65+ class FailureRecord (typing .TypedDict ):
66+ """Record of a package that failed during bootstrap in test mode.
67+
68+ Attributes:
69+ package: The package name that failed.
70+ version: The resolved version (None if resolution failed).
71+ exception_type: The exception class name.
72+ exception_message: The exception message string.
73+ failure_type: Category of failure for analysis.
74+ """
75+
76+ package : str
77+ version : str | None
78+ exception_type : str
79+ exception_message : str
80+ failure_type : FailureType
81+
82+
6183class Bootstrapper :
6284 def __init__ (
6385 self ,
@@ -98,8 +120,8 @@ def __init__(
98120
99121 self ._build_order_filename = self .ctx .work_dir / "build-order.json"
100122
101- # Track failed packages in test mode (list of dicts for JSON export)
102- self .failed_packages : list [dict [ str , typing . Any ] ] = []
123+ # Track failed packages in test mode (list of typed dicts for JSON export)
124+ self .failed_packages : list [FailureRecord ] = []
103125
104126 def resolve_and_add_top_level (
105127 self ,
@@ -141,17 +163,7 @@ def resolve_and_add_top_level(
141163 except Exception as err :
142164 if not self .test_mode :
143165 raise
144- logger .error (
145- "test mode: failed to resolve %s: %s" , req .name , err , exc_info = True
146- )
147- self .failed_packages .append (
148- {
149- "package" : str (req .name ),
150- "version" : None ,
151- "exception_type" : err .__class__ .__name__ ,
152- "exception_message" : str (err ),
153- }
154- )
166+ self ._record_test_mode_failure (req , None , err , "resolution" )
155167 return None
156168
157169 def resolve_version (
@@ -227,23 +239,18 @@ def bootstrap(self, req: Requirement, req_type: RequirementType) -> None:
227239 version = str (resolved_version )
228240 else :
229241 version = None
230- logger .error (
231- "test mode: failed to bootstrap %s: %s" , req .name , err , exc_info = True
232- )
233- self .failed_packages .append (
234- {
235- "package" : str (req .name ),
236- "version" : version ,
237- "exception_type" : err .__class__ .__name__ ,
238- "exception_message" : str (err ),
239- }
240- )
242+ self ._record_test_mode_failure (req , version , err , "bootstrap" )
241243
242244 def _bootstrap_impl (self , req : Requirement , req_type : RequirementType ) -> None :
243245 """Internal implementation of bootstrap logic.
244246
245- Errors raise exceptions for bootstrap() to catch and record in test mode.
246- In normal mode, exceptions propagate immediately (fail-fast).
247+ Error Handling:
248+ Fatal errors (version resolution, source build, prebuilt download)
249+ raise exceptions for bootstrap() to catch and record.
250+
251+ Non-fatal errors (post-hook, dependency extraction) are recorded
252+ locally and processing continues. These are recorded here because
253+ the package build succeeded - only optional post-processing failed.
247254 """
248255 logger .info (f"bootstrapping { req } as { req_type } dependency of { self .why [- 1 :]} " )
249256 constraint = self .ctx .constraints .get_constraint (req .name )
@@ -322,26 +329,45 @@ def _bootstrap_impl(self, req: Requirement, req_type: RequirementType) -> None:
322329 unpacked_cached_wheel = unpacked_cached_wheel ,
323330 )
324331
325- # Run post-bootstrap hooks
326- hooks .run_post_bootstrap_hooks (
327- ctx = self .ctx ,
328- req = req ,
329- dist_name = canonicalize_name (req .name ),
330- dist_version = str (resolved_version ),
331- sdist_filename = build_result .sdist_filename ,
332- wheel_filename = build_result .wheel_filename ,
333- )
332+ # Run post-bootstrap hooks (non-fatal in test mode)
333+ try :
334+ hooks .run_post_bootstrap_hooks (
335+ ctx = self .ctx ,
336+ req = req ,
337+ dist_name = canonicalize_name (req .name ),
338+ dist_version = str (resolved_version ),
339+ sdist_filename = build_result .sdist_filename ,
340+ wheel_filename = build_result .wheel_filename ,
341+ )
342+ except Exception as hook_error :
343+ if not self .test_mode :
344+ raise
345+ self ._record_test_mode_failure (
346+ req , str (resolved_version ), hook_error , "hook" , "warning"
347+ )
334348
335- # Extract install dependencies
336- install_dependencies = self ._get_install_dependencies (
337- req = req ,
338- resolved_version = resolved_version ,
339- wheel_filename = build_result .wheel_filename ,
340- sdist_filename = build_result .sdist_filename ,
341- sdist_root_dir = build_result .sdist_root_dir ,
342- build_env = build_result .build_env ,
343- unpack_dir = build_result .unpack_dir ,
344- )
349+ # Extract install dependencies (non-fatal in test mode)
350+ try :
351+ install_dependencies = self ._get_install_dependencies (
352+ req = req ,
353+ resolved_version = resolved_version ,
354+ wheel_filename = build_result .wheel_filename ,
355+ sdist_filename = build_result .sdist_filename ,
356+ sdist_root_dir = build_result .sdist_root_dir ,
357+ build_env = build_result .build_env ,
358+ unpack_dir = build_result .unpack_dir ,
359+ )
360+ except Exception as dep_error :
361+ if not self .test_mode :
362+ raise
363+ self ._record_test_mode_failure (
364+ req ,
365+ str (resolved_version ),
366+ dep_error ,
367+ "dependency_extraction" ,
368+ "warning" ,
369+ )
370+ install_dependencies = []
345371
346372 logger .debug (
347373 "install dependencies: %s" ,
@@ -388,6 +414,40 @@ def _track_why(
388414 finally :
389415 self .why .pop ()
390416
417+ def _record_test_mode_failure (
418+ self ,
419+ req : Requirement ,
420+ version : str | None ,
421+ err : Exception ,
422+ failure_type : FailureType ,
423+ log_level : typing .Literal ["error" , "warning" ] = "error" ,
424+ ) -> None :
425+ """Record a failure in test mode. Call this after checking test_mode.
426+
427+ Args:
428+ req: The requirement that failed.
429+ version: The version being processed (None if not yet resolved).
430+ err: The exception that was raised.
431+ failure_type: Category of failure for analysis.
432+ log_level: Log at error (fatal) or warning (non-fatal, continuing).
433+ """
434+ version_str = f"=={ version } " if version else ""
435+ msg = f"test mode: { failure_type } failed for { req .name } { version_str } "
436+ if log_level == "warning" :
437+ logger .warning ("%s: %s (continuing)" , msg , err )
438+ else :
439+ logger .error ("%s: %s" , msg , err , exc_info = True )
440+
441+ self .failed_packages .append (
442+ {
443+ "package" : str (req .name ),
444+ "version" : version ,
445+ "exception_type" : err .__class__ .__name__ ,
446+ "exception_message" : str (err ),
447+ "failure_type" : failure_type ,
448+ }
449+ )
450+
391451 @property
392452 def _explain (self ) -> str :
393453 """Return message formatting current version of why stack."""
0 commit comments