Skip to content

Commit d2a8800

Browse files
[mypyc] fix: reject invalid mypyc_attr args [1/1] (#19963)
This PR emits a builder error when an invalid key is passed into `mypyc_attr`. This change could have saved me ~20 minutes or so, when the root of my problem was just a simple typo. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9dc611f commit d2a8800

File tree

3 files changed

+62
-12
lines changed

3 files changed

+62
-12
lines changed

mypyc/irbuild/prepare.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def prepare_class_def(
357357
ir = mapper.type_to_ir[cdef.info]
358358
info = cdef.info
359359

360-
attrs, attrs_lines = get_mypyc_attrs(cdef)
360+
attrs, attrs_lines = get_mypyc_attrs(cdef, path, errors)
361361
if attrs.get("allow_interpreted_subclasses") is True:
362362
ir.allow_interpreted_subclasses = True
363363
if attrs.get("serializable") is True:

mypyc/irbuild/util.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from __future__ import annotations
44

5-
from typing import Any
5+
from typing import Any, Final, Literal, TypedDict, cast
6+
from typing_extensions import NotRequired
67

78
from mypy.nodes import (
89
ARG_NAMED,
@@ -31,7 +32,23 @@
3132
from mypy.types import FINAL_DECORATOR_NAMES
3233
from mypyc.errors import Errors
3334

34-
DATACLASS_DECORATORS = {"dataclasses.dataclass", "attr.s", "attr.attrs"}
35+
MYPYC_ATTRS: Final[frozenset[MypycAttr]] = frozenset(
36+
["native_class", "allow_interpreted_subclasses", "serializable", "free_list_len"]
37+
)
38+
39+
DATACLASS_DECORATORS: Final = frozenset(["dataclasses.dataclass", "attr.s", "attr.attrs"])
40+
41+
42+
MypycAttr = Literal[
43+
"native_class", "allow_interpreted_subclasses", "serializable", "free_list_len"
44+
]
45+
46+
47+
class MypycAttrs(TypedDict):
48+
native_class: NotRequired[bool]
49+
allow_interpreted_subclasses: NotRequired[bool]
50+
serializable: NotRequired[bool]
51+
free_list_len: NotRequired[int]
3552

3653

3754
def is_final_decorator(d: Expression) -> bool:
@@ -112,21 +129,39 @@ def get_mypyc_attr_call(d: Expression) -> CallExpr | None:
112129
return None
113130

114131

115-
def get_mypyc_attrs(stmt: ClassDef | Decorator) -> tuple[dict[str, Any], dict[str, int]]:
132+
def get_mypyc_attrs(
133+
stmt: ClassDef | Decorator, path: str, errors: Errors
134+
) -> tuple[MypycAttrs, dict[MypycAttr, int]]:
116135
"""Collect all the mypyc_attr attributes on a class definition or a function."""
117-
attrs: dict[str, Any] = {}
118-
lines: dict[str, int] = {}
136+
attrs: MypycAttrs = {}
137+
lines: dict[MypycAttr, int] = {}
138+
139+
def set_mypyc_attr(key: str, value: Any, line: int) -> None:
140+
if key in MYPYC_ATTRS:
141+
key = cast(MypycAttr, key)
142+
attrs[key] = value
143+
lines[key] = line
144+
else:
145+
errors.error(f'"{key}" is not a supported "mypyc_attr"', path, line)
146+
supported_keys = '", "'.join(sorted(MYPYC_ATTRS))
147+
errors.note(f'supported keys: "{supported_keys}"', path, line)
148+
119149
for dec in stmt.decorators:
120-
d = get_mypyc_attr_call(dec)
121-
if d:
150+
if d := get_mypyc_attr_call(dec):
151+
line = d.line
122152
for name, arg in zip(d.arg_names, d.args):
123153
if name is None:
124154
if isinstance(arg, StrExpr):
125-
attrs[arg.value] = True
126-
lines[arg.value] = d.line
155+
set_mypyc_attr(arg.value, True, line)
156+
else:
157+
errors.error(
158+
'All "mypyc_attr" positional arguments must be string literals.',
159+
path,
160+
line,
161+
)
127162
else:
128-
attrs[name] = get_mypyc_attr_literal(arg)
129-
lines[name] = d.line
163+
arg_value = get_mypyc_attr_literal(arg)
164+
set_mypyc_attr(name, arg_value, line)
130165

131166
return attrs, lines
132167

mypyc/test-data/irbuild-classes.test

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2837,3 +2837,18 @@ L0:
28372837
r6 = PyObject_VectorcallMethod(r3, r5, 9223372036854775812, 0)
28382838
keep_alive r2, self, key, val
28392839
return 1
2840+
2841+
[case testInvalidMypycAttr]
2842+
from mypy_extensions import mypyc_attr
2843+
2844+
@mypyc_attr("allow_interpreted_subclasses", "invalid_arg") # E: "invalid_arg" is not a supported "mypyc_attr" \
2845+
# N: supported keys: "allow_interpreted_subclasses", "free_list_len", "native_class", "serializable"
2846+
class InvalidArg:
2847+
pass
2848+
@mypyc_attr(invalid_kwarg=True) # E: "invalid_kwarg" is not a supported "mypyc_attr" \
2849+
# N: supported keys: "allow_interpreted_subclasses", "free_list_len", "native_class", "serializable"
2850+
class InvalidKwarg:
2851+
pass
2852+
@mypyc_attr(str()) # E: All "mypyc_attr" positional arguments must be string literals.
2853+
class InvalidLiteral:
2854+
pass

0 commit comments

Comments
 (0)