From dc2813e7373f182fb6b577d74e11fe8787803ccd Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 13 Mar 2019 13:54:44 -0700 Subject: [PATCH] Backport PR #13601: Add a make-parameter-keyword-only-with-deprecation decorator. --- doc/api/next_api_changes/2019-03-07-AL.rst | 9 +++++ lib/matplotlib/backend_bases.py | 4 +- lib/matplotlib/backends/backend_nbagg.py | 10 ++++- lib/matplotlib/backends/backend_template.py | 2 +- lib/matplotlib/cbook/__init__.py | 3 +- lib/matplotlib/cbook/deprecation.py | 44 +++++++++++++++++++++ lib/matplotlib/tests/test_cbook.py | 17 +++++++- 7 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 doc/api/next_api_changes/2019-03-07-AL.rst diff --git a/doc/api/next_api_changes/2019-03-07-AL.rst b/doc/api/next_api_changes/2019-03-07-AL.rst new file mode 100644 index 000000000000..0ecff94d2a6f --- /dev/null +++ b/doc/api/next_api_changes/2019-03-07-AL.rst @@ -0,0 +1,9 @@ +API changes +``````````` + +Passing the ``block`` argument of ``plt.show`` positionally is deprecated; it +should be passed by keyword. + +When using the nbagg backend, ``plt.show`` used to silently accept and ignore +all combinations of positional and keyword arguments. This behavior is +deprecated. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 89587c7a483d..e16031626c9d 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3217,8 +3217,10 @@ def draw_if_interactive(cls): cls.trigger_manager_draw(manager) @classmethod + @cbook._make_keyword_only("3.1", "block") def show(cls, block=None): - """Show all figures. + """ + Show all figures. `show` blocks by calling `mainloop` if *block* is ``True``, or if it is ``None`` and we are neither in IPython's ``%pylab`` mode, nor in diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index b2fae164b55a..7b779151cef1 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -17,7 +17,7 @@ # Jupyter/IPython 3.x or earlier from IPython.kernel.comm import Comm -from matplotlib import is_interactive +from matplotlib import cbook, is_interactive from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, NavigationToolbar2) @@ -241,7 +241,13 @@ def trigger_manager_draw(manager): manager.show() @staticmethod - def show(*args, **kwargs): + def show(*args, block=None, **kwargs): + if args or kwargs: + cbook.warn_deprecated( + "3.1", message="Passing arguments to show(), other than " + "passing 'block' by keyword, is deprecated %(since)s, and " + "support for it will be removed %(removal)s.") + ## TODO: something to do when keyword block==False ? from matplotlib._pylab_helpers import Gcf diff --git a/lib/matplotlib/backends/backend_template.py b/lib/matplotlib/backends/backend_template.py index 4087fd9e3a90..c03cdc6d4b05 100644 --- a/lib/matplotlib/backends/backend_template.py +++ b/lib/matplotlib/backends/backend_template.py @@ -173,7 +173,7 @@ def draw_if_interactive(): """ -def show(block=None): +def show(*, block=None): """ For image backends - is not required. For GUI backends - show() is usually the last line of a pyplot script and diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 248ceb524b29..c1a047740865 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -32,7 +32,8 @@ import matplotlib from .deprecation import ( - deprecated, warn_deprecated, _rename_parameter, _delete_parameter, + deprecated, warn_deprecated, + _rename_parameter, _delete_parameter, _make_keyword_only, _suppress_matplotlib_deprecation_warning, MatplotlibDeprecationWarning, mplDeprecation) diff --git a/lib/matplotlib/cbook/deprecation.py b/lib/matplotlib/cbook/deprecation.py index 0c9a242b594c..a5a8ab7312b5 100644 --- a/lib/matplotlib/cbook/deprecation.py +++ b/lib/matplotlib/cbook/deprecation.py @@ -371,6 +371,50 @@ def wrapper(*args, **kwargs): return wrapper +def _make_keyword_only(since, name, func=None): + """ + Decorator indicating that passing parameter *name* (or any of the following + ones) positionally to *func* is being deprecated. + + Note that this decorator **cannot** be applied to a function that has a + pyplot-level wrapper, as the wrapper always pass all arguments by keyword. + If it is used, users will see spurious DeprecationWarnings every time they + call the pyplot wrapper. + """ + + if func is None: + return functools.partial(_make_keyword_only, since, name) + + signature = inspect.signature(func) + POK = inspect.Parameter.POSITIONAL_OR_KEYWORD + KWO = inspect.Parameter.KEYWORD_ONLY + assert (name in signature.parameters + and signature.parameters[name].kind == POK), ( + f"Matplotlib internal error: {name!r} must be a positional-or-keyword " + f"parameter for {func.__name__}()") + names = [*signature.parameters] + kwonly = [name for name in names[names.index(name):] + if signature.parameters[name].kind == POK] + func.__signature__ = signature.replace(parameters=[ + param.replace(kind=inspect.Parameter.KEYWORD_ONLY) + if param.name in kwonly + else param + for param in signature.parameters.values()]) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + bound = signature.bind(*args, **kwargs) + if name in bound.arguments and name not in kwargs: + warn_deprecated( + since, message="Passing the %(name)s %(obj_type)s " + "positionally is deprecated since Matplotlib %(since)s; the " + "parameter will become keyword-only %(removal)s.", + name=name, obj_type=f"parameter of {func.__name__}()") + return func(*args, **kwargs) + + return wrapper + + @contextlib.contextmanager def _suppress_matplotlib_deprecation_warning(): with warnings.catch_warnings(): diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 5b6f593e7375..bacd9e5a3e99 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -13,7 +13,8 @@ import matplotlib.cbook as cbook import matplotlib.colors as mcolors -from matplotlib.cbook import delete_masked_points as dmp +from matplotlib.cbook import ( + MatplotlibDeprecationWarning, delete_masked_points as dmp) def test_is_hashable(): @@ -545,3 +546,17 @@ def test_safe_first_element_pandas_series(pd): s = pd.Series(range(5), index=range(10, 15)) actual = cbook.safe_first_element(s) assert actual == 0 + + +def test_make_keyword_only(recwarn): + @cbook._make_keyword_only("3.0", "arg") + def func(pre, arg, post=None): + pass + + func(1, arg=2) + assert len(recwarn) == 0 + + with pytest.warns(MatplotlibDeprecationWarning): + func(1, 2) + with pytest.warns(MatplotlibDeprecationWarning): + func(1, 2, 3)