Avoid flagging unfixable TypedDict and NamedTuple definitions (#3148)

This commit is contained in:
Charlie Marsh 2023-02-22 18:23:25 -05:00 committed by GitHub
parent 726adb7efc
commit 2d4fae45d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 202 additions and 196 deletions

View file

@ -2,36 +2,36 @@ from typing import TypedDict, NotRequired, Literal
import typing import typing
# dict literal # dict literal
MyType1 = TypedDict("MyType1", {"a": int, "b": str}) MyType = TypedDict("MyType", {"a": int, "b": str})
# dict call # dict call
MyType2 = TypedDict("MyType2", dict(a=int, b=str)) MyType = TypedDict("MyType", dict(a=int, b=str))
# kwargs # kwargs
MyType3 = TypedDict("MyType3", a=int, b=str) MyType = TypedDict("MyType", a=int, b=str)
# Empty TypedDict # Empty TypedDict
MyType4 = TypedDict("MyType4") MyType = TypedDict("MyType")
# Literal values # Literal values
MyType5 = TypedDict("MyType5", {"a": "hello"}) MyType = TypedDict("MyType", {"a": "hello"})
MyType6 = TypedDict("MyType6", a="hello") MyType = TypedDict("MyType", a="hello")
# NotRequired # NotRequired
MyType7 = TypedDict("MyType7", {"a": NotRequired[dict]}) MyType = TypedDict("MyType", {"a": NotRequired[dict]})
# total # total
MyType8 = TypedDict("MyType8", {"x": int, "y": int}, total=False) MyType = TypedDict("MyType", {"x": int, "y": int}, total=False)
# invalid identifiers
MyType9 = TypedDict("MyType9", {"in": int, "x-y": int})
# using Literal type # using Literal type
MyType10 = TypedDict("MyType10", {"key": Literal["value"]}) MyType = TypedDict("MyType", {"key": Literal["value"]})
# using namespace TypedDict # using namespace TypedDict
MyType11 = typing.TypedDict("MyType11", {"key": int}) MyType = typing.TypedDict("MyType", {"key": int})
# unpacking # invalid identifiers (OK)
MyType = TypedDict("MyType", {"in": int, "x-y": int})
# unpacking (OK)
c = {"c": float} c = {"c": float}
MyType12 = TypedDict("MyType1", {"a": int, "b": str, **c}) MyType = TypedDict("MyType", {"a": int, "b": str, **c})

View file

@ -2,21 +2,24 @@ from typing import NamedTuple
import typing import typing
# with complex annotations # with complex annotations
NT1 = NamedTuple("NT1", [("a", int), ("b", tuple[str, ...])]) MyType = NamedTuple("MyType", [("a", int), ("b", tuple[str, ...])])
# with default values as list # with default values as list
NT2 = NamedTuple( MyType = NamedTuple(
"NT2", "MyType",
[("a", int), ("b", str), ("c", list[bool])], [("a", int), ("b", str), ("c", list[bool])],
defaults=["foo", [True]], defaults=["foo", [True]],
) )
# with namespace # with namespace
NT3 = typing.NamedTuple("NT3", [("a", int), ("b", str)]) MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)])
# with too many default values # too many default values (OK)
NT4 = NamedTuple( MyType = NamedTuple(
"NT4", "MyType",
[("a", int), ("b", str)], [("a", int), ("b", str)],
defaults=[1, "bar", "baz"], defaults=[1, "bar", "baz"],
) )
# invalid identifiers (OK)
MyType = NamedTuple("MyType", [("x-y", int), ("b", tuple[str, ...])])

View file

