44"""
55import functools
66import re , matplotlib as mpl
7+ import threading
78from collections .abc import MutableMapping
89from numbers import Integral , Real
910
@@ -562,16 +563,42 @@ def _yaml_table(rcdict, comment=True, description=False):
562563
563564class _RcParams (MutableMapping , dict ):
564565 """
565- A simple dictionary with locked inputs and validated assignments.
566+ A thread-safe dictionary with validated assignments and thread-local storage used to store the configuration of UltraPlot.
567+
568+ It uses reentrant locks (RLock) to ensure that multiple threads can safely read and write to the configuration without causing data corruption.
569+
570+ Example
571+ -------
572+ >>> with rc_params:
573+ ... rc_params['key'] = 'value' # Thread-local change
574+ ... # Changes are automatically cleaned up when exiting the context
566575 """
567576
568577 # NOTE: By omitting __delitem__ in MutableMapping we effectively
569578 # disable mutability. Also disables deleting items with pop().
570579 def __init__ (self , source , validate ):
571580 self ._validate = validate
581+ self ._lock = threading .RLock ()
582+ self ._local = threading .local ()
583+ self ._local .changes = {} # Initialize thread-local storage
584+ # Register all initial keys in the validation dictionary
585+ for key in source :
586+ if key not in validate :
587+ validate [key ] = lambda x : x # Default validator
572588 for key , value in source .items ():
573589 self .__setitem__ (key , value ) # trigger validation
574590
591+ def __enter__ (self ):
592+ """Context manager entry - initialize thread-local storage if needed."""
593+ if not hasattr (self ._local , "changes" ):
594+ self ._local .changes = {}
595+ return self
596+
597+ def __exit__ (self , exc_type , exc_val , exc_tb ):
598+ """Context manager exit - clean up thread-local storage."""
599+ if hasattr (self ._local , "changes" ):
600+ del self ._local .changes
601+
575602 def __repr__ (self ):
576603 return RcParams .__repr__ (self )
577604
@@ -587,22 +614,33 @@ def __iter__(self):
587614 yield from sorted (dict .__iter__ (self ))
588615
589616 def __getitem__ (self , key ):
590- key , _ = self ._check_key (key )
591- return dict .__getitem__ (self , key )
617+ with self ._lock :
618+ key , _ = self ._check_key (key )
619+ # Check thread-local storage first
620+ if key in self ._local .changes :
621+ return self ._local .changes [key ]
622+ # Check global dictionary (will raise KeyError if not found)
623+ return dict .__getitem__ (self , key )
592624
593625 def __setitem__ (self , key , value ):
594- key , value = self ._check_key (key , value )
595- if key not in self ._validate :
596- raise KeyError (f"Invalid rc key { key !r} ." )
597- try :
598- value = self ._validate [key ](value )
599- except (ValueError , TypeError ) as error :
600- raise ValueError (f"Key { key } : { error } " ) from None
601- if key is not None :
602- dict .__setitem__ (self , key , value )
603-
604- @staticmethod
605- def _check_key (key , value = None ):
626+ with self ._lock :
627+ key , value = self ._check_key (key , value )
628+ # Validate the value
629+ try :
630+ value = self ._validate [key ](value )
631+ except KeyError :
632+ # If key doesn't exist in validation, add it with default validator
633+ self ._validate [key ] = lambda x : x
634+ # Re-validate with new validator
635+ value = self ._validate [key ](value )
636+ except (ValueError , TypeError ) as error :
637+ raise ValueError (f"Key { key } : { error } " ) from None
638+ if key is not None :
639+ # Store in both thread-local storage and main dictionary
640+ self ._local .changes [key ] = value
641+ dict .__setitem__ (self , key , value )
642+
643+ def _check_key (self , key , value = None ):
606644 # NOTE: If we assigned from the Configurator then the deprecated key will
607645 # still propagate to the same 'children' as the new key.
608646 # NOTE: This also translates values for special cases of renamed keys.
@@ -624,10 +662,21 @@ def _check_key(key, value=None):
624662 f"The rc setting { key !r} was removed in version { version } ."
625663 + (info and " " + info )
626664 )
665+ # Register new keys in the validation dictionary
666+ if key not in self ._validate :
667+ self ._validate [key ] = lambda x : x # Default validator
627668 return key , value
628669
629670 def copy (self ):
630- source = {key : dict .__getitem__ (self , key ) for key in self }
671+ with self ._lock :
672+ # Create a copy that includes both global and thread-local changes
673+ source = {}
674+ # Start with global values
675+ for key in self :
676+ if key not in self ._local .changes :
677+ source [key ] = dict .__getitem__ (self , key )
678+ # Add thread-local changes
679+ source .update (self ._local .changes )
631680 return _RcParams (source , self ._validate )
632681
633682
0 commit comments