Skip to content

Commit ebcc968

Browse files
committed
Simplify _preprocess_data using Signature.bind.
Public API change: `step` no longer defaults to using `y` as label_namer. This is consistent with other functions that wrap `plot` (`plot` itself, `loglog`, etc.). (Alternatively, we could make all these functions use `y` as label_namer; I don't really care either way.) The plot-specific data kwarg logic was moved to `_process_plot_var_args`, dropping the need for general callable `positional_parameter_names`, `_plot_args_replacer`, and `positional_parameter_names`. `test_positional_parameter_names_as_function` and tests using `plot_func_varargs` were removed as a consequence. `replace_all_args` can be replaced by making `replace_names=None` trigger replacement of all args, even the "unknown" ones. There was no real use of "replace all known args but not unknown ones" (even if there was, this can easily be handled by explicitly listing the args in replace_names). `test_function_call_with_replace_all_args` was removed as a consequence. `replace_names` no longer complains if some argument names it is given are not present in the "explicit" signature, as long as the function accepts `**kwargs` -- because it may find the arguments in kwargs instead. label_namer no longer triggers if `data` is not passed (if the argument specified by label_namer was a string, then it is likely a categorical and shouldn't be considered as a label anyways). `test_label_problems_at_runtime` was renamed to `test_label_namer_only_if_data` and modified accordingly. Calling data-replaced functions used to trigger RuntimeError in some cases of mismatched arguments; they now trigger TypeError similarly to how normal functions do (`test_more_args_than_pos_parameters`).
1 parent 9595a7d commit ebcc968

File tree

4 files changed

+186
-427
lines changed

4 files changed

+186
-427
lines changed

lib/matplotlib/__init__.py

