Do not fix `NamedTuple` calls containing both a list of fields and keywords (#5799)

## Summary

Fixes #5794

## Test Plan

Existing tests
This commit is contained in:
Harutaka Kawamura 2023-07-17 10:31:53 +09:00 committed by GitHub
parent ae431df146
commit cfec636046
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 118 additions and 134 deletions

View File

@ -4,23 +4,9 @@ import typing
# with complex annotations # with complex annotations
MyType = NamedTuple("MyType", [("a", int), ("b", tuple[str, ...])]) MyType = NamedTuple("MyType", [("a", int), ("b", tuple[str, ...])])
# with default values as list
MyType = NamedTuple(
"MyType",
[("a", int), ("b", str), ("c", list[bool])],
defaults=["foo", [True]],
)
# with namespace # with namespace
MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)]) MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)])
# too many default values (OK)
MyType = NamedTuple(
"MyType",
[("a", int), ("b", str)],
defaults=[1, "bar", "baz"],
)
# invalid identifiers (OK) # invalid identifiers (OK)
MyType = NamedTuple("MyType", [("x-y", int), ("b", tuple[str, ...])]) MyType = NamedTuple("MyType", [("x-y", int), ("b", tuple[str, ...])])
@ -29,3 +15,10 @@ MyType = typing.NamedTuple("MyType")
# empty fields # empty fields
MyType = typing.NamedTuple("MyType", []) MyType = typing.NamedTuple("MyType", [])
# keywords
MyType = typing.NamedTuple("MyType", a=int, b=tuple[str, ...])
# unfixable
MyType = typing.NamedTuple("MyType", [("a", int)], [("b", str)])
MyType = typing.NamedTuple("MyType", [("a", int)], b=str)

View File

