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