Skip to content

Commit a2d715a

Browse files
authored
Avoid narrowing to NewType (#20766)
Fixes #20733 I might make this type munging look a little nicer in a future refactor This introduces new errors in two primer projects
1 parent 17e4a45 commit a2d715a

3 files changed

Lines changed: 110 additions & 1 deletion

File tree

mypy/checker.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8347,6 +8347,15 @@ def conditional_types(
83478347
return proposed_type, remaining_type
83488348

83498349
proposed_type = make_simplified_union([type_range.item for type_range in proposed_type_ranges])
8350+
items = proposed_type.items if isinstance(proposed_type, UnionType) else [proposed_type]
8351+
for i in range(len(items)):
8352+
item = get_proper_type(items[i])
8353+
# Avoid ever narrowing to a NewType. The principle is values of NewType should only be
8354+
# produce by explicit wrapping
8355+
while isinstance(item, Instance) and item.type.is_newtype:
8356+
item = item.type.bases[0]
8357+
items[i] = item
8358+
proposed_type = get_proper_type(UnionType.make_union(items))
83508359

83518360
if isinstance(proper_type, AnyType):
83528361
return proposed_type, current_type

test-data/unit/check-narrowing.test

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3726,3 +3726,103 @@ def main(
37263726
reveal_type(v_all) # N: Revealed type is "builtins.bytes | builtins.bytearray | builtins.memoryview"
37273727
reveal_type(v_memoryview) # N: Revealed type is "builtins.memoryview"
37283728
[builtins fixtures/primitives.pyi]
3729+
3730+
3731+
[case testNarrowNewTypeVsSubclass]
3732+
# mypy: strict-equality, warn-unreachable
3733+
from typing import NewType
3734+
3735+
M1 = NewType("M1", int)
3736+
M2 = NewType("M2", int)
3737+
3738+
def check_m(base: int, m1: M1, m2: M2):
3739+
if m1 == m2: # E: Non-overlapping equality check (left operand type: "M1", right operand type: "M2")
3740+
reveal_type(m1) # N: Revealed type is "__main__.M1"
3741+
reveal_type(m2) # N: Revealed type is "__main__.M2"
3742+
3743+
if m1 == base:
3744+
# We do not narrow base
3745+
reveal_type(m1) # N: Revealed type is "__main__.M1"
3746+
reveal_type(base) # N: Revealed type is "builtins.int"
3747+
if m2 == base:
3748+
reveal_type(m2) # N: Revealed type is "__main__.M2"
3749+
reveal_type(base) # N: Revealed type is "builtins.int"
3750+
3751+
# We do narrow for subclasses! (assuming no custom equality)
3752+
class A: ...
3753+
class A1(A): ...
3754+
class A2(A): ...
3755+
3756+
def check_a(base: A, a1: A1, a2: A2):
3757+
if a1 == a2: # E: Non-overlapping equality check (left operand type: "A1", right operand type: "A2")
3758+
reveal_type(a1) # N: Revealed type is "__main__.A1"
3759+
reveal_type(a2) # N: Revealed type is "__main__.A2"
3760+
3761+
if a1 == base:
3762+
# We do narrow base
3763+
reveal_type(a1) # N: Revealed type is "__main__.A1"
3764+
reveal_type(base) # N: Revealed type is "__main__.A1"
3765+
if a2 == base: # E: Non-overlapping equality check (left operand type: "A2", right operand type: "A1")
3766+
reveal_type(a2) # N: Revealed type is "__main__.A2"
3767+
reveal_type(base) # N: Revealed type is "__main__.A1"
3768+
[builtins fixtures/primitives.pyi]
3769+
3770+
3771+
[case testNarrowNewTypeFromObject]
3772+
# mypy: strict-equality, warn-unreachable
3773+
from __future__ import annotations
3774+
from typing import NewType
3775+
3776+
UserId = NewType("UserId", int)
3777+
3778+
def f1(whatever: object, uid: UserId):
3779+
# The general principle is that we should not be able to produce a value of NewType
3780+
# without there being explicit wrapping somewhere
3781+
if whatever == uid:
3782+
reveal_type(whatever) # N: Revealed type is "builtins.int"
3783+
reveal_type(uid) # N: Revealed type is "__main__.UserId"
3784+
3785+
class Other: ...
3786+
3787+
def f2(whatever: object, uid: UserId | Other):
3788+
if whatever == uid:
3789+
reveal_type(whatever) # N: Revealed type is "builtins.int | __main__.Other"
3790+
reveal_type(uid) # N: Revealed type is "__main__.UserId | __main__.Other"
3791+
[builtins fixtures/primitives.pyi]
3792+
3793+
3794+
[case testNarrowNewTypeNested]
3795+
# mypy: strict-equality, warn-unreachable
3796+
from typing import NewType, Final
3797+
3798+
Path = NewType("Path", str)
3799+
NormPath = NewType("NormPath", Path)
3800+
3801+
def op(normpath: NormPath, path: Path):
3802+
if normpath == path:
3803+
# No narrowing
3804+
reveal_type(normpath) # N: Revealed type is "__main__.NormPath"
3805+
reveal_type(path) # N: Revealed type is "__main__.Path"
3806+
[builtins fixtures/primitives.pyi]
3807+
3808+
3809+
[case testNarrowNewTypeSharedValue]
3810+
# mypy: strict-equality, warn-unreachable
3811+
from typing import NewType, Final
3812+
3813+
UserId = NewType("UserId", int)
3814+
TeamId = NewType("TeamId", int)
3815+
INVALID = 123
3816+
3817+
def get_owner(uid: UserId, tid: TeamId):
3818+
# No narrowing for INVALID
3819+
if uid == INVALID:
3820+
reveal_type(uid) # N: Revealed type is "__main__.UserId"
3821+
reveal_type(tid) # N: Revealed type is "__main__.TeamId"
3822+
reveal_type(INVALID) # N: Revealed type is "builtins.int"
3823+
if tid == INVALID:
3824+
reveal_type(uid) # N: Revealed type is "__main__.UserId"
3825+
reveal_type(tid) # N: Revealed type is "__main__.TeamId"
3826+
reveal_type(INVALID) # N: Revealed type is "builtins.int"
3827+
return None
3828+
[builtins fixtures/primitives.pyi]

test-data/unit/check-newtype.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ from typing import NewType
368368
T = NewType('T', int)
369369
d: object
370370
if isinstance(d, T): # E: Cannot use isinstance() with NewType type
371-
reveal_type(d) # N: Revealed type is "__main__.T"
371+
reveal_type(d) # N: Revealed type is "builtins.int"
372372
issubclass(object, T) # E: Cannot use issubclass() with NewType type
373373
[builtins fixtures/isinstancelist.pyi]
374374

0 commit comments

Comments
 (0)