Skip to content

Cleanup matplotlib.use #13117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/api/next_api_changes/2019-01-06-TH-use-param.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
API Changes
```````````

The first parameter of `matplotlib.use` has been renamed from *arg* to
*backend*. This will only affect cases where that parameter has been set
as a keyword argument. The common usage pattern as a positional argument
``matplotlib.use('Qt5Agg')`` is not affected.
44 changes: 19 additions & 25 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1184,13 +1184,13 @@ def __exit__(self, exc_type, exc_value, exc_tb):
@cbook._rename_parameter("3.1", "arg", "backend")
def use(backend, warn=False, force=True):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, this is an API change. I'm 0.5 on this because it makes clear what the function does and what the argument is about. I doubt that someone has been doing matplotlib.use(arg='agg') so I dare go without a deprecation and backward compatibility code. (But that position is open for discussion).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs deprecation code, but an api change note perhaps?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt that someone has been doing matplotlib.use(arg='agg')

Users are very surprising ;) .

I'm -0.5 on this. It is definitely clearer, but is the improved clarity worth breaking even a handful of users and ruining their day?

Copy link
Contributor

@anntzer anntzer Jan 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think the answer is yes, it is; but if you insist

def _rename_parameter(since, old, new, func=None):
    """
    Decorator indicating that a function parameter is being renamed.

    The actual implementation of *func* should use *new*, not *old*.

    Examples
    --------

    ::
        @_rename_parameter("3.1", "bad_name", "good_name")
        def func(good_name): ...
    """

    if func is None:
        return functools.partial(_rename_parameter, since, old, new)

    new_sig = inspect.signature(func)
    old_sig = new_sig.replace(parameters=[
        param.replace(name=old) if param.name == new else param
        for param in new_sig.parameters.values()])

    def _bind_or_none(signature, *args, **kwargs):
        try:
            return signature.bind(*args, **kwargs)
        except TypeError:
            return None

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except TypeError:
            new_ba = _bind_or_none(new_sig, *args, **kwargs)
            if new_ba is not None:
                # Args match the new signature, so the TypeError was not raised
                # during argument binding; reraise it.
                raise
            old_ba = _bind_or_none(old_sig, *args, **kwargs)
            if old_ba is None:
                # Args match neither the old nor the new signature; reraise the
                # argument binding failure.
                raise
        # Move out of the except: block to avoid exception chaining.  The
        # function was called with the old signature.  Emit a deprecation
        # warning and call with the new signature.
        warn_deprecated(
            since, message=f"The {old!r} argument of "
            f"{func.__name__}() has been renamed {new!r} since "
            f"{since}; support for the old name will be dropped "
            f"%(removal)s.")
        return func(*old_ba.args,
                    **{key if key is not old else new: value
                       for key, value in old_ba.kwargs.items()})

    return wrapper

courtesy of the house.

Use:

@_rename_parameter("3.1", "arg", "backend")
def use(backend, ...): ...

(The implementation is kind of overly general but also prepares further cases I want to implement such as deleting an argument or making arguments keyword-only...)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 to adding this to cbook as per phone conversation.

"""
Set the matplotlib backend to one of the known backends.
Select the backend used for rendering and GUI integration.

Parameters
----------
backend : str
The backend to switch to. This can either be one of the
'standard' backend names:
The backend to switch to. This can either be one of the standard
backend names, which are case-insensitive:

- interactive backends:
GTK3Agg, GTK3Cairo, MacOSX, nbAgg,
Expand All @@ -1202,20 +1202,15 @@ def use(backend, warn=False, force=True):

or a string of the form: ``module://my.module.name``.

Note: Standard backend names are case-insensitive here.
warn : bool, optional, default: False
If True and not *force*, warn that the call will have no effect if
this is called after pyplot has been imported and a backend is set up.

*arg* is a deprecated synonym for this parameter.

warn : bool, optional
If True, warn if this is called after pyplot has been imported
and a backend is set up.

defaults to False.

force : bool, optional
force : bool, optional, default: True
If True, attempt to switch the backend. An ImportError is raised if
an interactive backend is selected, but another interactive
backend has already started. This defaults to True.
backend has already started.

See Also
--------
Expand All @@ -1224,32 +1219,31 @@ def use(backend, warn=False, force=True):
"""
name = validate_backend(backend)

# if setting back to the same thing, do nothing
if (dict.__getitem__(rcParams, 'backend') == name):
if dict.__getitem__(rcParams, 'backend') == name:
# Nothing to do if the requested backend is already set
pass

# Check if we have already imported pyplot and triggered
# backend selection, do a bit more work
elif 'matplotlib.pyplot' in sys.modules:
# If we are here then the requested is different than the current.
# pyplot has already been imported (which triggered backend selection)
# and the requested backend is different from the current one.

# If we are going to force the switch, never warn, else, if warn
# is True, then direct users to `plt.switch_backend`
if (not force) and warn:
cbook._warn_external(
("matplotlib.pyplot as already been imported, "
"this call will have no effect."))
"matplotlib.pyplot has already been imported, "
"this call will have no effect.")

# if we are going to force switching the backend, pull in
# `switch_backend` from pyplot. This will only happen if
# pyplot is already imported.
if force:
from matplotlib.pyplot import switch_backend
switch_backend(name)
# Finally if pyplot is not imported update both rcParams and
# rcDefaults so restoring the defaults later with rcdefaults
# won't change the backend. This is a bit of overkill as 'backend'
# is already in style.core.STYLE_BLACKLIST, but better to be safe.
else:
# Finally if pyplot is not imported update both rcParams and
# rcDefaults so restoring the defaults later with rcdefaults
# won't change the backend. This is a bit of overkill as 'backend'
# is already in style.core.STYLE_BLACKLIST, but better to be safe.
rcParams['backend'] = rcParamsDefault['backend'] = name


Expand Down