Skip to content

Commit

Permalink
Fixed dict availability logic. Introduced use_slots=True in construct…
Browse files Browse the repository at this point in the history
…or instead of used_dict=False.
  • Loading branch information
sg495 committed Mar 8, 2024
1 parent 7b7177e commit 0c6d94a
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 63 deletions.
28 changes: 21 additions & 7 deletions typed_descriptors/attr.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from inspect import signature
from typing import (
Any,
Literal,
Optional,
Protocol,
Type,
Expand Down Expand Up @@ -141,7 +142,8 @@ def validator(
*,
readonly: bool = False,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> Attr[T]: ...

@staticmethod
Expand All @@ -152,7 +154,8 @@ def validator(
*,
readonly: bool = False,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> ValidatedAttrFactory: ...

@staticmethod
Expand All @@ -162,7 +165,8 @@ def validator(
*,
readonly: bool = False,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> ValidatedAttrFactory | Attr[T]:
"""
Decorator used to create an :class:`Attr` from a validator function,
Expand Down Expand Up @@ -212,6 +216,7 @@ def w(self, value: Sequence[int]) -> bool:
readonly=readonly,
backed_by=backed_by,
use_dict=use_dict,
use_slots=use_slots,
)

def _validated_attr(validator_fun: ValidatorFunction[_T]) -> Attr[_T]:
Expand All @@ -220,6 +225,7 @@ def _validated_attr(validator_fun: ValidatorFunction[_T]) -> Attr[_T]:
readonly=readonly,
backed_by=backed_by,
use_dict=use_dict,
use_slots=use_slots,
)

return _validated_attr
Expand All @@ -236,7 +242,8 @@ def __init__(
*,
readonly: bool = False,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> None:
# pylint: disable = redefined-builtin
...
Expand All @@ -250,7 +257,8 @@ def __init__(
*,
readonly: bool = False,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> None:
# pylint: disable = redefined-builtin
...
Expand All @@ -263,7 +271,8 @@ def __init__(
*,
readonly: bool = False,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> None:
"""
Creates a new attribute with the given type and optional validator.
Expand All @@ -278,7 +287,12 @@ def __init__(
:meta public:
"""
# pylint: disable = redefined-builtin
super().__init__(type, backed_by=backed_by, use_dict=use_dict)
super().__init__(
type,
backed_by=backed_by,
use_dict=use_dict,
use_slots=use_slots,
)
if validator is not None:
validate_validator_fun(validator)
self.__doc__ = validator.__doc__
Expand Down
71 changes: 28 additions & 43 deletions typed_descriptors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import sys
from typing import (
Any,
Literal,
Optional,
Protocol,
Type,
Expand All @@ -31,42 +32,16 @@ def is_dict_available(owner: type) -> bool:
:obj:`object`, or if the following is true for any class ``cls`` in
``owner.__mro__[:-1]`` (i.e. excluding the MRO root):
1. ``cls`` has no ``__slots__`` attribute
2. the ``__slots__`` attribute of ``cls`` is empty
3. ``__dict__`` appears in the the ``__slots__`` attribute of ``cls``
Otherwise, returns :obj:`False`.
.. warning ::
If the function returns :obj:`False`, then ``__dict__`` is certainly
not available. However, it is possible for a class ``cls`` in the MRO to
satisfy one of the three conditions above and for ``__dict__`` to not be
available on instances of the descriptor owner class. For example:
1. The ``__slots__`` attribute might have been deleted at some point
after the slots creation process.
2. The ``__slots__`` attribute contents might have been cleared, or it
might have been an iterable which as been fully consumed as part of
the slots creation process.
3. A ``__dict__`` entry might have been added to ``__slots__`` at some
point after the slots creation process.
If this happens somewhere in the MRO of the owner class being inspected,
the library will incorrectly infer that ``__dict__`` is available on
class instances, resulting in incorrect behaviour.
In such situations, please manually set ``use_dict`` to :obj:`False` in
attribute constructors to ensure that the ``__dict__`` mechanism is not
used to back the descriptor.
1. ``cls`` does not define ``__slots__``, or
2. ``__dict__`` appears in the ``__slots__`` for ``cls``
"""
mro = owner.__mro__
if mro[-1] != object:
return False
assert mro[-1] == object, "All classes should inherit from object."
for cls in mro[:-1]:
if not hasattr(cls, "__slots__"):
return True
if not cls.__slots__:
if "__slots__" not in cls.__dict__:
return True
if "__dict__" in cls.__slots__:
return True
Expand Down Expand Up @@ -173,17 +148,18 @@ class DescriptorBase(TypedDescriptor[T]):
that ``__dict__`` is not available on instances of the descriptor owner
class (cf. :func:`is_dict_available`), then a :obj:`TypeError` is raised
at the time when ``__set_name__`` is called.
2. If the ``use_dict`` argument is set to :obj:`False` in the descriptor
2. If the ``use_slots`` argument is set to :obj:`True` in the descriptor
constructor, then the "attr" functions :func:`getattr`, :func:`setattr`,
:func:`delattr` and :func:`hasattr` will be used. If the library is
certain that ``__dict__`` is not available on instances of the descriptor
owner class (cf. :func:`is_dict_available`) and the backing attribute
name is not present in the class slots (cf. :func:`class_slots`), then a
:obj:`TypeError` is raised at the time when ``__set_name__`` is called.
3. If ``use_dict`` is set to :obj:`None` (default value) in the descriptor
constructor, then :func:`is_dict_available` is called and ``use_dict`` is
set to the resulting value. Further validation is then performed as
described in points 1. and 2. above.
3. If neither ``use_dict`` nor ``use_slots__`` is set to :obj:`True` in the
descriptor constructor (the default case), then :func:`is_dict_available`
is called and the result is used to determine whether to use ``__dict__``
or slots for the backing attribute. Further validation is then performed,
as described in points 1 and 2 above.
Naming logic for the backing attribute:
Expand Down Expand Up @@ -230,7 +206,8 @@ def __init__(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> None:
# pylint: disable = redefined-builtin
...
Expand All @@ -242,7 +219,8 @@ def __init__(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> None:
# pylint: disable = redefined-builtin
...
Expand All @@ -253,18 +231,19 @@ def __init__(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> None:
"""
Creates a new descriptor with the given type and optional validator.
:param type: the type of the descriptor.
:param backed_by: name for the backing attribute (optional, default name
used if not specified).
:param use_dict: whether to use ``__dict__`` or slots for access to the
backing attribute (optional, automatically determined
if not specified).
:param use_dict: if set to :obj:`True`, ``__dict__`` will be used to
store the the backing attribute.
:param use_dict: if set to :obj:`True`, ``__slots__`` will be used to
store the the backing attribute.
:raises TypeError: if the type cannot be validated by the
:mod:`typing_validation` library.
Expand All @@ -274,9 +253,15 @@ def __init__(
if not can_validate(type):
raise TypeError(f"Cannot validate type {type!r}.")
validate(backed_by, Optional[str])
if use_dict and use_slots:
raise ValueError(
"Cannot set both use_dict=True and use_slots=True."
)
self.__type = type
self.__temp_backed_by = backed_by
self.__temp_use_dict = use_dict
self.__temp_use_dict = (
True if use_dict else False if use_slots else None
)
self.__descriptor_type__ = type

@final
Expand Down
51 changes: 38 additions & 13 deletions typed_descriptors/prop.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import sys
from typing import (
Any,
Literal,
Optional,
Protocol,
Type,
Expand Down Expand Up @@ -107,7 +108,8 @@ def value(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> Prop[T]: ...

@staticmethod
Expand All @@ -117,7 +119,8 @@ def value(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> PropFactory: ...

@staticmethod
Expand All @@ -126,7 +129,8 @@ def value(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> PropFactory | Prop[T]:
"""
An alias for :func:`cached_property`.
Expand All @@ -136,7 +140,10 @@ def value(
decorator for attributes.
"""
return cached_property(
value_fun, backed_by=backed_by, use_dict=use_dict
value_fun,
backed_by=backed_by,
use_dict=use_dict,
use_slots=use_slots,
)

__value_fun: ValueFunction[T]
Expand All @@ -149,7 +156,8 @@ def __init__(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> None:
# pylint: disable = redefined-builtin
...
Expand All @@ -162,7 +170,8 @@ def __init__(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> None:
# pylint: disable = redefined-builtin
...
Expand All @@ -174,7 +183,8 @@ def __init__(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> None:
"""
Creates a new property with the given type and value function.
Expand All @@ -191,7 +201,12 @@ def __init__(
"""
# pylint: disable = redefined-builtin
validate_value_fun(value)
super().__init__(type, backed_by=backed_by, use_dict=use_dict)
super().__init__(
type,
backed_by=backed_by,
use_dict=use_dict,
use_slots=use_slots,
)
if not callable(value):
raise TypeError(f"Expected callable 'value', got {value!r}.")
self.__value_fun = value
Expand Down Expand Up @@ -328,7 +343,8 @@ def cached_property(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> Prop[T]: ...


Expand All @@ -338,7 +354,8 @@ def cached_property(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> PropFactory: ...


Expand All @@ -347,7 +364,8 @@ def cached_property(
/,
*,
backed_by: Optional[str] = None,
use_dict: Optional[bool] = None,
use_dict: Optional[Literal[True]] = None,
use_slots: Optional[Literal[True]] = None,
) -> PropFactory | Prop[T]:
"""
Decorator used to create a cached property from a value function,
Expand Down Expand Up @@ -392,12 +410,19 @@ def x(self) -> Sequence[str]:
if value_fun is not None:
prop_type = value_fun_return_type(value_fun)
return Prop(
prop_type, value_fun, backed_by=backed_by, use_dict=use_dict
prop_type,
value_fun,
backed_by=backed_by,
use_dict=use_dict,
use_slots=use_slots,
)

def _cached_property(value_fun: ValueFunction[_T]) -> Prop[_T]:
return cached_property(
value_fun, backed_by=backed_by, use_dict=use_dict
value_fun,
backed_by=backed_by,
use_dict=use_dict,
use_slots=use_slots,
)

return _cached_property

0 comments on commit 0c6d94a

Please sign in to comment.