Skip to content

Commit

Permalink
Merge pull request matplotlib#20857 from anntzer/module-getattr
Browse files Browse the repository at this point in the history
Propose a less error-prone helper for module-level getattrs.
  • Loading branch information
QuLogic authored Aug 20, 2021
2 parents 7b9e1ce + d427153 commit af68218
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 70 deletions.
18 changes: 8 additions & 10 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,14 @@ def _get_version():
return _version.version


@functools.lru_cache(None)
def __getattr__(name):
if name == "__version__":
return _get_version()
elif name == "__version_info__":
return _parse_to_version_info(__getattr__("__version__"))
elif name == "URL_REGEX": # module-level deprecation.
_api.warn_deprecated("3.5", name=name)
return re.compile(r'^http://|^https://|^ftp://|^file:')
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@_api.caching_module_getattr
class __getattr__:
__version__ = property(lambda self: _get_version())
__version_info__ = property(
lambda self: _parse_to_version_info(self.__version__))
# module-level deprecations
URL_REGEX = _api.deprecated("3.5", obj_type="")(property(
lambda self: re.compile(r'^http://|^https://|^ftp://|^file:')))


def _check_versions():
Expand Down
36 changes: 36 additions & 0 deletions lib/matplotlib/_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""

import functools
import itertools
import re
import sys
Expand Down Expand Up @@ -189,6 +190,41 @@ def check_getitem(_mapping, **kwargs):
.format(v, k, ', '.join(map(repr, mapping)))) from None


def caching_module_getattr(cls):
"""
Helper decorator for implementing module-level ``__getattr__`` as a class.
This decorator must be used at the module toplevel as follows::
@caching_module_getattr
class __getattr__: # The class *must* be named ``__getattr__``.
@property # Only properties are taken into account.
def name(self): ...
The ``__getattr__`` class will be replaced by a ``__getattr__``
function such that trying to access ``name`` on the module will
resolve the corresponding property (which may be decorated e.g. with
``_api.deprecated`` for deprecating module globals). The properties are
all implicitly cached. Moreover, a suitable AttributeError is generated
and raised if no property with the given name exists.
"""

assert cls.__name__ == "__getattr__"
# Don't accidentally export cls dunders.
props = {name: prop for name, prop in vars(cls).items()
if isinstance(prop, property)}
instance = cls()

@functools.lru_cache(None)
def __getattr__(name):
if name in props:
return props[name].__get__(instance)
raise AttributeError(
f"module {cls.__module__!r} has no attribute {name!r}")

return __getattr__


def select_matching_signature(funcs, *args, **kwargs):
"""
Select and call the function that accepts ``*args, **kwargs``.
Expand Down
5 changes: 3 additions & 2 deletions lib/matplotlib/_api/deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,14 @@ def finalize(wrapper, new_doc):
return obj

elif isinstance(obj, (property, classproperty)):
obj_type = "attribute"
if obj_type is None:
obj_type = "attribute"
func = None
name = name or obj.fget.__name__
old_doc = obj.__doc__

class _deprecated_property(type(obj)):
def __get__(self, instance, owner):
def __get__(self, instance, owner=None):
if instance is not None or owner is not None \
and isinstance(self, classproperty):
emit_warning()
Expand Down
12 changes: 5 additions & 7 deletions lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@
Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version())


# module-level deprecations.
@functools.lru_cache(None)
def __getattr__(name):
if name == "cursord":
_api.warn_deprecated("3.5", name=name)
@_api.caching_module_getattr # module-level deprecations
class __getattr__:
@_api.deprecated("3.5", obj_type="")
@property
def cursord(self):
try:
new_cursor = functools.partial(
Gdk.Cursor.new_from_name, Gdk.Display.get_default())
Expand All @@ -54,8 +54,6 @@ def __getattr__(name):
}
except TypeError as exc:
return {}
else:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


# Placeholder
Expand Down
32 changes: 13 additions & 19 deletions lib/matplotlib/backends/backend_wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,19 @@
PIXELS_PER_INCH = 75


# module-level deprecations.
@functools.lru_cache(None)
def __getattr__(name):
if name == "IDLE_DELAY":
_api.warn_deprecated("3.1", name=name)
return 5
elif name == "cursord":
_api.warn_deprecated("3.5", name=name)
return { # deprecated in Matplotlib 3.5.
cursors.MOVE: wx.CURSOR_HAND,
cursors.HAND: wx.CURSOR_HAND,
cursors.POINTER: wx.CURSOR_ARROW,
cursors.SELECT_REGION: wx.CURSOR_CROSS,
cursors.WAIT: wx.CURSOR_WAIT,
cursors.RESIZE_HORIZONTAL: wx.CURSOR_SIZEWE,
cursors.RESIZE_VERTICAL: wx.CURSOR_SIZENS,
}
else:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@_api.caching_module_getattr # module-level deprecations
class __getattr__:
IDLE_DELAY = _api.deprecated("3.1", obj_type="", removal="3.6")(property(
lambda self: 5))
cursord = _api.deprecated("3.5", obj_type="")(property(lambda self: {
cursors.MOVE: wx.CURSOR_HAND,
cursors.HAND: wx.CURSOR_HAND,
cursors.POINTER: wx.CURSOR_ARROW,
cursors.SELECT_REGION: wx.CURSOR_CROSS,
cursors.WAIT: wx.CURSOR_WAIT,
cursors.RESIZE_HORIZONTAL: wx.CURSOR_SIZEWE,
cursors.RESIZE_VERTICAL: wx.CURSOR_SIZENS,
}))


def error_msg_wx(msg, parent=None):
Expand Down
14 changes: 5 additions & 9 deletions lib/matplotlib/cm.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"""

