diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index f113b39505..d6cb15670f 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -957,7 +957,7 @@ where self.with_type_params( NodeWithScopeRef::TypeAliasTypeParameters(type_alias), - type_alias.type_params.as_ref(), + type_alias.type_params.as_deref(), |builder| { builder.push_scope(NodeWithScopeRef::TypeAlias(type_alias)); builder.visit_expr(&type_alias.value); diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 9f4d63a484..0c9864c6bf 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -548,11 +548,13 @@ impl SemanticSyntaxContext for Checker<'_> { } } SemanticSyntaxErrorKind::ReboundComprehensionVariable + | SemanticSyntaxErrorKind::DuplicateTypeParameter if self.settings.preview.is_enabled() => { self.semantic_errors.borrow_mut().push(error); } - SemanticSyntaxErrorKind::ReboundComprehensionVariable => {} + SemanticSyntaxErrorKind::ReboundComprehensionVariable + | SemanticSyntaxErrorKind::DuplicateTypeParameter => {} } } } diff --git a/crates/ruff_python_ast/ast.toml b/crates/ruff_python_ast/ast.toml index 612f8a8f14..e1288f0af2 100644 --- a/crates/ruff_python_ast/ast.toml +++ b/crates/ruff_python_ast/ast.toml @@ -104,7 +104,7 @@ fields = [{ name = "targets", type = "Expr*" }] doc = "See also [TypeAlias](https://docs.python.org/3/library/ast.html#ast.TypeAlias)" fields = [ { name = "name", type = "Expr" }, - { name = "type_params", type = "TypeParams?" }, + { name = "type_params", type = "Box?" }, { name = "value", type = "Expr" }, ] diff --git a/crates/ruff_python_ast/src/generated.rs b/crates/ruff_python_ast/src/generated.rs index e573315ab1..a421411422 100644 --- a/crates/ruff_python_ast/src/generated.rs +++ b/crates/ruff_python_ast/src/generated.rs @@ -6494,7 +6494,7 @@ pub struct StmtDelete { pub struct StmtTypeAlias { pub range: ruff_text_size::TextRange, pub name: Box, - pub type_params: Option, + pub type_params: Option>, pub value: Box, } diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 81bd07baae..4d207ee547 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -1622,10 +1622,10 @@ mod tests { }); let type_alias = Stmt::TypeAlias(StmtTypeAlias { name: Box::new(name.clone()), - type_params: Some(TypeParams { + type_params: Some(Box::new(TypeParams { type_params: vec![type_var_one, type_var_two], range: TextRange::default(), - }), + })), value: Box::new(constant_three.clone()), range: TextRange::default(), }); diff --git a/crates/ruff_python_parser/resources/inline/err/duplicate_type_parameter_names.py b/crates/ruff_python_parser/resources/inline/err/duplicate_type_parameter_names.py new file mode 100644 index 0000000000..938111c120 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/duplicate_type_parameter_names.py @@ -0,0 +1,7 @@ +type Alias[T, T] = ... +def f[T, T](t: T): ... +class C[T, T]: ... +type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ... +def f[T, T, T](): ... # two errors +def f[T, *T](): ... # star is still duplicate +def f[T, **T](): ... # as is double star diff --git a/crates/ruff_python_parser/resources/inline/ok/non_duplicate_type_parameter_names.py b/crates/ruff_python_parser/resources/inline/ok/non_duplicate_type_parameter_names.py new file mode 100644 index 0000000000..e27b6d417c --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/non_duplicate_type_parameter_names.py @@ -0,0 +1,5 @@ +type Alias[T] = list[T] +def f[T](t: T): ... +class C[T]: ... +class C[T, U, V]: ... +type Alias[T, U: str, V: (str, bytes), *Ts, **P, D = default] = ... diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 727a28803f..46016e6aab 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -974,7 +974,7 @@ impl<'src> Parser<'src> { ast::StmtTypeAlias { name: Box::new(name), - type_params, + type_params: type_params.map(Box::new), value: Box::new(value.expr), range: self.node_range(start), } diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index 185cf7e9a2..de03216ea1 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -11,7 +11,7 @@ use ruff_python_ast::{ visitor::{walk_expr, Visitor}, Expr, PythonVersion, Stmt, StmtExpr, StmtImportFrom, }; -use ruff_text_size::TextRange; +use ruff_text_size::{Ranged, TextRange}; #[derive(Debug)] pub struct SemanticSyntaxChecker { @@ -53,12 +53,60 @@ impl SemanticSyntaxChecker { }); } - fn check_stmt(&self, stmt: &ast::Stmt, ctx: &Ctx) { + fn check_stmt(&mut self, stmt: &ast::Stmt, ctx: &Ctx) { if let Stmt::ImportFrom(StmtImportFrom { range, module, .. }) = stmt { if self.seen_futures_boundary && matches!(module.as_deref(), Some("__future__")) { Self::add_error(ctx, SemanticSyntaxErrorKind::LateFutureImport, *range); } } + + Self::duplicate_type_parameter_name(stmt, ctx); + } + + fn duplicate_type_parameter_name(stmt: &ast::Stmt, ctx: &Ctx) { + let (Stmt::FunctionDef(ast::StmtFunctionDef { type_params, .. }) + | Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) + | Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. })) = stmt + else { + return; + }; + + let Some(type_params) = type_params else { + return; + }; + + if type_params.len() < 2 { + return; + } + + for (i, type_param) in type_params.iter().enumerate() { + if type_params + .iter() + .take(i) + .any(|t| t.name().id == type_param.name().id) + { + // test_ok non_duplicate_type_parameter_names + // type Alias[T] = list[T] + // def f[T](t: T): ... + // class C[T]: ... + // class C[T, U, V]: ... + // type Alias[T, U: str, V: (str, bytes), *Ts, **P, D = default] = ... + + // test_err duplicate_type_parameter_names + // type Alias[T, T] = ... + // def f[T, T](t: T): ... + // class C[T, T]: ... + // type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ... + // def f[T, T, T](): ... # two errors + // def f[T, *T](): ... # star is still duplicate + // def f[T, **T](): ... # as is double star + Self::add_error( + ctx, + SemanticSyntaxErrorKind::DuplicateTypeParameter, + type_param.range(), + ); + } + } } pub fn visit_stmt(&mut self, stmt: &ast::Stmt, ctx: &Ctx) { @@ -168,6 +216,9 @@ impl Display for SemanticSyntaxError { SemanticSyntaxErrorKind::ReboundComprehensionVariable => { f.write_str("assignment expression cannot rebind comprehension variable") } + SemanticSyntaxErrorKind::DuplicateTypeParameter => { + f.write_str("duplicate type parameter") + } } } } @@ -202,6 +253,18 @@ pub enum SemanticSyntaxErrorKind { /// ((a := 0) for a in range(0)) /// ``` ReboundComprehensionVariable, + + /// Represents a duplicate type parameter name in a function definition, class definition, or + /// type alias statement. + /// + /// ## Examples + /// + /// ```python + /// type Alias[T, T] = ... + /// def f[T, T](t: T): ... + /// class C[T, T]: ... + /// ``` + DuplicateTypeParameter, } /// Searches for the first named expression (`x := y`) rebinding one of the `iteration_variables` in diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap new file mode 100644 index 0000000000..88b7f1ffb7 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap @@ -0,0 +1,589 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/duplicate_type_parameter_names.py +--- +## AST + +``` +Module( + ModModule { + range: 0..261, + body: [ + TypeAlias( + StmtTypeAlias { + range: 0..22, + name: Name( + ExprName { + range: 5..10, + id: Name("Alias"), + ctx: Store, + }, + ), + type_params: Some( + TypeParams { + range: 10..16, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 11..12, + name: Identifier { + id: Name("T"), + range: 11..12, + }, + bound: None, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 14..15, + name: Identifier { + id: Name("T"), + range: 14..15, + }, + bound: None, + default: None, + }, + ), + ], + }, + ), + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 19..22, + }, + ), + }, + ), + FunctionDef( + StmtFunctionDef { + range: 23..45, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("f"), + range: 27..28, + }, + type_params: Some( + TypeParams { + range: 28..34, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 29..30, + name: Identifier { + id: Name("T"), + range: 29..30, + }, + bound: None, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 32..33, + name: Identifier { + id: Name("T"), + range: 32..33, + }, + bound: None, + default: None, + }, + ), + ], + }, + ), + parameters: Parameters { + range: 34..40, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 35..39, + parameter: Parameter { + range: 35..39, + name: Identifier { + id: Name("t"), + range: 35..36, + }, + annotation: Some( + Name( + ExprName { + range: 38..39, + id: Name("T"), + ctx: Load, + }, + ), + ), + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 42..45, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 42..45, + }, + ), + }, + ), + ], + }, + ), + ClassDef( + StmtClassDef { + range: 46..64, + decorator_list: [], + name: Identifier { + id: Name("C"), + range: 52..53, + }, + type_params: Some( + TypeParams { + range: 53..59, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 54..55, + name: Identifier { + id: Name("T"), + range: 54..55, + }, + bound: None, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 57..58, + name: Identifier { + id: Name("T"), + range: 57..58, + }, + bound: None, + default: None, + }, + ), + ], + }, + ), + arguments: None, + body: [ + Expr( + StmtExpr { + range: 61..64, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 61..64, + }, + ), + }, + ), + ], + }, + ), + TypeAlias( + StmtTypeAlias { + range: 65..132, + name: Name( + ExprName { + range: 70..75, + id: Name("Alias"), + ctx: Store, + }, + ), + type_params: Some( + TypeParams { + range: 75..126, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 76..77, + name: Identifier { + id: Name("T"), + range: 76..77, + }, + bound: None, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 79..85, + name: Identifier { + id: Name("U"), + range: 79..80, + }, + bound: Some( + Name( + ExprName { + range: 82..85, + id: Name("str"), + ctx: Load, + }, + ), + ), + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 87..102, + name: Identifier { + id: Name("V"), + range: 87..88, + }, + bound: Some( + Tuple( + ExprTuple { + range: 90..102, + elts: [ + Name( + ExprName { + range: 91..94, + id: Name("str"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 96..101, + id: Name("bytes"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: true, + }, + ), + ), + default: None, + }, + ), + TypeVarTuple( + TypeParamTypeVarTuple { + range: 104..107, + name: Identifier { + id: Name("Ts"), + range: 105..107, + }, + default: None, + }, + ), + ParamSpec( + TypeParamParamSpec { + range: 109..112, + name: Identifier { + id: Name("P"), + range: 111..112, + }, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 114..125, + name: Identifier { + id: Name("T"), + range: 114..115, + }, + bound: None, + default: Some( + Name( + ExprName { + range: 118..125, + id: Name("default"), + ctx: Load, + }, + ), + ), + }, + ), + ], + }, + ), + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 129..132, + }, + ), + }, + ), + FunctionDef( + StmtFunctionDef { + range: 133..154, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("f"), + range: 137..138, + }, + type_params: Some( + TypeParams { + range: 138..147, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 139..140, + name: Identifier { + id: Name("T"), + range: 139..140, + }, + bound: None, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 142..143, + name: Identifier { + id: Name("T"), + range: 142..143, + }, + bound: None, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 145..146, + name: Identifier { + id: Name("T"), + range: 145..146, + }, + bound: None, + default: None, + }, + ), + ], + }, + ), + parameters: Parameters { + range: 147..149, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 151..154, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 151..154, + }, + ), + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + range: 169..188, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("f"), + range: 173..174, + }, + type_params: Some( + TypeParams { + range: 174..181, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 175..176, + name: Identifier { + id: Name("T"), + range: 175..176, + }, + bound: None, + default: None, + }, + ), + TypeVarTuple( + TypeParamTypeVarTuple { + range: 178..180, + name: Identifier { + id: Name("T"), + range: 179..180, + }, + default: None, + }, + ), + ], + }, + ), + parameters: Parameters { + range: 181..183, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 185..188, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 185..188, + }, + ), + }, + ), + ], + }, + ), + FunctionDef( + StmtFunctionDef { + range: 218..238, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("f"), + range: 222..223, + }, + type_params: Some( + TypeParams { + range: 223..231, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 224..225, + name: Identifier { + id: Name("T"), + range: 224..225, + }, + bound: None, + default: None, + }, + ), + ParamSpec( + TypeParamParamSpec { + range: 227..230, + name: Identifier { + id: Name("T"), + range: 229..230, + }, + default: None, + }, + ), + ], + }, + ), + parameters: Parameters { + range: 231..233, + posonlyargs: [], + args: [], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 235..238, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 235..238, + }, + ), + }, + ), + ], + }, + ), + ], + }, +) +``` +## Semantic Syntax Errors + + | +1 | type Alias[T, T] = ... + | ^ Syntax Error: duplicate type parameter +2 | def f[T, T](t: T): ... +3 | class C[T, T]: ... + | + + + | +1 | type Alias[T, T] = ... +2 | def f[T, T](t: T): ... + | ^ Syntax Error: duplicate type parameter +3 | class C[T, T]: ... +4 | type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ... + | + + + | +1 | type Alias[T, T] = ... +2 | def f[T, T](t: T): ... +3 | class C[T, T]: ... + | ^ Syntax Error: duplicate type parameter +4 | type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ... +5 | def f[T, T, T](): ... # two errors + | + + + | +2 | def f[T, T](t: T): ... +3 | class C[T, T]: ... +4 | type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ... + | ^^^^^^^^^^^ Syntax Error: duplicate type parameter +5 | def f[T, T, T](): ... # two errors +6 | def f[T, *T](): ... # star is still duplicate + | + + + | +3 | class C[T, T]: ... +4 | type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ... +5 | def f[T, T, T](): ... # two errors + | ^ Syntax Error: duplicate type parameter +6 | def f[T, *T](): ... # star is still duplicate +7 | def f[T, **T](): ... # as is double star + | + + + | +3 | class C[T, T]: ... +4 | type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ... +5 | def f[T, T, T](): ... # two errors + | ^ Syntax Error: duplicate type parameter +6 | def f[T, *T](): ... # star is still duplicate +7 | def f[T, **T](): ... # as is double star + | + + + | +4 | type Alias[T, U: str, V: (str, bytes), *Ts, **P, T = default] = ... +5 | def f[T, T, T](): ... # two errors +6 | def f[T, *T](): ... # star is still duplicate + | ^^ Syntax Error: duplicate type parameter +7 | def f[T, **T](): ... # as is double star + | + + + | +5 | def f[T, T, T](): ... # two errors +6 | def f[T, *T](): ... # star is still duplicate +7 | def f[T, **T](): ... # as is double star + | ^^^ Syntax Error: duplicate type parameter + | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap new file mode 100644 index 0000000000..766ff4bc23 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@non_duplicate_type_parameter_names.py.snap @@ -0,0 +1,368 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/non_duplicate_type_parameter_names.py +--- +## AST + +``` +Module( + ModModule { + range: 0..150, + body: [ + TypeAlias( + StmtTypeAlias { + range: 0..23, + name: Name( + ExprName { + range: 5..10, + id: Name("Alias"), + ctx: Store, + }, + ), + type_params: Some( + TypeParams { + range: 10..13, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 11..12, + name: Identifier { + id: Name("T"), + range: 11..12, + }, + bound: None, + default: None, + }, + ), + ], + }, + ), + value: Subscript( + ExprSubscript { + range: 16..23, + value: Name( + ExprName { + range: 16..20, + id: Name("list"), + ctx: Load, + }, + ), + slice: Name( + ExprName { + range: 21..22, + id: Name("T"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + }, + ), + FunctionDef( + StmtFunctionDef { + range: 24..43, + is_async: false, + decorator_list: [], + name: Identifier { + id: Name("f"), + range: 28..29, + }, + type_params: Some( + TypeParams { + range: 29..32, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 30..31, + name: Identifier { + id: Name("T"), + range: 30..31, + }, + bound: None, + default: None, + }, + ), + ], + }, + ), + parameters: Parameters { + range: 32..38, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 33..37, + parameter: Parameter { + range: 33..37, + name: Identifier { + id: Name("t"), + range: 33..34, + }, + annotation: Some( + Name( + ExprName { + range: 36..37, + id: Name("T"), + ctx: Load, + }, + ), + ), + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + returns: None, + body: [ + Expr( + StmtExpr { + range: 40..43, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 40..43, + }, + ), + }, + ), + ], + }, + ), + ClassDef( + StmtClassDef { + range: 44..59, + decorator_list: [], + name: Identifier { + id: Name("C"), + range: 50..51, + }, + type_params: Some( + TypeParams { + range: 51..54, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 52..53, + name: Identifier { + id: Name("T"), + range: 52..53, + }, + bound: None, + default: None, + }, + ), + ], + }, + ), + arguments: None, + body: [ + Expr( + StmtExpr { + range: 56..59, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 56..59, + }, + ), + }, + ), + ], + }, + ), + ClassDef( + StmtClassDef { + range: 60..81, + decorator_list: [], + name: Identifier { + id: Name("C"), + range: 66..67, + }, + type_params: Some( + TypeParams { + range: 67..76, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 68..69, + name: Identifier { + id: Name("T"), + range: 68..69, + }, + bound: None, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 71..72, + name: Identifier { + id: Name("U"), + range: 71..72, + }, + bound: None, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 74..75, + name: Identifier { + id: Name("V"), + range: 74..75, + }, + bound: None, + default: None, + }, + ), + ], + }, + ), + arguments: None, + body: [ + Expr( + StmtExpr { + range: 78..81, + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 78..81, + }, + ), + }, + ), + ], + }, + ), + TypeAlias( + StmtTypeAlias { + range: 82..149, + name: Name( + ExprName { + range: 87..92, + id: Name("Alias"), + ctx: Store, + }, + ), + type_params: Some( + TypeParams { + range: 92..143, + type_params: [ + TypeVar( + TypeParamTypeVar { + range: 93..94, + name: Identifier { + id: Name("T"), + range: 93..94, + }, + bound: None, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 96..102, + name: Identifier { + id: Name("U"), + range: 96..97, + }, + bound: Some( + Name( + ExprName { + range: 99..102, + id: Name("str"), + ctx: Load, + }, + ), + ), + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 104..119, + name: Identifier { + id: Name("V"), + range: 104..105, + }, + bound: Some( + Tuple( + ExprTuple { + range: 107..119, + elts: [ + Name( + ExprName { + range: 108..111, + id: Name("str"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 113..118, + id: Name("bytes"), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: true, + }, + ), + ), + default: None, + }, + ), + TypeVarTuple( + TypeParamTypeVarTuple { + range: 121..124, + name: Identifier { + id: Name("Ts"), + range: 122..124, + }, + default: None, + }, + ), + ParamSpec( + TypeParamParamSpec { + range: 126..129, + name: Identifier { + id: Name("P"), + range: 128..129, + }, + default: None, + }, + ), + TypeVar( + TypeParamTypeVar { + range: 131..142, + name: Identifier { + id: Name("D"), + range: 131..132, + }, + bound: None, + default: Some( + Name( + ExprName { + range: 135..142, + id: Name("default"), + ctx: Load, + }, + ), + ), + }, + ), + ], + }, + ), + value: EllipsisLiteral( + ExprEllipsisLiteral { + range: 146..149, + }, + ), + }, + ), + ], + }, +) +```