From 07872adbafa7c00f42f1174d8aa3c3e324033009 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 26 Oct 2026 15:34:24 -0700 Subject: [PATCH 01/12] [ty] Preserve string literals in comparisons with `LiteralString` (#29741) Given `v = "1"`, ty used to narrow `v` to `Never` inside `if v != "42".upper()`. This hid the type error in `v + 1`, even though the branch runs. `"42".upper()` has type `LiteralString`, so its exact value is unknown. The comparison can narrow that operand to a known value or exclude that value. It now preserves the type of the concrete string or string-valued enum member on the other side. Fixes https://github.com/astral-sh/ty/issues/4826. ## Test plan Ran the reported example under CPython 3.13 and confirmed that the unequal branch raises `TypeError`. Checked the `!=` branch and the `else` branch of `==` with strict equality semantics enabled and disabled. In all four cases, ty reveals `Literal["1"]` and reports `unsupported-operator` for `v + 1`. --- .../mdtest/narrow/conditionals/eq.md | 31 +++++++++++++++++++ .../ty_python_semantic/src/types/equality.rs | 28 ++++++----------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index 147373c3f4..c8aa9f1ab6 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -1501,6 +1501,27 @@ def correlated_materialized_pattern(left: Top[MaterializedT], right: Materialize return 1 ``` +## `LiteralString` and string literals + +A comparison can narrow a `LiteralString` operand to a particular value or exclude that value. The +concrete string keeps its type in either branch. + +```py +from typing_extensions import LiteralString + +def compare(other: LiteralString): + value = "a" + if value != other: + reveal_type(value) # revealed: Literal["a"] + reveal_type(other) # revealed: LiteralString & ~Literal["a"] + value + 1 # error: [unsupported-operator] + + if other == value: + reveal_type(other) # revealed: Literal["a"] + else: + reveal_type(value) # revealed: Literal["a"] +``` + ## `LiteralString` and string-valued enums `LiteralString` can be narrowed by comparison with a string-valued enum member that inherits `str`'s @@ -1517,6 +1538,7 @@ from typing_extensions import LiteralString class Color(StrEnum): RED = "red" + BLUE = "blue" def narrow_literal_string_with_enum(value: LiteralString | None): if value == Color.RED: @@ -1530,6 +1552,15 @@ def narrow_literal_string_with_enum(value: LiteralString | None): reveal_type(value) # revealed: Literal["red"] ``` +The enum member keeps its type when its value differs from the `LiteralString`. + +```py +def compare_member(other: LiteralString): + color = Color.RED + if color != other: + reveal_type(color) # revealed: Literal[Color.RED] +``` + ## Module literals Modules compare equal only to the same module object: diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index f5a1cf5566..49f0172f70 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -54,7 +54,7 @@ enum ComparisonResult<'db> { /// The comparison allows the operand being constrained to be narrowed to the wrapped type. /// /// For example, if an object of type `LiteralString` compares equal to an object of type - /// `Literal["foo"]`, the equality branch can safely narrow either operand to `Literal["foo"]`. + /// `Literal["foo"]`, the equality branch narrows the `LiteralString` operand to `Literal["foo"]`. CanNarrow(Type<'db>), /// The comparison may evaluate to true or false, depending on runtime values. @@ -743,10 +743,8 @@ fn evaluate_structural_comparison<'db>( None => narrow_literal_comparison( db, env, - left, - right, - left_literal.kind(), - right_literal.kind(), + left_literal, + right_literal, operator.condition_expects_equality(branch), ), } @@ -1434,30 +1432,22 @@ fn finite_alternatives<'db>( /// Return a constraint for literal pairs whose equality cannot be decided statically. /// -/// This primarily handles `LiteralString`, which can be constrained by a concrete string literal -/// or a string-valued enum member without having a single statically known runtime value. +/// A concrete string or string-valued enum member can constrain a `LiteralString` operand. +/// In the reverse direction, the unknown string value places no constraint on the concrete operand. fn narrow_literal_comparison<'db>( db: &'db dyn Db, env: &ProgramEnvironment<'db>, - left: Type<'db>, - right: Type<'db>, - left_literal: LiteralValueTypeKind<'db>, - right_literal: LiteralValueTypeKind<'db>, + left: LiteralValueType<'db>, + right: LiteralValueType<'db>, equality_is_positive: bool, ) -> ComparisonResult<'db> { - match (left_literal, right_literal) { + match (left.kind(), right.kind()) { (LiteralValueTypeKind::LiteralString, LiteralValueTypeKind::String(_)) => { - ComparisonResult::CanNarrow(right.negate_if(db, env, !equality_is_positive)) - } - (LiteralValueTypeKind::String(_), LiteralValueTypeKind::LiteralString) => { - ComparisonResult::CanNarrow(left.negate_if(db, env, !equality_is_positive)) + ComparisonResult::CanNarrow(Type::from(right).negate_if(db, env, !equality_is_positive)) } (LiteralValueTypeKind::LiteralString, LiteralValueTypeKind::Enum(enum_literal)) => { narrow_literal_string_against_enum(db, env, enum_literal, equality_is_positive) } - (LiteralValueTypeKind::Enum(enum_literal), LiteralValueTypeKind::LiteralString) => { - narrow_literal_string_against_enum(db, env, enum_literal, equality_is_positive) - } _ => ComparisonResult::Ambiguous, } } From d1812878d5f81af6f5810d193ebc44413d1b21cc Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 26 Oct 2026 16:50:07 -0700 Subject: [PATCH 02/12] [ty] Index nominal union alternatives (#29196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR implements an optimization for type relation checks of a nominal class against a union, by doing a preliminary pass on the union to eliminate any union arms that can't possibly match, using a cached mapping from nominal type to the union elements that could possibly match it. This particularly improves tuple pattern narrowing, which can repeatedly compare narrowed intersections against the same original union. When multiple indexed classes match, the checker visits the union in its original order. It also uses the full traversal when collecting diagnostic details or when a gradual base could make the source assignable to an unrelated class. Related to astral-sh/ty#4676. ## Test plan These local measurements compare the index at `cc85ba9178` with its direct parent `e139fbd6e8`, before the rebase onto `6840db323c`. Each build used the standard fat-LTO release profile and ran `ty check` with one worker and Python 3.12. Timings include process startup. The build order varied across blocks, warmups were excluded, and the percentages are median paired wall-time changes. | Workload | Parent | With index | Paired change | | --- | ---: | ---: | ---: | | Nominal tuple, 10 cases | 204.3 ms | 181.6 ms | −10.6% | | Nominal tuple, 16 cases | 788.2 ms | 667.9 ms | −16.6% | | Nominal tuple, 32 cases | 6.02 s | 4.75 s | −21.2% | | Generic tuple, 32 cases | 14.14 s | 13.99 s | −1.1% | | Assignments spread across 96 arms | 74.6 ms | 67.1 ms | −10.2% | | Assignments matching the first arm | 65.3 ms | 67.3 ms | +2.0% | The assignment workload makes 1,536 calls after negative `isinstance` checks. Compared with a redundancy-only index, spreading matches across the 96 arms reduced time from 73.8 ms to 67.1 ms, a 9.9% improvement across 24 paired blocks with a win in every block. First-arm matches and unnarrowed arguments were about 2% slower than the redundancy-only index by paired median, with results varying by execution order. The generic 32-case control had only three blocks and showed little change. --- .../type_properties/is_assignable_to.md | 25 ++++ .../resources/mdtest/union_types.md | 118 ++++++++++++++++++ .../ty_python_semantic/src/types/relation.rs | 106 +++++++++++++++- 3 files changed, 247 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index cf7c465df9..902c624089 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -399,6 +399,31 @@ static_assert(is_assignable_to(type[Any], type[Arbitrary])) static_assert(is_assignable_to(type[Any], type[object])) ``` +### Narrowed instances with a gradual base + +A class with a gradual base can match a union arm outside its known inheritance hierarchy, even +after narrowing rules out another type. + +```py +from typing import Any +from missing import UnknownBase # error: [unresolved-import] +from ty_extensions import Intersection, Not, static_assert +from ty_extensions._internal import is_assignable_to + +class A: ... +class B: ... +class FromAny(Any): ... +class Mixed(A, UnknownBase): ... + +static_assert(is_assignable_to(Intersection[FromAny, Not[int]], A | B)) +``` + +The first arm excludes `Mixed` itself, but its unknown base can still match `B`. + +```py +static_assert(is_assignable_to(Intersection[Mixed, Not[int]], Intersection[A, Not[Mixed]] | B)) +``` + ## Heterogeneous tuple types ```py diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md index 1f24d98bfc..1b60c67d1b 100644 --- a/crates/ty_python_semantic/resources/mdtest/union_types.md +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -349,6 +349,124 @@ def f( reveal_type(h) # revealed: tuple[Any | str, ...] ``` +## Tuple unions with narrowed built-in elements + +A tuple alternative is redundant when its element type is covered by another alternative, including +when that element is a built-in type narrowed by an exclusion. The `float` annotation includes +`int`, so it also covers `int & ~bool`: + +```py +from ty_extensions import Intersection, Not + +def numeric(value: tuple[Intersection[int, Not[bool]]] | tuple[float]): + reveal_type(value) # revealed: tuple[float] +``` + +## Tuple unions with built-in exclusions + +Matching the positive class is not enough to establish redundancy: its exclusions also matter. For +example, a subclass of both `Marker` and `str` can inhabit the first tuple alternative below, but +neither branch of the second: + +```py +from ty_extensions import Intersection, Not + +class Marker: ... + +def different_exclusions( + value: tuple[Intersection[Marker, Not[int]]] | tuple[Intersection[Marker, Not[str]] | bytes], +): + reveal_type(value) # revealed: tuple[Marker & ~int] | tuple[(Marker & ~str) | bytes] +``` + +## Tuple unions with different exclusions on the same base + +`Base & ~Y` covers `Child & ~Y`, so adding the child's tuple leaves the union unchanged. Excluding a +third class, or starting from an unrelated class, leaves an additional tuple in the union. + +```py +from ty_extensions import Intersection, Not + +class Base: ... +class Child(Base): ... +class X: ... +class Y: ... +class Z: ... + +def compare( + flag: bool, + target: tuple[Intersection[Base, Not[X]] | Intersection[Base, Not[Y]]], + covered: tuple[Intersection[Child, Not[Y]]], + different_exclusion: tuple[Intersection[Child, Not[Z]]], + unrelated: tuple[Intersection[Z, Not[X]]], +): + reveal_type(covered if flag else target) # revealed: tuple[(Base & ~X) | (Base & ~Y)] + reveal_type(different_exclusion if flag else target) # revealed: tuple[Child & ~Z] | tuple[(Base & ~X) | (Base & ~Y)] + reveal_type(unrelated if flag else target) # revealed: tuple[Z & ~X] | tuple[(Base & ~X) | (Base & ~Y)] +``` + +## Tuple unions with multiple inheritance + +Either base of `Both` can cover the narrower tuple, depending on which type its element excludes. A +`Left` that excludes `str` can still be an `int` outside `Right`, so its tuple stays in the union. + +```py +from ty_extensions import Intersection, Not + +class Left: ... +class Right: ... +class Both(Left, Right): ... + +def compare( + flag: bool, + target: tuple[Intersection[Left, Not[int]] | Intersection[Right, Not[str]]], + via_left: tuple[Intersection[Both, Not[int]]], + via_right: tuple[Intersection[Both, Not[str]]], + left_excluding_str: tuple[Intersection[Left, Not[str]]], +): + reveal_type(via_left if flag else target) # revealed: tuple[(Left & ~int) | (Right & ~str)] + reveal_type(via_right if flag else target) # revealed: tuple[(Left & ~int) | (Right & ~str)] + reveal_type(left_excluding_str if flag else target) # revealed: tuple[Left & ~str] | tuple[(Left & ~int) | (Right & ~str)] +``` + +## Tuple unions with a base of `Any` + +Inheriting from `Any` preserves the distinction between a known subclass and an unrelated class when +simplifying tuple unions. + +```py +from typing import Any +from ty_extensions import Intersection, Not + +class Base(Any): ... +class Child(Base): ... + +def compare( + unrelated: tuple[Intersection[Base, Not[int]]] | tuple[str | bytes], + inherited: tuple[Intersection[Child, Not[int]]] | tuple[Intersection[Base, Not[int]] | bytes], +): + reveal_type(unrelated) # revealed: tuple[Base & ~int] | tuple[str | bytes] + reveal_type(inherited) # revealed: tuple[(Base & ~int) | bytes] +``` + +## Tuple unions with a recursive generic base + +`Node & ~Excluded` covers `Child & ~Excluded` even when `Node` appears in its own base class +specialization. + +```py +from ty_extensions import Intersection, Not + +class Node(list["Node"]): ... +class Child(Node): ... +class Excluded: ... + +def recursive( + value: tuple[Intersection[Child, Not[Excluded]]] | tuple[Intersection[Node, Not[Excluded]] | str], +): + reveal_type(value) # revealed: tuple[(Node & ~Excluded) | str] +``` + ## Unions of other generic containers ```toml diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 56def61308..8ab81aaf3c 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -2,7 +2,7 @@ use crate::ProgramEnvironment; use std::borrow::Cow; use itertools::Itertools; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use crate::place::{DefinedPlace, Place}; use crate::types::callable::CallableTypeKind; @@ -25,7 +25,7 @@ use crate::types::{ SubclassOfType, TypeVarBoundOrConstraints, UnionType, UpcastPolicy, }; use crate::{ - Db, + Db, Program, types::{ ErrorContext, ErrorContextTree, Type, TypePair, constraints::ConstraintSet, typevar::TypeVarSet, @@ -244,6 +244,71 @@ fn has_missing_generic_nominal_bound<'db>( }) } +/// Return the class of a nominal instance with no generic specialization or tuple +/// specification of its own, provided the class is defined by a `class` statement. +fn non_generic_nominal_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + let Type::NominalInstance(instance) = ty else { + return None; + }; + if instance.own_tuple_spec(db).is_some() { + return None; + } + let ClassType::NonGeneric(class @ ClassLiteral::Static(_)) = instance.class(db, env) else { + return None; + }; + Some(class) +} + +/// Recognize a single nominal positive and nominal exclusions, without alias, type-variable, or +/// enum-literal operands. These classes can still inherit specialized or recursive generic bases. +fn nominal_intersection_bound<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + let positive = if let Type::Intersection(intersection) = ty { + if intersection.positive(db).len() != 1 + || intersection + .negative(db) + .iter() + .any(|&negative| non_generic_nominal_class(db, env, negative).is_none()) + { + return None; + } + *intersection.positive(db).iter().next()? + } else { + ty + }; + non_generic_nominal_class(db, env, positive) +} + +/// Index union alternatives by their required nominal class before checking their exclusions. +#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] +fn nominal_union_index<'db>( + db: &'db dyn Db, + program: Program<'db>, + union: UnionType<'db>, +) -> Option, Box<[Type<'db>]>>> { + let env = ProgramEnvironment::from_program(program); + let mut index: FxHashMap<_, Vec<_>> = FxHashMap::default(); + for &element in union.elements(db) { + index + .entry(nominal_intersection_bound(db, &env, element)?) + .or_default() + .push(element); + } + let mut index: FxHashMap<_, _> = index + .into_iter() + .map(|(class, elements)| (class, elements.into_boxed_slice())) + .collect(); + index.shrink_to_fit(); + Some(index) +} + #[salsa::tracked] impl<'db> Type<'db> { /// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for @@ -1390,6 +1455,43 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { return self.check_type_pair(db, alternatives, target); } + // Fast path for pattern narrowing, which can compare many intersections against the + // same wide union. If the source intersection and every arm require a single nominal + // class and have only nominal exclusions, the cached index lets us skip arms the + // source cannot match. Keep the full traversal when collecting error context for each arm. + if source.is_intersection() + && !self.is_context_collection_enabled() + && let Some(source_class) = nominal_intersection_bound(db, self.env, source) + && let Some(index) = nominal_union_index(db, self.env.program(db), union) + // A gradual base can make the source assignable to a class outside its known MRO. + && !(self.relation.is_assignability() + && source_class.iter_mro(db).any(|base| { + matches!(base, ClassBase::Any | ClassBase::Dynamic(_) | ClassBase::Divergent(_)) + })) + { + // A successful alternative needs the same nominal class or one of its bases. + // Check class identity separately: an unspecialized generic class acquires default + // arguments in its MRO. The full relation still checks each alternative's exclusions. + let mut matching = std::iter::once(source_class) + .chain(source_class.iter_mro(db).filter_map(|base| match base { + ClassBase::Class(ClassType::NonGeneric(class)) if class != source_class => { + Some(class) + } + _ => None, + })) + .filter_map(|class| index.get(&class)); + let Some(elements) = matching.next() else { + return self.never(); + }; + // One group preserves the target union's comparison order. If several bounds match, + // use the original traversal so recursive inherited specializations keep that order. + if matching.next().is_none() { + return elements.iter().when_any(db, self.constraints, |&element| { + self.check_type_pair(db, source, element) + }); + } + } + let check_expanded_source = || { // Normally non-unions cannot directly contain unions in our model due to the fact that // we enforce a DNF structure on our set-theoretic types. However, it *is* possible for From fb2f9218793e3b0e979175d43d88dcac791e92d6 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 26 Oct 2026 16:54:26 -0700 Subject: [PATCH 03/12] [ty] Fix narrowing of strings and integers compared with enum members (#29743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plain string can compare equal to a `StrEnum` member. Previously, `if value == Color.RED` narrowed a `str` to the enum and allowed `value.name`, even when `value` was the string `"red"`. The same problem affected integers compared with `IntEnum` members. Keep the matching `str` or `int` type in equality checks, tuple membership tests, and `match` value patterns. A value already known to belong to the enum can still narrow to a specific member. Closes astral-sh/ty#4827 ## Test plan Ran the reproducer with Python 3.13. The plain values `"a"` and `1` entered all three kinds of branches, and `.name` on the string raised `AttributeError`. With both default and strict equality settings, ty now reveals `str` and `int` in those branches and reports the invalid `.name` access. ## Ecosystem report Apprise loses a false warning about converting a plain string to an enum member. pip gains an assignment error where typeshed requires `VerifyMode` and CPython also accepts the plain integer `2`. Both changes are expected. [Analyzed head `e0388646c9f0d4ac09dfea732b05ca2d84839950`](https://github.com/astral-sh/ruff/commit/e0388646c9f0d4ac09dfea732b05ca2d84839950) · [Full ecosystem report](https://github.com/astral-sh/ruff/pull/29743#issuecomment-6428093437) --- .../mdtest/narrow/conditionals/eq.md | 23 +++++++++++++++ .../mdtest/narrow/conditionals/in.md | 13 +++++++++ .../resources/mdtest/narrow/match.md | 6 ++++ .../ty_python_semantic/src/types/equality.rs | 28 ++++++++++++------- 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index c8aa9f1ab6..e32441a6ca 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -864,6 +864,29 @@ def loop_back(): value = Integer.Y ``` +The comparison narrows a `str | int` to the matching builtin type. A plain string can equal +`String.X` and reach the `.name` access. + +```py +def compare_with_enum_member(value: str | int): + if value == String.X: + reveal_type(value) # revealed: str + value.name # error: [unresolved-attribute] + + if Integer.X == value: + reveal_type(value) # revealed: int +``` + +Members of other `IntEnum` classes can also compare equal to `Integer.X`. + +```py +def compare_int_enums(value: IntEnum): + if value != Integer.X: + reveal_type(value) # revealed: IntEnum & ~Literal[Integer.X] + else: + reveal_type(value) # revealed: IntEnum +``` + A custom `__new__` can replace the value declared in an `IntEnum` class body. We can still narrow the members of `Foo`, whose runtime values are known, but must preserve all of `Shifted` because its members' runtime values cannot be determined statically: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md index c814fdc39c..fe04661a36 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -1442,6 +1442,19 @@ def _(colors: list[Color]): reveal_type(inline) # revealed: list[Color] ``` +Membership succeeds for the plain integer `1` as well as for the enum member `Code.OK`. + +```py +from enum import IntEnum + +class Code(IntEnum): + OK = 1 + +def check_membership(value: int): + if value in (Code.OK,): + reveal_type(value) # revealed: int +``` + An enum that can have additional runtime members can still be narrowed by a membership test against an explicit member. The other branch excludes that member without assuming that the declared members are exhaustive. diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index f4a7008462..592b5d8ea6 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -4032,6 +4032,12 @@ class Color(StrEnum): GREEN = "g" BLUE = "b" +def match_string(value: str): + match value: + case Color.GREEN as captured: + reveal_type(value) # revealed: str + reveal_type(captured) # revealed: str + def test_literal_as_enum(x: Literal["g"]) -> None: match x: case Color.RED: diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 49f0172f70..30ca71d9d0 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -1530,7 +1530,20 @@ fn compare_literal_to_other<'db>( }; let condition_expects_equality = operator.condition_expects_equality(branch); - // Treat broad builtin types as if only the literal itself can compare equal. This is + let literal_constraint = || { + if !condition_expects_equality { + ComparisonResult::CanNarrow(literal_type.negate(db, env)) + } else if matches!(literal, LiteralValueTypeKind::Enum(_)) + && literal_semantics != KnownComparisonSemantics::Object + { + // A plain string can compare equal to a `StrEnum` member. + ComparisonResult::Ambiguous + } else { + ComparisonResult::CanNarrow(literal_type) + } + }; + + // Treat broad builtin types as if only the builtin literal itself can compare equal. This is // intentionally unsafe: subclasses, including `bool` for `int`, can compare equal without // inhabiting the literal type. Explicitly typed subclasses do not take this path. if evaluator.soundness_policy.allow_unsafe_equality @@ -1539,7 +1552,7 @@ fn compare_literal_to_other<'db>( && let Some(other_semantics) = unsafe_narrowable_builtin_semantics(db, other) { return if literal_semantics == other_semantics { - ComparisonResult::CanNarrow(literal_type) + literal_constraint() } else { operator.result_from_equality(false) }; @@ -1564,17 +1577,12 @@ fn compare_literal_to_other<'db>( if literal_operand == LiteralOperand::Other && !other.is_disjoint_from(db, env, literal_type) => { - ComparisonResult::CanNarrow(literal_type.negate_if( - db, - env, - !condition_expects_equality, - )) + literal_constraint() } - Some(_) => ComparisonResult::Ambiguous, None if literal_operand == LiteralOperand::Other && !condition_expects_equality => { - ComparisonResult::CanNarrow(literal_type.negate(db, env)) + literal_constraint() } - None => ComparisonResult::Ambiguous, + _ => ComparisonResult::Ambiguous, } } From ba65abf8ba7bfbcc059f7fb9712be5ba9cab6abc Mon Sep 17 00:00:00 2001 From: "astral-automations-bot[bot]" <305554984+astral-automations-bot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:14:27 -0400 Subject: [PATCH 04/12] Bump version to 0.17.3 (#29745) Co-authored-by: astral-automations-bot[bot] <305554984+astral-automations-bot[bot]@users.noreply.github.com> Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/1_bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/3_question.yaml | 2 +- CHANGELOG.md | 28 +++++++ Cargo.lock | 82 +++++++++---------- Cargo.toml | 80 +++++++++--------- README.md | 10 +-- crates/ruff/Cargo.toml | 2 +- crates/ruff/README.md | 2 +- crates/ruff_annotate_snippets/Cargo.toml | 2 +- crates/ruff_cache/Cargo.toml | 2 +- crates/ruff_cache/README.md | 4 +- crates/ruff_command_line/Cargo.toml | 2 +- crates/ruff_command_line/README.md | 4 +- crates/ruff_db/Cargo.toml | 2 +- crates/ruff_db/README.md | 4 +- crates/ruff_diagnostics/Cargo.toml | 2 +- crates/ruff_diagnostics/README.md | 4 +- crates/ruff_formatter/Cargo.toml | 2 +- crates/ruff_formatter/README.md | 4 +- crates/ruff_graph/Cargo.toml | 2 +- crates/ruff_graph/README.md | 4 +- crates/ruff_index/Cargo.toml | 2 +- crates/ruff_index/README.md | 4 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_linter/README.md | 4 +- .../src/rules/ruff/rules/shared_clock_mock.rs | 2 +- crates/ruff_macros/Cargo.toml | 2 +- crates/ruff_macros/README.md | 4 +- crates/ruff_markdown/Cargo.toml | 2 +- crates/ruff_markdown/README.md | 4 +- crates/ruff_memory_usage/Cargo.toml | 2 +- crates/ruff_memory_usage/README.md | 4 +- crates/ruff_notebook/Cargo.toml | 2 +- crates/ruff_notebook/README.md | 4 +- crates/ruff_options_metadata/Cargo.toml | 2 +- crates/ruff_options_metadata/README.md | 4 +- crates/ruff_python_ast/Cargo.toml | 2 +- crates/ruff_python_ast/README.md | 4 +- crates/ruff_python_codegen/Cargo.toml | 2 +- crates/ruff_python_codegen/README.md | 4 +- crates/ruff_python_edits/Cargo.toml | 2 +- crates/ruff_python_edits/README.md | 4 +- crates/ruff_python_formatter/Cargo.toml | 2 +- crates/ruff_python_formatter/README.md | 4 +- crates/ruff_python_importer/Cargo.toml | 2 +- crates/ruff_python_importer/README.md | 4 +- crates/ruff_python_index/Cargo.toml | 2 +- crates/ruff_python_index/README.md | 4 +- crates/ruff_python_literal/Cargo.toml | 2 +- crates/ruff_python_literal/README.md | 4 +- crates/ruff_python_parser/Cargo.toml | 2 +- crates/ruff_python_parser/README.md | 4 +- crates/ruff_python_semantic/Cargo.toml | 2 +- crates/ruff_python_semantic/README.md | 4 +- crates/ruff_python_stdlib/Cargo.toml | 2 +- crates/ruff_python_stdlib/README.md | 4 +- crates/ruff_python_trivia/Cargo.toml | 2 +- crates/ruff_python_trivia/README.md | 4 +- crates/ruff_ranged_value/Cargo.toml | 2 +- crates/ruff_ranged_value/README.md | 4 +- crates/ruff_server/Cargo.toml | 2 +- crates/ruff_server/README.md | 4 +- crates/ruff_source_file/Cargo.toml | 2 +- crates/ruff_source_file/README.md | 4 +- crates/ruff_text_size/Cargo.toml | 2 +- crates/ruff_text_size/README.md | 4 +- crates/ruff_unicode_names2/Cargo.toml | 2 +- .../ruff_unicode_names2_generator/Cargo.toml | 2 +- .../ruff_unicode_names2_generator/README.md | 4 +- crates/ruff_wasm/Cargo.toml | 2 +- crates/ruff_wasm/README.md | 4 +- crates/ruff_workspace/Cargo.toml | 2 +- crates/ruff_workspace/README.md | 4 +- crates/ty_combine/Cargo.toml | 2 +- crates/ty_combine/README.md | 4 +- crates/ty_module_resolver/Cargo.toml | 2 +- crates/ty_module_resolver/README.md | 4 +- crates/ty_python_core/Cargo.toml | 2 +- crates/ty_python_core/README.md | 4 +- crates/ty_python_semantic/Cargo.toml | 2 +- crates/ty_python_semantic/README.md | 4 +- crates/ty_site_packages/Cargo.toml | 2 +- crates/ty_site_packages/README.md | 4 +- crates/ty_static/Cargo.toml | 2 +- crates/ty_static/README.md | 4 +- crates/ty_vendored/Cargo.toml | 2 +- docs/formatter.md | 2 +- docs/installation.md | 4 +- docs/integrations.md | 8 +- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- uv.lock | 2 +- 93 files changed, 244 insertions(+), 216 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.yaml b/.github/ISSUE_TEMPLATE/1_bug_report.yaml index 1baee4b861..f2e6d56a19 100644 --- a/.github/ISSUE_TEMPLATE/1_bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/1_bug_report.yaml @@ -26,6 +26,6 @@ body: attributes: label: Version description: What version of ruff are you using? (see `ruff version`) - placeholder: e.g., ruff 0.17.2 + placeholder: e.g., ruff 0.17.3 validations: required: false diff --git a/.github/ISSUE_TEMPLATE/3_question.yaml b/.github/ISSUE_TEMPLATE/3_question.yaml index d0856b81cd..0adb5792a4 100644 --- a/.github/ISSUE_TEMPLATE/3_question.yaml +++ b/.github/ISSUE_TEMPLATE/3_question.yaml @@ -13,6 +13,6 @@ body: attributes: label: Version description: What version of ruff are you using? (see `ruff version`) - placeholder: e.g., ruff 0.17.2 + placeholder: e.g., ruff 0.17.3 validations: required: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 304a46befb..5912e3084c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 0.17.3 + +Released on 2026-10-26. + +### Preview features + +- \[`suspicious`\] Add `shared-clock-mock` ([#29074](https://github.com/astral-sh/ruff/pull/29074)) + +### Bug fixes + +- \[`pyupgrade`\] Avoid incorrect conversions to f-strings (`UP032`) ([#25485](https://github.com/astral-sh/ruff/pull/25485)) + +### Performance + +- Buffer stdout in `ruff format` ([#29631](https://github.com/astral-sh/ruff/pull/29631)) +- Specialize ASCII identifier lexing ([#29503](https://github.com/astral-sh/ruff/pull/29503)) +- Speed up identifier and keyword lexing ([#29463](https://github.com/astral-sh/ruff/pull/29463)) + +### Documentation + +- \[`flake8-bandit`\] Describe what the rule actually checks for `httpx` (`S113`) ([#27936](https://github.com/astral-sh/ruff/pull/27936)) + +### Contributors + +- [@itsvedantkumar](https://github.com/itsvedantkumar) +- [@charliermarsh](https://github.com/charliermarsh) +- [@anishgirianish](https://github.com/anishgirianish) + ## 0.17.2 Released on 2026-10-22. diff --git a/Cargo.lock b/Cargo.lock index 7a3c5d0184..84eda7b221 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3098,7 +3098,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.17.2" +version = "0.17.3" dependencies = [ "anyhow", "assert_fs", @@ -3162,7 +3162,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anstream 1.0.0", "anstyle", @@ -3206,7 +3206,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.19" +version = "0.0.20" dependencies = [ "char_str", "filetime", @@ -3220,14 +3220,14 @@ dependencies = [ [[package]] name = "ruff_command_line" -version = "0.0.19" +version = "0.0.20" dependencies = [ "argfile", ] [[package]] name = "ruff_db" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anstyle", "arc-swap", @@ -3319,7 +3319,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", "is-macro", @@ -3329,7 +3329,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.19" +version = "0.0.20" dependencies = [ "drop_bomb", "ruff_cache", @@ -3345,7 +3345,7 @@ dependencies = [ [[package]] name = "ruff_graph" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "clap", @@ -3366,7 +3366,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", "ruff_macros", @@ -3376,7 +3376,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.17.2" +version = "0.17.3" dependencies = [ "aho-corasick", "anyhow", @@ -3438,7 +3438,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.19" +version = "0.0.20" dependencies = [ "heck", "itertools 0.15.0", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "ruff_markdown" -version = "0.0.19" +version = "0.0.20" dependencies = [ "insta", "regex", @@ -3482,14 +3482,14 @@ dependencies = [ [[package]] name = "ruff_memory_usage" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "rand 0.10.3", @@ -3505,14 +3505,14 @@ dependencies = [ [[package]] name = "ruff_options_metadata" -version = "0.0.19" +version = "0.0.20" dependencies = [ "serde", ] [[package]] name = "ruff_python_ast" -version = "0.0.19" +version = "0.0.20" dependencies = [ "aho-corasick", "arrayvec", @@ -3549,7 +3549,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -3561,7 +3561,7 @@ dependencies = [ [[package]] name = "ruff_python_edits" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -3571,7 +3571,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "clap", @@ -3604,7 +3604,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "insta", @@ -3619,7 +3619,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -3630,7 +3630,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.19" +version = "0.0.20" dependencies = [ "bitflags 2.13.2", "icu_normalizer", @@ -3641,7 +3641,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "bitflags 2.13.2", @@ -3672,7 +3672,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.19" +version = "0.0.20" dependencies = [ "bitflags 2.13.2", "insta", @@ -3693,7 +3693,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.19" +version = "0.0.20" dependencies = [ "bitflags 2.13.2", "unicode-ident", @@ -3701,7 +3701,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.19" +version = "0.0.20" dependencies = [ "itertools 0.15.0", "memchr", @@ -3724,7 +3724,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", "ruff_db", @@ -3737,7 +3737,7 @@ dependencies = [ [[package]] name = "ruff_server" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "crossbeam", @@ -3781,7 +3781,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", "memchr", @@ -3791,7 +3791,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.19" +version = "0.0.20" dependencies = [ "get-size2", "schemars", @@ -3802,7 +3802,7 @@ dependencies = [ [[package]] name = "ruff_unicode_names2" -version = "0.0.19" +version = "0.0.20" dependencies = [ "phf 0.14.0", "rand 0.10.3", @@ -3811,7 +3811,7 @@ dependencies = [ [[package]] name = "ruff_unicode_names2_generator" -version = "0.0.19" +version = "0.0.20" dependencies = [ "phf_codegen 0.14.0", "rand 0.10.3", @@ -3819,7 +3819,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.17.2" +version = "0.17.3" dependencies = [ "console_error_panic_hook", "console_log", @@ -3846,7 +3846,7 @@ dependencies = [ [[package]] name = "ruff_workspace" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "colored", @@ -4729,7 +4729,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ordermap", "ruff_db", @@ -4810,7 +4810,7 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "camino", @@ -4891,7 +4891,7 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "bitflags 2.13.2", @@ -4926,7 +4926,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.19" +version = "0.0.20" dependencies = [ "anyhow", "bitflags 2.13.2", @@ -5027,7 +5027,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.19" +version = "0.0.20" dependencies = [ "camino", "colored", @@ -5048,7 +5048,7 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.19" +version = "0.0.20" dependencies = [ "ruff_macros", ] @@ -5081,7 +5081,7 @@ dependencies = [ [[package]] name = "ty_vendored" -version = "0.0.19" +version = "0.0.20" dependencies = [ "path-slash", "ruff_db", diff --git a/Cargo.toml b/Cargo.toml index ae31696554..f5f3f6c54d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,55 +14,55 @@ license = "MIT" [workspace.dependencies] char_str = { version = "0.0.4", default-features = false, features = ["std"] } -ruff = { version = "0.17.2", path = "crates/ruff" } -ruff_annotate_snippets = { version = "0.0.19", path = "crates/ruff_annotate_snippets" } -ruff_cache = { version = "0.0.19", path = "crates/ruff_cache" } -ruff_command_line = { version = "0.0.19", path = "crates/ruff_command_line" } -ruff_db = { version = "0.0.19", path = "crates/ruff_db", default-features = false } -ruff_diagnostics = { version = "0.0.19", path = "crates/ruff_diagnostics" } -ruff_formatter = { version = "0.0.19", path = "crates/ruff_formatter" } -ruff_graph = { version = "0.0.19", path = "crates/ruff_graph" } -ruff_index = { version = "0.0.19", path = "crates/ruff_index" } -ruff_linter = { version = "0.17.2", path = "crates/ruff_linter" } -ruff_macros = { version = "0.0.19", path = "crates/ruff_macros" } -ruff_markdown = { version = "0.0.19", path = "crates/ruff_markdown" } -ruff_memory_usage = { version = "0.0.19", path = "crates/ruff_memory_usage" } -ruff_notebook = { version = "0.0.19", path = "crates/ruff_notebook" } -ruff_options_metadata = { version = "0.0.19", path = "crates/ruff_options_metadata" } -ruff_python_ast = { version = "0.0.19", path = "crates/ruff_python_ast" } -ruff_python_codegen = { version = "0.0.19", path = "crates/ruff_python_codegen" } -ruff_python_edits = { version = "0.0.19", path = "crates/ruff_python_edits" } -ruff_python_formatter = { version = "0.0.19", path = "crates/ruff_python_formatter" } -ruff_python_importer = { version = "0.0.19", path = "crates/ruff_python_importer" } -ruff_python_index = { version = "0.0.19", path = "crates/ruff_python_index" } -ruff_python_literal = { version = "0.0.19", path = "crates/ruff_python_literal" } -ruff_python_parser = { version = "0.0.19", path = "crates/ruff_python_parser" } -ruff_python_semantic = { version = "0.0.19", path = "crates/ruff_python_semantic" } -ruff_python_stdlib = { version = "0.0.19", path = "crates/ruff_python_stdlib" } -ruff_python_trivia = { version = "0.0.19", path = "crates/ruff_python_trivia" } -ruff_server = { version = "0.0.19", path = "crates/ruff_server" } -ruff_source_file = { version = "0.0.19", path = "crates/ruff_source_file" } +ruff = { version = "0.17.3", path = "crates/ruff" } +ruff_annotate_snippets = { version = "0.0.20", path = "crates/ruff_annotate_snippets" } +ruff_cache = { version = "0.0.20", path = "crates/ruff_cache" } +ruff_command_line = { version = "0.0.20", path = "crates/ruff_command_line" } +ruff_db = { version = "0.0.20", path = "crates/ruff_db", default-features = false } +ruff_diagnostics = { version = "0.0.20", path = "crates/ruff_diagnostics" } +ruff_formatter = { version = "0.0.20", path = "crates/ruff_formatter" } +ruff_graph = { version = "0.0.20", path = "crates/ruff_graph" } +ruff_index = { version = "0.0.20", path = "crates/ruff_index" } +ruff_linter = { version = "0.17.3", path = "crates/ruff_linter" } +ruff_macros = { version = "0.0.20", path = "crates/ruff_macros" } +ruff_markdown = { version = "0.0.20", path = "crates/ruff_markdown" } +ruff_memory_usage = { version = "0.0.20", path = "crates/ruff_memory_usage" } +ruff_notebook = { version = "0.0.20", path = "crates/ruff_notebook" } +ruff_options_metadata = { version = "0.0.20", path = "crates/ruff_options_metadata" } +ruff_python_ast = { version = "0.0.20", path = "crates/ruff_python_ast" } +ruff_python_codegen = { version = "0.0.20", path = "crates/ruff_python_codegen" } +ruff_python_edits = { version = "0.0.20", path = "crates/ruff_python_edits" } +ruff_python_formatter = { version = "0.0.20", path = "crates/ruff_python_formatter" } +ruff_python_importer = { version = "0.0.20", path = "crates/ruff_python_importer" } +ruff_python_index = { version = "0.0.20", path = "crates/ruff_python_index" } +ruff_python_literal = { version = "0.0.20", path = "crates/ruff_python_literal" } +ruff_python_parser = { version = "0.0.20", path = "crates/ruff_python_parser" } +ruff_python_semantic = { version = "0.0.20", path = "crates/ruff_python_semantic" } +ruff_python_stdlib = { version = "0.0.20", path = "crates/ruff_python_stdlib" } +ruff_python_trivia = { version = "0.0.20", path = "crates/ruff_python_trivia" } +ruff_server = { version = "0.0.20", path = "crates/ruff_server" } +ruff_source_file = { version = "0.0.20", path = "crates/ruff_source_file" } ruff_mdtest = { path = "crates/ruff_mdtest" } -ruff_ranged_value = { version = "0.0.19", path = "crates/ruff_ranged_value" } -ruff_text_size = { version = "0.0.19", path = "crates/ruff_text_size" } -ruff_unicode_names2 = { version = "0.0.19", path = "crates/ruff_unicode_names2" } -ruff_unicode_names2_generator = { version = "0.0.19", path = "crates/ruff_unicode_names2_generator" } -ruff_workspace = { version = "0.0.19", path = "crates/ruff_workspace" } +ruff_ranged_value = { version = "0.0.20", path = "crates/ruff_ranged_value" } +ruff_text_size = { version = "0.0.20", path = "crates/ruff_text_size" } +ruff_unicode_names2 = { version = "0.0.20", path = "crates/ruff_unicode_names2" } +ruff_unicode_names2_generator = { version = "0.0.20", path = "crates/ruff_unicode_names2_generator" } +ruff_workspace = { version = "0.0.20", path = "crates/ruff_workspace" } ty = { path = "crates/ty" } -ty_combine = { version = "0.0.19", path = "crates/ty_combine" } +ty_combine = { version = "0.0.20", path = "crates/ty_combine" } ty_completion_bench = { path = "crates/ty_completion_bench" } ty_completion_eval = { path = "crates/ty_completion_eval" } ty_ide = { path = "crates/ty_ide" } -ty_module_resolver = { version = "0.0.19", path = "crates/ty_module_resolver" } +ty_module_resolver = { version = "0.0.20", path = "crates/ty_module_resolver" } ty_project = { path = "crates/ty_project", default-features = false } -ty_python_semantic = { version = "0.0.19", path = "crates/ty_python_semantic" } -ty_python_core = { version = "0.0.19", path = "crates/ty_python_core" } +ty_python_semantic = { version = "0.0.20", path = "crates/ty_python_semantic" } +ty_python_core = { version = "0.0.20", path = "crates/ty_python_core" } ty_server = { path = "crates/ty_server" } -ty_site_packages = { version = "0.0.19", path = "crates/ty_site_packages" } -ty_static = { version = "0.0.19", path = "crates/ty_static" } +ty_site_packages = { version = "0.0.20", path = "crates/ty_site_packages" } +ty_static = { version = "0.0.20", path = "crates/ty_static" } ty_test = { path = "crates/ty_test" } -ty_vendored = { version = "0.0.19", path = "crates/ty_vendored" } +ty_vendored = { version = "0.0.20", path = "crates/ty_vendored" } mdtest = { path = "crates/mdtest" } diff --git a/README.md b/README.md index 9e55650ba4..978a8fa353 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,8 @@ Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI. Invoke Ruff directly with [`uvx`](https://docs.astral.sh/uv/): ```shell -uvx ruff@0.17.2 check # Lint all files in the current directory. -uvx ruff@0.17.2 format # Format all files in the current directory. +uvx ruff@0.17.3 check # Lint all files in the current directory. +uvx ruff@0.17.3 format # Format all files in the current directory. ``` Or install Ruff with `uv` (recommended), `pip`, or `pipx`: @@ -150,8 +150,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" # For a specific version. -curl -LsSf https://astral.sh/ruff/0.17.2/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.17.2/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.17.3/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.17.3/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -184,7 +184,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.17.2 + rev: v0.17.3 hooks: # Run the linter. - id: ruff-check diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index a7cd6cddec..d88c32273f 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.17.2" +version = "0.17.3" description = "An extremely fast Python linter and code formatter" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff/README.md b/crates/ruff/README.md index f356ec25e2..50f2e04e47 100644 --- a/crates/ruff/README.md +++ b/crates/ruff/README.md @@ -10,7 +10,7 @@ See the [documentation](https://docs.astral.sh/ruff/) or This crate is the entry point to the Ruff command-line interface. The Rust API exposed here is not considered public interface. -This is version 0.17.2. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff). +This is version 0.17.3. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff). The following Ruff workspace members are also available: diff --git a/crates/ruff_annotate_snippets/Cargo.toml b/crates/ruff_annotate_snippets/Cargo.toml index 749d443abc..94b2111985 100644 --- a/crates/ruff_annotate_snippets/Cargo.toml +++ b/crates/ruff_annotate_snippets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_annotate_snippets" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cache/Cargo.toml b/crates/ruff_cache/Cargo.toml index 8581f8aa64..ffb39b7e5e 100644 --- a/crates/ruff_cache/Cargo.toml +++ b/crates/ruff_cache/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cache" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cache/README.md b/crates/ruff_cache/README.md index f59adc01de..0b338e68ac 100644 --- a/crates/ruff_cache/README.md +++ b/crates/ruff_cache/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_cache). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_cache). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_command_line/Cargo.toml b/crates/ruff_command_line/Cargo.toml index d5c203b763..63f53d9fb0 100644 --- a/crates/ruff_command_line/Cargo.toml +++ b/crates/ruff_command_line/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_command_line" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_command_line/README.md b/crates/ruff_command_line/README.md index 9559481135..aa7d21dbd2 100644 --- a/crates/ruff_command_line/README.md +++ b/crates/ruff_command_line/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_command_line). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_command_line). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml index 2b2ff249e8..3e405e0f7b 100644 --- a/crates/ruff_db/Cargo.toml +++ b/crates/ruff_db/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_db" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_db/README.md b/crates/ruff_db/README.md index f1b63aea8e..fb307ffe67 100644 --- a/crates/ruff_db/README.md +++ b/crates/ruff_db/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_db). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_db). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_diagnostics/Cargo.toml b/crates/ruff_diagnostics/Cargo.toml index 5ac0c2b321..fd1fd1ac69 100644 --- a/crates/ruff_diagnostics/Cargo.toml +++ b/crates/ruff_diagnostics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_diagnostics" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_diagnostics/README.md b/crates/ruff_diagnostics/README.md index 16975d745c..299a2947b9 100644 --- a/crates/ruff_diagnostics/README.md +++ b/crates/ruff_diagnostics/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_diagnostics). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_diagnostics). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_formatter/Cargo.toml b/crates/ruff_formatter/Cargo.toml index 67276fbcdd..bc42603cd4 100644 --- a/crates/ruff_formatter/Cargo.toml +++ b/crates/ruff_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_formatter" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_formatter/README.md b/crates/ruff_formatter/README.md index 969ff0e7b7..77dfc5b876 100644 --- a/crates/ruff_formatter/README.md +++ b/crates/ruff_formatter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_formatter). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_graph/Cargo.toml b/crates/ruff_graph/Cargo.toml index 684b5bf273..93b387cbef 100644 --- a/crates/ruff_graph/Cargo.toml +++ b/crates/ruff_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_graph" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ruff_graph/README.md b/crates/ruff_graph/README.md index 48617fdddb..fc6a1eb9dd 100644 --- a/crates/ruff_graph/README.md +++ b/crates/ruff_graph/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_graph). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_graph). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_index/Cargo.toml b/crates/ruff_index/Cargo.toml index 32e4804493..d243c18223 100644 --- a/crates/ruff_index/Cargo.toml +++ b/crates/ruff_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_index" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_index/README.md b/crates/ruff_index/README.md index ec3604a20a..4d8e43d95a 100644 --- a/crates/ruff_index/README.md +++ b/crates/ruff_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_index). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 2c62cd4417..e6c222d19e 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.17.2" +version = "0.17.3" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/README.md b/crates/ruff_linter/README.md index 5ffb4068a6..2e0646549f 100644 --- a/crates/ruff_linter/README.md +++ b/crates/ruff_linter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.17.2) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_linter). +This version (0.17.3) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_linter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_linter/src/rules/ruff/rules/shared_clock_mock.rs b/crates/ruff_linter/src/rules/ruff/rules/shared_clock_mock.rs index bebee850ca..f07749c596 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/shared_clock_mock.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/shared_clock_mock.rs @@ -54,7 +54,7 @@ use crate::codes::Category; /// - [Python documentation: Where to patch](https://docs.python.org/3/library/unittest.mock.html#where-to-patch) /// - [pytest documentation: Monkeypatching](https://docs.pytest.org/en/stable/how-to/monkeypatch.html) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "NEXT_RUFF_VERSION", category = Category::Suspicious)] +#[violation_metadata(preview_since = "0.17.3", category = Category::Suspicious)] pub(crate) struct SharedClockMock { function: String, } diff --git a/crates/ruff_macros/Cargo.toml b/crates/ruff_macros/Cargo.toml index 77bb1bf984..9e7bd63bd4 100644 --- a/crates/ruff_macros/Cargo.toml +++ b/crates/ruff_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_macros" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_macros/README.md b/crates/ruff_macros/README.md index 437fde3a13..0c9b6f42f9 100644 --- a/crates/ruff_macros/README.md +++ b/crates/ruff_macros/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_macros). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_macros). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_markdown/Cargo.toml b/crates/ruff_markdown/Cargo.toml index 4ade56f472..93a866515c 100644 --- a/crates/ruff_markdown/Cargo.toml +++ b/crates/ruff_markdown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_markdown" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ruff_markdown/README.md b/crates/ruff_markdown/README.md index 55fe754831..5735de90f3 100644 --- a/crates/ruff_markdown/README.md +++ b/crates/ruff_markdown/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_markdown). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_markdown). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_memory_usage/Cargo.toml b/crates/ruff_memory_usage/Cargo.toml index d0805e144c..050a3aeb4d 100644 --- a/crates/ruff_memory_usage/Cargo.toml +++ b/crates/ruff_memory_usage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_memory_usage" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_memory_usage/README.md b/crates/ruff_memory_usage/README.md index cb75b8a2ab..ea55181f7c 100644 --- a/crates/ruff_memory_usage/README.md +++ b/crates/ruff_memory_usage/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_memory_usage). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_memory_usage). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_notebook/Cargo.toml b/crates/ruff_notebook/Cargo.toml index 9862f9391b..56c6e3abf8 100644 --- a/crates/ruff_notebook/Cargo.toml +++ b/crates/ruff_notebook/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_notebook" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_notebook/README.md b/crates/ruff_notebook/README.md index c1a704d690..45192e53cb 100644 --- a/crates/ruff_notebook/README.md +++ b/crates/ruff_notebook/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_notebook). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_notebook). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_options_metadata/Cargo.toml b/crates/ruff_options_metadata/Cargo.toml index 46cdb358f6..6f5f3a3efe 100644 --- a/crates/ruff_options_metadata/Cargo.toml +++ b/crates/ruff_options_metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_options_metadata" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_options_metadata/README.md b/crates/ruff_options_metadata/README.md index 02d39007a6..e00104c21e 100644 --- a/crates/ruff_options_metadata/README.md +++ b/crates/ruff_options_metadata/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_options_metadata). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_options_metadata). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index 678a6a648e..b782202e44 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_ast" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_ast/README.md b/crates/ruff_python_ast/README.md index 92b1b790d8..a7a05e6370 100644 --- a/crates/ruff_python_ast/README.md +++ b/crates/ruff_python_ast/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_ast). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_ast). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_codegen/Cargo.toml b/crates/ruff_python_codegen/Cargo.toml index c5fccdb9e2..56fd650454 100644 --- a/crates/ruff_python_codegen/Cargo.toml +++ b/crates/ruff_python_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_codegen" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_codegen/README.md b/crates/ruff_python_codegen/README.md index 78cbf6a256..e3be4aa5f2 100644 --- a/crates/ruff_python_codegen/README.md +++ b/crates/ruff_python_codegen/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_codegen). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_codegen). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_edits/Cargo.toml b/crates/ruff_python_edits/Cargo.toml index 3757204e3f..261518481a 100644 --- a/crates/ruff_python_edits/Cargo.toml +++ b/crates/ruff_python_edits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_edits" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_edits/README.md b/crates/ruff_python_edits/README.md index 5c5159cb0b..35ea097130 100644 --- a/crates/ruff_python_edits/README.md +++ b/crates/ruff_python_edits/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_edits). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_edits). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index 750e66356e..c5b0ad0aad 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_formatter" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index c8920bad62..47093b2e8c 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -32,8 +32,8 @@ Head to [The Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for usage i This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_formatter). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_importer/Cargo.toml b/crates/ruff_python_importer/Cargo.toml index 85a50a119e..ef028ba550 100644 --- a/crates/ruff_python_importer/Cargo.toml +++ b/crates/ruff_python_importer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_importer" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_importer/README.md b/crates/ruff_python_importer/README.md index 28f8265916..96b0570dbe 100644 --- a/crates/ruff_python_importer/README.md +++ b/crates/ruff_python_importer/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_importer). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_importer). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_index/Cargo.toml b/crates/ruff_python_index/Cargo.toml index ec5ba29309..4ba95cc01c 100644 --- a/crates/ruff_python_index/Cargo.toml +++ b/crates/ruff_python_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_index" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_index/README.md b/crates/ruff_python_index/README.md index bddfca59ec..3f8c1efa16 100644 --- a/crates/ruff_python_index/README.md +++ b/crates/ruff_python_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_index). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_literal/Cargo.toml b/crates/ruff_python_literal/Cargo.toml index baafff7e4d..72aedb4bac 100644 --- a/crates/ruff_python_literal/Cargo.toml +++ b/crates/ruff_python_literal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_literal" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_literal/README.md b/crates/ruff_python_literal/README.md index f9b487e26f..41b21591dc 100644 --- a/crates/ruff_python_literal/README.md +++ b/crates/ruff_python_literal/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_literal). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_literal). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index 36cc69fe5b..004325e51b 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_parser" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_parser/README.md b/crates/ruff_python_parser/README.md index 50b1a48dae..e97a7b8aa0 100644 --- a/crates/ruff_python_parser/README.md +++ b/crates/ruff_python_parser/README.md @@ -19,8 +19,8 @@ Refer to the [contributing guidelines](./CONTRIBUTING.md) to get started and Git This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_parser). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_parser). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index 8ed697eb82..a5f8c3a193 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_semantic" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_semantic/README.md b/crates/ruff_python_semantic/README.md index d267200ea0..1646250a74 100644 --- a/crates/ruff_python_semantic/README.md +++ b/crates/ruff_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_semantic). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_stdlib/Cargo.toml b/crates/ruff_python_stdlib/Cargo.toml index 95a75257bd..08e77890d6 100644 --- a/crates/ruff_python_stdlib/Cargo.toml +++ b/crates/ruff_python_stdlib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_stdlib" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_stdlib/README.md b/crates/ruff_python_stdlib/README.md index f103cc1fb7..25f633fed6 100644 --- a/crates/ruff_python_stdlib/README.md +++ b/crates/ruff_python_stdlib/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_stdlib). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_stdlib). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_trivia/Cargo.toml b/crates/ruff_python_trivia/Cargo.toml index 63327128d0..d3a9f66b19 100644 --- a/crates/ruff_python_trivia/Cargo.toml +++ b/crates/ruff_python_trivia/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_trivia" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_trivia/README.md b/crates/ruff_python_trivia/README.md index bd66aab07a..a8b79ad723 100644 --- a/crates/ruff_python_trivia/README.md +++ b/crates/ruff_python_trivia/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_python_trivia). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_python_trivia). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_ranged_value/Cargo.toml b/crates/ruff_ranged_value/Cargo.toml index 921a122482..91ea44a425 100644 --- a/crates/ruff_ranged_value/Cargo.toml +++ b/crates/ruff_ranged_value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_ranged_value" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_ranged_value/README.md b/crates/ruff_ranged_value/README.md index af9ff1e5f1..e8022d559e 100644 --- a/crates/ruff_ranged_value/README.md +++ b/crates/ruff_ranged_value/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_ranged_value). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_ranged_value). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index b9bd4a99a3..74bec3146f 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_server" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_server/README.md b/crates/ruff_server/README.md index 6b288166c4..96221f3436 100644 --- a/crates/ruff_server/README.md +++ b/crates/ruff_server/README.md @@ -24,8 +24,8 @@ You can also join us on [**Discord**](https://discord.com/invite/astral-sh). This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_server). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_server). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_source_file/Cargo.toml b/crates/ruff_source_file/Cargo.toml index 387d9bd2ce..c467d71b92 100644 --- a/crates/ruff_source_file/Cargo.toml +++ b/crates/ruff_source_file/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_source_file" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_source_file/README.md b/crates/ruff_source_file/README.md index 426ea0432b..d7b5ec16c4 100644 --- a/crates/ruff_source_file/README.md +++ b/crates/ruff_source_file/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_source_file). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_source_file). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml index 30c059463d..f43de12e57 100644 --- a/crates/ruff_text_size/Cargo.toml +++ b/crates/ruff_text_size/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_text_size" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_text_size/README.md b/crates/ruff_text_size/README.md index 25bacd9eff..7a4a41eec9 100644 --- a/crates/ruff_text_size/README.md +++ b/crates/ruff_text_size/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_text_size). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_text_size). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_unicode_names2/Cargo.toml b/crates/ruff_unicode_names2/Cargo.toml index de183ff12b..fde607672e 100644 --- a/crates/ruff_unicode_names2/Cargo.toml +++ b/crates/ruff_unicode_names2/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_unicode_names2" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_unicode_names2_generator/Cargo.toml b/crates/ruff_unicode_names2_generator/Cargo.toml index 199a11236a..91b26275f9 100644 --- a/crates/ruff_unicode_names2_generator/Cargo.toml +++ b/crates/ruff_unicode_names2_generator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_unicode_names2_generator" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_unicode_names2_generator/README.md b/crates/ruff_unicode_names2_generator/README.md index 29b92d0174..4a8acf2b6a 100644 --- a/crates/ruff_unicode_names2_generator/README.md +++ b/crates/ruff_unicode_names2_generator/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_unicode_names2_generator). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_unicode_names2_generator). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index d6bcbdd52b..64000c30ff 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.17.2" +version = "0.17.3" description = "WebAssembly bindings for Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/README.md b/crates/ruff_wasm/README.md index e8fdad13a4..ae86fc85b5 100644 --- a/crates/ruff_wasm/README.md +++ b/crates/ruff_wasm/README.md @@ -55,8 +55,8 @@ const formatted = workspace.format(exampleDocument); This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.17.2) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_wasm). +This version (0.17.3) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_wasm). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_workspace/Cargo.toml b/crates/ruff_workspace/Cargo.toml index c1ec75cd01..92dfe082d0 100644 --- a/crates/ruff_workspace/Cargo.toml +++ b/crates/ruff_workspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_workspace" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_workspace/README.md b/crates/ruff_workspace/README.md index bc1cef1b54..40b58f3352 100644 --- a/crates/ruff_workspace/README.md +++ b/crates/ruff_workspace/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ruff_workspace). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ruff_workspace). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_combine/Cargo.toml b/crates/ty_combine/Cargo.toml index 6b4c71f5ba..09c33cdbbc 100644 --- a/crates/ty_combine/Cargo.toml +++ b/crates/ty_combine/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_combine" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ty_combine/README.md b/crates/ty_combine/README.md index ead8e4cb03..e886b94af9 100644 --- a/crates/ty_combine/README.md +++ b/crates/ty_combine/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_combine). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_combine). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_module_resolver/Cargo.toml b/crates/ty_module_resolver/Cargo.toml index 5f2e63b483..03cd57822f 100644 --- a/crates/ty_module_resolver/Cargo.toml +++ b/crates/ty_module_resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_module_resolver" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_module_resolver/README.md b/crates/ty_module_resolver/README.md index 0d1586486c..495c8e85f6 100644 --- a/crates/ty_module_resolver/README.md +++ b/crates/ty_module_resolver/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_module_resolver). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_module_resolver). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_core/Cargo.toml b/crates/ty_python_core/Cargo.toml index b7b9aead4e..63e78018fe 100644 --- a/crates/ty_python_core/Cargo.toml +++ b/crates/ty_python_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_core" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_python_core/README.md b/crates/ty_python_core/README.md index f5b92893fa..0a778fb5fd 100644 --- a/crates/ty_python_core/README.md +++ b/crates/ty_python_core/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_python_core). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_python_core). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index af267bcdbd..36169df119 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_semantic" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_python_semantic/README.md b/crates/ty_python_semantic/README.md index 17791cab99..617c0f18e6 100644 --- a/crates/ty_python_semantic/README.md +++ b/crates/ty_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_python_semantic). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_site_packages/Cargo.toml b/crates/ty_site_packages/Cargo.toml index ec7b126e45..e6d8fca7bb 100644 --- a/crates/ty_site_packages/Cargo.toml +++ b/crates/ty_site_packages/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_site_packages" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_site_packages/README.md b/crates/ty_site_packages/README.md index ab9b82730a..9e44eaad38 100644 --- a/crates/ty_site_packages/README.md +++ b/crates/ty_site_packages/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_site_packages). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_site_packages). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_static/Cargo.toml b/crates/ty_static/Cargo.toml index d8f9429cd5..ccf1c2debd 100644 --- a/crates/ty_static/Cargo.toml +++ b/crates/ty_static/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_static" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ty_static/README.md b/crates/ty_static/README.md index d193bd0415..80533cc7b2 100644 --- a/crates/ty_static/README.md +++ b/crates/ty_static/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.19) is a component of [Ruff 0.17.2](https://crates.io/crates/ruff/0.17.2). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.2/crates/ty_static). +This version (0.0.20) is a component of [Ruff 0.17.3](https://crates.io/crates/ruff/0.17.3). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.17.3/crates/ty_static). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_vendored/Cargo.toml b/crates/ty_vendored/Cargo.toml index 70365fdbdf..b32733a925 100644 --- a/crates/ty_vendored/Cargo.toml +++ b/crates/ty_vendored/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_vendored" -version = "0.0.19" +version = "0.0.20" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/docs/formatter.md b/docs/formatter.md index 8d55abc602..e0c59f243e 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -303,7 +303,7 @@ support needs to be explicitly included by adding it to `types_or`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.17.2 + rev: v0.17.3 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] diff --git a/docs/installation.md b/docs/installation.md index 7bd8a1c1c4..f5a774fafd 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,8 +5,8 @@ Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI. Ruff can be invoked directly with [`uvx`](https://docs.astral.sh/uv/): ```shell -uvx ruff@0.17.2 check # Lint all files in the current directory. -uvx ruff@0.17.2 format # Format all files in the current directory. +uvx ruff@0.17.3 check # Lint all files in the current directory. +uvx ruff@0.17.3 format # Format all files in the current directory. ``` Or installed with `uv` (recommended), `pip`, or `pipx`: diff --git a/docs/integrations.md b/docs/integrations.md index e14bedc2ac..138d71e48a 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -81,7 +81,7 @@ Add the following configuration to `.gitlab-ci.yml` to run `ruff format` in para stage: build interruptible: true image: - name: ghcr.io/astral-sh/ruff:0.17.2-alpine + name: ghcr.io/astral-sh/ruff:0.17.3-alpine before_script: - cd $CI_PROJECT_DIR - ruff --version @@ -107,7 +107,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.17.2 + rev: v0.17.3 hooks: # Run the linter. - id: ruff-check @@ -120,7 +120,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.17.2 + rev: v0.17.3 hooks: # Run the linter. - id: ruff-check @@ -134,7 +134,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.17.2 + rev: v0.17.3 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index 7629122ecc..2cd131b5d4 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -378,7 +378,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.17.2 + rev: v0.17.3 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index 8fa2170a36..0ade15d0c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.17.2" +version = "0.17.3" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index 247973da6c..3b2341ca72 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.17.2" +version = "0.17.3" description = "" authors = ["Charles Marsh "] diff --git a/uv.lock b/uv.lock index 15aed65c4a..bd29f44d71 100644 --- a/uv.lock +++ b/uv.lock @@ -1529,7 +1529,7 @@ wheels = [ [[package]] name = "ruff" -version = "0.17.2" +version = "0.17.3" source = { editable = "." } [package.dev-dependencies] From c9caebb2db3219b43b3de271c5671a4d197fbd43 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Oct 2026 20:08:57 -0700 Subject: [PATCH 05/12] Defer value comparisons for repeated dictionary keys (F601, F602) (#29739) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Linting a file with 8,192 dictionaries nested 64 levels deep went from 44.66 ms to 10.85 ms (4.1× faster). On 755 files from the CPython 3.12.10 standard library, the difference was within measurement noise, both with the default rules and with only F601 and F602 enabled. F601 and F602 used to build a `ComparableExpr` for every dictionary value. In `{"a": {"a": {"a": 0, "b": 1}, "b": 1}, "b": 1}`, every key is unique within its own dictionary, yet Ruff walks the inner values again at each outer level. We now record where each key first appeared and build the set of comparable values when we report the first repeat. A later occurrence still gets an unsafe fix if its value matches any earlier value for the same key. | Dictionary depth | Before | After | | ---: | ---: | ---: | | 1 | 15.70 ms | 14.80 ms | | 8 | 14.90 ms | 11.67 ms | | 32 | 27.35 ms | 11.21 ms | | 64 | 44.66 ms | 10.85 ms | Each row contains the same number of two-key dictionaries. Times are medians from 30 alternating pairs of single-core runs with `--no-cache --select F841,F601,F602`. On a separate set of dictionaries with repeated literal and variable keys, the diagnostic output and unsafe-fix diffs matched byte for byte. --- .../src/rules/pyflakes/rules/repeated_keys.rs | 118 +++++++----------- 1 file changed, 44 insertions(+), 74 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs index 80d5a89103..9a8afd528a 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/repeated_keys.rs @@ -155,89 +155,59 @@ pub(crate) fn repeated_keys(checker: &Checker, dict: &ast::ExprDict) { return; } - // Generate a map from key to (index, value). - let mut seen: FxHashMap)> = - FxHashMap::with_capacity_and_hasher(dict.len(), FxBuildHasher); + let mut seen_keys = FxHashMap::with_capacity_and_hasher(dict.len(), FxBuildHasher); + let mut seen_values = FxHashMap::default(); - // Detect duplicate keys. for (i, ast::DictItem { key, value }) in dict.iter().enumerate() { let Some(key) = key else { continue; }; - let comparable_key = HashableExpr::from(key); - let comparable_value = ComparableExpr::from(value); - - match seen.entry(comparable_key) { + let (first_index, first_key) = match seen_keys.entry(HashableExpr::from(key)) { Entry::Vacant(entry) => { - entry.insert((key, FxHashSet::from_iter([comparable_value]))); + entry.insert((i, key)); + continue; } - Entry::Occupied(mut entry) => { - let (seen_key, seen_values) = entry.get_mut(); - match key { - key if is_literal_key(key) - && checker.is_rule_enabled(Rule::MultiValueRepeatedKeyLiteral) => - { - let mut diagnostic = checker.report_diagnostic( - MultiValueRepeatedKeyLiteral { - name: SourceCodeSnippet::from_str(checker.locator().slice(key)), - existing: SourceCodeSnippet::from_str( - checker.locator().slice(*seen_key), - ), - }, - key.range(), - ); - if !seen_values.insert(comparable_value) { - diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion( - parenthesized_range( - dict.value(i - 1).into(), - dict.into(), - checker.tokens(), - ) - .unwrap_or_else(|| dict.value(i - 1).range()) - .end(), - parenthesized_range( - dict.value(i).into(), - dict.into(), - checker.tokens(), - ) - .unwrap_or_else(|| dict.value(i).range()) - .end(), - ))); - } - } - Expr::Name(_) - if checker.is_rule_enabled(Rule::MultiValueRepeatedKeyVariable) => - { - let mut diagnostic = checker.report_diagnostic( - MultiValueRepeatedKeyVariable { - name: SourceCodeSnippet::from_str(checker.locator().slice(key)), - }, - key.range(), - ); - let comparable_value: ComparableExpr = dict.value(i).into(); - if !seen_values.insert(comparable_value) { - diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion( - parenthesized_range( - dict.value(i - 1).into(), - dict.into(), - checker.tokens(), - ) - .unwrap_or_else(|| dict.value(i - 1).range()) - .end(), - parenthesized_range( - dict.value(i).into(), - dict.into(), - checker.tokens(), - ) - .unwrap_or_else(|| dict.value(i).range()) - .end(), - ))); - } - } - _ => {} - } + Entry::Occupied(entry) => *entry.get(), + }; + + let mut diagnostic = match key { + key if is_literal_key(key) + && checker.is_rule_enabled(Rule::MultiValueRepeatedKeyLiteral) => + { + checker.report_diagnostic( + MultiValueRepeatedKeyLiteral { + name: SourceCodeSnippet::from_str(checker.locator().slice(key)), + existing: SourceCodeSnippet::from_str(checker.locator().slice(first_key)), + }, + key.range(), + ) } + Expr::Name(_) if checker.is_rule_enabled(Rule::MultiValueRepeatedKeyVariable) => { + checker.report_diagnostic( + MultiValueRepeatedKeyVariable { + name: SourceCodeSnippet::from_str(checker.locator().slice(key)), + }, + key.range(), + ) + } + _ => continue, + }; + + // Comparing values is only necessary to decide whether a repeated key can be fixed. + let values = seen_values.entry(first_index).or_insert_with(|| { + FxHashSet::from_iter([ComparableExpr::from(dict.value(first_index))]) + }); + if !values.insert(ComparableExpr::from(value)) { + let previous = dict.value(i - 1); + diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion( + parenthesized_range(previous.into(), dict.into(), checker.tokens()) + .unwrap_or_else(|| previous.range()) + .end(), + parenthesized_range(value.into(), dict.into(), checker.tokens()) + .unwrap_or_else(|| value.range()) + .end(), + ))); } } } From 18c5d27c41b359d7c049f4d92695a5f7fef65990 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Oct 2026 21:21:55 -0700 Subject: [PATCH 06/12] [ty] Preserve mapping capture types on narrowed subjects (#29749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary After a truthiness check on a dictionary, ty inferred `object` for values captured by a mapping pattern and reported an error on valid code. ```python def f(d: dict[str, int]) -> None: if d: match d: case {"a": c}: c.bit_length() # `c` is an `int`. ``` The truthiness check narrows `d` to an intersection. We previously walked the receiver’s class hierarchy to recognize `dict.get` and `Mapping.get`, but an intersection has no single class to start from. We now use the definition of the bound `get` method that member lookup already resolved. An inherited `dict.get` or `Mapping.get` preserves the value type, while a custom `get` can still return a different type. The ecosystem run added one `no-matching-overload` error in `cwltool`. After an earlier case handles mutable sequences, ty previously inferred `list[Unknown]` for an enum’s symbols and accepted the call to `str.join`. A valid CWL enum has string symbols, but `{"type": "enum", "symbols": [1]}` also satisfies the Python annotation and matches the pattern. The new error flags a call that would raise `TypeError` for that input. [cwltool source](https://github.com/common-workflow-language/cwltool/blob/ad9ba5b32003b9bc6db092cd6c684265bacba199/cwltool/main.py#L236-L245) Closes https://github.com/astral-sh/ty/issues/4831. --- .../resources/mdtest/narrow/match.md | 26 +++++++++++ crates/ty_python_semantic/src/types/narrow.rs | 45 ++++++------------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 592b5d8ea6..3636bb95e3 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -2183,6 +2183,32 @@ def test_match_mapping_uses_get(value: CustomGet) -> None: reveal_type(item) # revealed: object ``` +Narrowing a mapping preserves the type of its entries when it still uses the standard `get` method. +A truthiness check can exclude empty dictionaries, and an `isinstance` check can add another class +to the subject's type. + +```py +def match_nonempty_dict(value: dict[str, int]) -> None: + if value: + match value: + case {"item": item}: + reveal_type(item) # revealed: int + +class Tagged: ... + +def match_tagged_mapping(value: Mapping[str, bytes]) -> None: + if isinstance(value, Tagged): + match value: + case {"item": item}: + reveal_type(item) # revealed: bytes + +def match_nonempty_custom_get(value: CustomGet) -> None: + if value: + match value: + case {"item": item}: + reveal_type(item) # revealed: object +``` + A callable stored on the instance can also provide `get`. The captured value still has type `object`. diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 77c126fe2e..abe4919f34 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -2880,7 +2880,7 @@ impl<'db, 'pattern> PatternSuccessAnalyzer<'db, 'pattern> { else { return Some(Type::unknown()); }; - let default_ty = if self.mapping_pattern_uses_standard_get(subject_ty) { + let default_ty = if self.is_standard_mapping_get(get_method) { mapping_value_ty } else { Type::object() @@ -2897,40 +2897,21 @@ impl<'db, 'pattern> PatternSuccessAnalyzer<'db, 'pattern> { ) } - fn mapping_pattern_uses_standard_get(&self, subject_ty: Type<'db>) -> bool { + /// Identify `dict.get` and `Mapping.get` by their definitions, even when the receiver + /// has been narrowed to an intersection and no longer has a single nominal class. + fn is_standard_mapping_get(&self, get_method: Type<'db>) -> bool { let db = self.db; - let Some(class) = subject_ty.nominal_class(db, &self.env) else { + let Type::BoundMethod(method) = get_method else { return false; }; - for base in class.iter_mro(db) { - let class = match base { - ClassBase::Class(class) => class, - ClassBase::Generic | ClassBase::Protocol => continue, - ClassBase::Any - | ClassBase::Dynamic(_) - | ClassBase::Divergent(_) - | ClassBase::TypedDict(_) => { - return false; - } - }; - if !class - .own_instance_member(db, &self.env, "get") - .is_undefined() - { - return false; - } - if class - .own_class_member(db, &self.env, None, "get") - .is_undefined() - { - continue; - } - return matches!( - class.known(db), - Some(KnownClass::Dict | KnownClass::Mapping) - ); - } - false + let Some(function) = method.function(db) else { + return false; + }; + function.name(db) == "get" + && [KnownClass::Dict, KnownClass::Mapping] + .into_iter() + .filter_map(|class| class.try_to_class_literal(db, &self.env)) + .any(|class| class.body_scope(db) == function.definition(db).scope(db)) } fn mapping_pattern_key_types(&self, kind: &MappingPatternPredicateKind<'db>) -> Vec> { From 8aeb8e2ebc09714ba6c77d9f48c84536f7814e3d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Oct 2026 21:48:18 -0700 Subject: [PATCH 07/12] [ty] Reuse exception checkpoints when a new place is read (#29750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Cold CLI checks of Home Assistant’s KNX integration took 14% less time, and checks of its Reolink integration took 4.5% less time. Each row averages 12 paired runs of `ty check homeassistant/components/` with the project’s dependencies installed. Both integrations emitted the same diagnostics before and after. | Integration | Before | After | | --- | ---: | ---: | | KNX | 389.5 ms | 334.9 ms | | Reolink | 396.0 ms | 378.1 ms | The KNX decoder has a long sequence of branches inside a `try` block. Reading a name like `GroupValueRead` for the first time used to trigger another snapshot of the bindings visible to the exception handler. Restoring an earlier snapshot already gives that name the same undefined state. We now leave the checkpoint’s binding revision alone on that first read and advance it when the place gets a definition. In isolation, a cold CLI check importing the original `xknx.APCI.from_knx` body went from 84.5 ms to 36.4 ms (57%). For `reolink_aio.Host.map_channel_json_response`, it went from 41.9 ms to 32.4 ms (23%). --- crates/ty_python_core/src/use_def.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_core/src/use_def.rs b/crates/ty_python_core/src/use_def.rs index 619df20ddd..185101117f 100644 --- a/crates/ty_python_core/src/use_def.rs +++ b/crates/ty_python_core/src/use_def.rs @@ -2193,7 +2193,8 @@ impl<'db> UseDefMapBuilder<'db> { } pub(super) fn add_place(&mut self, place: ScopedPlaceId) { - self.checkpoint_state.record_binding_change(); + // Earlier snapshots restore new places as undefined. The first definition + // changes the bindings visible to an exception handler. match place { ScopedPlaceId::Symbol(symbol) => { let new_place = self.symbol_states.push(PendingPlaceState::new( From fbf83e2268b833326bb102dcc5e6830b20b0891d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Oct 2026 23:14:59 -0700 Subject: [PATCH 08/12] [ty] Avoid replaying conditions in long boolean expressions (#29754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Checking Home Assistant’s Anthropic integration took 283.1 ms before this change and 252.6 ms after (10.8% less time, with a 95% paired interval of 7.8–13.7% less). Each of 24 blocks ran `ty check homeassistant/components/anthropic` twice with the baseline build and once with the changed build, in balanced, randomized order. All runs emitted the same 41 lines of diagnostics. The full Home Assistant timings did not resolve this patch’s individual contribution. The Anthropic SDK’s synchronous and asynchronous streaming functions each test 53 event names in one `or` expression. At each operand, ty used to reapply every condition leading up to it, recording 1,378 conditions per expression. The continuation already carries those earlier conditions, so adding just the preceding operand’s condition brings the count down to 52. Checking a file that imports the two original function bodies went from 95.9 ms to 19.3 ms. Changing just those two `or` chains to tuple membership checks made both builds take about 12 ms. Toggling prefix replay in a single release executable also reproduced the integration improvement (270.1 ms to 237.5 ms). An assignment expression (`:=`) can introduce a new binding partway through a chain. Those chains keep the earlier conditions so the reassigned name gets the same reachability information. --- crates/ty_python_core/src/builder.rs | 27 ++++++++----------- .../resources/mdtest/boolean/short_circuit.md | 9 +++++++ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index 0db46ec7c1..5cd6d85769 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -3771,7 +3771,10 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { fn visit_bool_expression(&mut self, node: &'ast ast::ExprBoolOp, context: ExpressionContext) { let ast::ExprBoolOp { values, op, .. } = node; let mut snapshots = vec![]; - let mut reachability_constraints = vec![]; + let has_named_expression = values + .iter() + .any(|value| any_over_expr(value, &ast::Expr::is_named_expr)); + let mut reachability_constraints = SmallVec::<[ScopedReachabilityConstraintId; 1]>::new(); let mut last_condition_flow_snapshots = None; for (index, value) in values.iter().enumerate() { @@ -3812,19 +3815,17 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.flow_snapshot() }; - // We first model the short-circuiting behavior. We take the short-circuit - // path here if all of the previous short-circuit paths were not taken, so - // we record all previously existing reachability constraints, and negate the - // one for the current expression. - + // This path reaches the current operand and then short-circuits. self.record_negated_reachability_constraint(reachability_constraint); snapshots.push(self.flow_snapshot()); - // Then we model the non-short-circuiting behavior. Here, we need to delay - // the application of the reachability constraint until after the expression - // has been evaluated, so we only push it onto the stack here. self.flow_restore(continuation); self.record_narrowing_constraint_id_for_places(predicate_id, &possibly_narrowed); + // The continuation carries the conditions that led to this operand. Named + // expressions can introduce new bindings that still need the earlier gates. + if !has_named_expression { + reachability_constraints.clear(); + } reachability_constraints.push(reachability_constraint); } else { last_condition_flow_snapshots = self.take_condition_flow_snapshots(value); @@ -3838,13 +3839,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { condition_flow_snapshots.into_short_circuit_and_continuation(*op); (Some(short_circuit), Some(no_short_circuit)) } else { - ( - None, - values - .iter() - .any(|value| any_over_expr(value, &ast::Expr::is_named_expr)) - .then(|| self.flow_snapshot()), - ) + (None, has_named_expression.then(|| self.flow_snapshot())) }; if let Some(last_short_circuit) = last_short_circuit { diff --git a/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md b/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md index 6286b718a4..675ddad6a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md +++ b/crates/ty_python_semantic/resources/mdtest/boolean/short_circuit.md @@ -319,6 +319,15 @@ def nested_impossible_operands(other: object, value: bool, marker: int): reveal_type(marker) # revealed: Never ``` +An assignment expression can rebind a name tested earlier in the chain. Reaching the final operand +requires the new value to be `None`, so the branch is unreachable. + +```py +def reassigned_operand(value: int | None, replacement: int | None, marker: int): + if value is not None and (value := replacement) is None and value is None and isinstance(value, int): + reveal_type(marker) # revealed: Never +``` + ## Conditions with aliased `Never` operands A call cannot produce a result when its return type is an alias of `Never`. Only the preceding From 691b60a3f3bc0c28318ee866747da8c7f0af3d3a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Oct 2026 07:30:58 -0700 Subject: [PATCH 09/12] [ty] Fix a panic when checking truthiness in a loop (#29759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Ty could panic on `assert predicate` inside a loop where `predicate` starts as a lambda and a later iteration rebinds it to a class. When cycle recovery added the class to the operand type, ty looked up its metaclass to determine its truthiness. That lookup could introduce a new inference cycle while Salsa was recovering the first one. Reachability now evaluates the predicate from the operand types that expression inference has already normalized. Outside loops, it caches the result so repeated walks of the same condition can reuse it. Inside a loop, a condition can depend on an assignment it guards, so the caller reads the inferred operands directly. Expression inference no longer stores predicate truthiness or recomputes it during recovery. The 163-project ecosystem run produced identical diagnostics. Checking isort took 291 ms, up from 248 ms on the base revision, which gives back some of the speedup from #29706. The average timing factor across projects was 1.02×. CodSpeed found no significant changes among the 214 benchmarks it compared. [Ecosystem diff](https://dqhfo45wdv0ek.cloudfront.net/ruff/pull-requests/29759/ty-ecosystem/40467940782/1/diff.html) · [Timings](https://dqhfo45wdv0ek.cloudfront.net/ruff/pull-requests/29759/ty-ecosystem/40467940782/1/timing.html) · [CodSpeed](https://github.com/astral-sh/ruff/pull/29759#issuecomment-6435864937) Closes https://github.com/astral-sh/ty/issues/4834. --- crates/ty_python_core/src/expression.rs | 2 +- .../ty_4834_predicate_truthiness_cycle.py | 11 +++ .../resources/mdtest/loops/for.md | 15 ++++ crates/ty_python_semantic/src/types/infer.rs | 75 ++++++------------- .../src/types/infer/builder.rs | 1 - .../src/types/infer/tests.rs | 1 - 6 files changed, 51 insertions(+), 54 deletions(-) create mode 100644 crates/ty_python_semantic/resources/corpus/ty_4834_predicate_truthiness_cycle.py diff --git a/crates/ty_python_core/src/expression.rs b/crates/ty_python_core/src/expression.rs index c766f22e0b..7ab9a85b49 100644 --- a/crates/ty_python_core/src/expression.rs +++ b/crates/ty_python_core/src/expression.rs @@ -91,7 +91,7 @@ pub enum ExpressionContext { pub enum ExpressionKind { /// An ordinary value expression, such as `1` in `self.x: int = 1`. Normal, - /// A predicate whose truthiness is recorded in its evaluation context. + /// A predicate evaluated in value or condition context. Predicate { context: ExpressionContext, in_loop: bool, diff --git a/crates/ty_python_semantic/resources/corpus/ty_4834_predicate_truthiness_cycle.py b/crates/ty_python_semantic/resources/corpus/ty_4834_predicate_truthiness_cycle.py new file mode 100644 index 0000000000..777246eb46 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/ty_4834_predicate_truthiness_cycle.py @@ -0,0 +1,11 @@ +# Regression test for https://github.com/astral-sh/ty/issues/4834 + +class C("", (lambda: value, (predicate := lambda: value))): + pass + +# Read the lambda before the loop adds the class as another possible value of predicate. +predicate +while True: + assert predicate + value = 0 + predicate = C diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index 63c54a9a37..9a239c8ab8 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -1943,6 +1943,21 @@ for _ in range(1_000_000): x = 2 ``` +The condition can depend on an earlier iteration. Here `y` starts at `2` and only ever receives `1` +or `2` from `x`, so the loop never decrements `x`. + +```py +def f(n: int): + x = 1 + y = 2 + for _ in range(n): + if not y: + x, y = x - 1, x + else: + x, y = 2, x + reveal_type(x) # revealed: Literal[1, 2] +``` + ### Large reachability constraint graphs fall back to `Unknown` ```py diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index f5f298a8cf..70a09a1ae7 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -45,7 +45,7 @@ use crate::ProgramEnvironment; use itertools::Either; -use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashMap; @@ -579,7 +579,7 @@ pub(crate) fn infer_expression_types<'db>( } /// A loop predicate can depend on bindings it guards in the previous iteration. -/// Reading inference directly keeps the extra truthiness query out of that cycle. +/// Evaluate it in the caller's query to keep an extra query out of that cycle. pub(crate) fn infer_predicate_truthiness<'db>( db: &'db dyn Db, expression: Expression<'db>, @@ -588,24 +588,38 @@ pub(crate) fn infer_predicate_truthiness<'db>( expression.kind(db), ExpressionKind::Predicate { in_loop: true, .. } ) { - recorded_predicate_truthiness(db, expression) + evaluate_predicate_truthiness(db, expression) } else { cached_predicate_truthiness(db, expression) } } -/// Keep reachability queries cached when an operand's type changes but its truthiness stays the same. +/// Reachability walks can revisit the same predicate for many different bindings. #[salsa::tracked( returns(copy), cycle_initial = |_, _, _| Truthiness::Ambiguous, heap_size = get_size2::GetSize::get_heap_size )] fn cached_predicate_truthiness<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Truthiness { - recorded_predicate_truthiness(db, expression) + evaluate_predicate_truthiness(db, expression) } -fn recorded_predicate_truthiness<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Truthiness { - infer_expression_types_impl(db, InferExpression::Bare(expression)).predicate_truthiness +/// A widened operand can require a new metaclass lookup, so evaluate truthiness after +/// expression inference has normalized the operand types. +fn evaluate_predicate_truthiness<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Truthiness { + let ExpressionKind::Predicate { context, .. } = expression.kind(db) else { + return Truthiness::Ambiguous; + }; + let inference = infer_expression_types(db, expression, TypeContext::default()); + let env = ProgramEnvironment::from_scope(expression.scope(db)); + let module = parsed_module(db, expression.python_file(db)).load(db); + TruthinessAnalyzer::new( + db, + &env, + |node| inference.expression_type(node), + |node| inference.comparison_truthiness(node), + ) + .truthiness(expression.node_ref(db).node(&module), context) } #[salsa::tracked( @@ -614,7 +628,7 @@ fn recorded_predicate_truthiness<'db>(db: &'db dyn Db, expression: Expression<'d cycle_fn=|db, cycle, previous: &ExpressionInference<'db>, inference: ExpressionInference<'db>, input: InferExpression<'db>| { let (expression, _) = input.into_inner(db); let env = ProgramEnvironment::from_scope(expression.scope(db)); - inference.cycle_normalized(db, &env, previous, cycle, input) + inference.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -639,7 +653,7 @@ pub(super) fn infer_expression_types_impl<'db>( let env = ProgramEnvironment::from_file(program_file); - let mut inference = TypeInferenceBuilder::new( + TypeInferenceBuilder::new( db, &env, InferenceRegion::Expression(expression, tcx), @@ -648,9 +662,7 @@ pub(super) fn infer_expression_types_impl<'db>( index, &module, ) - .finish_expression(); - inference.record_predicate_truthiness(db, &env, input, Some(module)); - inference + .finish_expression() } fn expression_cycle_initial<'db>( @@ -2027,9 +2039,6 @@ pub(crate) struct ExpressionInference<'db> { /// The types of every expression in this region. expressions: FrozenMap>, - /// The predicate's possible outcomes in its evaluation context. - predicate_truthiness: Truthiness, - extra: Option>>, /// The scope this region is part of. @@ -2098,31 +2107,6 @@ struct ExpressionInferenceExtra<'db> { } impl<'db> ExpressionInference<'db> { - /// Reachability always asks for a predicate's truthiness without an expected type. - fn record_predicate_truthiness( - &mut self, - db: &'db dyn Db, - env: &ProgramEnvironment<'db>, - input: InferExpression<'db>, - module: Option, - ) { - let InferExpression::Bare(expression) = input else { - return; - }; - let ExpressionKind::Predicate { context, .. } = expression.kind(db) else { - return; - }; - let module = - module.unwrap_or_else(|| parsed_module(db, expression.python_file(db)).load(db)); - self.predicate_truthiness = TruthinessAnalyzer::new( - db, - env, - |node| self.expression_type(node), - |node| self.comparison_truthiness(node), - ) - .truthiness(expression.node_ref(db).node(&module), context); - } - fn cycle_initial(scope: ScopeId<'db>, cycle_recovery: Type<'db>) -> Self { let _ = scope; Self { @@ -2130,7 +2114,6 @@ impl<'db> ExpressionInference<'db> { cycle_recovery: Some(cycle_recovery), ..ExpressionInferenceExtra::default() })), - predicate_truthiness: Truthiness::Ambiguous, expressions: FrozenMap::default(), #[cfg(debug_assertions)] scope, @@ -2143,7 +2126,6 @@ impl<'db> ExpressionInference<'db> { env: &ProgramEnvironment<'db>, previous: &ExpressionInference<'db>, cycle: &salsa::Cycle, - input: InferExpression<'db>, ) -> ExpressionInference<'db> { if let Some(extra) = self.extra.as_mut() { for (binding, binding_ty) in &mut extra.bindings { @@ -2169,15 +2151,6 @@ impl<'db> ExpressionInference<'db> { *ty = ty.cycle_normalized(db, env, previous_ty, cycle); } - // Normalizing the operands can change whether the predicate completes. Once the first - // provisional results have cleared, retain earlier outcomes so the predicate converges. - self.record_predicate_truthiness(db, env, input, None); - if cycle.iteration() > crate::TAINTED_CYCLES { - self.predicate_truthiness = previous - .predicate_truthiness - .union(self.predicate_truthiness); - } - if cycle.iteration() > crate::TAINTED_CYCLES && let Some(previous_extra) = previous.extra.as_deref() && !previous_extra.collection_use_constraints.is_empty() diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ad5b8137fd..3fb1e5fa49 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -12949,7 +12949,6 @@ impl<'db> FullExpressionCacheEntry<'db> { }); ExpressionInference { - predicate_truthiness: Truthiness::Ambiguous, expressions: FrozenMap::from(self.expressions), extra, #[cfg(debug_assertions)] diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index 65bf0ed577..d2acdc23f7 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -574,7 +574,6 @@ fn comparison_truthiness_widens_across_sparse_cycle_results() -> anyhow::Result< let inference = |ty, truthiness: Option| { ( ExpressionInference { - predicate_truthiness: Truthiness::Ambiguous, expressions: [(expression, ty)].into_iter().collect(), extra: truthiness.map(|truthiness| { Box::new(ExpressionInferenceExtra { From abee0da3f2ccf12f322ac50817b621d32753f860 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Oct 2026 09:18:53 -0700 Subject: [PATCH 10/12] Accept a bare raise before a semicolon (#29761) ## Summary A bare `raise` can end at a semicolon. For example, Python accepts this handler and re-raises the current exception. ```python try: work() except ValueError: raise; report() ``` The parser tried to read the semicolon as the start of an exception expression and reported a syntax error. We now recognize both a newline and a semicolon as terminators for a bare `raise`. --- .../inline/ok/bare_raise_before_semicolon.py | 2 + .../src/parser/statement.rs | 5 +- ...syntax@bare_raise_before_semicolon.py.snap | 108 ++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_python_parser/resources/inline/ok/bare_raise_before_semicolon.py create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@bare_raise_before_semicolon.py.snap diff --git a/crates/ruff_python_parser/resources/inline/ok/bare_raise_before_semicolon.py b/crates/ruff_python_parser/resources/inline/ok/bare_raise_before_semicolon.py new file mode 100644 index 0000000000..dafdd5693d --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/bare_raise_before_semicolon.py @@ -0,0 +1,2 @@ +try: work() +except ValueError: raise; report() diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index cf2cdeb530..be7cd0253f 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -532,7 +532,10 @@ impl<'src> Parser<'src> { self.bump(TokenKind::Raise); let exc = match self.current_token_kind() { - TokenKind::Newline => None, + // test_ok bare_raise_before_semicolon + // try: work() + // except ValueError: raise; report() + TokenKind::Newline | TokenKind::Semi => None, TokenKind::From => { // test_err raise_stmt_from_without_exc // raise from exc diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@bare_raise_before_semicolon.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@bare_raise_before_semicolon.py.snap new file mode 100644 index 0000000000..5cf2c6b0bc --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@bare_raise_before_semicolon.py.snap @@ -0,0 +1,108 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/bare_raise_before_semicolon.py +--- +## AST + +``` +Module( + ModModule { + node_index: NodeIndex(None), + range: 0..47, + body: [ + Try( + StmtTry { + node_index: NodeIndex(None), + range: 0..46, + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 5..11, + value: Call( + ExprCall { + node_index: NodeIndex(None), + range: 5..11, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 5..9, + id: Name("work"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 9..11, + node_index: NodeIndex(None), + args: [], + keywords: [], + }, + }, + ), + }, + ), + ], + handlers: [ + ExceptHandler( + ExceptHandlerExceptHandler { + range: 12..46, + node_index: NodeIndex(None), + type_: Some( + Name( + ExprName { + node_index: NodeIndex(None), + range: 19..29, + id: Name("ValueError"), + ctx: Load, + }, + ), + ), + name: None, + body: [ + Raise( + StmtRaise { + node_index: NodeIndex(None), + range: 31..36, + exc: None, + cause: None, + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 38..46, + value: Call( + ExprCall { + node_index: NodeIndex(None), + range: 38..46, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 38..44, + id: Name("report"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 44..46, + node_index: NodeIndex(None), + args: [], + keywords: [], + }, + }, + ), + }, + ), + ], + }, + ), + ], + orelse: [], + finalbody: [], + is_star: false, + }, + ), + ], + }, +) +``` From 731a26b88f5c81d4c46991686673b34435f7bfdf Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 27 Oct 2026 18:23:54 +0100 Subject: [PATCH 11/12] [ty] Django: support abstract models (#29744) ## Summary Django does not add an automatic primary key to abstract models. Skip synthesizing one when the model declares `abstract = True`. closes https://github.com/astral-sh/ty/issues/4822. ## Test Plan New Markdown tests --- .../resources/mdtest/external/django.md | 152 ++++++++++++++++++ .../src/types/dedicated/django.rs | 75 ++++++++- 2 files changed, 225 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/external/django.md b/crates/ty_python_semantic/resources/mdtest/external/django.md index c565586f31..eeb7738afa 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/django.md +++ b/crates/ty_python_semantic/resources/mdtest/external/django.md @@ -389,4 +389,156 @@ def _(article: NewsArticle): reveal_type(Article.get_previous_by_published(article)) # revealed: NewsArticle ``` +## Abstract models + +### Basic + +An abstract model provides reusable fields without creating its own database table. Its concrete +subclasses receive an integer primary key unless they declare or inherit an explicit primary key. + +```py +from django.db import models + +class AbstractBook(models.Model): + title = models.CharField(max_length=200) + + class Meta: + abstract = True + +class Ebook(AbstractBook): + file_size = models.IntegerField() + +class Book(AbstractBook): + isbn = models.CharField(max_length=13, primary_key=True) + +def _(abstract: AbstractBook, ebook: Ebook, book: Book): + # Abstract models do not have an automatic primary key. + abstract.id # error: [unresolved-attribute] + + # Concrete models without an explicit primary key have an automatic integer key. + reveal_type(ebook.id) # revealed: int + reveal_type(ebook.pk) # revealed: int + reveal_type(ebook.title) # revealed: str + reveal_type(ebook.file_size) # revealed: int + + # The explicit ISBN primary key prevents an automatic `id`. + book.id # error: [unresolved-attribute] + reveal_type(book.pk) # revealed: str + reveal_type(book.title) # revealed: str + reveal_type(book.isbn) # revealed: str +``` + +### Annotated abstract setting + +A bare annotation for `Meta.abstract` does not make a model abstract. + +```py +from django.db import models + +class Book(models.Model): + class Meta: + abstract: bool + +def _(book: Book): + reveal_type(book.id) # revealed: int + reveal_type(book.pk) # revealed: int +``` + +When an assignment follows the annotation, its value determines whether the model is abstract. + +```py +class AbstractBook(models.Model): + class Meta: + abstract: bool + abstract = True + +def _(abstract: AbstractBook): + abstract.id # error: [unresolved-attribute] +``` + +### Inherited Meta configuration + +Django resets an abstract model's `Meta.abstract` to `False`. A child that subclasses this `Meta` is +concrete unless it explicitly sets `abstract = True` again. + +```py +from django.db import models + +class AbstractBook(models.Model): + class Meta: + abstract = True + +class NotAbstract(AbstractBook): + # error: [invalid-attribute-override] "Meta" + class Meta(AbstractBook.Meta): + ordering = ["id"] + +class Abstract(AbstractBook): + # error: [invalid-attribute-override] "Meta" + class Meta(AbstractBook.Meta): + abstract = True + +def _(not_abstract: NotAbstract, abstract: Abstract): + # concrete models have an automatic primary key + not_abstract.id + + # abstract models do not: + abstract.id # error: [unresolved-attribute] +``` + +Configuration inherited from an ordinary class retains its `abstract` setting. + +```py +class Options: + abstract = True + +class InheritedAbstract(models.Model): + class Meta(Options): + pass + +def _(abstract: InheritedAbstract): + abstract.id # error: [unresolved-attribute] +``` + +### Multiple inheritance + +An abstract base without a primary key does not shadow an inherited `id`, regardless of the base +class order. Regression test for . + +```py +from uuid import uuid4 +from django.db import models + +class AbstractHasTitle(models.Model): + title = models.CharField(max_length=200) + + class Meta: + abstract = True + +class AbstractHasId(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4) + + class Meta: + abstract = True + +# Django allows separate `Meta` configurations, but ty currently reports a conflict between them. +# error: [invalid-attribute-override] "Incompatible inherited attribute `Meta`" +class Book1(AbstractHasTitle, AbstractHasId): + pass + +# (see above) +# error: [invalid-attribute-override] "Incompatible inherited attribute `Meta`" +class Book2(AbstractHasId, AbstractHasTitle): + pass + +def _(book1: Book1, book2: Book2): + reveal_type(book1.id) # revealed: UUID + reveal_type(book1.pk) # revealed: UUID + reveal_type(book1.title) # revealed: str + + reveal_type(book2.id) # revealed: UUID + reveal_type(book2.pk) # revealed: UUID + reveal_type(book2.title) # revealed: str +``` + [django-types]: https://github.com/sbdchd/django-types diff --git a/crates/ty_python_semantic/src/types/dedicated/django.rs b/crates/ty_python_semantic/src/types/dedicated/django.rs index e0a09b0692..0473f5ba96 100644 --- a/crates/ty_python_semantic/src/types/dedicated/django.rs +++ b/crates/ty_python_semantic/src/types/dedicated/django.rs @@ -3,10 +3,11 @@ use ruff_python_ast::name::Name; use ty_module_resolver::KnownModule; use ty_python_core::{ definition::{Definition, DefinitionKind}, - place_table, use_def_map, + place_table, semantic_index, use_def_map, }; -use crate::place::known_module_symbol; +use crate::place::{known_module_symbol, place_from_bindings}; +use crate::types::infer::nearest_enclosing_class; use crate::types::member::class_member; use crate::types::{ BindingContext, BoundTypeVarInstance, ClassBase, GenericContext, KnownClass, Parameter, @@ -149,6 +150,71 @@ fn own_model_fields<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> Box .collect() } +/// Metadata extracted from a Django model's `Meta` configuration. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +struct ModelMetadata { + is_abstract: bool, +} + +#[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] +fn model_metadata<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> ModelMetadata { + let env = ProgramEnvironment::from_scope(class.body_scope(db)); + // Subclasses do not inherit `abstract = True`, so only inspect this model's own Meta. + let Some(meta) = own_meta(db, class) else { + return ModelMetadata::default(); + }; + + for meta_base in meta.iter_mro(db, None).filter_map(ClassBase::into_class) { + let Some((meta_base, _)) = meta_base.static_class_literal(db) else { + return ModelMetadata::default(); + }; + // Django resets an abstract model's Meta.abstract to False during model creation. + // A child can inherit other options from that Meta, but must set abstract=True itself. + let enclosing_scope = meta_base.definition(db).scope(db); + if let Some(owner) = nearest_enclosing_class( + db, + semantic_index(db, enclosing_scope.program_file(db)), + enclosing_scope, + ) && owner != class + && is_model(db, owner) + && own_meta(db, owner) == Some(meta_base) + { + return ModelMetadata::default(); + } + + let scope = meta_base.body_scope(db); + let Some(symbol) = place_table(db, scope).symbol_id("abstract") else { + continue; + }; + // The public type is widened to `bool`, so read the bindings' literal values instead. + let Some(abstract_flag) = place_from_bindings( + db, + &env, + use_def_map(db, scope).end_of_scope_symbol_bindings(symbol), + ) + .place + .ignore_possibly_undefined() else { + continue; + }; + return ModelMetadata { + is_abstract: abstract_flag == Type::bool_literal(true), + }; + } + ModelMetadata::default() +} + +fn own_meta<'db>( + db: &'db dyn Db, + class: StaticClassLiteral<'db>, +) -> Option> { + let meta = class_member(db, class.body_scope(db), "Meta") + .ignore_possibly_undefined()? + .as_class_literal()? + .as_static()?; + (meta.name(db).as_str() == "Meta" && meta.definition(db).scope(db) == class.body_scope(db)) + .then_some(meta) +} + /// Provides synthesized attributes and methods for Django models. pub(in crate::types) fn synthesized_member<'db>( db: &'db dyn Db, @@ -226,6 +292,11 @@ fn primary_key_member<'db>( } } + // Abstract models do not have an automatic primary key. + if model_metadata(db, class).is_abstract { + return None; + } + known_module_symbol(db, env, KnownModule::DjangoDbModelsFields, "AutoField") .place .ignore_possibly_undefined()? From 22c07fed17be9abc9011f7de603907667816c12e Mon Sep 17 00:00:00 2001 From: Anshaj Kumar Date: Wed, 28 Oct 2026 00:56:00 +0530 Subject: [PATCH 12/12] [`flake8-no-pep420`] Document preview behavior (`INP001`) (#29764) ## Summary INP001's preview-mode check for missing `__init__.py` files in namespace-package directories with nested packages is now documented. Empty directories and directory trees with no Python files are not flagged. ## Test Plan git diff --check passing. Documentation generation stalled while syncing the Rust 1.99 toolchain; no tests run. --------- Co-authored-by: Brent Westbrook --- .../rules/implicit_namespace_package.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs index 9add8101f6..98f003726a 100644 --- a/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs +++ b/crates/ruff_linter/src/rules/flake8_no_pep420/rules/implicit_namespace_package.rs @@ -30,6 +30,19 @@ use crate::package::PackageRoot; /// `__init__.py` file is typically meant to be a regular package, and /// the absence of the `__init__.py` file is probably an oversight. /// +/// ## Preview +/// +/// In preview, the rule also checks for packages without `__init__.py` files between two regular +/// packages, such as `bar` in the project structure below. +/// +/// ```text +/// foo/ +/// ├── __init__.py +/// └── bar/ +/// └── baz/ +/// └── __init__.py +/// ``` +/// /// ## Options /// - `namespace-packages` #[derive(ViolationMetadata)]