Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 161 additions & 91 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import sys
import types
import unicodedata
import weakref

from collections.abc import Callable, Mapping
from functools import cached_property
Expand Down Expand Up @@ -102,6 +103,60 @@ def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008
return _none_constructor, _args


class Hashability(enum.Enum):
"""
The hashability of a class.
"""

HASHABLE = "hashable" # write a __hash__
HASHABLE_CACHED = "hashable_cache" # write a __hash__ and cache the hash
UNHASHABLE = "unhashable" # set __hash__ to None
LEAVE_ALONE = "leave_alone" # don't touch __hash__


class KeywordOnly(enum.Enum):
"""
How attributes should be treated regarding keyword-only parameters.
"""

NO = "no" # attributes are not keyword-only
YES = "yes" # attributes in current class without kw_only=False are keyword-only
FORCE = "force" # all attributes are keyword-only


class ClassProps(NamedTuple):
"""
Effective class properties as derived from parameters to attr.s() or
define() decorators.

.. versionadded:: 25.4.0
"""

is_exception: bool
is_slotted: bool
has_weakref_slot: bool
is_frozen: bool
kw_only: KeywordOnly
collect_by_mro: bool
init: bool
repr: bool
eq: bool
order: bool
hash: Hashability
match_args: bool
str: bool
getstate_setstate: bool
on_setattr: Callable[[str, Any], Any]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these Callables be optional?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ever none? looking at the docs:

If left None, the default behavior is to run converters and validators whenever an attribute is set.

I think it's set to NO_OP if nothing is supposed to happen?

field_transformer: Callable[[Attribute], Attribute]

@property
def is_hashable(self):
return (
self.hash is Hashability.HASHABLE
or self.hash is Hashability.HASHABLE_CACHED
)


