diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7c87ec1..73d33e17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,3 +37,25 @@ repos: # Run the formatter - id: ruff-format types_or: [python, pyi] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "v1.10.0" + hooks: + - id: mypy + files: plum + additional_dependencies: + - pytest + exclude: | + (?x) + ^( + | plum/alias\.py + | plum/function\.py + | plum/method\.py + | plum/parametric\.py + | plum/promotion\.py + | plum/repr\.py + | plum/resolver\.py + | plum/signature\.py + | plum/type\.py + | plum/util\.py + )$ diff --git a/plum/__init__.py b/plum/__init__.py index 543a620a..2d9bc308 100644 --- a/plum/__init__.py +++ b/plum/__init__.py @@ -2,7 +2,9 @@ # versions from `typing`. To not break backward compatibility, we still export these # types. from functools import partial -from typing import Dict, List, Tuple, Union # noqa: F401 +from typing import Dict, List, Tuple, Type, TypeVar, Union # noqa: F401 + +from typing_extensions import TypeGuard # noqa: F401 from beartype import BeartypeConf as _BeartypeConf from beartype import BeartypeStrategy as _BeartypeStrategy @@ -31,8 +33,14 @@ # actually is not yet available, but we can already opt in to use it. _is_bearable = partial(_is_bearable, conf=_BeartypeConf(strategy=_BeartypeStrategy.On)) +T = TypeVar("T") +T2 = TypeVar("T2") + -def isinstance(instance, c): +# TODO: TypeGuard or +def isinstance( + instance: object, c: Union[Type[T], _TypeHint[T]] +) -> TypeGuard[Union[T, _TypeHint[T]]]: """Check if `instance` is of type or type hint `c`. This is a drop-in replace for the built-in :func:`ininstance` which supports type @@ -45,10 +53,13 @@ def isinstance(instance, c): Returns: bool: Whether `instance` is of type or type hint `c`. """ - return _is_bearable(instance, resolve_type_hint(c)) + pred: bool = _is_bearable(instance, resolve_type_hint(c)) + return pred -def issubclass(c1, c2): +def issubclass( + c1: Union[Type[T], _TypeHint[T]], c2: Union[Type[T2], _TypeHint[T2]] +) -> TypeGuard[Union[T2, _TypeHint[T2]]]: """Check if `c1` is a subclass or sub-type hint of `c2`. This is a drop-in replace for the built-in :func:`issubclass` which supports type @@ -61,4 +72,5 @@ def issubclass(c1, c2): Returns: bool: Whether `c1` is a subtype or sub-type hint of `c2`. """ - return _TypeHint(resolve_type_hint(c1)) <= _TypeHint(resolve_type_hint(c2)) + pred: bool = _TypeHint(resolve_type_hint(c1)) <= _TypeHint(resolve_type_hint(c2)) + return pred diff --git a/plum/autoreload.py b/plum/autoreload.py index 21d20e7d..f17f7278 100644 --- a/plum/autoreload.py +++ b/plum/autoreload.py @@ -6,7 +6,7 @@ __all__ = ["activate_autoreload", "deactivate_autoreload"] -def _update_instances(old, new): +def _update_instances(old: type, new: type) -> None: """First call the original implementation of Autoreload's :meth:`update_instances`, and then use :obj:`.type._type_mapping` to map type `old` to the type `new`. @@ -14,7 +14,7 @@ def _update_instances(old, new): old (type): Old type. new (type): New type. """ - _update_instances_original(old, new) + _update_instances_original(old, new) # type: ignore[misc] type_mapping[old] = new @@ -33,10 +33,10 @@ def _update_instances(old, new): """function: Original implementation of :func:`update_instances`.""" -def activate_autoreload(): +def activate_autoreload() -> None: """Pirate Autoreload's `update_instance` function to have Plum work with Autoreload.""" - from IPython.extensions import autoreload # type: ignore + from IPython.extensions import autoreload # First, cache the original method so we can deactivate ourselves. global _update_instances_original @@ -47,7 +47,7 @@ def activate_autoreload(): autoreload.update_instances = _update_instances -def deactivate_autoreload(): +def deactivate_autoreload() -> None: """Disable Plum's autoreload hack. This undoes what :func:`.autoreload.activate_autoreload` did.""" global _update_instances_original @@ -66,7 +66,7 @@ def deactivate_autoreload(): try: # Try to load iPython and get the iPython session, but don't crash if # this does not work (for example IPython not installed, or python shell) - from IPython import get_ipython # type: ignore + from IPython import get_ipython ip = get_ipython() ext_loaded = "IPython.extensions.storemagic" in ip.extension_manager.loaded diff --git a/plum/dispatcher.py b/plum/dispatcher.py index f81d650d..c69c5c2e 100644 --- a/plum/dispatcher.py +++ b/plum/dispatcher.py @@ -32,10 +32,10 @@ class Dispatcher: classes: Dict[str, Dict[str, Function]] = field(default_factory=dict) @overload - def __call__(self, method: T, precedence: int = ...) -> T: ... + def __call__(self, method: None, precedence: int) -> Callable[[T], T]: ... @overload - def __call__(self, method: None, precedence: int) -> Callable[[T], T]: ... + def __call__(self, method: T, precedence: int = ...) -> T: ... def __call__( self, method: Optional[T] = None, precedence: int = 0 @@ -130,13 +130,13 @@ def _add_method( f.register(method, signature, precedence) return f - def clear_cache(self): + def clear_cache(self) -> None: """Clear cache.""" for f in self.functions.values(): f.clear_cache() -def clear_all_cache(): +def clear_all_cache() -> None: """Clear all cache, including the cache of subclass checks. This should be called if types are modified.""" for f in Function._instances: diff --git a/pyproject.toml b/pyproject.toml index 5c04c32a..d7f8b140 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,15 @@ branch = true command_line = "-m pytest --verbose test" source = ["plum"] + +[tool.mypy] + disable_error_code = ["no-redef"] + enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] + files = ["plum"] + strict = true + follow_imports = "skip" + + [tool.pytest.ini_options] testpaths = ["tests/", "plum", "docs"] addopts = [ diff --git a/tests/typechecked/test_overload.py b/tests/typechecked/test_overload.py index 2bbb5135..074f846d 100644 --- a/tests/typechecked/test_overload.py +++ b/tests/typechecked/test_overload.py @@ -23,7 +23,7 @@ def f(x): # E: pyright(overloaded implementation is not consistent) def test_overload() -> None: assert f(1) == 1 assert f("1") == "1" - with pytest.raises(NotFoundLookupError): + with pytest.raises(NotFoundLookupError, match="no overload variant"): # E: pyright(argument of type "float" cannot be assigned to parameter "x") # E: pyright(no overloads for "f" match the provided arguments) # E: mypy(no overload variant of "f" matches argument type "float")