diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index 3f74be58fa5a..ee00665c4f1d 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -96,7 +96,7 @@ def type_name(tp): type_name(type(v)))) -def check_in_list(values, /, *, _print_supported_values=True, **kwargs): +def check_in_list(values, /, *, _print_supported_values=True, **kwargs): """ For each *key, value* pair in *kwargs*, check that *value* is in *values*; if not, raise an appropriate ValueError. @@ -378,6 +378,6 @@ def warn_external(message, category=None): frame.f_globals.get("__name__", "")): break frame = frame.f_back - # premetively break reference cycle between locals and the frame + # preemptively break reference cycle between locals and the frame del frame warnings.warn(message, category, stacklevel) diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi new file mode 100644 index 000000000000..84a712d300fa --- /dev/null +++ b/lib/matplotlib/_api/__init__.pyi @@ -0,0 +1,58 @@ +from collections.abc import Callable, Generator, Mapping, Sequence +from typing import Any, Iterable, TypeVar, overload + +from numpy.typing import NDArray + +from .deprecation import ( # noqa: re-exported API + deprecated as deprecated, + warn_deprecated as warn_deprecated, + rename_parameter as rename_parameter, + delete_parameter as delete_parameter, + make_keyword_only as make_keyword_only, + deprecate_method_override as deprecate_method_override, + deprecate_privatize_attribute as deprecate_privatize_attribute, + suppress_matplotlib_deprecation_warning as suppress_matplotlib_deprecation_warning, + MatplotlibDeprecationWarning as MatplotlibDeprecationWarning, +) + +_T = TypeVar("_T") + +class classproperty(Any): + def __init__( + self, + fget: Callable[[_T], Any], + fset: None = ..., + fdel: None = ..., + doc: str | None = None, + ): ... + @overload + def __get__(self, instance: None, owner: None) -> classproperty: ... + @overload + def __get__(self, instance: object, owner: type[object]) -> Any: ... + @property + def fget(self) -> Callable[[_T], Any]: ... + +def check_isinstance( + types: type | tuple[type | None, ...], /, **kwargs: Any +) -> None: ... +def check_in_list( + values: Sequence[Any], /, *, _print_supported_values: bool = ..., **kwargs: Any +) -> None: ... +def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ... +def check_getitem(mapping: Mapping[Any, Any], /, **kwargs: Any) -> Any: ... +def caching_module_getattr(cls: type) -> Callable[[str], Any]: ... +@overload +def define_aliases( + alias_d: dict[str, list[str]], cls: None = ... +) -> Callable[[type[_T]], type[_T]]: ... +@overload +def define_aliases(alias_d: dict[str, list[str]], cls: type[_T]) -> type[_T]: ... +def select_matching_signature( + funcs: list[Callable], *args: Any, **kwargs: Any +) -> Any: ... +def nargs_error(name: str, takes: int | str, given: int) -> TypeError: ... +def kwarg_error(name: str, kw: str | Iterable[str]) -> TypeError: ... +def recursive_subclasses(cls: type) -> Generator[type, None, None]: ... +def warn_external( + message: str | Warning, category: type[Warning] | None = ... +) -> None: ... diff --git a/lib/matplotlib/_api/deprecation.pyi b/lib/matplotlib/_api/deprecation.pyi new file mode 100644 index 000000000000..9619d1b484fc --- /dev/null +++ b/lib/matplotlib/_api/deprecation.pyi @@ -0,0 +1,76 @@ +from collections.abc import Callable +import contextlib +from typing import Any, TypedDict, TypeVar, overload +from typing_extensions import ( + ParamSpec, # < Py 3.10 + Unpack, # < Py 3.11 +) + +_P = ParamSpec("_P") +_R = TypeVar("_R") +_T = TypeVar("_T") + +class MatplotlibDeprecationWarning(DeprecationWarning): ... + +class DeprecationKwargs(TypedDict, total=False): + message: str + alternative: str + pending: bool + obj_type: str + addendum: str + removal: str + +class NamedDeprecationKwargs(DeprecationKwargs, total=False): + name: str + +def warn_deprecated(since: str, **kwargs: Unpack[NamedDeprecationKwargs]) -> None: ... +def deprecated( + since: str, **kwargs: Unpack[NamedDeprecationKwargs] +) -> Callable[[_T], _T]: ... + +class deprecate_privatize_attribute(Any): + def __init__(self, since: str, **kwargs: Unpack[NamedDeprecationKwargs]): ... + def __set_name__(self, owner: type[object], name: str) -> None: ... + +DECORATORS: dict[Callable, Callable] = ... + +@overload +def rename_parameter( + since: str, old: str, new: str, func: None = ... +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ... +@overload +def rename_parameter( + since: str, old: str, new: str, func: Callable[_P, _R] +) -> Callable[_P, _R]: ... + +class _deprecated_parameter_class: ... + +_deprecated_parameter: _deprecated_parameter_class + +@overload +def delete_parameter( + since: str, name: str, func: None = ..., **kwargs: Unpack[DeprecationKwargs] +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ... +@overload +def delete_parameter( + since: str, name: str, func: Callable[_P, _R], **kwargs: Unpack[DeprecationKwargs] +) -> Callable[_P, _R]: ... +@overload +def make_keyword_only( + since: str, name: str, func: None = ... +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ... +@overload +def make_keyword_only( + since: str, name: str, func: Callable[_P, _R] +) -> Callable[_P, _R]: ... +def deprecate_method_override( + method: Callable[_P, _R], + obj: object | type, + *, + allow_empty: bool = ..., + since: str, + **kwargs: Unpack[NamedDeprecationKwargs] +) -> Callable[_P, _R]: ... +def suppress_matplotlib_deprecation_warning() -> ( + contextlib.AbstractContextManager[None] +): ... diff --git a/lib/matplotlib/artist.pyi b/lib/matplotlib/artist.pyi index 6e5dddaef654..190cd95c7c2b 100644 --- a/lib/matplotlib/artist.pyi +++ b/lib/matplotlib/artist.pyi @@ -13,7 +13,7 @@ from .transforms import ( import numpy as np -from collections.abc import Callable +from collections.abc import Callable, Iterable from typing import Any, NamedTuple, TextIO, overload from numpy.typing import ArrayLike @@ -137,7 +137,6 @@ class Artist: def format_cursor_data(self, data: Any) -> str: ... def get_mouseover(self) -> bool: ... def set_mouseover(self, mouseover: bool) -> None: ... - @property def mouseover(self) -> bool: ... @mouseover.setter @@ -147,7 +146,9 @@ class ArtistInspector: oorig: Artist | type[Artist] o: type[Artist] aliasd: dict[str, set[str]] - def __init__(self, o) -> None: ... + def __init__( + self, o: Artist | type[Artist] | Iterable[Artist | type[Artist]] + ) -> None: ... def get_aliases(self) -> dict[str, set[str]]: ... def get_valid_values(self, attr: str) -> str | None: ... def get_setters(self) -> list[str]: ... @@ -177,4 +178,4 @@ def getp(obj: Artist, property: str | None = ...) -> Any: ... get = getp def setp(obj: Artist, *args, file: TextIO | None = ..., **kwargs): ... -def kwdoc(artist: Artist) -> str: ... +def kwdoc(artist: Artist | type[Artist] | Iterable[Artist | type[Artist]]) -> str: ... diff --git a/lib/matplotlib/cbook.pyi b/lib/matplotlib/cbook.pyi index 3bf0e345e997..227a23df4168 100644 --- a/lib/matplotlib/cbook.pyi +++ b/lib/matplotlib/cbook.pyi @@ -29,17 +29,18 @@ class CallbackRegistry: self, exception_handler: Callable[[Exception], Any] | None = ..., *, - signals: Iterable[Any] | None = ... + signals: Iterable[Any] | None = ..., ) -> None: ... def connect(self, signal: Any, func: Callable) -> int: ... def disconnect(self, cid: int) -> None: ... def process(self, s: Any, *args, **kwargs) -> None: ... - @contextlib.contextmanager - def blocked(self, *, signal: Any | None = ...): ... + def blocked( + self, *, signal: Any | None = ... + ) -> contextlib.AbstractContextManager[None]: ... class silent_list(list[_T]): type: str | None - def __init__(self, type, seq: Iterable[_T] | None = ...) -> None: ... + def __init__(self, type: str | None, seq: Iterable[_T] | None = ...) -> None: ... def strip_math(s: str) -> str: ... def is_writable_file_like(obj: Any) -> bool: ... @@ -61,7 +62,7 @@ def to_filehandle( @overload def to_filehandle( fname: str | os.PathLike | IO, - *, # if flag given, will match previous sig + *, # if flag given, will match previous sig return_opened: Literal[True], encoding: str | None = ..., ) -> tuple[IO, bool]: ... @@ -73,24 +74,18 @@ def open_file_cm( def is_scalar_or_string(val: Any) -> bool: ... @overload def get_sample_data( - fname: str | os.PathLike, - asfileobj: Literal[True] = ..., - *, - np_load: Literal[True] + fname: str | os.PathLike, asfileobj: Literal[True] = ..., *, np_load: Literal[True] ) -> np.ndarray: ... @overload def get_sample_data( fname: str | os.PathLike, asfileobj: Literal[True] = ..., *, - np_load: Literal[False] = ... + np_load: Literal[False] = ..., ) -> IO: ... @overload def get_sample_data( - fname: str | os.PathLike, - asfileobj: Literal[False], - *, - np_load: bool = ... + fname: str | os.PathLike, asfileobj: Literal[False], *, np_load: bool = ... ) -> str: ... def _get_data_path(*args: Path | str) -> Path: ... def flatten( @@ -164,7 +159,9 @@ def normalize_kwargs( kw: dict[str, Any], alias_mapping: dict[str, list[str]] | type[Artist] | Artist | None = ..., ) -> dict[str, Any]: ... +def _lock_path(path: str | os.PathLike) -> contextlib.AbstractContextManager[None]: ... def _str_equal(obj: Any, s: str) -> bool: ... +def _setattr_cm(obj: Any, **kwargs) -> contextlib.AbstractContextManager[None]: ... class _OrderedSet(collections.abc.MutableSet): def __init__(self) -> None: ... diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index eabde8dcde66..92d55a3fe6ae 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -1279,7 +1279,7 @@ def set_xdata(self, x): # When deprecation cycle is completed # raise RuntimeError('x must be a sequence') _api.warn_deprecated( - since=3.7, + since="3.7", message="Setting data with a non sequence type " "is deprecated since %(since)s and will be " "remove %(removal)s") @@ -1300,7 +1300,7 @@ def set_ydata(self, y): # When deprecation cycle is completed # raise RuntimeError('y must be a sequence') _api.warn_deprecated( - since=3.7, + since="3.7", message="Setting data with a non sequence type " "is deprecated since %(since)s and will be " "remove %(removal)s") diff --git a/lib/matplotlib/tests/test_api.py b/lib/matplotlib/tests/test_api.py index c6ac059c359c..34549efb0534 100644 --- a/lib/matplotlib/tests/test_api.py +++ b/lib/matplotlib/tests/test_api.py @@ -1,4 +1,8 @@ +from __future__ import annotations + import re +import typing +from typing import Any, Callable, TypeVar import numpy as np import pytest @@ -7,6 +11,12 @@ from matplotlib import _api +if typing.TYPE_CHECKING: + from typing_extensions import Self + +T = TypeVar('T') + + @pytest.mark.parametrize('target,test_shape', [((None, ), (1, 3)), ((None, 3), (1,)), @@ -14,7 +24,8 @@ ((1, 5), (1, 9)), ((None, 2, None), (1, 3, 1)) ]) -def test_check_shape(target, test_shape): +def test_check_shape(target: tuple[int | None, ...], + test_shape: tuple[int, ...]) -> None: error_pattern = (f"^'aardvark' must be {len(target)}D.*" + re.escape(f'has shape {test_shape}')) data = np.zeros(test_shape) @@ -22,11 +33,11 @@ def test_check_shape(target, test_shape): _api.check_shape(target, aardvark=data) -def test_classproperty_deprecation(): +def test_classproperty_deprecation() -> None: class A: @_api.deprecated("0.0.0") @_api.classproperty - def f(cls): + def f(cls: Self) -> None: pass with pytest.warns(mpl.MatplotlibDeprecationWarning): A.f @@ -35,12 +46,12 @@ def f(cls): a.f -def test_deprecate_privatize_attribute(): +def test_deprecate_privatize_attribute() -> None: class C: - def __init__(self): self._attr = 1 - def _meth(self, arg): return arg - attr = _api.deprecate_privatize_attribute("0.0") - meth = _api.deprecate_privatize_attribute("0.0") + def __init__(self) -> None: self._attr = 1 + def _meth(self, arg: T) -> T: return arg + attr: int = _api.deprecate_privatize_attribute("0.0") + meth: Callable = _api.deprecate_privatize_attribute("0.0") c = C() with pytest.warns(mpl.MatplotlibDeprecationWarning): @@ -53,21 +64,21 @@ def _meth(self, arg): return arg assert c.meth(42) == 42 -def test_delete_parameter(): +def test_delete_parameter() -> None: @_api.delete_parameter("3.0", "foo") - def func1(foo=None): + def func1(foo: Any = None) -> None: pass @_api.delete_parameter("3.0", "foo") - def func2(**kwargs): + def func2(**kwargs: Any) -> None: pass - for func in [func1, func2]: + for func in [func1, func2]: # type: ignore[list-item] func() # No warning. with pytest.warns(mpl.MatplotlibDeprecationWarning): func(foo="bar") - def pyplot_wrapper(foo=_api.deprecation._deprecated_parameter): + def pyplot_wrapper(foo: Any = _api.deprecation._deprecated_parameter) -> None: func1(foo) pyplot_wrapper() # No warning. @@ -75,9 +86,9 @@ def pyplot_wrapper(foo=_api.deprecation._deprecated_parameter): func(foo="bar") -def test_make_keyword_only(): +def test_make_keyword_only() -> None: @_api.make_keyword_only("3.0", "arg") - def func(pre, arg, post=None): + def func(pre: Any, arg: Any, post: Any = None) -> None: pass func(1, arg=2) # Check that no warning is emitted. @@ -88,14 +99,16 @@ def func(pre, arg, post=None): func(1, 2, 3) -def test_deprecation_alternative(): +def test_deprecation_alternative() -> None: alternative = "`.f1`, `f2`, `f3(x) <.f3>` or `f4(x)`" @_api.deprecated("1", alternative=alternative) - def f(): + def f() -> None: pass + if f.__doc__ is None: + pytest.skip('Documentation is disabled') assert alternative in f.__doc__ -def test_empty_check_in_list(): +def test_empty_check_in_list() -> None: with pytest.raises(TypeError, match="No argument to check!"): _api.check_in_list(["a"]) diff --git a/pyproject.toml b/pyproject.toml index e3d4d2117aa4..d6d08ade5269 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,3 +137,6 @@ exclude = [ ".*/tinypages", ] ignore_missing_imports = true +enable_incomplete_feature = [ + "Unpack", +] diff --git a/requirements/testing/mypy.txt b/requirements/testing/mypy.txt index 707965be5aa4..801601bcd6eb 100644 --- a/requirements/testing/mypy.txt +++ b/requirements/testing/mypy.txt @@ -1,7 +1,7 @@ # Extra pip requirements for the GitHub Actions mypy build mypy==1.1.1 -typing-extensions +typing-extensions>=4.1,<5 # Extra stubs distributed separately from the main pypi package pandas-stubs