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