Skip to content

Commit e35d7de

Browse files
FuegoFrodenbeigh2000
authored andcommitted
Add a class attribute hook to the plugin system
This adds a hook to modify attributes on *classes* (as opposed to the existing attribute hook, which is for attributes on *instances*). This also adds a test to demonstrate the new behavior. The modifications and added tests were modeled off of python@3acbf3f. This is intended to solve python#9645
1 parent f5fc579 commit e35d7de

File tree

4 files changed

+65
-3
lines changed

4 files changed

+65
-3
lines changed

mypy/checkmember.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,13 @@ def analyze_class_attribute_access(itype: Instance,
778778
if not mx.is_lvalue:
779779
result = analyze_descriptor_access(mx.original_type, result, mx.builtin_type,
780780
mx.msg, mx.context, chk=mx.chk)
781+
782+
# Call the class attribute hook before returning.
783+
fullname = '{}.{}'.format(info.fullname, name)
784+
hook = mx.chk.plugin.get_class_attribute_hook(fullname)
785+
if hook:
786+
result = hook(AttributeContext(get_proper_type(mx.original_type),
787+
result, mx.context, mx.chk))
781788
return result
782789
elif isinstance(node.node, Var):
783790
mx.not_ready_callback(name, mx.context)

mypy/plugin.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -616,10 +616,10 @@ def get_method_hook(self, fullname: str
616616

617617
def get_attribute_hook(self, fullname: str
618618
) -> Optional[Callable[[AttributeContext], Type]]:
619-
"""Adjust type of a class attribute.
619+
"""Adjust type of an instance attribute.
620620
621-
This method is called with attribute full name using the class where the attribute was
622-
defined (or Var.info.fullname for generated attributes).
621+
This method is called with attribute full name using the class of the instance where
622+
the attribute was defined (or Var.info.fullname for generated attributes).
623623
624624
For classes without __getattr__ or __getattribute__, this hook is only called for
625625
names of fields/properties (but not methods) that exist in the instance MRO.
@@ -646,6 +646,25 @@ class Derived(Base):
646646
"""
647647
return None
648648

649+
def get_class_attribute_hook(self, fullname: str
650+
) -> Optional[Callable[[AttributeContext], Type]]:
651+
"""
652+
Adjust type of a class attribute.
653+
654+
This method is called with attribute full name using the class where the attribute was
655+
defined (or Var.info.fullname for generated attributes).
656+
657+
For example:
658+
659+
class Cls:
660+
x: Any
661+
662+
Cls.x
663+
664+
get_class_attribute_hook is called with '__main__.Cls.x'.
665+
"""
666+
return None
667+
649668
def get_class_decorator_hook(self, fullname: str
650669
) -> Optional[Callable[[ClassDefContext], None]]:
651670
"""Update class definition for given class decorators.
@@ -767,6 +786,10 @@ def get_attribute_hook(self, fullname: str
767786
) -> Optional[Callable[[AttributeContext], Type]]:
768787
return self._find_hook(lambda plugin: plugin.get_attribute_hook(fullname))
769788

789+
def get_class_attribute_hook(self, fullname: str
790+
) -> Optional[Callable[[AttributeContext], Type]]:
791+
return self._find_hook(lambda plugin: plugin.get_class_attribute_hook(fullname))
792+
770793
def get_class_decorator_hook(self, fullname: str
771794
) -> Optional[Callable[[ClassDefContext], None]]:
772795
return self._find_hook(lambda plugin: plugin.get_class_decorator_hook(fullname))

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,3 +783,15 @@ reveal_type(dynamic_signature(1)) # N: Revealed type is "builtins.int"
783783
[file mypy.ini]
784784
\[mypy]
785785
plugins=<ROOT>/test-data/unit/plugins/function_sig_hook.py
786+
787+
[case testClassAttrPluginFile]
788+
# flags: --config-file tmp/mypy.ini
789+
790+
class Cls:
791+
attr = 'test'
792+
793+
reveal_type(Cls.attr) # N: Revealed type is 'builtins.int'
794+
reveal_type(Cls().attr) # N: Revealed type is 'builtins.str'
795+
[file mypy.ini]
796+
\[mypy]
797+
plugins=<ROOT>/test-data/unit/plugins/class_attr_hook.py
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from typing import Callable, Optional, Type as TypingType
2+
3+
from mypy.plugin import AttributeContext, Plugin
4+
from mypy.types import Type as MypyType
5+
6+
7+
class ClassAttrPlugin(Plugin):
8+
def get_class_attribute_hook(self, fullname: str
9+
) -> Optional[Callable[[AttributeContext], MypyType]]:
10+
if fullname == '__main__.Cls.attr':
11+
return my_hook
12+
return None
13+
14+
15+
def my_hook(ctx: AttributeContext) -> MypyType:
16+
return ctx.api.named_generic_type('builtins.int', [])
17+
18+
19+
def plugin(_version: str) -> TypingType[Plugin]:
20+
return ClassAttrPlugin

0 commit comments

Comments
 (0)