Skip to content

POC: Add validation of converters to formatters #25766

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,8 @@ def __init__(self, axes, *, pickradius=15):
self._major_tick_kw = dict()
self._minor_tick_kw = dict()

self._axisinfo = None

self.clear()
self._autoscale_on = True

Expand Down Expand Up @@ -1674,6 +1676,7 @@ def _update_axisinfo(self):
return

info = self.converter.axisinfo(self.units, self)
self._axisinfo = info

if info is None:
return
Expand Down Expand Up @@ -1821,6 +1824,11 @@ def _set_formatter(self, formatter, level):
_api.warn_external('FixedFormatter should only be used together '
'with FixedLocator')

assert isinstance(formatter, mticker.Formatter)

if hasattr(self, "converter") and self.converter is not None:
formatter.validate_converter(self.converter, self._axisinfo)

if level == self.major:
self.isDefault_majfmt = False
else:
Expand Down
6 changes: 6 additions & 0 deletions lib/matplotlib/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ def _text(value):
value = str(value)
return value

def validate_converter(self, converter, axisinfo):
if not isinstance(converter, StrCategoryConverter):
_api.warn_external(
"Expected a StrCategoryConverter for StrCategoryFormatter"
)


class UnitData:
def __init__(self, data=None):
Expand Down
18 changes: 16 additions & 2 deletions lib/matplotlib/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,11 @@ def __call__(self, x, pos=0):
def set_tzinfo(self, tz):
self.tz = _get_tzinfo(tz)

def validate_converter(self, converter, axisinfo):
if axisinfo.description != "days since matplotlib epoch":
print(converter, axisinfo.majfmt)
_api.warn_external("Converter may not be compatible with date formatting.")


class ConciseDateFormatter(ticker.Formatter):
"""
Expand Down Expand Up @@ -872,6 +877,10 @@ def get_offset(self):
def format_data_short(self, value):
return num2date(value, tz=self._tz).strftime('%Y-%m-%d %H:%M:%S')

def validate_converter(self, converter, axisinfo):
if axisinfo.description != "days since matplotlib epoch":
_api.warn_external("Converter may not be compatible with date formatting.")


class AutoDateFormatter(ticker.Formatter):
"""
Expand Down Expand Up @@ -992,6 +1001,9 @@ def __call__(self, x, pos=None):

return result

def validate_converter(self, converter, axisinfo):
self._formatter.validate_converter(converter, axisinfo)


class rrulewrapper:
"""
Expand Down Expand Up @@ -1804,7 +1816,8 @@ def axisinfo(self, unit, axis):
datemax = datetime.date(1970, 1, 2)

return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='',
default_limits=(datemin, datemax))
default_limits=(datemin, datemax),
description="days since matplotlib epoch")

@staticmethod
def convert(value, unit, axis):
Expand Down Expand Up @@ -1861,7 +1874,8 @@ def axisinfo(self, unit, axis):
datemin = datetime.date(1970, 1, 1)
datemax = datetime.date(1970, 1, 2)
return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='',
default_limits=(datemin, datemax))
default_limits=(datemin, datemax),
description="days since matplotlib epoch")


class _SwitchableDateConverter:
Expand Down
10 changes: 9 additions & 1 deletion lib/matplotlib/testing/jpl_units/EpochConverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ def axisinfo(unit, axis):
# docstring inherited
majloc = date_ticker.AutoDateLocator()
majfmt = date_ticker.AutoDateFormatter(majloc)
return units.AxisInfo(majloc=majloc, majfmt=majfmt, label=unit)

# There is actually an off by one error here, but allow DateFormatters
descr = "days since matplotlib epoch"
return units.AxisInfo(
majloc=majloc,
majfmt=majfmt,
label=unit,
description=descr,
)

@staticmethod
def float2epoch(value, unit):
Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ def _set_locator(self, locator):
"""Subclasses may want to override this to set a locator."""
pass

def validate_converter(self, converter, axisinfo):
"""Raise an exception if the converter is not valid for this formatter."""
# By default, accept any converter
pass


class NullFormatter(Formatter):
"""Always return the empty string."""
Expand Down
4 changes: 3 additions & 1 deletion lib/matplotlib/ticker.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from matplotlib.axis import Axis
from matplotlib.transforms import Transform
from matplotlib.projections.polar import _AxisWrapper
from matplotlib.transforms import Transform
from matplotlib.units import ConversionInterface, AxisInfo

from collections.abc import Callable, Sequence
from typing import Any, Literal
Expand Down Expand Up @@ -31,6 +32,7 @@ class Formatter(TickHelper):
def set_locs(self, locs: list[float]) -> None: ...
@staticmethod
def fix_minus(s: str) -> str: ...
def validate_converter(self, converter: ConversionInterface, axisinfo: AxisInfo) -> None: ...

class NullFormatter(Formatter): ...

Expand Down
9 changes: 6 additions & 3 deletions lib/matplotlib/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class AxisInfo:
"""
def __init__(self, majloc=None, minloc=None,
majfmt=None, minfmt=None, label=None,
default_limits=None):
default_limits=None, description=None):
"""
Parameters
----------
Expand All @@ -89,8 +89,10 @@ def __init__(self, majloc=None, minloc=None,
label : str, optional
The default axis label.
default_limits : optional
The default min and max limits of the axis if no data has
been plotted.
The default min and max limits of the axis if no data has been plotted.
description: str, optional
A human readable description which may additionally be used to validate
converters.

Notes
-----
Expand All @@ -103,6 +105,7 @@ def __init__(self, majloc=None, minloc=None,
self.minfmt = minfmt
self.label = label
self.default_limits = default_limits
self.description = description


class ConversionInterface:
Expand Down