Skip to content

Commit 13d6738

Browse files
Add is_lvalue attribute to AttributeContext (#17881)
Refs #17878
1 parent b10d781 commit 13d6738

File tree

5 files changed

+25
-4
lines changed

5 files changed

+25
-4
lines changed

docs/source/extending_mypy.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ mypy will call ``get_method_signature_hook("ctypes.Array.__setitem__")``
179179
so that the plugin can mimic the :py:mod:`ctypes` auto-convert behavior.
180180

181181
**get_attribute_hook()** overrides instance member field lookups and property
182-
access (not assignments, and not method calls). This hook is only called for
182+
access (not method calls). This hook is only called for
183183
fields which already exist on the class. *Exception:* if :py:meth:`__getattr__ <object.__getattr__>` or
184184
:py:meth:`__getattribute__ <object.__getattribute__>` is a method on the class, the hook is called for all
185185
fields which do not refer to methods.

mypy/checkmember.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,11 @@ def analyze_member_var_access(
572572
if hook:
573573
result = hook(
574574
AttributeContext(
575-
get_proper_type(mx.original_type), result, mx.context, mx.chk
575+
get_proper_type(mx.original_type),
576+
result,
577+
mx.is_lvalue,
578+
mx.context,
579+
mx.chk,
576580
)
577581
)
578582
return result
@@ -829,7 +833,9 @@ def analyze_var(
829833
result = analyze_descriptor_access(result, mx)
830834
if hook:
831835
result = hook(
832-
AttributeContext(get_proper_type(mx.original_type), result, mx.context, mx.chk)
836+
AttributeContext(
837+
get_proper_type(mx.original_type), result, mx.is_lvalue, mx.context, mx.chk
838+
)
833839
)
834840
return result
835841

@@ -1148,7 +1154,9 @@ def apply_class_attr_hook(
11481154
) -> Type | None:
11491155
if hook:
11501156
result = hook(
1151-
AttributeContext(get_proper_type(mx.original_type), result, mx.context, mx.chk)
1157+
AttributeContext(
1158+
get_proper_type(mx.original_type), result, mx.is_lvalue, mx.context, mx.chk
1159+
)
11521160
)
11531161
return result
11541162

mypy/plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ class MethodContext(NamedTuple):
495495
class AttributeContext(NamedTuple):
496496
type: ProperType # Type of object with attribute
497497
default_attr_type: Type # Original attribute type
498+
is_lvalue: bool # Whether the attribute is the target of an assignment
498499
context: Context # Relevant location context (e.g. for error messages)
499500
api: CheckerPluginInterface
500501

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ reveal_type(magic.non_magic_method()) # N: Revealed type is "builtins.int"
232232
reveal_type(magic.non_magic_field) # N: Revealed type is "builtins.int"
233233
magic.nonexistent_field # E: Field does not exist
234234
reveal_type(magic.fallback_example) # N: Revealed type is "Any"
235+
reveal_type(magic.no_assignment_field) # N: Revealed type is "builtins.float"
236+
magic.no_assignment_field = "bad" # E: Cannot assign to field
235237

236238
derived = DerivedMagic()
237239
reveal_type(derived.magic_field) # N: Revealed type is "builtins.str"
@@ -250,6 +252,7 @@ class Magic:
250252
def __getattr__(self, x: Any) -> Any: ...
251253
def non_magic_method(self) -> int: ...
252254
non_magic_field: int
255+
no_assignment_field: float
253256

254257
class DerivedMagic(Magic): ...
255258
[file mypy.ini]

test-data/unit/plugins/attrhook2.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def get_attribute_hook(self, fullname: str) -> Callable[[AttributeContext], Type
1212
return magic_field_callback
1313
if fullname == "m.Magic.nonexistent_field":
1414
return nonexistent_field_callback
15+
if fullname == "m.Magic.no_assignment_field":
16+
return no_assignment_field_callback
1517
return None
1618

1719

@@ -24,5 +26,12 @@ def nonexistent_field_callback(ctx: AttributeContext) -> Type:
2426
return AnyType(TypeOfAny.from_error)
2527

2628

29+
def no_assignment_field_callback(ctx: AttributeContext) -> Type:
30+
if ctx.is_lvalue:
31+
ctx.api.fail(f"Cannot assign to field", ctx.context)
32+
return AnyType(TypeOfAny.from_error)
33+
return ctx.default_attr_type
34+
35+
2736
def plugin(version: str) -> type[AttrPlugin]:
2837
return AttrPlugin

0 commit comments

Comments
 (0)