diff --git a/doc/api/next_api_changes/deprecations/18817-BGB.rst b/doc/api/next_api_changes/deprecations/18817-BGB.rst new file mode 100644 index 000000000000..3088cd5d0db5 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/18817-BGB.rst @@ -0,0 +1,3 @@ +Line2D and Patch no longer duplicate ``validJoin`` and ``validCap`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Validation of joinstyle and capstyles is now centralized in ``rcsetup``. diff --git a/doc/conf.py b/doc/conf.py index da3c981ec693..f220e1f28e37 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -13,8 +13,10 @@ import shutil import subprocess import sys +import warnings import matplotlib +from matplotlib._api import MatplotlibDeprecationWarning import sphinx from datetime import datetime @@ -109,6 +111,11 @@ def _check_dependencies(): autodoc_docstring_signature = True autodoc_default_options = {'members': None, 'undoc-members': None} +# make sure to ignore warnings that stem from simply inspecting deprecated +# class-level attributes +warnings.filterwarnings('ignore', category=MatplotlibDeprecationWarning, + module='sphinx.util.inspect') + # missing-references names matches sphinx>=3 behavior, so we can't be nitpicky # for older sphinxes. nitpicky = sphinx.version_info >= (3,) diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index 96e7d03c33f0..fba2d2e7e1dc 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -19,10 +19,44 @@ deprecated, warn_deprecated, _rename_parameter, _delete_parameter, _make_keyword_only, _deprecate_method_override, _deprecate_privatize_attribute, - _suppress_matplotlib_deprecation_warning, + suppress_matplotlib_deprecation_warning, MatplotlibDeprecationWarning) +class classproperty: + """ + Like `property`, but also triggers on access via the class, and it is the + *class* that's passed as argument. + + Examples + -------- + :: + + class C: + @classproperty + def foo(cls): + return cls.__name__ + + assert C.foo == "C" + """ + + def __init__(self, fget, fset=None, fdel=None, doc=None): + self._fget = fget + if fset is not None or fdel is not None: + raise ValueError('classproperty only implements fget.') + self.fset = fset + self.fdel = fdel + # docs are ignored for now + self._doc = doc + + def __get__(self, instance, owner): + return self._fget(owner) + + @property + def fget(self): + return self._fget + + def check_in_list(_values, *, _print_supported_values=True, **kwargs): """ For each *key, value* pair in *kwargs*, check that *value* is in *_values*. diff --git a/lib/matplotlib/_api/deprecation.py b/lib/matplotlib/_api/deprecation.py index 542cc25ae627..f29806261ab9 100644 --- a/lib/matplotlib/_api/deprecation.py +++ b/lib/matplotlib/_api/deprecation.py @@ -186,6 +186,7 @@ def the_function_to_deprecate(): def deprecate(obj, message=message, name=name, alternative=alternative, pending=pending, obj_type=obj_type, addendum=addendum): + from matplotlib._api import classproperty if isinstance(obj, type): if obj_type is None: @@ -202,15 +203,16 @@ def finalize(wrapper, new_doc): obj.__init__ = functools.wraps(obj.__init__)(wrapper) return obj - elif isinstance(obj, property): + elif isinstance(obj, (property, classproperty)): obj_type = "attribute" func = None name = name or obj.fget.__name__ old_doc = obj.__doc__ - class _deprecated_property(property): + class _deprecated_property(type(obj)): def __get__(self, instance, owner): - if instance is not None: + if instance is not None or owner is not None \ + and isinstance(self, classproperty): emit_warning() return super().__get__(instance, owner) @@ -518,7 +520,7 @@ def empty_with_docstring(): """doc""" @contextlib.contextmanager -def _suppress_matplotlib_deprecation_warning(): +def suppress_matplotlib_deprecation_warning(): with warnings.catch_warnings(): warnings.simplefilter("ignore", MatplotlibDeprecationWarning) yield diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index cbfe9db794f1..caa872424535 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -29,11 +29,13 @@ import matplotlib from matplotlib import _c_internal_utils -from matplotlib._api import warn_external as _warn_external +from matplotlib._api import ( + warn_external as _warn_external, classproperty as _classproperty) from matplotlib._api.deprecation import ( deprecated, warn_deprecated, _rename_parameter, _delete_parameter, _make_keyword_only, _deprecate_method_override, _deprecate_privatize_attribute, + suppress_matplotlib_deprecation_warning as _suppress_matplotlib_deprecation_warning, MatplotlibDeprecationWarning, mplDeprecation) @@ -2262,30 +2264,6 @@ def type_name(tp): type_name(type(v)))) -class _classproperty: - """ - Like `property`, but also triggers on access via the class, and it is the - *class* that's passed as argument. - - Examples - -------- - :: - - class C: - @classproperty - def foo(cls): - return cls.__name__ - - assert C.foo == "C" - """ - - def __init__(self, fget): - self._fget = fget - - def __get__(self, instance, owner): - return self._fget(owner) - - def _backend_module_name(name): """ Convert a backend name (either a standard backend -- "Agg", "TkAgg", ... -- diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 1aea828908e6..aac40606e015 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -3,7 +3,6 @@ colors. """ -# TODO: expose cap and join style attrs from numbers import Integral, Number, Real import logging @@ -251,8 +250,16 @@ class Line2D(Artist): fillStyles = MarkerStyle.fillstyles zorder = 2 - validCap = ('butt', 'round', 'projecting') - validJoin = ('miter', 'round', 'bevel') + + @_api.deprecated("3.4") + @_api.classproperty + def validCap(cls): + return ('butt', 'round', 'projecting') + + @_api.deprecated("3.4") + @_api.classproperty + def validJoin(cls): + return ('miter', 'round', 'bevel') def __str__(self): if self._label != "": diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index d91ff5dcee85..22d5da2c2ff9 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -33,8 +33,18 @@ class Patch(artist.Artist): are *None*, they default to their rc params setting. """ zorder = 1 - validCap = mlines.Line2D.validCap - validJoin = mlines.Line2D.validJoin + + @_api.deprecated("3.4") + @_api.classproperty + def validCap(cls): + with _api.suppress_matplotlib_deprecation_warning(): + return mlines.Line2D.validCap + + @_api.deprecated("3.4") + @_api.classproperty + def validJoin(cls): + with _api.suppress_matplotlib_deprecation_warning(): + return mlines.Line2D.validJoin # Whether to draw an edge by default. Set on a # subclass-by-subclass basis. diff --git a/lib/matplotlib/tests/test_api.py b/lib/matplotlib/tests/test_api.py index be2d80bb4244..5df3e4f75a2d 100644 --- a/lib/matplotlib/tests/test_api.py +++ b/lib/matplotlib/tests/test_api.py @@ -19,3 +19,16 @@ def test_check_shape(target, test_shape): data = np.zeros(test_shape) with pytest.raises(ValueError, match=error_pattern): _api.check_shape(target, aardvark=data) + + +def test_classproperty_deprecation(): + class A: + @_api.deprecated("0.0.0") + @_api.classproperty + def f(cls): + pass + with pytest.warns(_api.MatplotlibDeprecationWarning): + A.f + with pytest.warns(_api.MatplotlibDeprecationWarning): + a = A() + a.f