Fix mypy not picking up on missing attributes

Behaviour changed in recent releases:
https://github.com/python/mypy/issues/13319
This commit is contained in:
Damien Elmes 2022-11-04 14:56:36 +10:00
parent 3973f27ee4
commit 2504ad0b99
7 changed files with 70 additions and 62 deletions

View file

@ -7,7 +7,7 @@ import functools
import os
import pathlib
import traceback
from typing import Any, Callable, Union, no_type_check
from typing import TYPE_CHECKING, Any, Callable, Union
from anki._vendor import stringcase
@ -66,21 +66,20 @@ class DeprecatedNamesMixin:
# deprecated name -> [new internal name, new name shown to user]
_deprecated_attributes: dict[str, tuple[str, str | None]] = {}
# the @no_type_check lines are required to prevent mypy allowing arbitrary
# attributes on the consuming class
# TYPE_CHECKING check is required for https://github.com/python/mypy/issues/13319
if not TYPE_CHECKING:
@no_type_check
def __getattr__(self, name: str) -> Any:
try:
remapped, replacement = _get_remapped_and_replacement(self, name)
out = getattr(self, remapped)
except AttributeError:
raise AttributeError(
f"'{self.__class__.__name__}' object has no attribute '{name}'"
) from None
def __getattr__(self, name: str) -> Any:
try:
remapped, replacement = _get_remapped_and_replacement(self, name)
out = getattr(self, remapped)
except AttributeError:
raise AttributeError(
f"'{self.__class__.__name__}' object has no attribute '{name}'"
) from None
_print_replacement_warning(name, replacement)
return out
_print_replacement_warning(name, replacement)
return out
@classmethod
def register_deprecated_aliases(cls, **kwargs: DeprecatedAliasTarget) -> None:
@ -123,9 +122,9 @@ class DeprecatedNamesMixinForModule:
_deprecated_names.register_deprecated_aliases(...
_deprecated_names.register_deprecated_attributes(...
@no_type_check
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)
if not TYPE_CHECKING:
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)
```
See DeprecatedNamesMixin for more documentation.
"""
@ -135,19 +134,20 @@ class DeprecatedNamesMixinForModule:
self._deprecated_aliases: dict[str, str] = {}
self._deprecated_attributes: dict[str, tuple[str, str | None]] = {}
@no_type_check
def __getattr__(self, name: str) -> Any:
try:
remapped, replacement = _get_remapped_and_replacement(self, name)
out = self.module_globals[remapped]
except (AttributeError, KeyError):
raise AttributeError(
f"Module '{self.module_globals['__name__']}' has no attribute '{name}'"
) from None
if not TYPE_CHECKING:
# skip an additional frame as we are called from the module `__getattr__`
_print_replacement_warning(name, replacement, frame=2)
return out
def __getattr__(self, name: str) -> Any:
try:
remapped, replacement = _get_remapped_and_replacement(self, name)
out = self.module_globals[remapped]
except (AttributeError, KeyError):
raise AttributeError(
f"Module '{self.module_globals['__name__']}' has no attribute '{name}'"
) from None
# skip an additional frame as we are called from the module `__getattr__`
_print_replacement_warning(name, replacement, frame=2)
return out
def register_deprecated_aliases(self, **kwargs: DeprecatedAliasTarget) -> None:
self._deprecated_aliases = {k: _target_to_string(v) for k, v in kwargs.items()}

View file

@ -4,7 +4,7 @@
from __future__ import annotations
import sys
from typing import Any, NewType, no_type_check
from typing import TYPE_CHECKING, Any, NewType
from anki._legacy import DeprecatedNamesMixinForModule
@ -132,6 +132,7 @@ def new_card_scheduling_labels(
_deprecated_names = DeprecatedNamesMixinForModule(globals())
@no_type_check
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)
if not TYPE_CHECKING:
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)

View file

@ -4,7 +4,7 @@
from __future__ import annotations
import copy
from typing import TYPE_CHECKING, Any, Iterable, NewType, Sequence, no_type_check
from typing import TYPE_CHECKING, Any, Iterable, NewType, Sequence
if TYPE_CHECKING:
import anki
@ -590,15 +590,18 @@ DeckManager.register_deprecated_aliases(
)
@no_type_check
def __getattr__(name):
if name == "defaultDeck":
print_deprecation_warning(
"defaultDeck is deprecated; call decks.id() without it"
)
return 0
elif name == "defaultDynamicDeck":
print_deprecation_warning("defaultDynamicDeck is replaced with new_filtered()")
return 1
else:
raise AttributeError(f"module {__name__} has no attribute {name}")
if not TYPE_CHECKING:
def __getattr__(name):
if name == "defaultDeck":
print_deprecation_warning(
"defaultDeck is deprecated; call decks.id() without it"
)
return 0
elif name == "defaultDynamicDeck":
print_deprecation_warning(
"defaultDynamicDeck is replaced with new_filtered()"
)
return 1
else:
raise AttributeError(f"module {__name__} has no attribute {name}")

View file

@ -6,7 +6,7 @@ from __future__ import annotations
import locale
import re
import weakref
from typing import Any, no_type_check
from typing import TYPE_CHECKING, Any
import anki
import anki._backend
@ -224,6 +224,7 @@ def with_collapsed_whitespace(string: str) -> str:
_deprecated_names = DeprecatedNamesMixinForModule(globals())
@no_type_check
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)
if not TYPE_CHECKING:
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)

View file

@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Any, Callable, no_type_check
from typing import TYPE_CHECKING, Any, Callable
import anki.collection
import anki.models
@ -115,6 +115,7 @@ _deprecated_names.register_deprecated_attributes(
)
@no_type_check
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)
if not TYPE_CHECKING:
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)

View file

@ -16,7 +16,7 @@ import tempfile
import time
from contextlib import contextmanager
from hashlib import sha1
from typing import Any, Callable, Iterable, Iterator, no_type_check
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator
from anki._legacy import DeprecatedNamesMixinForModule
from anki.dbproxy import DBProxy
@ -324,6 +324,7 @@ _deprecated_names.register_deprecated_aliases(
_deprecated_names.register_deprecated_attributes(json=((_json, "_json"), None))
@no_type_check
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)
if not TYPE_CHECKING:
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)

View file

@ -9,7 +9,7 @@ import subprocess
import sys
from functools import partial, wraps
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence, Union, no_type_check
from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence, Union
from send2trash import send2trash
@ -1243,6 +1243,7 @@ class KeyboardModifiersPressed:
_deprecated_names = DeprecatedNamesMixinForModule(globals())
@no_type_check
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)
if not TYPE_CHECKING:
def __getattr__(name: str) -> Any:
return _deprecated_names.__getattr__(name)