From dcabb948f39ad553468f7127e1267ff0425835cf Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 14 Feb 2025 12:24:10 -0800 Subject: [PATCH] [red-knot] add special case for float/complex (#16166) When adjusting the existing tests, I aimed to avoid dealing with the special case in other tests if it's not necessary to do so (that is, avoid using `float` and `complex` as examples where we just need "some type"), and keep the tests for the special case mostly collected in the mdtest dedicated to that purpose. Fixes https://github.com/astral-sh/ruff/issues/14932 --- .../mdtest/annotations/int_float_complex.md | 90 +++++++++++++++++++ .../resources/mdtest/annotations/union.md | 8 +- .../resources/mdtest/assignment/augmented.md | 12 +-- .../resources/mdtest/binary/booleans.md | 10 +-- .../resources/mdtest/binary/instances.md | 21 +++-- .../resources/mdtest/binary/integers.md | 18 ++-- .../mdtest/call/callable_instance.md | 8 +- .../comparison/instances/rich_comparison.md | 30 +++---- .../resources/mdtest/comparison/tuples.md | 14 +-- .../mdtest/exception/control_flow.md | 76 ++++++++-------- .../mdtest/type_properties/is_subtype_of.md | 21 +++-- .../resources/mdtest/union_types.md | 4 +- crates/red_knot_python_semantic/src/types.rs | 32 +++++++ 13 files changed, 236 insertions(+), 108 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/annotations/int_float_complex.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/int_float_complex.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/int_float_complex.md new file mode 100644 index 0000000000..a3b6a7bf00 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/int_float_complex.md @@ -0,0 +1,90 @@ +# Special cases for int/float/complex in annotations + +In order to support common use cases, an annotation of `float` actually means `int | float`, and an +annotation of `complex` actually means `int | float | complex`. See +[the specification](https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex) + +## float + +An annotation of `float` means `int | float`, so `int` is assignable to it: + +```py +def takes_float(x: float): + pass + +def passes_int_to_float(x: int): + # no error! + takes_float(x) +``` + +It also applies to variable annotations: + +```py +def assigns_int_to_float(x: int): + # no error! + y: float = x +``` + +It doesn't work the other way around: + +```py +def takes_int(x: int): + pass + +def passes_float_to_int(x: float): + # error: [invalid-argument-type] + takes_int(x) + +def assigns_float_to_int(x: float): + # error: [invalid-assignment] + y: int = x +``` + +Unlike other type checkers, we choose not to obfuscate this special case by displaying `int | float` +as just `float`; we display the actual type: + +```py +def f(x: float): + reveal_type(x) # revealed: int | float +``` + +## complex + +An annotation of `complex` means `int | float | complex`, so `int` and `float` are both assignable +to it (but not the other way around): + +```py +def takes_complex(x: complex): + pass + +def passes_to_complex(x: float, y: int): + # no errors! + takes_complex(x) + takes_complex(y) + +def assigns_to_complex(x: float, y: int): + # no errors! + a: complex = x + b: complex = y + +def takes_int(x: int): + pass + +def takes_float(x: float): + pass + +def passes_complex(x: complex): + # error: [invalid-argument-type] + takes_int(x) + # error: [invalid-argument-type] + takes_float(x) + +def assigns_complex(x: complex): + # error: [invalid-assignment] + y: int = x + # error: [invalid-assignment] + z: float = x + +def f(x: complex): + reveal_type(x) # revealed: int | float | complex +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/union.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/union.md index 115bcc99ee..bdae8417fb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/union.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/union.md @@ -9,9 +9,9 @@ from typing import Union a: Union[int, str] a1: Union[int, bool] -a2: Union[int, Union[float, str]] +a2: Union[int, Union[bytes, str]] a3: Union[int, None] -a4: Union[Union[float, str]] +a4: Union[Union[bytes, str]] a5: Union[int] a6: Union[()] @@ -21,11 +21,11 @@ def f(): # Since bool is a subtype of int we simplify to int here. But we do allow assigning boolean values (see below). # revealed: int reveal_type(a1) - # revealed: int | float | str + # revealed: int | bytes | str reveal_type(a2) # revealed: int | None reveal_type(a3) - # revealed: float | str + # revealed: bytes | str reveal_type(a4) # revealed: int reveal_type(a5) diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md index cc096c53dd..cc87c85d12 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md @@ -9,7 +9,7 @@ reveal_type(x) # revealed: Literal[2] x = 1.0 x /= 2 -reveal_type(x) # revealed: float +reveal_type(x) # revealed: int | float ``` ## Dunder methods @@ -24,12 +24,12 @@ x -= 1 reveal_type(x) # revealed: str class C: - def __iadd__(self, other: str) -> float: - return 1.0 + def __iadd__(self, other: str) -> int: + return 1 x = C() x += "Hello" -reveal_type(x) # revealed: float +reveal_type(x) # revealed: int ``` ## Unsupported types @@ -130,10 +130,10 @@ def _(flag: bool): if flag: f = Foo() else: - f = 42.0 + f = 42 f += 12 - reveal_type(f) # revealed: str | float + reveal_type(f) # revealed: str | Literal[54] ``` ## Partially bound target union with `__add__` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md b/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md index 7d60e52f15..1e62ce8920 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md @@ -56,7 +56,7 @@ def _(a: bool): reveal_type(x - a) # revealed: int reveal_type(x * a) # revealed: int reveal_type(x // a) # revealed: int - reveal_type(x / a) # revealed: float + reveal_type(x / a) # revealed: int | float reveal_type(x % a) # revealed: int def rhs_is_int(x: int): @@ -64,7 +64,7 @@ def _(a: bool): reveal_type(a - x) # revealed: int reveal_type(a * x) # revealed: int reveal_type(a // x) # revealed: int - reveal_type(a / x) # revealed: float + reveal_type(a / x) # revealed: int | float reveal_type(a % x) # revealed: int def lhs_is_bool(x: bool): @@ -72,7 +72,7 @@ def _(a: bool): reveal_type(x - a) # revealed: int reveal_type(x * a) # revealed: int reveal_type(x // a) # revealed: int - reveal_type(x / a) # revealed: float + reveal_type(x / a) # revealed: int | float reveal_type(x % a) # revealed: int def rhs_is_bool(x: bool): @@ -80,7 +80,7 @@ def _(a: bool): reveal_type(a - x) # revealed: int reveal_type(a * x) # revealed: int reveal_type(a // x) # revealed: int - reveal_type(a / x) # revealed: float + reveal_type(a / x) # revealed: int | float reveal_type(a % x) # revealed: int def both_are_bool(x: bool, y: bool): @@ -88,6 +88,6 @@ def _(a: bool): reveal_type(x - y) # revealed: int reveal_type(x * y) # revealed: int reveal_type(x // y) # revealed: int - reveal_type(x / y) # revealed: float + reveal_type(x / y) # revealed: int | float reveal_type(x % y) # revealed: int ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 54b4c6c6d1..84116fa2c5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -268,23 +268,28 @@ reveal_type(B() + B()) # revealed: Unknown | int ## Integration test: numbers from typeshed +We get less precise results from binary operations on float/complex literals due to the special case +for annotations of `float` or `complex`, which applies also to return annotations for typeshed +dunder methods. Perhaps we could have a special-case on the special-case, to exclude these typeshed +return annotations from the widening, and preserve a bit more precision here? + ```py -reveal_type(3j + 3.14) # revealed: complex -reveal_type(4.2 + 42) # revealed: float -reveal_type(3j + 3) # revealed: complex +reveal_type(3j + 3.14) # revealed: int | float | complex +reveal_type(4.2 + 42) # revealed: int | float +reveal_type(3j + 3) # revealed: int | float | complex -# TODO should be complex, need to check arg type and fall back to `rhs.__radd__` -reveal_type(3.14 + 3j) # revealed: float +# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__` +reveal_type(3.14 + 3j) # revealed: int | float -# TODO should be float, need to check arg type and fall back to `rhs.__radd__` +# TODO should be int | float, need to check arg type and fall back to `rhs.__radd__` reveal_type(42 + 4.2) # revealed: int -# TODO should be complex, need to check arg type and fall back to `rhs.__radd__` +# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__` reveal_type(3 + 3j) # revealed: int def _(x: bool, y: int): reveal_type(x + y) # revealed: int - reveal_type(4.2 + x) # revealed: float + reveal_type(4.2 + x) # revealed: int | float # TODO should be float, need to check arg type and fall back to `rhs.__radd__` reveal_type(y + 4.12) # revealed: int diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md index e6d4b4c90d..0eb5a2cb31 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md @@ -19,7 +19,7 @@ def lhs(x: int): reveal_type(x - 4) # revealed: int reveal_type(x * -1) # revealed: int reveal_type(x // 3) # revealed: int - reveal_type(x / 3) # revealed: float + reveal_type(x / 3) # revealed: int | float reveal_type(x % 3) # revealed: int def rhs(x: int): @@ -27,7 +27,7 @@ def rhs(x: int): reveal_type(3 - x) # revealed: int reveal_type(3 * x) # revealed: int reveal_type(-3 // x) # revealed: int - reveal_type(-3 / x) # revealed: float + reveal_type(-3 / x) # revealed: int | float reveal_type(5 % x) # revealed: int def both(x: int): @@ -35,7 +35,7 @@ def both(x: int): reveal_type(x - x) # revealed: int reveal_type(x * x) # revealed: int reveal_type(x // x) # revealed: int - reveal_type(x / x) # revealed: float + reveal_type(x / x) # revealed: int | float reveal_type(x % x) # revealed: int ``` @@ -80,24 +80,20 @@ c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero" reveal_type(c) # revealed: int # error: "Cannot divide object of type `int` by zero" -# revealed: float -reveal_type(int() / 0) +reveal_type(int() / 0) # revealed: int | float # error: "Cannot divide object of type `Literal[1]` by zero" -# revealed: float -reveal_type(1 / False) +reveal_type(1 / False) # revealed: float # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" True / False # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" bool(1) / False # error: "Cannot divide object of type `float` by zero" -# revealed: float -reveal_type(1.0 / 0) +reveal_type(1.0 / 0) # revealed: int | float class MyInt(int): ... # No error for a subclass of int -# revealed: float -reveal_type(MyInt(3) / 0) +reveal_type(MyInt(3) / 0) # revealed: int | float ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md b/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md index f98d2cf1fc..10678ef2ba 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md @@ -4,14 +4,14 @@ ```py class Multiplier: - def __init__(self, factor: float): + def __init__(self, factor: int): self.factor = factor - def __call__(self, number: float) -> float: + def __call__(self, number: int) -> int: return number * self.factor -a = Multiplier(2.0)(3.0) -reveal_type(a) # revealed: float +a = Multiplier(2)(3) +reveal_type(a) # revealed: int class Unit: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md index c4bad9bbb3..29fb516e23 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md @@ -20,8 +20,8 @@ class A: def __eq__(self, other: A) -> int: return 42 - def __ne__(self, other: A) -> float: - return 42.0 + def __ne__(self, other: A) -> bytearray: + return bytearray() def __lt__(self, other: A) -> str: return "42" @@ -36,7 +36,7 @@ class A: return {42} reveal_type(A() == A()) # revealed: int -reveal_type(A() != A()) # revealed: float +reveal_type(A() != A()) # revealed: bytearray reveal_type(A() < A()) # revealed: str reveal_type(A() <= A()) # revealed: bytes reveal_type(A() > A()) # revealed: list @@ -55,8 +55,8 @@ class A: def __eq__(self, other: B) -> int: return 42 - def __ne__(self, other: B) -> float: - return 42.0 + def __ne__(self, other: B) -> bytearray: + return bytearray() def __lt__(self, other: B) -> str: return "42" @@ -73,7 +73,7 @@ class A: class B: ... reveal_type(A() == B()) # revealed: int -reveal_type(A() != B()) # revealed: float +reveal_type(A() != B()) # revealed: bytearray reveal_type(A() < B()) # revealed: str reveal_type(A() <= B()) # revealed: bytes reveal_type(A() > B()) # revealed: list @@ -93,8 +93,8 @@ class A: def __eq__(self, other: B) -> int: return 42 - def __ne__(self, other: B) -> float: - return 42.0 + def __ne__(self, other: B) -> bytearray: + return bytearray() def __lt__(self, other: B) -> str: return "42" @@ -117,7 +117,7 @@ class B: def __ne__(self, other: str) -> B: return B() -# TODO: should be `int` and `float`. +# TODO: should be `int` and `bytearray`. # Need to check arg type and fall back to `rhs.__eq__` and `rhs.__ne__`. # # Because `object.__eq__` and `object.__ne__` accept `object` in typeshed, @@ -136,11 +136,11 @@ class C: def __gt__(self, other: C) -> int: return 42 - def __ge__(self, other: C) -> float: - return 42.0 + def __ge__(self, other: C) -> bytearray: + return bytearray() reveal_type(C() < C()) # revealed: int -reveal_type(C() <= C()) # revealed: float +reveal_type(C() <= C()) # revealed: bytearray ``` ## Reflected Comparisons with Subclasses @@ -175,8 +175,8 @@ class B(A): def __eq__(self, other: A) -> int: return 42 - def __ne__(self, other: A) -> float: - return 42.0 + def __ne__(self, other: A) -> bytearray: + return bytearray() def __lt__(self, other: A) -> str: return "42" @@ -191,7 +191,7 @@ class B(A): return {42} reveal_type(A() == B()) # revealed: int -reveal_type(A() != B()) # revealed: float +reveal_type(A() != B()) # revealed: bytearray reveal_type(A() < B()) # revealed: list reveal_type(A() <= B()) # revealed: set diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md index db0a9fa098..963d8121b6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md @@ -151,11 +151,11 @@ class A: def __ne__(self, o: object) -> bytes: return b"world" - def __lt__(self, o: A) -> float: - return 3.14 + def __lt__(self, o: A) -> bytearray: + return bytearray() - def __le__(self, o: A) -> complex: - return complex(0.5, -0.5) + def __le__(self, o: A) -> memoryview: + return memoryview(b"") def __gt__(self, o: A) -> tuple: return (1, 2, 3) @@ -167,8 +167,8 @@ a = (A(), A()) reveal_type(a == a) # revealed: bool reveal_type(a != a) # revealed: bool -reveal_type(a < a) # revealed: float | Literal[False] -reveal_type(a <= a) # revealed: complex | Literal[True] +reveal_type(a < a) # revealed: bytearray | Literal[False] +reveal_type(a <= a) # revealed: memoryview | Literal[True] reveal_type(a > a) # revealed: tuple | Literal[False] reveal_type(a >= a) # revealed: list | Literal[True] @@ -187,7 +187,7 @@ class B: def __lt__(self, o: B) -> set: return set() -reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False] +reveal_type((A(), B()) < (A(), B())) # revealed: bytearray | set | Literal[False] ``` #### Special Handling of Eq and NotEq in Lexicographic Comparisons diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md b/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md index 74f8c2ebd8..871689ee77 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/control_flow.md @@ -303,8 +303,8 @@ An example with multiple `except` branches and a `finally` branch: def could_raise_returns_memoryview() -> memoryview: return memoryview(b"") -def could_raise_returns_float() -> float: - return 3.14 +def could_raise_returns_bytearray() -> bytearray: + return bytearray() x = 1 @@ -322,13 +322,13 @@ except ValueError: reveal_type(x) # revealed: Literal[1] | str x = could_raise_returns_memoryview() reveal_type(x) # revealed: memoryview - x = could_raise_returns_float() - reveal_type(x) # revealed: float + x = could_raise_returns_bytearray() + reveal_type(x) # revealed: bytearray finally: - # TODO: should be `Literal[1] | str | bytes | bool | memoryview | float` - reveal_type(x) # revealed: str | bool | float + # TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray` + reveal_type(x) # revealed: str | bool | bytearray -reveal_type(x) # revealed: str | bool | float +reveal_type(x) # revealed: str | bool | bytearray ``` ## Combining `except`, `else` and `finally` branches @@ -350,8 +350,8 @@ def could_raise_returns_bool() -> bool: def could_raise_returns_memoryview() -> memoryview: return memoryview(b"") -def could_raise_returns_float() -> float: - return 3.14 +def could_raise_returns_bytearray() -> bytearray: + return bytearray() x = 1 @@ -369,13 +369,13 @@ else: reveal_type(x) # revealed: str x = could_raise_returns_memoryview() reveal_type(x) # revealed: memoryview - x = could_raise_returns_float() - reveal_type(x) # revealed: float + x = could_raise_returns_bytearray() + reveal_type(x) # revealed: bytearray finally: - # TODO: should be `Literal[1] | str | bytes | bool | memoryview | float` - reveal_type(x) # revealed: bool | float + # TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray` + reveal_type(x) # revealed: bool | bytearray -reveal_type(x) # revealed: bool | float +reveal_type(x) # revealed: bool | bytearray ``` The same again, this time with multiple `except` branches: @@ -403,8 +403,8 @@ except ValueError: reveal_type(x) # revealed: Literal[1] | str x = could_raise_returns_memoryview() reveal_type(x) # revealed: memoryview - x = could_raise_returns_float() - reveal_type(x) # revealed: float + x = could_raise_returns_bytearray() + reveal_type(x) # revealed: bytearray else: reveal_type(x) # revealed: str x = could_raise_returns_range() @@ -412,10 +412,10 @@ else: x = could_raise_returns_slice() reveal_type(x) # revealed: slice finally: - # TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice` - reveal_type(x) # revealed: bool | float | slice + # TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray | range | slice` + reveal_type(x) # revealed: bool | bytearray | slice -reveal_type(x) # revealed: bool | float | slice +reveal_type(x) # revealed: bool | bytearray | slice ``` ## Nested `try`/`except` blocks @@ -441,8 +441,8 @@ def could_raise_returns_bool() -> bool: def could_raise_returns_memoryview() -> memoryview: return memoryview(b"") -def could_raise_returns_float() -> float: - return 3.14 +def could_raise_returns_property() -> property: + return property() def could_raise_returns_range() -> range: return range(42) @@ -450,8 +450,8 @@ def could_raise_returns_range() -> range: def could_raise_returns_slice() -> slice: return slice(None) -def could_raise_returns_complex() -> complex: - return 3j +def could_raise_returns_super() -> super: + return super() def could_raise_returns_bytearray() -> bytearray: return bytearray() @@ -482,8 +482,8 @@ try: reveal_type(x) # revealed: Literal[1] | str x = could_raise_returns_memoryview() reveal_type(x) # revealed: memoryview - x = could_raise_returns_float() - reveal_type(x) # revealed: float + x = could_raise_returns_property() + reveal_type(x) # revealed: property else: reveal_type(x) # revealed: str x = could_raise_returns_range() @@ -491,15 +491,15 @@ try: x = could_raise_returns_slice() reveal_type(x) # revealed: slice finally: - # TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice` - reveal_type(x) # revealed: bool | float | slice + # TODO: should be `Literal[1] | str | bytes | bool | memoryview | property | range | slice` + reveal_type(x) # revealed: bool | property | slice x = 2 reveal_type(x) # revealed: Literal[2] reveal_type(x) # revealed: Literal[2] except: - reveal_type(x) # revealed: Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice - x = could_raise_returns_complex() - reveal_type(x) # revealed: complex + reveal_type(x) # revealed: Literal[1, 2] | str | bytes | bool | memoryview | property | range | slice + x = could_raise_returns_super() + reveal_type(x) # revealed: super x = could_raise_returns_bytearray() reveal_type(x) # revealed: bytearray else: @@ -509,7 +509,7 @@ else: x = could_raise_returns_Bar() reveal_type(x) # revealed: Bar finally: - # TODO: should be `Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice | complex | bytearray | Foo | Bar` + # TODO: should be `Literal[1, 2] | str | bytes | bool | memoryview | property | range | slice | super | bytearray | Foo | Bar` reveal_type(x) # revealed: bytearray | Bar # Either one `except` branch or the `else` @@ -535,8 +535,8 @@ def could_raise_returns_range() -> range: def could_raise_returns_bytearray() -> bytearray: return bytearray() -def could_raise_returns_float() -> float: - return 3.14 +def could_raise_returns_memoryview() -> memoryview: + return memoryview(b"") x = 1 @@ -553,12 +553,12 @@ try: reveal_type(x) # revealed: str | bytes x = could_raise_returns_bytearray() reveal_type(x) # revealed: bytearray - x = could_raise_returns_float() - reveal_type(x) # revealed: float + x = could_raise_returns_memoryview() + reveal_type(x) # revealed: memoryview finally: - # TODO: should be `str | bytes | bytearray | float` - reveal_type(x) # revealed: bytes | float - reveal_type(x) # revealed: bytes | float + # TODO: should be `str | bytes | bytearray | memoryview` + reveal_type(x) # revealed: bytes | memoryview + reveal_type(x) # revealed: bytes | memoryview x = foo reveal_type(x) # revealed: Literal[foo] except: diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 17c7f7dc13..95907565d8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -11,11 +11,15 @@ See the [typing documentation] for more information. - `bool` is a subtype of `int`. This is modeled after Python's runtime behavior, where `int` is a supertype of `bool` (present in `bool`s bases and MRO). -- `int` is not a subtype of `float`/`complex`, even though `float`/`complex` can be used in place of - `int` in some contexts (see [special case for float and complex]). +- `int` is not a subtype of `float`/`complex`, although this is muddied by the + [special case for float and complex] where annotations of `float` and `complex` are interpreted + as `int | float` and `int | float | complex`, respectively. ```py -from knot_extensions import is_subtype_of, static_assert +from knot_extensions import is_subtype_of, static_assert, TypeOf + +type JustFloat = TypeOf[1.0] +type JustComplex = TypeOf[1j] static_assert(is_subtype_of(bool, bool)) static_assert(is_subtype_of(bool, int)) @@ -30,8 +34,8 @@ static_assert(not is_subtype_of(int, bool)) static_assert(not is_subtype_of(int, str)) static_assert(not is_subtype_of(object, int)) -static_assert(not is_subtype_of(int, float)) -static_assert(not is_subtype_of(int, complex)) +static_assert(not is_subtype_of(int, JustFloat)) +static_assert(not is_subtype_of(int, JustComplex)) static_assert(is_subtype_of(TypeError, Exception)) static_assert(is_subtype_of(FloatingPointError, Exception)) @@ -79,7 +83,9 @@ static_assert(is_subtype_of(C, object)) ```py from typing_extensions import Literal, LiteralString -from knot_extensions import is_subtype_of, static_assert +from knot_extensions import is_subtype_of, static_assert, TypeOf + +type JustFloat = TypeOf[1.0] # Boolean literals static_assert(is_subtype_of(Literal[True], bool)) @@ -92,8 +98,7 @@ static_assert(is_subtype_of(Literal[1], object)) static_assert(not is_subtype_of(Literal[1], bool)) -# See the note above (or link below) concerning int and float/complex -static_assert(not is_subtype_of(Literal[1], float)) +static_assert(not is_subtype_of(Literal[1], JustFloat)) # String literals static_assert(is_subtype_of(Literal["foo"], LiteralString)) diff --git a/crates/red_knot_python_semantic/resources/mdtest/union_types.md b/crates/red_knot_python_semantic/resources/mdtest/union_types.md index a215a6cff2..44d4d93d1d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/union_types.md +++ b/crates/red_knot_python_semantic/resources/mdtest/union_types.md @@ -70,11 +70,11 @@ from typing import Literal def _( u1: (int | str) | bytes, u2: int | (str | bytes), - u3: int | (str | (bytes | complex)), + u3: int | (str | (bytes | bytearray)), ) -> None: reveal_type(u1) # revealed: int | str | bytes reveal_type(u2) # revealed: int | str | bytes - reveal_type(u3) # revealed: int | str | bytes | complex + reveal_type(u3) # revealed: int | str | bytes | bytearray ``` ## Simplification using subtyping diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 95c6d5abd8..4e932798b9 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1765,6 +1765,7 @@ impl<'db> Type<'db> { | KnownClass::Type | KnownClass::Int | KnownClass::Float + | KnownClass::Complex | KnownClass::Str | KnownClass::List | KnownClass::Tuple @@ -2433,6 +2434,31 @@ impl<'db> Type<'db> { db: &'db dyn Db, ) -> Result, InvalidTypeExpressionError<'db>> { match self { + // Special cases for `float` and `complex` + // https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex + Type::ClassLiteral(ClassLiteralType { class }) + if class.is_known(db, KnownClass::Float) => + { + Ok(UnionType::from_elements( + db, + [ + KnownClass::Int.to_instance(db), + KnownClass::Float.to_instance(db), + ], + )) + } + Type::ClassLiteral(ClassLiteralType { class }) + if class.is_known(db, KnownClass::Complex) => + { + Ok(UnionType::from_elements( + db, + [ + KnownClass::Int.to_instance(db), + KnownClass::Float.to_instance(db), + KnownClass::Complex.to_instance(db), + ], + )) + } // In a type expression, a bare `type` is interpreted as "instance of `type`", which is // equivalent to `type[object]`. Type::ClassLiteral(_) | Type::SubclassOf(_) => Ok(self.to_instance(db)), @@ -2808,6 +2834,7 @@ pub enum KnownClass { Type, Int, Float, + Complex, Str, List, Tuple, @@ -2853,6 +2880,7 @@ impl<'db> KnownClass { Self::Tuple => "tuple", Self::Int => "int", Self::Float => "float", + Self::Complex => "complex", Self::FrozenSet => "frozenset", Self::Str => "str", Self::Set => "set", @@ -2922,6 +2950,7 @@ impl<'db> KnownClass { | Self::Type | Self::Int | Self::Float + | Self::Complex | Self::Str | Self::List | Self::Tuple @@ -2971,6 +3000,7 @@ impl<'db> KnownClass { | Self::Tuple | Self::Int | Self::Float + | Self::Complex | Self::Str | Self::Set | Self::FrozenSet @@ -3007,6 +3037,7 @@ impl<'db> KnownClass { "type" => Self::Type, "int" => Self::Int, "float" => Self::Float, + "complex" => Self::Complex, "str" => Self::Str, "set" => Self::Set, "frozenset" => Self::FrozenSet, @@ -3046,6 +3077,7 @@ impl<'db> KnownClass { | Self::Type | Self::Int | Self::Float + | Self::Complex | Self::Str | Self::List | Self::Tuple