Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2a32d73

Browse files
lincolnqilevkivskyi
authored andcommitted
Use the get_attribute hook for dynamic classes (#6371)
Whenever we find `__getattr__` or `__getattribute__` on an instance, the getattribute hook is called to give the hook a chance to modify the return type of one of those functions. Closes #6259, #5910
1 parent f403165 commit 2a32d73

6 files changed

Lines changed: 85 additions & 12 deletions

File tree

docs/source/extending_mypy.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,11 @@ For example in this code:
184184
mypy will call ``get_method_signature_hook("ctypes.Array.__setitem__")``
185185
so that the plugin can mimic the ``ctypes`` auto-convert behavior.
186186

187-
**get_attribute_hook** can be used to give more precise type of an instance
188-
attribute. Note however, that this method is only called for variables that
189-
already exist in the class symbol table. If you want to add some generated
190-
variables/methods to the symbol table you can use one of the three hooks
191-
below.
187+
**get_attribute_hook** overrides instance member field lookups and property
188+
access (not assignments, and not method calls). This hook is only called for
189+
fields which already exist on the class. *Exception:* if ``__getattr__`` or
190+
``__getattribute__`` is a method on the class, the hook is called for all
191+
fields which do not refer to methods.
192192

193193
**get_class_decorator_hook()** can be used to update class definition for
194194
given class decorators. For example, you can add some attributes to the class

mypy/checkmember.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,15 @@ def analyze_member_var_access(name: str,
340340
typ = map_instance_to_supertype(itype, method.info)
341341
getattr_type = expand_type_by_instance(bound_method, typ)
342342
if isinstance(getattr_type, CallableType):
343-
return getattr_type.ret_type
343+
result = getattr_type.ret_type
344+
345+
# Call the attribute hook before returning.
346+
fullname = '{}.{}'.format(method.info.fullname(), name)
347+
hook = mx.chk.plugin.get_attribute_hook(fullname)
348+
if hook:
349+
result = hook(AttributeContext(mx.original_type, result,
350+
mx.context, mx.chk))
351+
return result
344352
else:
345353
setattr_meth = info.get_method('__setattr__')
346354
if setattr_meth and setattr_meth.info.fullname() != 'builtins.object':

mypy/plugin.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -440,8 +440,15 @@ def get_attribute_hook(self, fullname: str
440440
"""Adjust type of a class attribute.
441441
442442
This method is called with attribute full name using the class where the attribute was
443-
defined (or Var.info.fullname() for generated attributes). Currently, this hook is only
444-
called for names that exist in the class MRO, for example in:
443+
defined (or Var.info.fullname() for generated attributes).
444+
445+
For classes without __getattr__ or __getattribute__, this hook is only called for
446+
names of fields/properties (but not methods) that exist in the instance MRO.
447+
448+
For classes that implement __getattr__ or __getattribute__, this hook is called
449+
for all fields/properties, including nonexistent ones (but still not methods).
450+
451+
For example:
445452
446453
class Base:
447454
x: Any
@@ -454,7 +461,9 @@ class Derived(Base):
454461
var.x
455462
var.y
456463
457-
this method is only called with '__main__.Base.x'.
464+
get_attribute_hook is called with '__main__.Base.x' and '__main__.Base.y'.
465+
However, if we had not implemented __getattr__ on Base, you would only get
466+
the callback for 'var.x'; 'var.y' would produce an error without calling the hook.
458467
"""
459468
return None
460469

test-data/unit/check-custom-plugin.test

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,19 +143,49 @@ tmp/mypy.ini:2: error: Return value of "plugin" must be a subclass of "mypy.plug
143143
[case testAttributeTypeHookPlugin]
144144
# flags: --config-file tmp/mypy.ini
145145
from typing import Callable
146-
from m import Signal
146+
from m import Signal, DerivedSignal
147147
s: Signal[Callable[[int], None]] = Signal()
148148
s(1)
149149
s('') # E: Argument 1 has incompatible type "str"; expected "int"
150+
151+
ds: DerivedSignal[Callable[[int], None]] = DerivedSignal()
152+
ds('') # E: Argument 1 has incompatible type "str"; expected "int"
150153
[file m.py]
151154
from typing import TypeVar, Generic, Callable
152155
T = TypeVar('T', bound=Callable[..., None])
153156
class Signal(Generic[T]):
154157
__call__: Callable[..., None] # This type is replaced by the plugin
158+
159+
class DerivedSignal(Signal[T]): ...
155160
[file mypy.ini]
156161
[[mypy]
157162
plugins=<ROOT>/test-data/unit/plugins/attrhook.py
158163

164+
[case testAttributeHookPluginForDynamicClass]
165+
# flags: --config-file tmp/mypy.ini
166+
from m import Magic, DerivedMagic
167+
168+
magic = Magic()
169+
reveal_type(magic.magic_field) # E: Revealed type is 'builtins.str'
170+
reveal_type(magic.non_magic_method()) # E: Revealed type is 'builtins.int'
171+
reveal_type(magic.non_magic_field) # E: Revealed type is 'builtins.int'
172+
magic.nonexistent_field # E: Field does not exist
173+
reveal_type(magic.fallback_example) # E: Revealed type is 'Any'
174+
reveal_type(DerivedMagic().magic_field) # E: Revealed type is 'builtins.str'
175+
[file m.py]
176+
from typing import Any
177+
class Magic:
178+
# Triggers plugin infrastructure:
179+
def __getattr__(self, x: Any) -> Any: ...
180+
def non_magic_method(self) -> int: ...
181+
non_magic_field: int
182+
183+
class DerivedMagic(Magic): ...
184+
185+
[file mypy.ini]
186+
[[mypy]
187+
plugins=<ROOT>/test-data/unit/plugins/attrhook2.py
188+
159189
[case testTypeAnalyzeHookPlugin]
160190
# flags: --config-file tmp/mypy.ini
161191
from typing import Callable

test-data/unit/plugins/attrhook.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte
1212

1313

1414
def signal_call_callback(ctx: AttributeContext) -> Type:
15-
if isinstance(ctx.type, Instance) and ctx.type.type.fullname() == 'm.Signal':
15+
if isinstance(ctx.type, Instance):
1616
return ctx.type.args[0]
17-
return ctx.inferred_attr_type
17+
return ctx.default_attr_type
1818

1919

2020
def plugin(version):
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from typing import Optional, Callable
2+
3+
from mypy.plugin import Plugin, AttributeContext
4+
from mypy.types import Type, AnyType, TypeOfAny
5+
6+
7+
class AttrPlugin(Plugin):
8+
def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeContext], Type]]:
9+
if fullname == 'm.Magic.magic_field':
10+
return magic_field_callback
11+
if fullname == 'm.Magic.nonexistent_field':
12+
return nonexistent_field_callback
13+
return None
14+
15+
16+
def magic_field_callback(ctx: AttributeContext) -> Type:
17+
return ctx.api.named_generic_type("builtins.str", [])
18+
19+
20+
def nonexistent_field_callback(ctx: AttributeContext) -> Type:
21+
ctx.api.fail("Field does not exist", ctx.context)
22+
return AnyType(TypeOfAny.from_error)
23+
24+
25+
def plugin(version):
26+
return AttrPlugin

0 commit comments

Comments
 (0)