Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
83 changes: 83 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ stored in :obj:`~ultraplot.config.rc_matplotlib`
and :ref:`ultraplot settings <ug_rcUltraPlot>`
stored in :obj:`~ultraplot.config.rc_ultraplot`.

Both :obj:`~ultraplot.config.rc_matplotlib` and :obj:`~ultraplot.config.rc_ultraplot`
are **thread-safe** and support **thread-local isolation** via context managers.
See :ref:`thread-safety and context managers <ug_rcthreadsafe>` for details.

To change global settings on-the-fly, simply update :obj:`~ultraplot.config.rc`
using either dot notation or as you would any other dictionary:

Expand Down Expand Up @@ -51,6 +55,85 @@ to the :func:`~ultraplot.config.Configurator.context` command:
with uplt.rc.context({'name1': value1, 'name2': value2}):
fig, ax = uplt.subplots()

See :ref:`thread-safety and context managers <ug_rcthreadsafe>` for important
information about thread-local isolation and parallel testing.

.. _ug_rcthreadsafe:

Thread-safety and context managers
-----------------------------------

Both :obj:`~ultraplot.config.rc_matplotlib` and :obj:`~ultraplot.config.rc_ultraplot`
are **thread-safe** and support **thread-local isolation** through context managers.
This is particularly useful for parallel testing or multi-threaded applications.

**Global changes** (outside context managers) are persistent and visible to all threads:

.. code-block:: python

import ultraplot as uplt

# Global change - persists and affects all threads
uplt.rc['font.size'] = 12
uplt.rc_matplotlib['axes.grid'] = True

**Thread-local changes** (inside context managers) are isolated and temporary:

.. code-block:: python

import ultraplot as uplt

original_size = uplt.rc['font.size'] # e.g., 10

with uplt.rc_matplotlib:
# This change is ONLY visible in the current thread
uplt.rc_matplotlib['font.size'] = 20
print(uplt.rc_matplotlib['font.size']) # 20

# After exiting context, change is discarded
print(uplt.rc_matplotlib['font.size']) # 10 (back to original)

This is especially useful for **parallel test execution**, where each test thread
can modify settings without affecting other threads or the main thread:

.. code-block:: python

import threading
import ultraplot as uplt

def test_worker(thread_id):
"""Each thread can have isolated settings."""
with uplt.rc_matplotlib:
# Thread-specific settings
uplt.rc_matplotlib['font.size'] = 10 + thread_id * 2
uplt.rc['axes.grid'] = True

# Create plots, run tests, etc.
fig, ax = uplt.subplots()
# ...

# Settings automatically restored after context exit

# Run tests in parallel - no interference between threads
threads = [threading.Thread(target=test_worker, args=(i,)) for i in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()

**Key points:**

* Changes **outside** a context manager are **global and persistent**
* Changes **inside** a context manager (``with rc:`` or ``with rc_matplotlib:``) are **thread-local and temporary**
* Thread-local changes are automatically discarded when the context exits
* Each thread sees its own isolated copy of settings within a context
* This works for both :obj:`~ultraplot.config.rc`, :obj:`~ultraplot.config.rc_matplotlib`, and :obj:`~ultraplot.config.rc_ultraplot`

.. note::

A complete working example demonstrating thread-safe configuration usage
can be found in ``docs/thread_safety_example.py``.


In all of these examples, if the setting name contains dots,
you can simply omit the dots. For example, to change the
Expand Down
148 changes: 148 additions & 0 deletions docs/thread_safety_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
Thread-Safe Configuration
==========================

This example demonstrates the thread-safe behavior of UltraPlot's
configuration system, showing how settings can be isolated per-thread
using context managers.
"""

import threading
import time

import ultraplot as uplt

# %%
# Global vs Thread-Local Changes
# -------------------------------
# Changes outside a context manager are global and persistent.
# Changes inside a context manager are thread-local and temporary.

# Store original font size
original_size = uplt.rc["font.size"]
print(f"Original font size: {original_size}")

# Global change (persistent)
uplt.rc["font.size"] = 12
print(f"After global change: {uplt.rc['font.size']}")

# Thread-local change (temporary)
with uplt.rc_matplotlib:
uplt.rc_matplotlib["font.size"] = 20
print(f"Inside context: {uplt.rc_matplotlib['font.size']}")

# After context, reverts to previous value
print(f"After context: {uplt.rc_matplotlib['font.size']}")

# Restore original
uplt.rc["font.size"] = original_size

# %%
# Parallel Thread Testing
# ------------------------
# Each thread can have its own isolated settings when using context managers.


def create_plot_in_thread(thread_id, results):
"""Create a plot with thread-specific settings."""
with uplt.rc_matplotlib:
# Each thread uses different settings
thread_font_size = 8 + thread_id * 2
uplt.rc_matplotlib["font.size"] = thread_font_size
uplt.rc["axes.grid"] = thread_id % 2 == 0 # Grid on/off alternating

# Verify settings are isolated
actual_size = uplt.rc_matplotlib["font.size"]
results[thread_id] = {
"expected": thread_font_size,
"actual": actual_size,
"isolated": (actual_size == thread_font_size),
}

# Small delay to increase chance of interference if not thread-safe
time.sleep(0.1)

# Create a simple plot
fig, ax = uplt.subplots(figsize=(3, 2))
ax.plot([1, 2, 3], [1, 2, 3])
ax.format(
title=f"Thread {thread_id}",
xlabel="x",
ylabel="y",
)
uplt.close(fig) # Clean up

# After context, settings are restored
print(f"Thread {thread_id}: Settings isolated = {results[thread_id]['isolated']}")


# Run threads in parallel
results = {}
threads = [
threading.Thread(target=create_plot_in_thread, args=(i, results)) for i in range(5)
]

print("\nRunning parallel threads with isolated settings...")
for t in threads:
t.start()
for t in threads:
t.join()

# Verify all threads had isolated settings
all_isolated = all(r["isolated"] for r in results.values())
print(f"\nAll threads had isolated settings: {all_isolated}")

# %%
# Use Case: Parallel Testing
# ---------------------------
# This is particularly useful for running tests in parallel where each
# test needs different matplotlib/ultraplot settings.


def run_test_with_settings(test_id, settings):
"""Run a test with specific settings."""
with uplt.rc_matplotlib:
# Apply test-specific settings
uplt.rc.update(settings)

# Run test code
fig, axs = uplt.subplots(ncols=2, figsize=(6, 2))
axs[0].plot([1, 2, 3], [1, 4, 2])
axs[1].scatter([1, 2, 3], [2, 1, 3])
axs.format(suptitle=f"Test {test_id}")

# Verify settings
print(f"Test {test_id}: font.size = {uplt.rc['font.size']}")

uplt.close(fig) # Clean up


# Different tests with different settings
test_settings = [
{"font.size": 10, "axes.grid": True},
{"font.size": 14, "axes.grid": False},
{"font.size": 12, "axes.titleweight": "bold"},
]

print("\nRunning parallel tests with different settings...")
test_threads = [
threading.Thread(target=run_test_with_settings, args=(i, settings))
for i, settings in enumerate(test_settings)
]

for t in test_threads:
t.start()
for t in test_threads:
t.join()

print("\nAll tests completed without interference!")

# %%
# Important Notes
# ---------------
# 1. Changes outside context managers are global and affect all threads
# 2. Changes inside context managers are thread-local and temporary
# 3. Context managers automatically clean up when exiting
# 4. This works for rc, rc_matplotlib, and rc_ultraplot
# 5. Perfect for parallel test execution and multi-threaded applications
Loading
Loading