Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
07efdf8887 [ty] Test for @asynccontextmanager 2025-12-08 12:59:28 +01:00
53 changed files with 821 additions and 1901 deletions

View File

@@ -1440,78 +1440,6 @@ def function():
Ok(())
}
#[test]
fn ignore_noqa() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
import os # noqa: F401
# ruff: disable[F401]
import sys
"#,
)?;
// without --ignore-noqa
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py"),
@r"
success: false
exit_code: 1
----- stdout -----
noqa.py:5:8: F401 [*] `sys` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
");
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.args(["--preview"]),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
");
// with --ignore-noqa --preview
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.args(["--ignore-noqa", "--preview"]),
@r"
success: false
exit_code: 1
----- stdout -----
noqa.py:2:8: F401 [*] `os` imported but unused
noqa.py:5:8: F401 [*] `sys` imported but unused
Found 2 errors.
[*] 2 fixable with the `--fix` option.
----- stderr -----
");
Ok(())
}
#[test]
fn add_noqa() -> Result<()> {
let fixture = CliTest::new()?;
@@ -1704,100 +1632,6 @@ def unused(x): # noqa: ANN001, ARG001, D103
Ok(())
}
#[test]
fn add_noqa_existing_file_level_noqa() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
# ruff: noqa F401
import os
"#,
)?;
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.arg("--preview")
.args(["--add-noqa"])
.arg("-")
.pass_stdin(r#"
"#), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let test_code =
fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
insta::assert_snapshot!(test_code, @r"
# ruff: noqa F401
import os
");
Ok(())
}
#[test]
fn add_noqa_existing_range_suppression() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"ruff.toml",
r#"
[lint]
select = ["F401"]
"#,
)?;
fixture.write_file(
"noqa.py",
r#"
# ruff: disable[F401]
import os
"#,
)?;
assert_cmd_snapshot!(fixture
.check_command()
.args(["--config", "ruff.toml"])
.arg("noqa.py")
.arg("--preview")
.args(["--add-noqa"])
.arg("-")
.pass_stdin(r#"
"#), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
let test_code =
fs::read_to_string(fixture.root().join("noqa.py")).expect("should read test file");
insta::assert_snapshot!(test_code, @r"
# ruff: disable[F401]
import os
");
Ok(())
}
#[test]
fn add_noqa_multiline_comment() -> Result<()> {
let fixture = CliTest::new()?;

View File

@@ -166,8 +166,28 @@ impl Diagnostic {
/// Returns the primary message for this diagnostic.
///
/// A diagnostic always has a message, but it may be empty.
///
/// NOTE: At present, this routine will return the first primary
/// annotation's message as the primary message when the main diagnostic
/// message is empty. This is meant to facilitate an incremental migration
/// in ty over to the new diagnostic data model. (The old data model
/// didn't distinguish between messages on the entire diagnostic and
/// messages attached to a particular span.)
pub fn primary_message(&self) -> &str {
self.inner.message.as_str()
if !self.inner.message.as_str().is_empty() {
return self.inner.message.as_str();
}
// FIXME: As a special case, while we're migrating ty
// to the new diagnostic data model, we'll look for a primary
// message from the primary annotation. This is because most
// ty diagnostics are created with an empty diagnostic
// message and instead attach the message to the annotation.
// Fixing this will require touching basically every diagnostic
// in ty, so we do it this way for now to match the old
// semantics. ---AG
self.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default()
}
/// Introspects this diagnostic and returns what kind of "primary" message
@@ -179,6 +199,18 @@ impl Diagnostic {
/// contains *essential* information or context for understanding the
/// diagnostic.
///
/// The reason why we don't just always return both the main diagnostic
/// message and the primary annotation message is because this was written
/// in the midst of an incremental migration of ty over to the new
/// diagnostic data model. At time of writing, diagnostics were still
/// constructed in the old model where the main diagnostic message and the
/// primary annotation message were not distinguished from each other. So
/// for now, we carefully return what kind of messages this diagnostic
/// contains. In effect, if this diagnostic has a non-empty main message
/// *and* a non-empty primary annotation message, then the diagnostic is
/// 100% using the new diagnostic data model and we can format things
/// appropriately.
///
/// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what
/// you want.
@@ -192,10 +224,11 @@ impl Diagnostic {
.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default();
if annotation.is_empty() {
ConciseMessage::MainDiagnostic(main)
} else {
ConciseMessage::Both { main, annotation }
match (main.is_empty(), annotation.is_empty()) {
(false, true) => ConciseMessage::MainDiagnostic(main),
(true, false) => ConciseMessage::PrimaryAnnotation(annotation),
(false, false) => ConciseMessage::Both { main, annotation },
(true, true) => ConciseMessage::Empty,
}
}
@@ -660,6 +693,18 @@ impl SubDiagnostic {
/// contains *essential* information or context for understanding the
/// diagnostic.
///
/// The reason why we don't just always return both the main diagnostic
/// message and the primary annotation message is because this was written
/// in the midst of an incremental migration of ty over to the new
/// diagnostic data model. At time of writing, diagnostics were still
/// constructed in the old model where the main diagnostic message and the
/// primary annotation message were not distinguished from each other. So
/// for now, we carefully return what kind of messages this diagnostic
/// contains. In effect, if this diagnostic has a non-empty main message
/// *and* a non-empty primary annotation message, then the diagnostic is
/// 100% using the new diagnostic data model and we can format things
/// appropriately.
///
/// The type returned implements the `std::fmt::Display` trait. In most
/// cases, just converting it to a string (or printing it) will do what
/// you want.
@@ -669,10 +714,11 @@ impl SubDiagnostic {
.primary_annotation()
.and_then(|ann| ann.get_message())
.unwrap_or_default();
if annotation.is_empty() {
ConciseMessage::MainDiagnostic(main)
} else {
ConciseMessage::Both { main, annotation }
match (main.is_empty(), annotation.is_empty()) {
(false, true) => ConciseMessage::MainDiagnostic(main),
(true, false) => ConciseMessage::PrimaryAnnotation(annotation),
(false, false) => ConciseMessage::Both { main, annotation },
(true, true) => ConciseMessage::Empty,
}
}
}
@@ -842,10 +888,6 @@ impl Annotation {
pub fn hide_snippet(&mut self, yes: bool) {
self.hide_snippet = yes;
}
pub fn is_primary(&self) -> bool {
self.is_primary
}
}
/// Tags that can be associated with an annotation.
@@ -1466,10 +1508,28 @@ pub enum DiagnosticFormat {
pub enum ConciseMessage<'a> {
/// A diagnostic contains a non-empty main message and an empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "new" data model.
MainDiagnostic(&'a str),
/// A diagnostic contains an empty main message and a non-empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "old" data model.
PrimaryAnnotation(&'a str),
/// A diagnostic contains a non-empty main message and a non-empty
/// primary annotation message.
///
/// This strongly suggests that the diagnostic is using the
/// "new" data model.
Both { main: &'a str, annotation: &'a str },
/// A diagnostic contains an empty main message and an empty
/// primary annotation message.
///
/// This indicates that the diagnostic is probably using the old
/// model.
Empty,
/// A custom concise message has been provided.
Custom(&'a str),
}
@@ -1480,9 +1540,13 @@ impl std::fmt::Display for ConciseMessage<'_> {
ConciseMessage::MainDiagnostic(main) => {
write!(f, "{main}")
}
ConciseMessage::PrimaryAnnotation(annotation) => {
write!(f, "{annotation}")
}
ConciseMessage::Both { main, annotation } => {
write!(f, "{main}: {annotation}")
}
ConciseMessage::Empty => Ok(()),
ConciseMessage::Custom(message) => {
write!(f, "{message}")
}

View File

@@ -199,9 +199,6 @@ def bytes_okay(value=bytes(1)):
def int_okay(value=int("12")):
pass
# Allow immutable slice()
def slice_okay(value=slice(1,2)):
pass
# Allow immutable complex() value
def complex_okay(value=complex(1,2)):

View File

@@ -218,26 +218,3 @@ def should_not_fail(payload, Args):
Args:
The other arguments.
"""
# Test cases for Unpack[TypedDict] kwargs
from typing import TypedDict
from typing_extensions import Unpack
class User(TypedDict):
id: int
name: str
def function_with_unpack_args_should_not_fail(query: str, **kwargs: Unpack[User]):
"""Function with Unpack kwargs.
Args:
query: some arg
"""
def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
"""Function with Unpack kwargs but missing query arg documentation.
Args:
**kwargs: keyword arguments
"""

View File

@@ -1,56 +0,0 @@
def f():
# These should both be ignored by the range suppression.
# ruff: disable[E741, F841]
I = 1
# ruff: enable[E741, F841]
def f():
# These should both be ignored by the implicit range suppression.
# Should also generate an "unmatched suppression" warning.
# ruff:disable[E741,F841]
I = 1
def f():
# Neither warning is ignored, and an "unmatched suppression"
# should be generated.
I = 1
# ruff: enable[E741, F841]
def f():
# One should be ignored by the range suppression, and
# the other logged to the user.
# ruff: disable[E741]
I = 1
# ruff: enable[E741]
def f():
# Test interleaved range suppressions. The first and last
# lines should each log a different warning, while the
# middle line should be completely silenced.
# ruff: disable[E741]
l = 0
# ruff: disable[F841]
O = 1
# ruff: enable[E741]
I = 2
# ruff: enable[F841]
def f():
# Neither of these are ignored and warnings are
# logged to user
# ruff: disable[E501]
I = 1
# ruff: enable[E501]
def f():
# These should both be ignored by the range suppression,
# and an unusued noqa diagnostic should be logged.
# ruff:disable[E741,F841]
I = 1 # noqa: E741,F841
# ruff:enable[E741,F841]

View File

@@ -12,20 +12,17 @@ use crate::fix::edits::delete_comment;
use crate::noqa::{
Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping,
};
use crate::preview::is_range_suppressions_enabled;
use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target;
use crate::rules::pygrep_hooks;
use crate::rules::ruff;
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA};
use crate::settings::LinterSettings;
use crate::suppression::Suppressions;
use crate::{Edit, Fix, Locator};
use super::ast::LintContext;
/// RUF100
#[expect(clippy::too_many_arguments)]
pub(crate) fn check_noqa(
context: &mut LintContext,
path: &Path,
@@ -34,7 +31,6 @@ pub(crate) fn check_noqa(
noqa_line_for: &NoqaMapping,
analyze_directives: bool,
settings: &LinterSettings,
suppressions: &Suppressions,
) -> Vec<usize> {
// Identify any codes that are globally exempted (within the current file).
let file_noqa_directives =
@@ -44,7 +40,7 @@ pub(crate) fn check_noqa(
let mut noqa_directives =
NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator);
if file_noqa_directives.is_empty() && noqa_directives.is_empty() && suppressions.is_empty() {
if file_noqa_directives.is_empty() && noqa_directives.is_empty() {
return Vec::new();
}
@@ -64,19 +60,11 @@ pub(crate) fn check_noqa(
continue;
}
// Apply file-level suppressions first
if exemption.contains_secondary_code(code) {
ignored_diagnostics.push(index);
continue;
}
// Apply ranged suppressions next
if is_range_suppressions_enabled(settings) && suppressions.check_diagnostic(diagnostic) {
ignored_diagnostics.push(index);
continue;
}
// Apply end-of-line noqa suppressions last
let noqa_offsets = diagnostic
.parent()
.into_iter()

View File

@@ -32,7 +32,6 @@ use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule};
use crate::settings::types::UnsafeFixes;
use crate::settings::{LinterSettings, TargetVersion, flags};
use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::{Locator, directives, fs};
pub(crate) mod float;
@@ -129,7 +128,6 @@ pub fn check_path(
source_type: PySourceType,
parsed: &Parsed<ModModule>,
target_version: TargetVersion,
suppressions: &Suppressions,
) -> Vec<Diagnostic> {
// Aggregate all diagnostics.
let mut context = LintContext::new(path, locator.contents(), settings);
@@ -341,7 +339,6 @@ pub fn check_path(
&directives.noqa_line_for,
parsed.has_valid_syntax(),
settings,
suppressions,
);
if noqa.is_enabled() {
for index in ignored.iter().rev() {
@@ -403,9 +400,6 @@ pub fn add_noqa_to_path(
&indexer,
);
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics, ignoring any existing `noqa` directives.
let diagnostics = check_path(
path,
@@ -420,7 +414,6 @@ pub fn add_noqa_to_path(
source_type,
&parsed,
target_version,
&suppressions,
);
// Add any missing `# noqa` pragmas.
@@ -434,7 +427,6 @@ pub fn add_noqa_to_path(
&directives.noqa_line_for,
stylist.line_ending(),
reason,
&suppressions,
)
}
@@ -469,9 +461,6 @@ pub fn lint_only(
&indexer,
);
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics.
let diagnostics = check_path(
path,
@@ -486,7 +475,6 @@ pub fn lint_only(
source_type,
&parsed,
target_version,
&suppressions,
);
LinterResult {
@@ -578,9 +566,6 @@ pub fn lint_fix<'a>(
&indexer,
);
// Parse range suppression comments
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
// Generate diagnostics.
let diagnostics = check_path(
path,
@@ -595,7 +580,6 @@ pub fn lint_fix<'a>(
source_type,
&parsed,
target_version,
&suppressions,
);
if iterations == 0 {
@@ -785,7 +769,6 @@ mod tests {
use crate::registry::Rule;
use crate::settings::LinterSettings;
use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::test::{TestedNotebook, assert_notebook_path, test_contents, test_snippet};
use crate::{Locator, assert_diagnostics, directives, settings};
@@ -961,7 +944,6 @@ mod tests {
&locator,
&indexer,
);
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
let mut diagnostics = check_path(
path,
None,
@@ -975,7 +957,6 @@ mod tests {
source_type,
&parsed,
target_version,
&suppressions,
);
diagnostics.sort_by(Diagnostic::ruff_start_ordering);
diagnostics

View File

@@ -20,14 +20,12 @@ use crate::Locator;
use crate::fs::relativize_path;
use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target;
use crate::suppression::Suppressions;
/// Generates an array of edits that matches the length of `messages`.
/// Each potential edit in the array is paired, in order, with the associated diagnostic.
/// Each edit will add a `noqa` comment to the appropriate line in the source to hide
/// the diagnostic. These edits may conflict with each other and should not be applied
/// simultaneously.
#[expect(clippy::too_many_arguments)]
pub fn generate_noqa_edits(
path: &Path,
diagnostics: &[Diagnostic],
@@ -36,19 +34,11 @@ pub fn generate_noqa_edits(
external: &[String],
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
suppressions: &Suppressions,
) -> Vec<Option<Edit>> {
let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path);
let exemption = FileExemption::from(&file_directives);
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
let comments = find_noqa_comments(
diagnostics,
locator,
&exemption,
&directives,
noqa_line_for,
suppressions,
);
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
build_noqa_edits_by_diagnostic(comments, locator, line_ending, None)
}
@@ -735,7 +725,6 @@ pub(crate) fn add_noqa(
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
reason: Option<&str>,
suppressions: &Suppressions,
) -> Result<usize> {
let (count, output) = add_noqa_inner(
path,
@@ -746,7 +735,6 @@ pub(crate) fn add_noqa(
noqa_line_for,
line_ending,
reason,
suppressions,
);
fs::write(path, output)?;
@@ -763,7 +751,6 @@ fn add_noqa_inner(
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
reason: Option<&str>,
suppressions: &Suppressions,
) -> (usize, String) {
let mut count = 0;
@@ -773,14 +760,7 @@ fn add_noqa_inner(
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
let comments = find_noqa_comments(
diagnostics,
locator,
&exemption,
&directives,
noqa_line_for,
suppressions,
);
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
let edits = build_noqa_edits_by_line(comments, locator, line_ending, reason);
@@ -879,7 +859,6 @@ fn find_noqa_comments<'a>(
exemption: &'a FileExemption,
directives: &'a NoqaDirectives,
noqa_line_for: &NoqaMapping,
suppressions: &Suppressions,
) -> Vec<Option<NoqaComment<'a>>> {
// List of noqa comments, ordered to match up with `messages`
let mut comments_by_line: Vec<Option<NoqaComment<'a>>> = vec![];
@@ -896,12 +875,6 @@ fn find_noqa_comments<'a>(
continue;
}
// Apply ranged suppressions next
if suppressions.check_diagnostic(message) {
comments_by_line.push(None);
continue;
}
// Is the violation ignored by a `noqa` directive on the parent line?
if let Some(parent) = message.parent() {
if let Some(directive_line) =
@@ -1280,7 +1253,6 @@ mod tests {
use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon};
use crate::rules::pyflakes::rules::UnusedVariable;
use crate::rules::pyupgrade::rules::PrintfStringFormatting;
use crate::suppression::Suppressions;
use crate::{Edit, Violation};
use crate::{Locator, generate_noqa_edits};
@@ -2876,7 +2848,6 @@ mod tests {
&noqa_line_for,
LineEnding::Lf,
None,
&Suppressions::default(),
);
assert_eq!(count, 0);
assert_eq!(output, format!("{contents}"));
@@ -2901,7 +2872,6 @@ mod tests {
&noqa_line_for,
LineEnding::Lf,
None,
&Suppressions::default(),
);
assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: F841\n");
@@ -2933,7 +2903,6 @@ mod tests {
&noqa_line_for,
LineEnding::Lf,
None,
&Suppressions::default(),
);
assert_eq!(count, 1);
assert_eq!(output, "x = 1 # noqa: E741, F841\n");
@@ -2965,7 +2934,6 @@ mod tests {
&noqa_line_for,
LineEnding::Lf,
None,
&Suppressions::default(),
);
assert_eq!(count, 0);
assert_eq!(output, "x = 1 # noqa");
@@ -2988,7 +2956,6 @@ print(
let messages = [PrintfStringFormatting
.into_diagnostic(TextRange::new(12.into(), 79.into()), &source_file)];
let comment_ranges = CommentRanges::default();
let suppressions = Suppressions::default();
let edits = generate_noqa_edits(
path,
&messages,
@@ -2997,7 +2964,6 @@ print(
&[],
&noqa_line_for,
LineEnding::Lf,
&suppressions,
);
assert_eq!(
edits,
@@ -3021,7 +2987,6 @@ bar =
[UselessSemicolon.into_diagnostic(TextRange::new(4.into(), 5.into()), &source_file)];
let noqa_line_for = NoqaMapping::default();
let comment_ranges = CommentRanges::default();
let suppressions = Suppressions::default();
let edits = generate_noqa_edits(
path,
&messages,
@@ -3030,7 +2995,6 @@ bar =
&[],
&noqa_line_for,
LineEnding::Lf,
&suppressions,
);
assert_eq!(
edits,

View File

@@ -286,8 +286,3 @@ pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled(
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21623
pub(crate) const fn is_range_suppressions_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -236,227 +236,227 @@ help: Replace with `None`; initialize within function
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:242:20
--> B006_B008.py:239:20
|
240 | # B006 and B008
241 | # We should handle arbitrary nesting of these B008.
242 | def nested_combo(a=[float(3), dt.datetime.now()]):
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
243 | pass
240 | pass
|
help: Replace with `None`; initialize within function
239 |
240 | # B006 and B008
241 | # We should handle arbitrary nesting of these B008.
236 |
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
- def nested_combo(a=[float(3), dt.datetime.now()]):
242 + def nested_combo(a=None):
243 | pass
244 |
245 |
239 + def nested_combo(a=None):
240 | pass
241 |
242 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:279:27
--> B006_B008.py:276:27
|
278 | def mutable_annotations(
279 | a: list[int] | None = [],
275 | def mutable_annotations(
276 | a: list[int] | None = [],
| ^^
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
help: Replace with `None`; initialize within function
276 |
277 |
278 | def mutable_annotations(
273 |
274 |
275 | def mutable_annotations(
- a: list[int] | None = [],
279 + a: list[int] | None = None,
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 + a: list[int] | None = None,
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:280:35
--> B006_B008.py:277:35
|
278 | def mutable_annotations(
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
| ^^
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
help: Replace with `None`; initialize within function
277 |
278 | def mutable_annotations(
279 | a: list[int] | None = [],
274 |
275 | def mutable_annotations(
276 | a: list[int] | None = [],
- b: Optional[Dict[int, int]] = {},
280 + b: Optional[Dict[int, int]] = None,
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
277 + b: Optional[Dict[int, int]] = None,
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:281:62
--> B006_B008.py:278:62
|
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
|
help: Replace with `None`; initialize within function
278 | def mutable_annotations(
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
281 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
284 | pass
278 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
281 | pass
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:282:80
--> B006_B008.py:279:80
|
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^
283 | ):
284 | pass
280 | ):
281 | pass
|
help: Replace with `None`; initialize within function
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
283 | ):
284 | pass
285 |
279 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
280 | ):
281 | pass
282 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:287:52
--> B006_B008.py:284:52
|
287 | def single_line_func_wrong(value: dict[str, str] = {}):
284 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
288 | """Docstring"""
285 | """Docstring"""
|
help: Replace with `None`; initialize within function
284 | pass
285 |
281 | pass
282 |
283 |
- def single_line_func_wrong(value: dict[str, str] = {}):
284 + def single_line_func_wrong(value: dict[str, str] = None):
285 | """Docstring"""
286 |
- def single_line_func_wrong(value: dict[str, str] = {}):
287 + def single_line_func_wrong(value: dict[str, str] = None):
288 | """Docstring"""
289 |
290 |
287 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:291:52
--> B006_B008.py:288:52
|
291 | def single_line_func_wrong(value: dict[str, str] = {}):
288 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
292 | """Docstring"""
293 | ...
289 | """Docstring"""
290 | ...
|
help: Replace with `None`; initialize within function
288 | """Docstring"""
289 |
290 |
285 | """Docstring"""
286 |
287 |
- def single_line_func_wrong(value: dict[str, str] = {}):
291 + def single_line_func_wrong(value: dict[str, str] = None):
292 | """Docstring"""
293 | ...
294 |
288 + def single_line_func_wrong(value: dict[str, str] = None):
289 | """Docstring"""
290 | ...
291 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:296:52
--> B006_B008.py:293:52
|
296 | def single_line_func_wrong(value: dict[str, str] = {}):
293 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
297 | """Docstring"""; ...
294 | """Docstring"""; ...
|
help: Replace with `None`; initialize within function
293 | ...
294 |
290 | ...
291 |
292 |
- def single_line_func_wrong(value: dict[str, str] = {}):
293 + def single_line_func_wrong(value: dict[str, str] = None):
294 | """Docstring"""; ...
295 |
- def single_line_func_wrong(value: dict[str, str] = {}):
296 + def single_line_func_wrong(value: dict[str, str] = None):
297 | """Docstring"""; ...
298 |
299 |
296 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:300:52
--> B006_B008.py:297:52
|
300 | def single_line_func_wrong(value: dict[str, str] = {}):
297 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
301 | """Docstring"""; \
302 | ...
298 | """Docstring"""; \
299 | ...
|
help: Replace with `None`; initialize within function
297 | """Docstring"""; ...
298 |
299 |
294 | """Docstring"""; ...
295 |
296 |
- def single_line_func_wrong(value: dict[str, str] = {}):
300 + def single_line_func_wrong(value: dict[str, str] = None):
301 | """Docstring"""; \
302 | ...
303 |
297 + def single_line_func_wrong(value: dict[str, str] = None):
298 | """Docstring"""; \
299 | ...
300 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:305:52
--> B006_B008.py:302:52
|
305 | def single_line_func_wrong(value: dict[str, str] = {
302 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^
306 | | # This is a comment
307 | | }):
303 | | # This is a comment
304 | | }):
| |_^
308 | """Docstring"""
305 | """Docstring"""
|
help: Replace with `None`; initialize within function
302 | ...
303 |
304 |
299 | ...
300 |
301 |
- def single_line_func_wrong(value: dict[str, str] = {
- # This is a comment
- }):
305 + def single_line_func_wrong(value: dict[str, str] = None):
306 | """Docstring"""
307 |
308 |
302 + def single_line_func_wrong(value: dict[str, str] = None):
303 | """Docstring"""
304 |
305 |
note: This is an unsafe fix and may change runtime behavior
B006 Do not use mutable data structures for argument defaults
--> B006_B008.py:311:52
--> B006_B008.py:308:52
|
311 | def single_line_func_wrong(value: dict[str, str] = {}) \
308 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^
312 | : \
313 | """Docstring"""
309 | : \
310 | """Docstring"""
|
help: Replace with `None`; initialize within function
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:316:52
--> B006_B008.py:313:52
|
316 | def single_line_func_wrong(value: dict[str, str] = {}):
313 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
317 | """Docstring without newline"""
314 | """Docstring without newline"""
|
help: Replace with `None`; initialize within function
313 | """Docstring"""
314 |
315 |
310 | """Docstring"""
311 |
312 |
- def single_line_func_wrong(value: dict[str, str] = {}):
316 + def single_line_func_wrong(value: dict[str, str] = None):
317 | """Docstring without newline"""
313 + def single_line_func_wrong(value: dict[str, str] = None):
314 | """Docstring without newline"""
note: This is an unsafe fix and may change runtime behavior

View File

@@ -53,39 +53,39 @@ B008 Do not perform function call in argument defaults; instead, perform the cal
|
B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:242:31
--> B006_B008.py:239:31
|
240 | # B006 and B008
241 | # We should handle arbitrary nesting of these B008.
242 | def nested_combo(a=[float(3), dt.datetime.now()]):
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^
243 | pass
240 | pass
|
B008 Do not perform function call `map` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:248:22
--> B006_B008.py:245:22
|
246 | # Don't flag nested B006 since we can't guarantee that
247 | # it isn't made mutable by the outer operation.
248 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
243 | # Don't flag nested B006 since we can't guarantee that
244 | # it isn't made mutable by the outer operation.
245 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
249 | pass
246 | pass
|
B008 Do not perform function call `random.randint` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:253:19
--> B006_B008.py:250:19
|
252 | # B008-ception.
253 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
254 | pass
251 | pass
|
B008 Do not perform function call `dt.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
--> B006_B008.py:253:37
--> B006_B008.py:250:37
|
252 | # B008-ception.
253 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^
254 | pass
251 | pass
|

View File

@@ -236,227 +236,227 @@ help: Replace with `None`; initialize within function
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:242:20
--> B006_B008.py:239:20
|
240 | # B006 and B008
241 | # We should handle arbitrary nesting of these B008.
242 | def nested_combo(a=[float(3), dt.datetime.now()]):
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
243 | pass
240 | pass
|
help: Replace with `None`; initialize within function
239 |
240 | # B006 and B008
241 | # We should handle arbitrary nesting of these B008.
236 |
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
- def nested_combo(a=[float(3), dt.datetime.now()]):
242 + def nested_combo(a=None):
243 | pass
244 |
245 |
239 + def nested_combo(a=None):
240 | pass
241 |
242 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:279:27
--> B006_B008.py:276:27
|
278 | def mutable_annotations(
279 | a: list[int] | None = [],
275 | def mutable_annotations(
276 | a: list[int] | None = [],
| ^^
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
help: Replace with `None`; initialize within function
276 |
277 |
278 | def mutable_annotations(
273 |
274 |
275 | def mutable_annotations(
- a: list[int] | None = [],
279 + a: list[int] | None = None,
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 + a: list[int] | None = None,
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:280:35
--> B006_B008.py:277:35
|
278 | def mutable_annotations(
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
| ^^
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
help: Replace with `None`; initialize within function
277 |
278 | def mutable_annotations(
279 | a: list[int] | None = [],
274 |
275 | def mutable_annotations(
276 | a: list[int] | None = [],
- b: Optional[Dict[int, int]] = {},
280 + b: Optional[Dict[int, int]] = None,
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
277 + b: Optional[Dict[int, int]] = None,
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:281:62
--> B006_B008.py:278:62
|
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
|
help: Replace with `None`; initialize within function
278 | def mutable_annotations(
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
281 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
283 | ):
284 | pass
278 + c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
281 | pass
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:282:80
--> B006_B008.py:279:80
|
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^
283 | ):
284 | pass
280 | ):
281 | pass
|
help: Replace with `None`; initialize within function
279 | a: list[int] | None = [],
280 | b: Optional[Dict[int, int]] = {},
281 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
282 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
283 | ):
284 | pass
285 |
279 + d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
280 | ):
281 | pass
282 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:287:52
--> B006_B008.py:284:52
|
287 | def single_line_func_wrong(value: dict[str, str] = {}):
284 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
288 | """Docstring"""
285 | """Docstring"""
|
help: Replace with `None`; initialize within function
284 | pass
285 |
281 | pass
282 |
283 |
- def single_line_func_wrong(value: dict[str, str] = {}):
284 + def single_line_func_wrong(value: dict[str, str] = None):
285 | """Docstring"""
286 |
- def single_line_func_wrong(value: dict[str, str] = {}):
287 + def single_line_func_wrong(value: dict[str, str] = None):
288 | """Docstring"""
289 |
290 |
287 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:291:52
--> B006_B008.py:288:52
|
291 | def single_line_func_wrong(value: dict[str, str] = {}):
288 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
292 | """Docstring"""
293 | ...
289 | """Docstring"""
290 | ...
|
help: Replace with `None`; initialize within function
288 | """Docstring"""
289 |
290 |
285 | """Docstring"""
286 |
287 |
- def single_line_func_wrong(value: dict[str, str] = {}):
291 + def single_line_func_wrong(value: dict[str, str] = None):
292 | """Docstring"""
293 | ...
294 |
288 + def single_line_func_wrong(value: dict[str, str] = None):
289 | """Docstring"""
290 | ...
291 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:296:52
--> B006_B008.py:293:52
|
296 | def single_line_func_wrong(value: dict[str, str] = {}):
293 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
297 | """Docstring"""; ...
294 | """Docstring"""; ...
|
help: Replace with `None`; initialize within function
293 | ...
294 |
290 | ...
291 |
292 |
- def single_line_func_wrong(value: dict[str, str] = {}):
293 + def single_line_func_wrong(value: dict[str, str] = None):
294 | """Docstring"""; ...
295 |
- def single_line_func_wrong(value: dict[str, str] = {}):
296 + def single_line_func_wrong(value: dict[str, str] = None):
297 | """Docstring"""; ...
298 |
299 |
296 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:300:52
--> B006_B008.py:297:52
|
300 | def single_line_func_wrong(value: dict[str, str] = {}):
297 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
301 | """Docstring"""; \
302 | ...
298 | """Docstring"""; \
299 | ...
|
help: Replace with `None`; initialize within function
297 | """Docstring"""; ...
298 |
299 |
294 | """Docstring"""; ...
295 |
296 |
- def single_line_func_wrong(value: dict[str, str] = {}):
300 + def single_line_func_wrong(value: dict[str, str] = None):
301 | """Docstring"""; \
302 | ...
303 |
297 + def single_line_func_wrong(value: dict[str, str] = None):
298 | """Docstring"""; \
299 | ...
300 |
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:305:52
--> B006_B008.py:302:52
|
305 | def single_line_func_wrong(value: dict[str, str] = {
302 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^
306 | | # This is a comment
307 | | }):
303 | | # This is a comment
304 | | }):
| |_^
308 | """Docstring"""
305 | """Docstring"""
|
help: Replace with `None`; initialize within function
302 | ...
303 |
304 |
299 | ...
300 |
301 |
- def single_line_func_wrong(value: dict[str, str] = {
- # This is a comment
- }):
305 + def single_line_func_wrong(value: dict[str, str] = None):
306 | """Docstring"""
307 |
308 |
302 + def single_line_func_wrong(value: dict[str, str] = None):
303 | """Docstring"""
304 |
305 |
note: This is an unsafe fix and may change runtime behavior
B006 Do not use mutable data structures for argument defaults
--> B006_B008.py:311:52
--> B006_B008.py:308:52
|
311 | def single_line_func_wrong(value: dict[str, str] = {}) \
308 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^
312 | : \
313 | """Docstring"""
309 | : \
310 | """Docstring"""
|
help: Replace with `None`; initialize within function
B006 [*] Do not use mutable data structures for argument defaults
--> B006_B008.py:316:52
--> B006_B008.py:313:52
|
316 | def single_line_func_wrong(value: dict[str, str] = {}):
313 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^
317 | """Docstring without newline"""
314 | """Docstring without newline"""
|
help: Replace with `None`; initialize within function
313 | """Docstring"""
314 |
315 |
310 | """Docstring"""
311 |
312 |
- def single_line_func_wrong(value: dict[str, str] = {}):
316 + def single_line_func_wrong(value: dict[str, str] = None):
317 | """Docstring without newline"""
313 + def single_line_func_wrong(value: dict[str, str] = None):
314 | """Docstring without newline"""
note: This is an unsafe fix and may change runtime behavior

View File

@@ -4,9 +4,7 @@ use rustc_hash::FxHashSet;
use std::sync::LazyLock;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::Parameter;
use ruff_python_ast::docstrings::{clean_space, leading_space};
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::identifier::Identifier;
use ruff_python_semantic::analyze::visibility::is_staticmethod;
use ruff_python_trivia::textwrap::dedent;
@@ -1186,9 +1184,6 @@ impl AlwaysFixableViolation for MissingSectionNameColon {
/// This rule is enabled when using the `google` convention, and disabled when
/// using the `pep257` and `numpy` conventions.
///
/// Parameters annotated with `typing.Unpack` are exempt from this rule.
/// This follows the Python typing specification for unpacking keyword arguments.
///
/// ## Example
/// ```python
/// def calculate_speed(distance: float, time: float) -> float:
@@ -1238,7 +1233,6 @@ impl AlwaysFixableViolation for MissingSectionNameColon {
/// - [PEP 257 Docstring Conventions](https://peps.python.org/pep-0257/)
/// - [PEP 287 reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
/// - [Python - Unpack for keyword arguments](https://typing.python.org/en/latest/spec/callables.html#unpack-kwargs)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.73")]
pub(crate) struct UndocumentedParam {
@@ -1814,9 +1808,7 @@ fn missing_args(checker: &Checker, docstring: &Docstring, docstrings_args: &FxHa
missing_arg_names.insert(starred_arg_name);
}
}
if let Some(arg) = function.parameters.kwarg.as_ref()
&& !has_unpack_annotation(checker, arg)
{
if let Some(arg) = function.parameters.kwarg.as_ref() {
let arg_name = arg.name.as_str();
let starred_arg_name = format!("**{arg_name}");
if !arg_name.starts_with('_')
@@ -1842,15 +1834,6 @@ fn missing_args(checker: &Checker, docstring: &Docstring, docstrings_args: &FxHa
}
}
/// Returns `true` if the parameter is annotated with `typing.Unpack`
fn has_unpack_annotation(checker: &Checker, parameter: &Parameter) -> bool {
parameter.annotation.as_ref().is_some_and(|annotation| {
checker
.semantic()
.match_typing_expr(map_subscript(annotation), "Unpack")
})
}
// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`.
static GOOGLE_ARGS_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap());

View File

@@ -101,13 +101,3 @@ D417 Missing argument description in the docstring for `should_fail`: `Args`
200 | """
201 | Send a message.
|
D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query`
--> D417.py:238:5
|
236 | """
237 |
238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
239 | """Function with Unpack kwargs but missing query arg documentation.
|

View File

@@ -83,13 +83,3 @@ D417 Missing argument description in the docstring for `should_fail`: `Args`
200 | """
201 | Send a message.
|
D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query`
--> D417.py:238:5
|
236 | """
237 |
238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
239 | """Function with Unpack kwargs but missing query arg documentation.
|

View File

@@ -101,13 +101,3 @@ D417 Missing argument description in the docstring for `should_fail`: `Args`
200 | """
201 | Send a message.
|
D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query`
--> D417.py:238:5
|
236 | """
237 |
238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
239 | """Function with Unpack kwargs but missing query arg documentation.
|

View File

@@ -101,13 +101,3 @@ D417 Missing argument description in the docstring for `should_fail`: `Args`
200 | """
201 | Send a message.
|
D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query`
--> D417.py:238:5
|
236 | """
237 |
238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
239 | """Function with Unpack kwargs but missing query arg documentation.
|

View File

@@ -28,7 +28,6 @@ mod tests {
use crate::settings::types::PreviewMode;
use crate::settings::{LinterSettings, flags};
use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::test::{test_contents, test_path, test_snippet};
use crate::{Locator, assert_diagnostics, assert_diagnostics_diff, directives};
@@ -956,8 +955,6 @@ mod tests {
&locator,
&indexer,
);
let suppressions =
Suppressions::from_tokens(&settings, locator.contents(), parsed.tokens());
let mut messages = check_path(
Path::new("<filename>"),
None,
@@ -971,7 +968,6 @@ mod tests {
source_type,
&parsed,
target_version,
&suppressions,
);
messages.sort_by(Diagnostic::ruff_start_ordering);
let actual = messages

View File

@@ -305,25 +305,6 @@ mod tests {
Ok(())
}
#[test]
fn range_suppressions() -> Result<()> {
assert_diagnostics_diff!(
Path::new("ruff/suppressions.py"),
&settings::LinterSettings::for_rules(vec![
Rule::UnusedVariable,
Rule::AmbiguousVariableName,
Rule::UnusedNOQA,
]),
&settings::LinterSettings::for_rules(vec![
Rule::UnusedVariable,
Rule::AmbiguousVariableName,
Rule::UnusedNOQA,
])
.with_preview_mode(),
);
Ok(())
}
#[test]
fn ruf100_0() -> Result<()> {
let diagnostics = test_path(

View File

@@ -1,168 +0,0 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 9
Added: 1
--- Removed ---
E741 Ambiguous variable name: `I`
--> suppressions.py:4:5
|
2 | # These should both be ignored by the range suppression.
3 | # ruff: disable[E741, F841]
4 | I = 1
| ^
5 | # ruff: enable[E741, F841]
|
F841 [*] Local variable `I` is assigned to but never used
--> suppressions.py:4:5
|
2 | # These should both be ignored by the range suppression.
3 | # ruff: disable[E741, F841]
4 | I = 1
| ^
5 | # ruff: enable[E741, F841]
|
help: Remove assignment to unused variable `I`
1 | def f():
2 | # These should both be ignored by the range suppression.
3 | # ruff: disable[E741, F841]
- I = 1
4 + pass
5 | # ruff: enable[E741, F841]
6 |
7 |
note: This is an unsafe fix and may change runtime behavior
E741 Ambiguous variable name: `I`
--> suppressions.py:12:5
|
10 | # Should also generate an "unmatched suppression" warning.
11 | # ruff:disable[E741,F841]
12 | I = 1
| ^
|
F841 [*] Local variable `I` is assigned to but never used
--> suppressions.py:12:5
|
10 | # Should also generate an "unmatched suppression" warning.
11 | # ruff:disable[E741,F841]
12 | I = 1
| ^
|
help: Remove assignment to unused variable `I`
9 | # These should both be ignored by the implicit range suppression.
10 | # Should also generate an "unmatched suppression" warning.
11 | # ruff:disable[E741,F841]
- I = 1
12 + pass
13 |
14 |
15 | def f():
note: This is an unsafe fix and may change runtime behavior
E741 Ambiguous variable name: `I`
--> suppressions.py:26:5
|
24 | # the other logged to the user.
25 | # ruff: disable[E741]
26 | I = 1
| ^
27 | # ruff: enable[E741]
|
E741 Ambiguous variable name: `l`
--> suppressions.py:35:5
|
33 | # middle line should be completely silenced.
34 | # ruff: disable[E741]
35 | l = 0
| ^
36 | # ruff: disable[F841]
37 | O = 1
|
E741 Ambiguous variable name: `O`
--> suppressions.py:37:5
|
35 | l = 0
36 | # ruff: disable[F841]
37 | O = 1
| ^
38 | # ruff: enable[E741]
39 | I = 2
|
F841 [*] Local variable `O` is assigned to but never used
--> suppressions.py:37:5
|
35 | l = 0
36 | # ruff: disable[F841]
37 | O = 1
| ^
38 | # ruff: enable[E741]
39 | I = 2
|
help: Remove assignment to unused variable `O`
34 | # ruff: disable[E741]
35 | l = 0
36 | # ruff: disable[F841]
- O = 1
37 | # ruff: enable[E741]
38 | I = 2
39 | # ruff: enable[F841]
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `I` is assigned to but never used
--> suppressions.py:39:5
|
37 | O = 1
38 | # ruff: enable[E741]
39 | I = 2
| ^
40 | # ruff: enable[F841]
|
help: Remove assignment to unused variable `I`
36 | # ruff: disable[F841]
37 | O = 1
38 | # ruff: enable[E741]
- I = 2
39 | # ruff: enable[F841]
40 |
41 |
note: This is an unsafe fix and may change runtime behavior
--- Added ---
RUF100 [*] Unused `noqa` directive (unused: `E741`, `F841`)
--> suppressions.py:55:12
|
53 | # and an unusued noqa diagnostic should be logged.
54 | # ruff:disable[E741,F841]
55 | I = 1 # noqa: E741,F841
| ^^^^^^^^^^^^^^^^^
56 | # ruff:enable[E741,F841]
|
help: Remove unused `noqa` directive
52 | # These should both be ignored by the range suppression,
53 | # and an unusued noqa diagnostic should be logged.
54 | # ruff:disable[E741,F841]
- I = 1 # noqa: E741,F841
55 + I = 1
56 | # ruff:enable[E741,F841]

View File

@@ -465,12 +465,6 @@ impl LinterSettings {
self
}
#[must_use]
pub fn with_preview_mode(mut self) -> Self {
self.preview = PreviewMode::Enabled;
self
}
/// Resolve the [`TargetVersion`] to use for linting.
///
/// This method respects the per-file version overrides in

View File

@@ -1,6 +1,5 @@
use compact_str::CompactString;
use core::fmt;
use ruff_db::diagnostic::Diagnostic;
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::whitespace::indentation;
use std::{error::Error, fmt::Formatter};
@@ -10,9 +9,6 @@ use ruff_python_trivia::Cursor;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize, TextSlice};
use smallvec::{SmallVec, smallvec};
use crate::preview::is_range_suppressions_enabled;
use crate::settings::LinterSettings;
#[allow(unused)]
#[derive(Clone, Debug, Eq, PartialEq)]
enum SuppressionAction {
@@ -102,8 +98,8 @@ pub(crate) struct InvalidSuppression {
}
#[allow(unused)]
#[derive(Debug, Default)]
pub struct Suppressions {
#[derive(Debug)]
pub(crate) struct Suppressions {
/// Valid suppression ranges with associated comments
valid: Vec<Suppression>,
@@ -116,41 +112,9 @@ pub struct Suppressions {
#[allow(unused)]
impl Suppressions {
pub fn from_tokens(settings: &LinterSettings, source: &str, tokens: &Tokens) -> Suppressions {
if is_range_suppressions_enabled(settings) {
let builder = SuppressionsBuilder::new(source);
builder.load_from_tokens(tokens)
} else {
Suppressions::default()
}
}
pub(crate) fn is_empty(&self) -> bool {
self.valid.is_empty()
}
/// Check if a diagnostic is suppressed by any known range suppressions
pub(crate) fn check_diagnostic(&self, diagnostic: &Diagnostic) -> bool {
if self.valid.is_empty() {
return false;
}
let Some(code) = diagnostic.secondary_code() else {
return false;
};
let Some(span) = diagnostic.primary_span() else {
return false;
};
let Some(range) = span.range() else {
return false;
};
for suppression in &self.valid {
if *code == suppression.code.as_str() && suppression.range.contains_range(range) {
return true;
}
}
false
pub(crate) fn from_tokens(source: &str, tokens: &Tokens) -> Suppressions {
let builder = SuppressionsBuilder::new(source);
builder.load_from_tokens(tokens)
}
}
@@ -493,12 +457,9 @@ mod tests {
use ruff_text_size::{TextRange, TextSize};
use similar::DiffableStr;
use crate::{
settings::LinterSettings,
suppression::{
InvalidSuppression, ParseError, Suppression, SuppressionAction, SuppressionComment,
SuppressionParser, Suppressions,
},
use crate::suppression::{
InvalidSuppression, ParseError, Suppression, SuppressionAction, SuppressionComment,
SuppressionParser, Suppressions,
};
#[test]
@@ -1415,11 +1376,7 @@ def bar():
/// Parse all suppressions and errors in a module for testing
fn debug(source: &'_ str) -> DebugSuppressions<'_> {
let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap();
let suppressions = Suppressions::from_tokens(
&LinterSettings::default().with_preview_mode(),
source,
parsed.tokens(),
);
let suppressions = Suppressions::from_tokens(source, parsed.tokens());
DebugSuppressions {
source,
suppressions,

View File

@@ -32,7 +32,6 @@ use crate::packaging::detect_package_root;
use crate::settings::types::UnsafeFixes;
use crate::settings::{LinterSettings, flags};
use crate::source_kind::SourceKind;
use crate::suppression::Suppressions;
use crate::{Applicability, FixAvailability};
use crate::{Locator, directives};
@@ -235,7 +234,6 @@ pub(crate) fn test_contents<'a>(
&locator,
&indexer,
);
let suppressions = Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
let messages = check_path(
path,
path.parent()
@@ -251,7 +249,6 @@ pub(crate) fn test_contents<'a>(
source_type,
&parsed,
target_version,
&suppressions,
);
let source_has_errors = parsed.has_invalid_syntax();
@@ -302,8 +299,6 @@ pub(crate) fn test_contents<'a>(
&indexer,
);
let suppressions =
Suppressions::from_tokens(settings, locator.contents(), parsed.tokens());
let fixed_messages = check_path(
path,
None,
@@ -317,7 +312,6 @@ pub(crate) fn test_contents<'a>(
source_type,
&parsed,
target_version,
&suppressions,
);
if parsed.has_invalid_syntax() && !source_has_errors {

View File

@@ -326,15 +326,7 @@ pub fn is_immutable_return_type(qualified_name: &[&str]) -> bool {
| ["re", "compile"]
| [
"",
"bool"
| "bytes"
| "complex"
| "float"
| "frozenset"
| "int"
| "str"
| "tuple"
| "slice"
"bool" | "bytes" | "complex" | "float" | "frozenset" | "int" | "str" | "tuple"
]
)
}

View File

@@ -20,7 +20,6 @@ use ruff_linter::{
packaging::detect_package_root,
settings::flags,
source_kind::SourceKind,
suppression::Suppressions,
};
use ruff_notebook::Notebook;
use ruff_python_codegen::Stylist;
@@ -119,10 +118,6 @@ pub(crate) fn check(
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = extract_directives(parsed.tokens(), Flags::all(), &locator, &indexer);
// Parse range suppression comments
let suppressions =
Suppressions::from_tokens(&settings.linter, locator.contents(), parsed.tokens());
// Generate checks.
let diagnostics = check_path(
&document_path,
@@ -137,7 +132,6 @@ pub(crate) fn check(
source_type,
&parsed,
target_version,
&suppressions,
);
let noqa_edits = generate_noqa_edits(
@@ -148,7 +142,6 @@ pub(crate) fn check(
&settings.linter.external,
&directives.noqa_line_for,
stylist.line_ending(),
&suppressions,
);
let mut diagnostics_map = DiagnosticsMap::default();

View File

@@ -33,29 +33,26 @@ impl LineIndex {
line_starts.push(TextSize::default());
let bytes = text.as_bytes();
let mut utf8 = false;
assert!(u32::try_from(bytes.len()).is_ok());
for i in memchr::memchr2_iter(b'\n', b'\r', bytes) {
// Skip `\r` in `\r\n` sequences (only count the `\n`).
if bytes[i] == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
continue;
for (i, byte) in bytes.iter().enumerate() {
utf8 |= !byte.is_ascii();
match byte {
// Only track one line break for `\r\n`.
b'\r' if bytes.get(i + 1) == Some(&b'\n') => continue,
b'\n' | b'\r' => {
// SAFETY: Assertion above guarantees `i <= u32::MAX`
#[expect(clippy::cast_possible_truncation)]
line_starts.push(TextSize::from(i as u32) + TextSize::from(1));
}
_ => {}
}
// SAFETY: Assertion above guarantees `i <= u32::MAX`
#[expect(clippy::cast_possible_truncation)]
line_starts.push(TextSize::from(i as u32) + TextSize::from(1));
}
// Determine whether the source text is ASCII.
//
// Empirically, this simple loop is auto-vectorized by LLVM and benchmarks faster than both
// `str::is_ascii()` and hand-written SIMD.
let mut has_non_ascii = false;
for byte in bytes {
has_non_ascii |= !byte.is_ascii();
}
let kind = if has_non_ascii {
let kind = if utf8 {
IndexKind::Utf8
} else {
IndexKind::Ascii

View File

@@ -2,7 +2,6 @@ use std::path::Path;
use js_sys::Error;
use ruff_linter::settings::types::PythonVersion;
use ruff_linter::suppression::Suppressions;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
@@ -213,9 +212,6 @@ impl Workspace {
&indexer,
);
let suppressions =
Suppressions::from_tokens(&self.settings.linter, locator.contents(), parsed.tokens());
// Generate checks.
let diagnostics = check_path(
Path::new("<filename>"),
@@ -230,7 +226,6 @@ impl Workspace {
source_type,
&parsed,
target_version,
&suppressions,
);
let source_code = locator.to_source_code();

View File

@@ -43,7 +43,7 @@ fn config_override_python_version() -> anyhow::Result<()> {
|
2 | [tool.ty.environment]
3 | python-version = "3.11"
| ^^^^^^ Python version configuration
| ^^^^^^ Python 3.11 assumed due to this configuration setting
|
info: rule `unresolved-attribute` is enabled by default
@@ -143,7 +143,7 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -159,14 +159,14 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any
|
2 | [tool.ty.environment]
3 | python-version = "3.8"
| ^^^^^ Python version configuration
| ^^^^^ Python 3.8 assumed due to this configuration setting
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
"#);
"###);
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r###"
success: false
@@ -772,7 +772,7 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu
("test.py", "aiter"),
])?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -787,7 +787,7 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu
--> venv/pyvenv.cfg:2:11
|
2 | version = 3.8
| ^^^ Virtual environment metadata
| ^^^ Python version inferred from virtual environment metadata file
3 | home = foo/bar/bin
|
info: No Python version was specified on the command line or in a configuration file
@@ -796,7 +796,7 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu
Found 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -831,7 +831,7 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
("test.py", "aiter"),
])?;
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -846,7 +846,7 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
--> venv/pyvenv.cfg:4:23
|
4 | version = 3.8
| ^^^ Virtual environment metadata
| ^^^ Python version inferred from virtual environment metadata file
|
info: No Python version was specified on the command line or in a configuration file
info: rule `unresolved-reference` is enabled by default
@@ -854,7 +854,7 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
Found 1 diagnostic
----- stderr -----
");
"###);
Ok(())
}
@@ -898,7 +898,7 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any
|
2 | [project]
3 | requires-python = ">=3.8"
| ^^^^^^^ Python version configuration
| ^^^^^^^ Python 3.8 assumed due to this configuration setting
|
Found 1 diagnostic
@@ -1206,7 +1206,7 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
2 | [environment]
3 | python-version = "3.10"
| ^^^^^^ Python version configuration
| ^^^^^^ Python 3.10 assumed due to this configuration setting
4 | python-platform = "linux"
|
info: rule `unresolved-attribute` is enabled by default
@@ -1225,7 +1225,7 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
2 | [environment]
3 | python-version = "3.10"
| ^^^^^^ Python version configuration
| ^^^^^^ Python 3.10 assumed due to this configuration setting
4 | python-platform = "linux"
|
info: rule `unresolved-import` is enabled by default

View File

@@ -5,8 +5,7 @@ use ruff_diagnostics::Edit;
use ruff_text_size::TextRange;
use ty_project::Db;
use ty_python_semantic::create_suppression_fix;
use ty_python_semantic::lint::LintId;
use ty_python_semantic::types::{UNDEFINED_REVEAL, UNRESOLVED_REFERENCE};
use ty_python_semantic::types::UNRESOLVED_REFERENCE;
/// A `QuickFix` Code Action
#[derive(Debug, Clone)]
@@ -29,17 +28,12 @@ pub fn code_actions(
let mut actions = Vec::new();
// Suggest imports for unresolved references (often ideal)
// TODO: suggest qualifying with an already imported symbol
let is_unresolved_reference =
lint_id == LintId::of(&UNRESOLVED_REFERENCE) || lint_id == LintId::of(&UNDEFINED_REVEAL);
if is_unresolved_reference
if lint_id.name() == UNRESOLVED_REFERENCE.name()
&& let Some(import_quick_fix) = create_import_symbol_quick_fix(db, file, diagnostic_range)
{
actions.extend(import_quick_fix);
}
// Suggest just suppressing the lint (always a valid option, but never ideal)
actions.push(QuickFix {
title: format!("Ignore '{}' for this line", lint_id.name()),
edits: create_suppression_fix(db, file, lint_id, diagnostic_range).into_edits(),

View File

@@ -9,7 +9,6 @@ use ruff_python_ast::token::{Token, TokenAt, TokenKind, Tokens};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_codegen::Stylist;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use rustc_hash::FxHashSet;
use ty_python_semantic::types::UnionType;
use ty_python_semantic::{
Completion as SemanticCompletion, KnownModule, ModuleName, NameKind, SemanticModel,
@@ -21,7 +20,7 @@ use crate::find_node::covering_node;
use crate::goto::Definitions;
use crate::importer::{ImportRequest, Importer};
use crate::symbols::QueryPattern;
use crate::{Db, all_symbols, signature_help};
use crate::{Db, all_symbols};
/// A collection of completions built up from various sources.
#[derive(Clone)]
@@ -437,10 +436,6 @@ pub fn completion<'db>(
);
}
}
if let Some(arg_completions) = detect_function_arg_completions(db, file, &parsed, offset) {
completions.extend(arg_completions);
}
}
if is_raising_exception(tokens) {
@@ -456,89 +451,10 @@ pub fn completion<'db>(
!ty.is_notimplemented(db)
});
}
completions.into_completions()
}
/// Detect and construct completions for unset function arguments.
///
/// Suggestions are only provided if the cursor is currently inside a
/// function call and the function arguments have not 1) already been
/// set and 2) been defined as positional-only.
fn detect_function_arg_completions<'db>(
db: &'db dyn Db,
file: File,
parsed: &ParsedModuleRef,
offset: TextSize,
) -> Option<Vec<Completion<'db>>> {
let sig_help = signature_help(db, file, offset)?;
let set_function_args = detect_set_function_args(parsed, offset);
let completions = sig_help
.signatures
.iter()
.flat_map(|sig| &sig.parameters)
.filter(|p| !p.is_positional_only && !set_function_args.contains(&p.name.as_str()))
.map(|p| {
let name = Name::new(&p.name);
let documentation = p
.documentation
.as_ref()
.map(|d| Docstring::new(d.to_owned()));
let insert = Some(format!("{name}=").into_boxed_str());
Completion {
name,
qualified: None,
insert,
ty: p.ty,
kind: Some(CompletionKind::Variable),
module_name: None,
import: None,
builtin: false,
is_type_check_only: false,
is_definitively_raisable: false,
documentation,
}
})
.collect();
Some(completions)
}
/// Returns function arguments that have already been set.
///
/// If `offset` is inside an arguments node, this returns
/// the list of argument names that are already set.
///
/// For example, given:
///
/// ```python
/// def abc(foo, bar, baz): ...
/// abc(foo=1, bar=2, b<CURSOR>)
/// ```
///
/// the resulting value is `["foo", "bar"]`
///
/// This is useful to be able to exclude autocomplete suggestions
/// for arguments that have already been set to some value.
///
/// If the parent node is not an arguments node, the return value
/// is an empty Vec.
fn detect_set_function_args(parsed: &ParsedModuleRef, offset: TextSize) -> FxHashSet<&str> {
let range = TextRange::empty(offset);
covering_node(parsed.syntax().into(), range)
.parent()
.and_then(|node| match node {
ast::AnyNodeRef::Arguments(args) => Some(args),
_ => None,
})
.map(|args| {
args.keywords
.iter()
.filter_map(|kw| kw.arg.as_ref().map(|ident| ident.id.as_str()))
.collect()
})
.unwrap_or_default()
}
pub(crate) struct ImportEdit {
pub label: String,
pub edit: Edit,
@@ -2470,11 +2386,10 @@ def frob(): ...
",
);
// FIXME: Should include `foo`.
assert_snapshot!(
builder.skip_keywords().skip_builtins().build().snapshot(),
@r"
foo
",
@"<No completions found after filtering out completions>",
);
}
@@ -2486,11 +2401,10 @@ def frob(): ...
",
);
// FIXME: Should include `foo`.
assert_snapshot!(
builder.skip_keywords().skip_builtins().build().snapshot(),
@r"
foo
",
@"<No completions found after filtering out completions>",
);
}
@@ -3125,6 +3039,7 @@ quux.<CURSOR>
");
}
// We don't yet take function parameters into account.
#[test]
fn call_prefix1() {
let builder = completion_test_builder(
@@ -3137,157 +3052,7 @@ bar(o<CURSOR>
",
);
assert_snapshot!(
builder.skip_keywords().skip_builtins().build().snapshot(),
@r"
foo
okay
"
);
}
#[test]
fn call_keyword_only_argument() {
let builder = completion_test_builder(
"\
def bar(*, okay): ...
foo = 1
bar(o<CURSOR>
",
);
assert_snapshot!(
builder.skip_keywords().skip_builtins().build().snapshot(),
@r"
foo
okay
"
);
}
#[test]
fn call_multiple_keyword_arguments() {
let builder = completion_test_builder(
"\
def foo(bar, baz, barbaz): ...
foo(b<CURSOR>
",
);
assert_snapshot!(
builder.skip_keywords().skip_builtins().build().snapshot(),
@r"
bar
barbaz
baz
"
);
}
#[test]
fn call_multiple_keyword_arguments_some_set() {
let builder = completion_test_builder(
"\
def foo(bar, baz): ...
foo(bar=1, b<CURSOR>
",
);
assert_snapshot!(
builder.skip_keywords().skip_builtins().build().snapshot(),
@r"
baz
"
);
}
#[test]
fn call_arguments_multi_def() {
let builder = completion_test_builder(
"\
def abc(okay, x): ...
def bar(not_okay, y): ...
def baz(foobarbaz, z): ...
abc(o<CURSOR>
",
);
assert_snapshot!(
builder.skip_keywords().skip_builtins().build().snapshot(),
@r"
okay
"
);
}
#[test]
fn call_arguments_cursor_middle() {
let builder = completion_test_builder(
"\
def abc(okay, foo, bar, baz): ...
abc(okay=1, ba<CURSOR> baz=5
",
);
assert_snapshot!(
builder.skip_keywords().skip_builtins().build().snapshot(),
@r"
bar
"
);
}
#[test]
fn call_positional_only_argument() {
// If the parameter is positional only we don't
// want to suggest it as specifying by name
// is not valid.
let builder = completion_test_builder(
"\
def bar(okay, /): ...
foo = 1
bar(o<CURSOR>
",
);
assert_snapshot!(
builder.skip_keywords().skip_builtins().build().snapshot(),
@"foo"
);
}
#[test]
fn call_positional_only_keyword_only_argument_mix() {
// If the parameter is positional only we don't
// want to suggest it as specifying by name
// is not valid.
let builder = completion_test_builder(
"\
def bar(not_okay, no, /, okay, *, okay_abc, okay_okay): ...
foo = 1
bar(o<CURSOR>
",
);
assert_snapshot!(
builder.skip_keywords().skip_builtins().build().snapshot(),
@r"
foo
okay
okay_abc
okay_okay
"
);
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"foo");
}
#[test]
@@ -3305,7 +3070,6 @@ bar(<CURSOR>
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
bar
foo
okay
");
}

View File

@@ -19,22 +19,11 @@ pub struct InlayHint {
}
impl InlayHint {
fn variable_type(
expr: &Expr,
rhs: &Expr,
ty: Type,
db: &dyn Db,
allow_edits: bool,
) -> Option<Self> {
fn variable_type(expr: &Expr, ty: Type, db: &dyn Db, allow_edits: bool) -> Self {
let position = expr.range().end();
// Render the type to a string, and get subspans for all the types that make it up
let details = ty.display(db).to_string_parts();
// Filter out a reptitive hints like `x: T = T()`
if call_matches_name(rhs, &details.label) {
return None;
}
// Ok so the idea here is that we potentially have a random soup of spans here,
// and each byte of the string can have at most one target associate with it.
// Thankfully, they were generally pushed in print order, with the inner smaller types
@@ -84,12 +73,12 @@ impl InlayHint {
vec![]
};
Some(Self {
Self {
position,
kind: InlayHintKind::Type,
label: InlayHintLabel { parts: label_parts },
text_edits,
})
}
}
fn call_argument_name(
@@ -261,7 +250,7 @@ struct InlayHintVisitor<'a, 'db> {
db: &'db dyn Db,
model: SemanticModel<'db>,
hints: Vec<InlayHint>,
assignment_rhs: Option<&'a Expr>,
in_assignment: bool,
range: TextRange,
settings: &'a InlayHintSettings,
in_no_edits_allowed: bool,
@@ -273,21 +262,21 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
db,
model: SemanticModel::new(db, file),
hints: Vec::new(),
assignment_rhs: None,
in_assignment: false,
range,
settings,
in_no_edits_allowed: false,
}
}
fn add_type_hint(&mut self, expr: &Expr, rhs: &Expr, ty: Type<'db>, allow_edits: bool) {
fn add_type_hint(&mut self, expr: &Expr, ty: Type<'db>, allow_edits: bool) {
if !self.settings.variable_types {
return;
}
if let Some(inlay_hint) = InlayHint::variable_type(expr, rhs, ty, self.db, allow_edits) {
self.hints.push(inlay_hint);
}
let inlay_hint = InlayHint::variable_type(expr, ty, self.db, allow_edits);
self.hints.push(inlay_hint);
}
fn add_call_argument_name(
@@ -310,8 +299,8 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
}
}
impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal {
if self.range.intersect(node.range()).is_some() {
TraversalSignal::Traverse
} else {
@@ -319,7 +308,7 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
}
}
fn visit_stmt(&mut self, stmt: &'a Stmt) {
fn visit_stmt(&mut self, stmt: &Stmt) {
let node = AnyNodeRef::from(stmt);
if !self.enter_node(node).is_traverse() {
@@ -328,9 +317,7 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
match stmt {
Stmt::Assign(assign) => {
if !type_hint_is_excessive_for_expr(&assign.value) {
self.assignment_rhs = Some(&*assign.value);
}
self.in_assignment = !type_hint_is_excessive_for_expr(&assign.value);
if !annotations_are_valid_syntax(assign) {
self.in_no_edits_allowed = true;
}
@@ -338,7 +325,7 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
self.visit_expr(target);
}
self.in_no_edits_allowed = false;
self.assignment_rhs = None;
self.in_assignment = false;
self.visit_expr(&assign.value);
@@ -357,22 +344,22 @@ impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
source_order::walk_stmt(self, stmt);
}
fn visit_expr(&mut self, expr: &'a Expr) {
fn visit_expr(&mut self, expr: &'_ Expr) {
match expr {
Expr::Name(name) => {
if let Some(rhs) = self.assignment_rhs {
if self.in_assignment {
if name.ctx.is_store() {
let ty = expr.inferred_type(&self.model);
self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed);
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
}
}
source_order::walk_expr(self, expr);
}
Expr::Attribute(attribute) => {
if let Some(rhs) = self.assignment_rhs {
if self.in_assignment {
if attribute.ctx.is_store() {
let ty = expr.inferred_type(&self.model);
self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed);
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
}
}
source_order::walk_expr(self, expr);
@@ -429,26 +416,6 @@ fn arg_matches_name(arg_or_keyword: &ArgOrKeyword, name: &str) -> bool {
}
}
/// Given a function call, check if the expression is the "same name"
/// as the function being called.
///
/// This allows us to filter out reptitive inlay hints like `x: T = T(...)`.
/// While still allowing non-trivial ones like `x: T[U] = T()`.
fn call_matches_name(expr: &Expr, name: &str) -> bool {
// Only care about function calls
let Expr::Call(call) = expr else {
return false;
};
match &*call.func {
// `x: T = T()` is a match
Expr::Name(expr_name) => expr_name.id.as_str() == name,
// `x: T = a.T()` is a match
Expr::Attribute(expr_attribute) => expr_attribute.attr.as_str() == name,
_ => false,
}
}
/// Given an expression that's the RHS of an assignment, would it be excessive to
/// emit an inlay type hint for the variable assigned to it?
///
@@ -1862,16 +1829,35 @@ mod tests {
",
);
assert_snapshot!(test.inlay_hints(), @r"
assert_snapshot!(test.inlay_hints(), @r#"
class A:
def __init__(self, y):
self.x = int(1)
self.x[: int] = int(1)
self.y[: Unknown] = y
a = A([y=]2)
a.y = int(3)
a[: A] = A([y=]2)
a.y[: int] = int(3)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main2.py:4:18
|
2 | class A:
3 | def __init__(self, y):
4 | self.x[: int] = int(1)
| ^^^
5 | self.y[: Unknown] = y
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/ty_extensions.pyi:20:1
|
@@ -1885,11 +1871,29 @@ mod tests {
--> main2.py:5:18
|
3 | def __init__(self, y):
4 | self.x = int(1)
4 | self.x[: int] = int(1)
5 | self.y[: Unknown] = y
| ^^^^^^^
6 |
7 | a = A([y=]2)
7 | a[: A] = A([y=]2)
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:7
|
2 | class A:
| ^
3 | def __init__(self, y):
4 | self.x = int(1)
|
info: Source
--> main2.py:7:5
|
5 | self.y[: Unknown] = y
6 |
7 | a[: A] = A([y=]2)
| ^
8 | a.y[: int] = int(3)
|
info[inlay-hint-location]: Inlay Hint Target
@@ -1902,13 +1906,30 @@ mod tests {
5 | self.y = y
|
info: Source
--> main2.py:7:8
--> main2.py:7:13
|
5 | self.y[: Unknown] = y
6 |
7 | a = A([y=]2)
| ^
8 | a.y = int(3)
7 | a[: A] = A([y=]2)
| ^
8 | a.y[: int] = int(3)
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:348:7
|
347 | @disjoint_base
348 | class int:
| ^^^
349 | """int([x]) -> integer
350 | int(x, base=10) -> integer
|
info: Source
--> main2.py:8:7
|
7 | a[: A] = A([y=]2)
8 | a.y[: int] = int(3)
| ^^^
|
---------------------------------------------
@@ -1917,12 +1938,12 @@ mod tests {
class A:
def __init__(self, y):
self.x = int(1)
self.x: int = int(1)
self.y: Unknown = y
a = A(2)
a.y = int(3)
");
a: A = A(2)
a.y: int = int(3)
"#);
}
#[test]
@@ -2916,12 +2937,31 @@ mod tests {
def __init__(self):
self.x: int = 1
x = MyClass()
x[: MyClass] = MyClass()
y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
a[: MyClass], b[: MyClass] = MyClass(), MyClass()
c[: MyClass], d[: MyClass] = (MyClass(), MyClass())
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:7
|
2 | class MyClass:
| ^^^^^^^
3 | def __init__(self):
4 | self.x: int = 1
|
info: Source
--> main2.py:6:5
|
4 | self.x: int = 1
5 |
6 | x[: MyClass] = MyClass()
| ^^^^^^^
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
|
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:2695:7
|
@@ -2933,7 +2973,7 @@ mod tests {
info: Source
--> main2.py:7:5
|
6 | x = MyClass()
6 | x[: MyClass] = MyClass()
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
| ^^^^^
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
@@ -2951,7 +2991,7 @@ mod tests {
info: Source
--> main2.py:7:11
|
6 | x = MyClass()
6 | x[: MyClass] = MyClass()
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
| ^^^^^^^
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
@@ -2969,7 +3009,7 @@ mod tests {
info: Source
--> main2.py:7:20
|
6 | x = MyClass()
6 | x[: MyClass] = MyClass()
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
| ^^^^^^^
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
@@ -2987,7 +3027,7 @@ mod tests {
info: Source
--> main2.py:8:5
|
6 | x = MyClass()
6 | x[: MyClass] = MyClass()
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
| ^^^^^^^
@@ -3005,7 +3045,7 @@ mod tests {
info: Source
--> main2.py:8:19
|
6 | x = MyClass()
6 | x[: MyClass] = MyClass()
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
| ^^^^^^^
@@ -3054,7 +3094,7 @@ mod tests {
def __init__(self):
self.x: int = 1
x = MyClass()
x: MyClass = MyClass()
y: tuple[MyClass, MyClass] = (MyClass(), MyClass())
a, b = MyClass(), MyClass()
c, d = (MyClass(), MyClass())
@@ -4057,11 +4097,31 @@ mod tests {
def __init__(self):
self.x: int = 1
self.y: int = 2
val = MyClass()
val[: MyClass] = MyClass()
foo(val.x)
foo([x=]val.y)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:3:7
|
2 | def foo(x: int): pass
3 | class MyClass:
| ^^^^^^^
4 | def __init__(self):
5 | self.x: int = 1
|
info: Source
--> main2.py:7:7
|
5 | self.x: int = 1
6 | self.y: int = 2
7 | val[: MyClass] = MyClass()
| ^^^^^^^
8 |
9 | foo(val.x)
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:9
|
@@ -4077,6 +4137,20 @@ mod tests {
10 | foo([x=]val.y)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
self.x: int = 1
self.y: int = 2
val: MyClass = MyClass()
foo(val.x)
foo(val.y)
");
}
@@ -4102,11 +4176,31 @@ mod tests {
def __init__(self):
self.x: int = 1
self.y: int = 2
x = MyClass()
x[: MyClass] = MyClass()
foo(x.x)
foo([x=]x.y)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:3:7
|
2 | def foo(x: int): pass
3 | class MyClass:
| ^^^^^^^
4 | def __init__(self):
5 | self.x: int = 1
|
info: Source
--> main2.py:7:5
|
5 | self.x: int = 1
6 | self.y: int = 2
7 | x[: MyClass] = MyClass()
| ^^^^^^^
8 |
9 | foo(x.x)
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:9
|
@@ -4122,6 +4216,20 @@ mod tests {
10 | foo([x=]x.y)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
self.x: int = 1
self.y: int = 2
x: MyClass = MyClass()
foo(x.x)
foo(x.y)
");
}
@@ -4150,11 +4258,31 @@ mod tests {
return 1
def y() -> int:
return 2
val = MyClass()
val[: MyClass] = MyClass()
foo(val.x())
foo([x=]val.y())
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:3:7
|
2 | def foo(x: int): pass
3 | class MyClass:
| ^^^^^^^
4 | def __init__(self):
5 | def x() -> int:
|
info: Source
--> main2.py:9:7
|
7 | def y() -> int:
8 | return 2
9 | val[: MyClass] = MyClass()
| ^^^^^^^
10 |
11 | foo(val.x())
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:9
|
@@ -4170,6 +4298,22 @@ mod tests {
12 | foo([x=]val.y())
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
def foo(x: int): pass
class MyClass:
def __init__(self):
def x() -> int:
return 1
def y() -> int:
return 2
val: MyClass = MyClass()
foo(val.x())
foo(val.y())
");
}
@@ -4202,11 +4346,31 @@ mod tests {
return 1
def y() -> List[int]:
return 2
val = MyClass()
val[: MyClass] = MyClass()
foo(val.x()[0])
foo([x=]val.y()[1])
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:5:7
|
4 | def foo(x: int): pass
5 | class MyClass:
| ^^^^^^^
6 | def __init__(self):
7 | def x() -> List[int]:
|
info: Source
--> main2.py:11:7
|
9 | def y() -> List[int]:
10 | return 2
11 | val[: MyClass] = MyClass()
| ^^^^^^^
12 |
13 | foo(val.x()[0])
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:4:9
|
@@ -4224,6 +4388,24 @@ mod tests {
14 | foo([x=]val.y()[1])
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
from typing import List
def foo(x: int): pass
class MyClass:
def __init__(self):
def x() -> List[int]:
return 1
def y() -> List[int]:
return 2
val: MyClass = MyClass()
foo(val.x()[0])
foo(val.y()[1])
");
}
@@ -4515,7 +4697,7 @@ mod tests {
class Foo:
def __init__(self, x: int): pass
Foo([x=]1)
f = Foo([x=]1)
f[: Foo] = Foo([x=]1)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:3:24
@@ -4533,7 +4715,24 @@ mod tests {
3 | def __init__(self, x: int): pass
4 | Foo([x=]1)
| ^
5 | f = Foo([x=]1)
5 | f[: Foo] = Foo([x=]1)
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:7
|
2 | class Foo:
| ^^^
3 | def __init__(self, x: int): pass
4 | Foo(1)
|
info: Source
--> main2.py:5:5
|
3 | def __init__(self, x: int): pass
4 | Foo([x=]1)
5 | f[: Foo] = Foo([x=]1)
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
@@ -4546,13 +4745,22 @@ mod tests {
5 | f = Foo(1)
|
info: Source
--> main2.py:5:10
--> main2.py:5:17
|
3 | def __init__(self, x: int): pass
4 | Foo([x=]1)
5 | f = Foo([x=]1)
| ^
5 | f[: Foo] = Foo([x=]1)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class Foo:
def __init__(self, x: int): pass
Foo(1)
f: Foo = Foo(1)
");
}
@@ -4570,7 +4778,7 @@ mod tests {
class Foo:
def __new__(cls, x: int): pass
Foo([x=]1)
f = Foo([x=]1)
f[: Foo] = Foo([x=]1)
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> main.py:3:22
@@ -4588,7 +4796,24 @@ mod tests {
3 | def __new__(cls, x: int): pass
4 | Foo([x=]1)
| ^
5 | f = Foo([x=]1)
5 | f[: Foo] = Foo([x=]1)
|
info[inlay-hint-location]: Inlay Hint Target
--> main.py:2:7
|
2 | class Foo:
| ^^^
3 | def __new__(cls, x: int): pass
4 | Foo(1)
|
info: Source
--> main2.py:5:5
|
3 | def __new__(cls, x: int): pass
4 | Foo([x=]1)
5 | f[: Foo] = Foo([x=]1)
| ^^^
|
info[inlay-hint-location]: Inlay Hint Target
@@ -4601,13 +4826,22 @@ mod tests {
5 | f = Foo(1)
|
info: Source
--> main2.py:5:10
--> main2.py:5:17
|
3 | def __new__(cls, x: int): pass
4 | Foo([x=]1)
5 | f = Foo([x=]1)
| ^
5 | f[: Foo] = Foo([x=]1)
| ^
|
---------------------------------------------
info[inlay-hint-edit]: File after edits
info: Source
class Foo:
def __new__(cls, x: int): pass
Foo(1)
f: Foo = Foo(1)
");
}

View File

@@ -20,7 +20,6 @@ use ty_python_semantic::semantic_index::definition::Definition;
use ty_python_semantic::types::ide_support::{
CallSignatureDetails, call_signature_details, find_active_signature_from_details,
};
use ty_python_semantic::types::{ParameterKind, Type};
// TODO: We may want to add special-case handling for calls to constructors
// so the class docstring is used in place of (or inaddition to) any docstring
@@ -28,29 +27,25 @@ use ty_python_semantic::types::{ParameterKind, Type};
/// Information about a function parameter
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParameterDetails<'db> {
pub struct ParameterDetails {
/// The parameter name (e.g., "param1")
pub name: String,
/// The parameter label in the signature (e.g., "param1: str")
pub label: String,
/// The annotated type of the parameter, if any
pub ty: Option<Type<'db>>,
/// Documentation specific to the parameter, typically extracted from the
/// function's docstring
pub documentation: Option<String>,
/// True if the parameter is positional-only.
pub is_positional_only: bool,
}
/// Information about a function signature
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignatureDetails<'db> {
pub struct SignatureDetails {
/// Text representation of the full signature (including input parameters and return type).
pub label: String,
/// Documentation for the signature, typically from the function's docstring.
pub documentation: Option<Docstring>,
/// Information about each of the parameters in left-to-right order.
pub parameters: Vec<ParameterDetails<'db>>,
pub parameters: Vec<ParameterDetails>,
/// Index of the parameter that corresponds to the argument where the
/// user's cursor is currently positioned.
pub active_parameter: Option<usize>,
@@ -58,18 +53,18 @@ pub struct SignatureDetails<'db> {
/// Signature help information for function calls
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignatureHelpInfo<'db> {
pub struct SignatureHelpInfo {
/// Information about each of the signatures for the function call. We
/// need to handle multiple because of unions, overloads, and composite
/// calls like constructors (which invoke both __new__ and __init__).
pub signatures: Vec<SignatureDetails<'db>>,
pub signatures: Vec<SignatureDetails>,
/// Index of the "active signature" which is the first signature where
/// all arguments that are currently present in the code map to parameters.
pub active_signature: Option<usize>,
}
/// Signature help information for function calls at the given position
pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option<SignatureHelpInfo<'_>> {
pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option<SignatureHelpInfo> {
let parsed = parsed_module(db, file).load(db);
// Get the call expression at the given position.
@@ -171,11 +166,11 @@ fn get_argument_index(call_expr: &ast::ExprCall, offset: TextSize) -> usize {
}
/// Create signature details from `CallSignatureDetails`.
fn create_signature_details_from_call_signature_details<'db>(
fn create_signature_details_from_call_signature_details(
db: &dyn crate::Db,
details: &CallSignatureDetails<'db>,
details: &CallSignatureDetails,
current_arg_index: usize,
) -> SignatureDetails<'db> {
) -> SignatureDetails {
let signature_label = details.label.clone();
let documentation = get_callable_documentation(db, details.definition);
@@ -205,8 +200,6 @@ fn create_signature_details_from_call_signature_details<'db>(
&signature_label,
documentation.as_ref(),
&details.parameter_names,
&details.parameter_kinds,
&details.parameter_types,
);
SignatureDetails {
label: signature_label,
@@ -225,14 +218,12 @@ fn get_callable_documentation(
}
/// Create `ParameterDetails` objects from parameter label offsets.
fn create_parameters_from_offsets<'db>(
fn create_parameters_from_offsets(
parameter_offsets: &[TextRange],
signature_label: &str,
docstring: Option<&Docstring>,
parameter_names: &[String],
parameter_kinds: &[ParameterKind],
parameter_types: &[Option<Type<'db>>],
) -> Vec<ParameterDetails<'db>> {
) -> Vec<ParameterDetails> {
// Extract parameter documentation from the function's docstring if available.
let param_docs = if let Some(docstring) = docstring {
docstring.parameter_documentation()
@@ -254,18 +245,11 @@ fn create_parameters_from_offsets<'db>(
// Get the parameter name for documentation lookup.
let param_name = parameter_names.get(i).map(String::as_str).unwrap_or("");
let is_positional_only = matches!(
parameter_kinds.get(i),
Some(ParameterKind::PositionalOnly { .. })
);
let ty = parameter_types.get(i).copied().flatten();
ParameterDetails {
name: param_name.to_string(),
label,
ty,
documentation: param_docs.get(param_name).cloned(),
is_positional_only,
}
})
.collect()
@@ -1189,7 +1173,7 @@ def ab(a: int, *, c: int):
}
impl CursorTest {
fn signature_help(&self) -> Option<SignatureHelpInfo<'_>> {
fn signature_help(&self) -> Option<SignatureHelpInfo> {
crate::signature_help::signature_help(&self.db, self.cursor.file, self.cursor.offset)
}

View File

@@ -82,6 +82,24 @@ async def main():
reveal_type(b) # revealed: int
```
### `asynccontextmanager`
```py
from contextlib import asynccontextmanager
from typing import AsyncGenerator
class Session: ...
@asynccontextmanager
async def connect() -> AsyncGenerator[Session]:
yield Session()
async def main():
async with connect() as session:
# TODO: should be `Session`
reveal_type(session) # revealed: Unknown
```
## Under the hood
```toml

View File

@@ -9,9 +9,9 @@ python-platform = "linux"
dependencies = ["SQLAlchemy==2.0.44"]
```
## ORM Model
## Basic model
This test makes sure that ty understands SQLAlchemy's `dataclass_transform` setup:
Here, we mostly make sure that ty understands SQLAlchemy's dataclass-transformer setup:
```py
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
@@ -40,12 +40,14 @@ reveal_type(User.__init__) # revealed: def __init__(self, **kw: Any) -> Unknown
invalid_user = User(invalid_arg=42)
```
## Basic query example
## Queries
First, set up a `Session`:
First, the basic setup:
```py
from sqlalchemy import select, Integer, Text, Boolean
from datetime import datetime
from sqlalchemy import select, Integer, Text, Boolean, DateTime
from sqlalchemy.orm import Session
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped, mapped_column
@@ -55,7 +57,7 @@ engine = create_engine("sqlite://example.db")
session = Session(engine)
```
And define a simple model:
Now we can declare a simple model:
```py
class Base(DeclarativeBase):
@@ -69,7 +71,7 @@ class User(Base):
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
```
Finally, we can execute queries:
And perform simple queries:
```py
stmt = select(User)
@@ -82,118 +84,41 @@ for row in session.execute(stmt):
reveal_type(row) # revealed: Row[tuple[User]]
stmt = select(User).where(User.name == "Alice")
alice1 = session.scalars(stmt).first()
reveal_type(alice1) # revealed: User | None
alice = session.scalars(stmt).first()
reveal_type(alice) # revealed: User | None
alice2 = session.scalar(stmt)
reveal_type(alice2) # revealed: User | None
result = session.execute(stmt)
row = result.one_or_none()
assert row is not None
(alice3,) = row._tuple()
reveal_type(alice3) # revealed: User
```
This also works with more complex queries:
```py
stmt = select(User).where(User.is_admin == True).order_by(User.name).limit(10)
admin_users = session.scalars(stmt).all()
reveal_type(admin_users) # revealed: Sequence[User]
```
We can also specify particular columns to select:
```py
reveal_type(User.id) # revealed: InstrumentedAttribute[int]
stmt = select(User.id, User.name)
reveal_type(stmt) # revealed: Select[tuple[int, str]]
ids_and_names = session.execute(stmt).all()
reveal_type(ids_and_names) # revealed: Sequence[Row[tuple[int, str]]]
for row in session.execute(stmt):
reveal_type(row) # revealed: Row[tuple[int, str]]
for user_id, name in session.execute(stmt).tuples():
reveal_type(user_id) # revealed: int
reveal_type(name) # revealed: str
result = session.execute(stmt)
row = result.one_or_none()
assert row is not None
(user_id, name) = row._tuple()
reveal_type(user_id) # revealed: int
reveal_type(name) # revealed: str
stmt = select(User.id).where(User.name == "Alice")
reveal_type(stmt) # revealed: Select[tuple[int]]
alice_id = session.scalars(stmt).first()
reveal_type(alice_id) # revealed: int | None
alice_id = session.scalar(stmt)
reveal_type(alice_id) # revealed: int | None
```
Using the legacy `query` API also works:
This also works with the legacy `query` API:
```py
users_legacy = session.query(User).all()
reveal_type(users_legacy) # revealed: list[User]
query = session.query(User)
reveal_type(query) # revealed: Query[User]
reveal_type(query.all()) # revealed: list[User]
for row in query:
reveal_type(row) # revealed: User
```
And similarly when specifying particular columns:
We can also specify particular columns to select:
```py
stmt = select(User.id, User.name)
# TODO: should be `Select[tuple[int, str]]`
reveal_type(stmt) # revealed: Select[tuple[Unknown, Unknown]]
for row in session.execute(stmt):
# TODO: should be `Row[Tuple[int, str]]`
reveal_type(row) # revealed: Row[tuple[Unknown, Unknown]]
```
And similarly with the legacy `query` API:
```py
query = session.query(User.id, User.name)
# TODO: should be `RowReturningQuery[tuple[int, str]]`
reveal_type(query) # revealed: RowReturningQuery[tuple[Unknown, Unknown]]
# TODO: should be `list[Row[tuple[int, str]]]`
reveal_type(query.all()) # revealed: list[Row[tuple[Unknown, Unknown]]]
for row in query:
# TODO: should be `Row[tuple[int, str]]`
for row in query.all():
# TODO: should be `Row[Tuple[int, str]]`
reveal_type(row) # revealed: Row[tuple[Unknown, Unknown]]
```
## Async API
The async API is supported as well:
```py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, Integer, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(Text)
async def test_async(session: AsyncSession):
stmt = select(User).where(User.name == "Alice")
alice = await session.scalar(stmt)
reveal_type(alice) # revealed: User | None
stmt = select(User.id, User.name)
result = await session.execute(stmt)
for user_id, name in result.tuples():
reveal_type(user_id) # revealed: int
reveal_type(name) # revealed: str
```

View File

@@ -102,38 +102,6 @@ Other values are invalid.
P4 = ParamSpec("P4", default=int)
```
### `default` parameter in `typing_extensions.ParamSpec`
```toml
[environment]
python-version = "3.12"
```
The `default` parameter to `ParamSpec` is available from `typing_extensions` in Python 3.12 and
earlier.
```py
from typing import ParamSpec
from typing_extensions import ParamSpec as ExtParamSpec
# This shouldn't emit a diagnostic
P1 = ExtParamSpec("P1", default=[int, str])
# But, this should
# error: [invalid-paramspec] "The `default` parameter of `typing.ParamSpec` was added in Python 3.13"
P2 = ParamSpec("P2", default=[int, str])
```
And, it allows the same set of values as `typing.ParamSpec`.
```py
P3 = ExtParamSpec("P3", default=...)
P4 = ExtParamSpec("P4", default=P3)
# error: [invalid-paramspec]
P5 = ExtParamSpec("P5", default=int)
```
### Forward references in stub files
Stubs natively support forward references, so patterns that would raise `NameError` at runtime are

View File

@@ -128,16 +128,3 @@ InvalidEmptyUnion = Union[]
def _(u: InvalidEmptyUnion):
reveal_type(u) # revealed: Unknown
```
### `typing.Annotated`
```py
from typing import Annotated
# error: [invalid-syntax] "Expected index or slice expression"
# error: [invalid-type-form] "Special form `typing.Annotated` expected at least 2 arguments (one type and at least one metadata element)"
InvalidEmptyAnnotated = Annotated[]
def _(a: InvalidEmptyAnnotated):
reveal_type(a) # revealed: Unknown
```

View File

@@ -1,19 +0,0 @@
# `ParamSpec` regression on 3.9
```toml
[environment]
python-version = "3.9"
```
This used to panic when run on Python 3.9 because `ParamSpec` was introduced in Python 3.10 and the
diagnostic message for `invalid-exception-caught` expects to construct `typing.ParamSpec`.
```py
# error: [invalid-syntax]
def foo[**P]() -> None:
try:
pass
# error: [invalid-exception-caught] "Invalid object caught in an exception handler: Object has type `typing.ParamSpec`"
except P:
pass
```

View File

@@ -58,8 +58,9 @@ pub fn add_inferred_python_version_hint_to_diagnostic(
SubDiagnosticSeverity::Info,
format_args!("Python {version} was assumed when {action}"),
);
sub_diagnostic
.annotate(Annotation::primary(span).message("Python version configuration"));
sub_diagnostic.annotate(Annotation::primary(span).message(format_args!(
"Python {version} assumed due to this configuration setting"
)));
diagnostic.sub(sub_diagnostic);
} else {
diagnostic.info(format_args!(
@@ -75,8 +76,10 @@ pub fn add_inferred_python_version_hint_to_diagnostic(
"Python {version} was assumed when {action} because of your virtual environment"
),
);
sub_diagnostic
.annotate(Annotation::primary(span).message("Virtual environment metadata"));
sub_diagnostic.annotate(
Annotation::primary(span)
.message("Python version inferred from virtual environment metadata file"),
);
// TODO: it would also be nice to tell them how we resolved their virtual environment...
diagnostic.sub(sub_diagnostic);
} else {

View File

@@ -335,12 +335,6 @@ pub enum KnownModule {
#[cfg(test)]
Uuid,
Warnings,
#[strum(serialize = "sqlalchemy.sql.selectable")]
SqlalchemySqlSelectable,
#[strum(serialize = "sqlalchemy.sql._selectable_constructors")]
SqlalchemySqlSelectableConstructors,
#[strum(serialize = "sqlalchemy.orm.attributes")]
SqlalchemyOrmAttributes,
}
impl KnownModule {
@@ -369,9 +363,6 @@ impl KnownModule {
#[cfg(test)]
Self::Uuid => "uuid",
Self::Templatelib => "string.templatelib",
Self::SqlalchemySqlSelectable => "sqlalchemy.sql.selectable",
Self::SqlalchemySqlSelectableConstructors => "sqlalchemy.sql._selectable_constructors",
Self::SqlalchemyOrmAttributes => "sqlalchemy.orm.attributes",
}
}
@@ -387,20 +378,7 @@ impl KnownModule {
if search_path.is_standard_library() {
Self::from_str(name.as_str()).ok()
} else {
// For non-stdlib search paths, check for known third-party modules
Self::try_from_third_party_name(name)
}
}
/// Returns a known module for third-party packages, if applicable.
fn try_from_third_party_name(name: &ModuleName) -> Option<Self> {
match name.as_str() {
"sqlalchemy.sql.selectable" => Some(Self::SqlalchemySqlSelectable),
"sqlalchemy.sql._selectable_constructors" => {
Some(Self::SqlalchemySqlSelectableConstructors)
}
"sqlalchemy.orm.attributes" => Some(Self::SqlalchemyOrmAttributes),
_ => None,
None
}
}
@@ -441,11 +419,6 @@ mod tests {
let stdlib_search_path = SearchPath::vendored_stdlib();
for module in KnownModule::iter() {
// Third-party modules aren't available in the vendored stdlib
if module.is_third_party() {
continue;
}
let module_name = module.name();
assert_eq!(

View File

@@ -30,7 +30,6 @@ pub(crate) use self::infer::{
TypeContext, infer_deferred_types, infer_definition_types, infer_expression_type,
infer_expression_types, infer_scope_types, static_expression_truthiness,
};
pub use self::signatures::ParameterKind;
pub(crate) use self::signatures::{CallableSignature, Signature};
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
pub use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic;

View File

@@ -4168,8 +4168,6 @@ pub enum KnownClass {
SpecialForm,
TypeVar,
ParamSpec,
// typing_extensions.ParamSpec
ExtensionsParamSpec, // must be distinct from typing.ParamSpec, backports new features
ParamSpecArgs,
ParamSpecKwargs,
ProtocolMeta,
@@ -4207,9 +4205,6 @@ pub enum KnownClass {
ConstraintSet,
GenericContext,
Specialization,
// sqlalchemy
SqlalchemySelect,
SqlalchemyInstrumentedAttribute,
}
impl KnownClass {
@@ -4244,7 +4239,6 @@ impl KnownClass {
| Self::TypeVar
| Self::ExtensionsTypeVar
| Self::ParamSpec
| Self::ExtensionsParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::TypeVarTuple
@@ -4318,9 +4312,7 @@ impl KnownClass {
| Self::GenericContext
| Self::Specialization
| Self::ProtocolMeta
| Self::TypedDictFallback
| Self::SqlalchemySelect
| Self::SqlalchemyInstrumentedAttribute => Some(Truthiness::Ambiguous),
| Self::TypedDictFallback => Some(Truthiness::Ambiguous),
Self::Tuple => None,
}
@@ -4379,7 +4371,6 @@ impl KnownClass {
| KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec
| KnownClass::ExtensionsParamSpec
| KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs
| KnownClass::TypeVarTuple
@@ -4410,9 +4401,7 @@ impl KnownClass {
| KnownClass::BuiltinFunctionType
| KnownClass::ProtocolMeta
| KnownClass::Template
| KnownClass::Path
| KnownClass::SqlalchemySelect
| KnownClass::SqlalchemyInstrumentedAttribute => false,
| KnownClass::Path => false,
}
}
@@ -4468,7 +4457,6 @@ impl KnownClass {
| KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec
| KnownClass::ExtensionsParamSpec
| KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs
| KnownClass::TypeVarTuple
@@ -4499,9 +4487,7 @@ impl KnownClass {
| KnownClass::BuiltinFunctionType
| KnownClass::ProtocolMeta
| KnownClass::Template
| KnownClass::Path
| KnownClass::SqlalchemySelect
| KnownClass::SqlalchemyInstrumentedAttribute => false,
| KnownClass::Path => false,
}
}
@@ -4557,7 +4543,6 @@ impl KnownClass {
| KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec
| KnownClass::ExtensionsParamSpec
| KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs
| KnownClass::TypeVarTuple
@@ -4587,9 +4572,7 @@ impl KnownClass {
| KnownClass::BuiltinFunctionType
| KnownClass::ProtocolMeta
| KnownClass::Template
| KnownClass::Path
| KnownClass::SqlalchemySelect
| KnownClass::SqlalchemyInstrumentedAttribute => false,
| KnownClass::Path => false,
}
}
@@ -4651,7 +4634,6 @@ impl KnownClass {
| Self::TypeVar
| Self::ExtensionsTypeVar
| Self::ParamSpec
| Self::ExtensionsParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::TypeVarTuple
@@ -4688,9 +4670,7 @@ impl KnownClass {
| Self::ProtocolMeta
| Self::Template
| Self::Path
| Self::Mapping
| Self::SqlalchemySelect
| Self::SqlalchemyInstrumentedAttribute => false,
| Self::Mapping => false,
}
}
@@ -4753,7 +4733,6 @@ impl KnownClass {
| KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec
| KnownClass::ExtensionsParamSpec
| KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs
| KnownClass::ProtocolMeta
@@ -4779,9 +4758,7 @@ impl KnownClass {
| KnownClass::ConstraintSet
| KnownClass::GenericContext
| KnownClass::Specialization
| KnownClass::InitVar
| KnownClass::SqlalchemySelect
| KnownClass::SqlalchemyInstrumentedAttribute => false,
| KnownClass::InitVar => false,
KnownClass::NamedTupleFallback | KnownClass::TypedDictFallback => true,
}
}
@@ -4829,7 +4806,6 @@ impl KnownClass {
Self::TypeVar => "TypeVar",
Self::ExtensionsTypeVar => "TypeVar",
Self::ParamSpec => "ParamSpec",
Self::ExtensionsParamSpec => "ParamSpec",
Self::ParamSpecArgs => "ParamSpecArgs",
Self::ParamSpecKwargs => "ParamSpecKwargs",
Self::TypeVarTuple => "TypeVarTuple",
@@ -4897,8 +4873,6 @@ impl KnownClass {
Self::Template => "Template",
Self::Path => "Path",
Self::ProtocolMeta => "_ProtocolMeta",
Self::SqlalchemySelect => "Select",
Self::SqlalchemyInstrumentedAttribute => "InstrumentedAttribute",
}
}
@@ -5165,18 +5139,11 @@ impl KnownClass {
Self::TypeAliasType
| Self::ExtensionsTypeVar
| Self::TypeVarTuple
| Self::ExtensionsParamSpec
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::Deprecated
| Self::NewType => KnownModule::TypingExtensions,
Self::ParamSpec => {
if Program::get(db).python_version(db) >= PythonVersion::PY310 {
KnownModule::Typing
} else {
KnownModule::TypingExtensions
}
}
Self::NoDefaultType => {
let python_version = Program::get(db).python_version(db);
@@ -5220,8 +5187,6 @@ impl KnownClass {
| Self::Specialization => KnownModule::TyExtensions,
Self::Template => KnownModule::Templatelib,
Self::Path => KnownModule::Pathlib,
Self::SqlalchemySelect => KnownModule::SqlalchemySqlSelectable,
Self::SqlalchemyInstrumentedAttribute => KnownModule::SqlalchemyOrmAttributes,
}
}
@@ -5282,7 +5247,6 @@ impl KnownClass {
| Self::TypeVar
| Self::ExtensionsTypeVar
| Self::ParamSpec
| Self::ExtensionsParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::TypeVarTuple
@@ -5310,9 +5274,7 @@ impl KnownClass {
| Self::BuiltinFunctionType
| Self::ProtocolMeta
| Self::Template
| Self::Path
| Self::SqlalchemySelect
| Self::SqlalchemyInstrumentedAttribute => Some(false),
| Self::Path => Some(false),
Self::Tuple => None,
}
@@ -5375,7 +5337,6 @@ impl KnownClass {
| Self::TypeVar
| Self::ExtensionsTypeVar
| Self::ParamSpec
| Self::ExtensionsParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::TypeVarTuple
@@ -5404,9 +5365,7 @@ impl KnownClass {
| Self::BuiltinFunctionType
| Self::ProtocolMeta
| Self::Template
| Self::Path
| Self::SqlalchemySelect
| Self::SqlalchemyInstrumentedAttribute => false,
| Self::Path => false,
}
}
@@ -5461,7 +5420,7 @@ impl KnownClass {
"Iterable" => &[Self::Iterable],
"Iterator" => &[Self::Iterator],
"Mapping" => &[Self::Mapping],
"ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec],
"ParamSpec" => &[Self::ParamSpec],
"ParamSpecArgs" => &[Self::ParamSpecArgs],
"ParamSpecKwargs" => &[Self::ParamSpecKwargs],
"TypeVarTuple" => &[Self::TypeVarTuple],
@@ -5512,8 +5471,6 @@ impl KnownClass {
"Template" => &[Self::Template],
"Path" => &[Self::Path],
"_ProtocolMeta" => &[Self::ProtocolMeta],
"Select" => &[Self::SqlalchemySelect],
"InstrumentedAttribute" => &[Self::SqlalchemyInstrumentedAttribute],
_ => return None,
};
@@ -5585,8 +5542,6 @@ impl KnownClass {
| Self::TypedDictFallback
| Self::TypeVar
| Self::ExtensionsTypeVar
| Self::ParamSpec
| Self::ExtensionsParamSpec
| Self::NamedTupleLike
| Self::ConstraintSet
| Self::GenericContext
@@ -5594,14 +5549,13 @@ impl KnownClass {
| Self::Awaitable
| Self::Generator
| Self::Template
| Self::Path
| Self::SqlalchemySelect
| Self::SqlalchemyInstrumentedAttribute => module == self.canonical_module(db),
| Self::Path => module == self.canonical_module(db),
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
Self::SpecialForm
| Self::TypeAliasType
| Self::NoDefaultType
| Self::SupportsIndex
| Self::ParamSpec
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::TypeVarTuple
@@ -5951,10 +5905,6 @@ mod tests {
source: PythonVersionSource::default(),
});
for class in KnownClass::iter() {
if class.canonical_module(&db).is_third_party() {
continue;
}
let class_name = class.name(&db);
let class_module =
resolve_module_confident(&db, &class.canonical_module(&db).name()).unwrap();
@@ -5983,10 +5933,6 @@ mod tests {
});
for class in KnownClass::iter() {
if class.canonical_module(&db).is_third_party() {
continue;
}
// Check the class can be looked up successfully
class.try_to_class_literal_without_logging(&db).unwrap();
@@ -6012,7 +5958,6 @@ mod tests {
// This makes the test far faster as it minimizes the number of times
// we need to change the Python version in the loop.
let mut classes: Vec<(KnownClass, PythonVersion)> = KnownClass::iter()
.filter(|class| !class.canonical_module(&db).is_third_party())
.map(|class| {
let version_added = match class {
KnownClass::Template => PythonVersion::PY314,
@@ -6025,7 +5970,6 @@ mod tests {
KnownClass::Member | KnownClass::Nonmember | KnownClass::StrEnum => {
PythonVersion::PY311
}
KnownClass::ParamSpec => PythonVersion::PY310,
_ => PythonVersion::PY37,
};
(class, version_added)

View File

@@ -1353,10 +1353,6 @@ pub enum KnownFunction {
RevealProtocolInterface,
/// `ty_extensions.reveal_mro`
RevealMro,
/// `sqlalchemy.select`
#[strum(serialize = "select")]
SqlalchemySelect,
}
impl KnownFunction {
@@ -1429,9 +1425,6 @@ impl KnownFunction {
Self::TypeCheckOnly => matches!(module, KnownModule::Typing),
Self::NamedTuple => matches!(module, KnownModule::Collections),
Self::SqlalchemySelect => {
matches!(module, KnownModule::SqlalchemySqlSelectableConstructors)
}
}
}
@@ -1903,56 +1896,6 @@ impl KnownFunction {
overload.set_return_type(Type::module_literal(db, file, module));
}
KnownFunction::SqlalchemySelect => {
// Try to extract types from InstrumentedAttribute[T] arguments.
// If all arguments are InstrumentedAttribute instances, we construct
// Select[tuple[T_1, T_2, ...]] where T_i are the inner types.
//
// We check the class via `class_literal.known(db)` rather than using
// `known_specialization` because the class may be re-exported and not
// directly importable from its canonical module.
let inner_types: Option<Vec<_>> = parameter_types
.iter()
.flatten()
.map(|param_type| {
let Type::NominalInstance(instance) = param_type else {
return None;
};
let class = instance.class(db);
let (class_literal, specialization) = class.class_literal(db);
if class_literal.known(db)
!= Some(KnownClass::SqlalchemyInstrumentedAttribute)
{
return None;
}
specialization?.types(db).first().copied()
})
.collect();
let Some(inner_types) = inner_types else {
// Fall back to whatever we infer from the function signature
return;
};
if inner_types.is_empty() {
return;
}
// Construct Select[tuple[T1, T2, ...]]
// We get the return type's class from the overload rather than looking
// it up via try_to_class_literal, since the class may be re-exported.
let Type::NominalInstance(return_instance) = overload.return_type() else {
return;
};
let select_class = return_instance.class(db).class_literal(db).0;
let tuple_type = Type::heterogeneous_tuple(db, inner_types);
let class_type = select_class.apply_specialization(db, |generic_context| {
generic_context.specialize(db, vec![tuple_type].into())
});
overload.set_return_type(Type::instance(db, class_type));
}
_ => {}
}
}
@@ -2021,8 +1964,6 @@ pub(crate) mod tests {
KnownFunction::ImportModule => KnownModule::ImportLib,
KnownFunction::NamedTuple => KnownModule::Collections,
KnownFunction::SqlalchemySelect => continue,
};
let function_definition = known_module_symbol(&db, module, function_name)

View File

@@ -6,7 +6,7 @@ use crate::semantic_index::definition::Definition;
use crate::semantic_index::definition::DefinitionKind;
use crate::semantic_index::{attribute_scopes, global_scope, semantic_index, use_def_map};
use crate::types::call::{CallArguments, MatchedArgument};
use crate::types::signatures::{ParameterKind, Signature};
use crate::types::signatures::Signature;
use crate::types::{CallDunderError, UnionType};
use crate::types::{CallableTypes, ClassBase, KnownClass, Type, TypeContext};
use crate::{Db, DisplaySettings, HasType, SemanticModel};
@@ -459,12 +459,6 @@ pub struct CallSignatureDetails<'db> {
/// This provides easy access to parameter names for documentation lookup.
pub parameter_names: Vec<String>,
/// Parameter kinds, useful to determine correct autocomplete suggestions.
pub parameter_kinds: Vec<ParameterKind<'db>>,
/// Parameter kinds, useful to determine correct autocomplete suggestions.
pub parameter_types: Vec<Option<Type<'db>>>,
/// The definition where this callable was originally defined (useful for
/// extracting docstrings).
pub definition: Option<Definition<'db>>,
@@ -523,12 +517,6 @@ pub fn call_signature_details<'db>(
let display_details = signature.display(model.db()).to_string_parts();
let parameter_label_offsets = display_details.parameter_ranges;
let parameter_names = display_details.parameter_names;
let (parameter_kinds, parameter_types): (Vec<ParameterKind>, Vec<Option<Type>>) =
signature
.parameters()
.iter()
.map(|param| (param.kind().clone(), param.annotated_type()))
.unzip();
CallSignatureDetails {
definition: signature.definition(),
@@ -536,8 +524,6 @@ pub fn call_signature_details<'db>(
label: display_details.label,
parameter_label_offsets,
parameter_names,
parameter_kinds,
parameter_types,
argument_to_parameter_mapping,
}
})

View File

@@ -5033,15 +5033,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
) => {
self.infer_legacy_typevar(target, call_expr, definition, typevar_class)
}
Some(
paramspec_class @ (KnownClass::ParamSpec
| KnownClass::ExtensionsParamSpec),
) => self.infer_legacy_paramspec(
target,
call_expr,
definition,
paramspec_class,
),
Some(KnownClass::ParamSpec) => {
self.infer_paramspec(target, call_expr, definition)
}
Some(KnownClass::NewType) => {
self.infer_newtype_expression(target, call_expr, definition)
}
@@ -5086,12 +5080,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
target_ty
}
fn infer_legacy_paramspec(
fn infer_paramspec(
&mut self,
target: &ast::Expr,
call_expr: &ast::ExprCall,
definition: Definition<'db>,
known_class: KnownClass,
) -> Type<'db> {
fn error<'db>(
context: &InferContext<'db, '_>,
@@ -5108,8 +5101,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let db = self.db();
let arguments = &call_expr.arguments;
let is_typing_extensions = known_class == KnownClass::ExtensionsParamSpec;
let assume_all_features = self.in_stub() || is_typing_extensions;
let assume_all_features = self.in_stub();
let python_version = Program::get(db).python_version(db);
let have_features_from =
|version: PythonVersion| assume_all_features || python_version >= version;
@@ -5602,10 +5594,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_type_expression(&bound.value);
}
if let Some(default) = arguments.find_keyword("default") {
if matches!(
known_class,
Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec)
) {
if let Some(KnownClass::ParamSpec) = known_class {
self.infer_paramspec_default(&default.value);
} else {
self.infer_type_expression(&default.value);
@@ -8451,7 +8440,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
}
}
Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec) => {
Some(KnownClass::ParamSpec) => {
if let Some(builder) = self
.context
.report_lint(&INVALID_PARAMSPEC, call_expression)

View File

@@ -1172,11 +1172,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
)
.in_type_expression(db, self.scope(), None)
.unwrap_or_else(|err| err.into_fallback_type(&self.context, subscript, true));
// Only store on the tuple slice; non-tuple cases are handled by
// `infer_subscript_load_impl` via `infer_expression`.
if arguments_slice.is_tuple_expr() {
self.store_expression_type(arguments_slice, ty);
}
self.store_expression_type(arguments_slice, ty);
ty
}
SpecialFormType::Literal => match self.infer_literal_parameter_type(arguments_slice) {

View File

@@ -2292,7 +2292,7 @@ impl<'db> Parameter<'db> {
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
pub enum ParameterKind<'db> {
pub(crate) enum ParameterKind<'db> {
/// Positional-only parameter, e.g. `def f(x, /): ...`
PositionalOnly {
/// Parameter name.

View File

@@ -349,7 +349,6 @@ pub(super) fn to_lsp_diagnostic(
sub_diagnostic
.annotations()
.iter()
.filter(|annotation| !annotation.is_primary())
.filter_map(|annotation| {
annotation_to_related_information(db, annotation, encoding)
}),

View File

@@ -132,44 +132,11 @@ x: Literal[1] = 1
";
let ty_toml = SystemPath::new("ty.toml");
let ty_toml_content = "";
let mut server = TestServerBuilder::new()?
.with_workspace(workspace_root, None)?
.with_file(ty_toml, ty_toml_content)?
.with_file(foo, foo_content)?
.enable_pull_diagnostics(true)
.build()
.wait_until_workspaces_are_initialized();
server.open_text_document(foo, foo_content, 1);
// Wait for diagnostics to be computed.
let diagnostics = server.document_diagnostic_request(foo, None);
let range = full_range(foo_content);
let code_action_params = code_actions_at(&server, diagnostics, foo, range);
// Get code actions
let code_action_id = server.send_request::<CodeActionRequest>(code_action_params);
let code_actions = server.await_response::<CodeActionRequest>(&code_action_id);
insta::assert_json_snapshot!(code_actions);
Ok(())
}
// `Literal` is available from two places so we should suggest two possible imports
#[test]
fn code_action_undefined_reveal_type() -> Result<()> {
let workspace_root = SystemPath::new("src");
let foo = SystemPath::new("src/foo.py");
let foo_content = "\
reveal_type(1)
let ty_toml_content = "\
[rules]
unused-ignore-comment = \"warn\"
";
let ty_toml = SystemPath::new("ty.toml");
let ty_toml_content = "";
let mut server = TestServerBuilder::new()?
.with_workspace(workspace_root, None)?
.with_file(ty_toml, ty_toml_content)?

View File

@@ -1,98 +0,0 @@
---
source: crates/ty_server/tests/e2e/code_actions.rs
expression: code_actions
---
[
{
"title": "import typing.reveal_type",
"kind": "quickfix",
"diagnostics": [
{
"range": {
"start": {
"line": 0,
"character": 0
},
"end": {
"line": 0,
"character": 11
}
},
"severity": 2,
"code": "undefined-reveal",
"codeDescription": {
"href": "https://ty.dev/rules#undefined-reveal"
},
"source": "ty",
"message": "`reveal_type` used without importing it",
"relatedInformation": []
}
],
"edit": {
"changes": {
"file://<temp_dir>/src/foo.py": [
{
"range": {
"start": {
"line": 0,
"character": 0
},
"end": {
"line": 0,
"character": 0
}
},
"newText": "from typing import reveal_type\n"
}
]
}
},
"isPreferred": true
},
{
"title": "Ignore 'undefined-reveal' for this line",
"kind": "quickfix",
"diagnostics": [
{
"range": {
"start": {
"line": 0,
"character": 0
},
"end": {
"line": 0,
"character": 11
}
},
"severity": 2,
"code": "undefined-reveal",
"codeDescription": {
"href": "https://ty.dev/rules#undefined-reveal"
},
"source": "ty",
"message": "`reveal_type` used without importing it",
"relatedInformation": []
}
],
"edit": {
"changes": {
"file://<temp_dir>/src/foo.py": [
{
"range": {
"start": {
"line": 0,
"character": 14
},
"end": {
"line": 0,
"character": 14
}
},
"newText": " # ty:ignore[undefined-reveal]"
}
]
}
},
"isPreferred": false
}
]

View File

@@ -427,16 +427,10 @@ mod tests {
let mut diag = if self.id == DiagnosticId::RevealedType {
Diagnostic::new(self.id, Severity::Error, "Revealed type")
} else {
Diagnostic::new(self.id, Severity::Error, self.message)
Diagnostic::new(self.id, Severity::Error, "")
};
let span = Span::from(file).with_range(self.range);
let mut annotation = Annotation::primary(span);
if self.id == DiagnosticId::RevealedType {
annotation = annotation.message(self.message);
}
diag.annotate(annotation);
diag.annotate(Annotation::primary(span).message(self.message));
diag
}
}

View File

@@ -33,7 +33,7 @@ from collections.abc import Callable
from dataclasses import KW_ONLY, dataclass
from functools import partial
from pathlib import Path
from typing import Final, NewType, NoReturn, assert_never
from typing import NewType, NoReturn, assert_never
from pysource_codegen import generate as generate_random_code
from pysource_minimize import CouldNotMinimize, minimize as minimize_repro
@@ -44,12 +44,6 @@ MinimizedSourceCode = NewType("MinimizedSourceCode", str)
Seed = NewType("Seed", int)
ExitCode = NewType("ExitCode", int)
TY_TARGET_PLATFORM: Final = "linux"
# ty supports `--python-version=3.8`, but typeshed only supports 3.9+,
# so that's probably the oldest version we can usefully test with.
OLDEST_SUPPORTED_PYTHON: Final = "3.9"
def ty_contains_bug(code: str, *, ty_executable: Path) -> bool:
"""Return `True` if the code triggers a panic in type-checking code."""
@@ -57,17 +51,7 @@ def ty_contains_bug(code: str, *, ty_executable: Path) -> bool:
input_file = Path(tempdir, "input.py")
input_file.write_text(code)
completed_process = subprocess.run(
[
ty_executable,
"check",
input_file,
"--python-version",
OLDEST_SUPPORTED_PYTHON,
"--python-platform",
TY_TARGET_PLATFORM,
],
capture_output=True,
text=True,
[ty_executable, "check", input_file], capture_output=True, text=True
)
return completed_process.returncode not in {0, 1, 2}
@@ -153,10 +137,7 @@ class FuzzResult:
case Executable.RUFF:
panic_message = f"The following code triggers a {new}parser bug:"
case Executable.TY:
panic_message = (
f"The following code triggers a {new}ty panic with "
f"`--python-version={OLDEST_SUPPORTED_PYTHON} --python-platform={TY_TARGET_PLATFORM}`:"
)
panic_message = f"The following code triggers a {new}ty panic:"
case _ as unreachable:
assert_never(unreachable)

View File

@@ -19,7 +19,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = ["mypy", "ruff", "ty"]
dev = ["mypy", "ruff"]
[tool.hatch.build.targets.wheel]
include = ["fuzz.py"]

View File

@@ -89,7 +89,6 @@ dependencies = [
dev = [
{ name = "mypy" },
{ name = "ruff" },
{ name = "ty" },
]
[package.metadata]
@@ -105,7 +104,6 @@ requires-dist = [
dev = [
{ name = "mypy" },
{ name = "ruff" },
{ name = "ty" },
]
[[package]]
@@ -198,31 +196,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" },
]
[[package]]
name = "ty"
version = "0.0.1a32"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/26/92/8da015685fb83734a2a83de02080e64d182509de77fa9bcf3eed12eeab4b/ty-0.0.1a32.tar.gz", hash = "sha256:12f62e8a3dd0eaeb9557d74b1c32f0616ae40eae10a4f411e1e2a73225f67ff2", size = 4689151, upload-time = "2025-12-05T21:04:26.885Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e6/fdc35c9ba047f16afdfedf36fb51c221e0190ccde9f70ee28e77084d6612/ty-0.0.1a32-py3-none-linux_armv6l.whl", hash = "sha256:ffe595eaf616f06f58f951766477830a55c2502d2c9f77dde8f60d9a836e0645", size = 9673128, upload-time = "2025-12-05T21:04:17.702Z" },
{ url = "https://files.pythonhosted.org/packages/19/20/eaff31048e2f309f37478f7d715c8de9f9bab03cba4758da27b9311147af/ty-0.0.1a32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:07f1dce88ad6028fb14665aefe4e6697012c34bd48edd37d02b7eb6a833dbf62", size = 9434094, upload-time = "2025-12-05T21:04:03.383Z" },
{ url = "https://files.pythonhosted.org/packages/67/d4/ea8ed57d11b81c459f23561fd6bfb0f54a8d4120cf72541e3bdf71d46202/ty-0.0.1a32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8fab7ed12528c77ddd600a9638ca859156a53c20f1e381353fa87a255bd397eb", size = 8980296, upload-time = "2025-12-05T21:04:28.912Z" },
{ url = "https://files.pythonhosted.org/packages/49/02/3ce98bbfbb3916678d717ee69358d38a404ca9a39391dda8874b66dd5ee7/ty-0.0.1a32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace395280fc21e25eff0a53cfbd68170f90a4b8ef2f85dfabe1ecbca2ced456b", size = 9263054, upload-time = "2025-12-05T21:04:05.619Z" },
{ url = "https://files.pythonhosted.org/packages/b7/be/a639638bcd1664de2d70a87da6c4fe0e3272a60b7fa3f0c108a956a456bd/ty-0.0.1a32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2bcbeed7f5ed8e3c1c7e525fce541e7b943ac04ee7fe369a926551b5e50ea4a8", size = 9451396, upload-time = "2025-12-05T21:04:01.265Z" },
{ url = "https://files.pythonhosted.org/packages/1f/a4/2bcf54e842a3d10dc14b369f28a3bab530c5d7ddba624e910b212bda93ee/ty-0.0.1a32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60ff2e4493f90f81a260205d87719bb1d3420928a1e4a2a7454af7cbdfed2047", size = 9862726, upload-time = "2025-12-05T21:04:08.806Z" },
{ url = "https://files.pythonhosted.org/packages/5f/c7/19e6719496e59f2f082f34bcac312698366cf50879fdcc3ef76298bfe6a0/ty-0.0.1a32-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:53cad50a59a0d943b06872e0b10f9f2b564805c2ea93f64c7798852bc1901954", size = 10475051, upload-time = "2025-12-05T21:04:31.059Z" },
{ url = "https://files.pythonhosted.org/packages/88/77/bdf0ddb066d2b62f141d058f8a33bb7c8628cdbb8bfa75b20e296b79fb4e/ty-0.0.1a32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:343d43cdc1d7f649ea2baa64ac2b479da3d679239b94509f1df12f7211561ea9", size = 10232712, upload-time = "2025-12-05T21:04:19.849Z" },
{ url = "https://files.pythonhosted.org/packages/ed/07/f73260a461762a581a007015c1019d40658828ce41576f8c1db88dee574d/ty-0.0.1a32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f45483e4a84bcf622413712164ea687ce323a9f7013b9e7977c5d623ed937ca9", size = 10237705, upload-time = "2025-12-05T21:04:35.366Z" },
{ url = "https://files.pythonhosted.org/packages/2c/57/dbb92206cf2f798d8c51ea16504e8afb90a139d0ff105c31cec9a1db29f9/ty-0.0.1a32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d452f30d47002a6bafc36d1b6aee42c321e9ec9f7f43a04a2ee7d48c208b86c", size = 9766469, upload-time = "2025-12-05T21:04:22.236Z" },
{ url = "https://files.pythonhosted.org/packages/c3/5e/143d93bd143abcebcbaa98c8aeec78898553d62d0a5a432cd79e0cf5bd6d/ty-0.0.1a32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:86c4e31737fe954637890cef1f3e1b479ffb20e836cac3b76050bdbe80005010", size = 9238592, upload-time = "2025-12-05T21:04:11.33Z" },
{ url = "https://files.pythonhosted.org/packages/21/b8/225230ae097ed88f3c92ad974dd77f8e4f86f2594d9cd0c729da39769878/ty-0.0.1a32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:daf15fa03bc39a76a0fbc9c2d81d79d528f584e3fbe08d71981e3f7912db91d6", size = 9502161, upload-time = "2025-12-05T21:04:37.642Z" },
{ url = "https://files.pythonhosted.org/packages/85/13/cc89955c9637f25f3aca2dd7749c6008639ef036f0b9bea3e9d89e892ff9/ty-0.0.1a32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6128f6bab5c6dab3d08689fed1d529dc34f50f221f89c8e16064ed0c549dad7a", size = 9603058, upload-time = "2025-12-05T21:04:39.532Z" },
{ url = "https://files.pythonhosted.org/packages/46/77/1fe2793c8065a02d1f70ca7da1b87db49ca621bcbbdb79a18ad79d5d0ab2/ty-0.0.1a32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:55aab688be1b46776a5a458a1993cae0da7725932c45393399c479c2fa979337", size = 9879903, upload-time = "2025-12-05T21:04:13.567Z" },
{ url = "https://files.pythonhosted.org/packages/fc/47/fd58e80a3e42310b4b649340d5d97403fe796146cae8678b3a031a414b8e/ty-0.0.1a32-py3-none-win32.whl", hash = "sha256:f55ec25088a09236ad1578b656a07fa009c3a353f5923486905ba48175d142a6", size = 9077703, upload-time = "2025-12-05T21:04:15.849Z" },
{ url = "https://files.pythonhosted.org/packages/8d/96/209c417c69317339ea8e9b3277fd98364a0e97dd1ffd3585e143ec7b4e57/ty-0.0.1a32-py3-none-win_amd64.whl", hash = "sha256:ed8d5cbd4e47dfed86aaa27e243008aa4e82b6a5434f3ab95c26d3ee5874d9d7", size = 9922426, upload-time = "2025-12-05T21:04:33.289Z" },
{ url = "https://files.pythonhosted.org/packages/e0/1c/350fd851fb91244f8c80cec218009cbee7564d76c14e2f423b47e69a5cbc/ty-0.0.1a32-py3-none-win_arm64.whl", hash = "sha256:dbb25f9b513d34cee8ce419514eaef03313f45c3f7ab4eb6e6d427ea1f6854af", size = 9453761, upload-time = "2025-12-05T21:04:24.502Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"