1111from pathlib import Path
1212from typing import Any , Literal
1313
14- from pydantic import AliasChoices , BaseModel , Field , field_validator
14+ from pydantic import AliasChoices , BaseModel , ConfigDict , Field , field_validator
1515from pydantic_settings import (
1616 BaseSettings ,
1717 PydanticBaseSettingsSource ,
4040]
4141
4242
43+ class CallableFactoryConfig (BaseModel ):
44+ model_config = ConfigDict (extra = "allow" , populate_by_name = True )
45+
46+ callable : str = Field (
47+ description = "Custom callable" ,
48+ validation_alias = AliasChoices ("callable" , "()" ),
49+ serialization_alias = "()" ,
50+ )
51+
52+
4353# Formatter Models
4454class FormatterConfig (BaseModel ):
4555 """Configuration for a logging formatter."""
@@ -92,9 +102,9 @@ class FilterConfig(BaseModel):
92102# Handler Models
93103class BaseHandlerConfig (BaseModel ):
94104 """Base configuration for all handlers."""
95-
96- model_config = { " extra" : " allow"}
97-
105+
106+ model_config = ConfigDict ( extra = " allow")
107+
98108 class_ : str = Field (
99109 validation_alias = AliasChoices ("class_" , "class" ),
100110 serialization_alias = "class" ,
@@ -389,7 +399,7 @@ class QueueHandlerConfig(BaseHandlerConfig):
389399 validation_alias = AliasChoices ("class_" , "class" ),
390400 serialization_alias = "class"
391401 )
392- queue : str = Field (
402+ queue : str | CallableFactoryConfig = Field (
393403 description = "Queue object reference"
394404 )
395405
@@ -402,7 +412,7 @@ class QueueListenerConfig(BaseHandlerConfig):
402412 validation_alias = AliasChoices ("class_" , "class" ),
403413 serialization_alias = "class"
404414 )
405- queue : str = Field (
415+ queue : str | CallableFactoryConfig = Field (
406416 description = "Queue object reference"
407417 )
408418 handlers : list [str ] = Field (
@@ -421,7 +431,7 @@ class QueueListenerConfig(BaseHandlerConfig):
421431# Logger configuration
422432class LoggerConfig (BaseModel ):
423433 """Configuration for a logger."""
424-
434+
425435 level : str | None = Field (
426436 default = None ,
427437 description = "Logging level"
@@ -478,7 +488,7 @@ def _load_file(self) -> dict[str, Any]:
478488
479489 with open (self .toml_file , "rb" ) as f :
480490 data = tomllib .load (f )
481-
491+
482492 # Navigate to specified table if provided
483493 if self .toml_table :
484494 for key in self .toml_table :
@@ -508,11 +518,11 @@ def __init__(
508518 super ().__init__ (settings_cls )
509519 self .json_file = json_file
510520 self ._data = self ._load_file ()
511-
521+
512522 def _load_file (self ) -> dict [str , Any ]:
513523 if not self .json_file or not Path (self .json_file ).exists ():
514524 return {}
515-
525+
516526 with open (self .json_file , "r" ) as f :
517527 return json .load (f )
518528
@@ -565,7 +575,7 @@ def _convert_ini_to_dictconfig(self, config: ConfigParser) -> dict[str, Any]:
565575 if key and config .has_section (f"formatter_{ key } " ):
566576 result ["formatters" ][key ] = self ._parse_formatter_section (config , f"formatter_{ key } " )
567577
568- # Parse handlers
578+ # Parse handlers
569579 if config .has_section ("handlers" ):
570580 handler_keys = config .get ("handlers" , "keys" , fallback = "" ).split ("," )
571581 for key in handler_keys :
@@ -583,7 +593,7 @@ def _convert_ini_to_dictconfig(self, config: ConfigParser) -> dict[str, Any]:
583593 result ["root" ] = self ._parse_logger_section (config , f"logger_{ key } " , is_root = True )
584594 else :
585595 result ["loggers" ][key ] = self ._parse_logger_section (config , f"logger_{ key } " , is_root = False )
586-
596+
587597 # Parse global settings if they exist
588598 if config .has_option ("DEFAULT" , "disable_existing_loggers" ):
589599 result ["disable_existing_loggers" ] = config .getboolean ("DEFAULT" , "disable_existing_loggers" )
@@ -593,7 +603,7 @@ def _convert_ini_to_dictconfig(self, config: ConfigParser) -> dict[str, Any]:
593603 def _parse_formatter_section (self , config : ConfigParser , section : str ) -> dict [str , Any ]:
594604 """Parse a formatter section."""
595605 formatter = {}
596-
606+
597607 if config .has_option (section , "format" ):
598608 formatter ["format" ] = config .get (section , "format" )
599609 if config .has_option (section , "datefmt" ):
@@ -606,7 +616,24 @@ def _parse_formatter_section(self, config: ConfigParser, section: str) -> dict[s
606616 formatter ["validate" ] = config .getboolean (section , "validate" )
607617 if config .has_option (section , "class" ):
608618 formatter ["class" ] = config .get (section , "class" )
609-
619+
620+ # Add any other options as extra fields
621+ for option in config .options (section ):
622+ if option not in ["format" , "datefmt" , "style" , "validate" , "class" ]:
623+ value = config .get (section , option )
624+ # Try to convert to appropriate type
625+ try :
626+ # Try boolean
627+ if value .lower () in ["true" , "false" ]:
628+ formatter [option ] = config .getboolean (section , option )
629+ # Try integer
630+ elif value .isdigit ():
631+ formatter [option ] = config .getint (section , option )
632+ else :
633+ formatter [option ] = value
634+ except (ValueError , AttributeError ):
635+ formatter [option ] = value
636+
610637 return formatter
611638
612639 def _parse_handler_section (self , config : ConfigParser , section : str ) -> dict [str , Any ]:
@@ -639,15 +666,15 @@ def _parse_handler_section(self, config: ConfigParser, section: str) -> dict[str
639666 # Handle args - this is complex as it can be a Python expression
640667 args_str = config .get (section , "args" )
641668 # For basic cases, try to parse as a simple tuple
642- if args_str == "()" or args_str == "()" :
669+ if args_str == "()" :
643670 handler ["args" ] = []
644671 elif args_str .startswith ("(" ) and args_str .endswith (")" ):
645672 # Store as string - the logging module will evaluate it
646673 handler ["args" ] = args_str
647-
674+
648675 # Add any other options as extra fields
649676 for option in config .options (section ):
650- if option not in ["class" , "level" , "formatter" , "stream" , "filename" , "mode" ,
677+ if option not in ["class" , "level" , "formatter" , "stream" , "filename" , "mode" ,
651678 "maxBytes" , "backupCount" , "when" , "interval" , "utc" , "args" ]:
652679 value = config .get (section , option )
653680 # Try to convert to appropriate type
@@ -704,7 +731,7 @@ class LoggingSettings(BaseSettings):
704731 4. logging.toml - TOML configuration file
705732 5. logging.ini - INI configuration file (logging.config.fileConfig format)
706733 6. pyproject.toml [tool.logging] section (lowest priority)
707-
734+
708735 The model_dump() method returns a dictionary that can be passed
709736 directly to logging.config.dictConfig().
710737 """
@@ -713,15 +740,15 @@ class LoggingSettings(BaseSettings):
713740 default = 1 ,
714741 description = "Configuration schema version"
715742 )
716- formatters : dict [str , FormatterConfig ] = Field (
743+ formatters : dict [str , FormatterConfig | CallableFactoryConfig ] = Field (
717744 default_factory = dict ,
718745 description = "Formatter configurations"
719746 )
720- filters : dict [str , FilterConfig ] = Field (
747+ filters : dict [str , FilterConfig | CallableFactoryConfig ] = Field (
721748 default_factory = dict ,
722749 description = "Filter configurations"
723750 )
724- handlers : dict [str , Any ] = Field (
751+ handlers : dict [str , HandlerConfig | CallableFactoryConfig ] = Field (
725752 default_factory = dict ,
726753 description = "Handler configurations"
727754 )
@@ -804,7 +831,7 @@ def settings_customise_sources(
804831 sources .append (JsonConfigSettingsSource (settings_cls , json_file = json_file ))
805832 elif Path ("logging.json" ).exists ():
806833 sources .append (JsonConfigSettingsSource (settings_cls , json_file = "logging.json" ))
807-
834+
808835 if toml_file and Path (toml_file ).exists ():
809836 sources .append (TomlConfigSettingsSource (settings_cls , toml_file = toml_file ))
810837 elif Path ("logging.toml" ).exists ():
@@ -814,7 +841,7 @@ def settings_customise_sources(
814841 sources .append (IniConfigSettingsSource (settings_cls , ini_file = ini_file ))
815842 elif Path ("logging.ini" ).exists ():
816843 sources .append (IniConfigSettingsSource (settings_cls , ini_file = "logging.ini" ))
817-
844+
818845 # Add pyproject.toml source (lowest priority file)
819846 if Path ("pyproject.toml" ).exists ():
820847 sources .append (
@@ -938,7 +965,7 @@ def model_dump(self, **kwargs) -> dict[str, Any]:
938965 else :
939966 handlers [name ] = handler
940967 data ["handlers" ] = handlers
941-
968+
942969 # Convert formatter models to dicts
943970 if "formatters" in data :
944971 formatters = {}
@@ -958,7 +985,7 @@ def model_dump(self, **kwargs) -> dict[str, Any]:
958985 else :
959986 filters [name ] = filter_config
960987 data ["filters" ] = filters
961-
988+
962989 # Convert logger models to dicts
963990 if "loggers" in data :
964991 loggers = {}
@@ -968,7 +995,7 @@ def model_dump(self, **kwargs) -> dict[str, Any]:
968995 else :
969996 loggers [name ] = logger
970997 data ["loggers" ] = loggers
971-
998+
972999 # Convert root logger to dict
9731000 if "root" in data and isinstance (data ["root" ], BaseModel ):
9741001 data ["root" ] = data ["root" ].model_dump (exclude_none = True )
0 commit comments