Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Allow FuncFormatter to take functions with only one field #17288

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 11 commits into
base: main
Choose a base branch
from
Draft
2 changes: 1 addition & 1 deletion lib/matplotlib/tests/test_polar.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def test_polar_units_2(fig_test, fig_ref):

ax = fig_ref.add_subplot(projection="polar")
ax.plot(np.deg2rad(xs), ys)
ax.xaxis.set_major_formatter(mpl.ticker.FuncFormatter("{:.12}".format))
ax.xaxis.set_major_formatter(mpl.ticker.StrMethodFormatter("{x:.12}"))
ax.set(xlabel="rad", ylabel="km")


Expand Down
25 changes: 25 additions & 0 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,31 @@ def test_basic(self):
assert '00002' == tmp_form(2)


class TestFuncFormatter:
@pytest.mark.parametrize("func, args, expected",
[(lambda x: f"{x}!", [2], "2!"),
(lambda x, pos: f"{x}+{pos}!", [2, 3], "2+3!")])
def test_arguments(self, func, args, expected):
assert expected == mticker.FuncFormatter(func)(*args)

@pytest.mark.parametrize("func, args, expected",
[("{}!".format, [2], "2!"),
("{}+{}!".format, [2, 3], "2+3!")])
def test_builtins(self, func, args, expected):
with pytest.raises(UserWarning, match=r'not support format'):
assert expected == mticker.FuncFormatter(func)(*args)

def test_typerror(self):
with pytest.raises(TypeError, match=r'must take at most'):
mticker.FuncFormatter((lambda x, y, z: " "))

def test_update(self):
formatter = mticker.FuncFormatter(lambda x, pos: f"{x}+{pos}")
assert "1+2" == formatter(1, 2)
with pytest.raises(TypeError, match=r'must take at most'):
formatter.func = lambda x, pos, error: "!"


class TestStrMethodFormatter:
test_data = [
('{x:05d}', (2,), '00002'),
Expand Down
38 changes: 34 additions & 4 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
"""

import itertools
import inspect
import logging
import locale
import math
Expand Down Expand Up @@ -377,9 +378,9 @@ class FuncFormatter(Formatter):
"""
Use a user-defined function for formatting.

The function should take in two inputs (a tick value ``x`` and a
position ``pos``), and return a string containing the corresponding
tick label.
The function can take in at most two inputs (a required tick value ``x``
and an optional position ``pos``), and must return a string containing
the corresponding tick label.
"""
def __init__(self, func):
self.func = func
Expand All @@ -390,7 +391,36 @@ def __call__(self, x, pos=None):

*x* and *pos* are passed through as-is.
"""
return self.func(x, pos)
if self._nargs == 1:
return self._func(x)
return self._func(x, pos)

@property
def func(self):
return self._func

@func.setter
def func(self, func):
try:
sig = inspect.signature(func)
except ValueError:
self._nargs = 2
cbook._warn_external("FuncFormatter may not support "
f"{func.__name__}. Please look at the "
"other formatters in `matplotlib.ticker`.")
else:
try:
sig.bind(None, None)
self._nargs = 2
except TypeError:
try:
sig.bind(None)
self._nargs = 1
except TypeError:
raise TypeError(f"{func.__name__} must take "
"at most 2 arguments: "
"x (required), pos (optional).")
Comment on lines +404 to +422
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm over-simplifying, but can we just try?

Suggested change
try:
sig = inspect.signature(func)
except ValueError:
self._nargs = 2
cbook._warn_external("FuncFormatter may not support "
f"{func.__name__}. Please look at the "
"other formatters in `matplotlib.ticker`.")
else:
try:
sig.bind(None, None)
self._nargs = 2
except TypeError:
try:
sig.bind(None)
self._nargs = 1
except TypeError:
raise TypeError(f"{func.__name__} must take "
"at most 2 arguments: "
"x (required), pos (optional).")
self._nargs = 2
# check whether func really suppors two args
try:
func(0, 0)
except TypeError as e:
# Only catch the specific error that a single-arg func would raise:
# TypeError: func() takes 1 positional argument but 2 were given
# every other error is not our business here and will raise on
# actual usage. Since the TypeError will be thrown before any
# internal errors in the function, this approach is a safe way to
# identify single-arg functions without introspection need.
if "1 positional argument" in str(e):
self._nargs = 1

The only risk I see is that python changes its exception message, but i) by testing only against "1 positional argument" were more resilent wrt minor changes, and ii) we should add a test that monitors the exception message thrown in such cases.

self._func = func


class FormatStrFormatter(Formatter):
Expand Down