From 601013648c3400bf6eb3def6a07365d16bff7c14 Mon Sep 17 00:00:00 2001 From: Nitish Agarwal <1592163+nitishagar@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:34:52 +0530 Subject: [PATCH] Exempt base-dictated Any positions in @override methods from explicit-any MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A method decorated with @typing.override cannot narrow or drop an `Any` that the base signature already declares — repeating it is required to keep the override compatible — yet explicit-any fired on exactly those annotations, pointing at code that cannot be changed (issue #4548; ruff reached the same conclusion for ANN401). The override context is known at binding time (where the name-only is_override check already exists), so the method's annotations now carry the method name when they belong to an @override method. The solver, which can resolve the base signature, consumes it: parameter and return annotations report explicit-any from solve_annotation instead of expr_untype, exempting a position only when both the annotation is exactly Any and the corresponding base position (param by name, *args, **kwargs, or return — unwrapping Coroutine for async bases, considering every overload) is explicitly annotated Any. Everything else still reports: undictated positions, missing @override, missing base member, unannotated base positions, and nested Any like list[Any]. Fixes #4548 --- pyrefly/lib/alt/class/class_field.rs | 28 +++- pyrefly/lib/alt/class/pydantic.rs | 4 +- pyrefly/lib/alt/solve.rs | 95 ++++++++++++- pyrefly/lib/binding/binding.rs | 16 ++- pyrefly/lib/binding/bindings.rs | 8 +- pyrefly/lib/binding/class.rs | 1 + pyrefly/lib/binding/function.rs | 38 ++++- pyrefly/lib/binding/stmt.rs | 2 + pyrefly/lib/test/inference.rs | 203 +++++++++++++++++++++++++++ 9 files changed, 375 insertions(+), 20 deletions(-) diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index 6f3f1efd49..3a7770b7bc 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -2657,7 +2657,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { if ty.is_some_and(Self::is_proxy_method_type) { return true; } - let BindingAnnotation::AnnotateExpr(_, expr, _) = self.bindings().get(annotation) else { + let BindingAnnotation::AnnotateExpr(_, expr, _, _) = self.bindings().get(annotation) else { return false; }; Self::proxy_method_annotation_syntax_mentions_name(expr) @@ -2669,7 +2669,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { &self, annotation: Idx, ) -> ProxyMethodAnnotationForm { - let BindingAnnotation::AnnotateExpr(_, expr, _) = self.bindings().get(annotation) else { + let BindingAnnotation::AnnotateExpr(_, expr, _, _) = self.bindings().get(annotation) else { return ProxyMethodAnnotationForm::Other; }; self.proxy_method_annotation_expr_form(expr) @@ -4885,12 +4885,14 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { }) } - /// Return the first inherited method signature (parameters and flags) for `name`. - pub(crate) fn inherited_method_signature( + /// Return the instantiated type of the first inherited member for `name` + /// whose type is accepted by `accept`, walking the MRO. + pub(crate) fn inherited_member_where( &self, cls: &Class, name: &Name, - ) -> Option<(ParamList, FuncFlags)> { + accept: impl Fn(&Type) -> bool, + ) -> Option { let derived_instance = self.instantiate(cls); for ancestor in self.get_mro_for_class(cls).ancestors(self.stdlib) { let parent_cls = ancestor.class_object(); @@ -4899,13 +4901,25 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { }; let instance = Instance::of_protocol(ancestor, derived_instance.clone()); let instantiated = member.instantiate_for(self.heap, &instance); - if let Some(sig) = Self::callable_params_and_flags(instantiated.ty()) { - return Some(sig); + let ty = instantiated.ty(); + if accept(&ty) { + return Some(ty); } } None } + /// Return the first inherited method signature (parameters and flags) for `name`. + pub(crate) fn inherited_method_signature( + &self, + cls: &Class, + name: &Name, + ) -> Option<(ParamList, FuncFlags)> { + Self::callable_params_and_flags(self.inherited_member_where(cls, name, |ty| { + Self::callable_params_and_flags(ty.clone()).is_some() + })?) + } + pub fn get_metaclass_attribute( &self, cls: &ClassBase, diff --git a/pyrefly/lib/alt/class/pydantic.rs b/pyrefly/lib/alt/class/pydantic.rs index 6bbdeb7b7b..5b07f5d241 100644 --- a/pyrefly/lib/alt/class/pydantic.rs +++ b/pyrefly/lib/alt/class/pydantic.rs @@ -707,7 +707,9 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { if !metadata.is_pydantic_model() { return None; } - if let BindingAnnotation::AnnotateExpr(_, annotation_expr, _) = self.bindings().get(annot) { + if let BindingAnnotation::AnnotateExpr(_, annotation_expr, _, _) = + self.bindings().get(annot) + { let mut keywords = None; let metadata_items = self.get_annotated_metadata( annotation_expr, diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index 88ed26a40f..e79f7f729a 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -151,6 +151,7 @@ use crate::types::annotation::Qualifier; use crate::types::callable::Callable; use crate::types::callable::Param; use crate::types::callable::ParamList; +use crate::types::callable::Params; use crate::types::callable::Required; use crate::types::class::AttrsFieldSpecifierKind; use crate::types::class::Class; @@ -545,9 +546,25 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { errors: &ErrorCollector, ) -> AnnotationWithTarget { match binding { - BindingAnnotation::AnnotateExpr(target, x, class_key) => { + BindingAnnotation::AnnotateExpr(target, x, class_key, override_method) => { let type_form_context = target.type_form_context(); - let mut ann = self.expr_annotation(x, type_form_context, errors); + // An `@override` method repeating the base signature's `Any` is exempt + // from `explicit-any`; solve into a local collector only then, so the + // dictated error can be filtered out. + let may_exempt = override_method.is_some() + && matches!( + type_form_context, + TypeFormContext::ParameterAnnotation + | TypeFormContext::ParameterArgsAnnotation + | TypeFormContext::ParameterKwargsAnnotation + | TypeFormContext::ReturnAnnotation + ); + let ann_errors = may_exempt.then(|| self.error_collector()); + let mut ann = self.expr_annotation( + x, + type_form_context, + ann_errors.as_ref().unwrap_or(errors), + ); if let Some(class_key) = class_key && let Some(ty) = &mut ann.ty { @@ -580,6 +597,18 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { ); } } + if let Some(ann_errors) = ann_errors { + // An `@override` method cannot narrow or drop an `Any` the base + // signature declares: repeating it is required, so the + // `explicit-any` reported where it is written is dropped. + let dictated = matches!(&ann.ty, Some(Type::Any(AnyStyle::Explicit))) + && override_method.as_ref().is_some_and(|method| { + self.override_base_dictates_any(*class_key, method, target) + }); + errors.extend_filtered(ann_errors, |error| { + !(dictated && error.error_kind() == ErrorKind::ExplicitAny) + }); + } AnnotationWithTarget { target: target.clone(), annotation: ann, @@ -3764,7 +3793,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { /// Returns `None` for special forms which don't have a source expression. pub(crate) fn annotation_range(&self, key: Idx) -> Option { match self.bindings().get(key) { - BindingAnnotation::AnnotateExpr(_, expr, _) => Some(expr.range()), + BindingAnnotation::AnnotateExpr(_, expr, _, _) => Some(expr.range()), BindingAnnotation::SpecialForm(..) => None, } } @@ -7082,6 +7111,66 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { ty } + /// For a parameter or return annotation of an `@override` method: does the + /// base-class signature dictate `Any` at this position? The override cannot + /// narrow or drop such an `Any` without breaking the override, so it is + /// exempt from `explicit-any`. Only an explicitly-`Any`-annotated base + /// position exempts — an unannotated one does not. + fn override_base_dictates_any( + &self, + class_key: Option>, + method_name: &Name, + target: &AnnotationTarget, + ) -> bool { + let Some(class_key) = class_key else { + return false; + }; + let class = &*self.get_idx(class_key); + let Some(cls) = &class.0 else { + return false; + }; + let Some(base) = + self.inherited_member_where(cls, method_name, |ty| ty.is_toplevel_callable()) + else { + return false; + }; + let is_explicit_any = |ty: &Type| matches!(ty, Type::Any(AnyStyle::Explicit)); + // An async base stores its declared return as `Coroutine[Any, Any, T]`. + let return_dictates = |ret: &Type| { + is_explicit_any(ret) + || self + .unwrap_coroutine(ret) + .is_some_and(|(_, _, ret)| is_explicit_any(&ret)) + }; + base.toplevel_callable_signatures().any(|(sig, _)| { + let items: &[Param] = match &sig.params { + Params::List(params) | Params::Partial(params) => params.items(), + _ => &[], + }; + match target { + AnnotationTarget::Return(_) => return_dictates(&sig.ret), + AnnotationTarget::Param(name) => items.iter().any(|p| match p { + Param::Pos(param_name, ty, _) | Param::KwOnly(param_name, ty, _) => { + param_name.as_str() == name.as_str() + && matches!(ty, Type::Any(AnyStyle::Explicit)) + } + Param::PosOnly(Some(param_name), ty, _) => { + param_name.as_str() == name.as_str() + && matches!(ty, Type::Any(AnyStyle::Explicit)) + } + _ => false, + }), + AnnotationTarget::ArgsParam(_) => items.iter().any(|p| { + matches!(p, Param::Varargs(_, ty) if matches!(ty, Type::Any(AnyStyle::Explicit))) + }), + AnnotationTarget::KwargsParam(_) => items.iter().any(|p| { + matches!(p, Param::Kwargs(_, ty) if matches!(ty, Type::Any(AnyStyle::Explicit))) + }), + _ => false, + } + }) + } + /// Type check a delete expression, including ensuring that the target of the /// delete is legal. fn check_del_statement(&self, delete_target: &Expr, errors: &ErrorCollector) { diff --git a/pyrefly/lib/binding/binding.rs b/pyrefly/lib/binding/binding.rs index 7d23020b3b..08e0cf3fcc 100644 --- a/pyrefly/lib/binding/binding.rs +++ b/pyrefly/lib/binding/binding.rs @@ -128,7 +128,7 @@ assert_words!(KeyUndecoratedFunction, 1); assert_words!(Binding, 4); assert_words!(BindingExpect, 12); assert_words!(BindingTypeAlias, 6); -assert_words!(BindingAnnotation, 11); +assert_words!(BindingAnnotation, 13); assert_words!(BindingClass, 10); assert_words!(BindingTParams, 9); assert_words!(BindingClassBaseType, 3); @@ -3232,7 +3232,10 @@ impl AnnotationTarget { pub enum BindingAnnotation { /// The type is annotated to be this key, will have the outer type removed. /// Optionally occurring within a class, in which case Self refers to this class. - AnnotateExpr(AnnotationTarget, Expr, Option>), + /// For a parameter/return annotation of an `@override` method, the final field + /// is the method's name, used to exempt annotations dictated by the base + /// signature (e.g. from `explicit-any`); `None` otherwise. + AnnotateExpr(AnnotationTarget, Expr, Option>, Option), /// A special form declaration like `Literal: _SpecialForm`. SpecialForm(AnnotationTarget, SpecialForm), } @@ -3240,14 +3243,17 @@ pub enum BindingAnnotation { impl DisplayWith for BindingAnnotation { fn fmt(&self, f: &mut fmt::Formatter<'_>, ctx: &Bindings) -> fmt::Result { match self { - Self::AnnotateExpr(target, x, class_key) => write!( + Self::AnnotateExpr(target, x, class_key, override_method) => write!( f, - "AnnotateExpr({target}, {}, {})", + "AnnotateExpr({target}, {}, {}, {})", ctx.module().display(x), match class_key { None => "None".to_owned(), Some(t) => ctx.display(*t).to_string(), - } + }, + override_method + .as_ref() + .map_or_else(|| "None".to_owned(), ToString::to_string), ), Self::SpecialForm(target, sf) => write!(f, "SpecialForm({target}, {sf})"), } diff --git a/pyrefly/lib/binding/bindings.rs b/pyrefly/lib/binding/bindings.rs index 98be513406..08795e191e 100644 --- a/pyrefly/lib/binding/bindings.rs +++ b/pyrefly/lib/binding/bindings.rs @@ -2373,6 +2373,7 @@ impl<'a> BindingsBuilder<'a> { undecorated_idx: Idx, class_key: Option>, is_variadic: bool, + override_method: Option, ignore_annotation: bool, ) { let name = x.name(); @@ -2385,7 +2386,12 @@ impl<'a> BindingsBuilder<'a> { x.annotation().map(|x| { self.insert_binding( KeyAnnotation::Annotation(ShortIdentifier::new(name)), - BindingAnnotation::AnnotateExpr(target.clone(), x.clone(), class_key), + BindingAnnotation::AnnotateExpr( + target.clone(), + x.clone(), + class_key, + override_method, + ), ) }) }; diff --git a/pyrefly/lib/binding/class.rs b/pyrefly/lib/binding/class.rs index d5c4f6dfab..d6ae69a7dc 100644 --- a/pyrefly/lib/binding/class.rs +++ b/pyrefly/lib/binding/class.rs @@ -982,6 +982,7 @@ impl<'a> BindingsBuilder<'a> { AnnotationTarget::ClassMember(member_name.clone()), annotation_expr, None, + None, ), ) }); diff --git a/pyrefly/lib/binding/function.rs b/pyrefly/lib/binding/function.rs index cff0ffd872..41dd959408 100644 --- a/pyrefly/lib/binding/function.rs +++ b/pyrefly/lib/binding/function.rs @@ -287,6 +287,7 @@ impl<'a> BindingsBuilder<'a> { undecorated_idx: Idx, class_key: Option>, method_self_kind: MethodSelfKind, + override_method: Option, ignore_annotations: bool, ) { let mut self_name = None; @@ -300,6 +301,7 @@ impl<'a> BindingsBuilder<'a> { undecorated_idx, class_key, false, + override_method.clone(), ignore_annotations, ); } @@ -310,6 +312,7 @@ impl<'a> BindingsBuilder<'a> { undecorated_idx, class_key, true, + override_method.clone(), ignore_annotations, ); } @@ -320,6 +323,7 @@ impl<'a> BindingsBuilder<'a> { undecorated_idx, class_key, true, + override_method, ignore_annotations, ); } @@ -332,6 +336,7 @@ impl<'a> BindingsBuilder<'a> { mut x: Expr, func_name: &Identifier, class_key: Option>, + override_method: Option, tparams_builder: Option<&mut LegacyTParamCollector>, ) -> (TextRange, Idx) { self.ensure_type(&mut x, tparams_builder); @@ -343,6 +348,7 @@ impl<'a> BindingsBuilder<'a> { AnnotationTarget::Return(func_name.id.clone()), x, class_key, + override_method, ), ), ) @@ -353,6 +359,7 @@ impl<'a> BindingsBuilder<'a> { x: &mut StmtFunctionDef, func_name: &Identifier, class_key: Option>, + override_method: Option, usage: &mut Usage, parent: &NestingContext, ) -> ( @@ -376,7 +383,13 @@ impl<'a> BindingsBuilder<'a> { } let return_ann_with_range = mem::take(&mut x.returns).map(|e| { - self.to_return_annotation_with_range(*e, func_name, class_key, Some(&mut legacy)) + self.to_return_annotation_with_range( + *e, + func_name, + class_key, + override_method, + Some(&mut legacy), + ) }); self.add_name_definitions(&legacy); @@ -399,6 +412,7 @@ impl<'a> BindingsBuilder<'a> { class_key: Option>, is_async: bool, method_self_kind: MethodSelfKind, + override_method: Option, ) -> ( YieldsAndReturns, Option, @@ -412,6 +426,7 @@ impl<'a> BindingsBuilder<'a> { undecorated_idx, class_key, method_self_kind, + override_method, false, ); self.init_static_scope(&body, false); @@ -456,6 +471,7 @@ impl<'a> BindingsBuilder<'a> { class_key: Option>, is_async: bool, method_self_kind: MethodSelfKind, + override_method: Option, ignore_annotations: bool, ) -> Option { // Push a scope to create the parameter keys (but do nothing else with it). @@ -466,6 +482,7 @@ impl<'a> BindingsBuilder<'a> { undecorated_idx, class_key, method_self_kind, + override_method, ignore_annotations, ); self.scopes.pop(); @@ -765,6 +782,8 @@ impl<'a> BindingsBuilder<'a> { let is_unannotated = !self.check_unannotated_defs && !is_annotated(&return_ann_with_range, parameters); + let override_method = + (decorators.is_override && class_key.is_some()).then(|| func_name.id.clone()); let (is_return_inferred, self_assignments) = if decorators.has_no_type_check || (is_unannotated && !self.analyze_unannotated_for_ide) { @@ -778,6 +797,7 @@ impl<'a> BindingsBuilder<'a> { class_key, is_async, method_self_kind, + override_method, decorators.has_no_type_check, ); (false, self_assignments) @@ -793,6 +813,7 @@ impl<'a> BindingsBuilder<'a> { class_key, is_async, method_self_kind, + override_method, ); self.analyze_return_type( func_name, @@ -820,6 +841,7 @@ impl<'a> BindingsBuilder<'a> { class_key, is_async, method_self_kind, + override_method, ); if !ignore_unused_parameters { self.record_unused_parameters(unused_parameters); @@ -897,10 +919,20 @@ impl<'a> BindingsBuilder<'a> { self.maybe_record_pytest_fixture_definition(&x, class_key); let decorators = self.decorators(mem::take(&mut x.decorator_list), def_idx.usage()); + // Method annotations of an `@override` method may be dictated by the base + // signature; stamping the method name lets the solver exempt them. + let override_method = + (decorators.is_override && class_key.is_some()).then(|| func_name.id.clone()); self.scopes.push(Scope::annotation(x.range, false)); - let (return_ann_with_range, legacy_tparams) = - self.function_header(&mut x, &func_name, class_key, def_idx.usage(), parent); + let (return_ann_with_range, legacy_tparams) = self.function_header( + &mut x, + &func_name, + class_key, + override_method, + def_idx.usage(), + parent, + ); let docstring_range = Docstring::range_from_stmts(x.body.as_slice()); let calls_super_method = SuperMethodCallFinder::find(&func_name.id, &x.body); diff --git a/pyrefly/lib/binding/stmt.rs b/pyrefly/lib/binding/stmt.rs index 0e9c7d88a7..3227883431 100644 --- a/pyrefly/lib/binding/stmt.rs +++ b/pyrefly/lib/binding/stmt.rs @@ -739,6 +739,7 @@ impl<'a> BindingsBuilder<'a> { }, annotation.clone(), None, + None, ) }; self.insert_binding(ann_key, ann_val) @@ -1151,6 +1152,7 @@ impl<'a> BindingsBuilder<'a> { AnnotationTarget::AttrAssign(attr_name.clone()), *x.annotation, None, + None, ), ); let value = match x.value { diff --git a/pyrefly/lib/test/inference.rs b/pyrefly/lib/test/inference.rs index ee6d49e34f..877a52dc52 100644 --- a/pyrefly/lib/test/inference.rs +++ b/pyrefly/lib/test/inference.rs @@ -314,6 +314,209 @@ def twice(x: Callable[[Any], Any]) -> None: ... # E: Explicit `Any` is not allo "#, ); +testcase!( + test_explicit_any_exempt_for_override_dictated_by_base, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any, override + +class Base: + def method(self, value: Any) -> Any: ... # E: Explicit `Any` is not allowed # E: Explicit `Any` is not allowed + +class Child(Base): + @override + def method(self, value: Any) -> Any: + return value +"#, +); + +testcase!( + test_explicit_any_still_reported_for_undictated_override_positions, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any, override + +class Base: + def method(self, value: int) -> int: ... + +class Child(Base): + @override + def method(self, value: Any) -> Any: # E: Explicit `Any` is not allowed # E: Explicit `Any` is not allowed + return 1 +"#, +); + +testcase!( + test_explicit_any_still_reported_without_override_decorator, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any + +class Base: + def method(self, value: Any) -> Any: ... # E: Explicit `Any` is not allowed # E: Explicit `Any` is not allowed + +class Child(Base): + def method(self, value: Any) -> Any: # E: Explicit `Any` is not allowed # E: Explicit `Any` is not allowed + return value +"#, +); + +testcase!( + test_explicit_any_per_position_granularity, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any, override + +class Base: + def method(self, value: Any) -> Any: ... # E: Explicit `Any` is not allowed # E: Explicit `Any` is not allowed + +class Child(Base): + @override + def method(self, value: Any, extra: Any = 1) -> Any: # E: Explicit `Any` is not allowed + return value +"#, +); + +testcase!( + test_explicit_any_still_reported_for_override_without_base_member, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any, override + +class Base: + ... + +class Child(Base): + @override + def missing( # E: Class member `Child.missing` is marked as an override, but no parent class has a matching attribute + self, value: Any # E: Explicit `Any` is not allowed + ) -> Any: # E: Explicit `Any` is not allowed + return value +"#, +); + +testcase!( + test_explicit_any_still_reported_when_base_param_unannotated, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any, override + +class Base: + def method(self, value) -> int: ... + +class Child(Base): + @override + def method(self, value: Any) -> int: # E: Explicit `Any` is not allowed + return 1 +"#, +); + +testcase!( + test_explicit_any_exempt_for_override_dictated_args_kwargs, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any, override + +class Base: + def method(self, *args: Any, **kwargs: Any) -> None: ... # E: Explicit `Any` is not allowed # E: Explicit `Any` is not allowed + +class Child(Base): + @override + def method(self, *args: Any, **kwargs: Any) -> None: + pass +"#, +); + +testcase!( + test_explicit_any_still_reported_for_args_kwargs_without_base_counterpart, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any, override + +class Base: + def method(self, value: int) -> None: ... + +class Child(Base): + @override + def method( + self, value: int, *args: Any, **kwargs: Any # E: Explicit `Any` is not allowed # E: Explicit `Any` is not allowed + ) -> None: + pass +"#, +); + +testcase!( + test_explicit_any_still_reported_for_nested_any_in_override, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any, override + +class Base: + def method(self, value: list[int]) -> dict[str, int]: ... + +class Child(Base): + @override + def method( + self, value: list[Any] # E: Explicit `Any` is not allowed + ) -> dict[str, Any]: # E: Explicit `Any` is not allowed + return {} +"#, +); + +testcase!( + test_explicit_any_exempt_for_override_dictated_by_one_overload, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any, override, overload + +class Base: + @overload + def method(self, value: int) -> int: ... + @overload + def method(self, value: Any) -> Any: ... # E: Explicit `Any` is not allowed # E: Explicit `Any` is not allowed + def method(self, value): + return value + +class Child(Base): + @override + def method(self, value: Any) -> Any: + return value +"#, +); + +testcase!( + test_explicit_any_exempt_for_override_from_typing_extensions, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any +from typing_extensions import override + +class Base: + def method(self, value: Any) -> Any: ... # E: Explicit `Any` is not allowed # E: Explicit `Any` is not allowed + +class Child(Base): + @override + def method(self, value: Any) -> Any: + return value +"#, +); + +testcase!( + test_explicit_any_exempt_for_override_dictated_async_return, + TestEnv::new().enable_explicit_any_error(), + r#" +from typing import Any, override + +class Base: + async def method(self) -> Any: ... # E: Explicit `Any` is not allowed + +class Child(Base): + @override + async def method(self) -> Any: + return 1 +"#, +); + testcase!( test_warn_on_implicit_any_in_attribute, TestEnv::new().enable_implicit_any_attribute_error(),