@ -1,9 +1,10 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use log::debug; use log::debug;
use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Keyword, Stmt, StmtKind};
use ruff_macros::{define_violation, derive_message_formats}; use ruff_macros::{define_violation, derive_message_formats};
use ruff_python::identifiers::is_identifier; use ruff_python::identifiers::is_identifier;
use ruff_python::keyword::KWLIST; use ruff_python::keyword::KWLIST;
use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::helpers::{create_expr, create_stmt, unparse_stmt}; use crate::ast::helpers::{create_expr, create_stmt, unparse_stmt};
use crate::ast::types::Range; use crate::ast::types::Range;
@ -11,23 +12,29 @@ use crate::checkers::ast::Checker;
use crate::fix::Fix; use crate::fix::Fix;
use crate::registry::Diagnostic; use crate::registry::Diagnostic;
use crate::source_code::Stylist; use crate::source_code::Stylist;
use crate::violation::AlwaysAutofixableViolation; use crate::violation::{Availability, Violation};
use crate::AutofixKind;
define_violation!( define_violation!(
pub struct ConvertNamedTupleFunctionalToClass { pub struct ConvertNamedTupleFunctionalToClass {
pub name: String, pub name: String,
pub fixable: bool,
} }
); );
impl AlwaysAutofixableViolation for ConvertNamedTupleFunctionalToClass { impl Violation for ConvertNamedTupleFunctionalToClass {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let ConvertNamedTupleFunctionalToClass { name } = self; let ConvertNamedTupleFunctionalToClass { name, .. } = self;
format!("Convert `{name}` from `NamedTuple` functional to class syntax") format!("Convert `{name}` from `NamedTuple` functional to class syntax")
} }
fn autofix_title(&self) -> String { fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let ConvertNamedTupleFunctionalToClass { name } = self; self.fixable
format!("Convert `{name}` to class syntax") .then_some(|ConvertNamedTupleFunctionalToClass { name, .. }| {
format!("Convert `{name}` to class syntax")
})
} }
} }
@ -172,28 +179,33 @@ pub fn convert_named_tuple_functional_to_class(
{ {
return; return;
}; };
let properties = match match_defaults(keywords)
.and_then(|defaults| create_properties_from_args(args, defaults))
{
Ok(properties) => properties,
Err(err) => {
debug!("Skipping `NamedTuple` \"{typename}\": {err}");
return;
}
};
// TODO(charlie): Preserve indentation, to remove the first-column requirement.
let fixable = stmt.location.column() == 0;
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
ConvertNamedTupleFunctionalToClass { ConvertNamedTupleFunctionalToClass {
name: typename.to_string(), name: typename.to_string(),
fixable,
}, },
Range::from_located(stmt), Range::from_located(stmt),
); );
// TODO(charlie): Preserve indentation, to remove the first-column requirement. if fixable && checker.patch(diagnostic.kind.rule()) {
if checker.patch(diagnostic.kind.rule()) && stmt.location.column() == 0 { diagnostic.amend(convert_to_class(
match match_defaults(keywords) stmt,
.and_then(|defaults| create_properties_from_args(args, defaults)) typename,
{ properties,
Ok(properties) => { base_class,
diagnostic.amend(convert_to_class( checker.stylist,
stmt, ));
typename,
properties,
base_class,
checker.stylist,
));
}
Err(err) => debug!("Skipping ineligible `NamedTuple` \"{typename}\": {err}"),
};
} }
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }

View file

