Skip to content
11 changes: 11 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/annotations/any.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,19 @@ And `Any` cannot be used in `isinstance()` checks:
isinstance("", Any)
```

The same applies when `Any` is nested inside a tuple, including non-literal tuples:

```py
isinstance("", (int, Any)) # error: [invalid-argument-type]
isinstance("", (int, (str, Any))) # error: [invalid-argument-type]
classes = (int, Any)
isinstance("", classes) # error: [invalid-argument-type]
```

But `issubclass()` checks are fine:

```py
issubclass(object, Any) # no error!
issubclass(object, (int, Any)) # no error!
issubclass(object, (int, (str, Any))) # no error!
```
8 changes: 8 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/call/builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ isinstance("", t.Callable | t.Deque)
# `Any` is valid in `issubclass()` calls but not `isinstance()` calls
issubclass(list, t.Any)
issubclass(list, t.Any | t.Dict)

# The same works in tuples
isinstance("", (int, t.Dict))
isinstance("", (int, t.Callable))
issubclass(list, (int, t.Any))
```

But for other special forms that are not permitted as the second argument, we still emit an error:
Expand All @@ -173,6 +178,9 @@ isinstance("", t.TypeGuard) # error: [invalid-argument-type]
isinstance("", t.ClassVar) # error: [invalid-argument-type]
isinstance("", t.Final) # error: [invalid-argument-type]
isinstance("", t.Any) # error: [invalid-argument-type]

# The same applies when `Any` is nested inside a tuple
isinstance("", (int, t.Any)) # error: [invalid-argument-type]
```

## The builtin `NotImplemented` constant is not callable
Expand Down
44 changes: 44 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,41 @@ def _(x: int | list[int] | bytes):
reveal_type(x) # revealed: int | list[int] | bytes
```

The same validation also applies when an invalid `UnionType` is nested inside a tuple:

```py
def _(x: int | list[int] | bytes):
# error: [invalid-argument-type]
if isinstance(x, (int, list[int] | bytes)):
reveal_type(x) # revealed: int | list[int] | bytes
else:
reveal_type(x) # revealed: int | list[int] | bytes
```

Including nested tuples:

```py
def _(x: int | list[int] | bytes):
# error: [invalid-argument-type]
if isinstance(x, (int, (str, list[int] | bytes))):
reveal_type(x) # revealed: int | list[int] | bytes
else:
reveal_type(x) # revealed: int | list[int] | bytes
```

And non-literal tuples:

```py
classes = (int, list[int] | bytes)

def _(x: int | list[int] | bytes):
# error: [invalid-argument-type]
if isinstance(x, classes):
reveal_type(x) # revealed: int | list[int] | bytes
else:
reveal_type(x) # revealed: int | list[int] | bytes
```

## PEP-604 unions on Python \<3.10

PEP-604 unions were added in Python 3.10, so attempting to use them on Python 3.9 does not lead to
Expand Down Expand Up @@ -312,6 +347,15 @@ def _(flag: bool):
reveal_type(x) # revealed: Literal[1, "a"]
```

## Splatted calls with invalid `classinfo`

Diagnostics are still emitted for invalid `classinfo` types when the arguments are splatted:

```py
args = (object(), int | list[str])
isinstance(*args) # error: [invalid-argument-type]
```

## Generic aliases are not supported as second argument

The `classinfo` argument cannot be a generic alias:
Expand Down
35 changes: 35 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,41 @@ def _(x: type[int | list | bytes]):
reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
```

The same validation also applies when an invalid `UnionType` is nested inside a tuple:

```py
def _(x: type[int | list | bytes]):
# error: [invalid-argument-type]
if issubclass(x, (int, list[int] | bytes)):
reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
else:
reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
```

Including nested tuples:

```py
def _(x: type[int | list | bytes]):
# error: [invalid-argument-type]
if issubclass(x, (int, (str, list[int] | bytes))):
reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
else:
reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
```

And non-literal tuples:

```py
classes = (int, list[int] | bytes)

def _(x: type[int | list | bytes]):
# error: [invalid-argument-type]
if issubclass(x, classes):
reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
else:
reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
```