@ -93,11 +93,7 @@ fn match_named_tuple_assign<'a>(
/// Generate a `Stmt::AnnAssign` representing the provided property /// Generate a `Stmt::AnnAssign` representing the provided property
/// definition. /// definition.
fn create_property_assignment_stmt( fn create_property_assignment_stmt(property: &str, annotation: &Expr) -> Stmt {
property: &str,
annotation: &Expr,
value: Option<&Expr>,
) -> Stmt {
ast::StmtAnnAssign { ast::StmtAnnAssign {
target: Box::new( target: Box::new(
ast::ExprName { ast::ExprName {
@ -108,40 +104,15 @@ fn create_property_assignment_stmt(
.into(), .into(),
), ),
annotation: Box::new(annotation.clone()), annotation: Box::new(annotation.clone()),
value: value.map(|value| Box::new(value.clone())), value: None,
simple: true, simple: true,
range: TextRange::default(), range: TextRange::default(),
} }
.into() .into()
} }
/// Match the `defaults` keyword in a `NamedTuple(...)` call. /// Create a list of property assignments from the `NamedTuple` fields argument.
fn match_defaults(keywords: &[Keyword]) -> Result<&[Expr]> { fn create_properties_from_fields_arg(fields: &Expr) -> Result<Vec<Stmt>> {
let defaults = keywords.iter().find(|keyword| {
if let Some(arg) = &keyword.arg {
arg == "defaults"
} else {
false
}
});
match defaults {
Some(defaults) => match &defaults.value {
Expr::List(ast::ExprList { elts, .. }) => Ok(elts),
Expr::Tuple(ast::ExprTuple { elts, .. }) => Ok(elts),
_ => bail!("Expected defaults to be `Expr::List` | `Expr::Tuple`"),
},
None => Ok(&[]),
}
}
/// Create a list of property assignments from the `NamedTuple` arguments.
fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<Stmt>> {
let Some(fields) = args.get(1) else {
let node = Stmt::Pass(ast::StmtPass {
range: TextRange::default(),
});
return Ok(vec![node]);
};
let Expr::List(ast::ExprList { elts, .. }) = &fields else { let Expr::List(ast::ExprList { elts, .. }) = &fields else {
bail!("Expected argument to be `Expr::List`"); bail!("Expected argument to be `Expr::List`");
}; };
@ -151,16 +122,8 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<S
}); });
return Ok(vec![node]); return Ok(vec![node]);
} }
let padded_defaults = if elts.len() >= defaults.len() {
std::iter::repeat(None)
.take(elts.len() - defaults.len())
.chain(defaults.iter().map(Some))
} else {
bail!("Defaults must be `None` or an iterable of at least the number of fields")
};
elts.iter() elts.iter()
.zip(padded_defaults) .map(|field| {
.map(|(field, default)| {
let Expr::Tuple(ast::ExprTuple { elts, .. }) = &field else { let Expr::Tuple(ast::ExprTuple { elts, .. }) = &field else {
bail!("Expected `field` to be `Expr::Tuple`") bail!("Expected `field` to be `Expr::Tuple`")
}; };
@ -180,9 +143,21 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<S
if is_dunder(property) { if is_dunder(property) {
bail!("Cannot use dunder property name: {}", property) bail!("Cannot use dunder property name: {}", property)
} }
Ok(create_property_assignment_stmt( Ok(create_property_assignment_stmt(property, annotation))
property, annotation, default, })
)) .collect()
}
/// Create a list of property assignments from the `NamedTuple` keyword arguments.
fn create_properties_from_keywords(keywords: &[Keyword]) -> Result<Vec<Stmt>> {
keywords
.iter()
.map(|keyword| {
let Keyword { arg, value, .. } = keyword;
let Some(arg) = arg else {
bail!("Expected `keyword` to have an `arg`")
};
Ok(create_property_assignment_stmt(arg.as_str(), value))
}) })
.collect() .collect()
} }
@ -228,12 +203,32 @@ pub(crate) fn convert_named_tuple_functional_to_class(
return; return;
}; };
let properties = match match_defaults(keywords) let properties = match (&args[1..], keywords) {
.and_then(|defaults| create_properties_from_args(args, defaults)) // Ex) NamedTuple("MyType")
{ ([], []) => vec![Stmt::Pass(ast::StmtPass {
Ok(properties) => properties, range: TextRange::default(),
Err(err) => { })],
debug!("Skipping `NamedTuple` \"{typename}\": {err}"); // Ex) NamedTuple("MyType", [("a", int), ("b", str)])
([fields], []) => {
if let Ok(properties) = create_properties_from_fields_arg(fields) {
properties
} else {
debug!("Skipping `NamedTuple` \"{typename}\": unable to parse fields");
return;
}
}
// Ex) NamedTuple("MyType", a=int, b=str)
([], keywords) => {
if let Ok(properties) = create_properties_from_keywords(keywords) {
properties
} else {
debug!("Skipping `NamedTuple` \"{typename}\": unable to parse keywords");
return;
}
}
// Unfixable
_ => {
debug!("Skipping `NamedTuple` \"{typename}\": mixed fields and keywords");
return; return;
} }
}; };

View File