+110-227
Original file line numberDiff line numberDiff line change
@@ -1503,19 +1503,19 @@ def test(verbosity=None, coverage=False, switch_backend_warn=True,
15031503
test.__test__ = False # pytest: this function is not a test
15041504

15051505

1506-
def _replacer(data, key):
1506+
def _replacer(data, value):
15071507
"""Either returns data[key] or passes data back. Also
15081508
converts input data to a sequence as needed.
15091509
"""
1510-
# if key isn't a string don't bother
1511-
if not isinstance(key, str):
1512-
return key
1513-
# try to use __getitem__
15141510
try:
1515-
return sanitize_sequence(data[key])
1516-
# key does not exist, silently fall back to key
1517-
except KeyError:
1518-
return key
1511+
# if key isn't a string don't bother
1512+
if isinstance(value, str):
1513+
# try to use __getitem__
1514+
value = data[value]
1515+
except Exception:
1516+
# key does not exist, silently fall back to key
1517+
pass
1518+
return sanitize_sequence(value)
15191519

15201520

15211521
_DATA_DOC_APPENDIX = """
@@ -1529,43 +1529,33 @@ def _replacer(data, key):
15291529
"""
15301530

15311531

1532-
def _add_data_doc(docstring, replace_names, replace_all_args):
1532+
def _add_data_doc(docstring, replace_names):
15331533
"""Add documentation for a *data* field to the given docstring.
15341534
15351535
Parameters
15361536
----------
15371537
docstring : str
15381538
The input docstring.
1539-
replace_names : list of strings or None
1539+
replace_names : List[str] or None
15401540
The list of parameter names which arguments should be replaced by
1541-
`data[name]`. If None, all arguments are replaced if they are
1542-
included in `data`.
1543-
replace_all_args : bool
1544-
If True, all arguments in *args get replaced, even if they are not
1545-
in replace_names.
1541+
``data[name]`` (if ``data[name]`` does not throw an exception). If
1542+
None, replacement is attempted for all arguments.
15461543
15471544
Returns
15481545
-------
15491546
The augmented docstring.
15501547
"""
1551-
if docstring is None:
1552-
docstring = ''
1553-
else:
1554-
docstring = dedent(docstring)
1555-
_repl = ""
1556-
if replace_names is None:
1557-
_repl = "* All positional and all keyword arguments."
1558-
else:
1559-
if len(replace_names) != 0:
1560-
_repl = "* All arguments with the following names: '{names}'."
1561-
if replace_all_args:
1562-
_repl += "\n * All positional arguments."
1563-
_repl = _repl.format(names="', '".join(sorted(replace_names)))
1564-
return docstring + _DATA_DOC_APPENDIX.format(replaced=_repl)
1548+
docstring = dedent(docstring) if docstring is not None else ""
1549+
repl = ("* All positional and all keyword arguments."
1550+
if replace_names is None else
1551+
""
1552+
if len(replace_names) == 0 else
1553+
"* All arguments with the following names: {}.".format(
1554+
", ".join(map(repr, sorted(replace_names)))))
1555+
return docstring + _DATA_DOC_APPENDIX.format(replaced=repl)
15651556

15661557

1567-
def _preprocess_data(replace_names=None, replace_all_args=False,
1568-
label_namer=None, positional_parameter_names=None):
1558+
def _preprocess_data(func=None, *, replace_names=None, label_namer=None):
15691559
"""
15701560
A decorator to add a 'data' kwarg to any a function. The signature
15711561
of the input function must include the ax argument at the first position ::
@@ -1576,216 +1566,109 @@ def foo(ax, *args, **kwargs)
15761566
15771567
Parameters
15781568
----------
1579-
replace_names : list of strings, optional, default: None
1569+
replace_names : List[str] or None, optional, default: None
15801570
The list of parameter names which arguments should be replaced by
1581-
`data[name]`. If None, all arguments are replaced if they are
1582-
included in `data`.
1583-
replace_all_args : bool, default: False
1584-
If True, all arguments in *args get replaced, even if they are not
1585-
in replace_names.
1571+
``data[name]`` (if ``data[name]`` does not throw an exception). If
1572+
None, replacement is attempted for all arguments.
15861573
label_namer : string, optional, default: None
15871574
The name of the parameter which argument should be used as label, if
15881575
label is not set. If None, the label keyword argument is not set.
1589-
positional_parameter_names : list of strings or callable, optional
1590-
The full list of positional parameter names (excluding an explicit
1591-
`ax`/'self' argument at the first place and including all possible
1592-
positional parameter in `*args`), in the right order. Can also include
1593-
all other keyword parameter. Only needed if the wrapped function does
1594-
contain `*args` and (replace_names is not None or replace_all_args is
1595-
False). If it is a callable, it will be called with the actual
1596-
tuple of *args and the data and should return a list like
1597-
above.
1598-
NOTE: callables should only be used when the names and order of *args
1599-
can only be determined at runtime. Please use list of names
1600-
when the order and names of *args is clear before runtime!
16011576
16021577
.. note:: decorator also converts MappingView input data to list.
16031578
"""
1579+
1580+
if func is None:
1581+
return functools.partial(
1582+
_preprocess_data,
1583+
replace_names=replace_names, label_namer=label_namer)
1584+
1585+
sig = inspect.signature(func)
1586+
varargs_name = None
1587+
varkwargs_name = None
1588+
arg_names = []
1589+
params = list(sig.parameters.values())
1590+
for p in params:
1591+
if p.kind is Parameter.VAR_POSITIONAL:
1592+
varargs_name = p.name
1593+
elif p.kind is Parameter.VAR_KEYWORD:
1594+
varkwargs_name = p.name
1595+
else:
1596+
arg_names.append(p.name)
1597+
data_param = Parameter("data", Parameter.KEYWORD_ONLY, default=None)
1598+
if varkwargs_name:
1599+
params.insert(-1, data_param)
1600+
else:
1601+
params.append(data_param)
1602+
new_sig = sig.replace(parameters=params)
1603+
arg_names = arg_names[1:] # remove the first "ax" / self arg
1604+
16041605
if replace_names is not None:
16051606
replace_names = set(replace_names)
16061607

1607-
def param(func):
1608-
sig = inspect.signature(func)
1609-
_has_varargs = False
1610-
_has_varkwargs = False
1611-
_arg_names = []
1612-
params = list(sig.parameters.values())
1613-
for p in params:
1614-
if p.kind is Parameter.VAR_POSITIONAL:
1615-
_has_varargs = True
1616-
elif p.kind is Parameter.VAR_KEYWORD:
1617-
_has_varkwargs = True
1608+
assert (replace_names or set()) <= set(arg_names) or varkwargs_name, (
1609+
"Matplotlib internal error: invalid replace_names ({!r}) for {!r}"
1610+
.format(replace_names, func.__name__))
1611+
assert label_namer is None or label_namer in arg_names or varkwargs_name, (
1612+
"Matplotlib internal error: invalid label_namer ({!r}) for {!r}"
1613+
.format(label_namer, func.__name__))
1614+
1615+
@functools.wraps(func)
1616+
def inner(ax, *args, **kwargs):
1617+
data = kwargs.pop("data", None)
1618+
if data is None:
1619+
return func(ax, *map(sanitize_sequence, args), **kwargs)
1620+
1621+
bound = new_sig.bind(ax, *args, **kwargs)
1622+
needs_label = (label_namer
1623+
and "label" not in bound.arguments
1624+
and "label" not in bound.kwargs)
1625+
auto_label = (bound.arguments.get(label_namer)
1626+
or bound.kwargs.get(label_namer))
1627+
if not isinstance(auto_label, str):
1628+
auto_label = None
1629+
1630+
for k, v in bound.arguments.items():
1631+
if k == varkwargs_name:
1632+
for k1, v1 in v.items():
1633+
if replace_names is None or k1 in replace_names:
1634+
v[k1] = _replacer(data, v1)
1635+
elif k == varargs_name:
1636+
if replace_names is None:
1637+
bound.arguments[k] = tuple(
1638+
_replacer(data, v1) for v1 in v)
16181639
else:
1619-
_arg_names.append(p.name)
1620-
data_param = Parameter('data', Parameter.KEYWORD_ONLY, default=None)
1621-
if _has_varkwargs:
1622-
params.insert(-1, data_param)
1623-
else:
1624-
params.append(data_param)
1625-
new_sig = sig.replace(parameters=params)
1626-
# Import-time check: do we have enough information to replace *args?
1627-
arg_names_at_runtime = False
1628-
# there can't be any positional arguments behind *args and no
1629-
# positional args can end up in **kwargs, so only *varargs make
1630-
# problems.
1631-
# http://stupidpythonideas.blogspot.de/2013/08/arguments-and-parameters.html
1632-
if not _has_varargs:
1633-
# all args are "named", so no problem
1634-
# remove the first "ax" / self arg
1635-
arg_names = _arg_names[1:]
1636-
else:
1637-
# Here we have "unnamed" variables and we need a way to determine
1638-
# whether to replace a arg or not
1639-
if replace_names is None:
1640-
# all argnames should be replaced
1641-
arg_names = None
1642-
elif len(replace_names) == 0:
1643-
# No argnames should be replaced
1644-
arg_names = []
1645-
elif len(_arg_names) > 1 and (positional_parameter_names is None):
1646-
# we got no manual parameter names but more than an 'ax' ...
1647-
if len(replace_names - set(_arg_names[1:])) == 0:
1648-
# all to be replaced arguments are in the list
1649-
arg_names = _arg_names[1:]
1650-
else:
1651-
raise AssertionError(
1652-
"Got unknown 'replace_names' and wrapped function "
1653-
"{!r} uses '*args', need 'positional_parameter_names'"
1654-
.format(func.__name__))
1640+
if replace_names is None or k in replace_names:
1641+
bound.arguments[k] = _replacer(data, v)
1642+
1643+
bound.apply_defaults()
1644+
del bound.arguments["data"]
1645+
1646+
all_kwargs = {**bound.arguments, **bound.kwargs}
1647+
if needs_label:
1648+
if label_namer not in all_kwargs:
1649+
warnings.warn(
1650+
"Tried to set a label via parameter %r in func %r but "
1651+
"couldn't find such an argument.\n"
1652+
"(This is a programming error, please report to "
1653+
"the Matplotlib list!)" % (label_namer, func.__name__),
1654+
RuntimeWarning, stacklevel=2)
16551655
else:
1656-
if positional_parameter_names is not None:
1657-
if callable(positional_parameter_names):
1658-
# determined by the function at runtime
1659-
arg_names_at_runtime = True
1660-
# so that we don't compute the label_pos at import time
1661-
arg_names = []
1662-
else:
1663-
arg_names = positional_parameter_names
1656+
label = get_label(all_kwargs[label_namer], auto_label)
1657+
if "label" in arg_names:
1658+
bound.arguments["label"] = label
1659+
try:
1660+
bound.arguments.move_to_end(varkwargs_name)
1661+
except KeyError:
1662+
pass
16641663
else:
1665-
if replace_all_args:
1666-
arg_names = []
1667-
else:
1668-
raise AssertionError(
1669-
"Got 'replace_names' and wrapped function {!r} "
1670-
"uses *args, need 'positional_parameter_names' or "
1671-
"'replace_all_args'".format(func.__name__))
1672-
1673-
# compute the possible label_namer and label position in positional
1674-
# arguments
1675-
label_pos = 9999 # bigger than all "possible" argument lists
1676-
label_namer_pos = 9999 # bigger than all "possible" argument lists
1677-
if (label_namer and # we actually want a label here ...
1678-
arg_names and # and we can determine a label in *args ...
1679-
label_namer in arg_names): # and it is in *args
1680-
label_namer_pos = arg_names.index(label_namer)
1681-
if "label" in arg_names:
1682-
label_pos = arg_names.index("label")
1683-
1684-
# Check the case we know a label_namer but we can't find it the
1685-
# arg_names... Unfortunately the label_namer can be in **kwargs,
1686-
# which we can't detect here and which results in a non-set label
1687-
# which might surprise the user :-(
1688-
if label_namer and not arg_names_at_runtime and not _has_varkwargs:
1689-
if not arg_names:
1690-
raise AssertionError(
1691-
"label_namer {!r} can't be found as the parameter without "
1692-
"'positional_parameter_names'".format(label_namer))
1693-
elif label_namer not in arg_names:
1694-
raise AssertionError(
1695-
"label_namer {!r} can't be found in the parameter names "
1696-
"(known argnames: %s).".format(label_namer, arg_names))
1697-
else:
1698-
# this is the case when the name is in arg_names
1699-
pass
1700-
1701-
@functools.wraps(func)
1702-
def inner(ax, *args, **kwargs):
1703-
# this is needed because we want to change these values if
1704-
# arg_names_at_runtime==True, but python does not allow assigning
1705-
# to a variable in a outer scope. So use some new local ones and
1706-
# set them to the already computed values.
1707-
_label_pos = label_pos
1708-
_label_namer_pos = label_namer_pos
1709-
_arg_names = arg_names
1710-
1711-
label = None
1664+
bound.arguments.setdefault(
1665+
varkwargs_name, {})["label"] = label
17121666

1713-
data = kwargs.pop('data', None)
1667+
return func(*bound.args, **bound.kwargs)
17141668

1715-
if data is None: # data validation
1716-
args = tuple(sanitize_sequence(a) for a in args)
1717-
else:
1718-
if arg_names_at_runtime:
1719-
# update the information about replace names and
1720-
# label position
1721-
_arg_names = positional_parameter_names(args, data)
1722-
if (label_namer and # we actually want a label here ...
1723-
_arg_names and # and we can find a label in *args
1724-
(label_namer in _arg_names)): # and it is in *args
1725-
_label_namer_pos = _arg_names.index(label_namer)
1726-
if "label" in _arg_names:
1727-
_label_pos = arg_names.index("label")
1728-
1729-
# save the current label_namer value so that it can be used as
1730-
# a label
1731-
if _label_namer_pos < len(args):
1732-
label = args[_label_namer_pos]
1733-
else:
1734-
label = kwargs.get(label_namer, None)
1735-
# ensure a string, as label can't be anything else
1736-
if not isinstance(label, str):
1737-
label = None
1738-
1739-
if (replace_names is None) or (replace_all_args is True):
1740-
# all should be replaced
1741-
args = tuple(_replacer(data, a) for
1742-
j, a in enumerate(args))
1743-
else:
1744-
# An arg is replaced if the arg_name of that position is
1745-
# in replace_names ...
1746-
if len(_arg_names) < len(args):
1747-
raise RuntimeError(
1748-
"Got more args than function expects")
1749-
args = tuple(_replacer(data, a)
1750-
if _arg_names[j] in replace_names else a
1751-
for j, a in enumerate(args))
1752-
1753-
if replace_names is None:
1754-
# replace all kwargs ...
1755-
kwargs = {k: _replacer(data, v) for k, v in kwargs.items()}
1756-
else:
1757-
# ... or only if a kwarg of that name is in replace_names
1758-
kwargs = {
1759-
k: _replacer(data, v) if k in replace_names else v
1760-
for k, v in kwargs.items()}
1761-
1762-
# replace the label if this func "wants" a label arg and the user
1763-
# didn't set one. Note: if the user puts in "label=None", it does
1764-
# *NOT* get replaced!
1765-
user_supplied_label = (
1766-
len(args) >= _label_pos or # label is included in args
1767-
'label' in kwargs # ... or in kwargs
1768-
)
1769-
if label_namer and not user_supplied_label:
1770-
if _label_namer_pos < len(args):
1771-
kwargs['label'] = get_label(args[_label_namer_pos], label)
1772-
elif label_namer in kwargs:
1773-
kwargs['label'] = get_label(kwargs[label_namer], label)
1774-
else:
1775-
warnings.warn(
1776-
"Tried to set a label via parameter %r in func %r but "
1777-
"couldn't find such an argument.\n"
1778-
"(This is a programming error, please report to "
1779-
"the Matplotlib list!)" % (label_namer, func.__name__),
1780-
RuntimeWarning, stacklevel=2)
1781-
return func(ax, *args, **kwargs)
1782-
1783-
inner.__doc__ = _add_data_doc(inner.__doc__,
1784-
replace_names, replace_all_args)
1785-
inner.__signature__ = new_sig
1786-
return inner
1787-
1788-
return param
1669+
inner.__doc__ = _add_data_doc(inner.__doc__, replace_names)
1670+
inner.__signature__ = new_sig
1671+
return inner
17891672

17901673
_log.debug('matplotlib version %s', __version__)
17911674
_log.debug('interactive is %s', is_interactive())

0 commit comments

Comments
 (0)