Skip to content

Commit 2014999

Browse files
committed
Add attrs.inspect()
1 parent 975e7d6 commit 2014999

File tree

9 files changed

+155
-96
lines changed

9 files changed

+155
-96
lines changed

docs/api.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,18 @@ Helpers
190190
>>> attrs.has(object)
191191
False
192192

193+
.. autofunction:: attrs.inspect
194+
195+
For example:
196+
197+
.. doctest::
198+
199+
>>> @define
200+
... class C:
201+
... pass
202+
>>> attrs.inspect(C)
203+
ClassProps(is_exception=False, is_slotted=False, has_weakref_slot=True, is_frozen=False, kw_only=KeywordOnly.NO, collect_by_mro=False, init=True, repr=True, eq=True, order=True, hash=Hashability.UNHASHABLE, match_args=True, str=False, getstate_setstate=False, on_setattr=None, field_transformer=None)
204+
193205
.. autofunction:: attrs.resolve_types
194206

195207
For example:

src/attr/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
validate,
2727
)
2828
from ._next_gen import define, field, frozen, mutable
29+
from ._props import ClassProps, inspect
2930
from ._version_info import VersionInfo
3031

3132

@@ -44,6 +45,7 @@ class AttrsInstance(Protocol):
4445
"NOTHING",
4546
"Attribute",
4647
"AttrsInstance",
48+
"ClassProps",
4749
"Converter",
4850
"Factory",
4951
"NothingType",
@@ -67,6 +69,7 @@ class AttrsInstance(Protocol):
6769
"get_run_validators",
6870
"has",
6971
"ib",
72+
"inspect",
7073
"make_class",
7174
"mutable",
7275
"resolve_types",

