Skip to content

Commit

Permalink
Don't special-case class instances in unary expression inference (ast…
Browse files Browse the repository at this point in the history
…ral-sh#15045)

We have a handy `to_meta_type` that does the right thing for class
instances, and also works for all of the other types that are “instances
of” something. Unless I'm missing something, this should let us get rid
of the catch-all clause in one fell swoop.

cf astral-sh#14548
  • Loading branch information
dcreager authored Dec 18, 2024
1 parent ed2bce6 commit 2802cbd
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 13 deletions.
165 changes: 165 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/unary/custom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Custom unary operations

## Class instances

```py
class Yes:
def __pos__(self) -> bool:
return False

def __neg__(self) -> str:
return "negative"

def __invert__(self) -> int:
return 17

class Sub(Yes): ...
class No: ...

reveal_type(+Yes()) # revealed: bool
reveal_type(-Yes()) # revealed: str
reveal_type(~Yes()) # revealed: int

reveal_type(+Sub()) # revealed: bool
reveal_type(-Sub()) # revealed: str
reveal_type(~Sub()) # revealed: int

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `No`"
reveal_type(+No()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `No`"
reveal_type(-No()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `No`"
reveal_type(~No()) # revealed: Unknown
```

## Classes

```py
class Yes:
def __pos__(self) -> bool:
return False

def __neg__(self) -> str:
return "negative"

def __invert__(self) -> int:
return 17

class Sub(Yes): ...
class No: ...

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Yes]`"
reveal_type(+Yes) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Yes]`"
reveal_type(-Yes) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Yes]`"
reveal_type(~Yes) # revealed: Unknown

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[Sub]`"
reveal_type(+Sub) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[Sub]`"
reveal_type(-Sub) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[Sub]`"
reveal_type(~Sub) # revealed: Unknown

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`"
reveal_type(+No) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`"
reveal_type(-No) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`"
reveal_type(~No) # revealed: Unknown
```

## Function literals

```py
def f():
pass

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[f]`"
reveal_type(+f) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[f]`"
reveal_type(-f) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[f]`"
reveal_type(~f) # revealed: Unknown
```

## Subclass

```py
class Yes:
def __pos__(self) -> bool:
return False

def __neg__(self) -> str:
return "negative"

def __invert__(self) -> int:
return 17

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] "Unary operator `+` is unsupported for type `type[Yes]`"
reveal_type(+yes()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Yes]`"
reveal_type(-yes()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Yes]`"
reveal_type(~yes()) # revealed: Unknown

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[Sub]`"
reveal_type(+sub()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[Sub]`"
reveal_type(-sub()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[Sub]`"
reveal_type(~sub()) # revealed: Unknown

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `type[No]`"
reveal_type(+no()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `type[No]`"
reveal_type(-no()) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `type[No]`"
reveal_type(~no()) # revealed: Unknown
```

## Metaclass

```py
class Meta(type):
def __pos__(self) -> bool:
return False

def __neg__(self) -> str:
return "negative"

def __invert__(self) -> int:
return 17

class Yes(metaclass=Meta): ...
class Sub(Yes): ...
class No: ...

reveal_type(+Yes) # revealed: bool
reveal_type(-Yes) # revealed: str
reveal_type(~Yes) # revealed: int

reveal_type(+Sub) # revealed: bool
reveal_type(-Sub) # revealed: str
reveal_type(~Sub) # revealed: int

# error: [unsupported-operator] "Unary operator `+` is unsupported for type `Literal[No]`"
reveal_type(+No) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `-` is unsupported for type `Literal[No]`"
reveal_type(-No) # revealed: Unknown
# error: [unsupported-operator] "Unary operator `~` is unsupported for type `Literal[No]`"
reveal_type(~No) # revealed: Unknown
```
41 changes: 28 additions & 13 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, todo_type,
typing_extensions_symbol, Boundness, Class, ClassLiteralType, FunctionType, InstanceType,
IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction,
KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, Symbol,
Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay, TypeVarBoundOrConstraints,
TypeVarInstance, UnionBuilder, UnionType,
typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType, FunctionType,
InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass,
KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType,
Symbol, Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay,
TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
};
use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
Expand Down Expand Up @@ -3201,6 +3201,11 @@ impl<'db> TypeInferenceBuilder<'db> {
let operand_type = self.infer_expression(operand);

match (op, operand_type) {
(_, Type::Any) => Type::Any,
(_, Type::Todo(_)) => operand_type,
(_, Type::Never) => Type::Never,
(_, Type::Unknown) => Type::Unknown,

(UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value),
(UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value),
(UnaryOp::Invert, Type::IntLiteral(value)) => Type::IntLiteral(!value),
Expand All @@ -3210,11 +3215,23 @@ impl<'db> TypeInferenceBuilder<'db> {
(UnaryOp::Invert, Type::BooleanLiteral(bool)) => Type::IntLiteral(!i64::from(bool)),

(UnaryOp::Not, ty) => ty.bool(self.db()).negate().into_type(self.db()),
(_, Type::Any) => Type::Any,
(_, Type::Unknown) => Type::Unknown,
(
op @ (UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert),
Type::Instance(InstanceType { class }),
Type::FunctionLiteral(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
| Type::SubclassOf(_)
| Type::Instance(_)
| Type::KnownInstance(_)
| Type::Union(_)
| Type::Intersection(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::StringLiteral(_)
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_),
) => {
let unary_dunder_method = match op {
UnaryOp::Invert => "__invert__",
Expand All @@ -3225,11 +3242,10 @@ impl<'db> TypeInferenceBuilder<'db> {
}
};

if let Symbol::Type(class_member, _) =
class.class_member(self.db(), unary_dunder_method)
if let CallDunderResult::CallOutcome(call)
| CallDunderResult::PossiblyUnbound(call) =
operand_type.call_dunder(self.db(), unary_dunder_method, &[operand_type])
{
let call = class_member.call(self.db(), &[operand_type]);

match call.return_ty_result(&self.context, AnyNodeRef::ExprUnaryOp(unary)) {
Ok(t) => t,
Err(e) => {
Expand Down Expand Up @@ -3257,7 +3273,6 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown
}
}
_ => todo_type!(), // TODO other unary op types
}
}

Expand Down

0 comments on commit 2802cbd

Please sign in to comment.