@ -1,9 +1,10 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use log::debug; use log::debug;
use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Keyword, Stmt, StmtKind};
use ruff_macros::{define_violation, derive_message_formats}; use ruff_macros::{define_violation, derive_message_formats};
use ruff_python::identifiers::is_identifier; use ruff_python::identifiers::is_identifier;
use ruff_python::keyword::KWLIST; use ruff_python::keyword::KWLIST;
use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::helpers::{create_expr, create_stmt, unparse_stmt}; use crate::ast::helpers::{create_expr, create_stmt, unparse_stmt};
use crate::ast::types::Range; use crate::ast::types::Range;
@ -11,23 +12,29 @@ use crate::checkers::ast::Checker;
use crate::fix::Fix; use crate::fix::Fix;
use crate::registry::Diagnostic; use crate::registry::Diagnostic;
use crate::source_code::Stylist; use crate::source_code::Stylist;
use crate::violation::AlwaysAutofixableViolation; use crate::violation::{Availability, Violation};
use crate::AutofixKind;
define_violation!( define_violation!(
pub struct ConvertTypedDictFunctionalToClass { pub struct ConvertTypedDictFunctionalToClass {
pub name: String, pub name: String,
pub fixable: bool,
} }
); );
impl AlwaysAutofixableViolation for ConvertTypedDictFunctionalToClass { impl Violation for ConvertTypedDictFunctionalToClass {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let ConvertTypedDictFunctionalToClass { name } = self; let ConvertTypedDictFunctionalToClass { name, .. } = self;
format!("Convert `{name}` from `TypedDict` functional to class syntax") format!("Convert `{name}` from `TypedDict` functional to class syntax")
} }
fn autofix_title(&self) -> String { fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let ConvertTypedDictFunctionalToClass { name } = self; self.fixable
format!("Convert `{name}` to class syntax") .then_some(|ConvertTypedDictFunctionalToClass { name, .. }| {
format!("Convert `{name}` to class syntax")
})
} }
} }
@ -219,27 +226,31 @@ pub fn convert_typed_dict_functional_to_class(
return; return;
}; };
let (body, total_keyword) = match match_properties_and_total(args, keywords) {
Ok((body, total_keyword)) => (body, total_keyword),
Err(err) => {
debug!("Skipping ineligible `TypedDict` \"{class_name}\": {err}");
return;
}
};
// TODO(charlie): Preserve indentation, to remove the first-column requirement.
let fixable = stmt.location.column() == 0;
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
ConvertTypedDictFunctionalToClass { ConvertTypedDictFunctionalToClass {
name: class_name.to_string(), name: class_name.to_string(),
fixable,
}, },
Range::from_located(stmt), Range::from_located(stmt),
); );
// TODO(charlie): Preserve indentation, to remove the first-column requirement. if fixable && checker.patch(diagnostic.kind.rule()) {
if checker.patch(diagnostic.kind.rule()) && stmt.location.column() == 0 { diagnostic.amend(convert_to_class(
match match_properties_and_total(args, keywords) { stmt,
Ok((body, total_keyword)) => { class_name,
diagnostic.amend(convert_to_class( body,
stmt, total_keyword,
class_name, base_class,
body, checker.stylist,
total_keyword, ));
base_class,
checker.stylist,
));
}
Err(err) => debug!("Skipping ineligible `TypedDict` \"{class_name}\": {err}"),
};
} }
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }

View file