src/attr/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ from . import filters as filters
2020
from . import setters as setters
2121
from . import validators as validators
2222
from ._cmp import cmp_using as cmp_using
23+
from ._funcs import inspect as inspect
24+
from ._props import ClassProps as ClassProps
2325
from ._typing_compat import AttrsInstance_
2426
from ._version_info import VersionInfo
2527
from attrs import (

src/attr/_make.py

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from functools import cached_property
1919
from typing import Any, NamedTuple, TypeVar
2020

21+
from attr._props import ClassProps, Hashability, KeywordOnly
22+
2123
# We need to import _compat itself in addition to the _compat members to avoid
2224
# having the thread-local in the globals here.
2325
from . import _compat, _config, setters
@@ -103,60 +105,6 @@ def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008
103105
return _none_constructor, _args
104106

105107

106-
class Hashability(enum.Enum):
107-
"""
108-
The hashability of a class.
109-
"""
110-
111-
HASHABLE = "hashable" # write a __hash__
112-
HASHABLE_CACHED = "hashable_cache" # write a __hash__ and cache the hash
113-
UNHASHABLE = "unhashable" # set __hash__ to None
114-
LEAVE_ALONE = "leave_alone" # don't touch __hash__
115-
116-
117-
class KeywordOnly(enum.Enum):
118-
"""
119-
How attributes should be treated regarding keyword-only parameters.
120-
"""
121-
122-
NO = "no" # attributes are not keyword-only
123-
YES = "yes" # attributes in current class without kw_only=False are keyword-only
124-
FORCE = "force" # all attributes are keyword-only
125-
126-
127-
class ClassProps(NamedTuple):
128-
"""
129-
Effective class properties as derived from parameters to attr.s() or
130-
define() decorators.
131-
132-
.. versionadded:: 25.4.0
133-
"""
134-
135-
is_exception: bool
136-
is_slotted: bool
137-
has_weakref_slot: bool
138-
is_frozen: bool
139-
kw_only: KeywordOnly
140-
collect_by_mro: bool
141-
init: bool
142-
repr: bool
143-
eq: bool
144-
order: bool
145-
hash: Hashability
146-
match_args: bool
147-
str: bool
148-
getstate_setstate: bool
149-
on_setattr: Callable[[str, Any], Any]
150-
field_transformer: Callable[[Attribute], Attribute]
151-
152-
@property
153-
def is_hashable(self):
154-
return (
155-
self.hash is Hashability.HASHABLE
156-
or self.hash is Hashability.HASHABLE_CACHED
157-
)
158-
159-
160108
def attrib(
161109
default=NOTHING,
162110
validator=None,

src/attr/_props.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# SPDX-License-Identifier: MIT
2+
3+
from __future__ import annotations
4+
5+
import enum
6+
7+
from collections.abc import Callable
8+
from typing import TYPE_CHECKING, Any, NamedTuple
9+
10+
from .exceptions import NotAnAttrsClassError
11+
12+
13+
if TYPE_CHECKING:
14+
from ._make import Attribute
15+
16+
17+
class Hashability(enum.Enum):
18+
"""
19+
The hashability of a class.
20+
"""
21+
22+
HASHABLE = "hashable" # write a __hash__
23+
HASHABLE_CACHED = "hashable_cache" # write a __hash__ and cache the hash
24+
UNHASHABLE = "unhashable" # set __hash__ to None
25+
LEAVE_ALONE = "leave_alone" # don't touch __hash__
26+
27+
28+
class KeywordOnly(enum.Enum):
29+
"""
30+
How attributes should be treated regarding keyword-only parameters.
31+
"""
32+
33+
NO = "no" # attributes are not keyword-only
34+
YES = "yes" # attributes in current class without kw_only=False are keyword-only
35+
FORCE = "force" # all attributes are keyword-only
36+
37+
38+
class ClassProps(NamedTuple):
39+
"""
40+
Effective class properties as derived from parameters to attr.s() or
41+
define() decorators.
42+
43+
.. versionadded:: 25.4.0
44+
"""
45+
46+
is_exception: bool
47+
is_slotted: bool
48+
has_weakref_slot: bool
49+
is_frozen: bool
50+
kw_only: KeywordOnly
51+
collect_by_mro: bool
52+
init: bool
53+
repr: bool
54+
eq: bool
55+
order: bool
56+
hash: Hashability
57+
match_args: bool
58+
str: bool
59+
getstate_setstate: bool
60+
on_setattr: Callable[[str, Any], Any]
61+
field_transformer: Callable[[Attribute], Attribute]
62+
63+
@property
64+
def is_hashable(self):
65+
return (
66+
self.hash is Hashability.HASHABLE
67+
or self.hash is Hashability.HASHABLE_CACHED
68+
)
69+
70+
71+
def inspect(cls: type) -> ClassProps:
72+
"""
73+
Inspect the class and return it's effective build parameters.
74+
75+
Args:
76+
cls: The class to inspect.
77+
78+
Returns:
79+
The effective build parameters of the class.
80+
81+
Raises:
82+
NotAnAttrsClassError: If the class is not an *attrs*-decorated class.
83+
84+
.. versionadded:: 25.4.0
85+
"""
86+
try:
87+
return cls.__dict__["__attrs_props__"]
88+
except IndexError:
89+
msg = f"{cls!r} is not an attrs-decorated class."
90+
raise NotAnAttrsClassError(msg) from None

src/attrs/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
NOTHING,
55
Attribute,
66
AttrsInstance,
7+
ClassProps,
78
Converter,
89
Factory,
910
NothingType,
@@ -17,6 +18,7 @@
1718
fields_dict,
1819
frozen,
1920
has,
21+
inspect,
2022
make_class,
2123
mutable,
2224
resolve_types,
@@ -31,6 +33,7 @@
3133
"NOTHING",
3234
"Attribute",
3335
"AttrsInstance",
36+
"ClassProps",
3437
"Converter",
3538
"Factory",
3639
"NothingType",
@@ -58,6 +61,7 @@
5861
"filters",
5962
"frozen",
6063
"has",
64+
"inspect",
6165
"make_class",
6266
"mutable",
6367
"resolve_types",

src/attrs/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ from attr import validate as validate
4141
from attr import validators as validators
4242
from attr import attrib, asdict as asdict, astuple as astuple
4343
from attr import NothingType as NothingType
44+
from attr import ClassProps as ClassProps
45+
from attr import inspect as inspect
4446

4547
if sys.version_info >= (3, 11):
4648
from typing import dataclass_transform

tests/test_make.py

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from hypothesis.strategies import booleans, integers, lists, sampled_from, text
2222

2323
import attr
24+
import attrs
2425

2526
from attr import _config
2627
from attr._compat import PY_3_10_PLUS
@@ -531,27 +532,24 @@ def test_sets_attrs_props(self):
531532
class C:
532533
x: int = attr.ib()
533534

534-
assert (
535-
ClassProps(
536-
is_exception=False,
537-
is_slotted=True,
538-
is_frozen=True,
539-
init=True,
540-
repr=True,
541-
eq=True,
542-
order=True,
543-
hash=Hashability.HASHABLE_CACHED,
544-
match_args=False,
545-
kw_only=KeywordOnly.FORCE,
546-
has_weakref_slot=True,
547-
collect_by_mro=False,
548-
str=True,
549-
getstate_setstate=True,
550-
on_setattr=None,
551-
field_transformer=None,
552-
)
553-
== C.__attrs_props__
554-
)
535+
assert ClassProps(
536+
is_exception=False,
537+
is_slotted=True,
538+
is_frozen=True,
539+
init=True,
540+
repr=True,
541+
eq=True,
542+
order=True,
543+
hash=Hashability.HASHABLE_CACHED,
544+
match_args=False,
545+
kw_only=KeywordOnly.FORCE,
546+
has_weakref_slot=True,
547+
collect_by_mro=False,
548+
str=True,
549+
getstate_setstate=True,
550+
on_setattr=None,
551+
field_transformer=None,
552+
) == attrs.inspect(C)
555553

556554
def test_sets_attrs_props_defaults(self):
557555
"""
@@ -563,27 +561,24 @@ def test_sets_attrs_props_defaults(self):
563561
class CDef:
564562
x = attr.ib()
565563

566-
assert (
567-
ClassProps(
568-
is_exception=False,
569-
is_slotted=False,
570-
is_frozen=False,
571-
init=True,
572-
repr=True,
573-
eq=True,
574-
order=True,
575-
hash=Hashability.UNHASHABLE,
576-
match_args=True,
577-
kw_only=KeywordOnly.NO,
578-
has_weakref_slot=True,
579-
collect_by_mro=False,
580-
str=False,
581-
getstate_setstate=False,
582-
on_setattr=None,
583-
field_transformer=None,
584-
)
585-
== CDef.__attrs_props__
586-
)
564+
assert ClassProps(
565+
is_exception=False,
566+
is_slotted=False,
567+
is_frozen=False,
568+
init=True,
569+
repr=True,
570+
eq=True,
571+
order=True,
572+
hash=Hashability.UNHASHABLE,
573+
match_args=True,
574+
kw_only=KeywordOnly.NO,
575+
has_weakref_slot=True,
576+
collect_by_mro=False,
577+
str=False,
578+
getstate_setstate=False,
579+
on_setattr=None,
580+
field_transformer=None,
581+
) == attrs.inspect(CDef)
587582

588583
def test_empty(self):
589584
"""

tests/typing_example.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,6 @@ class Hashable:
513513
def test(cls: type) -> None:
514514
if attr.has(cls):
515515
attr.resolve_types(cls)
516+
517+
518+
cp: attrs.ClassProps = attrs.inspect(Hashable)

0 commit comments

Comments
 (0)