From ec15cce2c4213fd692d5796a5d9ed70057ca259a Mon Sep 17 00:00:00 2001 From: Dream <2468001320@qq.com> Date: Sat, 29 Aug 2026 16:29:39 +0800 Subject: [PATCH 1/2] fix: compare Attribute instances by identity in filters - src/attr/filters.py --- src/attr/filters.py | 144 ++++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/src/attr/filters.py b/src/attr/filters.py index 689b1705a..664f65be3 100644 --- a/src/attr/filters.py +++ b/src/attr/filters.py @@ -1,72 +1,72 @@ -# SPDX-License-Identifier: MIT - -""" -Commonly useful filters for `attrs.asdict` and `attrs.astuple`. -""" - -from ._make import Attribute - - -def _split_what(what): - """ - Returns a tuple of `frozenset`s of classes and attributes. - """ - return ( - frozenset(cls for cls in what if isinstance(cls, type)), - frozenset(cls for cls in what if isinstance(cls, str)), - frozenset(cls for cls in what if isinstance(cls, Attribute)), - ) - - -def include(*what): - """ - Create a filter that only allows *what*. - - Args: - what (list[type, str, attrs.Attribute]): - What to include. Can be a type, a name, or an attribute. - - Returns: - Callable: - A callable that can be passed to `attrs.asdict`'s and - `attrs.astuple`'s *filter* argument. - - .. versionchanged:: 23.1.0 Accept strings with field names. - """ - cls, names, attrs = _split_what(what) - - def include_(attribute, value): - return ( - value.__class__ in cls - or attribute.name in names - or attribute in attrs - ) - - return include_ - - -def exclude(*what): - """ - Create a filter that does **not** allow *what*. - - Args: - what (list[type, str, attrs.Attribute]): - What to exclude. Can be a type, a name, or an attribute. - - Returns: - Callable: - A callable that can be passed to `attrs.asdict`'s and - `attrs.astuple`'s *filter* argument. - - .. versionchanged:: 23.3.0 Accept field name string as input argument - """ - cls, names, attrs = _split_what(what) - - def exclude_(attribute, value): - return not ( - value.__class__ in cls - or attribute.name in names - or attribute in attrs - ) - - return exclude_ +# SPDX-License-Identifier: MIT + +""" +Commonly useful filters for `attrs.asdict` and `attrs.astuple`. +""" + +from ._make import Attribute + + +def _split_what(what): + """ + Returns a tuple of `frozenset`s of classes and attributes. + """ + return ( + frozenset(cls for cls in what if isinstance(cls, type)), + frozenset(cls for cls in what if isinstance(cls, str)), + frozenset(cls for cls in what if isinstance(cls, Attribute)), + ) + + +def include(*what): + """ + Create a filter that only allows *what*. + + Args: + what (list[type, str, attrs.Attribute]): + What to include. Can be a type, a name, or an attribute. + + Returns: + Callable: + A callable that can be passed to `attrs.asdict`'s and + `attrs.astuple`'s *filter* argument. + + .. versionchanged:: 23.1.0 Accept strings with field names. + """ + cls, names, attrs = _split_what(what) + + def include_(attribute, value): + return ( + value.__class__ in cls + or attribute.name in names + or any(attribute is a for a in attrs) + ) + + return include_ + + +def exclude(*what): + """ + Create a filter that does **not** allow *what*. + + Args: + what (list[type, str, attrs.Attribute]): + What to exclude. Can be a type, a name, or an attribute. + + Returns: + Callable: + A callable that can be passed to `attrs.asdict`'s and + `attrs.astuple`'s *filter* argument. + + .. versionchanged:: 23.3.0 Accept field name string as input argument + """ + cls, names, attrs = _split_what(what) + + def exclude_(attribute, value): + return not ( + value.__class__ in cls + or attribute.name in names + or any(attribute is a for a in attrs) + ) + + return exclude_ From cb325406937643cbb8ca250f9d00714105e14780 Mon Sep 17 00:00:00 2001 From: Dream <2468001320@qq.com> Date: Sat, 29 Aug 2026 16:29:41 +0800 Subject: [PATCH 2/2] fix: compare Attribute instances by identity in filters - tests/test_filters.py --- tests/test_filters.py | 282 +++++++++++++++++++++++------------------- 1 file changed, 156 insertions(+), 126 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 08314fa88..15de22739 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,126 +1,156 @@ -# SPDX-License-Identifier: MIT - -""" -Tests for `attr.filters`. -""" - -import pytest - -import attr - -from attr import fields -from attr.filters import _split_what, exclude, include - - -@attr.s -class C: - a = attr.ib() - b = attr.ib() - - -class TestSplitWhat: - """ - Tests for `_split_what`. - """ - - def test_splits(self): - """ - Splits correctly. - """ - assert ( - frozenset((int, str)), - frozenset(("abcd", "123")), - frozenset((fields(C).a,)), - ) == _split_what((str, "123", fields(C).a, int, "abcd")) - - -class TestInclude: - """ - Tests for `include`. - """ - - @pytest.mark.parametrize( - ("incl", "value"), - [ - ((int,), 42), - ((str,), "hello"), - ((str, fields(C).a), 42), - ((str, fields(C).b), "hello"), - (("a",), 42), - (("a",), "hello"), - (("a", str), 42), - (("a", fields(C).b), "hello"), - ], - ) - def test_allow(self, incl, value): - """ - Return True if a class or attribute is included. - """ - i = include(*incl) - assert i(fields(C).a, value) is True - - @pytest.mark.parametrize( - ("incl", "value"), - [ - ((str,), 42), - ((int,), "hello"), - ((str, fields(C).b), 42), - ((int, fields(C).b), "hello"), - (("b",), 42), - (("b",), "hello"), - (("b", str), 42), - (("b", fields(C).b), "hello"), - ], - ) - def test_drop_class(self, incl, value): - """ - Return False on non-included classes and attributes. - """ - i = include(*incl) - assert i(fields(C).a, value) is False - - -class TestExclude: - """ - Tests for `exclude`. - """ - - @pytest.mark.parametrize( - ("excl", "value"), - [ - ((str,), 42), - ((int,), "hello"), - ((str, fields(C).b), 42), - ((int, fields(C).b), "hello"), - (("b",), 42), - (("b",), "hello"), - (("b", str), 42), - (("b", fields(C).b), "hello"), - ], - ) - def test_allow(self, excl, value): - """ - Return True if class or attribute is not excluded. - """ - e = exclude(*excl) - assert e(fields(C).a, value) is True - - @pytest.mark.parametrize( - ("excl", "value"), - [ - ((int,), 42), - ((str,), "hello"), - ((str, fields(C).a), 42), - ((str, fields(C).b), "hello"), - (("a",), 42), - (("a",), "hello"), - (("a", str), 42), - (("a", fields(C).b), "hello"), - ], - ) - def test_drop_class(self, excl, value): - """ - Return True on non-excluded classes and attributes. - """ - e = exclude(*excl) - assert e(fields(C).a, value) is False +# SPDX-License-Identifier: MIT + +""" +Tests for `attr.filters`. +""" + +import pytest + +import attr + +from attr import fields +from attr.filters import _split_what, exclude, include + + +@attr.s +class C: + a = attr.ib() + b = attr.ib() + + +class TestSplitWhat: + """ + Tests for `_split_what`. + """ + + def test_splits(self): + """ + Splits correctly. + """ + assert ( + frozenset((int, str)), + frozenset(("abcd", "123")), + frozenset((fields(C).a,)), + ) == _split_what((str, "123", fields(C).a, int, "abcd")) + + +class TestInclude: + """ + Tests for `include`. + """ + + @pytest.mark.parametrize( + ("incl", "value"), + [ + ((int,), 42), + ((str,), "hello"), + ((str, fields(C).a), 42), + ((str, fields(C).b), "hello"), + (("a",), 42), + (("a",), "hello"), + (("a", str), 42), + (("a", fields(C).b), "hello"), + ], + ) + def test_allow(self, incl, value): + """ + Return True if a class or attribute is included. + """ + i = include(*incl) + assert i(fields(C).a, value) is True + + @pytest.mark.parametrize( + ("incl", "value"), + [ + ((str,), 42), + ((int,), "hello"), + ((str, fields(C).b), 42), + ((int, fields(C).b), "hello"), + (("b",), 42), + (("b",), "hello"), + (("b", str), 42), + (("b", fields(C).b), "hello"), + ], + ) + def test_drop_class(self, incl, value): + """ + Return False on non-included classes and attributes. + """ + i = include(*incl) + assert i(fields(C).a, value) is False + + +class TestExclude: + """ + Tests for `exclude`. + """ + + @pytest.mark.parametrize( + ("excl", "value"), + [ + ((str,), 42), + ((int,), "hello"), + ((str, fields(C).b), 42), + ((int, fields(C).b), "hello"), + (("b",), 42), + (("b",), "hello"), + (("b", str), 42), + (("b", fields(C).b), "hello"), + ], + ) + def test_allow(self, excl, value): + """ + Return True if class or attribute is not excluded. + """ + e = exclude(*excl) + assert e(fields(C).a, value) is True + + @pytest.mark.parametrize( + ("excl", "value"), + [ + ((int,), 42), + ((str,), "hello"), + ((str, fields(C).a), 42), + ((str, fields(C).b), "hello"), + (("a",), 42), + (("a",), "hello"), + (("a", str), 42), + (("a", fields(C).b), "hello"), + ], + ) + def test_drop_class(self, excl, value): + """ + Return True on non-excluded classes and attributes. + """ + e = exclude(*excl) + assert e(fields(C).a, value) is False + + +@attr.s +class C2: + a = attr.ib() + repeated = attr.ib() + + +@attr.s +class D2: + b = attr.ib() + repeated = attr.ib() + + +class TestSameNameAcrossClasses: + """ + Two classes may define attributes with the same name but they are + different `Attribute` instances; excluding one must not exclude the + other (https://github.com/python-attrs/attrs/issues/864). + """ + + def test_exclude_identity_not_equality(self): + e = exclude(fields(C2).repeated) + assert e(fields(C2).repeated, "x") is False + assert e(fields(D2).repeated, "x") is True + + def test_include_identity_not_equality(self): + i = include(fields(C2).repeated) + assert i(fields(C2).repeated, "x") is True + assert i(fields(D2).repeated, "x") is False