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..dc7e45ea3293 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,152 @@ 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" (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. + 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 - 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__)) + 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, ( + "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: - 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 - 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 replace_names is None or k in replace_names: + bound.arguments[k] = _replacer(data, v) + + bound.apply_defaults() + del bound.arguments["data"] + + if needs_label: + 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: - 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)) + bound.arguments["label"] = label + try: + bound.arguments.move_to_end(varkwargs_name) + except KeyError: + pass 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 e57963467e11..98a8859f54ff 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, @@ -1368,13 +1327,12 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, return colls - # ### 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) + #### Basic plotting + + # 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. @@ -1485,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 @@ -1512,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** @@ -1601,14 +1556,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, data=data, **kwargs)] + for line in lines: self.add_line(line) - lines.append(line) - self.autoscale_view(scalex=scalex, scaley=scaley) return lines @@ -1991,8 +1942,8 @@ def xcorr(self, x, y, normed=True, detrend=mlab.detrend_none, #### Specialized plotting - @_preprocess_data(replace_names=["x", "y"], label_namer="y") - def step(self, x, y, *args, where='pre', **kwargs): + # @_preprocess_data() # let 'plot' do the unpacking.. + def step(self, x, y, *args, where='pre', data=None, **kwargs): """ Make a step plot. @@ -2057,17 +2008,9 @@ 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) - - @_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 - ) + return self.plot(x, y, *args, data=data, **kwargs) + + @_preprocess_data() @docstring.dedent_interpd def bar(self, x, height, width=0.8, bottom=None, *, align="center", **kwargs): @@ -2459,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): """ @@ -2530,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. @@ -2690,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, @@ -3306,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, @@ -4835,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) @@ -4848,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, @@ -4879,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): """ @@ -4893,9 +4834,9 @@ 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"]) - def fill(self, *args, **kwargs): + # Uses a custom implementation of data-kwarg handling in + # _process_plot_var_args. + def fill(self, *args, data=None, **kwargs): """ Plot filled polygons. @@ -4918,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` @@ -4935,14 +4883,13 @@ 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() 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): @@ -5124,8 +5071,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): @@ -5307,7 +5253,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, @@ -5574,7 +5520,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): @@ -5811,7 +5757,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): @@ -6024,7 +5970,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): @@ -6788,7 +6734,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): """ @@ -6896,7 +6842,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, @@ -7131,7 +7077,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, @@ -7234,7 +7180,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): @@ -7316,7 +7262,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): @@ -7397,7 +7343,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, @@ -7462,7 +7408,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, @@ -7814,7 +7760,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 6b89e26c8dcb..f143c3a5c8aa 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,25 +159,77 @@ 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) - ret = self._grab_next_args(*args, **kwargs) - return ret + 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 + + # 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): + 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.""" @@ -354,9 +404,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]) @@ -381,14 +428,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): """ @@ -687,8 +726,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), @@ -714,7 +752,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), @@ -764,7 +802,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), @@ -790,8 +828,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), @@ -1756,7 +1793,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..9b233d034553 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) @@ -75,36 +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_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 real_func(x, y): - pass - - @_preprocess_data(label_namer="x") - def func(*args, **kwargs): - real_func(**kwargs) - - # This sets a label although the function can't handle it. - with pytest.raises(TypeError): - func(None, x="a", y="b") - @pytest.mark.parametrize('func', all_funcs, ids=all_func_ids) def test_function_call_without_data(func): @@ -205,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""" @@ -267,46 +184,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 +196,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 +239,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', '!!') | {}"