3939 ConfigurationError ,
4040 ConfigurationKeyError ,
4141 DuplicatedConfigurationError ,
42+ ImproperAccessError ,
4243)
4344from layered_config_tree .types import InputData
4445
@@ -548,12 +549,26 @@ def _set_with_metadata(
548549 self ._children [name ].update (value , layer , source )
549550
550551 def __setattr__ (self , name : str , value : Any ) -> None :
551- """Set a value on the outermost layer."""
552+ """Set a value on the outermost layer.
553+
554+ Notes
555+ -----
556+ We allow keys that look like dunder attributes, i.e. start and end with
557+ "__". However, to avoid conflict with actual dunder methods and attributes,
558+ we do not allow setting them via this method and instead require dictionary
559+ access (i.e. bracket notation).
560+ """
552561 if name not in self :
553562 raise ConfigurationKeyError (
554563 "New configuration keys can only be created with the update method." ,
555564 self ._name ,
556565 )
566+ if name .startswith ("__" ) and name .endswith ("__" ):
567+ raise ImproperAccessError (
568+ "Cannot set an attribute starting and ending with '__' via attribute "
569+ "access (i.e. dot notation). Use dictionary access instead "
570+ "(i.e. bracket notation)."
571+ )
557572 self ._set_with_metadata (name , value , layer = None , source = None )
558573
559574 def __setitem__ (self , name : str , value : Any ) -> None :
@@ -566,10 +581,35 @@ def __setitem__(self, name: str, value: Any) -> None:
566581 self ._set_with_metadata (name , value , layer = None , source = None )
567582
568583 # FIXME: We expect the return to be a ConfigNode or LayeredConfigTree but
569- # the type checker doesn 't know what you're getting back in chained
570- # attribute calls. We return Any as a workaround.
584+ # static type checkers don 't know what you're getting back in chained
585+ # attribute calls. We type hint returning Any as a workaround.
571586 def __getattr__ (self , name : str ) -> Any :
572- """Get a value from the outermost layer in which it appears."""
587+ """Get a value from the outermost layer in which it appears.
588+
589+ Notes
590+ -----
591+ We allow keys that look like dunder attributes, i.e. start and end with
592+ "__". However, to avoid conflict with actual dunder methods and attributes,
593+ we do not allow getting them via this method and instead require dictionary
594+ access (i.e. bracket notation).
595+
596+ If the requested attribute starts and ends with "__" but *does not* actually
597+ exist, it is critical that we raise an AttributeError since some functions
598+ specifically handle it, e.g. ``pickle`` and ``copy.deepcopy``.
599+ See https://stackoverflow.com/a/50888571/
600+
601+ One the other hand, if the requested attribute starts and ends with "__"
602+ and *does* exist, it is critical that we raise a non-AttributeError exception
603+ so as not to conflict with dunder methods and attributes.
604+ """
605+ if name .startswith ("__" ) and name .endswith ("__" ):
606+ if name not in self :
607+ raise AttributeError # Do not change from AttributeError
608+ raise ImproperAccessError (
609+ "Cannot get an attribute starting and ending with '__' via attribute "
610+ "access (i.e. dot notation). Use dictionary access instead "
611+ "(i.e. bracket notation)."
612+ )
573613 return self .get_from_layer (name )
574614
575615 # We need custom definitions of __getstate__ and __setstate__
0 commit comments