def attrib(
default=NOTHING,
validator=None,
Expand Down Expand Up @@ -380,7 +435,6 @@ def _transform_attrs(
these,
auto_attribs,
kw_only,
force_kw_only,
collect_by_mro,
field_transformer,
) -> _Attributes:
Expand Down Expand Up @@ -438,7 +492,7 @@ def _transform_attrs(

fca = Attribute.from_counting_attr
own_attrs = [
fca(attr_name, ca, kw_only, anns.get(attr_name))
fca(attr_name, ca, kw_only is not KeywordOnly.NO, anns.get(attr_name))
for attr_name, ca in ca_list
]

Expand All @@ -451,7 +505,7 @@ def _transform_attrs(
cls, {a.name for a in own_attrs}
)

if kw_only and force_kw_only:
if kw_only is KeywordOnly.FORCE:
own_attrs = [a.evolve(kw_only=True) for a in own_attrs]
base_attrs = [a.evolve(kw_only=True) for a in base_attrs]

Expand Down Expand Up @@ -661,40 +715,29 @@ def __init__(
self,
cls: type,
these,
slots,
frozen,
weakref_slot,
getstate_setstate,
auto_attribs,
kw_only,
force_kw_only,
cache_hash,
is_exc,
collect_by_mro,
on_setattr,
has_custom_setattr,
field_transformer,
auto_attribs: bool,
props: ClassProps,
has_custom_setattr: bool,
):
attrs, base_attrs, base_map = _transform_attrs(
cls,
these,
auto_attribs,
kw_only,
force_kw_only,
collect_by_mro,
field_transformer,
props.kw_only,
props.collect_by_mro,
props.field_transformer,
)

self._cls = cls
self._cls_dict = dict(cls.__dict__) if slots else {}
self._cls_dict = dict(cls.__dict__) if props.is_slotted else {}
self._attrs = attrs
self._base_names = {a.name for a in base_attrs}
self._base_attr_map = base_map
self._attr_names = tuple(a.name for a in attrs)
self._slots = slots
self._frozen = frozen
self._weakref_slot = weakref_slot
self._cache_hash = cache_hash
self._slots = props.is_slotted
self._frozen = props.is_frozen
self._weakref_slot = props.has_weakref_slot
self._cache_hash = props.hash is Hashability.HASHABLE_CACHED
self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
self._pre_init_has_args = False
if self._has_pre_init:
Expand All @@ -705,20 +748,21 @@ def __init__(
self._pre_init_has_args = len(pre_init_signature.parameters) > 1
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
self._delete_attribs = not bool(these)
self._is_exc = is_exc
self._on_setattr = on_setattr
self._is_exc = props.is_exception
self._on_setattr = props.on_setattr

self._has_custom_setattr = has_custom_setattr
self._wrote_own_setattr = False

self._cls_dict["__attrs_attrs__"] = self._attrs
self._cls_dict["__attrs_props__"] = props

if frozen:
if props.is_frozen:
self._cls_dict["__setattr__"] = _frozen_setattrs
self._cls_dict["__delattr__"] = _frozen_delattrs

self._wrote_own_setattr = True
elif on_setattr in (
elif self._on_setattr in (
_DEFAULT_ON_SETATTR,
setters.validate,
setters.convert,
Expand All @@ -734,18 +778,18 @@ def __init__(
break
if (
(
on_setattr == _DEFAULT_ON_SETATTR
self._on_setattr == _DEFAULT_ON_SETATTR
and not (has_validator or has_converter)
)
or (on_setattr == setters.validate and not has_validator)
or (on_setattr == setters.convert and not has_converter)
or (self._on_setattr == setters.validate and not has_validator)
or (self._on_setattr == setters.convert and not has_converter)
):
# If class-level on_setattr is set to convert + validate, but
# there's no field to convert or validate, pretend like there's
# no on_setattr.
self._on_setattr = None

if getstate_setstate:
if props.getstate_setstate:
(
self._cls_dict["__getstate__"],
self._cls_dict["__setstate__"],
Expand Down Expand Up @@ -796,6 +840,7 @@ def build_class(self):
self._eval_snippets()
if self._slots is True:
cls = self._create_slots_class()
self._cls.__attrs_base_of_slotted__ = weakref.ref(cls)
else:
cls = self._patch_original_class()
if PY_3_10_PLUS:
Expand Down Expand Up @@ -1434,6 +1479,7 @@ def attrs(
on_setattr = setters.pipe(*on_setattr)

def wrap(cls):
nonlocal hash
is_frozen = frozen or _has_frozen_base_class(cls)
is_exc = auto_exc is True and issubclass(cls, BaseException)
has_own_setattr = auto_detect and _has_own_attribute(
Expand All @@ -1444,85 +1490,109 @@ def wrap(cls):
msg = "Can't freeze a class with a custom __setattr__."
raise ValueError(msg)

builder = _ClassBuilder(
cls,
these,
slots,
is_frozen,
weakref_slot,
_determine_whether_to_implement(
eq = not is_exc and _determine_whether_to_implement(
cls, eq_, auto_detect, ("__eq__", "__ne__")
)

if is_exc:
hashability = Hashability.LEAVE_ALONE
elif hash is True:
hashability = (
Hashability.HASHABLE_CACHED
if cache_hash
else Hashability.HASHABLE
)
elif hash is False:
hashability = Hashability.LEAVE_ALONE
elif hash is None:
if auto_detect is True and _has_own_attribute(cls, "__hash__"):
hashability = Hashability.LEAVE_ALONE
elif eq is True and is_frozen is True:
hashability = (
Hashability.HASHABLE_CACHED
if cache_hash
else Hashability.HASHABLE
)
elif eq is False:
hashability = Hashability.LEAVE_ALONE
else:
hashability = Hashability.UNHASHABLE
else:
msg = "Invalid value for hash. Must be True, False, or None."
raise TypeError(msg)

if kw_only:
kwo = KeywordOnly.FORCE if force_kw_only else KeywordOnly.YES
else:
kwo = KeywordOnly.NO

props = ClassProps(
is_exception=is_exc,
is_frozen=is_frozen,
is_slotted=slots,
collect_by_mro=collect_by_mro,
init=_determine_whether_to_implement(
cls, init, auto_detect, ("__init__",)
),
repr=_determine_whether_to_implement(
cls, repr, auto_detect, ("__repr__",)
),
eq=eq,
order=not is_exc
and _determine_whether_to_implement(
cls,
order_,
auto_detect,
("__lt__", "__le__", "__gt__", "__ge__"),
),
hash=hashability,
match_args=match_args,
kw_only=kwo,
has_weakref_slot=weakref_slot,
str=str,
getstate_setstate=_determine_whether_to_implement(
cls,
getstate_setstate,
auto_detect,
("__getstate__", "__setstate__"),
default=slots,
),
auto_attribs,
kw_only,
force_kw_only,
cache_hash,
is_exc,
collect_by_mro,
on_setattr,
has_own_setattr,
field_transformer,
on_setattr=on_setattr,
field_transformer=field_transformer,
)

if _determine_whether_to_implement(
cls, repr, auto_detect, ("__repr__",)
):
if not props.is_hashable and cache_hash:
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
raise TypeError(msg)

builder = _ClassBuilder(
cls,
these,
auto_attribs=auto_attribs,
props=props,
has_custom_setattr=has_own_setattr,
)

if props.repr is True:
builder.add_repr(repr_ns)

if str is True:
if props.str is True:
builder.add_str()

eq = _determine_whether_to_implement(
cls, eq_, auto_detect, ("__eq__", "__ne__")
)
if not is_exc and eq is True:
if props.eq is True:
builder.add_eq()
if not is_exc and _determine_whether_to_implement(
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
):
if props.order is True:
builder.add_order()

if not frozen:
builder.add_setattr()

nonlocal hash
if (
hash is None
and auto_detect is True
and _has_own_attribute(cls, "__hash__")
):
hash = False

if hash is not True and hash is not False and hash is not None:
# Can't use `hash in` because 1 == True for example.
msg = "Invalid value for hash. Must be True, False, or None."
raise TypeError(msg)

if hash is False or (hash is None and eq is False) or is_exc:
# Don't do anything. Should fall back to __object__'s __hash__
# which is by id.
if cache_hash:
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
raise TypeError(msg)
elif hash is True or (
hash is None and eq is True and is_frozen is True
):
# Build a __hash__ if told so, or if it's safe.
if props.is_hashable:
builder.add_hash()
else:
# Raise TypeError on attempts to hash.
if cache_hash:
msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled."
raise TypeError(msg)
elif props.hash is Hashability.UNHASHABLE:
builder.make_unhashable()

if _determine_whether_to_implement(
cls, init, auto_detect, ("__init__",)
):
if props.init:
builder.add_init()
else:
builder.add_attrs_init()
Expand Down
Loading