mirror of https://github.com/astral-sh/ruff
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:
parent
d1ad20c9ea
commit
87aa5d6b66
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue