Repository: Ultraplot/UltraPlot

Issue:
Bug when setting pad length with unit
Code:
```
import ultraplot as uplt
fig, ax = uplt.subplot()
ax.format(ylabelpad='0em')
```

Traceback info:
```
Traceback (most recent call last):
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/artist.py", line 1410, in _get_tightbbox_for_layout_only
    return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
           ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/axis.py", line 1372, in get_tightbbox
    self._update_label_position(renderer)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/axis.py", line 2666, in _update_label_position
    (left - self.labelpad * self.figure.dpi / 72, y)
            ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~
TypeError: can't multiply sequence by non-int of type 'float'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/figure.py", line 1787, in get_tightbbox
    bbox = ax.get_tightbbox(
        renderer, bbox_extra_artists=bbox_extra_artists)
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/ultraplot/axes/cartesian.py", line 1549, in get_tightbbox
    return super().get_tightbbox(renderer, *args, **kwargs)
           ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/ultraplot/axes/base.py", line 3202, in get_tightbbox
    self._tight_bbox = super().get_tightbbox(renderer, *args, **kwargs)
                       ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/_api/deprecation.py", line 457, in wrapper
    return func(*args, **kwargs)
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/axes/_base.py", line 4476, in get_tightbbox
    ba = martist._get_tightbbox_for_layout_only(axis, renderer)
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/artist.py", line 1412, in _get_tightbbox_for_layout_only
    return obj.get_tightbbox(*args, **kwargs)
           ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/axis.py", line 1372, in get_tightbbox
    self._update_label_position(renderer)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/axis.py", line 2666, in _update_label_position
    (left - self.labelpad * self.figure.dpi / 72, y)
            ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~
TypeError: can't multiply sequence by non-int of type 'float'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/artist.py", line 1410, in _get_tightbbox_for_layout_only
    return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
           ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/axis.py", line 1372, in get_tightbbox
    self._update_label_position(renderer)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/axis.py", line 2666, in _update_label_position
    (left - self.labelpad * self.figure.dpi / 72, y)
            ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~
TypeError: can't multiply sequence by non-int of type 'float'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/backend_bases.py", line 1179, in _on_timer
    ret = func(*args, **kwargs)
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/backends/backend_macosx.py", line 71, in callback_func
    callback()
    ~~~~~~~~^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/backends/backend_macosx.py", line 93, in _draw_idle
    self.draw()
    ~~~~~~~~~^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/ultraplot/figure.py", line 472, in _canvas_preprocess
    fig.auto_layout()
    ~~~~~~~~~~~~~~~^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/ultraplot/figure.py", line 1795, in auto_layout
    gs._auto_layout_tight(renderer)
    ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/ultraplot/gridspec.py", line 913, in _auto_layout_tight
    bbox = fig.get_tightbbox(renderer)
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/_api/deprecation.py", line 457, in wrapper
    return func(*args, **kwargs)
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/figure.py", line 1790, in get_tightbbox
    bbox = ax.get_tightbbox(renderer)
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/ultraplot/axes/cartesian.py", line 1549, in get_tightbbox
    return super().get_tightbbox(renderer, *args, **kwargs)
           ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/ultraplot/axes/base.py", line 3202, in get_tightbbox
    self._tight_bbox = super().get_tightbbox(renderer, *args, **kwargs)
                       ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/_api/deprecation.py", line 457, in wrapper
    return func(*args, **kwargs)
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/axes/_base.py", line 4476, in get_tightbbox
    ba = martist._get_tightbbox_for_layout_only(axis, renderer)
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/artist.py", line 1412, in _get_tightbbox_for_layout_only
    return obj.get_tightbbox(*args, **kwargs)
           ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/axis.py", line 1372, in get_tightbbox
    self._update_label_position(renderer)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/Users/user/local/conda/envs/pynew/lib/python3.13/site-packages/matplotlib/axis.py", line 2666, in _update_label_position
    (left - self.labelpad * self.figure.dpi / 72, y)
            ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~
TypeError: can't multiply sequence by non-int of type 'float'

If you suspect this is an IPython 9.2.0 bug, please report it at:
    https://github.com/ipython/ipython/issues
or send an email to the mailing list at ipython-dev@python.org

You can print a more detailed traceback right now with "%tb", or use "%debug"
to interactively debug it.

Extra-detailed tracebacks for bug-reporting purposes can be enabled via:
    %config Application.verbose_crash=True
```

Discussion hints:
cvanelteren: Going ahead an merging this as it is minor enough. 

Solution patch:
diff --git a/ultraplot/axes/cartesian.py b/ultraplot/axes/cartesian.py
index f5538050..46685b5d 100644
--- a/ultraplot/axes/cartesian.py
+++ b/ultraplot/axes/cartesian.py
@@ -20,6 +20,8 @@ from ..internals import _not_none, _pop_rc, _version_mpl, docstring, labels, war
 from . import plot, shared
 import matplotlib.axis as maxis
 
+from ..utils import units
+
 __all__ = ["CartesianAxes"]
 
 
@@ -1391,6 +1393,14 @@ class CartesianAxes(shared._SharedAxes, plot.PlotAxes):
                     scale = constructor.Scale(scale, **scale_kw)
                     getattr(self, f"set_{s}scale")(scale)
 
+                # Explicitly sanitize unit-accepting arguments for this axis
+                ticklen = units(ticklen)
+                ticklabelpad = units(ticklabelpad)
+                labelpad = units(labelpad)
+                tickwidth = units(tickwidth)
+                labelsize = units(labelsize)
+                ticklabelsize = units(ticklabelsize)
+
                 # Axis limits
                 self._update_limits(s, min_=min_, max_=max_, lim=lim, reverse=reverse)
                 if margin is not None:


Test patch:
diff --git a/ultraplot/tests/test_axes.py b/ultraplot/tests/test_axes.py
index fdd02157..a04c2233 100644
--- a/ultraplot/tests/test_axes.py
+++ b/ultraplot/tests/test_axes.py
@@ -8,6 +8,88 @@ import ultraplot as uplt
 from ultraplot.internals.warnings import UltraPlotWarning
 
 
+@pytest.mark.parametrize(
+    "value",
+    [
+        5,  # int
+        5.0,  # float
+        "1em",  # string with unit
+        "10pt",  # string with unit
+        "2px",  # string with unit
+    ],
+)
+@pytest.mark.parametrize(
+    "kw",
+    [
+        "xticklen",
+        "yticklen",
+        "xticklabelpad",
+        "yticklabelpad",
+        "xlabelpad",
+        "ylabelpad",
+        "xtickwidth",
+        "ytickwidth",
+        "xlabelsize",
+        "ylabelsize",
+        "xticklabelsize",
+        "yticklabelsize",
+    ],
+)
+def test_cartesian_format_units_accepts_various_types(kw, value):
+    """
+    Test that CartesianAxes.format() accepts int, float, and string with units
+    for all relevant padding/size/width/len arguments.
+    """
+    fig, ax = uplt.subplots(proj="cart")
+    kwargs = {kw: value}
+    ax.format(**kwargs)
+
+
+@pytest.mark.parametrize(
+    "kw",
+    [
+        "xticklen",
+        "yticklen",
+        "xticklabelpad",
+        "yticklabelpad",
+        "xlabelpad",
+        "ylabelpad",
+        "xtickwidth",
+        "ytickwidth",
+        "xlabelsize",
+        "ylabelsize",
+        "xticklabelsize",
+        "yticklabelsize",
+    ],
+)
+def test_cartesian_format_units_invalid_type_raises(kw):
+    fig, ax = uplt.subplots(proj="cart")
+    with pytest.raises((TypeError, ValueError)):
+        ax.format(**{kw: object()})
+
+
+def test_cartesian_format_all_units_types():
+    """
+    Test that all relevant unit/padding/size/width/len arguments accept int, float, and string.
+    """
+    fig, ax = uplt.subplots(proj="cart")
+    kwargs = {
+        "xticklen": "1em",
+        "yticklen": 5,
+        "xticklabelpad": 2.5,
+        "yticklabelpad": "2px",
+        "xlabelpad": 3,
+        "ylabelpad": "10pt",
+        "xtickwidth": 1.5,
+        "ytickwidth": "2px",
+        "xlabelsize": "12pt",
+        "ylabelsize": 14,
+        "xticklabelsize": "1em",
+        "yticklabelsize": 10.0,
+    }
+    ax.format(**kwargs)
+
+
 def test_axis_access():
     # attempt to access the ax object 2d and linearly
     fig, ax = uplt.subplots(ncols=2, nrows=2)