Skip to content

Commit a36a421

Browse files
authored
Merge pull request #237 from darki73/feat/pep695-type-params
PEP 695 type parameter syntax for Python 3.12+
2 parents 28005a5 + 5b63e63 commit a36a421

File tree

6 files changed

+115
-25
lines changed

6 files changed

+115
-25
lines changed

conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,15 @@ def works_ge_py38(each_version):
145145
def works_ge_py39(each_version):
146146
version_info = parse_version_string(each_version)
147147
return Checker(each_version, version_info >= (3, 9))
148+
149+
150+
@pytest.fixture
151+
def works_ge_py312(each_version):
152+
version_info = parse_version_string(each_version)
153+
return Checker(each_version, version_info >= (3, 12))
154+
155+
156+
@pytest.fixture
157+
def works_ge_py313(each_version):
158+
version_info = parse_version_string(each_version)
159+
return Checker(each_version, version_info >= (3, 13))

parso/python/grammar312.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ decorators: decorator+
1717
decorated: decorators (classdef | funcdef | async_funcdef)
1818

1919
async_funcdef: 'async' funcdef
20-
funcdef: 'def' NAME parameters ['->' test] ':' suite
20+
funcdef: 'def' NAME [type_params] parameters ['->' test] ':' suite
21+
22+
type_params: '[' type_param (',' type_param)* [','] ']'
23+
type_param: NAME [type_param_bound] | '*' NAME | '**' NAME
24+
type_param_bound: ':' test
2125

