diff --git a/README.md b/README.md index 091e0d3cfe..98eb377010 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,11 @@ Ruff is extremely actively developed and used in major open-source projects like ...and [many more](#whos-using-ruff). -Ruff is backed by [Astral](https://astral.sh). Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff), -or the original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster). +Ruff is backed by [Astral](https://astral.sh), the creators of +[uv](https://github.com/astral-sh/uv) and [ty](https://github.com/astral-sh/ty). + +Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff), or the +original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster). ## Testimonials diff --git a/crates/ruff_dev/src/generate_ty_options.rs b/crates/ruff_dev/src/generate_ty_options.rs index 733af8b00a..6322f65981 100644 --- a/crates/ruff_dev/src/generate_ty_options.rs +++ b/crates/ruff_dev/src/generate_ty_options.rs @@ -166,8 +166,9 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S output.push('\n'); let _ = writeln!(output, "**Type**: `{}`", field.value_type); output.push('\n'); - output.push_str("**Example usage** (`pyproject.toml`):\n\n"); + output.push_str("**Example usage**:\n\n"); output.push_str(&format_example( + "pyproject.toml", &format_header( field.scope, field.example, @@ -179,11 +180,11 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S output.push('\n'); } -fn format_example(header: &str, content: &str) -> String { +fn format_example(title: &str, header: &str, content: &str) -> String { if header.is_empty() { - format!("```toml\n{content}\n```\n",) + format!("```toml title=\"{title}\"\n{content}\n```\n",) } else { - format!("```toml\n{header}\n{content}\n```\n",) + format!("```toml title=\"{title}\"\n{header}\n{content}\n```\n",) } } diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md index edbea9c803..ff720a5d8e 100644 --- a/crates/ty/docs/configuration.md +++ b/crates/ty/docs/configuration.md @@ -18,9 +18,9 @@ Valid severities are: **Type**: `dict[RuleName, "ignore" | "warn" | "error"]` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.rules] possibly-unresolved-reference = "warn" division-by-zero = "ignore" @@ -45,9 +45,9 @@ configuration setting. **Type**: `list[str]` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.environment] extra-paths = ["./shared/my-search-path"] ``` @@ -76,9 +76,9 @@ This option can be used to point to virtual or system Python environments. **Type**: `str` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.environment] python = "./custom-venv-location/.venv" ``` @@ -103,9 +103,9 @@ If no platform is specified, ty will use the current platform: **Type**: `"win32" | "darwin" | "android" | "ios" | "linux" | "all" | str` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.environment] # Tailor type stubs and conditionalized type definitions to windows. python-platform = "win32" @@ -137,9 +137,9 @@ to reflect the differing contents of the standard library across Python versions **Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | "3.14" | .` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.environment] python-version = "3.12" ``` @@ -165,9 +165,9 @@ it will also be included in the first party search path. **Type**: `list[str]` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.environment] # Multiple directories (priority order) root = ["./src", "./lib", "./vendor"] @@ -185,9 +185,9 @@ bundled as a zip file in the binary **Type**: `str` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.environment] typeshed = "/path/to/custom/typeshed" ``` @@ -240,9 +240,9 @@ If not specified, defaults to `[]` (excludes no files). **Type**: `list[str]` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [[tool.ty.overrides]] exclude = [ "generated", @@ -268,9 +268,9 @@ If not specified, defaults to `["**"]` (matches all files). **Type**: `list[str]` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [[tool.ty.overrides]] include = [ "src", @@ -292,9 +292,9 @@ severity levels or disable them entirely. **Type**: `dict[RuleName, "ignore" | "warn" | "error"]` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [[tool.ty.overrides]] include = ["src"] @@ -358,9 +358,9 @@ to re-include `dist` use `exclude = ["!dist"]` **Type**: `list[str]` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.src] exclude = [ "generated", @@ -399,9 +399,9 @@ matches `/src` and not `/test/src`). **Type**: `list[str]` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.src] include = [ "src", @@ -421,9 +421,9 @@ Enabled by default. **Type**: `bool` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.src] respect-ignore-files = false ``` @@ -450,9 +450,9 @@ it will also be included in the first party search path. **Type**: `str` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.src] root = "./app" ``` @@ -471,9 +471,9 @@ Defaults to `false`. **Type**: `bool` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.terminal] # Error if ty emits any warning-level diagnostics. error-on-warning = true @@ -491,9 +491,9 @@ Defaults to `full`. **Type**: `full | concise` -**Example usage** (`pyproject.toml`): +**Example usage**: -```toml +```toml title="pyproject.toml" [tool.ty.terminal] output-format = "concise" ``` diff --git a/crates/ty/docs/environment.md b/crates/ty/docs/environment.md index 29c15b6bd0..0baf1f0ed1 100644 --- a/crates/ty/docs/environment.md +++ b/crates/ty/docs/environment.md @@ -2,6 +2,15 @@ ty defines and respects the following environment variables: +### `TY_CONFIG_FILE` + +Path to a `ty.toml` configuration file to use. + +When set, ty will use this file for configuration instead of +discovering configuration files automatically. + +Equivalent to the `--config-file` command-line argument. + ### `TY_LOG` If set, ty will use this value as the log level for its `--verbose` output. diff --git a/crates/ty/src/args.rs b/crates/ty/src/args.rs index ac334f37bf..37ae9b0bae 100644 --- a/crates/ty/src/args.rs +++ b/crates/ty/src/args.rs @@ -9,6 +9,7 @@ use ty_combine::Combine; use ty_project::metadata::options::{EnvironmentOptions, Options, SrcOptions, TerminalOptions}; use ty_project::metadata::value::{RangedValue, RelativeGlobPattern, RelativePathBuf, ValueSource}; use ty_python_semantic::lint; +use ty_static::EnvVars; // Configures Clap v3-style help menu colors const STYLES: Styles = Styles::styled() @@ -121,7 +122,7 @@ pub(crate) struct CheckCommand { /// The path to a `ty.toml` file to use for configuration. /// /// While ty configuration can be included in a `pyproject.toml` file, it is not allowed in this context. - #[arg(long, env = "TY_CONFIG_FILE", value_name = "PATH")] + #[arg(long, env = EnvVars::TY_CONFIG_FILE, value_name = "PATH")] pub(crate) config_file: Option, /// The format to use for printing diagnostic messages. diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md index db152ae907..810927d7a3 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -152,6 +152,20 @@ The expressions in these string annotations aren't valid expressions in this con shouldn't panic. ```py +# Regression test for https://github.com/astral-sh/ty/issues/1865 +# error: [fstring-type-annotation] +stringified_fstring_with_conditional: "f'{1 if 1 else 1}'" +# error: [fstring-type-annotation] +stringified_fstring_with_boolean_expression: "f'{1 or 2}'" +# error: [fstring-type-annotation] +stringified_fstring_with_generator_expression: "f'{(i for i in range(5))}'" +# error: [fstring-type-annotation] +stringified_fstring_with_list_comprehension: "f'{[i for i in range(5)]}'" +# error: [fstring-type-annotation] +stringified_fstring_with_dict_comprehension: "f'{ {i: i for i in range(5)} }'" +# error: [fstring-type-annotation] +stringified_fstring_with_set_comprehension: "f'{ {i for i in range(5)} }'" + a: "1 or 2" b: "(x := 1)" # error: [invalid-type-form] diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index 0d58d86b3f..f9342151ea 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -114,6 +114,7 @@ but fall back to `bool` otherwise. ```py from enum import Enum from types import FunctionType +from typing import TypeVar class Answer(Enum): NO = 0 @@ -137,6 +138,7 @@ reveal_type(isinstance("", int)) # revealed: bool class A: ... class SubclassOfA(A): ... +class OtherSubclassOfA(A): ... class B: ... reveal_type(isinstance(A, type)) # revealed: Literal[True] @@ -161,6 +163,29 @@ def _(x: A | B, y: list[int]): else: reveal_type(x) # revealed: B & ~A reveal_type(isinstance(x, B)) # revealed: Literal[True] + +T = TypeVar("T") +T_bound_A = TypeVar("T_bound_A", bound=A) +T_constrained = TypeVar("T_constrained", SubclassOfA, OtherSubclassOfA) + +def _( + x: T, + x_bound_a: T_bound_A, + x_constrained_sub_a: T_constrained, +): + reveal_type(isinstance(x, object)) # revealed: Literal[True] + reveal_type(isinstance(x, A)) # revealed: bool + + reveal_type(isinstance(x_bound_a, object)) # revealed: Literal[True] + reveal_type(isinstance(x_bound_a, A)) # revealed: Literal[True] + reveal_type(isinstance(x_bound_a, SubclassOfA)) # revealed: bool + reveal_type(isinstance(x_bound_a, B)) # revealed: bool + + reveal_type(isinstance(x_constrained_sub_a, object)) # revealed: Literal[True] + reveal_type(isinstance(x_constrained_sub_a, A)) # revealed: Literal[True] + reveal_type(isinstance(x_constrained_sub_a, SubclassOfA)) # revealed: bool + reveal_type(isinstance(x_constrained_sub_a, OtherSubclassOfA)) # revealed: bool + reveal_type(isinstance(x_constrained_sub_a, B)) # revealed: bool ``` Certain special forms in the typing module are not instances of `type`, so are strictly-speaking diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index a38b0a7ded..43a0a3d0af 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -522,6 +522,11 @@ impl<'db> SemanticIndex<'db> { self.scopes_by_node[&node.node_key()] } + /// Returns the id of the scope that `node` creates, if it exists. + pub(crate) fn try_node_scope(&self, node: NodeWithScopeRef) -> Option { + self.scopes_by_node.get(&node.node_key()).copied() + } + /// Checks if there is an import of `__future__.annotations` in the global scope, which affects /// the logic for type inference. pub(super) fn has_future_annotations(&self) -> bool { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 260728caa4..d5fd42ace7 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -5077,35 +5077,52 @@ impl KnownClass { /// /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option> { - // a cache of the `KnownClass`es that we have already failed to lookup in typeshed - // (and therefore that we've already logged a warning for) - static MESSAGES: LazyLock>> = LazyLock::new(Mutex::default); + #[salsa::interned(heap_size=ruff_memory_usage::heap_size)] + struct KnownClassArgument { + class: KnownClass, + } - self.try_to_class_literal_without_logging(db) - .or_else(|lookup_error| { - if MESSAGES.lock().unwrap().insert(self) { + fn known_class_to_class_literal_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _class: KnownClassArgument<'db>, + ) -> Option> { + None + } + + #[salsa::tracked(cycle_initial=known_class_to_class_literal_initial, heap_size=ruff_memory_usage::heap_size)] + fn known_class_to_class_literal<'db>( + db: &'db dyn Db, + class: KnownClassArgument<'db>, + ) -> Option> { + let class = class.class(db); + class + .try_to_class_literal_without_logging(db) + .or_else(|lookup_error| { if matches!( lookup_error, KnownClassLookupError::ClassPossiblyUnbound { .. } ) { - tracing::info!("{}", lookup_error.display(db, self)); + tracing::info!("{}", lookup_error.display(db, class)); } else { tracing::info!( "{}. Falling back to `Unknown` for the symbol instead.", - lookup_error.display(db, self) + lookup_error.display(db, class) ); } - } - match lookup_error { - KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => { - Ok(class_literal) + match lookup_error { + KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. } => { + Ok(class_literal) + } + KnownClassLookupError::ClassNotFound { .. } + | KnownClassLookupError::SymbolNotAClass { .. } => Err(()), } - KnownClassLookupError::ClassNotFound { .. } - | KnownClassLookupError::SymbolNotAClass { .. } => Err(()), - } - }) - .ok() + }) + .ok() + } + + known_class_to_class_literal(db, KnownClassArgument::new(db, self)) } /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal. diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index e727e6663b..37c9a6d184 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -84,8 +84,8 @@ use crate::types::{ ClassBase, ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, NormalizedVisitor, SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, - TypeContext, TypeMapping, TypeRelation, UnionBuilder, binding_type, definition_expression_type, - infer_definition_types, walk_signature, + TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, UnionBuilder, binding_type, + definition_expression_type, infer_definition_types, walk_signature, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -1268,6 +1268,19 @@ fn is_instance_truthiness<'db>( Type::TypeAlias(alias) => is_instance_truthiness(db, alias.value_type(db), class), + Type::TypeVar(bound_typevar) => match bound_typevar.typevar(db).bound_or_constraints(db) { + None => is_instance_truthiness(db, Type::object(), class), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + is_instance_truthiness(db, bound, class) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => always_true_if( + constraints + .elements(db) + .iter() + .all(|c| is_instance_truthiness(db, *c, class).is_always_true()), + ), + }, + Type::BoundMethod(..) | Type::KnownBoundMethod(..) | Type::WrapperDescriptor(..) @@ -1281,7 +1294,6 @@ fn is_instance_truthiness<'db>( | Type::PropertyInstance(..) | Type::AlwaysTruthy | Type::AlwaysFalsy - | Type::TypeVar(..) | Type::BoundSuper(..) | Type::TypeIs(..) | Type::Callable(..) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index d691b46cf9..76271b07ff 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7926,7 +7926,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let Some(first_comprehension) = comprehensions_iter.next() else { unreachable!("Comprehension must contain at least one generator"); }; - self.infer_standalone_expression(&first_comprehension.iter, TypeContext::default()); + self.infer_maybe_standalone_expression(&first_comprehension.iter, TypeContext::default()); if first_comprehension.is_async { EvaluationMode::Async @@ -7946,9 +7946,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let evaluation_mode = self.infer_first_comprehension_iter(generators); - let scope_id = self + let Some(scope_id) = self .index - .node_scope(NodeWithScopeRef::GeneratorExpression(generator)); + .try_node_scope(NodeWithScopeRef::GeneratorExpression(generator)) + else { + return Type::unknown(); + }; let scope = scope_id.to_scope_id(self.db(), self.file()); let inference = infer_scope_types(self.db(), scope); let yield_type = inference.expression_type(elt.as_ref()); @@ -8021,9 +8024,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_first_comprehension_iter(generators); - let scope_id = self + let Some(scope_id) = self .index - .node_scope(NodeWithScopeRef::ListComprehension(listcomp)); + .try_node_scope(NodeWithScopeRef::ListComprehension(listcomp)) + else { + return Type::unknown(); + }; let scope = scope_id.to_scope_id(self.db(), self.file()); let inference = infer_scope_types(self.db(), scope); let element_type = inference.expression_type(elt.as_ref()); @@ -8046,9 +8052,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_first_comprehension_iter(generators); - let scope_id = self + let Some(scope_id) = self .index - .node_scope(NodeWithScopeRef::DictComprehension(dictcomp)); + .try_node_scope(NodeWithScopeRef::DictComprehension(dictcomp)) + else { + return Type::unknown(); + }; let scope = scope_id.to_scope_id(self.db(), self.file()); let inference = infer_scope_types(self.db(), scope); let key_type = inference.expression_type(key.as_ref()); @@ -8071,9 +8080,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_first_comprehension_iter(generators); - let scope_id = self + let Some(scope_id) = self .index - .node_scope(NodeWithScopeRef::SetComprehension(setcomp)); + .try_node_scope(NodeWithScopeRef::SetComprehension(setcomp)) + else { + return Type::unknown(); + }; let scope = scope_id.to_scope_id(self.db(), self.file()); let inference = infer_scope_types(self.db(), scope); let element_type = inference.expression_type(elt.as_ref()); @@ -8165,14 +8177,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.module(), ) } else { - builder.infer_standalone_expression(iter, tcx) + builder.infer_maybe_standalone_expression(iter, tcx) } .iterate(builder.db()) .homogeneous_element_type(builder.db()) }); for expr in ifs { - self.infer_standalone_expression(expr, TypeContext::default()); + self.infer_maybe_standalone_expression(expr, TypeContext::default()); } } @@ -8278,7 +8290,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { orelse, } = if_expression; - let test_ty = self.infer_standalone_expression(test, TypeContext::default()); + let test_ty = self.infer_maybe_standalone_expression(test, TypeContext::default()); let body_ty = self.infer_expression(body, tcx); let orelse_ty = self.infer_expression(orelse, tcx); @@ -10341,7 +10353,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let ty = if index == values.len() - 1 { builder.infer_expression(value, TypeContext::default()) } else { - builder.infer_standalone_expression(value, TypeContext::default()) + builder.infer_maybe_standalone_expression(value, TypeContext::default()) }; (ty, value.range()) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index ba37bd2f9a..9c9831d901 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -10,9 +10,10 @@ //! argument types and return types. For each callable type in the union, the call expression's //! arguments must match _at least one_ overload. -use std::{collections::HashMap, slice::Iter}; +use std::slice::Iter; use itertools::{EitherOrBoth, Itertools}; +use rustc_hash::FxHashMap; use smallvec::{SmallVec, smallvec_inline}; use super::{DynamicType, Type, TypeVarVariance, definition_expression_type, semantic_index}; @@ -1470,7 +1471,7 @@ impl<'db> Signature<'db> { let (self_parameters, other_parameters) = parameters.into_remaining(); // Collect all the keyword-only parameters and the unmatched standard parameters. - let mut self_keywords = HashMap::new(); + let mut self_keywords = FxHashMap::default(); // Type of the variadic keyword parameter in `self`. // @@ -1483,7 +1484,7 @@ impl<'db> Signature<'db> { match self_parameter.kind() { ParameterKind::KeywordOnly { name, .. } | ParameterKind::PositionalOrKeyword { name, .. } => { - self_keywords.insert(name.clone(), self_parameter); + self_keywords.insert(name.as_str(), self_parameter); } ParameterKind::KeywordVariadic { .. } => { self_keyword_variadic = Some(self_parameter.annotated_type()); @@ -1509,7 +1510,7 @@ impl<'db> Signature<'db> { name: other_name, default_type: other_default, } => { - if let Some(self_parameter) = self_keywords.remove(other_name) { + if let Some(self_parameter) = self_keywords.remove(other_name.as_str()) { match self_parameter.kind() { ParameterKind::PositionalOrKeyword { default_type: self_default, diff --git a/crates/ty_static/src/env_vars.rs b/crates/ty_static/src/env_vars.rs index b15edffd7e..a31b544d23 100644 --- a/crates/ty_static/src/env_vars.rs +++ b/crates/ty_static/src/env_vars.rs @@ -39,6 +39,14 @@ impl EnvVars { /// when necessary, e.g. to watch for file system changes or a dedicated UI thread. pub const TY_MAX_PARALLELISM: &'static str = "TY_MAX_PARALLELISM"; + /// Path to a `ty.toml` configuration file to use. + /// + /// When set, ty will use this file for configuration instead of + /// discovering configuration files automatically. + /// + /// Equivalent to the `--config-file` command-line argument. + pub const TY_CONFIG_FILE: &'static str = "TY_CONFIG_FILE"; + /// Used to detect an activated virtual environment. pub const VIRTUAL_ENV: &'static str = "VIRTUAL_ENV"; diff --git a/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py b/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py index fde0c433c4..3a66a15e58 100644 --- a/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py +++ b/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py @@ -376,6 +376,10 @@ class IncrementalEditTest(LspTest): "The after edit diagnostics should be initialized if the test ran at least once. Did you forget to call `run`?" ) + new_diagnostics = diff_diagnostics( + self.before_edit_diagnostics, self.after_edit_diagnostics + ) + before_edit_count = sum( len(diagnostics) for _, diagnostics in self.before_edit_diagnostics ) @@ -384,7 +388,7 @@ class IncrementalEditTest(LspTest): len(diagnostics) for _, diagnostics in self.after_edit_diagnostics ) - assert after_edit_count > before_edit_count, ( + assert len(new_diagnostics) > 0, ( f"Expected more diagnostics after the change. " f"Initial: {before_edit_count}, After change: {after_edit_count}" )