From 5e87238f3391fe449d609dadab8f7e1405947853 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 13 Dec 2025 17:04:13 -0500 Subject: [PATCH] Add __doc__ --- .../mdtest/scopes/class_implicit_attrs.md | 5 ++-- crates/ty_python_semantic/src/place.rs | 11 +++++++-- .../src/types/infer/builder.rs | 24 +++++++++---------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md index cf6ff80d71..7130538acf 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md @@ -2,14 +2,15 @@ ## Class body implicit attributes -Python makes certain names available implicitly inside class body scopes. These are `__qualname__` -and `__module__`, as documented at +Python makes certain names available implicitly inside class body scopes. These are `__qualname__`, +`__module__`, and `__doc__`, as documented at . ```py class Foo: reveal_type(__qualname__) # revealed: str reveal_type(__module__) # revealed: str + reveal_type(__doc__) # revealed: str | None ``` ## `__firstlineno__` (Python 3.13+) diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 45bce10c18..21dfb955a9 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1637,8 +1637,8 @@ mod implicit_globals { /// Looks up the type of an "implicit class body symbol". Returns [`Place::Undefined`] if /// `name` is not present as an implicit symbol in class bodies. /// -/// Implicit class body symbols are symbols such as `__qualname__`, `__module__`, and -/// `__firstlineno__` that Python implicitly makes available inside a class body during +/// Implicit class body symbols are symbols such as `__qualname__`, `__module__`, `__doc__`, +/// and `__firstlineno__` that Python implicitly makes available inside a class body during /// class creation. /// /// See @@ -1649,6 +1649,13 @@ pub(crate) fn class_body_implicit_symbol<'db>( match name { "__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(), "__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(), + // __doc__ is `str` if there's a docstring, `None` if there isn't + "__doc__" => Place::bound(UnionType::from_elements( + db, + [KnownClass::Str.to_instance(db), Type::none(db)], + )) + .into(), + // __firstlineno__ was added in Python 3.13 "__firstlineno__" if Program::get(db).python_version(db) >= PythonVersion::PY313 => { Place::bound(KnownClass::Int.to_instance(db)).into() } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 8a43d06ac3..75286a7a7b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -9173,18 +9173,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If we're in a class body, check for implicit class body symbols first. // These take precedence over globals. .or_fall_back_to(db, || { - if scope.node(db).scope_kind().is_class() { - if let Some(symbol) = place_expr.as_symbol() { - let implicit = class_body_implicit_symbol(db, symbol.name()); - if implicit.place.is_definitely_bound() { - return implicit.map_type(|ty| { - self.narrow_place_with_applicable_constraints( - place_expr, - ty, - &constraint_keys, - ) - }); - } + if scope.node(db).scope_kind().is_class() + && let Some(symbol) = place_expr.as_symbol() + { + let implicit = class_body_implicit_symbol(db, symbol.name()); + if implicit.place.is_definitely_bound() { + return implicit.map_type(|ty| { + self.narrow_place_with_applicable_constraints( + place_expr, + ty, + &constraint_keys, + ) + }); } } Place::Undefined.into()