From 9953dede9e84b18020283f1bbf6099d720518deb Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 20 Feb 2025 15:44:50 +0100 Subject: [PATCH] Attempt to model getattr_static on gradual types --- .../resources/mdtest/call/getattr_static.md | 26 +++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 28 +++++++++++++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md b/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md index 6fa5b718e4..01d252ff96 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md @@ -104,4 +104,30 @@ inspect.getattr_static(C(), 1) inspect.getattr_static(C(), "x", "default-arg", "one too many") ``` +## Possibly unbound attributes + +```py +import inspect + +def _(flag: bool): + class C: + if flag: + x: int = 1 + + reveal_type(inspect.getattr_static(C, "x", "default")) # revealed: int | Literal["default"] +``` + +## Gradual types + +```py +import inspect +from typing import Any + +def _(a: Any, tuple_of_any: tuple[Any]): + reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"] + + # TODO: Ideally, this would just be `Literal[index]` + reveal_type(inspect.getattr_static(tuple_of_any, "index", "default")) # revealed: Literal[index] | Literal["default"] +``` + [official documentation]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 888826531c..c69323a437 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -2037,11 +2037,29 @@ impl<'db> Type<'db> { default }; - let static_member = instance_ty - .static_member(db, attr_name.value(db)) - .ignore_possibly_unbound() // TODO: we could emit a diagnostic here (if default is not set) - .unwrap_or(default); - binding.set_return_type(static_member); + let union_with_default = |ty| UnionType::from_elements(db, [ty, default]); + + // TODO: we could emit a diagnostic here (if default is not set) + binding.set_return_type( + match instance_ty.static_member(db, attr_name.value(db)) { + Symbol::Type(ty, Boundness::Bound) => { + if instance_ty.is_fully_static(db) { + ty + } else { + // Here, we attempt to model the fact that an attribute lookup on + // a non-fully static type could fail. This is an approximation, + // as there are gradual types like `tuple[Any]`, on which a lookup + // of (e.g. of the `index` method) would always succeed. + + union_with_default(ty) + } + } + Symbol::Type(ty, Boundness::PossiblyUnbound) => { + union_with_default(ty) + } + Symbol::Unbound => default, + }, + ); } _ => {}