mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:47 +00:00
Enable annotation quoting for multi-line expressions (#9142)
Given: ```python x: DataFrame[ int ] = 1 ``` We currently wrap the annotation in single quotes, which leads to a syntax error: ```python x: "DataFrame[ int ]" = 1 ``` There are a few options for what to suggest for users here... Use triple quotes: ```python x: """DataFrame[ int ]""" = 1 ``` Or, use an implicit string concatenation (which may require parentheses): ```python x: ("DataFrame[" "int" "]") = 1 ``` The solution I settled on here is to use the `Generator`, which effectively means we write it out on a single line, like: ```python x: "DataFrame[int]" = 1 ``` It's kind of the "least opinionated" solution, but it does mean we'll expand to a very long line in some cases. Closes https://github.com/astral-sh/ruff/issues/9136.
This commit is contained in:
parent
6c224cec52
commit
d1a7bc38ff
6 changed files with 113 additions and 14 deletions
|
@ -7,7 +7,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
|
|
||||||
/// A text edit to be applied to a source file. Inserts, deletes, or replaces
|
/// A text edit to be applied to a source file. Inserts, deletes, or replaces
|
||||||
/// content at a given location.
|
/// content at a given location.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct Edit {
|
pub struct Edit {
|
||||||
/// The start location of the edit.
|
/// The start location of the edit.
|
||||||
|
|
|
@ -72,3 +72,18 @@ def f():
|
||||||
|
|
||||||
def baz() -> DataFrame | Series:
|
def baz() -> DataFrame | Series:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def f():
|
||||||
|
from pandas import DataFrame, Series
|
||||||
|
|
||||||
|
def baz() -> (
|
||||||
|
DataFrame |
|
||||||
|
Series
|
||||||
|
):
|
||||||
|
...
|
||||||
|
|
||||||
|
class C:
|
||||||
|
x: DataFrame[
|
||||||
|
int
|
||||||
|
] = 1
|
||||||
|
|
|
@ -5,7 +5,7 @@ use ruff_diagnostics::Edit;
|
||||||
use ruff_python_ast::call_path::from_qualified_name;
|
use ruff_python_ast::call_path::from_qualified_name;
|
||||||
use ruff_python_ast::helpers::{map_callable, map_subscript};
|
use ruff_python_ast::helpers::{map_callable, map_subscript};
|
||||||
use ruff_python_ast::{self as ast, Expr};
|
use ruff_python_ast::{self as ast, Expr};
|
||||||
use ruff_python_codegen::Stylist;
|
use ruff_python_codegen::{Generator, Stylist};
|
||||||
use ruff_python_semantic::{
|
use ruff_python_semantic::{
|
||||||
Binding, BindingId, BindingKind, NodeId, ResolvedReference, SemanticModel,
|
Binding, BindingId, BindingKind, NodeId, ResolvedReference, SemanticModel,
|
||||||
};
|
};
|
||||||
|
@ -215,6 +215,7 @@ pub(crate) fn quote_annotation(
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
stylist: &Stylist,
|
stylist: &Stylist,
|
||||||
|
generator: Generator,
|
||||||
) -> Result<Edit> {
|
) -> Result<Edit> {
|
||||||
let expr = semantic.expression(node_id).expect("Expression not found");
|
let expr = semantic.expression(node_id).expect("Expression not found");
|
||||||
if let Some(parent_id) = semantic.parent_expression_id(node_id) {
|
if let Some(parent_id) = semantic.parent_expression_id(node_id) {
|
||||||
|
@ -224,7 +225,7 @@ pub(crate) fn quote_annotation(
|
||||||
// If we're quoting the value of a subscript, we need to quote the entire
|
// If we're quoting the value of a subscript, we need to quote the entire
|
||||||
// expression. For example, when quoting `DataFrame` in `DataFrame[int]`, we
|
// expression. For example, when quoting `DataFrame` in `DataFrame[int]`, we
|
||||||
// should generate `"DataFrame[int]"`.
|
// should generate `"DataFrame[int]"`.
|
||||||
return quote_annotation(parent_id, semantic, locator, stylist);
|
return quote_annotation(parent_id, semantic, locator, stylist, generator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Expr::Attribute(parent)) => {
|
Some(Expr::Attribute(parent)) => {
|
||||||
|
@ -232,7 +233,7 @@ pub(crate) fn quote_annotation(
|
||||||
// If we're quoting the value of an attribute, we need to quote the entire
|
// If we're quoting the value of an attribute, we need to quote the entire
|
||||||
// expression. For example, when quoting `DataFrame` in `pd.DataFrame`, we
|
// expression. For example, when quoting `DataFrame` in `pd.DataFrame`, we
|
||||||
// should generate `"pd.DataFrame"`.
|
// should generate `"pd.DataFrame"`.
|
||||||
return quote_annotation(parent_id, semantic, locator, stylist);
|
return quote_annotation(parent_id, semantic, locator, stylist, generator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Expr::Call(parent)) => {
|
Some(Expr::Call(parent)) => {
|
||||||
|
@ -240,7 +241,7 @@ pub(crate) fn quote_annotation(
|
||||||
// If we're quoting the function of a call, we need to quote the entire
|
// If we're quoting the function of a call, we need to quote the entire
|
||||||
// expression. For example, when quoting `DataFrame` in `DataFrame()`, we
|
// expression. For example, when quoting `DataFrame` in `DataFrame()`, we
|
||||||
// should generate `"DataFrame()"`.
|
// should generate `"DataFrame()"`.
|
||||||
return quote_annotation(parent_id, semantic, locator, stylist);
|
return quote_annotation(parent_id, semantic, locator, stylist, generator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Expr::BinOp(parent)) => {
|
Some(Expr::BinOp(parent)) => {
|
||||||
|
@ -248,27 +249,30 @@ pub(crate) fn quote_annotation(
|
||||||
// If we're quoting the left or right side of a binary operation, we need to
|
// If we're quoting the left or right side of a binary operation, we need to
|
||||||
// quote the entire expression. For example, when quoting `DataFrame` in
|
// quote the entire expression. For example, when quoting `DataFrame` in
|
||||||
// `DataFrame | Series`, we should generate `"DataFrame | Series"`.
|
// `DataFrame | Series`, we should generate `"DataFrame | Series"`.
|
||||||
return quote_annotation(parent_id, semantic, locator, stylist);
|
return quote_annotation(parent_id, semantic, locator, stylist, generator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let annotation = locator.slice(expr);
|
|
||||||
|
|
||||||
// If the annotation already contains a quote, avoid attempting to re-quote it. For example:
|
// If the annotation already contains a quote, avoid attempting to re-quote it. For example:
|
||||||
// ```python
|
// ```python
|
||||||
// from typing import Literal
|
// from typing import Literal
|
||||||
//
|
//
|
||||||
// Set[Literal["Foo"]]
|
// Set[Literal["Foo"]]
|
||||||
// ```
|
// ```
|
||||||
if annotation.contains('\'') || annotation.contains('"') {
|
let text = locator.slice(expr);
|
||||||
|
if text.contains('\'') || text.contains('"') {
|
||||||
return Err(anyhow::anyhow!("Annotation already contains a quote"));
|
return Err(anyhow::anyhow!("Annotation already contains a quote"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're quoting a name, we need to quote the entire expression.
|
// Quote the entire expression.
|
||||||
let quote = stylist.quote();
|
let quote = stylist.quote();
|
||||||
let annotation = format!("{quote}{annotation}{quote}");
|
let annotation = generator.expr(expr);
|
||||||
Ok(Edit::range_replacement(annotation, expr.range()))
|
|
||||||
|
Ok(Edit::range_replacement(
|
||||||
|
format!("{quote}{annotation}{quote}"),
|
||||||
|
expr.range(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -274,6 +274,7 @@ fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding])
|
||||||
checker.semantic(),
|
checker.semantic(),
|
||||||
checker.locator(),
|
checker.locator(),
|
||||||
checker.stylist(),
|
checker.stylist(),
|
||||||
|
checker.generator(),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -282,7 +283,7 @@ fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding])
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
let mut rest = quote_reference_edits.into_iter().dedup();
|
let mut rest = quote_reference_edits.into_iter().unique();
|
||||||
let head = rest.next().expect("Expected at least one reference");
|
let head = rest.next().expect("Expected at least one reference");
|
||||||
Ok(Fix::unsafe_edits(head, rest).isolate(Checker::isolation(
|
Ok(Fix::unsafe_edits(head, rest).isolate(Checker::isolation(
|
||||||
checker.semantic().parent_statement_id(node_id),
|
checker.semantic().parent_statement_id(node_id),
|
||||||
|
|
|
@ -494,6 +494,7 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
|
||||||
checker.semantic(),
|
checker.semantic(),
|
||||||
checker.locator(),
|
checker.locator(),
|
||||||
checker.stylist(),
|
checker.stylist(),
|
||||||
|
checker.generator(),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -507,7 +508,7 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
|
||||||
add_import_edit
|
add_import_edit
|
||||||
.into_edits()
|
.into_edits()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(quote_reference_edits.into_iter().dedup()),
|
.chain(quote_reference_edits.into_iter().unique()),
|
||||||
)
|
)
|
||||||
.isolate(Checker::isolation(
|
.isolate(Checker::isolation(
|
||||||
checker.semantic().parent_statement_id(node_id),
|
checker.semantic().parent_statement_id(node_id),
|
||||||
|
|
|
@ -223,6 +223,8 @@ quote.py:71:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a typ
|
||||||
73 |- def baz() -> DataFrame | Series:
|
73 |- def baz() -> DataFrame | Series:
|
||||||
76 |+ def baz() -> "DataFrame | Series":
|
76 |+ def baz() -> "DataFrame | Series":
|
||||||
74 77 | ...
|
74 77 | ...
|
||||||
|
75 78 |
|
||||||
|
76 79 |
|
||||||
|
|
||||||
quote.py:71:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
|
quote.py:71:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
|
||||||
|
|
|
|
||||||
|
@ -251,5 +253,81 @@ quote.py:71:35: TCH002 [*] Move third-party import `pandas.Series` into a type-c
|
||||||
73 |- def baz() -> DataFrame | Series:
|
73 |- def baz() -> DataFrame | Series:
|
||||||
76 |+ def baz() -> "DataFrame | Series":
|
76 |+ def baz() -> "DataFrame | Series":
|
||||||
74 77 | ...
|
74 77 | ...
|
||||||
|
75 78 |
|
||||||
|
76 79 |
|
||||||
|
|
||||||
|
quote.py:78:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
||||||
|
|
|
||||||
|
77 | def f():
|
||||||
|
78 | from pandas import DataFrame, Series
|
||||||
|
| ^^^^^^^^^ TCH002
|
||||||
|
79 |
|
||||||
|
80 | def baz() -> (
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
1 |+from typing import TYPE_CHECKING
|
||||||
|
2 |+
|
||||||
|
3 |+if TYPE_CHECKING:
|
||||||
|
4 |+ from pandas import DataFrame, Series
|
||||||
|
1 5 | def f():
|
||||||
|
2 6 | from pandas import DataFrame
|
||||||
|
3 7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
75 79 |
|
||||||
|
76 80 |
|
||||||
|
77 81 | def f():
|
||||||
|
78 |- from pandas import DataFrame, Series
|
||||||
|
79 82 |
|
||||||
|
80 83 | def baz() -> (
|
||||||
|
81 |- DataFrame |
|
||||||
|
82 |- Series
|
||||||
|
84 |+ "DataFrame | Series"
|
||||||
|
83 85 | ):
|
||||||
|
84 86 | ...
|
||||||
|
85 87 |
|
||||||
|
86 88 | class C:
|
||||||
|
87 |- x: DataFrame[
|
||||||
|
88 |- int
|
||||||
|
89 |- ] = 1
|
||||||
|
89 |+ x: "DataFrame[int]" = 1
|
||||||
|
|
||||||
|
quote.py:78:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
|
||||||
|
|
|
||||||
|
77 | def f():
|
||||||
|
78 | from pandas import DataFrame, Series
|
||||||
|
| ^^^^^^ TCH002
|
||||||
|
79 |
|
||||||
|
80 | def baz() -> (
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
1 |+from typing import TYPE_CHECKING
|
||||||
|
2 |+
|
||||||
|
3 |+if TYPE_CHECKING:
|
||||||
|
4 |+ from pandas import DataFrame, Series
|
||||||
|
1 5 | def f():
|
||||||
|
2 6 | from pandas import DataFrame
|
||||||
|
3 7 |
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
75 79 |
|
||||||
|
76 80 |
|
||||||
|
77 81 | def f():
|
||||||
|
78 |- from pandas import DataFrame, Series
|
||||||
|
79 82 |
|
||||||
|
80 83 | def baz() -> (
|
||||||
|
81 |- DataFrame |
|
||||||
|
82 |- Series
|
||||||
|
84 |+ "DataFrame | Series"
|
||||||
|
83 85 | ):
|
||||||
|
84 86 | ...
|
||||||
|
85 87 |
|
||||||
|
86 88 | class C:
|
||||||
|
87 |- x: DataFrame[
|
||||||
|
88 |- int
|
||||||
|
89 |- ] = 1
|
||||||
|
89 |+ x: "DataFrame[int]" = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue