From df1fc7ac38c8e73ceb5b95c613c83e4f55408d4f Mon Sep 17 00:00:00 2001 From: Louisvranderick <73151698+Louisvranderick@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:19:09 -0400 Subject: [PATCH] Fix NotImplemented incorrectly treated as callable (#2918) NotImplementedType stubs inherit Any, so has_base_any made the singleton callable via the implicit-Any call path. Exclude types.NotImplementedType and builtins._NotImplementedType from that branch. Tests: callable/calls regressions; return NotImplemented unchanged. Made-with: Cursor --- pyrefly/lib/alt/call.rs | 8 +++++++- pyrefly/lib/test/callable.rs | 10 ++++++++++ pyrefly/lib/test/calls.rs | 5 ++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pyrefly/lib/alt/call.rs b/pyrefly/lib/alt/call.rs index c89e8f0092..ca3ee322a2 100644 --- a/pyrefly/lib/alt/call.rs +++ b/pyrefly/lib/alt/call.rs @@ -366,9 +366,15 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { // If the class has an unknown base (e.g. inherits from an // unresolved name), it might have inherited `__call__` from // that base, so treat it as callable with implicit Any. + // + // `NotImplemented` is a singleton instance of `NotImplementedType`; it must + // never be treated as callable even when stubs use `NotImplementedType(Any)`, + // which would otherwise set `has_base_any` and hit this branch. None if self .get_metadata_for_class(cls.class_object()) - .has_base_any() => + .has_base_any() + && !cls.has_qname("types", "NotImplementedType") + && !cls.has_qname("builtins", "_NotImplementedType") => { CallTargetLookup::Ok(Box::new(CallTarget::Any(AnyStyle::Implicit))) } diff --git a/pyrefly/lib/test/callable.rs b/pyrefly/lib/test/callable.rs index 86dc0da691..8ed8716684 100644 --- a/pyrefly/lib/test/callable.rs +++ b/pyrefly/lib/test/callable.rs @@ -1449,3 +1449,13 @@ def after_func() -> None: ... schedule(1000, after_func) "#, ); + +// Regression test for https://github.com/facebook/pyrefly/issues/2918 +testcase!( + test_notimplemented_not_callable, + r#" +NotImplemented() # E: Expected a callable +NotImplemented("not yet done") # E: Expected a callable +raise NotImplementedError() +"#, +); diff --git a/pyrefly/lib/test/calls.rs b/pyrefly/lib/test/calls.rs index 9c5bee4b0e..20c8c3c402 100644 --- a/pyrefly/lib/test/calls.rs +++ b/pyrefly/lib/test/calls.rs @@ -408,16 +408,15 @@ def get_flow_version(run_id: str | None) -> str | None: // https://github.com/facebook/pyrefly/issues/2918 testcase!( - bug = "Should error when calling NotImplemented (a constant, not a class)", test_call_not_implemented_constant, r#" # NotImplemented is a singleton constant, not a callable class. # Using NotImplemented() is always a mistake; they mean NotImplementedError(). def broken(): - raise NotImplemented() + raise NotImplemented() # E: Expected a callable def also_broken(): - raise NotImplemented("not yet done") + raise NotImplemented("not yet done") # E: Expected a callable "#, );