Merge pull request #237 from darki73/feat/pep695-type-params

PEP 695 type parameter syntax for Python 3.12+
This commit is contained in:
Dave Halter
2026-04-03 20:09:17 +00:00
committed by GitHub
6 changed files with 115 additions and 25 deletions

View File

@@ -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))

View File

@@ -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)* [',']

View File

@@ -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)* [',']

View File

@@ -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)* [',']

View File

@@ -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):

View File

@@ -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)