## PEP-604 unions on Python \<3.10

PEP-604 unions were added in Python 3.10, so attempting to use them on Python 3.9 does not lead to
Expand Down
39 changes: 38 additions & 1 deletion crates/ty_python_semantic/resources/mdtest/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -2601,6 +2601,41 @@ def f(arg1: type):
reveal_type(arg1) # revealed: type & ~type[OnlyClassmethodMembers]
```

The same diagnostics are also emitted when protocol classes appear inside a tuple passed as the
second argument to `isinstance()` or `issubclass()`:

```py
def g(arg: object, arg2: type):
isinstance(arg, (HasX, RuntimeCheckableHasX)) # error: [isinstance-against-protocol]
isinstance(arg, (HasX, int)) # error: [isinstance-against-protocol]

# error: [isinstance-against-protocol]
# error: [isinstance-against-protocol]
issubclass(arg2, (HasX, RuntimeCheckableHasX))

issubclass(arg2, (HasX, OnlyMethodMembers)) # error: [isinstance-against-protocol]
```

This includes nested tuples:

```py
def g2(arg: object, arg2: type):
isinstance(arg, (int, (HasX, str))) # error: [isinstance-against-protocol]

# error: [isinstance-against-protocol]
# error: [isinstance-against-protocol]
issubclass(arg2, (int, (HasX, RuntimeCheckableHasX)))
```

This also works when the tuple is not a literal in the source:

```py
classes = (HasX, int)

def h(arg: object):
isinstance(arg, classes) # error: [isinstance-against-protocol]
```

## Match class patterns and protocols

<!-- snapshot-diagnostics -->
Expand Down Expand Up @@ -3049,11 +3084,13 @@ static_assert(not is_disjoint_from(Proto, Nominal))
This snippet caused us to panic on an early version of the implementation for protocols.

```py
from typing import Protocol
from typing import Protocol, runtime_checkable

@runtime_checkable
class A(Protocol):
def x(self) -> "B | A": ...

@runtime_checkable
class B(Protocol):
def y(self): ...

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md
12 | reveal_type(x) # revealed: int | list[int] | bytes
13 | else:
14 | reveal_type(x) # revealed: int | list[int] | bytes
15 | def _(x: int | list[int] | bytes):
16 | # error: [invalid-argument-type]
17 | if isinstance(x, (int, list[int] | bytes)):
18 | reveal_type(x) # revealed: int | list[int] | bytes
19 | else:
20 | reveal_type(x) # revealed: int | list[int] | bytes
21 | def _(x: int | list[int] | bytes):
22 | # error: [invalid-argument-type]
23 | if isinstance(x, (int, (str, list[int] | bytes))):
24 | reveal_type(x) # revealed: int | list[int] | bytes
25 | else:
26 | reveal_type(x) # revealed: int | list[int] | bytes
27 | classes = (int, list[int] | bytes)
28 |
29 | def _(x: int | list[int] | bytes):
30 | # error: [invalid-argument-type]
31 | if isinstance(x, classes):
32 | reveal_type(x) # revealed: int | list[int] | bytes
33 | else:
34 | reveal_type(x) # revealed: int | list[int] | bytes
```

# Diagnostics
Expand Down Expand Up @@ -87,3 +107,58 @@ info: Element `<special-form 'typing.Any'>` in the union, and 2 more elements, a
info: rule `invalid-argument-type` is enabled by default

```

```
error[invalid-argument-type]: Invalid second argument to `isinstance`
--> src/mdtest_snippet.py:17:8
|
15 | def _(x: int | list[int] | bytes):
16 | # error: [invalid-argument-type]
17 | if isinstance(x, (int, list[int] | bytes)):
| ^^^^^^^^^^^^^^^^^^^^-----------------^^
| |
| This `UnionType` instance contains non-class elements
18 | reveal_type(x) # revealed: int | list[int] | bytes
19 | else:
|
info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects
info: Element `<class 'list[int]'>` in the union is not a class object
info: rule `invalid-argument-type` is enabled by default

```

