From 50cbc622fd6b4d813b814c7e449adf6ff173c0b5 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 21 Jul 2026 14:05:05 +1000 Subject: [PATCH 1/2] Remove deprecated items --- docs/api.rst | 3 -- docs/usage.rst | 2 +- ultraplot/__init__.py | 6 --- ultraplot/axes/plot.py | 5 --- ultraplot/colors.py | 45 ------------------- ultraplot/config.py | 14 ------ ultraplot/constructor.py | 17 +------ ultraplot/demos.py | 1 - ultraplot/figure.py | 5 --- .../tests/test_constructor_helpers_extra.py | 5 +-- ultraplot/tests/test_imports.py | 3 +- ultraplot/utils.py | 11 ----- 12 files changed, 5 insertions(+), 112 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 5e2489e2b..d3a5f3bb3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -68,7 +68,6 @@ Configuration tools .. automodsumm:: ultraplot.config :toctree: api - :skip: inline_backend_fmt, RcConfigurator Constructor functions @@ -79,7 +78,6 @@ Constructor functions .. automodsumm:: ultraplot.constructor :toctree: api - :skip: Colors Locators and formatters @@ -110,7 +108,6 @@ Colormaps and normalizers .. automodsumm:: ultraplot.colors :toctree: api - :skip: ListedColormap, LinearSegmentedColormap, PerceptuallyUniformColormap, LinearSegmentedNorm Projection classes diff --git a/docs/usage.rst b/docs/usage.rst index f70d90a6f..298c12132 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -243,5 +243,5 @@ classes and :ref:`constructor functions `. :ref:`modifying individual settings, changing settings in bulk, and temporarily changing settings in context blocks `. It also introduces several :ref:`new setings ` - and sets up the inline plotting backend with :func:`~ultraplot.config.inline_backend_fmt` + and sets up the inline plotting backend with :func:`~ultraplot.config.config_inline_backend` so that your inline figures look the same as your saved figures. diff --git a/ultraplot/__init__.py b/ultraplot/__init__.py index 0632ecfec..cd9b5d467 100644 --- a/ultraplot/__init__.py +++ b/ultraplot/__init__.py @@ -34,11 +34,7 @@ from .colors import DiscreteColormap as DiscreteColormap from .colors import DiscreteNorm as DiscreteNorm from .colors import DivergingNorm as DivergingNorm - from .colors import LinearSegmentedColormap as LinearSegmentedColormap - from .colors import LinearSegmentedNorm as LinearSegmentedNorm - from .colors import ListedColormap as ListedColormap from .colors import PerceptualColormap as PerceptualColormap - from .colors import PerceptuallyUniformColormap as PerceptuallyUniformColormap from .colors import SegmentedNorm as SegmentedNorm from .colors import _cmap_database as colormaps from .config import config_inline_backend as config_inline_backend @@ -138,14 +134,12 @@ from .utils import edges as edges from .utils import edges2d as edges2d from .utils import get_colors as get_colors - from .utils import saturate as saturate from .utils import scale_luminance as scale_luminance from .utils import scale_saturation as scale_saturation from .utils import set_alpha as set_alpha from .utils import set_hue as set_hue from .utils import set_luminance as set_luminance from .utils import set_saturation as set_saturation - from .utils import shade as shade from .utils import shift_hue as shift_hue from .utils import to_hex as to_hex from .utils import to_rgb as to_rgb diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 48254187e..6bd62acaf 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -4063,7 +4063,6 @@ def _scatter_c_is_scalar_data( return False return values.shape[0] == point_count - @warnings._rename_kwargs("0.6.0", centers="values") def _parse_cmap( self, *args, @@ -7703,9 +7702,5 @@ def _iter_arg_cols(self, *args, label=None, labels=None, values=None, **kwargs): # Related parsing functions for warnings _level_parsers = (_parse_level_vals, _parse_level_num, _parse_level_lim) - # Rename the shorthands - boxes = warnings._rename_objs("0.8.0", boxes=box) - violins = warnings._rename_objs("0.8.0", violins=violin) - # mock commit diff --git a/ultraplot/colors.py b/ultraplot/colors.py index bab2f5c7a..08e4a655d 100644 --- a/ultraplot/colors.py +++ b/ultraplot/colors.py @@ -71,10 +71,6 @@ def _cycle_handler(value): "SegmentedNorm", "ColorDatabase", "ColormapDatabase", - "ListedColormap", # deprecated - "LinearSegmentedColormap", # deprecated - "PerceptuallyUniformColormap", # deprecated - "LinearSegmentedNorm", # deprecated ] # Default colormap properties @@ -1689,16 +1685,6 @@ def from_list(cls, *args, **kwargs): cdict[key] = _make_segment_data(values, coords, ratios) return cls(name, cdict, **kwargs) - # Deprecated - to_listed = warnings._rename_objs("0.8.0", to_listed=to_discrete) - concatenate, punched, truncated, updated = warnings._rename_objs( - "0.6.0", - concatenate=append, - punched=cut, - truncated=truncate, - updated=copy, - ) - class DiscreteColormap(mcolors.ListedColormap, _Colormap): r""" @@ -1987,14 +1973,6 @@ def from_file(cls, path, *, warn_on_failure=False): """ return cls._from_file(path, warn_on_failure=warn_on_failure) - # Rename methods - concatenate, truncated, updated = warnings._rename_objs( - "0.6.0", - concatenate=append, - truncated=truncate, - updated=copy, - ) - class PerceptualColormap(ContinuousColormap): """ @@ -2224,7 +2202,6 @@ def to_continuous(self, name=None, **kwargs): @classmethod @docstring._snippet_manager - @warnings._rename_kwargs("0.7.0", fade="saturation", shade="luminance") def from_color(cls, *args, **kwargs): """ Return a simple monochromatic "sequential" colormap that blends from white @@ -2420,11 +2397,6 @@ def from_list(cls, *args, adjust_grays=True, **kwargs): return cls(name, cdict, **kwargs) - # Deprecated - to_linear_segmented = warnings._rename_objs( - "0.8.0", to_linear_segmented=to_continuous - ) - def _interpolate_scalar(x, x0, x1, y0, y1): """ @@ -2490,9 +2462,6 @@ class DiscreteNorm(mcolors.BoundaryNorm): # WARNING: Must be child of BoundaryNorm. Many methods in ColorBarBase # test for class membership, crucially including _process_values(), which # if it doesn't detect BoundaryNorm will try to use DiscreteNorm.inverse(). - @warnings._rename_kwargs( - "0.7.0", extend="unique", descending="DiscreteNorm(descending_levels)" - ) def __init__( self, levels, @@ -3325,17 +3294,3 @@ def register_lazy(self, name, path, type, is_default=False): # Initialize databases _cmap_database = _init_cmap_database() _color_database = _init_color_database() - -# Deprecated -( - ListedColormap, - LinearSegmentedColormap, - PerceptuallyUniformColormap, - LinearSegmentedNorm, -) = warnings._rename_objs( # noqa: E501 - "0.8.0", - ListedColormap=DiscreteColormap, - LinearSegmentedColormap=ContinuousColormap, - PerceptuallyUniformColormap=PerceptualColormap, - LinearSegmentedNorm=SegmentedNorm, -) diff --git a/ultraplot/config.py b/ultraplot/config.py index b3270a492..af66ed6f9 100644 --- a/ultraplot/config.py +++ b/ultraplot/config.py @@ -56,8 +56,6 @@ "register_cycles", "register_colors", "register_fonts", - "RcConfigurator", # deprecated - "inline_backend_fmt", # deprecated ] # Constants @@ -1888,9 +1886,6 @@ def changed(self): rcdict.pop("axes.prop_cycle", None) return _filter_style_dict(rcdict, warn=False) - # Renamed methods - load_file = warnings._rename_objs("0.8.0", load_file=load) - # Initialize locations _init_user_folders() @@ -1907,12 +1902,3 @@ def changed(self): #: 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 -) diff --git a/ultraplot/constructor.py b/ultraplot/constructor.py index 1cb9e6600..04d315f69 100644 --- a/ultraplot/constructor.py +++ b/ultraplot/constructor.py @@ -41,7 +41,7 @@ ic, # noqa: F401 warnings, ) -from .utils import get_colors, to_hex, to_rgba +from .utils import to_hex, to_rgba try: from mpl_toolkits.basemap import Basemap @@ -62,7 +62,6 @@ "Colormap", "Norm", "Cycle", - "Colors", # deprecated ] # Color cycle constants @@ -482,9 +481,6 @@ def _modify_colormap(cmap, *, cut, left, right, reverse, shift, alpha, samples): return cmap -@warnings._rename_kwargs( - "0.8.0", fade="saturation", shade="luminance", to_listed="discrete" -) def Colormap( *args, name=None, @@ -711,13 +707,6 @@ def _pop_modification(key): "Colormap() requires either positional arguments or " "'hue', 'chroma', 'saturation', and/or 'luminance' keywords." ) - deprecated = {"listed": "discrete", "linear": "continuous"} - if listmode in deprecated: - oldmode, listmode = listmode, deprecated[listmode] - warnings._warn_ultraplot( - f"Please use listmode={listmode!r} instead of listmode={oldmode!r}." - "Option was renamed in v0.8 and will be removed in a future relase." - ) options = {"discrete", "continuous", "perceptual"} for key, mode in zip(("listmode", "filemode"), (listmode, filemode)): if mode not in options: @@ -1792,7 +1781,3 @@ def Proj( proj._proj_backend = backend return proj - - -# Deprecated -Colors = warnings._rename_objs("0.8.0", Colors=get_colors) diff --git a/ultraplot/demos.py b/ultraplot/demos.py index 85b010e6c..e16d5378f 100644 --- a/ultraplot/demos.py +++ b/ultraplot/demos.py @@ -602,7 +602,6 @@ def show_colorspaces(*, luminance=None, saturation=None, hue=None, refwidth=2): return fig, axs -@warnings._rename_kwargs("0.8.0", categories="include") @warnings._rename_kwargs("0.10.0", rasterize="rasterized") def _draw_bars( cmaps, diff --git a/ultraplot/figure.py b/ultraplot/figure.py index ed2955e99..60cf99014 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -718,13 +718,8 @@ def __repr__(self): opts = ", ".join(f"{key}={value!r}" for key, value in opts.items()) return f"Figure({geom}{opts})" - # NOTE: If _rename_kwargs argument is an invalid identifier, it is - # simply used in the warning message. @docstring._obfuscate_kwargs @docstring._snippet_manager - @warnings._rename_kwargs( - "0.7.0", axpad="innerpad", autoformat="uplt.rc.autoformat = {}" - ) @_alias_kwargs( refnum=("ref",), refaspect=("aspect",), diff --git a/ultraplot/tests/test_constructor_helpers_extra.py b/ultraplot/tests/test_constructor_helpers_extra.py index e8d464c34..7d75dc3f6 100644 --- a/ultraplot/tests/test_constructor_helpers_extra.py +++ b/ultraplot/tests/test_constructor_helpers_extra.py @@ -29,9 +29,8 @@ def fake_save(self, **kwargs): monkeypatch.setattr(pcolors.DiscreteColormap, "save", fake_save) - with pytest.warns(UltraPlotWarning, match="listmode='discrete'"): - deprecated = constructor.Colormap(["red", "blue"], listmode="listed") - assert isinstance(deprecated, pcolors.DiscreteColormap) + discrete = constructor.Colormap(["red", "blue"], listmode="discrete") + assert isinstance(discrete, pcolors.DiscreteColormap) cmap = constructor.Colormap( str(hex_path), diff --git a/ultraplot/tests/test_imports.py b/ultraplot/tests/test_imports.py index 7a2c12ff3..ff253a8d7 100644 --- a/ultraplot/tests/test_imports.py +++ b/ultraplot/tests/test_imports.py @@ -209,8 +209,7 @@ def test_type_checking_block_covers_public_all(): - Module-level scalars defined directly in ``__init__.py`` (``name``, ``version``, ``__version__``, ``setup``) — already visible to type checkers without an import. - - Deprecated ``_rename_objs`` wrappers (e.g. ``RcConfigurator``, - ``inline_backend_fmt``, ``Colors``) — their runtime type is a dynamically + - Deprecated ``_rename_objs`` wrappers — their runtime type is a dynamically generated class/function from ``internals.warnings``; they cannot be imported cleanly and are not worth exposing to type checkers. """ diff --git a/ultraplot/utils.py b/ultraplot/utils.py index d07edb9a9..a8bc4242e 100644 --- a/ultraplot/utils.py +++ b/ultraplot/utils.py @@ -39,8 +39,6 @@ "to_rgba", "to_xyza", "units", - "shade", # deprecated - "saturate", # deprecated ] UNIT_REGEX = re.compile( @@ -768,7 +766,6 @@ def _fontsize_to_pt(size): ) -@warnings._rename_kwargs("0.6.0", units="dest") def units( value, numeric=None, dest=None, *, fontsize=None, figure=None, axes=None, width=None ): @@ -1158,11 +1155,3 @@ def check_for_update(package_name: str) -> None: f"{current_version} → {latest_version}\n" f"Run: pip install -U {package_name}\033[0m" ) - - -# Deprecations -shade, saturate = warnings._rename_objs( - "0.6.0", - shade=scale_luminance, - saturate=scale_saturation, -) From e0e0b0a6b1adb10cd20f70eedd54f7611b8a982c Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 30 Jul 2026 10:55:12 +1000 Subject: [PATCH 2/2] init --- ultraplot/_subplots.py | 238 +++++++++++++++++++++++------------------ ultraplot/axes/base.py | 2 +- ultraplot/figure.py | 43 ++++++-- 3 files changed, 166 insertions(+), 117 deletions(-) diff --git a/ultraplot/_subplots.py b/ultraplot/_subplots.py index b0594973b..0650bc12f 100644 --- a/ultraplot/_subplots.py +++ b/ultraplot/_subplots.py @@ -3,7 +3,7 @@ """ from numbers import Integral -from typing import TYPE_CHECKING +from typing import Protocol import matplotlib.axes as maxes import matplotlib.gridspec as mgridspec @@ -15,22 +15,138 @@ from . import gridspec as pgridspec from .internals import _not_none, _pop_params, warnings -if TYPE_CHECKING: - from .figure import Figure + +def parse_backend(backend=None, basemap=None): + """ + Handle deprecation of basemap and cartopy package. + """ + if backend == "basemap": + warnings._warn_ultraplot( + f"{backend=} will be deprecated in next major release (v2.0). " + "See https://github.com/Ultraplot/ultraplot/pull/243" + ) + return backend + + +def parse_proj( + proj=None, + projection=None, + proj_kw=None, + projection_kw=None, + backend=None, + basemap=None, + **kwargs, +): + """ + Translate user-input projection into a registered matplotlib axes class. + """ + # Parse arguments + proj = _not_none(proj=proj, projection=projection, default="cartesian") + proj_kw = _not_none(proj_kw=proj_kw, projection_kw=projection_kw, default={}) + backend = parse_backend(backend, basemap) + if isinstance(proj, str): + proj = proj.lower() + + # Search axes projections + name = None + + # Handle cartopy/basemap Projection objects directly + # These should be converted to Ultraplot GeoAxes + if not isinstance(proj, str): + if constructor.Projection is not object and isinstance( + proj, constructor.Projection + ): + name = "ultraplot_cartopy" + kwargs["map_projection"] = proj + elif constructor.Basemap is not object and isinstance( + proj, constructor.Basemap + ): + name = "ultraplot_basemap" + kwargs["map_projection"] = proj + + if name is None and isinstance(proj, str): + try: + mproj.get_projection_class("ultraplot_" + proj) + except (KeyError, ValueError): + pass + else: + name = "ultraplot_" + proj + if name is None and isinstance(proj, str): + # Try geographic projections first if cartopy/basemap available + if constructor.Projection is not object or constructor.Basemap is not object: + try: + proj_obj = constructor.Proj( + proj, backend=backend, include_axes=True, **proj_kw + ) + name = "ultraplot_" + proj_obj._proj_backend + kwargs["map_projection"] = proj_obj + except ValueError: + pass # not a geographic projection, try matplotlib registry below + + # If not geographic, check if registered globally in matplotlib + # (e.g., 'ternary', 'polar', '3d') + if name is None and proj in mproj.get_projection_names(): + name = proj + + if name is None and isinstance(proj, str): + raise ValueError( + f"Invalid projection name {proj!r}. If you are trying to generate a " + "GeoAxes with a cartopy.crs.Projection or mpl_toolkits.basemap.Basemap " + "then cartopy or basemap must be installed. Otherwise the known axes " + f"subclasses are:\n{paxes._cls_table}" + ) + + if name is not None: + kwargs["projection"] = name + return kwargs + + +class FigureHost(Protocol): + """ + The members of `~ultraplot.figure.Figure` that `SubplotManager` calls *directly*. + + Naming them keeps the upward dependency deliberate: a change to `Figure` reaches + the manager through these four names and nowhere else, and adding a fifth is a + visible edit here rather than a new attribute poke buried in a method. + + WARNING: This is not a sufficient interface -- satisfying it is not enough to + drive a `SubplotManager`. `gridspec` assigns ``gs.figure = self.figure``, and + `~ultraplot.gridspec.GridSpec.figure` rejects anything that is not a real + `Figure`, then reads ``_gridspec_params`` and calls ``set_size_inches``. So the + manager still requires a concrete `Figure`; that requirement simply arrives + through `GridSpec` rather than through this protocol. + """ + + def _native_add_subplot(self, *args, **kwargs): + """Create an axes with matplotlib's ``add_subplot``, skipping overrides.""" + + def _mark_layout_dirty(self) -> None: + """Record that the layout must be recomputed before the next draw.""" + + def _pop_format_params(self, kwargs: dict) -> dict: + """Remove and return the figure-level ``format`` keywords in *kwargs*.""" + + def format(self, **kwargs) -> None: + """Apply figure-level formatting.""" class SubplotManager: """ - Manages subplot creation, gridspec ownership, and projection parsing - for a Figure instance. + Manages subplot creation and gridspec ownership for a figure. Parameters ---------- figure : `~ultraplot.figure.Figure` - The parent figure. + The parent figure. `FigureHost` documents which of its members are used. """ - def __init__(self, figure: "Figure"): + # Projection parsing is a pure function of its arguments -- aliased here + # because `ultraplot.ui` introspects these signatures to split figure + # keywords from subplot keywords. + parse_backend = staticmethod(parse_backend) + parse_proj = staticmethod(parse_proj) + + def __init__(self, figure: FigureHost): self.figure = figure self.subplot_dict: dict = {} self.counter: int = 0 @@ -60,101 +176,13 @@ def gridspec(self, gs): self._gridspec = gs gs.figure = self.figure # gridspec.figure should reference the real Figure - @staticmethod - def parse_backend(backend=None, basemap=None): - """ - Handle deprecation of basemap and cartopy package. - """ - if backend == "basemap": - warnings._warn_ultraplot( - f"{backend=} will be deprecated in next major release (v2.0). " - "See https://github.com/Ultraplot/ultraplot/pull/243" - ) - return backend - - def parse_proj( - self, - proj=None, - projection=None, - proj_kw=None, - projection_kw=None, - backend=None, - basemap=None, - **kwargs, - ): - """ - Translate user-input projection into a registered matplotlib axes class. - """ - # Parse arguments - proj = _not_none(proj=proj, projection=projection, default="cartesian") - proj_kw = _not_none(proj_kw=proj_kw, projection_kw=projection_kw, default={}) - backend = self.parse_backend(backend, basemap) - if isinstance(proj, str): - proj = proj.lower() - - # Search axes projections - name = None - - # Handle cartopy/basemap Projection objects directly - # These should be converted to Ultraplot GeoAxes - if not isinstance(proj, str): - if constructor.Projection is not object and isinstance( - proj, constructor.Projection - ): - name = "ultraplot_cartopy" - kwargs["map_projection"] = proj - elif constructor.Basemap is not object and isinstance( - proj, constructor.Basemap - ): - name = "ultraplot_basemap" - kwargs["map_projection"] = proj - - if name is None and isinstance(proj, str): - try: - mproj.get_projection_class("ultraplot_" + proj) - except (KeyError, ValueError): - pass - else: - name = "ultraplot_" + proj - if name is None and isinstance(proj, str): - # Try geographic projections first if cartopy/basemap available - if ( - constructor.Projection is not object - or constructor.Basemap is not object - ): - try: - proj_obj = constructor.Proj( - proj, backend=backend, include_axes=True, **proj_kw - ) - name = "ultraplot_" + proj_obj._proj_backend - kwargs["map_projection"] = proj_obj - except ValueError: - pass # not a geographic projection, try matplotlib registry below - - # If not geographic, check if registered globally in matplotlib - # (e.g., 'ternary', 'polar', '3d') - if name is None and proj in mproj.get_projection_names(): - name = proj - - if name is None and isinstance(proj, str): - raise ValueError( - f"Invalid projection name {proj!r}. If you are trying to generate a " - "GeoAxes with a cartopy.crs.Projection or mpl_toolkits.basemap.Basemap " - "then cartopy or basemap must be installed. Otherwise the known axes " - f"subclasses are:\n{paxes._cls_table}" - ) - - if name is not None: - kwargs["projection"] = name - return kwargs - def add_subplot(self, *args, **kwargs): """ The driver function for adding single subplots. """ fig = self.figure - fig._layout_dirty = True - kwargs = self.parse_proj(**kwargs) + fig._mark_layout_dirty() + kwargs = parse_proj(**kwargs) args = args or (1, 1, 1) gs = self.gridspec @@ -261,13 +289,9 @@ def add_subplot(self, *args, **kwargs): kwargs.pop("_subplot_spec", None) - # NOTE: Skip past Figure.add_subplot (which routes back here) to the - # matplotlib implementation. Using super() rather than naming the - # matplotlib class keeps any mixin between Figure and matplotlib's - # Figure in a subclass MRO from being bypassed. - from .figure import Figure - - ax = super(Figure, fig).add_subplot(ss, **kwargs) + # NOTE: The host skips past its own add_subplot (which routes back here) + # to the matplotlib implementation. + ax = fig._native_add_subplot(ss, **kwargs) if ax.number: self.subplot_dict[ax.number] = ax return ax @@ -352,7 +376,7 @@ def _axes_dict(naxs, input, kw=False, default=None): proj = _axes_dict(naxs, proj, kw=False, default="cartesian") proj_kw = _not_none(projection_kw=projection_kw, proj_kw=proj_kw) or {} proj_kw = _axes_dict(naxs, proj_kw, kw=True) - backend = self.parse_backend(backend, basemap) + backend = parse_backend(backend, basemap) backend = _axes_dict(naxs, backend, kw=False) axes_kw = { num: {"proj": proj[num], "proj_kw": proj_kw[num], "backend": backend[num]} @@ -367,7 +391,7 @@ def _axes_dict(naxs, input, kw=False, default=None): "parameters as keyword arguments instead." ) kwargs.update(kw or {}) - figure_kw = _pop_params(kwargs, fig._format_signature) + figure_kw = fig._pop_format_params(kwargs) gridspec_kw = _pop_params(kwargs, pgridspec.GridSpec._update_params) # Create or update the gridspec and add subplots with subplotspecs @@ -388,7 +412,7 @@ def _axes_dict(naxs, input, kw=False, default=None): y0, y1 = axrows[idx, 0], axrows[idx, 1] ss = gs[y0 : y1 + 1, x0 : x1 + 1] kw = {**kwargs, **axes_kw[num], "number": num} - axs[idx] = fig.add_subplot(ss, **kw) + axs[idx] = self.add_subplot(ss, **kw) fig.format(skip_axes=True, **figure_kw) return pgridspec.SubplotGrid(axs) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 735e99bee..df4d7d5e2 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3336,7 +3336,7 @@ def format( ultraplot.config.Configurator.context """ if self.figure is not None: - self.figure._layout_dirty = True + self.figure._mark_layout_dirty() skip_figure = kwargs.pop("skip_figure", False) # internal keyword arg params = _pop_params(kwargs, self.figure._format_signature) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 60cf99014..d24a65524 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -41,7 +41,7 @@ labels, warnings, ) -from ._subplots import SubplotManager +from ._subplots import SubplotManager, parse_backend, parse_proj from .utils import _Crawler, units __all__ = [ @@ -1082,7 +1082,7 @@ def clear(self, keep_observers=False): self._subplots.reset() self._panel_dict = {"left": [], "right": [], "bottom": [], "top": []} self._layout_initialized = False - self._layout_dirty = True + self._mark_layout_dirty() self._init_super_labels() @override @@ -1726,12 +1726,33 @@ def _context_authorized(self): @staticmethod def _parse_backend(backend=None, basemap=None): - """Delegate to SubplotManager.""" - return SubplotManager.parse_backend(backend, basemap) + """Delegate to the projection parser.""" + return parse_backend(backend, basemap) def _parse_proj(self, *args, **kwargs): - """Delegate to SubplotManager.""" - return self._subplots.parse_proj(*args, **kwargs) + """Delegate to the projection parser.""" + return parse_proj(*args, **kwargs) + + # Implementation of `ultraplot._subplots.FigureHost` -- the only members + # `SubplotManager` is permitted to reach for. Keep this seam narrow: anything + # else the manager needs should become an explicit method here first. + def _native_add_subplot(self, *args, **kwargs): + """ + Add a subplot with matplotlib's implementation, skipping this class's + `add_subplot` (which routes back into `SubplotManager`). + + NOTE: Uses ``super()`` rather than naming `matplotlib.figure.Figure` so a + mixin between this class and matplotlib's in a subclass MRO is not bypassed. + """ + return super().add_subplot(*args, **kwargs) + + def _mark_layout_dirty(self): + """Record that the layout must be recomputed before the next draw.""" + self._layout_dirty = True + + def _pop_format_params(self, kwargs): + """Remove and return the figure-level `format` keywords in *kwargs*.""" + return _pop_params(kwargs, self._format_signature) def _get_align_axes(self, side): """ @@ -2145,7 +2166,7 @@ def _add_figure_panel( """ Add a figure panel. """ - self._layout_dirty = True + self._mark_layout_dirty() # Interpret args and enforce sensible keyword args side = _translate_loc(side, "panel", default="right") if side in ("left", "right"): @@ -2258,6 +2279,10 @@ def get_key(ax): else: ref._shared_axes[which].join(ref, other) + # NOTE: The manager builds each subplot through its own `add_subplot`, not + # this class's, so the border cache is invalidated once here rather than + # once per subplot. + @_clear_border_cache def _add_subplots(self, *args, **kwargs): """Delegate to SubplotManager.""" return self._subplots.add_subplots(*args, **kwargs) @@ -3414,7 +3439,7 @@ def format( ultraplot.gridspec.SubplotGrid.format ultraplot.config.Configurator.context """ - self._layout_dirty = True + self._mark_layout_dirty() # Initiate context block axs = axs or self._iter_subplots() skip_axes = kwargs.pop("skip_axes", False) # internal keyword arg @@ -4106,7 +4131,7 @@ def set_size_inches(self, w, h=None, *, forward=True, internal=False, eps=None): if not samesize: # gridspec positions will resolve differently self.gridspec.update() if not backend and not internal: - self._layout_dirty = True + self._mark_layout_dirty() def _iter_axes(self, hidden=False, children=False, panels=True): """