Skip to content

Commit c96bd89

Browse files
committed
Validate attrs init aliases before code generation
1 parent 3823062 commit c96bd89

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

src/attr/_make.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import enum
99
import inspect
1010
import itertools
11+
import keyword
1112
import linecache
1213
import sys
1314
import types
@@ -496,6 +497,8 @@ def _transform_attrs(
496497
_OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))
497498
_OBJ_SETATTR.__get__(a)("alias_is_default", True)
498499

500+
_validate_init_aliases(attrs)
501+
499502
# Create AttrsClass *after* applying the field_transformer since it may
500503
# add or remove attributes!
501504
attr_names = [a.name for a in attrs]
@@ -2426,6 +2429,27 @@ def _default_init_alias_for(name: str) -> str:
24262429
return name.lstrip("_")
24272430

24282431

2432+
def _validate_init_aliases(attrs: tuple["Attribute", ...]) -> None:
2433+
"""
2434+
Ensure init aliases are valid Python parameter names.
2435+
"""
2436+
for a in attrs:
2437+
if a.init is False:
2438+
continue
2439+
2440+
alias = a.alias
2441+
if (
2442+
not isinstance(alias, str)
2443+
or not alias.isidentifier()
2444+
or keyword.iskeyword(alias)
2445+
):
2446+
msg = (
2447+
f"Invalid initialization alias {alias!r} for attribute "
2448+
f"{a.name!r}. Aliases must be valid Python identifiers."
2449+
)
2450+
raise TypeError(msg)
2451+
2452+
24292453
class Attribute:
24302454
"""
24312455
*Read-only* representation of an attribute.
@@ -3423,4 +3447,4 @@ def pipe_converter(val):
34233447

34243448
if return_instance:
34253449
return Converter(pipe_converter, takes_self=True, takes_field=True)
3426-
return pipe_converter
3450+
return pipe_converter

tests/test_make.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2448,6 +2448,46 @@ class EvolveCase:
24482448
dunder__=5,
24492449
) == EvolveCase(1, 4, 5)
24502450

2451+
@pytest.mark.parametrize("alias", ["x=1", "class", "not valid", 1])
2452+
def test_invalid_alias(self, alias):
2453+
"""
2454+
Invalid aliases are rejected before they can be used in generated
2455+
__init__ source code.
2456+
"""
2457+
2458+
with pytest.raises(
2459+
TypeError, match="Invalid initialization alias"
2460+
):
2461+
@attrs.define
2462+
class C:
2463+
x: int = attrs.field(alias=alias)
2464+
2465+
def test_invalid_alias_not_executed(self, monkeypatch):
2466+
"""
2467+
Aliases are parameter names, not Python source code.
2468+
"""
2469+
2470+
marker = "_attrs_alias_executed"
2471+
monkeypatch.delattr("builtins." + marker, raising=False)
2472+
2473+
with pytest.raises(
2474+
TypeError, match="Invalid initialization alias"
2475+
):
2476+
attr.make_class(
2477+
"C",
2478+
{
2479+
"x": attr.ib(
2480+
alias=(
2481+
"x=__import__('builtins').setattr("
2482+
"__import__('builtins'), "
2483+
f"{marker!r}, True)"
2484+
)
2485+
)
2486+
},
2487+
)
2488+
2489+
assert getattr(__import__("builtins"), marker, False) is False
2490+
24512491
def test_alias_is_default(self):
24522492
"""
24532493
alias_is_default is True for auto-generated aliases and False for
@@ -3211,4 +3251,4 @@ def test_make_class(self):
32113251
assert () == C1.__match_args__
32123252

32133253
C1 = make_class("C1", {"a": attr.ib(kw_only=True), "b": attr.ib()})
3214-
assert ("b",) == C1.__match_args__
3254+
assert ("b",) == C1.__match_args__

0 commit comments

Comments
 (0)