-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Don't special-case class instances in binary expression inference #15161
base: main
Are you sure you want to change the base?
Changes from 10 commits
c8c99f6
58ada96
029932d
3b1622f
33409ba
9fb3013
f4a65c2
a5f0dad
5239fa9
a52486f
dffd23d
fe73357
a70fc31
0acdbe4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Binary operations on classes | ||
|
||
## Union of two classes | ||
|
||
Unioning two classes via the `|` operator is only available in Python 3.10 and later. | ||
|
||
```toml | ||
[environment] | ||
python-version = "3.10" | ||
``` | ||
|
||
```py | ||
class A: ... | ||
class B: ... | ||
|
||
reveal_type(A | B) # revealed: Literal[A, B] | ||
``` | ||
|
||
## Union of two classes (prior to 3.10) | ||
|
||
```py | ||
class A: ... | ||
class B: ... | ||
|
||
# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`" | ||
reveal_type(A | B) # revealed: Unknown | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
# Custom binary operations | ||
|
||
## Class instances | ||
|
||
```py | ||
class Yes: | ||
def __add__(self, other) -> Literal["+"]: | ||
return "+" | ||
|
||
def __sub__(self, other) -> Literal["-"]: | ||
return "-" | ||
|
||
def __mul__(self, other) -> Literal["*"]: | ||
return "*" | ||
|
||
def __matmul__(self, other) -> Literal["@"]: | ||
return "@" | ||
|
||
def __truediv__(self, other) -> Literal["/"]: | ||
return "/" | ||
|
||
def __mod__(self, other) -> Literal["%"]: | ||
return "%" | ||
|
||
def __pow__(self, other) -> Literal["**"]: | ||
return "**" | ||
|
||
def __lshift__(self, other) -> Literal["<<"]: | ||
return "<<" | ||
|
||
def __rshift__(self, other) -> Literal[">>"]: | ||
return ">>" | ||
|
||
def __or__(self, other) -> Literal["|"]: | ||
return "|" | ||
|
||
def __xor__(self, other) -> Literal["^"]: | ||
return "^" | ||
|
||
def __and__(self, other) -> Literal["&"]: | ||
return "&" | ||
|
||
def __floordiv__(self, other) -> Literal["//"]: | ||
return "//" | ||
|
||
class Sub(Yes): ... | ||
class No: ... | ||
|
||
# Yes implements all of the dunder methods. | ||
reveal_type(Yes() + Yes()) # revealed: Literal["+"] | ||
reveal_type(Yes() - Yes()) # revealed: Literal["-"] | ||
reveal_type(Yes() * Yes()) # revealed: Literal["*"] | ||
reveal_type(Yes() @ Yes()) # revealed: Literal["@"] | ||
reveal_type(Yes() / Yes()) # revealed: Literal["/"] | ||
reveal_type(Yes() % Yes()) # revealed: Literal["%"] | ||
reveal_type(Yes() ** Yes()) # revealed: Literal["**"] | ||
reveal_type(Yes() << Yes()) # revealed: Literal["<<"] | ||
reveal_type(Yes() >> Yes()) # revealed: Literal[">>"] | ||
reveal_type(Yes() | Yes()) # revealed: Literal["|"] | ||
reveal_type(Yes() ^ Yes()) # revealed: Literal["^"] | ||
reveal_type(Yes() & Yes()) # revealed: Literal["&"] | ||
reveal_type(Yes() // Yes()) # revealed: Literal["//"] | ||
|
||
# Sub inherits Yes's implementation of the dunder methods. | ||
reveal_type(Sub() + Sub()) # revealed: Literal["+"] | ||
reveal_type(Sub() - Sub()) # revealed: Literal["-"] | ||
reveal_type(Sub() * Sub()) # revealed: Literal["*"] | ||
reveal_type(Sub() @ Sub()) # revealed: Literal["@"] | ||
reveal_type(Sub() / Sub()) # revealed: Literal["/"] | ||
reveal_type(Sub() % Sub()) # revealed: Literal["%"] | ||
reveal_type(Sub() ** Sub()) # revealed: Literal["**"] | ||
reveal_type(Sub() << Sub()) # revealed: Literal["<<"] | ||
reveal_type(Sub() >> Sub()) # revealed: Literal[">>"] | ||
reveal_type(Sub() | Sub()) # revealed: Literal["|"] | ||
reveal_type(Sub() ^ Sub()) # revealed: Literal["^"] | ||
reveal_type(Sub() & Sub()) # revealed: Literal["&"] | ||
reveal_type(Sub() // Sub()) # revealed: Literal["//"] | ||
|
||
# No does not implement any of the dunder methods. | ||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() + No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() - No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() * No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() @ No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() / No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() % No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() ** No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() << No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() >> No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() | No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() ^ No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() & No()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`" | ||
reveal_type(No() // No()) # revealed: Unknown | ||
|
||
# Yes does not implement any of the reflected dunder methods. | ||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() + Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() - Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() * Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() @ Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() / Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() % Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() ** Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() << Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() >> Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() | Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() ^ Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() & Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() // Yes()) # revealed: Unknown | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels like we are missing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't sure if that would be needed, since So I think what I want is another stanza, where |
||
|
||
## Classes | ||
|
||
Dunder methods defined in a class are available to instances of that class, but not to the class | ||
itself. (For these operators to work on the class itself, they would have to be defined on the | ||
class's type, i.e. `type`.) | ||
|
||
```py | ||
class Yes: | ||
def __add__(self, other) -> Literal["+"]: | ||
return "+" | ||
|
||
class Sub(Yes): ... | ||
class No: ... | ||
|
||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`" | ||
reveal_type(Yes + Yes) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`" | ||
reveal_type(Sub + Sub) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`" | ||
reveal_type(No + No) # revealed: Unknown | ||
``` | ||
|
||
## Function literals | ||
|
||
```py | ||
def f(): | ||
pass | ||
|
||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f + f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f - f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f * f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f @ f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f / f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f % f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f**f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f << f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f >> f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f | f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f ^ f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f & f) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `Literal[f]` and `Literal[f]`" | ||
reveal_type(f // f) # revealed: Unknown | ||
``` | ||
|
||
## Subclass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels very closely related to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
```py | ||
class Yes: | ||
def __add__(self, other) -> Literal["+"]: | ||
return "+" | ||
|
||
class Sub(Yes): ... | ||
class No: ... | ||
|
||
def yes() -> type[Yes]: | ||
return Yes | ||
|
||
def sub() -> type[Sub]: | ||
return Sub | ||
|
||
def no() -> type[No]: | ||
return No | ||
|
||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`" | ||
reveal_type(yes() + yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`" | ||
reveal_type(sub() + sub()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`" | ||
reveal_type(no() + no()) # revealed: Unknown | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,34 @@ reveal_type(3 * -1) # revealed: Literal[-3] | |
reveal_type(-3 // 3) # revealed: Literal[-1] | ||
reveal_type(-3 / 3) # revealed: float | ||
reveal_type(5 % 3) # revealed: Literal[2] | ||
|
||
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared | ||
# formal parameter type. | ||
reveal_type(2 + "f") # revealed: int | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test produced a |
||
|
||
def lhs(x: int): | ||
reveal_type(x + 1) # revealed: 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 | ||
|
||
def rhs(x: int): | ||
reveal_type(2 + x) # revealed: 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(5 % x) # revealed: int | ||
|
||
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: int | ||
reveal_type(x / x) # revealed: float | ||
reveal_type(x % x) # revealed: int | ||
``` | ||
|
||
## Power | ||
|
@@ -21,6 +49,11 @@ largest_u32 = 4_294_967_295 | |
reveal_type(2**2) # revealed: Literal[4] | ||
reveal_type(1 ** (largest_u32 + 1)) # revealed: int | ||
reveal_type(2**largest_u32) # revealed: int | ||
|
||
def variable(x: int): | ||
reveal_type(x**2) # revealed: @Todo(return type) | ||
reveal_type(2**x) # revealed: @Todo(return type) | ||
reveal_type(x**x) # revealed: @Todo(return type) | ||
``` | ||
|
||
## Division by Zero | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think
Literal[A, B]
is the right type to assign to the value expressionA | B
.In a type expression,
A | B
spells the typeType::Union[Type::Instance(A), Type::Instance(B)]
-- that is, the type consisting of all instances ofA
(or any subclass) and all instances ofB
(or any subclass).The value expression
A if flag else B
would correctly be assigned the typeLiteral[A, B]
, since its runtime value must either be the class objectA
or the class objectB
.But the runtime value of the value expression
A | B
is not either of those, so its type can't beLiteral[A, B]
. Its runtime value is an instance oftypes.UnionType
, which can't be treated as if it were a class object at all. So the type we should assign to it isType::Instance(types.UnionType)
.If we did any special-casing here, the correct special casing would be to create a custom type representation for
types.UnionType
so we can record the fact that its__args__
attribute (in this case) has the typetuple[Literal[A], Literal[B]]
. But I don't think this is worth doing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted the special case, this is now returning
UnionType
again as before