@ -4,204 +4,192 @@ expression: diagnostics
--- ---
- kind: - kind:
ConvertTypedDictFunctionalToClass: ConvertTypedDictFunctionalToClass:
name: MyType1 name: MyType
fixable: true
location: location:
row: 5 row: 5
column: 0 column: 0
end_location: end_location:
row: 5 row: 5
column: 52
fix:
content: "class MyType1(TypedDict):\n a: int\n b: str"
location:
row: 5
column: 0
end_location:
row: 5
column: 52
parent: ~
- kind:
ConvertTypedDictFunctionalToClass:
name: MyType2
location:
row: 8
column: 0
end_location:
row: 8
column: 50 column: 50
fix: fix:
content: "class MyType2(TypedDict):\n a: int\n b: str" content: "class MyType(TypedDict):\n a: int\n b: str"
location: location:
row: 8 row: 5
column: 0 column: 0
end_location: end_location:
row: 8 row: 5
column: 50 column: 50
parent: ~ parent: ~
- kind: - kind:
ConvertTypedDictFunctionalToClass: ConvertTypedDictFunctionalToClass:
name: MyType3 name: MyType
fixable: true
location:
row: 8
column: 0
end_location:
row: 8
column: 48
fix:
content: "class MyType(TypedDict):\n a: int\n b: str"
location:
row: 8
column: 0
end_location:
row: 8
column: 48
parent: ~
- kind:
ConvertTypedDictFunctionalToClass:
name: MyType
fixable: true
location: location:
row: 11 row: 11
column: 0 column: 0
end_location: end_location:
row: 11 row: 11
column: 44 column: 42
fix: fix:
content: "class MyType3(TypedDict):\n a: int\n b: str" content: "class MyType(TypedDict):\n a: int\n b: str"
location: location:
row: 11 row: 11
column: 0 column: 0
end_location: end_location:
row: 11 row: 11
column: 42
parent: ~
- kind:
ConvertTypedDictFunctionalToClass:
name: MyType
fixable: true
location:
row: 14
column: 0
end_location:
row: 14
column: 28
fix:
content: "class MyType(TypedDict):\n pass"
location:
row: 14
column: 0
end_location:
row: 14
column: 28
parent: ~
- kind:
ConvertTypedDictFunctionalToClass:
name: MyType
fixable: true
location:
row: 17
column: 0
end_location:
row: 17
column: 44
fix:
content: "class MyType(TypedDict):\n a: \"hello\""
location:
row: 17
column: 0
end_location:
row: 17
column: 44 column: 44
parent: ~ parent: ~
- kind: - kind:
ConvertTypedDictFunctionalToClass: ConvertTypedDictFunctionalToClass:
name: MyType4 name: MyType
location: fixable: true
row: 14
column: 0
end_location:
row: 14
column: 30
fix:
content: "class MyType4(TypedDict):\n pass"
location:
row: 14
column: 0
end_location:
row: 14
column: 30
parent: ~
- kind:
ConvertTypedDictFunctionalToClass:
name: MyType5
location:
row: 17
column: 0
end_location:
row: 17
column: 46
fix:
content: "class MyType5(TypedDict):\n a: \"hello\""
location:
row: 17
column: 0
end_location:
row: 17
column: 46
parent: ~
- kind:
ConvertTypedDictFunctionalToClass:
name: MyType6
location: location:
row: 18 row: 18
column: 0 column: 0
end_location: end_location:
row: 18 row: 18
column: 41 column: 39
fix: fix:
content: "class MyType6(TypedDict):\n a: \"hello\"" content: "class MyType(TypedDict):\n a: \"hello\""
location: location:
row: 18 row: 18
column: 0 column: 0
end_location: end_location:
row: 18 row: 18
column: 41 column: 39
parent: ~ parent: ~
- kind: - kind:
ConvertTypedDictFunctionalToClass: ConvertTypedDictFunctionalToClass:
name: MyType7 name: MyType
fixable: true
location: location:
row: 21 row: 21
column: 0 column: 0
end_location: end_location:
row: 21 row: 21
column: 56 column: 54
fix: fix:
content: "class MyType7(TypedDict):\n a: NotRequired[dict]" content: "class MyType(TypedDict):\n a: NotRequired[dict]"
location: location:
row: 21 row: 21
column: 0 column: 0
end_location: end_location:
row: 21 row: 21
column: 56 column: 54
parent: ~ parent: ~
- kind: - kind:
ConvertTypedDictFunctionalToClass: ConvertTypedDictFunctionalToClass:
name: MyType8 name: MyType
fixable: true
location: location:
row: 24 row: 24
column: 0 column: 0
end_location: end_location:
row: 24 row: 24
column: 65 column: 63
fix: fix:
content: "class MyType8(TypedDict, total=False):\n x: int\n y: int" content: "class MyType(TypedDict, total=False):\n x: int\n y: int"
location: location:
row: 24 row: 24
column: 0 column: 0
end_location: end_location:
row: 24 row: 24
column: 65 column: 63
parent: ~ parent: ~
- kind: - kind:
ConvertTypedDictFunctionalToClass: ConvertTypedDictFunctionalToClass:
name: MyType9 name: MyType
fixable: true
location: location:
row: 27 row: 27
column: 0 column: 0
end_location: end_location:
row: 27 row: 27
column: 55 column: 55
fix: ~ fix:
content: "class MyType(TypedDict):\n key: Literal[\"value\"]"
location:
row: 27
column: 0
end_location:
row: 27
column: 55
parent: ~ parent: ~
- kind: - kind:
ConvertTypedDictFunctionalToClass: ConvertTypedDictFunctionalToClass:
name: MyType10 name: MyType
fixable: true
location: location:
row: 30 row: 30
column: 0 column: 0
end_location: end_location:
row: 30 row: 30
column: 59 column: 49
fix: fix:
content: "class MyType10(TypedDict):\n key: Literal[\"value\"]" content: "class MyType(typing.TypedDict):\n key: int"
location: location:
row: 30 row: 30
column: 0 column: 0
end_location: end_location:
row: 30 row: 30
column: 59 column: 49
parent: ~
- kind:
ConvertTypedDictFunctionalToClass:
name: MyType11
location:
row: 33
column: 0
end_location:
row: 33
column: 53
fix:
content: "class MyType11(typing.TypedDict):\n key: int"
location:
row: 33
column: 0
end_location:
row: 33
column: 53
parent: ~
- kind:
ConvertTypedDictFunctionalToClass:
name: MyType12
location:
row: 37
column: 0
end_location:
row: 37
column: 58
fix: ~
parent: ~ parent: ~

