From 0c6d94add184a5f52d26344fc7d024a93266dcc9 Mon Sep 17 00:00:00 2001 From: sg495 Date: Fri, 8 Mar 2024 19:26:37 +0000 Subject: [PATCH] Fixed dict availability logic. Introduced use_slots=True in constructor instead of used_dict=False. --- typed_descriptors/attr.py | 28 +++++++++++---- typed_descriptors/base.py | 71 +++++++++++++++------------------------ typed_descriptors/prop.py | 51 +++++++++++++++++++++------- 3 files changed, 87 insertions(+), 63 deletions(-) diff --git a/typed_descriptors/attr.py b/typed_descriptors/attr.py index c416c79..65c90ff 100644 --- a/typed_descriptors/attr.py +++ b/typed_descriptors/attr.py @@ -9,6 +9,7 @@ from inspect import signature from typing import ( Any, + Literal, Optional, Protocol, Type, @@ -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 @@ -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 @@ -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, @@ -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]: @@ -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 @@ -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 ... @@ -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 ... @@ -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. @@ -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__ diff --git a/typed_descriptors/base.py b/typed_descriptors/base.py index 3ac23c7..fa0b741 100644 --- a/typed_descriptors/base.py +++ b/typed_descriptors/base.py @@ -10,6 +10,7 @@ import sys from typing import ( Any, + Literal, Optional, Protocol, Type, @@ -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 @@ -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: @@ -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 ... @@ -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 ... @@ -253,7 +231,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 descriptor with the given type and optional validator. @@ -261,10 +240,10 @@ def __init__( :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. @@ -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 diff --git a/typed_descriptors/prop.py b/typed_descriptors/prop.py index ba2c377..b45228c 100644 --- a/typed_descriptors/prop.py +++ b/typed_descriptors/prop.py @@ -10,6 +10,7 @@ import sys from typing import ( Any, + Literal, Optional, Protocol, Type, @@ -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 @@ -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 @@ -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`. @@ -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] @@ -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 ... @@ -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 ... @@ -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. @@ -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 @@ -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]: ... @@ -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: ... @@ -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, @@ -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