From fa1decdc5bff2f036182c7707e4efbed4af2c070 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 23 May 2018 19:30:22 -0700 Subject: [PATCH] Automagically set the stacklevel on warnings. There are many places in Matplotlib where it is impossible to set a static stacklevel on warnings that works in all cases, because a same function may either be called directly or via some wrapper (e.g. pyplot). Instead, compute the stacklevel by walking the stack. Given that warnings refer to conditions that should, well, be avoided, I believe we don't need to worry too much about the runtime cost. As an example, use this mechanism for the "ambiguous second argument to plot" warning. Now both ``` plt.gca().plot("x", "y", data={"x": 1, "y": 2}) ``` and ``` plt.plot("x", "y", data={"x": 1, "y": 2}) ``` emit a warning that refers to the relevant line in the user source, whereas previously the second snippet would refer to the pyplot wrapper, which is irrelevant to the user. Note that this only works from source, not from IPython, as the latter uses `exec` and AFAICT there is no value of stacklevel that correctly refers to the string being exec'd. Of course, the idea would be to ultimately use this throughout the codebase. --- lib/matplotlib/axes/_axes.py | 4 ++-- lib/matplotlib/cbook/__init__.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d6e4f50a2d7e..b93f8aec3142 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -64,11 +64,11 @@ def _plot_args_replacer(args, data): except ValueError: pass else: - warnings.warn( + 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, stacklevel=3) + RuntimeWarning) return ["x", "y"] elif len(args) == 3: return ["x", "y", "c"] diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index cd2177e75f6b..395ade0f649f 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -14,7 +14,7 @@ import glob import gzip import io -from itertools import repeat +import itertools import locale import numbers import operator @@ -1254,7 +1254,7 @@ def _compute_conf_interval(data, med, iqr, bootstrap): ncols = len(X) if labels is None: - labels = repeat(None) + labels = itertools.repeat(None) elif len(labels) != ncols: raise ValueError("Dimensions of labels and X must be compatible") @@ -2032,3 +2032,21 @@ def _setattr_cm(obj, **kwargs): delattr(obj, attr) else: setattr(obj, attr, orig) + + +def _warn_external(message, category=None): + """ + `warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib". + + The original emitter of the warning can be obtained by patching this + function back to `warnings.warn`, i.e. ``cbook._warn_external = + warnings.warn`` (or ``functools.partial(warnings.warn, stacklevel=2)``, + etc.). + """ + frame = sys._getframe() + for stacklevel in itertools.count(1): + if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.)", + frame.f_globals["__name__"]): + break + frame = frame.f_back + warnings.warn(message, category, stacklevel)