diff options
author | Bas van Beek <b.f.van.beek@vu.nl> | 2021-09-30 11:13:06 +0200 |
---|---|---|
committer | Bas van Beek <b.f.van.beek@vu.nl> | 2021-09-30 11:51:51 +0200 |
commit | afd99d7c76b4caeee419d09f1d2bed661226a59e (patch) | |
tree | a924700798b5c2a2f02e223adf67f07c8bd3a631 /numpy | |
parent | 1cadccc6012124e38e02a78b39bf268123d272c1 (diff) | |
download | numpy-afd99d7c76b4caeee419d09f1d2bed661226a59e.tar.gz |
STY: Introduce various linting fixes to `numpy.typing`
* Introduce various linting fixes to `numpy.typing`
* Add annotations to a few internal testing functions
* Switch to PEP 585 `builtins` annotations where possible
Diffstat (limited to 'numpy')
-rw-r--r-- | numpy/typing/__init__.py | 54 | ||||
-rw-r--r-- | numpy/typing/_add_docstring.py | 15 | ||||
-rw-r--r-- | numpy/typing/_dtype_like.py | 19 | ||||
-rw-r--r-- | numpy/typing/_extended_precision.py | 3 | ||||
-rw-r--r-- | numpy/typing/mypy_plugin.py | 36 | ||||
-rw-r--r-- | numpy/typing/tests/test_runtime.py | 4 | ||||
-rw-r--r-- | numpy/typing/tests/test_typing.py | 108 |
7 files changed, 165 insertions, 74 deletions
diff --git a/numpy/typing/__init__.py b/numpy/typing/__init__.py index 1e366eb34..d5cfbf5ac 100644 --- a/numpy/typing/__init__.py +++ b/numpy/typing/__init__.py @@ -114,8 +114,9 @@ runtime, they're not necessarily considered as sub-classes. Timedelta64 ~~~~~~~~~~~ -The `~numpy.timedelta64` class is not considered a subclass of `~numpy.signedinteger`, -the former only inheriting from `~numpy.generic` while static type checking. +The `~numpy.timedelta64` class is not considered a subclass of +`~numpy.signedinteger`, the former only inheriting from `~numpy.generic` +while static type checking. 0D arrays ~~~~~~~~~ @@ -154,8 +155,10 @@ API # NOTE: The API section will be appended with additional entries # further down in this file +from __future__ import annotations + from numpy import ufunc -from typing import TYPE_CHECKING, List, final +from typing import TYPE_CHECKING, final if not TYPE_CHECKING: __all__ = ["ArrayLike", "DTypeLike", "NBitBase", "NDArray"] @@ -166,14 +169,14 @@ else: # # Declare to mypy that `__all__` is a list of strings without assigning # an explicit value - __all__: List[str] - __path__: List[str] + __all__: list[str] + __path__: list[str] @final # Disallow the creation of arbitrary `NBitBase` subclasses class NBitBase: """ - An object representing `numpy.number` precision during static type checking. + A type representing `numpy.number` precision during static type checking. Used exclusively for the purpose static type checking, `NBitBase` represents the base of a hierarchical set of subclasses. @@ -184,9 +187,9 @@ class NBitBase: Examples -------- - Below is a typical usage example: `NBitBase` is herein used for annotating a - function that takes a float and integer of arbitrary precision as arguments - and returns a new float of whichever precision is largest + Below is a typical usage example: `NBitBase` is herein used for annotating + a function that takes a float and integer of arbitrary precision + as arguments and returns a new float of whichever precision is largest (*e.g.* ``np.float16 + np.int64 -> np.float64``). .. code-block:: python @@ -226,14 +229,29 @@ class NBitBase: # Silence errors about subclassing a `@final`-decorated class -class _256Bit(NBitBase): ... # type: ignore[misc] -class _128Bit(_256Bit): ... # type: ignore[misc] -class _96Bit(_128Bit): ... # type: ignore[misc] -class _80Bit(_96Bit): ... # type: ignore[misc] -class _64Bit(_80Bit): ... # type: ignore[misc] -class _32Bit(_64Bit): ... # type: ignore[misc] -class _16Bit(_32Bit): ... # type: ignore[misc] -class _8Bit(_16Bit): ... # type: ignore[misc] +class _256Bit(NBitBase): # type: ignore[misc] + pass + +class _128Bit(_256Bit): # type: ignore[misc] + pass + +class _96Bit(_128Bit): # type: ignore[misc] + pass + +class _80Bit(_96Bit): # type: ignore[misc] + pass + +class _64Bit(_80Bit): # type: ignore[misc] + pass + +class _32Bit(_64Bit): # type: ignore[misc] + pass + +class _16Bit(_32Bit): # type: ignore[misc] + pass + +class _8Bit(_16Bit): # type: ignore[misc] + pass from ._nested_sequence import _NestedSequence @@ -363,7 +381,7 @@ else: _GUFunc_Nin2_Nout1 = ufunc # Clean up the namespace -del TYPE_CHECKING, final, List, ufunc +del TYPE_CHECKING, final, ufunc if __doc__ is not None: from ._add_docstring import _docstrings diff --git a/numpy/typing/_add_docstring.py b/numpy/typing/_add_docstring.py index 846b67042..10d77f516 100644 --- a/numpy/typing/_add_docstring.py +++ b/numpy/typing/_add_docstring.py @@ -50,16 +50,17 @@ def _parse_docstrings() -> str: new_lines.append("") else: new_lines.append(f"{indent}{line}") - s = "\n".join(new_lines) - # Done. - type_list_ret.append(f""".. data:: {name}\n :value: {value}\n {s}""") + s = "\n".join(new_lines) + s_block = f""".. data:: {name}\n :value: {value}\n {s}""" + type_list_ret.append(s_block) return "\n".join(type_list_ret) add_newdoc('ArrayLike', 'typing.Union[...]', """ - A `~typing.Union` representing objects that can be coerced into an `~numpy.ndarray`. + A `~typing.Union` representing objects that can be coerced + into an `~numpy.ndarray`. Among others this includes the likes of: @@ -88,7 +89,8 @@ add_newdoc('ArrayLike', 'typing.Union[...]', add_newdoc('DTypeLike', 'typing.Union[...]', """ - A `~typing.Union` representing objects that can be coerced into a `~numpy.dtype`. + A `~typing.Union` representing objects that can be coerced + into a `~numpy.dtype`. Among others this includes the likes of: @@ -101,7 +103,8 @@ add_newdoc('DTypeLike', 'typing.Union[...]', See Also -------- :ref:`Specifying and constructing data types <arrays.dtypes.constructing>` - A comprehensive overview of all objects that can be coerced into data types. + A comprehensive overview of all objects that can be coerced + into data types. Examples -------- diff --git a/numpy/typing/_dtype_like.py b/numpy/typing/_dtype_like.py index 0955f5b18..fdfb955b5 100644 --- a/numpy/typing/_dtype_like.py +++ b/numpy/typing/_dtype_like.py @@ -1,4 +1,14 @@ -from typing import Any, List, Sequence, Tuple, Union, Type, TypeVar, Protocol, TypedDict +from typing import ( + Any, + List, + Sequence, + Tuple, + Union, + Type, + TypeVar, + Protocol, + TypedDict, +) import numpy as np @@ -55,18 +65,23 @@ class _DTypeDictBase(TypedDict): names: Sequence[str] formats: Sequence[_DTypeLikeNested] + # Mandatory + optional keys class _DTypeDict(_DTypeDictBase, total=False): + # Only `str` elements are usable as indexing aliases, + # but `titles` can in principle accept any object offsets: Sequence[int] - titles: Sequence[Any] # Only `str` elements are usable as indexing aliases, but all objects are legal + titles: Sequence[Any] itemsize: int aligned: bool + # A protocol for anything with the dtype attribute class _SupportsDType(Protocol[_DType_co]): @property def dtype(self) -> _DType_co: ... + # Would create a dtype[np.void] _VoidDTypeLike = Union[ # (flexible_dtype, itemsize) diff --git a/numpy/typing/_extended_precision.py b/numpy/typing/_extended_precision.py index 0900bc659..edc1778ce 100644 --- a/numpy/typing/_extended_precision.py +++ b/numpy/typing/_extended_precision.py @@ -1,4 +1,5 @@ -"""A module with platform-specific extended precision `numpy.number` subclasses. +"""A module with platform-specific extended precision +`numpy.number` subclasses. The subclasses are defined here (instead of ``__init__.pyi``) such that they can be imported conditionally via the numpy's mypy plugin. diff --git a/numpy/typing/mypy_plugin.py b/numpy/typing/mypy_plugin.py index 091980d65..5421d6bfa 100644 --- a/numpy/typing/mypy_plugin.py +++ b/numpy/typing/mypy_plugin.py @@ -33,7 +33,8 @@ To enable the plugin, one must add it to their mypy `configuration file`_: from __future__ import annotations -import typing as t +from collections.abc import Iterable +from typing import Final, TYPE_CHECKING, Callable import numpy as np @@ -44,15 +45,15 @@ try: from mypy.nodes import MypyFile, ImportFrom, Statement from mypy.build import PRI_MED - _HookFunc = t.Callable[[AnalyzeTypeContext], Type] + _HookFunc = Callable[[AnalyzeTypeContext], Type] MYPY_EX: None | ModuleNotFoundError = None except ModuleNotFoundError as ex: MYPY_EX = ex -__all__: t.List[str] = [] +__all__: list[str] = [] -def _get_precision_dict() -> t.Dict[str, str]: +def _get_precision_dict() -> dict[str, str]: names = [ ("_NBitByte", np.byte), ("_NBitShort", np.short), @@ -73,7 +74,7 @@ def _get_precision_dict() -> t.Dict[str, str]: return ret -def _get_extended_precision_list() -> t.List[str]: +def _get_extended_precision_list() -> list[str]: extended_types = [np.ulonglong, np.longlong, np.longdouble, np.clongdouble] extended_names = { "uint128", @@ -107,13 +108,13 @@ def _get_c_intp_name() -> str: #: A dictionary mapping type-aliases in `numpy.typing._nbit` to #: concrete `numpy.typing.NBitBase` subclasses. -_PRECISION_DICT: t.Final = _get_precision_dict() +_PRECISION_DICT: Final = _get_precision_dict() #: A list with the names of all extended precision `np.number` subclasses. -_EXTENDED_PRECISION_LIST: t.Final = _get_extended_precision_list() +_EXTENDED_PRECISION_LIST: Final = _get_extended_precision_list() #: The name of the ctypes quivalent of `np.intp` -_C_INTP: t.Final = _get_c_intp_name() +_C_INTP: Final = _get_c_intp_name() def _hook(ctx: AnalyzeTypeContext) -> Type: @@ -124,8 +125,8 @@ def _hook(ctx: AnalyzeTypeContext) -> Type: return api.named_type(name_new) -if t.TYPE_CHECKING or MYPY_EX is None: - def _index(iterable: t.Iterable[Statement], id: str) -> int: +if TYPE_CHECKING or MYPY_EX is None: + def _index(iterable: Iterable[Statement], id: str) -> int: """Identify the first ``ImportFrom`` instance the specified `id`.""" for i, value in enumerate(iterable): if getattr(value, "id", None) == id: @@ -137,7 +138,7 @@ if t.TYPE_CHECKING or MYPY_EX is None: def _override_imports( file: MypyFile, module: str, - imports: t.List[t.Tuple[str, t.Optional[str]]], + imports: list[tuple[str, None | str]], ) -> None: """Override the first `module`-based import with new `imports`.""" # Construct a new `from module import y` statement @@ -145,7 +146,7 @@ if t.TYPE_CHECKING or MYPY_EX is None: import_obj.is_top_level = True # Replace the first `module`-based import statement with `import_obj` - for lst in [file.defs, file.imports]: # type: t.List[Statement] + for lst in [file.defs, file.imports]: # type: list[Statement] i = _index(lst, module) lst[i] = import_obj @@ -153,7 +154,8 @@ if t.TYPE_CHECKING or MYPY_EX is None: """A mypy plugin for handling versus numpy-specific typing tasks.""" def get_type_analyze_hook(self, fullname: str) -> None | _HookFunc: - """Set the precision of platform-specific `numpy.number` subclasses. + """Set the precision of platform-specific `numpy.number` + subclasses. For example: `numpy.int_`, `numpy.longlong` and `numpy.longdouble`. """ @@ -161,7 +163,9 @@ if t.TYPE_CHECKING or MYPY_EX is None: return _hook return None - def get_additional_deps(self, file: MypyFile) -> t.List[t.Tuple[int, str, int]]: + def get_additional_deps( + self, file: MypyFile + ) -> list[tuple[int, str, int]]: """Handle all import-based overrides. * Import platform-specific extended-precision `numpy.number` @@ -184,11 +188,11 @@ if t.TYPE_CHECKING or MYPY_EX is None: ) return ret - def plugin(version: str) -> t.Type[_NumpyPlugin]: + def plugin(version: str) -> type[_NumpyPlugin]: """An entry-point for mypy.""" return _NumpyPlugin else: - def plugin(version: str) -> t.Type[_NumpyPlugin]: + def plugin(version: str) -> type[_NumpyPlugin]: """An entry-point for mypy.""" raise MYPY_EX diff --git a/numpy/typing/tests/test_runtime.py b/numpy/typing/tests/test_runtime.py index 151b06bed..5b5df49dc 100644 --- a/numpy/typing/tests/test_runtime.py +++ b/numpy/typing/tests/test_runtime.py @@ -3,7 +3,7 @@ from __future__ import annotations import sys -from typing import get_type_hints, Union, Tuple, NamedTuple, get_args, get_origin +from typing import get_type_hints, Union, NamedTuple, get_args, get_origin import pytest import numpy as np @@ -12,7 +12,7 @@ import numpy.typing as npt class TypeTup(NamedTuple): typ: type - args: Tuple[type, ...] + args: tuple[type, ...] origin: None | type diff --git a/numpy/typing/tests/test_typing.py b/numpy/typing/tests/test_typing.py index 81863c780..d5338d2b1 100644 --- a/numpy/typing/tests/test_typing.py +++ b/numpy/typing/tests/test_typing.py @@ -1,10 +1,13 @@ +from __future__ import annotations + import importlib.util import itertools import os import re import shutil from collections import defaultdict -from typing import Optional, IO, Dict, List +from collections.abc import Iterator +from typing import IO, TYPE_CHECKING import pytest import numpy as np @@ -21,6 +24,10 @@ except ImportError: else: NO_MYPY = False +if TYPE_CHECKING: + # We need this as annotation, but it's located in a private namespace. + # As a compromise, do *not* import it during runtime + from _pytest.mark.structures import ParameterSet DATA_DIR = os.path.join(os.path.dirname(__file__), "data") PASS_DIR = os.path.join(DATA_DIR, "pass") @@ -32,7 +39,7 @@ CACHE_DIR = os.path.join(DATA_DIR, ".mypy_cache") #: A dictionary with file names as keys and lists of the mypy stdout as values. #: To-be populated by `run_mypy`. -OUTPUT_MYPY: Dict[str, List[str]] = {} +OUTPUT_MYPY: dict[str, list[str]] = {} def _key_func(key: str) -> str: @@ -62,7 +69,10 @@ def run_mypy() -> None: NUMPY_TYPING_TEST_CLEAR_CACHE=0 pytest numpy/typing/tests """ - if os.path.isdir(CACHE_DIR) and bool(os.environ.get("NUMPY_TYPING_TEST_CLEAR_CACHE", True)): + if ( + os.path.isdir(CACHE_DIR) + and bool(os.environ.get("NUMPY_TYPING_TEST_CLEAR_CACHE", True)) + ): shutil.rmtree(CACHE_DIR) for directory in (PASS_DIR, REVEAL_DIR, FAIL_DIR, MISC_DIR): @@ -85,7 +95,7 @@ def run_mypy() -> None: OUTPUT_MYPY.update((k, list(v)) for k, v in iterator if k) -def get_test_cases(directory): +def get_test_cases(directory: str) -> Iterator[ParameterSet]: for root, _, files in os.walk(directory): for fname in files: if os.path.splitext(fname)[-1] == ".py": @@ -103,7 +113,7 @@ def get_test_cases(directory): @pytest.mark.slow @pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed") @pytest.mark.parametrize("path", get_test_cases(PASS_DIR)) -def test_success(path): +def test_success(path) -> None: # Alias `OUTPUT_MYPY` so that it appears in the local namespace output_mypy = OUTPUT_MYPY if path in output_mypy: @@ -115,7 +125,7 @@ def test_success(path): @pytest.mark.slow @pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed") @pytest.mark.parametrize("path", get_test_cases(FAIL_DIR)) -def test_fail(path): +def test_fail(path: str) -> None: __tracebackhide__ = True with open(path) as fin: @@ -138,7 +148,10 @@ def test_fail(path): for i, line in enumerate(lines): lineno = i + 1 - if line.startswith('#') or (" E:" not in line and lineno not in errors): + if ( + line.startswith('#') + or (" E:" not in line and lineno not in errors) + ): continue target_line = lines[lineno - 1] @@ -162,14 +175,19 @@ Observed error: {!r} """ -def _test_fail(path: str, error: str, expected_error: Optional[str], lineno: int) -> None: +def _test_fail( + path: str, + error: str, + expected_error: None | str, + lineno: int, +) -> None: if expected_error is None: raise AssertionError(_FAIL_MSG1.format(lineno, error)) elif error not in expected_error: raise AssertionError(_FAIL_MSG2.format(lineno, expected_error, error)) -def _construct_format_dict(): +def _construct_format_dict() -> dict[str, str]: dct = {k.split(".")[-1]: v.replace("numpy", "numpy.typing") for k, v in _PRECISION_DICT.items()} @@ -193,12 +211,18 @@ def _construct_format_dict(): "float96": "numpy.floating[numpy.typing._96Bit]", "float128": "numpy.floating[numpy.typing._128Bit]", "float256": "numpy.floating[numpy.typing._256Bit]", - "complex64": "numpy.complexfloating[numpy.typing._32Bit, numpy.typing._32Bit]", - "complex128": "numpy.complexfloating[numpy.typing._64Bit, numpy.typing._64Bit]", - "complex160": "numpy.complexfloating[numpy.typing._80Bit, numpy.typing._80Bit]", - "complex192": "numpy.complexfloating[numpy.typing._96Bit, numpy.typing._96Bit]", - "complex256": "numpy.complexfloating[numpy.typing._128Bit, numpy.typing._128Bit]", - "complex512": "numpy.complexfloating[numpy.typing._256Bit, numpy.typing._256Bit]", + "complex64": ("numpy.complexfloating" + "[numpy.typing._32Bit, numpy.typing._32Bit]"), + "complex128": ("numpy.complexfloating" + "[numpy.typing._64Bit, numpy.typing._64Bit]"), + "complex160": ("numpy.complexfloating" + "[numpy.typing._80Bit, numpy.typing._80Bit]"), + "complex192": ("numpy.complexfloating" + "[numpy.typing._96Bit, numpy.typing._96Bit]"), + "complex256": ("numpy.complexfloating" + "[numpy.typing._128Bit, numpy.typing._128Bit]"), + "complex512": ("numpy.complexfloating" + "[numpy.typing._256Bit, numpy.typing._256Bit]"), "ubyte": f"numpy.unsignedinteger[{dct['_NBitByte']}]", "ushort": f"numpy.unsignedinteger[{dct['_NBitShort']}]", @@ -217,9 +241,14 @@ def _construct_format_dict(): "single": f"numpy.floating[{dct['_NBitSingle']}]", "double": f"numpy.floating[{dct['_NBitDouble']}]", "longdouble": f"numpy.floating[{dct['_NBitLongDouble']}]", - "csingle": f"numpy.complexfloating[{dct['_NBitSingle']}, {dct['_NBitSingle']}]", - "cdouble": f"numpy.complexfloating[{dct['_NBitDouble']}, {dct['_NBitDouble']}]", - "clongdouble": f"numpy.complexfloating[{dct['_NBitLongDouble']}, {dct['_NBitLongDouble']}]", + "csingle": ("numpy.complexfloating" + f"[{dct['_NBitSingle']}, {dct['_NBitSingle']}]"), + "cdouble": ("numpy.complexfloating" + f"[{dct['_NBitDouble']}, {dct['_NBitDouble']}]"), + "clongdouble": ( + "numpy.complexfloating" + f"[{dct['_NBitLongDouble']}, {dct['_NBitLongDouble']}]" + ), # numpy.typing "_NBitInt": dct['_NBitInt'], @@ -231,14 +260,16 @@ def _construct_format_dict(): #: A dictionary with all supported format keys (as keys) #: and matching values -FORMAT_DICT: Dict[str, str] = _construct_format_dict() +FORMAT_DICT: dict[str, str] = _construct_format_dict() -def _parse_reveals(file: IO[str]) -> List[str]: - """Extract and parse all ``" # E: "`` comments from the passed file-like object. +def _parse_reveals(file: IO[str]) -> list[str]: + """Extract and parse all ``" # E: "`` comments from the passed + file-like object. - All format keys will be substituted for their respective value from `FORMAT_DICT`, - *e.g.* ``"{float64}"`` becomes ``"numpy.floating[numpy.typing._64Bit]"``. + All format keys will be substituted for their respective value + from `FORMAT_DICT`, *e.g.* ``"{float64}"`` becomes + ``"numpy.floating[numpy.typing._64Bit]"``. """ string = file.read().replace("*", "") @@ -250,7 +281,8 @@ def _parse_reveals(file: IO[str]) -> List[str]: # there is the risk of accidentally grabbing dictionaries and sets key_set = set(re.findall(r"\{(.*?)\}", comments)) kwargs = { - k: FORMAT_DICT.get(k, f"<UNRECOGNIZED FORMAT KEY {k!r}>") for k in key_set + k: FORMAT_DICT.get(k, f"<UNRECOGNIZED FORMAT KEY {k!r}>") for + k in key_set } fmt_str = comments.format(**kwargs) @@ -260,7 +292,10 @@ def _parse_reveals(file: IO[str]) -> List[str]: @pytest.mark.slow @pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed") @pytest.mark.parametrize("path", get_test_cases(REVEAL_DIR)) -def test_reveal(path): +def test_reveal(path: str) -> None: + """Validate that mypy correctly infers the return-types of + the expressions in `path`. + """ __tracebackhide__ = True with open(path) as fin: @@ -290,18 +325,33 @@ Observed reveal: {!r} """ -def _test_reveal(path: str, reveal: str, expected_reveal: str, lineno: int) -> None: +def _test_reveal( + path: str, + reveal: str, + expected_reveal: str, + lineno: int, +) -> None: + """Error-reporting helper function for `test_reveal`.""" if reveal not in expected_reveal: - raise AssertionError(_REVEAL_MSG.format(lineno, expected_reveal, reveal)) + raise AssertionError( + _REVEAL_MSG.format(lineno, expected_reveal, reveal) + ) @pytest.mark.slow @pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed") @pytest.mark.parametrize("path", get_test_cases(PASS_DIR)) -def test_code_runs(path): +def test_code_runs(path: str) -> None: + """Validate that the code in `path` properly during runtime.""" path_without_extension, _ = os.path.splitext(path) dirname, filename = path.split(os.sep)[-2:] - spec = importlib.util.spec_from_file_location(f"{dirname}.{filename}", path) + + spec = importlib.util.spec_from_file_location( + f"{dirname}.{filename}", path + ) + assert spec is not None + assert spec.loader is not None + test_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(test_module) |