From 7b69287a137e92ddc5323a87fe0899690b2f3290 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 30 Mar 2018 15:07:06 -0700 Subject: [PATCH 1/5] Inline _grab_next_args. --- lib/matplotlib/axes/_axes.py | 11 ++++------- lib/matplotlib/axes/_base.py | 16 ++++++---------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index e57963467e11..c3dbdb92995a 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -1368,7 +1368,8 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, return colls - # ### Basic plotting + #### Basic plotting + # The label_naming happens in `matplotlib.axes._base._plot_args` @_preprocess_data(replace_names=["x", "y"], positional_parameter_names=_plot_args_replacer, @@ -1601,14 +1602,10 @@ def plot(self, *args, scalex=True, scaley=True, **kwargs): 'k^:' # black triangle_up markers connected by a dotted line """ - lines = [] - kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D._alias_map) - - for line in self._get_lines(*args, **kwargs): + lines = [*self._get_lines(*args, **kwargs)] + for line in lines: self.add_line(line) - lines.append(line) - self.autoscale_view(scalex=scalex, scaley=scaley) return lines diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 6b89e26c8dcb..e914565cb92f 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -178,8 +178,12 @@ def __call__(self, *args, **kwargs): if yunits != self.axes.yaxis.units: self.axes.yaxis.set_units(yunits) - ret = self._grab_next_args(*args, **kwargs) - return ret + while args: + this, args = args[:2], args[2:] + if args and isinstance(args[0], str): + this += args[0], + args = args[1:] + yield from self._plot_args(this, kwargs) def get_next_color(self): """Return the next color in the cycle.""" @@ -381,14 +385,6 @@ def _plot_args(self, tup, kwargs): ret.append(seg) return ret - def _grab_next_args(self, *args, **kwargs): - while args: - this, args = args[:2], args[2:] - if args and isinstance(args[0], str): - this += args[0], - args = args[1:] - yield from self._plot_args(this, kwargs) - class _AxesBase(martist.Artist): """ From 2445465c3fca611a3d8c2c5d45fa95f58528de02 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 30 Mar 2018 15:41:07 -0700 Subject: [PATCH 2/5] 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`). --- doc/api/next_api_changes/2018-03-31-AL.rst | 6 + .../2018-08-17-AL-deprecations.rst | 1 + lib/matplotlib/__init__.py | 370 +++++++----------- lib/matplotlib/axes/_axes.py | 120 ++---- lib/matplotlib/axes/_base.py | 76 +++- lib/matplotlib/cbook/__init__.py | 1 + lib/matplotlib/tests/test_preprocess_data.py | 121 +----- 7 files changed, 247 insertions(+), 448 deletions(-) create mode 100644 doc/api/next_api_changes/2018-03-31-AL.rst diff --git a/doc/api/next_api_changes/2018-03-31-AL.rst b/doc/api/next_api_changes/2018-03-31-AL.rst new file mode 100644 index 000000000000..8d886568cf69 --- /dev/null +++ b/doc/api/next_api_changes/2018-03-31-AL.rst @@ -0,0 +1,6 @@ +Axes methods now raise TypeError instead of RuntimeError on mismatched calls +```````````````````````````````````````````````````````````````````````````` + +In certain cases, Axes methods (and pyplot functions) used to raise a +RuntimeError if they were called with a ``data`` kwarg and otherwise mismatched +arguments. They now raise a ``TypeError`` instead. diff --git a/doc/api/next_api_changes/2018-08-17-AL-deprecations.rst b/doc/api/next_api_changes/2018-08-17-AL-deprecations.rst index c195dfd2d057..86808c8dcabb 100644 --- a/doc/api/next_api_changes/2018-08-17-AL-deprecations.rst +++ b/doc/api/next_api_changes/2018-08-17-AL-deprecations.rst @@ -6,5 +6,6 @@ The following API elements are deprecated: - ``get_py2exe_datafiles``, ``tk_window_focus``, - ``backend_ps.PsBackendHelper``, ``backend_ps.ps_backend_helper``, - ``cbook.iterable``, +- ``cbook.get_label``, ``cbook.iterable``, - ``font_manager.OSXInstalledFonts``, - ``mlab.demean``, diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index b99026767032..d490d9bee908 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1499,19 +1499,29 @@ def test(verbosity=None, coverage=False, switch_backend_warn=True, test.__test__ = False # pytest: this function is not a test -def _replacer(data, key): - """Either returns data[key] or passes data back. Also - converts input data to a sequence as needed. +def _replacer(data, value): """ - # if key isn't a string don't bother - if not isinstance(key, str): - return key - # try to use __getitem__ + Either returns ``data[value]`` or passes ``data`` back, converts either to + a sequence. + """ + try: + # if key isn't a string don't bother + if isinstance(value, str): + # try to use __getitem__ + value = data[value] + except Exception: + # key does not exist, silently fall back to key + pass + return sanitize_sequence(value) + + +def _label_from_arg(y, default_name): try: - return sanitize_sequence(data[key]) - # key does not exist, silently fall back to key - except KeyError: - return key + return y.name + except AttributeError: + if isinstance(default_name, str): + return default_name + return None _DATA_DOC_APPENDIX = """ @@ -1528,261 +1538,151 @@ def _replacer(data, key): """ -def _add_data_doc(docstring, replace_names, replace_all_args): +def _add_data_doc(docstring, replace_names): """Add documentation for a *data* field to the given docstring. Parameters ---------- docstring : str The input docstring. - replace_names : list of strings or None + replace_names : list of str or None The list of parameter names which arguments should be replaced by - `data[name]`. If None, all arguments are replaced if they are - included in `data`. - replace_all_args : bool - If True, all arguments in *args get replaced, even if they are not - in replace_names. + ``data[name]`` (if ``data[name]`` does not throw an exception). If + None, replacement is attempted for all arguments. Returns ------- The augmented docstring. """ - if docstring is None: - docstring = '' - else: - docstring = dedent(docstring) - _repl = "" - if replace_names is None: - _repl = "* All positional and all keyword arguments." - else: - if len(replace_names) != 0: - _repl = "* All arguments with the following names: '{names}'." - if replace_all_args: - _repl += "\n * All positional arguments." - _repl = _repl.format(names="', '".join(sorted(replace_names))) - return docstring + _DATA_DOC_APPENDIX.format(replaced=_repl) + docstring = dedent(docstring) if docstring is not None else "" + repl = ("* All positional and all keyword arguments." + if replace_names is None else + "" + if len(replace_names) == 0 else + "* All arguments with the following names: {}.".format( + ", ".join(map(repr, sorted(replace_names))))) + return docstring + _DATA_DOC_APPENDIX.format(replaced=repl) -def _preprocess_data(replace_names=None, replace_all_args=False, - label_namer=None, positional_parameter_names=None): +def _preprocess_data(func=None, *, replace_names=None, label_namer=None): """ - A decorator to add a 'data' kwarg to any a function. The signature - of the input function must include the ax argument at the first position :: + A decorator to add a 'data' kwarg to a function. - def foo(ax, *args, **kwargs) + :: + @_preprocess_data() + def func(ax, *args, **kwargs): ... - so this is suitable for use with Axes methods. + is a function with signature ``decorated(ax, *args, data=None, **kwargs)`` + with the following behavior: + + - if called with ``data=None``, forward the other arguments to ``func``; + - otherwise, *data* must be a mapping; for any argument passed in as a + string ``name``, replace the argument by ``data[name]`` (if this does not + throw an exception), then forward the arguments to ``func``. + + In either case, any argument that is a `MappingView` is also converted to a + list. Parameters ---------- - replace_names : list of strings, optional, default: None - The list of parameter names which arguments should be replaced by - `data[name]`. If None, all arguments are replaced if they are - included in `data`. - replace_all_args : bool, default: False - If True, all arguments in *args get replaced, even if they are not - in replace_names. + replace_names : list of str or None, optional, default: None + The list of parameter names for which lookup into *data* should be + attempted. If None, replacement is attempted for all arguments. label_namer : string, optional, default: None - The name of the parameter which argument should be used as label, if - label is not set. If None, the label keyword argument is not set. - positional_parameter_names : list of strings or callable, optional - The full list of positional parameter names (excluding an explicit - `ax`/'self' argument at the first place and including all possible - positional parameter in `*args`), in the right order. Can also include - all other keyword parameter. Only needed if the wrapped function does - contain `*args` and (replace_names is not None or replace_all_args is - False). If it is a callable, it will be called with the actual - tuple of *args and the data and should return a list like - above. - NOTE: callables should only be used when the names and order of *args - can only be determined at runtime. Please use list of names - when the order and names of *args is clear before runtime! - - .. note:: decorator also converts MappingView input data to list. + If set e.g. to "namer", if a ``namer`` kwarg is passed as a string, and + a ``label`` kwarg is not passed, then pass the value of the ``namer`` + kwarg as the ``label`` kwarg as well. """ + + if func is None: # Return the actual decorator. + return functools.partial( + _preprocess_data, + replace_names=replace_names, label_namer=label_namer) + + sig = inspect.signature(func) + varargs_name = None + varkwargs_name = None + arg_names = [] + params = list(sig.parameters.values()) + for p in params: + if p.kind is Parameter.VAR_POSITIONAL: + varargs_name = p.name + elif p.kind is Parameter.VAR_KEYWORD: + varkwargs_name = p.name + else: + arg_names.append(p.name) + data_param = Parameter("data", Parameter.KEYWORD_ONLY, default=None) + if varkwargs_name: + params.insert(-1, data_param) + else: + params.append(data_param) + new_sig = sig.replace(parameters=params) + arg_names = arg_names[1:] # remove the first "ax" / self arg + if replace_names is not None: replace_names = set(replace_names) - def param(func): - sig = inspect.signature(func) - _has_varargs = False - _has_varkwargs = False - _arg_names = [] - params = list(sig.parameters.values()) - for p in params: - if p.kind is Parameter.VAR_POSITIONAL: - _has_varargs = True - elif p.kind is Parameter.VAR_KEYWORD: - _has_varkwargs = True + assert (replace_names or set()) <= set(arg_names) or varkwargs_name, ( + "Matplotlib internal error: invalid replace_names ({!r}) for {!r}" + .format(replace_names, func.__name__)) + assert label_namer is None or label_namer in arg_names or varkwargs_name, ( + "Matplotlib internal error: invalid label_namer ({!r}) for {!r}" + .format(label_namer, func.__name__)) + + @functools.wraps(func) + def inner(ax, *args, data=None, **kwargs): + if data is None: + return func(ax, *map(sanitize_sequence, args), **kwargs) + + bound = new_sig.bind(ax, *args, **kwargs) + needs_label = (label_namer + and "label" not in bound.arguments + and "label" not in bound.kwargs) + auto_label = (bound.arguments.get(label_namer) + or bound.kwargs.get(label_namer)) + + for k, v in bound.arguments.items(): + if k == varkwargs_name: + for k1, v1 in v.items(): + if replace_names is None or k1 in replace_names: + v[k1] = _replacer(data, v1) + elif k == varargs_name: + if replace_names is None: + bound.arguments[k] = tuple(_replacer(data, v1) for v1 in v) else: - _arg_names.append(p.name) - data_param = Parameter('data', Parameter.KEYWORD_ONLY, default=None) - if _has_varkwargs: - params.insert(-1, data_param) - else: - params.append(data_param) - new_sig = sig.replace(parameters=params) - # Import-time check: do we have enough information to replace *args? - arg_names_at_runtime = False - # there can't be any positional arguments behind *args and no - # positional args can end up in **kwargs, so only *varargs make - # problems. - # http://stupidpythonideas.blogspot.de/2013/08/arguments-and-parameters.html - if not _has_varargs: - # all args are "named", so no problem - # remove the first "ax" / self arg - arg_names = _arg_names[1:] - else: - # Here we have "unnamed" variables and we need a way to determine - # whether to replace a arg or not - if replace_names is None: - # all argnames should be replaced - arg_names = None - elif len(replace_names) == 0: - # No argnames should be replaced - arg_names = [] - elif len(_arg_names) > 1 and (positional_parameter_names is None): - # we got no manual parameter names but more than an 'ax' ... - if len(replace_names - set(_arg_names[1:])) == 0: - # all to be replaced arguments are in the list - arg_names = _arg_names[1:] - else: - raise AssertionError( - "Got unknown 'replace_names' and wrapped function " - "{!r} uses '*args', need 'positional_parameter_names'" - .format(func.__name__)) + if replace_names is None or k in replace_names: + bound.arguments[k] = _replacer(data, v) + + bound.apply_defaults() + del bound.arguments["data"] + + all_kwargs = {**bound.arguments, **bound.kwargs} + if needs_label: + if label_namer not in all_kwargs: + cbook._warn_external( + "Tried to set a label via parameter {!r} in func {!r} but " + "couldn't find such an argument.\n(This is a programming " + "error, please report to the Matplotlib list!)".format( + label_namer, func.__name__), + RuntimeWarning) else: - if positional_parameter_names is not None: - if callable(positional_parameter_names): - # determined by the function at runtime - arg_names_at_runtime = True - # so that we don't compute the label_pos at import time - arg_names = [] - else: - arg_names = positional_parameter_names + label = _label_from_arg(all_kwargs[label_namer], auto_label) + if "label" in arg_names: + bound.arguments["label"] = label + try: + bound.arguments.move_to_end(varkwargs_name) + except KeyError: + pass else: - if replace_all_args: - arg_names = [] - else: - raise AssertionError( - "Got 'replace_names' and wrapped function {!r} " - "uses *args, need 'positional_parameter_names' or " - "'replace_all_args'".format(func.__name__)) - - # compute the possible label_namer and label position in positional - # arguments - label_pos = 9999 # bigger than all "possible" argument lists - label_namer_pos = 9999 # bigger than all "possible" argument lists - if (label_namer and # we actually want a label here ... - arg_names and # and we can determine a label in *args ... - label_namer in arg_names): # and it is in *args - label_namer_pos = arg_names.index(label_namer) - if "label" in arg_names: - label_pos = arg_names.index("label") - - # Check the case we know a label_namer but we can't find it the - # arg_names... Unfortunately the label_namer can be in **kwargs, - # which we can't detect here and which results in a non-set label - # which might surprise the user :-( - if label_namer and not arg_names_at_runtime and not _has_varkwargs: - if not arg_names: - raise AssertionError( - "label_namer {!r} can't be found as the parameter without " - "'positional_parameter_names'".format(label_namer)) - elif label_namer not in arg_names: - raise AssertionError( - "label_namer {!r} can't be found in the parameter names " - "(known argnames: %s).".format(label_namer, arg_names)) - else: - # this is the case when the name is in arg_names - pass + bound.arguments.setdefault( + varkwargs_name, {})["label"] = label - @functools.wraps(func) - def inner(ax, *args, data=None, **kwargs): - # this is needed because we want to change these values if - # arg_names_at_runtime==True, but python does not allow assigning - # to a variable in a outer scope. So use some new local ones and - # set them to the already computed values. - _label_pos = label_pos - _label_namer_pos = label_namer_pos - _arg_names = arg_names + return func(*bound.args, **bound.kwargs) - label = None + inner.__doc__ = _add_data_doc(inner.__doc__, replace_names) + inner.__signature__ = new_sig + return inner - if data is None: # data validation - args = tuple(sanitize_sequence(a) for a in args) - else: - if arg_names_at_runtime: - # update the information about replace names and - # label position - _arg_names = positional_parameter_names(args, data) - if (label_namer and # we actually want a label here ... - _arg_names and # and we can find a label in *args - (label_namer in _arg_names)): # and it is in *args - _label_namer_pos = _arg_names.index(label_namer) - if "label" in _arg_names: - _label_pos = arg_names.index("label") - - # save the current label_namer value so that it can be used as - # a label - if _label_namer_pos < len(args): - label = args[_label_namer_pos] - else: - label = kwargs.get(label_namer, None) - # ensure a string, as label can't be anything else - if not isinstance(label, str): - label = None - - if replace_names is None or replace_all_args: - # all should be replaced - args = tuple(_replacer(data, a) for - j, a in enumerate(args)) - else: - # An arg is replaced if the arg_name of that position is - # in replace_names ... - if len(_arg_names) < len(args): - raise RuntimeError( - "Got more args than function expects") - args = tuple(_replacer(data, a) - if _arg_names[j] in replace_names else a - for j, a in enumerate(args)) - - if replace_names is None: - # replace all kwargs ... - kwargs = {k: _replacer(data, v) for k, v in kwargs.items()} - else: - # ... or only if a kwarg of that name is in replace_names - kwargs = { - k: _replacer(data, v) if k in replace_names else v - for k, v in kwargs.items()} - - # replace the label if this func "wants" a label arg and the user - # didn't set one. Note: if the user puts in "label=None", it does - # *NOT* get replaced! - user_supplied_label = ( - len(args) >= _label_pos or # label is included in args - 'label' in kwargs # ... or in kwargs - ) - if label_namer and not user_supplied_label: - if _label_namer_pos < len(args): - kwargs['label'] = get_label(args[_label_namer_pos], label) - elif label_namer in kwargs: - kwargs['label'] = get_label(kwargs[label_namer], label) - else: - cbook._warn_external( - "Tried to set a label via parameter %r in func %r but " - "couldn't find such an argument.\n(This is a " - "programming error, please report to the Matplotlib " - "list!)" % (label_namer, func.__name__), - RuntimeWarning) - return func(ax, *args, **kwargs) - - inner.__doc__ = _add_data_doc(inner.__doc__, - replace_names, replace_all_args) - inner.__signature__ = new_sig - return inner - - return param _log.debug('matplotlib version %s', __version__) _log.debug('interactive is %s', is_interactive()) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index c3dbdb92995a..09e3b2a26554 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -42,46 +42,6 @@ rcParams = matplotlib.rcParams -def _has_item(data, name): - """Return whether *data* can be item-accessed with *name*. - - This supports data with a dict-like interface (`in` checks item - availability) and with numpy.arrays. - """ - try: - return data.dtype.names is not None and name in data.dtype.names - except AttributeError: # not a numpy array - return name in data - - -def _plot_args_replacer(args, data): - if len(args) == 1: - return ["y"] - elif len(args) == 2: - # this can be two cases: x,y or y,c - if not _has_item(data, args[1]): - return ["y", "c"] - # it's data, but could be a color code like 'ro' or 'b--' - # -> warn the user in that case... - try: - _process_plot_format(args[1]) - except ValueError: - pass - else: - cbook._warn_external( - "Second argument {!r} is ambiguous: could be a color spec but " - "is in data; using as data. Either rename the entry in data " - "or use three arguments to plot.".format(args[1]), - RuntimeWarning) - return ["x", "y"] - elif len(args) == 3: - return ["x", "y", "c"] - else: - raise ValueError("Using arbitrary long args with data is not " - "supported due to ambiguity of arguments.\nUse " - "multiple plotting calls instead.") - - def _make_inset_locator(bounds, trans, parent): """ Helper function to locate inset axes, used in @@ -1153,8 +1113,7 @@ def vlines(self, x, ymin, ymax, colors='k', linestyles='solid', @_preprocess_data(replace_names=["positions", "lineoffsets", "linelengths", "linewidths", - "colors", "linestyles"], - label_namer=None) + "colors", "linestyles"]) @docstring.dedent_interpd def eventplot(self, positions, orientation='horizontal', lineoffsets=1, linelengths=1, linewidths=None, colors=None, @@ -1370,10 +1329,8 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, #### Basic plotting - # The label_naming happens in `matplotlib.axes._base._plot_args` - @_preprocess_data(replace_names=["x", "y"], - positional_parameter_names=_plot_args_replacer, - label_namer=None) + # Uses a custom implementation of data-kwarg handling in + # _process_plot_var_args. @docstring.dedent_interpd def plot(self, *args, scalex=True, scaley=True, **kwargs): """ @@ -1486,7 +1443,6 @@ def plot(self, *args, scalex=True, scaley=True, **kwargs): You may suppress the warning by adding an empty format string `plot('n', 'o', '', data=obj)`. - Other Parameters ---------------- scalex, scaley : bool, optional, default: True @@ -1513,13 +1469,11 @@ def plot(self, *args, scalex=True, scaley=True, **kwargs): lines A list of `.Line2D` objects representing the plotted data. - See Also -------- scatter : XY scatter plot with markers of varying size and/or color ( sometimes also called bubble chart). - Notes ----- **Format Strings** @@ -1988,7 +1942,7 @@ def xcorr(self, x, y, normed=True, detrend=mlab.detrend_none, #### Specialized plotting - @_preprocess_data(replace_names=["x", "y"], label_namer="y") + # @_preprocess_data() # let 'plot' do the unpacking.. def step(self, x, y, *args, where='pre', **kwargs): """ Make a step plot. @@ -2056,15 +2010,7 @@ def step(self, x, y, *args, where='pre', **kwargs): kwargs['drawstyle'] = 'steps-' + where return self.plot(x, y, *args, **kwargs) - @_preprocess_data(replace_names=["x", "left", - "height", "width", - "y", "bottom", - "color", "edgecolor", "linewidth", - "tick_label", "xerr", "yerr", - "ecolor"], - label_namer=None, - replace_all_args=True - ) + @_preprocess_data() @docstring.dedent_interpd def bar(self, x, height, width=0.8, bottom=None, *, align="center", **kwargs): @@ -2456,7 +2402,7 @@ def barh(self, y, width, height=0.8, left=None, *, align="center", align=align, **kwargs) return patches - @_preprocess_data(label_namer=None) + @_preprocess_data() @docstring.dedent_interpd def broken_barh(self, xranges, yrange, **kwargs): """ @@ -2527,9 +2473,9 @@ def broken_barh(self, xranges, yrange, **kwargs): return col - @_preprocess_data(replace_all_args=True, label_namer=None) - def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, - bottom=0, label=None): + @_preprocess_data() + def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, + label=None): """ Create a stem plot. @@ -2687,8 +2633,7 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, return stem_container - @_preprocess_data(replace_names=["x", "explode", "labels", "colors"], - label_namer=None) + @_preprocess_data(replace_names=["x", "explode", "labels", "colors"]) def pie(self, x, explode=None, labels=None, colors=None, autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1, startangle=None, radius=None, counterclock=True, @@ -3303,7 +3248,7 @@ def extract_err(err, data): return errorbar_container # (l0, caplines, barcols) - @_preprocess_data(label_namer=None) + @_preprocess_data() def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, positions=None, widths=None, patch_artist=None, bootstrap=None, usermedians=None, conf_intervals=None, @@ -4832,7 +4777,7 @@ def _quiver_units(self, args, kw): return args # args can by a combination if X, Y, U, V, C and all should be replaced - @_preprocess_data(replace_all_args=True, label_namer=None) + @_preprocess_data() def quiver(self, *args, **kw): # Make sure units are handled for x and y values args = self._quiver_units(args, kw) @@ -4845,13 +4790,12 @@ def quiver(self, *args, **kw): quiver.__doc__ = mquiver.Quiver.quiver_doc # args can by either Y or y1,y2,... and all should be replaced - @_preprocess_data(replace_all_args=True, label_namer=None) + @_preprocess_data() def stackplot(self, x, *args, **kwargs): return mstack.stackplot(self, x, *args, **kwargs) stackplot.__doc__ = mstack.stackplot.__doc__ - @_preprocess_data(replace_names=["x", "y", "u", "v", "start_points"], - label_namer=None) + @_preprocess_data(replace_names=["x", "y", "u", "v", "start_points"]) def streamplot(self, x, y, u, v, density=1, linewidth=None, color=None, cmap=None, norm=None, arrowsize=1, arrowstyle='-|>', minlength=0.1, transform=None, zorder=None, @@ -4876,7 +4820,7 @@ def streamplot(self, x, y, u, v, density=1, linewidth=None, color=None, streamplot.__doc__ = mstream.streamplot.__doc__ # args can be some combination of X, Y, U, V, C and all should be replaced - @_preprocess_data(replace_all_args=True, label_namer=None) + @_preprocess_data() @docstring.dedent_interpd def barbs(self, *args, **kw): """ @@ -4890,8 +4834,8 @@ def barbs(self, *args, **kw): self.autoscale_view() return b - @_preprocess_data(replace_names=["x", "y"], label_namer=None, - positional_parameter_names=["x", "y", "c"]) + # Uses a custom implementation of data-kwarg handling in + # _process_plot_var_args. def fill(self, *args, **kwargs): """ Plot filled polygons. @@ -4938,8 +4882,7 @@ def fill(self, *args, **kwargs): self.autoscale_view() return patches - @_preprocess_data(replace_names=["x", "y1", "y2", "where"], - label_namer=None) + @_preprocess_data(replace_names=["x", "y1", "y2", "where"]) @docstring.dedent_interpd def fill_between(self, x, y1, y2=0, where=None, interpolate=False, step=None, **kwargs): @@ -5121,8 +5064,7 @@ def get_interp_point(ind): self.autoscale_view() return collection - @_preprocess_data(replace_names=["y", "x1", "x2", "where"], - label_namer=None) + @_preprocess_data(replace_names=["y", "x1", "x2", "where"]) @docstring.dedent_interpd def fill_betweenx(self, y, x1, x2=0, where=None, step=None, interpolate=False, **kwargs): @@ -5304,7 +5246,7 @@ def get_interp_point(ind): return collection #### plotting z(x,y): imshow, pcolor and relatives, contour - @_preprocess_data(label_namer=None) + @_preprocess_data() def imshow(self, X, cmap=None, norm=None, aspect=None, interpolation=None, alpha=None, vmin=None, vmax=None, origin=None, extent=None, shape=None, filternorm=1, @@ -5571,7 +5513,7 @@ def _pcolorargs(funcname, *args, allmatch=False): C = cbook.safe_masked_invalid(C) return X, Y, C - @_preprocess_data(label_namer=None) + @_preprocess_data() @docstring.dedent_interpd def pcolor(self, *args, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, **kwargs): @@ -5808,7 +5750,7 @@ def pcolor(self, *args, alpha=None, norm=None, cmap=None, vmin=None, self.autoscale_view() return collection - @_preprocess_data(label_namer=None) + @_preprocess_data() @docstring.dedent_interpd def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, shading='flat', antialiased=False, **kwargs): @@ -6021,7 +5963,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, self.autoscale_view() return collection - @_preprocess_data(label_namer=None) + @_preprocess_data() @docstring.dedent_interpd def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, **kwargs): @@ -6785,7 +6727,7 @@ def hist(self, x, bins=None, range=None, density=None, weights=None, else: return tops, bins, cbook.silent_list('Lists of Patches', patches) - @_preprocess_data(replace_names=["x", "y", "weights"], label_namer=None) + @_preprocess_data(replace_names=["x", "y", "weights"]) def hist2d(self, x, y, bins=10, range=None, normed=False, weights=None, cmin=None, cmax=None, **kwargs): """ @@ -6893,7 +6835,7 @@ def hist2d(self, x, y, bins=10, range=None, normed=False, weights=None, return h, xedges, yedges, pc - @_preprocess_data(replace_names=["x"], label_namer=None) + @_preprocess_data(replace_names=["x"]) @docstring.dedent_interpd def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, @@ -7128,7 +7070,7 @@ def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, else: return pxy, freqs, line - @_preprocess_data(replace_names=["x"], label_namer=None) + @_preprocess_data(replace_names=["x"]) @docstring.dedent_interpd def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, scale=None, @@ -7231,7 +7173,7 @@ def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] - @_preprocess_data(replace_names=["x"], label_namer=None) + @_preprocess_data(replace_names=["x"]) @docstring.dedent_interpd def angle_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, **kwargs): @@ -7313,7 +7255,7 @@ def angle_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] - @_preprocess_data(replace_names=["x"], label_namer=None) + @_preprocess_data(replace_names=["x"]) @docstring.dedent_interpd def phase_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, **kwargs): @@ -7394,7 +7336,7 @@ def phase_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] - @_preprocess_data(replace_names=["x", "y"], label_namer=None) + @_preprocess_data(replace_names=["x", "y"]) @docstring.dedent_interpd def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, window=mlab.window_hanning, noverlap=0, pad_to=None, @@ -7459,7 +7401,7 @@ def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, return cxy, freqs - @_preprocess_data(replace_names=["x"], label_namer=None) + @_preprocess_data(replace_names=["x"]) @docstring.dedent_interpd def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, @@ -7811,7 +7753,7 @@ def matshow(self, Z, **kwargs): integer=True)) return im - @_preprocess_data(replace_names=["dataset"], label_namer=None) + @_preprocess_data(replace_names=["dataset"]) def violinplot(self, dataset, positions=None, vert=True, widths=0.5, showmeans=False, showextrema=True, showmedians=False, points=100, bw_method=None): diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index e914565cb92f..6791766c6e9a 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -8,8 +8,7 @@ import numpy as np -import matplotlib - +import matplotlib as mpl from matplotlib import cbook, rcParams from matplotlib.cbook import ( _OrderedSet, _check_1d, _string_to_bool, index_of, get_label) @@ -26,7 +25,6 @@ import matplotlib.font_manager as font_manager import matplotlib.text as mtext import matplotlib.image as mimage - from matplotlib.rcsetup import cycler, validate_axisbelow _log = logging.getLogger(__name__) @@ -161,23 +159,66 @@ def set_prop_cycle(self, *args, **kwargs): self._prop_keys = prop_cycler.keys def __call__(self, *args, **kwargs): + # Process units. if self.axes.xaxis is not None and self.axes.yaxis is not None: xunits = kwargs.pop('xunits', self.axes.xaxis.units) - if self.axes.name == 'polar': xunits = kwargs.pop('thetaunits', xunits) - + if xunits != self.axes.xaxis.units: + self.axes.xaxis.set_units(xunits) yunits = kwargs.pop('yunits', self.axes.yaxis.units) - if self.axes.name == 'polar': yunits = kwargs.pop('runits', yunits) - - if xunits != self.axes.xaxis.units: - self.axes.xaxis.set_units(xunits) - if yunits != self.axes.yaxis.units: self.axes.yaxis.set_units(yunits) + if not args: + return + + # Process the 'data' kwarg. + data = kwargs.pop("data", None) + if data is not None: + replaced = [mpl._replacer(data, arg) for arg in args] + if len(args) == 1: + label_namer_idx = 0 + elif len(args) == 2: # Can be x, y or y, c. + # Figure out what the second argument is. + # 1) If the second argument cannot be a format shorthand, the + # second argument is the label_namer. + # 2) Otherwise (it could have been a format shorthand), + # a) if we did perform a substitution, emit a warning, and + # use it as label_namer. + # b) otherwise, it is indeed a format shorthand; use the + # first argument as label_namer. + try: + _process_plot_format(args[1]) + except ValueError: # case 1) + label_namer_idx = 1 + else: + if replaced[1] is not args[1]: # case 2a) + cbook._warn_external( + "Second argument {!r} is ambiguous: could be a " + "color spec but is in data; using as data. " + "Either rename the entry in data or use three " + "arguments to plot.".format(args[1]), + RuntimeWarning) + label_namer_idx = 1 + else: # case 2b) + label_namer_idx = 0 + elif len(args) == 3: + label_namer_idx = 1 + else: + raise ValueError( + "Using arbitrary long args with data is not supported due " + "to ambiguity of arguments; use multiple plotting calls " + "instead") + if kwargs.get("label") is None: + kwargs["label"] = mpl._label_from_arg( + replaced[label_namer_idx], args[label_namer_idx]) + args = replaced + + # Repeatedly grab (x, y) or (x, y, format) from the front of args and + # massage them into arguments to plot() or fill(). while args: this, args = args[:2], args[2:] if args and isinstance(args[0], str): @@ -358,9 +399,6 @@ def _plot_args(self, tup, kwargs): if v is not None: kw[k] = v - if 'label' not in kwargs or kwargs['label'] is None: - kwargs['label'] = get_label(tup[-1], None) - if len(tup) == 2: x = _check_1d(tup[0]) y = _check_1d(tup[-1]) @@ -683,8 +721,7 @@ def get_xaxis_text1_transform(self, pad_points): overridden by new kinds of projections that may need to place axis elements in different locations. """ - labels_align = matplotlib.rcParams["xtick.alignment"] - + labels_align = rcParams["xtick.alignment"] return (self.get_xaxis_transform(which='tick1') + mtransforms.ScaledTranslation(0, -1 * pad_points / 72, self.figure.dpi_scale_trans), @@ -710,7 +747,7 @@ def get_xaxis_text2_transform(self, pad_points): overridden by new kinds of projections that may need to place axis elements in different locations. """ - labels_align = matplotlib.rcParams["xtick.alignment"] + labels_align = rcParams["xtick.alignment"] return (self.get_xaxis_transform(which='tick2') + mtransforms.ScaledTranslation(0, pad_points / 72, self.figure.dpi_scale_trans), @@ -760,7 +797,7 @@ def get_yaxis_text1_transform(self, pad_points): overridden by new kinds of projections that may need to place axis elements in different locations. """ - labels_align = matplotlib.rcParams["ytick.alignment"] + labels_align = rcParams["ytick.alignment"] return (self.get_yaxis_transform(which='tick1') + mtransforms.ScaledTranslation(-1 * pad_points / 72, 0, self.figure.dpi_scale_trans), @@ -786,8 +823,7 @@ def get_yaxis_text2_transform(self, pad_points): overridden by new kinds of projections that may need to place axis elements in different locations. """ - labels_align = matplotlib.rcParams["ytick.alignment"] - + labels_align = rcParams["ytick.alignment"] return (self.get_yaxis_transform(which='tick2') + mtransforms.ScaledTranslation(pad_points / 72, 0, self.figure.dpi_scale_trans), @@ -1752,7 +1788,7 @@ def _sci(self, im): `~.pyplot.viridis`, and other functions such as `~.pyplot.clim`. The current image is an attribute of the current axes. """ - if isinstance(im, matplotlib.contour.ContourSet): + if isinstance(im, mpl.contour.ContourSet): if im.collections[0] not in self.collections: raise ValueError("ContourSet must be in current Axes") elif im not in self.images and im not in self.collections: diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 22c804b88da5..dc1fbec8067a 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1732,6 +1732,7 @@ def normalize_kwargs(kw, alias_mapping=None, required=(), forbidden=(), return ret +@deprecated("3.1") def get_label(y, default_name): try: return y.name diff --git a/lib/matplotlib/tests/test_preprocess_data.py b/lib/matplotlib/tests/test_preprocess_data.py index 7f36447340ba..82eaa6ba940f 100644 --- a/lib/matplotlib/tests/test_preprocess_data.py +++ b/lib/matplotlib/tests/test_preprocess_data.py @@ -12,29 +12,15 @@ # test_axes.test_pie_linewidth_0 -# these two get used in multiple tests, so define them here +# this gets used in multiple tests, so define it here @_preprocess_data(replace_names=["x", "y"], label_namer="y") def plot_func(ax, x, y, ls="x", label=None, w="xyz"): return ("x: %s, y: %s, ls: %s, w: %s, label: %s" % ( list(x), list(y), ls, w, label)) -@_preprocess_data(replace_names=["x", "y"], label_namer="y", - positional_parameter_names=["x", "y", "ls", "label", "w"]) -def plot_func_varargs(ax, *args, **kwargs): - all_args = [None, None, "x", None, "xyz"] - for i, v in enumerate(args): - all_args[i] = v - for i, k in enumerate(["x", "y", "ls", "label", "w"]): - if k in kwargs: - all_args[i] = kwargs[k] - x, y, ls, label, w = all_args - return ("x: %s, y: %s, ls: %s, w: %s, label: %s" % ( - list(x), list(y), ls, w, label)) - - -all_funcs = [plot_func, plot_func_varargs] -all_func_ids = ['plot_func', 'plot_func_varargs'] +all_funcs = [plot_func] +all_func_ids = ['plot_func'] def test_compiletime_checks(): @@ -59,9 +45,6 @@ def func_no_ax_args(*args, **kwargs): pass # z is unknown _preprocess_data(replace_names=["x", "y", "z"])(func_args) - with pytest.raises(AssertionError): - _preprocess_data(replace_names=["x", "y"])(func_no_ax_args) - # no replacements at all -> all ok... _preprocess_data(replace_names=[], label_namer=None)(func) _preprocess_data(replace_names=[], label_namer=None)(func_args) @@ -80,19 +63,8 @@ def func_no_ax_args(*args, **kwargs): pass _preprocess_data(label_namer="z")(func_no_ax_args) -def test_label_problems_at_runtime(): - """Tests for behaviour which would actually be nice to get rid of.""" - - @_preprocess_data(label_namer="z") - def func(*args, **kwargs): - pass - - # This is a programming mistake: the parameter which should add the - # label is not present in the function call. Unfortunately this was masked - # due to the **kwargs usage - # This would be nice to handle as a compiletime check (see above...) - with pytest.warns(RuntimeWarning): - func(None, x="a", y="b") +def test_label_namer_only_if_data(): + """label_namer should only apply when data is passed.""" def real_func(x, y): pass @@ -101,9 +73,10 @@ def real_func(x, y): def func(*args, **kwargs): real_func(**kwargs) - # This sets a label although the function can't handle it. + func(None, x="a", y="b") with pytest.raises(TypeError): - func(None, x="a", y="b") + # This sets a label although the function can't handle it. + func(None, x="a", y="b", data={"a": "A", "b": "B"}) @pytest.mark.parametrize('func', all_funcs, ids=all_func_ids) @@ -267,46 +240,10 @@ def func(ax, x, y, z=1): pass data = {"a": [1, 2], "b": [8, 9], "w": "NOT"} - with pytest.raises(RuntimeError): + with pytest.raises(TypeError): func(None, "a", "b", "z", "z", data=data) -def test_function_call_with_replace_all_args(): - """Test with a "replace_all_args" argument, all *args should be replaced""" - data = {"a": [1, 2], "b": [8, 9], "x": "xyz"} - - def funcy(ax, *args, **kwargs): - all_args = [None, None, "x", None, "NOT"] - for i, v in enumerate(args): - all_args[i] = v - for i, k in enumerate(["x", "y", "ls", "label", "w"]): - if k in kwargs: - all_args[i] = kwargs[k] - x, y, ls, label, w = all_args - return "x: %s, y: %s, ls: %s, w: %s, label: %s" % ( - list(x), list(y), ls, w, label) - - func = _preprocess_data(replace_all_args=True, replace_names=["w"], - label_namer="y")(funcy) - - assert (func(None, "a", "b", w="x", label="", data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") - assert (func(None, "a", "b", w="x", label="text", data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") - - func2 = _preprocess_data(replace_all_args=True, replace_names=["w"], - label_namer="y", - positional_parameter_names=["x", "y", "ls", - "label", "w"])(funcy) - - assert (func2(None, "a", "b", w="x", data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b") - assert (func2(None, "a", "b", w="x", label="", data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") - assert (func2(None, "a", "b", w="x", label="text", data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") - - def test_docstring_addition(): @_preprocess_data() def funcy(ax, *args, **kwargs): @@ -315,28 +252,30 @@ def funcy(ax, *args, **kwargs): assert re.search(r".*All positional and all keyword arguments\.", funcy.__doc__) - assert not re.search(r".*All positional arguments\.", funcy.__doc__) + assert not re.search(r".*All positional arguments\.", + funcy.__doc__) assert not re.search(r".*All arguments with the following names: .*", funcy.__doc__) - @_preprocess_data(replace_all_args=True, replace_names=[]) + @_preprocess_data(replace_names=[]) def funcy(ax, x, y, z, bar=None): """Funcy does nothing""" pass - assert re.search(r".*All positional arguments\.", - funcy.__doc__) + assert not re.search(r".*All positional arguments\.", + funcy.__doc__) assert not re.search(r".*All positional and all keyword arguments\.", funcy.__doc__) assert not re.search(r".*All arguments with the following names: .*", funcy.__doc__) - @_preprocess_data(replace_all_args=True, replace_names=["bar"]) + @_preprocess_data(replace_names=["bar"]) def funcy(ax, x, y, z, bar=None): """Funcy does nothing""" pass - assert re.search(r".*All positional arguments\.", funcy.__doc__) + assert not re.search(r".*All positional arguments\.", + funcy.__doc__) assert re.search(r".*All arguments with the following names: 'bar'\.", funcy.__doc__) assert not re.search(r".*All positional and all keyword arguments\.", @@ -356,29 +295,3 @@ def funcy(ax, x, y, z, bar=None): funcy.__doc__) assert not re.search(r".*All positional arguments\.", funcy.__doc__) - - -def test_positional_parameter_names_as_function(): - # Also test the _plot_arg_replacer for plot... - from matplotlib.axes._axes import _plot_args_replacer - - @_preprocess_data(replace_names=["x", "y"], - positional_parameter_names=_plot_args_replacer) - def funcy(ax, *args, **kwargs): - return "{args} | {kwargs}".format(args=args, kwargs=kwargs) - - # the normal case... - data = {"x": "X", "hy1": "Y"} - assert funcy(None, "x", "hy1", data=data) == "('X', 'Y') | {}" - assert funcy(None, "x", "hy1", "c", data=data) == "('X', 'Y', 'c') | {}" - - # no arbitrary long args with data - with pytest.raises(ValueError): - assert (funcy(None, "x", "y", "c", "x", "y", "x", "y", data=data) == - "('X', 'Y', 'c', 'X', 'Y', 'X', 'Y') | {}") - - # In the two arg case, if a valid color spec is in data, we warn but use - # it as data... - data = {"x": "X", "y": "Y", "ro": "!!"} - with pytest.warns(RuntimeWarning): - assert funcy(None, "y", "ro", data=data) == "('Y', '!!') | {}" From 6e4c5c14fc000de27bd689a335496165be739a05 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 7 Jun 2018 07:58:16 +0200 Subject: [PATCH 3/5] Maintain data=None in plot()'s explicit signature. --- lib/matplotlib/axes/_axes.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 09e3b2a26554..98a8859f54ff 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -1332,7 +1332,7 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, # Uses a custom implementation of data-kwarg handling in # _process_plot_var_args. @docstring.dedent_interpd - def plot(self, *args, scalex=True, scaley=True, **kwargs): + def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): """ Plot y versus x as lines and/or markers. @@ -1557,7 +1557,7 @@ def plot(self, *args, scalex=True, scaley=True, **kwargs): """ kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D._alias_map) - lines = [*self._get_lines(*args, **kwargs)] + lines = [*self._get_lines(*args, data=data, **kwargs)] for line in lines: self.add_line(line) self.autoscale_view(scalex=scalex, scaley=scaley) @@ -1943,7 +1943,7 @@ def xcorr(self, x, y, normed=True, detrend=mlab.detrend_none, #### Specialized plotting # @_preprocess_data() # let 'plot' do the unpacking.. - def step(self, x, y, *args, where='pre', **kwargs): + def step(self, x, y, *args, where='pre', data=None, **kwargs): """ Make a step plot. @@ -2008,7 +2008,7 @@ def step(self, x, y, *args, where='pre', **kwargs): raise ValueError("'where' argument to step must be " "'pre', 'post' or 'mid'") kwargs['drawstyle'] = 'steps-' + where - return self.plot(x, y, *args, **kwargs) + return self.plot(x, y, *args, data=data, **kwargs) @_preprocess_data() @docstring.dedent_interpd @@ -4836,7 +4836,7 @@ def barbs(self, *args, **kw): # Uses a custom implementation of data-kwarg handling in # _process_plot_var_args. - def fill(self, *args, **kwargs): + def fill(self, *args, data=None, **kwargs): """ Plot filled polygons. @@ -4859,6 +4859,13 @@ def fill(self, *args, **kwargs): ax.fill(x, y, x2, y2) # two polygons ax.fill(x, y, "b", x2, y2, "r") # a blue and a red polygon + data : indexable object, optional + An object with labelled data. If given, provide the label names to + plot in *x* and *y*, e.g.:: + + ax.fill("time", "signal", + data={"time": [0, 1, 2], "signal": [0, 1, 0]}) + Returns ------- a list of :class:`~matplotlib.patches.Polygon` @@ -4876,7 +4883,7 @@ def fill(self, *args, **kwargs): kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D._alias_map) patches = [] - for poly in self._get_patches_for_fill(*args, **kwargs): + for poly in self._get_patches_for_fill(*args, data=data, **kwargs): self.add_patch(poly) patches.append(poly) self.autoscale_view() From b07377fdd4d83bf45d83edad4f6f666d1984ac8c Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 9 Sep 2018 23:14:42 +0200 Subject: [PATCH 4/5] Make _preprocess_data stricter regarding label_namer. Assert that label_namer (if given) is a parameter in the function signature (not "possibly" present via `**kwargs`). This seems more consistent with the expectations of the check later; in fact we can now just remove that check. --- lib/matplotlib/__init__.py | 45 ++++++++-------- lib/matplotlib/tests/test_preprocess_data.py | 56 -------------------- 2 files changed, 23 insertions(+), 78 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index d490d9bee908..dc7e45ea3293 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1589,9 +1589,17 @@ def func(ax, *args, **kwargs): ... The list of parameter names for which lookup into *data* should be attempted. If None, replacement is attempted for all arguments. label_namer : string, optional, default: None - If set e.g. to "namer", if a ``namer`` kwarg is passed as a string, and - a ``label`` kwarg is not passed, then pass the value of the ``namer`` - kwarg as the ``label`` kwarg as well. + If set e.g. to "namer" (which must be a kwarg in the function's + signature -- not as ``**kwargs``), if the *namer* argument passed in is + a (string) key of *data* and no *label* kwarg is passed, then use the + (string) value of the *namer* as *label*. :: + + @_preprocess_data(label_namer="foo") + def func(foo, label=None): ... + + func("key", data={"key": value}) + # is equivalent to + func.__wrapped__(value, label="key") """ if func is None: # Return the actual decorator. @@ -1625,7 +1633,7 @@ def func(ax, *args, **kwargs): ... assert (replace_names or set()) <= set(arg_names) or varkwargs_name, ( "Matplotlib internal error: invalid replace_names ({!r}) for {!r}" .format(replace_names, func.__name__)) - assert label_namer is None or label_namer in arg_names or varkwargs_name, ( + assert label_namer is None or label_namer in arg_names, ( "Matplotlib internal error: invalid label_namer ({!r}) for {!r}" .format(label_namer, func.__name__)) @@ -1656,26 +1664,19 @@ def inner(ax, *args, data=None, **kwargs): bound.apply_defaults() del bound.arguments["data"] - all_kwargs = {**bound.arguments, **bound.kwargs} if needs_label: - if label_namer not in all_kwargs: - cbook._warn_external( - "Tried to set a label via parameter {!r} in func {!r} but " - "couldn't find such an argument.\n(This is a programming " - "error, please report to the Matplotlib list!)".format( - label_namer, func.__name__), - RuntimeWarning) + all_kwargs = {**bound.arguments, **bound.kwargs} + # label_namer will be in all_kwargs as we asserted above that + # `label_namer is None or label_namer in arg_names`. + label = _label_from_arg(all_kwargs[label_namer], auto_label) + if "label" in arg_names: + bound.arguments["label"] = label + try: + bound.arguments.move_to_end(varkwargs_name) + except KeyError: + pass else: - label = _label_from_arg(all_kwargs[label_namer], auto_label) - if "label" in arg_names: - bound.arguments["label"] = label - try: - bound.arguments.move_to_end(varkwargs_name) - except KeyError: - pass - else: - bound.arguments.setdefault( - varkwargs_name, {})["label"] = label + bound.arguments.setdefault(varkwargs_name, {})["label"] = label return func(*bound.args, **bound.kwargs) diff --git a/lib/matplotlib/tests/test_preprocess_data.py b/lib/matplotlib/tests/test_preprocess_data.py index 82eaa6ba940f..9b233d034553 100644 --- a/lib/matplotlib/tests/test_preprocess_data.py +++ b/lib/matplotlib/tests/test_preprocess_data.py @@ -58,26 +58,6 @@ def func_no_ax_args(*args, **kwargs): pass with pytest.raises(AssertionError): _preprocess_data(label_namer="z")(func_args) - # but "ok-ish", if func has kwargs -> will show up at runtime :-( - _preprocess_data(label_namer="z")(func_kwargs) - _preprocess_data(label_namer="z")(func_no_ax_args) - - -def test_label_namer_only_if_data(): - """label_namer should only apply when data is passed.""" - - def real_func(x, y): - pass - - @_preprocess_data(label_namer="x") - def func(*args, **kwargs): - real_func(**kwargs) - - func(None, x="a", y="b") - with pytest.raises(TypeError): - # This sets a label although the function can't handle it. - func(None, x="a", y="b", data={"a": "A", "b": "B"}) - @pytest.mark.parametrize('func', all_funcs, ids=all_func_ids) def test_function_call_without_data(func): @@ -178,42 +158,6 @@ def func_replace_all(ax, x, y, ls="x", label=None, w="NOT"): func_replace_all(None, x="a", y="b", w="x", label="text", data=data) == "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") - @_preprocess_data(label_namer="y") - def func_varags_replace_all(ax, *args, **kwargs): - all_args = [None, None, "x", None, "xyz"] - for i, v in enumerate(args): - all_args[i] = v - for i, k in enumerate(["x", "y", "ls", "label", "w"]): - if k in kwargs: - all_args[i] = kwargs[k] - x, y, ls, label, w = all_args - return "x: %s, y: %s, ls: %s, w: %s, label: %s" % ( - list(x), list(y), ls, w, label) - - # in the first case, we can't get a "y" argument, - # as we don't know the names of the *args - assert (func_varags_replace_all(None, x="a", y="b", w="x", data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b") - assert ( - func_varags_replace_all(None, "a", "b", w="x", label="", data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") - assert ( - func_varags_replace_all(None, "a", "b", w="x", label="text", - data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") - assert ( - func_varags_replace_all(None, x="a", y="b", w="x", label="", - data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") - assert ( - func_varags_replace_all(None, x="a", y="b", w="x", label="text", - data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") - - with pytest.warns(RuntimeWarning): - assert (func_varags_replace_all(None, "a", "b", w="x", data=data) == - "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: None") - def test_no_label_replacements(): """Test with "label_namer=None" -> no label replacement at all""" From 434a6a59a9f59574277997f75380bd0619732de5 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 8 Nov 2018 10:58:06 +0100 Subject: [PATCH 5/5] Error out when x/y are passed as kwargs. We only support passing them positionally; right now passing them as kwargs silently does nothing. The error message is the standard one for unknown kwargs. --- lib/matplotlib/axes/_base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 6791766c6e9a..f143c3a5c8aa 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -172,6 +172,11 @@ def __call__(self, *args, **kwargs): if yunits != self.axes.yaxis.units: self.axes.yaxis.set_units(yunits) + for pos_only in "xy": + if pos_only in kwargs: + raise TypeError("{} got an unexpected keyword argument {!r}" + .format(self.command, pos_only)) + if not args: return