From ffdbe6e7c02adcace1b4955c5ccb5788f2239e99 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 1 Jun 2015 22:23:28 -0400 Subject: [PATCH 01/11] ENH/WIP: first draft of ensure_ax Decorator for making user functions using the suggested signature work with the state machine to plot the the implicit currently active Axes. --- lib/matplotlib/pyplot.py | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 39d4c3858c61..cd3eac68f022 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -25,6 +25,9 @@ import types from cycler import cycler + +from functools import wraps + import matplotlib import matplotlib.colorbar from matplotlib import style @@ -190,6 +193,74 @@ def uninstall_repl_displayhook(): draw_all = _pylab_helpers.Gcf.draw_all +def ensure_ax(func): + """Decorator to ensure that the function gets an `Axes` object. + + + The intent of this decorator is to simplify the writing of helper + plotting functions that are useful for both interactive and + programmatic usage. + + The encouraged signature for third-party and user functions :: + + def my_function(ax, data, style) + + explicitly expects an Axes object as input rather than using + plt.gca() or creating axes with in the function body. This + allows for great flexibility, but some find it verbose for + interactive use. This decorator allows the Axes input to be + omitted in which case `plt.gca()` is passed into the function. + Thus :: + + wrapped = ensure_ax(my_function) + + can be called as any of :: + + wrapped(data, style) + wrapped(ax, data, style) + wrapped(data, style, ax=plt.gca()) + + + """ + @wraps(func) + def inner(*args, **kwargs): + if 'ax' in kwargs: + ax = kwargs.pop('ax', None) + elif len(args) > 0 and isinstance(args[0], Axes): + ax = args[0] + args = args[1:] + else: + ax = gca() + return func(ax, *args, **kwargs) + return inner + + +def ensure_ax_meth(func): + """ + The same as ensure axes, but for class methods :: + + class foo(object): + @ensure_ax_meth + def my_function(self, ax, style): + + will allow you to call your objects plotting methods with + out explicitly passing in an `Axes` object. + """ + @wraps(func) + def inner(*args, **kwargs): + s = args[0] + args = args[1:] + if 'ax' in kwargs: + ax = kwargs.pop('ax', None) + elif len(args) > 1 and isinstance(args[0], Axes): + ax = args[0] + args = args[1:] + else: + ax = gca() + return func(s, ax, *args, **kwargs) + return inner + + @docstring.copy_dedent(Artist.findobj) def findobj(o=None, match=None, include_self=True): if o is None: From a7918be843024df85e322f48c93b8c8f17db0cfd Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 7 Jun 2015 17:00:51 -0400 Subject: [PATCH 02/11] ENH: add registration function --- lib/matplotlib/pyplot.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index cd3eac68f022..b628062ceb84 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -261,6 +261,33 @@ def inner(*args, **kwargs): return inner +def register_func(func): + """ + Registers a function to be wrapped and made available in the + pyplot namespace for convenient interactive command line use. + + Parameters + ---------- + func : callable + + First parameter of function must be an `Axes` object. Will + be wrapped by `ensure_ax` + + Returns + ------- + callable + + The wrapped function. + """ + mod = sys.modules[__name__] + f_name = func.__name__ + if hasattr(mod, f_name): + raise RuntimeError("Function already exists with that name") + setattr(mod, f_name, ensure_ax(func)) + + return getattr(mod, f_name) + + @docstring.copy_dedent(Artist.findobj) def findobj(o=None, match=None, include_self=True): if o is None: From d2711897e794937cf298428261ce74877e9fc6ed Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 5 Jul 2015 20:30:24 -0400 Subject: [PATCH 03/11] ENH: mutate docstring in ensure_ax --- lib/matplotlib/pyplot.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index b628062ceb84..cbf45866a086 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -192,6 +192,18 @@ def uninstall_repl_displayhook(): draw_all = _pylab_helpers.Gcf.draw_all +_ENSURE_AX_DOC = """ +This function has been decorated by pyplot to have +an implicit reference to the `plt.gca()` passed as the first argument. + +The wrapped function can be called as any of :: + + func(*args, **kwargs) + func(ax, *args, **kwargs) + func(.., ax=ax) + +""" + def ensure_ax(func): """Decorator to ensure that the function gets an `Axes` object. @@ -232,6 +244,13 @@ def inner(*args, **kwargs): else: ax = gca() return func(ax, *args, **kwargs) + pre_doc = inner.__doc__ + if pre_doc is None: + pre_doc = '' + else: + pre_doc = dedent(pre_doc) + inner.__doc__ = _ENSURE_AX_DOC + pre_doc + return inner @@ -258,6 +277,12 @@ def inner(*args, **kwargs): else: ax = gca() return func(s, ax, *args, **kwargs) + pre_doc = inner.__doc__ + if pre_doc is None: + pre_doc = '' + else: + pre_doc = dedent(pre_doc) + inner.__doc__ = _ENSURE_AX_DOC + pre_doc return inner From 35010596408759c4b3313aed6fb0e69a9e0e2294 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 7 Jul 2015 21:16:37 -0400 Subject: [PATCH 04/11] FIX: remove default values --- lib/matplotlib/pyplot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index cbf45866a086..8f6000492bdc 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -237,7 +237,7 @@ def my_function(ax, data, style) @wraps(func) def inner(*args, **kwargs): if 'ax' in kwargs: - ax = kwargs.pop('ax', None) + ax = kwargs.pop('ax') elif len(args) > 0 and isinstance(args[0], Axes): ax = args[0] args = args[1:] @@ -270,7 +270,7 @@ def inner(*args, **kwargs): s = args[0] args = args[1:] if 'ax' in kwargs: - ax = kwargs.pop('ax', None) + ax = kwargs.pop('ax') elif len(args) > 1 and isinstance(args[0], Axes): ax = args[0] args = args[1:] From 8351ceb867bde4b50748432287bcf290fb390b7d Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 11 Jul 2015 01:55:26 -0400 Subject: [PATCH 05/11] ENH: make generated docstrings more descriptive --- lib/matplotlib/pyplot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 8f6000492bdc..a63ab70b8de5 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -198,9 +198,9 @@ def uninstall_repl_displayhook(): The wrapped function can be called as any of :: - func(*args, **kwargs) - func(ax, *args, **kwargs) - func(.., ax=ax) + {obj}{func}(*args, **kwargs) + {obj}{func}(ax, *args, **kwargs) + {obj}{func}(.., ax=ax) """ @@ -249,7 +249,7 @@ def inner(*args, **kwargs): pre_doc = '' else: pre_doc = dedent(pre_doc) - inner.__doc__ = _ENSURE_AX_DOC + pre_doc + inner.__doc__ = _ENSURE_AX_DOC.format(func=func.__name__, obj='') + pre_doc return inner @@ -282,7 +282,8 @@ def inner(*args, **kwargs): pre_doc = '' else: pre_doc = dedent(pre_doc) - inner.__doc__ = _ENSURE_AX_DOC + pre_doc + inner.__doc__ = _ENSURE_AX_DOC.format(func=func.__name__, + obj='obj.') + pre_doc return inner From 5feeb534f74aa2f45ea7798c03abb3248e242a99 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 15 Jul 2015 08:19:22 -0400 Subject: [PATCH 06/11] MNT: removed bad idea This seems more dangerous than useful on consideration --- lib/matplotlib/pyplot.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index a63ab70b8de5..abbddd677322 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -287,33 +287,6 @@ def inner(*args, **kwargs): return inner -def register_func(func): - """ - Registers a function to be wrapped and made available in the - pyplot namespace for convenient interactive command line use. - - Parameters - ---------- - func : callable - - First parameter of function must be an `Axes` object. Will - be wrapped by `ensure_ax` - - Returns - ------- - callable - - The wrapped function. - """ - mod = sys.modules[__name__] - f_name = func.__name__ - if hasattr(mod, f_name): - raise RuntimeError("Function already exists with that name") - setattr(mod, f_name, ensure_ax(func)) - - return getattr(mod, f_name) - - @docstring.copy_dedent(Artist.findobj) def findobj(o=None, match=None, include_self=True): if o is None: From fe8eb341ce815eaf149676f12a667718b0c9a8be Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 15 Jul 2015 08:39:14 -0400 Subject: [PATCH 07/11] ENH: add convince method to get a new axes To make using the OO interface at the repl easier, provide a function which will return the axes object for a single Axes in a new figure. --- lib/matplotlib/pyplot.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index abbddd677322..eaabbbffbc8f 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1339,6 +1339,30 @@ def subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True, return ret +def quick_axes(figsize=None, tight_layout=False): + """ + Create a single new axes in a new figure. + + This is a convenience function for working interactively + and should not be used in scripts. + + Parameters + ---------- + figsize : tuple, optional + Figure size in inches (w, h) + + tight_layout : bool, optional + If tight layout shoudl be used. + + Returns + ------- + ax : Axes + New axes + """ + _, ax = subplots(figsize=figsize, tight_layout=tight_layout) + return ax + + def subplot2grid(shape, loc, rowspan=1, colspan=1, **kwargs): """ Create a subplot in a grid. The grid is specified by *shape*, at From 9c0859bb021d9fab5601cf5c8c464fc5ca8ffc9f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 15 Jul 2015 09:00:46 -0400 Subject: [PATCH 08/11] ENH: add decorator that makes a new axes if needed Not sure if this is a good idea. It is different that the current behavior and still a bit too magical/implicit. --- lib/matplotlib/pyplot.py | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index eaabbbffbc8f..89f3e6f2215b 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -205,6 +205,22 @@ def uninstall_repl_displayhook(): """ +_ENSURE_AX_NEW_DOC = """ +This function has been decorated by pyplot to create a new +axes if one is not explicitly passed. + +The wrapped function can be called as any of :: + + {obj}{func}(*args, **kwargs) + {obj}{func}(ax, *args, **kwargs) + {obj}{func}(.., ax=ax) + +The first will make a new figure and axes, the other two +will add to the axes passed in. + +""" + + def ensure_ax(func): """Decorator to ensure that the function gets an `Axes` object. @@ -254,6 +270,34 @@ def inner(*args, **kwargs): return inner +def ensure_new_ax(func): + """Decorator to ensure that the function gets a new `Axes` object. + + Same as ensure_ax expect that a new figure and axes are created + if an Axes is not explicitly passed. + + """ + @wraps(func) + def inner(*args, **kwargs): + if 'ax' in kwargs: + ax = kwargs.pop('ax') + elif len(args) > 0 and isinstance(args[0], Axes): + ax = args[0] + args = args[1:] + else: + ax = quick_axes() + return func(ax, *args, **kwargs) + pre_doc = inner.__doc__ + if pre_doc is None: + pre_doc = '' + else: + pre_doc = dedent(pre_doc) + inner.__doc__ = (_ENSURE_AX_NEW_DOC.format(func=func.__name__, obj='') + + pre_doc) + + return inner + + def ensure_ax_meth(func): """ The same as ensure axes, but for class methods :: From f9ace37096c61c4b9c32567f2d8361f319529675 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 30 Jul 2015 17:35:08 -0400 Subject: [PATCH 09/11] DOC: add whats_new entry --- doc/users/whats_new/2015-07-30_ensure_ax.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 doc/users/whats_new/2015-07-30_ensure_ax.rst diff --git a/doc/users/whats_new/2015-07-30_ensure_ax.rst b/doc/users/whats_new/2015-07-30_ensure_ax.rst new file mode 100644 index 000000000000..fe051dbb2996 --- /dev/null +++ b/doc/users/whats_new/2015-07-30_ensure_ax.rst @@ -0,0 +1,18 @@ +Decorators to ensure an Axes exists & ``plt.quick_axes`` +-------------------------------------------------------- + +Added a top-level function to `pyplot` to create a single axes +figure and return the `Axes` object. + +Added decorators :: + + ensure_ax + ensure_ax_meth + ensure_new_ax + +which take a function or method that expects an `Axes` as the first +positional argument and returns a function which allows the axes to be +passed either as the first positional argument or as a kwarg. If the +`Axes` is not provided either gets the current axes (via `plt.gca()`) +for `ensure_ax` and `ensure_ax_meth` or creating a new single-axes figure +(via `plt.quick_axes`) for `ensure_new_ax` From c44b7ddfd6aafba84b9b7693adeabe21cfa8b7ff Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 6 Aug 2015 15:50:03 -0400 Subject: [PATCH 10/11] MNT: put wrapping doc at end of docstring --- lib/matplotlib/pyplot.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 89f3e6f2215b..8c808829083b 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -193,6 +193,7 @@ def uninstall_repl_displayhook(): draw_all = _pylab_helpers.Gcf.draw_all _ENSURE_AX_DOC = """ + This function has been decorated by pyplot to have an implicit reference to the `plt.gca()` passed as the first argument. @@ -206,6 +207,7 @@ def uninstall_repl_displayhook(): _ENSURE_AX_NEW_DOC = """ + This function has been decorated by pyplot to create a new axes if one is not explicitly passed. @@ -265,7 +267,7 @@ def inner(*args, **kwargs): pre_doc = '' else: pre_doc = dedent(pre_doc) - inner.__doc__ = _ENSURE_AX_DOC.format(func=func.__name__, obj='') + pre_doc + inner.__doc__ = pre_doc + _ENSURE_AX_DOC.format(func=func.__name__, obj='') return inner @@ -292,8 +294,8 @@ def inner(*args, **kwargs): pre_doc = '' else: pre_doc = dedent(pre_doc) - inner.__doc__ = (_ENSURE_AX_NEW_DOC.format(func=func.__name__, obj='') + - pre_doc) + inner.__doc__ = (pre_doc + + _ENSURE_AX_NEW_DOC.format(func=func.__name__, obj='')) return inner @@ -326,8 +328,8 @@ def inner(*args, **kwargs): pre_doc = '' else: pre_doc = dedent(pre_doc) - inner.__doc__ = _ENSURE_AX_DOC.format(func=func.__name__, - obj='obj.') + pre_doc + inner.__doc__ = pre_doc + _ENSURE_AX_DOC.format(func=func.__name__, + obj='obj.') return inner From e398ad85038e97678d31841e8e4ca77991683b18 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 29 Aug 2015 14:00:34 -0400 Subject: [PATCH 11/11] API: rename quick_axes -> gna In phone conversation with @efiring he suggesting changing 'quick_axes' to `gna` for 'get new axes' in analogy and contrast to `gca` -> 'get current axes (and create one if you have to)'. --- doc/users/whats_new/2015-07-30_ensure_ax.rst | 6 +++--- lib/matplotlib/pyplot.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/users/whats_new/2015-07-30_ensure_ax.rst b/doc/users/whats_new/2015-07-30_ensure_ax.rst index fe051dbb2996..c830f8e81d48 100644 --- a/doc/users/whats_new/2015-07-30_ensure_ax.rst +++ b/doc/users/whats_new/2015-07-30_ensure_ax.rst @@ -1,5 +1,5 @@ -Decorators to ensure an Axes exists & ``plt.quick_axes`` --------------------------------------------------------- +Decorators to ensure an Axes exists & ``plt.gna`` +------------------------------------------------- Added a top-level function to `pyplot` to create a single axes figure and return the `Axes` object. @@ -15,4 +15,4 @@ positional argument and returns a function which allows the axes to be passed either as the first positional argument or as a kwarg. If the `Axes` is not provided either gets the current axes (via `plt.gca()`) for `ensure_ax` and `ensure_ax_meth` or creating a new single-axes figure -(via `plt.quick_axes`) for `ensure_new_ax` +(via `plt.gna`) for `ensure_new_ax` diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 8c808829083b..bc28d2cf2055 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -287,7 +287,7 @@ def inner(*args, **kwargs): ax = args[0] args = args[1:] else: - ax = quick_axes() + ax = gna() return func(ax, *args, **kwargs) pre_doc = inner.__doc__ if pre_doc is None: @@ -1385,7 +1385,7 @@ def subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True, return ret -def quick_axes(figsize=None, tight_layout=False): +def gna(figsize=None, tight_layout=False): """ Create a single new axes in a new figure.