mirror of https://github.com/astral-sh/ruff
Merge remote-tracking branch 'origin/main' into dcreager/callable-return
* origin/main: Fluent formatting of method chains (#21369) [ty] Avoid stack overflow when calculating inferable typevars (#21971) [ty] Add "qualify ..." code fix for undefined references (#21968) [ty] Use jemalloc on linux (#21975) Update MSRV to 1.90 (#21987) [ty] Improve check enforcing that an overloaded function must have an implementation (#21978) Update actions/checkout digest to 8e8c483 (#21982) [ty] Use `ParamSpec` without the attr for inferable check (#21934) [ty] Emit diagnostic when a type variable with a default is followed by one without a default (#21787)
This commit is contained in:
commit
dfcdbcffec
|
|
@ -60,7 +60,7 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
|
@ -123,7 +123,7 @@ jobs:
|
|||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
|
@ -174,7 +174,7 @@ jobs:
|
|||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
|
@ -250,7 +250,7 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
|
|
|||
|
|
@ -4390,6 +4390,7 @@ dependencies = [
|
|||
"ruff_python_trivia",
|
||||
"salsa",
|
||||
"tempfile",
|
||||
"tikv-jemallocator",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-flame",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ resolver = "2"
|
|||
[workspace.package]
|
||||
# Please update rustfmt.toml when bumping the Rust edition
|
||||
edition = "2024"
|
||||
rust-version = "1.89"
|
||||
rust-version = "1.90"
|
||||
homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use anyhow::bail;
|
|||
use clap::builder::Styles;
|
||||
use clap::builder::styling::{AnsiColor, Effects};
|
||||
use clap::builder::{TypedValueParser, ValueParserFactory};
|
||||
use clap::{Parser, Subcommand, command};
|
||||
use clap::{Parser, Subcommand};
|
||||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use path_absolutize::path_dedot;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use std::sync::mpsc::channel;
|
|||
use anyhow::Result;
|
||||
use clap::CommandFactory;
|
||||
use colored::Colorize;
|
||||
use log::{error, warn};
|
||||
use log::error;
|
||||
use notify::{RecursiveMode, Watcher, recommended_watcher};
|
||||
|
||||
use args::{GlobalConfigArgs, ServerCommand};
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ macro_rules! best_fitting {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
use crate::{FormatState, SimpleFormatOptions, VecBuffer, write};
|
||||
use crate::{FormatState, SimpleFormatOptions, VecBuffer};
|
||||
|
||||
struct TestFormat;
|
||||
|
||||
|
|
@ -385,8 +385,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn best_fitting_variants_print_as_lists() {
|
||||
use crate::Formatted;
|
||||
use crate::prelude::*;
|
||||
use crate::{Formatted, format, format_args};
|
||||
|
||||
// The second variant below should be selected when printing at a width of 30
|
||||
let formatted_best_fitting = format!(
|
||||
|
|
|
|||
|
|
@ -286,12 +286,7 @@ pub(crate) fn add_argument(argument: &str, arguments: &Arguments, tokens: &Token
|
|||
|
||||
/// Generic function to add a (regular) parameter to a function definition.
|
||||
pub(crate) fn add_parameter(parameter: &str, parameters: &Parameters, source: &str) -> Edit {
|
||||
if let Some(last) = parameters
|
||||
.args
|
||||
.iter()
|
||||
.filter(|arg| arg.default.is_none())
|
||||
.next_back()
|
||||
{
|
||||
if let Some(last) = parameters.args.iter().rfind(|arg| arg.default.is_none()) {
|
||||
// Case 1: at least one regular parameter, so append after the last one.
|
||||
Edit::insertion(format!(", {parameter}"), last.end())
|
||||
} else if !parameters.args.is_empty() {
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Resu
|
|||
let left = (*comparison.left).clone();
|
||||
|
||||
// Copy the right side to the left side.
|
||||
comparison.left = Box::new(comparison.comparisons[0].comparator.clone());
|
||||
*comparison.left = comparison.comparisons[0].comparator.clone();
|
||||
|
||||
// Copy the left side to the right side.
|
||||
comparison.comparisons[0].comparator = left;
|
||||
|
|
|
|||
|
|
@ -1247,6 +1247,7 @@ impl<'a> Generator<'a> {
|
|||
self.p_bytes_repr(&bytes_literal.value, bytes_literal.flags);
|
||||
}
|
||||
}
|
||||
#[expect(clippy::eq_op)]
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
|
||||
static INF_STR: &str = "1e309";
|
||||
assert_eq!(f64::MAX_10_EXP, 308);
|
||||
|
|
|
|||
1
crates/ruff_python_formatter/resources/test/fixtures/ruff/fluent.options.json
vendored
Normal file
1
crates/ruff_python_formatter/resources/test/fixtures/ruff/fluent.options.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[{"line_width":8}]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# Fixtures for fluent formatting of call chains
|
||||
# Note that `fluent.options.json` sets line width to 8
|
||||
|
||||
|
||||
x = a.b()
|
||||
|
||||
x = a.b().c()
|
||||
|
||||
x = a.b().c().d
|
||||
|
||||
x = a.b.c.d().e()
|
||||
|
||||
x = a.b.c().d.e().f.g()
|
||||
|
||||
# Consecutive calls/subscripts are grouped together
|
||||
# for the purposes of fluent formatting (though, as 2025.12.15,
|
||||
# there may be a break inside of one of these
|
||||
# calls/subscripts, but that is unrelated to the fluent format.)
|
||||
|
||||
x = a()[0]().b().c()
|
||||
|
||||
x = a.b()[0].c.d()[1]().e
|
||||
|
||||
# Parentheses affect both where the root of the call
|
||||
# chain is and how many calls we require before applying
|
||||
# fluent formatting (just 1, in the presence of a parenthesized
|
||||
# root, as of 2025.12.15.)
|
||||
|
||||
x = (a).b()
|
||||
|
||||
x = (a()).b()
|
||||
|
||||
x = (a.b()).d.e()
|
||||
|
||||
x = (a.b().d).e()
|
||||
|
|
@ -216,3 +216,69 @@ max_message_id = (
|
|||
.baz()
|
||||
)
|
||||
|
||||
# Note in preview we split at `pl` which some
|
||||
# folks may dislike. (Similarly with common
|
||||
# `np` and `pd` invocations).
|
||||
#
|
||||
# This is because we cannot reliably predict,
|
||||
# just from syntax, whether a short identifier
|
||||
# is being used as a 'namespace' or as an 'object'.
|
||||
#
|
||||
# As of 2025.12.15, we do not indent methods in
|
||||
# fluent formatting. If we ever decide to do so,
|
||||
# it may make sense to special case call chain roots
|
||||
# that are shorter than the indent-width (like Prettier does).
|
||||
# This would have the benefit of handling these common
|
||||
# two-letter aliases for libraries.
|
||||
|
||||
|
||||
expr = (
|
||||
pl.scan_parquet("/data/pypi-parquet/*.parquet")
|
||||
.filter(
|
||||
[
|
||||
pl.col("path").str.contains(
|
||||
r"\.(asm|c|cc|cpp|cxx|h|hpp|rs|[Ff][0-9]{0,2}(?:or)?|go)$"
|
||||
),
|
||||
~pl.col("path").str.contains(r"(^|/)test(|s|ing)"),
|
||||
~pl.col("path").str.contains("/site-packages/", literal=True),
|
||||
]
|
||||
)
|
||||
.with_columns(
|
||||
month=pl.col("uploaded_on").dt.truncate("1mo"),
|
||||
ext=pl.col("path")
|
||||
.str.extract(pattern=r"\.([a-z0-9]+)$", group_index=1)
|
||||
.str.replace_all(pattern=r"cxx|cpp|cc|c|hpp|h", value="C/C++")
|
||||
.str.replace_all(pattern="^f.*$", value="Fortran")
|
||||
.str.replace("rs", "Rust", literal=True)
|
||||
.str.replace("go", "Go", literal=True)
|
||||
.str.replace("asm", "Assembly", literal=True)
|
||||
.replace({"": None}),
|
||||
)
|
||||
.group_by(["month", "ext"])
|
||||
.agg(project_count=pl.col("project_name").n_unique())
|
||||
.drop_nulls(["ext"])
|
||||
.sort(["month", "project_count"], descending=True)
|
||||
)
|
||||
|
||||
def indentation_matching_for_loop_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
identical_hidden_layer_sizes = all(
|
||||
current_hidden_layer_sizes == first_hidden_layer_sizes
|
||||
for current_hidden_layer_sizes in self.component_config[
|
||||
HIDDEN_LAYERS_SIZES
|
||||
].values().attr
|
||||
)
|
||||
|
||||
def indentation_matching_walrus_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
with self.read_ctx(book_type) as cursor:
|
||||
if (entry_count := len(names := cursor.execute(
|
||||
'SELECT name FROM address_book WHERE address=?',
|
||||
(address,),
|
||||
).fetchall().some_attr)) == 0 or len(set(names)) > 1:
|
||||
return
|
||||
|
||||
# behavior with parenthesized roots
|
||||
x = (aaaaaaaaaaaaaaaaaaaaaa).bbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccc().dddddddddddddddddddddddd().eeeeeeeeeeee
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, ValueEnum, command};
|
||||
use clap::{Parser, ValueEnum};
|
||||
|
||||
use ruff_formatter::SourceCode;
|
||||
use ruff_python_ast::{PySourceType, PythonVersion};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use crate::expression::parentheses::{
|
|||
NeedsParentheses, OptionalParentheses, Parentheses, is_expression_parenthesized,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_fluent_layout_split_first_call_enabled;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatExprAttribute {
|
||||
|
|
@ -47,20 +48,26 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
|
|||
)
|
||||
};
|
||||
|
||||
if call_chain_layout == CallChainLayout::Fluent {
|
||||
if call_chain_layout.is_fluent() {
|
||||
if parenthesize_value {
|
||||
// Don't propagate the call chain layout.
|
||||
value.format().with_options(Parentheses::Always).fmt(f)?;
|
||||
} else {
|
||||
match value.as_ref() {
|
||||
Expr::Attribute(expr) => {
|
||||
expr.format().with_options(call_chain_layout).fmt(f)?;
|
||||
expr.format()
|
||||
.with_options(call_chain_layout.transition_after_attribute())
|
||||
.fmt(f)?;
|
||||
}
|
||||
Expr::Call(expr) => {
|
||||
expr.format().with_options(call_chain_layout).fmt(f)?;
|
||||
expr.format()
|
||||
.with_options(call_chain_layout.transition_after_attribute())
|
||||
.fmt(f)?;
|
||||
}
|
||||
Expr::Subscript(expr) => {
|
||||
expr.format().with_options(call_chain_layout).fmt(f)?;
|
||||
expr.format()
|
||||
.with_options(call_chain_layout.transition_after_attribute())
|
||||
.fmt(f)?;
|
||||
}
|
||||
_ => {
|
||||
value.format().with_options(Parentheses::Never).fmt(f)?;
|
||||
|
|
@ -105,8 +112,30 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
|
|||
// Allow the `.` on its own line if this is a fluent call chain
|
||||
// and the value either requires parenthesizing or is a call or subscript expression
|
||||
// (it's a fluent chain but not the first element).
|
||||
else if call_chain_layout == CallChainLayout::Fluent {
|
||||
if parenthesize_value || value.is_call_expr() || value.is_subscript_expr() {
|
||||
//
|
||||
// In preview we also break _at_ the first call in the chain.
|
||||
// For example:
|
||||
//
|
||||
// ```diff
|
||||
// # stable formatting vs. preview
|
||||
// x = (
|
||||
// - df.merge()
|
||||
// + df
|
||||
// + .merge()
|
||||
// .groupby()
|
||||
// .agg()
|
||||
// .filter()
|
||||
// )
|
||||
// ```
|
||||
else if call_chain_layout.is_fluent() {
|
||||
if parenthesize_value
|
||||
|| value.is_call_expr()
|
||||
|| value.is_subscript_expr()
|
||||
// Remember to update the doc-comment above when
|
||||
// stabilizing this behavior.
|
||||
|| (is_fluent_layout_split_first_call_enabled(f.context())
|
||||
&& call_chain_layout.is_first_call_like())
|
||||
{
|
||||
soft_line_break().fmt(f)?;
|
||||
}
|
||||
}
|
||||
|
|
@ -148,8 +177,8 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
|
|||
)
|
||||
});
|
||||
|
||||
let is_call_chain_root = self.call_chain_layout == CallChainLayout::Default
|
||||
&& call_chain_layout == CallChainLayout::Fluent;
|
||||
let is_call_chain_root =
|
||||
self.call_chain_layout == CallChainLayout::Default && call_chain_layout.is_fluent();
|
||||
if is_call_chain_root {
|
||||
write!(f, [group(&format_inner)])
|
||||
} else {
|
||||
|
|
@ -169,7 +198,8 @@ impl NeedsParentheses for ExprAttribute {
|
|||
self.into(),
|
||||
context.comments().ranges(),
|
||||
context.source(),
|
||||
) == CallChainLayout::Fluent
|
||||
)
|
||||
.is_fluent()
|
||||
{
|
||||
OptionalParentheses::Multiline
|
||||
} else if context.comments().has_dangling(self) {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
|
|||
func.format().with_options(Parentheses::Always).fmt(f)
|
||||
} else {
|
||||
match func.as_ref() {
|
||||
Expr::Attribute(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
Expr::Attribute(expr) => expr
|
||||
.format()
|
||||
.with_options(call_chain_layout.decrement_call_like_count())
|
||||
.fmt(f),
|
||||
Expr::Call(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
Expr::Subscript(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
_ => func.format().with_options(Parentheses::Never).fmt(f),
|
||||
|
|
@ -67,9 +70,7 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
|
|||
// queryset.distinct().order_by(field.name).values_list(field_name_flat_long_long=True)
|
||||
// )
|
||||
// ```
|
||||
if call_chain_layout == CallChainLayout::Fluent
|
||||
&& self.call_chain_layout == CallChainLayout::Default
|
||||
{
|
||||
if call_chain_layout.is_fluent() && self.call_chain_layout == CallChainLayout::Default {
|
||||
group(&fmt_func).fmt(f)
|
||||
} else {
|
||||
fmt_func.fmt(f)
|
||||
|
|
@ -87,7 +88,8 @@ impl NeedsParentheses for ExprCall {
|
|||
self.into(),
|
||||
context.comments().ranges(),
|
||||
context.source(),
|
||||
) == CallChainLayout::Fluent
|
||||
)
|
||||
.is_fluent()
|
||||
{
|
||||
OptionalParentheses::Multiline
|
||||
} else if context.comments().has_dangling(self) {
|
||||
|
|
|
|||
|
|
@ -397,7 +397,8 @@ impl Format<PyFormatContext<'_>> for FormatBody<'_> {
|
|||
body.into(),
|
||||
comments.ranges(),
|
||||
f.context().source(),
|
||||
) == CallChainLayout::Fluent
|
||||
)
|
||||
.is_fluent()
|
||||
{
|
||||
parenthesize_if_expands(&unparenthesized).fmt(f)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,10 @@ impl FormatNodeRule<ExprSubscript> for FormatExprSubscript {
|
|||
value.format().with_options(Parentheses::Always).fmt(f)
|
||||
} else {
|
||||
match value.as_ref() {
|
||||
Expr::Attribute(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
Expr::Attribute(expr) => expr
|
||||
.format()
|
||||
.with_options(call_chain_layout.decrement_call_like_count())
|
||||
.fmt(f),
|
||||
Expr::Call(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
Expr::Subscript(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
_ => value.format().with_options(Parentheses::Never).fmt(f),
|
||||
|
|
@ -71,8 +74,8 @@ impl FormatNodeRule<ExprSubscript> for FormatExprSubscript {
|
|||
.fmt(f)
|
||||
});
|
||||
|
||||
let is_call_chain_root = self.call_chain_layout == CallChainLayout::Default
|
||||
&& call_chain_layout == CallChainLayout::Fluent;
|
||||
let is_call_chain_root =
|
||||
self.call_chain_layout == CallChainLayout::Default && call_chain_layout.is_fluent();
|
||||
if is_call_chain_root {
|
||||
write!(f, [group(&format_inner)])
|
||||
} else {
|
||||
|
|
@ -92,7 +95,8 @@ impl NeedsParentheses for ExprSubscript {
|
|||
self.into(),
|
||||
context.comments().ranges(),
|
||||
context.source(),
|
||||
) == CallChainLayout::Fluent
|
||||
)
|
||||
.is_fluent()
|
||||
{
|
||||
OptionalParentheses::Multiline
|
||||
} else if is_expression_parenthesized(
|
||||
|
|
|
|||
|
|
@ -876,6 +876,22 @@ impl<'a> First<'a> {
|
|||
/// )
|
||||
/// ).all()
|
||||
/// ```
|
||||
///
|
||||
/// In [`preview`](crate::preview::is_fluent_layout_split_first_call_enabled), we also track the position of the leftmost call or
|
||||
/// subscript on an attribute in the chain and break just before the dot.
|
||||
///
|
||||
/// So, for example, the right-hand summand in the above expression
|
||||
/// would get formatted as:
|
||||
/// ```python
|
||||
/// Blog.objects
|
||||
/// .filter(
|
||||
/// entry__headline__contains="McCartney",
|
||||
/// )
|
||||
/// .limit_results[:10]
|
||||
/// .filter(
|
||||
/// entry__pub_date__year=2010,
|
||||
/// )
|
||||
/// ```
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum CallChainLayout {
|
||||
/// The root of a call chain
|
||||
|
|
@ -883,19 +899,149 @@ pub enum CallChainLayout {
|
|||
Default,
|
||||
|
||||
/// A nested call chain element that uses fluent style.
|
||||
Fluent,
|
||||
Fluent(AttributeState),
|
||||
|
||||
/// A nested call chain element not using fluent style.
|
||||
NonFluent,
|
||||
}
|
||||
|
||||
/// Records information about the current position within
|
||||
/// a call chain.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AttributeState {
|
||||
/// Stores the number of calls or subscripts
|
||||
/// to the left of the current position in a chain.
|
||||
///
|
||||
/// Consecutive calls/subscripts on a single
|
||||
/// object only count once. For example, if we are at
|
||||
/// `c` in `a.b()[0]()().c()` then this number would be 1.
|
||||
///
|
||||
/// Caveat: If the root of the chain is parenthesized,
|
||||
/// it contributes +1 to this count, even if it is not
|
||||
/// a call or subscript. But the name
|
||||
/// `CallLikeOrParenthesizedRootPreceding`
|
||||
/// is a tad unwieldy, and this also rarely occurs.
|
||||
CallLikePreceding(u32),
|
||||
/// Indicates that we are at the first called or
|
||||
/// subscripted object in the chain
|
||||
///
|
||||
/// For example, if we are at `b` in `a.b()[0]()().c()`
|
||||
FirstCallLike,
|
||||
/// Indicates that we are to the left of the first
|
||||
/// called or subscripted object in the chain, and therefore
|
||||
/// need not break.
|
||||
///
|
||||
/// For example, if we are at `a` in `a.b()[0]()().c()`
|
||||
BeforeFirstCallLike,
|
||||
}
|
||||
|
||||
impl CallChainLayout {
|
||||
/// Returns new state decreasing count of remaining calls/subscripts
|
||||
/// to traverse, or the state `FirstCallOrSubscript`, as appropriate.
|
||||
#[must_use]
|
||||
pub(crate) fn decrement_call_like_count(self) -> Self {
|
||||
match self {
|
||||
Self::Fluent(AttributeState::CallLikePreceding(x)) => {
|
||||
if x > 1 {
|
||||
// Recall that we traverse call chains from right to
|
||||
// left. So after moving from a call/subscript into
|
||||
// an attribute, we _decrease_ the count of
|
||||
// _remaining_ calls or subscripts to the left of our
|
||||
// current position.
|
||||
Self::Fluent(AttributeState::CallLikePreceding(x - 1))
|
||||
} else {
|
||||
Self::Fluent(AttributeState::FirstCallLike)
|
||||
}
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns with state change
|
||||
/// `FirstCallOrSubscript` -> `BeforeFirstCallOrSubscript`
|
||||
/// and otherwise returns unchanged.
|
||||
#[must_use]
|
||||
pub(crate) fn transition_after_attribute(self) -> Self {
|
||||
match self {
|
||||
Self::Fluent(AttributeState::FirstCallLike) => {
|
||||
Self::Fluent(AttributeState::BeforeFirstCallLike)
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_first_call_like(self) -> bool {
|
||||
matches!(self, Self::Fluent(AttributeState::FirstCallLike))
|
||||
}
|
||||
|
||||
/// Returns either `Fluent` or `NonFluent` depending on a
|
||||
/// heuristic computed for the whole chain.
|
||||
///
|
||||
/// Explicitly, the criterion to return `Fluent` is
|
||||
/// as follows:
|
||||
///
|
||||
/// 1. Beginning from the right (i.e. the `expr` itself),
|
||||
/// traverse inwards past calls, subscripts, and attribute
|
||||
/// expressions until we meet the first expression that is
|
||||
/// either none of these or else is parenthesized. This will
|
||||
/// be the _root_ of the call chain.
|
||||
/// 2. Count the number of _attribute values_ that are _called
|
||||
/// or subscripted_ in the chain (note that this includes the
|
||||
/// root but excludes the rightmost attribute in the chain since
|
||||
/// it is not the _value_ of some attribute).
|
||||
/// 3. If the root is parenthesized, add 1 to that value.
|
||||
/// 4. If the total is at least 2, return `Fluent`. Otherwise
|
||||
/// return `NonFluent`
|
||||
pub(crate) fn from_expression(
|
||||
mut expr: ExprRef,
|
||||
comment_ranges: &CommentRanges,
|
||||
source: &str,
|
||||
) -> Self {
|
||||
let mut attributes_after_parentheses = 0;
|
||||
// TODO(dylan): Once the fluent layout preview style is
|
||||
// stabilized, see if it is possible to simplify some of
|
||||
// the logic around parenthesized roots. (While supporting
|
||||
// both styles it is more difficult to do this.)
|
||||
|
||||
// Count of attribute _values_ which are called or
|
||||
// subscripted, after the leftmost parenthesized
|
||||
// value.
|
||||
//
|
||||
// Examples:
|
||||
// ```
|
||||
// # Count of 3 - notice that .d()
|
||||
// # does not contribute
|
||||
// a().b().c[0]()().d()
|
||||
// # Count of 2 - notice that a()
|
||||
// # does not contribute
|
||||
// (a()).b().c[0].d
|
||||
// ```
|
||||
let mut computed_attribute_values_after_parentheses = 0;
|
||||
|
||||
// Similar to the above, but instead looks at all calls
|
||||
// and subscripts rather than looking only at those on
|
||||
// _attribute values_. So this count can differ from the
|
||||
// above.
|
||||
//
|
||||
// Examples of `computed_attribute_values_after_parentheses` vs
|
||||
// `call_like_count`:
|
||||
//
|
||||
// a().b ---> 1 vs 1
|
||||
// a.b().c --> 1 vs 1
|
||||
// a.b() ---> 0 vs 1
|
||||
let mut call_like_count = 0;
|
||||
|
||||
// Going from right to left, we traverse calls, subscripts,
|
||||
// and attributes until we get to an expression of a different
|
||||
// kind _or_ to a parenthesized expression. This records
|
||||
// the case where we end the traversal at a parenthesized expression.
|
||||
//
|
||||
// In these cases, the inferred semantics of the chain are different.
|
||||
// We interpret this as the user indicating:
|
||||
// "this parenthesized value is the object of interest and we are
|
||||
// doing transformations on it". This increases our confidence that
|
||||
// this should be fluently formatted, and also means we should make
|
||||
// our first break after this value.
|
||||
let mut root_value_parenthesized = false;
|
||||
loop {
|
||||
match expr {
|
||||
ExprRef::Attribute(ast::ExprAttribute { value, .. }) => {
|
||||
|
|
@ -907,10 +1053,10 @@ impl CallChainLayout {
|
|||
// ```
|
||||
if is_expression_parenthesized(value.into(), comment_ranges, source) {
|
||||
// `(a).b`. We preserve these parentheses so don't recurse
|
||||
attributes_after_parentheses += 1;
|
||||
root_value_parenthesized = true;
|
||||
break;
|
||||
} else if matches!(value.as_ref(), Expr::Call(_) | Expr::Subscript(_)) {
|
||||
attributes_after_parentheses += 1;
|
||||
computed_attribute_values_after_parentheses += 1;
|
||||
}
|
||||
|
||||
expr = ExprRef::from(value.as_ref());
|
||||
|
|
@ -925,31 +1071,68 @@ impl CallChainLayout {
|
|||
// ```
|
||||
ExprRef::Call(ast::ExprCall { func: inner, .. })
|
||||
| ExprRef::Subscript(ast::ExprSubscript { value: inner, .. }) => {
|
||||
// We preserve these parentheses so don't recurse
|
||||
// e.g. (a)[0].x().y().z()
|
||||
// ^stop here
|
||||
if is_expression_parenthesized(inner.into(), comment_ranges, source) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Accumulate the `call_like_count`, but we only
|
||||
// want to count things like `a()[0]()()` once.
|
||||
if !inner.is_call_expr() && !inner.is_subscript_expr() {
|
||||
call_like_count += 1;
|
||||
}
|
||||
|
||||
expr = ExprRef::from(inner.as_ref());
|
||||
}
|
||||
_ => {
|
||||
// We to format the following in fluent style:
|
||||
// ```
|
||||
// f2 = (a).w().t(1,)
|
||||
// ^ expr
|
||||
// ```
|
||||
if is_expression_parenthesized(expr, comment_ranges, source) {
|
||||
attributes_after_parentheses += 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We preserve these parentheses so don't recurse
|
||||
if is_expression_parenthesized(expr, comment_ranges, source) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if attributes_after_parentheses < 2 {
|
||||
|
||||
if computed_attribute_values_after_parentheses + u32::from(root_value_parenthesized) < 2 {
|
||||
CallChainLayout::NonFluent
|
||||
} else {
|
||||
CallChainLayout::Fluent
|
||||
CallChainLayout::Fluent(AttributeState::CallLikePreceding(
|
||||
// We count a parenthesized root value as an extra
|
||||
// call for the purposes of tracking state.
|
||||
//
|
||||
// The reason is that, in this case, we want the first
|
||||
// "special" break to happen right after the root, as
|
||||
// opposed to right after the first called/subscripted
|
||||
// attribute.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// ```
|
||||
// (object_of_interest)
|
||||
// .data.filter()
|
||||
// .agg()
|
||||
// .etc()
|
||||
// ```
|
||||
//
|
||||
// instead of (in preview):
|
||||
//
|
||||
// ```
|
||||
// (object_of_interest)
|
||||
// .data
|
||||
// .filter()
|
||||
// .etc()
|
||||
// ```
|
||||
//
|
||||
// For comparison, if we didn't have parentheses around
|
||||
// the root, we want (and get, in preview):
|
||||
//
|
||||
// ```
|
||||
// object_of_interest.data
|
||||
// .filter()
|
||||
// .agg()
|
||||
// .etc()
|
||||
// ```
|
||||
call_like_count + u32::from(root_value_parenthesized),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -972,9 +1155,13 @@ impl CallChainLayout {
|
|||
CallChainLayout::NonFluent
|
||||
}
|
||||
}
|
||||
layout @ (CallChainLayout::Fluent | CallChainLayout::NonFluent) => layout,
|
||||
layout @ (CallChainLayout::Fluent(_) | CallChainLayout::NonFluent) => layout,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_fluent(self) -> bool {
|
||||
matches!(self, CallChainLayout::Fluent(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -59,3 +59,10 @@ pub(crate) const fn is_avoid_parens_for_long_as_captures_enabled(
|
|||
pub(crate) const fn is_parenthesize_lambda_bodies_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// Returns `true` if the
|
||||
/// [`fluent_layout_split_first_call`](https://github.com/astral-sh/ruff/pull/21369) preview
|
||||
/// style is enabled.
|
||||
pub(crate) const fn is_fluent_layout_split_first_call_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ class Random:
|
|||
}
|
||||
x = {
|
||||
"foobar": (123) + 456,
|
||||
@@ -97,24 +94,20 @@
|
||||
@@ -97,24 +94,21 @@
|
||||
|
||||
|
||||
my_dict = {
|
||||
|
|
@ -221,13 +221,14 @@ class Random:
|
|||
- .second_call()
|
||||
- .third_call(some_args="some value")
|
||||
- )
|
||||
+ "a key in my dict": MyClass.some_attribute.first_call()
|
||||
+ "a key in my dict": MyClass.some_attribute
|
||||
+ .first_call()
|
||||
+ .second_call()
|
||||
+ .third_call(some_args="some value")
|
||||
}
|
||||
|
||||
{
|
||||
@@ -139,17 +132,17 @@
|
||||
@@ -139,17 +133,17 @@
|
||||
|
||||
class Random:
|
||||
def func():
|
||||
|
|
@ -363,7 +364,8 @@ my_dict = {
|
|||
/ 100000.0
|
||||
}
|
||||
my_dict = {
|
||||
"a key in my dict": MyClass.some_attribute.first_call()
|
||||
"a key in my dict": MyClass.some_attribute
|
||||
.first_call()
|
||||
.second_call()
|
||||
.third_call(some_args="some value")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/await.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
|
|
@ -142,3 +141,20 @@ test_data = await (
|
|||
.to_list()
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -65,7 +65,8 @@
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/8644
|
||||
test_data = await (
|
||||
- Stream.from_async(async_data)
|
||||
+ Stream
|
||||
+ .from_async(async_data)
|
||||
.flat_map_async()
|
||||
.map()
|
||||
.filter_async(is_valid_data)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
|
|
@ -557,3 +556,20 @@ result = (
|
|||
|
||||
result = (object[complicate_caller])("argument").a["b"].test(argument)
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -57,7 +57,8 @@
|
||||
|
||||
# Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains)
|
||||
result = (
|
||||
- session.query(models.Customer.id)
|
||||
+ session
|
||||
+ .query(models.Customer.id)
|
||||
.filter(
|
||||
models.Customer.account_id == 10000,
|
||||
models.Customer.email == "user@example.org",
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2155,7 +2155,7 @@ transform = (
|
|||
),
|
||||
param(
|
||||
lambda left, right: (
|
||||
@@ -471,9 +463,9 @@
|
||||
@@ -471,15 +463,16 @@
|
||||
),
|
||||
param(lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date)),
|
||||
param(
|
||||
|
|
@ -2168,7 +2168,15 @@ transform = (
|
|||
),
|
||||
# This is too long on one line in the lambda body and gets wrapped
|
||||
# inside the body.
|
||||
@@ -507,16 +499,18 @@
|
||||
param(
|
||||
lambda left, right: (
|
||||
- ibis.timestamp("2017-04-01")
|
||||
+ ibis
|
||||
+ .timestamp("2017-04-01")
|
||||
.cast(dt.date)
|
||||
.between(left, right)
|
||||
.between(left, right)
|
||||
@@ -507,16 +500,18 @@
|
||||
]
|
||||
|
||||
# adds parentheses around the body
|
||||
|
|
@ -2190,7 +2198,7 @@ transform = (
|
|||
|
||||
lambda x, y, z: (
|
||||
x + y + z
|
||||
@@ -527,7 +521,7 @@
|
||||
@@ -527,7 +522,7 @@
|
||||
x + y + z # trailing eol body
|
||||
)
|
||||
|
||||
|
|
@ -2199,7 +2207,7 @@ transform = (
|
|||
|
||||
lambda x, y, z: (
|
||||
# leading body
|
||||
@@ -539,21 +533,23 @@
|
||||
@@ -539,21 +534,23 @@
|
||||
)
|
||||
|
||||
(
|
||||
|
|
@ -2233,7 +2241,7 @@ transform = (
|
|||
# dangling header comment
|
||||
source_bucket
|
||||
if name == source_bucket_name
|
||||
@@ -561,8 +557,7 @@
|
||||
@@ -561,8 +558,7 @@
|
||||
)
|
||||
|
||||
(
|
||||
|
|
@ -2243,7 +2251,7 @@ transform = (
|
|||
source_bucket
|
||||
if name == source_bucket_name
|
||||
else storage.Bucket(mock_service, destination_bucket_name)
|
||||
@@ -570,61 +565,70 @@
|
||||
@@ -570,61 +566,71 @@
|
||||
)
|
||||
|
||||
(
|
||||
|
|
@ -2293,7 +2301,8 @@ transform = (
|
|||
- .cast(dt.date)
|
||||
- .between(left, right)
|
||||
+ lambda left, right: (
|
||||
+ ibis.timestamp("2017-04-01") # comment
|
||||
+ ibis
|
||||
+ .timestamp("2017-04-01") # comment
|
||||
+ .cast(dt.date)
|
||||
+ .between(left, right)
|
||||
+ )
|
||||
|
|
@ -2346,7 +2355,7 @@ transform = (
|
|||
)
|
||||
|
||||
(
|
||||
@@ -637,27 +641,31 @@
|
||||
@@ -637,27 +643,31 @@
|
||||
(
|
||||
lambda
|
||||
# comment
|
||||
|
|
@ -2386,7 +2395,7 @@ transform = (
|
|||
)
|
||||
|
||||
(
|
||||
@@ -672,25 +680,28 @@
|
||||
@@ -672,25 +682,28 @@
|
||||
)
|
||||
|
||||
(
|
||||
|
|
@ -2427,7 +2436,7 @@ transform = (
|
|||
)
|
||||
|
||||
(
|
||||
@@ -698,9 +709,9 @@
|
||||
@@ -698,9 +711,9 @@
|
||||
# comment 1
|
||||
*ergs,
|
||||
# comment 2
|
||||
|
|
@ -2440,7 +2449,7 @@ transform = (
|
|||
)
|
||||
|
||||
(
|
||||
@@ -708,19 +719,20 @@
|
||||
@@ -708,19 +721,20 @@
|
||||
# 2
|
||||
left, # 3
|
||||
# 4
|
||||
|
|
@ -2471,7 +2480,7 @@ transform = (
|
|||
)
|
||||
)
|
||||
)
|
||||
@@ -738,48 +750,52 @@
|
||||
@@ -738,48 +752,52 @@
|
||||
foo(
|
||||
lambda from_ts, # but still wrap the body if it gets too long
|
||||
to_ts,
|
||||
|
|
@ -2548,7 +2557,7 @@ transform = (
|
|||
)
|
||||
|
||||
(
|
||||
@@ -828,8 +844,7 @@
|
||||
@@ -828,8 +846,7 @@
|
||||
)
|
||||
|
||||
(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/split_empty_brackets.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fluent.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
# Fixtures for fluent formatting of call chains
|
||||
# Note that `fluent.options.json` sets line width to 8
|
||||
|
||||
|
||||
x = a.b()
|
||||
|
||||
x = a.b().c()
|
||||
|
||||
x = a.b().c().d
|
||||
|
||||
x = a.b.c.d().e()
|
||||
|
||||
x = a.b.c().d.e().f.g()
|
||||
|
||||
# Consecutive calls/subscripts are grouped together
|
||||
# for the purposes of fluent formatting (though, as 2025.12.15,
|
||||
# there may be a break inside of one of these
|
||||
# calls/subscripts, but that is unrelated to the fluent format.)
|
||||
|
||||
x = a()[0]().b().c()
|
||||
|
||||
x = a.b()[0].c.d()[1]().e
|
||||
|
||||
# Parentheses affect both where the root of the call
|
||||
# chain is and how many calls we require before applying
|
||||
# fluent formatting (just 1, in the presence of a parenthesized
|
||||
# root, as of 2025.12.15.)
|
||||
|
||||
x = (a).b()
|
||||
|
||||
x = (a()).b()
|
||||
|
||||
x = (a.b()).d.e()
|
||||
|
||||
x = (a.b().d).e()
|
||||
```
|
||||
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 8
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Disabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Disabled
|
||||
target_version = 3.10
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
# Fixtures for fluent formatting of call chains
|
||||
# Note that `fluent.options.json` sets line width to 8
|
||||
|
||||
|
||||
x = a.b()
|
||||
|
||||
x = a.b().c()
|
||||
|
||||
x = (
|
||||
a.b()
|
||||
.c()
|
||||
.d
|
||||
)
|
||||
|
||||
x = a.b.c.d().e()
|
||||
|
||||
x = (
|
||||
a.b.c()
|
||||
.d.e()
|
||||
.f.g()
|
||||
)
|
||||
|
||||
# Consecutive calls/subscripts are grouped together
|
||||
# for the purposes of fluent formatting (though, as 2025.12.15,
|
||||
# there may be a break inside of one of these
|
||||
# calls/subscripts, but that is unrelated to the fluent format.)
|
||||
|
||||
x = (
|
||||
a()[
|
||||
0
|
||||
]()
|
||||
.b()
|
||||
.c()
|
||||
)
|
||||
|
||||
x = (
|
||||
a.b()[
|
||||
0
|
||||
]
|
||||
.c.d()[
|
||||
1
|
||||
]()
|
||||
.e
|
||||
)
|
||||
|
||||
# Parentheses affect both where the root of the call
|
||||
# chain is and how many calls we require before applying
|
||||
# fluent formatting (just 1, in the presence of a parenthesized
|
||||
# root, as of 2025.12.15.)
|
||||
|
||||
x = (
|
||||
a
|
||||
).b()
|
||||
|
||||
x = (
|
||||
a()
|
||||
).b()
|
||||
|
||||
x = (
|
||||
a.b()
|
||||
).d.e()
|
||||
|
||||
x = (
|
||||
a.b().d
|
||||
).e()
|
||||
```
|
||||
|
||||
|
||||
#### Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -7,7 +7,8 @@
|
||||
x = a.b().c()
|
||||
|
||||
x = (
|
||||
- a.b()
|
||||
+ a
|
||||
+ .b()
|
||||
.c()
|
||||
.d
|
||||
)
|
||||
@@ -15,7 +16,8 @@
|
||||
x = a.b.c.d().e()
|
||||
|
||||
x = (
|
||||
- a.b.c()
|
||||
+ a.b
|
||||
+ .c()
|
||||
.d.e()
|
||||
.f.g()
|
||||
)
|
||||
@@ -34,7 +36,8 @@
|
||||
)
|
||||
|
||||
x = (
|
||||
- a.b()[
|
||||
+ a
|
||||
+ .b()[
|
||||
0
|
||||
]
|
||||
.c.d()[
|
||||
```
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/parentheses/call_chains.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
|
|
@ -223,6 +222,72 @@ max_message_id = (
|
|||
.baz()
|
||||
)
|
||||
|
||||
# Note in preview we split at `pl` which some
|
||||
# folks may dislike. (Similarly with common
|
||||
# `np` and `pd` invocations).
|
||||
#
|
||||
# This is because we cannot reliably predict,
|
||||
# just from syntax, whether a short identifier
|
||||
# is being used as a 'namespace' or as an 'object'.
|
||||
#
|
||||
# As of 2025.12.15, we do not indent methods in
|
||||
# fluent formatting. If we ever decide to do so,
|
||||
# it may make sense to special case call chain roots
|
||||
# that are shorter than the indent-width (like Prettier does).
|
||||
# This would have the benefit of handling these common
|
||||
# two-letter aliases for libraries.
|
||||
|
||||
|
||||
expr = (
|
||||
pl.scan_parquet("/data/pypi-parquet/*.parquet")
|
||||
.filter(
|
||||
[
|
||||
pl.col("path").str.contains(
|
||||
r"\.(asm|c|cc|cpp|cxx|h|hpp|rs|[Ff][0-9]{0,2}(?:or)?|go)$"
|
||||
),
|
||||
~pl.col("path").str.contains(r"(^|/)test(|s|ing)"),
|
||||
~pl.col("path").str.contains("/site-packages/", literal=True),
|
||||
]
|
||||
)
|
||||
.with_columns(
|
||||
month=pl.col("uploaded_on").dt.truncate("1mo"),
|
||||
ext=pl.col("path")
|
||||
.str.extract(pattern=r"\.([a-z0-9]+)$", group_index=1)
|
||||
.str.replace_all(pattern=r"cxx|cpp|cc|c|hpp|h", value="C/C++")
|
||||
.str.replace_all(pattern="^f.*$", value="Fortran")
|
||||
.str.replace("rs", "Rust", literal=True)
|
||||
.str.replace("go", "Go", literal=True)
|
||||
.str.replace("asm", "Assembly", literal=True)
|
||||
.replace({"": None}),
|
||||
)
|
||||
.group_by(["month", "ext"])
|
||||
.agg(project_count=pl.col("project_name").n_unique())
|
||||
.drop_nulls(["ext"])
|
||||
.sort(["month", "project_count"], descending=True)
|
||||
)
|
||||
|
||||
def indentation_matching_for_loop_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
identical_hidden_layer_sizes = all(
|
||||
current_hidden_layer_sizes == first_hidden_layer_sizes
|
||||
for current_hidden_layer_sizes in self.component_config[
|
||||
HIDDEN_LAYERS_SIZES
|
||||
].values().attr
|
||||
)
|
||||
|
||||
def indentation_matching_walrus_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
with self.read_ctx(book_type) as cursor:
|
||||
if (entry_count := len(names := cursor.execute(
|
||||
'SELECT name FROM address_book WHERE address=?',
|
||||
(address,),
|
||||
).fetchall().some_attr)) == 0 or len(set(names)) > 1:
|
||||
return
|
||||
|
||||
# behavior with parenthesized roots
|
||||
x = (aaaaaaaaaaaaaaaaaaaaaa).bbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccc().dddddddddddddddddddddddd().eeeeeeeeeeee
|
||||
```
|
||||
|
||||
## Output
|
||||
|
|
@ -466,4 +531,237 @@ max_message_id = (
|
|||
.sum()
|
||||
.baz()
|
||||
)
|
||||
|
||||
# Note in preview we split at `pl` which some
|
||||
# folks may dislike. (Similarly with common
|
||||
# `np` and `pd` invocations).
|
||||
#
|
||||
# This is because we cannot reliably predict,
|
||||
# just from syntax, whether a short identifier
|
||||
# is being used as a 'namespace' or as an 'object'.
|
||||
#
|
||||
# As of 2025.12.15, we do not indent methods in
|
||||
# fluent formatting. If we ever decide to do so,
|
||||
# it may make sense to special case call chain roots
|
||||
# that are shorter than the indent-width (like Prettier does).
|
||||
# This would have the benefit of handling these common
|
||||
# two-letter aliases for libraries.
|
||||
|
||||
|
||||
expr = (
|
||||
pl.scan_parquet("/data/pypi-parquet/*.parquet")
|
||||
.filter(
|
||||
[
|
||||
pl.col("path").str.contains(
|
||||
r"\.(asm|c|cc|cpp|cxx|h|hpp|rs|[Ff][0-9]{0,2}(?:or)?|go)$"
|
||||
),
|
||||
~pl.col("path").str.contains(r"(^|/)test(|s|ing)"),
|
||||
~pl.col("path").str.contains("/site-packages/", literal=True),
|
||||
]
|
||||
)
|
||||
.with_columns(
|
||||
month=pl.col("uploaded_on").dt.truncate("1mo"),
|
||||
ext=pl.col("path")
|
||||
.str.extract(pattern=r"\.([a-z0-9]+)$", group_index=1)
|
||||
.str.replace_all(pattern=r"cxx|cpp|cc|c|hpp|h", value="C/C++")
|
||||
.str.replace_all(pattern="^f.*$", value="Fortran")
|
||||
.str.replace("rs", "Rust", literal=True)
|
||||
.str.replace("go", "Go", literal=True)
|
||||
.str.replace("asm", "Assembly", literal=True)
|
||||
.replace({"": None}),
|
||||
)
|
||||
.group_by(["month", "ext"])
|
||||
.agg(project_count=pl.col("project_name").n_unique())
|
||||
.drop_nulls(["ext"])
|
||||
.sort(["month", "project_count"], descending=True)
|
||||
)
|
||||
|
||||
|
||||
def indentation_matching_for_loop_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
identical_hidden_layer_sizes = all(
|
||||
current_hidden_layer_sizes == first_hidden_layer_sizes
|
||||
for current_hidden_layer_sizes in self.component_config[
|
||||
HIDDEN_LAYERS_SIZES
|
||||
]
|
||||
.values()
|
||||
.attr
|
||||
)
|
||||
|
||||
|
||||
def indentation_matching_walrus_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
with self.read_ctx(book_type) as cursor:
|
||||
if (
|
||||
entry_count := len(
|
||||
names := cursor.execute(
|
||||
"SELECT name FROM address_book WHERE address=?",
|
||||
(address,),
|
||||
)
|
||||
.fetchall()
|
||||
.some_attr
|
||||
)
|
||||
) == 0 or len(set(names)) > 1:
|
||||
return
|
||||
|
||||
|
||||
# behavior with parenthesized roots
|
||||
x = (
|
||||
(aaaaaaaaaaaaaaaaaaaaaa)
|
||||
.bbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccc()
|
||||
.dddddddddddddddddddddddd()
|
||||
.eeeeeeeeeeee
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -21,7 +21,8 @@
|
||||
)
|
||||
|
||||
raise OsError("") from (
|
||||
- Blog.objects.filter(
|
||||
+ Blog.objects
|
||||
+ .filter(
|
||||
entry__headline__contains="Lennon",
|
||||
)
|
||||
.filter(
|
||||
@@ -33,7 +34,8 @@
|
||||
)
|
||||
|
||||
raise OsError("sökdjffffsldkfjlhsakfjhalsökafhsöfdahsödfjösaaksjdllllllllllllll") from (
|
||||
- Blog.objects.filter(
|
||||
+ Blog.objects
|
||||
+ .filter(
|
||||
entry__headline__contains="Lennon",
|
||||
)
|
||||
.filter(
|
||||
@@ -46,7 +48,8 @@
|
||||
|
||||
# Break only after calls and indexing
|
||||
b1 = (
|
||||
- session.query(models.Customer.id)
|
||||
+ session
|
||||
+ .query(models.Customer.id)
|
||||
.filter(
|
||||
models.Customer.account_id == account_id, models.Customer.email == email_address
|
||||
)
|
||||
@@ -54,7 +57,8 @@
|
||||
)
|
||||
|
||||
b2 = (
|
||||
- Blog.objects.filter(
|
||||
+ Blog.objects
|
||||
+ .filter(
|
||||
entry__headline__contains="Lennon",
|
||||
)
|
||||
.limit_results[:10]
|
||||
@@ -70,7 +74,8 @@
|
||||
).filter(
|
||||
entry__pub_date__year=2008,
|
||||
)
|
||||
- + Blog.objects.filter(
|
||||
+ + Blog.objects
|
||||
+ .filter(
|
||||
entry__headline__contains="McCartney",
|
||||
)
|
||||
.limit_results[:10]
|
||||
@@ -89,7 +94,8 @@
|
||||
d11 = x.e().e().e() #
|
||||
d12 = x.e().e().e() #
|
||||
d13 = (
|
||||
- x.e() #
|
||||
+ x
|
||||
+ .e() #
|
||||
.e()
|
||||
.e()
|
||||
)
|
||||
@@ -101,7 +107,8 @@
|
||||
|
||||
# Doesn't fit, fluent style
|
||||
d3 = (
|
||||
- x.e() #
|
||||
+ x
|
||||
+ .e() #
|
||||
.esadjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk()
|
||||
.esadjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk()
|
||||
)
|
||||
@@ -218,7 +225,8 @@
|
||||
|
||||
(
|
||||
(
|
||||
- df1_aaaaaaaaaaaa.merge()
|
||||
+ df1_aaaaaaaaaaaa
|
||||
+ .merge()
|
||||
.groupby(
|
||||
1,
|
||||
)
|
||||
@@ -228,7 +236,8 @@
|
||||
|
||||
(
|
||||
(
|
||||
- df1_aaaaaaaaaaaa.merge()
|
||||
+ df1_aaaaaaaaaaaa
|
||||
+ .merge()
|
||||
.groupby(
|
||||
1,
|
||||
)
|
||||
@@ -255,19 +264,19 @@
|
||||
|
||||
|
||||
expr = (
|
||||
- pl.scan_parquet("/data/pypi-parquet/*.parquet")
|
||||
- .filter(
|
||||
- [
|
||||
- pl.col("path").str.contains(
|
||||
- r"\.(asm|c|cc|cpp|cxx|h|hpp|rs|[Ff][0-9]{0,2}(?:or)?|go)$"
|
||||
- ),
|
||||
- ~pl.col("path").str.contains(r"(^|/)test(|s|ing)"),
|
||||
- ~pl.col("path").str.contains("/site-packages/", literal=True),
|
||||
- ]
|
||||
- )
|
||||
+ pl
|
||||
+ .scan_parquet("/data/pypi-parquet/*.parquet")
|
||||
+ .filter([
|
||||
+ pl.col("path").str.contains(
|
||||
+ r"\.(asm|c|cc|cpp|cxx|h|hpp|rs|[Ff][0-9]{0,2}(?:or)?|go)$"
|
||||
+ ),
|
||||
+ ~pl.col("path").str.contains(r"(^|/)test(|s|ing)"),
|
||||
+ ~pl.col("path").str.contains("/site-packages/", literal=True),
|
||||
+ ])
|
||||
.with_columns(
|
||||
month=pl.col("uploaded_on").dt.truncate("1mo"),
|
||||
- ext=pl.col("path")
|
||||
+ ext=pl
|
||||
+ .col("path")
|
||||
.str.extract(pattern=r"\.([a-z0-9]+)$", group_index=1)
|
||||
.str.replace_all(pattern=r"cxx|cpp|cc|c|hpp|h", value="C/C++")
|
||||
.str.replace_all(pattern="^f.*$", value="Fortran")
|
||||
@@ -288,9 +297,8 @@
|
||||
if more_nested_because_line_length:
|
||||
identical_hidden_layer_sizes = all(
|
||||
current_hidden_layer_sizes == first_hidden_layer_sizes
|
||||
- for current_hidden_layer_sizes in self.component_config[
|
||||
- HIDDEN_LAYERS_SIZES
|
||||
- ]
|
||||
+ for current_hidden_layer_sizes in self
|
||||
+ .component_config[HIDDEN_LAYERS_SIZES]
|
||||
.values()
|
||||
.attr
|
||||
)
|
||||
@@ -302,7 +310,8 @@
|
||||
with self.read_ctx(book_type) as cursor:
|
||||
if (
|
||||
entry_count := len(
|
||||
- names := cursor.execute(
|
||||
+ names := cursor
|
||||
+ .execute(
|
||||
"SELECT name FROM address_book WHERE address=?",
|
||||
(address,),
|
||||
)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -51,5 +51,11 @@ regex = { workspace = true }
|
|||
tempfile = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[target.'cfg(all(not(target_os = "macos"), not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
|
||||
tikv-jemallocator = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -557,7 +557,7 @@ a: int = ''
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1998" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2003" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -751,7 +751,7 @@ except ZeroDivisionError:
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.28">0.0.1-alpha.28</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1668" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1673" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -793,7 +793,7 @@ class D(A):
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.35">0.0.1-alpha.35</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-frozen-dataclass-subclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2224" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2229" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -848,16 +848,21 @@ Checks for the creation of invalid generic classes
|
|||
**Why is this bad?**
|
||||
|
||||
There are several requirements that you must follow when defining a generic class.
|
||||
Many of these result in `TypeError` being raised at runtime if they are violated.
|
||||
|
||||
**Examples**
|
||||
|
||||
```python
|
||||
from typing import Generic, TypeVar
|
||||
from typing_extensions import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T") # okay
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U", default=int)
|
||||
|
||||
# error: class uses both PEP-695 syntax and legacy syntax
|
||||
class C[U](Generic[T]): ...
|
||||
|
||||
# error: type parameter with default comes before type parameter without default
|
||||
class D(Generic[U, T]): ...
|
||||
```
|
||||
|
||||
**References**
|
||||
|
|
@ -909,7 +914,7 @@ carol = Person(name="Carol", age=25) # typo!
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L917" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L922" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -944,7 +949,7 @@ def f(t: TypeVar("U")): ...
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1014" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1019" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -978,7 +983,7 @@ class B(metaclass=f): ...
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2126" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2131" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1139,7 +1144,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L990" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L995" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1169,7 +1174,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1041" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1046" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1219,7 +1224,7 @@ def foo(x: int) -> int: ...
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1140" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1145" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1245,7 +1250,7 @@ def f(a: int = ''): ...
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L945" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L950" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1310,7 +1315,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1160" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1165" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1384,7 +1389,7 @@ def func() -> int:
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1203" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1208" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1442,7 +1447,7 @@ TODO #14889
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L969" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L974" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1469,7 +1474,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1435" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1440" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1516,7 +1521,7 @@ Bar[int] # error: too few arguments
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1242" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1247" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1546,7 +1551,7 @@ TYPE_CHECKING = ''
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1266" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1271" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1576,7 +1581,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1318" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1610,7 +1615,7 @@ f(10) # Error
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1295" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1644,7 +1649,7 @@ class C:
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1346" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1351" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1679,7 +1684,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1375" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1380" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1704,7 +1709,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2099" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2104" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1737,7 +1742,7 @@ alice["age"] # KeyError
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1394" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1399" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1766,7 +1771,7 @@ func("string") # error: [no-matching-overload]
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1417" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1422" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1790,7 +1795,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1476" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1481" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1816,7 +1821,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1641" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1646" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1849,7 +1854,7 @@ class B(A):
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1527" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1532" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1876,7 +1881,7 @@ f(1, x=2) # Error raised here
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1852" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1857" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1934,7 +1939,7 @@ def test(): -> "int":
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1974" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1979" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1964,7 +1969,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1618" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1623" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -1993,7 +1998,7 @@ class B(A): ... # Error raised here
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.30">0.0.1-alpha.30</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20super-call-in-named-tuple-method" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1786" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1791" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2027,7 +2032,7 @@ class F(NamedTuple):
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1726" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1731" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2054,7 +2059,7 @@ f("foo") # Error raised here
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1704" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1709" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2082,7 +2087,7 @@ def _(x: int):
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1747" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1752" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2128,7 +2133,7 @@ class A:
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1831" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1836" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2155,7 +2160,7 @@ f(x=1, y=2) # Error raised here
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1873" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1878" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2183,7 +2188,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1895" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1900" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2208,7 +2213,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1914" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1919" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2233,7 +2238,7 @@ print(x) # NameError: name 'x' is not defined
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1496" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1501" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2270,7 +2275,7 @@ b1 < b2 < b1 # exception raised here
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1933" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1938" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2298,7 +2303,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1955" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1960" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2452,7 +2457,7 @@ a = 20 / 0 # type: ignore
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1548" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1553" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2512,7 +2517,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1570" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1575" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2544,7 +2549,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2026" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2031" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2571,7 +2576,7 @@ cast(int, f()) # Redundant
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1813" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1818" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2595,7 +2600,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2047" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2052" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2692,7 +2697,7 @@ class D(C): ... # error: [unsupported-base]
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1084" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1089" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
@ -2779,7 +2784,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
|||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1596" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1601" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,22 @@ use colored::Colorize;
|
|||
use std::io;
|
||||
use ty::{ExitStatus, run};
|
||||
|
||||
#[cfg(all(
|
||||
not(target_os = "macos"),
|
||||
not(target_os = "windows"),
|
||||
not(target_os = "openbsd"),
|
||||
not(target_os = "aix"),
|
||||
not(target_os = "android"),
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
target_arch = "powerpc64",
|
||||
target_arch = "riscv64"
|
||||
)
|
||||
))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
||||
|
||||
pub fn main() -> ExitStatus {
|
||||
run().unwrap_or_else(|error| {
|
||||
use io::Write;
|
||||
|
|
|
|||
|
|
@ -29,12 +29,11 @@ pub fn code_actions(
|
|||
|
||||
let mut actions = Vec::new();
|
||||
|
||||
// Suggest imports for unresolved references (often ideal)
|
||||
// TODO: suggest qualifying with an already imported symbol
|
||||
// Suggest imports/qualifications for unresolved references (often ideal)
|
||||
let is_unresolved_reference =
|
||||
lint_id == LintId::of(&UNRESOLVED_REFERENCE) || lint_id == LintId::of(&UNDEFINED_REVEAL);
|
||||
if is_unresolved_reference
|
||||
&& let Some(import_quick_fix) = create_import_symbol_quick_fix(db, file, diagnostic_range)
|
||||
&& let Some(import_quick_fix) = unresolved_fixes(db, file, diagnostic_range)
|
||||
{
|
||||
actions.extend(import_quick_fix);
|
||||
}
|
||||
|
|
@ -49,7 +48,7 @@ pub fn code_actions(
|
|||
actions
|
||||
}
|
||||
|
||||
fn create_import_symbol_quick_fix(
|
||||
fn unresolved_fixes(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
diagnostic_range: TextRange,
|
||||
|
|
@ -59,7 +58,7 @@ fn create_import_symbol_quick_fix(
|
|||
let symbol = &node.expr_name()?.id;
|
||||
|
||||
Some(
|
||||
completion::missing_imports(db, file, &parsed, symbol, node)
|
||||
completion::unresolved_fixes(db, file, &parsed, symbol, node)
|
||||
.into_iter()
|
||||
.map(|import| QuickFix {
|
||||
title: import.label,
|
||||
|
|
@ -84,6 +83,7 @@ mod tests {
|
|||
system::{DbWithWritableSystem, SystemPathBuf},
|
||||
};
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ty_project::ProjectMetadata;
|
||||
use ty_python_semantic::{
|
||||
|
|
@ -149,15 +149,14 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero]
|
||||
| ^
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero]
|
||||
| ^
|
||||
|
|
||||
1 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero]
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||
3 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero]
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -171,15 +170,14 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero,]
|
||||
| ^
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero,]
|
||||
| ^
|
||||
|
|
||||
1 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero,]
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||
3 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero,]
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -193,15 +191,14 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero ]
|
||||
| ^
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero ]
|
||||
| ^
|
||||
|
|
||||
1 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero ]
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference ]
|
||||
3 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero ]
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference ]
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -215,15 +212,14 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero] some explanation
|
||||
| ^
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero] some explanation
|
||||
| ^
|
||||
|
|
||||
1 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero] some explanation
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty:ignore[unresolved-reference]
|
||||
3 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero] some explanation
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty:ignore[unresolved-reference]
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -241,22 +237,22 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:3:21
|
||||
--> main.py:3:9
|
||||
|
|
||||
2 | b = (
|
||||
3 | / a # ty:ignore[division-by-zero]
|
||||
4 | | /
|
||||
5 | | 0
|
||||
| |_____________________^
|
||||
6 | )
|
||||
2 | b = (
|
||||
3 | / a # ty:ignore[division-by-zero]
|
||||
4 | | /
|
||||
5 | | 0
|
||||
| |_________^
|
||||
6 | )
|
||||
|
|
||||
1 |
|
||||
2 | b = (
|
||||
- a # ty:ignore[division-by-zero]
|
||||
3 + a # ty:ignore[division-by-zero, unresolved-reference]
|
||||
4 | /
|
||||
5 | 0
|
||||
6 | )
|
||||
2 | b = (
|
||||
- a # ty:ignore[division-by-zero]
|
||||
3 + a # ty:ignore[division-by-zero, unresolved-reference]
|
||||
4 | /
|
||||
5 | 0
|
||||
6 | )
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -274,22 +270,21 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:3:21
|
||||
--> main.py:3:9
|
||||
|
|
||||
2 | b = (
|
||||
3 | / a
|
||||
4 | | /
|
||||
5 | | 0 # ty:ignore[division-by-zero]
|
||||
| |_____________________^
|
||||
6 | )
|
||||
2 | b = (
|
||||
3 | / a
|
||||
4 | | /
|
||||
5 | | 0 # ty:ignore[division-by-zero]
|
||||
| |_________^
|
||||
6 | )
|
||||
|
|
||||
2 | b = (
|
||||
3 | a
|
||||
4 | /
|
||||
- 0 # ty:ignore[division-by-zero]
|
||||
5 + 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||
6 | )
|
||||
7 |
|
||||
2 | b = (
|
||||
3 | a
|
||||
4 | /
|
||||
- 0 # ty:ignore[division-by-zero]
|
||||
5 + 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||
6 | )
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -307,22 +302,22 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:3:21
|
||||
--> main.py:3:9
|
||||
|
|
||||
2 | b = (
|
||||
3 | / a # ty:ignore[division-by-zero]
|
||||
4 | | /
|
||||
5 | | 0 # ty:ignore[division-by-zero]
|
||||
| |_____________________^
|
||||
6 | )
|
||||
2 | b = (
|
||||
3 | / a # ty:ignore[division-by-zero]
|
||||
4 | | /
|
||||
5 | | 0 # ty:ignore[division-by-zero]
|
||||
| |_________^
|
||||
6 | )
|
||||
|
|
||||
1 |
|
||||
2 | b = (
|
||||
- a # ty:ignore[division-by-zero]
|
||||
3 + a # ty:ignore[division-by-zero, unresolved-reference]
|
||||
4 | /
|
||||
5 | 0 # ty:ignore[division-by-zero]
|
||||
6 | )
|
||||
2 | b = (
|
||||
- a # ty:ignore[division-by-zero]
|
||||
3 + a # ty:ignore[division-by-zero, unresolved-reference]
|
||||
4 | /
|
||||
5 | 0 # ty:ignore[division-by-zero]
|
||||
6 | )
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -339,20 +334,19 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:3:18
|
||||
--> main.py:3:6
|
||||
|
|
||||
2 | b = f"""
|
||||
3 | {a}
|
||||
| ^
|
||||
4 | more text
|
||||
5 | """
|
||||
2 | b = f"""
|
||||
3 | {a}
|
||||
| ^
|
||||
4 | more text
|
||||
5 | """
|
||||
|
|
||||
2 | b = f"""
|
||||
3 | {a}
|
||||
4 | more text
|
||||
- """
|
||||
5 + """ # ty:ignore[unresolved-reference]
|
||||
6 |
|
||||
2 | b = f"""
|
||||
3 | {a}
|
||||
4 | more text
|
||||
- """
|
||||
5 + """ # ty:ignore[unresolved-reference]
|
||||
"#);
|
||||
}
|
||||
|
||||
|
|
@ -371,23 +365,23 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:4:17
|
||||
--> main.py:4:5
|
||||
|
|
||||
2 | b = f"""
|
||||
3 | {
|
||||
4 | a
|
||||
| ^
|
||||
5 | }
|
||||
6 | more text
|
||||
2 | b = f"""
|
||||
3 | {
|
||||
4 | a
|
||||
| ^
|
||||
5 | }
|
||||
6 | more text
|
||||
|
|
||||
1 |
|
||||
2 | b = f"""
|
||||
3 | {
|
||||
- a
|
||||
4 + a # ty:ignore[unresolved-reference]
|
||||
5 | }
|
||||
6 | more text
|
||||
7 | """
|
||||
2 | b = f"""
|
||||
3 | {
|
||||
- a
|
||||
4 + a # ty:ignore[unresolved-reference]
|
||||
5 | }
|
||||
6 | more text
|
||||
7 | """
|
||||
"#);
|
||||
}
|
||||
|
||||
|
|
@ -403,19 +397,18 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | b = a + """
|
||||
| ^
|
||||
3 | more text
|
||||
4 | """
|
||||
2 | b = a + """
|
||||
| ^
|
||||
3 | more text
|
||||
4 | """
|
||||
|
|
||||
1 |
|
||||
2 | b = a + """
|
||||
3 | more text
|
||||
- """
|
||||
4 + """ # ty:ignore[unresolved-reference]
|
||||
5 |
|
||||
2 | b = a + """
|
||||
3 | more text
|
||||
- """
|
||||
4 + """ # ty:ignore[unresolved-reference]
|
||||
"#);
|
||||
}
|
||||
|
||||
|
|
@ -430,17 +423,16 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | b = a \
|
||||
| ^
|
||||
3 | + "test"
|
||||
2 | b = a \
|
||||
| ^
|
||||
3 | + "test"
|
||||
|
|
||||
1 |
|
||||
2 | b = a \
|
||||
- + "test"
|
||||
3 + + "test" # ty:ignore[unresolved-reference]
|
||||
4 |
|
||||
2 | b = a \
|
||||
- + "test"
|
||||
3 + + "test" # ty:ignore[unresolved-reference]
|
||||
"#);
|
||||
}
|
||||
|
||||
|
|
@ -454,27 +446,249 @@ mod tests {
|
|||
|
||||
assert_snapshot!(test.code_actions(&UNDEFINED_REVEAL), @r"
|
||||
info[code-action]: import typing.reveal_type
|
||||
--> main.py:2:13
|
||||
--> main.py:2:1
|
||||
|
|
||||
2 | reveal_type(1)
|
||||
| ^^^^^^^^^^^
|
||||
2 | reveal_type(1)
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
help: This is a preferred code action
|
||||
1 + from typing import reveal_type
|
||||
2 |
|
||||
3 | reveal_type(1)
|
||||
4 |
|
||||
3 | reveal_type(1)
|
||||
|
||||
info[code-action]: Ignore 'undefined-reveal' for this line
|
||||
--> main.py:2:13
|
||||
--> main.py:2:1
|
||||
|
|
||||
2 | reveal_type(1)
|
||||
| ^^^^^^^^^^^
|
||||
2 | reveal_type(1)
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
1 |
|
||||
- reveal_type(1)
|
||||
2 + reveal_type(1) # ty:ignore[undefined-reveal]
|
||||
- reveal_type(1)
|
||||
2 + reveal_type(1) # ty:ignore[undefined-reveal]
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unresolved_deprecated() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
@<START>deprecated<END>("do not use")
|
||||
def my_func(): ...
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||
info[code-action]: import warnings.deprecated
|
||||
--> main.py:2:2
|
||||
|
|
||||
2 | @deprecated("do not use")
|
||||
| ^^^^^^^^^^
|
||||
3 | def my_func(): ...
|
||||
|
|
||||
help: This is a preferred code action
|
||||
1 + from warnings import deprecated
|
||||
2 |
|
||||
3 | @deprecated("do not use")
|
||||
4 | def my_func(): ...
|
||||
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:2
|
||||
|
|
||||
2 | @deprecated("do not use")
|
||||
| ^^^^^^^^^^
|
||||
3 | def my_func(): ...
|
||||
|
|
||||
1 |
|
||||
- @deprecated("do not use")
|
||||
2 + @deprecated("do not use") # ty:ignore[unresolved-reference]
|
||||
3 | def my_func(): ...
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unresolved_deprecated_warnings_imported() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
import warnings
|
||||
|
||||
@<START>deprecated<END>("do not use")
|
||||
def my_func(): ...
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||
info[code-action]: import warnings.deprecated
|
||||
--> main.py:4:2
|
||||
|
|
||||
2 | import warnings
|
||||
3 |
|
||||
4 | @deprecated("do not use")
|
||||
| ^^^^^^^^^^
|
||||
5 | def my_func(): ...
|
||||
|
|
||||
help: This is a preferred code action
|
||||
1 + from warnings import deprecated
|
||||
2 |
|
||||
3 | import warnings
|
||||
4 |
|
||||
|
||||
info[code-action]: qualify warnings.deprecated
|
||||
--> main.py:4:2
|
||||
|
|
||||
2 | import warnings
|
||||
3 |
|
||||
4 | @deprecated("do not use")
|
||||
| ^^^^^^^^^^
|
||||
5 | def my_func(): ...
|
||||
|
|
||||
help: This is a preferred code action
|
||||
1 |
|
||||
2 | import warnings
|
||||
3 |
|
||||
- @deprecated("do not use")
|
||||
4 + @warnings.deprecated("do not use")
|
||||
5 | def my_func(): ...
|
||||
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:4:2
|
||||
|
|
||||
2 | import warnings
|
||||
3 |
|
||||
4 | @deprecated("do not use")
|
||||
| ^^^^^^^^^^
|
||||
5 | def my_func(): ...
|
||||
|
|
||||
1 |
|
||||
2 | import warnings
|
||||
3 |
|
||||
- @deprecated("do not use")
|
||||
4 + @deprecated("do not use") # ty:ignore[unresolved-reference]
|
||||
5 | def my_func(): ...
|
||||
"#);
|
||||
}
|
||||
|
||||
// using `importlib.abc.ExecutionLoader` when no imports are in scope
|
||||
#[test]
|
||||
fn unresolved_loader() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
<START>ExecutionLoader<END>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: import importlib.abc.ExecutionLoader
|
||||
--> main.py:2:1
|
||||
|
|
||||
2 | ExecutionLoader
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: This is a preferred code action
|
||||
1 + from importlib.abc import ExecutionLoader
|
||||
2 |
|
||||
3 | ExecutionLoader
|
||||
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:1
|
||||
|
|
||||
2 | ExecutionLoader
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
1 |
|
||||
- ExecutionLoader
|
||||
2 + ExecutionLoader # ty:ignore[unresolved-reference]
|
||||
");
|
||||
}
|
||||
|
||||
// using `importlib.abc.ExecutionLoader` when `import importlib` is in scope
|
||||
//
|
||||
// TODO: `importlib.abc` is available whenever `importlib` is, so qualifying
|
||||
// `importlib.abc.ExecutionLoader` without adding imports is actually legal here!
|
||||
#[test]
|
||||
fn unresolved_loader_importlib_imported() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
import importlib
|
||||
<START>ExecutionLoader<END>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: import importlib.abc.ExecutionLoader
|
||||
--> main.py:3:1
|
||||
|
|
||||
2 | import importlib
|
||||
3 | ExecutionLoader
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: This is a preferred code action
|
||||
1 + from importlib.abc import ExecutionLoader
|
||||
2 |
|
||||
3 | import importlib
|
||||
4 | ExecutionLoader
|
||||
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:3:1
|
||||
|
|
||||
2 | import importlib
|
||||
3 | ExecutionLoader
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
1 |
|
||||
2 | import importlib
|
||||
- ExecutionLoader
|
||||
3 + ExecutionLoader # ty:ignore[unresolved-reference]
|
||||
");
|
||||
}
|
||||
|
||||
// Using `importlib.abc.ExecutionLoader` when `import importlib.abc` is in scope
|
||||
#[test]
|
||||
fn unresolved_loader_abc_imported() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
import importlib.abc
|
||||
<START>ExecutionLoader<END>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: import importlib.abc.ExecutionLoader
|
||||
--> main.py:3:1
|
||||
|
|
||||
2 | import importlib.abc
|
||||
3 | ExecutionLoader
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: This is a preferred code action
|
||||
1 + from importlib.abc import ExecutionLoader
|
||||
2 |
|
||||
3 | import importlib.abc
|
||||
4 | ExecutionLoader
|
||||
|
||||
info[code-action]: qualify importlib.abc.ExecutionLoader
|
||||
--> main.py:3:1
|
||||
|
|
||||
2 | import importlib.abc
|
||||
3 | ExecutionLoader
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: This is a preferred code action
|
||||
1 |
|
||||
2 | import importlib.abc
|
||||
- ExecutionLoader
|
||||
3 + importlib.abc.ExecutionLoader
|
||||
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:3:1
|
||||
|
|
||||
2 | import importlib.abc
|
||||
3 | ExecutionLoader
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
1 |
|
||||
2 | import importlib.abc
|
||||
- ExecutionLoader
|
||||
3 + ExecutionLoader # ty:ignore[unresolved-reference]
|
||||
");
|
||||
}
|
||||
|
||||
|
|
@ -493,7 +707,7 @@ mod tests {
|
|||
|
||||
db.init_program().unwrap();
|
||||
|
||||
let mut cleansed = source.to_string();
|
||||
let mut cleansed = dedent(source).to_string();
|
||||
|
||||
let start = cleansed
|
||||
.find("<START>")
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ impl<'db> Completions<'db> {
|
|||
self.items
|
||||
}
|
||||
|
||||
// Convert this collection into a list of "import..." fixes
|
||||
fn into_imports(mut self) -> Vec<ImportEdit> {
|
||||
self.items.sort_by(compare_suggestions);
|
||||
self.items
|
||||
|
|
@ -82,6 +83,28 @@ impl<'db> Completions<'db> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
// Convert this collection into a list of "qualify..." fixes
|
||||
fn into_qualifications(mut self, range: TextRange) -> Vec<ImportEdit> {
|
||||
self.items.sort_by(compare_suggestions);
|
||||
self.items
|
||||
.dedup_by(|c1, c2| (&c1.name, c1.module_name) == (&c2.name, c2.module_name));
|
||||
self.items
|
||||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
// If we would have to actually import something, don't suggest the qualification
|
||||
// (we could, maybe we should, but for now, we don't)
|
||||
if item.import.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ImportEdit {
|
||||
label: format!("qualify {}", item.insert.as_ref()?),
|
||||
edit: Edit::replacement(item.insert?.into_string(), range.start(), range.end()),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Attempts to adds the given completion to this collection.
|
||||
///
|
||||
/// When added, `true` is returned.
|
||||
|
|
@ -555,15 +578,19 @@ pub(crate) struct ImportEdit {
|
|||
pub edit: Edit,
|
||||
}
|
||||
|
||||
pub(crate) fn missing_imports(
|
||||
/// Get fixes that would resolve an unresolved reference
|
||||
pub(crate) fn unresolved_fixes(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
parsed: &ParsedModuleRef,
|
||||
symbol: &str,
|
||||
node: AnyNodeRef,
|
||||
) -> Vec<ImportEdit> {
|
||||
let mut completions = Completions::exactly(db, symbol);
|
||||
let mut results = Vec::new();
|
||||
let scoped = ScopedTarget { node };
|
||||
|
||||
// Request imports we could add to put the symbol in scope
|
||||
let mut completions = Completions::exactly(db, symbol);
|
||||
add_unimported_completions(
|
||||
db,
|
||||
file,
|
||||
|
|
@ -574,8 +601,23 @@ pub(crate) fn missing_imports(
|
|||
},
|
||||
&mut completions,
|
||||
);
|
||||
results.extend(completions.into_imports());
|
||||
|
||||
completions.into_imports()
|
||||
// Request qualifications we could apply to the symbol to make it resolve
|
||||
let mut completions = Completions::exactly(db, symbol);
|
||||
add_unimported_completions(
|
||||
db,
|
||||
file,
|
||||
parsed,
|
||||
scoped,
|
||||
|module_name: &ModuleName, symbol: &str| {
|
||||
ImportRequest::import(module_name.as_str(), symbol).force()
|
||||
},
|
||||
&mut completions,
|
||||
);
|
||||
results.extend(completions.into_qualifications(node.range()));
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Adds completions derived from keywords.
|
||||
|
|
@ -4711,8 +4753,7 @@ from os.<CURSOR>
|
|||
let last_nonunderscore = test
|
||||
.completions()
|
||||
.iter()
|
||||
.filter(|c| !c.name.starts_with('_'))
|
||||
.next_back()
|
||||
.rfind(|c| !c.name.starts_with('_'))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&last_nonunderscore.name, "type_check_only");
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ use std::iter::FusedIterator;
|
|||
use std::panic::{AssertUnwindSafe, UnwindSafe};
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
use ty_python_semantic::add_inferred_python_version_hint_to_diagnostic;
|
||||
use ty_python_semantic::lint::RuleSelection;
|
||||
use ty_python_semantic::types::check_types;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# Invalid Order of Legacy Type Parameters
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import TypeVar, Generic, Protocol
|
||||
|
||||
T1 = TypeVar("T1", default=int)
|
||||
|
||||
T2 = TypeVar("T2")
|
||||
T3 = TypeVar("T3")
|
||||
|
||||
DefaultStrT = TypeVar("DefaultStrT", default=str)
|
||||
|
||||
class SubclassMe(Generic[T1, DefaultStrT]):
|
||||
x: DefaultStrT
|
||||
|
||||
class Baz(SubclassMe[int, DefaultStrT]):
|
||||
pass
|
||||
|
||||
# error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
|
||||
class Foo(Generic[T1, T2]):
|
||||
pass
|
||||
|
||||
class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
|
||||
pass
|
||||
|
||||
class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
|
||||
pass
|
||||
|
||||
class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
|
||||
pass
|
||||
|
||||
class VeryBad(
|
||||
Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
|
||||
Generic[T1, T2, DefaultStrT, T3],
|
||||
): ...
|
||||
```
|
||||
|
|
@ -424,9 +424,8 @@ p3 = ParamSpecWithDefault4[[int], [str]]()
|
|||
reveal_type(p3.attr1) # revealed: (int, /) -> None
|
||||
reveal_type(p3.attr2) # revealed: (str, /) -> None
|
||||
|
||||
# TODO: error
|
||||
# Un-ordered type variables as the default of `PAnother` is `P`
|
||||
class ParamSpecWithDefault5(Generic[PAnother, P]):
|
||||
class ParamSpecWithDefault5(Generic[PAnother, P]): # error: [invalid-generic-class]
|
||||
attr: Callable[PAnother, None]
|
||||
|
||||
# TODO: error
|
||||
|
|
|
|||
|
|
@ -800,6 +800,29 @@ def func(x: D): ...
|
|||
func(G()) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Self-referential protocol with different specialization
|
||||
|
||||
This is a minimal reproduction for [ty#1874](https://github.com/astral-sh/ty/issues/1874).
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
from typing import Protocol
|
||||
from ty_extensions import generic_context
|
||||
|
||||
class A[S, R](Protocol):
|
||||
def get(self, s: S) -> R: ...
|
||||
def set(self, s: S, r: R) -> S: ...
|
||||
def merge[R2](self, other: A[S, R2]) -> A[S, tuple[R, R2]]: ...
|
||||
|
||||
class Impl[S, R](A[S, R]):
|
||||
def foo(self, s: S) -> S:
|
||||
return self.set(s, self.get(s))
|
||||
|
||||
reveal_type(generic_context(A.get)) # revealed: ty_extensions.GenericContext[Self@get]
|
||||
reveal_type(generic_context(A.merge)) # revealed: ty_extensions.GenericContext[Self@merge, R2@merge]
|
||||
reveal_type(generic_context(Impl.foo)) # revealed: ty_extensions.GenericContext[Self@foo]
|
||||
```
|
||||
|
||||
## Tuple as a PEP-695 generic class
|
||||
|
||||
Our special handling for `tuple` does not break if `tuple` is defined as a PEP-695 generic class in
|
||||
|
|
|
|||
|
|
@ -687,3 +687,59 @@ reveal_type(with_parameters(int_int, 1)) # revealed: Overload[(x: int) -> str,
|
|||
# error: [invalid-argument-type]
|
||||
reveal_type(with_parameters(int_int, "a")) # revealed: Overload[(x: int) -> str, (x: str) -> str]
|
||||
```
|
||||
|
||||
## ParamSpec attribute assignability
|
||||
|
||||
When comparing signatures with `ParamSpec` attributes (`P.args` and `P.kwargs`), two different
|
||||
inferable `ParamSpec` attributes with the same kind are assignable to each other. This enables
|
||||
method overrides where both methods have their own `ParamSpec`.
|
||||
|
||||
### Same attribute kind, both inferable
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
class Parent:
|
||||
def method[**P](self, callback: Callable[P, None]) -> Callable[P, None]:
|
||||
return callback
|
||||
|
||||
class Child1(Parent):
|
||||
# This is a valid override: Q.args matches P.args, Q.kwargs matches P.kwargs
|
||||
def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]:
|
||||
return callback
|
||||
|
||||
# Both signatures use ParamSpec, so they should be compatible
|
||||
def outer[**P](f: Callable[P, int]) -> Callable[P, int]:
|
||||
def inner[**Q](g: Callable[Q, int]) -> Callable[Q, int]:
|
||||
return g
|
||||
return inner(f)
|
||||
```
|
||||
|
||||
We can explicitly mark it as an override using the `@override` decorator.
|
||||
|
||||
```py
|
||||
from typing import override
|
||||
|
||||
class Child2(Parent):
|
||||
@override
|
||||
def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]:
|
||||
return callback
|
||||
```
|
||||
|
||||
### One `ParamSpec` not inferable
|
||||
|
||||
Here, `P` is in a non-inferable position while `Q` is inferable. So, they are not considered
|
||||
assignable.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
class Container[**P]:
|
||||
def method(self, f: Callable[P, None]) -> Callable[P, None]:
|
||||
return f
|
||||
|
||||
def try_assign[**Q](self, f: Callable[Q, None]) -> Callable[Q, None]:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `(**Q@try_assign) -> None`, found `(**P@Container) -> None`"
|
||||
# error: [invalid-argument-type] "Argument to bound method `method` is incorrect: Expected `(**P@Container) -> None`, found `(**Q@try_assign) -> None`"
|
||||
return self.method(f)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -418,6 +418,18 @@ Using the `@abstractmethod` decorator requires that the class's metaclass is `AB
|
|||
from it.
|
||||
|
||||
```py
|
||||
from abc import ABCMeta
|
||||
|
||||
class CustomAbstractMetaclass(ABCMeta): ...
|
||||
|
||||
class Fine(metaclass=CustomAbstractMetaclass):
|
||||
@overload
|
||||
@abstractmethod
|
||||
def f(self, x: int) -> int: ...
|
||||
@overload
|
||||
@abstractmethod
|
||||
def f(self, x: str) -> str: ...
|
||||
|
||||
class Foo:
|
||||
@overload
|
||||
@abstractmethod
|
||||
|
|
@ -448,6 +460,52 @@ class PartialFoo(ABC):
|
|||
def f(self, x: str) -> str: ...
|
||||
```
|
||||
|
||||
#### `TYPE_CHECKING` blocks
|
||||
|
||||
As in other areas of ty, we treat `TYPE_CHECKING` blocks the same as "inline stub files", so we
|
||||
permit overloaded functions to exist without an implementation if all overloads are defined inside
|
||||
an `if TYPE_CHECKING` block:
|
||||
|
||||
```py
|
||||
from typing import overload, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@overload
|
||||
def a() -> str: ...
|
||||
@overload
|
||||
def a(x: int) -> int: ...
|
||||
|
||||
class F:
|
||||
@overload
|
||||
def method(self) -> None: ...
|
||||
@overload
|
||||
def method(self, x: int) -> int: ...
|
||||
|
||||
class G:
|
||||
if TYPE_CHECKING:
|
||||
@overload
|
||||
def method(self) -> None: ...
|
||||
@overload
|
||||
def method(self, x: int) -> int: ...
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@overload
|
||||
def b() -> str: ...
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@overload
|
||||
def b(x: int) -> int: ...
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@overload
|
||||
def c() -> None: ...
|
||||
|
||||
# not all overloads are in a `TYPE_CHECKING` block, so this is an error
|
||||
@overload
|
||||
# error: [invalid-overload]
|
||||
def c(x: int) -> int: ...
|
||||
```
|
||||
|
||||
### `@overload`-decorated functions with non-stub bodies
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_type_parameter_order.md - Invalid Order of Legacy Type Parameters
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import TypeVar, Generic, Protocol
|
||||
2 |
|
||||
3 | T1 = TypeVar("T1", default=int)
|
||||
4 |
|
||||
5 | T2 = TypeVar("T2")
|
||||
6 | T3 = TypeVar("T3")
|
||||
7 |
|
||||
8 | DefaultStrT = TypeVar("DefaultStrT", default=str)
|
||||
9 |
|
||||
10 | class SubclassMe(Generic[T1, DefaultStrT]):
|
||||
11 | x: DefaultStrT
|
||||
12 |
|
||||
13 | class Baz(SubclassMe[int, DefaultStrT]):
|
||||
14 | pass
|
||||
15 |
|
||||
16 | # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
|
||||
17 | class Foo(Generic[T1, T2]):
|
||||
18 | pass
|
||||
19 |
|
||||
20 | class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
|
||||
21 | pass
|
||||
22 |
|
||||
23 | class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
|
||||
24 | pass
|
||||
25 |
|
||||
26 | class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
|
||||
27 | pass
|
||||
28 |
|
||||
29 | class VeryBad(
|
||||
30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
|
||||
31 | Generic[T1, T2, DefaultStrT, T3],
|
||||
32 | ): ...
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
|
||||
--> src/mdtest_snippet.py:17:19
|
||||
|
|
||||
16 | # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
|
||||
17 | class Foo(Generic[T1, T2]):
|
||||
| ^^^^^^
|
||||
| |
|
||||
| Type variable `T2` does not have a default
|
||||
| Earlier TypeVar `T1` does
|
||||
18 | pass
|
||||
|
|
||||
::: src/mdtest_snippet.py:3:1
|
||||
|
|
||||
1 | from typing import TypeVar, Generic, Protocol
|
||||
2 |
|
||||
3 | T1 = TypeVar("T1", default=int)
|
||||
| ------------------------------- `T1` defined here
|
||||
4 |
|
||||
5 | T2 = TypeVar("T2")
|
||||
| ------------------ `T2` defined here
|
||||
6 | T3 = TypeVar("T3")
|
||||
|
|
||||
info: rule `invalid-generic-class` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
|
||||
--> src/mdtest_snippet.py:20:19
|
||||
|
|
||||
18 | pass
|
||||
19 |
|
||||
20 | class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
|
||||
| ^^^^^^^^^^
|
||||
| |
|
||||
| Type variable `T3` does not have a default
|
||||
| Earlier TypeVar `T1` does
|
||||
21 | pass
|
||||
|
|
||||
::: src/mdtest_snippet.py:3:1
|
||||
|
|
||||
1 | from typing import TypeVar, Generic, Protocol
|
||||
2 |
|
||||
3 | T1 = TypeVar("T1", default=int)
|
||||
| ------------------------------- `T1` defined here
|
||||
4 |
|
||||
5 | T2 = TypeVar("T2")
|
||||
6 | T3 = TypeVar("T3")
|
||||
| ------------------ `T3` defined here
|
||||
7 |
|
||||
8 | DefaultStrT = TypeVar("DefaultStrT", default=str)
|
||||
|
|
||||
info: rule `invalid-generic-class` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
|
||||
--> src/mdtest_snippet.py:23:20
|
||||
|
|
||||
21 | pass
|
||||
22 |
|
||||
23 | class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
| |
|
||||
| Type variables `T2` and `T3` do not have defaults
|
||||
| Earlier TypeVar `T1` does
|
||||
24 | pass
|
||||
|
|
||||
::: src/mdtest_snippet.py:3:1
|
||||
|
|
||||
1 | from typing import TypeVar, Generic, Protocol
|
||||
2 |
|
||||
3 | T1 = TypeVar("T1", default=int)
|
||||
| ------------------------------- `T1` defined here
|
||||
4 |
|
||||
5 | T2 = TypeVar("T2")
|
||||
| ------------------ `T2` defined here
|
||||
6 | T3 = TypeVar("T3")
|
||||
|
|
||||
info: rule `invalid-generic-class` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
|
||||
--> src/mdtest_snippet.py:26:20
|
||||
|
|
||||
24 | pass
|
||||
25 |
|
||||
26 | class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
| |
|
||||
| Type variables `T2` and `T3` do not have defaults
|
||||
| Earlier TypeVar `T1` does
|
||||
27 | pass
|
||||
|
|
||||
::: src/mdtest_snippet.py:3:1
|
||||
|
|
||||
1 | from typing import TypeVar, Generic, Protocol
|
||||
2 |
|
||||
3 | T1 = TypeVar("T1", default=int)
|
||||
| ------------------------------- `T1` defined here
|
||||
4 |
|
||||
5 | T2 = TypeVar("T2")
|
||||
| ------------------ `T2` defined here
|
||||
6 | T3 = TypeVar("T3")
|
||||
|
|
||||
info: rule `invalid-generic-class` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
|
||||
--> src/mdtest_snippet.py:30:14
|
||||
|
|
||||
29 | class VeryBad(
|
||||
30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
| |
|
||||
| Type variables `T2` and `T3` do not have defaults
|
||||
| Earlier TypeVar `T1` does
|
||||
31 | Generic[T1, T2, DefaultStrT, T3],
|
||||
32 | ): ...
|
||||
|
|
||||
::: src/mdtest_snippet.py:3:1
|
||||
|
|
||||
1 | from typing import TypeVar, Generic, Protocol
|
||||
2 |
|
||||
3 | T1 = TypeVar("T1", default=int)
|
||||
| ------------------------------- `T1` defined here
|
||||
4 |
|
||||
5 | T2 = TypeVar("T2")
|
||||
| ------------------ `T2` defined here
|
||||
6 | T3 = TypeVar("T3")
|
||||
|
|
||||
info: rule `invalid-generic-class` is enabled by default
|
||||
|
||||
```
|
||||
|
|
@ -42,7 +42,11 @@ error[invalid-overload]: Overloads for function `func` must be followed by a non
|
|||
9 | class Foo:
|
||||
|
|
||||
info: Attempting to call `func` will raise `TypeError` at runtime
|
||||
info: Overloaded functions without implementations are only permitted in stub files, on protocols, or for abstract methods
|
||||
info: Overloaded functions without implementations are only permitted:
|
||||
info: - in stub files
|
||||
info: - in `if TYPE_CHECKING` blocks
|
||||
info: - as methods on protocol classes
|
||||
info: - or as `@abstractmethod`-decorated methods on abstract classes
|
||||
info: See https://docs.python.org/3/library/typing.html#typing.overload for more details
|
||||
info: rule `invalid-overload` is enabled by default
|
||||
|
||||
|
|
@ -58,7 +62,11 @@ error[invalid-overload]: Overloads for function `method` must be followed by a n
|
|||
| ^^^^^^
|
||||
|
|
||||
info: Attempting to call `method` will raise `TypeError` at runtime
|
||||
info: Overloaded functions without implementations are only permitted in stub files, on protocols, or for abstract methods
|
||||
info: Overloaded functions without implementations are only permitted:
|
||||
info: - in stub files
|
||||
info: - in `if TYPE_CHECKING` blocks
|
||||
info: - as methods on protocol classes
|
||||
info: - or as `@abstractmethod`-decorated methods on abstract classes
|
||||
info: See https://docs.python.org/3/library/typing.html#typing.overload for more details
|
||||
info: rule `invalid-overload` is enabled by default
|
||||
|
||||
|
|
|
|||
|
|
@ -10869,7 +10869,6 @@ pub struct UnionTypeInstance<'db> {
|
|||
/// `<class 'str'>`. For `Union[int, str]`, this field is `None`, as we infer
|
||||
/// the elements as type expressions. Use `value_expression_types` to get the
|
||||
/// corresponding value expression types.
|
||||
#[expect(clippy::ref_option)]
|
||||
#[returns(ref)]
|
||||
_value_expr_types: Option<Box<[Type<'db>]>>,
|
||||
|
||||
|
|
|
|||
|
|
@ -1830,13 +1830,6 @@ impl<'db> ClassLiteral<'db> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Determine if this is an abstract class.
|
||||
pub(super) fn is_abstract(self, db: &'db dyn Db) -> bool {
|
||||
self.metaclass(db)
|
||||
.as_class_literal()
|
||||
.is_some_and(|metaclass| metaclass.is_known(db, KnownClass::ABCMeta))
|
||||
}
|
||||
|
||||
/// Return the types of the decorators on this class
|
||||
#[salsa::tracked(returns(deref), cycle_initial=decorators_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
|
||||
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ use crate::types::{
|
|||
ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, binding_type,
|
||||
protocol_class::ProtocolClass,
|
||||
};
|
||||
use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy};
|
||||
use crate::types::{DataclassFlags, KnownInstanceType, MemberLookupPolicy, TypeVarInstance};
|
||||
use crate::{Db, DisplaySettings, FxIndexMap, Module, ModuleName, Program, declare_lint};
|
||||
use itertools::Itertools;
|
||||
use ruff_db::{
|
||||
|
|
@ -894,15 +894,20 @@ declare_lint! {
|
|||
///
|
||||
/// ## Why is this bad?
|
||||
/// There are several requirements that you must follow when defining a generic class.
|
||||
/// Many of these result in `TypeError` being raised at runtime if they are violated.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// from typing import Generic, TypeVar
|
||||
/// from typing_extensions import Generic, TypeVar
|
||||
///
|
||||
/// T = TypeVar("T") # okay
|
||||
/// T = TypeVar("T")
|
||||
/// U = TypeVar("U", default=int)
|
||||
///
|
||||
/// # error: class uses both PEP-695 syntax and legacy syntax
|
||||
/// class C[U](Generic[T]): ...
|
||||
///
|
||||
/// # error: type parameter with default comes before type parameter without default
|
||||
/// class D(Generic[U, T]): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
|
@ -3695,6 +3700,90 @@ pub(crate) fn report_cannot_pop_required_field_on_typed_dict<'db>(
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn report_invalid_type_param_order<'db>(
|
||||
context: &InferContext<'db, '_>,
|
||||
class: ClassLiteral<'db>,
|
||||
node: &ast::StmtClassDef,
|
||||
typevar_with_default: TypeVarInstance<'db>,
|
||||
invalid_later_typevars: &[TypeVarInstance<'db>],
|
||||
) {
|
||||
let db = context.db();
|
||||
|
||||
let base_index = class
|
||||
.explicit_bases(db)
|
||||
.iter()
|
||||
.position(|base| {
|
||||
matches!(
|
||||
base,
|
||||
Type::KnownInstance(
|
||||
KnownInstanceType::SubscriptedProtocol(_)
|
||||
| KnownInstanceType::SubscriptedGeneric(_)
|
||||
)
|
||||
)
|
||||
})
|
||||
.expect(
|
||||
"It should not be possible for a class to have a legacy generic context \
|
||||
if it does not inherit from `Protocol[]` or `Generic[]`",
|
||||
);
|
||||
|
||||
let base_node = &node.bases()[base_index];
|
||||
|
||||
let primary_diagnostic_range = base_node
|
||||
.as_subscript_expr()
|
||||
.map(|subscript| &*subscript.slice)
|
||||
.unwrap_or(base_node)
|
||||
.range();
|
||||
|
||||
let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, primary_diagnostic_range)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic = builder.into_diagnostic(
|
||||
"Type parameters without defaults cannot follow type parameters with defaults",
|
||||
);
|
||||
|
||||
diagnostic.set_concise_message(format_args!(
|
||||
"Type parameter `{}` without a default cannot follow earlier parameter `{}` with a default",
|
||||
invalid_later_typevars[0].name(db),
|
||||
typevar_with_default.name(db),
|
||||
));
|
||||
|
||||
if let [single_typevar] = invalid_later_typevars {
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Type variable `{}` does not have a default",
|
||||
single_typevar.name(db),
|
||||
));
|
||||
} else {
|
||||
let later_typevars =
|
||||
format_enumeration(invalid_later_typevars.iter().map(|tv| tv.name(db)));
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Type variables {later_typevars} do not have defaults",
|
||||
));
|
||||
}
|
||||
|
||||
diagnostic.annotate(
|
||||
Annotation::primary(Span::from(context.file()).with_range(primary_diagnostic_range))
|
||||
.message(format_args!(
|
||||
"Earlier TypeVar `{}` does",
|
||||
typevar_with_default.name(db)
|
||||
)),
|
||||
);
|
||||
|
||||
for tvar in [typevar_with_default, invalid_later_typevars[0]] {
|
||||
let Some(definition) = tvar.definition(db) else {
|
||||
continue;
|
||||
};
|
||||
let file = definition.file(db);
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(Span::from(
|
||||
definition.full_range(db, &parsed_module(db, file).load(db)),
|
||||
))
|
||||
.message(format_args!("`{}` defined here", tvar.name(db))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn report_rebound_typevar<'db>(
|
||||
context: &InferContext<'db, '_>,
|
||||
typevar_name: &ast::name::Name,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use crate::types::{
|
|||
IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor,
|
||||
OnlyReorder, Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints,
|
||||
TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionType, declaration_type,
|
||||
walk_bound_type_var_type,
|
||||
walk_type_var_bounds,
|
||||
};
|
||||
use crate::{Db, FxOrderMap, FxOrderSet};
|
||||
|
||||
|
|
@ -290,6 +290,18 @@ impl<'db> GenericContext<'db> {
|
|||
)
|
||||
}
|
||||
|
||||
/// Returns the typevars that are inferable in this generic context. This set might include
|
||||
/// more typevars than the ones directly bound by the generic context. For instance, consider a
|
||||
/// method of a generic class:
|
||||
///
|
||||
/// ```py
|
||||
/// class C[A]:
|
||||
/// def method[T](self, t: T):
|
||||
/// ```
|
||||
///
|
||||
/// In this example, `method`'s generic context binds `Self` and `T`, but its inferable set
|
||||
/// also includes `A@C`. This is needed because at each call site, we need to infer the
|
||||
/// specialized class instance type whose method is being invoked.
|
||||
pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> {
|
||||
#[derive(Default)]
|
||||
struct CollectTypeVars<'db> {
|
||||
|
|
@ -299,7 +311,7 @@ impl<'db> GenericContext<'db> {
|
|||
|
||||
impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> {
|
||||
fn should_visit_lazy_type_attributes(&self) -> bool {
|
||||
true
|
||||
false
|
||||
}
|
||||
|
||||
fn visit_bound_type_var_type(
|
||||
|
|
@ -310,7 +322,10 @@ impl<'db> GenericContext<'db> {
|
|||
self.typevars
|
||||
.borrow_mut()
|
||||
.insert(bound_typevar.identity(db));
|
||||
walk_bound_type_var_type(db, bound_typevar, self);
|
||||
let typevar = bound_typevar.typevar(db);
|
||||
if let Some(bound_or_constraints) = typevar.bound_or_constraints(db) {
|
||||
walk_type_var_bounds(db, bound_or_constraints, self);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ use crate::types::diagnostic::{
|
|||
report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type,
|
||||
report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base,
|
||||
report_invalid_return_type, report_invalid_type_checking_constant,
|
||||
report_named_tuple_field_with_leading_underscore,
|
||||
report_invalid_type_param_order, report_named_tuple_field_with_leading_underscore,
|
||||
report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable,
|
||||
report_possibly_missing_attribute, report_possibly_unresolved_reference,
|
||||
report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
|
||||
|
|
@ -949,23 +949,62 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
}
|
||||
|
||||
let scope = class.body_scope(self.db()).scope(self.db());
|
||||
if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS)
|
||||
&& let Some(parent) = scope.parent()
|
||||
{
|
||||
for self_typevar in class.typevars_referenced_in_definition(self.db()) {
|
||||
let self_typevar_name = self_typevar.typevar(self.db()).name(self.db());
|
||||
for enclosing in enclosing_generic_contexts(self.db(), self.index, parent) {
|
||||
if let Some(other_typevar) =
|
||||
enclosing.binds_named_typevar(self.db(), self_typevar_name)
|
||||
{
|
||||
report_rebound_typevar(
|
||||
&self.context,
|
||||
self_typevar_name,
|
||||
class,
|
||||
class_node,
|
||||
other_typevar,
|
||||
);
|
||||
if self.context.is_lint_enabled(&INVALID_GENERIC_CLASS) {
|
||||
if !class.has_pep_695_type_params(self.db())
|
||||
&& let Some(generic_context) = class.legacy_generic_context(self.db())
|
||||
{
|
||||
struct State<'db> {
|
||||
typevar_with_default: TypeVarInstance<'db>,
|
||||
invalid_later_tvars: Vec<TypeVarInstance<'db>>,
|
||||
}
|
||||
|
||||
let mut state: Option<State<'db>> = None;
|
||||
|
||||
for bound_typevar in generic_context.variables(self.db()) {
|
||||
let typevar = bound_typevar.typevar(self.db());
|
||||
let has_default = typevar.default_type(self.db()).is_some();
|
||||
|
||||
if let Some(state) = state.as_mut() {
|
||||
if !has_default {
|
||||
state.invalid_later_tvars.push(typevar);
|
||||
}
|
||||
} else if has_default {
|
||||
state = Some(State {
|
||||
typevar_with_default: typevar,
|
||||
invalid_later_tvars: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(state) = state
|
||||
&& !state.invalid_later_tvars.is_empty()
|
||||
{
|
||||
report_invalid_type_param_order(
|
||||
&self.context,
|
||||
class,
|
||||
class_node,
|
||||
state.typevar_with_default,
|
||||
&state.invalid_later_tvars,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let scope = class.body_scope(self.db()).scope(self.db());
|
||||
if let Some(parent) = scope.parent() {
|
||||
for self_typevar in class.typevars_referenced_in_definition(self.db()) {
|
||||
let self_typevar_name = self_typevar.typevar(self.db()).name(self.db());
|
||||
for enclosing in enclosing_generic_contexts(self.db(), self.index, parent) {
|
||||
if let Some(other_typevar) =
|
||||
enclosing.binds_named_typevar(self.db(), self_typevar_name)
|
||||
{
|
||||
report_rebound_typevar(
|
||||
&self.context,
|
||||
self_typevar_name,
|
||||
class,
|
||||
class_node,
|
||||
other_typevar,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1104,7 +1143,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
if implementation.is_none() && !self.in_stub() {
|
||||
let mut implementation_required = true;
|
||||
|
||||
if let NodeWithScopeKind::Class(class_node_ref) = scope {
|
||||
if function
|
||||
.iter_overloads_and_implementation(self.db())
|
||||
.all(|f| {
|
||||
f.body_scope(self.db())
|
||||
.scope(self.db())
|
||||
.in_type_checking_block()
|
||||
})
|
||||
{
|
||||
implementation_required = false;
|
||||
} else if let NodeWithScopeKind::Class(class_node_ref) = scope {
|
||||
let class = binding_type(
|
||||
self.db(),
|
||||
self.index
|
||||
|
|
@ -1113,7 +1161,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
.expect_class_literal();
|
||||
|
||||
if class.is_protocol(self.db())
|
||||
|| (class.is_abstract(self.db())
|
||||
|| (Type::ClassLiteral(class)
|
||||
.is_subtype_of(self.db(), KnownClass::ABCMeta.to_instance(self.db()))
|
||||
&& overloads.iter().all(|overload| {
|
||||
overload.has_known_decorator(
|
||||
self.db(),
|
||||
|
|
@ -1140,8 +1189,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
&function_node.name
|
||||
));
|
||||
diagnostic.info(
|
||||
"Overloaded functions without implementations are only permitted \
|
||||
in stub files, on protocols, or for abstract methods",
|
||||
"Overloaded functions without implementations are only permitted:",
|
||||
);
|
||||
diagnostic.info(" - in stub files");
|
||||
diagnostic.info(" - in `if TYPE_CHECKING` blocks");
|
||||
diagnostic.info(" - as methods on protocol classes");
|
||||
diagnostic.info(
|
||||
" - or as `@abstractmethod`-decorated methods on abstract classes",
|
||||
);
|
||||
diagnostic.info(
|
||||
"See https://docs.python.org/3/library/typing.html#typing.overload \
|
||||
|
|
|
|||
|
|
@ -1209,6 +1209,29 @@ impl<'db> Signature<'db> {
|
|||
let mut check_types = |type1: Option<Type<'db>>, type2: Option<Type<'db>>| {
|
||||
let type1 = type1.unwrap_or(Type::unknown());
|
||||
let type2 = type2.unwrap_or(Type::unknown());
|
||||
|
||||
match (type1, type2) {
|
||||
// This is a special case where the _same_ components of two different `ParamSpec`
|
||||
// type variables are assignable to each other when they're both in an inferable
|
||||
// position.
|
||||
//
|
||||
// `ParamSpec` type variables can only occur in parameter lists so this special case
|
||||
// is present here instead of in `Type::has_relation_to_impl`.
|
||||
(Type::TypeVar(typevar1), Type::TypeVar(typevar2))
|
||||
if typevar1.paramspec_attr(db).is_some()
|
||||
&& typevar1.paramspec_attr(db) == typevar2.paramspec_attr(db)
|
||||
&& typevar1
|
||||
.without_paramspec_attr(db)
|
||||
.is_inferable(db, inferable)
|
||||
&& typevar2
|
||||
.without_paramspec_attr(db)
|
||||
.is_inferable(db, inferable) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
!result
|
||||
.intersect(
|
||||
db,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,51 @@ expression: code_actions
|
|||
},
|
||||
"isPreferred": true
|
||||
},
|
||||
{
|
||||
"title": "qualify warnings.deprecated",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 3,
|
||||
"character": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 3,
|
||||
"character": 11
|
||||
}
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "unresolved-reference",
|
||||
"codeDescription": {
|
||||
"href": "https://ty.dev/rules#unresolved-reference"
|
||||
},
|
||||
"source": "ty",
|
||||
"message": "Name `deprecated` used when not defined"
|
||||
}
|
||||
],
|
||||
"edit": {
|
||||
"changes": {
|
||||
"file://<temp_dir>/src/foo.py": [
|
||||
{
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 3,
|
||||
"character": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 3,
|
||||
"character": 11
|
||||
}
|
||||
},
|
||||
"newText": "warnings.deprecated"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"isPreferred": true
|
||||
},
|
||||
{
|
||||
"title": "Ignore 'unresolved-reference' for this line",
|
||||
"kind": "quickfix",
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
|
|||
global = "depot-ubuntu-latest-4"
|
||||
|
||||
[dist.github-action-commits]
|
||||
"actions/checkout" = "1af3b93b6815bc44a9784bd300feb67ff0d1eeb3" # v6.0.0
|
||||
"actions/checkout" = "8e8c483db84b4bee98b60c0593521ed34d9990e8" # v6.0.1
|
||||
"actions/upload-artifact" = "330a01c490aca151604b8cf639adc76d48f6c5d4" # v5.0.0
|
||||
"actions/download-artifact" = "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53" # v6.0.0
|
||||
"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3
|
||||
|
|
|
|||
|
|
@ -501,6 +501,48 @@ If you want Ruff to split an f-string across multiple lines, ensure there's a li
|
|||
[self-documenting f-string]: https://realpython.com/python-f-strings/#self-documenting-expressions-for-debugging
|
||||
[configured quote style]: settings.md/#format_quote-style
|
||||
|
||||
#### Fluent layout for method chains
|
||||
|
||||
At times, when developers write long chains of methods on an object, such as
|
||||
|
||||
```python
|
||||
x = df.filter(cond).agg(func).merge(other)
|
||||
```
|
||||
|
||||
the intent is to perform a sequence of transformations or operations
|
||||
on a fixed object of interest - in this example, the object `df`.
|
||||
Assuming the assigned expression exceeds the `line-length`, this preview
|
||||
style will format the above as:
|
||||
|
||||
```python
|
||||
x = (
|
||||
df
|
||||
.filter(cond)
|
||||
.agg(func)
|
||||
.merge(other)
|
||||
)
|
||||
```
|
||||
|
||||
This deviates from the stable formatting, and also from Black, both
|
||||
of which would produce:
|
||||
|
||||
|
||||
```python
|
||||
x = (
|
||||
df.filter(cond)
|
||||
.agg(func)
|
||||
.merge(other)
|
||||
)
|
||||
```
|
||||
|
||||
Both the stable and preview formatting are variants of something
|
||||
called a **fluent layout**.
|
||||
|
||||
In general, this preview style differs from the stable style
|
||||
only at the first attribute that precedes
|
||||
a call or subscript. The preview formatting breaks _before_ this attribute,
|
||||
while the stable formatting breaks _after_ the call or subscript.
|
||||
|
||||
## Sorting imports
|
||||
|
||||
Currently, the Ruff formatter does not sort imports. In order to both sort imports and format,
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "1.91"
|
||||
channel = "1.92"
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ def main() -> None:
|
|||
|
||||
parser.add_argument(
|
||||
"--ty-path",
|
||||
action="append",
|
||||
type=Path,
|
||||
help="Path to the ty binary to benchmark.",
|
||||
)
|
||||
|
|
@ -108,7 +109,11 @@ def main() -> None:
|
|||
for tool_name in args.tool or TOOL_CHOICES:
|
||||
match tool_name:
|
||||
case "ty":
|
||||
suites.append(Ty(path=args.ty_path))
|
||||
if args.ty_path:
|
||||
for path in args.ty_path:
|
||||
suites.append(Ty(path=path))
|
||||
else:
|
||||
suites.append(Ty())
|
||||
case "pyrefly":
|
||||
suites.append(Pyrefly())
|
||||
case "pyright":
|
||||
|
|
|
|||
|
|
@ -595,7 +595,7 @@
|
|||
},
|
||||
"invalid-generic-class": {
|
||||
"title": "detects invalid generic classes",
|
||||
"description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\n\n## Examples\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\") # okay\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)",
|
||||
"description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\nMany of these result in `TypeError` being raised at runtime if they are violated.\n\n## Examples\n```python\nfrom typing_extensions import Generic, TypeVar\n\nT = TypeVar(\"T\")\nU = TypeVar(\"U\", default=int)\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n\n# error: type parameter with default comes before type parameter without default\nclass D(Generic[U, T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)",
|
||||
"default": "error",
|
||||
"oneOf": [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue