From 2c49c6ea1d6a41ea7d060488ab4c99109675c700 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 22 Mar 2026 22:07:04 +0000 Subject: [PATCH] [ty] Add more tests for `NewType` subtyping --- .../resources/mdtest/annotations/new_types.md | 44 +++++++++++++++++++ .../resources/mdtest/annotations/self.md | 29 ++++++++++++ 2 files changed, 73 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index f0166b72cb03a9..123c1f2e17ea99 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -26,6 +26,7 @@ Foo = NewType("Foo", int) Bar = NewType("Bar", Foo) static_assert(is_subtype_of(Foo, int)) +static_assert(is_subtype_of(Foo, int | None)) static_assert(not is_equivalent_to(Foo, int)) static_assert(is_subtype_of(Bar, Foo)) @@ -57,14 +58,23 @@ h(Bar(Foo(42))) FloatNewType = NewType("FloatNewType", float) static_assert(is_subtype_of(FloatNewType, float)) +static_assert(is_subtype_of(FloatNewType, FloatNewType | None)) static_assert(is_subtype_of(Intersection[FloatNewType, AlwaysFalsy], float)) +static_assert(is_subtype_of(Intersection[FloatNewType, AlwaysFalsy], FloatNewType)) +static_assert(is_subtype_of(Intersection[FloatNewType, AlwaysFalsy], FloatNewType | None)) static_assert(is_subtype_of(Intersection[FloatNewType, Not[AlwaysFalsy]], float)) +static_assert(is_subtype_of(Intersection[FloatNewType, Not[AlwaysFalsy]], FloatNewType)) +static_assert(is_subtype_of(Intersection[FloatNewType, Not[AlwaysFalsy]], FloatNewType | None)) ComplexNewType = NewType("ComplexNewType", complex) static_assert(is_subtype_of(ComplexNewType, complex)) static_assert(is_subtype_of(Intersection[ComplexNewType, AlwaysFalsy], complex)) +static_assert(is_subtype_of(Intersection[ComplexNewType, AlwaysFalsy], ComplexNewType)) +static_assert(is_subtype_of(Intersection[ComplexNewType, AlwaysFalsy], ComplexNewType | None)) static_assert(is_subtype_of(Intersection[ComplexNewType, Not[AlwaysFalsy]], complex)) +static_assert(is_subtype_of(Intersection[ComplexNewType, Not[AlwaysFalsy]], ComplexNewType)) +static_assert(is_subtype_of(Intersection[ComplexNewType, Not[AlwaysFalsy]], ComplexNewType | None)) static_assert(not is_assignable_to(ComplexNewType, float)) static_assert(not is_assignable_to(Intersection[ComplexNewType, AlwaysFalsy], float)) static_assert(not is_assignable_to(Intersection[ComplexNewType, Not[AlwaysFalsy]], float)) @@ -717,3 +727,37 @@ NT = NewType("NT", C) x = NT(C()) reveal_type(x.copy()) # revealed: NT ``` + +## TypeVar solving for `NewType`s in combination with generic `Protocol`s + +In an early version of , we revealed `F[Baz]` on the +final line here, rather than `F[N]`: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import NewType, Protocol, overload + +class Foo: ... +class Bar: ... +class Baz: ... + +N = NewType("N", Baz) + +class I[T](Protocol): + def f(self) -> T: ... + +class F[T]: + @overload + def __new__(cls, xs: I[T | Foo]): ... + @overload + def __new__(cls, xs: I[T]): ... + def __new__(cls, xs): + raise NotImplementedError + +def taxa(xs: I[N]): + reveal_type(F(xs)) # revealed: F[N] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index e4d5940f237bb9..2f60d65e973b04 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -639,8 +639,13 @@ class C[T: Base]: # Calling a method on a specialized instance should not produce an error C[Base]().g() +BaseNewType = NewType("BaseNewType", Base) + +C[BaseNewType]().g() + # Test with a NewType bound K = NewType("K", int) +K2 = NewType("K2", K) class D[T: K]: x: T @@ -650,6 +655,30 @@ class D[T: K]: # Calling a method on a specialized instance should not produce an error D[K]().h() +D[K2]().h() + +# Test with a union-NewType bound +K3 = NewType("K3", float) +K4 = NewType("K4", K3) + +class D2[T: K3]: + x: T + + def h(self) -> None: + pass + +# Calling a method on a specialized instance should not produce an error +D2[K3]().h() +D2[K4]().h() + +class D3[T: float]: + x: T + + def h(self) -> None: + pass + +D3[K3]().h() +D3[K4]().h() ``` ## Protocols