Skip to content

Commit 878d892

Browse files
Better names of and more compatibility between ad hoc intersections of instances (#18506)
While working on #18433, we encountered [this bug.](#18433 (comment)). @ilevkivskyi identified [the underlying problem](#18433 (comment)), and we decided to try to reuse previously created ad hoc intersections of instances instead of always creating new ones. While working on this PR, I realised that reusing intersections requires more complete names. Currently, module and type variable specifications are not included, which could result in mistakes when using these names as identifiers. So, I switched to more complete names. Now, for example, `<subclass of "A" and "A">` becomes `<subclass of "mod1.A[builtins.int]" and "mod2.A">`. Hence, I had to adjust many existing test cases where `reveal_type` is used. `testReuseIntersectionForRepeatedIsinstanceCalls` confirms that the mentioned bug is fixed. `testIsInstanceAdHocIntersectionIncrementalNestedClass` and `testIsInstanceAdHocIntersectionIncrementalUnions` are in a separate commit. I think they are not really necessary, so we might prefer to remove them. I added them originally because I had to adjust `lookup_fully_qualified` a little. The change is very simple, but I could not create a test case where it is not sufficient. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 58de753 commit 878d892

11 files changed

+159
-90
lines changed

mypy/checker.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5501,13 +5501,9 @@ def intersect_instances(
55015501
theoretical subclass of the instances the user may be trying to use
55025502
the generated intersection can serve as a placeholder.
55035503
5504-
This function will create a fresh subclass every time you call it,
5505-
even if you pass in the exact same arguments. So this means calling
5506-
`self.intersect_intersection([inst_1, inst_2], ctx)` twice will result
5507-
in instances of two distinct subclasses of inst_1 and inst_2.
5508-
5509-
This is by design: we want each ad-hoc intersection to be unique since
5510-
they're supposed represent some other unknown subclass.
5504+
This function will create a fresh subclass the first time you call it.
5505+
So this means calling `self.intersect_intersection([inst_1, inst_2], ctx)`
5506+
twice will return the same subclass of inst_1 and inst_2.
55115507
55125508
Returns None if creating the subclass is impossible (e.g. due to
55135509
MRO errors or incompatible signatures). If we do successfully create
@@ -5540,20 +5536,19 @@ def _get_base_classes(instances_: tuple[Instance, Instance]) -> list[Instance]:
55405536
return base_classes_
55415537

55425538
def _make_fake_typeinfo_and_full_name(
5543-
base_classes_: list[Instance], curr_module_: MypyFile
5539+
base_classes_: list[Instance], curr_module_: MypyFile, options: Options
55445540
) -> tuple[TypeInfo, str]:
5545-
names_list = pretty_seq([x.type.name for x in base_classes_], "and")
5546-
short_name = f"<subclass of {names_list}>"
5547-
full_name_ = gen_unique_name(short_name, curr_module_.names)
5548-
cdef, info_ = self.make_fake_typeinfo(
5549-
curr_module_.fullname, full_name_, short_name, base_classes_
5550-
)
5551-
return info_, full_name_
5541+
names = [format_type_bare(x, options=options, verbosity=2) for x in base_classes_]
5542+
name = f"<subclass of {pretty_seq(names, 'and')}>"
5543+
if (symbol := curr_module_.names.get(name)) is not None:
5544+
assert isinstance(symbol.node, TypeInfo)
5545+
return symbol.node, name
5546+
cdef, info_ = self.make_fake_typeinfo(curr_module_.fullname, name, name, base_classes_)
5547+
return info_, name
55525548

55535549
base_classes = _get_base_classes(instances)
5554-
# We use the pretty_names_list for error messages but can't
5555-
# use it for the real name that goes into the symbol table
5556-
# because it can have dots in it.
5550+
# We use the pretty_names_list for error messages but for the real name that goes
5551+
# into the symbol table because it is not specific enough.
55575552
pretty_names_list = pretty_seq(
55585553
format_type_distinctly(*base_classes, options=self.options, bare=True), "and"
55595554
)
@@ -5567,13 +5562,17 @@ def _make_fake_typeinfo_and_full_name(
55675562
return None
55685563

55695564
try:
5570-
info, full_name = _make_fake_typeinfo_and_full_name(base_classes, curr_module)
5565+
info, full_name = _make_fake_typeinfo_and_full_name(
5566+
base_classes, curr_module, self.options
5567+
)
55715568
with self.msg.filter_errors() as local_errors:
55725569
self.check_multiple_inheritance(info)
55735570
if local_errors.has_new_errors():
55745571
# "class A(B, C)" unsafe, now check "class A(C, B)":
55755572
base_classes = _get_base_classes(instances[::-1])
5576-
info, full_name = _make_fake_typeinfo_and_full_name(base_classes, curr_module)
5573+
info, full_name = _make_fake_typeinfo_and_full_name(
5574+
base_classes, curr_module, self.options
5575+
)
55775576
with self.msg.filter_errors() as local_errors:
55785577
self.check_multiple_inheritance(info)
55795578
info.is_intersection = True

mypy/lookup.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ def lookup_fully_qualified(
2222
This function should *not* be used to find a module. Those should be looked
2323
in the modules dictionary.
2424
"""
25-
head = name
25+
# 1. Exclude the names of ad hoc instance intersections from step 2.
26+
i = name.find("<subclass ")
27+
head = name if i == -1 else name[:i]
2628
rest = []
27-
# 1. Find a module tree in modules dictionary.
29+
# 2. Find a module tree in modules dictionary.
2830
while True:
2931
if "." not in head:
3032
if raise_on_missing:
@@ -36,12 +38,14 @@ def lookup_fully_qualified(
3638
if mod is not None:
3739
break
3840
names = mod.names
39-
# 2. Find the symbol in the module tree.
41+
# 3. Find the symbol in the module tree.
4042
if not rest:
4143
# Looks like a module, don't use this to avoid confusions.
4244
if raise_on_missing:
4345
assert rest, f"Cannot find {name}, got a module symbol"
4446
return None
47+
if i != -1:
48+
rest[0] += name[i:]
4549
while True:
4650
key = rest.pop()
4751
if key not in names:

test-data/unit/check-incremental.test

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5268,7 +5268,7 @@ reveal_type(Foo().x)
52685268
[builtins fixtures/isinstance.pyi]
52695269
[out]
52705270
[out2]
5271-
tmp/b.py:2: note: Revealed type is "a.<subclass of "A" and "B">"
5271+
tmp/b.py:2: note: Revealed type is "a.<subclass of "a.A" and "a.B">"
52725272

52735273
[case testIsInstanceAdHocIntersectionIncrementalNoChangeSameName]
52745274
import b
@@ -5291,7 +5291,7 @@ reveal_type(Foo().x)
52915291
[builtins fixtures/isinstance.pyi]
52925292
[out]
52935293
[out2]
5294-
tmp/b.py:2: note: Revealed type is "a.<subclass of "B" and "B">"
5294+
tmp/b.py:2: note: Revealed type is "a.<subclass of "c.B" and "a.B">"
52955295

52965296

52975297
[case testIsInstanceAdHocIntersectionIncrementalNoChangeTuple]
@@ -5313,7 +5313,7 @@ reveal_type(Foo().x)
53135313
[builtins fixtures/isinstance.pyi]
53145314
[out]
53155315
[out2]
5316-
tmp/b.py:2: note: Revealed type is "a.<subclass of "tuple" and "B">"
5316+
tmp/b.py:2: note: Revealed type is "a.<subclass of "Tuple[builtins.int, ...]" and "a.B">"
53175317

53185318
[case testIsInstanceAdHocIntersectionIncrementalIsInstanceChange]
53195319
import c
@@ -5347,9 +5347,9 @@ from b import y
53475347
reveal_type(y)
53485348
[builtins fixtures/isinstance.pyi]
53495349
[out]
5350-
tmp/c.py:2: note: Revealed type is "a.<subclass of "A" and "B">"
5350+
tmp/c.py:2: note: Revealed type is "a.<subclass of "a.A" and "a.B">"
53515351
[out2]
5352-
tmp/c.py:2: note: Revealed type is "a.<subclass of "A" and "C">"
5352+
tmp/c.py:2: note: Revealed type is "a.<subclass of "a.A" and "a.C">"
53535353

53545354
[case testIsInstanceAdHocIntersectionIncrementalUnderlyingObjChang]
53555355
import c
@@ -5375,9 +5375,9 @@ from b import y
53755375
reveal_type(y)
53765376
[builtins fixtures/isinstance.pyi]
53775377
[out]
5378-
tmp/c.py:2: note: Revealed type is "b.<subclass of "A" and "B">"
5378+
tmp/c.py:2: note: Revealed type is "b.<subclass of "a.A" and "a.B">"
53795379
[out2]
5380-
tmp/c.py:2: note: Revealed type is "b.<subclass of "A" and "C">"
5380+
tmp/c.py:2: note: Revealed type is "b.<subclass of "a.A" and "a.C">"
53815381

53825382
[case testIsInstanceAdHocIntersectionIncrementalIntersectionToUnreachable]
53835383
import c
@@ -5408,7 +5408,7 @@ from b import z
54085408
reveal_type(z)
54095409
[builtins fixtures/isinstance.pyi]
54105410
[out]
5411-
tmp/c.py:2: note: Revealed type is "a.<subclass of "A" and "B">"
5411+
tmp/c.py:2: note: Revealed type is "a.<subclass of "a.A" and "a.B">"
54125412
[out2]
54135413
tmp/b.py:2: error: Cannot determine type of "y"
54145414
tmp/c.py:2: note: Revealed type is "Any"
@@ -5445,7 +5445,60 @@ reveal_type(z)
54455445
tmp/b.py:2: error: Cannot determine type of "y"
54465446
tmp/c.py:2: note: Revealed type is "Any"
54475447
[out2]
5448-
tmp/c.py:2: note: Revealed type is "a.<subclass of "A" and "B">"
5448+
tmp/c.py:2: note: Revealed type is "a.<subclass of "a.A" and "a.B">"
5449+
5450+
[case testIsInstanceAdHocIntersectionIncrementalNestedClass]
5451+
import b
5452+
[file a.py]
5453+
class A:
5454+
class B: ...
5455+
class C: ...
5456+
class D:
5457+
def __init__(self) -> None:
5458+
x: A.B
5459+
assert isinstance(x, A.C)
5460+
self.x = x
5461+
[file b.py]
5462+
from a import A
5463+
[file b.py.2]
5464+
from a import A
5465+
reveal_type(A.D.x)
5466+
[builtins fixtures/isinstance.pyi]
5467+
[out]
5468+
[out2]
5469+
tmp/b.py:2: note: Revealed type is "a.<subclass of "a.A.B" and "a.A.C">"
5470+
5471+
[case testIsInstanceAdHocIntersectionIncrementalUnions]
5472+
import c
5473+
[file a.py]
5474+
import b
5475+
class A:
5476+
p: b.D
5477+
class B:
5478+
p: b.D
5479+
class C:
5480+
p: b.D
5481+
c: str
5482+
x: A
5483+
assert isinstance(x, (B, C))
5484+
y = x
5485+
[file b.py]
5486+
class D:
5487+
p: int
5488+
[file c.py]
5489+
from a import y
5490+
[file c.py.2]
5491+
from a import y, C
5492+
reveal_type(y)
5493+
reveal_type(y.p.p)
5494+
assert isinstance(y, C)
5495+
reveal_type(y.c)
5496+
[builtins fixtures/isinstance.pyi]
5497+
[out]
5498+
[out2]
5499+
tmp/c.py:2: note: Revealed type is "Union[a.<subclass of "a.A" and "a.B">, a.<subclass of "a.A" and "a.C">]"
5500+
tmp/c.py:3: note: Revealed type is "builtins.int"
5501+
tmp/c.py:5: note: Revealed type is "builtins.str"
54495502

54505503
[case testStubFixupIssues]
54515504
import a

0 commit comments

Comments
 (0)