Skip to content

Commit 71d4f1d

Browse files
committed
Add instance support to attrs.fields()
fixes #1400
1 parent 3a68d49 commit 71d4f1d

File tree

6 files changed

+37
-11
lines changed

6 files changed

+37
-11
lines changed

changelog.d/1400.change.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
It's now possible to pass *attrs* **instances** in addition to *attrs* **classes** to `attrs.fields()`.

docs/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ Helpers
154154
... y = field()
155155
>>> attrs.fields(C)
156156
(Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x'), Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y'))
157+
>>> attrs.fields(C(1, 2)) is attrs.fields(C)
158+
True
157159
>>> attrs.fields(C)[1]
158160
Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y')
159161
>>> attrs.fields(C).y is attrs.fields(C)[1]

src/attr/__init__.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ def attrs(
308308
match_args: bool = ...,
309309
unsafe_hash: bool | None = ...,
310310
) -> Callable[[_C], _C]: ...
311-
def fields(cls: type[AttrsInstance]) -> Any: ...
311+
def fields(cls: type[AttrsInstance] | AttrsInstance) -> Any: ...
312312
def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ...
313313
def validate(inst: AttrsInstance) -> None: ...
314314
def resolve_types(

src/attr/_make.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,16 +1890,16 @@ def _add_repr(cls, ns=None, attrs=None):
18901890

18911891
def fields(cls):
18921892
"""
1893-
Return the tuple of *attrs* attributes for a class.
1893+
Return the tuple of *attrs* attributes for a class or instance.
18941894
18951895
The tuple also allows accessing the fields by their names (see below for
18961896
examples).
18971897
18981898
Args:
1899-
cls (type): Class to introspect.
1899+
cls (type): Class or instance to introspect.
19001900
19011901
Raises:
1902-
TypeError: If *cls* is not a class.
1902+
TypeError: If *cls* is neither a class nor an *attrs* instance.
19031903
19041904
attrs.exceptions.NotAnAttrsClassError:
19051905
If *cls* is not an *attrs* class.
@@ -1910,12 +1910,17 @@ def fields(cls):
19101910
.. versionchanged:: 16.2.0 Returned tuple allows accessing the fields
19111911
by name.
19121912
.. versionchanged:: 23.1.0 Add support for generic classes.
1913+
.. versionchanged:: 26.1.0 Add support for instances.
19131914
"""
19141915
generic_base = get_generic_base(cls)
19151916

19161917
if generic_base is None and not isinstance(cls, type):
1917-
msg = "Passed object must be a class."
1918-
raise TypeError(msg)
1918+
type_ = type(cls)
1919+
if getattr(type_, "__attrs_attrs__", None) is None:
1920+
msg = "Passed object must be a class or attrs instance."
1921+
raise TypeError(msg)
1922+
1923+
return fields(type_)
19191924

19201925
attrs = getattr(cls, "__attrs_attrs__", None)
19211926

tests/test_make.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,12 +1550,19 @@ class TestFields:
15501550
@given(simple_classes())
15511551
def test_instance(self, C):
15521552
"""
1553-
Raises `TypeError` on non-classes.
1553+
Returns the class fields for *attrs* instances too.
15541554
"""
1555-
with pytest.raises(TypeError) as e:
1556-
fields(C())
1555+
assert fields(C()) is fields(C)
15571556

1558-
assert "Passed object must be a class." == e.value.args[0]
1557+
def test_handler_non_attrs_instance(self):
1558+
"""
1559+
Raises `TypeError` on non-*attrs* instances.
1560+
"""
1561+
with pytest.raises(
1562+
TypeError,
1563+
match=r"Passed object must be a class or attrs instance\.",
1564+
):
1565+
fields(object())
15591566

15601567
def test_handler_non_attrs_class(self):
15611568
"""

tests/test_mypy.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,17 @@
14311431
14321432
reveal_type(fields(A)) # N: Revealed type is "[Tt]uple\[attr.Attribute\[builtins.int\], attr.Attribute\[builtins.str\], fallback=main.A.__main_A_AttrsAttributes__\]"
14331433
1434+
- case: testFieldsInstance
1435+
main: |
1436+
from attrs import define, fields
1437+
1438+
@define
1439+
class A:
1440+
a: int
1441+
b: str
1442+
1443+
fields(A(1, "x"))
1444+
14341445
- case: testFieldsError
14351446
regex: true
14361447
main: |
@@ -1440,7 +1451,7 @@
14401451
a: int
14411452
b: str
14421453
1443-
fields(A) # E: Argument 1 to "fields" has incompatible type "[Tt]ype\[A\]"; expected "[Tt]ype\[AttrsInstance\]" \[arg-type\]
1454+
fields(A) # E: Argument 1 to "fields" has incompatible type "type\[A\]"; expected "type\[AttrsInstance\] \| AttrsInstance" \[arg-type\]
14441455
14451456
- case: testAsDict
14461457
main: |

0 commit comments

Comments
 (0)