Skip to content

Commit a26dfc8

Browse files
Fix filter attribute identity matching
1 parent 3823062 commit a26dfc8

4 files changed

Lines changed: 35 additions & 5 deletions

File tree

changelog.d/864.change.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
`attrs.filters.include()` and `attrs.filters.exclude()` now match `attrs.Attribute` instances by identity.
2+
Passing a field returned by `attrs.fields()` therefore only matches that exact class's field; pass a string field name to match same-named fields across classes.

docs/examples.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ For the common case where you want to [`include`](attrs.filters.include) or [`ex
271271
Though using string names directly is convenient, mistyping attribute names will silently do the wrong thing and neither Python nor your type checker can help you.
272272
{func}`attrs.fields()` will raise an `AttributeError` when the field doesn't exist while literal string names won't.
273273
Using {func}`attrs.fields()` to get attributes is worth being recommended in most cases.
274+
String names match all fields with that name, while fields returned from {func}`attrs.fields()` only match that exact class's field.
274275

275276
```{doctest}
276277
>>> asdict(

src/attr/filters.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@
99

1010
def _split_what(what):
1111
"""
12-
Returns a tuple of `frozenset`s of classes and attributes.
12+
Returns a tuple of classes, names, and attributes to match.
1313
"""
1414
return (
1515
frozenset(cls for cls in what if isinstance(cls, type)),
1616
frozenset(cls for cls in what if isinstance(cls, str)),
17-
frozenset(cls for cls in what if isinstance(cls, Attribute)),
17+
tuple(cls for cls in what if isinstance(cls, Attribute)),
1818
)
1919

2020

21+
def _matches_attribute(attribute, attrs):
22+
return any(attribute is a for a in attrs)
23+
24+
2125
def include(*what):
2226
"""
2327
Create a filter that only allows *what*.
@@ -39,7 +43,7 @@ def include_(attribute, value):
3943
return (
4044
value.__class__ in cls
4145
or attribute.name in names
42-
or attribute in attrs
46+
or _matches_attribute(attribute, attrs)
4347
)
4448

4549
return include_
@@ -66,7 +70,7 @@ def exclude_(attribute, value):
6670
return not (
6771
value.__class__ in cls
6872
or attribute.name in names
69-
or attribute in attrs
73+
or _matches_attribute(attribute, attrs)
7074
)
7175

7276
return exclude_

tests/test_filters.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ class C:
1818
b = attr.ib()
1919

2020

21+
@attr.s
22+
class D:
23+
a = attr.ib()
24+
25+
2126
class TestSplitWhat:
2227
"""
2328
Tests for `_split_what`.
@@ -30,7 +35,7 @@ def test_splits(self):
3035
assert (
3136
frozenset((int, str)),
3237
frozenset(("abcd", "123")),
33-
frozenset((fields(C).a,)),
38+
(fields(C).a,),
3439
) == _split_what((str, "123", fields(C).a, int, "abcd"))
3540

3641

@@ -79,6 +84,15 @@ def test_drop_class(self, incl, value):
7984
i = include(*incl)
8085
assert i(fields(C).a, value) is False
8186

87+
def test_allow_attributes_by_identity(self):
88+
"""
89+
Attributes with the same name on other classes are not included.
90+
"""
91+
i = include(fields(C).a)
92+
93+
assert i(fields(C).a, 42) is True
94+
assert i(fields(D).a, 42) is False
95+
8296

8397
class TestExclude:
8498
"""
@@ -124,3 +138,12 @@ def test_drop_class(self, excl, value):
124138
"""
125139
e = exclude(*excl)
126140
assert e(fields(C).a, value) is False
141+
142+
def test_drop_attributes_by_identity(self):
143+
"""
144+
Attributes with the same name on other classes are not excluded.
145+
"""
146+
e = exclude(fields(C).a)
147+
148+
assert e(fields(C).a, 42) is False
149+
assert e(fields(D).a, 42) is True

0 commit comments

Comments
 (0)