From 1f7d294ef0eb505fbd8a60f34e151a776adb7487 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 20 Apr 2024 22:22:00 +0100 Subject: [PATCH] Allow overloaded __exit__ and __aexit__ definitions --- .../test/fixtures/flake8_pyi/PYI036.py | 96 ++++++- .../test/fixtures/flake8_pyi/PYI036.pyi | 85 +++++- .../src/checkers/ast/analyze/statement.rs | 2 +- .../flake8_pyi/rules/exit_annotations.rs | 250 +++++++++++++++--- ...__flake8_pyi__tests__PYI036_PYI036.py.snap | 52 +++- ..._flake8_pyi__tests__PYI036_PYI036.pyi.snap | 46 +++- 6 files changed, 481 insertions(+), 50 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI036.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI036.py index 57f71dc39e014..6ae20a7d683ed 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI036.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI036.py @@ -3,7 +3,7 @@ import typing from collections.abc import Awaitable from types import TracebackType -from typing import Any, Type +from typing import Any, Type, overload import _typeshed import typing_extensions @@ -73,3 +73,97 @@ async def __aexit__(self, /, typ: type[BaseException] | None, *args: Any) -> Awa class BadSix: def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default + +class AllPositionalOnlyArgs: + def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ... + +class BadAllPositionalOnlyArgs: + def __exit__(self, typ: type[Exception] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType, /) -> None: ... + +# Definitions not in a class scope can do whatever, we don't care +def __exit__(self, *args: bool) -> None: ... +async def __aexit__(self, *, go_crazy: bytes) -> list[str]: ... + +# Here come the overloads... + +class AcceptableOverload1: + @overload + def __exit__(self, exc_typ: None, exc: None, exc_tb: None) -> None: ... + @overload + def __exit__(self, exc_typ: type[BaseException], exc: BaseException, exc_tb: TracebackType) -> None: ... + def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, exc_tb: TracebackType | None) -> None: ... + +# Using `object` or `Unused` in an overload definition is kinda strange, +# but let's allow it to be on the safe side +class AcceptableOverload2: + @overload + def __exit__(self, exc_typ: None, exc: None, exc_tb: object) -> None: ... + @overload + def __exit__(self, exc_typ: Unused, exc: BaseException, exc_tb: object) -> None: ... + def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, exc_tb: TracebackType | None) -> None: ... + +class AcceptableOverload3: + # Just ignore any overloads that don't have exactly 3 annotated non-self parameters. + # We don't have the ability (yet) to do arbitrary checking + # of whether one function definition is a subtype of another... + @overload + def __exit__(self, exc_typ: bool, exc: bool, exc_tb: bool, weird_extra_arg: bool) -> None: ... + @overload + def __exit__(self, *args: object) -> None: ... + def __exit__(self, *args: object) -> None: ... + @overload + async def __aexit__(self, exc_typ: bool, /, exc: bool, exc_tb: bool, *, keyword_only: str) -> None: ... + @overload + async def __aexit__(self, *args: object) -> None: ... + async def __aexit__(self, *args: object) -> None: ... + +class AcceptableOverload4: + # Same as above + @overload + def __exit__(self, exc_typ: type[Exception], exc: type[Exception], exc_tb: types.TracebackType) -> None: ... + @overload + def __exit__(self, *args: object) -> None: ... + def __exit__(self, *args: object) -> None: ... + @overload + async def __aexit__(self, exc_typ: type[Exception], exc: type[Exception], exc_tb: types.TracebackType, *, extra: str = "foo") -> None: ... + @overload + async def __aexit__(self, exc_typ: None, exc: None, tb: None) -> None: ... + async def __aexit__(self, *args: object) -> None: ... + +class StrangeNumberOfOverloads: + # Only one overload? Type checkers will emit an error, but we should just ignore it + @overload + def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ... + def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ... + # More than two overloads? Anything could be going on; again, just ignore all the overloads + @overload + async def __aexit__(self, arg: bool) -> None: ... + @overload + async def __aexit__(self, arg: None, arg2: None, arg3: None) -> None: ... + @overload + async def __aexit__(self, arg: bool, arg2: bool, arg3: bool) -> None: ... + async def __aexit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ... + +# TODO: maybe we should emit an error on this one as well? +class BizarreAsyncSyncOverloadMismatch: + @overload + def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ... + @overload + async def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ... + def __exit__(self, *args: object) -> None: ... + +class UnacceptableOverload1: + @overload + def __exit__(self, exc_typ: None, exc: None, tb: None) -> None: ... # Okay + @overload + def __exit__(self, exc_typ: Exception, exc: Exception, tb: TracebackType) -> None: ... # PYI036 + def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ... + +class UnacceptableOverload2: + @overload + def __exit__(self, exc_typ: type[BaseException] | None, exc: None, tb: None) -> None: ... # PYI036 + @overload + def __exit__(self, exc_typ: object, exc: Exception, tb: builtins.TracebackType) -> None: ... # PYI036 + def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ... diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI036.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI036.pyi index 3f58441c94f21..4020472dea6e1 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI036.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI036.pyi @@ -3,7 +3,7 @@ import types import typing from collections.abc import Awaitable from types import TracebackType -from typing import Any, Type +from typing import Any, Type, overload import _typeshed import typing_extensions @@ -80,3 +80,86 @@ def isolated_scope(): class ShouldNotError: def __exit__(self, typ: Type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ... + +class AllPositionalOnlyArgs: + def __exit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ... + +class BadAllPositionalOnlyArgs: + def __exit__(self, typ: type[Exception] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ... + async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType, /) -> None: ... + +# Definitions not in a class scope can do whatever, we don't care +def __exit__(self, *args: bool) -> None: ... +async def __aexit__(self, *, go_crazy: bytes) -> list[str]: ... + +# Here come the overloads... + +class AcceptableOverload1: + @overload + def __exit__(self, exc_typ: None, exc: None, exc_tb: None) -> None: ... + @overload + def __exit__(self, exc_typ: type[BaseException], exc: BaseException, exc_tb: TracebackType) -> None: ... + +# Using `object` or `Unused` in an overload definition is kinda strange, +# but let's allow it to be on the safe side +class AcceptableOverload2: + @overload + def __exit__(self, exc_typ: None, exc: None, exc_tb: object) -> None: ... + @overload + def __exit__(self, exc_typ: Unused, exc: BaseException, exc_tb: object) -> None: ... + +class AcceptableOverload3: + # Just ignore any overloads that don't have exactly 3 annotated non-self parameters. + # We don't have the ability (yet) to do arbitrary checking + # of whether one function definition is a subtype of another... + @overload + def __exit__(self, exc_typ: bool, exc: bool, exc_tb: bool, weird_extra_arg: bool) -> None: ... + @overload + def __exit__(self, *args: object) -> None: ... + @overload + async def __aexit__(self, exc_typ: bool, /, exc: bool, exc_tb: bool, *, keyword_only: str) -> None: ... + @overload + async def __aexit__(self, *args: object) -> None: ... + +class AcceptableOverload4: + # Same as above + @overload + def __exit__(self, exc_typ: type[Exception], exc: type[Exception], exc_tb: types.TracebackType) -> None: ... + @overload + def __exit__(self, *args: object) -> None: ... + @overload + async def __aexit__(self, exc_typ: type[Exception], exc: type[Exception], exc_tb: types.TracebackType, *, extra: str = "foo") -> None: ... + @overload + async def __aexit__(self, exc_typ: None, exc: None, tb: None) -> None: ... + +class StrangeNumberOfOverloads: + # Only one overload? Type checkers will emit an error, but we should just ignore it + @overload + def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ... + # More than two overloads? Anything could be going on; again, just ignore all the overloads + @overload + async def __aexit__(self, arg: bool) -> None: ... + @overload + async def __aexit__(self, arg: None, arg2: None, arg3: None) -> None: ... + @overload + async def __aexit__(self, arg: bool, arg2: bool, arg3: bool) -> None: ... + +# TODO: maybe we should emit an error on this one as well? +class BizarreAsyncSyncOverloadMismatch: + @overload + def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ... + @overload + async def __exit__(self, exc_typ: bool, exc: bool, tb: bool) -> None: ... + +class UnacceptableOverload1: + @overload + def __exit__(self, exc_typ: None, exc: None, tb: None) -> None: ... # Okay + @overload + def __exit__(self, exc_typ: Exception, exc: Exception, tb: TracebackType) -> None: ... # PYI036 + +class UnacceptableOverload2: + @overload + def __exit__(self, exc_typ: type[BaseException] | None, exc: None, tb: None) -> None: ... # PYI036 + @overload + def __exit__(self, exc_typ: object, exc: Exception, tb: builtins.TracebackType) -> None: ... # PYI036 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 18326189b8cf1..0190de443528c 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -174,7 +174,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { } } if checker.enabled(Rule::BadExitAnnotation) { - flake8_pyi::rules::bad_exit_annotation(checker, *is_async, name, parameters); + flake8_pyi::rules::bad_exit_annotation(checker, function_def); } if checker.enabled(Rule::RedundantNumericUnion) { flake8_pyi::rules::redundant_numeric_union(checker, parameters); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs index c7b18b3d8eb47..3e42284a0b793 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/exit_annotations.rs @@ -1,16 +1,16 @@ use std::fmt::{Display, Formatter}; use ruff_python_ast::{ - Expr, ExprBinOp, ExprSubscript, ExprTuple, Identifier, Operator, ParameterWithDefault, - Parameters, + Expr, ExprBinOp, ExprSubscript, ExprTuple, Operator, ParameterWithDefault, Parameters, Stmt, + StmtClassDef, StmtFunctionDef, }; use smallvec::SmallVec; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::SemanticModel; -use ruff_text_size::Ranged; +use ruff_python_semantic::{analyze::visibility::is_overload, SemanticModel}; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -68,6 +68,10 @@ impl Violation for BadExitAnnotation { ErrorKind::FirstArgBadAnnotation => format!("The first argument in `{method_name}` should be annotated with `object` or `type[BaseException] | None`"), ErrorKind::SecondArgBadAnnotation => format!("The second argument in `{method_name}` should be annotated with `object` or `BaseException | None`"), ErrorKind::ThirdArgBadAnnotation => format!("The third argument in `{method_name}` should be annotated with `object` or `types.TracebackType | None`"), + ErrorKind::UnrecognizedExitOverload => format!( + "Annotations for a three-argument `{method_name}` overload (excluding `self`) \ + should either be `None, None, None` or `type[BaseException], BaseException, types.TracebackType`" + ) } } @@ -80,21 +84,27 @@ impl Violation for BadExitAnnotation { } } -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, is_macro::Is)] enum FuncKind { Sync, Async, } -impl Display for FuncKind { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl FuncKind { + const fn as_str(self) -> &'static str { match self { - FuncKind::Sync => write!(f, "__exit__"), - FuncKind::Async => write!(f, "__aexit__"), + Self::Async => "__aexit__", + Self::Sync => "__exit__", } } } +impl Display for FuncKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum ErrorKind { StarArgsNotAnnotated, @@ -104,37 +114,59 @@ enum ErrorKind { ThirdArgBadAnnotation, ArgsAfterFirstFourMustHaveDefault, AllKwargsMustHaveDefault, + UnrecognizedExitOverload, } /// PYI036 -pub(crate) fn bad_exit_annotation( - checker: &mut Checker, - is_async: bool, - name: &Identifier, - parameters: &Parameters, -) { +pub(crate) fn bad_exit_annotation(checker: &mut Checker, function: &StmtFunctionDef) { + let StmtFunctionDef { + is_async, + decorator_list, + name, + parameters, + .. + } = function; + let func_kind = match name.as_str() { "__exit__" if !is_async => FuncKind::Sync, - "__aexit__" if is_async => FuncKind::Async, + "__aexit__" if *is_async => FuncKind::Async, _ => return, }; - let positional_args = parameters - .args + let semantic = checker.semantic(); + + let Some(Stmt::ClassDef(parent_class_def)) = semantic.current_statement_parent() else { + return; + }; + + let non_self_positional_args: SmallVec<[&ParameterWithDefault; 3]> = parameters + .posonlyargs .iter() - .chain(parameters.posonlyargs.iter()) - .collect::>(); + .chain(parameters.args.iter()) + .skip(1) + .collect(); + + if is_overload(decorator_list, semantic) { + check_positional_args_for_overloaded_method( + checker, + &non_self_positional_args, + func_kind, + parent_class_def, + parameters.range(), + ); + return; + } // If there are less than three positional arguments, at least one of them must be a star-arg, // and it must be annotated with `object`. - if positional_args.len() < 4 { + if non_self_positional_args.len() < 3 { check_short_args_list(checker, parameters, func_kind); } // Every positional argument (beyond the first four) must have a default. - for parameter in positional_args + for parameter in non_self_positional_args .iter() - .skip(4) + .skip(3) .filter(|parameter| parameter.default.is_none()) { checker.diagnostics.push(Diagnostic::new( @@ -161,7 +193,7 @@ pub(crate) fn bad_exit_annotation( )); } - check_positional_args(checker, &positional_args, func_kind); + check_positional_args_for_non_overloaded_method(checker, &non_self_positional_args, func_kind); } /// Determine whether a "short" argument list (i.e., an argument list with less than four elements) @@ -204,11 +236,11 @@ fn check_short_args_list(checker: &mut Checker, parameters: &Parameters, func_ki } } -/// Determines whether the positional arguments of an `__exit__` or `__aexit__` method are -/// annotated correctly. -fn check_positional_args( +/// Determines whether the positional arguments of an `__exit__` or `__aexit__` method +/// (that is not decorated with `@typing.overload`) are annotated correctly. +fn check_positional_args_for_non_overloaded_method( checker: &mut Checker, - positional_args: &[&ParameterWithDefault], + non_self_positional_args: &[&ParameterWithDefault], kind: FuncKind, ) { // For each argument, define the predicate against which to check the annotation. @@ -222,7 +254,7 @@ fn check_positional_args( (ErrorKind::ThirdArgBadAnnotation, is_traceback_type), ]; - for (arg, (error_info, predicate)) in positional_args.iter().skip(1).take(3).zip(validations) { + for (arg, (error_info, predicate)) in non_self_positional_args.iter().take(3).zip(validations) { let Some(annotation) = arg.parameter.annotation.as_ref() else { continue; }; @@ -249,6 +281,148 @@ fn check_positional_args( } } +/// Determines whether the positional arguments of an `__exit__` or `__aexit__` method +/// overload are annotated correctly. +fn check_positional_args_for_overloaded_method( + checker: &mut Checker, + non_self_positional_args: &[&ParameterWithDefault], + kind: FuncKind, + parent_class_def: &StmtClassDef, + parameters_range: TextRange, +) { + fn parameter_annotation_loosely_matches_predicate( + parameter: &ParameterWithDefault, + predicate: impl FnOnce(&Expr) -> bool, + semantic: &SemanticModel, + ) -> bool { + parameter + .parameter + .annotation + .as_ref() + .map_or(true, |annotation| { + predicate(annotation) || is_object_or_unused(annotation, semantic) + }) + } + + let semantic = checker.semantic(); + + // Collect all the overloads for this method into a SmallVec + let function_overloads: SmallVec<[&StmtFunctionDef; 2]> = parent_class_def + .body + .iter() + .filter_map(|stmt| { + let func_def = stmt.as_function_def_stmt()?; + if &func_def.name == kind.as_str() && is_overload(&func_def.decorator_list, semantic) { + Some(func_def) + } else { + None + } + }) + .collect(); + + // If the number of overloads for this method is not exactly 2, don't do any checking + if function_overloads.len() != 2 { + return; + } + + for function_def in &function_overloads { + let StmtFunctionDef { + is_async, + parameters, + .. + } = function_def; + + // If any overloads are an unexpected sync/async colour, don't do any checking + if *is_async != kind.is_async() { + return; + } + + // If any overloads have any variadic arguments, don't do any checking + let Parameters { + range: _, + posonlyargs, + args, + vararg: None, + kwonlyargs, + kwarg: None, + } = &**parameters + else { + return; + }; + + // If any overloads have any keyword-only arguments, don't do any checking + if !kwonlyargs.is_empty() { + return; + } + + // If the number of non-keyword-only arguments is not exactly equal to 4 + // for any overloads, don't do any checking + if posonlyargs.len() + args.len() != 4 { + return; + } + } + + debug_assert!( + function_overloads.contains(&semantic.current_statement().as_function_def_stmt().unwrap()) + ); + + // We've now established that no overloads for this method have any variadic parameters, + // no overloads have any keyword-only parameters, all overloads are the expected + // sync/async colour, and all overloads have exactly 3 non-`self` non-keyword-only parameters. + // The method we're currently looking at is one of those overloads. + // It therefore follows that, in order for it to be correctly annotated, it must be + // one of the following two possible overloads: + // + // ``` + // @overload + // def __(a)exit__(self, typ: None, exc: None, tb: None) -> None: ... + // @overload + // def __(a)exit__(self, typ: type[BaseException], exc: BaseException, tb: TracebackType) -> None: ... + // ``` + // + // We'll allow small variations on either of these (if, e.g. a parameter is unannotated, + // annotated with `object` or `_typeshed.Unused`). *Basically*, though, the rule is: + // - If the function overload matches *either* of those, it's okay. + // - If not: emit a diagnostic. + + // Start by checking the first possibility: + if non_self_positional_args.iter().all(|parameter| { + parameter_annotation_loosely_matches_predicate( + parameter, + Expr::is_none_literal_expr, + semantic, + ) + }) { + return; + } + + // Now check the second: + if parameter_annotation_loosely_matches_predicate( + non_self_positional_args[0], + |annotation| is_base_exception_type(annotation, semantic), + semantic, + ) && parameter_annotation_loosely_matches_predicate( + non_self_positional_args[1], + |annotation| semantic.match_builtin_expr(annotation, "BaseException"), + semantic, + ) && parameter_annotation_loosely_matches_predicate( + non_self_positional_args[2], + |annotation| is_traceback_type(annotation, semantic), + semantic, + ) { + return; + } + + // Okay, neither of them match... + checker.diagnostics.push(Diagnostic::new( + BadExitAnnotation { + func_kind: kind, + error_kind: ErrorKind::UnrecognizedExitOverload, + }, + parameters_range, + )); +} + /// Return the non-`None` annotation element of a PEP 604-style union or `Optional` annotation. fn non_none_annotation_element<'a>( annotation: &'a Expr, @@ -256,12 +430,9 @@ fn non_none_annotation_element<'a>( ) -> Option<&'a Expr> { // E.g., `typing.Union` or `typing.Optional` if let Expr::Subscript(ExprSubscript { value, slice, .. }) = annotation { - let qualified_name = semantic.resolve_qualified_name(value); + let qualified_name = semantic.resolve_qualified_name(value)?; - if qualified_name - .as_ref() - .is_some_and(|value| semantic.match_typing_qualified_name(value, "Optional")) - { + if semantic.match_typing_qualified_name(&qualified_name, "Optional") { return if slice.is_none_literal_expr() { None } else { @@ -269,16 +440,11 @@ fn non_none_annotation_element<'a>( }; } - if !qualified_name - .as_ref() - .is_some_and(|value| semantic.match_typing_qualified_name(value, "Union")) - { + if !semantic.match_typing_qualified_name(&qualified_name, "Union") { return None; } - let Expr::Tuple(ExprTuple { elts, .. }) = slice.as_ref() else { - return None; - }; + let ExprTuple { elts, .. } = slice.as_tuple_expr()?; let [left, right] = elts.as_slice() else { return None; @@ -318,7 +484,6 @@ fn non_none_annotation_element<'a>( fn is_object_or_unused(expr: &Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(expr) - .as_ref() .is_some_and(|qualified_name| { matches!( qualified_name.segments(), @@ -331,7 +496,6 @@ fn is_object_or_unused(expr: &Expr, semantic: &SemanticModel) -> bool { fn is_traceback_type(expr: &Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(expr) - .as_ref() .is_some_and(|qualified_name| { matches!(qualified_name.segments(), ["types", "TracebackType"]) }) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.py.snap index 5e891fa5d9323..2f3dc38a504bd 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.py.snap @@ -156,6 +156,52 @@ PYI036.py:75:48: PYI036 All keyword-only arguments in `__aexit__` must have a de 74 | def __exit__(self, typ, exc, tb, weird_extra_arg, extra_arg2 = None) -> None: ... # PYI036: Extra arg must have default 75 | async def __aexit__(self, typ, exc, tb, *, weird_extra_arg) -> None: ... # PYI036: kwargs must have default | ^^^^^^^^^^^^^^^ PYI036 - | - - +76 | +77 | class AllPositionalOnlyArgs: + | + +PYI036.py:82:29: PYI036 The first argument in `__exit__` should be annotated with `object` or `type[BaseException] | None` + | +81 | class BadAllPositionalOnlyArgs: +82 | def __exit__(self, typ: type[Exception] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ... + | ^^^^^^^^^^^^^^^^^^^^^^ PYI036 +83 | async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType, /) -> None: ... + | + +PYI036.py:83:95: PYI036 The third argument in `__aexit__` should be annotated with `object` or `types.TracebackType | None` + | +81 | class BadAllPositionalOnlyArgs: +82 | def __exit__(self, typ: type[Exception] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ... +83 | async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType, /) -> None: ... + | ^^^^^^^^^^^^^ PYI036 +84 | +85 | # Definitions not in a class scope can do whatever, we don't care + | + +PYI036.py:161:17: PYI036 Annotations for a three-argument `__exit__` overload (excluding `self`) should either be `None, None, None` or `type[BaseException], BaseException, types.TracebackType` + | +159 | def __exit__(self, exc_typ: None, exc: None, tb: None) -> None: ... # Okay +160 | @overload +161 | def __exit__(self, exc_typ: Exception, exc: Exception, tb: TracebackType) -> None: ... # PYI036 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI036 +162 | def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ... + | + +PYI036.py:166:17: PYI036 Annotations for a three-argument `__exit__` overload (excluding `self`) should either be `None, None, None` or `type[BaseException], BaseException, types.TracebackType` + | +164 | class UnacceptableOverload2: +165 | @overload +166 | def __exit__(self, exc_typ: type[BaseException] | None, exc: None, tb: None) -> None: ... # PYI036 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI036 +167 | @overload +168 | def __exit__(self, exc_typ: object, exc: Exception, tb: builtins.TracebackType) -> None: ... # PYI036 + | + +PYI036.py:168:17: PYI036 Annotations for a three-argument `__exit__` overload (excluding `self`) should either be `None, None, None` or `type[BaseException], BaseException, types.TracebackType` + | +166 | def __exit__(self, exc_typ: type[BaseException] | None, exc: None, tb: None) -> None: ... # PYI036 +167 | @overload +168 | def __exit__(self, exc_typ: object, exc: Exception, tb: builtins.TracebackType) -> None: ... # PYI036 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI036 +169 | def __exit__(self, exc_typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ... + | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap index 2d7817652e298..7f31ca0ccdf0b 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI036_PYI036.pyi.snap @@ -168,4 +168,48 @@ PYI036.pyi:75:48: PYI036 All keyword-only arguments in `__aexit__` must have a d | ^^^^^^^^^^^^^^^ PYI036 | - +PYI036.pyi:89:29: PYI036 The first argument in `__exit__` should be annotated with `object` or `type[BaseException] | None` + | +88 | class BadAllPositionalOnlyArgs: +89 | def __exit__(self, typ: type[Exception] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ... + | ^^^^^^^^^^^^^^^^^^^^^^ PYI036 +90 | async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType, /) -> None: ... + | + +PYI036.pyi:90:95: PYI036 The third argument in `__aexit__` should be annotated with `object` or `types.TracebackType | None` + | +88 | class BadAllPositionalOnlyArgs: +89 | def __exit__(self, typ: type[Exception] | None, exc: BaseException | None, tb: TracebackType | None, /) -> None: ... +90 | async def __aexit__(self, typ: type[BaseException] | None, exc: BaseException | None, tb: TracebackType, /) -> None: ... + | ^^^^^^^^^^^^^ PYI036 +91 | +92 | # Definitions not in a class scope can do whatever, we don't care + | + +PYI036.pyi:159:17: PYI036 Annotations for a three-argument `__exit__` overload (excluding `self`) should either be `None, None, None` or `type[BaseException], BaseException, types.TracebackType` + | +157 | def __exit__(self, exc_typ: None, exc: None, tb: None) -> None: ... # Okay +158 | @overload +159 | def __exit__(self, exc_typ: Exception, exc: Exception, tb: TracebackType) -> None: ... # PYI036 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI036 +160 | +161 | class UnacceptableOverload2: + | + +PYI036.pyi:163:17: PYI036 Annotations for a three-argument `__exit__` overload (excluding `self`) should either be `None, None, None` or `type[BaseException], BaseException, types.TracebackType` + | +161 | class UnacceptableOverload2: +162 | @overload +163 | def __exit__(self, exc_typ: type[BaseException] | None, exc: None, tb: None) -> None: ... # PYI036 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI036 +164 | @overload +165 | def __exit__(self, exc_typ: object, exc: Exception, tb: builtins.TracebackType) -> None: ... # PYI036 + | + +PYI036.pyi:165:17: PYI036 Annotations for a three-argument `__exit__` overload (excluding `self`) should either be `None, None, None` or `type[BaseException], BaseException, types.TracebackType` + | +163 | def __exit__(self, exc_typ: type[BaseException] | None, exc: None, tb: None) -> None: ... # PYI036 +164 | @overload +165 | def __exit__(self, exc_typ: object, exc: Exception, tb: builtins.TracebackType) -> None: ... # PYI036 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI036 + |