2226
parameters: '(' [typedargslist] ')'
2327
typedargslist: (
@@ -131,7 +135,7 @@ dictorsetmaker: ( ((test ':' test | '**' expr)
131135
((test [':=' test] | star_expr)
132136
(comp_for | (',' (test [':=' test] | star_expr))* [','])) )
133137

134-
classdef: 'class' NAME ['(' [arglist] ')'] ':' suite
138+
classdef: 'class' NAME [type_params] ['(' [arglist] ')'] ':' suite
135139

136140
arglist: argument (',' argument)* [',']
137141

parso/python/grammar313.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ decorators: decorator+
1717
decorated: decorators (classdef | funcdef | async_funcdef)
1818

1919
async_funcdef: 'async' funcdef
20-
funcdef: 'def' NAME parameters ['->' test] ':' suite
20+
funcdef: 'def' NAME [type_params] parameters ['->' test] ':' suite
21+
22+
type_params: '[' type_param (',' type_param)* [','] ']'
23+
type_param: NAME [type_param_bound] [type_param_default] | '*' NAME [type_param_default] | '**' NAME [type_param_default]
24+
type_param_bound: ':' test
25+
type_param_default: '=' test
2126

2227
parameters: '(' [typedargslist] ')'
2328
typedargslist: (
@@ -131,7 +136,7 @@ dictorsetmaker: ( ((test ':' test | '**' expr)
131136
((test [':=' test] | star_expr)
132137
(comp_for | (',' (test [':=' test] | star_expr))* [','])) )
133138

134-
classdef: 'class' NAME ['(' [arglist] ')'] ':' suite
139+
classdef: 'class' NAME [type_params] ['(' [arglist] ')'] ':' suite
135140

136141
arglist: argument (',' argument)* [',']
137142

parso/python/grammar314.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ decorators: decorator+
1717
decorated: decorators (classdef | funcdef | async_funcdef)
1818

1919
async_funcdef: 'async' funcdef
20-
funcdef: 'def' NAME parameters ['->' test] ':' suite
20+
funcdef: 'def' NAME [type_params] parameters ['->' test] ':' suite
21+
22+
type_params: '[' type_param (',' type_param)* [','] ']'
23+
type_param: NAME [type_param_bound] [type_param_default] | '*' NAME [type_param_default] | '**' NAME [type_param_default]
24+
type_param_bound: ':' test
25+
type_param_default: '=' test
2126

2227
parameters: '(' [typedargslist] ')'
2328
typedargslist: (
@@ -131,7 +136,7 @@ dictorsetmaker: ( ((test ':' test | '**' expr)
131136
((test [':=' test] | star_expr)
132137
(comp_for | (',' (test [':=' test] | star_expr))* [','])) )
133138

134-
classdef: 'class' NAME ['(' [arglist] ')'] ':' suite
139+
classdef: 'class' NAME [type_params] ['(' [arglist] ')'] ':' suite
135140

136141
arglist: argument (',' argument)* [',']
137142

parso/python/tree.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -479,13 +479,13 @@ def get_super_arglist(self):
479479
Returns the `arglist` node that defines the super classes. It returns
480480
None if there are no arguments.
481481
"""
482-
if self.children[2] != '(': # Has no parentheses
483-
return None
484-
else:
485-
if self.children[3] == ')': # Empty parentheses
486-
return None
487-
else:
488-
return self.children[3]
482+
for i, child in enumerate(self.children):
483+
if child == '(':
484+
next_child = self.children[i + 1]
485+
if next_child == ')':
486+
return None
487+
return next_child
488+
return None
489489

490490

491491
def _create_params(parent, argslist_list):
@@ -552,15 +552,21 @@ class Function(ClassOrFunc):
552552

553553
def __init__(self, children):
554554
super().__init__(children)
555-
parameters = self.children[2] # After `def foo`
555+
parameters = self._find_parameters()
556556
parameters_children = parameters.children[1:-1]
557-
# If input parameters list already has Param objects, keep it as is;
558-
# otherwise, convert it to a list of Param objects.
559557
if not any(isinstance(child, Param) for child in parameters_children):
560-
parameters.children[1:-1] = _create_params(parameters, parameters_children)
558+
parameters.children[1:-1] = _create_params(
559+
parameters, parameters_children
560+
)
561+
562+
def _find_parameters(self):
563+
for child in self.children:
564+
if child.type == 'parameters':
565+
return child
566+
raise Exception("A function should always have parameters")
561567

562568
def _get_param_nodes(self):
563-
return self.children[2].children
569+
return self._find_parameters().children
564570

565571
def get_params(self):
566572
"""
@@ -633,13 +639,10 @@ def annotation(self):
633639
"""
634640
Returns the test node after `->` or `None` if there is no annotation.
635641
"""
636-
try:
637-
if self.children[3] == "->":
638-
return self.children[4]
639-
assert self.children[3] == ":"
640-
return None
641-
except IndexError:
642-
return None
642+
for i, child in enumerate(self.children):
643+
if child == '->':
644+
return self.children[i + 1]
645+
return None
643646

644647

645648
class Lambda(Function):

test/test_parser.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,64 @@ def test_positional_only_arguments(works_ge_py38, param_code):
206206
)
207207
def test_decorator_expression(works_ge_py39, expression):
208208
works_ge_py39.parse("@%s\ndef x(): pass" % expression)
209+
210+
211+
@pytest.mark.parametrize(
212+
'code', [
213+
'class Foo[T]: pass',
214+
'class Foo[T: str]: pass',
215+
'class Foo[T, U]: pass',
216+
'class Foo[T: str, U: int]: pass',
217+
'class Foo[T](Base): pass',
218+
'class Foo[T: str](Base, Mixin): pass',
219+
'class Foo[*Ts]: pass',
220+
'class Foo[**P]: pass',
221+
]
222+
)
223+
def test_pep695_generic_class(works_ge_py312, code):
224+
works_ge_py312.parse(code)
225+
226+
227+
@pytest.mark.parametrize(
228+
'code', [
229+
'def foo[T](x: T) -> T: pass',
230+
'def foo[T: int](x: T) -> T: pass',
231+
'def foo[T, U](x: T, y: U): pass',
232+
'def foo[*Ts](*args): pass',
233+
'def foo[**P](*args): pass',
234+
]
235+
)
236+
def test_pep695_generic_function(works_ge_py312, code):
237+
works_ge_py312.parse(code)
238+
239+
240+
def test_pep695_class_get_super_arglist(works_ge_py312):
241+
module = works_ge_py312.parse('class Foo[T](Bar, Baz): pass')
242+
if module is None:
243+
return
244+
classdef = module.children[0]
245+
arglist = classdef.get_super_arglist()
246+
assert arglist is not None
247+
assert 'Bar' in arglist.get_code()
248+
assert 'Baz' in arglist.get_code()
249+
250+
251+
def test_pep695_class_no_bases(works_ge_py312):
252+
module = works_ge_py312.parse('class Foo[T]: pass')
253+
if module is None:
254+
return
255+
classdef = module.children[0]
256+
assert classdef.get_super_arglist() is None
257+
258+
259+
@pytest.mark.parametrize(
260+
'code', [
261+
'class Foo[T = int]: pass',
262+
'class Foo[T: str = "default"]: pass',
263+
'class Foo[*Ts = tuple[int, ...]]: pass',
264+
'class Foo[**P = None]: pass',
265+
'def foo[T = int](x: T) -> T: pass',
266+
]
267+
)
268+
def test_pep696_type_param_defaults(works_ge_py313, code):
269+
works_ge_py313.parse(code)

0 commit comments

Comments
 (0)