Skip to content

Commit 0d562aa

Browse files
[ty] Check inherited attribute conflicts (astral-sh#28559)
## Summary We now report incompatible attributes inherited from different bases, even when the subclass does not override them: ```python class Integer: value: int class String: value: str class Combined(Integer, String): ... # invalid-attribute-override ``` We check the member selected by the MRO against the base declarations, including properties and annotations on `self`. Conflicts already present in a parent are not reported again. Part of astral-sh/ty#2158.
1 parent 48dcdd0 commit 0d562aa

10 files changed

Lines changed: 817 additions & 210 deletions

File tree

‎crates/ty_python_semantic/resources/mdtest/abstract.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,7 @@ Concrete() # no diagnostic
414414

415415
## Slots and abstract properties in the MRO
416416

417-
A slot inherited from a mixin overrides an abstract property only if the mixin comes before the
418-
property's defining class in the MRO.
417+
A slot inherited from a mixin overrides an abstract property when the mixin comes first in the MRO.
419418

420419
```py
421420
from abc import ABC, abstractmethod
@@ -429,9 +428,16 @@ class SlotMixin:
429428
__slots__ = ("value",)
430429

431430
class Concrete(SlotMixin, Abstract): ...
432-
class StillAbstract(Abstract, SlotMixin): ...
433431

434432
Concrete() # no diagnostic
433+
```
434+
435+
With the bases reversed, the class remains abstract. The getter-only property also removes writes
436+
permitted by the mixin's slot, so the class definition receives an override error.
437+
438+
```py
439+
class StillAbstract(Abstract, SlotMixin): ... # error: [invalid-property-type-override]
440+
435441
StillAbstract() # error: [call-non-callable]
436442
```
437443

‎crates/ty_python_semantic/resources/mdtest/attributes.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,11 +2115,9 @@ The attribute stored by the metaclass also shadows a descriptor inherited from a
21152115
that non-data class attribute and an instance assignment remain possible:
21162116

21172117
```py
2118-
from typing import Literal
2119-
21202118
class InheritedGeneratedProperty:
21212119
@property
2122-
def generated(self) -> Literal["property"]:
2120+
def generated(self) -> object:
21232121
return "property"
21242122

21252123
class StringStoringMeta(type):
@@ -2144,6 +2142,8 @@ If the metaclass instead stores a data descriptor on the new class, the descript
21442142
over an instance assignment:
21452143

21462144
```py
2145+
from typing import Literal
2146+
21472147
class GeneratedDescriptor:
21482148
def __get__(self, instance: object, owner: type | None = None) -> Literal["descriptor"]:
21492149
return "descriptor"

‎crates/ty_python_semantic/resources/mdtest/cycle/implicit_instance_attributes.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,3 +662,39 @@ class ChildContext(Context):
662662
# TODO: Report an invalid-assignment error for assigning `ChildContext` to `int`.
663663
cls.current = ChildContext()
664664
```
665+
666+
## Lazy initialization in a class property
667+
668+
The metaclass initializes `_name` to `None`. Reading the class property can therefore enter the
669+
branch that fills the cache, including on a class with multiple bases.
670+
671+
```toml
672+
[rules]
673+
redundant-condition-strict = "error"
674+
```
675+
676+
```py
677+
def module_name(module: str) -> str:
678+
return module
679+
680+
class ClassProperty:
681+
def __init__(self, getter):
682+
self.getter = getter
683+
684+
def __get__(self, instance, owner):
685+
return self.getter(owner)
686+
687+
class Meta(type):
688+
def __new__(cls, name, bases, namespace):
689+
namespace["_name"] = None
690+
return super(Meta, cls).__new__(cls, name, bases, namespace)
691+
692+
class Left: ...
693+
class Right: ...
694+
695+
class Package(Left, Right, metaclass=Meta):
696+
@ClassProperty
697+
def name(cls):
698+
if cls._name is None: # no diagnostic
699+
cls._name = module_name(cls.__module__)
700+
```

‎crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4185,5 +4185,39 @@ def narrow_intersection_bounded(value: Intersection[Parent[BoundedS], Other[Boun
41854185
reveal_type(value) # revealed: ChildBoth[BoundedS@narrow_intersection_bounded]
41864186
```
41874187

4188+
## Inheriting different specializations of a property
4189+
4190+
`String` can return `str` for the property inherited from `Base[Any]`. Combining it with `Integer`
4191+
also requires the property to return `int`, so the new subclass is invalid.
4192+
4193+
```py
4194+
from typing import Any, Generic, TypeVar
4195+
4196+
T = TypeVar("T")
4197+
4198+
class Base(Generic[T]):
4199+
@property
4200+
def value(self) -> T:
4201+
raise NotImplementedError
4202+
4203+
class String(Base[Any]): # no diagnostic
4204+
@property
4205+
def value(self) -> str:
4206+
return ""
4207+
4208+
class Integer(Base[int]): ...
4209+
class Conflict(String, Integer): ... # error: [invalid-property-type-override]
4210+
```
4211+
4212+
An explicit override is also checked against `Base[int]`, and the error is reported only at the
4213+
override.
4214+
4215+
```py
4216+
class Explicit(String, Integer):
4217+
@property
4218+
def value(self) -> str: # error: [invalid-property-type-override]
4219+
return ""
4220+
```
4221+
41884222
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
41894223
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification

‎crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,10 +1731,10 @@ from typing import TypeVar, Generic
17311731
T = TypeVar("T")
17321732

17331733
class P(Generic[T]):
1734-
x: T
1734+
p: T
17351735

17361736
class Q(Generic[T]):
1737-
x: T
1737+
q: T
17381738

17391739
def extract_t(x: P[T] | Q[T]) -> T:
17401740
raise NotImplementedError

‎crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3226,5 +3226,37 @@ def narrow_intersection_bounded[S: str](value: Intersection[Parent[S], Other[S]]
32263226
reveal_type(value) # revealed: ChildBoth[S@narrow_intersection_bounded]
32273227
```
32283228

3229+
## Inheriting different specializations of a property
3230+
3231+
`String` can return `str` for the property inherited from `Base[Any]`. Combining it with `Integer`
3232+
also requires the property to return `int`, so the new subclass is invalid.
3233+
3234+
```py
3235+
from typing import Any
3236+
3237+
class Base[T]:
3238+
@property
3239+
def value(self) -> T:
3240+
raise NotImplementedError
3241+
3242+
class String(Base[Any]): # no diagnostic
3243+
@property
3244+
def value(self) -> str:
3245+
return ""
3246+
3247+
class Integer(Base[int]): ...
3248+
class Conflict(String, Integer): ... # error: [invalid-property-type-override]
3249+
```
3250+
3251+
An explicit override is also checked against `Base[int]`, and the error is reported only at the
3252+
override.
3253+
3254+
```py
3255+
class Explicit(String, Integer):
3256+
@property
3257+
def value(self) -> str: # error: [invalid-property-type-override]
3258+
return ""
3259+
```
3260+
32293261
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
32303262
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification

‎crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,10 +2893,10 @@ the actual argument even for non-final classes.
28932893

28942894
```py
28952895
class P[T]:
2896-
x: T # invariant
2896+
p: T # invariant
28972897

28982898
class Q[T]:
2899-
x: T # invariant
2899+
q: T # invariant
29002900

29012901
def extract_t[T](x: P[T] | Q[T]) -> T:
29022902
raise NotImplementedError

0 commit comments

Comments
 (0)