gh-119933: Improve `SyntaxError` message for invalid type parameters expressions (#119976)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
Bénédikt Tran 2024-06-17 15:51:03 +02:00 committed by GitHub
parent 274f844830
commit 4bf17c381f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 277 additions and 55 deletions

View File

@ -31,21 +31,74 @@ Generating Symbol Tables
Examining Symbol Tables
-----------------------
.. class:: SymbolTableType
An enumeration indicating the type of a :class:`SymbolTable` object.
.. attribute:: MODULE
:value: "module"
Used for the symbol table of a module.
.. attribute:: FUNCTION
:value: "function"
Used for the symbol table of a function.
.. attribute:: CLASS
:value: "class"
Used for the symbol table of a class.
The following members refer to different flavors of
:ref:`annotation scopes <annotation-scopes>`.
.. attribute:: ANNOTATION
:value: "annotation"
Used for annotations if ``from __future__ import annotations`` is active.
.. attribute:: TYPE_ALIAS
:value: "type alias"
Used for the symbol table of :keyword:`type` constructions.
.. attribute:: TYPE_PARAMETERS
:value: "type parameters"
Used for the symbol table of :ref:`generic functions <generic-functions>`
or :ref:`generic classes <generic-classes>`.
.. attribute:: TYPE_VARIABLE
:value: "type variable"
Used for the symbol table of the bound, the constraint tuple or the
default value of a single type variable in the formal sense, i.e.,
a TypeVar, a TypeVarTuple or a ParamSpec object (the latter two do
not support a bound or a constraint tuple).
.. versionadded:: 3.13
.. class:: SymbolTable
A namespace table for a block. The constructor is not public.
.. method:: get_type()
Return the type of the symbol table. Possible values are ``'class'``,
``'module'``, ``'function'``, ``'annotation'``, ``'TypeVar bound'``,
``'type alias'``, and ``'type parameter'``. The latter four refer to
different flavors of :ref:`annotation scopes <annotation-scopes>`.
Return the type of the symbol table. Possible values are members
of the :class:`SymbolTableType` enumeration.
.. versionchanged:: 3.12
Added ``'annotation'``, ``'TypeVar bound'``, ``'type alias'``,
and ``'type parameter'`` as possible return values.
.. versionchanged:: 3.13
Return values are members of the :class:`SymbolTableType` enumeration.
The exact values of the returned string may change in the future,
and thus, it is recommended to use :class:`SymbolTableType` members
instead of hard-coded strings.
.. method:: get_id()
Return the table's identifier.

View File

@ -16,11 +16,23 @@ typedef enum _block_type {
// annotation blocks cannot bind names and are not evaluated. Otherwise, they
// are lazily evaluated (see PEP 649).
AnnotationBlock,
// Used for generics and type aliases. These work mostly like functions
// (see PEP 695 for details). The three different blocks function identically;
// they are different enum entries only so that error messages can be more
// precise.
TypeVarBoundBlock, TypeAliasBlock, TypeParamBlock
// The following blocks are used for generics and type aliases. These work
// mostly like functions (see PEP 695 for details). The three different
// blocks function identically; they are different enum entries only so
// that error messages can be more precise.
// The block to enter when processing a "type" (PEP 695) construction,
// e.g., "type MyGeneric[T] = list[T]".
TypeAliasBlock,
// The block to enter when processing a "generic" (PEP 695) object,
// e.g., "def foo[T](): pass" or "class A[T]: pass".
TypeParametersBlock,
// The block to enter when processing the bound, the constraint tuple
// or the default value of a single "type variable" in the formal sense,
// i.e., a TypeVar, a TypeVarTuple or a ParamSpec object (the latter two
// do not support a bound or a constraint tuple).
TypeVariableBlock,
} _Py_block_ty;
typedef enum _comprehension_type {
@ -83,7 +95,16 @@ typedef struct _symtable_entry {
PyObject *ste_children; /* list of child blocks */
PyObject *ste_directives;/* locations of global and nonlocal statements */
PyObject *ste_mangled_names; /* set of names for which mangling should be applied */
_Py_block_ty ste_type;
// Optional string set by symtable.c and used when reporting errors.
// The content of that string is a description of the current "context".
//
// For instance, if we are processing the default value of the type
// variable "T" in "def foo[T = int](): pass", `ste_scope_info` is
// set to "a TypeVar default".
const char *ste_scope_info;
int ste_nested; /* true if block is nested */
unsigned ste_free : 1; /* true if block has free variables */
unsigned ste_child_free : 1; /* true if a child block has free vars,

View File

@ -13,8 +13,9 @@ from _symtable import (
)
import weakref
from enum import StrEnum
__all__ = ["symtable", "SymbolTable", "Class", "Function", "Symbol"]
__all__ = ["symtable", "SymbolTableType", "SymbolTable", "Class", "Function", "Symbol"]
def symtable(code, filename, compile_type):
""" Return the toplevel *SymbolTable* for the source code.
@ -46,6 +47,16 @@ class SymbolTableFactory:
_newSymbolTable = SymbolTableFactory()
class SymbolTableType(StrEnum):
MODULE = "module"
FUNCTION = "function"
CLASS = "class"
ANNOTATION = "annotation"
TYPE_ALIAS = "type alias"
TYPE_PARAMETERS = "type parameters"
TYPE_VARIABLE = "type variable"
class SymbolTable:
def __init__(self, raw_table, filename):
@ -69,23 +80,23 @@ class SymbolTable:
def get_type(self):
"""Return the type of the symbol table.
The values returned are 'class', 'module', 'function',
'annotation', 'TypeVar bound', 'type alias', and 'type parameter'.
The value returned is one of the values in
the ``SymbolTableType`` enumeration.
"""
if self._table.type == _symtable.TYPE_MODULE:
return "module"
return SymbolTableType.MODULE
if self._table.type == _symtable.TYPE_FUNCTION:
return "function"
return SymbolTableType.FUNCTION
if self._table.type == _symtable.TYPE_CLASS:
return "class"
return SymbolTableType.CLASS
if self._table.type == _symtable.TYPE_ANNOTATION:
return "annotation"
if self._table.type == _symtable.TYPE_TYPE_VAR_BOUND:
return "TypeVar bound"
return SymbolTableType.ANNOTATION
if self._table.type == _symtable.TYPE_TYPE_ALIAS:
return "type alias"
if self._table.type == _symtable.TYPE_TYPE_PARAM:
return "type parameter"
return SymbolTableType.TYPE_ALIAS
if self._table.type == _symtable.TYPE_TYPE_PARAMETERS:
return SymbolTableType.TYPE_PARAMETERS
if self._table.type == _symtable.TYPE_TYPE_VARIABLE:
return SymbolTableType.TYPE_VARIABLE
assert False, f"unexpected type: {self._table.type}"
def get_id(self):

View File

@ -49,7 +49,7 @@ type GenericAlias[T] = list[T]
def generic_spam[T](a):
pass
class GenericMine[T: int]:
class GenericMine[T: int, U: (int, str) = int]:
pass
"""
@ -78,6 +78,7 @@ class SymtableTest(unittest.TestCase):
GenericMine = find_block(top, "GenericMine")
GenericMine_inner = find_block(GenericMine, "GenericMine")
T = find_block(GenericMine, "T")
U = find_block(GenericMine, "U")
def test_type(self):
self.assertEqual(self.top.get_type(), "module")
@ -87,13 +88,14 @@ class SymtableTest(unittest.TestCase):
self.assertEqual(self.internal.get_type(), "function")
self.assertEqual(self.foo.get_type(), "function")
self.assertEqual(self.Alias.get_type(), "type alias")
self.assertEqual(self.GenericAlias.get_type(), "type parameter")
self.assertEqual(self.GenericAlias.get_type(), "type parameters")
self.assertEqual(self.GenericAlias_inner.get_type(), "type alias")
self.assertEqual(self.generic_spam.get_type(), "type parameter")
self.assertEqual(self.generic_spam.get_type(), "type parameters")
self.assertEqual(self.generic_spam_inner.get_type(), "function")
self.assertEqual(self.GenericMine.get_type(), "type parameter")
self.assertEqual(self.GenericMine.get_type(), "type parameters")
self.assertEqual(self.GenericMine_inner.get_type(), "class")
self.assertEqual(self.T.get_type(), "TypeVar bound")
self.assertEqual(self.T.get_type(), "type variable")
self.assertEqual(self.U.get_type(), "type variable")
def test_id(self):
self.assertGreater(self.top.get_id(), 0)

View File

@ -2046,16 +2046,91 @@ Invalid expressions in type scopes:
...
SyntaxError: Type parameter list cannot be empty
>>> def f[T: (x:=3)](): pass
Traceback (most recent call last):
...
SyntaxError: named expression cannot be used within a TypeVar bound
>>> def f[T: ((x:= 3), int)](): pass
Traceback (most recent call last):
...
SyntaxError: named expression cannot be used within a TypeVar constraint
>>> def f[T = ((x:=3))](): pass
Traceback (most recent call last):
...
SyntaxError: named expression cannot be used within a TypeVar default
>>> async def f[T: (x:=3)](): pass
Traceback (most recent call last):
...
SyntaxError: named expression cannot be used within a TypeVar bound
>>> async def f[T: ((x:= 3), int)](): pass
Traceback (most recent call last):
...
SyntaxError: named expression cannot be used within a TypeVar constraint
>>> async def f[T = ((x:=3))](): pass
Traceback (most recent call last):
...
SyntaxError: named expression cannot be used within a TypeVar default
>>> type A[T: (x:=3)] = int
Traceback (most recent call last):
...
SyntaxError: named expression cannot be used within a TypeVar bound
>>> type A[T: ((x:= 3), int)] = int
Traceback (most recent call last):
...
SyntaxError: named expression cannot be used within a TypeVar constraint
>>> type A[T = ((x:=3))] = int
Traceback (most recent call last):
...
SyntaxError: named expression cannot be used within a TypeVar default
>>> def f[T: (yield)](): pass
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVar bound
>>> def f[T: (int, (yield))](): pass
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVar constraint
>>> def f[T = (yield)](): pass
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVar default
>>> def f[*Ts = (yield)](): pass
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVarTuple default
>>> def f[**P = [(yield), int]](): pass
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a ParamSpec default
>>> type A[T: (yield 3)] = int
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVar bound
>>> type A[T: (int, (yield 3))] = int
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVar constraint
>>> type A[T = (yield 3)] = int
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVar default
>>> type A[T: (await 3)] = int
Traceback (most recent call last):
...
@ -2066,6 +2141,31 @@ Invalid expressions in type scopes:
...
SyntaxError: yield expression cannot be used within a TypeVar bound
>>> class A[T: (yield 3)]: pass
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVar bound
>>> class A[T: (int, (yield 3))]: pass
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVar constraint
>>> class A[T = (yield)]: pass
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVar default
>>> class A[*Ts = (yield)]: pass
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a TypeVarTuple default
>>> class A[**P = [(yield), int]]: pass
Traceback (most recent call last):
...
SyntaxError: yield expression cannot be used within a ParamSpec default
>>> type A = (x := 3)
Traceback (most recent call last):
...

View File

@ -0,0 +1,4 @@
Improve :exc:`SyntaxError` messages for invalid expressions in a type
parameters bound, a type parameter constraint tuple or a default type
parameter.
Patch by Bénédikt Tran.

View File

@ -0,0 +1,3 @@
Add the :class:`symtable.SymbolTableType` enumeration to represent the
possible outputs of the :class:`symtable.SymbolTable.get_type` method. Patch
by Bénédikt Tran.

View File

@ -91,11 +91,11 @@ symtable_init_constants(PyObject *m)
return -1;
if (PyModule_AddIntConstant(m, "TYPE_ANNOTATION", AnnotationBlock) < 0)
return -1;
if (PyModule_AddIntConstant(m, "TYPE_TYPE_VAR_BOUND", TypeVarBoundBlock) < 0)
return -1;
if (PyModule_AddIntConstant(m, "TYPE_TYPE_ALIAS", TypeAliasBlock) < 0)
return -1;
if (PyModule_AddIntConstant(m, "TYPE_TYPE_PARAM", TypeParamBlock) < 0)
if (PyModule_AddIntConstant(m, "TYPE_TYPE_PARAMETERS", TypeParametersBlock) < 0)
return -1;
if (PyModule_AddIntConstant(m, "TYPE_TYPE_VARIABLE", TypeVariableBlock) < 0)
return -1;
if (PyModule_AddIntMacro(m, LOCAL) < 0) return -1;

View File

@ -58,13 +58,13 @@
#define ANNOTATION_NOT_ALLOWED \
"%s cannot be used within an annotation"
#define TYPEVAR_BOUND_NOT_ALLOWED \
"%s cannot be used within a TypeVar bound"
#define EXPR_NOT_ALLOWED_IN_TYPE_VARIABLE \
"%s cannot be used within %s"
#define TYPEALIAS_NOT_ALLOWED \
#define EXPR_NOT_ALLOWED_IN_TYPE_ALIAS \
"%s cannot be used within a type alias"
#define TYPEPARAM_NOT_ALLOWED \
#define EXPR_NOT_ALLOWED_IN_TYPE_PARAMETERS \
"%s cannot be used within the definition of a generic"
#define DUPLICATE_TYPE_PARAM \
@ -106,6 +106,8 @@ ste_new(struct symtable *st, identifier name, _Py_block_ty block,
ste->ste_mangled_names = NULL;
ste->ste_type = block;
ste->ste_scope_info = NULL;
ste->ste_nested = 0;
ste->ste_free = 0;
ste->ste_varargs = 0;
@ -269,9 +271,9 @@ static void _dump_symtable(PySTEntryObject* ste, PyObject* prefix)
case ClassBlock: blocktype = "ClassBlock"; break;
case ModuleBlock: blocktype = "ModuleBlock"; break;
case AnnotationBlock: blocktype = "AnnotationBlock"; break;
case TypeVarBoundBlock: blocktype = "TypeVarBoundBlock"; break;
case TypeVariableBlock: blocktype = "TypeVariableBlock"; break;
case TypeAliasBlock: blocktype = "TypeAliasBlock"; break;
case TypeParamBlock: blocktype = "TypeParamBlock"; break;
case TypeParametersBlock: blocktype = "TypeParametersBlock"; break;
}
const char *comptype = "";
switch (ste->ste_comprehension) {
@ -544,9 +546,9 @@ _PyST_IsFunctionLike(PySTEntryObject *ste)
{
return ste->ste_type == FunctionBlock
|| ste->ste_type == AnnotationBlock
|| ste->ste_type == TypeVarBoundBlock
|| ste->ste_type == TypeVariableBlock
|| ste->ste_type == TypeAliasBlock
|| ste->ste_type == TypeParamBlock;
|| ste->ste_type == TypeParametersBlock;
}
static int
@ -1519,7 +1521,7 @@ symtable_enter_type_param_block(struct symtable *st, identifier name,
int end_lineno, int end_col_offset)
{
_Py_block_ty current_type = st->st_cur->ste_type;
if(!symtable_enter_block(st, name, TypeParamBlock, ast, lineno,
if(!symtable_enter_block(st, name, TypeParametersBlock, ast, lineno,
col_offset, end_lineno, end_col_offset)) {
return 0;
}
@ -2122,20 +2124,20 @@ symtable_extend_namedexpr_scope(struct symtable *st, expr_ty e)
}
/* Disallow usage in ClassBlock and type scopes */
if (ste->ste_type == ClassBlock ||
ste->ste_type == TypeParamBlock ||
ste->ste_type == TypeParametersBlock ||
ste->ste_type == TypeAliasBlock ||
ste->ste_type == TypeVarBoundBlock) {
ste->ste_type == TypeVariableBlock) {
switch (ste->ste_type) {
case ClassBlock:
PyErr_Format(PyExc_SyntaxError, NAMED_EXPR_COMP_IN_CLASS);
break;
case TypeParamBlock:
case TypeParametersBlock:
PyErr_Format(PyExc_SyntaxError, NAMED_EXPR_COMP_IN_TYPEPARAM);
break;
case TypeAliasBlock:
PyErr_Format(PyExc_SyntaxError, NAMED_EXPR_COMP_IN_TYPEALIAS);
break;
case TypeVarBoundBlock:
case TypeVariableBlock:
PyErr_Format(PyExc_SyntaxError, NAMED_EXPR_COMP_IN_TYPEVAR_BOUND);
break;
default:
@ -2341,19 +2343,27 @@ symtable_visit_expr(struct symtable *st, expr_ty e)
}
static int
symtable_visit_type_param_bound_or_default(struct symtable *st, expr_ty e, identifier name, void *key)
symtable_visit_type_param_bound_or_default(
struct symtable *st, expr_ty e, identifier name,
void *key, const char *ste_scope_info)
{
if (e) {
int is_in_class = st->st_cur->ste_can_see_class_scope;
if (!symtable_enter_block(st, name, TypeVarBoundBlock, key, LOCATION(e)))
if (!symtable_enter_block(st, name, TypeVariableBlock, key, LOCATION(e)))
return 0;
st->st_cur->ste_can_see_class_scope = is_in_class;
if (is_in_class && !symtable_add_def(st, &_Py_ID(__classdict__), USE, LOCATION(e))) {
VISIT_QUIT(st, 0);
}
assert(ste_scope_info != NULL);
st->st_cur->ste_scope_info = ste_scope_info;
VISIT(st, expr, e);
if (!symtable_exit_block(st))
if (!symtable_exit_block(st)) {
return 0;
}
}
return 1;
}
@ -2371,6 +2381,12 @@ symtable_visit_type_param(struct symtable *st, type_param_ty tp)
if (!symtable_add_def(st, tp->v.TypeVar.name, DEF_TYPE_PARAM | DEF_LOCAL, LOCATION(tp)))
VISIT_QUIT(st, 0);
const char *ste_scope_info = NULL;
const expr_ty bound = tp->v.TypeVar.bound;
if (bound != NULL) {
ste_scope_info = bound->kind == Tuple_kind ? "a TypeVar constraint" : "a TypeVar bound";
}
// We must use a different key for the bound and default. The obvious choice would be to
// use the .bound and .default_value pointers, but that fails when the expression immediately
// inside the bound or default is a comprehension: we would reuse the same key for
@ -2378,11 +2394,12 @@ symtable_visit_type_param(struct symtable *st, type_param_ty tp)
// The only requirement for the key is that it is unique and it matches the logic in
// compile.c where the scope is retrieved.
if (!symtable_visit_type_param_bound_or_default(st, tp->v.TypeVar.bound, tp->v.TypeVar.name,
(void *)tp)) {
(void *)tp, ste_scope_info)) {
VISIT_QUIT(st, 0);
}
if (!symtable_visit_type_param_bound_or_default(st, tp->v.TypeVar.default_value, tp->v.TypeVar.name,
(void *)((uintptr_t)tp + 1))) {
(void *)((uintptr_t)tp + 1), "a TypeVar default")) {
VISIT_QUIT(st, 0);
}
break;
@ -2390,8 +2407,9 @@ symtable_visit_type_param(struct symtable *st, type_param_ty tp)
if (!symtable_add_def(st, tp->v.TypeVarTuple.name, DEF_TYPE_PARAM | DEF_LOCAL, LOCATION(tp))) {
VISIT_QUIT(st, 0);
}
if (!symtable_visit_type_param_bound_or_default(st, tp->v.TypeVarTuple.default_value, tp->v.TypeVarTuple.name,
(void *)tp)) {
(void *)tp, "a TypeVarTuple default")) {
VISIT_QUIT(st, 0);
}
break;
@ -2399,8 +2417,9 @@ symtable_visit_type_param(struct symtable *st, type_param_ty tp)
if (!symtable_add_def(st, tp->v.ParamSpec.name, DEF_TYPE_PARAM | DEF_LOCAL, LOCATION(tp))) {
VISIT_QUIT(st, 0);
}
if (!symtable_visit_type_param_bound_or_default(st, tp->v.ParamSpec.default_value, tp->v.ParamSpec.name,
(void *)tp)) {
(void *)tp, "a ParamSpec default")) {
VISIT_QUIT(st, 0);
}
break;
@ -2829,12 +2848,21 @@ symtable_raise_if_annotation_block(struct symtable *st, const char *name, expr_t
_Py_block_ty type = st->st_cur->ste_type;
if (type == AnnotationBlock)
PyErr_Format(PyExc_SyntaxError, ANNOTATION_NOT_ALLOWED, name);
else if (type == TypeVarBoundBlock)
PyErr_Format(PyExc_SyntaxError, TYPEVAR_BOUND_NOT_ALLOWED, name);
else if (type == TypeAliasBlock)
PyErr_Format(PyExc_SyntaxError, TYPEALIAS_NOT_ALLOWED, name);
else if (type == TypeParamBlock)
PyErr_Format(PyExc_SyntaxError, TYPEPARAM_NOT_ALLOWED, name);
else if (type == TypeVariableBlock) {
const char *info = st->st_cur->ste_scope_info;
assert(info != NULL); // e.g., info == "a ParamSpec default"
PyErr_Format(PyExc_SyntaxError, EXPR_NOT_ALLOWED_IN_TYPE_VARIABLE, name, info);
}
else if (type == TypeAliasBlock) {
// for now, we do not have any extra information
assert(st->st_cur->ste_scope_info == NULL);
PyErr_Format(PyExc_SyntaxError, EXPR_NOT_ALLOWED_IN_TYPE_ALIAS, name);
}
else if (type == TypeParametersBlock) {
// for now, we do not have any extra information
assert(st->st_cur->ste_scope_info == NULL);
PyErr_Format(PyExc_SyntaxError, EXPR_NOT_ALLOWED_IN_TYPE_PARAMETERS, name);
}
else
return 1;