Coverage for sources/ictruck/vehicles.py: 100%
234 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-07 00:10 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-07 00:10 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
4#============================================================================#
5# #
6# Licensed under the Apache License, Version 2.0 (the "License"); #
7# you may not use this file except in compliance with the License. #
8# You may obtain a copy of the License at #
9# #
10# http://www.apache.org/licenses/LICENSE-2.0 #
11# #
12# Unless required by applicable law or agreed to in writing, software #
13# distributed under the License is distributed on an "AS IS" BASIS, #
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
15# See the License for the specific language governing permissions and #
16# limitations under the License. #
17# #
18#============================================================================#
21''' Vehicles which vend flavors of Icecream debugger. '''
24from __future__ import annotations
26import icecream as _icecream
28from . import __
29from . import configuration as _cfg
30from . import exceptions as _exceptions
31from . import printers as _printers
34if __.typx.TYPE_CHECKING: # pragma: no cover
35 import _typeshed
38_installer_lock: __.threads.Lock = __.threads.Lock( )
39_registrar_lock: __.threads.Lock = __.threads.Lock( )
40_self_modulecfg: _cfg.ModuleConfiguration = _cfg.ModuleConfiguration(
41 flavors = __.ImmutableDictionary(
42 note = _cfg.FlavorConfiguration( prefix_emitter = 'NOTE| ' ),
43 error = _cfg.FlavorConfiguration( prefix_emitter = 'ERROR| ' ) ) )
44_validate_arguments = (
45 __.validate_arguments(
46 globalvars = globals( ),
47 errorclass = _exceptions.ArgumentClassInvalidity ) )
50class ModulesConfigurationsRegistry(
51 __.AccretiveDictionary[ str, _cfg.ModuleConfiguration ]
52):
53 ''' Accretive dictionary specifically for module registrations. '''
55 def __init__(
56 self,
57 *iterables: __.DictionaryPositionalArgument[
58 str, _cfg.ModuleConfiguration ],
59 **entries: __.DictionaryNominativeArgument[
60 _cfg.ModuleConfiguration ],
61 ):
62 super( ).__init__( { __.package_name: _self_modulecfg } )
63 self.update( *iterables, **entries )
66class Omniflavor( __.enum.Enum ):
67 ''' Singleton to match any flavor. '''
69 Instance = __.enum.auto( )
72builtins_alias_default: __.typx.Annotated[
73 str,
74 __.typx.Doc( ''' Default alias for global truck in builtins module. ''' ),
75] = 'ictr'
76modulecfgs: __.typx.Annotated[
77 ModulesConfigurationsRegistry,
78 __.typx.Doc( ''' Global registry of module configurations. ''' ),
79] = ModulesConfigurationsRegistry( )
80omniflavor: __.typx.Annotated[
81 Omniflavor, __.typx.Doc( ''' Matches any flavor. ''' )
82] = Omniflavor.Instance
85class Truck( metaclass = __.ImmutableCompleteDataclass ):
86 ''' Vends flavors of Icecream debugger. '''
88 active_flavors: __.typx.Annotated[
89 ActiveFlavorsRegistry,
90 __.typx.Doc(
91 ''' Mapping of module names to active flavor sets.
93 Key ``None`` applies globally. Module-specific entries
94 override globals for that module.
95 ''' ),
96 ] = __.dcls.field( default_factory = __.ImmutableDictionary ) # pyright: ignore
97 generalcfg: __.typx.Annotated[
98 _cfg.VehicleConfiguration,
99 __.typx.Doc(
100 ''' General configuration.
102 Top of configuration inheritance hierarchy.
103 Default is suitable for application use.
104 ''' ),
105 ] = __.dcls.field( default_factory = _cfg.VehicleConfiguration )
106 modulecfgs: __.typx.Annotated[
107 ModulesConfigurationsRegistry,
108 __.typx.Doc(
109 ''' Registry of per-module configurations.
111 Modules inherit configuration from their parent packages.
112 Top-level packages inherit from general instance
113 configruration.
114 ''' ),
115 ] = __.dcls.field( default_factory = lambda: modulecfgs )
116 printer_factory: __.typx.Annotated[
117 _printers.PrinterFactoryUnion,
118 __.typx.Doc(
119 ''' Factory which produces callables to output text somewhere.
121 May also be writable text stream.
122 Factories take two arguments, module name and flavor, and
123 return a callable which takes one argument, the string
124 produced by a formatter.
125 ''' ),
126 ] = __.funct.partial( _printers.produce_simple_printer, __.sys.stderr )
127 trace_levels: __.typx.Annotated[
128 TraceLevelsRegistry,
129 __.typx.Doc(
130 ''' Mapping of module names to maximum trace depths.
132 Key ``None`` applies globally. Module-specific entries
133 override globals for that module.
134 ''' ),
135 ] = __.dcls.field(
136 default_factory = lambda: __.ImmutableDictionary( { None: -1 } ) )
137 _debuggers: __.typx.Annotated[
138 __.AccretiveDictionary[
139 tuple[ str, _cfg.Flavor ], _icecream.IceCreamDebugger ],
140 __.typx.Doc(
141 ''' Cache of debugger instances by module and flavor. ''' ),
142 ] = __.dcls.field( default_factory = __.AccretiveDictionary ) # pyright: ignore
143 _debuggers_lock: __.typx.Annotated[
144 __.threads.Lock,
145 __.typx.Doc( ''' Access lock for cache of debugger instances. ''' ),
146 ] = __.dcls.field( default_factory = __.threads.Lock )
148 @_validate_arguments
149 def __call__(
150 self,
151 flavor: _cfg.Flavor, *,
152 module_name: __.Absential[ str ] = __.absent,
153 ) -> _icecream.IceCreamDebugger:
154 ''' Vends flavor of Icecream debugger. '''
155 mname = (
156 _discover_invoker_module_name( ) if __.is_absent( module_name )
157 else module_name )
158 cache_index = ( mname, flavor )
159 if cache_index in self._debuggers:
160 with self._debuggers_lock:
161 return self._debuggers[ cache_index ]
162 configuration = _produce_ic_configuration( self, mname, flavor )
163 control = _cfg.FormatterControl( )
164 initargs = _calculate_ic_initargs(
165 self, configuration, control, mname, flavor )
166 debugger = _icecream.IceCreamDebugger( **initargs )
167 if isinstance( flavor, int ):
168 trace_level = (
169 _calculate_effective_trace_level( self.trace_levels, mname) )
170 debugger.enabled = flavor <= trace_level
171 elif isinstance( flavor, str ): # pragma: no branch
172 active_flavors = (
173 _calculate_effective_flavors( self.active_flavors, mname ) )
174 debugger.enabled = (
175 isinstance( active_flavors, Omniflavor )
176 or flavor in active_flavors )
177 with self._debuggers_lock:
178 self._debuggers[ cache_index ] = debugger
179 return debugger
181 @_validate_arguments
182 def install( self, alias: str = builtins_alias_default ) -> __.typx.Self:
183 ''' Installs truck into builtins with provided alias.
185 Replaces an existing truck. Preserves global module configurations.
187 Library developers should call :py:func:`register_module` instead.
188 '''
189 import builtins
190 with _installer_lock:
191 truck_o = getattr( builtins, alias, None )
192 if isinstance( truck_o, Truck ):
193 self( 'note', module_name = __name__ )(
194 'Installed truck is being replaced.' )
195 setattr( builtins, alias, self )
196 else:
197 __.install_builtin_safely(
198 alias, self, _exceptions.AttributeNondisplacement )
199 return self
201 @_validate_arguments
202 def register_module(
203 self,
204 name: __.Absential[ str ] = __.absent,
205 configuration: __.Absential[ _cfg.ModuleConfiguration ] = __.absent,
206 ) -> __.typx.Self:
207 ''' Registers configuration for module.
209 If no module or package name is given, then the current module is
210 inferred.
212 If no configuration is provided, then a default is generated.
213 '''
214 if __.is_absent( name ):
215 name = _discover_invoker_module_name( )
216 if __.is_absent( configuration ):
217 configuration = _cfg.ModuleConfiguration( )
218 with _registrar_lock:
219 self.modulecfgs[ name ] = configuration
220 return self
223ActiveFlavors: __.typx.TypeAlias = Omniflavor | frozenset[ _cfg.Flavor ]
224ActiveFlavorsLiberal: __.typx.TypeAlias = __.typx.Union[
225 Omniflavor,
226 __.cabc.Sequence[ _cfg.Flavor ],
227 __.cabc.Set[ _cfg.Flavor ],
228]
229ActiveFlavorsRegistry: __.typx.TypeAlias = (
230 __.ImmutableDictionary[ str | None, ActiveFlavors ] )
231ActiveFlavorsRegistryLiberal: __.typx.TypeAlias = (
232 __.cabc.Mapping[ str | None, ActiveFlavorsLiberal ] )
233ModulesConfigurationsRegistryLiberal: __.typx.TypeAlias = (
234 __.cabc.Mapping[ str, _cfg.ModuleConfiguration ] )
235TraceLevelsRegistry: __.typx.TypeAlias = (
236 __.ImmutableDictionary[ str | None, int ] )
237TraceLevelsRegistryLiberal: __.typx.TypeAlias = (
238 __.cabc.Mapping[ str | None, int ] )
240InstallAliasArgument: __.typx.TypeAlias = __.typx.Annotated[
241 str,
242 __.typx.Doc(
243 ''' Alias under which the truck is installed in builtins. ''' ),
244]
245ProduceTruckActiveFlavorsArgument: __.typx.TypeAlias = __.typx.Annotated[
246 __.Absential[ ActiveFlavorsLiberal | ActiveFlavorsRegistryLiberal ],
247 __.typx.Doc(
248 ''' Flavors to activate.
250 Can be collection, which applies globally across all registered
251 modules. Or, can be mapping of module names to sets.
253 Module-specific entries merge with global entries.
254 ''' ),
255]
256ProduceTruckEvnActiveFlavorsArgument: __.typx.TypeAlias = __.typx.Annotated[
257 __.Absential[ __.typx.Optional[ str ] ],
258 __.typx.Doc(
259 ''' Name of environment variable for active flavors or ``None``.
261 If absent, then a default environment variable name is used.
263 If ``None``, then active flavors are not parsed from the process
264 environment.
266 If active flavors are supplied directly to a function,
267 which also accepts this argument, then active flavors are not
268 parsed from the process environment.
269 ''' ),
270]
271ProduceTruckEvnTraceLevelsArgument: __.typx.TypeAlias = __.typx.Annotated[
272 __.Absential[ __.typx.Optional[ str ] ],
273 __.typx.Doc(
274 ''' Name of environment variable for trace levels or ``None``.
276 If absent, then a default environment variable name is used.
278 If ``None``, then trace levels are not parsed from the process
279 environment.
281 If trace levels are supplied directly to a function,
282 which also accepts this argument, then trace levels are not
283 parsed from the process environment.
284 ''' ),
285]
286ProduceTruckFlavorsArgument: __.typx.TypeAlias = __.typx.Annotated[
287 __.Absential[ _cfg.FlavorsRegistryLiberal ],
288 __.typx.Doc( ''' Registry of flavor identifiers to configurations. ''' ),
289]
290ProduceTruckGeneralcfgArgument: __.typx.TypeAlias = __.typx.Annotated[
291 __.Absential[ _cfg.VehicleConfiguration ],
292 __.typx.Doc(
293 ''' General configuration for the truck.
295 Top of configuration inheritance hierarchy. If absent,
296 defaults to a suitable configuration for application use.
297 ''' ),
298]
299ProduceTruckModulecfgsArgument: __.typx.TypeAlias = __.typx.Annotated[
300 __.Absential[ ModulesConfigurationsRegistryLiberal ],
301 __.typx.Doc(
302 ''' Module configurations for the truck.
304 If absent, defaults to global modules registry.
305 ''' ),
306]
307ProduceTruckPrinterFactoryArgument: __.typx.TypeAlias = __.typx.Annotated[
308 __.Absential[ _printers.PrinterFactoryUnion ],
309 __.typx.Doc(
310 ''' Factory which produces callables to output text somewhere.
312 May also be writable text stream.
313 Factories take two arguments, module name and flavor, and
314 return a callable which takes one argument, the string
315 produced by a formatter.
317 If absent, uses a default.
318 ''' ),
319]
320ProduceTruckTraceLevelsArgument: __.typx.TypeAlias = __.typx.Annotated[
321 __.Absential[ int | TraceLevelsRegistryLiberal ],
322 __.typx.Doc(
323 ''' Maximum trace depths.
325 Can be an integer, which applies globally across all registered
326 modules. Or, can be a mapping of module names to integers.
328 Module-specific entries override global entries.
329 ''' ),
330]
331RegisterModuleFormatterFactoryArgument: __.typx.TypeAlias = __.typx.Annotated[
332 __.Absential[ _cfg.FormatterFactory ],
333 __.typx.Doc(
334 ''' Factory which produces formatter callable.
336 Takes formatter control, module name, and flavor as arguments.
337 Returns formatter to convert an argument to a string.
338 ''' ),
339]
340RegisterModuleIncludeContextArgument: __.typx.TypeAlias = __.typx.Annotated[
341 __.Absential[ bool ],
342 __.typx.Doc( ''' Include stack frame with output? ''' ),
343]
344RegisterModuleNameArgument: __.typx.TypeAlias = __.typx.Annotated[
345 __.Absential[ str ],
346 __.typx.Doc(
347 ''' Name of the module to register.
349 If absent, infers the current module name.
350 ''' ),
351]
352RegisterModulePrefixEmitterArgument: __.typx.TypeAlias = __.typx.Annotated[
353 __.Absential[ _cfg.PrefixEmitterUnion ],
354 __.typx.Doc(
355 ''' String or factory which produces output prefix string.
357 Factory takes formatter control, module name, and flavor as
358 arguments. Returns prefix string.
359 ''' ),
360]
363def active_flavors_from_environment(
364 evname: __.Absential[ str ] = __.absent
365) -> ActiveFlavorsRegistry:
366 ''' Extracts active flavors from named environment variable. '''
367 active_flavors: ActiveFlavorsRegistryLiberal = { }
368 name = 'ICTRUCK_ACTIVE_FLAVORS' if __.is_absent( evname ) else evname
369 value = __.os.getenv( name, '' )
370 for part in value.split( '+' ):
371 if not part: continue
372 if ':' in part:
373 mname, flavors = part.split( ':', 1 )
374 else: mname, flavors = None, part
375 match flavors:
376 case '*': active_flavors[ mname ] = omniflavor
377 case _: active_flavors[ mname ] = flavors.split( ',' )
378 return __.ImmutableDictionary( {
379 mname:
380 flavors if isinstance( flavors, Omniflavor )
381 else frozenset( flavors )
382 for mname, flavors in active_flavors.items( ) } )
385def trace_levels_from_environment(
386 evname: __.Absential[ str ] = __.absent
387) -> TraceLevelsRegistry:
388 ''' Extracts trace levels from named environment variable. '''
389 trace_levels: TraceLevelsRegistryLiberal = { None: -1 }
390 name = 'ICTRUCK_TRACE_LEVELS' if __.is_absent( evname ) else evname
391 value = __.os.getenv( name, '' )
392 for part in value.split( '+' ):
393 if not part: continue
394 if ':' in part: mname, level = part.split( ':', 1 )
395 else: mname, level = None, part
396 if not level.isdigit( ):
397 __.warnings.warn(
398 f"Non-integer trace level {level!r} "
399 f"in environment variable {name!r}." )
400 continue
401 trace_levels[ mname ] = int( level )
402 return __.ImmutableDictionary( trace_levels )
405@_validate_arguments
406def install( # noqa: PLR0913
407 alias: InstallAliasArgument = builtins_alias_default,
408 active_flavors: ProduceTruckActiveFlavorsArgument = __.absent,
409 generalcfg: ProduceTruckGeneralcfgArgument = __.absent,
410 printer_factory: ProduceTruckPrinterFactoryArgument = __.absent,
411 trace_levels: ProduceTruckTraceLevelsArgument = __.absent,
412 evname_active_flavors: ProduceTruckEvnActiveFlavorsArgument = __.absent,
413 evname_trace_levels: ProduceTruckEvnTraceLevelsArgument = __.absent,
414) -> Truck:
415 ''' Produces truck and installs it into builtins with alias.
417 Replaces an existing truck, preserving global module configurations.
419 Library developers should call :py:func:`register_module` instead.
420 '''
421 truck = produce_truck(
422 active_flavors = active_flavors,
423 generalcfg = generalcfg,
424 printer_factory = printer_factory,
425 trace_levels = trace_levels,
426 evname_active_flavors = evname_active_flavors,
427 evname_trace_levels = evname_trace_levels )
428 return truck.install( alias = alias )
431@_validate_arguments
432def produce_truck( # noqa: PLR0913
433 active_flavors: ProduceTruckActiveFlavorsArgument = __.absent,
434 generalcfg: ProduceTruckGeneralcfgArgument = __.absent,
435 modulecfgs: ProduceTruckModulecfgsArgument = __.absent,
436 printer_factory: ProduceTruckPrinterFactoryArgument = __.absent,
437 trace_levels: ProduceTruckTraceLevelsArgument = __.absent,
438 evname_active_flavors: ProduceTruckEvnActiveFlavorsArgument = __.absent,
439 evname_trace_levels: ProduceTruckEvnTraceLevelsArgument = __.absent,
440) -> Truck:
441 ''' Produces icecream truck with some shorthand argument values. '''
442 # TODO: Deeper validation of active flavors and trace levels.
443 # TODO: Deeper validation of printer factory.
444 initargs: dict[ str, __.typx.Any ] = { }
445 if not __.is_absent( generalcfg ):
446 initargs[ 'generalcfg' ] = generalcfg
447 if not __.is_absent( modulecfgs ):
448 initargs[ 'modulecfgs' ] = ModulesConfigurationsRegistry(
449 { mname: configuration for mname, configuration
450 in modulecfgs.items( ) } )
451 if not __.is_absent( printer_factory ):
452 initargs[ 'printer_factory' ] = printer_factory
453 _add_truck_initarg_active_flavors(
454 initargs, active_flavors, evname_active_flavors )
455 _add_truck_initarg_trace_levels(
456 initargs, trace_levels, evname_trace_levels )
457 return Truck( **initargs )
460@_validate_arguments
461def register_module(
462 name: RegisterModuleNameArgument = __.absent,
463 flavors: ProduceTruckFlavorsArgument = __.absent,
464 formatter_factory: RegisterModuleFormatterFactoryArgument = __.absent,
465 include_context: RegisterModuleIncludeContextArgument = __.absent,
466 prefix_emitter: RegisterModulePrefixEmitterArgument = __.absent,
467) -> _cfg.ModuleConfiguration:
468 ''' Registers module configuration on the builtin truck.
470 If no truck exists in builtins, installs one which produces null
471 printers.
473 Intended for library developers to configure debugging flavors
474 without overriding anything set by the application or other libraries.
475 Application developers should call :py:func:`install` instead.
476 '''
477 import builtins
478 truck = getattr( builtins, builtins_alias_default, None )
479 if not isinstance( truck, Truck ):
480 truck = Truck( )
481 __.install_builtin_safely(
482 builtins_alias_default,
483 truck,
484 _exceptions.AttributeNondisplacement )
485 nomargs: dict[ str, __.typx.Any ] = { }
486 if not __.is_absent( flavors ):
487 nomargs[ 'flavors' ] = __.ImmutableDictionary( flavors )
488 if not __.is_absent( formatter_factory ):
489 nomargs[ 'formatter_factory' ] = formatter_factory
490 if not __.is_absent( include_context ):
491 nomargs[ 'include_context' ] = include_context
492 if not __.is_absent( prefix_emitter ):
493 nomargs[ 'prefix_emitter' ] = prefix_emitter
494 configuration = _cfg.ModuleConfiguration( **nomargs )
495 return truck.register_module( name = name, configuration = configuration )
498def _add_truck_initarg_active_flavors(
499 initargs: dict[ str, __.typx.Any ],
500 active_flavors: ProduceTruckActiveFlavorsArgument = __.absent,
501 evname_active_flavors: ProduceTruckEvnActiveFlavorsArgument = __.absent,
502) -> None:
503 name = 'active_flavors'
504 if not __.is_absent( active_flavors ):
505 if isinstance( active_flavors, Omniflavor ):
506 initargs[ name ] = __.ImmutableDictionary(
507 { None: active_flavors } )
508 elif isinstance( active_flavors, ( __.cabc.Sequence, __.cabc.Set ) ):
509 initargs[ name ] = __.ImmutableDictionary(
510 { None: frozenset( active_flavors ) } )
511 else:
512 initargs[ name ] = __.ImmutableDictionary( {
513 mname:
514 flavors if isinstance( flavors, Omniflavor )
515 else frozenset( flavors )
516 for mname, flavors in active_flavors.items( ) } )
517 elif evname_active_flavors is not None:
518 initargs[ name ] = (
519 active_flavors_from_environment( evname = evname_active_flavors ) )
522def _add_truck_initarg_trace_levels(
523 initargs: dict[ str, __.typx.Any ],
524 trace_levels: ProduceTruckTraceLevelsArgument = __.absent,
525 evname_trace_levels: ProduceTruckEvnTraceLevelsArgument = __.absent,
526) -> None:
527 name = 'trace_levels'
528 if not __.is_absent( trace_levels ):
529 if isinstance( trace_levels, int ):
530 initargs[ name ] = __.ImmutableDictionary( { None: trace_levels } )
531 else:
532 trace_levels_: TraceLevelsRegistryLiberal = { None: -1 }
533 trace_levels_.update( trace_levels )
534 initargs[ name ] = __.ImmutableDictionary( trace_levels_ )
535 elif evname_trace_levels is not None:
536 initargs[ name ] = (
537 trace_levels_from_environment( evname = evname_trace_levels ) )
540def _calculate_effective_flavors(
541 flavors: ActiveFlavorsRegistry, mname: str
542) -> ActiveFlavors:
543 result_ = flavors.get( None ) or frozenset( )
544 if isinstance( result_, Omniflavor ): return result_
545 result = result_
546 for mname_ in _iterate_module_name_ancestry( mname ):
547 if mname_ in flavors:
548 result_ = flavors.get( mname_ ) or frozenset( )
549 if isinstance( result_, Omniflavor ): return result_
550 result |= result_
551 return result
554def _calculate_effective_trace_level(
555 levels: TraceLevelsRegistry, mname: str
556) -> int:
557 result = levels.get( None, -1 )
558 for mname_ in _iterate_module_name_ancestry( mname ):
559 if mname_ in levels:
560 result = levels[ mname_ ]
561 return result
564def _calculate_ic_initargs(
565 truck: Truck,
566 configuration: __.ImmutableDictionary[ str, __.typx.Any ],
567 control: _cfg.FormatterControl,
568 mname: str,
569 flavor: _cfg.Flavor,
570) -> dict[ str, __.typx.Any ]:
571 nomargs: dict[ str, __.typx.Any ] = { }
572 nomargs[ 'argToStringFunction' ] = (
573 configuration[ 'formatter_factory' ]( control, mname, flavor ) )
574 nomargs[ 'includeContext' ] = configuration[ 'include_context' ]
575 if isinstance( truck.printer_factory, __.io.TextIOBase ):
576 printer = __.funct.partial( print, file = truck.printer_factory )
577 else: printer = truck.printer_factory( mname, flavor )
578 nomargs[ 'outputFunction' ] = printer
579 prefix_emitter = configuration[ 'prefix_emitter' ]
580 nomargs[ 'prefix' ] = (
581 prefix_emitter if isinstance( prefix_emitter, str )
582 else prefix_emitter( mname, flavor ) )
583 return nomargs
586def _dict_from_dataclass(
587 obj: _typeshed.DataclassInstance
588) -> dict[ str, __.typx.Any ]:
589 return {
590 field.name: getattr( obj, field.name )
591 for field in __.dcls.fields( obj ) }
594def _discover_invoker_module_name( ) -> str:
595 frame = __.inspect.currentframe( )
596 while frame: # pragma: no branch
597 module = __.inspect.getmodule( frame )
598 if module is None:
599 if '<stdin>' == frame.f_code.co_filename: # pragma: no cover
600 name = '__main__'
601 break
602 raise _exceptions.ModuleInferenceFailure
603 name = module.__name__
604 if not name.startswith( f"{__.package_name}." ): break
605 frame = frame.f_back
606 return name
609def _iterate_module_name_ancestry( name: str ) -> __.cabc.Iterator[ str ]:
610 parts = name.split( '.' )
611 for i in range( len( parts ) ):
612 yield '.'.join( parts[ : i + 1 ] )
615def _merge_ic_configuration(
616 base: dict[ str, __.typx.Any ], update_obj: _typeshed.DataclassInstance
617) -> dict[ str, __.typx.Any ]:
618 update: dict[ str, __.typx.Any ] = _dict_from_dataclass( update_obj )
619 result: dict[ str, __.typx.Any ] = { }
620 result[ 'flavors' ] = (
621 dict( base.get( 'flavors', dict( ) ) )
622 | dict( update.get( 'flavors', dict( ) ) ) )
623 for ename in ( 'formatter_factory', 'include_context', 'prefix_emitter' ):
624 uvalue = update.get( ename )
625 if uvalue is not None: result[ ename ] = uvalue
626 elif ename in base: result[ ename ] = base[ ename ]
627 return result
630def _produce_ic_configuration(
631 vehicle: Truck, mname: str, flavor: _cfg.Flavor
632) -> __.ImmutableDictionary[ str, __.typx.Any ]:
633 fconfigs: list[ _cfg.FlavorConfiguration ] = [ ]
634 vconfig = vehicle.generalcfg
635 configd: dict[ str, __.typx.Any ] = {
636 field.name: getattr( vconfig, field.name )
637 for field in __.dcls.fields( vconfig ) }
638 if flavor in vconfig.flavors:
639 fconfigs.append( vconfig.flavors[ flavor ] )
640 for mname_ in _iterate_module_name_ancestry( mname ):
641 if mname_ not in vehicle.modulecfgs: continue
642 mconfig = vehicle.modulecfgs[ mname_ ]
643 configd = _merge_ic_configuration( configd, mconfig )
644 if flavor in mconfig.flavors:
645 fconfigs.append( mconfig.flavors[ flavor ] )
646 if not fconfigs: raise _exceptions.FlavorInavailability( flavor )
647 # Apply collected flavor configs after general and module configs.
648 # (Applied in top-down order for correct overrides.)
649 for fconfig in fconfigs:
650 configd = _merge_ic_configuration( configd, fconfig )
651 return __.ImmutableDictionary( configd )