diff --git a/ultraplot/internals/rcsetup.py b/ultraplot/internals/rcsetup.py index 40d52af6b..63f91605b 100644 --- a/ultraplot/internals/rcsetup.py +++ b/ultraplot/internals/rcsetup.py @@ -734,6 +734,14 @@ def copy(self): if not hasattr(RcParams, "validate"): # not mission critical so skip warnings._warn_ultraplot("Failed to update matplotlib rcParams validators.") else: + + def _validator_accepts(validator, value): + try: + validator(value) + return True + except Exception: + return False + _validate = RcParams.validate _validate["image.cmap"] = _validate_cmap("continuous") _validate["legend.loc"] = _validate_belongs(*LEGEND_LOCS) @@ -752,6 +760,20 @@ def copy(self): _validate[_key] = functools.partial(_validate_color, alternative="auto") if _validator is getattr(msetup, "validate_color_or_inherit", None): _validate[_key] = functools.partial(_validate_color, alternative="inherit") + # Matplotlib may wrap fontsize validators in callable objects instead of + # exposing validate_fontsize directly. Detect these by behavior so custom + # shorthands like "med-large" remain valid regardless of import order. + if ( + _key.endswith("size") + and _key not in FONT_KEYS + and _validator_accepts(_validator, "large") + and not _validator_accepts(_validator, "med-large") + ): + FONT_KEYS.add(_key) + if _validator_accepts(_validator, None): + _validate[_key] = _validate_or_none(_validate_fontsize) + else: + _validate[_key] = _validate_fontsize for _keys, _validator_replace in ((EM_KEYS, _validate_em), (PT_KEYS, _validate_pt)): for _key in _keys: _validator = _validate.get(_key, None) diff --git a/ultraplot/tests/test_config.py b/ultraplot/tests/test_config.py index 962b98186..064808850 100644 --- a/ultraplot/tests/test_config.py +++ b/ultraplot/tests/test_config.py @@ -1,4 +1,8 @@ import importlib +import os +import pathlib +import subprocess +import sys import threading from queue import Queue @@ -212,3 +216,45 @@ def _reader(): observed = [results.get() for _ in range(results.qsize())] assert observed, "No rcParams observations were recorded." assert all(value in allowed for value in observed) + + +def _run_in_subprocess(code): + code = ( + "import pathlib\n" + "import sys\n" + "sys.path.insert(0, str(pathlib.Path.cwd()))\n" + code + ) + env = os.environ.copy() + env["MPLBACKEND"] = "Agg" + return subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + cwd=str(pathlib.Path(__file__).resolve().parents[2]), + env=env, + ) + + +def test_matplotlib_import_before_ultraplot_allows_rc_mutation(): + """ + Import order regression test for issue #568. + """ + result = _run_in_subprocess( + "import matplotlib.pyplot as plt\n" + "import ultraplot as uplt\n" + "uplt.rc['figure.facecolor'] = 'white'\n" + ) + assert result.returncode == 0, result.stderr + + +def test_matplotlib_import_before_ultraplot_allows_custom_fontsize_tokens(): + """ + Ensure patched fontsize validators are active regardless of import order. + """ + result = _run_in_subprocess( + "import matplotlib.pyplot as plt\n" + "import ultraplot as uplt\n" + "for key in ('axes.titlesize', 'figure.titlesize', 'legend.fontsize', 'xtick.labelsize'):\n" + " uplt.rc[key] = 'med-large'\n" + ) + assert result.returncode == 0, result.stderr