Skip to content

Commit a6572a5

Browse files
authored
[red-knot] Attribute access on intersection types (#16665)
## Summary Implements attribute access on intersection types, which didn't previously work. For example: ```py from typing import Any class P: ... class Q: ... class A: x: P = P() class B: x: Any = Q() def _(obj: A): if isinstance(obj, B): reveal_type(obj.x) # revealed: P & Any ``` Refers to [this comment]. [this comment]: #16416 (comment) ## Test Plan New Markdown tests
1 parent b250304 commit a6572a5

File tree

2 files changed

+136
-11
lines changed

2 files changed

+136
-11
lines changed

crates/red_knot_python_semantic/resources/mdtest/attributes.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,132 @@ reveal_type(A.__mro__)
10421042
reveal_type(A.X) # revealed: Unknown | Literal[42]
10431043
```
10441044

1045+
## Intersections of attributes
1046+
1047+
### Attribute only available on one element
1048+
1049+
```py
1050+
from knot_extensions import Intersection
1051+
1052+
class A:
1053+
x: int = 1
1054+
1055+
class B: ...
1056+
1057+
def _(a_and_b: Intersection[A, B]):
1058+
reveal_type(a_and_b.x) # revealed: int
1059+
1060+
# Same for class objects
1061+
def _(a_and_b: Intersection[type[A], type[B]]):
1062+
reveal_type(a_and_b.x) # revealed: int
1063+
```
1064+
1065+
### Attribute available on both elements
1066+
1067+
```py
1068+
from knot_extensions import Intersection
1069+
1070+
class P: ...
1071+
class Q: ...
1072+
1073+
class A:
1074+
x: P = P()
1075+
1076+
class B:
1077+
x: Q = Q()
1078+
1079+
def _(a_and_b: Intersection[A, B]):
1080+
reveal_type(a_and_b.x) # revealed: P & Q
1081+
1082+
# Same for class objects
1083+
def _(a_and_b: Intersection[type[A], type[B]]):
1084+
reveal_type(a_and_b.x) # revealed: P & Q
1085+
```
1086+
1087+
### Possible unboundness
1088+
1089+
```py
1090+
from knot_extensions import Intersection
1091+
1092+
class P: ...
1093+
class Q: ...
1094+
1095+
def _(flag: bool):
1096+
class A1:
1097+
if flag:
1098+
x: P = P()
1099+
1100+
class B1: ...
1101+
1102+
def inner1(a_and_b: Intersection[A1, B1]):
1103+
# error: [possibly-unbound-attribute]
1104+
reveal_type(a_and_b.x) # revealed: P
1105+
# Same for class objects
1106+
def inner1_class(a_and_b: Intersection[type[A1], type[B1]]):
1107+
# error: [possibly-unbound-attribute]
1108+
reveal_type(a_and_b.x) # revealed: P
1109+
1110+
class A2:
1111+
if flag:
1112+
x: P = P()
1113+
1114+
class B1:
1115+
x: Q = Q()
1116+
1117+
def inner2(a_and_b: Intersection[A2, B1]):
1118+
reveal_type(a_and_b.x) # revealed: P & Q
1119+
# Same for class objects
1120+
def inner2_class(a_and_b: Intersection[type[A2], type[B1]]):
1121+
reveal_type(a_and_b.x) # revealed: P & Q
1122+
1123+
class A3:
1124+
if flag:
1125+
x: P = P()
1126+
1127+
class B3:
1128+
if flag:
1129+
x: Q = Q()
1130+
1131+
def inner3(a_and_b: Intersection[A3, B3]):
1132+
# error: [possibly-unbound-attribute]
1133+
reveal_type(a_and_b.x) # revealed: P & Q
1134+
# Same for class objects
1135+
def inner3_class(a_and_b: Intersection[type[A3], type[B3]]):
1136+
# error: [possibly-unbound-attribute]
1137+
reveal_type(a_and_b.x) # revealed: P & Q
1138+
1139+
class A4: ...
1140+
class B4: ...
1141+
1142+
def inner4(a_and_b: Intersection[A4, B4]):
1143+
# error: [unresolved-attribute]
1144+
reveal_type(a_and_b.x) # revealed: Unknown
1145+
# Same for class objects
1146+
def inner4_class(a_and_b: Intersection[type[A4], type[B4]]):
1147+
# error: [unresolved-attribute]
1148+
reveal_type(a_and_b.x) # revealed: Unknown
1149+
```
1150+
1151+
### Intersection of implicit instance attributes
1152+
1153+
```py
1154+
from knot_extensions import Intersection
1155+
1156+
class P: ...
1157+
class Q: ...
1158+
1159+
class A:
1160+
def __init__(self):
1161+
self.x: P = P()
1162+
1163+
class B:
1164+
def __init__(self):
1165+
self.x: Q = Q()
1166+
1167+
def _(a_and_b: Intersection[A, B]):
1168+
reveal_type(a_and_b.x) # revealed: P & Q
1169+
```
1170+
10451171
## Attribute access on `Any`
10461172

10471173
The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows

crates/red_knot_python_semantic/src/types.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5065,33 +5065,32 @@ impl<'db> IntersectionType<'db> {
50655065

50665066
let mut builder = IntersectionBuilder::new(db);
50675067

5068-
let mut any_unbound = false;
5069-
let mut any_possibly_unbound = false;
5068+
let mut all_unbound = true;
5069+
let mut any_definitely_bound = false;
50705070
for ty in self.positive(db) {
50715071
let ty_member = transform_fn(ty);
50725072
match ty_member {
5073-
Symbol::Unbound => {
5074-
any_unbound = true;
5075-
}
5073+
Symbol::Unbound => {}
50765074
Symbol::Type(ty_member, member_boundness) => {
5077-
if member_boundness == Boundness::PossiblyUnbound {
5078-
any_possibly_unbound = true;
5075+
all_unbound = false;
5076+
if member_boundness == Boundness::Bound {
5077+
any_definitely_bound = true;
50795078
}
50805079

50815080
builder = builder.add_positive(ty_member);
50825081
}
50835082
}
50845083
}
50855084

5086-
if any_unbound {
5085+
if all_unbound {
50875086
Symbol::Unbound
50885087
} else {
50895088
Symbol::Type(
50905089
builder.build(),
5091-
if any_possibly_unbound {
5092-
Boundness::PossiblyUnbound
5093-
} else {
5090+
if any_definitely_bound {
50945091
Boundness::Bound
5092+
} else {
5093+
Boundness::PossiblyUnbound
50955094
},
50965095
)
50975096
}

0 commit comments

Comments
 (0)