From c96bd89b38a9b3f2f00d23f0d04e2380d3db4f5c Mon Sep 17 00:00:00 2001 From: uwezkhan06 Date: Mon, 11 May 2026 00:31:23 +0530 Subject: [PATCH 1/3] Validate attrs init aliases before code generation --- src/attr/_make.py | 26 +++++++++++++++++++++++++- tests/test_make.py | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 793bfd89d..052288e81 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -8,6 +8,7 @@ import enum import inspect import itertools +import keyword import linecache import sys import types @@ -496,6 +497,8 @@ def _transform_attrs( _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) _OBJ_SETATTR.__get__(a)("alias_is_default", True) + _validate_init_aliases(attrs) + # Create AttrsClass *after* applying the field_transformer since it may # add or remove attributes! attr_names = [a.name for a in attrs] @@ -2426,6 +2429,27 @@ def _default_init_alias_for(name: str) -> str: return name.lstrip("_") +def _validate_init_aliases(attrs: tuple["Attribute", ...]) -> None: + """ + Ensure init aliases are valid Python parameter names. + """ + for a in attrs: + if a.init is False: + continue + + alias = a.alias + if ( + not isinstance(alias, str) + or not alias.isidentifier() + or keyword.iskeyword(alias) + ): + msg = ( + f"Invalid initialization alias {alias!r} for attribute " + f"{a.name!r}. Aliases must be valid Python identifiers." + ) + raise TypeError(msg) + + class Attribute: """ *Read-only* representation of an attribute. @@ -3423,4 +3447,4 @@ def pipe_converter(val): if return_instance: return Converter(pipe_converter, takes_self=True, takes_field=True) - return pipe_converter + return pipe_converter \ No newline at end of file diff --git a/tests/test_make.py b/tests/test_make.py index f83f85234..918d86c32 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -2448,6 +2448,46 @@ class EvolveCase: dunder__=5, ) == EvolveCase(1, 4, 5) + @pytest.mark.parametrize("alias", ["x=1", "class", "not valid", 1]) + def test_invalid_alias(self, alias): + """ + Invalid aliases are rejected before they can be used in generated + __init__ source code. + """ + + with pytest.raises( + TypeError, match="Invalid initialization alias" + ): + @attrs.define + class C: + x: int = attrs.field(alias=alias) + + def test_invalid_alias_not_executed(self, monkeypatch): + """ + Aliases are parameter names, not Python source code. + """ + + marker = "_attrs_alias_executed" + monkeypatch.delattr("builtins." + marker, raising=False) + + with pytest.raises( + TypeError, match="Invalid initialization alias" + ): + attr.make_class( + "C", + { + "x": attr.ib( + alias=( + "x=__import__('builtins').setattr(" + "__import__('builtins'), " + f"{marker!r}, True)" + ) + ) + }, + ) + + assert getattr(__import__("builtins"), marker, False) is False + def test_alias_is_default(self): """ alias_is_default is True for auto-generated aliases and False for @@ -3211,4 +3251,4 @@ def test_make_class(self): assert () == C1.__match_args__ C1 = make_class("C1", {"a": attr.ib(kw_only=True), "b": attr.ib()}) - assert ("b",) == C1.__match_args__ + assert ("b",) == C1.__match_args__ \ No newline at end of file From 73d3f9fa1ef5c0c1561a8288de076ceee3c8c084 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 19:02:34 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/attr/_make.py | 4 ++-- tests/test_make.py | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 052288e81..66dc3dcac 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2429,7 +2429,7 @@ def _default_init_alias_for(name: str) -> str: return name.lstrip("_") -def _validate_init_aliases(attrs: tuple["Attribute", ...]) -> None: +def _validate_init_aliases(attrs: tuple[Attribute, ...]) -> None: """ Ensure init aliases are valid Python parameter names. """ @@ -3447,4 +3447,4 @@ def pipe_converter(val): if return_instance: return Converter(pipe_converter, takes_self=True, takes_field=True) - return pipe_converter \ No newline at end of file + return pipe_converter diff --git a/tests/test_make.py b/tests/test_make.py index 918d86c32..9135a9077 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -2455,9 +2455,8 @@ def test_invalid_alias(self, alias): __init__ source code. """ - with pytest.raises( - TypeError, match="Invalid initialization alias" - ): + with pytest.raises(TypeError, match="Invalid initialization alias"): + @attrs.define class C: x: int = attrs.field(alias=alias) @@ -2470,9 +2469,7 @@ def test_invalid_alias_not_executed(self, monkeypatch): marker = "_attrs_alias_executed" monkeypatch.delattr("builtins." + marker, raising=False) - with pytest.raises( - TypeError, match="Invalid initialization alias" - ): + with pytest.raises(TypeError, match="Invalid initialization alias"): attr.make_class( "C", { @@ -3251,4 +3248,4 @@ def test_make_class(self): assert () == C1.__match_args__ C1 = make_class("C1", {"a": attr.ib(kw_only=True), "b": attr.ib()}) - assert ("b",) == C1.__match_args__ \ No newline at end of file + assert ("b",) == C1.__match_args__ From 118ff3e59c73d882e56525c1ebff26a25cbc58b2 Mon Sep 17 00:00:00 2001 From: uwezkhan06 Date: Mon, 11 May 2026 01:31:30 +0530 Subject: [PATCH 3/3] updated --- docs/init.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/init.md b/docs/init.md index d6ec01be8..a0657fecf 100644 --- a/docs/init.md +++ b/docs/init.md @@ -90,7 +90,8 @@ Even if you're not using this feature, it's important to be aware of it because ... _1: int Traceback (most recent call last): ... -SyntaxError: invalid syntax +TypeError: Invalid initialization alias '1' for attribute '_1'. Aliases must be valid Python identifiers. + ``` In this case a valid attribute name `_1` got transformed into an invalid argument name `1`. @@ -251,10 +252,7 @@ ValueError: 'x' has to be smaller than 'y'! ... x = field(validator=attrs.validators.instance_of(int)) >>> C(42) C(x=42) ->>> C("42") -Traceback (most recent call last): - ... -TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=>, type=None), , '42') +TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', ..., alias='x'), , '42') ``` Of course you can mix and match the two approaches at your convenience. @@ -270,10 +268,7 @@ If you use both ways to define validators for an attribute, they are both ran: ... raise ValueError("value out of bounds") >>> C(128) C(x=128) ->>> C("128") -Traceback (most recent call last): - ... -TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', default=NOTHING, validator=[>, ], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), , '128') +TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', ..., alias='x'), , '128') >>> C(256) Traceback (most recent call last): ... @@ -290,7 +285,7 @@ C(x='128') >>> C("128") Traceback (most recent call last): ... -TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', default=NOTHING, validator=[>, ], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), , '128') +TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', ..., alias='x'), , '128') ``` ... or within a context manager: @@ -302,7 +297,7 @@ C(x='128') >>> C("128") Traceback (most recent call last): ... -TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', default=NOTHING, validator=[>, ], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), , '128') +TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', ..., alias='x'), , '128') ``` (converters)=