from collections.abc import Mapping, MutableMapping
import functools

import numpy as np
from numpy import ma
Expand All @@ -27,14 +26,11 @@
from matplotlib._cm_listed import cmaps as cmaps_listed


# module-level deprecations.
@functools.lru_cache(None)
def __getattr__(name):
if name == "LUTSIZE":
_api.warn_deprecated("3.5", name=name,
alternative="rcParams['image.lut']")
return _LUTSIZE
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@_api.caching_module_getattr # module-level deprecations
class __getattr__:
LUTSIZE = _api.deprecated(
"3.5", obj_type="", alternative="rcParams['image.lut']")(
property(lambda self: _LUTSIZE))


_LUTSIZE = mpl.rcParams['image.lut']
Expand Down
23 changes: 8 additions & 15 deletions lib/matplotlib/colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"""

import copy
import functools
import logging
import textwrap

Expand Down Expand Up @@ -195,20 +194,14 @@
_colormap_kw_doc))


# module-level deprecations.
@functools.lru_cache(None)
def __getattr__(name):
if name == "colorbar_doc":
_api.warn_deprecated("3.4", name=name)
return docstring.interpd.params["colorbar_doc"]
elif name == "colormap_kw_doc":
_api.warn_deprecated("3.4", name=name)
return _colormap_kw_doc
elif name == "make_axes_kw_doc":
_api.warn_deprecated("3.4", name=name)
return _make_axes_param_doc + _make_axes_other_param_doc
else:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@_api.caching_module_getattr # module-level deprecations
class __getattr__:
colorbar_doc = _api.deprecated("3.4", obj_type="")(property(
lambda self: docstring.interpd.params["colorbar_doc"]))
colorbar_kw_doc = _api.deprecated("3.4", obj_type="")(property(
lambda self: _colormap_kw_doc))
make_axes_kw_doc = _api.deprecated("3.4", obj_type="")(property(
lambda self: _make_axes_param_doc + _make_axes_other_param_doc))


def _set_ticks_on_axis_warn(*args, **kw):
Expand Down
12 changes: 4 additions & 8 deletions lib/matplotlib/style/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"""

import contextlib
import functools
import logging
import os
from pathlib import Path
Expand All @@ -27,13 +26,10 @@
__all__ = ['use', 'context', 'available', 'library', 'reload_library']


# module-level deprecations.
@functools.lru_cache(None)
def __getattr__(name):
if name == "STYLE_FILE_PATTERN":
_api.warn_deprecated("3.5", name=name)
return re.compile(r'([\S]+).%s$' % STYLE_EXTENSION)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@_api.caching_module_getattr # module-level deprecations
class __getattr__:
STYLE_FILE_PATTERN = _api.deprecated("3.5", obj_type="")(property(
lambda self: re.compile(r'([\S]+).%s$' % STYLE_EXTENSION)))


BASE_LIBRARY_PATH = os.path.join(mpl.get_data_path(), 'stylelib')
Expand Down
28 changes: 28 additions & 0 deletions lib/matplotlib/tests/test_getattr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from importlib import import_module
from pkgutil import walk_packages

import matplotlib
import pytest

# Get the names of all matplotlib submodules, except for the unit tests.
module_names = [m.name for m in walk_packages(path=matplotlib.__path__,
prefix=f'{matplotlib.__name__}.')
if not m.name.startswith(__package__)]


@pytest.mark.parametrize('module_name', module_names)
@pytest.mark.filterwarnings('ignore::DeprecationWarning')
def test_getattr(module_name):
"""
Test that __getattr__ methods raise AttributeError for unknown keys.
See #20822, #20855.
"""
try:
module = import_module(module_name)
except (ImportError, RuntimeError) as e:
# Skip modules that cannot be imported due to missing dependencies
pytest.skip(f'Cannot import {module_name} due to {e}')

key = 'THIS_SYMBOL_SHOULD_NOT_EXIST'
if hasattr(module, key):
delattr(module, key)

0 comments on commit af68218

Please sign in to comment.