@@ -548,12 +548,26 @@ def _set_with_metadata(
548548 self ._children [name ].update (value , layer , source )
549549
550550 def __setattr__ (self , name : str , value : Any ) -> None :
551- """Set a value on the outermost layer."""
551+ """Set a value on the outermost layer.
552+
553+ Notes
554+ -----
555+ We allow keys that look like dunder attributes, i.e. start and end with
556+ "__". However, to avoid conflict with actual dunder methods and attributes,
557+ we do not allow setting them via this method and instead require dictionary
558+ access (i.e. bracket notation).
559+ """
552560 if name not in self :
553561 raise ConfigurationKeyError (
554562 "New configuration keys can only be created with the update method." ,
555563 self ._name ,
556564 )
565+ if name .startswith ("__" ) and name .endswith ("__" ):
566+ raise RuntimeError (
567+ "Cannot set an attribute starting and ending with '__' via attribute "
568+ "access (i.e. dot notation). Use dicationary access instead "
569+ "(i.e. bracket notation)."
570+ )
557571 self ._set_with_metadata (name , value , layer = None , source = None )
558572
559573 def __setitem__ (self , name : str , value : Any ) -> None :
@@ -566,10 +580,35 @@ def __setitem__(self, name: str, value: Any) -> None:
566580 self ._set_with_metadata (name , value , layer = None , source = None )
567581
568582 # 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.
583+ # static type checkers don 't know what you're getting back in chained
584+ # attribute calls. We type hint returning Any as a workaround.
571585 def __getattr__ (self , name : str ) -> Any :
572- """Get a value from the outermost layer in which it appears."""
586+ """Get a value from the outermost layer in which it appears.
587+
588+ Notes
589+ -----
590+ We allow keys that look like dunder attributes, i.e. start and end with
591+ "__". However, to avoid conflict with actual dunder methods and attributes,
592+ we do not allow getting them via this method and instead require dictionary
593+ access (i.e. bracket notation).
594+
595+ If the requested attribute starts and ends with "__" but *does not* actually
596+ exist, it is critical that we raise an AttributeError since some functions
597+ specifically handle it, e.g. ``pickle`` and ``copy.deepcopy``.
598+ See https://stackoverflow.com/a/50888571/
599+
600+ One the other hand, if the requested attribute starts and ends with "__"
601+ and *does* exist, it is critical that we raise a non-AttributeError exception
602+ so as not to conflict with dunder methods and attributes.
603+ """
604+ if name .startswith ("__" ) and name .endswith ("__" ):
605+ if name not in self :
606+ raise AttributeError # Do not change from AttributeError
607+ raise RuntimeError (
608+ "Cannot get an attribute starting and ending with '__' via attribute "
609+ "access (i.e. dot notation). Use dictionary access instead "
610+ "(i.e. bracket notation)."
611+ )
573612 return self .get_from_layer (name )
574613
575614 # We need custom definitions of __getstate__ and __setstate__
0 commit comments