Avoid panic when `typename` is provided as a keyword argument (#6955)

## Summary

The `typename` argument to `NamedTuple` and `TypedDict` is a required
positional argument. We assumed as much, but panicked if it was provided
as a keyword argument or otherwise omitted. This PR handles the case
gracefully.

Closes https://github.com/astral-sh/ruff/issues/6953.
This commit is contained in:
Charlie Marsh 2023-08-28 17:03:18 -04:00 committed by GitHub
parent d1ad20c9ea
commit 87aa5d6b66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 34 additions and 29 deletions

View File

@ -22,3 +22,4 @@ MyType = typing.NamedTuple("MyType", a=int, b=tuple[str, ...])
# unfixable # unfixable
MyType = typing.NamedTuple("MyType", [("a", int)], [("b", str)]) MyType = typing.NamedTuple("MyType", [("a", int)], [("b", str)])
MyType = typing.NamedTuple("MyType", [("a", int)], b=str) MyType = typing.NamedTuple("MyType", [("a", int)], b=str)
MyType = typing.NamedTuple(typename="MyType", a=int, b=str)

View File

@ -72,8 +72,7 @@ fn match_named_tuple_assign<'a>(
value: &'a Expr, value: &'a Expr,
semantic: &SemanticModel, semantic: &SemanticModel,
) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a Expr)> { ) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a Expr)> {
let target = targets.get(0)?; let [Expr::Name(ast::ExprName { id: typename, .. })] = targets else {
let Expr::Name(ast::ExprName { id: typename, .. }) = target else {
return None; return None;
}; };
let Expr::Call(ast::ExprCall { let Expr::Call(ast::ExprCall {
@ -209,13 +208,13 @@ pub(crate) fn convert_named_tuple_functional_to_class(
return; return;
}; };
let properties = match (&args[1..], keywords) { let properties = match (args, keywords) {
// Ex) NamedTuple("MyType") // Ex) NamedTuple("MyType")
([], []) => vec![Stmt::Pass(ast::StmtPass { ([_typename], []) => vec![Stmt::Pass(ast::StmtPass {
range: TextRange::default(), range: TextRange::default(),
})], })],
// Ex) NamedTuple("MyType", [("a", int), ("b", str)]) // Ex) NamedTuple("MyType", [("a", int), ("b", str)])
([fields], []) => { ([_typename, fields], []) => {
if let Ok(properties) = create_properties_from_fields_arg(fields) { if let Ok(properties) = create_properties_from_fields_arg(fields) {
properties properties
} else { } else {
@ -224,7 +223,7 @@ pub(crate) fn convert_named_tuple_functional_to_class(
} }
} }
// Ex) NamedTuple("MyType", a=int, b=str) // Ex) NamedTuple("MyType", a=int, b=str)
([], keywords) => { ([_typename], keywords) => {
if let Ok(properties) = create_properties_from_keywords(keywords) { if let Ok(properties) = create_properties_from_keywords(keywords) {
properties properties
} else { } else {

View File

@ -71,8 +71,7 @@ fn match_typed_dict_assign<'a>(
value: &'a Expr, value: &'a Expr,
semantic: &SemanticModel, semantic: &SemanticModel,
) -> Option<(&'a str, &'a Arguments, &'a Expr)> { ) -> Option<(&'a str, &'a Arguments, &'a Expr)> {
let target = targets.get(0)?; let [Expr::Name(ast::ExprName { id: class_name, .. })] = targets else {
let Expr::Name(ast::ExprName { id: class_name, .. }) = target else {
return None; return None;
}; };
let Expr::Call(ast::ExprCall { let Expr::Call(ast::ExprCall {
@ -210,28 +209,34 @@ fn match_properties_and_total(arguments: &Arguments) -> Result<(Vec<Stmt>, Optio
// ``` // ```
// MyType = TypedDict('MyType', {'a': int, 'b': str}, a=int, b=str) // MyType = TypedDict('MyType', {'a': int, 'b': str}, a=int, b=str)
// ``` // ```
if let Some(dict) = arguments.args.get(1) { match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
let total = arguments.find_keyword("total"); // Ex) `TypedDict("MyType", {"a": int, "b": str})`
match dict { ([_typename, fields], [..]) => {
Expr::Dict(ast::ExprDict { let total = arguments.find_keyword("total");
keys, match fields {
values, Expr::Dict(ast::ExprDict {
range: _, keys,
}) => Ok((properties_from_dict_literal(keys, values)?, total)), values,
Expr::Call(ast::ExprCall { range: _,
func, }) => Ok((properties_from_dict_literal(keys, values)?, total)),
arguments: Arguments { keywords, .. }, Expr::Call(ast::ExprCall {
.. func,
}) => Ok((properties_from_dict_call(func, keywords)?, total)), arguments: Arguments { keywords, .. },
_ => bail!("Expected `arg` to be `Expr::Dict` or `Expr::Call`"), range: _,
}) => Ok((properties_from_dict_call(func, keywords)?, total)),
_ => bail!("Expected `arg` to be `Expr::Dict` or `Expr::Call`"),
}
} }
} else if !arguments.keywords.is_empty() { // Ex) `TypedDict("MyType")`
Ok((properties_from_keywords(&arguments.keywords)?, None)) ([_typename], []) => {
} else { let node = Stmt::Pass(ast::StmtPass {
let node = Stmt::Pass(ast::StmtPass { range: TextRange::default(),
range: TextRange::default(), });
}); Ok((vec![node], None))
Ok((vec![node], None)) }
// Ex) `TypedDict("MyType", a=int, b=str)`
([_typename], fields) => Ok((properties_from_keywords(fields)?, None)),
_ => bail!("Expected `args` to have exactly one or two elements"),
} }
} }