3535from pathlib import Path
3636from typing import Any , Iterator , Literal
3737
38- from pydantic import Field , SecretStr , field_validator
38+ from pydantic import Field , SecretStr , field_validator , model_validator
3939from pydantic_settings import BaseSettings , SettingsConfigDict
4040
4141from .errors import DataJointError
@@ -288,6 +288,13 @@ class Config(BaseSettings):
288288 _config_path : Path | None = None
289289 _secrets_dir : Path | None = None
290290
291+ @model_validator (mode = "after" )
292+ def _init_dynamic (self ):
293+ """Initialize dynamic settings storage after model validation."""
294+ if not hasattr (self , "_dynamic" ):
295+ object .__setattr__ (self , "_dynamic" , {})
296+ return self
297+
291298 @field_validator ("loglevel" , mode = "after" )
292299 @classmethod
293300 def set_logger_level (cls , v : str ) -> str :
@@ -655,6 +662,10 @@ def override(self, **kwargs: Any) -> Iterator["Config"]:
655662 # Dict-like access for convenience
656663 def __getitem__ (self , key : str ) -> Any :
657664 """Get setting by dot-notation key (e.g., 'database.host')."""
665+ # Check dynamic settings first for backwards compatibility
666+ if key in self ._dynamic :
667+ return self ._dynamic [key ]
668+
658669 parts = key .split ("." )
659670 obj : Any = self
660671 for part in parts :
@@ -673,15 +684,30 @@ def __setitem__(self, key: str, value: Any) -> None:
673684 """Set setting by dot-notation key (e.g., 'database.host')."""
674685 parts = key .split ("." )
675686 if len (parts ) == 1 :
676- if hasattr (self , key ):
687+ if hasattr (self , key ) and not key . startswith ( "_" ) :
677688 setattr (self , key , value )
678689 else :
679- raise KeyError (f"Setting '{ key } ' not found" )
690+ # Store unknown top-level keys in _dynamic
691+ self ._dynamic [key ] = value
680692 else :
681- obj : Any = self
682- for part in parts [:- 1 ]:
683- obj = getattr (obj , part )
684- setattr (obj , parts [- 1 ], value )
693+ # Try to set via typed schema first
694+ try :
695+ obj : Any = self
696+ for part in parts [:- 1 ]:
697+ if hasattr (obj , part ):
698+ obj = getattr (obj , part )
699+ else :
700+ # Fall back to dynamic storage
701+ self ._dynamic [key ] = value
702+ return
703+ if hasattr (obj , parts [- 1 ]):
704+ setattr (obj , parts [- 1 ], value )
705+ else :
706+ # Attribute doesn't exist, store in dynamic
707+ self ._dynamic [key ] = value
708+ except AttributeError :
709+ # Path doesn't exist in typed schema, store in dynamic
710+ self ._dynamic [key ] = value
685711
686712 def get (self , key : str , default : Any = None ) -> Any :
687713 """Get setting with optional default value."""
0 commit comments