@ -7,7 +7,7 @@ UP014.py:5:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class s
5 | MyType = NamedTuple("MyType", [("a", int), ("b", tuple[str, ...])]) 5 | MyType = NamedTuple("MyType", [("a", int), ("b", tuple[str, ...])])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP014 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP014
6 | 6 |
7 | # with default values as list 7 | # with namespace
| |
= help: Convert `MyType` to class syntax = help: Convert `MyType` to class syntax
@ -20,97 +20,93 @@ UP014.py:5:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class s
6 |+ a: int 6 |+ a: int
7 |+ b: tuple[str, ...] 7 |+ b: tuple[str, ...]
6 8 | 6 8 |
7 9 | # with default values as list 7 9 | # with namespace
8 10 | MyType = NamedTuple( 8 10 | MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)])
UP014.py:8:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class syntax UP014.py:8:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class syntax
| |
7 | # with default values as list 7 | # with namespace
8 | / MyType = NamedTuple( 8 | MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)])
9 | | "MyType", | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP014
10 | | [("a", int), ("b", str), ("c", list[bool])], 9 |
11 | | defaults=["foo", [True]], 10 | # invalid identifiers (OK)
12 | | )
| |_^ UP014
13 |
14 | # with namespace
| |
= help: Convert `MyType` to class syntax = help: Convert `MyType` to class syntax
Suggested fix Suggested fix
5 5 | MyType = NamedTuple("MyType", [("a", int), ("b", tuple[str, ...])]) 5 5 | MyType = NamedTuple("MyType", [("a", int), ("b", tuple[str, ...])])
6 6 | 6 6 |
7 7 | # with default values as list 7 7 | # with namespace
8 |-MyType = NamedTuple( 8 |-MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)])
9 |- "MyType", 8 |+class MyType(typing.NamedTuple):
10 |- [("a", int), ("b", str), ("c", list[bool])],
11 |- defaults=["foo", [True]],
12 |-)
8 |+class MyType(NamedTuple):
9 |+ a: int 9 |+ a: int
10 |+ b: str = "foo" 10 |+ b: str
11 |+ c: list[bool] = [True] 9 11 |
13 12 | 10 12 | # invalid identifiers (OK)
14 13 | # with namespace 11 13 | MyType = NamedTuple("MyType", [("x-y", int), ("b", tuple[str, ...])])
15 14 | MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)])
UP014.py:15:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class syntax UP014.py:14:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class syntax
| |
14 | # with namespace 13 | # no fields
15 | MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)]) 14 | MyType = typing.NamedTuple("MyType")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP014
16 |
17 | # too many default values (OK)
|
= help: Convert `MyType` to class syntax
Suggested fix
12 12 | )
13 13 |
14 14 | # with namespace
15 |-MyType = typing.NamedTuple("MyType", [("a", int), ("b", str)])
15 |+class MyType(typing.NamedTuple):
16 |+ a: int
17 |+ b: str
16 18 |
17 19 | # too many default values (OK)
18 20 | MyType = NamedTuple(
UP014.py:28:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class syntax
|
27 | # no fields
28 | MyType = typing.NamedTuple("MyType")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP014 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP014
29 | 15 |
30 | # empty fields 16 | # empty fields
| |
= help: Convert `MyType` to class syntax = help: Convert `MyType` to class syntax
Suggested fix Suggested fix
25 25 | MyType = NamedTuple("MyType", [("x-y", int), ("b", tuple[str, ...])]) 11 11 | MyType = NamedTuple("MyType", [("x-y", int), ("b", tuple[str, ...])])
26 26 | 12 12 |
27 27 | # no fields 13 13 | # no fields
28 |-MyType = typing.NamedTuple("MyType") 14 |-MyType = typing.NamedTuple("MyType")
28 |+class MyType(typing.NamedTuple): 14 |+class MyType(typing.NamedTuple):
29 |+ pass 15 |+ pass
29 30 | 15 16 |
30 31 | # empty fields 16 17 | # empty fields
31 32 | MyType = typing.NamedTuple("MyType", []) 17 18 | MyType = typing.NamedTuple("MyType", [])
UP014.py:31:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class syntax UP014.py:17:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class syntax
| |
30 | # empty fields 16 | # empty fields
31 | MyType = typing.NamedTuple("MyType", []) 17 | MyType = typing.NamedTuple("MyType", [])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP014 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP014
18 |
19 | # keywords
| |
= help: Convert `MyType` to class syntax = help: Convert `MyType` to class syntax
Suggested fix Suggested fix
28 28 | MyType = typing.NamedTuple("MyType") 14 14 | MyType = typing.NamedTuple("MyType")
29 29 | 15 15 |
30 30 | # empty fields 16 16 | # empty fields
31 |-MyType = typing.NamedTuple("MyType", []) 17 |-MyType = typing.NamedTuple("MyType", [])
31 |+class MyType(typing.NamedTuple): 17 |+class MyType(typing.NamedTuple):
32 |+ pass 18 |+ pass
18 19 |
19 20 | # keywords
20 21 | MyType = typing.NamedTuple("MyType", a=int, b=tuple[str, ...])
UP014.py:20:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class syntax
|
19 | # keywords
20 | MyType = typing.NamedTuple("MyType", a=int, b=tuple[str, ...])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP014
21 |
22 | # unfixable
|
= help: Convert `MyType` to class syntax
Suggested fix
17 17 | MyType = typing.NamedTuple("MyType", [])
18 18 |
19 19 | # keywords
20 |-MyType = typing.NamedTuple("MyType", a=int, b=tuple[str, ...])
20 |+class MyType(typing.NamedTuple):
21 |+ a: int
22 |+ b: tuple[str, ...]
21 23 |
22 24 | # unfixable
23 25 | MyType = typing.NamedTuple("MyType", [("a", int)], [("b", str)])