View file

@ -4,25 +4,27 @@ expression: diagnostics
--- ---
- kind: - kind:
ConvertNamedTupleFunctionalToClass: ConvertNamedTupleFunctionalToClass:
name: NT1 name: MyType
fixable: true
location: location:
row: 5 row: 5
column: 0 column: 0
end_location: end_location:
row: 5 row: 5
column: 61 column: 67
fix: fix:
content: "class NT1(NamedTuple):\n a: int\n b: tuple[str, ...]" content: "class MyType(NamedTuple):\n a: int\n b: tuple[str, ...]"
location: location:
row: 5 row: 5
column: 0 column: 0
end_location: end_location:
row: 5 row: 5
column: 61 column: 67
parent: ~ parent: ~
- kind: - kind:
ConvertNamedTupleFunctionalToClass: ConvertNamedTupleFunctionalToClass:
name: NT2 name: MyType
fixable: true
location: location:
row: 8 row: 8
column: 0 column: 0
@ -30,7 +32,7 @@ expression: diagnostics
row: 12 row: 12
column: 1 column: 1
fix: fix:
content: "class NT2(NamedTuple):\n a: int\n b: str = \"foo\"\n c: list[bool] = [True]" content: "class MyType(NamedTuple):\n a: int\n b: str = \"foo\"\n c: list[bool] = [True]"
location: location:
row: 8 row: 8
column: 0 column: 0
@ -40,31 +42,21 @@ expression: diagnostics
parent: ~ parent: ~
- kind: - kind:
ConvertNamedTupleFunctionalToClass: ConvertNamedTupleFunctionalToClass:
name: NT3 name: MyType
fixable: true
location: location:
row: 15 row: 15
column: 0 column: 0
end_location: end_location:
row: 15 row: 15
column: 56 column: 62
fix: fix:
content: "class NT3(typing.NamedTuple):\n a: int\n b: str" content: "class MyType(typing.NamedTuple):\n a: int\n b: str"
location: location:
row: 15 row: 15
column: 0 column: 0
end_location: end_location:
row: 15 row: 15
column: 56 column: 62
parent: ~
- kind:
ConvertNamedTupleFunctionalToClass:
name: NT4
location:
row: 18
column: 0
end_location:
row: 22
column: 1
fix: ~
parent: ~ parent: ~