Skip to content

Commit 648bd55

Browse files
committed
Add typing for internal helpers
While we don't normally type private API, these are used all over Matplotlib, and it makes sense to type them for internal checking purposes.
1 parent 4f65e66 commit 648bd55

File tree

7 files changed

+168
-35
lines changed

7 files changed

+168
-35
lines changed

lib/matplotlib/_api/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def type_name(tp):
9696
type_name(type(v))))
9797

9898

99-
def check_in_list(values, /, *, _print_supported_values=True, **kwargs):
99+
def check_in_list(values, /, *, _print_supported_values=True, **kwargs):
100100
"""
101101
For each *key, value* pair in *kwargs*, check that *value* is in *values*;
102102
if not, raise an appropriate ValueError.
@@ -378,6 +378,6 @@ def warn_external(message, category=None):
378378
frame.f_globals.get("__name__", "")):
379379
break
380380
frame = frame.f_back
381-
# premetively break reference cycle between locals and the frame
381+
# preemptively break reference cycle between locals and the frame
382382
del frame
383383
warnings.warn(message, category, stacklevel)

lib/matplotlib/_api/__init__.pyi

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from collections.abc import Callable, Generator, Mapping, Sequence
2+
from typing import Any, Iterable, TypeVar, overload
3+
4+
from numpy.typing import NDArray
5+
6+
from .deprecation import ( # noqa: re-exported API
7+
deprecated as deprecated,
8+
warn_deprecated as warn_deprecated,
9+
rename_parameter as rename_parameter,
10+
delete_parameter as delete_parameter,
11+
make_keyword_only as make_keyword_only,
12+
deprecate_method_override as deprecate_method_override,
13+
deprecate_privatize_attribute as deprecate_privatize_attribute,
14+
suppress_matplotlib_deprecation_warning as suppress_matplotlib_deprecation_warning,
15+
MatplotlibDeprecationWarning as MatplotlibDeprecationWarning,
16+
)
17+
18+
T = TypeVar("T")
19+
20+
class classproperty:
21+
def __init__(
22+
self,
23+
fget: Callable[[T], Any],
24+
fset: None = ...,
25+
fdel: None = ...,
26+
doc: str | None = None,
27+
): ...
28+
@overload
29+
def __get__(self, instance: None, owner: None) -> classproperty: ...
30+
@overload
31+
def __get__(self, instance: object, owner: type[object]) -> Any: ...
32+
@property
33+
def fget(self) -> Callable[[T], Any]: ...
34+
35+
def check_isinstance(types: type | tuple[type | None, ...], /, **kwargs: Any): ...
36+
def check_in_list(
37+
values: Sequence, /, *, _print_supported_values: bool = ..., **kwargs: Any
38+
) -> None: ...
39+
def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ...
40+
def check_getitem(mapping: Mapping, /, **kwargs: Any) -> Any: ...
41+
def caching_module_getattr(cls: type) -> Callable[[str], Any]: ...
42+
def define_aliases(
43+
alias_d: dict[str, list[str]], cls: type | None = ...
44+
) -> type | Callable: ...
45+
def select_matching_signature(funcs: list[Callable], *args, **kwargs) -> Any: ...
46+
def nargs_error(name: str, takes: int | str, given: int) -> TypeError: ...
47+
def kwarg_error(name: str, kw: str | Iterable[str]) -> TypeError: ...
48+
def recursive_subclasses(cls: type) -> Generator[type, None, None]: ...
49+
def warn_external(
50+
message: str | Warning, category: type[Warning] | None = ...
51+
) -> None: ...

lib/matplotlib/_api/deprecation.pyi

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from collections.abc import Callable
2+
import contextlib
3+
from typing import Literal, TypedDict, TypeVar, overload
4+
from typing_extensions import (
5+
ParamSpec, # < Py 3.10
6+
Unpack, # < Py 3.11
7+
)
8+
9+
P = ParamSpec("P")
10+
R = TypeVar("R")
11+
12+
class MatplotlibDeprecationWarning(DeprecationWarning): ...
13+
14+
class DeprecationKwargs(TypedDict, total=False):
15+
message: str
16+
alternative: str
17+
pending: bool
18+
obj_type: str
19+
addendum: str
20+
removal: str
21+
22+
class NamedDeprecationKwargs(DeprecationKwargs, total=False):
23+
name: str
24+
25+
def warn_deprecated(since: str, **kwargs: Unpack[NamedDeprecationKwargs]) -> None: ...
26+
def deprecated(since: str, **kwargs: Unpack[NamedDeprecationKwargs]) -> Callable: ...
27+
28+
class deprecate_privatize_attribute:
29+
def __init__(self, since: str, **kwargs: Unpack[NamedDeprecationKwargs]): ...
30+
def __set_name__(self, owner: type[object], name: str) -> None: ...
31+
32+
DECORATORS: dict[Callable, Callable] = ...
33+
34+
@overload
35+
def rename_parameter(
36+
since: str, old: str, new: str, func: Literal[None] = None
37+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
38+
@overload
39+
def rename_parameter(func: Callable[P, R]) -> Callable[P, R]: ...
40+
41+
class _deprecated_parameter_class: ...
42+
43+
_deprecated_parameter: _deprecated_parameter_class
44+
45+
@overload
46+
def delete_parameter(
47+
since: str,
48+
name: str,
49+
func: Literal[None] = None,
50+
**kwargs: Unpack[DeprecationKwargs]
51+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
52+
@overload
53+
def delete_parameter(func: Callable[P, R]) -> Callable[P, R]: ...
54+
@overload
55+
def make_keyword_only(
56+
since: str, name: str, func: Literal[None] = None
57+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
58+
@overload
59+
def make_keyword_only(func: Callable[P, R]) -> Callable[P, R]: ...
60+
def deprecate_method_override(
61+
method: Callable[P, R],
62+
obj: object | type,
63+
*,
64+
allow_empty: bool = ...,
65+
since: str,
66+
**kwargs: Unpack[NamedDeprecationKwargs]
67+
) -> Callable[P, R]: ...
68+
def suppress_matplotlib_deprecation_warning() -> (
69+
contextlib.AbstractContextManager[None]
70+
): ...

lib/matplotlib/cbook.pyi

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,18 @@ class CallbackRegistry:
2727
self,
2828
exception_handler: Callable[[Exception], Any] | None = ...,
2929
*,
30-
signals: Iterable[Any] | None = ...
30+
signals: Iterable[Any] | None = ...,
3131
) -> None: ...
3232
def connect(self, signal: Any, func: Callable) -> int: ...
3333
def disconnect(self, cid: int) -> None: ...
3434
def process(self, s: Any, *args, **kwargs) -> None: ...
35-
@contextlib.contextmanager
36-
def blocked(self, *, signal: Any | None = ...): ...
35+
def blocked(
36+
self, *, signal: Any | None = ...
37+
) -> contextlib.AbstractContextManager[None]: ...
3738

3839
class silent_list(list[_T]):
3940
type: str | None
40-
def __init__(self, type, seq: Iterable[_T] | None = ...) -> None: ...
41+
def __init__(self, type: str | None, seq: Iterable[_T] | None = ...) -> None: ...
4142

4243
def strip_math(s: str) -> str: ...
4344
def is_writable_file_like(obj: Any) -> bool: ...
@@ -59,7 +60,7 @@ def to_filehandle(
5960
@overload
6061
def to_filehandle(
6162
fname: str | os.PathLike | IO,
62-
*, # if flag given, will match previous sig
63+
*, # if flag given, will match previous sig
6364
return_opened: Literal[True],
6465
encoding: str | None = ...,
6566
) -> tuple[IO, bool]: ...
@@ -71,24 +72,18 @@ def open_file_cm(
7172
def is_scalar_or_string(val: Any) -> bool: ...
7273
@overload
7374
def get_sample_data(
74-
fname: str | os.PathLike,
75-
asfileobj: Literal[True] = ...,
76-
*,
77-
np_load: Literal[True]
75+
fname: str | os.PathLike, asfileobj: Literal[True] = ..., *, np_load: Literal[True]
7876
) -> np.ndarray: ...
7977
@overload
8078
def get_sample_data(
8179
fname: str | os.PathLike,
8280
asfileobj: Literal[True] = ...,
8381
*,
84-
np_load: Literal[False] = ...
82+
np_load: Literal[False] = ...,
8583
) -> IO: ...
8684
@overload
8785
def get_sample_data(
88-
fname: str | os.PathLike,
89-
asfileobj: Literal[False],
90-
*,
91-
np_load: bool = ...
86+
fname: str | os.PathLike, asfileobj: Literal[False], *, np_load: bool = ...
9287
) -> str: ...
9388
def _get_data_path(*args: Path | str) -> Path: ...
9489
def flatten(
@@ -162,6 +157,8 @@ def normalize_kwargs(
162157
kw: dict[str, Any],
163158
alias_mapping: dict[str, list[str]] | type[Artist] | Artist | None = ...,
164159
) -> dict[str, Any]: ...
160+
def _lock_path(path: str | os.PathLike) -> contextlib.AbstractContextManager: ...
161+
def _setattr_cm(obj: Any, **kwargs) -> contextlib.AbstractContextManager: ...
165162

166163
class _OrderedSet(collections.abc.MutableSet):
167164
def __init__(self) -> None: ...

lib/matplotlib/lines.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,7 @@ def set_xdata(self, x):
12791279
# When deprecation cycle is completed
12801280
# raise RuntimeError('x must be a sequence')
12811281
_api.warn_deprecated(
1282-
since=3.7,
1282+
since="3.7",
12831283
message="Setting data with a non sequence type "
12841284
"is deprecated since %(since)s and will be "
12851285
"remove %(removal)s")
@@ -1300,7 +1300,7 @@ def set_ydata(self, y):
13001300
# When deprecation cycle is completed
13011301
# raise RuntimeError('y must be a sequence')
13021302
_api.warn_deprecated(
1303-
since=3.7,
1303+
since="3.7",
13041304
message="Setting data with a non sequence type "
13051305
"is deprecated since %(since)s and will be "
13061306
"remove %(removal)s")

lib/matplotlib/tests/test_api.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
from __future__ import annotations
2+
13
import re
4+
import typing
5+
from typing import Any, Callable, TypeVar
26

37
import numpy as np
48
import pytest
@@ -7,26 +11,33 @@
711
from matplotlib import _api
812

913

14+
if typing.TYPE_CHECKING:
15+
from typing_extensions import Self
16+
17+
T = TypeVar('T')
18+
19+
1020
@pytest.mark.parametrize('target,test_shape',
1121
[((None, ), (1, 3)),
1222
((None, 3), (1,)),
1323
((None, 3), (1, 2)),
1424
((1, 5), (1, 9)),
1525
((None, 2, None), (1, 3, 1))
1626
])
17-
def test_check_shape(target, test_shape):
27+
def test_check_shape(target: tuple[int | None, ...],
28+
test_shape: tuple[int, ...]) -> None:
1829
error_pattern = (f"^'aardvark' must be {len(target)}D.*" +
1930
re.escape(f'has shape {test_shape}'))
2031
data = np.zeros(test_shape)
2132
with pytest.raises(ValueError, match=error_pattern):
2233
_api.check_shape(target, aardvark=data)
2334

2435

25-
def test_classproperty_deprecation():
36+
def test_classproperty_deprecation() -> None:
2637
class A:
2738
@_api.deprecated("0.0.0")
2839
@_api.classproperty
29-
def f(cls):
40+
def f(cls: Self) -> None:
3041
pass
3142
with pytest.warns(mpl.MatplotlibDeprecationWarning):
3243
A.f
@@ -35,12 +46,12 @@ def f(cls):
3546
a.f
3647

3748

38-
def test_deprecate_privatize_attribute():
49+
def test_deprecate_privatize_attribute() -> None:
3950
class C:
40-
def __init__(self): self._attr = 1
41-
def _meth(self, arg): return arg
42-
attr = _api.deprecate_privatize_attribute("0.0")
43-
meth = _api.deprecate_privatize_attribute("0.0")
51+
def __init__(self) -> None: self._attr = 1
52+
def _meth(self, arg: T) -> T: return arg
53+
attr: 'type[Self._attr]' = _api.deprecate_privatize_attribute("0.0")
54+
meth: 'type[Self._meth]' = _api.deprecate_privatize_attribute("0.0")
4455

4556
c = C()
4657
with pytest.warns(mpl.MatplotlibDeprecationWarning):
@@ -53,31 +64,32 @@ def _meth(self, arg): return arg
5364
assert c.meth(42) == 42
5465

5566

56-
def test_delete_parameter():
67+
def test_delete_parameter() -> None:
5768
@_api.delete_parameter("3.0", "foo")
58-
def func1(foo=None):
69+
def func1(foo: Any = None) -> None:
5970
pass
6071

6172
@_api.delete_parameter("3.0", "foo")
62-
def func2(**kwargs):
73+
def func2(**kwargs: Any) -> None:
6374
pass
6475

76+
func: Callable[..., Any]
6577
for func in [func1, func2]:
6678
func() # No warning.
6779
with pytest.warns(mpl.MatplotlibDeprecationWarning):
6880
func(foo="bar")
6981

70-
def pyplot_wrapper(foo=_api.deprecation._deprecated_parameter):
82+
def pyplot_wrapper(foo: Any = _api.deprecation._deprecated_parameter) -> None:
7183
func1(foo)
7284

7385
pyplot_wrapper() # No warning.
7486
with pytest.warns(mpl.MatplotlibDeprecationWarning):
7587
func(foo="bar")
7688

7789

78-
def test_make_keyword_only():
90+
def test_make_keyword_only() -> None:
7991
@_api.make_keyword_only("3.0", "arg")
80-
def func(pre, arg, post=None):
92+
def func(pre: Any, arg: Any, post: Any = None) -> None:
8193
pass
8294

8395
func(1, arg=2) # Check that no warning is emitted.
@@ -88,14 +100,14 @@ def func(pre, arg, post=None):
88100
func(1, 2, 3)
89101

90102

91-
def test_deprecation_alternative():
103+
def test_deprecation_alternative() -> None:
92104
alternative = "`.f1`, `f2`, `f3(x) <.f3>` or `f4(x)<f4>`"
93105
@_api.deprecated("1", alternative=alternative)
94-
def f():
106+
def f() -> None:
95107
pass
96108
assert alternative in f.__doc__
97109

98110

99-
def test_empty_check_in_list():
111+
def test_empty_check_in_list() -> None:
100112
with pytest.raises(TypeError, match="No argument to check!"):
101113
_api.check_in_list(["a"])

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,6 @@ exclude = [
137137
".*/tinypages",
138138
]
139139
ignore_missing_imports = true
140+
enable_incomplete_feature = [
141+
"Unpack",
142+
]

0 commit comments

Comments
 (0)