```
error[invalid-argument-type]: Invalid second argument to `isinstance`
--> src/mdtest_snippet.py:23:8
|
21 | def _(x: int | list[int] | bytes):
22 | # error: [invalid-argument-type]
23 | if isinstance(x, (int, (str, list[int] | bytes))):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^-----------------^^^
| |
| This `UnionType` instance contains non-class elements
24 | reveal_type(x) # revealed: int | list[int] | bytes
25 | else:
|
info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects
info: Element `<class 'list[int]'>` in the union is not a class object
info: rule `invalid-argument-type` is enabled by default

```

```
error[invalid-argument-type]: Invalid second argument to `isinstance`
--> src/mdtest_snippet.py:31:8
|
29 | def _(x: int | list[int] | bytes):
30 | # error: [invalid-argument-type]
31 | if isinstance(x, classes):
| ^^^^^^^^^^^^^^^^^^^^^^
32 | reveal_type(x) # revealed: int | list[int] | bytes
33 | else:
|
info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects
info: Element `<class 'list[int]'>` in the union `list[int] | bytes` is not a class object
info: rule `invalid-argument-type` is enabled by default

```
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,32 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md
## mdtest_snippet.py

```
1 | def _(x: type[int | list | bytes]):
2 | # error: [invalid-argument-type]
3 | if issubclass(x, int | list[int]):
4 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
5 | else:
6 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
1 | def _(x: type[int | list | bytes]):
2 | # error: [invalid-argument-type]
3 | if issubclass(x, int | list[int]):
4 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
5 | else:
6 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
7 | def _(x: type[int | list | bytes]):
8 | # error: [invalid-argument-type]
9 | if issubclass(x, (int, list[int] | bytes)):
10 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
11 | else:
12 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
13 | def _(x: type[int | list | bytes]):
14 | # error: [invalid-argument-type]
15 | if issubclass(x, (int, (str, list[int] | bytes))):
16 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
17 | else:
18 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
19 | classes = (int, list[int] | bytes)
20 |
21 | def _(x: type[int | list | bytes]):
22 | # error: [invalid-argument-type]
23 | if issubclass(x, classes):
24 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
25 | else:
26 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
```

# Diagnostics
Expand All @@ -41,3 +61,58 @@ info: Element `<class 'list[int]'>` in the union is not a class object
info: rule `invalid-argument-type` is enabled by default

```

```
error[invalid-argument-type]: Invalid second argument to `issubclass`
--> src/mdtest_snippet.py:9:8
|
7 | def _(x: type[int | list | bytes]):
8 | # error: [invalid-argument-type]
9 | if issubclass(x, (int, list[int] | bytes)):
| ^^^^^^^^^^^^^^^^^^^^-----------------^^
| |
| This `UnionType` instance contains non-class elements
10 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
11 | else:
|
info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects
info: Element `<class 'list[int]'>` in the union is not a class object
info: rule `invalid-argument-type` is enabled by default

```

```
error[invalid-argument-type]: Invalid second argument to `issubclass`
--> src/mdtest_snippet.py:15:8
|
13 | def _(x: type[int | list | bytes]):
14 | # error: [invalid-argument-type]
15 | if issubclass(x, (int, (str, list[int] | bytes))):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^-----------------^^^
| |
| This `UnionType` instance contains non-class elements
16 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
17 | else:
|
info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects
info: Element `<class 'list[int]'>` in the union is not a class object
info: rule `invalid-argument-type` is enabled by default

```

```
error[invalid-argument-type]: Invalid second argument to `issubclass`
--> src/mdtest_snippet.py:23:8
|
21 | def _(x: type[int | list | bytes]):
22 | # error: [invalid-argument-type]
23 | if issubclass(x, classes):
| ^^^^^^^^^^^^^^^^^^^^^^
24 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
25 | else:
|
info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects
info: Element `<class 'list[int]'>` in the union `list[int] | bytes` is not a class object
info: rule `invalid-argument-type` is enabled by default

```
Loading