Skip to content

Make RC configurator thread local #276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 51 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
c98be52
lock on retrieval
cvanelteren Jun 19, 2025
ddde31c
restore lock
cvanelteren Jun 19, 2025
413a57e
add loadscope
cvanelteren Jun 19, 2025
d0e032a
further make configuration threadsafe
cvanelteren Jun 19, 2025
6c9a242
rm unnecessary funcs
cvanelteren Jun 19, 2025
fb71696
reset rc after test
cvanelteren Jun 19, 2025
f06562f
reset before and after explicitly
cvanelteren Jun 19, 2025
edf814b
restore build.yml
cvanelteren Jun 19, 2025
bddf96f
further attempt to fix the settings
cvanelteren Jun 19, 2025
a060684
lock retrieval behind thread local setting
cvanelteren Jun 19, 2025
3028449
Add xdist to image compare (#266)
cvanelteren Jun 18, 2025
4efb834
redo thread safety and add a unittest
cvanelteren Jun 20, 2025
6804b22
actually add the unittest
cvanelteren Jun 20, 2025
27178f5
add thread exception raising
cvanelteren Jun 20, 2025
813604d
correct test values
cvanelteren Jun 20, 2025
05d4f54
refactor unittest
cvanelteren Jun 20, 2025
427367b
more refactoring
cvanelteren Jun 20, 2025
5129f15
save state
cvanelteren Jun 20, 2025
f124b91
save state
cvanelteren Jun 20, 2025
0a9b6aa
Merge branch 'main' into hotfix-thread-safety-config-read
cvanelteren Jun 20, 2025
a1b0e6b
restore conftest.py
cvanelteren Jun 20, 2025
255c16a
restore files
cvanelteren Jun 20, 2025
b39e542
rm more remnants
cvanelteren Jun 20, 2025
7a7d62b
rm more remnants
cvanelteren Jun 20, 2025
b92306c
rm more remnants
cvanelteren Jun 20, 2025
0167532
Merge branch 'main' into hotfix-thread-safety-config-read
beckermr Jun 20, 2025
7b728fe
rm global rc dicts
cvanelteren Jun 20, 2025
53a816d
stash
cvanelteren Jun 21, 2025
186b744
move validation to initializion
cvanelteren Jun 26, 2025
48519b3
clean up results prior to testin
cvanelteren Jun 26, 2025
cac0c87
add more threads
cvanelteren Jun 26, 2025
fa6aa1c
Merge branch 'main' into hotfix-thread-safety-config-read
cvanelteren Jun 26, 2025
8ca56c8
add some rc unittests
cvanelteren Jun 26, 2025
0175f52
test uplt tight layout settings raises warning
cvanelteren Jun 26, 2025
deb4c80
add explicit validator to unittest
cvanelteren Jun 26, 2025
799b62c
add context modfiications on single thread
cvanelteren Jun 26, 2025
ff0f98b
emulate load with sleep
cvanelteren Jun 28, 2025
8d48b50
adjust sleep timing
cvanelteren Jun 28, 2025
41e03e3
improve thread safety test
cvanelteren Jun 28, 2025
7cece90
remove keyerror in matplotlib and update tests to prevent
cvanelteren Jun 28, 2025
97696b3
modify test to warn on rc setting
cvanelteren Jun 28, 2025
41840a8
add another close
cvanelteren Jun 28, 2025
1c463c5
add lock on warnings
cvanelteren Jun 28, 2025
ea6cd6a
add timeout to check for deadlock
cvanelteren Jun 28, 2025
fbe6601
ensure thread safety
cvanelteren Jun 28, 2025
4aa7c92
unify calls
cvanelteren Jun 28, 2025
07677a2
rm lock
cvanelteren Jun 28, 2025
c38d03f
simplify check key
cvanelteren Jun 28, 2025
42d3c1c
rm timeout and reduce workers
cvanelteren Jun 28, 2025
dfca2d3
Merge branch 'main' into hotfix-thread-safety-config-read
cvanelteren Jun 28, 2025
98a5b83
separate plugin source
cvanelteren Jun 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ dependencies:
- pyarrow
- pip:
- git+https://github.com/ultraplot/UltraTheme.git
- git+https://github.com/ultraplot/UltraImageCompare.git
4 changes: 1 addition & 3 deletions ultraplot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,8 @@

# Validate color names now that colors are registered
# NOTE: This updates all settings with 'color' in name (harmless if it's not a color)
from .config import rc_ultraplot, rc_matplotlib

rcsetup.VALIDATE_REGISTERED_COLORS = True
for _src in (rc_ultraplot, rc_matplotlib):
for _src in (rc.rc_ultraplot, rc.rc_matplotlib):
for _key in _src: # loop through unsynced properties
if "color" not in _key:
continue
Expand Down
221 changes: 128 additions & 93 deletions ultraplot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@
__all__ = [
"Configurator",
"rc",
"rc_ultraplot",
"rc_matplotlib",
"use_style",
"config_inline_backend",
"register_cmaps",
Expand Down Expand Up @@ -375,7 +373,7 @@
return kw_ultraplot


def config_inline_backend(fmt=None):
def config_inline_backend(fmt=None, rc_ultraplot=None):
"""
Set up the ipython `inline backend display format \
<https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-matplotlib>`__
Expand Down Expand Up @@ -735,19 +733,104 @@
on import. See the :ref:`user guide <ug_config>` for details.
"""

@docstring._snippet_manager
def __init__(self, local=True, user=True, default=True, **kwargs):
"""
Parameters
----------
%(rc.params)s
"""
import threading

# Initialize threading first to avoid recursion issues
super().__setattr__("_thread_local", threading.local())
super().__setattr__("_initialized", False)
self._init(local=local, user=user, default=default, **kwargs)
super().__setattr__("_initialized", True)

def _init(self, *, local, user, default, skip_cycle=False):
"""
Initialize the configurator.
Note: this is also used to reset the class.
"""
# Always remove context objects - use direct access to avoid recursion
if hasattr(self, "_thread_local"):
context = self._get_thread_local_copy("_context", [])
context.clear()

# Update from default settings
# NOTE: see _remove_blacklisted_style_params bugfix
if default:
self.rc_matplotlib.update(_get_style_dict("original", filter=False))
self.rc_matplotlib.update(rcsetup._rc_matplotlib_default)
self.rc_ultraplot.update(rcsetup._rc_ultraplot_default)
for key, value in self.rc_ultraplot.items():
kw_ultraplot, kw_matplotlib = self._get_item_dicts(
key, value, skip_cycle=skip_cycle
)
self.rc_matplotlib.update(kw_matplotlib)
self.rc_ultraplot.update(kw_ultraplot)
self.rc_matplotlib["backend"] = mpl.get_backend()

# Update from user home
user_path = None
if user:
user_path = self.user_file()
if os.path.isfile(user_path):
self.load(user_path)

# Update from local paths
if local:
local_paths = self.local_files()
for path in local_paths:
if path == user_path: # local files always have precedence
continue
self.load(path)

Check warning on line 788 in ultraplot/config.py

View check run for this annotation

Codecov / codecov/patch

ultraplot/config.py#L787-L788

Added lines #L787 - L788 were not covered by tests

@property
def _context(self):
return self._get_thread_local_copy("_context", [])

@_context.setter
def _context(self, value):
if isinstance(value, list):
self._thread_local._context = value

Check warning on line 797 in ultraplot/config.py

View check run for this annotation

Codecov / codecov/patch

ultraplot/config.py#L797

Added line #L797 was not covered by tests

def _get_thread_local_copy(self, attr, source):
if not hasattr(self._thread_local, attr):
# Initialize with a copy of the source dictionary
setattr(self._thread_local, attr, source.copy())
return getattr(self._thread_local, attr)

@property
def rc_matplotlib(self):
return self._get_thread_local_copy("rc_matplotlib", mpl.rcParams)

@property
def rc_ultraplot(self):
return self._get_thread_local_copy(
"rc_ultraplot", rcsetup._rc_ultraplot_default
)

def __repr__(self):
cls = type("rc", (dict,), {}) # temporary class with short name
src = cls({key: val for key, val in rc_ultraplot.items() if "." not in key})
return type(rc_matplotlib).__repr__(src).strip()[:-1] + ",\n ...\n })"
src = cls(

Check warning on line 817 in ultraplot/config.py

View check run for this annotation

Codecov / codecov/patch

ultraplot/config.py#L817

Added line #L817 was not covered by tests
{key: val for key, val in self.rc_ultraplot.items() if "." not in key}
)
return (

Check warning on line 820 in ultraplot/config.py

View check run for this annotation

Codecov / codecov/patch

ultraplot/config.py#L820

Added line #L820 was not covered by tests
type(self.rc_matplotlib).__repr__(src).strip()[:-1] + ",\n ...\n })"
)

def __str__(self):
cls = type("rc", (dict,), {}) # temporary class with short name
src = cls({key: val for key, val in rc_ultraplot.items() if "." not in key})
return type(rc_matplotlib).__str__(src) + "\n..."
src = cls(

Check warning on line 826 in ultraplot/config.py

View check run for this annotation

Codecov / codecov/patch

ultraplot/config.py#L826

Added line #L826 was not covered by tests
{key: val for key, val in self.rc_ultraplot.items() if "." not in key}
)
return type(self.rc_matplotlib).__str__(src) + "\n..."

Check warning on line 829 in ultraplot/config.py

View check run for this annotation

Codecov / codecov/patch

ultraplot/config.py#L829

Added line #L829 was not covered by tests

def __iter__(self):
yield from rc_ultraplot # sorted ultraplot settings, ignoring deprecations
yield from rc_matplotlib # sorted matplotlib settings, ignoring deprecations
yield from self.rc_ultraplot # sorted ultraplot settings, ignoring deprecations
yield from self.rc_matplotlib # sorted matplotlib settings, ignoring deprecations

def __len__(self):
return len(tuple(iter(self)))
Expand All @@ -758,36 +841,29 @@
def __delattr__(self, attr): # noqa: U100
raise RuntimeError("rc settings cannot be deleted.")

@docstring._snippet_manager
def __init__(self, local=True, user=True, default=True, **kwargs):
"""
Parameters
----------
%(rc.params)s
"""
self._context = []
self._init(local=local, user=user, default=default, **kwargs)

def __getitem__(self, key):
"""
Return an `rc_matplotlib` or `rc_ultraplot` setting using dictionary notation
(e.g., ``value = uplt.rc[name]``).
"""
key, _ = self._validate_key(key) # might issue ultraplot removed/renamed error
try:
return rc_ultraplot[key]
return self.rc_ultraplot[key]
except KeyError:
pass
return rc_matplotlib[key] # might issue matplotlib removed/renamed error
return self.rc_matplotlib[key] # might issue matplotlib removed/renamed error

def __setitem__(self, key, value):
"""
Modify an `rc_matplotlib` or `rc_ultraplot` setting using dictionary notation
(e.g., ``uplt.rc[name] = value``).
"""
key, value = self._validate_key(
key, value
) # might issue ultraplot removed/renamed error
kw_ultraplot, kw_matplotlib = self._get_item_dicts(key, value)
rc_ultraplot.update(kw_ultraplot)
rc_matplotlib.update(kw_matplotlib)
self.rc_ultraplot.update(kw_ultraplot)
self.rc_matplotlib.update(kw_matplotlib)

def __getattr__(self, attr):
"""
Expand All @@ -804,10 +880,14 @@
Modify an `rc_matplotlib` or `rc_ultraplot` setting using "dot" notation
(e.g., ``uplt.rc.name = value``).
"""
if attr[:1] == "_":
if attr[:1] == "_" or attr in ("_thread_local", "_initialized"):
super().__setattr__(attr, value)
else:
self.__setitem__(attr, value)
# Check if we're initialized to avoid recursion during __init__
if not getattr(self, "_initialized", False):
super().__setattr__(attr, value)

Check warning on line 888 in ultraplot/config.py

View check run for this annotation

Codecov / codecov/patch

ultraplot/config.py#L888

Added line #L888 was not covered by tests
else:
self.__setitem__(attr, value)

Check warning on line 890 in ultraplot/config.py

View check run for this annotation

Codecov / codecov/patch

ultraplot/config.py#L890

Added line #L890 was not covered by tests

def __enter__(self):
"""
Expand All @@ -829,7 +909,7 @@
raise e

for rc_dict, kw_new in zip(
(rc_ultraplot, rc_matplotlib),
(self.rc_ultraplot, self.rc_matplotlib),
(kw_ultraplot, kw_matplotlib),
):
for key, value in kw_new.items():
Expand All @@ -847,47 +927,11 @@
context = self._context[-1]
for key, value in context.rc_old.items():
kw_ultraplot, kw_matplotlib = self._get_item_dicts(key, value)
rc_ultraplot.update(kw_ultraplot)
rc_matplotlib.update(kw_matplotlib)
self.rc_ultraplot.update(kw_ultraplot)
self.rc_matplotlib.update(kw_matplotlib)
del self._context[-1]

def _init(self, *, local, user, default, skip_cycle=False):
"""
Initialize the configurator.
"""
# Always remove context objects
self._context.clear()

# Update from default settings
# NOTE: see _remove_blacklisted_style_params bugfix
if default:
rc_matplotlib.update(_get_style_dict("original", filter=False))
rc_matplotlib.update(rcsetup._rc_matplotlib_default)
rc_ultraplot.update(rcsetup._rc_ultraplot_default)
for key, value in rc_ultraplot.items():
kw_ultraplot, kw_matplotlib = self._get_item_dicts(
key, value, skip_cycle=skip_cycle
)
rc_matplotlib.update(kw_matplotlib)
rc_ultraplot.update(kw_ultraplot)

# Update from user home
user_path = None
if user:
user_path = self.user_file()
if os.path.isfile(user_path):
self.load(user_path)

# Update from local paths
if local:
local_paths = self.local_files()
for path in local_paths:
if path == user_path: # local files always have precedence
continue
self.load(path)

@staticmethod
def _validate_key(key, value=None):
def _validate_key(self, key, value=None):
"""
Validate setting names and handle `rc_ultraplot` deprecations.
"""
Expand All @@ -899,13 +943,11 @@
key = key.lower()
if "." not in key:
key = rcsetup._rc_nodots.get(key, key)
key, value = rc_ultraplot._check_key(
key, value
) # may issue deprecation warning
# Use the raw thread-local copy of rc_ultraplot instead of the property getter
key, value = self.rc_ultraplot._check_key(key, value)
return key, value

@staticmethod
def _validate_value(key, value):
def _validate_value(self, key, value):
"""
Validate setting values and convert numpy ndarray to list if possible.
"""
Expand All @@ -917,11 +959,11 @@
# are being read rather than after the end of the file reading.
if isinstance(value, np.ndarray):
value = value.item() if value.size == 1 else value.tolist()
validate_matplotlib = getattr(rc_matplotlib, "validate", None)
validate_ultraplot = rc_ultraplot._validate
validate_matplotlib = getattr(self.rc_matplotlib, "validate", None)
validate_ultraplot = getattr(self.rc_ultraplot, "_validate", None)
if validate_matplotlib is not None and key in validate_matplotlib:
value = validate_matplotlib[key](value)
elif key in validate_ultraplot:
elif validate_ultraplot is not None and key in validate_ultraplot:
value = validate_ultraplot[key](value)
return value

Expand All @@ -935,9 +977,9 @@
mode = self._context_mode
cache = tuple(context.rc_new for context in self._context)
if mode == 0:
rcdicts = (*cache, rc_ultraplot, rc_matplotlib)
rcdicts = (*cache, self.rc_ultraplot, self.rc_matplotlib)
elif mode == 1:
rcdicts = (*cache, rc_ultraplot) # added settings only!
rcdicts = (*cache, self.rc_ultraplot) # added settings only!
elif mode == 2:
rcdicts = (*cache,)
else:
Expand Down Expand Up @@ -973,16 +1015,16 @@
warnings.simplefilter("ignore", mpl.MatplotlibDeprecationWarning)
warnings.simplefilter("ignore", warnings.UltraPlotWarning)
for key in keys:
if key in rc_matplotlib:
if key in self.rc_matplotlib:
kw_matplotlib[key] = value
elif key in rc_ultraplot:
elif key in self.rc_ultraplot:
kw_ultraplot[key] = value
else:
raise KeyError(f"Invalid rc setting {key!r}.")

# Special key: configure inline backend
if contains("inlineformat"):
config_inline_backend(value)
config_inline_backend(value, self.rc_ultraplot)

# Special key: apply stylesheet
elif contains("style"):
Expand Down Expand Up @@ -1013,14 +1055,14 @@
kw_ultraplot.update(
{
key: value
for key, value in rc_ultraplot.items()
for key, value in self.rc_ultraplot.items()
if key in rcsetup.FONT_KEYS and value in mfonts.font_scalings
}
)
kw_matplotlib.update(
{
key: value
for key, value in rc_matplotlib.items()
for key, value in self.rc_matplotlib.items()
if key in rcsetup.FONT_KEYS and value in mfonts.font_scalings
}
)
Expand All @@ -1029,9 +1071,9 @@
elif contains("tick.len", "tick.lenratio"):
if contains("tick.len"):
ticklen = value
ratio = rc_ultraplot["tick.lenratio"]
ratio = self.rc_ultraplot["tick.lenratio"]
else:
ticklen = rc_ultraplot["tick.len"]
ticklen = self.rc_ultraplot["tick.len"]
ratio = value
kw_matplotlib["xtick.minor.size"] = ticklen * ratio
kw_matplotlib["ytick.minor.size"] = ticklen * ratio
Expand All @@ -1040,9 +1082,9 @@
elif contains("tick.width", "tick.widthratio"):
if contains("tick.width"):
tickwidth = value
ratio = rc_ultraplot["tick.widthratio"]
ratio = self.rc_ultraplot["tick.widthratio"]
else:
tickwidth = rc_ultraplot["tick.width"]
tickwidth = self.rc_ultraplot["tick.width"]
ratio = value
kw_matplotlib["xtick.minor.width"] = tickwidth * ratio
kw_matplotlib["ytick.minor.width"] = tickwidth * ratio
Expand All @@ -1051,9 +1093,9 @@
elif contains("grid.width", "grid.widthratio"):
if contains("grid.width"):
gridwidth = value
ratio = rc_ultraplot["grid.widthratio"]
ratio = self.rc_ultraplot["grid.widthratio"]
else:
gridwidth = rc_ultraplot["grid.width"]
gridwidth = self.rc_ultraplot["grid.width"]
ratio = value
kw_ultraplot["gridminor.linewidth"] = gridwidth * ratio
kw_ultraplot["gridminor.width"] = gridwidth * ratio
Expand Down Expand Up @@ -1812,13 +1854,6 @@
_init_user_folders()
_init_user_file()

#: A dictionary-like container of matplotlib settings. Assignments are
#: validated and restricted to recognized setting names.
rc_matplotlib = mpl.rcParams # PEP8 4 lyfe

#: A dictionary-like container of ultraplot settings. Assignments are
#: validated and restricted to recognized setting names.
rc_ultraplot = rcsetup._rc_ultraplot_default.copy() # a validated rcParams-style dict

#: Instance of `Configurator`. This controls both `rc_matplotlib` and `rc_ultraplot`
#: settings. See the :ref:`configuration guide <ug_config>` for details.
Expand Down
Loading
Loading