From 16e2dae0c2390ae8766b5e0e5daa6f755d19fa6a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 27 Feb 2023 11:18:34 -0500 Subject: [PATCH] Handle empty NamedTuple and TypedDict conversions (#3251) --- .../test/fixtures/pyupgrade/UP013.py | 6 +++ .../test/fixtures/pyupgrade/UP014.py | 6 +++ ...convert_named_tuple_functional_to_class.rs | 5 ++- .../convert_typed_dict_functional_to_class.rs | 21 ++++++---- ...ff__rules__pyupgrade__tests__UP013.py.snap | 38 +++++++++++++++++++ ...ff__rules__pyupgrade__tests__UP014.py.snap | 38 +++++++++++++++++++ 6 files changed, 106 insertions(+), 8 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP013.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP013.py index 8c187c1a44..0f2628dddd 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP013.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP013.py @@ -35,3 +35,9 @@ MyType = TypedDict("MyType", {"in": int, "x-y": int}) # unpacking (OK) c = {"c": float} MyType = TypedDict("MyType", {"a": int, "b": str, **c}) + +# Empty dict literal +MyType = TypedDict("MyType", {}) + +# Empty dict call +MyType = TypedDict("MyType", dict()) diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP014.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP014.py index 812f43e5e3..1df6e8d035 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP014.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP014.py @@ -23,3 +23,9 @@ MyType = NamedTuple( # invalid identifiers (OK) MyType = NamedTuple("MyType", [("x-y", int), ("b", tuple[str, ...])]) + +# no fields +MyType = typing.NamedTuple("MyType") + +# empty fields +MyType = typing.NamedTuple("MyType", []) diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs index 7c46593ad0..4b8244f611 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs @@ -103,11 +103,14 @@ fn match_defaults(keywords: &[Keyword]) -> Result<&[Expr]> { /// Create a list of property assignments from the `NamedTuple` arguments. fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result> { let Some(fields) = args.get(1) else { - return Ok(vec![]); + return Ok(vec![create_stmt(StmtKind::Pass)]); }; let ExprKind::List { elts, .. } = &fields.node else { bail!("Expected argument to be `ExprKind::List`"); }; + if elts.is_empty() { + return Ok(vec![create_stmt(StmtKind::Pass)]); + } let padded_defaults = if elts.len() >= defaults.len() { std::iter::repeat(None) .take(elts.len() - defaults.len()) diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index f58a5e687e..0c7bedcefa 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -78,11 +78,6 @@ fn create_property_assignment_stmt(property: &str, annotation: &ExprKind) -> Stm }) } -/// Generate a `StmtKind::Pass` statement. -fn create_pass_stmt() -> Stmt { - create_stmt(StmtKind::Pass) -} - /// Generate a `StmtKind:ClassDef` statement based on the provided body, /// keywords and base class. fn create_class_def_stmt( @@ -105,6 +100,10 @@ fn create_class_def_stmt( } fn properties_from_dict_literal(keys: &[Option], values: &[Expr]) -> Result> { + if keys.is_empty() { + return Ok(vec![create_stmt(StmtKind::Pass)]); + } + keys.iter() .zip(values.iter()) .map(|(key, value)| match key { @@ -134,11 +133,19 @@ fn properties_from_dict_call(func: &Expr, keywords: &[Keyword]) -> Result Result> { + if keywords.is_empty() { + return Ok(vec![create_stmt(StmtKind::Pass)]); + } + keywords .iter() .map(|keyword| { @@ -185,12 +192,12 @@ fn match_properties_and_total<'a>( ExprKind::Call { func, keywords, .. } => { Ok((properties_from_dict_call(func, keywords)?, total)) } - _ => Ok((vec![create_pass_stmt()], total)), + _ => bail!("Expected `arg` to be `ExprKind::Dict` or `ExprKind::Call`"), } } else if !keywords.is_empty() { Ok((properties_from_keywords(keywords)?, None)) } else { - Ok((vec![create_pass_stmt()], None)) + Ok((vec![create_stmt(StmtKind::Pass)], None)) } } diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP013.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP013.py.snap index 6729d468f7..b4ac653305 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP013.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP013.py.snap @@ -192,4 +192,42 @@ expression: diagnostics row: 30 column: 49 parent: ~ +- kind: + ConvertTypedDictFunctionalToClass: + name: MyType + fixable: true + location: + row: 40 + column: 0 + end_location: + row: 40 + column: 32 + fix: + content: "class MyType(TypedDict):\n pass" + location: + row: 40 + column: 0 + end_location: + row: 40 + column: 32 + parent: ~ +- kind: + ConvertTypedDictFunctionalToClass: + name: MyType + fixable: true + location: + row: 43 + column: 0 + end_location: + row: 43 + column: 36 + fix: + content: "class MyType(TypedDict):\n pass" + location: + row: 43 + column: 0 + end_location: + row: 43 + column: 36 + parent: ~ diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP014.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP014.py.snap index fc620b0b16..4871c56f67 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP014.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP014.py.snap @@ -59,4 +59,42 @@ expression: diagnostics row: 15 column: 62 parent: ~ +- kind: + ConvertNamedTupleFunctionalToClass: + name: MyType + fixable: true + location: + row: 28 + column: 0 + end_location: + row: 28 + column: 36 + fix: + content: "class MyType(typing.NamedTuple):\n pass" + location: + row: 28 + column: 0 + end_location: + row: 28 + column: 36 + parent: ~ +- kind: + ConvertNamedTupleFunctionalToClass: + name: MyType + fixable: true + location: + row: 31 + column: 0 + end_location: + row: 31 + column: 40 + fix: + content: "class MyType(typing.NamedTuple):\n pass" + location: + row: 31 + column: 0 + end_location: + row: 31 + column: 40 + parent: ~