From f5fe48352d4a68528b713746a6e242c1a2d5cfa0 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 27 Jan 2026 05:01:02 +1000 Subject: [PATCH 1/4] Fix log formatter tickrange crash Drop unsupported tickrange kwarg for non-UltraPlot formatters and add a regression test for log colorbars. --- ultraplot/constructor.py | 27 +++++++++++++++++++++++++-- ultraplot/tests/test_colorbar.py | 16 ++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/ultraplot/constructor.py b/ultraplot/constructor.py index 66f5a5f4a..d178390fe 100644 --- a/ultraplot/constructor.py +++ b/ultraplot/constructor.py @@ -11,6 +11,7 @@ # part of documentation, but this is redundant and pollutes the namespace. # User should just inspect docstrings, use trial-error, or see online tables. import copy +import inspect import os import re from functools import partial @@ -1256,6 +1257,19 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): ultraplot.axes.Axes.colorbar ultraplot.constructor.Locator """ # noqa: E501 + + def _pop_unsupported_kwarg(cls, kw, name): + if name not in kw: + return + try: + sig = inspect.signature(cls) + except (TypeError, ValueError): + return + if any(p.kind == p.VAR_KEYWORD for p in sig.parameters.values()): + return + if name not in sig.parameters: + kw.pop(name, None) + if ( np.iterable(formatter) and not isinstance(formatter, str) @@ -1266,12 +1280,16 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): return copy.copy(formatter) if isinstance(formatter, str): if re.search(r"{x(:.+)?}", formatter): # str.format + _pop_unsupported_kwarg(mticker.StrMethodFormatter, kwargs, "tickrange") formatter = mticker.StrMethodFormatter(formatter, *args, **kwargs) elif "%" in formatter: # str % format cls = mdates.DateFormatter if date else mticker.FormatStrFormatter + _pop_unsupported_kwarg(cls, kwargs, "tickrange") formatter = cls(formatter, *args, **kwargs) elif formatter in FORMATTERS: - formatter = FORMATTERS[formatter](*args, **kwargs) + cls = FORMATTERS[formatter] + _pop_unsupported_kwarg(cls, kwargs, "tickrange") + formatter = cls(*args, **kwargs) else: raise ValueError( f"Unknown formatter {formatter!r}. Options are: " @@ -1279,12 +1297,17 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): + "." ) elif formatter is True: + _pop_unsupported_kwarg(pticker.AutoFormatter, kwargs, "tickrange") formatter = pticker.AutoFormatter(*args, **kwargs) elif formatter is False: + _pop_unsupported_kwarg(mticker.NullFormatter, kwargs, "tickrange") formatter = mticker.NullFormatter(*args, **kwargs) elif np.iterable(formatter): - formatter = (mticker.FixedFormatter, pticker.IndexFormatter)[index](formatter) + cls = (mticker.FixedFormatter, pticker.IndexFormatter)[index] + _pop_unsupported_kwarg(cls, kwargs, "tickrange") + formatter = cls(formatter) elif callable(formatter): + _pop_unsupported_kwarg(mticker.FuncFormatter, kwargs, "tickrange") formatter = mticker.FuncFormatter(formatter, *args, **kwargs) else: raise ValueError(f"Invalid formatter {formatter!r}.") diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 81118762f..980647a98 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -151,6 +151,22 @@ def test_colorbar_ticks(): return fig +def test_colorbar_log_formatter_no_tickrange_error(rng): + data = 11 ** (0.25 * np.cumsum(rng.random((20, 20)), axis=0)) + fig, ax = uplt.subplots() + m = ax.pcolormesh(data, cmap="magma", norm="log") + ax.colorbar(m, formatter="log") + fig.canvas.draw() + + +def test_colorbar_log_formatter_no_tickrange_error(rng): + data = 11 ** (0.25 * np.cumsum(rng.random((20, 20)), axis=0)) + fig, ax = uplt.subplots() + m = ax.pcolormesh(data, cmap="magma", norm="log") + ax.colorbar(m, formatter="log") + fig.canvas.draw() + + @pytest.mark.mpl_image_compare def test_discrete_ticks(rng): """ From 2472535f9c3b242c6c455906196c9ae554fc38e3 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 27 Jan 2026 05:06:23 +1000 Subject: [PATCH 2/4] Harden formatter tickrange handling Retry formatter construction without tickrange on TypeError to avoid crashes for formatters lacking that kwarg. --- ultraplot/constructor.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/ultraplot/constructor.py b/ultraplot/constructor.py index d178390fe..e0f8a8c3f 100644 --- a/ultraplot/constructor.py +++ b/ultraplot/constructor.py @@ -1270,6 +1270,16 @@ def _pop_unsupported_kwarg(cls, kw, name): if name not in sig.parameters: kw.pop(name, None) + def _construct_formatter(cls, *f_args, **f_kwargs): + try: + return cls(*f_args, **f_kwargs) + except TypeError: + if "tickrange" in f_kwargs: + f_kwargs = dict(f_kwargs) + f_kwargs.pop("tickrange", None) + return cls(*f_args, **f_kwargs) + raise + if ( np.iterable(formatter) and not isinstance(formatter, str) @@ -1281,15 +1291,17 @@ def _pop_unsupported_kwarg(cls, kw, name): if isinstance(formatter, str): if re.search(r"{x(:.+)?}", formatter): # str.format _pop_unsupported_kwarg(mticker.StrMethodFormatter, kwargs, "tickrange") - formatter = mticker.StrMethodFormatter(formatter, *args, **kwargs) + formatter = _construct_formatter( + mticker.StrMethodFormatter, formatter, *args, **kwargs + ) elif "%" in formatter: # str % format cls = mdates.DateFormatter if date else mticker.FormatStrFormatter _pop_unsupported_kwarg(cls, kwargs, "tickrange") - formatter = cls(formatter, *args, **kwargs) + formatter = _construct_formatter(cls, formatter, *args, **kwargs) elif formatter in FORMATTERS: cls = FORMATTERS[formatter] _pop_unsupported_kwarg(cls, kwargs, "tickrange") - formatter = cls(*args, **kwargs) + formatter = _construct_formatter(cls, *args, **kwargs) else: raise ValueError( f"Unknown formatter {formatter!r}. Options are: " @@ -1298,17 +1310,19 @@ def _pop_unsupported_kwarg(cls, kw, name): ) elif formatter is True: _pop_unsupported_kwarg(pticker.AutoFormatter, kwargs, "tickrange") - formatter = pticker.AutoFormatter(*args, **kwargs) + formatter = _construct_formatter(pticker.AutoFormatter, *args, **kwargs) elif formatter is False: _pop_unsupported_kwarg(mticker.NullFormatter, kwargs, "tickrange") - formatter = mticker.NullFormatter(*args, **kwargs) + formatter = _construct_formatter(mticker.NullFormatter, *args, **kwargs) elif np.iterable(formatter): cls = (mticker.FixedFormatter, pticker.IndexFormatter)[index] _pop_unsupported_kwarg(cls, kwargs, "tickrange") - formatter = cls(formatter) + formatter = _construct_formatter(cls, formatter) elif callable(formatter): _pop_unsupported_kwarg(mticker.FuncFormatter, kwargs, "tickrange") - formatter = mticker.FuncFormatter(formatter, *args, **kwargs) + formatter = _construct_formatter( + mticker.FuncFormatter, formatter, *args, **kwargs + ) else: raise ValueError(f"Invalid formatter {formatter!r}.") return formatter From 2e65c08f1b9b47ff0a21a7a0bd8b50a76928f9d6 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 27 Jan 2026 05:10:16 +1000 Subject: [PATCH 3/4] Simplify formatter tickrange handling Drop signature inspection and rely on a TypeError retry without tickrange for maximum compatibility. --- ultraplot/constructor.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/ultraplot/constructor.py b/ultraplot/constructor.py index e0f8a8c3f..77a448516 100644 --- a/ultraplot/constructor.py +++ b/ultraplot/constructor.py @@ -11,7 +11,6 @@ # part of documentation, but this is redundant and pollutes the namespace. # User should just inspect docstrings, use trial-error, or see online tables. import copy -import inspect import os import re from functools import partial @@ -1258,18 +1257,6 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): ultraplot.constructor.Locator """ # noqa: E501 - def _pop_unsupported_kwarg(cls, kw, name): - if name not in kw: - return - try: - sig = inspect.signature(cls) - except (TypeError, ValueError): - return - if any(p.kind == p.VAR_KEYWORD for p in sig.parameters.values()): - return - if name not in sig.parameters: - kw.pop(name, None) - def _construct_formatter(cls, *f_args, **f_kwargs): try: return cls(*f_args, **f_kwargs) @@ -1290,17 +1277,14 @@ def _construct_formatter(cls, *f_args, **f_kwargs): return copy.copy(formatter) if isinstance(formatter, str): if re.search(r"{x(:.+)?}", formatter): # str.format - _pop_unsupported_kwarg(mticker.StrMethodFormatter, kwargs, "tickrange") formatter = _construct_formatter( mticker.StrMethodFormatter, formatter, *args, **kwargs ) elif "%" in formatter: # str % format cls = mdates.DateFormatter if date else mticker.FormatStrFormatter - _pop_unsupported_kwarg(cls, kwargs, "tickrange") formatter = _construct_formatter(cls, formatter, *args, **kwargs) elif formatter in FORMATTERS: cls = FORMATTERS[formatter] - _pop_unsupported_kwarg(cls, kwargs, "tickrange") formatter = _construct_formatter(cls, *args, **kwargs) else: raise ValueError( @@ -1309,17 +1293,13 @@ def _construct_formatter(cls, *f_args, **f_kwargs): + "." ) elif formatter is True: - _pop_unsupported_kwarg(pticker.AutoFormatter, kwargs, "tickrange") formatter = _construct_formatter(pticker.AutoFormatter, *args, **kwargs) elif formatter is False: - _pop_unsupported_kwarg(mticker.NullFormatter, kwargs, "tickrange") formatter = _construct_formatter(mticker.NullFormatter, *args, **kwargs) elif np.iterable(formatter): cls = (mticker.FixedFormatter, pticker.IndexFormatter)[index] - _pop_unsupported_kwarg(cls, kwargs, "tickrange") formatter = _construct_formatter(cls, formatter) elif callable(formatter): - _pop_unsupported_kwarg(mticker.FuncFormatter, kwargs, "tickrange") formatter = _construct_formatter( mticker.FuncFormatter, formatter, *args, **kwargs ) From 65c4230eed9556d577187af3720177feb8020e3b Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 27 Jan 2026 05:13:11 +1000 Subject: [PATCH 4/4] Add img cmp --- ultraplot/tests/test_colorbar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 980647a98..47ec5f246 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -151,14 +151,17 @@ def test_colorbar_ticks(): return fig +@pytest.mark.mpl_image_compare def test_colorbar_log_formatter_no_tickrange_error(rng): data = 11 ** (0.25 * np.cumsum(rng.random((20, 20)), axis=0)) fig, ax = uplt.subplots() m = ax.pcolormesh(data, cmap="magma", norm="log") ax.colorbar(m, formatter="log") fig.canvas.draw() + return fig +@pytest.mark.mpl_image_compare def test_colorbar_log_formatter_no_tickrange_error(rng): data = 11 ** (0.25 * np.cumsum(rng.random((20, 20)), axis=0)) fig, ax = uplt.subplots()