From 9c3c59aca9704c22bd7404e98271b0058e1ff1a1 Mon Sep 17 00:00:00 2001 From: Harutaka Kawamura Date: Mon, 9 Dec 2024 23:32:37 +0900 Subject: [PATCH 01/14] [`flake8-bugbear`] Skip `B028` if `warnings.warn` is called with `*args` or `**kwargs` (#14870) --- .../test/fixtures/flake8_bugbear/B028.py | 7 ++++++ .../rules/no_explicit_stacklevel.rs | 13 +++++++++- ...__flake8_bugbear__tests__B028_B028.py.snap | 24 +++++++++---------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py index d8f79372e2253..943f5c4d93090 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B028.py @@ -11,6 +11,13 @@ warnings.warn("test", DeprecationWarning, stacklevel=1) warnings.warn("test", DeprecationWarning, 1) warnings.warn("test", category=DeprecationWarning, stacklevel=1) +args = ("test", DeprecationWarning, 1) +warnings.warn(*args) +kwargs = {"message": "test", "category": DeprecationWarning, "stacklevel": 1} +warnings.warn(**kwargs) +args = ("test", DeprecationWarning) +kwargs = {"stacklevel": 1} +warnings.warn(*args, **kwargs) warnings.warn( "test", diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs index 50fdbf1f62d03..fb3077283f5a7 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/no_explicit_stacklevel.rs @@ -60,7 +60,18 @@ pub(crate) fn no_explicit_stacklevel(checker: &mut Checker, call: &ast::ExprCall return; } - if call.arguments.find_argument("stacklevel", 2).is_some() { + if call.arguments.find_argument("stacklevel", 2).is_some() + || call + .arguments + .args + .iter() + .any(ruff_python_ast::Expr::is_starred_expr) + || call + .arguments + .keywords + .iter() + .any(|keyword| keyword.arg.is_none()) + { return; } let mut diagnostic = Diagnostic::new(NoExplicitStacklevel, call.func.range()); diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap index 2330dbf281e39..f8d765b4e08c6 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B028_B028.py.snap @@ -44,21 +44,21 @@ B028.py:9:1: B028 [*] No explicit `stacklevel` keyword argument found 12 12 | warnings.warn("test", DeprecationWarning, 1) 13 13 | warnings.warn("test", category=DeprecationWarning, stacklevel=1) -B028.py:15:1: B028 [*] No explicit `stacklevel` keyword argument found +B028.py:22:1: B028 [*] No explicit `stacklevel` keyword argument found | -13 | warnings.warn("test", category=DeprecationWarning, stacklevel=1) -14 | -15 | warnings.warn( +20 | warnings.warn(*args, **kwargs) +21 | +22 | warnings.warn( | ^^^^^^^^^^^^^ B028 -16 | "test", -17 | DeprecationWarning, +23 | "test", +24 | DeprecationWarning, | = help: Set `stacklevel=2` ℹ Unsafe fix -16 16 | "test", -17 17 | DeprecationWarning, -18 18 | # some comments here -19 |- source = None # no trailing comma - 19 |+ source = None, stacklevel=2 # no trailing comma -20 20 | ) +23 23 | "test", +24 24 | DeprecationWarning, +25 25 | # some comments here +26 |- source = None # no trailing comma + 26 |+ source = None, stacklevel=2 # no trailing comma +27 27 | ) From 0e9427255fe3f9f42e1947a6f35af4483c101e95 Mon Sep 17 00:00:00 2001 From: Harutaka Kawamura Date: Mon, 9 Dec 2024 23:54:57 +0900 Subject: [PATCH 02/14] [`pyupgrade`] Remove unreachable code in `UP015` implementation (#14871) --- .../pyupgrade/rules/redundant_open_modes.rs | 59 ++++++------------- 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs index 1b4dccc0c27ea..68738cd6a41ef 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/redundant_open_modes.rs @@ -71,47 +71,24 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, call: &ast::ExprCall) return; } - match call.arguments.find_argument("mode", 1) { - None => { - if !call.arguments.is_empty() { - if let Some(keyword) = call.arguments.find_keyword("mode") { - if let Expr::StringLiteral(ast::ExprStringLiteral { - value: mode_param_value, - .. - }) = &keyword.value - { - if let Ok(mode) = OpenMode::from_chars(mode_param_value.chars()) { - let reduced = mode.reduce(); - if reduced != mode { - checker.diagnostics.push(create_diagnostic( - call, - &keyword.value, - reduced, - checker.tokens(), - checker.stylist(), - )); - } - } - } - } - } - } - Some(mode_param) => { - if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &mode_param { - if let Ok(mode) = OpenMode::from_chars(value.chars()) { - let reduced = mode.reduce(); - if reduced != mode { - checker.diagnostics.push(create_diagnostic( - call, - mode_param, - reduced, - checker.tokens(), - checker.stylist(), - )); - } - } - } - } + let Some(mode_param) = call.arguments.find_argument("mode", 1) else { + return; + }; + let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &mode_param else { + return; + }; + let Ok(mode) = OpenMode::from_chars(value.chars()) else { + return; + }; + let reduced = mode.reduce(); + if reduced != mode { + checker.diagnostics.push(create_diagnostic( + call, + mode_param, + reduced, + checker.tokens(), + checker.stylist(), + )); } } From aa6b812a736ad585c9c2dc54033e6a3e9e33d168 Mon Sep 17 00:00:00 2001 From: InSync Date: Mon, 9 Dec 2024 21:59:12 +0700 Subject: [PATCH 03/14] [`flake8-pyi`] Also remove `self` and `cls`'s annotation (`PYI034`) (#14801) Co-authored-by: Alex Waygood --- .../test/fixtures/flake8_pyi/PYI034.py | 40 ++- .../test/fixtures/flake8_pyi/PYI034.pyi | 39 ++- .../flake8_pyi/rules/non_self_return_type.rs | 119 +++++--- .../rules/flake8_pyi/rules/simple_defaults.rs | 2 + ...__flake8_pyi__tests__PYI034_PYI034.py.snap | 276 +++++++++++++++++- ..._flake8_pyi__tests__PYI034_PYI034.pyi.snap | 270 ++++++++++++++++- .../ruff_python_semantic/src/analyze/class.rs | 63 +++- .../src/analyze/typing.rs | 39 +++ 8 files changed, 780 insertions(+), 68 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py index 179c9db90c4c5..52a612a79e3c0 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.py @@ -8,7 +8,7 @@ from abc import ABCMeta, abstractmethod from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator from enum import EnumMeta -from typing import Any, overload +from typing import Any, Generic, ParamSpec, Type, TypeVar, TypeVarTuple, overload import typing_extensions from _typeshed import Self @@ -321,3 +321,41 @@ def __imul__(self, other: Any) -> list[str]: class UsesStringizedAnnotations: def __iadd__(self, other: "UsesStringizedAnnotations") -> "typing.Self": return self + + +class NonGeneric1(tuple): + def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ... + def __enter__(self: NonGeneric1) -> NonGeneric1: ... + +class NonGeneric2(tuple): + def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ... + +class Generic1[T](list): + def __new__(cls: type[Generic1]) -> Generic1: ... + def __enter__(self: Generic1) -> Generic1: ... + + +### Correctness of typevar-likes are not verified. + +T = TypeVar('T') +P = ParamSpec() +Ts = TypeVarTuple('foo') + +class Generic2(Generic[T]): + def __new__(cls: type[Generic2]) -> Generic2: ... + def __enter__(self: Generic2) -> Generic2: ... + +class Generic3(tuple[*Ts]): + def __new__(cls: type[Generic3]) -> Generic3: ... + def __enter__(self: Generic3) -> Generic3: ... + +class Generic4(collections.abc.Callable[P, ...]): + def __new__(cls: type[Generic4]) -> Generic4: ... + def __enter__(self: Generic4) -> Generic4: ... + +from some_module import PotentialTypeVar + +class Generic5(list[PotentialTypeVar]): + def __new__(cls: type[Generic5]) -> Generic5: ... + def __enter__(self: Generic5) -> Generic5: ... + diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi index ebecadfbd50cc..9567b343ac7cf 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI034.pyi @@ -8,7 +8,7 @@ import typing from abc import ABCMeta, abstractmethod from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator from enum import EnumMeta -from typing import Any, overload +from typing import Any, Generic, ParamSpec, Type, TypeVar, TypeVarTuple, overload import typing_extensions from _typeshed import Self @@ -215,3 +215,40 @@ def __imul__(self, other: Any) -> list[str]: ... class UsesStringizedAnnotations: def __iadd__(self, other: "UsesStringizedAnnotations") -> "typing.Self": ... + + +class NonGeneric1(tuple): + def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ... + def __enter__(self: NonGeneric1) -> NonGeneric1: ... + +class NonGeneric2(tuple): + def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ... + +class Generic1[T](list): + def __new__(cls: type[Generic1]) -> Generic1: ... + def __enter__(self: Generic1) -> Generic1: ... + + +### Correctness of typevar-likes are not verified. + +T = TypeVar('T') +P = ParamSpec() +Ts = TypeVarTuple('foo') + +class Generic2(Generic[T]): + def __new__(cls: type[Generic2]) -> Generic2: ... + def __enter__(self: Generic2) -> Generic2: ... + +class Generic3(tuple[*Ts]): + def __new__(cls: type[Generic3]) -> Generic3: ... + def __enter__(self: Generic3) -> Generic3: ... + +class Generic4(collections.abc.Callable[P, ...]): + def __new__(cls: type[Generic4]) -> Generic4: ... + def __enter__(self: Generic4) -> Generic4: ... + +from some_module import PotentialTypeVar + +class Generic5(list[PotentialTypeVar]): + def __new__(cls: type[Generic5]) -> Generic5: ... + def __enter__(self: Generic5) -> Generic5: ... diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index d8c79dacc8a2d..3d4155af83767 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -1,15 +1,16 @@ -use ruff_python_ast::{self as ast, Decorator, Expr, Parameters, Stmt}; - use crate::checkers::ast::Checker; use crate::importer::ImportRequest; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use crate::settings::types::PythonVersion; +use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast as ast; use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze; +use ruff_python_semantic::analyze::class::might_be_generic; use ruff_python_semantic::analyze::visibility::{is_abstract, is_final, is_overload}; use ruff_python_semantic::{ScopeKind, SemanticModel}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::Ranged; /// ## What it does /// Checks for methods that are annotated with a fixed return type which @@ -71,6 +72,10 @@ use ruff_text_size::{Ranged, TextRange}; /// async def __aenter__(self) -> Self: ... /// def __iadd__(self, other: Foo) -> Self: ... /// ``` +/// +/// ## Fix safety +/// This rule's fix is marked as unsafe as it changes the meaning of your type annotations. +/// /// ## References /// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) #[derive(ViolationMetadata)] @@ -104,12 +109,12 @@ impl Violation for NonSelfReturnType { /// PYI034 pub(crate) fn non_self_return_type( checker: &mut Checker, - stmt: &Stmt, + stmt: &ast::Stmt, is_async: bool, name: &str, - decorator_list: &[Decorator], - returns: Option<&Expr>, - parameters: &Parameters, + decorator_list: &[ast::Decorator], + returns: Option<&ast::Expr>, + parameters: &ast::Parameters, ) { let semantic = checker.semantic(); @@ -126,7 +131,7 @@ pub(crate) fn non_self_return_type( }; // PEP 673 forbids the use of `typing(_extensions).Self` in metaclasses. - if analyze::class::is_metaclass(class_def, semantic).into() { + if analyze::class::is_metaclass(class_def, semantic).is_yes() { return; } @@ -183,15 +188,34 @@ pub(crate) fn non_self_return_type( /// Add a diagnostic for the given method. fn add_diagnostic( checker: &mut Checker, - stmt: &Stmt, - returns: &Expr, + stmt: &ast::Stmt, + returns: &ast::Expr, class_def: &ast::StmtClassDef, method_name: &str, ) { - /// Return an [`Edit`] that imports `typing.Self` from `typing` or `typing_extensions`. - fn import_self(checker: &Checker, range: TextRange) -> Option { - let target_version = checker.settings.target_version.as_tuple(); - let source_module = if target_version >= (3, 11) { + let mut diagnostic = Diagnostic::new( + NonSelfReturnType { + class_name: class_def.name.to_string(), + method_name: method_name.to_string(), + }, + stmt.identifier(), + ); + + diagnostic.try_set_fix(|| replace_with_self_fix(checker, stmt, returns, class_def)); + + checker.diagnostics.push(diagnostic); +} + +fn replace_with_self_fix( + checker: &mut Checker, + stmt: &ast::Stmt, + returns: &ast::Expr, + class_def: &ast::StmtClassDef, +) -> anyhow::Result { + let semantic = checker.semantic(); + + let (self_import, self_binding) = { + let source_module = if checker.settings.target_version >= PythonVersion::Py311 { "typing" } else { "typing_extensions" @@ -199,32 +223,47 @@ fn add_diagnostic( let (importer, semantic) = (checker.importer(), checker.semantic()); let request = ImportRequest::import_from(source_module, "Self"); + importer.get_or_import_symbol(&request, returns.start(), semantic)? + }; - let (edit, ..) = importer - .get_or_import_symbol(&request, range.start(), semantic) - .ok()?; + let mut others = Vec::with_capacity(2); - Some(edit) - } + let remove_first_argument_type_hint = || -> Option { + let ast::StmtFunctionDef { parameters, .. } = stmt.as_function_def_stmt()?; + let first = parameters.iter().next()?; + let annotation = first.annotation()?; - /// Generate a [`Fix`] that replaces the return type with `Self`. - fn replace_with_self(checker: &mut Checker, range: TextRange) -> Option { - let import_self = import_self(checker, range)?; - let replace_with_self = Edit::range_replacement("Self".to_string(), range); - Some(Fix::unsafe_edits(import_self, [replace_with_self])) + is_class_reference(semantic, annotation, &class_def.name) + .then(|| Edit::deletion(first.name().end(), annotation.end())) + }; + + others.extend(remove_first_argument_type_hint()); + others.push(Edit::range_replacement(self_binding, returns.range())); + + let applicability = if might_be_generic(class_def, checker.semantic()) { + Applicability::DisplayOnly + } else { + Applicability::Unsafe + }; + + Ok(Fix::applicable_edits(self_import, others, applicability)) +} + +/// Return true if `annotation` is either `ClassName` or `type[ClassName]` +fn is_class_reference(semantic: &SemanticModel, annotation: &ast::Expr, expected: &str) -> bool { + if is_name(annotation, expected) { + return true; } - let mut diagnostic = Diagnostic::new( - NonSelfReturnType { - class_name: class_def.name.to_string(), - method_name: method_name.to_string(), - }, - stmt.identifier(), - ); - if let Some(fix) = replace_with_self(checker, returns.range()) { - diagnostic.set_fix(fix); + let ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = annotation else { + return false; + }; + + if !semantic.match_builtin_expr(value, "type") && !semantic.match_typing_expr(value, "Type") { + return false; } - checker.diagnostics.push(diagnostic); + + is_name(slice, expected) } /// Returns `true` if the method is an in-place binary operator. @@ -248,15 +287,15 @@ fn is_inplace_bin_op(name: &str) -> bool { } /// Return `true` if the given expression resolves to the given name. -fn is_name(expr: &Expr, name: &str) -> bool { - let Expr::Name(ast::ExprName { id, .. }) = expr else { +fn is_name(expr: &ast::Expr, name: &str) -> bool { + let ast::Expr::Name(ast::ExprName { id, .. }) = expr else { return false; }; id.as_str() == name } /// Return `true` if the given expression resolves to `typing.Self`. -fn is_self(expr: &Expr, checker: &Checker) -> bool { +fn is_self(expr: &ast::Expr, checker: &Checker) -> bool { checker.match_maybe_stringized_annotation(expr, |expr| { checker.semantic().match_typing_expr(expr, "Self") }) @@ -273,7 +312,7 @@ fn subclasses_iterator(class_def: &ast::StmtClassDef, semantic: &SemanticModel) } /// Return `true` if the given expression resolves to `collections.abc.Iterable` or `collections.abc.Iterator`. -fn is_iterable_or_iterator(expr: &Expr, semantic: &SemanticModel) -> bool { +fn is_iterable_or_iterator(expr: &ast::Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(map_subscript(expr)) .is_some_and(|qualified_name| { @@ -296,7 +335,7 @@ fn subclasses_async_iterator(class_def: &ast::StmtClassDef, semantic: &SemanticM } /// Return `true` if the given expression resolves to `collections.abc.AsyncIterable` or `collections.abc.AsyncIterator`. -fn is_async_iterable_or_iterator(expr: &Expr, semantic: &SemanticModel) -> bool { +fn is_async_iterable_or_iterator(expr: &ast::Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(map_subscript(expr)) .is_some_and(|qualified_name| { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs index d8d918bd72781..1ae0b94fa6617 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -425,6 +425,8 @@ fn is_valid_default_value_without_annotation(default: &Expr) -> bool { /// Returns `true` if an [`Expr`] appears to be `TypeVar`, `TypeVarTuple`, `NewType`, or `ParamSpec` /// call. +/// +/// See also [`ruff_python_semantic::analyze::typing::TypeVarLikeChecker::is_type_var_like_call`]. fn is_type_var_like_call(expr: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, .. }) = expr else { return false; diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap index 74a638f7ff569..d2b3356ff6077 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs -snapshot_kind: text --- PYI034.py:21:9: PYI034 [*] `__new__` methods in classes like `Bad` usually return `self` at runtime | @@ -17,7 +16,7 @@ PYI034.py:21:9: PYI034 [*] `__new__` methods in classes like `Bad` usually retur 19 19 | object 20 20 | ): # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3 21 |- def __new__(cls, *args: Any, **kwargs: Any) -> Bad: - 21 |+ def __new__(cls, *args: Any, **kwargs: Any) -> Self: + 21 |+ def __new__(cls, *args: Any, **kwargs: Any) -> typing.Self: 22 22 | ... # Y034 "__new__" methods usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__new__", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..." 23 23 | 24 24 | def __repr__(self) -> str: @@ -37,7 +36,7 @@ PYI034.py:36:9: PYI034 [*] `__enter__` methods in classes like `Bad` usually ret 34 34 | ... # Y032 Prefer "object" to "Any" for the second parameter in "__ne__" methods 35 35 | 36 |- def __enter__(self) -> Bad: - 36 |+ def __enter__(self) -> Self: + 36 |+ def __enter__(self) -> typing.Self: 37 37 | ... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..." 38 38 | 39 39 | async def __aenter__(self) -> Bad: @@ -57,7 +56,7 @@ PYI034.py:39:15: PYI034 [*] `__aenter__` methods in classes like `Bad` usually r 37 37 | ... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..." 38 38 | 39 |- async def __aenter__(self) -> Bad: - 39 |+ async def __aenter__(self) -> Self: + 39 |+ async def __aenter__(self) -> typing.Self: 40 40 | ... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..." 41 41 | 42 42 | def __iadd__(self, other: Bad) -> Bad: @@ -77,7 +76,7 @@ PYI034.py:42:9: PYI034 [*] `__iadd__` methods in classes like `Bad` usually retu 40 40 | ... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..." 41 41 | 42 |- def __iadd__(self, other: Bad) -> Bad: - 42 |+ def __iadd__(self, other: Bad) -> Self: + 42 |+ def __iadd__(self, other: Bad) -> typing.Self: 43 43 | ... # Y034 "__iadd__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__iadd__", e.g. "def __iadd__(self, other: Bad) -> Self: ..." 44 44 | 45 45 | @@ -96,7 +95,7 @@ PYI034.py:165:9: PYI034 [*] `__iter__` methods in classes like `BadIterator1` us 163 163 | 164 164 | class BadIterator1(Iterator[int]): 165 |- def __iter__(self) -> Iterator[int]: - 165 |+ def __iter__(self) -> Self: + 165 |+ def __iter__(self) -> typing.Self: 166 166 | ... # Y034 "__iter__" methods in classes like "BadIterator1" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator1.__iter__", e.g. "def __iter__(self) -> Self: ..." 167 167 | 168 168 | @@ -116,7 +115,7 @@ PYI034.py:172:9: PYI034 [*] `__iter__` methods in classes like `BadIterator2` us 170 170 | typing.Iterator[int] 171 171 | ): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax) 172 |- def __iter__(self) -> Iterator[int]: - 172 |+ def __iter__(self) -> Self: + 172 |+ def __iter__(self) -> typing.Self: 173 173 | ... # Y034 "__iter__" methods in classes like "BadIterator2" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator2.__iter__", e.g. "def __iter__(self) -> Self: ..." 174 174 | 175 175 | @@ -136,7 +135,7 @@ PYI034.py:179:9: PYI034 [*] `__iter__` methods in classes like `BadIterator3` us 177 177 | typing.Iterator[int] 178 178 | ): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax) 179 |- def __iter__(self) -> collections.abc.Iterator[int]: - 179 |+ def __iter__(self) -> Self: + 179 |+ def __iter__(self) -> typing.Self: 180 180 | ... # Y034 "__iter__" methods in classes like "BadIterator3" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator3.__iter__", e.g. "def __iter__(self) -> Self: ..." 181 181 | 182 182 | @@ -156,7 +155,7 @@ PYI034.py:185:9: PYI034 [*] `__iter__` methods in classes like `BadIterator4` us 183 183 | class BadIterator4(Iterator[int]): 184 184 | # Note: *Iterable*, not *Iterator*, returned! 185 |- def __iter__(self) -> Iterable[int]: - 185 |+ def __iter__(self) -> Self: + 185 |+ def __iter__(self) -> typing.Self: 186 186 | ... # Y034 "__iter__" methods in classes like "BadIterator4" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator4.__iter__", e.g. "def __iter__(self) -> Self: ..." 187 187 | 188 188 | @@ -175,7 +174,7 @@ PYI034.py:195:9: PYI034 [*] `__aiter__` methods in classes like `BadAsyncIterato 193 193 | 194 194 | class BadAsyncIterator(collections.abc.AsyncIterator[str]): 195 |- def __aiter__(self) -> typing.AsyncIterator[str]: - 195 |+ def __aiter__(self) -> Self: + 195 |+ def __aiter__(self) -> typing.Self: 196 196 | ... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self) -> Self: ..." # Y022 Use "collections.abc.AsyncIterator[T]" instead of "typing.AsyncIterator[T]" (PEP 585 syntax) 197 197 | 198 198 | class SubclassOfBadIterator3(BadIterator3): @@ -194,7 +193,7 @@ PYI034.py:199:9: PYI034 [*] `__iter__` methods in classes like `SubclassOfBadIte 197 197 | 198 198 | class SubclassOfBadIterator3(BadIterator3): 199 |- def __iter__(self) -> Iterator[int]: # Y034 - 199 |+ def __iter__(self) -> Self: # Y034 + 199 |+ def __iter__(self) -> typing.Self: # Y034 200 200 | ... 201 201 | 202 202 | class SubclassOfBadAsyncIterator(BadAsyncIterator): @@ -213,7 +212,260 @@ PYI034.py:203:9: PYI034 [*] `__aiter__` methods in classes like `SubclassOfBadAs 201 201 | 202 202 | class SubclassOfBadAsyncIterator(BadAsyncIterator): 203 |- def __aiter__(self) -> collections.abc.AsyncIterator[str]: # Y034 - 203 |+ def __aiter__(self) -> Self: # Y034 + 203 |+ def __aiter__(self) -> typing.Self: # Y034 204 204 | ... 205 205 | 206 206 | class AsyncIteratorReturningAsyncIterable: + +PYI034.py:327:9: PYI034 [*] `__new__` methods in classes like `NonGeneric1` usually return `self` at runtime + | +326 | class NonGeneric1(tuple): +327 | def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ... + | ^^^^^^^ PYI034 +328 | def __enter__(self: NonGeneric1) -> NonGeneric1: ... + | + = help: Use `Self` as return type + +ℹ Unsafe fix +324 324 | +325 325 | +326 326 | class NonGeneric1(tuple): +327 |- def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ... + 327 |+ def __new__(cls, *args, **kwargs) -> typing.Self: ... +328 328 | def __enter__(self: NonGeneric1) -> NonGeneric1: ... +329 329 | +330 330 | class NonGeneric2(tuple): + +PYI034.py:328:9: PYI034 [*] `__enter__` methods in classes like `NonGeneric1` usually return `self` at runtime + | +326 | class NonGeneric1(tuple): +327 | def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ... +328 | def __enter__(self: NonGeneric1) -> NonGeneric1: ... + | ^^^^^^^^^ PYI034 +329 | +330 | class NonGeneric2(tuple): + | + = help: Use `Self` as return type + +ℹ Unsafe fix +325 325 | +326 326 | class NonGeneric1(tuple): +327 327 | def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ... +328 |- def __enter__(self: NonGeneric1) -> NonGeneric1: ... + 328 |+ def __enter__(self) -> typing.Self: ... +329 329 | +330 330 | class NonGeneric2(tuple): +331 331 | def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ... + +PYI034.py:331:9: PYI034 [*] `__new__` methods in classes like `NonGeneric2` usually return `self` at runtime + | +330 | class NonGeneric2(tuple): +331 | def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ... + | ^^^^^^^ PYI034 +332 | +333 | class Generic1[T](list): + | + = help: Use `Self` as return type + +ℹ Unsafe fix +328 328 | def __enter__(self: NonGeneric1) -> NonGeneric1: ... +329 329 | +330 330 | class NonGeneric2(tuple): +331 |- def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ... + 331 |+ def __new__(cls) -> typing.Self: ... +332 332 | +333 333 | class Generic1[T](list): +334 334 | def __new__(cls: type[Generic1]) -> Generic1: ... + +PYI034.py:334:9: PYI034 `__new__` methods in classes like `Generic1` usually return `self` at runtime + | +333 | class Generic1[T](list): +334 | def __new__(cls: type[Generic1]) -> Generic1: ... + | ^^^^^^^ PYI034 +335 | def __enter__(self: Generic1) -> Generic1: ... + | + = help: Use `Self` as return type + +ℹ Display-only fix +331 331 | def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ... +332 332 | +333 333 | class Generic1[T](list): +334 |- def __new__(cls: type[Generic1]) -> Generic1: ... + 334 |+ def __new__(cls) -> typing.Self: ... +335 335 | def __enter__(self: Generic1) -> Generic1: ... +336 336 | +337 337 | + +PYI034.py:335:9: PYI034 `__enter__` methods in classes like `Generic1` usually return `self` at runtime + | +333 | class Generic1[T](list): +334 | def __new__(cls: type[Generic1]) -> Generic1: ... +335 | def __enter__(self: Generic1) -> Generic1: ... + | ^^^^^^^^^ PYI034 + | + = help: Use `Self` as return type + +ℹ Display-only fix +332 332 | +333 333 | class Generic1[T](list): +334 334 | def __new__(cls: type[Generic1]) -> Generic1: ... +335 |- def __enter__(self: Generic1) -> Generic1: ... + 335 |+ def __enter__(self) -> typing.Self: ... +336 336 | +337 337 | +338 338 | ### Correctness of typevar-likes are not verified. + +PYI034.py:345:9: PYI034 `__new__` methods in classes like `Generic2` usually return `self` at runtime + | +344 | class Generic2(Generic[T]): +345 | def __new__(cls: type[Generic2]) -> Generic2: ... + | ^^^^^^^ PYI034 +346 | def __enter__(self: Generic2) -> Generic2: ... + | + = help: Use `Self` as return type + +ℹ Display-only fix +342 342 | Ts = TypeVarTuple('foo') +343 343 | +344 344 | class Generic2(Generic[T]): +345 |- def __new__(cls: type[Generic2]) -> Generic2: ... + 345 |+ def __new__(cls) -> typing.Self: ... +346 346 | def __enter__(self: Generic2) -> Generic2: ... +347 347 | +348 348 | class Generic3(tuple[*Ts]): + +PYI034.py:346:9: PYI034 `__enter__` methods in classes like `Generic2` usually return `self` at runtime + | +344 | class Generic2(Generic[T]): +345 | def __new__(cls: type[Generic2]) -> Generic2: ... +346 | def __enter__(self: Generic2) -> Generic2: ... + | ^^^^^^^^^ PYI034 +347 | +348 | class Generic3(tuple[*Ts]): + | + = help: Use `Self` as return type + +ℹ Display-only fix +343 343 | +344 344 | class Generic2(Generic[T]): +345 345 | def __new__(cls: type[Generic2]) -> Generic2: ... +346 |- def __enter__(self: Generic2) -> Generic2: ... + 346 |+ def __enter__(self) -> typing.Self: ... +347 347 | +348 348 | class Generic3(tuple[*Ts]): +349 349 | def __new__(cls: type[Generic3]) -> Generic3: ... + +PYI034.py:349:9: PYI034 `__new__` methods in classes like `Generic3` usually return `self` at runtime + | +348 | class Generic3(tuple[*Ts]): +349 | def __new__(cls: type[Generic3]) -> Generic3: ... + | ^^^^^^^ PYI034 +350 | def __enter__(self: Generic3) -> Generic3: ... + | + = help: Use `Self` as return type + +ℹ Display-only fix +346 346 | def __enter__(self: Generic2) -> Generic2: ... +347 347 | +348 348 | class Generic3(tuple[*Ts]): +349 |- def __new__(cls: type[Generic3]) -> Generic3: ... + 349 |+ def __new__(cls) -> typing.Self: ... +350 350 | def __enter__(self: Generic3) -> Generic3: ... +351 351 | +352 352 | class Generic4(collections.abc.Callable[P, ...]): + +PYI034.py:350:9: PYI034 `__enter__` methods in classes like `Generic3` usually return `self` at runtime + | +348 | class Generic3(tuple[*Ts]): +349 | def __new__(cls: type[Generic3]) -> Generic3: ... +350 | def __enter__(self: Generic3) -> Generic3: ... + | ^^^^^^^^^ PYI034 +351 | +352 | class Generic4(collections.abc.Callable[P, ...]): + | + = help: Use `Self` as return type + +ℹ Display-only fix +347 347 | +348 348 | class Generic3(tuple[*Ts]): +349 349 | def __new__(cls: type[Generic3]) -> Generic3: ... +350 |- def __enter__(self: Generic3) -> Generic3: ... + 350 |+ def __enter__(self) -> typing.Self: ... +351 351 | +352 352 | class Generic4(collections.abc.Callable[P, ...]): +353 353 | def __new__(cls: type[Generic4]) -> Generic4: ... + +PYI034.py:353:9: PYI034 `__new__` methods in classes like `Generic4` usually return `self` at runtime + | +352 | class Generic4(collections.abc.Callable[P, ...]): +353 | def __new__(cls: type[Generic4]) -> Generic4: ... + | ^^^^^^^ PYI034 +354 | def __enter__(self: Generic4) -> Generic4: ... + | + = help: Use `Self` as return type + +ℹ Display-only fix +350 350 | def __enter__(self: Generic3) -> Generic3: ... +351 351 | +352 352 | class Generic4(collections.abc.Callable[P, ...]): +353 |- def __new__(cls: type[Generic4]) -> Generic4: ... + 353 |+ def __new__(cls) -> typing.Self: ... +354 354 | def __enter__(self: Generic4) -> Generic4: ... +355 355 | +356 356 | from some_module import PotentialTypeVar + +PYI034.py:354:9: PYI034 `__enter__` methods in classes like `Generic4` usually return `self` at runtime + | +352 | class Generic4(collections.abc.Callable[P, ...]): +353 | def __new__(cls: type[Generic4]) -> Generic4: ... +354 | def __enter__(self: Generic4) -> Generic4: ... + | ^^^^^^^^^ PYI034 +355 | +356 | from some_module import PotentialTypeVar + | + = help: Use `Self` as return type + +ℹ Display-only fix +351 351 | +352 352 | class Generic4(collections.abc.Callable[P, ...]): +353 353 | def __new__(cls: type[Generic4]) -> Generic4: ... +354 |- def __enter__(self: Generic4) -> Generic4: ... + 354 |+ def __enter__(self) -> typing.Self: ... +355 355 | +356 356 | from some_module import PotentialTypeVar +357 357 | + +PYI034.py:359:9: PYI034 [*] `__new__` methods in classes like `Generic5` usually return `self` at runtime + | +358 | class Generic5(list[PotentialTypeVar]): +359 | def __new__(cls: type[Generic5]) -> Generic5: ... + | ^^^^^^^ PYI034 +360 | def __enter__(self: Generic5) -> Generic5: ... + | + = help: Use `Self` as return type + +ℹ Unsafe fix +356 356 | from some_module import PotentialTypeVar +357 357 | +358 358 | class Generic5(list[PotentialTypeVar]): +359 |- def __new__(cls: type[Generic5]) -> Generic5: ... + 359 |+ def __new__(cls) -> typing.Self: ... +360 360 | def __enter__(self: Generic5) -> Generic5: ... +361 361 | + +PYI034.py:360:9: PYI034 [*] `__enter__` methods in classes like `Generic5` usually return `self` at runtime + | +358 | class Generic5(list[PotentialTypeVar]): +359 | def __new__(cls: type[Generic5]) -> Generic5: ... +360 | def __enter__(self: Generic5) -> Generic5: ... + | ^^^^^^^^^ PYI034 + | + = help: Use `Self` as return type + +ℹ Unsafe fix +357 357 | +358 358 | class Generic5(list[PotentialTypeVar]): +359 359 | def __new__(cls: type[Generic5]) -> Generic5: ... +360 |- def __enter__(self: Generic5) -> Generic5: ... + 360 |+ def __enter__(self) -> typing.Self: ... +361 361 | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap index 11c9ca99c6a95..941c11fe99173 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI034_PYI034.pyi.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs -snapshot_kind: text --- PYI034.pyi:20:9: PYI034 [*] `__new__` methods in classes like `Bad` usually return `self` at runtime | @@ -18,7 +17,7 @@ PYI034.pyi:20:9: PYI034 [*] `__new__` methods in classes like `Bad` usually retu 20 20 | def __new__( 21 21 | cls, *args: Any, **kwargs: Any 22 |- ) -> Bad: ... # Y034 "__new__" methods usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__new__", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..." - 22 |+ ) -> Self: ... # Y034 "__new__" methods usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__new__", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..." + 22 |+ ) -> typing.Self: ... # Y034 "__new__" methods usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__new__", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..." 23 23 | def __repr__( 24 24 | self, 25 25 | ) -> str: ... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant @@ -39,7 +38,7 @@ PYI034.pyi:35:9: PYI034 [*] `__enter__` methods in classes like `Bad` usually re 35 35 | def __enter__( 36 36 | self, 37 |- ) -> Bad: ... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..." - 37 |+ ) -> Self: ... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..." + 37 |+ ) -> typing.Self: ... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..." 38 38 | async def __aenter__( 39 39 | self, 40 40 | ) -> Bad: ... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..." @@ -60,7 +59,7 @@ PYI034.pyi:38:15: PYI034 [*] `__aenter__` methods in classes like `Bad` usually 38 38 | async def __aenter__( 39 39 | self, 40 |- ) -> Bad: ... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..." - 40 |+ ) -> Self: ... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..." + 40 |+ ) -> typing.Self: ... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..." 41 41 | def __iadd__( 42 42 | self, other: Bad 43 43 | ) -> Bad: ... # Y034 "__iadd__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__iadd__", e.g. "def __iadd__(self, other: Bad) -> Self: ..." @@ -81,7 +80,7 @@ PYI034.pyi:41:9: PYI034 [*] `__iadd__` methods in classes like `Bad` usually ret 41 41 | def __iadd__( 42 42 | self, other: Bad 43 |- ) -> Bad: ... # Y034 "__iadd__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__iadd__", e.g. "def __iadd__(self, other: Bad) -> Self: ..." - 43 |+ ) -> Self: ... # Y034 "__iadd__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__iadd__", e.g. "def __iadd__(self, other: Bad) -> Self: ..." + 43 |+ ) -> typing.Self: ... # Y034 "__iadd__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__iadd__", e.g. "def __iadd__(self, other: Bad) -> Self: ..." 44 44 | 45 45 | class AlsoBad( 46 46 | int, builtins.object @@ -103,7 +102,7 @@ PYI034.pyi:104:9: PYI034 [*] `__iter__` methods in classes like `BadIterator1` u 106 |- ) -> Iterator[ 107 |- int 108 |- ]: ... # Y034 "__iter__" methods in classes like "BadIterator1" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator1.__iter__", e.g. "def __iter__(self) -> Self: ..." - 106 |+ ) -> Self: ... # Y034 "__iter__" methods in classes like "BadIterator1" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator1.__iter__", e.g. "def __iter__(self) -> Self: ..." + 106 |+ ) -> typing.Self: ... # Y034 "__iter__" methods in classes like "BadIterator1" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator1.__iter__", e.g. "def __iter__(self) -> Self: ..." 109 107 | 110 108 | class BadIterator2( 111 109 | typing.Iterator[int] @@ -126,7 +125,7 @@ PYI034.pyi:113:9: PYI034 [*] `__iter__` methods in classes like `BadIterator2` u 115 |- ) -> Iterator[ 116 |- int 117 |- ]: ... # Y034 "__iter__" methods in classes like "BadIterator2" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator2.__iter__", e.g. "def __iter__(self) -> Self: ..." - 115 |+ ) -> Self: ... # Y034 "__iter__" methods in classes like "BadIterator2" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator2.__iter__", e.g. "def __iter__(self) -> Self: ..." + 115 |+ ) -> typing.Self: ... # Y034 "__iter__" methods in classes like "BadIterator2" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator2.__iter__", e.g. "def __iter__(self) -> Self: ..." 118 116 | 119 117 | class BadIterator3( 120 118 | typing.Iterator[int] @@ -149,7 +148,7 @@ PYI034.pyi:122:9: PYI034 [*] `__iter__` methods in classes like `BadIterator3` u 124 |- ) -> collections.abc.Iterator[ 125 |- int 126 |- ]: ... # Y034 "__iter__" methods in classes like "BadIterator3" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator3.__iter__", e.g. "def __iter__(self) -> Self: ..." - 124 |+ ) -> Self: ... # Y034 "__iter__" methods in classes like "BadIterator3" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator3.__iter__", e.g. "def __iter__(self) -> Self: ..." + 124 |+ ) -> typing.Self: ... # Y034 "__iter__" methods in classes like "BadIterator3" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator3.__iter__", e.g. "def __iter__(self) -> Self: ..." 127 125 | 128 126 | class BadIterator4(Iterator[int]): 129 127 | # Note: *Iterable*, not *Iterator*, returned! @@ -172,7 +171,7 @@ PYI034.pyi:130:9: PYI034 [*] `__iter__` methods in classes like `BadIterator4` u 132 |- ) -> Iterable[ 133 |- int 134 |- ]: ... # Y034 "__iter__" methods in classes like "BadIterator4" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator4.__iter__", e.g. "def __iter__(self) -> Self: ..." - 132 |+ ) -> Self: ... # Y034 "__iter__" methods in classes like "BadIterator4" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator4.__iter__", e.g. "def __iter__(self) -> Self: ..." + 132 |+ ) -> typing.Self: ... # Y034 "__iter__" methods in classes like "BadIterator4" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator4.__iter__", e.g. "def __iter__(self) -> Self: ..." 135 133 | 136 134 | class IteratorReturningIterable: 137 135 | def __iter__( @@ -194,7 +193,258 @@ PYI034.pyi:144:9: PYI034 [*] `__aiter__` methods in classes like `BadAsyncIterat 146 |- ) -> typing.AsyncIterator[ 147 |- str 148 |- ]: ... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self) -> Self: ..." # Y022 Use "collections.abc.AsyncIterator[T]" instead of "typing.AsyncIterator[T]" (PEP 585 syntax) - 146 |+ ) -> Self: ... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self) -> Self: ..." # Y022 Use "collections.abc.AsyncIterator[T]" instead of "typing.AsyncIterator[T]" (PEP 585 syntax) + 146 |+ ) -> typing.Self: ... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self) -> Self: ..." # Y022 Use "collections.abc.AsyncIterator[T]" instead of "typing.AsyncIterator[T]" (PEP 585 syntax) 149 147 | 150 148 | class AsyncIteratorReturningAsyncIterable: 151 149 | def __aiter__( + +PYI034.pyi:221:9: PYI034 [*] `__new__` methods in classes like `NonGeneric1` usually return `self` at runtime + | +220 | class NonGeneric1(tuple): +221 | def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ... + | ^^^^^^^ PYI034 +222 | def __enter__(self: NonGeneric1) -> NonGeneric1: ... + | + = help: Use `Self` as return type + +ℹ Unsafe fix +218 218 | +219 219 | +220 220 | class NonGeneric1(tuple): +221 |- def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ... + 221 |+ def __new__(cls, *args, **kwargs) -> typing.Self: ... +222 222 | def __enter__(self: NonGeneric1) -> NonGeneric1: ... +223 223 | +224 224 | class NonGeneric2(tuple): + +PYI034.pyi:222:9: PYI034 [*] `__enter__` methods in classes like `NonGeneric1` usually return `self` at runtime + | +220 | class NonGeneric1(tuple): +221 | def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ... +222 | def __enter__(self: NonGeneric1) -> NonGeneric1: ... + | ^^^^^^^^^ PYI034 +223 | +224 | class NonGeneric2(tuple): + | + = help: Use `Self` as return type + +ℹ Unsafe fix +219 219 | +220 220 | class NonGeneric1(tuple): +221 221 | def __new__(cls: type[NonGeneric1], *args, **kwargs) -> NonGeneric1: ... +222 |- def __enter__(self: NonGeneric1) -> NonGeneric1: ... + 222 |+ def __enter__(self) -> typing.Self: ... +223 223 | +224 224 | class NonGeneric2(tuple): +225 225 | def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ... + +PYI034.pyi:225:9: PYI034 [*] `__new__` methods in classes like `NonGeneric2` usually return `self` at runtime + | +224 | class NonGeneric2(tuple): +225 | def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ... + | ^^^^^^^ PYI034 +226 | +227 | class Generic1[T](list): + | + = help: Use `Self` as return type + +ℹ Unsafe fix +222 222 | def __enter__(self: NonGeneric1) -> NonGeneric1: ... +223 223 | +224 224 | class NonGeneric2(tuple): +225 |- def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ... + 225 |+ def __new__(cls) -> typing.Self: ... +226 226 | +227 227 | class Generic1[T](list): +228 228 | def __new__(cls: type[Generic1]) -> Generic1: ... + +PYI034.pyi:228:9: PYI034 `__new__` methods in classes like `Generic1` usually return `self` at runtime + | +227 | class Generic1[T](list): +228 | def __new__(cls: type[Generic1]) -> Generic1: ... + | ^^^^^^^ PYI034 +229 | def __enter__(self: Generic1) -> Generic1: ... + | + = help: Use `Self` as return type + +ℹ Display-only fix +225 225 | def __new__(cls: Type[NonGeneric2]) -> NonGeneric2: ... +226 226 | +227 227 | class Generic1[T](list): +228 |- def __new__(cls: type[Generic1]) -> Generic1: ... + 228 |+ def __new__(cls) -> typing.Self: ... +229 229 | def __enter__(self: Generic1) -> Generic1: ... +230 230 | +231 231 | + +PYI034.pyi:229:9: PYI034 `__enter__` methods in classes like `Generic1` usually return `self` at runtime + | +227 | class Generic1[T](list): +228 | def __new__(cls: type[Generic1]) -> Generic1: ... +229 | def __enter__(self: Generic1) -> Generic1: ... + | ^^^^^^^^^ PYI034 + | + = help: Use `Self` as return type + +ℹ Display-only fix +226 226 | +227 227 | class Generic1[T](list): +228 228 | def __new__(cls: type[Generic1]) -> Generic1: ... +229 |- def __enter__(self: Generic1) -> Generic1: ... + 229 |+ def __enter__(self) -> typing.Self: ... +230 230 | +231 231 | +232 232 | ### Correctness of typevar-likes are not verified. + +PYI034.pyi:239:9: PYI034 `__new__` methods in classes like `Generic2` usually return `self` at runtime + | +238 | class Generic2(Generic[T]): +239 | def __new__(cls: type[Generic2]) -> Generic2: ... + | ^^^^^^^ PYI034 +240 | def __enter__(self: Generic2) -> Generic2: ... + | + = help: Use `Self` as return type + +ℹ Display-only fix +236 236 | Ts = TypeVarTuple('foo') +237 237 | +238 238 | class Generic2(Generic[T]): +239 |- def __new__(cls: type[Generic2]) -> Generic2: ... + 239 |+ def __new__(cls) -> typing.Self: ... +240 240 | def __enter__(self: Generic2) -> Generic2: ... +241 241 | +242 242 | class Generic3(tuple[*Ts]): + +PYI034.pyi:240:9: PYI034 `__enter__` methods in classes like `Generic2` usually return `self` at runtime + | +238 | class Generic2(Generic[T]): +239 | def __new__(cls: type[Generic2]) -> Generic2: ... +240 | def __enter__(self: Generic2) -> Generic2: ... + | ^^^^^^^^^ PYI034 +241 | +242 | class Generic3(tuple[*Ts]): + | + = help: Use `Self` as return type + +ℹ Display-only fix +237 237 | +238 238 | class Generic2(Generic[T]): +239 239 | def __new__(cls: type[Generic2]) -> Generic2: ... +240 |- def __enter__(self: Generic2) -> Generic2: ... + 240 |+ def __enter__(self) -> typing.Self: ... +241 241 | +242 242 | class Generic3(tuple[*Ts]): +243 243 | def __new__(cls: type[Generic3]) -> Generic3: ... + +PYI034.pyi:243:9: PYI034 `__new__` methods in classes like `Generic3` usually return `self` at runtime + | +242 | class Generic3(tuple[*Ts]): +243 | def __new__(cls: type[Generic3]) -> Generic3: ... + | ^^^^^^^ PYI034 +244 | def __enter__(self: Generic3) -> Generic3: ... + | + = help: Use `Self` as return type + +ℹ Display-only fix +240 240 | def __enter__(self: Generic2) -> Generic2: ... +241 241 | +242 242 | class Generic3(tuple[*Ts]): +243 |- def __new__(cls: type[Generic3]) -> Generic3: ... + 243 |+ def __new__(cls) -> typing.Self: ... +244 244 | def __enter__(self: Generic3) -> Generic3: ... +245 245 | +246 246 | class Generic4(collections.abc.Callable[P, ...]): + +PYI034.pyi:244:9: PYI034 `__enter__` methods in classes like `Generic3` usually return `self` at runtime + | +242 | class Generic3(tuple[*Ts]): +243 | def __new__(cls: type[Generic3]) -> Generic3: ... +244 | def __enter__(self: Generic3) -> Generic3: ... + | ^^^^^^^^^ PYI034 +245 | +246 | class Generic4(collections.abc.Callable[P, ...]): + | + = help: Use `Self` as return type + +ℹ Display-only fix +241 241 | +242 242 | class Generic3(tuple[*Ts]): +243 243 | def __new__(cls: type[Generic3]) -> Generic3: ... +244 |- def __enter__(self: Generic3) -> Generic3: ... + 244 |+ def __enter__(self) -> typing.Self: ... +245 245 | +246 246 | class Generic4(collections.abc.Callable[P, ...]): +247 247 | def __new__(cls: type[Generic4]) -> Generic4: ... + +PYI034.pyi:247:9: PYI034 `__new__` methods in classes like `Generic4` usually return `self` at runtime + | +246 | class Generic4(collections.abc.Callable[P, ...]): +247 | def __new__(cls: type[Generic4]) -> Generic4: ... + | ^^^^^^^ PYI034 +248 | def __enter__(self: Generic4) -> Generic4: ... + | + = help: Use `Self` as return type + +ℹ Display-only fix +244 244 | def __enter__(self: Generic3) -> Generic3: ... +245 245 | +246 246 | class Generic4(collections.abc.Callable[P, ...]): +247 |- def __new__(cls: type[Generic4]) -> Generic4: ... + 247 |+ def __new__(cls) -> typing.Self: ... +248 248 | def __enter__(self: Generic4) -> Generic4: ... +249 249 | +250 250 | from some_module import PotentialTypeVar + +PYI034.pyi:248:9: PYI034 `__enter__` methods in classes like `Generic4` usually return `self` at runtime + | +246 | class Generic4(collections.abc.Callable[P, ...]): +247 | def __new__(cls: type[Generic4]) -> Generic4: ... +248 | def __enter__(self: Generic4) -> Generic4: ... + | ^^^^^^^^^ PYI034 +249 | +250 | from some_module import PotentialTypeVar + | + = help: Use `Self` as return type + +ℹ Display-only fix +245 245 | +246 246 | class Generic4(collections.abc.Callable[P, ...]): +247 247 | def __new__(cls: type[Generic4]) -> Generic4: ... +248 |- def __enter__(self: Generic4) -> Generic4: ... + 248 |+ def __enter__(self) -> typing.Self: ... +249 249 | +250 250 | from some_module import PotentialTypeVar +251 251 | + +PYI034.pyi:253:9: PYI034 `__new__` methods in classes like `Generic5` usually return `self` at runtime + | +252 | class Generic5(list[PotentialTypeVar]): +253 | def __new__(cls: type[Generic5]) -> Generic5: ... + | ^^^^^^^ PYI034 +254 | def __enter__(self: Generic5) -> Generic5: ... + | + = help: Use `Self` as return type + +ℹ Display-only fix +250 250 | from some_module import PotentialTypeVar +251 251 | +252 252 | class Generic5(list[PotentialTypeVar]): +253 |- def __new__(cls: type[Generic5]) -> Generic5: ... + 253 |+ def __new__(cls) -> typing.Self: ... +254 254 | def __enter__(self: Generic5) -> Generic5: ... + +PYI034.pyi:254:9: PYI034 `__enter__` methods in classes like `Generic5` usually return `self` at runtime + | +252 | class Generic5(list[PotentialTypeVar]): +253 | def __new__(cls: type[Generic5]) -> Generic5: ... +254 | def __enter__(self: Generic5) -> Generic5: ... + | ^^^^^^^^^ PYI034 + | + = help: Use `Self` as return type + +ℹ Display-only fix +251 251 | +252 252 | class Generic5(list[PotentialTypeVar]): +253 253 | def __new__(cls: type[Generic5]) -> Generic5: ... +254 |- def __enter__(self: Generic5) -> Generic5: ... + 254 |+ def __enter__(self) -> typing.Self: ... diff --git a/crates/ruff_python_semantic/src/analyze/class.rs b/crates/ruff_python_semantic/src/analyze/class.rs index e4419031d040a..64e49421a7330 100644 --- a/crates/ruff_python_semantic/src/analyze/class.rs +++ b/crates/ruff_python_semantic/src/analyze/class.rs @@ -1,10 +1,11 @@ use rustc_hash::FxHashSet; +use crate::analyze::typing; use crate::{BindingId, SemanticModel}; use ruff_python_ast as ast; use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::name::QualifiedName; -use ruff_python_ast::Expr; +use ruff_python_ast::{Expr, ExprName, ExprStarred, ExprSubscript, ExprTuple}; /// Return `true` if any base class matches a [`QualifiedName`] predicate. pub fn any_qualified_base_class( @@ -129,9 +130,9 @@ pub enum IsMetaclass { Maybe, } -impl From for bool { - fn from(value: IsMetaclass) -> Self { - matches!(value, IsMetaclass::Yes) +impl IsMetaclass { + pub const fn is_yes(self) -> bool { + matches!(self, IsMetaclass::Yes) } } @@ -170,3 +171,57 @@ pub fn is_metaclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> (false, _) => IsMetaclass::No, } } + +/// Returns true if a class might generic. +/// +/// A class is considered generic if at least one of its direct bases +/// is subscripted with a `TypeVar`-like, +/// or if it is defined using PEP 695 syntax. +/// +/// Therefore, a class *might* be generic if it uses PEP-695 syntax +/// or at least one of its direct bases is a subscript expression that +/// is subscripted with an object that *might* be a `TypeVar`-like. +pub fn might_be_generic(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { + if class_def.type_params.is_some() { + return true; + } + + class_def.bases().iter().any(|base| { + let Expr::Subscript(ExprSubscript { slice, .. }) = base else { + return false; + }; + + let Expr::Tuple(ExprTuple { elts, .. }) = slice.as_ref() else { + return expr_might_be_typevar_like(slice, semantic); + }; + + elts.iter() + .any(|elt| expr_might_be_typevar_like(elt, semantic)) + }) +} + +fn expr_might_be_typevar_like(expr: &Expr, semantic: &SemanticModel) -> bool { + is_known_typevar(expr, semantic) || expr_might_be_old_style_typevar_like(expr, semantic) +} + +fn is_known_typevar(expr: &Expr, semantic: &SemanticModel) -> bool { + semantic.match_typing_expr(expr, "AnyStr") +} + +fn expr_might_be_old_style_typevar_like(expr: &Expr, semantic: &SemanticModel) -> bool { + match expr { + Expr::Attribute(..) => true, + Expr::Name(name) => might_be_old_style_typevar_like(name, semantic), + Expr::Starred(ExprStarred { value, .. }) => { + expr_might_be_old_style_typevar_like(value, semantic) + } + _ => false, + } +} + +fn might_be_old_style_typevar_like(name: &ExprName, semantic: &SemanticModel) -> bool { + let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { + return !semantic.has_builtin_binding(&name.id); + }; + typing::is_type_var_like(binding, semantic) +} diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 3620a11822e8e..1dc71ab700504 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -778,6 +778,40 @@ impl TypeChecker for PathlibPathChecker { } } +pub struct TypeVarLikeChecker; + +impl TypeVarLikeChecker { + /// Returns `true` if an [`Expr`] is a `TypeVar`, `TypeVarTuple`, or `ParamSpec` call. + /// + /// See also [`ruff_linter::rules::flake8_pyi::rules::simple_defaults::is_type_var_like_call`]. + fn is_type_var_like_call(expr: &Expr, semantic: &SemanticModel) -> bool { + let Expr::Call(ast::ExprCall { func, .. }) = expr else { + return false; + }; + let Some(qualified_name) = semantic.resolve_qualified_name(func) else { + return false; + }; + + matches!( + qualified_name.segments(), + [ + "typing" | "typing_extensions", + "TypeVar" | "TypeVarTuple" | "ParamSpec" + ] + ) + } +} + +impl TypeChecker for TypeVarLikeChecker { + fn match_annotation(_annotation: &Expr, _semantic: &SemanticModel) -> bool { + false + } + + fn match_initializer(initializer: &Expr, semantic: &SemanticModel) -> bool { + Self::is_type_var_like_call(initializer, semantic) + } +} + /// Test whether the given binding can be considered a list. /// /// For this, we check what value might be associated with it through it's initialization and @@ -867,6 +901,11 @@ pub fn is_pathlib_path(binding: &Binding, semantic: &SemanticModel) -> bool { check_type::(binding, semantic) } +/// Test whether the given binding is for an old-style `TypeVar`, `TypeVarTuple` or a `ParamSpec`. +pub fn is_type_var_like(binding: &Binding, semantic: &SemanticModel) -> bool { + check_type::(binding, semantic) +} + /// Find the [`ParameterWithDefault`] corresponding to the given [`Binding`]. #[inline] fn find_parameter<'a>( From 0f4350e10ec71cdad8d42a52156b05284fc0b20f Mon Sep 17 00:00:00 2001 From: InSync Date: Mon, 9 Dec 2024 22:36:42 +0700 Subject: [PATCH 04/14] Fix a typo in `class.rs` (#14877) (Accidentally introduced in #14801.) --- crates/ruff_python_semantic/src/analyze/class.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_python_semantic/src/analyze/class.rs b/crates/ruff_python_semantic/src/analyze/class.rs index 64e49421a7330..bcdaf5cbe0e00 100644 --- a/crates/ruff_python_semantic/src/analyze/class.rs +++ b/crates/ruff_python_semantic/src/analyze/class.rs @@ -172,7 +172,7 @@ pub fn is_metaclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> } } -/// Returns true if a class might generic. +/// Returns true if a class might be generic. /// /// A class is considered generic if at least one of its direct bases /// is subscripted with a `TypeVar`-like, From 68eb0a25111c23b0891afb07e68e785978963904 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:47:26 +0100 Subject: [PATCH 05/14] Stop referring to early ruff versions (#14862) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Referring to old versions has become more distracting than useful. ## Test Plan — --- crates/ruff_python_formatter/README.md | 2 +- docs/formatter.md | 6 ------ docs/linter.md | 5 ----- docs/preview.md | 2 +- docs/tutorial.md | 2 +- pyproject.toml | 1 + 6 files changed, 4 insertions(+), 14 deletions(-) diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index 83370089bc1ba..19f0c0065159e 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -23,4 +23,4 @@ For details, see [Black compatibility](https://docs.astral.sh/ruff/formatter/#bl ## Getting started -The Ruff formatter is available as of Ruff v0.1.2. Head to [The Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for usage instructions and a comparison to Black. +Head to [The Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for usage instructions and a comparison to Black. diff --git a/docs/formatter.md b/docs/formatter.md index c85b8c82c42c5..bf5d13231d1f2 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -3,8 +3,6 @@ The Ruff formatter is an extremely fast Python code formatter designed as a drop-in replacement for [Black](https://pypi.org/project/black/), available as part of the `ruff` CLI via `ruff format`. -The Ruff formatter is available as of Ruff [v0.1.2](https://astral.sh/blog/the-ruff-formatter). - ## `ruff format` `ruff format` is the primary entrypoint to the formatter. It accepts a list of files or @@ -22,10 +20,6 @@ and instead exit with a non-zero status code upon detecting any unformatted file For the full list of supported options, run `ruff format --help`. -!!! note - As of Ruff v0.1.7 the `ruff format` command uses the current working directory (`.`) as the default path to format. - See [the file discovery documentation](configuration.md#python-file-discovery) for details. - ## Philosophy The initial goal of the Ruff formatter is _not_ to innovate on code style, but rather, to innovate diff --git a/docs/linter.md b/docs/linter.md index c307bdbacabc4..ed605ee241b43 100644 --- a/docs/linter.md +++ b/docs/linter.md @@ -19,11 +19,6 @@ $ ruff check path/to/code/ # Lint all files in `path/to/code` (and any subdir For the full list of supported options, run `ruff check --help`. -!!! note - As of Ruff v0.1.7 the `ruff check` command uses the current working directory (`.`) as the default path to check. - On older versions, you must provide this manually e.g. `ruff check .`. - See [the file discovery documentation](configuration.md#python-file-discovery) for details. - ## Rule selection The set of enabled rules is controlled via the [`lint.select`](settings.md#lint_select), diff --git a/docs/preview.md b/docs/preview.md index bba1b5bd3e4e5..66a9877b0a806 100644 --- a/docs/preview.md +++ b/docs/preview.md @@ -12,7 +12,7 @@ Enabling preview mode does not on its own enable all preview rules. See the [rul Preview mode can be enabled with the `--preview` flag on the CLI or by setting `preview = true` in your Ruff configuration file. -Preview mode can be configured separately for linting and formatting (requires Ruff v0.1.1+). To enable preview lint rules without preview style formatting: +Preview mode can be configured separately for linting and formatting. To enable preview lint rules without preview style formatting: === "pyproject.toml" diff --git a/docs/tutorial.md b/docs/tutorial.md index 313745f985a2b..ea37fd56fa2a5 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -349,7 +349,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.4 + rev: v0.8.2 hooks: # Run the linter. - id: ruff diff --git a/pyproject.toml b/pyproject.toml index 8b24416182dc0..cbb173d1f022e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,6 +106,7 @@ changelog_contributors = false version_files = [ "README.md", "docs/integrations.md", + "docs/tutorial.md", "crates/ruff/Cargo.toml", "crates/ruff_linter/Cargo.toml", "crates/ruff_wasm/Cargo.toml", From c62ba48ad4509b5aa0f4578ebd723d71d70a422d Mon Sep 17 00:00:00 2001 From: InSync Date: Mon, 9 Dec 2024 22:51:27 +0700 Subject: [PATCH 06/14] [`ruff`] Do not simplify `round()` calls (`RUF046`) (#14832) ## Summary Part 1 of the big change introduced in #14828. This temporarily causes all fixes for `round(...)` to be considered unsafe, but they will eventually be enhanced. ## Test Plan `cargo nextest run` and `cargo insta test`. --- .../resources/test/fixtures/ruff/RUF046.py | 52 +- .../ruff/rules/unnecessary_cast_to_int.rs | 105 ++- ...uff__tests__preview__RUF046_RUF046.py.snap | 731 +++++++++++------- .../src/analyze/typing.rs | 13 + 4 files changed, 548 insertions(+), 353 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py index e558cf05148c3..932782e0d3194 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF046.py @@ -1,9 +1,12 @@ import math +inferred_int = 1 +inferred_float = 1. + + ### Safely fixable -# Arguments are not checked int(id()) int(len([])) int(ord(foo)) @@ -17,6 +20,15 @@ int(math.isqrt()) int(math.perm()) +int(round(1, 0)) +int(round(1, 10)) + +int(round(1)) +int(round(1, None)) + +int(round(1.)) +int(round(1., None)) + ### Unsafe @@ -24,27 +36,35 @@ int(math.floor()) int(math.trunc()) +int(round(inferred_int, 0)) +int(round(inferred_int, 10)) -### `round()` +int(round(inferred_int)) +int(round(inferred_int, None)) -## Errors -int(round(0)) -int(round(0, 0)) -int(round(0, None)) +int(round(inferred_float)) +int(round(inferred_float, None)) -int(round(0.1)) -int(round(0.1, None)) +int(round(unknown)) +int(round(unknown, None)) -# Argument type is not checked -foo = type("Foo", (), {"__round__": lambda self: 4.2})() -int(round(foo)) -int(round(foo, 0)) -int(round(foo, None)) +### No errors + +int(round(1, unknown)) +int(round(1., unknown)) + +int(round(1., 0)) +int(round(inferred_float, 0)) + +int(round(inferred_int, unknown)) +int(round(inferred_float, unknown)) + +int(round(unknown, 0)) +int(round(unknown, unknown)) -## No errors int(round(0, 3.14)) -int(round(0, non_literal)) +int(round(inferred_int, 3.14)) + int(round(0, 0), base) int(round(0, 0, extra=keyword)) -int(round(0.1, 0)) diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs index ddbe26a24ae47..b533c7ea45dcd 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_cast_to_int.rs @@ -1,7 +1,7 @@ use crate::checkers::ast::Checker; use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{Arguments, Expr, ExprCall, ExprName, ExprNumberLiteral, Number}; +use ruff_python_ast::{Arguments, Expr, ExprCall, ExprNumberLiteral, Number}; use ruff_python_semantic::analyze::typing; use ruff_python_semantic::SemanticModel; use ruff_text_size::TextRange; @@ -74,7 +74,7 @@ pub(crate) fn unnecessary_cast_to_int(checker: &mut Checker, call: &ExprCall) { // Depends on `ndigits` and `number.__round__` ["" | "builtins", "round"] => { - if let Some(fix) = replace_with_shortened_round_call(checker, outer_range, arguments) { + if let Some(fix) = replace_with_round(checker, outer_range, inner_range, arguments) { fix } else { return; @@ -89,9 +89,9 @@ pub(crate) fn unnecessary_cast_to_int(checker: &mut Checker, call: &ExprCall) { _ => return, }; - checker - .diagnostics - .push(Diagnostic::new(UnnecessaryCastToInt, call.range).with_fix(fix)); + let diagnostic = Diagnostic::new(UnnecessaryCastToInt, call.range); + + checker.diagnostics.push(diagnostic.with_fix(fix)); } fn single_argument_to_int_call<'a>( @@ -117,12 +117,29 @@ fn single_argument_to_int_call<'a>( Some(argument) } -/// Returns an [`Edit`] when the call is of any of the forms: -/// * `round(integer)`, `round(integer, 0)`, `round(integer, None)` -/// * `round(whatever)`, `round(whatever, None)` -fn replace_with_shortened_round_call( +/// The type of the first argument to `round()` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum Rounded { + InferredInt, + InferredFloat, + LiteralInt, + LiteralFloat, + Other, +} + +/// The type of the second argument to `round()` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum Ndigits { + NotGiven, + LiteralInt, + LiteralNone, + Other, +} + +fn replace_with_round( checker: &Checker, outer_range: TextRange, + inner_range: TextRange, arguments: &Arguments, ) -> Option { if arguments.len() > 2 { @@ -132,48 +149,56 @@ fn replace_with_shortened_round_call( let number = arguments.find_argument("number", 0)?; let ndigits = arguments.find_argument("ndigits", 1); - let number_is_int = match number { - Expr::Name(name) => is_int(checker.semantic(), name), - Expr::NumberLiteral(ExprNumberLiteral { value, .. }) => matches!(value, Number::Int(..)), - _ => false, - }; + let number_kind = match number { + Expr::Name(name) => { + let semantic = checker.semantic(); - match ndigits { - Some(Expr::NumberLiteral(ExprNumberLiteral { value, .. })) - if is_literal_zero(value) && number_is_int => {} - Some(Expr::NoneLiteral(_)) | None => {} - _ => return None, - }; + match semantic.only_binding(name).map(|id| semantic.binding(id)) { + Some(binding) if typing::is_int(binding, semantic) => Rounded::InferredInt, + Some(binding) if typing::is_float(binding, semantic) => Rounded::InferredFloat, + _ => Rounded::Other, + } + } - let number_expr = checker.locator().slice(number); - let new_content = format!("round({number_expr})"); + Expr::NumberLiteral(ExprNumberLiteral { value, .. }) => match value { + Number::Int(..) => Rounded::LiteralInt, + Number::Float(..) => Rounded::LiteralFloat, + Number::Complex { .. } => Rounded::Other, + }, - let applicability = if number_is_int { - Applicability::Safe - } else { - Applicability::Unsafe + _ => Rounded::Other, }; - Some(Fix::applicable_edit( - Edit::range_replacement(new_content, outer_range), - applicability, - )) -} + let ndigits_kind = match ndigits { + None => Ndigits::NotGiven, + Some(Expr::NoneLiteral(_)) => Ndigits::LiteralNone, -fn is_int(semantic: &SemanticModel, name: &ExprName) -> bool { - let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else { - return false; + Some(Expr::NumberLiteral(ExprNumberLiteral { + value: Number::Int(..), + .. + })) => Ndigits::LiteralInt, + + _ => Ndigits::Other, }; - typing::is_int(binding, semantic) -} + let applicability = match (number_kind, ndigits_kind) { + (Rounded::LiteralInt, Ndigits::LiteralInt) + | (Rounded::LiteralInt | Rounded::LiteralFloat, Ndigits::NotGiven | Ndigits::LiteralNone) => { + Applicability::Safe + } + + (Rounded::InferredInt, Ndigits::LiteralInt) + | ( + Rounded::InferredInt | Rounded::InferredFloat | Rounded::Other, + Ndigits::NotGiven | Ndigits::LiteralNone, + ) => Applicability::Unsafe, -fn is_literal_zero(value: &Number) -> bool { - let Number::Int(int) = value else { - return false; + _ => return None, }; - matches!(int.as_u8(), Some(0)) + let edit = replace_with_inner(checker, outer_range, inner_range); + + Some(Fix::applicable_edit(edit, applicability)) } fn replace_with_inner(checker: &Checker, outer_range: TextRange, inner_range: TextRange) -> Edit { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap index 22e139816bcf8..867ba99180dc2 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF046_RUF046.py.snap @@ -2,429 +2,566 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs snapshot_kind: text --- -RUF046.py:7:1: RUF046 [*] Value being casted is already an integer - | -6 | # Arguments are not checked -7 | int(id()) - | ^^^^^^^^^ RUF046 -8 | int(len([])) -9 | int(ord(foo)) - | - = help: Remove unnecessary conversion to `int` +RUF046.py:10:1: RUF046 [*] Value being casted is already an integer + | + 8 | ### Safely fixable + 9 | +10 | int(id()) + | ^^^^^^^^^ RUF046 +11 | int(len([])) +12 | int(ord(foo)) + | + = help: Remove unnecessary conversion to `int` ℹ Safe fix -4 4 | ### Safely fixable -5 5 | -6 6 | # Arguments are not checked -7 |-int(id()) - 7 |+id() -8 8 | int(len([])) -9 9 | int(ord(foo)) -10 10 | int(hash(foo, bar)) - -RUF046.py:8:1: RUF046 [*] Value being casted is already an integer - | - 6 | # Arguments are not checked - 7 | int(id()) - 8 | int(len([])) +7 7 | +8 8 | ### Safely fixable +9 9 | +10 |-int(id()) + 10 |+id() +11 11 | int(len([])) +12 12 | int(ord(foo)) +13 13 | int(hash(foo, bar)) + +RUF046.py:11:1: RUF046 [*] Value being casted is already an integer + | +10 | int(id()) +11 | int(len([])) | ^^^^^^^^^^^^ RUF046 - 9 | int(ord(foo)) -10 | int(hash(foo, bar)) +12 | int(ord(foo)) +13 | int(hash(foo, bar)) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -5 5 | -6 6 | # Arguments are not checked -7 7 | int(id()) -8 |-int(len([])) - 8 |+len([]) -9 9 | int(ord(foo)) -10 10 | int(hash(foo, bar)) -11 11 | int(int('')) - -RUF046.py:9:1: RUF046 [*] Value being casted is already an integer - | - 7 | int(id()) - 8 | int(len([])) - 9 | int(ord(foo)) +8 8 | ### Safely fixable +9 9 | +10 10 | int(id()) +11 |-int(len([])) + 11 |+len([]) +12 12 | int(ord(foo)) +13 13 | int(hash(foo, bar)) +14 14 | int(int('')) + +RUF046.py:12:1: RUF046 [*] Value being casted is already an integer + | +10 | int(id()) +11 | int(len([])) +12 | int(ord(foo)) | ^^^^^^^^^^^^^ RUF046 -10 | int(hash(foo, bar)) -11 | int(int('')) +13 | int(hash(foo, bar)) +14 | int(int('')) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -6 6 | # Arguments are not checked -7 7 | int(id()) -8 8 | int(len([])) -9 |-int(ord(foo)) - 9 |+ord(foo) -10 10 | int(hash(foo, bar)) -11 11 | int(int('')) -12 12 | +9 9 | +10 10 | int(id()) +11 11 | int(len([])) +12 |-int(ord(foo)) + 12 |+ord(foo) +13 13 | int(hash(foo, bar)) +14 14 | int(int('')) +15 15 | -RUF046.py:10:1: RUF046 [*] Value being casted is already an integer +RUF046.py:13:1: RUF046 [*] Value being casted is already an integer | - 8 | int(len([])) - 9 | int(ord(foo)) -10 | int(hash(foo, bar)) +11 | int(len([])) +12 | int(ord(foo)) +13 | int(hash(foo, bar)) | ^^^^^^^^^^^^^^^^^^^ RUF046 -11 | int(int('')) +14 | int(int('')) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -7 7 | int(id()) -8 8 | int(len([])) -9 9 | int(ord(foo)) -10 |-int(hash(foo, bar)) - 10 |+hash(foo, bar) -11 11 | int(int('')) -12 12 | -13 13 | int(math.comb()) +10 10 | int(id()) +11 11 | int(len([])) +12 12 | int(ord(foo)) +13 |-int(hash(foo, bar)) + 13 |+hash(foo, bar) +14 14 | int(int('')) +15 15 | +16 16 | int(math.comb()) -RUF046.py:11:1: RUF046 [*] Value being casted is already an integer +RUF046.py:14:1: RUF046 [*] Value being casted is already an integer | - 9 | int(ord(foo)) -10 | int(hash(foo, bar)) -11 | int(int('')) +12 | int(ord(foo)) +13 | int(hash(foo, bar)) +14 | int(int('')) | ^^^^^^^^^^^^ RUF046 -12 | -13 | int(math.comb()) +15 | +16 | int(math.comb()) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -8 8 | int(len([])) -9 9 | int(ord(foo)) -10 10 | int(hash(foo, bar)) -11 |-int(int('')) - 11 |+int('') -12 12 | -13 13 | int(math.comb()) -14 14 | int(math.factorial()) +11 11 | int(len([])) +12 12 | int(ord(foo)) +13 13 | int(hash(foo, bar)) +14 |-int(int('')) + 14 |+int('') +15 15 | +16 16 | int(math.comb()) +17 17 | int(math.factorial()) -RUF046.py:13:1: RUF046 [*] Value being casted is already an integer +RUF046.py:16:1: RUF046 [*] Value being casted is already an integer | -11 | int(int('')) -12 | -13 | int(math.comb()) +14 | int(int('')) +15 | +16 | int(math.comb()) | ^^^^^^^^^^^^^^^^ RUF046 -14 | int(math.factorial()) -15 | int(math.gcd()) +17 | int(math.factorial()) +18 | int(math.gcd()) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -10 10 | int(hash(foo, bar)) -11 11 | int(int('')) -12 12 | -13 |-int(math.comb()) - 13 |+math.comb() -14 14 | int(math.factorial()) -15 15 | int(math.gcd()) -16 16 | int(math.lcm()) +13 13 | int(hash(foo, bar)) +14 14 | int(int('')) +15 15 | +16 |-int(math.comb()) + 16 |+math.comb() +17 17 | int(math.factorial()) +18 18 | int(math.gcd()) +19 19 | int(math.lcm()) -RUF046.py:14:1: RUF046 [*] Value being casted is already an integer +RUF046.py:17:1: RUF046 [*] Value being casted is already an integer | -13 | int(math.comb()) -14 | int(math.factorial()) +16 | int(math.comb()) +17 | int(math.factorial()) | ^^^^^^^^^^^^^^^^^^^^^ RUF046 -15 | int(math.gcd()) -16 | int(math.lcm()) +18 | int(math.gcd()) +19 | int(math.lcm()) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -11 11 | int(int('')) -12 12 | -13 13 | int(math.comb()) -14 |-int(math.factorial()) - 14 |+math.factorial() -15 15 | int(math.gcd()) -16 16 | int(math.lcm()) -17 17 | int(math.isqrt()) - -RUF046.py:15:1: RUF046 [*] Value being casted is already an integer - | -13 | int(math.comb()) -14 | int(math.factorial()) -15 | int(math.gcd()) +14 14 | int(int('')) +15 15 | +16 16 | int(math.comb()) +17 |-int(math.factorial()) + 17 |+math.factorial() +18 18 | int(math.gcd()) +19 19 | int(math.lcm()) +20 20 | int(math.isqrt()) + +RUF046.py:18:1: RUF046 [*] Value being casted is already an integer + | +16 | int(math.comb()) +17 | int(math.factorial()) +18 | int(math.gcd()) | ^^^^^^^^^^^^^^^ RUF046 -16 | int(math.lcm()) -17 | int(math.isqrt()) +19 | int(math.lcm()) +20 | int(math.isqrt()) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -12 12 | -13 13 | int(math.comb()) -14 14 | int(math.factorial()) -15 |-int(math.gcd()) - 15 |+math.gcd() -16 16 | int(math.lcm()) -17 17 | int(math.isqrt()) -18 18 | int(math.perm()) - -RUF046.py:16:1: RUF046 [*] Value being casted is already an integer - | -14 | int(math.factorial()) -15 | int(math.gcd()) -16 | int(math.lcm()) +15 15 | +16 16 | int(math.comb()) +17 17 | int(math.factorial()) +18 |-int(math.gcd()) + 18 |+math.gcd() +19 19 | int(math.lcm()) +20 20 | int(math.isqrt()) +21 21 | int(math.perm()) + +RUF046.py:19:1: RUF046 [*] Value being casted is already an integer + | +17 | int(math.factorial()) +18 | int(math.gcd()) +19 | int(math.lcm()) | ^^^^^^^^^^^^^^^ RUF046 -17 | int(math.isqrt()) -18 | int(math.perm()) +20 | int(math.isqrt()) +21 | int(math.perm()) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -13 13 | int(math.comb()) -14 14 | int(math.factorial()) -15 15 | int(math.gcd()) -16 |-int(math.lcm()) - 16 |+math.lcm() -17 17 | int(math.isqrt()) -18 18 | int(math.perm()) -19 19 | +16 16 | int(math.comb()) +17 17 | int(math.factorial()) +18 18 | int(math.gcd()) +19 |-int(math.lcm()) + 19 |+math.lcm() +20 20 | int(math.isqrt()) +21 21 | int(math.perm()) +22 22 | -RUF046.py:17:1: RUF046 [*] Value being casted is already an integer +RUF046.py:20:1: RUF046 [*] Value being casted is already an integer | -15 | int(math.gcd()) -16 | int(math.lcm()) -17 | int(math.isqrt()) +18 | int(math.gcd()) +19 | int(math.lcm()) +20 | int(math.isqrt()) | ^^^^^^^^^^^^^^^^^ RUF046 -18 | int(math.perm()) +21 | int(math.perm()) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -14 14 | int(math.factorial()) -15 15 | int(math.gcd()) -16 16 | int(math.lcm()) -17 |-int(math.isqrt()) - 17 |+math.isqrt() -18 18 | int(math.perm()) -19 19 | -20 20 | +17 17 | int(math.factorial()) +18 18 | int(math.gcd()) +19 19 | int(math.lcm()) +20 |-int(math.isqrt()) + 20 |+math.isqrt() +21 21 | int(math.perm()) +22 22 | +23 23 | int(round(1, 0)) -RUF046.py:18:1: RUF046 [*] Value being casted is already an integer +RUF046.py:21:1: RUF046 [*] Value being casted is already an integer | -16 | int(math.lcm()) -17 | int(math.isqrt()) -18 | int(math.perm()) +19 | int(math.lcm()) +20 | int(math.isqrt()) +21 | int(math.perm()) | ^^^^^^^^^^^^^^^^ RUF046 +22 | +23 | int(round(1, 0)) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -15 15 | int(math.gcd()) -16 16 | int(math.lcm()) -17 17 | int(math.isqrt()) -18 |-int(math.perm()) - 18 |+math.perm() -19 19 | -20 20 | -21 21 | ### Unsafe +18 18 | int(math.gcd()) +19 19 | int(math.lcm()) +20 20 | int(math.isqrt()) +21 |-int(math.perm()) + 21 |+math.perm() +22 22 | +23 23 | int(round(1, 0)) +24 24 | int(round(1, 10)) RUF046.py:23:1: RUF046 [*] Value being casted is already an integer | -21 | ### Unsafe +21 | int(math.perm()) 22 | -23 | int(math.ceil()) +23 | int(round(1, 0)) | ^^^^^^^^^^^^^^^^ RUF046 -24 | int(math.floor()) -25 | int(math.trunc()) +24 | int(round(1, 10)) | = help: Remove unnecessary conversion to `int` -ℹ Unsafe fix -20 20 | -21 21 | ### Unsafe +ℹ Safe fix +20 20 | int(math.isqrt()) +21 21 | int(math.perm()) 22 22 | -23 |-int(math.ceil()) - 23 |+math.ceil() -24 24 | int(math.floor()) -25 25 | int(math.trunc()) -26 26 | +23 |-int(round(1, 0)) + 23 |+round(1, 0) +24 24 | int(round(1, 10)) +25 25 | +26 26 | int(round(1)) RUF046.py:24:1: RUF046 [*] Value being casted is already an integer | -23 | int(math.ceil()) -24 | int(math.floor()) +23 | int(round(1, 0)) +24 | int(round(1, 10)) | ^^^^^^^^^^^^^^^^^ RUF046 -25 | int(math.trunc()) +25 | +26 | int(round(1)) | = help: Remove unnecessary conversion to `int` -ℹ Unsafe fix -21 21 | ### Unsafe +ℹ Safe fix +21 21 | int(math.perm()) 22 22 | -23 23 | int(math.ceil()) -24 |-int(math.floor()) - 24 |+math.floor() -25 25 | int(math.trunc()) -26 26 | -27 27 | - -RUF046.py:25:1: RUF046 [*] Value being casted is already an integer - | -23 | int(math.ceil()) -24 | int(math.floor()) -25 | int(math.trunc()) - | ^^^^^^^^^^^^^^^^^ RUF046 +23 23 | int(round(1, 0)) +24 |-int(round(1, 10)) + 24 |+round(1, 10) +25 25 | +26 26 | int(round(1)) +27 27 | int(round(1, None)) + +RUF046.py:26:1: RUF046 [*] Value being casted is already an integer + | +24 | int(round(1, 10)) +25 | +26 | int(round(1)) + | ^^^^^^^^^^^^^ RUF046 +27 | int(round(1, None)) | = help: Remove unnecessary conversion to `int` -ℹ Unsafe fix -22 22 | -23 23 | int(math.ceil()) -24 24 | int(math.floor()) -25 |-int(math.trunc()) - 25 |+math.trunc() -26 26 | -27 27 | -28 28 | ### `round()` - -RUF046.py:31:1: RUF046 [*] Value being casted is already an integer - | -30 | ## Errors -31 | int(round(0)) - | ^^^^^^^^^^^^^ RUF046 -32 | int(round(0, 0)) -33 | int(round(0, None)) +ℹ Safe fix +23 23 | int(round(1, 0)) +24 24 | int(round(1, 10)) +25 25 | +26 |-int(round(1)) + 26 |+round(1) +27 27 | int(round(1, None)) +28 28 | +29 29 | int(round(1.)) + +RUF046.py:27:1: RUF046 [*] Value being casted is already an integer + | +26 | int(round(1)) +27 | int(round(1, None)) + | ^^^^^^^^^^^^^^^^^^^ RUF046 +28 | +29 | int(round(1.)) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -28 28 | ### `round()` -29 29 | -30 30 | ## Errors -31 |-int(round(0)) - 31 |+round(0) -32 32 | int(round(0, 0)) -33 33 | int(round(0, None)) -34 34 | - -RUF046.py:32:1: RUF046 [*] Value being casted is already an integer - | -30 | ## Errors -31 | int(round(0)) -32 | int(round(0, 0)) - | ^^^^^^^^^^^^^^^^ RUF046 -33 | int(round(0, None)) +24 24 | int(round(1, 10)) +25 25 | +26 26 | int(round(1)) +27 |-int(round(1, None)) + 27 |+round(1, None) +28 28 | +29 29 | int(round(1.)) +30 30 | int(round(1., None)) + +RUF046.py:29:1: RUF046 [*] Value being casted is already an integer + | +27 | int(round(1, None)) +28 | +29 | int(round(1.)) + | ^^^^^^^^^^^^^^ RUF046 +30 | int(round(1., None)) | = help: Remove unnecessary conversion to `int` ℹ Safe fix -29 29 | -30 30 | ## Errors -31 31 | int(round(0)) -32 |-int(round(0, 0)) - 32 |+round(0) -33 33 | int(round(0, None)) -34 34 | -35 35 | int(round(0.1)) +26 26 | int(round(1)) +27 27 | int(round(1, None)) +28 28 | +29 |-int(round(1.)) + 29 |+round(1.) +30 30 | int(round(1., None)) +31 31 | +32 32 | -RUF046.py:33:1: RUF046 [*] Value being casted is already an integer +RUF046.py:30:1: RUF046 [*] Value being casted is already an integer | -31 | int(round(0)) -32 | int(round(0, 0)) -33 | int(round(0, None)) - | ^^^^^^^^^^^^^^^^^^^ RUF046 -34 | -35 | int(round(0.1)) +29 | int(round(1.)) +30 | int(round(1., None)) + | ^^^^^^^^^^^^^^^^^^^^ RUF046 | = help: Remove unnecessary conversion to `int` ℹ Safe fix -30 30 | ## Errors -31 31 | int(round(0)) -32 32 | int(round(0, 0)) -33 |-int(round(0, None)) - 33 |+round(0) -34 34 | -35 35 | int(round(0.1)) -36 36 | int(round(0.1, None)) +27 27 | int(round(1, None)) +28 28 | +29 29 | int(round(1.)) +30 |-int(round(1., None)) + 30 |+round(1., None) +31 31 | +32 32 | +33 33 | ### Unsafe RUF046.py:35:1: RUF046 [*] Value being casted is already an integer | -33 | int(round(0, None)) +33 | ### Unsafe 34 | -35 | int(round(0.1)) - | ^^^^^^^^^^^^^^^ RUF046 -36 | int(round(0.1, None)) +35 | int(math.ceil()) + | ^^^^^^^^^^^^^^^^ RUF046 +36 | int(math.floor()) +37 | int(math.trunc()) | = help: Remove unnecessary conversion to `int` ℹ Unsafe fix -32 32 | int(round(0, 0)) -33 33 | int(round(0, None)) +32 32 | +33 33 | ### Unsafe 34 34 | -35 |-int(round(0.1)) - 35 |+round(0.1) -36 36 | int(round(0.1, None)) -37 37 | -38 38 | # Argument type is not checked +35 |-int(math.ceil()) + 35 |+math.ceil() +36 36 | int(math.floor()) +37 37 | int(math.trunc()) +38 38 | RUF046.py:36:1: RUF046 [*] Value being casted is already an integer | -35 | int(round(0.1)) -36 | int(round(0.1, None)) - | ^^^^^^^^^^^^^^^^^^^^^ RUF046 -37 | -38 | # Argument type is not checked +35 | int(math.ceil()) +36 | int(math.floor()) + | ^^^^^^^^^^^^^^^^^ RUF046 +37 | int(math.trunc()) | = help: Remove unnecessary conversion to `int` ℹ Unsafe fix -33 33 | int(round(0, None)) +33 33 | ### Unsafe 34 34 | -35 35 | int(round(0.1)) -36 |-int(round(0.1, None)) - 36 |+round(0.1) -37 37 | -38 38 | # Argument type is not checked -39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() - -RUF046.py:41:1: RUF046 [*] Value being casted is already an integer - | -39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() -40 | -41 | int(round(foo)) - | ^^^^^^^^^^^^^^^ RUF046 -42 | int(round(foo, 0)) -43 | int(round(foo, None)) +35 35 | int(math.ceil()) +36 |-int(math.floor()) + 36 |+math.floor() +37 37 | int(math.trunc()) +38 38 | +39 39 | int(round(inferred_int, 0)) + +RUF046.py:37:1: RUF046 [*] Value being casted is already an integer + | +35 | int(math.ceil()) +36 | int(math.floor()) +37 | int(math.trunc()) + | ^^^^^^^^^^^^^^^^^ RUF046 +38 | +39 | int(round(inferred_int, 0)) | = help: Remove unnecessary conversion to `int` ℹ Unsafe fix -38 38 | # Argument type is not checked -39 39 | foo = type("Foo", (), {"__round__": lambda self: 4.2})() -40 40 | -41 |-int(round(foo)) - 41 |+round(foo) -42 42 | int(round(foo, 0)) -43 43 | int(round(foo, None)) +34 34 | +35 35 | int(math.ceil()) +36 36 | int(math.floor()) +37 |-int(math.trunc()) + 37 |+math.trunc() +38 38 | +39 39 | int(round(inferred_int, 0)) +40 40 | int(round(inferred_int, 10)) + +RUF046.py:39:1: RUF046 [*] Value being casted is already an integer + | +37 | int(math.trunc()) +38 | +39 | int(round(inferred_int, 0)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF046 +40 | int(round(inferred_int, 10)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +36 36 | int(math.floor()) +37 37 | int(math.trunc()) +38 38 | +39 |-int(round(inferred_int, 0)) + 39 |+round(inferred_int, 0) +40 40 | int(round(inferred_int, 10)) +41 41 | +42 42 | int(round(inferred_int)) + +RUF046.py:40:1: RUF046 [*] Value being casted is already an integer + | +39 | int(round(inferred_int, 0)) +40 | int(round(inferred_int, 10)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF046 +41 | +42 | int(round(inferred_int)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +37 37 | int(math.trunc()) +38 38 | +39 39 | int(round(inferred_int, 0)) +40 |-int(round(inferred_int, 10)) + 40 |+round(inferred_int, 10) +41 41 | +42 42 | int(round(inferred_int)) +43 43 | int(round(inferred_int, None)) + +RUF046.py:42:1: RUF046 [*] Value being casted is already an integer + | +40 | int(round(inferred_int, 10)) +41 | +42 | int(round(inferred_int)) + | ^^^^^^^^^^^^^^^^^^^^^^^^ RUF046 +43 | int(round(inferred_int, None)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +39 39 | int(round(inferred_int, 0)) +40 40 | int(round(inferred_int, 10)) +41 41 | +42 |-int(round(inferred_int)) + 42 |+round(inferred_int) +43 43 | int(round(inferred_int, None)) 44 44 | +45 45 | int(round(inferred_float)) RUF046.py:43:1: RUF046 [*] Value being casted is already an integer | -41 | int(round(foo)) -42 | int(round(foo, 0)) -43 | int(round(foo, None)) - | ^^^^^^^^^^^^^^^^^^^^^ RUF046 +42 | int(round(inferred_int)) +43 | int(round(inferred_int, None)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF046 +44 | +45 | int(round(inferred_float)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +40 40 | int(round(inferred_int, 10)) +41 41 | +42 42 | int(round(inferred_int)) +43 |-int(round(inferred_int, None)) + 43 |+round(inferred_int, None) +44 44 | +45 45 | int(round(inferred_float)) +46 46 | int(round(inferred_float, None)) + +RUF046.py:45:1: RUF046 [*] Value being casted is already an integer + | +43 | int(round(inferred_int, None)) 44 | -45 | ## No errors +45 | int(round(inferred_float)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF046 +46 | int(round(inferred_float, None)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +42 42 | int(round(inferred_int)) +43 43 | int(round(inferred_int, None)) +44 44 | +45 |-int(round(inferred_float)) + 45 |+round(inferred_float) +46 46 | int(round(inferred_float, None)) +47 47 | +48 48 | int(round(unknown)) + +RUF046.py:46:1: RUF046 [*] Value being casted is already an integer + | +45 | int(round(inferred_float)) +46 | int(round(inferred_float, None)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF046 +47 | +48 | int(round(unknown)) | = help: Remove unnecessary conversion to `int` ℹ Unsafe fix -40 40 | -41 41 | int(round(foo)) -42 42 | int(round(foo, 0)) -43 |-int(round(foo, None)) - 43 |+round(foo) +43 43 | int(round(inferred_int, None)) 44 44 | -45 45 | ## No errors -46 46 | int(round(0, 3.14)) +45 45 | int(round(inferred_float)) +46 |-int(round(inferred_float, None)) + 46 |+round(inferred_float, None) +47 47 | +48 48 | int(round(unknown)) +49 49 | int(round(unknown, None)) + +RUF046.py:48:1: RUF046 [*] Value being casted is already an integer + | +46 | int(round(inferred_float, None)) +47 | +48 | int(round(unknown)) + | ^^^^^^^^^^^^^^^^^^^ RUF046 +49 | int(round(unknown, None)) + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +45 45 | int(round(inferred_float)) +46 46 | int(round(inferred_float, None)) +47 47 | +48 |-int(round(unknown)) + 48 |+round(unknown) +49 49 | int(round(unknown, None)) +50 50 | +51 51 | + +RUF046.py:49:1: RUF046 [*] Value being casted is already an integer + | +48 | int(round(unknown)) +49 | int(round(unknown, None)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF046 + | + = help: Remove unnecessary conversion to `int` + +ℹ Unsafe fix +46 46 | int(round(inferred_float, None)) +47 47 | +48 48 | int(round(unknown)) +49 |-int(round(unknown, None)) + 49 |+round(unknown, None) +50 50 | +51 51 | +52 52 | ### No errors diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 1dc71ab700504..b346390a8908c 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -665,6 +665,14 @@ impl BuiltinTypeChecker for IntChecker { const EXPR_TYPE: PythonType = PythonType::Number(NumberLike::Integer); } +struct FloatChecker; + +impl BuiltinTypeChecker for FloatChecker { + const BUILTIN_TYPE_NAME: &'static str = "float"; + const TYPING_NAME: Option<&'static str> = None; + const EXPR_TYPE: PythonType = PythonType::Number(NumberLike::Float); +} + pub struct IoBaseChecker; impl TypeChecker for IoBaseChecker { @@ -850,6 +858,11 @@ pub fn is_int(binding: &Binding, semantic: &SemanticModel) -> bool { check_type::(binding, semantic) } +/// Test whether the given binding can be considered an instance of `float`. +pub fn is_float(binding: &Binding, semantic: &SemanticModel) -> bool { + check_type::(binding, semantic) +} + /// Test whether the given binding can be considered a set. /// /// For this, we check what value might be associated with it through it's initialization and From 533e8a6ee642848b6ee38e896991c090e167a0c8 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 9 Dec 2024 09:02:14 -0800 Subject: [PATCH 07/14] [red-knot] move standalone expression_ty to TypeInferenceBuilder::file_expression_ty (#14879) ## Summary Per suggestion in https://github.com/astral-sh/ruff/pull/14802#discussion_r1875455417 This is a bit less error-prone and allows us to handle both expressions in the current scope or a different scope. Also, there's currently no need for this method outside of `TypeInferenceBuilder`, so no reason to expose it in `types.rs`. ## Test Plan Pure refactor, no functional change; existing tests pass. --------- Co-authored-by: Alex Waygood --- crates/red_knot_python_semantic/src/types.rs | 12 ------- .../src/types/infer.rs | 35 +++++++++++++++---- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 2ceefbb0b546d..32d26ea075f88 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -225,18 +225,6 @@ fn definition_expression_ty<'db>( } } -/// Get the type of an expression from an arbitrary scope. -/// -/// Can cause query cycles if used carelessly; caller must be sure that type inference isn't -/// currently in progress for the expression's scope. -fn expression_ty<'db>(db: &'db dyn Db, file: File, expression: &ast::Expr) -> Type<'db> { - let index = semantic_index(db, file); - let file_scope = index.expression_scope_id(expression); - let scope = file_scope.to_scope_id(db, file); - let expr_id = expression.scoped_expression_id(db, scope); - infer_scope_types(db, scope).expression_ty(expr_id) -} - /// Infer the combined type of an iterator of bindings. /// /// Will return a union if there is more than one binding. diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index ccd90bb6e261b..0570e8ebe614f 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -63,7 +63,6 @@ use crate::unpack::Unpack; use crate::util::subscript::{PyIndex, PySlice}; use crate::Db; -use super::expression_ty; use super::string_annotation::parse_string_annotation; /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. @@ -407,13 +406,37 @@ impl<'db> TypeInferenceBuilder<'db> { /// Get the already-inferred type of an expression node. /// - /// PANIC if no type has been inferred for this node. + /// ## Panics + /// If the expression is not within this region, or if no type has yet been inferred for + /// this node. #[track_caller] fn expression_ty(&self, expr: &ast::Expr) -> Type<'db> { self.types .expression_ty(expr.scoped_expression_id(self.db, self.scope())) } + /// Get the type of an expression from any scope in the same file. + /// + /// If the expression is in the current scope, and we are inferring the entire scope, just look + /// up the expression in our own results, otherwise call [`infer_scope_types()`] for the scope + /// of the expression. + /// + /// ## Panics + /// + /// If the expression is in the current scope but we haven't yet inferred a type for it. + /// + /// Can cause query cycles if the expression is from a different scope and type inference is + /// already in progress for that scope (further up the stack). + fn file_expression_ty(&self, expression: &ast::Expr) -> Type<'db> { + let file_scope = self.index.expression_scope_id(expression); + let expr_scope = file_scope.to_scope_id(self.db, self.file); + let expr_id = expression.scoped_expression_id(self.db, expr_scope); + match self.region { + InferenceRegion::Scope(scope) if scope == expr_scope => self.expression_ty(expression), + _ => infer_scope_types(self.db, expr_scope).expression_ty(expr_id), + } + } + /// Infers types in the given [`InferenceRegion`]. fn infer_region(&mut self) { match self.region { @@ -1081,9 +1104,9 @@ impl<'db> TypeInferenceBuilder<'db> { } = parameter_with_default; let default_ty = default .as_ref() - .map(|default| expression_ty(self.db, self.file, default)); + .map(|default| self.file_expression_ty(default)); if let Some(annotation) = parameter.annotation.as_ref() { - let declared_ty = expression_ty(self.db, self.file, annotation); + let declared_ty = self.file_expression_ty(annotation); let inferred_ty = if let Some(default_ty) = default_ty { if default_ty.is_assignable_to(self.db, declared_ty) { UnionType::from_elements(self.db, [declared_ty, default_ty]) @@ -1127,7 +1150,7 @@ impl<'db> TypeInferenceBuilder<'db> { definition: Definition<'db>, ) { if let Some(annotation) = parameter.annotation.as_ref() { - let _annotated_ty = expression_ty(self.db, self.file, annotation); + let _annotated_ty = self.file_expression_ty(annotation); // TODO `tuple[annotated_ty, ...]` let ty = KnownClass::Tuple.to_instance(self.db); self.add_declaration_with_binding(parameter.into(), definition, ty, ty); @@ -1152,7 +1175,7 @@ impl<'db> TypeInferenceBuilder<'db> { definition: Definition<'db>, ) { if let Some(annotation) = parameter.annotation.as_ref() { - let _annotated_ty = expression_ty(self.db, self.file, annotation); + let _annotated_ty = self.file_expression_ty(annotation); // TODO `dict[str, annotated_ty]` let ty = KnownClass::Dict.to_instance(self.db); self.add_declaration_with_binding(parameter.into(), definition, ty, ty); From 64944f2cf59549be7b4d86ddb58ae47cd45d8f75 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Mon, 9 Dec 2024 23:47:34 +0100 Subject: [PATCH 08/14] More typos found by codespell (#14880) --- crates/red_knot_python_semantic/src/semantic_index/use_def.rs | 4 ++-- crates/red_knot_python_semantic/src/types.rs | 2 +- crates/ruff_formatter/src/buffer.rs | 2 +- crates/ruff_linter/src/checkers/ast/mod.rs | 2 +- .../src/rules/fastapi/rules/fastapi_unused_path_parameter.rs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs b/crates/red_knot_python_semantic/src/semantic_index/use_def.rs index cfb8318b59222..9f3e197c74eee 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/use_def.rs @@ -47,7 +47,7 @@ //! The **declared type** represents the code author's declaration (usually through a type //! annotation) that a given variable should not be assigned any type outside the declared type. In //! our model, declared types are also control-flow-sensitive; we allow the code author to -//! explicitly re-declare the same variable with a different type. So for a given binding of a +//! explicitly redeclare the same variable with a different type. So for a given binding of a //! variable, we will want to ask which declarations of that variable can reach that binding, in //! order to determine whether the binding is permitted, or should be a type error. For example: //! @@ -62,7 +62,7 @@ //! assignable to `str`. This is the purpose of declared types: they prevent accidental assignment //! of the wrong type to a variable. //! -//! But in some cases it is useful to "shadow" or "re-declare" a variable with a new type, and we +//! But in some cases it is useful to "shadow" or "redeclare" a variable with a new type, and we //! permit this, as long as it is done with an explicit re-annotation. So `path: Path = //! Path(path)`, with the explicit `: Path` annotation, is permitted. //! diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 32d26ea075f88..1d3b82e7563ff 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -887,7 +887,7 @@ impl<'db> Type<'db> { Type::SubclassOf(_), ) => true, (Type::SubclassOf(_), _) | (_, Type::SubclassOf(_)) => { - // TODO: Once we have support for final classes, we can determine disjointness in some cases + // TODO: Once we have support for final classes, we can determine disjointedness in some cases // here. However, note that it might be better to turn `Type::SubclassOf('FinalClass')` into // `Type::ClassLiteral('FinalClass')` during construction, instead of adding special cases for // final classes inside `Type::SubclassOf` everywhere. diff --git a/crates/ruff_formatter/src/buffer.rs b/crates/ruff_formatter/src/buffer.rs index b376fcf8eb1d6..12d24c7294122 100644 --- a/crates/ruff_formatter/src/buffer.rs +++ b/crates/ruff_formatter/src/buffer.rs @@ -656,7 +656,7 @@ where let elements = buffer.elements(); let recorded = if self.start > elements.len() { - // May happen if buffer was rewinded. + // May happen if buffer was rewound. &[] } else { &elements[self.start..] diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 5fcf91b8e5bb0..edad3ff05d6d4 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -282,7 +282,7 @@ impl<'a> Checker<'a> { // TODO(charlie): `noqa` directives are mostly enforced in `check_lines.rs`. // However, in rare cases, we need to check them here. For example, when // removing unused imports, we create a single fix that's applied to all - // unused members on a single import. We need to pre-emptively omit any + // unused members on a single import. We need to preemptively omit any // members from the fix that will eventually be excluded by a `noqa`. // Unfortunately, we _do_ want to register a `Diagnostic` for each // eventually-ignored import, so that our `noqa` counts are accurate. diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs index 2c0535d762bf2..779fa9835eaf3 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_unused_path_parameter.rs @@ -267,7 +267,7 @@ impl<'a> Iterator for PathParamIterator<'a> { if c == '{' { if let Some((end, _)) = self.chars.by_ref().find(|&(_, ch)| ch == '}') { let param_content = &self.input[start + 1..end]; - // We ignore text after a colon, since those are path convertors + // We ignore text after a colon, since those are path converters // See also: https://fastapi.tiangolo.com/tutorial/path-params/?h=path#path-convertor let param_name_end = param_content.find(':').unwrap_or(param_content.len()); let param_name = ¶m_content[..param_name_end].trim(); From ab26d9cf9a5899406e1b4a3555c54e6d0be8ed96 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 9 Dec 2024 22:49:58 +0000 Subject: [PATCH 09/14] [red-knot] Improve type inference for except handlers (#14838) --- .../resources/mdtest/exception/basic.md | 42 ++++++++-- .../resources/mdtest/exception/except_star.md | 41 ++++++++-- crates/red_knot_python_semantic/src/types.rs | 20 +++++ .../src/types/diagnostic.rs | 12 +++ .../src/types/infer.rs | 76 +++++++++++-------- 5 files changed, 150 insertions(+), 41 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md b/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md index c0d2d1e2f0ec8..167692d422474 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md @@ -49,12 +49,44 @@ def foo( try: help() except x as e: - # TODO: should be `AttributeError` - reveal_type(e) # revealed: @Todo(exception type) + reveal_type(e) # revealed: AttributeError except y as f: - # TODO: should be `OSError | RuntimeError` - reveal_type(f) # revealed: @Todo(exception type) + reveal_type(f) # revealed: OSError | RuntimeError except z as g: # TODO: should be `BaseException` - reveal_type(g) # revealed: @Todo(exception type) + reveal_type(g) # revealed: @Todo(full tuple[...] support) +``` + +## Invalid exception handlers + +```py +try: + pass +# error: [invalid-exception] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" +except 3 as e: + reveal_type(e) # revealed: Unknown + +try: + pass +# error: [invalid-exception] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" +# error: [invalid-exception] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" +except (ValueError, OSError, "foo", b"bar") as e: + reveal_type(e) # revealed: ValueError | OSError | Unknown + +def foo( + x: type[str], + y: tuple[type[OSError], type[RuntimeError], int], + z: tuple[type[str], ...], +): + try: + help() + # error: [invalid-exception] + except x as e: + reveal_type(e) # revealed: Unknown + # error: [invalid-exception] + except y as f: + reveal_type(f) # revealed: OSError | RuntimeError | Unknown + except z as g: + # TODO: should emit a diagnostic here: + reveal_type(g) # revealed: @Todo(full tuple[...] support) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md b/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md index b543544e56b35..47f6f2d97bd3a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md @@ -1,30 +1,59 @@ -# Except star +# `except*` -## Except\* with BaseException +## `except*` with `BaseException` ```py try: help() except* BaseException as e: + # TODO: should be `BaseExceptionGroup[BaseException]` --Alex reveal_type(e) # revealed: BaseExceptionGroup ``` -## Except\* with specific exception +## `except*` with specific exception ```py try: help() except* OSError as e: - # TODO(Alex): more precise would be `ExceptionGroup[OSError]` + # TODO: more precise would be `ExceptionGroup[OSError]` --Alex + # (needs homogenous tuples + generics) reveal_type(e) # revealed: BaseExceptionGroup ``` -## Except\* with multiple exceptions +## `except*` with multiple exceptions ```py try: help() except* (TypeError, AttributeError) as e: - # TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`. + # TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex + # (needs homogenous tuples + generics) + reveal_type(e) # revealed: BaseExceptionGroup +``` + +## `except*` with mix of `Exception`s and `BaseException`s + +```py +try: + help() +except* (KeyboardInterrupt, AttributeError) as e: + # TODO: more precise would be `BaseExceptionGroup[KeyboardInterrupt | AttributeError]` --Alex + reveal_type(e) # revealed: BaseExceptionGroup +``` + +## Invalid `except*` handlers + +```py +try: + help() +except* 3 as e: # error: [invalid-exception] + # TODO: Should be `BaseExceptionGroup[Unknown]` --Alex + reveal_type(e) # revealed: BaseExceptionGroup + +try: + help() +except* (AttributeError, 42) as e: # error: [invalid-exception] + # TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex reveal_type(e) # revealed: BaseExceptionGroup ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1d3b82e7563ff..6778ba4f29786 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1173,6 +1173,8 @@ impl<'db> Type<'db> { | KnownClass::Set | KnownClass::Dict | KnownClass::Slice + | KnownClass::BaseException + | KnownClass::BaseExceptionGroup | KnownClass::GenericAlias | KnownClass::ModuleType | KnownClass::FunctionType @@ -1845,6 +1847,8 @@ pub enum KnownClass { Set, Dict, Slice, + BaseException, + BaseExceptionGroup, // Types GenericAlias, ModuleType, @@ -1875,6 +1879,8 @@ impl<'db> KnownClass { Self::List => "list", Self::Type => "type", Self::Slice => "slice", + Self::BaseException => "BaseException", + Self::BaseExceptionGroup => "BaseExceptionGroup", Self::GenericAlias => "GenericAlias", Self::ModuleType => "ModuleType", Self::FunctionType => "FunctionType", @@ -1902,6 +1908,12 @@ impl<'db> KnownClass { .unwrap_or(Type::Unknown) } + pub fn to_subclass_of(self, db: &'db dyn Db) -> Option> { + self.to_class_literal(db) + .into_class_literal() + .map(|ClassLiteralType { class }| Type::subclass_of(class)) + } + /// Return the module in which we should look up the definition for this class pub(crate) fn canonical_module(self, db: &'db dyn Db) -> CoreStdlibModule { match self { @@ -1916,6 +1928,8 @@ impl<'db> KnownClass { | Self::Tuple | Self::Set | Self::Dict + | Self::BaseException + | Self::BaseExceptionGroup | Self::Slice => CoreStdlibModule::Builtins, Self::VersionInfo => CoreStdlibModule::Sys, Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types, @@ -1959,6 +1973,8 @@ impl<'db> KnownClass { | Self::ModuleType | Self::FunctionType | Self::SpecialForm + | Self::BaseException + | Self::BaseExceptionGroup | Self::TypeVar => false, } } @@ -1980,6 +1996,8 @@ impl<'db> KnownClass { "dict" => Self::Dict, "list" => Self::List, "slice" => Self::Slice, + "BaseException" => Self::BaseException, + "BaseExceptionGroup" => Self::BaseExceptionGroup, "GenericAlias" => Self::GenericAlias, "NoneType" => Self::NoneType, "ModuleType" => Self::ModuleType, @@ -2016,6 +2034,8 @@ impl<'db> KnownClass { | Self::GenericAlias | Self::ModuleType | Self::VersionInfo + | Self::BaseException + | Self::BaseExceptionGroup | Self::FunctionType => module.name() == self.canonical_module(db).as_str(), Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"), Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => { diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 36b87247b9926..24b0a3a0d13df 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -289,6 +289,18 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> { ); } + pub(super) fn add_invalid_exception(&mut self, db: &dyn Db, node: &ast::Expr, ty: Type) { + self.add( + node.into(), + "invalid-exception", + format_args!( + "Cannot catch object of type `{}` in an exception handler \ + (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)", + ty.display(db) + ), + ); + } + /// Adds a new diagnostic. /// /// The diagnostic does not get added if the rule isn't enabled for this file. diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 0570e8ebe614f..c9889cc0cbc12 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1535,40 +1535,56 @@ impl<'db> TypeInferenceBuilder<'db> { except_handler_definition: &ExceptHandlerDefinitionKind, definition: Definition<'db>, ) { - let node_ty = except_handler_definition - .handled_exceptions() - .map(|ty| self.infer_expression(ty)) - // If there is no handled exception, it's invalid syntax; - // a diagnostic will have already been emitted - .unwrap_or(Type::Unknown); + let node = except_handler_definition.handled_exceptions(); + + // If there is no handled exception, it's invalid syntax; + // a diagnostic will have already been emitted + let node_ty = node.map_or(Type::Unknown, |ty| self.infer_expression(ty)); + + // If it's an `except*` handler, this won't actually be the type of the bound symbol; + // it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`. + let symbol_ty = if let Type::Tuple(tuple) = node_ty { + let type_base_exception = KnownClass::BaseException + .to_subclass_of(self.db) + .unwrap_or(Type::Unknown); + let mut builder = UnionBuilder::new(self.db); + for element in tuple.elements(self.db).iter().copied() { + builder = builder.add(if element.is_assignable_to(self.db, type_base_exception) { + element.to_instance(self.db) + } else { + if let Some(node) = node { + self.diagnostics + .add_invalid_exception(self.db, node, element); + } + Type::Unknown + }); + } + builder.build() + } else if node_ty.is_subtype_of(self.db, KnownClass::Tuple.to_instance(self.db)) { + todo_type!("Homogeneous tuple in exception handler") + } else { + let type_base_exception = KnownClass::BaseException + .to_subclass_of(self.db) + .unwrap_or(Type::Unknown); + if node_ty.is_assignable_to(self.db, type_base_exception) { + node_ty.to_instance(self.db) + } else { + if let Some(node) = node { + self.diagnostics + .add_invalid_exception(self.db, node, node_ty); + } + Type::Unknown + } + }; let symbol_ty = if except_handler_definition.is_star() { - // TODO should be generic --Alex + // TODO: we should infer `ExceptionGroup` if `node_ty` is a subtype of `tuple[type[Exception], ...]` + // (needs support for homogeneous tuples). // - // TODO should infer `ExceptionGroup` if all caught exceptions - // are subclasses of `Exception` --Alex - builtins_symbol(self.db, "BaseExceptionGroup") - .ignore_possibly_unbound() - .unwrap_or(Type::Unknown) - .to_instance(self.db) + // TODO: should be generic with `symbol_ty` as the generic parameter + KnownClass::BaseExceptionGroup.to_instance(self.db) } else { - // TODO: anything that's a consistent subtype of - // `type[BaseException] | tuple[type[BaseException], ...]` should be valid; - // anything else is invalid and should lead to a diagnostic being reported --Alex - match node_ty { - Type::Any | Type::Unknown => node_ty, - Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(class), - Type::Tuple(tuple) => UnionType::from_elements( - self.db, - tuple.elements(self.db).iter().map(|ty| { - ty.into_class_literal().map_or( - todo_type!("exception type"), - |ClassLiteralType { class }| Type::instance(class), - ) - }), - ), - _ => todo_type!("exception type"), - } + symbol_ty }; self.add_binding( From e3f34b8f5bd1eb715e732e5a277aacedac17358b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 9 Dec 2024 23:11:44 +0000 Subject: [PATCH 10/14] [`ruff`] Mark autofix for `RUF052` as always unsafe (#14824) --- .../rules/ruff/rules/used_dummy_variable.rs | 25 ++++++++++++++---- ..._rules__ruff__tests__RUF052_RUF052.py.snap | 26 +++++++++---------- ...var_regexp_preset__RUF052_RUF052.py_1.snap | 26 +++++++++---------- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs b/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs index 4147badc30b9f..8e7575d4cbb91 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/used_dummy_variable.rs @@ -16,8 +16,6 @@ use crate::{checkers::ast::Checker, renamer::Renamer}; /// By default, "dummy variables" are any variables with names that start with leading /// underscores. However, this is customisable using the [`lint.dummy-variable-rgx`] setting). /// -/// Dunder variables are ignored by this rule, as are variables named `_`. -/// /// ## Why is this bad? /// Marking a variable with a leading underscore conveys that it is intentionally unused within the function or method. /// When these variables are later referenced in the code, it causes confusion and potential misunderstandings about @@ -27,18 +25,25 @@ use crate::{checkers::ast::Checker, renamer::Renamer}; /// Sometimes leading underscores are used to avoid variables shadowing other variables, Python builtins, or Python /// keywords. However, [PEP 8] recommends to use trailing underscores for this rather than leading underscores. /// +/// Dunder variables are ignored by this rule, as are variables named `_`. +/// Only local variables in function scopes are flagged by the rule. +/// /// ## Example /// ```python /// def function(): /// _variable = 3 -/// return _variable + 1 +/// # important: avoid shadowing the builtin `id()` function! +/// _id = 4 +/// return _variable + _id /// ``` /// /// Use instead: /// ```python /// def function(): /// variable = 3 -/// return variable + 1 +/// # important: avoid shadowing the builtin `id()` function! +/// id_ = 4 +/// return variable + id_ /// ``` /// /// ## Fix availability @@ -47,6 +52,16 @@ use crate::{checkers::ast::Checker, renamer::Renamer}; /// would not shadow any other known variables already accessible from the scope /// in which the variable is defined. /// +/// ## Fix safety +/// This rule's fix is marked as unsafe. +/// +/// For this rule's fix, Ruff renames the variable and fixes up all known references to +/// it so they point to the renamed variable. However, some renamings also require other +/// changes such as different arguments to constructor calls or alterations to comments. +/// Ruff is aware of some of these cases: `_T = TypeVar("_T")` will be fixed to +/// `T = TypeVar("T")` if the `_T` binding is flagged by this rule. However, in general, +/// cases like these are hard to detect and hard to automatically fix. +/// /// ## Options /// - [`lint.dummy-variable-rgx`] /// @@ -146,7 +161,7 @@ pub(crate) fn used_dummy_variable(checker: &Checker, binding: &Binding) -> Optio if let Some(fix) = get_possible_fix(name, shadowed_kind, binding.scope, checker) { diagnostic.try_set_fix(|| { Renamer::rename(name, &fix, scope, semantic, checker.stylist()) - .map(|(edit, rest)| Fix::safe_edits(edit, rest)) + .map(|(edit, rest)| Fix::unsafe_edits(edit, rest)) }); } } diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF052_RUF052.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF052_RUF052.py.snap index 610a2fc2514d8..06ecf0da87a10 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF052_RUF052.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF052_RUF052.py.snap @@ -11,7 +11,7 @@ RUF052.py:77:9: RUF052 [*] Local dummy variable `_var` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 74 74 | 75 75 | class Class_: 76 76 | def fun(self): @@ -32,7 +32,7 @@ RUF052.py:84:5: RUF052 [*] Local dummy variable `_list` is accessed | = help: Prefer using trailing underscores to avoid shadowing a built-in -ℹ Safe fix +ℹ Unsafe fix 81 81 | return _var 82 82 | 83 83 | def fun(): @@ -54,7 +54,7 @@ RUF052.py:91:5: RUF052 [*] Local dummy variable `_x` is accessed | = help: Prefer using trailing underscores to avoid shadowing a variable -ℹ Safe fix +ℹ Unsafe fix 88 88 | 89 89 | def fun(): 90 90 | global x @@ -77,7 +77,7 @@ RUF052.py:98:5: RUF052 [*] Local dummy variable `_x` is accessed | = help: Prefer using trailing underscores to avoid shadowing a variable -ℹ Safe fix +ℹ Unsafe fix 95 95 | x = "outer" 96 96 | def bar(): 97 97 | nonlocal x @@ -99,7 +99,7 @@ RUF052.py:105:5: RUF052 [*] Local dummy variable `_x` is accessed | = help: Prefer using trailing underscores to avoid shadowing a variable -ℹ Safe fix +ℹ Unsafe fix 102 102 | 103 103 | def fun(): 104 104 | x = "local" @@ -160,7 +160,7 @@ RUF052.py:138:5: RUF052 [*] Local dummy variable `_P` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 135 135 | from enum import Enum 136 136 | from collections import namedtuple 137 137 | @@ -186,7 +186,7 @@ RUF052.py:139:5: RUF052 [*] Local dummy variable `_T` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 136 136 | from collections import namedtuple 137 137 | 138 138 | _P = ParamSpec("_P") @@ -213,7 +213,7 @@ RUF052.py:140:5: RUF052 [*] Local dummy variable `_NT` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 137 137 | 138 138 | _P = ParamSpec("_P") 139 139 | _T = TypeVar(name="_T", covariant=True, bound=int|str) @@ -239,7 +239,7 @@ RUF052.py:141:5: RUF052 [*] Local dummy variable `_E` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 138 138 | _P = ParamSpec("_P") 139 139 | _T = TypeVar(name="_T", covariant=True, bound=int|str) 140 140 | _NT = NamedTuple("_NT", [("foo", int)]) @@ -264,7 +264,7 @@ RUF052.py:142:5: RUF052 [*] Local dummy variable `_NT2` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 139 139 | _T = TypeVar(name="_T", covariant=True, bound=int|str) 140 140 | _NT = NamedTuple("_NT", [("foo", int)]) 141 141 | _E = Enum("_E", ["a", "b", "c"]) @@ -288,7 +288,7 @@ RUF052.py:143:5: RUF052 [*] Local dummy variable `_NT3` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 140 140 | _NT = NamedTuple("_NT", [("foo", int)]) 141 141 | _E = Enum("_E", ["a", "b", "c"]) 142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z']) @@ -310,7 +310,7 @@ RUF052.py:144:5: RUF052 [*] Local dummy variable `_DynamicClass` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 141 141 | _E = Enum("_E", ["a", "b", "c"]) 142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z']) 143 143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z']) @@ -332,7 +332,7 @@ RUF052.py:145:5: RUF052 [*] Local dummy variable `_NotADynamicClass` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z']) 143 143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z']) 144 144 | _DynamicClass = type("_DynamicClass", (), {}) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__custom_dummy_var_regexp_preset__RUF052_RUF052.py_1.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__custom_dummy_var_regexp_preset__RUF052_RUF052.py_1.snap index 610a2fc2514d8..06ecf0da87a10 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__custom_dummy_var_regexp_preset__RUF052_RUF052.py_1.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__custom_dummy_var_regexp_preset__RUF052_RUF052.py_1.snap @@ -11,7 +11,7 @@ RUF052.py:77:9: RUF052 [*] Local dummy variable `_var` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 74 74 | 75 75 | class Class_: 76 76 | def fun(self): @@ -32,7 +32,7 @@ RUF052.py:84:5: RUF052 [*] Local dummy variable `_list` is accessed | = help: Prefer using trailing underscores to avoid shadowing a built-in -ℹ Safe fix +ℹ Unsafe fix 81 81 | return _var 82 82 | 83 83 | def fun(): @@ -54,7 +54,7 @@ RUF052.py:91:5: RUF052 [*] Local dummy variable `_x` is accessed | = help: Prefer using trailing underscores to avoid shadowing a variable -ℹ Safe fix +ℹ Unsafe fix 88 88 | 89 89 | def fun(): 90 90 | global x @@ -77,7 +77,7 @@ RUF052.py:98:5: RUF052 [*] Local dummy variable `_x` is accessed | = help: Prefer using trailing underscores to avoid shadowing a variable -ℹ Safe fix +ℹ Unsafe fix 95 95 | x = "outer" 96 96 | def bar(): 97 97 | nonlocal x @@ -99,7 +99,7 @@ RUF052.py:105:5: RUF052 [*] Local dummy variable `_x` is accessed | = help: Prefer using trailing underscores to avoid shadowing a variable -ℹ Safe fix +ℹ Unsafe fix 102 102 | 103 103 | def fun(): 104 104 | x = "local" @@ -160,7 +160,7 @@ RUF052.py:138:5: RUF052 [*] Local dummy variable `_P` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 135 135 | from enum import Enum 136 136 | from collections import namedtuple 137 137 | @@ -186,7 +186,7 @@ RUF052.py:139:5: RUF052 [*] Local dummy variable `_T` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 136 136 | from collections import namedtuple 137 137 | 138 138 | _P = ParamSpec("_P") @@ -213,7 +213,7 @@ RUF052.py:140:5: RUF052 [*] Local dummy variable `_NT` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 137 137 | 138 138 | _P = ParamSpec("_P") 139 139 | _T = TypeVar(name="_T", covariant=True, bound=int|str) @@ -239,7 +239,7 @@ RUF052.py:141:5: RUF052 [*] Local dummy variable `_E` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 138 138 | _P = ParamSpec("_P") 139 139 | _T = TypeVar(name="_T", covariant=True, bound=int|str) 140 140 | _NT = NamedTuple("_NT", [("foo", int)]) @@ -264,7 +264,7 @@ RUF052.py:142:5: RUF052 [*] Local dummy variable `_NT2` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 139 139 | _T = TypeVar(name="_T", covariant=True, bound=int|str) 140 140 | _NT = NamedTuple("_NT", [("foo", int)]) 141 141 | _E = Enum("_E", ["a", "b", "c"]) @@ -288,7 +288,7 @@ RUF052.py:143:5: RUF052 [*] Local dummy variable `_NT3` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 140 140 | _NT = NamedTuple("_NT", [("foo", int)]) 141 141 | _E = Enum("_E", ["a", "b", "c"]) 142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z']) @@ -310,7 +310,7 @@ RUF052.py:144:5: RUF052 [*] Local dummy variable `_DynamicClass` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 141 141 | _E = Enum("_E", ["a", "b", "c"]) 142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z']) 143 143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z']) @@ -332,7 +332,7 @@ RUF052.py:145:5: RUF052 [*] Local dummy variable `_NotADynamicClass` is accessed | = help: Remove leading underscores -ℹ Safe fix +ℹ Unsafe fix 142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z']) 143 143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z']) 144 144 | _DynamicClass = type("_DynamicClass", (), {}) From 4b8c815b279dc407da55b106e86edcb3f267805a Mon Sep 17 00:00:00 2001 From: InSync Date: Tue, 10 Dec 2024 15:39:46 +0700 Subject: [PATCH 11/14] [`flake8-bugbear`] `itertools.batched()` without explicit `strict` (`B911`) (#14408) ## Summary Resolves #14387. ## Test Plan `cargo nextest run` and `cargo insta test`. --------- Co-authored-by: Micha Reiser --- .../test/fixtures/flake8_bugbear/B911.py | 59 ++++++ .../src/checkers/ast/analyze/expression.rs | 3 + crates/ruff_linter/src/codes.rs | 1 + .../src/rules/flake8_bugbear/mod.rs | 1 + .../rules/batched_without_explicit_strict.rs | 91 ++++++++++ .../src/rules/flake8_bugbear/rules/mod.rs | 2 + .../rules/zip_without_explicit_strict.rs | 6 +- ...__flake8_bugbear__tests__B911_B911.py.snap | 169 ++++++++++++++++++ ruff.schema.json | 2 + 9 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B911.py create mode 100644 crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs create mode 100644 crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B911_B911.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B911.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B911.py new file mode 100644 index 0000000000000..44a027a88a7b0 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B911.py @@ -0,0 +1,59 @@ +from itertools import batched, count, cycle, repeat + + +# Errors +batched(range(3), 1) +batched("abc", 2) +batched([i for i in range(42)], some_n) +batched((foo for foo in cycle())) +batched(itertools.batched([1, 2, 3], strict=True)) + +# Errors (limited iterators). +batched(repeat(1, 1)) +batched(repeat(1, times=4)) + +# No fix +batched([], **kwargs) + +# No errors +batched() +batched(range(3), 0, strict=True) +batched(["a", "b"], count, strict=False) +batched(("a", "b", "c"), zip(repeat()), strict=True) + +# No errors (infinite iterators) +batched(cycle("ABCDEF"), 3) +batched(count(), qux + lorem) +batched(repeat(1), ipsum // 19 @ 0x1) +batched(repeat(1, None)) +batched(repeat(1, times=None)) + + +import itertools + +# Errors +itertools.batched(range(3), 1) +itertools.batched("abc", 2) +itertools.batched([i for i in range(42)], some_n) +itertools.batched((foo for foo in cycle())) +itertools.batched(itertools.batched([1, 2, 3], strict=True)) + +# Errors (limited iterators). +itertools.batched(repeat(1, 1)) +itertools.batched(repeat(1, times=4)) + +# No fix +itertools.batched([], **kwargs) + +# No errors +itertools.batched() +itertools.batched(range(3), 0, strict=True) +itertools.batched(["a", "b"], count, strict=False) +itertools.batched(("a", "b", "c"), zip(repeat()), strict=True) + +# No errors (infinite iterators) +itertools.batched(cycle("ABCDEF"), 3) +itertools.batched(count(), qux + lorem) +itertools.batched(repeat(1), ipsum // 19 @ 0x1) +itertools.batched(repeat(1, None)) +itertools.batched(repeat(1, times=None)) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index b86389c74d8d4..10f56f3f99941 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1099,6 +1099,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::DotlessPathlibWithSuffix) { flake8_use_pathlib::rules::dotless_pathlib_with_suffix(checker, call); } + if checker.enabled(Rule::BatchedWithoutExplicitStrict) { + flake8_bugbear::rules::batched_without_explicit_strict(checker, call); + } } Expr::Dict(dict) => { if checker.any_enabled(&[ diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 7a87494341276..c673a0987d0ab 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -358,6 +358,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bugbear, "904") => (RuleGroup::Stable, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept), (Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict), (Flake8Bugbear, "909") => (RuleGroup::Preview, rules::flake8_bugbear::rules::LoopIteratorMutation), + (Flake8Bugbear, "911") => (RuleGroup::Preview, rules::flake8_bugbear::rules::BatchedWithoutExplicitStrict), // flake8-blind-except (Flake8BlindExcept, "001") => (RuleGroup::Stable, rules::flake8_blind_except::rules::BlindExcept), diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs index afe9d349f468c..b254847c38937 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs @@ -66,6 +66,7 @@ mod tests { #[test_case(Rule::ReturnInGenerator, Path::new("B901.py"))] #[test_case(Rule::LoopIteratorMutation, Path::new("B909.py"))] #[test_case(Rule::MutableContextvarDefault, Path::new("B039.py"))] + #[test_case(Rule::BatchedWithoutExplicitStrict, Path::new("B911.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs new file mode 100644 index 0000000000000..34893b109f375 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs @@ -0,0 +1,91 @@ +use crate::checkers::ast::Checker; +use crate::rules::flake8_bugbear::rules::is_infinite_iterable; +use crate::settings::types::PythonVersion; +use ruff_diagnostics::{Diagnostic, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast::ExprCall; + +/// ## What it does +/// Checks for `itertools.batched` calls without an explicit `strict` parameter. +/// +/// ## Why is this bad? +/// By default, if the length of the iterable is not divisible by +/// the second argument to `itertools.batched`, the last batch +/// will be shorter than the rest. +/// +/// In Python 3.13, a `strict` parameter was added which allows controlling if the batches must be of uniform length. +/// Pass `strict=True` to raise a `ValueError` if the batches are of non-uniform length. +/// Otherwise, pass `strict=False` to make the intention explicit. +/// +/// ## Example +/// ```python +/// itertools.batched(iterable, n) +/// ``` +/// +/// Use instead if the batches must be of uniform length: +/// ```python +/// itertools.batched(iterable, n, strict=True) +/// ``` +/// +/// Or if the batches can be of non-uniform length: +/// ```python +/// itertools.batched(iterable, n, strict=False) +/// ``` +/// +/// ## Known deviations +/// Unlike the upstream `B911`, this rule will not report infinite iterators +/// (e.g., `itertools.cycle(...)`). +/// +/// ## Options +/// - `target-version` +/// +/// ## References +/// - [Python documentation: `batched`](https://docs.python.org/3/library/itertools.html#batched) +#[derive(ViolationMetadata)] +pub(crate) struct BatchedWithoutExplicitStrict; + +impl Violation for BatchedWithoutExplicitStrict { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::None; + + #[derive_message_formats] + fn message(&self) -> String { + "`itertools.batched()` without an explicit `strict` parameter".to_string() + } + + fn fix_title(&self) -> Option { + Some("Add an explicit `strict` parameter".to_string()) + } +} + +/// B911 +pub(crate) fn batched_without_explicit_strict(checker: &mut Checker, call: &ExprCall) { + if checker.settings.target_version < PythonVersion::Py313 { + return; + } + + let semantic = checker.semantic(); + let (func, arguments) = (&call.func, &call.arguments); + + let Some(qualified_name) = semantic.resolve_qualified_name(func) else { + return; + }; + + if !matches!(qualified_name.segments(), ["itertools", "batched"]) { + return; + } + + if arguments.find_keyword("strict").is_some() { + return; + } + + let Some(iterable) = arguments.find_positional(0) else { + return; + }; + + if is_infinite_iterable(iterable, semantic) { + return; + } + + let diagnostic = Diagnostic::new(BatchedWithoutExplicitStrict, call.range); + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mod.rs index b4af7755dd435..8fcca6883ee3f 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/mod.rs @@ -2,6 +2,7 @@ pub(crate) use abstract_base_class::*; pub(crate) use assert_false::*; pub(crate) use assert_raises_exception::*; pub(crate) use assignment_to_os_environ::*; +pub(crate) use batched_without_explicit_strict::*; pub(crate) use cached_instance_method::*; pub(crate) use duplicate_exceptions::*; pub(crate) use duplicate_value::*; @@ -40,6 +41,7 @@ mod abstract_base_class; mod assert_false; mod assert_raises_exception; mod assignment_to_os_environ; +mod batched_without_explicit_strict; mod cached_instance_method; mod duplicate_exceptions; mod duplicate_value; diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index 272ea7a596ac6..9d512b577b16e 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -17,7 +17,7 @@ use crate::fix::edits::add_argument; /// iterable. This can lead to subtle bugs. /// /// Pass `strict=True` to raise a `ValueError` if the iterables are of -/// non-uniform length. Alternatively, if the iterables are deliberately +/// non-uniform length. Alternatively, if the iterables are deliberately of /// different lengths, pass `strict=False` to make the intention explicit. /// /// ## Example @@ -61,7 +61,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::Exp .arguments .args .iter() - .any(|arg| is_infinite_iterator(arg, semantic)) + .any(|arg| is_infinite_iterable(arg, semantic)) { checker.diagnostics.push( Diagnostic::new(ZipWithoutExplicitStrict, call.range()).with_fix(Fix::applicable_edit( @@ -89,7 +89,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::Exp /// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to /// `itertools.cycle` or similar). -fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool { +pub(crate) fn is_infinite_iterable(arg: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, arguments: Arguments { args, keywords, .. }, diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B911_B911.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B911_B911.py.snap new file mode 100644 index 0000000000000..850a173070028 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B911_B911.py.snap @@ -0,0 +1,169 @@ +--- +source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +snapshot_kind: text +--- +B911.py:5:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +4 | # Errors +5 | batched(range(3), 1) + | ^^^^^^^^^^^^^^^^^^^^ B911 +6 | batched("abc", 2) +7 | batched([i for i in range(42)], some_n) + | + = help: Add an explicit `strict` parameter + +B911.py:6:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +4 | # Errors +5 | batched(range(3), 1) +6 | batched("abc", 2) + | ^^^^^^^^^^^^^^^^^ B911 +7 | batched([i for i in range(42)], some_n) +8 | batched((foo for foo in cycle())) + | + = help: Add an explicit `strict` parameter + +B911.py:7:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +5 | batched(range(3), 1) +6 | batched("abc", 2) +7 | batched([i for i in range(42)], some_n) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +8 | batched((foo for foo in cycle())) +9 | batched(itertools.batched([1, 2, 3], strict=True)) + | + = help: Add an explicit `strict` parameter + +B911.py:8:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +6 | batched("abc", 2) +7 | batched([i for i in range(42)], some_n) +8 | batched((foo for foo in cycle())) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +9 | batched(itertools.batched([1, 2, 3], strict=True)) + | + = help: Add an explicit `strict` parameter + +B911.py:9:1: B911 `itertools.batched()` without an explicit `strict` parameter + | + 7 | batched([i for i in range(42)], some_n) + 8 | batched((foo for foo in cycle())) + 9 | batched(itertools.batched([1, 2, 3], strict=True)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +10 | +11 | # Errors (limited iterators). + | + = help: Add an explicit `strict` parameter + +B911.py:12:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +11 | # Errors (limited iterators). +12 | batched(repeat(1, 1)) + | ^^^^^^^^^^^^^^^^^^^^^ B911 +13 | batched(repeat(1, times=4)) + | + = help: Add an explicit `strict` parameter + +B911.py:13:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +11 | # Errors (limited iterators). +12 | batched(repeat(1, 1)) +13 | batched(repeat(1, times=4)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +14 | +15 | # No fix + | + = help: Add an explicit `strict` parameter + +B911.py:16:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +15 | # No fix +16 | batched([], **kwargs) + | ^^^^^^^^^^^^^^^^^^^^^ B911 +17 | +18 | # No errors + | + = help: Add an explicit `strict` parameter + +B911.py:35:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +34 | # Errors +35 | itertools.batched(range(3), 1) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +36 | itertools.batched("abc", 2) +37 | itertools.batched([i for i in range(42)], some_n) + | + = help: Add an explicit `strict` parameter + +B911.py:36:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +34 | # Errors +35 | itertools.batched(range(3), 1) +36 | itertools.batched("abc", 2) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +37 | itertools.batched([i for i in range(42)], some_n) +38 | itertools.batched((foo for foo in cycle())) + | + = help: Add an explicit `strict` parameter + +B911.py:37:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +35 | itertools.batched(range(3), 1) +36 | itertools.batched("abc", 2) +37 | itertools.batched([i for i in range(42)], some_n) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +38 | itertools.batched((foo for foo in cycle())) +39 | itertools.batched(itertools.batched([1, 2, 3], strict=True)) + | + = help: Add an explicit `strict` parameter + +B911.py:38:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +36 | itertools.batched("abc", 2) +37 | itertools.batched([i for i in range(42)], some_n) +38 | itertools.batched((foo for foo in cycle())) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +39 | itertools.batched(itertools.batched([1, 2, 3], strict=True)) + | + = help: Add an explicit `strict` parameter + +B911.py:39:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +37 | itertools.batched([i for i in range(42)], some_n) +38 | itertools.batched((foo for foo in cycle())) +39 | itertools.batched(itertools.batched([1, 2, 3], strict=True)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +40 | +41 | # Errors (limited iterators). + | + = help: Add an explicit `strict` parameter + +B911.py:42:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +41 | # Errors (limited iterators). +42 | itertools.batched(repeat(1, 1)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +43 | itertools.batched(repeat(1, times=4)) + | + = help: Add an explicit `strict` parameter + +B911.py:43:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +41 | # Errors (limited iterators). +42 | itertools.batched(repeat(1, 1)) +43 | itertools.batched(repeat(1, times=4)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +44 | +45 | # No fix + | + = help: Add an explicit `strict` parameter + +B911.py:46:1: B911 `itertools.batched()` without an explicit `strict` parameter + | +45 | # No fix +46 | itertools.batched([], **kwargs) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911 +47 | +48 | # No errors + | + = help: Add an explicit `strict` parameter diff --git a/ruff.schema.json b/ruff.schema.json index 85e2c08b914cd..a464a0b142c06 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2891,6 +2891,8 @@ "B904", "B905", "B909", + "B91", + "B911", "BLE", "BLE0", "BLE00", From 7a0e9b34d0b97b905f3fbee52c98a0432a3438bf Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 10 Dec 2024 10:53:53 +0100 Subject: [PATCH 12/14] Reference `suppress-dummy-regex-options` in documentation of rules supporting it (#14888) ## Summary Fixes https://github.com/astral-sh/ruff/issues/14663 --- .../src/rules/flake8_annotations/rules/definition.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs index ee7ad9b1aecbc..cad9679a559aa 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs @@ -33,6 +33,9 @@ use crate::rules::ruff::typing::type_hint_resolves_to_any; /// ```python /// def foo(x: int): ... /// ``` +/// +/// ## Options +/// - `lint.flake8-annotations.suppress-dummy-args` #[derive(ViolationMetadata)] pub(crate) struct MissingTypeFunctionArgument { name: String, @@ -65,6 +68,9 @@ impl Violation for MissingTypeFunctionArgument { /// ```python /// def foo(*args: int): ... /// ``` +/// +/// ## Options +/// - `lint.flake8-annotations.suppress-dummy-args` #[derive(ViolationMetadata)] pub(crate) struct MissingTypeArgs { name: String, @@ -97,6 +103,9 @@ impl Violation for MissingTypeArgs { /// ```python /// def foo(**kwargs: int): ... /// ``` +/// +/// ## Options +/// - `lint.flake8-annotations.suppress-dummy-args` #[derive(ViolationMetadata)] pub(crate) struct MissingTypeKwargs { name: String, From 15fe5402518157ad581f3f85065eda11d0463712 Mon Sep 17 00:00:00 2001 From: InSync Date: Tue, 10 Dec 2024 20:05:51 +0700 Subject: [PATCH 13/14] Improve mdtests style (#14884) Co-authored-by: Alex Waygood --- .../resources/mdtest/annotations/any.md | 12 +- .../mdtest/annotations/literal_string.md | 32 +- .../resources/mdtest/annotations/string.md | 146 ++++----- .../mdtest/assignment/annotations.md | 24 +- .../resources/mdtest/assignment/augmented.md | 162 +++++----- .../resources/mdtest/attributes.md | 99 +++--- .../resources/mdtest/binary/instances.md | 18 +- .../resources/mdtest/boolean/short_circuit.md | 60 ++-- .../mdtest/call/callable_instance.md | 55 ++-- .../resources/mdtest/call/function.md | 14 +- .../resources/mdtest/call/union.md | 101 +++---- .../resources/mdtest/comparison/identity.md | 45 ++- .../comparison/instances/rich_comparison.md | 18 +- .../resources/mdtest/comparison/integers.md | 10 +- .../mdtest/comparison/intersections.md | 86 +++--- .../resources/mdtest/comparison/strings.md | 25 +- .../resources/mdtest/comparison/tuples.md | 199 ++++++------ .../resources/mdtest/comparison/unions.md | 87 +++--- .../mdtest/comparison/unsupported.md | 72 +++-- .../mdtest/conditional/if_expression.md | 44 +-- .../mdtest/conditional/if_statement.md | 163 +++++----- .../resources/mdtest/conditional/match.md | 1 + .../resources/mdtest/declaration/error.md | 47 ++- .../mdtest/exception/invalid_syntax.md | 1 - .../resources/mdtest/expression/attribute.md | 31 +- .../resources/mdtest/expression/boolean.md | 55 ++-- .../resources/mdtest/expression/if.md | 18 +- .../resources/mdtest/expression/len.md | 3 +- .../resources/mdtest/import/conditional.md | 27 +- .../resources/mdtest/loops/for.md | 70 ++--- .../resources/mdtest/loops/while_loop.md | 61 ++-- .../resources/mdtest/narrow/bool-call.md | 38 +-- .../resources/mdtest/narrow/boolean.md | 82 ++--- .../mdtest/narrow/conditionals/boolean.md | 285 +++++++----------- .../mdtest/narrow/conditionals/elif_else.md | 74 ++--- .../mdtest/narrow/conditionals/is.md | 87 +++--- .../mdtest/narrow/conditionals/is_not.md | 82 +++-- .../mdtest/narrow/conditionals/nested.md | 63 ++-- .../mdtest/narrow/conditionals/not.md | 30 +- .../mdtest/narrow/conditionals/not_eq.md | 130 ++++---- .../resources/mdtest/narrow/isinstance.md | 192 +++++------- .../resources/mdtest/narrow/issubclass.md | 136 ++++----- .../resources/mdtest/narrow/match.md | 19 +- .../mdtest/narrow/post_if_statement.md | 63 ++-- .../resources/mdtest/narrow/type.md | 114 +++---- .../resources/mdtest/scopes/unbound.md | 10 +- .../mdtest/shadowing/variable_declaration.md | 15 +- .../resources/mdtest/subscript/bytes.md | 28 +- .../resources/mdtest/subscript/class.md | 89 +++--- .../resources/mdtest/subscript/instance.md | 24 +- .../resources/mdtest/subscript/string.md | 26 +- .../resources/mdtest/subscript/tuple.md | 9 +- .../resources/mdtest/type_of/basic.md | 49 +-- .../resources/mdtest/unary/not.md | 42 ++- .../resources/mdtest/with/sync.md | 71 ++--- 55 files changed, 1482 insertions(+), 2062 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md index a1db55272c3bb..17e5a99b2aead 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md @@ -34,8 +34,7 @@ If you define your own class named `Any`, using that in a type expression refers isn't a spelling of the Any type. ```py -class Any: - pass +class Any: ... x: Any @@ -59,8 +58,7 @@ assignable to `int`. ```py from typing import Any -class Subclass(Any): - pass +class Subclass(Any): ... reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]] @@ -68,8 +66,6 @@ x: Subclass = 1 # error: [invalid-assignment] # TODO: no diagnostic y: int = Subclass() # error: [invalid-assignment] -def f() -> Subclass: - pass - -reveal_type(f()) # revealed: Subclass +def _(s: Subclass): + reveal_type(s) # revealed: Subclass ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md index 28bc3898f5c05..35cf90a0cb3c4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md @@ -89,28 +89,26 @@ vice versa. ```py from typing_extensions import Literal, LiteralString -def coinflip() -> bool: - return True +def _(flag: bool): + foo_1: Literal["foo"] = "foo" + bar_1: LiteralString = foo_1 # fine -foo_1: Literal["foo"] = "foo" -bar_1: LiteralString = foo_1 # fine + foo_2 = "foo" if flag else "bar" + reveal_type(foo_2) # revealed: Literal["foo", "bar"] + bar_2: LiteralString = foo_2 # fine -foo_2 = "foo" if coinflip() else "bar" -reveal_type(foo_2) # revealed: Literal["foo", "bar"] -bar_2: LiteralString = foo_2 # fine + foo_3: LiteralString = "foo" * 1_000_000_000 + bar_3: str = foo_2 # fine -foo_3: LiteralString = "foo" * 1_000_000_000 -bar_3: str = foo_2 # fine + baz_1: str = str() + qux_1: LiteralString = baz_1 # error: [invalid-assignment] -baz_1: str = str() -qux_1: LiteralString = baz_1 # error: [invalid-assignment] + baz_2: LiteralString = "baz" * 1_000_000_000 + qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment] -baz_2: LiteralString = "baz" * 1_000_000_000 -qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment] - -baz_3 = "foo" if coinflip() else 1 -reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1] -qux_3: LiteralString = baz_3 # error: [invalid-assignment] + baz_3 = "foo" if flag else 1 + reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1] + qux_3: LiteralString = baz_3 # error: [invalid-assignment] ``` ### Narrowing diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/string.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/string.md index 140a9ad0033c7..b72b542529343 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/string.md @@ -3,75 +3,56 @@ ## Simple ```py -def f() -> "int": - return 1 - -reveal_type(f()) # revealed: int +def f(v: "int"): + reveal_type(v) # revealed: int ``` ## Nested ```py -def f() -> "'int'": - return 1 - -reveal_type(f()) # revealed: int +def f(v: "'int'"): + reveal_type(v) # revealed: int ``` ## Type expression ```py -def f1() -> "int | str": - return 1 - -def f2() -> "tuple[int, str]": - return 1 - -reveal_type(f1()) # revealed: int | str -reveal_type(f2()) # revealed: tuple[int, str] +def f1(v: "int | str", w: "tuple[int, str]"): + reveal_type(v) # revealed: int | str + reveal_type(w) # revealed: tuple[int, str] ``` ## Partial ```py -def f() -> tuple[int, "str"]: - return 1 - -reveal_type(f()) # revealed: tuple[int, str] +def f(v: tuple[int, "str"]): + reveal_type(v) # revealed: tuple[int, str] ``` ## Deferred ```py -def f() -> "Foo": - return Foo() - -class Foo: - pass +def f(v: "Foo"): + reveal_type(v) # revealed: Foo -reveal_type(f()) # revealed: Foo +class Foo: ... ``` ## Deferred (undefined) ```py # error: [unresolved-reference] -def f() -> "Foo": - pass - -reveal_type(f()) # revealed: Unknown +def f(v: "Foo"): + reveal_type(v) # revealed: Unknown ``` ## Partial deferred ```py -def f() -> int | "Foo": - return 1 - -class Foo: - pass +def f(v: int | "Foo"): + reveal_type(v) # revealed: int | Foo -reveal_type(f()) # revealed: int | Foo +class Foo: ... ``` ## `typing.Literal` @@ -79,65 +60,43 @@ reveal_type(f()) # revealed: int | Foo ```py from typing import Literal -def f1() -> Literal["Foo", "Bar"]: - return "Foo" - -def f2() -> 'Literal["Foo", "Bar"]': - return "Foo" +def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'): + reveal_type(v) # revealed: Literal["Foo", "Bar"] + reveal_type(w) # revealed: Literal["Foo", "Bar"] -class Foo: - pass - -reveal_type(f1()) # revealed: Literal["Foo", "Bar"] -reveal_type(f2()) # revealed: Literal["Foo", "Bar"] +class Foo: ... ``` ## Various string kinds ```py -# error: [annotation-raw-string] "Type expressions cannot use raw string literal" -def f1() -> r"int": - return 1 - -# error: [annotation-f-string] "Type expressions cannot use f-strings" -def f2() -> f"int": - return 1 - -# error: [annotation-byte-string] "Type expressions cannot use bytes literal" -def f3() -> b"int": - return 1 - -def f4() -> "int": - return 1 - -# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals" -def f5() -> "in" "t": - return 1 - -# error: [annotation-escape-character] "Type expressions cannot contain escape characters" -def f6() -> "\N{LATIN SMALL LETTER I}nt": - return 1 - -# error: [annotation-escape-character] "Type expressions cannot contain escape characters" -def f7() -> "\x69nt": - return 1 - -def f8() -> """int""": - return 1 - -# error: [annotation-byte-string] "Type expressions cannot use bytes literal" -def f9() -> "b'int'": - return 1 - -reveal_type(f1()) # revealed: Unknown -reveal_type(f2()) # revealed: Unknown -reveal_type(f3()) # revealed: Unknown -reveal_type(f4()) # revealed: int -reveal_type(f5()) # revealed: Unknown -reveal_type(f6()) # revealed: Unknown -reveal_type(f7()) # revealed: Unknown -reveal_type(f8()) # revealed: int -reveal_type(f9()) # revealed: Unknown +def f1( + # error: [annotation-raw-string] "Type expressions cannot use raw string literal" + a: r"int", + # error: [annotation-f-string] "Type expressions cannot use f-strings" + b: f"int", + # error: [annotation-byte-string] "Type expressions cannot use bytes literal" + c: b"int", + d: "int", + # error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals" + e: "in" "t", + # error: [annotation-escape-character] "Type expressions cannot contain escape characters" + f: "\N{LATIN SMALL LETTER I}nt", + # error: [annotation-escape-character] "Type expressions cannot contain escape characters" + g: "\x69nt", + h: """int""", + # error: [annotation-byte-string] "Type expressions cannot use bytes literal" + i: "b'int'", +): + reveal_type(a) # revealed: Unknown + reveal_type(b) # revealed: Unknown + reveal_type(c) # revealed: Unknown + reveal_type(d) # revealed: int + reveal_type(e) # revealed: Unknown + reveal_type(f) # revealed: Unknown + reveal_type(g) # revealed: Unknown + reveal_type(h) # revealed: int + reveal_type(i) # revealed: Unknown ``` ## Various string kinds in `typing.Literal` @@ -145,10 +104,8 @@ reveal_type(f9()) # revealed: Unknown ```py from typing import Literal -def f() -> Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]: - return "normal" - -reveal_type(f()) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"] +def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]): + reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"] ``` ## Class variables @@ -175,8 +132,7 @@ c: "Foo" # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`" d: "Foo" = 1 -class Foo: - pass +class Foo: ... c = Foo() diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md index c83628b83ec2c..c3977ed46b6c4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md @@ -78,20 +78,10 @@ c: tuple[str | int, str] = ([], "foo") ## PEP-604 annotations are supported ```py -def foo() -> str | int | None: - return None - -reveal_type(foo()) # revealed: str | int | None - -def bar() -> str | str | None: - return None - -reveal_type(bar()) # revealed: str | None - -def baz() -> str | str: - return "Hello, world!" - -reveal_type(baz()) # revealed: str +def foo(v: str | int | None, w: str | str | None, x: str | str): + reveal_type(v) # revealed: str | int | None + reveal_type(w) # revealed: str | None + reveal_type(x) # revealed: str ``` ## Attribute expressions in type annotations are understood @@ -118,8 +108,7 @@ from __future__ import annotations x: Foo -class Foo: - pass +class Foo: ... x = Foo() reveal_type(x) # revealed: Foo @@ -130,8 +119,7 @@ reveal_type(x) # revealed: Foo ```pyi path=main.pyi x: Foo -class Foo: - pass +class Foo: ... x = Foo() reveal_type(x) # revealed: Foo diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md index 1e28506e1f5d3..cff18e7fb29c8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md @@ -49,134 +49,116 @@ reveal_type(x) # revealed: int ## Method union ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + class Foo: + if flag: + def __iadd__(self, other: int) -> str: + return "Hello, world!" + else: + def __iadd__(self, other: int) -> int: + return 42 -flag = bool_instance() - -class Foo: - if bool_instance(): - def __iadd__(self, other: int) -> str: - return "Hello, world!" - else: - def __iadd__(self, other: int) -> int: - return 42 - -f = Foo() -f += 12 + f = Foo() + f += 12 -reveal_type(f) # revealed: str | int + reveal_type(f) # revealed: str | int ``` ## Partially bound `__iadd__` ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + class Foo: + if flag: + def __iadd__(self, other: str) -> int: + return 42 -class Foo: - if bool_instance(): - def __iadd__(self, other: str) -> int: - return 42 - -f = Foo() + f = Foo() -# TODO: We should emit an `unsupported-operator` error here, possibly with the information -# that `Foo.__iadd__` may be unbound as additional context. -f += "Hello, world!" + # TODO: We should emit an `unsupported-operator` error here, possibly with the information + # that `Foo.__iadd__` may be unbound as additional context. + f += "Hello, world!" -reveal_type(f) # revealed: int | Unknown + reveal_type(f) # revealed: int | Unknown ``` ## Partially bound with `__add__` ```py -def bool_instance() -> bool: - return True - -class Foo: - def __add__(self, other: str) -> str: - return "Hello, world!" - if bool_instance(): - def __iadd__(self, other: str) -> int: - return 42 +def _(flag: bool): + class Foo: + def __add__(self, other: str) -> str: + return "Hello, world!" + if flag: + def __iadd__(self, other: str) -> int: + return 42 -f = Foo() -f += "Hello, world!" + f = Foo() + f += "Hello, world!" -reveal_type(f) # revealed: int | str + reveal_type(f) # revealed: int | str ``` ## Partially bound target union ```py -def bool_instance() -> bool: - return True - -class Foo: - def __add__(self, other: int) -> str: - return "Hello, world!" - if bool_instance(): - def __iadd__(self, other: int) -> int: - return 42 +def _(flag1: bool, flag2: bool): + class Foo: + def __add__(self, other: int) -> str: + return "Hello, world!" + if flag1: + def __iadd__(self, other: int) -> int: + return 42 -if bool_instance(): - f = Foo() -else: - f = 42.0 -f += 12 + if flag2: + f = Foo() + else: + f = 42.0 + f += 12 -reveal_type(f) # revealed: int | str | float + reveal_type(f) # revealed: int | str | float ``` ## Target union ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -class Foo: - def __iadd__(self, other: int) -> str: - return "Hello, world!" +def _(flag: bool): + class Foo: + def __iadd__(self, other: int) -> str: + return "Hello, world!" -if flag: - f = Foo() -else: - f = 42.0 -f += 12 + if flag: + f = Foo() + else: + f = 42.0 + f += 12 -reveal_type(f) # revealed: str | float + reveal_type(f) # revealed: str | float ``` ## Partially bound target union with `__add__` ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -class Foo: - def __add__(self, other: int) -> str: - return "Hello, world!" - if bool_instance(): - def __iadd__(self, other: int) -> int: - return 42 +def f(flag: bool, flag2: bool): + class Foo: + def __add__(self, other: int) -> str: + return "Hello, world!" + if flag: + def __iadd__(self, other: int) -> int: + return 42 -class Bar: - def __add__(self, other: int) -> bytes: - return b"Hello, world!" + class Bar: + def __add__(self, other: int) -> bytes: + return b"Hello, world!" - def __iadd__(self, other: int) -> float: - return 42.0 + def __iadd__(self, other: int) -> float: + return 42.0 -if flag: - f = Foo() -else: - f = Bar() -f += 12 + if flag2: + f = Foo() + else: + f = Bar() + f += 12 -reveal_type(f) # revealed: int | str | float + reveal_type(f) # revealed: int | str | float ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index d4b6184f72d3b..df7cbaabcaa8c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -3,27 +3,23 @@ ## Union of attributes ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -if flag: - class C1: - x = 1 - -else: - class C1: - x = 2 - -class C2: +def _(flag: bool): if flag: - x = 3 + class C1: + x = 1 + else: - x = 4 + class C1: + x = 2 -reveal_type(C1.x) # revealed: Literal[1, 2] -reveal_type(C2.x) # revealed: Literal[3, 4] + class C2: + if flag: + x = 3 + else: + x = 4 + + reveal_type(C1.x) # revealed: Literal[1, 2] + reveal_type(C2.x) # revealed: Literal[3, 4] ``` ## Inherited attributes @@ -68,24 +64,19 @@ reveal_type(A.X) # revealed: Literal[42] In this example, the `x` attribute is not defined in the `C2` element of the union: ```py -def bool_instance() -> bool: - return True - -class C1: - x = 1 - -class C2: ... +def _(flag1: bool, flag2: bool): + class C1: + x = 1 -class C3: - x = 3 + class C2: ... -flag1 = bool_instance() -flag2 = bool_instance() + class C3: + x = 3 -C = C1 if flag1 else C2 if flag2 else C3 + C = C1 if flag1 else C2 if flag2 else C3 -# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound" -reveal_type(C.x) # revealed: Literal[1, 3] + # error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound" + reveal_type(C.x) # revealed: Literal[1, 3] ``` ### Possibly-unbound within a class @@ -94,26 +85,21 @@ We raise the same diagnostic if the attribute is possibly-unbound in at least on union: ```py -def bool_instance() -> bool: - return True - -class C1: - x = 1 - -class C2: - if bool_instance(): - x = 2 +def _(flag: bool, flag1: bool, flag2: bool): + class C1: + x = 1 -class C3: - x = 3 + class C2: + if flag: + x = 2 -flag1 = bool_instance() -flag2 = bool_instance() + class C3: + x = 3 -C = C1 if flag1 else C2 if flag2 else C3 + C = C1 if flag1 else C2 if flag2 else C3 -# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound" -reveal_type(C.x) # revealed: Literal[1, 2, 3] + # error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound" + reveal_type(C.x) # revealed: Literal[1, 2, 3] ``` ## Unions with all paths unbound @@ -121,16 +107,11 @@ reveal_type(C.x) # revealed: Literal[1, 2, 3] If the symbol is unbound in all elements of the union, we detect that: ```py -def bool_instance() -> bool: - return True - -class C1: ... -class C2: ... - -flag = bool_instance() - -C = C1 if flag else C2 +def _(flag: bool): + class C1: ... + class C2: ... + C = C1 if flag else C2 -# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`" -reveal_type(C.x) # revealed: Unknown + # error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`" + reveal_type(C.x) # revealed: Unknown ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 15029fead98eb..0f614802f3584 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -281,20 +281,12 @@ reveal_type(42 + 4.2) # revealed: int # TODO should be complex, need to check arg type and fall back to `rhs.__radd__` reveal_type(3 + 3j) # revealed: int -def returns_int() -> int: - return 42 +def _(x: bool, y: int): + reveal_type(x + y) # revealed: int + reveal_type(4.2 + x) # revealed: float -def returns_bool() -> bool: - return True - -x = returns_bool() -y = returns_int() - -reveal_type(x + y) # revealed: int -reveal_type(4.2 + x) # revealed: float - -# TODO should be float, need to check arg type and fall back to `rhs.__radd__` -reveal_type(y + 4.12) # revealed: int + # TODO should be float, need to check arg type and fall back to `rhs.__radd__` + reveal_type(y + 4.12) # revealed: int ``` ## With literal types diff --git a/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md b/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md index fc475af2641f8..9ee078606d5cd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md +++ b/crates/red_knot_python_semantic/resources/mdtest/boolean/short_circuit.md @@ -7,29 +7,25 @@ Similarly, in `and` expressions, if the left-hand side is falsy, the right-hand evaluated. ```py -def bool_instance() -> bool: - return True - -if bool_instance() or (x := 1): - # error: [possibly-unresolved-reference] - reveal_type(x) # revealed: Literal[1] - -if bool_instance() and (x := 1): - # error: [possibly-unresolved-reference] - reveal_type(x) # revealed: Literal[1] +def _(flag: bool): + if flag or (x := 1): + # error: [possibly-unresolved-reference] + reveal_type(x) # revealed: Literal[1] + + if flag and (x := 1): + # error: [possibly-unresolved-reference] + reveal_type(x) # revealed: Literal[1] ``` ## First expression is always evaluated ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + if (x := 1) or flag: + reveal_type(x) # revealed: Literal[1] -if (x := 1) or bool_instance(): - reveal_type(x) # revealed: Literal[1] - -if (x := 1) and bool_instance(): - reveal_type(x) # revealed: Literal[1] + if (x := 1) and flag: + reveal_type(x) # revealed: Literal[1] ``` ## Statically known truthiness @@ -49,30 +45,26 @@ if True and (x := 1): ## Later expressions can always use variables from earlier expressions ```py -def bool_instance() -> bool: - return True - -bool_instance() or (x := 1) or reveal_type(x) # revealed: Literal[1] +def _(flag: bool): + flag or (x := 1) or reveal_type(x) # revealed: Literal[1] -# error: [unresolved-reference] -bool_instance() or reveal_type(y) or (y := 1) # revealed: Unknown + # error: [unresolved-reference] + flag or reveal_type(y) or (y := 1) # revealed: Unknown ``` ## Nested expressions ```py -def bool_instance() -> bool: - return True - -if bool_instance() or ((x := 1) and bool_instance()): - # error: [possibly-unresolved-reference] - reveal_type(x) # revealed: Literal[1] +def _(flag1: bool, flag2: bool): + if flag1 or ((x := 1) and flag2): + # error: [possibly-unresolved-reference] + reveal_type(x) # revealed: Literal[1] -if ((y := 1) and bool_instance()) or bool_instance(): - reveal_type(y) # revealed: Literal[1] + if ((y := 1) and flag1) or flag2: + reveal_type(y) # revealed: Literal[1] -# error: [possibly-unresolved-reference] -if (bool_instance() and (z := 1)) or reveal_type(z): # revealed: Literal[1] # error: [possibly-unresolved-reference] - reveal_type(z) # revealed: Literal[1] + if (flag1 and (z := 1)) or reveal_type(z): # revealed: Literal[1] + # error: [possibly-unresolved-reference] + reveal_type(z) # revealed: Literal[1] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md b/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md index 95e82fc5720b1..746aee725f547 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md @@ -22,29 +22,27 @@ reveal_type(b) # revealed: Unknown ## Possibly unbound `__call__` method ```py -def flag() -> bool: ... - -class PossiblyNotCallable: - if flag(): - def __call__(self) -> int: ... - -a = PossiblyNotCallable() -result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" -reveal_type(result) # revealed: int +def _(flag: bool): + class PossiblyNotCallable: + if flag: + def __call__(self) -> int: ... + + a = PossiblyNotCallable() + result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)" + reveal_type(result) # revealed: int ``` ## Possibly unbound callable ```py -def flag() -> bool: ... - -if flag(): - class PossiblyUnbound: - def __call__(self) -> int: ... - -# error: [possibly-unresolved-reference] -a = PossiblyUnbound() -reveal_type(a()) # revealed: int +def _(flag: bool): + if flag: + class PossiblyUnbound: + def __call__(self) -> int: ... + + # error: [possibly-unresolved-reference] + a = PossiblyUnbound() + reveal_type(a()) # revealed: int ``` ## Non-callable `__call__` @@ -61,15 +59,14 @@ reveal_type(a()) # revealed: Unknown ## Possibly non-callable `__call__` ```py -def flag() -> bool: ... - -class NonCallable: - if flag(): - __call__ = 1 - else: - def __call__(self) -> int: ... - -a = NonCallable() -# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)" -reveal_type(a()) # revealed: Unknown | int +def _(flag: bool): + class NonCallable: + if flag: + __call__ = 1 + else: + def __call__(self) -> int: ... + + a = NonCallable() + # error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)" + reveal_type(a()) # revealed: Unknown | int ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/function.md b/crates/red_knot_python_semantic/resources/mdtest/call/function.md index b67b14d558585..af7c1e2582cae 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/function.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/function.md @@ -57,12 +57,10 @@ x = nonsense() # error: "Object of type `Literal[123]` is not callable" ## Potentially unbound function ```py -def flag() -> bool: ... - -if flag(): - def foo() -> int: - return 42 - -# error: [possibly-unresolved-reference] -reveal_type(foo()) # revealed: int +def _(flag: bool): + if flag: + def foo() -> int: + return 42 + # error: [possibly-unresolved-reference] + reveal_type(foo()) # revealed: int ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/union.md b/crates/red_knot_python_semantic/resources/mdtest/call/union.md index 911f21947ef3c..55483854888af 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/union.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/union.md @@ -3,22 +3,14 @@ ## Union of return types ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -if flag: - - def f() -> int: - return 1 - -else: - - def f() -> str: - return "foo" - -reveal_type(f()) # revealed: int | str +def _(flag: bool): + if flag: + def f() -> int: + return 1 + else: + def f() -> str: + return "foo" + reveal_type(f()) # revealed: int | str ``` ## Calling with an unknown union @@ -26,13 +18,10 @@ reveal_type(f()) # revealed: int | str ```py from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`" -def bool_instance() -> bool: +def coinflip() -> bool: return True -flag = bool_instance() - -if flag: - +if coinflip(): def f() -> int: return 1 @@ -44,20 +33,14 @@ reveal_type(f()) # revealed: Unknown | int Calling a union with a non-callable element should emit a diagnostic. ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -if flag: - f = 1 -else: - - def f() -> int: - return 1 - -x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)" -reveal_type(x) # revealed: Unknown | int +def _(flag: bool): + if flag: + f = 1 + else: + def f() -> int: + return 1 + x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)" + reveal_type(x) # revealed: Unknown | int ``` ## Multiple non-callable elements in a union @@ -65,23 +48,17 @@ reveal_type(x) # revealed: Unknown | int Calling a union with multiple non-callable elements should mention all of them in the diagnostic. ```py -def bool_instance() -> bool: - return True - -flag, flag2 = bool_instance(), bool_instance() - -if flag: - f = 1 -elif flag2: - f = "foo" -else: - - def f() -> int: - return 1 - -# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])" -# revealed: Unknown | int -reveal_type(f()) +def _(flag: bool, flag2: bool): + if flag: + f = 1 + elif flag2: + f = "foo" + else: + def f() -> int: + return 1 + # error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])" + # revealed: Unknown | int + reveal_type(f()) ``` ## All non-callable union elements @@ -89,16 +66,12 @@ reveal_type(f()) Calling a union with no callable elements can emit a simpler diagnostic. ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -if flag: - f = 1 -else: - f = "foo" - -x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable" -reveal_type(x) # revealed: Unknown +def _(flag: bool): + if flag: + f = 1 + else: + f = "foo" + + x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable" + reveal_type(x) # revealed: Unknown ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/identity.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/identity.md index bf162efa0a9e0..a636307a1dbfa 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/identity.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/identity.md @@ -3,38 +3,31 @@ ```py class A: ... -def get_a() -> A: ... -def get_object() -> object: ... +def _(a1: A, a2: A, o: object): + n1 = None + n2 = None -a1 = get_a() -a2 = get_a() + reveal_type(a1 is a1) # revealed: bool + reveal_type(a1 is a2) # revealed: bool -n1 = None -n2 = None + reveal_type(n1 is n1) # revealed: Literal[True] + reveal_type(n1 is n2) # revealed: Literal[True] -o = get_object() + reveal_type(a1 is n1) # revealed: Literal[False] + reveal_type(n1 is a1) # revealed: Literal[False] -reveal_type(a1 is a1) # revealed: bool -reveal_type(a1 is a2) # revealed: bool + reveal_type(a1 is o) # revealed: bool + reveal_type(n1 is o) # revealed: bool -reveal_type(n1 is n1) # revealed: Literal[True] -reveal_type(n1 is n2) # revealed: Literal[True] + reveal_type(a1 is not a1) # revealed: bool + reveal_type(a1 is not a2) # revealed: bool -reveal_type(a1 is n1) # revealed: Literal[False] -reveal_type(n1 is a1) # revealed: Literal[False] + reveal_type(n1 is not n1) # revealed: Literal[False] + reveal_type(n1 is not n2) # revealed: Literal[False] -reveal_type(a1 is o) # revealed: bool -reveal_type(n1 is o) # revealed: bool + reveal_type(a1 is not n1) # revealed: Literal[True] + reveal_type(n1 is not a1) # revealed: Literal[True] -reveal_type(a1 is not a1) # revealed: bool -reveal_type(a1 is not a2) # revealed: bool - -reveal_type(n1 is not n1) # revealed: Literal[False] -reveal_type(n1 is not n2) # revealed: Literal[False] - -reveal_type(a1 is not n1) # revealed: Literal[True] -reveal_type(n1 is not a1) # revealed: Literal[True] - -reveal_type(a1 is not o) # revealed: bool -reveal_type(n1 is not o) # revealed: bool + reveal_type(a1 is not o) # revealed: bool + reveal_type(n1 is not o) # revealed: bool ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md index cffec0bf724a0..c4bad9bbb30a9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md @@ -312,17 +312,9 @@ reveal_type(1 <= 2j) # revealed: bool reveal_type(1 > 2j) # revealed: bool reveal_type(1 >= 2j) # revealed: bool -def bool_instance() -> bool: - return True - -def int_instance() -> int: - return 42 - -x = bool_instance() -y = int_instance() - -reveal_type(x < y) # revealed: bool -reveal_type(y < x) # revealed: bool -reveal_type(4.2 < x) # revealed: bool -reveal_type(x < 4.2) # revealed: bool +def f(x: bool, y: int): + reveal_type(x < y) # revealed: bool + reveal_type(y < x) # revealed: bool + reveal_type(4.2 < x) # revealed: bool + reveal_type(x < 4.2) # revealed: bool ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md index a2092af10983b..a59e1510bf91d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md @@ -20,10 +20,8 @@ reveal_type(1 <= "" and 0 < 1) # revealed: bool ```py # TODO: implement lookup of `__eq__` on typeshed `int` stub. -def int_instance() -> int: - return 42 - -reveal_type(1 == int_instance()) # revealed: bool -reveal_type(9 < int_instance()) # revealed: bool -reveal_type(int_instance() < int_instance()) # revealed: bool +def _(a: int, b: int): + reveal_type(1 == a) # revealed: bool + reveal_type(9 < a) # revealed: bool + reveal_type(a < b) # revealed: bool ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md index 39efa500fa1fe..c8c3ffa977d12 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md @@ -14,21 +14,19 @@ class Child1(Base): class Child2(Base): ... -def get_base() -> Base: ... +def _(x: Base): + c1 = Child1() -x = get_base() -c1 = Child1() + # Create an intersection type through narrowing: + if isinstance(x, Child1): + if isinstance(x, Child2): + reveal_type(x) # revealed: Child1 & Child2 -# Create an intersection type through narrowing: -if isinstance(x, Child1): - if isinstance(x, Child2): - reveal_type(x) # revealed: Child1 & Child2 + reveal_type(x == 1) # revealed: Literal[True] - reveal_type(x == 1) # revealed: Literal[True] - - # Other comparison operators fall back to the base type: - reveal_type(x > 1) # revealed: bool - reveal_type(x is c1) # revealed: bool + # Other comparison operators fall back to the base type: + reveal_type(x > 1) # revealed: bool + reveal_type(x is c1) # revealed: bool ``` ## Negative contributions @@ -73,18 +71,15 @@ if x != "abc": #### Integers ```py -def get_int() -> int: ... - -x = get_int() - -if x != 1: - reveal_type(x) # revealed: int & ~Literal[1] +def _(x: int): + if x != 1: + reveal_type(x) # revealed: int & ~Literal[1] - reveal_type(x != 1) # revealed: Literal[True] - reveal_type(x != 2) # revealed: bool + reveal_type(x != 1) # revealed: Literal[True] + reveal_type(x != 2) # revealed: bool - reveal_type(x == 1) # revealed: Literal[False] - reveal_type(x == 2) # revealed: bool + reveal_type(x == 1) # revealed: Literal[False] + reveal_type(x == 2) # revealed: bool ``` ### Identity comparisons @@ -92,18 +87,15 @@ if x != 1: ```py class A: ... -def get_object() -> object: ... - -o = object() - -a = A() -n = None +def _(o: object): + a = A() + n = None -if o is not None: - reveal_type(o) # revealed: object & ~None + if o is not None: + reveal_type(o) # revealed: object & ~None - reveal_type(o is n) # revealed: Literal[False] - reveal_type(o is not n) # revealed: Literal[True] + reveal_type(o is n) # revealed: Literal[False] + reveal_type(o is not n) # revealed: Literal[True] ``` ## Diagnostics @@ -119,16 +111,13 @@ class Container: class NonContainer: ... -def get_object() -> object: ... +def _(x: object): + if isinstance(x, Container): + if isinstance(x, NonContainer): + reveal_type(x) # revealed: Container & NonContainer -x = get_object() - -if isinstance(x, Container): - if isinstance(x, NonContainer): - reveal_type(x) # revealed: Container & NonContainer - - # error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`" - reveal_type(2 in x) # revealed: bool + # error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`" + reveal_type(2 in x) # revealed: bool ``` ### Unsupported operators for negative contributions @@ -142,14 +131,11 @@ class Container: class NonContainer: ... -def get_object() -> object: ... - -x = get_object() - -if isinstance(x, Container): - if not isinstance(x, NonContainer): - reveal_type(x) # revealed: Container & ~NonContainer +def _(x: object): + if isinstance(x, Container): + if not isinstance(x, NonContainer): + reveal_type(x) # revealed: Container & ~NonContainer - # No error here! - reveal_type(2 in x) # revealed: bool + # No error here! + reveal_type(2 in x) # revealed: bool ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md index 04cfc6771ed21..80015b6a257dc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md @@ -3,18 +3,17 @@ ## String literals ```py -def str_instance() -> str: ... +def _(x: str): + reveal_type("abc" == "abc") # revealed: Literal[True] + reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True] + reveal_type("abc" in "ab cd") # revealed: Literal[False] + reveal_type("" not in "hello") # revealed: Literal[False] + reveal_type("--" is "--") # revealed: bool + reveal_type("A" is "B") # revealed: Literal[False] + reveal_type("--" is not "--") # revealed: bool + reveal_type("A" is not "B") # revealed: Literal[True] + reveal_type(x < "...") # revealed: bool -reveal_type("abc" == "abc") # revealed: Literal[True] -reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True] -reveal_type("abc" in "ab cd") # revealed: Literal[False] -reveal_type("" not in "hello") # revealed: Literal[False] -reveal_type("--" is "--") # revealed: bool -reveal_type("A" is "B") # revealed: Literal[False] -reveal_type("--" is not "--") # revealed: bool -reveal_type("A" is not "B") # revealed: Literal[True] -reveal_type(str_instance() < "...") # revealed: bool - -# ensure we're not comparing the interned salsa symbols, which compare by order of declaration. -reveal_type("ab" < "ab_cd") # revealed: Literal[True] + # ensure we're not comparing the interned salsa symbols, which compare by order of declaration. + reveal_type("ab" < "ab_cd") # revealed: Literal[True] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md index f21ea7257e6e7..8fe7f29541a9a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md @@ -58,28 +58,23 @@ reveal_type(c >= d) # revealed: Literal[True] #### Results with Ambiguity ```py -def bool_instance() -> bool: - return True - -def int_instance() -> int: - return 42 - -a = (bool_instance(),) -b = (int_instance(),) - -reveal_type(a == a) # revealed: bool -reveal_type(a != a) # revealed: bool -reveal_type(a < a) # revealed: bool -reveal_type(a <= a) # revealed: bool -reveal_type(a > a) # revealed: bool -reveal_type(a >= a) # revealed: bool - -reveal_type(a == b) # revealed: bool -reveal_type(a != b) # revealed: bool -reveal_type(a < b) # revealed: bool -reveal_type(a <= b) # revealed: bool -reveal_type(a > b) # revealed: bool -reveal_type(a >= b) # revealed: bool +def _(x: bool, y: int): + a = (x,) + b = (y,) + + reveal_type(a == a) # revealed: bool + reveal_type(a != a) # revealed: bool + reveal_type(a < a) # revealed: bool + reveal_type(a <= a) # revealed: bool + reveal_type(a > a) # revealed: bool + reveal_type(a >= a) # revealed: bool + + reveal_type(a == b) # revealed: bool + reveal_type(a != b) # revealed: bool + reveal_type(a < b) # revealed: bool + reveal_type(a <= b) # revealed: bool + reveal_type(a > b) # revealed: bool + reveal_type(a >= b) # revealed: bool ``` #### Comparison Unsupported @@ -197,7 +192,7 @@ reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False] #### Special Handling of Eq and NotEq in Lexicographic Comparisons -> Example: `(int_instance(), "foo") == (int_instance(), "bar")` +> Example: `(, "foo") == (, "bar")` `Eq` and `NotEq` have unique behavior compared to other operators in lexicographic comparisons. Specifically, for `Eq`, if any non-equal pair exists within the tuples being compared, we can @@ -208,42 +203,38 @@ In contrast, with operators like `<` and `>`, the comparison must consider each sequentially, and the final outcome might remain ambiguous until all pairs are compared. ```py -def str_instance() -> str: - return "hello" - -def int_instance() -> int: - return 42 - -reveal_type("foo" == "bar") # revealed: Literal[False] -reveal_type(("foo",) == ("bar",)) # revealed: Literal[False] -reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False] -reveal_type((int_instance(), "foo") == (int_instance(), "bar")) # revealed: Literal[False] - -a = (str_instance(), int_instance(), "foo") - -reveal_type(a == a) # revealed: bool -reveal_type(a != a) # revealed: bool -reveal_type(a < a) # revealed: bool -reveal_type(a <= a) # revealed: bool -reveal_type(a > a) # revealed: bool -reveal_type(a >= a) # revealed: bool - -b = (str_instance(), int_instance(), "bar") - -reveal_type(a == b) # revealed: Literal[False] -reveal_type(a != b) # revealed: Literal[True] -reveal_type(a < b) # revealed: bool -reveal_type(a <= b) # revealed: bool -reveal_type(a > b) # revealed: bool -reveal_type(a >= b) # revealed: bool - -c = (str_instance(), int_instance(), "foo", "different_length") -reveal_type(a == c) # revealed: Literal[False] -reveal_type(a != c) # revealed: Literal[True] -reveal_type(a < c) # revealed: bool -reveal_type(a <= c) # revealed: bool -reveal_type(a > c) # revealed: bool -reveal_type(a >= c) # revealed: bool +def _(x: str, y: int): + reveal_type("foo" == "bar") # revealed: Literal[False] + reveal_type(("foo",) == ("bar",)) # revealed: Literal[False] + reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False] + reveal_type((y, "foo") == (y, "bar")) # revealed: Literal[False] + + a = (x, y, "foo") + + reveal_type(a == a) # revealed: bool + reveal_type(a != a) # revealed: bool + reveal_type(a < a) # revealed: bool + reveal_type(a <= a) # revealed: bool + reveal_type(a > a) # revealed: bool + reveal_type(a >= a) # revealed: bool + + b = (x, y, "bar") + + reveal_type(a == b) # revealed: Literal[False] + reveal_type(a != b) # revealed: Literal[True] + reveal_type(a < b) # revealed: bool + reveal_type(a <= b) # revealed: bool + reveal_type(a > b) # revealed: bool + reveal_type(a >= b) # revealed: bool + + c = (x, y, "foo", "different_length") + + reveal_type(a == c) # revealed: Literal[False] + reveal_type(a != c) # revealed: Literal[True] + reveal_type(a < c) # revealed: bool + reveal_type(a <= c) # revealed: bool + reveal_type(a > c) # revealed: bool + reveal_type(a >= c) # revealed: bool ``` #### Error Propagation @@ -252,42 +243,36 @@ Errors occurring within a tuple comparison should propagate outward. However, if comparison can clearly conclude before encountering an error, the error should not be raised. ```py -def int_instance() -> int: - return 42 - -def str_instance() -> str: - return "hello" - -class A: ... - -# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`" -A() < A() -# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`" -A() <= A() -# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`" -A() > A() -# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`" -A() >= A() - -a = (0, int_instance(), A()) - -# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`" -reveal_type(a < a) # revealed: Unknown -# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`" -reveal_type(a <= a) # revealed: Unknown -# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`" -reveal_type(a > a) # revealed: Unknown -# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`" -reveal_type(a >= a) # revealed: Unknown - -# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`, -# and should terminate immediately. -b = (99999, int_instance(), A()) - -reveal_type(a < b) # revealed: Literal[True] -reveal_type(a <= b) # revealed: Literal[True] -reveal_type(a > b) # revealed: Literal[False] -reveal_type(a >= b) # revealed: Literal[False] +def _(n: int, s: str): + class A: ... + # error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`" + A() < A() + # error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`" + A() <= A() + # error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`" + A() > A() + # error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`" + A() >= A() + + a = (0, n, A()) + + # error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`" + reveal_type(a < a) # revealed: Unknown + # error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`" + reveal_type(a <= a) # revealed: Unknown + # error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`" + reveal_type(a > a) # revealed: Unknown + # error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`" + reveal_type(a >= a) # revealed: Unknown + + # Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`, + # and should terminate immediately. + b = (99999, n, A()) + + reveal_type(a < b) # revealed: Literal[True] + reveal_type(a <= b) # revealed: Literal[True] + reveal_type(a > b) # revealed: Literal[False] + reveal_type(a >= b) # revealed: Literal[False] ``` ### Membership Test Comparisons @@ -295,22 +280,20 @@ reveal_type(a >= b) # revealed: Literal[False] "Membership Test Comparisons" refers to the operators `in` and `not in`. ```py -def int_instance() -> int: - return 42 - -a = (1, 2) -b = ((3, 4), (1, 2)) -c = ((1, 2, 3), (4, 5, 6)) -d = ((int_instance(), int_instance()), (int_instance(), int_instance())) +def _(n: int): + a = (1, 2) + b = ((3, 4), (1, 2)) + c = ((1, 2, 3), (4, 5, 6)) + d = ((n, n), (n, n)) -reveal_type(a in b) # revealed: Literal[True] -reveal_type(a not in b) # revealed: Literal[False] + reveal_type(a in b) # revealed: Literal[True] + reveal_type(a not in b) # revealed: Literal[False] -reveal_type(a in c) # revealed: Literal[False] -reveal_type(a not in c) # revealed: Literal[True] + reveal_type(a in c) # revealed: Literal[False] + reveal_type(a not in c) # revealed: Literal[True] -reveal_type(a in d) # revealed: bool -reveal_type(a not in d) # revealed: bool + reveal_type(a in d) # revealed: bool + reveal_type(a not in d) # revealed: bool ``` ### Identity Comparisons diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md index b6b6c738517db..56924ecc7fad0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md @@ -5,49 +5,46 @@ Comparisons on union types need to consider all possible cases: ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + one_or_two = 1 if flag else 2 -flag = bool_instance() -one_or_two = 1 if flag else 2 + reveal_type(one_or_two <= 2) # revealed: Literal[True] + reveal_type(one_or_two <= 1) # revealed: bool + reveal_type(one_or_two <= 0) # revealed: Literal[False] -reveal_type(one_or_two <= 2) # revealed: Literal[True] -reveal_type(one_or_two <= 1) # revealed: bool -reveal_type(one_or_two <= 0) # revealed: Literal[False] + reveal_type(2 >= one_or_two) # revealed: Literal[True] + reveal_type(1 >= one_or_two) # revealed: bool + reveal_type(0 >= one_or_two) # revealed: Literal[False] -reveal_type(2 >= one_or_two) # revealed: Literal[True] -reveal_type(1 >= one_or_two) # revealed: bool -reveal_type(0 >= one_or_two) # revealed: Literal[False] + reveal_type(one_or_two < 1) # revealed: Literal[False] + reveal_type(one_or_two < 2) # revealed: bool + reveal_type(one_or_two < 3) # revealed: Literal[True] -reveal_type(one_or_two < 1) # revealed: Literal[False] -reveal_type(one_or_two < 2) # revealed: bool -reveal_type(one_or_two < 3) # revealed: Literal[True] + reveal_type(one_or_two > 0) # revealed: Literal[True] + reveal_type(one_or_two > 1) # revealed: bool + reveal_type(one_or_two > 2) # revealed: Literal[False] -reveal_type(one_or_two > 0) # revealed: Literal[True] -reveal_type(one_or_two > 1) # revealed: bool -reveal_type(one_or_two > 2) # revealed: Literal[False] + reveal_type(one_or_two == 3) # revealed: Literal[False] + reveal_type(one_or_two == 1) # revealed: bool -reveal_type(one_or_two == 3) # revealed: Literal[False] -reveal_type(one_or_two == 1) # revealed: bool + reveal_type(one_or_two != 3) # revealed: Literal[True] + reveal_type(one_or_two != 1) # revealed: bool -reveal_type(one_or_two != 3) # revealed: Literal[True] -reveal_type(one_or_two != 1) # revealed: bool + a_or_ab = "a" if flag else "ab" -a_or_ab = "a" if flag else "ab" + reveal_type(a_or_ab in "ab") # revealed: Literal[True] + reveal_type("a" in a_or_ab) # revealed: Literal[True] -reveal_type(a_or_ab in "ab") # revealed: Literal[True] -reveal_type("a" in a_or_ab) # revealed: Literal[True] + reveal_type("c" not in a_or_ab) # revealed: Literal[True] + reveal_type("a" not in a_or_ab) # revealed: Literal[False] -reveal_type("c" not in a_or_ab) # revealed: Literal[True] -reveal_type("a" not in a_or_ab) # revealed: Literal[False] + reveal_type("b" in a_or_ab) # revealed: bool + reveal_type("b" not in a_or_ab) # revealed: bool -reveal_type("b" in a_or_ab) # revealed: bool -reveal_type("b" not in a_or_ab) # revealed: bool + one_or_none = 1 if flag else None -one_or_none = 1 if flag else None - -reveal_type(one_or_none is None) # revealed: bool -reveal_type(one_or_none is not None) # revealed: bool + reveal_type(one_or_none is None) # revealed: bool + reveal_type(one_or_none is not None) # revealed: bool ``` ## Union on both sides of the comparison @@ -56,18 +53,15 @@ With unions on both sides, we need to consider the full cross product of options resulting (union) type: ```py -def bool_instance() -> bool: - return True - -flag_s, flag_l = bool_instance(), bool_instance() -small = 1 if flag_s else 2 -large = 2 if flag_l else 3 +def _(flag_s: bool, flag_l: bool): + small = 1 if flag_s else 2 + large = 2 if flag_l else 3 -reveal_type(small <= large) # revealed: Literal[True] -reveal_type(small >= large) # revealed: bool + reveal_type(small <= large) # revealed: Literal[True] + reveal_type(small >= large) # revealed: bool -reveal_type(small < large) # revealed: bool -reveal_type(small > large) # revealed: Literal[False] + reveal_type(small < large) # revealed: bool + reveal_type(small > large) # revealed: Literal[False] ``` ## Unsupported operations @@ -77,12 +71,9 @@ back to `bool` for the result type instead of trying to infer something more pre (supported) variants: ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = [1, 2] if flag else 1 +def _(flag: bool): + x = [1, 2] if flag else 1 -result = 1 in x # error: "Operator `in` is not supported" -reveal_type(result) # revealed: bool + result = 1 in x # error: "Operator `in` is not supported" + reveal_type(result) # revealed: bool ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md index 472cac5073193..5dc769ac48020 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md @@ -1,42 +1,38 @@ # Comparison: Unsupported operators ```py -def bool_instance() -> bool: - return True - -class A: ... - -a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`" -reveal_type(a) # revealed: bool - -b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`" -reveal_type(b) # revealed: bool - -# TODO: should error, once operand type check is implemented -# ("Operator `<` is not supported for types `object` and `int`") -c = object() < 5 -# TODO: should be Unknown, once operand type check is implemented -reveal_type(c) # revealed: bool - -# TODO: should error, once operand type check is implemented -# ("Operator `<` is not supported for types `int` and `object`") -d = 5 < object() -# TODO: should be Unknown, once operand type check is implemented -reveal_type(d) # revealed: bool - -flag = bool_instance() -int_literal_or_str_literal = 1 if flag else "foo" -# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`" -e = 42 in int_literal_or_str_literal -reveal_type(e) # revealed: bool - -# TODO: should error, need to check if __lt__ signature is valid for right operand -# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]` -f = (1, 2) < (1, "hello") -# TODO: should be Unknown, once operand type check is implemented -reveal_type(f) # revealed: bool - -# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`" -g = (bool_instance(), A()) < (bool_instance(), A()) -reveal_type(g) # revealed: Unknown +def _(flag: bool, flag1: bool, flag2: bool): + class A: ... + a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`" + reveal_type(a) # revealed: bool + + b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`" + reveal_type(b) # revealed: bool + + # TODO: should error, once operand type check is implemented + # ("Operator `<` is not supported for types `object` and `int`") + c = object() < 5 + # TODO: should be Unknown, once operand type check is implemented + reveal_type(c) # revealed: bool + + # TODO: should error, once operand type check is implemented + # ("Operator `<` is not supported for types `int` and `object`") + d = 5 < object() + # TODO: should be Unknown, once operand type check is implemented + reveal_type(d) # revealed: bool + + int_literal_or_str_literal = 1 if flag else "foo" + # error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`" + e = 42 in int_literal_or_str_literal + reveal_type(e) # revealed: bool + + # TODO: should error, need to check if __lt__ signature is valid for right operand + # error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]` + f = (1, 2) < (1, "hello") + # TODO: should be Unknown, once operand type check is implemented + reveal_type(f) # revealed: bool + + # error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`" + g = (flag1, A()) < (flag2, A()) + reveal_type(g) # revealed: Unknown ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md index f162a3a39d704..d9ef65b8f325b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md @@ -3,47 +3,35 @@ ## Simple if-expression ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = 1 if flag else 2 -reveal_type(x) # revealed: Literal[1, 2] +def _(flag: bool): + x = 1 if flag else 2 + reveal_type(x) # revealed: Literal[1, 2] ``` ## If-expression with walrus operator ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -y = 0 -z = 0 -x = (y := 1) if flag else (z := 2) -reveal_type(x) # revealed: Literal[1, 2] -reveal_type(y) # revealed: Literal[0, 1] -reveal_type(z) # revealed: Literal[0, 2] +def _(flag: bool): + y = 0 + z = 0 + x = (y := 1) if flag else (z := 2) + reveal_type(x) # revealed: Literal[1, 2] + reveal_type(y) # revealed: Literal[0, 1] + reveal_type(z) # revealed: Literal[0, 2] ``` ## Nested if-expression ```py -def bool_instance() -> bool: - return True - -flag, flag2 = bool_instance(), bool_instance() -x = 1 if flag else 2 if flag2 else 3 -reveal_type(x) # revealed: Literal[1, 2, 3] +def _(flag: bool, flag2: bool): + x = 1 if flag else 2 if flag2 else 3 + reveal_type(x) # revealed: Literal[1, 2, 3] ``` ## None ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = 1 if flag else None -reveal_type(x) # revealed: Literal[1] | None +def _(flag: bool): + x = 1 if flag else None + reveal_type(x) # revealed: Literal[1] | None ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md index 1e3ec4ea50e53..b436a739a1141 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md @@ -3,128 +3,115 @@ ## Simple if ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -y = 1 -y = 2 +def _(flag: bool): + y = 1 + y = 2 -if flag: - y = 3 + if flag: + y = 3 -reveal_type(y) # revealed: Literal[2, 3] + reveal_type(y) # revealed: Literal[2, 3] ``` ## Simple if-elif-else ```py -def bool_instance() -> bool: - return True - -flag, flag2 = bool_instance(), bool_instance() -y = 1 -y = 2 -if flag: - y = 3 -elif flag2: - y = 4 -else: - r = y - y = 5 - s = y -x = y - -reveal_type(x) # revealed: Literal[3, 4, 5] - -# revealed: Literal[2] -# error: [possibly-unresolved-reference] -reveal_type(r) - -# revealed: Literal[5] -# error: [possibly-unresolved-reference] -reveal_type(s) +def _(flag: bool, flag2: bool): + y = 1 + y = 2 + + if flag: + y = 3 + elif flag2: + y = 4 + else: + r = y + y = 5 + s = y + x = y + + reveal_type(x) # revealed: Literal[3, 4, 5] + + # revealed: Literal[2] + # error: [possibly-unresolved-reference] + reveal_type(r) + + # revealed: Literal[5] + # error: [possibly-unresolved-reference] + reveal_type(s) ``` ## Single symbol across if-elif-else ```py -def bool_instance() -> bool: - return True - -flag, flag2 = bool_instance(), bool_instance() +def _(flag: bool, flag2: bool): + if flag: + y = 1 + elif flag2: + y = 2 + else: + y = 3 -if flag: - y = 1 -elif flag2: - y = 2 -else: - y = 3 -reveal_type(y) # revealed: Literal[1, 2, 3] + reveal_type(y) # revealed: Literal[1, 2, 3] ``` ## if-elif-else without else assignment ```py -def bool_instance() -> bool: - return True +def _(flag: bool, flag2: bool): + y = 0 -flag, flag2 = bool_instance(), bool_instance() -y = 0 -if flag: - y = 1 -elif flag2: - y = 2 -else: - pass -reveal_type(y) # revealed: Literal[0, 1, 2] + if flag: + y = 1 + elif flag2: + y = 2 + else: + pass + + reveal_type(y) # revealed: Literal[0, 1, 2] ``` ## if-elif-else with intervening assignment ```py -def bool_instance() -> bool: - return True +def _(flag: bool, flag2: bool): + y = 0 -flag, flag2 = bool_instance(), bool_instance() -y = 0 -if flag: - y = 1 - z = 3 -elif flag2: - y = 2 -else: - pass -reveal_type(y) # revealed: Literal[0, 1, 2] + if flag: + y = 1 + z = 3 + elif flag2: + y = 2 + else: + pass + + reveal_type(y) # revealed: Literal[0, 1, 2] ``` ## Nested if statement ```py -def bool_instance() -> bool: - return True +def _(flag: bool, flag2: bool): + y = 0 -flag, flag2 = bool_instance(), bool_instance() -y = 0 -if flag: - if flag2: - y = 1 -reveal_type(y) # revealed: Literal[0, 1] + if flag: + if flag2: + y = 1 + + reveal_type(y) # revealed: Literal[0, 1] ``` ## if-elif without else ```py -def bool_instance() -> bool: - return True - -flag, flag2 = bool_instance(), bool_instance() -y = 1 -y = 2 -if flag: - y = 3 -elif flag2: - y = 4 - -reveal_type(y) # revealed: Literal[2, 3, 4] +def _(flag: bool, flag2: bool): + y = 1 + y = 2 + + if flag: + y = 3 + elif flag2: + y = 4 + + reveal_type(y) # revealed: Literal[2, 3, 4] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md index a173354b4cf7c..81dd93e738ade 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md @@ -31,6 +31,7 @@ reveal_type(y) ```py y = 1 y = 2 + match 0: case 1: y = 3 diff --git a/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md b/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md index 19fbfa20a5459..f6cb268312292 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md +++ b/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md @@ -10,42 +10,35 @@ x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred t ## Incompatible declarations ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -if flag: - x: str -else: - x: int -x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int" +def _(flag: bool): + if flag: + x: str + else: + x: int + + x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int" ``` ## Partial declarations ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + if flag: + x: int -flag = bool_instance() -if flag: - x: int -x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int" + x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int" ``` ## Incompatible declarations with bad assignment ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -if flag: - x: str -else: - x: int - -# error: [conflicting-declarations] -# error: [invalid-assignment] -x = b"foo" +def _(flag: bool): + if flag: + x: str + else: + x: int + + # error: [conflicting-declarations] + # error: [invalid-assignment] + x = b"foo" ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/invalid_syntax.md b/crates/red_knot_python_semantic/resources/mdtest/exception/invalid_syntax.md index fcf1780f1d710..19bb7c43772f1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/invalid_syntax.md +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/invalid_syntax.md @@ -9,5 +9,4 @@ try: print except as e: # error: [invalid-syntax] reveal_type(e) # revealed: Unknown - ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/attribute.md b/crates/red_knot_python_semantic/resources/mdtest/expression/attribute.md index 06e3cb9e504ab..66f71e8dad398 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/attribute.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/attribute.md @@ -3,26 +3,25 @@ ## Boundness ```py -def flag() -> bool: ... +def _(flag: bool): + class A: + always_bound = 1 -class A: - always_bound = 1 + if flag: + union = 1 + else: + union = "abc" - if flag(): - union = 1 - else: - union = "abc" + if flag: + possibly_unbound = "abc" - if flag(): - possibly_unbound = "abc" + reveal_type(A.always_bound) # revealed: Literal[1] -reveal_type(A.always_bound) # revealed: Literal[1] + reveal_type(A.union) # revealed: Literal[1] | Literal["abc"] -reveal_type(A.union) # revealed: Literal[1] | Literal["abc"] + # error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound" + reveal_type(A.possibly_unbound) # revealed: Literal["abc"] -# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound" -reveal_type(A.possibly_unbound) # revealed: Literal["abc"] - -# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`" -reveal_type(A.non_existent) # revealed: Unknown + # error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`" + reveal_type(A.non_existent) # revealed: Unknown ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md index a84017ba709ee..7ce689164248a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md @@ -3,54 +3,45 @@ ## OR ```py -def foo() -> str: - pass - -reveal_type(True or False) # revealed: Literal[True] -reveal_type("x" or "y" or "z") # revealed: Literal["x"] -reveal_type("" or "y" or "z") # revealed: Literal["y"] -reveal_type(False or "z") # revealed: Literal["z"] -reveal_type(False or True) # revealed: Literal[True] -reveal_type(False or False) # revealed: Literal[False] -reveal_type(foo() or False) # revealed: str | Literal[False] -reveal_type(foo() or True) # revealed: str | Literal[True] +def _(foo: str): + reveal_type(True or False) # revealed: Literal[True] + reveal_type("x" or "y" or "z") # revealed: Literal["x"] + reveal_type("" or "y" or "z") # revealed: Literal["y"] + reveal_type(False or "z") # revealed: Literal["z"] + reveal_type(False or True) # revealed: Literal[True] + reveal_type(False or False) # revealed: Literal[False] + reveal_type(foo or False) # revealed: str | Literal[False] + reveal_type(foo or True) # revealed: str | Literal[True] ``` ## AND ```py -def foo() -> str: - pass - -reveal_type(True and False) # revealed: Literal[False] -reveal_type(False and True) # revealed: Literal[False] -reveal_type(foo() and False) # revealed: str | Literal[False] -reveal_type(foo() and True) # revealed: str | Literal[True] -reveal_type("x" and "y" and "z") # revealed: Literal["z"] -reveal_type("x" and "y" and "") # revealed: Literal[""] -reveal_type("" and "y") # revealed: Literal[""] +def _(foo: str): + reveal_type(True and False) # revealed: Literal[False] + reveal_type(False and True) # revealed: Literal[False] + reveal_type(foo and False) # revealed: str | Literal[False] + reveal_type(foo and True) # revealed: str | Literal[True] + reveal_type("x" and "y" and "z") # revealed: Literal["z"] + reveal_type("x" and "y" and "") # revealed: Literal[""] + reveal_type("" and "y") # revealed: Literal[""] ``` ## Simple function calls to bool ```py -def returns_bool() -> bool: - return True - -if returns_bool(): - x = True -else: - x = False +def _(flag: bool): + if flag: + x = True + else: + x = False -reveal_type(x) # revealed: bool + reveal_type(x) # revealed: bool ``` ## Complex ```py -def foo() -> str: - pass - reveal_type("x" and "y" or "z") # revealed: Literal["y"] reveal_type("x" or "y" and "z") # revealed: Literal["x"] reveal_type("" and "y" or "z") # revealed: Literal["z"] diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/if.md b/crates/red_knot_python_semantic/resources/mdtest/expression/if.md index d3f9eef48f124..79faa45426855 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/if.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/if.md @@ -3,10 +3,8 @@ ## Union ```py -def bool_instance() -> bool: - return True - -reveal_type(1 if bool_instance() else 2) # revealed: Literal[1, 2] +def _(flag: bool): + reveal_type(1 if flag else 2) # revealed: Literal[1, 2] ``` ## Statically known branches @@ -30,14 +28,12 @@ reveal_type(1 if 0 else 2) # revealed: Literal[2] The test inside an if expression should not affect code outside of the expression. ```py -def bool_instance() -> bool: - return True - -x: Literal[42, "hello"] = 42 if bool_instance() else "hello" +def _(flag: bool): + x: Literal[42, "hello"] = 42 if flag else "hello" -reveal_type(x) # revealed: Literal[42] | Literal["hello"] + reveal_type(x) # revealed: Literal[42] | Literal["hello"] -_ = ... if isinstance(x, str) else ... + _ = ... if isinstance(x, str) else ... -reveal_type(x) # revealed: Literal[42] | Literal["hello"] + reveal_type(x) # revealed: Literal[42] | Literal["hello"] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/len.md b/crates/red_knot_python_semantic/resources/mdtest/expression/len.md index 04f6efff5fc32..3418adc77be47 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/len.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/len.md @@ -211,8 +211,7 @@ reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1] ### No `__len__` ```py -class NoDunderLen: - pass +class NoDunderLen: ... # TODO: Emit a diagnostic reveal_type(len(NoDunderLen())) # revealed: int diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md b/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md index 23056cbf80599..79686f8e74676 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md +++ b/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md @@ -3,11 +3,10 @@ ## Maybe unbound ```py path=maybe_unbound.py -def bool_instance() -> bool: +def coinflip() -> bool: return True -flag = bool_instance() -if flag: +if coinflip(): y = 3 x = y # error: [possibly-unresolved-reference] @@ -31,13 +30,12 @@ reveal_type(y) # revealed: Literal[3] ## Maybe unbound annotated ```py path=maybe_unbound_annotated.py -def bool_instance() -> bool: +def coinflip() -> bool: return True -flag = bool_instance() - -if flag: +if coinflip(): y: int = 3 + x = y # error: [possibly-unresolved-reference] # revealed: Literal[3] @@ -63,10 +61,10 @@ reveal_type(y) # revealed: int Importing a possibly undeclared name still gives us its declared type: ```py path=maybe_undeclared.py -def bool_instance() -> bool: +def coinflip() -> bool: return True -if bool_instance(): +if coinflip(): x: int ``` @@ -83,14 +81,12 @@ def f(): ... ``` ```py path=b.py -def bool_instance() -> bool: +def coinflip() -> bool: return True -flag = bool_instance() -if flag: +if coinflip(): from c import f else: - def f(): ... ``` @@ -111,11 +107,10 @@ x: int ``` ```py path=b.py -def bool_instance() -> bool: +def coinflip() -> bool: return True -flag = bool_instance() -if flag: +if coinflip(): from c import x else: x = 1 diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md b/crates/red_knot_python_semantic/resources/mdtest/loops/for.md index 09a9bc81873d0..58675475abe72 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/for.md @@ -106,23 +106,19 @@ reveal_type(x) ## With non-callable iterator ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -class NotIterable: - if flag: - __iter__ = 1 - else: - __iter__ = None - -for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable" - pass - -# revealed: Unknown -# error: [possibly-unresolved-reference] -reveal_type(x) +def _(flag: bool): + class NotIterable: + if flag: + __iter__ = 1 + else: + __iter__ = None + + for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable" + pass + + # revealed: Unknown + # error: [possibly-unresolved-reference] + reveal_type(x) ``` ## Invalid iterable @@ -160,13 +156,9 @@ class Test2: def __iter__(self) -> TestIter: return TestIter() -def bool_instance() -> bool: - return True - -flag = bool_instance() - -for x in Test() if flag else Test2(): - reveal_type(x) # revealed: int +def _(flag: bool): + for x in Test() if flag else Test2(): + reveal_type(x) # revealed: int ``` ## Union type as iterator @@ -215,13 +207,9 @@ class Test2: def __iter__(self) -> TestIter3 | TestIter4: return TestIter3() -def bool_instance() -> bool: - return True - -flag = bool_instance() - -for x in Test() if flag else Test2(): - reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview +def _(flag: bool): + for x in Test() if flag else Test2(): + reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview ``` ## Union type as iterable where one union element has no `__iter__` method @@ -235,12 +223,10 @@ class Test: def __iter__(self) -> TestIter: return TestIter() -def coinflip() -> bool: - return True - -# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound" -for x in Test() if coinflip() else 42: - reveal_type(x) # revealed: int +def _(flag: bool): + # error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound" + for x in Test() if flag else 42: + reveal_type(x) # revealed: int ``` ## Union type as iterable where one union element has invalid `__iter__` method @@ -258,12 +244,10 @@ class Test2: def __iter__(self) -> int: return 42 -def coinflip() -> bool: - return True - -# error: "Object of type `Test | Test2` is not iterable" -for x in Test() if coinflip() else Test2(): - reveal_type(x) # revealed: Unknown +def _(flag: bool): + # error: "Object of type `Test | Test2` is not iterable" + for x in Test() if flag else Test2(): + reveal_type(x) # revealed: Unknown ``` ## Union type as iterator where one union element has no `__next__` method diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md b/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md index a03c2cf83fa5c..28c44df393d88 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md @@ -3,54 +3,45 @@ ## Basic While Loop ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = 1 -while flag: - x = 2 +def _(flag: bool): + x = 1 + while flag: + x = 2 -reveal_type(x) # revealed: Literal[1, 2] + reveal_type(x) # revealed: Literal[1, 2] ``` ## While with else (no break) ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = 1 -while flag: - x = 2 -else: - reveal_type(x) # revealed: Literal[1, 2] - x = 3 +def _(flag: bool): + x = 1 + while flag: + x = 2 + else: + reveal_type(x) # revealed: Literal[1, 2] + x = 3 -reveal_type(x) # revealed: Literal[3] + reveal_type(x) # revealed: Literal[3] ``` ## While with Else (may break) ```py -def bool_instance() -> bool: - return True - -flag, flag2 = bool_instance(), bool_instance() -x = 1 -y = 0 -while flag: - x = 2 - if flag2: - y = 4 - break -else: - y = x - x = 3 +def _(flag: bool, flag2: bool): + x = 1 + y = 0 + while flag: + x = 2 + if flag2: + y = 4 + break + else: + y = x + x = 3 -reveal_type(x) # revealed: Literal[2, 3] -reveal_type(y) # revealed: Literal[1, 2, 4] + reveal_type(x) # revealed: Literal[2, 3] + reveal_type(y) # revealed: Literal[1, 2, 4] ``` ## Nested while loops diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md index d7ae47b4fdd07..9bf3007e91fe0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md @@ -1,32 +1,32 @@ ## Narrowing for `bool(..)` checks ```py -def flag() -> bool: ... +def _(flag: bool): -x = 1 if flag() else None + x = 1 if flag else None -# valid invocation, positive -reveal_type(x) # revealed: Literal[1] | None -if bool(x is not None): - reveal_type(x) # revealed: Literal[1] + # valid invocation, positive + reveal_type(x) # revealed: Literal[1] | None + if bool(x is not None): + reveal_type(x) # revealed: Literal[1] -# valid invocation, negative -reveal_type(x) # revealed: Literal[1] | None -if not bool(x is not None): - reveal_type(x) # revealed: None + # valid invocation, negative + reveal_type(x) # revealed: Literal[1] | None + if not bool(x is not None): + reveal_type(x) # revealed: None -# no args/narrowing -reveal_type(x) # revealed: Literal[1] | None -if not bool(): + # no args/narrowing reveal_type(x) # revealed: Literal[1] | None + if not bool(): + reveal_type(x) # revealed: Literal[1] | None -# invalid invocation, too many positional args -reveal_type(x) # revealed: Literal[1] | None -if bool(x is not None, 5): # TODO diagnostic + # invalid invocation, too many positional args reveal_type(x) # revealed: Literal[1] | None + if bool(x is not None, 5): # TODO diagnostic + reveal_type(x) # revealed: Literal[1] | None -# invalid invocation, too many kwargs -reveal_type(x) # revealed: Literal[1] | None -if bool(x is not None, y=5): # TODO diagnostic + # invalid invocation, too many kwargs reveal_type(x) # revealed: Literal[1] | None + if bool(x is not None, y=5): # TODO diagnostic + reveal_type(x) # revealed: Literal[1] | None ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/boolean.md index 26b63f0840bd2..48553faf0c795 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/boolean.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/boolean.md @@ -9,85 +9,67 @@ Similarly, in `and` expressions, the right-hand side is evaluated only if the le ## Narrowing in `or` ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + class A: ... + x: A | None = A() if flag else None -class A: ... - -x: A | None = A() if bool_instance() else None - -isinstance(x, A) or reveal_type(x) # revealed: None -x is None or reveal_type(x) # revealed: A -reveal_type(x) # revealed: A | None + isinstance(x, A) or reveal_type(x) # revealed: None + x is None or reveal_type(x) # revealed: A + reveal_type(x) # revealed: A | None ``` ## Narrowing in `and` ```py -def bool_instance() -> bool: - return True - -class A: ... +def _(flag: bool): + class A: ... + x: A | None = A() if flag else None -x: A | None = A() if bool_instance() else None - -isinstance(x, A) and reveal_type(x) # revealed: A -x is None and reveal_type(x) # revealed: None -reveal_type(x) # revealed: A | None + isinstance(x, A) and reveal_type(x) # revealed: A + x is None and reveal_type(x) # revealed: None + reveal_type(x) # revealed: A | None ``` ## Multiple `and` arms ```py -def bool_instance() -> bool: - return True - -class A: ... - -x: A | None = A() if bool_instance() else None +def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool): + class A: ... + x: A | None = A() if flag1 else None -bool_instance() and isinstance(x, A) and reveal_type(x) # revealed: A -isinstance(x, A) and bool_instance() and reveal_type(x) # revealed: A -reveal_type(x) and isinstance(x, A) and bool_instance() # revealed: A | None + flag2 and isinstance(x, A) and reveal_type(x) # revealed: A + isinstance(x, A) and flag2 and reveal_type(x) # revealed: A + reveal_type(x) and isinstance(x, A) and flag3 # revealed: A | None ``` ## Multiple `or` arms ```py -def bool_instance() -> bool: - return True +def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool): + class A: ... + x: A | None = A() if flag1 else None -class A: ... - -x: A | None = A() if bool_instance() else None - -bool_instance() or isinstance(x, A) or reveal_type(x) # revealed: None -isinstance(x, A) or bool_instance() or reveal_type(x) # revealed: None -reveal_type(x) or isinstance(x, A) or bool_instance() # revealed: A | None + flag2 or isinstance(x, A) or reveal_type(x) # revealed: None + isinstance(x, A) or flag3 or reveal_type(x) # revealed: None + reveal_type(x) or isinstance(x, A) or flag4 # revealed: A | None ``` ## Multiple predicates ```py -def bool_instance() -> bool: - return True - -class A: ... +def _(flag1: bool, flag2: bool): + class A: ... + x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1 -x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1 - -x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1] + x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1] ``` ## Mix of `and` and `or` ```py -def bool_instance() -> bool: - return True - -class A: ... - -x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1 +def _(flag1: bool, flag2: bool): + class A: ... + x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1 -isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1] + isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md index 854e09ff0a50b..c0e1af2f3dd9a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md @@ -6,15 +6,11 @@ class A: ... class B: ... -def instance() -> A | B: - return A() - -x = instance() - -if isinstance(x, A) and isinstance(x, B): - reveal_type(x) # revealed: A & B -else: - reveal_type(x) # revealed: B & ~A | A & ~B +def _(x: A | B): + if isinstance(x, A) and isinstance(x, B): + reveal_type(x) # revealed: A & B + else: + reveal_type(x) # revealed: B & ~A | A & ~B ``` ## Arms might not add narrowing constraints @@ -23,25 +19,18 @@ else: class A: ... class B: ... -def bool_instance() -> bool: - return True +def _(flag: bool, x: A | B): + if isinstance(x, A) and flag: + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: A | B -def instance() -> A | B: - return A() + if flag and isinstance(x, A): + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: A | B -x = instance() - -if isinstance(x, A) and bool_instance(): - reveal_type(x) # revealed: A -else: reveal_type(x) # revealed: A | B - -if bool_instance() and isinstance(x, A): - reveal_type(x) # revealed: A -else: - reveal_type(x) # revealed: A | B - -reveal_type(x) # revealed: A | B ``` ## Statically known arms @@ -50,39 +39,35 @@ reveal_type(x) # revealed: A | B class A: ... class B: ... -def instance() -> A | B: - return A() - -x = instance() +def _(x: A | B): + if isinstance(x, A) and True: + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: B & ~A + + if True and isinstance(x, A): + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: B & ~A + + if False and isinstance(x, A): + # TODO: should emit an `unreachable code` diagnostic + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: A | B + + if False or isinstance(x, A): + reveal_type(x) # revealed: A + else: + reveal_type(x) # revealed: B & ~A + + if True or isinstance(x, A): + reveal_type(x) # revealed: A | B + else: + # TODO: should emit an `unreachable code` diagnostic + reveal_type(x) # revealed: B & ~A -if isinstance(x, A) and True: - reveal_type(x) # revealed: A -else: - reveal_type(x) # revealed: B & ~A - -if True and isinstance(x, A): - reveal_type(x) # revealed: A -else: - reveal_type(x) # revealed: B & ~A - -if False and isinstance(x, A): - # TODO: should emit an `unreachable code` diagnostic - reveal_type(x) # revealed: A -else: reveal_type(x) # revealed: A | B - -if False or isinstance(x, A): - reveal_type(x) # revealed: A -else: - reveal_type(x) # revealed: B & ~A - -if True or isinstance(x, A): - reveal_type(x) # revealed: A | B -else: - # TODO: should emit an `unreachable code` diagnostic - reveal_type(x) # revealed: B & ~A - -reveal_type(x) # revealed: A | B ``` ## The type of multiple symbols can be narrowed down @@ -91,22 +76,17 @@ reveal_type(x) # revealed: A | B class A: ... class B: ... -def instance() -> A | B: - return A() - -x = instance() -y = instance() +def _(x: A | B, y: A | B): + if isinstance(x, A) and isinstance(y, B): + reveal_type(x) # revealed: A + reveal_type(y) # revealed: B + else: + # No narrowing: Only-one or both checks might have failed + reveal_type(x) # revealed: A | B + reveal_type(y) # revealed: A | B -if isinstance(x, A) and isinstance(y, B): - reveal_type(x) # revealed: A - reveal_type(y) # revealed: B -else: - # No narrowing: Only-one or both checks might have failed reveal_type(x) # revealed: A | B reveal_type(y) # revealed: A | B - -reveal_type(x) # revealed: A | B -reveal_type(y) # revealed: A | B ``` ## Narrowing in `or` conditional @@ -116,15 +96,11 @@ class A: ... class B: ... class C: ... -def instance() -> A | B | C: - return A() - -x = instance() - -if isinstance(x, A) or isinstance(x, B): - reveal_type(x) # revealed: A | B -else: - reveal_type(x) # revealed: C & ~A & ~B +def _(x: A | B | C): + if isinstance(x, A) or isinstance(x, B): + reveal_type(x) # revealed: A | B + else: + reveal_type(x) # revealed: C & ~A & ~B ``` ## In `or`, all arms should add constraint in order to narrow @@ -134,18 +110,11 @@ class A: ... class B: ... class C: ... -def instance() -> A | B | C: - return A() - -def bool_instance() -> bool: - return True - -x = instance() - -if isinstance(x, A) or isinstance(x, B) or bool_instance(): - reveal_type(x) # revealed: A | B | C -else: - reveal_type(x) # revealed: C & ~A & ~B +def _(flag: bool, x: A | B | C): + if isinstance(x, A) or isinstance(x, B) or flag: + reveal_type(x) # revealed: A | B | C + else: + reveal_type(x) # revealed: C & ~A & ~B ``` ## in `or`, all arms should narrow the same set of symbols @@ -155,28 +124,23 @@ class A: ... class B: ... class C: ... -def instance() -> A | B | C: - return A() - -x = instance() -y = instance() - -if isinstance(x, A) or isinstance(y, A): - # The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here. - reveal_type(x) # revealed: A | B | C - # The same for `y` - reveal_type(y) # revealed: A | B | C -else: - reveal_type(x) # revealed: B & ~A | C & ~A - reveal_type(y) # revealed: B & ~A | C & ~A - -if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)): - # Here, types of `x` and `y` can be narrowd since all `or` arms constraint them. - reveal_type(x) # revealed: A | B - reveal_type(y) # revealed: A | B -else: - reveal_type(x) # revealed: A | B | C - reveal_type(y) # revealed: A | B | C +def _(x: A | B | C, y: A | B | C): + if isinstance(x, A) or isinstance(y, A): + # The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here. + reveal_type(x) # revealed: A | B | C + # The same for `y` + reveal_type(y) # revealed: A | B | C + else: + reveal_type(x) # revealed: B & ~A | C & ~A + reveal_type(y) # revealed: B & ~A | C & ~A + + if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)): + # Here, types of `x` and `y` can be narrowd since all `or` arms constraint them. + reveal_type(x) # revealed: A | B + reveal_type(y) # revealed: A | B + else: + reveal_type(x) # revealed: A | B | C + reveal_type(y) # revealed: A | B | C ``` ## mixing `and` and `not` @@ -186,16 +150,12 @@ class A: ... class B: ... class C: ... -def instance() -> A | B | C: - return A() - -x = instance() - -if isinstance(x, B) and not isinstance(x, C): - reveal_type(x) # revealed: B & ~C -else: - # ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C - reveal_type(x) # revealed: A & ~B | C +def _(x: A | B | C): + if isinstance(x, B) and not isinstance(x, C): + reveal_type(x) # revealed: B & ~C + else: + # ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C + reveal_type(x) # revealed: A & ~B | C ``` ## mixing `or` and `not` @@ -205,15 +165,11 @@ class A: ... class B: ... class C: ... -def instance() -> A | B | C: - return A() - -x = instance() - -if isinstance(x, B) or not isinstance(x, C): - reveal_type(x) # revealed: B | A & ~C -else: - reveal_type(x) # revealed: C & ~B +def _(x: A | B | C): + if isinstance(x, B) or not isinstance(x, C): + reveal_type(x) # revealed: B | A & ~C + else: + reveal_type(x) # revealed: C & ~B ``` ## `or` with nested `and` @@ -223,16 +179,12 @@ class A: ... class B: ... class C: ... -def instance() -> A | B | C: - return A() - -x = instance() - -if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)): - reveal_type(x) # revealed: A | B & ~C -else: - # ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B) - reveal_type(x) # revealed: C & ~A +def _(x: A | B | C): + if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)): + reveal_type(x) # revealed: A | B & ~C + else: + # ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B) + reveal_type(x) # revealed: C & ~A ``` ## `and` with nested `or` @@ -242,41 +194,32 @@ class A: ... class B: ... class C: ... -def instance() -> A | B | C: - return A() - -x = instance() - -if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)): - # A & (B | ~C) -> (A & B) | (A & ~C) - reveal_type(x) # revealed: A & B | A & ~C -else: - # ~((A & B) | (A & ~C)) -> - # ~(A & B) & ~(A & ~C) -> - # (~A | ~B) & (~A | C) -> - # [(~A | ~B) & ~A] | [(~A | ~B) & C] -> - # ~A | (~A & C) | (~B & C) -> - # ~A | (C & ~B) -> - # ~A | (C & ~B) The positive side of ~A is A | B | C -> - reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B +def _(x: A | B | C): + if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)): + # A & (B | ~C) -> (A & B) | (A & ~C) + reveal_type(x) # revealed: A & B | A & ~C + else: + # ~((A & B) | (A & ~C)) -> + # ~(A & B) & ~(A & ~C) -> + # (~A | ~B) & (~A | C) -> + # [(~A | ~B) & ~A] | [(~A | ~B) & C] -> + # ~A | (~A & C) | (~B & C) -> + # ~A | (C & ~B) -> + # ~A | (C & ~B) The positive side of ~A is A | B | C -> + reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B ``` ## Boolean expression internal narrowing ```py -def optional_string() -> str | None: - return None - -x = optional_string() -y = optional_string() - -if x is None and y is not x: - reveal_type(y) # revealed: str +def _(x: str | None, y: str | None): + if x is None and y is not x: + reveal_type(y) # revealed: str -# Neither of the conditions alone is sufficient for narrowing y's type: -if x is None: - reveal_type(y) # revealed: str | None + # Neither of the conditions alone is sufficient for narrowing y's type: + if x is None: + reveal_type(y) # revealed: str | None -if y is not x: - reveal_type(y) # revealed: str | None + if y is not x: + reveal_type(y) # revealed: str | None ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md index 7ac16f4b8d597..76eae880ef39e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/elif_else.md @@ -3,55 +3,47 @@ ## Positive contributions become negative in elif-else blocks ```py -def int_instance() -> int: - return 42 - -x = int_instance() - -if x == 1: - # cannot narrow; could be a subclass of `int` - reveal_type(x) # revealed: int -elif x == 2: - reveal_type(x) # revealed: int & ~Literal[1] -elif x != 3: - reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3] +def _(x: int): + if x == 1: + # cannot narrow; could be a subclass of `int` + reveal_type(x) # revealed: int + elif x == 2: + reveal_type(x) # revealed: int & ~Literal[1] + elif x != 3: + reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3] ``` ## Positive contributions become negative in elif-else blocks, with simplification ```py -def bool_instance() -> bool: - return True - -x = 1 if bool_instance() else 2 if bool_instance() else 3 - -if x == 1: - # TODO should be Literal[1] - reveal_type(x) # revealed: Literal[1, 2, 3] -elif x == 2: - # TODO should be Literal[2] - reveal_type(x) # revealed: Literal[2, 3] -else: - reveal_type(x) # revealed: Literal[3] +def _(flag1: bool, flag2: bool): + x = 1 if flag1 else 2 if flag2 else 3 + + if x == 1: + # TODO should be Literal[1] + reveal_type(x) # revealed: Literal[1, 2, 3] + elif x == 2: + # TODO should be Literal[2] + reveal_type(x) # revealed: Literal[2, 3] + else: + reveal_type(x) # revealed: Literal[3] ``` ## Multiple negative contributions using elif, with simplification ```py -def bool_instance() -> bool: - return True - -x = 1 if bool_instance() else 2 if bool_instance() else 3 - -if x != 1: - reveal_type(x) # revealed: Literal[2, 3] -elif x != 2: - # TODO should be `Literal[1]` - reveal_type(x) # revealed: Literal[1, 3] -elif x == 3: - # TODO should be Never - reveal_type(x) # revealed: Literal[1, 2, 3] -else: - # TODO should be Never - reveal_type(x) # revealed: Literal[1, 2] +def _(flag1: bool, flag2: bool): + x = 1 if flag1 else 2 if flag2 else 3 + + if x != 1: + reveal_type(x) # revealed: Literal[2, 3] + elif x != 2: + # TODO should be `Literal[1]` + reveal_type(x) # revealed: Literal[1, 3] + elif x == 3: + # TODO should be Never + reveal_type(x) # revealed: Literal[1, 2, 3] + else: + # TODO should be Never + reveal_type(x) # revealed: Literal[1, 2] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md index b9cd22897d18d..c0aa140b408d3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -3,77 +3,64 @@ ## `is None` ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + x = None if flag else 1 -flag = bool_instance() -x = None if flag else 1 + if x is None: + reveal_type(x) # revealed: None + else: + reveal_type(x) # revealed: Literal[1] -if x is None: - reveal_type(x) # revealed: None -else: - reveal_type(x) # revealed: Literal[1] - -reveal_type(x) # revealed: None | Literal[1] + reveal_type(x) # revealed: None | Literal[1] ``` ## `is` for other types ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -class A: ... +def _(flag: bool): + class A: ... + x = A() + y = x if flag else None -x = A() -y = x if flag else None + if y is x: + reveal_type(y) # revealed: A + else: + reveal_type(y) # revealed: A | None -if y is x: - reveal_type(y) # revealed: A -else: reveal_type(y) # revealed: A | None - -reveal_type(y) # revealed: A | None ``` ## `is` in chained comparisons ```py -def bool_instance() -> bool: - return True - -x_flag, y_flag = bool_instance(), bool_instance() -x = True if x_flag else False -y = True if y_flag else False +def _(x_flag: bool, y_flag: bool): + x = True if x_flag else False + y = True if y_flag else False -reveal_type(x) # revealed: bool -reveal_type(y) # revealed: bool - -if y is x is False: # Interpreted as `(y is x) and (x is False)` - reveal_type(x) # revealed: Literal[False] - reveal_type(y) # revealed: bool -else: - # The negation of the clause above is (y is not x) or (x is not False) - # So we can't narrow the type of x or y here, because each arm of the `or` could be true reveal_type(x) # revealed: bool reveal_type(y) # revealed: bool + + if y is x is False: # Interpreted as `(y is x) and (x is False)` + reveal_type(x) # revealed: Literal[False] + reveal_type(y) # revealed: bool + else: + # The negation of the clause above is (y is not x) or (x is not False) + # So we can't narrow the type of x or y here, because each arm of the `or` could be true + reveal_type(x) # revealed: bool + reveal_type(y) # revealed: bool ``` ## `is` in elif clause ```py -def bool_instance() -> bool: - return True - -x = None if bool_instance() else (1 if bool_instance() else True) - -reveal_type(x) # revealed: None | Literal[1] | Literal[True] -if x is None: - reveal_type(x) # revealed: None -elif x is True: - reveal_type(x) # revealed: Literal[True] -else: - reveal_type(x) # revealed: Literal[1] +def _(flag1: bool, flag2: bool): + x = None if flag1 else (1 if flag2 else True) + + reveal_type(x) # revealed: None | Literal[1] | Literal[True] + if x is None: + reveal_type(x) # revealed: None + elif x is True: + reveal_type(x) # revealed: Literal[True] + else: + reveal_type(x) # revealed: Literal[1] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md index ed89f3f7ae87d..980a66a68d2ee 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is_not.md @@ -5,34 +5,28 @@ The type guard removes `None` from the union type: ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + x = None if flag else 1 -flag = bool_instance() -x = None if flag else 1 + if x is not None: + reveal_type(x) # revealed: Literal[1] + else: + reveal_type(x) # revealed: None -if x is not None: - reveal_type(x) # revealed: Literal[1] -else: - reveal_type(x) # revealed: None - -reveal_type(x) # revealed: None | Literal[1] + reveal_type(x) # revealed: None | Literal[1] ``` ## `is not` for other singleton types ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = True if flag else False -reveal_type(x) # revealed: bool +def _(flag: bool): + x = True if flag else False + reveal_type(x) # revealed: bool -if x is not False: - reveal_type(x) # revealed: Literal[True] -else: - reveal_type(x) # revealed: Literal[False] + if x is not False: + reveal_type(x) # revealed: Literal[True] + else: + reveal_type(x) # revealed: Literal[False] ``` ## `is not` for non-singleton types @@ -53,20 +47,17 @@ else: ## `is not` for other types ```py -def bool_instance() -> bool: - return True - -class A: ... +def _(flag: bool): + class A: ... + x = A() + y = x if flag else None -x = A() -y = x if bool_instance() else None + if y is not x: + reveal_type(y) # revealed: A | None + else: + reveal_type(y) # revealed: A -if y is not x: reveal_type(y) # revealed: A | None -else: - reveal_type(y) # revealed: A - -reveal_type(y) # revealed: A | None ``` ## `is not` in chained comparisons @@ -74,23 +65,20 @@ reveal_type(y) # revealed: A | None The type guard removes `False` from the union type of the tested value only. ```py -def bool_instance() -> bool: - return True - -x_flag, y_flag = bool_instance(), bool_instance() -x = True if x_flag else False -y = True if y_flag else False - -reveal_type(x) # revealed: bool -reveal_type(y) # revealed: bool - -if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)` - reveal_type(x) # revealed: Literal[True] - reveal_type(y) # revealed: bool -else: - # The negation of the clause above is (y is x) or (x is False) - # So we can't narrow the type of x or y here, because each arm of the `or` could be true +def _(x_flag: bool, y_flag: bool): + x = True if x_flag else False + y = True if y_flag else False reveal_type(x) # revealed: bool reveal_type(y) # revealed: bool + + if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)` + reveal_type(x) # revealed: Literal[True] + reveal_type(y) # revealed: bool + else: + # The negation of the clause above is (y is x) or (x is False) + # So we can't narrow the type of x or y here, because each arm of the `or` could be true + + reveal_type(x) # revealed: bool + reveal_type(y) # revealed: bool ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md index cc0f79165e742..fa69fe8863bc0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/nested.md @@ -3,54 +3,45 @@ ## Multiple negative contributions ```py -def int_instance() -> int: - return 42 - -x = int_instance() - -if x != 1: - if x != 2: - if x != 3: - reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3] +def _(x: int): + if x != 1: + if x != 2: + if x != 3: + reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3] ``` ## Multiple negative contributions with simplification ```py -def bool_instance() -> bool: - return True - -flag1, flag2 = bool_instance(), bool_instance() -x = 1 if flag1 else 2 if flag2 else 3 +def _(flag1: bool, flag2: bool): + x = 1 if flag1 else 2 if flag2 else 3 -if x != 1: - reveal_type(x) # revealed: Literal[2, 3] - if x != 2: - reveal_type(x) # revealed: Literal[3] + if x != 1: + reveal_type(x) # revealed: Literal[2, 3] + if x != 2: + reveal_type(x) # revealed: Literal[3] ``` ## elif-else blocks ```py -def bool_instance() -> bool: - return True - -x = 1 if bool_instance() else 2 if bool_instance() else 3 +def _(flag1: bool, flag2: bool): + x = 1 if flag1 else 2 if flag2 else 3 -if x != 1: - reveal_type(x) # revealed: Literal[2, 3] - if x == 2: - # TODO should be `Literal[2]` + if x != 1: reveal_type(x) # revealed: Literal[2, 3] - elif x == 3: - reveal_type(x) # revealed: Literal[3] + if x == 2: + # TODO should be `Literal[2]` + reveal_type(x) # revealed: Literal[2, 3] + elif x == 3: + reveal_type(x) # revealed: Literal[3] + else: + reveal_type(x) # revealed: Never + + elif x != 2: + # TODO should be Literal[1] + reveal_type(x) # revealed: Literal[1, 3] else: - reveal_type(x) # revealed: Never - -elif x != 2: - # TODO should be Literal[1] - reveal_type(x) # revealed: Literal[1, 3] -else: - # TODO should be Never - reveal_type(x) # revealed: Literal[1, 2, 3] + # TODO should be Never + reveal_type(x) # revealed: Literal[1, 2, 3] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not.md index 0cf341496c337..c0a305d1eaf22 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not.md @@ -5,29 +5,25 @@ The `not` operator negates a constraint. ## `not is None` ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + x = None if flag else 1 -x = None if bool_instance() else 1 + if not x is None: + reveal_type(x) # revealed: Literal[1] + else: + reveal_type(x) # revealed: None -if not x is None: - reveal_type(x) # revealed: Literal[1] -else: - reveal_type(x) # revealed: None - -reveal_type(x) # revealed: None | Literal[1] + reveal_type(x) # revealed: None | Literal[1] ``` ## `not isinstance` ```py -def bool_instance() -> bool: - return True - -x = 1 if bool_instance() else "a" +def _(flag: bool): + x = 1 if flag else "a" -if not isinstance(x, (int)): - reveal_type(x) # revealed: Literal["a"] -else: - reveal_type(x) # revealed: Literal[1] + if not isinstance(x, (int)): + reveal_type(x) # revealed: Literal["a"] + else: + reveal_type(x) # revealed: Literal[1] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md index 3ad8ebcb68e10..abe0c4d5aaea1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/not_eq.md @@ -3,82 +3,66 @@ ## `x != None` ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = None if flag else 1 - -if x != None: - reveal_type(x) # revealed: Literal[1] -else: - # TODO should be None - reveal_type(x) # revealed: None | Literal[1] +def _(flag: bool): + x = None if flag else 1 + + if x != None: + reveal_type(x) # revealed: Literal[1] + else: + # TODO should be None + reveal_type(x) # revealed: None | Literal[1] ``` ## `!=` for other singleton types ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = True if flag else False - -if x != False: - reveal_type(x) # revealed: Literal[True] -else: - # TODO should be Literal[False] - reveal_type(x) # revealed: bool +def _(flag: bool): + x = True if flag else False + + if x != False: + reveal_type(x) # revealed: Literal[True] + else: + # TODO should be Literal[False] + reveal_type(x) # revealed: bool ``` ## `x != y` where `y` is of literal type ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + x = 1 if flag else 2 -flag = bool_instance() -x = 1 if flag else 2 - -if x != 1: - reveal_type(x) # revealed: Literal[2] + if x != 1: + reveal_type(x) # revealed: Literal[2] ``` ## `x != y` where `y` is a single-valued type ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -class A: ... -class B: ... - -C = A if flag else B - -if C != A: - reveal_type(C) # revealed: Literal[B] -else: - # TODO should be Literal[A] - reveal_type(C) # revealed: Literal[A, B] +def _(flag: bool): + class A: ... + class B: ... + C = A if flag else B + + if C != A: + reveal_type(C) # revealed: Literal[B] + else: + # TODO should be Literal[A] + reveal_type(C) # revealed: Literal[A, B] ``` ## `x != y` where `y` has multiple single-valued options ```py -def bool_instance() -> bool: - return True - -x = 1 if bool_instance() else 2 -y = 2 if bool_instance() else 3 - -if x != y: - reveal_type(x) # revealed: Literal[1, 2] -else: - # TODO should be Literal[2] - reveal_type(x) # revealed: Literal[1, 2] +def _(flag1: bool, flag2: bool): + x = 1 if flag1 else 2 + y = 2 if flag2 else 3 + + if x != y: + reveal_type(x) # revealed: Literal[1, 2] + else: + # TODO should be Literal[2] + reveal_type(x) # revealed: Literal[1, 2] ``` ## `!=` for non-single-valued types @@ -86,34 +70,22 @@ else: Only single-valued types should narrow the type: ```py -def bool_instance() -> bool: - return True - -def int_instance() -> int: - return 42 +def _(flag: bool, a: int, y: int): + x = a if flag else None -flag = bool_instance() -x = int_instance() if flag else None -y = int_instance() - -if x != y: - reveal_type(x) # revealed: int | None + if x != y: + reveal_type(x) # revealed: int | None ``` ## Mix of single-valued and non-single-valued types ```py -def int_instance() -> int: - return 42 - -def bool_instance() -> bool: - return True - -x = 1 if bool_instance() else 2 -y = 2 if bool_instance() else int_instance() - -if x != y: - reveal_type(x) # revealed: Literal[1, 2] -else: - reveal_type(x) # revealed: Literal[1, 2] +def _(flag1: bool, flag2: bool, a: int): + x = 1 if flag1 else 2 + y = 2 if flag2 else a + + if x != y: + reveal_type(x) # revealed: Literal[1, 2] + else: + reveal_type(x) # revealed: Literal[1, 2] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md index 849d5f802ce13..f2a24ae9656a7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md @@ -5,23 +5,19 @@ Narrowing for `isinstance(object, classinfo)` expressions. ## `classinfo` is a single type ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + x = 1 if flag else "a" -flag = bool_instance() - -x = 1 if flag else "a" - -if isinstance(x, int): - reveal_type(x) # revealed: Literal[1] - -if isinstance(x, str): - reveal_type(x) # revealed: Literal["a"] if isinstance(x, int): - reveal_type(x) # revealed: Never + reveal_type(x) # revealed: Literal[1] -if isinstance(x, (int, object)): - reveal_type(x) # revealed: Literal[1] | Literal["a"] + if isinstance(x, str): + reveal_type(x) # revealed: Literal["a"] + if isinstance(x, int): + reveal_type(x) # revealed: Never + + if isinstance(x, (int, object)): + reveal_type(x) # revealed: Literal[1] | Literal["a"] ``` ## `classinfo` is a tuple of types @@ -30,56 +26,48 @@ Note: `isinstance(x, (int, str))` should not be confused with `isinstance(x, tup The former is equivalent to `isinstance(x, int | str)`: ```py -def bool_instance() -> bool: - return True - -flag, flag1, flag2 = bool_instance(), bool_instance(), bool_instance() - -x = 1 if flag else "a" +def _(flag: bool, flag1: bool, flag2: bool): + x = 1 if flag else "a" -if isinstance(x, (int, str)): - reveal_type(x) # revealed: Literal[1] | Literal["a"] -else: - reveal_type(x) # revealed: Never + if isinstance(x, (int, str)): + reveal_type(x) # revealed: Literal[1] | Literal["a"] + else: + reveal_type(x) # revealed: Never -if isinstance(x, (int, bytes)): - reveal_type(x) # revealed: Literal[1] + if isinstance(x, (int, bytes)): + reveal_type(x) # revealed: Literal[1] -if isinstance(x, (bytes, str)): - reveal_type(x) # revealed: Literal["a"] + if isinstance(x, (bytes, str)): + reveal_type(x) # revealed: Literal["a"] -# No narrowing should occur if a larger type is also -# one of the possibilities: -if isinstance(x, (int, object)): - reveal_type(x) # revealed: Literal[1] | Literal["a"] -else: - reveal_type(x) # revealed: Never + # No narrowing should occur if a larger type is also + # one of the possibilities: + if isinstance(x, (int, object)): + reveal_type(x) # revealed: Literal[1] | Literal["a"] + else: + reveal_type(x) # revealed: Never -y = 1 if flag1 else "a" if flag2 else b"b" -if isinstance(y, (int, str)): - reveal_type(y) # revealed: Literal[1] | Literal["a"] + y = 1 if flag1 else "a" if flag2 else b"b" + if isinstance(y, (int, str)): + reveal_type(y) # revealed: Literal[1] | Literal["a"] -if isinstance(y, (int, bytes)): - reveal_type(y) # revealed: Literal[1] | Literal[b"b"] + if isinstance(y, (int, bytes)): + reveal_type(y) # revealed: Literal[1] | Literal[b"b"] -if isinstance(y, (str, bytes)): - reveal_type(y) # revealed: Literal["a"] | Literal[b"b"] + if isinstance(y, (str, bytes)): + reveal_type(y) # revealed: Literal["a"] | Literal[b"b"] ``` ## `classinfo` is a nested tuple of types ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() +def _(flag: bool): + x = 1 if flag else "a" -x = 1 if flag else "a" - -if isinstance(x, (bool, (bytes, int))): - reveal_type(x) # revealed: Literal[1] -else: - reveal_type(x) # revealed: Literal["a"] + if isinstance(x, (bool, (bytes, int))): + reveal_type(x) # revealed: Literal[1] + else: + reveal_type(x) # revealed: Literal["a"] ``` ## Class types @@ -89,9 +77,7 @@ class A: ... class B: ... class C: ... -def get_object() -> object: ... - -x = get_object() +x = object() if isinstance(x, A): reveal_type(x) # revealed: A @@ -112,50 +98,40 @@ else: ## No narrowing for instances of `builtins.type` ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + t = type("t", (), {}) -flag = bool_instance() + # This isn't testing what we want it to test if we infer anything more precise here: + reveal_type(t) # revealed: type -t = type("t", (), {}) + x = 1 if flag else "foo" -# This isn't testing what we want it to test if we infer anything more precise here: -reveal_type(t) # revealed: type -x = 1 if flag else "foo" - -if isinstance(x, t): - reveal_type(x) # revealed: Literal[1] | Literal["foo"] + if isinstance(x, t): + reveal_type(x) # revealed: Literal[1] | Literal["foo"] ``` ## Do not use custom `isinstance` for narrowing ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -def isinstance(x, t): - return True +def _(flag: bool): + def isinstance(x, t): + return True + x = 1 if flag else "a" -x = 1 if flag else "a" -if isinstance(x, int): - reveal_type(x) # revealed: Literal[1] | Literal["a"] + if isinstance(x, int): + reveal_type(x) # revealed: Literal[1] | Literal["a"] ``` ## Do support narrowing if `isinstance` is aliased ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() +def _(flag: bool): + isinstance_alias = isinstance -isinstance_alias = isinstance + x = 1 if flag else "a" -x = 1 if flag else "a" -if isinstance_alias(x, int): - reveal_type(x) # revealed: Literal[1] + if isinstance_alias(x, int): + reveal_type(x) # revealed: Literal[1] ``` ## Do support narrowing if `isinstance` is imported @@ -163,46 +139,38 @@ if isinstance_alias(x, int): ```py from builtins import isinstance as imported_isinstance -def bool_instance() -> bool: - return True +def _(flag: bool): + x = 1 if flag else "a" -flag = bool_instance() -x = 1 if flag else "a" -if imported_isinstance(x, int): - reveal_type(x) # revealed: Literal[1] + if imported_isinstance(x, int): + reveal_type(x) # revealed: Literal[1] ``` ## Do not narrow if second argument is not a type ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = 1 if flag else "a" - -# TODO: this should cause us to emit a diagnostic during -# type checking -if isinstance(x, "a"): - reveal_type(x) # revealed: Literal[1] | Literal["a"] - -# TODO: this should cause us to emit a diagnostic during -# type checking -if isinstance(x, "int"): - reveal_type(x) # revealed: Literal[1] | Literal["a"] +def _(flag: bool): + x = 1 if flag else "a" + + # TODO: this should cause us to emit a diagnostic during + # type checking + if isinstance(x, "a"): + reveal_type(x) # revealed: Literal[1] | Literal["a"] + + # TODO: this should cause us to emit a diagnostic during + # type checking + if isinstance(x, "int"): + reveal_type(x) # revealed: Literal[1] | Literal["a"] ``` ## Do not narrow if there are keyword arguments ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() -x = 1 if flag else "a" +def _(flag: bool): + x = 1 if flag else "a" -# TODO: this should cause us to emit a diagnostic -# (`isinstance` has no `foo` parameter) -if isinstance(x, int, foo="bar"): - reveal_type(x) # revealed: Literal[1] | Literal["a"] + # TODO: this should cause us to emit a diagnostic + # (`isinstance` has no `foo` parameter) + if isinstance(x, int, foo="bar"): + reveal_type(x) # revealed: Literal[1] | Literal["a"] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md index 44184f634c73b..7da1ad3e36126 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md @@ -7,45 +7,43 @@ Narrowing for `issubclass(class, classinfo)` expressions. ### Basic example ```py -def flag() -> bool: ... - -t = int if flag() else str +def _(flag: bool): + t = int if flag else str -if issubclass(t, bytes): - reveal_type(t) # revealed: Never + if issubclass(t, bytes): + reveal_type(t) # revealed: Never -if issubclass(t, object): - reveal_type(t) # revealed: Literal[int, str] + if issubclass(t, object): + reveal_type(t) # revealed: Literal[int, str] -if issubclass(t, int): - reveal_type(t) # revealed: Literal[int] -else: - reveal_type(t) # revealed: Literal[str] - -if issubclass(t, str): - reveal_type(t) # revealed: Literal[str] if issubclass(t, int): - reveal_type(t) # revealed: Never + reveal_type(t) # revealed: Literal[int] + else: + reveal_type(t) # revealed: Literal[str] + + if issubclass(t, str): + reveal_type(t) # revealed: Literal[str] + if issubclass(t, int): + reveal_type(t) # revealed: Never ``` ### Proper narrowing in `elif` and `else` branches ```py -def flag() -> bool: ... - -t = int if flag() else str if flag() else bytes +def _(flag1: bool, flag2: bool): + t = int if flag1 else str if flag2 else bytes -if issubclass(t, int): - reveal_type(t) # revealed: Literal[int] -else: - reveal_type(t) # revealed: Literal[str, bytes] + if issubclass(t, int): + reveal_type(t) # revealed: Literal[int] + else: + reveal_type(t) # revealed: Literal[str, bytes] -if issubclass(t, int): - reveal_type(t) # revealed: Literal[int] -elif issubclass(t, str): - reveal_type(t) # revealed: Literal[str] -else: - reveal_type(t) # revealed: Literal[bytes] + if issubclass(t, int): + reveal_type(t) # revealed: Literal[int] + elif issubclass(t, str): + reveal_type(t) # revealed: Literal[str] + else: + reveal_type(t) # revealed: Literal[bytes] ``` ### Multiple derived classes @@ -56,29 +54,28 @@ class Derived1(Base): ... class Derived2(Base): ... class Unrelated: ... -def flag() -> bool: ... +def _(flag1: bool, flag2: bool, flag3: bool): + t1 = Derived1 if flag1 else Derived2 -t1 = Derived1 if flag() else Derived2 + if issubclass(t1, Base): + reveal_type(t1) # revealed: Literal[Derived1, Derived2] -if issubclass(t1, Base): - reveal_type(t1) # revealed: Literal[Derived1, Derived2] + if issubclass(t1, Derived1): + reveal_type(t1) # revealed: Literal[Derived1] + else: + reveal_type(t1) # revealed: Literal[Derived2] -if issubclass(t1, Derived1): - reveal_type(t1) # revealed: Literal[Derived1] -else: - reveal_type(t1) # revealed: Literal[Derived2] + t2 = Derived1 if flag2 else Base -t2 = Derived1 if flag() else Base + if issubclass(t2, Base): + reveal_type(t2) # revealed: Literal[Derived1, Base] -if issubclass(t2, Base): - reveal_type(t2) # revealed: Literal[Derived1, Base] + t3 = Derived1 if flag3 else Unrelated -t3 = Derived1 if flag() else Unrelated - -if issubclass(t3, Base): - reveal_type(t3) # revealed: Literal[Derived1] -else: - reveal_type(t3) # revealed: Literal[Unrelated] + if issubclass(t3, Base): + reveal_type(t3) # revealed: Literal[Derived1] + else: + reveal_type(t3) # revealed: Literal[Unrelated] ``` ### Narrowing for non-literals @@ -87,16 +84,13 @@ else: class A: ... class B: ... -def get_class() -> type[object]: ... - -t = get_class() - -if issubclass(t, A): - reveal_type(t) # revealed: type[A] - if issubclass(t, B): - reveal_type(t) # revealed: type[A] & type[B] -else: - reveal_type(t) # revealed: type[object] & ~type[A] +def _(t: type[object]): + if issubclass(t, A): + reveal_type(t) # revealed: type[A] + if issubclass(t, B): + reveal_type(t) # revealed: type[A] & type[B] + else: + reveal_type(t) # revealed: type[object] & ~type[A] ``` ### Handling of `None` @@ -107,16 +101,15 @@ else: # error: [possibly-unbound-import] "Member `NoneType` of module `types` is possibly unbound" from types import NoneType -def flag() -> bool: ... - -t = int if flag() else NoneType +def _(flag: bool): + t = int if flag else NoneType -if issubclass(t, NoneType): - reveal_type(t) # revealed: Literal[NoneType] + if issubclass(t, NoneType): + reveal_type(t) # revealed: Literal[NoneType] -if issubclass(t, type(None)): - # TODO: this should be just `Literal[NoneType]` - reveal_type(t) # revealed: Literal[int, NoneType] + if issubclass(t, type(None)): + # TODO: this should be just `Literal[NoneType]` + reveal_type(t) # revealed: Literal[int, NoneType] ``` ## `classinfo` contains multiple types @@ -126,14 +119,13 @@ if issubclass(t, type(None)): ```py class Unrelated: ... -def flag() -> bool: ... - -t = int if flag() else str if flag() else bytes +def _(flag1: bool, flag2: bool): + t = int if flag1 else str if flag2 else bytes -if issubclass(t, (int, (Unrelated, (bytes,)))): - reveal_type(t) # revealed: Literal[int, bytes] -else: - reveal_type(t) # revealed: Literal[str] + if issubclass(t, (int, (Unrelated, (bytes,)))): + reveal_type(t) # revealed: Literal[int, bytes] + else: + reveal_type(t) # revealed: Literal[str] ``` ## Special cases @@ -148,9 +140,7 @@ to `issubclass`: ```py class A: ... -def get_object() -> object: ... - -t = get_object() +t = object() # TODO: we should emit a diagnostic here if issubclass(t, A): diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md index 2144f91f9eff7..da5678f3109a7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md @@ -3,19 +3,16 @@ ## Single `match` pattern ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + x = None if flag else 1 -flag = bool_instance() + reveal_type(x) # revealed: None | Literal[1] -x = None if flag else 1 -reveal_type(x) # revealed: None | Literal[1] + y = 0 -y = 0 + match x: + case None: + y = x -match x: - case None: - y = x - -reveal_type(y) # revealed: Literal[0] | None + reveal_type(y) # revealed: Literal[0] | None ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/post_if_statement.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/post_if_statement.md index 267e820d595da..fbbe5794d8355 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/post_if_statement.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/post_if_statement.md @@ -3,62 +3,49 @@ ## After if-else statements, narrowing has no effect if the variable is not mutated in any branch ```py -def optional_int() -> int | None: ... +def _(x: int | None): + if x is None: + pass + else: + pass -x = optional_int() - -if x is None: - pass -else: - pass - -reveal_type(x) # revealed: int | None + reveal_type(x) # revealed: int | None ``` ## Narrowing can have a persistent effect if the variable is mutated in one branch ```py -def optional_int() -> int | None: ... - -x = optional_int() - -if x is None: - x = 10 -else: - pass +def _(x: int | None): + if x is None: + x = 10 + else: + pass -reveal_type(x) # revealed: int + reveal_type(x) # revealed: int ``` ## An if statement without an explicit `else` branch is equivalent to one with a no-op `else` branch ```py -def optional_int() -> int | None: ... +def _(x: int | None, y: int | None): + if x is None: + x = 0 -x = optional_int() -y = optional_int() + if y is None: + pass -if x is None: - x = 0 - -if y is None: - pass - -reveal_type(x) # revealed: int -reveal_type(y) # revealed: int | None + reveal_type(x) # revealed: int + reveal_type(y) # revealed: int | None ``` ## An if-elif without an explicit else branch is equivalent to one with an empty else branch ```py -def optional_int() -> int | None: ... - -x = optional_int() - -if x is None: - x = 0 -elif x > 50: - x = 50 +def _(x: int | None): + if x is None: + x = 0 + elif x > 50: + x = 50 -reveal_type(x) # revealed: int + reveal_type(x) # revealed: int ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md index 01d2646198f80..386a508aab3ad 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md @@ -6,18 +6,14 @@ class A: ... class B: ... -def get_a_or_b() -> A | B: - return A() - -x = get_a_or_b() - -if type(x) is A: - reveal_type(x) # revealed: A -else: - # It would be wrong to infer `B` here. The type - # of `x` could be a subclass of `A`, so we need - # to infer the full union type: - reveal_type(x) # revealed: A | B +def _(x: A | B): + if type(x) is A: + reveal_type(x) # revealed: A + else: + # It would be wrong to infer `B` here. The type + # of `x` could be a subclass of `A`, so we need + # to infer the full union type: + reveal_type(x) # revealed: A | B ``` ## `type(x) is not C` @@ -26,16 +22,12 @@ else: class A: ... class B: ... -def get_a_or_b() -> A | B: - return A() - -x = get_a_or_b() - -if type(x) is not A: - # Same reasoning as above: no narrowing should occur here. - reveal_type(x) # revealed: A | B -else: - reveal_type(x) # revealed: A +def _(x: A | B): + if type(x) is not A: + # Same reasoning as above: no narrowing should occur here. + reveal_type(x) # revealed: A | B + else: + reveal_type(x) # revealed: A ``` ## `type(x) == C`, `type(x) != C` @@ -54,16 +46,12 @@ class IsEqualToEverything(type): class A(metaclass=IsEqualToEverything): ... class B(metaclass=IsEqualToEverything): ... -def get_a_or_b() -> A | B: - return B() - -x = get_a_or_b() - -if type(x) == A: - reveal_type(x) # revealed: A | B +def _(x: A | B): + if type(x) == A: + reveal_type(x) # revealed: A | B -if type(x) != A: - reveal_type(x) # revealed: A | B + if type(x) != A: + reveal_type(x) # revealed: A | B ``` ## No narrowing for custom `type` callable @@ -75,15 +63,11 @@ class B: ... def type(x): return int -def get_a_or_b() -> A | B: - return A() - -x = get_a_or_b() - -if type(x) is A: - reveal_type(x) # revealed: A | B -else: - reveal_type(x) # revealed: A | B +def _(x: A | B): + if type(x) is A: + reveal_type(x) # revealed: A | B + else: + reveal_type(x) # revealed: A | B ``` ## No narrowing for multiple arguments @@ -91,15 +75,11 @@ else: No narrowing should occur if `type` is used to dynamically create a class: ```py -def get_str_or_int() -> str | int: - return "test" - -x = get_str_or_int() - -if type(x, (), {}) is str: - reveal_type(x) # revealed: str | int -else: - reveal_type(x) # revealed: str | int +def _(x: str | int): + if type(x, (), {}) is str: + reveal_type(x) # revealed: str | int + else: + reveal_type(x) # revealed: str | int ``` ## No narrowing for keyword arguments @@ -107,14 +87,10 @@ else: `type` can't be used with a keyword argument: ```py -def get_str_or_int() -> str | int: - return "test" - -x = get_str_or_int() - -# TODO: we could issue a diagnostic here -if type(object=x) is str: - reveal_type(x) # revealed: str | int +def _(x: str | int): + # TODO: we could issue a diagnostic here + if type(object=x) is str: + reveal_type(x) # revealed: str | int ``` ## Narrowing if `type` is aliased @@ -125,13 +101,9 @@ class B: ... alias_for_type = type -def get_a_or_b() -> A | B: - return A() - -x = get_a_or_b() - -if alias_for_type(x) is A: - reveal_type(x) # revealed: A +def _(x: A | B): + if alias_for_type(x) is A: + reveal_type(x) # revealed: A ``` ## Limitations @@ -140,13 +112,9 @@ if alias_for_type(x) is A: class Base: ... class Derived(Base): ... -def get_base() -> Base: - return Base() - -x = get_base() - -if type(x) is Base: - # Ideally, this could be narrower, but there is now way to - # express a constraint like `Base & ~ProperSubtypeOf[Base]`. - reveal_type(x) # revealed: Base +def _(x: Base): + if type(x) is Base: + # Ideally, this could be narrower, but there is now way to + # express a constraint like `Base & ~ProperSubtypeOf[Base]`. + reveal_type(x) # revealed: Base ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md index fc6ee0de10aa6..0a50dee2b22ca 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/unbound.md @@ -5,10 +5,10 @@ Name lookups within a class scope fall back to globals, but lookups of class attributes don't. ```py -def bool_instance() -> bool: +def coinflip() -> bool: return True -flag = bool_instance() +flag = coinflip() x = 1 class C: @@ -24,14 +24,14 @@ reveal_type(C.y) # revealed: Literal[1] ## Possibly unbound in class and global scope ```py -def bool_instance() -> bool: +def coinflip() -> bool: return True -if bool_instance(): +if coinflip(): x = "abc" class C: - if bool_instance(): + if coinflip(): x = 1 # error: [possibly-unresolved-reference] diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md b/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md index 44a93668c9801..8c4a9259ed5b1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md +++ b/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md @@ -3,14 +3,11 @@ ## Shadow after incompatible declarations is OK ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + if flag: + x: str + else: + x: int -flag = bool_instance() - -if flag: - x: str -else: - x: int -x: bytes = b"foo" + x: bytes = b"foo" ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md index 71c7775b7614c..beb52730e6a04 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md @@ -22,12 +22,10 @@ reveal_type(x) # revealed: Unknown y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5" reveal_type(y) # revealed: Unknown -def int_instance() -> int: - return 42 - -a = b"abcde"[int_instance()] -# TODO: Support overloads... Should be `bytes` -reveal_type(a) # revealed: @Todo(return type) +def _(n: int): + a = b"abcde"[n] + # TODO: Support overloads... Should be `bytes` + reveal_type(a) # revealed: @Todo(return type) ``` ## Slices @@ -43,15 +41,13 @@ b[:4:0] # error: [zero-stepsize-in-slice] b[0::0] # error: [zero-stepsize-in-slice] b[::0] # error: [zero-stepsize-in-slice] -def int_instance() -> int: ... - -byte_slice1 = b[int_instance() : int_instance()] -# TODO: Support overloads... Should be `bytes` -reveal_type(byte_slice1) # revealed: @Todo(return type) - -def bytes_instance() -> bytes: ... +def _(m: int, n: int): + byte_slice1 = b[m:n] + # TODO: Support overloads... Should be `bytes` + reveal_type(byte_slice1) # revealed: @Todo(return type) -byte_slice2 = bytes_instance()[0:5] -# TODO: Support overloads... Should be `bytes` -reveal_type(byte_slice2) # revealed: @Todo(return type) +def _(s: bytes) -> bytes: + byte_slice2 = s[0:5] + # TODO: Support overloads... Should be `bytes` + reveal_type(byte_slice2) # revealed: @Todo(return type) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md index ee26274ae027c..2903153b07f1b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md @@ -21,77 +21,66 @@ reveal_type(Identity[0]) # revealed: str ## Class getitem union ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + class UnionClassGetItem: + if flag: + def __class_getitem__(cls, item: int) -> str: + return item + else: + def __class_getitem__(cls, item: int) -> int: + return item + + reveal_type(UnionClassGetItem[0]) # revealed: str | int +``` -class UnionClassGetItem: - if bool_instance(): +## Class getitem with class union +```py +def _(flag: bool): + class A: def __class_getitem__(cls, item: int) -> str: return item - else: + class B: def __class_getitem__(cls, item: int) -> int: return item -reveal_type(UnionClassGetItem[0]) # revealed: str | int -``` - -## Class getitem with class union - -```py -def bool_instance() -> bool: - return True - -class A: - def __class_getitem__(cls, item: int) -> str: - return item - -class B: - def __class_getitem__(cls, item: int) -> int: - return item - -x = A if bool_instance() else B + x = A if flag else B -reveal_type(x) # revealed: Literal[A, B] -reveal_type(x[0]) # revealed: str | int + reveal_type(x) # revealed: Literal[A, B] + reveal_type(x[0]) # revealed: str | int ``` ## Class getitem with unbound method union ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + if flag: + class Spam: + def __class_getitem__(self, x: int) -> str: + return "foo" -if bool_instance(): - class Spam: - def __class_getitem__(self, x: int) -> str: - return "foo" - -else: - class Spam: ... - -# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound" -# revealed: str -reveal_type(Spam[42]) + else: + class Spam: ... + # error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound" + # revealed: str + reveal_type(Spam[42]) ``` ## TODO: Class getitem non-class union ```py -def bool_instance() -> bool: - return True +def _(flag: bool): + if flag: + class Eggs: + def __class_getitem__(self, x: int) -> str: + return "foo" -if bool_instance(): - class Eggs: - def __class_getitem__(self, x: int) -> str: - return "foo" - -else: - Eggs = 1 + else: + Eggs = 1 -a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method" + a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method" -# TODO: should _probably_ emit `str | Unknown` -reveal_type(a) # revealed: Unknown + # TODO: should _probably_ emit `str | Unknown` + reveal_type(a) # revealed: Unknown ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md index b9d17ffb002f1..eeb251dbcbbe7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md @@ -30,18 +30,14 @@ reveal_type(Identity()[0]) # revealed: int ## Getitem union ```py -def bool_instance() -> bool: - return True - -class Identity: - if bool_instance(): - - def __getitem__(self, index: int) -> int: - return index - else: - - def __getitem__(self, index: int) -> str: - return str(index) - -reveal_type(Identity()[0]) # revealed: int | str +def _(flag: bool): + class Identity: + if flag: + def __getitem__(self, index: int) -> int: + return index + else: + def __getitem__(self, index: int) -> str: + return str(index) + + reveal_type(Identity()[0]) # revealed: int | str ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md index 46274074596c8..f6fa36ce7c6a2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md @@ -19,11 +19,10 @@ reveal_type(a) # revealed: Unknown b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5" reveal_type(b) # revealed: Unknown -def int_instance() -> int: ... - -a = "abcde"[int_instance()] -# TODO: Support overloads... Should be `str` -reveal_type(a) # revealed: @Todo(return type) +def _(n: int): + a = "abcde"[n] + # TODO: Support overloads... Should be `str` + reveal_type(a) # revealed: @Todo(return type) ``` ## Slices @@ -74,17 +73,14 @@ s[:4:0] # error: [zero-stepsize-in-slice] s[0::0] # error: [zero-stepsize-in-slice] s[::0] # error: [zero-stepsize-in-slice] -def int_instance() -> int: ... - -substring1 = s[int_instance() : int_instance()] -# TODO: Support overloads... Should be `LiteralString` -reveal_type(substring1) # revealed: @Todo(return type) - -def str_instance() -> str: ... +def _(m: int, n: int, s2: str): + substring1 = s[m:n] + # TODO: Support overloads... Should be `LiteralString` + reveal_type(substring1) # revealed: @Todo(return type) -substring2 = str_instance()[0:5] -# TODO: Support overloads... Should be `str` -reveal_type(substring2) # revealed: @Todo(return type) + substring2 = s2[0:5] + # TODO: Support overloads... Should be `str` + reveal_type(substring2) # revealed: @Todo(return type) ``` ## Unsupported slice types diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index cb088f009fd74..5582fa8a920c4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -67,9 +67,8 @@ t[:4:0] # error: [zero-stepsize-in-slice] t[0::0] # error: [zero-stepsize-in-slice] t[::0] # error: [zero-stepsize-in-slice] -def int_instance() -> int: ... - -tuple_slice = t[int_instance() : int_instance()] -# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]` -reveal_type(tuple_slice) # revealed: @Todo(return type) +def _(m: int, n: int): + tuple_slice = t[m:n] + # TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]` + reveal_type(tuple_slice) # revealed: @Todo(return type) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md b/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md index 642e070c7934a..723c8a904973f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md @@ -5,10 +5,8 @@ ```py class A: ... -def f() -> type[A]: - return A - -reveal_type(f()) # revealed: type[A] +def _(c: type[A]): + reveal_type(c) # revealed: type[A] ``` ## Nested class literal @@ -17,10 +15,8 @@ reveal_type(f()) # revealed: type[A] class A: class B: ... -def f() -> type[A.B]: - return A.B - -reveal_type(f()) # revealed: type[B] +def f(c: type[A.B]): + reveal_type(c) # revealed: type[B] ``` ## Deeply nested class literal @@ -30,10 +26,8 @@ class A: class B: class C: ... -def f() -> type[A.B.C]: - return A.B.C - -reveal_type(f()) # revealed: type[C] +def f(c: type[A.B.C]): + reveal_type(c) # revealed: type[C] ``` ## Class literal from another module @@ -41,10 +35,8 @@ reveal_type(f()) # revealed: type[C] ```py from a import A -def f() -> type[A]: - return A - -reveal_type(f()) # revealed: type[A] +def f(c: type[A]): + reveal_type(c) # revealed: type[A] ``` ```py path=a.py @@ -56,10 +48,8 @@ class A: ... ```py import a -def f() -> type[a.B]: - return a.B - -reveal_type(f()) # revealed: type[B] +def f(c: type[a.B]): + reveal_type(c) # revealed: type[B] ``` ```py path=a.py @@ -73,12 +63,8 @@ import a.b # TODO: no diagnostic # error: [unresolved-attribute] -def f() -> type[a.b.C]: - # TODO: no diagnostic - # error: [unresolved-attribute] - return a.b.C - -reveal_type(f()) # revealed: @Todo(unsupported type[X] special form) +def f(c: type[a.b.C]): + reveal_type(c) # revealed: @Todo(unsupported type[X] special form) ``` ```py path=a/__init__.py @@ -98,11 +84,9 @@ class A: class B: class C: ... -def get_user() -> type[BasicUser | ProUser | A.B.C]: - return BasicUser - -# revealed: type[BasicUser] | type[ProUser] | type[C] -reveal_type(get_user()) +def _(u: type[BasicUser | ProUser | A.B.C]): + # revealed: type[BasicUser] | type[ProUser] | type[C] + reveal_type(u) ``` ## Old-style union of classes @@ -147,6 +131,5 @@ class A: ... class B: ... # error: [invalid-type-form] -def get_user() -> type[A, B]: - return A +_: type[A, B] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md b/crates/red_knot_python_semantic/resources/mdtest/unary/not.md index a193437328326..0b887ee036458 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unary/not.md @@ -35,29 +35,25 @@ y = 1 ## Union ```py -def bool_instance() -> bool: - return True - -flag = bool_instance() - -if flag: - p = 1 - q = 3.3 - r = "hello" - s = "world" - t = 0 -else: - p = "hello" - q = 4 - r = "" - s = 0 - t = "" - -reveal_type(not p) # revealed: Literal[False] -reveal_type(not q) # revealed: bool -reveal_type(not r) # revealed: bool -reveal_type(not s) # revealed: bool -reveal_type(not t) # revealed: Literal[True] +def _(flag: bool): + if flag: + p = 1 + q = 3.3 + r = "hello" + s = "world" + t = 0 + else: + p = "hello" + q = 4 + r = "" + s = 0 + t = "" + + reveal_type(not p) # revealed: Literal[False] + reveal_type(not q) # revealed: bool + reveal_type(not r) # revealed: bool + reveal_type(not s) # revealed: bool + reveal_type(not t) # revealed: Literal[True] ``` ## Integer literal diff --git a/crates/red_knot_python_semantic/resources/mdtest/with/sync.md b/crates/red_knot_python_semantic/resources/mdtest/with/sync.md index ab7745223b980..408a9f9c232e3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/with/sync.md +++ b/crates/red_knot_python_semantic/resources/mdtest/with/sync.md @@ -21,25 +21,23 @@ with Manager() as f: ## Union context manager ```py -def coinflip() -> bool: - return True - -class Manager1: - def __enter__(self) -> str: - return "foo" +def _(flag: bool): + class Manager1: + def __enter__(self) -> str: + return "foo" - def __exit__(self, exc_type, exc_value, traceback): ... + def __exit__(self, exc_type, exc_value, traceback): ... -class Manager2: - def __enter__(self) -> int: - return 42 + class Manager2: + def __enter__(self) -> int: + return 42 - def __exit__(self, exc_type, exc_value, traceback): ... + def __exit__(self, exc_type, exc_value, traceback): ... -context_expr = Manager1() if coinflip() else Manager2() + context_expr = Manager1() if flag else Manager2() -with context_expr as f: - reveal_type(f) # revealed: str | int + with context_expr as f: + reveal_type(f) # revealed: str | int ``` ## Context manager without an `__enter__` or `__exit__` method @@ -103,39 +101,34 @@ with Manager(): ## Context expression with possibly-unbound union variants ```py -def coinflip() -> bool: - return True - -class Manager1: - def __enter__(self) -> str: - return "foo" - - def __exit__(self, exc_type, exc_value, traceback): ... +def _(flag: bool): + class Manager1: + def __enter__(self) -> str: + return "foo" -class NotAContextManager: ... + def __exit__(self, exc_type, exc_value, traceback): ... -context_expr = Manager1() if coinflip() else NotAContextManager() + class NotAContextManager: ... + context_expr = Manager1() if flag else NotAContextManager() -# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__enter__` is possibly unbound" -# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__exit__` is possibly unbound" -with context_expr as f: - reveal_type(f) # revealed: str + # error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__enter__` is possibly unbound" + # error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__exit__` is possibly unbound" + with context_expr as f: + reveal_type(f) # revealed: str ``` ## Context expression with "sometimes" callable `__enter__` method ```py -def coinflip() -> bool: - return True - -class Manager: - if coinflip(): - def __enter__(self) -> str: - return "abcd" +def _(flag: bool): + class Manager: + if flag: + def __enter__(self) -> str: + return "abcd" - def __exit__(self, *args): ... + def __exit__(self, *args): ... -# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound" -with Manager() as f: - reveal_type(f) # revealed: str + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound" + with Manager() as f: + reveal_type(f) # revealed: str ``` From dc0d9446080d877570652088e7723be57a9e5373 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Tue, 10 Dec 2024 18:49:28 +0530 Subject: [PATCH 14/14] [`airflow`] Add fix to remove deprecated keyword arguments (`AIR302`) (#14887) ## Summary Add replacement fixes to deprecated arguments of a DAG. Ref #14582 #14626 ## Test Plan Diff was verified and snapshots were updated. --------- Co-authored-by: Dhruv Manilawala --- .../src/rules/airflow/rules/removal_in_3.rs | 30 +++- ...airflow__tests__AIR302_AIR302_args.py.snap | 104 ++++++++++++-- ...irflow__tests__AIR302_AIR302_names.py.snap | 129 ++++++++++++------ 3 files changed, 207 insertions(+), 56 deletions(-) diff --git a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs index 05c51d1cb0519..8f91a3d67815f 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/removal_in_3.rs @@ -1,4 +1,4 @@ -use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::{name::QualifiedName, Arguments, Expr, ExprAttribute, ExprCall}; use ruff_python_semantic::Modules; @@ -43,6 +43,8 @@ pub(crate) struct Airflow3Removal { } impl Violation for Airflow3Removal { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let Airflow3Removal { @@ -51,14 +53,23 @@ impl Violation for Airflow3Removal { } = self; match replacement { Replacement::None => format!("`{deprecated}` is removed in Airflow 3.0"), - Replacement::Name(name) => { - format!("`{deprecated}` is removed in Airflow 3.0; use `{name}` instead") + Replacement::Name(_) => { + format!("`{deprecated}` is removed in Airflow 3.0") } Replacement::Message(message) => { format!("`{deprecated}` is removed in Airflow 3.0; {message}") } } } + + fn fix_title(&self) -> Option { + let Airflow3Removal { replacement, .. } = self; + if let Replacement::Name(name) = replacement { + Some(format!("Use `{name}` instead")) + } else { + None + } + } } fn diagnostic_for_argument( @@ -67,7 +78,7 @@ fn diagnostic_for_argument( replacement: Option<&str>, ) -> Option { let keyword = arguments.find_keyword(deprecated)?; - Some(Diagnostic::new( + let mut diagnostic = Diagnostic::new( Airflow3Removal { deprecated: (*deprecated).to_string(), replacement: match replacement { @@ -79,7 +90,16 @@ fn diagnostic_for_argument( .arg .as_ref() .map_or_else(|| keyword.range(), Ranged::range), - )) + ); + + if let Some(replacement) = replacement { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + replacement.to_string(), + diagnostic.range, + ))); + } + + Some(diagnostic) } fn removed_argument(checker: &mut Checker, qualname: &QualifiedName, arguments: &Arguments) { diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_args.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_args.py.snap index eb9fc002b3bcd..bd90b530f0883 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_args.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_args.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/airflow/mod.rs snapshot_kind: text --- -AIR302_args.py:15:39: AIR302 `schedule_interval` is removed in Airflow 3.0; use `schedule` instead +AIR302_args.py:15:39: AIR302 [*] `schedule_interval` is removed in Airflow 3.0 | 13 | DAG(dag_id="class_schedule", schedule="@hourly") 14 | @@ -11,14 +11,36 @@ AIR302_args.py:15:39: AIR302 `schedule_interval` is removed in Airflow 3.0; use 16 | 17 | DAG(dag_id="class_timetable", timetable=NullTimetable()) | + = help: Use `schedule` instead -AIR302_args.py:17:31: AIR302 `timetable` is removed in Airflow 3.0; use `schedule` instead +ℹ Safe fix +12 12 | +13 13 | DAG(dag_id="class_schedule", schedule="@hourly") +14 14 | +15 |-DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") + 15 |+DAG(dag_id="class_schedule_interval", schedule="@hourly") +16 16 | +17 17 | DAG(dag_id="class_timetable", timetable=NullTimetable()) +18 18 | + +AIR302_args.py:17:31: AIR302 [*] `timetable` is removed in Airflow 3.0 | 15 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") 16 | 17 | DAG(dag_id="class_timetable", timetable=NullTimetable()) | ^^^^^^^^^ AIR302 | + = help: Use `schedule` instead + +ℹ Safe fix +14 14 | +15 15 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly") +16 16 | +17 |-DAG(dag_id="class_timetable", timetable=NullTimetable()) + 17 |+DAG(dag_id="class_timetable", schedule=NullTimetable()) +18 18 | +19 19 | +20 20 | def sla_callback(*arg, **kwargs): AIR302_args.py:24:34: AIR302 `sla_miss_callback` is removed in Airflow 3.0 | @@ -26,21 +48,43 @@ AIR302_args.py:24:34: AIR302 `sla_miss_callback` is removed in Airflow 3.0 | ^^^^^^^^^^^^^^^^^ AIR302 | -AIR302_args.py:32:6: AIR302 `schedule_interval` is removed in Airflow 3.0; use `schedule` instead +AIR302_args.py:32:6: AIR302 [*] `schedule_interval` is removed in Airflow 3.0 | 32 | @dag(schedule_interval="0 * * * *") | ^^^^^^^^^^^^^^^^^ AIR302 33 | def decorator_schedule_interval(): 34 | pass | + = help: Use `schedule` instead -AIR302_args.py:37:6: AIR302 `timetable` is removed in Airflow 3.0; use `schedule` instead +ℹ Safe fix +29 29 | pass +30 30 | +31 31 | +32 |-@dag(schedule_interval="0 * * * *") + 32 |+@dag(schedule="0 * * * *") +33 33 | def decorator_schedule_interval(): +34 34 | pass +35 35 | + +AIR302_args.py:37:6: AIR302 [*] `timetable` is removed in Airflow 3.0 | 37 | @dag(timetable=NullTimetable()) | ^^^^^^^^^ AIR302 38 | def decorator_timetable(): 39 | pass | + = help: Use `schedule` instead + +ℹ Safe fix +34 34 | pass +35 35 | +36 36 | +37 |-@dag(timetable=NullTimetable()) + 37 |+@dag(schedule=NullTimetable()) +38 38 | def decorator_timetable(): +39 39 | pass +40 40 | AIR302_args.py:42:6: AIR302 `sla_miss_callback` is removed in Airflow 3.0 | @@ -50,7 +94,7 @@ AIR302_args.py:42:6: AIR302 `sla_miss_callback` is removed in Airflow 3.0 44 | pass | -AIR302_args.py:50:39: AIR302 `execution_date` is removed in Airflow 3.0; use `logical_date` instead +AIR302_args.py:50:39: AIR302 [*] `execution_date` is removed in Airflow 3.0 | 48 | def decorator_deprecated_operator_args(): 49 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator( @@ -59,8 +103,19 @@ AIR302_args.py:50:39: AIR302 `execution_date` is removed in Airflow 3.0; use `lo 51 | ) 52 | trigger_dagrun_op2 = TriggerDagRunOperator( | + = help: Use `logical_date` instead -AIR302_args.py:53:39: AIR302 `execution_date` is removed in Airflow 3.0; use `logical_date` instead +ℹ Safe fix +47 47 | @dag() +48 48 | def decorator_deprecated_operator_args(): +49 49 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator( +50 |- task_id="trigger_dagrun_op1", execution_date="2024-12-04" + 50 |+ task_id="trigger_dagrun_op1", logical_date="2024-12-04" +51 51 | ) +52 52 | trigger_dagrun_op2 = TriggerDagRunOperator( +53 53 | task_id="trigger_dagrun_op2", execution_date="2024-12-04" + +AIR302_args.py:53:39: AIR302 [*] `execution_date` is removed in Airflow 3.0 | 51 | ) 52 | trigger_dagrun_op2 = TriggerDagRunOperator( @@ -68,8 +123,19 @@ AIR302_args.py:53:39: AIR302 `execution_date` is removed in Airflow 3.0; use `lo | ^^^^^^^^^^^^^^ AIR302 54 | ) | + = help: Use `logical_date` instead + +ℹ Safe fix +50 50 | task_id="trigger_dagrun_op1", execution_date="2024-12-04" +51 51 | ) +52 52 | trigger_dagrun_op2 = TriggerDagRunOperator( +53 |- task_id="trigger_dagrun_op2", execution_date="2024-12-04" + 53 |+ task_id="trigger_dagrun_op2", logical_date="2024-12-04" +54 54 | ) +55 55 | +56 56 | branch_dt_op = datetime.BranchDateTimeOperator( -AIR302_args.py:57:33: AIR302 `use_task_execution_day` is removed in Airflow 3.0; use `use_task_logical_date` instead +AIR302_args.py:57:33: AIR302 [*] `use_task_execution_day` is removed in Airflow 3.0 | 56 | branch_dt_op = datetime.BranchDateTimeOperator( 57 | task_id="branch_dt_op", use_task_execution_day=True @@ -77,8 +143,19 @@ AIR302_args.py:57:33: AIR302 `use_task_execution_day` is removed in Airflow 3.0; 58 | ) 59 | branch_dt_op2 = BranchDateTimeOperator( | + = help: Use `use_task_logical_date` instead -AIR302_args.py:60:34: AIR302 `use_task_execution_day` is removed in Airflow 3.0; use `use_task_logical_date` instead +ℹ Safe fix +54 54 | ) +55 55 | +56 56 | branch_dt_op = datetime.BranchDateTimeOperator( +57 |- task_id="branch_dt_op", use_task_execution_day=True + 57 |+ task_id="branch_dt_op", use_task_logical_date=True +58 58 | ) +59 59 | branch_dt_op2 = BranchDateTimeOperator( +60 60 | task_id="branch_dt_op2", use_task_execution_day=True + +AIR302_args.py:60:34: AIR302 [*] `use_task_execution_day` is removed in Airflow 3.0 | 58 | ) 59 | branch_dt_op2 = BranchDateTimeOperator( @@ -86,3 +163,14 @@ AIR302_args.py:60:34: AIR302 `use_task_execution_day` is removed in Airflow 3.0; | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 61 | ) | + = help: Use `use_task_logical_date` instead + +ℹ Safe fix +57 57 | task_id="branch_dt_op", use_task_execution_day=True +58 58 | ) +59 59 | branch_dt_op2 = BranchDateTimeOperator( +60 |- task_id="branch_dt_op2", use_task_execution_day=True + 60 |+ task_id="branch_dt_op2", use_task_logical_date=True +61 61 | ) +62 62 | +63 63 | dof_task_sensor = weekday.DayOfWeekSensor( diff --git a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_names.py.snap b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_names.py.snap index ffed2cf1e5f36..26633649bd4a1 100644 --- a/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_names.py.snap +++ b/crates/ruff_linter/src/rules/airflow/snapshots/ruff_linter__rules__airflow__tests__AIR302_AIR302_names.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/airflow/mod.rs snapshot_kind: text --- -AIR302_names.py:52:1: AIR302 `airflow.PY36` is removed in Airflow 3.0; use `sys.version_info` instead +AIR302_names.py:52:1: AIR302 `airflow.PY36` is removed in Airflow 3.0 | 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key 51 | @@ -11,8 +11,9 @@ AIR302_names.py:52:1: AIR302 `airflow.PY36` is removed in Airflow 3.0; use `sys. 53 | 54 | AWSAthenaHook | + = help: Use `sys.version_info` instead -AIR302_names.py:52:7: AIR302 `airflow.PY37` is removed in Airflow 3.0; use `sys.version_info` instead +AIR302_names.py:52:7: AIR302 `airflow.PY37` is removed in Airflow 3.0 | 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key 51 | @@ -21,8 +22,9 @@ AIR302_names.py:52:7: AIR302 `airflow.PY37` is removed in Airflow 3.0; use `sys. 53 | 54 | AWSAthenaHook | + = help: Use `sys.version_info` instead -AIR302_names.py:52:13: AIR302 `airflow.PY38` is removed in Airflow 3.0; use `sys.version_info` instead +AIR302_names.py:52:13: AIR302 `airflow.PY38` is removed in Airflow 3.0 | 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key 51 | @@ -31,8 +33,9 @@ AIR302_names.py:52:13: AIR302 `airflow.PY38` is removed in Airflow 3.0; use `sys 53 | 54 | AWSAthenaHook | + = help: Use `sys.version_info` instead -AIR302_names.py:52:19: AIR302 `airflow.PY39` is removed in Airflow 3.0; use `sys.version_info` instead +AIR302_names.py:52:19: AIR302 `airflow.PY39` is removed in Airflow 3.0 | 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key 51 | @@ -41,8 +44,9 @@ AIR302_names.py:52:19: AIR302 `airflow.PY39` is removed in Airflow 3.0; use `sys 53 | 54 | AWSAthenaHook | + = help: Use `sys.version_info` instead -AIR302_names.py:52:25: AIR302 `airflow.PY310` is removed in Airflow 3.0; use `sys.version_info` instead +AIR302_names.py:52:25: AIR302 `airflow.PY310` is removed in Airflow 3.0 | 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key 51 | @@ -51,8 +55,9 @@ AIR302_names.py:52:25: AIR302 `airflow.PY310` is removed in Airflow 3.0; use `sy 53 | 54 | AWSAthenaHook | + = help: Use `sys.version_info` instead -AIR302_names.py:52:32: AIR302 `airflow.PY311` is removed in Airflow 3.0; use `sys.version_info` instead +AIR302_names.py:52:32: AIR302 `airflow.PY311` is removed in Airflow 3.0 | 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key 51 | @@ -61,8 +66,9 @@ AIR302_names.py:52:32: AIR302 `airflow.PY311` is removed in Airflow 3.0; use `sy 53 | 54 | AWSAthenaHook | + = help: Use `sys.version_info` instead -AIR302_names.py:52:39: AIR302 `airflow.PY312` is removed in Airflow 3.0; use `sys.version_info` instead +AIR302_names.py:52:39: AIR302 `airflow.PY312` is removed in Airflow 3.0 | 50 | from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_for_key 51 | @@ -71,6 +77,7 @@ AIR302_names.py:52:39: AIR302 `airflow.PY312` is removed in Airflow 3.0; use `sy 53 | 54 | AWSAthenaHook | + = help: Use `sys.version_info` instead AIR302_names.py:54:1: AIR302 `airflow.contrib.aws_athena_hook.AWSAthenaHook` is removed in Airflow 3.0 | @@ -90,7 +97,7 @@ AIR302_names.py:55:1: AIR302 `airflow.triggers.external_task.TaskStateTrigger` i 57 | requires_access | -AIR302_names.py:57:1: AIR302 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0; use `airflow.api_connexion.security.requires_access_*` instead +AIR302_names.py:57:1: AIR302 `airflow.api_connexion.security.requires_access` is removed in Airflow 3.0 | 55 | TaskStateTrigger 56 | @@ -99,8 +106,9 @@ AIR302_names.py:57:1: AIR302 `airflow.api_connexion.security.requires_access` is 58 | 59 | AllowListValidator | + = help: Use `airflow.api_connexion.security.requires_access_*` instead -AIR302_names.py:59:1: AIR302 `airflow.metrics.validators.AllowListValidator` is removed in Airflow 3.0; use `airflow.metrics.validators.PatternAllowListValidator` instead +AIR302_names.py:59:1: AIR302 `airflow.metrics.validators.AllowListValidator` is removed in Airflow 3.0 | 57 | requires_access 58 | @@ -108,8 +116,9 @@ AIR302_names.py:59:1: AIR302 `airflow.metrics.validators.AllowListValidator` is | ^^^^^^^^^^^^^^^^^^ AIR302 60 | BlockListValidator | + = help: Use `airflow.metrics.validators.PatternAllowListValidator` instead -AIR302_names.py:60:1: AIR302 `airflow.metrics.validators.BlockListValidator` is removed in Airflow 3.0; use `airflow.metrics.validators.PatternBlockListValidator` instead +AIR302_names.py:60:1: AIR302 `airflow.metrics.validators.BlockListValidator` is removed in Airflow 3.0 | 59 | AllowListValidator 60 | BlockListValidator @@ -117,6 +126,7 @@ AIR302_names.py:60:1: AIR302 `airflow.metrics.validators.BlockListValidator` is 61 | 62 | SubDagOperator | + = help: Use `airflow.metrics.validators.PatternBlockListValidator` instead AIR302_names.py:62:1: AIR302 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0 | @@ -128,7 +138,7 @@ AIR302_names.py:62:1: AIR302 `airflow.operators.subdag.SubDagOperator` is remove 64 | dates.date_range | -AIR302_names.py:64:7: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0; use `airflow.timetables.` instead +AIR302_names.py:64:7: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0 | 62 | SubDagOperator 63 | @@ -136,8 +146,9 @@ AIR302_names.py:64:7: AIR302 `airflow.utils.dates.date_range` is removed in Airf | ^^^^^^^^^^ AIR302 65 | dates.days_ago | + = help: Use `airflow.timetables.` instead -AIR302_names.py:65:7: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead +AIR302_names.py:65:7: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0 | 64 | dates.date_range 65 | dates.days_ago @@ -145,8 +156,9 @@ AIR302_names.py:65:7: AIR302 `airflow.utils.dates.days_ago` is removed in Airflo 66 | 67 | date_range | + = help: Use `pendulum.today('UTC').add(days=-N, ...)` instead -AIR302_names.py:67:1: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0; use `airflow.timetables.` instead +AIR302_names.py:67:1: AIR302 `airflow.utils.dates.date_range` is removed in Airflow 3.0 | 65 | dates.days_ago 66 | @@ -155,8 +167,9 @@ AIR302_names.py:67:1: AIR302 `airflow.utils.dates.date_range` is removed in Airf 68 | days_ago 69 | parse_execution_date | + = help: Use `airflow.timetables.` instead -AIR302_names.py:68:1: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead +AIR302_names.py:68:1: AIR302 `airflow.utils.dates.days_ago` is removed in Airflow 3.0 | 67 | date_range 68 | days_ago @@ -164,6 +177,7 @@ AIR302_names.py:68:1: AIR302 `airflow.utils.dates.days_ago` is removed in Airflo 69 | parse_execution_date 70 | round_time | + = help: Use `pendulum.today('UTC').add(days=-N, ...)` instead AIR302_names.py:69:1: AIR302 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0 | @@ -202,7 +216,7 @@ AIR302_names.py:72:1: AIR302 `airflow.utils.dates.infer_time_unit` is removed in | ^^^^^^^^^^^^^^^ AIR302 | -AIR302_names.py:79:1: AIR302 `airflow.configuration.get` is removed in Airflow 3.0; use `airflow.configuration.conf.get` instead +AIR302_names.py:79:1: AIR302 `airflow.configuration.get` is removed in Airflow 3.0 | 77 | dates.datetime_to_nano 78 | @@ -211,8 +225,9 @@ AIR302_names.py:79:1: AIR302 `airflow.configuration.get` is removed in Airflow 3 80 | 81 | get_connection, load_connections | + = help: Use `airflow.configuration.conf.get` instead -AIR302_names.py:79:6: AIR302 `airflow.configuration.getboolean` is removed in Airflow 3.0; use `airflow.configuration.conf.getboolean` instead +AIR302_names.py:79:6: AIR302 `airflow.configuration.getboolean` is removed in Airflow 3.0 | 77 | dates.datetime_to_nano 78 | @@ -221,8 +236,9 @@ AIR302_names.py:79:6: AIR302 `airflow.configuration.getboolean` is removed in Ai 80 | 81 | get_connection, load_connections | + = help: Use `airflow.configuration.conf.getboolean` instead -AIR302_names.py:79:18: AIR302 `airflow.configuration.getfloat` is removed in Airflow 3.0; use `airflow.configuration.conf.getfloat` instead +AIR302_names.py:79:18: AIR302 `airflow.configuration.getfloat` is removed in Airflow 3.0 | 77 | dates.datetime_to_nano 78 | @@ -231,8 +247,9 @@ AIR302_names.py:79:18: AIR302 `airflow.configuration.getfloat` is removed in Air 80 | 81 | get_connection, load_connections | + = help: Use `airflow.configuration.conf.getfloat` instead -AIR302_names.py:79:28: AIR302 `airflow.configuration.getint` is removed in Airflow 3.0; use `airflow.configuration.conf.getint` instead +AIR302_names.py:79:28: AIR302 `airflow.configuration.getint` is removed in Airflow 3.0 | 77 | dates.datetime_to_nano 78 | @@ -241,8 +258,9 @@ AIR302_names.py:79:28: AIR302 `airflow.configuration.getint` is removed in Airfl 80 | 81 | get_connection, load_connections | + = help: Use `airflow.configuration.conf.getint` instead -AIR302_names.py:79:36: AIR302 `airflow.configuration.has_option` is removed in Airflow 3.0; use `airflow.configuration.conf.has_option` instead +AIR302_names.py:79:36: AIR302 `airflow.configuration.has_option` is removed in Airflow 3.0 | 77 | dates.datetime_to_nano 78 | @@ -251,8 +269,9 @@ AIR302_names.py:79:36: AIR302 `airflow.configuration.has_option` is removed in A 80 | 81 | get_connection, load_connections | + = help: Use `airflow.configuration.conf.has_option` instead -AIR302_names.py:79:48: AIR302 `airflow.configuration.remove_option` is removed in Airflow 3.0; use `airflow.configuration.conf.remove_option` instead +AIR302_names.py:79:48: AIR302 `airflow.configuration.remove_option` is removed in Airflow 3.0 | 77 | dates.datetime_to_nano 78 | @@ -261,8 +280,9 @@ AIR302_names.py:79:48: AIR302 `airflow.configuration.remove_option` is removed i 80 | 81 | get_connection, load_connections | + = help: Use `airflow.configuration.conf.remove_option` instead -AIR302_names.py:79:63: AIR302 `airflow.configuration.as_dict` is removed in Airflow 3.0; use `airflow.configuration.conf.as_dict` instead +AIR302_names.py:79:63: AIR302 `airflow.configuration.as_dict` is removed in Airflow 3.0 | 77 | dates.datetime_to_nano 78 | @@ -271,8 +291,9 @@ AIR302_names.py:79:63: AIR302 `airflow.configuration.as_dict` is removed in Airf 80 | 81 | get_connection, load_connections | + = help: Use `airflow.configuration.conf.as_dict` instead -AIR302_names.py:79:72: AIR302 `airflow.configuration.set` is removed in Airflow 3.0; use `airflow.configuration.conf.set` instead +AIR302_names.py:79:72: AIR302 `airflow.configuration.set` is removed in Airflow 3.0 | 77 | dates.datetime_to_nano 78 | @@ -281,32 +302,36 @@ AIR302_names.py:79:72: AIR302 `airflow.configuration.set` is removed in Airflow 80 | 81 | get_connection, load_connections | + = help: Use `airflow.configuration.conf.set` instead -AIR302_names.py:81:1: AIR302 `airflow.secrets.local_filesystem.get_connection` is removed in Airflow 3.0; use `airflow.secrets.local_filesystem.load_connections_dict` instead +AIR302_names.py:81:1: AIR302 `airflow.secrets.local_filesystem.get_connection` is removed in Airflow 3.0 | 79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set 80 | 81 | get_connection, load_connections | ^^^^^^^^^^^^^^ AIR302 | + = help: Use `airflow.secrets.local_filesystem.load_connections_dict` instead -AIR302_names.py:81:17: AIR302 `airflow.secrets.local_filesystem.load_connections` is removed in Airflow 3.0; use `airflow.secrets.local_filesystem.load_connections_dict` instead +AIR302_names.py:81:17: AIR302 `airflow.secrets.local_filesystem.load_connections` is removed in Airflow 3.0 | 79 | get, getboolean, getfloat, getint, has_option, remove_option, as_dict, set 80 | 81 | get_connection, load_connections | ^^^^^^^^^^^^^^^^ AIR302 | + = help: Use `airflow.secrets.local_filesystem.load_connections_dict` instead -AIR302_names.py:84:1: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` is removed in Airflow 3.0; use `airflow.sensors.external_task.ExternalTaskSensorLink` instead +AIR302_names.py:84:1: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` is removed in Airflow 3.0 | 84 | ExternalTaskSensorLink | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 85 | BashOperator 86 | BaseBranchOperator | + = help: Use `airflow.sensors.external_task.ExternalTaskSensorLink` instead -AIR302_names.py:85:1: AIR302 `airflow.operators.bash_operator.BashOperator` is removed in Airflow 3.0; use `airflow.operators.bash.BashOperator` instead +AIR302_names.py:85:1: AIR302 `airflow.operators.bash_operator.BashOperator` is removed in Airflow 3.0 | 84 | ExternalTaskSensorLink 85 | BashOperator @@ -314,8 +339,9 @@ AIR302_names.py:85:1: AIR302 `airflow.operators.bash_operator.BashOperator` is r 86 | BaseBranchOperator 87 | EmptyOperator, DummyOperator | + = help: Use `airflow.operators.bash.BashOperator` instead -AIR302_names.py:86:1: AIR302 `airflow.operators.branch_operator.BaseBranchOperator` is removed in Airflow 3.0; use `airflow.operators.branch.BaseBranchOperator` instead +AIR302_names.py:86:1: AIR302 `airflow.operators.branch_operator.BaseBranchOperator` is removed in Airflow 3.0 | 84 | ExternalTaskSensorLink 85 | BashOperator @@ -324,8 +350,9 @@ AIR302_names.py:86:1: AIR302 `airflow.operators.branch_operator.BaseBranchOperat 87 | EmptyOperator, DummyOperator 88 | dummy_operator.EmptyOperator | + = help: Use `airflow.operators.branch.BaseBranchOperator` instead -AIR302_names.py:87:16: AIR302 `airflow.operators.dummy.DummyOperator` is removed in Airflow 3.0; use `airflow.operators.empty.EmptyOperator` instead +AIR302_names.py:87:16: AIR302 `airflow.operators.dummy.DummyOperator` is removed in Airflow 3.0 | 85 | BashOperator 86 | BaseBranchOperator @@ -334,8 +361,9 @@ AIR302_names.py:87:16: AIR302 `airflow.operators.dummy.DummyOperator` is removed 88 | dummy_operator.EmptyOperator 89 | dummy_operator.DummyOperator | + = help: Use `airflow.operators.empty.EmptyOperator` instead -AIR302_names.py:88:16: AIR302 `airflow.operators.dummy_operator.EmptyOperator` is removed in Airflow 3.0; use `airflow.operators.empty.EmptyOperator` instead +AIR302_names.py:88:16: AIR302 `airflow.operators.dummy_operator.EmptyOperator` is removed in Airflow 3.0 | 86 | BaseBranchOperator 87 | EmptyOperator, DummyOperator @@ -344,8 +372,9 @@ AIR302_names.py:88:16: AIR302 `airflow.operators.dummy_operator.EmptyOperator` i 89 | dummy_operator.DummyOperator 90 | EmailOperator | + = help: Use `airflow.operators.empty.EmptyOperator` instead -AIR302_names.py:89:16: AIR302 `airflow.operators.dummy_operator.DummyOperator` is removed in Airflow 3.0; use `airflow.operators.empty.EmptyOperator` instead +AIR302_names.py:89:16: AIR302 `airflow.operators.dummy_operator.DummyOperator` is removed in Airflow 3.0 | 87 | EmptyOperator, DummyOperator 88 | dummy_operator.EmptyOperator @@ -354,8 +383,9 @@ AIR302_names.py:89:16: AIR302 `airflow.operators.dummy_operator.DummyOperator` i 90 | EmailOperator 91 | BaseSensorOperator | + = help: Use `airflow.operators.empty.EmptyOperator` instead -AIR302_names.py:90:1: AIR302 `airflow.operators.email_operator.EmailOperator` is removed in Airflow 3.0; use `airflow.operators.email.EmailOperator` instead +AIR302_names.py:90:1: AIR302 `airflow.operators.email_operator.EmailOperator` is removed in Airflow 3.0 | 88 | dummy_operator.EmptyOperator 89 | dummy_operator.DummyOperator @@ -364,8 +394,9 @@ AIR302_names.py:90:1: AIR302 `airflow.operators.email_operator.EmailOperator` is 91 | BaseSensorOperator 92 | DateTimeSensor | + = help: Use `airflow.operators.email.EmailOperator` instead -AIR302_names.py:91:1: AIR302 `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0; use `airflow.sensors.base.BaseSensorOperator` instead +AIR302_names.py:91:1: AIR302 `airflow.sensors.base_sensor_operator.BaseSensorOperator` is removed in Airflow 3.0 | 89 | dummy_operator.DummyOperator 90 | EmailOperator @@ -374,8 +405,9 @@ AIR302_names.py:91:1: AIR302 `airflow.sensors.base_sensor_operator.BaseSensorOpe 92 | DateTimeSensor 93 | (ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink) | + = help: Use `airflow.sensors.base.BaseSensorOperator` instead -AIR302_names.py:92:1: AIR302 `airflow.sensors.date_time_sensor.DateTimeSensor` is removed in Airflow 3.0; use `airflow.sensors.date_time.DateTimeSensor` instead +AIR302_names.py:92:1: AIR302 `airflow.sensors.date_time_sensor.DateTimeSensor` is removed in Airflow 3.0 | 90 | EmailOperator 91 | BaseSensorOperator @@ -384,8 +416,9 @@ AIR302_names.py:92:1: AIR302 `airflow.sensors.date_time_sensor.DateTimeSensor` i 93 | (ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink) 94 | TimeDeltaSensor | + = help: Use `airflow.sensors.date_time.DateTimeSensor` instead -AIR302_names.py:93:2: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskMarker` is removed in Airflow 3.0; use `airflow.sensors.external_task.ExternalTaskMarker` instead +AIR302_names.py:93:2: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskMarker` is removed in Airflow 3.0 | 91 | BaseSensorOperator 92 | DateTimeSensor @@ -393,8 +426,9 @@ AIR302_names.py:93:2: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskM | ^^^^^^^^^^^^^^^^^^ AIR302 94 | TimeDeltaSensor | + = help: Use `airflow.sensors.external_task.ExternalTaskMarker` instead -AIR302_names.py:93:22: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensor` is removed in Airflow 3.0; use `airflow.sensors.external_task.ExternalTaskSensor` instead +AIR302_names.py:93:22: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensor` is removed in Airflow 3.0 | 91 | BaseSensorOperator 92 | DateTimeSensor @@ -402,8 +436,9 @@ AIR302_names.py:93:22: AIR302 `airflow.sensors.external_task_sensor.ExternalTask | ^^^^^^^^^^^^^^^^^^ AIR302 94 | TimeDeltaSensor | + = help: Use `airflow.sensors.external_task.ExternalTaskSensor` instead -AIR302_names.py:93:42: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` is removed in Airflow 3.0; use `airflow.sensors.external_task.ExternalTaskSensorLink` instead +AIR302_names.py:93:42: AIR302 `airflow.sensors.external_task_sensor.ExternalTaskSensorLink` is removed in Airflow 3.0 | 91 | BaseSensorOperator 92 | DateTimeSensor @@ -411,8 +446,9 @@ AIR302_names.py:93:42: AIR302 `airflow.sensors.external_task_sensor.ExternalTask | ^^^^^^^^^^^^^^^^^^^^^^ AIR302 94 | TimeDeltaSensor | + = help: Use `airflow.sensors.external_task.ExternalTaskSensorLink` instead -AIR302_names.py:94:1: AIR302 `airflow.sensors.time_delta_sensor.TimeDeltaSensor` is removed in Airflow 3.0; use `airflow.sensors.time_delta.TimeDeltaSensor` instead +AIR302_names.py:94:1: AIR302 `airflow.sensors.time_delta_sensor.TimeDeltaSensor` is removed in Airflow 3.0 | 92 | DateTimeSensor 93 | (ExternalTaskMarker, ExternalTaskSensor, ExternalTaskSensorLink) @@ -421,6 +457,7 @@ AIR302_names.py:94:1: AIR302 `airflow.sensors.time_delta_sensor.TimeDeltaSensor` 95 | 96 | apply_defaults | + = help: Use `airflow.sensors.time_delta.TimeDeltaSensor` instead AIR302_names.py:96:1: AIR302 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0; `apply_defaults` is now unconditionally done and can be safely removed. | @@ -441,7 +478,7 @@ AIR302_names.py:98:1: AIR302 `airflow.utils.file.TemporaryDirectory` is removed 99 | mkdirs | -AIR302_names.py:99:1: AIR302 `airflow.utils.file.mkdirs` is removed in Airflow 3.0; use `pendulum.today('UTC').add(days=-N, ...)` instead +AIR302_names.py:99:1: AIR302 `airflow.utils.file.mkdirs` is removed in Airflow 3.0 | 98 | TemporaryDirectory 99 | mkdirs @@ -449,8 +486,9 @@ AIR302_names.py:99:1: AIR302 `airflow.utils.file.mkdirs` is removed in Airflow 3 100 | 101 | chain | + = help: Use `pendulum.today('UTC').add(days=-N, ...)` instead -AIR302_names.py:101:1: AIR302 `airflow.utils.helpers.chain` is removed in Airflow 3.0; use `airflow.models.baseoperator.chain` instead +AIR302_names.py:101:1: AIR302 `airflow.utils.helpers.chain` is removed in Airflow 3.0 | 99 | mkdirs 100 | @@ -458,8 +496,9 @@ AIR302_names.py:101:1: AIR302 `airflow.utils.helpers.chain` is removed in Airflo | ^^^^^ AIR302 102 | cross_downstream | + = help: Use `airflow.models.baseoperator.chain` instead -AIR302_names.py:102:1: AIR302 `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0; use `airflow.models.baseoperator.cross_downstream` instead +AIR302_names.py:102:1: AIR302 `airflow.utils.helpers.cross_downstream` is removed in Airflow 3.0 | 101 | chain 102 | cross_downstream @@ -467,6 +506,7 @@ AIR302_names.py:102:1: AIR302 `airflow.utils.helpers.cross_downstream` is remove 103 | 104 | SHUTDOWN | + = help: Use `airflow.models.baseoperator.cross_downstream` instead AIR302_names.py:104:1: AIR302 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0 | @@ -514,7 +554,7 @@ AIR302_names.py:110:1: AIR302 `airflow.utils.dag_cycle_tester.test_cycle` is rem 112 | has_access | -AIR302_names.py:112:1: AIR302 `airflow.www.auth.has_access` is removed in Airflow 3.0; use `airflow.www.auth.has_access_*` instead +AIR302_names.py:112:1: AIR302 `airflow.www.auth.has_access` is removed in Airflow 3.0 | 110 | test_cycle 111 | @@ -522,17 +562,20 @@ AIR302_names.py:112:1: AIR302 `airflow.www.auth.has_access` is removed in Airflo | ^^^^^^^^^^ AIR302 113 | get_sensitive_variables_fields, should_hide_value_for_key | + = help: Use `airflow.www.auth.has_access_*` instead -AIR302_names.py:113:1: AIR302 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0; use `airflow.utils.log.secrets_masker.get_sensitive_variables_fields` instead +AIR302_names.py:113:1: AIR302 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0 | 112 | has_access 113 | get_sensitive_variables_fields, should_hide_value_for_key | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 | + = help: Use `airflow.utils.log.secrets_masker.get_sensitive_variables_fields` instead -AIR302_names.py:113:33: AIR302 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0; use `airflow.utils.log.secrets_masker.should_hide_value_for_key` instead +AIR302_names.py:113:33: AIR302 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0 | 112 | has_access 113 | get_sensitive_variables_fields, should_hide_value_for_key | ^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302 | + = help: Use `airflow.utils.log.secrets_masker.should_hide_value_for_key` instead