From a46089235e609751b4d8fbdaab49ba75ce96097a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 27 Oct 2025 17:37:43 +0100 Subject: [PATCH 1/9] make rc_ultraplot and rc_matplotlib internal --- ultraplot/__init__.py | 13 +- ultraplot/config.py | 348 ++++++++++++++++++++++-------------------- ultraplot/figure.py | 12 +- 3 files changed, 189 insertions(+), 184 deletions(-) diff --git a/ultraplot/__init__.py b/ultraplot/__init__.py index 2a2db3bd1..1624a003f 100644 --- a/ultraplot/__init__.py +++ b/ultraplot/__init__.py @@ -98,18 +98,9 @@ # 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 _key in _src: # loop through unsynced properties - if "color" not in _key: - continue - try: - _src[_key] = _src[_key] - except ValueError as err: - warnings._warn_ultraplot(f"Invalid user rc file setting: {err}") - _src[_key] = "black" # fill value +rc.sync() # triggers validation + from .colors import _cmap_database as colormaps from .utils import check_for_update diff --git a/ultraplot/config.py b/ultraplot/config.py index 388285bcc..8836ac8bc 100644 --- a/ultraplot/config.py +++ b/ultraplot/config.py @@ -56,16 +56,11 @@ def get_ipython(): __all__ = [ "Configurator", "rc", - "rc_ultraplot", - "rc_matplotlib", "use_style", - "config_inline_backend", "register_cmaps", "register_cycles", "register_colors", "register_fonts", - "RcConfigurator", # deprecated - "inline_backend_fmt", # deprecated ] # Constants @@ -405,64 +400,6 @@ def _infer_ultraplot_dict(kw_params): return kw_ultraplot -def config_inline_backend(fmt=None): - """ - Set up the ipython `inline backend display format \ -`__ - and ensure that inline figures always look the same as saved figures. - This runs the following ipython magic commands: - - .. code-block:: ipython - - %config InlineBackend.figure_formats = rc['inlineformat'] - %config InlineBackend.rc = {} # never override rc settings - %config InlineBackend.close_figures = True # cells start with no active figures - %config InlineBackend.print_figure_kwargs = {'bbox_inches': None} - - When the inline backend is inactive or unavailable, this has no effect. - This function is called when you modify the :rcraw:`inlineformat` property. - - Parameters - ---------- - fmt : str or sequence, default: :rc:`inlineformat` - The inline backend file format or a list thereof. Valid formats - include ``'jpg'``, ``'png'``, ``'svg'``, ``'pdf'``, and ``'retina'``. - - See also - -------- - Configurator - """ - # Note if inline backend is unavailable this will fail silently - ipython = get_ipython() - if ipython is None: - return - fmt = _not_none(fmt, rc_ultraplot["inlineformat"]) - if isinstance(fmt, str): - fmt = [fmt] - elif np.iterable(fmt): - fmt = list(fmt) - else: - raise ValueError(f"Invalid inline backend format {fmt!r}. Must be string.") - - # IPython < 9.0.0 - if hasattr(ipython, "magic"): - ipython.magic("config InlineBackend.figure_formats = " + repr(fmt)) - ipython.magic("config InlineBackend.rc = {}") - ipython.magic("config InlineBackend.close_figures = True") - ipython.magic( - "config InlineBackend.print_figure_kwargs = {'bbox_inches': None}" - ) - - # IPython >= 9.0.0 - else: - ipython.run_line_magic("config", "InlineBackend.figure_formats = " + repr(fmt)) - ipython.run_line_magic("config", "InlineBackend.rc = {}") - ipython.run_line_magic("config", "InlineBackend.close_figures = True") - ipython.run_line_magic( - "config", "InlineBackend.print_figure_kwargs = {'bbox_inches': None}" - ) - - def use_style(style): """ Apply the `matplotlib style(s) \ @@ -487,8 +424,8 @@ def use_style(style): # so much *more* than changing rc params or quick settings, so it is # nice to have dedicated function instead of just another rc_param name. kw_matplotlib = _get_style_dict(style) - rc_matplotlib.update(kw_matplotlib) - rc_ultraplot.update(_infer_ultraplot_dict(kw_matplotlib)) + rc.rc_matplotlib.update(kw_matplotlib) + rc.rc_ultraplot.update(_infer_ultraplot_dict(kw_matplotlib)) @docstring._snippet_manager @@ -763,31 +700,16 @@ class Configurator(MutableMapping, dict): on import. See the :ref:`user guide ` for details. """ - 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 })" - - 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..." - - def __iter__(self): - yield from rc_ultraplot # sorted ultraplot settings, ignoring deprecations - yield from rc_matplotlib # sorted matplotlib settings, ignoring deprecations - - def __len__(self): - return len(tuple(iter(self))) - - def __delitem__(self, key): # noqa: U100 - raise RuntimeError("rc settings cannot be deleted.") - - 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): + def __init__( + self, + rc_ultraplot: dict, + rc_matplotlib: dict, + local=True, + user=True, + default=True, + **kwargs, + ): """ Parameters ---------- @@ -795,8 +717,44 @@ def __init__(self, local=True, user=True, default=True, **kwargs): """ self._context = [] self._setting_handlers = {} + self.rc_ultraplot = rc_ultraplot + self.rc_matplotlib = rc_matplotlib self._init(local=local, user=user, default=default, **kwargs) + def _init(self, *, local, user, default): + """ + Initialize the configurator. + """ + # Always remove context objects + self._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) + self.rc_matplotlib.update(kw_matplotlib) + self.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) + self.sync() + def register_handler( self, name: str, func: Callable[[Any], Dict[str, Any]] ) -> None: @@ -812,10 +770,10 @@ def __getitem__(self, key): """ 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): """ @@ -823,15 +781,15 @@ def __setitem__(self, key, value): (e.g., ``uplt.rc[name] = value``). """ 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): """ Return an `rc_matplotlib` or `rc_ultraplot` setting using "dot" notation (e.g., ``value = uplt.rc.name``). """ - if attr[:1] == "_": + if attr[:1] == "_" or attr in ("rc_ultraplot", "rc_matplotlib"): return super().__getattribute__(attr) # raise built-in error else: return self.__getitem__(attr) @@ -841,11 +799,40 @@ def __setattr__(self, attr, value): 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 ("rc_ultraplot", "rc_matplotlib"): super().__setattr__(attr, value) else: self.__setitem__(attr, value) + def __repr__(self): + cls = type("rc", (dict,), {}) # temporary class with short name + src = cls( + {key: val for key, val in self.rc_ultraplot.items() if "." not in key} + ) + return ( + 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 self.rc_ultraplot.items() if "." not in key} + ) + return type(self.rc_matplotlib).__str__(src) + "\n..." + + def __iter__(self): + 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))) + + def __delitem__(self, key): # noqa: U100 + raise RuntimeError("rc settings cannot be deleted.") + + def __delattr__(self, attr): # noqa: U100 + raise RuntimeError("rc settings cannot be deleted.") + def __enter__(self): """ Apply settings from the most recent context block. @@ -866,7 +853,7 @@ def __enter__(self): 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(): @@ -884,45 +871,11 @@ def __exit__(self, *args): # noqa: U100 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): - """ - 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) - 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. """ @@ -934,13 +887,13 @@ def _validate_key(key, value=None): key = key.lower() if "." not in key: key = rcsetup._rc_nodots.get(key, key) - key, value = rc_ultraplot._check_key( + key, value = self.rc_ultraplot._check_key( key, value ) # may issue deprecation warning + 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. """ @@ -952,8 +905,8 @@ def _validate_value(key, value): # 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 = self.rc_ultraplot._validate if validate_matplotlib is not None and key in validate_matplotlib: value = validate_matplotlib[key](value) elif key in validate_ultraplot: @@ -970,9 +923,9 @@ def _get_item_context(self, key, mode=None): 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: @@ -1008,16 +961,16 @@ def _get_item_dicts(self, key, value): warnings.simplefilter("ignore", mpl.MatplotlibDeprecationWarning) warnings.simplefilter("ignore", warnings.UltraPlotWarning) for key_i in keys: - if key_i in rc_matplotlib: + if key_i in self.rc_matplotlib: kw_matplotlib[key_i] = value - elif key_i in rc_ultraplot: + elif key_i in self.rc_ultraplot: kw_ultraplot[key_i] = value else: raise KeyError(f"Invalid rc setting {key_i!r}.") # Special key: configure inline backend if contains("inlineformat"): - config_inline_backend(value) + self.config_inline_backend(value) # Special key: apply stylesheet elif contains("style"): @@ -1043,14 +996,14 @@ def _get_item_dicts(self, key, value): 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 } ) @@ -1059,9 +1012,9 @@ def _get_item_dicts(self, key, value): 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 @@ -1070,9 +1023,9 @@ def _get_item_dicts(self, key, value): 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 @@ -1081,9 +1034,9 @@ def _get_item_dicts(self, key, value): 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 @@ -1098,6 +1051,65 @@ def _get_item_dicts(self, key, value): return kw_ultraplot, kw_matplotlib + def config_inline_backend(self, fmt=None): + """ + Set up the ipython `inline backend display format \ + `__ + and ensure that inline figures always look the same as saved figures. + This runs the following ipython magic commands: + + .. code-block:: ipython + + %config InlineBackend.figure_formats = rc['inlineformat'] + %config InlineBackend.rc = {} # never override rc settings + %config InlineBackend.close_figures = True # cells start with no active figures + %config InlineBackend.print_figure_kwargs = {'bbox_inches': None} + + When the inline backend is inactive or unavailable, this has no effect. + This function is called when you modify the :rcraw:`inlineformat` property. + + Parameters + ---------- + fmt : str or sequence, default: :rc:`inlineformat` + The inline backend file format or a list thereof. Valid formats + include ``'jpg'``, ``'png'``, ``'svg'``, ``'pdf'``, and ``'retina'``. + + See also + -------- + Configurator + """ + # Note if inline backend is unavailable this will fail silently + ipython = get_ipython() + if ipython is None: + return + fmt = _not_none(fmt, self.rc_ultraplot["inlineformat"]) + if isinstance(fmt, str): + fmt = [fmt] + elif np.iterable(fmt): + fmt = list(fmt) + else: + raise ValueError(f"Invalid inline backend format {fmt!r}. Must be string.") + + # IPython < 9.0.0 + if hasattr(ipython, "magic"): + ipython.magic("config InlineBackend.figure_formats = " + repr(fmt)) + ipython.magic("config InlineBackend.rc = {}") + ipython.magic("config InlineBackend.close_figures = True") + ipython.magic( + "config InlineBackend.print_figure_kwargs = {'bbox_inches': None}" + ) + + # IPython >= 9.0.0 + else: + ipython.run_line_magic( + "config", "InlineBackend.figure_formats = " + repr(fmt) + ) + ipython.run_line_magic("config", "InlineBackend.rc = {}") + ipython.run_line_magic("config", "InlineBackend.close_figures = True") + ipython.run_line_magic( + "config", "InlineBackend.print_figure_kwargs = {'bbox_inches': None}" + ) + @staticmethod def _get_axisbelow_zorder(axisbelow): """ @@ -1797,6 +1809,20 @@ def save(self, path=None, user=True, comment=None, backup=True, description=Fals user_dict = self.changed if user else None self._save_yaml(path, user_dict, comment=comment, description=description) + def sync(self): + """ + Sync settings between ultraplot and matplotlib rc dictionaries. + """ + for _src in (self.rc_ultraplot, self.rc_matplotlib): + for _key in _src: # loop through unsynced properties + if "color" not in _key: + continue + try: + _src[_key] = _src[_key] + except ValueError as err: + warnings._warn_ultraplot(f"Invalid user rc file setting: {err}") + _src[_key] = "black" # fill value + @property def _context_mode(self): """ @@ -1842,23 +1868,9 @@ def changed(self): _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 ` for details. -rc = Configurator() - -# Deprecated -RcConfigurator = warnings._rename_objs( - "0.8.0", - RcConfigurator=Configurator, -) -inline_backend_fmt = warnings._rename_objs( - "0.6.0", inline_backend_fmt=config_inline_backend +rc = Configurator( + rc_ultraplot=rcsetup._rc_ultraplot_default.copy(), + rc_matplotlib=mpl.rcParams, ) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index d28e929c8..7bf168a54 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -29,7 +29,7 @@ from . import axes as paxes from . import constructor from . import gridspec as pgridspec -from .config import rc, rc_matplotlib +from .config import rc from .internals import ic # noqa: F401 from .internals import ( _not_none, @@ -731,21 +731,23 @@ def __init__( warnings._warn_ultraplot( "Ignoring constrained_layout=True. " + self._tight_message ) - if rc_matplotlib.get("figure.autolayout", False): + if rc.rc_matplotlib.get("figure.autolayout", False): warnings._warn_ultraplot( "Setting rc['figure.autolayout'] to False. " + self._tight_message ) - if rc_matplotlib.get("figure.constrained_layout.use", False): + if rc.rc_matplotlib.get("figure.constrained_layout.use", False): warnings._warn_ultraplot( "Setting rc['figure.constrained_layout.use'] to False. " + self._tight_message # noqa: E501 ) try: - rc_matplotlib["figure.autolayout"] = False # this is rcParams + rc.rc_matplotlib["figure.autolayout"] = False # this is rcParams except KeyError: pass try: - rc_matplotlib["figure.constrained_layout.use"] = False # this is rcParams + rc.rc_matplotlib["figure.constrained_layout.use"] = ( + False # this is rcParams + ) except KeyError: pass self._tight_active = _not_none(tight, rc["subplots.tight"]) From f920b8e508c96b80094d1e928beb91b99c62b388 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 27 Oct 2025 17:47:29 +0100 Subject: [PATCH 2/9] update docs --- docs/basics.py | 2 +- docs/configuration.rst | 12 ++++++------ docs/why.rst | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/basics.py b/docs/basics.py index 95a747b0a..9941ebbdf 100644 --- a/docs/basics.py +++ b/docs/basics.py @@ -477,7 +477,7 @@ # `__ and # :ref:`ultraplot settings `. The matplotlib-specific settings are # stored in :func:`~ultraplot.config.rc_matplotlib` (our name for :data:`~matplotlib.rcParams`) and -# the UltraPlot-specific settings are stored in :class:`~ultraplot.config.rc_ultraplot`. +# the UltraPlot-specific settings are stored in :class:`~ultraplot.config.rc.rc_ultraplot`. # UltraPlot also includes a :rcraw:`style` setting that can be used to # switch between `matplotlib stylesheets # `__. diff --git a/docs/configuration.rst b/docs/configuration.rst index 16974ba30..261329095 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -14,10 +14,10 @@ A dictionary-like object named :obj:`~ultraplot.config.rc`, belonging to the :class:`~ultraplot.config.Configurator` class, is created when you import UltraPlot. This is your one-stop shop for working with `matplotlib settings `_ -stored in :obj:`~ultraplot.config.rc_matplotlib` +stored in :attr:`~ultraplot.config.rc.rc_matplotlib` (our name for the :obj:`~matplotlib.rcParams` dictionary) and :ref:`ultraplot settings ` -stored in :obj:`~ultraplot.config.rc_ultraplot`. +stored in :attr:`~ultraplot.config.rc.rc_ultraplot`. To change global settings on-the-fly, simply update :obj:`~ultraplot.config.rc` using either dot notation or as you would any other dictionary: @@ -73,7 +73,7 @@ Matplotlib settings Matplotlib settings are natively stored in the :obj:`~matplotlib.rcParams` dictionary. UltraPlot makes this dictionary available in the top-level namespace as -:obj:`~ultraplot.config.rc_matplotlib`. All matplotlib settings can also be changed with +:obj:`~ultraplot.config.rc.rc_matplotlib`. All matplotlib settings can also be changed with :obj:`~ultraplot.config.rc`. Details on the matplotlib settings can be found on `this page `_. @@ -82,9 +82,9 @@ dictionary. UltraPlot makes this dictionary available in the top-level namespace UltraPlot settings ---------------- -UltraPlot settings are natively stored in the :obj:`~ultraplot.config.rc_ultraplot` dictionary. +UltraPlot settings are natively stored in the :obj:`~ultraplot.config.rc.rc_ultraplot` dictionary. They should almost always be changed with :obj:`~ultraplot.config.rc` rather than -:obj:`~ultraplot.config.rc_ultraplot` to ensure that :ref:`meta-settings ` are +:obj:`~ultraplot.config.rc.rc_ultraplot` to ensure that :ref:`meta-settings ` are synced. These settings are not found in :obj:`~matplotlib.rcParams` -- they either control UltraPlot-managed features (e.g., a-b-c labels and geographic gridlines) or they represent existing matplotlib settings with more clear or succinct names. @@ -119,7 +119,7 @@ Meta-settings Some UltraPlot settings may be more accurately described as "meta-settings", as they change several matplotlib and UltraPlot settings at once (note that settings are only synced when they are changed on the :obj:`~ultraplot.config.rc` object rather than -the :obj:`~ultraplot.config.rc_UltraPlot` and :obj:`~ultraplot.config.rc_matplotlib` dictionaries). +the :obj:`~ultraplot.config.rc.rc_ultraplot` and :obj:`~ultraplot.config.rc.rc_matplotlib` dictionaries). Here's a broad overview of the "meta-settings": * Setting :rcraw:`font.small` (or, equivalently, :rcraw:`fontsmall`) changes diff --git a/docs/why.rst b/docs/why.rst index 392a5616d..78cf950e7 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -815,8 +815,8 @@ Acceptable units include inches, centimeters, millimeters, pixels, `points `__, and `picas `__ (a table of acceptable units is found :ref:`here `). Note the :func:`~ultraplot.utils.units` engine -also translates rc settings assigned to :func:`~ultraplot.config.rc_matplotlib` and -:obj:`~ultraplot.config.rc_UltraPlot`, e.g. :rcraw:`subplots.refwidth`, +also translates rc settings assigned to :func:`~ultraplot.config.rc.rc_matplotlib` and +:obj:`~ultraplot.config.rc.rc_ultraplot`, e.g. :rcraw:`subplots.refwidth`, :rcraw:`legend.columnspacing`, and :rcraw:`axes.labelpad`. Links @@ -848,8 +848,8 @@ Changes ------- In UltraPlot, you can use the :obj:`~ultraplot.config.rc` object to change both native -matplotlib settings (found in :obj:`~ultraplot.config.rc_matplotlib`) and added UltraPlot -settings (found in :obj:`~ultraplot.config.rc_UltraPlot`). Assigned settings are always +matplotlib settings (found in :obj:`~ultraplot.config.rc.rc_matplotlib`) and added UltraPlot +settings (found in :obj:`~ultraplot.config.rc.rc_ultraplot`). Assigned settings are always validated, and "meta" settings like ``meta.edgecolor``, ``meta.linewidth``, and ``font.smallsize`` can be used to update many settings all at once. Settings can be changed with ``uplt.rc.key = value``, ``uplt.rc[key] = value``, From 1bc455d0b840e3a96c13d298ea10d57b1cf438cb Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 27 Oct 2025 17:50:48 +0100 Subject: [PATCH 3/9] label as object in docs --- docs/configuration.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 261329095..3db8ca1bc 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -14,10 +14,10 @@ A dictionary-like object named :obj:`~ultraplot.config.rc`, belonging to the :class:`~ultraplot.config.Configurator` class, is created when you import UltraPlot. This is your one-stop shop for working with `matplotlib settings `_ -stored in :attr:`~ultraplot.config.rc.rc_matplotlib` +stored in :obj:`~ultraplot.config.rc.rc_matplotlib` (our name for the :obj:`~matplotlib.rcParams` dictionary) and :ref:`ultraplot settings ` -stored in :attr:`~ultraplot.config.rc.rc_ultraplot`. +stored in :obj:`~ultraplot.config.rc.rc_ultraplot`. To change global settings on-the-fly, simply update :obj:`~ultraplot.config.rc` using either dot notation or as you would any other dictionary: From b3f702126b9f6497cbb904c7885c55d5912ea220 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Mon, 27 Oct 2025 17:52:50 +0100 Subject: [PATCH 4/9] Update ultraplot/config.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ultraplot/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/config.py b/ultraplot/config.py index 8836ac8bc..d45d5eccd 100644 --- a/ultraplot/config.py +++ b/ultraplot/config.py @@ -721,7 +721,7 @@ def __init__( self.rc_matplotlib = rc_matplotlib self._init(local=local, user=user, default=default, **kwargs) - def _init(self, *, local, user, default): + def _init(self, *, local, user, default, **kwargs): """ Initialize the configurator. """ From f2b3d7d62aeec41e245b485f915788606598e565 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 27 Oct 2025 17:54:14 +0100 Subject: [PATCH 5/9] add new params to docstring --- ultraplot/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ultraplot/config.py b/ultraplot/config.py index d45d5eccd..f6140cff1 100644 --- a/ultraplot/config.py +++ b/ultraplot/config.py @@ -68,6 +68,8 @@ def get_ipython(): # Configurator docstrings _rc_docstring = """ +rc_ultraplot : ~`ultraplot.rcsetup._RcParams`: rc parameters specific to ultraplot. +rc_matplotlib : ~`matplotlib.RcParams`: rc parameters specific to matplotlib. local : bool, default: True Whether to load settings from the `~Configurator.local_files` file. user : bool, default: True From c0ebba1d12daf11e472c5b50f0b4a81f769ce5e9 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 27 Oct 2025 18:13:53 +0100 Subject: [PATCH 6/9] add defaults --- ultraplot/config.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ultraplot/config.py b/ultraplot/config.py index f6140cff1..c2ff8fe98 100644 --- a/ultraplot/config.py +++ b/ultraplot/config.py @@ -68,8 +68,8 @@ def get_ipython(): # Configurator docstrings _rc_docstring = """ -rc_ultraplot : ~`ultraplot.rcsetup._RcParams`: rc parameters specific to ultraplot. -rc_matplotlib : ~`matplotlib.RcParams`: rc parameters specific to matplotlib. +rc_ultraplot : ~`ultraplot.rcsetup._RcParams` | None: rc parameters specific to ultraplot. Defaults to `rcsetup._rc_ultraplot_default`. +rc_matplotlib : ~`matplotlib.RcParams` | None: rc parameters specific to matplotlib. Defualts to `matplotlib.rcParams`. local : bool, default: True Whether to load settings from the `~Configurator.local_files` file. user : bool, default: True @@ -705,8 +705,8 @@ class Configurator(MutableMapping, dict): @docstring._snippet_manager def __init__( self, - rc_ultraplot: dict, - rc_matplotlib: dict, + rc_ultraplot: dict | None = None, + rc_matplotlib: dict | None = None, local=True, user=True, default=True, @@ -719,6 +719,10 @@ def __init__( """ self._context = [] self._setting_handlers = {} + if rc_ultraplot is None: + rc_ultraplot = rcsetup._rc_ultraplot_default.copy() + if rc_matplotlib is None: + rc_matplotlib = mpl.rcParams self.rc_ultraplot = rc_ultraplot self.rc_matplotlib = rc_matplotlib self._init(local=local, user=user, default=default, **kwargs) From 33e054a3bfd5739dc632c81fd0ac47f66ddab1da Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 27 Oct 2025 18:55:34 +0100 Subject: [PATCH 7/9] refactor tests --- ultraplot/tests/test_config.py | 133 +++++++++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 16 deletions(-) diff --git a/ultraplot/tests/test_config.py b/ultraplot/tests/test_config.py index 11a308b56..79ec4ed21 100644 --- a/ultraplot/tests/test_config.py +++ b/ultraplot/tests/test_config.py @@ -1,20 +1,21 @@ -import ultraplot as uplt, pytest -import importlib +from unittest.mock import patch, MagicMock +from importlib.metadata import PackageNotFoundError +import ultraplot as uplt, matplotlib as mpl, pytest, io +from ultraplot.utils import check_for_update def test_wrong_keyword_reset(): """ The context should reset after a failed attempt. """ - # Init context uplt.rc.context() config = uplt.rc - # Set a wrong key + with pytest.raises(KeyError): config._get_item_dicts("non_existing_key", "non_existing_value") - # Set a known good value + + # Confirm a subsequent valid operation still works config._get_item_dicts("coastcolor", "black") - # Confirm we can still plot fig, ax = uplt.subplots(proj="cyl") ax.format(coastcolor="black") fig.canvas.draw() @@ -28,18 +29,10 @@ def test_cycle_in_rc_file(tmp_path): rc_file = tmp_path / "test.rc" rc_file.write_text(rc_content) - # Load the file directly. This should overwrite any existing settings. uplt.rc.load(str(rc_file)) - assert uplt.rc["cycle"] == "colorblind" -import io -from unittest.mock import patch, MagicMock -from importlib.metadata import PackageNotFoundError -from ultraplot.utils import check_for_update - - @patch("builtins.print") @patch("importlib.metadata.version") def test_package_not_installed(mock_version, mock_print): @@ -118,5 +111,113 @@ def test_cycle_rc_setting(cycle, raises_error): if raises_error: with pytest.raises(ValueError): uplt.rc["cycle"] = cycle - else: - uplt.rc["cycle"] = cycle + + +def test_special_methods(): + """ + Test special methods of the Configurator class. + """ + with uplt.rc.context(): + # __repr__ / __str__ / __len__ + assert isinstance(repr(uplt.rc), str) and len(repr(uplt.rc)) > 0 + assert isinstance(str(uplt.rc), str) and len(str(uplt.rc)) > 0 + assert len(uplt.rc) > 0 + + # attribute access + assert hasattr(uplt.rc, "figure.facecolor") + + # deletion should raise + with pytest.raises(RuntimeError, match="rc settings cannot be deleted"): + del uplt.rc["figure.facecolor"] + with pytest.raises(RuntimeError, match="rc settings cannot be deleted"): + del uplt.rc.figure + + # restore a safe cycle setting + uplt.rc["cycle"] = "qual1" + + +def test_sync_method(): + """ + Test that the sync method properly synchronizes settings between ultraplot + and matplotlib rc dictionaries, and that invalid color assignments during + sync are handled by resetting matplotlib's rc to the fill value ('black') + while preserving the user's ultraplot setting. + """ + with uplt.rc.context(**{"figure.facecolor": "red"}): + # baseline: sync with valid color keeps the value + uplt.rc.sync() + assert uplt.rc["figure.facecolor"] == "red" + + # Monkeypatch the class-level RcParams.__setitem__ so that matplotlib + # will reject non-fallback colors. We patch the class method because + # mapping assignment (_src[key] = val) looks up the special method on + # the type. + original_class_setitem = mpl.RcParams.__setitem__ + + def patched_class_setitem(self, key, val): + # Simulate matplotlib rejecting non-fallback colors but allow the + # fallback 'black' used by Configurator.sync to succeed. + if key == "figure.facecolor" and val != "black": + raise ValueError("Invalid color") + return original_class_setitem(self, key, val) + + try: + # Apply the patch at class-level for the duration of sync() + with patch.object(mpl.RcParams, "__setitem__", new=patched_class_setitem): + # Ensure a user value exists (will update both rc_ultraplot and rc_matplotlib) + uplt.rc["figure.facecolor"] = "red" + # Run the sync which should attempt to re-assign and hit our patched setter. + uplt.rc.sync() + except: + pass + finally: + # Restore original to avoid side-effects on other tests + mpl.RcParams.__setitem__ = original_class_setitem + + # After sync the matplotlib rc should have been set to the fallback 'black' + # while the ultraplot rc (user setting) remains 'red'. + assert uplt.rc["figure.facecolor"] == "red" + + +def test_config_inline_backend(): + """ + Test that config_inline_backend properly configures the IPython inline backend. + """ + with uplt.rc.context(): + mock_ipython = MagicMock() + mock_ipython.run_line_magic = MagicMock() + mock_ipython.magic = MagicMock() + + # Test with string format + with patch("ultraplot.config.get_ipython", return_value=mock_ipython): + uplt.rc.config_inline_backend("png") + assert ( + mock_ipython.run_line_magic.call_count > 0 + or mock_ipython.magic.call_count > 0 + ) + all_calls = ( + mock_ipython.run_line_magic.call_args_list + + mock_ipython.magic.call_args_list + ) + calls = [args[0] for args in all_calls] + assert any("figure_formats" in str(call) for call in calls) + + # Test with list format + mock_ipython.reset_mock() + with patch("ultraplot.config.get_ipython", return_value=mock_ipython): + uplt.rc.config_inline_backend(["png", "svg"]) + assert ( + mock_ipython.run_line_magic.call_count > 0 + or mock_ipython.magic.call_count > 0 + ) + all_calls = ( + mock_ipython.run_line_magic.call_args_list + + mock_ipython.magic.call_args_list + ) + calls = [args[0] for args in all_calls] + assert any("figure_formats" in str(call) for call in calls) + + # Test with invalid format + with patch("ultraplot.config.get_ipython", return_value=mock_ipython): + with pytest.raises(ValueError, match="Invalid inline backend format"): + uplt.rc.config_inline_backend(123) From c0105d38c6ebf5b330cf307f070cdababae73a9c Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 27 Oct 2025 18:56:14 +0100 Subject: [PATCH 8/9] rm comment --- ultraplot/tests/test_config.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ultraplot/tests/test_config.py b/ultraplot/tests/test_config.py index 79ec4ed21..87180dc6a 100644 --- a/ultraplot/tests/test_config.py +++ b/ultraplot/tests/test_config.py @@ -173,9 +173,6 @@ def patched_class_setitem(self, key, val): finally: # Restore original to avoid side-effects on other tests mpl.RcParams.__setitem__ = original_class_setitem - - # After sync the matplotlib rc should have been set to the fallback 'black' - # while the ultraplot rc (user setting) remains 'red'. assert uplt.rc["figure.facecolor"] == "red" From 25e6b2e8d46ac1f15e2596c78cbdf59aa5e50d68 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 27 Oct 2025 19:26:49 +0100 Subject: [PATCH 9/9] more tests --- ultraplot/tests/test_config.py | 54 ++++++++++++++++++++++++++++++---- ultraplot/tests/test_figure.py | 27 +++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/ultraplot/tests/test_config.py b/ultraplot/tests/test_config.py index 87180dc6a..59440cbe3 100644 --- a/ultraplot/tests/test_config.py +++ b/ultraplot/tests/test_config.py @@ -162,18 +162,19 @@ def patched_class_setitem(self, key, val): return original_class_setitem(self, key, val) try: + # Ensure a user value exists before patching so the initial assignment + # does not trigger the simulated ValueError. The patched setter should + # only affect the subsequent sync() operation. + uplt.rc["figure.facecolor"] = "red" + # Apply the patch at class-level for the duration of sync() with patch.object(mpl.RcParams, "__setitem__", new=patched_class_setitem): - # Ensure a user value exists (will update both rc_ultraplot and rc_matplotlib) - uplt.rc["figure.facecolor"] = "red" # Run the sync which should attempt to re-assign and hit our patched setter. uplt.rc.sync() - except: - pass finally: # Restore original to avoid side-effects on other tests mpl.RcParams.__setitem__ = original_class_setitem - assert uplt.rc["figure.facecolor"] == "red" + assert uplt.rc["figure.facecolor"] == "black" def test_config_inline_backend(): @@ -218,3 +219,46 @@ def test_config_inline_backend(): with patch("ultraplot.config.get_ipython", return_value=mock_ipython): with pytest.raises(ValueError, match="Invalid inline backend format"): uplt.rc.config_inline_backend(123) + + +def test_sync_class_level_setitem_rejected(): + """ + Ensure that a class-level patch of matplotlib.RcParams.__setitem__ that + raises ValueError for non-fallback colors causes Configurator.sync() to + apply the fallback ('black') into the matplotlib rc mapping while keeping + the user's ultraplot rc value. + + Note: assign the user value before patching the class-level setter so the + initial assignment does not trigger the simulated ValueError. The patched + setter should only affect the subsequent sync() operation. + """ + # Use a fresh context so changes are isolated + with uplt.rc.context(**{"figure.facecolor": "red"}): + # Capture original class-level implementation + original_class_setitem = mpl.RcParams.__setitem__ + + def patched_class_setitem(self, key, val): + # Simulate matplotlib rejecting non-fallback colors but allow the + # fallback 'black' used by Configurator.sync to succeed. + if key == "figure.facecolor" and val != "black": + raise ValueError("Invalid color") + return original_class_setitem(self, key, val) + + try: + # Ensure a user value exists before patching so the assignment does not + # hit our patched class-level setter. + uplt.rc["figure.facecolor"] = "red" + + # Apply the patch at class level for the duration of the sync call + with patch.object(mpl.RcParams, "__setitem__", new=patched_class_setitem): + # Run sync which should attempt to write the user value into + # matplotlib rc and thus trigger the patched setter, causing the + # fallback write of 'black' into matplotlib rc. + uplt.rc.sync() + finally: + # Restore original to avoid side-effects on other tests + mpl.RcParams.__setitem__ = original_class_setitem + + # After sync, matplotlib's rc mapping should have the fallback 'black' + # while ultraplot's rc still contains the user's 'red' + assert uplt.rc["figure.facecolor"] == "black" diff --git a/ultraplot/tests/test_figure.py b/ultraplot/tests/test_figure.py index cffa3c7f6..0a9e4c91d 100644 --- a/ultraplot/tests/test_figure.py +++ b/ultraplot/tests/test_figure.py @@ -250,3 +250,30 @@ def test_suptitle_kw_position_reverted(ha, expectation): assert np.isclose(x, expectation, atol=0.1), f"Expected x={expectation}, got {x=}" uplt.close("all") + + +def test_disable_matplotlib_native_layout_flags_and_warnings(): + """ + Ensure that native matplotlib layout flags are disabled by ultraplot and that + UltraPlotWarning is emitted when creating a Figure while the matplotlib + rc flags 'figure.autolayout' or 'figure.constrained_layout.use' are True. + + This verifies the branches that call warnings._warn_ultraplot(...) and + then set the flags to False inside Figure.__init__. + """ + # Ensure the matplotlib-related rc flags are set on the ultraplot rc wrapper + # prior to creating a figure. + uplt.rc.rc_matplotlib["figure.autolayout"] = True + uplt.rc.rc_matplotlib["figure.constrained_layout.use"] = True + + # Creating the figure should emit at least one UltraPlotWarning (we don't + # rely on exact message content) and should reset the matplotlib rc flags. + with pytest.warns(uplt.internals.warnings.UltraPlotWarning): + fig = uplt.figure() + + # After construction, the matplotlib rc flags should have been set to False. + assert uplt.rc.rc_matplotlib.get("figure.autolayout", False) is False + assert uplt.rc.rc_matplotlib.get("figure.constrained_layout.use", False) is False + + # Clean up + uplt.close(fig)