From 5b63e63911e399a742d37d221d192d46736a1c31 Mon Sep 17 00:00:00 2001 From: darki73 Date: Tue, 31 Mar 2026 17:35:07 +0400 Subject: [PATCH] feat: PEP 695 type parameter syntax for Python 3.12+ --- conftest.py | 12 ++++++++ parso/python/grammar312.txt | 8 +++-- parso/python/grammar313.txt | 9 ++++-- parso/python/grammar314.txt | 9 ++++-- parso/python/tree.py | 41 +++++++++++++------------ test/test_parser.py | 61 +++++++++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 25 deletions(-) diff --git a/conftest.py b/conftest.py index 5d0c9fa..2cba256 100644 --- a/conftest.py +++ b/conftest.py @@ -145,3 +145,15 @@ def works_ge_py38(each_version): def works_ge_py39(each_version): version_info = parse_version_string(each_version) return Checker(each_version, version_info >= (3, 9)) + + +@pytest.fixture +def works_ge_py312(each_version): + version_info = parse_version_string(each_version) + return Checker(each_version, version_info >= (3, 12)) + + +@pytest.fixture +def works_ge_py313(each_version): + version_info = parse_version_string(each_version) + return Checker(each_version, version_info >= (3, 13)) diff --git a/parso/python/grammar312.txt b/parso/python/grammar312.txt index f092050..e81a792 100644 --- a/parso/python/grammar312.txt +++ b/parso/python/grammar312.txt @@ -17,7 +17,11 @@ decorators: decorator+ decorated: decorators (classdef | funcdef | async_funcdef) async_funcdef: 'async' funcdef -funcdef: 'def' NAME parameters ['->' test] ':' suite +funcdef: 'def' NAME [type_params] parameters ['->' test] ':' suite + +type_params: '[' type_param (',' type_param)* [','] ']' +type_param: NAME [type_param_bound] | '*' NAME | '**' NAME +type_param_bound: ':' test parameters: '(' [typedargslist] ')' typedargslist: ( @@ -131,7 +135,7 @@ dictorsetmaker: ( ((test ':' test | '**' expr) ((test [':=' test] | star_expr) (comp_for | (',' (test [':=' test] | star_expr))* [','])) ) -classdef: 'class' NAME ['(' [arglist] ')'] ':' suite +classdef: 'class' NAME [type_params] ['(' [arglist] ')'] ':' suite arglist: argument (',' argument)* [','] diff --git a/parso/python/grammar313.txt b/parso/python/grammar313.txt index f092050..04cd2d5 100644 --- a/parso/python/grammar313.txt +++ b/parso/python/grammar313.txt @@ -17,7 +17,12 @@ decorators: decorator+ decorated: decorators (classdef | funcdef | async_funcdef) async_funcdef: 'async' funcdef -funcdef: 'def' NAME parameters ['->' test] ':' suite +funcdef: 'def' NAME [type_params] parameters ['->' test] ':' suite + +type_params: '[' type_param (',' type_param)* [','] ']' +type_param: NAME [type_param_bound] [type_param_default] | '*' NAME [type_param_default] | '**' NAME [type_param_default] +type_param_bound: ':' test +type_param_default: '=' test parameters: '(' [typedargslist] ')' typedargslist: ( @@ -131,7 +136,7 @@ dictorsetmaker: ( ((test ':' test | '**' expr) ((test [':=' test] | star_expr) (comp_for | (',' (test [':=' test] | star_expr))* [','])) ) -classdef: 'class' NAME ['(' [arglist] ')'] ':' suite +classdef: 'class' NAME [type_params] ['(' [arglist] ')'] ':' suite arglist: argument (',' argument)* [','] diff --git a/parso/python/grammar314.txt b/parso/python/grammar314.txt index f092050..04cd2d5 100644 --- a/parso/python/grammar314.txt +++ b/parso/python/grammar314.txt @@ -17,7 +17,12 @@ decorators: decorator+ decorated: decorators (classdef | funcdef | async_funcdef) async_funcdef: 'async' funcdef -funcdef: 'def' NAME parameters ['->' test] ':' suite +funcdef: 'def' NAME [type_params] parameters ['->' test] ':' suite + +type_params: '[' type_param (',' type_param)* [','] ']' +type_param: NAME [type_param_bound] [type_param_default] | '*' NAME [type_param_default] | '**' NAME [type_param_default] +type_param_bound: ':' test +type_param_default: '=' test parameters: '(' [typedargslist] ')' typedargslist: ( @@ -131,7 +136,7 @@ dictorsetmaker: ( ((test ':' test | '**' expr) ((test [':=' test] | star_expr) (comp_for | (',' (test [':=' test] | star_expr))* [','])) ) -classdef: 'class' NAME ['(' [arglist] ')'] ':' suite +classdef: 'class' NAME [type_params] ['(' [arglist] ')'] ':' suite arglist: argument (',' argument)* [','] diff --git a/parso/python/tree.py b/parso/python/tree.py index 7c500ca..60a982d 100644 --- a/parso/python/tree.py +++ b/parso/python/tree.py @@ -479,13 +479,13 @@ class Class(ClassOrFunc): Returns the `arglist` node that defines the super classes. It returns None if there are no arguments. """ - if self.children[2] != '(': # Has no parentheses - return None - else: - if self.children[3] == ')': # Empty parentheses - return None - else: - return self.children[3] + for i, child in enumerate(self.children): + if child == '(': + next_child = self.children[i + 1] + if next_child == ')': + return None + return next_child + return None def _create_params(parent, argslist_list): @@ -552,15 +552,21 @@ class Function(ClassOrFunc): def __init__(self, children): super().__init__(children) - parameters = self.children[2] # After `def foo` + parameters = self._find_parameters() parameters_children = parameters.children[1:-1] - # If input parameters list already has Param objects, keep it as is; - # otherwise, convert it to a list of Param objects. if not any(isinstance(child, Param) for child in parameters_children): - parameters.children[1:-1] = _create_params(parameters, parameters_children) + parameters.children[1:-1] = _create_params( + parameters, parameters_children + ) + + def _find_parameters(self): + for child in self.children: + if child.type == 'parameters': + return child + raise Exception("A function should always have parameters") def _get_param_nodes(self): - return self.children[2].children + return self._find_parameters().children def get_params(self): """ @@ -633,13 +639,10 @@ class Function(ClassOrFunc): """ Returns the test node after `->` or `None` if there is no annotation. """ - try: - if self.children[3] == "->": - return self.children[4] - assert self.children[3] == ":" - return None - except IndexError: - return None + for i, child in enumerate(self.children): + if child == '->': + return self.children[i + 1] + return None class Lambda(Function): diff --git a/test/test_parser.py b/test/test_parser.py index 81e7170..d8a0fad 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -206,3 +206,64 @@ def test_positional_only_arguments(works_ge_py38, param_code): ) def test_decorator_expression(works_ge_py39, expression): works_ge_py39.parse("@%s\ndef x(): pass" % expression) + + +@pytest.mark.parametrize( + 'code', [ + 'class Foo[T]: pass', + 'class Foo[T: str]: pass', + 'class Foo[T, U]: pass', + 'class Foo[T: str, U: int]: pass', + 'class Foo[T](Base): pass', + 'class Foo[T: str](Base, Mixin): pass', + 'class Foo[*Ts]: pass', + 'class Foo[**P]: pass', + ] +) +def test_pep695_generic_class(works_ge_py312, code): + works_ge_py312.parse(code) + + +@pytest.mark.parametrize( + 'code', [ + 'def foo[T](x: T) -> T: pass', + 'def foo[T: int](x: T) -> T: pass', + 'def foo[T, U](x: T, y: U): pass', + 'def foo[*Ts](*args): pass', + 'def foo[**P](*args): pass', + ] +) +def test_pep695_generic_function(works_ge_py312, code): + works_ge_py312.parse(code) + + +def test_pep695_class_get_super_arglist(works_ge_py312): + module = works_ge_py312.parse('class Foo[T](Bar, Baz): pass') + if module is None: + return + classdef = module.children[0] + arglist = classdef.get_super_arglist() + assert arglist is not None + assert 'Bar' in arglist.get_code() + assert 'Baz' in arglist.get_code() + + +def test_pep695_class_no_bases(works_ge_py312): + module = works_ge_py312.parse('class Foo[T]: pass') + if module is None: + return + classdef = module.children[0] + assert classdef.get_super_arglist() is None + + +@pytest.mark.parametrize( + 'code', [ + 'class Foo[T = int]: pass', + 'class Foo[T: str = "default"]: pass', + 'class Foo[*Ts = tuple[int, ...]]: pass', + 'class Foo[**P = None]: pass', + 'def foo[T = int](x: T) -> T: pass', + ] +) +def test_pep696_type_param_defaults(works_ge_py313, code): + works_ge_py313.parse(code)