From efe668dc0b622363b0ab33f64533f4a7d037d4d1 Mon Sep 17 00:00:00 2001 From: devteamaegis Date: Thu, 13 Aug 2026 16:00:16 -0700 Subject: [PATCH] Don't resolve implicit `__bool__` through `__getattr__` Implicit dunder methods such as `__bool__` are looked up on the type and bypass `__getattr__` at runtime. When evaluating truthiness, Pyrefly resolved `__bool__` with the `__getattr__` fallback enabled, so a class whose `__getattr__` returns a non-callable type (e.g. `torch.nn.Module`, whose `__getattr__` is typed to return `Tensor | Module`) was treated as defining a non-callable `__bool__` and wrongly reported as `not-callable` in boolean contexts. Disable the `__getattr__` fallback when looking up `__bool__` in `as_bool`. Closes #4467 --- pyrefly/lib/alt/expr.rs | 8 ++++++-- pyrefly/lib/test/attributes.rs | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index be18b8eab4..76db512864 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -1974,9 +1974,13 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } ty.as_bool().or_else(|| { - // If the object defines `__bool__`, we can check if it returns a statically known value + // If the object defines `__bool__`, we can check if it returns a statically known value. + // Implicit dunder lookups (like the `__bool__` used for truthiness) are resolved on the + // type and do not go through `__getattr__`, so we disable the `__getattr__` fallback here. + // Otherwise a class with a `__getattr__` returning a non-callable type would be treated as + // defining a non-callable `__bool__` and wrongly reported as `not-callable` (see #4467). if self - .type_of_magic_dunder_attr(ty, &dunder::BOOL, range, errors, None, "as_bool", true)? + .type_of_magic_dunder_attr(ty, &dunder::BOOL, range, errors, None, "as_bool", false)? .is_never() { return None; diff --git a/pyrefly/lib/test/attributes.rs b/pyrefly/lib/test/attributes.rs index 6c19c98ca9..8df0bf5e74 100644 --- a/pyrefly/lib/test/attributes.rs +++ b/pyrefly/lib/test/attributes.rs @@ -944,6 +944,22 @@ def test(foo: Foo) -> None: "#, ); +testcase!( + test_getattr_does_not_provide_dunder_bool, + r#" +# Implicit dunders like `__bool__` are looked up on the type and are not +# resolved through `__getattr__`, so a `__getattr__` returning a non-callable +# type must not make an instance look like it has a non-callable `__bool__`. +# Regression test for https://github.com/facebook/pyrefly/issues/4467 +class Tensor: ... +class M: + def __getattr__(self, name: str) -> "Tensor | M": ... + +def f(x: M | None) -> M: + return x or M() + "#, +); + testcase!( test_object_setattr_wrong_signature, r#"