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

Skip to content

Commit c768f0d

Browse files
committed
ENH: accept a function for arg names in @unpack_labeled_data
Now variable length *args can be named and individually replaced. This unfortunately does not help for plot() because some abiguity as there is no way to know in some cases if a users wants to get something replaced or not: ```python data = {"x":X,"y":Y, "c":C} plot(x,y,c,x,y,c, data=data) plot(x,y,x,y,x,y, data=data) ``` Here the first call will get all args replaced and matplotlib would then see all 6 arrays which would translate into three lines with the same color information (like the second call). In some cases this can be catched (like for one/three lines with 'x,y,c' combos -> 3/9 args, will end up with one "missing" if all are treated as replaceable) but in some cases it's not possible without guesswork. :-( But in other settings, where the number of args determines the order/names, this works. Also activate the tests for the first time...
1 parent 8dbb2cc commit c768f0d

File tree

2 files changed

+190
-40
lines changed

2 files changed

+190
-40
lines changed

lib/matplotlib/__init__.py

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,7 @@ def tk_window_focus():
14581458
'matplotlib.tests.test_triangulation',
14591459
'matplotlib.tests.test_widgets',
14601460
'matplotlib.tests.test_cycles',
1461+
'matplotlib.tests.test_labeled_data_unpacking',
14611462
'matplotlib.sphinxext.tests.test_tinypages',
14621463
'mpl_toolkits.tests.test_mplot3d',
14631464
'mpl_toolkits.tests.test_axes_grid1',
@@ -1566,18 +1567,21 @@ def foo(ax, *args, **kwargs)
15661567
replace_all_args : bool, default: False
15671568
If True, all arguments in *args get replaced, even if they are not
15681569
in replace_names.
1569-
NOTE: this should be used only when the order of the names depends on
1570-
the number of *args.
15711570
label_namer : string, optional, default: None
15721571
The name of the parameter which argument should be used as label, if
15731572
label is not set. If None, the label keyword argument is not set.
1574-
positional_parameter_names : list of strings, optional, default: None
1573+
positional_parameter_names : list of strings or callable, optional
15751574
The full list of positional parameter names (excluding an explicit
15761575
`ax`/'self' argument at the first place and including all possible
15771576
positional parameter in `*args`), in the right order. Can also include
15781577
all other keyword parameter. Only needed if the wrapped function does
15791578
contain `*args` and (replace_names is not None or replace_all_args is
1580-
False).
1579+
False). If it is a callable, it will be called with the actual
1580+
tuple of *args and the data and should return a list like
1581+
above.
1582+
NOTE: callables should only be used when the names and order of *args
1583+
can only be determined at runtime. Please use list of names
1584+
when the order and names of *args is clear before runtime!
15811585
"""
15821586
if replace_names is not None:
15831587
replace_names = set(replace_names)
@@ -1590,16 +1594,19 @@ def param(func):
15901594
_has_no_varargs = arg_spec.varargs is None
15911595
_has_varkwargs = arg_spec.keywords is not None
15921596

1597+
# Import-time check: do we have enough information to replace *args?
1598+
arg_names_at_runtime = False
15931599
# there can't be any positional arguments behind *args and no
1594-
# positional args can end up in **kwargs, so we only need to check for
1595-
# varargs:
1600+
# positional args can end up in **kwargs, so only *varargs make
1601+
# problems.
15961602
# http://stupidpythonideas.blogspot.de/2013/08/arguments-and-parameters.html
15971603
if _has_no_varargs:
1604+
# all args are "named", so no problem
15981605
# remove the first "ax" / self arg
15991606
arg_names = _arg_names[1:]
16001607
else:
1601-
# in this case we need a supplied list of arguments or we need to
1602-
# replace all variables -> compile time check
1608+
# Here we have "unnamed" variables and we need a way to determine
1609+
# whether to replace a arg or not
16031610
if replace_names is None:
16041611
# all argnames should be replaced
16051612
arg_names = None
@@ -1618,7 +1625,13 @@ def param(func):
16181625
raise AssertionError(msg % func.__name__)
16191626
else:
16201627
if positional_parameter_names is not None:
1621-
arg_names = positional_parameter_names
1628+
if callable(positional_parameter_names):
1629+
# determined by the function at runtime
1630+
arg_names_at_runtime = True
1631+
# so that we don't compute the label_pos at import time
1632+
arg_names = []
1633+
else:
1634+
arg_names = positional_parameter_names
16221635
else:
16231636
if replace_all_args:
16241637
arg_names = []
@@ -1633,7 +1646,9 @@ def param(func):
16331646
# arguments
16341647
label_pos = 9999 # bigger than all "possible" argument lists
16351648
label_namer_pos = 9999 # bigger than all "possible" argument lists
1636-
if label_namer and arg_names and (label_namer in arg_names):
1649+
if (label_namer # we actually want a label here ...
1650+
and arg_names # and we can determine a label in *args ...
1651+
and (label_namer in arg_names)): # and it is in *args
16371652
label_namer_pos = arg_names.index(label_namer)
16381653
if "label" in arg_names:
16391654
label_pos = arg_names.index("label")
@@ -1642,7 +1657,7 @@ def param(func):
16421657
# arg_names... Unfortunately the label_namer can be in **kwargs,
16431658
# which we can't detect here and which results in a non-set label
16441659
# which might surprise the user :-(
1645-
if label_namer and not _has_varkwargs:
1660+
if label_namer and not arg_names_at_runtime and not _has_varkwargs:
16461661
if not arg_names:
16471662
msg = ("label_namer '%s' can't be found as the parameter "
16481663
"without 'positional_parameter_names'.")
@@ -1657,53 +1672,74 @@ def param(func):
16571672

16581673
@functools.wraps(func)
16591674
def inner(ax, *args, **kwargs):
1660-
data = kwargs.pop('data', None)
1675+
# this is needed because we want to change these values if
1676+
# arg_names_at_runtime==True, but python does not allow assigning
1677+
# to a variable in a outer scope. So use some new local ones and
1678+
# set them to the already computed values.
1679+
_label_pos = label_pos
1680+
_label_namer_pos = label_namer_pos
1681+
_arg_names = arg_names
1682+
16611683
label = None
1684+
1685+
data = kwargs.pop('data', None)
16621686
if data is not None:
1687+
if arg_names_at_runtime:
1688+
# update the information about replace names and
1689+
# label position
1690+
_arg_names = positional_parameter_names(args, data)
1691+
if (label_namer # we actually want a label here ...
1692+
and _arg_names # and we can find a label in *args ...
1693+
and (label_namer in _arg_names)): # and it is in *args
1694+
_label_namer_pos = _arg_names.index(label_namer)
1695+
if "label" in _arg_names:
1696+
_label_pos = arg_names.index("label")
1697+
16631698
# save the current label_namer value so that it can be used as
16641699
# a label
1665-
if label_namer_pos < len(args):
1666-
label = args[label_namer_pos]
1700+
if _label_namer_pos < len(args):
1701+
label = args[_label_namer_pos]
16671702
else:
16681703
label = kwargs.get(label_namer, None)
16691704
# ensure a string, as label can't be anything else
16701705
if not isinstance(label, six.string_types):
16711706
label = None
16721707

1673-
if (replace_names is None) or replace_all_args:
1708+
if (replace_names is None) or (replace_all_args is True):
16741709
# all should be replaced
16751710
args = tuple(_replacer(data, a) for
16761711
j, a in enumerate(args))
16771712
else:
1678-
# An arg is replaced if the arg_name of that position is in
1679-
# replace_names ...
1680-
if len(arg_names) < len(args):
1713+
# An arg is replaced if the arg_name of that position is
1714+
# in replace_names ...
1715+
if len(_arg_names) < len(args):
16811716
raise RuntimeError(
16821717
"Got more args than function expects")
16831718
args = tuple(_replacer(data, a)
1684-
if arg_names[j] in replace_names else a
1719+
if _arg_names[j] in replace_names else a
16851720
for j, a in enumerate(args))
16861721

16871722
if replace_names is None:
1723+
# replace all kwargs ...
16881724
kwargs = dict((k, _replacer(data, v))
16891725
for k, v in six.iteritems(kwargs))
16901726
else:
1691-
# ... or a kwarg of that name in replace_names
1727+
# ... or only if a kwarg of that name is in replace_names
16921728
kwargs = dict((k, _replacer(data, v)
16931729
if k in replace_names else v)
16941730
for k, v in six.iteritems(kwargs))
16951731

16961732
# replace the label if this func "wants" a label arg and the user
1697-
# didn't set one Note: if the usere puts in "label=None", it does
1733+
# didn't set one. Note: if the user puts in "label=None", it does
16981734
# *NOT* get replaced!
16991735
user_supplied_label = (
1700-
(len(args) >= label_pos) or # label is included in args
1736+
(len(args) >= _label_pos) or # label is included in args
17011737
('label' in kwargs) # ... or in kwargs
17021738
)
17031739
if (label_namer and not user_supplied_label):
1704-
if label_namer_pos < len(args):
1740+
if _label_namer_pos < len(args):
17051741
try:
1706-
kwargs['label'] = args[label_namer_pos].name
1742+
kwargs['label'] = args[_label_namer_pos].name
17071743
except AttributeError:
17081744
kwargs['label'] = label
17091745
elif label_namer in kwargs:
@@ -1740,6 +1776,7 @@ def inner(ax, *args, **kwargs):
17401776
return inner
17411777
return param
17421778

1779+
17431780
verbose.report('matplotlib version %s' % __version__)
17441781
verbose.report('verbose.level %s' % verbose.level)
17451782
verbose.report('interactive is %s' % is_interactive())

0 commit comments

Comments
 (0)