From bb430859391e1a07cf65de6d85e0eb6112bd2fff Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 19 Dec 2024 15:54:01 +0000 Subject: [PATCH] [red-knot] Reduce TODOs in `Type::member()` (#15066) --- .../resources/mdtest/attributes.md | 68 ++++++++++++++++++- crates/red_knot_python_semantic/src/types.rs | 62 +++++++++++------ 2 files changed, 110 insertions(+), 20 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 27e093e618..c79cff9509 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1,4 +1,6 @@ -# Class attributes +# Attributes + +Tests for attribute access on various kinds of types. ## Union of attributes @@ -155,3 +157,67 @@ class Foo: ... reveal_type(Foo.__class__) # revealed: Literal[type] ``` + +## Function-literal attributes + +Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all +functions are instances of that class: + +```py path=a.py +def f(): ... + +reveal_type(f.__defaults__) # revealed: @Todo(instance attributes) +reveal_type(f.__kwdefaults__) # revealed: @Todo(instance attributes) +``` + +Some attributes are special-cased, however: + +```py path=b.py +def f(): ... + +reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions) +reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions) +``` + +## Int-literal attributes + +Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal +integers are instances of that class: + +```py path=a.py +reveal_type((2).bit_length) # revealed: @Todo(instance attributes) +reveal_type((2).denominator) # revealed: @Todo(instance attributes) +``` + +Some attributes are special-cased, however: + +```py path=b.py +reveal_type((2).numerator) # revealed: Literal[2] +reveal_type((2).real) # revealed: Literal[2] +``` + +## Literal `bool` attributes + +Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal +bols are instances of that class: + +```py path=a.py +reveal_type(True.__and__) # revealed: @Todo(instance attributes) +reveal_type(False.__or__) # revealed: @Todo(instance attributes) +``` + +Some attributes are special-cased, however: + +```py path=b.py +reveal_type(True.numerator) # revealed: Literal[1] +reveal_type(False.real) # revealed: Literal[0] +``` + +## Bytes-literal attributes + +All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`: + +```py +reveal_type(b"foo".join) # revealed: @Todo(instance attributes) +reveal_type(b"foo".endswith) # revealed: @Todo(instance attributes) +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 4c39197f12..ab91ae56e3 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1459,10 +1459,15 @@ impl<'db> Type<'db> { match self { Type::Any | Type::Unknown | Type::Todo(_) => self.into(), + Type::Never => todo_type!("attribute lookup on Never").into(), - Type::FunctionLiteral(_) => { - todo_type!("Attribute access on `FunctionLiteral` types").into() - } + + Type::FunctionLiteral(_) => match name { + "__get__" => todo_type!("`__get__` method on functions").into(), + "__call__" => todo_type!("`__call__` method on functions").into(), + _ => KnownClass::FunctionType.to_instance(db).member(db, name), + }, + Type::ModuleLiteral(module_ref) => { // `__dict__` is a very special member that is never overridden by module globals; // we should always look it up directly as an attribute on `types.ModuleType`, @@ -1522,9 +1527,13 @@ impl<'db> Type<'db> { global_lookup } } + Type::ClassLiteral(class_ty) => class_ty.member(db, name), + Type::SubclassOf(subclass_of_ty) => subclass_of_ty.member(db, name), + Type::KnownInstance(known_instance) => known_instance.member(db, name), + Type::Instance(InstanceType { class }) => { let ty = match (class.known(db), name) { (Some(KnownClass::VersionInfo), "major") => { @@ -1538,6 +1547,7 @@ impl<'db> Type<'db> { }; ty.into() } + Type::Union(union) => { let mut builder = UnionBuilder::new(db); @@ -1573,41 +1583,55 @@ impl<'db> Type<'db> { ) } } + Type::Intersection(_) => { // TODO perform the get_member on each type in the intersection // TODO return the intersection of those results todo_type!("Attribute access on `Intersection` types").into() } - Type::IntLiteral(_) => todo_type!("Attribute access on `IntLiteral` types").into(), - Type::BooleanLiteral(_) => { - todo_type!("Attribute access on `BooleanLiteral` types").into() - } + + Type::IntLiteral(_) => match name { + "real" | "numerator" => self.into(), + // TODO more attributes could probably be usefully special-cased + _ => KnownClass::Int.to_instance(db).member(db, name), + }, + + Type::BooleanLiteral(bool_value) => match name { + "real" | "numerator" => Type::IntLiteral(i64::from(*bool_value)).into(), + _ => KnownClass::Bool.to_instance(db).member(db, name), + }, + Type::StringLiteral(_) => { // TODO defer to `typing.LiteralString`/`builtins.str` methods // from typeshed's stubs todo_type!("Attribute access on `StringLiteral` types").into() } + Type::LiteralString => { // TODO defer to `typing.LiteralString`/`builtins.str` methods // from typeshed's stubs todo_type!("Attribute access on `LiteralString` types").into() } - Type::BytesLiteral(_) => { - // TODO defer to Type::Instance().member - todo_type!("Attribute access on `BytesLiteral` types").into() - } - Type::SliceLiteral(_) => { - // TODO defer to `builtins.slice` methods - todo_type!("Attribute access on `SliceLiteral` types").into() - } + + Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).member(db, name), + + // We could plausibly special-case `start`, `step`, and `stop` here, + // but it doesn't seem worth the complexity given the very narrow range of places + // where we infer `SliceLiteral` types. + Type::SliceLiteral(_) => KnownClass::Slice.to_instance(db).member(db, name), + Type::Tuple(_) => { // TODO: implement tuple methods todo_type!("Attribute access on heterogeneous tuple types").into() } - Type::AlwaysTruthy | Type::AlwaysFalsy => { - // TODO return `Callable[[], Literal[True/False]]` for `__bool__` access - KnownClass::Object.to_instance(db).member(db, name) - } + + Type::AlwaysTruthy | Type::AlwaysFalsy => match name { + "__bool__" => { + // TODO should be `Callable[[], Literal[True/False]]` + todo_type!("`__bool__` for `AlwaysTruthy`/`AlwaysFalsy` Type variants").into() + } + _ => KnownClass::Object.to_instance(db).member(db, name), + }, } }