twilight_utils.more_typing.undefined

This module provides the Undefined class and related utilities for handling undefined values in Python.

  1"""This module provides the `Undefined` class and related utilities for handling undefined values in Python."""
  2
  3__all__ = [
  4    "DOC_UNDEFINED",
  5    "FALSEY_UNDEFINED",
  6    "STRINGABLE_FALSEY_UNDEFINED",
  7    "STRINGABLE_UNDEFINED",
  8    "UNDEFINED",
  9    "AllowedAttribute",
 10    "Undefined",
 11    "is_undefined",
 12]
 13
 14import dataclasses
 15import os
 16from collections.abc import Callable, Collection
 17from typing import Any, ClassVar, Final, NoReturn, Self, TypeIs
 18
 19
 20@dataclasses.dataclass(frozen=True, slots=True)
 21class AllowedAttribute:
 22    """Data class to store information describing allowed attribute for the Undefined class."""
 23
 24    attribute: str
 25    """
 26    Attribute name to allow access.
 27    """
 28    callback: Callable[..., Any]
 29    """
 30    Function callback to call on access to the attribute.
 31
 32    Must repeat the interface of the target method.
 33    """
 34    alias: str | None = None
 35    """
 36    The alias of the attribute. Optional.
 37
 38    Useful for cases you need to define several undefined instances with the same allowed arguments but different
 39    callback implementations.
 40    """
 41
 42
 43_ALWAYS_ALLOWED_ATTRIBUTES: tuple[str, ...] = (
 44    "_Undefined__allowed_attributes",
 45    "_Undefined__raise_access_error",
 46    "__init__",
 47    "__class__",
 48    "__wrapped__",
 49    "__module__",
 50    "__qualname__",
 51    "__isabstractmethod__",
 52)
 53"""
 54Collection of attributes that are always allowed for the Undefined class.
 55
 56Attributes in this collection are required for the internal implementation, and access to them is always allowed.
 57They are moved outside of the Undefined class to avoid recursion in the `__getattribute__` method.
 58"""
 59
 60
 61class Undefined:
 62    """
 63    A class to represent an undefined value.
 64
 65    Useful for scenarios where value will be defined later, but you need to define a variable on the initialization
 66    stage. For example, you want to define existing of some lazy entity, but you don't want to fetch it without a need.
 67
 68    This class is not a singleton, but restrict the number of instances to one per parameter set based on the exact
 69    usage of it. In the most strict case, this class will raise an attribute error on any attempt to access properties
 70    of the instance.
 71
 72    Note:
 73        Python has different behaviour between calling of the magic methods and access to them using built-in functions.
 74        Basic implementation of this class guarantees correct behaviour for calling `__str__`, `__bool__`,
 75        and `__repr__` based on specified configuration.
 76
 77        If you need to implement similar behaviour for another build-in calls, you need explicitly inherit
 78        from this class and override the necessary methods.
 79
 80        See details: https://docs.python.org/3/reference/datamodel.html#special-method-lookup
 81
 82    Args:
 83        allowed_attributes (Collection[AllowedAttribute]): The list of allowed attributes for
 84        the instance. It may be an attribute name, or a tuple with the attribute name and the callback to call.
 85    """
 86
 87    __instances: ClassVar[dict[frozenset[str], Self]] = {}
 88
 89    def __new__(cls, *allowed_attributes: AllowedAttribute) -> Self:
 90        """
 91        Return the same instance of the class per each set of allowed arguments.
 92
 93        Args:
 94            allowed_attributes (Collection[AllowedAttribute]): The list of allowed attributes for
 95            the instance. It may be an attribute name, or a tuple with the attribute name and the callback to call.
 96
 97        Returns:
 98            Self: The instance of the class with the defined callbacks for the allowed attributes.
 99        """
100        instance_identifier = cls.__to_instance_identifier(allowed_attributes)
101        if instance_identifier not in cls.__instances:
102            instance = super().__new__(cls)
103            # __new__ and __init__ methods are final and must be compatible
104            # The only reason class itself is not final it a need of inheritance for the possible built-in methods
105            # workaround
106            instance.__init__(*allowed_attributes)  # type: ignore[misc]
107            cls.__instances[instance_identifier] = instance
108
109        return cls.__instances[instance_identifier]
110
111    def __init__(self, *allowed_attributes: AllowedAttribute) -> None:
112        self.__allowed_attributes: dict[str, Callable[..., Any]] = {}
113        for item in allowed_attributes:
114            setattr(self, item.attribute, item.callback)
115            self.__allowed_attributes[item.attribute] = item.callback
116
117    def __str__(self) -> str:
118        """
119        Override of the `__str__` method to return the default message for the Undefined object.
120
121        Returns:
122            str: Default message for Undefined object.
123
124        Raises:
125            ValueError: If the access to the attribute is restricted.
126            AssertionError: If method returns a value of the wrong type.
127        """
128        if "__str__" in self.__allowed_attributes:
129            return Undefined.__validate_correct_type(self.__allowed_attributes["__str__"](), str)
130        Undefined.__raise_access_error("__str__")
131
132    def __repr__(self) -> str:
133        """
134        Override of the `__repr__` method to return the default message for the Undefined object.
135
136        If you need to access to this method for the documentation generation, you can specify
137        `STD_UTILS__UNDEFINED__DOC_GENERATING` environment variable to `1`.
138
139        Returns:
140            str: Default message for Undefined object.
141
142        Raises:
143            ValueError: If the access to the attribute is restricted.
144            AssertionError: If method returns a value of the wrong type.
145        """
146        if os.getenv("STD_UTILS__UNDEFINED__DOC_GENERATING", "0") == "1":
147            return "[REQUIRED]"
148        if "__repr__" in self.__allowed_attributes:
149            return Undefined.__validate_correct_type(self.__allowed_attributes["__repr__"](), str)
150        Undefined.__raise_access_error("__repr__")
151
152    def __bool__(self) -> bool:
153        """
154        Override of the `__bool__` method to always return False.
155
156        Returns:
157            bool: Default value for the Undefined object.
158
159        Raises:
160            ValueError: If the access to the attribute is restricted.
161            AssertionError: If method returns a value of the wrong type.
162        """
163        if "__bool__" in self.__allowed_attributes:
164            return Undefined.__validate_correct_type(self.__allowed_attributes["__bool__"](), bool)
165        Undefined.__raise_access_error("__bool__")
166
167    def __getattribute__(self, item: str) -> Any:  # noqa: ANN401 - Any is useful here
168        """
169        Override of the `__getattribute__` method to raise an error on any access to the instance properties.
170
171        Args:
172            item (str): The name of the attribute to access.
173
174        Raises:
175            ValueError: Always, as the access to the attribute is restricted. Exception made for several methods,
176            such as `__str__`, `__bool__`, and `__repr__`, if they are allowed for the instance.
177
178        Returns:
179            Any: The value of the attribute if it is allowed for the instance.
180        """
181        if item in _ALWAYS_ALLOWED_ATTRIBUTES or item in self.__allowed_attributes:
182            return super().__getattribute__(item)
183        Undefined.__raise_access_error(item)
184
185    @staticmethod
186    def __raise_access_error(item: str) -> NoReturn:
187        msg = (
188            f"[UNDEFINED] You are referencing undefined object. Access to the attribute {item!r} is impossible. "
189            f"Ensure you populate the value before using any of its properties."
190        )
191        raise ValueError(msg)
192
193    @staticmethod
194    def __validate_correct_type[T](result: Any, expected_type: type[T]) -> T:  # noqa: ANN401 - Any is useful here
195        if not isinstance(result, expected_type):
196            msg = f"The __str__ method must return a string, got {type(result)} instead."
197            raise AssertionError(msg)  # noqa: TRY004 - It's a developer mistake, not a code error
198        return result
199
200    @classmethod
201    def __to_instance_identifier(cls, allowed_attributes: Collection[AllowedAttribute] = ()) -> frozenset[str]:
202        result = set()
203        for attribute in allowed_attributes:
204            if attribute.attribute in _ALWAYS_ALLOWED_ATTRIBUTES:
205                msg = f"Attribute {attribute!r} is reserved for the internal use."
206                raise ValueError(msg)
207            if attribute.attribute in result:
208                msg = f"Duplicate argument {attribute.attribute!r} in the allowed arguments."
209                raise ValueError(msg)
210            result |= {f"{attribute.attribute}|{attribute.alias}" if attribute.alias else attribute.attribute}
211        return frozenset(result)
212
213
214UNDEFINED: Final[Any] = Undefined()
215"""
216The basic and most simple instance of the Undefined class.
217
218Does not allow access to any attribute except for methods required for the correct work of the class.
219"""
220STRINGABLE_UNDEFINED: Final[Any] = Undefined(
221    AllowedAttribute("__str__", lambda: "[UNDEFINED]"),
222    AllowedAttribute("__repr__", lambda: "[UNDEFINED]"),
223)
224"""
225The instance of the Undefined class that allows access to the `__str__` and `__repr__` methods.
226
227Useful for cases where you need to access the string representation of the object at runtime without raising an error,
228for example, in the dataclasses, pydantic models. loggers, etc.
229
230If you need alternative implementation for the `__str__` or `__repr__` methods, you need to create a custom instance of
231the Undefined class with aliases specified.
232"""
233DOC_UNDEFINED: Final[Any] = Undefined(
234    AllowedAttribute("__repr__", lambda: "[REQUIRED]"),
235)
236"""
237Specific instance of the Undefined class that allows access to the `__repr__` method.
238
239Used for cases you are using pdoc3 or sphinx to generate documentation. This object will prevent the generation from
240unexpected crashes on processing of the UNDEFINED object.
241"""
242FALSEY_UNDEFINED: Final[Any] = Undefined(AllowedAttribute("__bool__", lambda: False))
243"""
244Simple instance of the Undefined class that allows access to the `__bool__` method.
245
246Useful for cases where UNDEFINED is actually a falsy value, and you need to check it in the if statement or any other
247boolean context.
248"""
249STRINGABLE_FALSEY_UNDEFINED: Final[Any] = Undefined(
250    AllowedAttribute("__str__", lambda: "[UNDEFINED]"),
251    AllowedAttribute("__repr__", lambda: "[UNDEFINED]"),
252    AllowedAttribute("__bool__", lambda: False),
253)
254"""
255Combined instance of STRINGABLE_UNDEFINED and FALSEY_UNDEFINED.
256"""
257
258
259def is_undefined(value: Any) -> TypeIs[Undefined]:  # noqa: ANN401 - Any is useful here
260    """
261    Check if the value is an undefined value.
262
263    Args:
264        value (Any): The value to check.
265
266    Returns:
267        bool: True if the value is an undefined value, False otherwise.
268    """
269    return isinstance(value, Undefined)
DOC_UNDEFINED: Final[Any] = [REQUIRED]

Specific instance of the Undefined class that allows access to the __repr__ method.

Used for cases you are using pdoc3 or sphinx to generate documentation. This object will prevent the generation from unexpected crashes on processing of the UNDEFINED object.

FALSEY_UNDEFINED: Final[Any] = [REQUIRED]

Simple instance of the Undefined class that allows access to the __bool__ method.

Useful for cases where UNDEFINED is actually a falsy value, and you need to check it in the if statement or any other boolean context.

STRINGABLE_FALSEY_UNDEFINED: Final[Any] = [REQUIRED]

Combined instance of STRINGABLE_UNDEFINED and FALSEY_UNDEFINED.

STRINGABLE_UNDEFINED: Final[Any] = [REQUIRED]

The instance of the Undefined class that allows access to the __str__ and __repr__ methods.

Useful for cases where you need to access the string representation of the object at runtime without raising an error, for example, in the dataclasses, pydantic models. loggers, etc.

If you need alternative implementation for the __str__ or __repr__ methods, you need to create a custom instance of the Undefined class with aliases specified.

UNDEFINED: Final[Any] = [REQUIRED]

The basic and most simple instance of the Undefined class.

Does not allow access to any attribute except for methods required for the correct work of the class.

@dataclasses.dataclass(frozen=True, slots=True)
class AllowedAttribute:
21@dataclasses.dataclass(frozen=True, slots=True)
22class AllowedAttribute:
23    """Data class to store information describing allowed attribute for the Undefined class."""
24
25    attribute: str
26    """
27    Attribute name to allow access.
28    """
29    callback: Callable[..., Any]
30    """
31    Function callback to call on access to the attribute.
32
33    Must repeat the interface of the target method.
34    """
35    alias: str | None = None
36    """
37    The alias of the attribute. Optional.
38
39    Useful for cases you need to define several undefined instances with the same allowed arguments but different
40    callback implementations.
41    """

Data class to store information describing allowed attribute for the Undefined class.

AllowedAttribute( attribute: str, callback: Callable[..., typing.Any], alias: str | None = None)
attribute: str

Attribute name to allow access.

callback: Callable[..., typing.Any]

Function callback to call on access to the attribute.

Must repeat the interface of the target method.

alias: str | None

The alias of the attribute. Optional.

Useful for cases you need to define several undefined instances with the same allowed arguments but different callback implementations.

class Undefined:
 62class Undefined:
 63    """
 64    A class to represent an undefined value.
 65
 66    Useful for scenarios where value will be defined later, but you need to define a variable on the initialization
 67    stage. For example, you want to define existing of some lazy entity, but you don't want to fetch it without a need.
 68
 69    This class is not a singleton, but restrict the number of instances to one per parameter set based on the exact
 70    usage of it. In the most strict case, this class will raise an attribute error on any attempt to access properties
 71    of the instance.
 72
 73    Note:
 74        Python has different behaviour between calling of the magic methods and access to them using built-in functions.
 75        Basic implementation of this class guarantees correct behaviour for calling `__str__`, `__bool__`,
 76        and `__repr__` based on specified configuration.
 77
 78        If you need to implement similar behaviour for another build-in calls, you need explicitly inherit
 79        from this class and override the necessary methods.
 80
 81        See details: https://docs.python.org/3/reference/datamodel.html#special-method-lookup
 82
 83    Args:
 84        allowed_attributes (Collection[AllowedAttribute]): The list of allowed attributes for
 85        the instance. It may be an attribute name, or a tuple with the attribute name and the callback to call.
 86    """
 87
 88    __instances: ClassVar[dict[frozenset[str], Self]] = {}
 89
 90    def __new__(cls, *allowed_attributes: AllowedAttribute) -> Self:
 91        """
 92        Return the same instance of the class per each set of allowed arguments.
 93
 94        Args:
 95            allowed_attributes (Collection[AllowedAttribute]): The list of allowed attributes for
 96            the instance. It may be an attribute name, or a tuple with the attribute name and the callback to call.
 97
 98        Returns:
 99            Self: The instance of the class with the defined callbacks for the allowed attributes.
100        """
101        instance_identifier = cls.__to_instance_identifier(allowed_attributes)
102        if instance_identifier not in cls.__instances:
103            instance = super().__new__(cls)
104            # __new__ and __init__ methods are final and must be compatible
105            # The only reason class itself is not final it a need of inheritance for the possible built-in methods
106            # workaround
107            instance.__init__(*allowed_attributes)  # type: ignore[misc]
108            cls.__instances[instance_identifier] = instance
109
110        return cls.__instances[instance_identifier]
111
112    def __init__(self, *allowed_attributes: AllowedAttribute) -> None:
113        self.__allowed_attributes: dict[str, Callable[..., Any]] = {}
114        for item in allowed_attributes:
115            setattr(self, item.attribute, item.callback)
116            self.__allowed_attributes[item.attribute] = item.callback
117
118    def __str__(self) -> str:
119        """
120        Override of the `__str__` method to return the default message for the Undefined object.
121
122        Returns:
123            str: Default message for Undefined object.
124
125        Raises:
126            ValueError: If the access to the attribute is restricted.
127            AssertionError: If method returns a value of the wrong type.
128        """
129        if "__str__" in self.__allowed_attributes:
130            return Undefined.__validate_correct_type(self.__allowed_attributes["__str__"](), str)
131        Undefined.__raise_access_error("__str__")
132
133    def __repr__(self) -> str:
134        """
135        Override of the `__repr__` method to return the default message for the Undefined object.
136
137        If you need to access to this method for the documentation generation, you can specify
138        `STD_UTILS__UNDEFINED__DOC_GENERATING` environment variable to `1`.
139
140        Returns:
141            str: Default message for Undefined object.
142
143        Raises:
144            ValueError: If the access to the attribute is restricted.
145            AssertionError: If method returns a value of the wrong type.
146        """
147        if os.getenv("STD_UTILS__UNDEFINED__DOC_GENERATING", "0") == "1":
148            return "[REQUIRED]"
149        if "__repr__" in self.__allowed_attributes:
150            return Undefined.__validate_correct_type(self.__allowed_attributes["__repr__"](), str)
151        Undefined.__raise_access_error("__repr__")
152
153    def __bool__(self) -> bool:
154        """
155        Override of the `__bool__` method to always return False.
156
157        Returns:
158            bool: Default value for the Undefined object.
159
160        Raises:
161            ValueError: If the access to the attribute is restricted.
162            AssertionError: If method returns a value of the wrong type.
163        """
164        if "__bool__" in self.__allowed_attributes:
165            return Undefined.__validate_correct_type(self.__allowed_attributes["__bool__"](), bool)
166        Undefined.__raise_access_error("__bool__")
167
168    def __getattribute__(self, item: str) -> Any:  # noqa: ANN401 - Any is useful here
169        """
170        Override of the `__getattribute__` method to raise an error on any access to the instance properties.
171
172        Args:
173            item (str): The name of the attribute to access.
174
175        Raises:
176            ValueError: Always, as the access to the attribute is restricted. Exception made for several methods,
177            such as `__str__`, `__bool__`, and `__repr__`, if they are allowed for the instance.
178
179        Returns:
180            Any: The value of the attribute if it is allowed for the instance.
181        """
182        if item in _ALWAYS_ALLOWED_ATTRIBUTES or item in self.__allowed_attributes:
183            return super().__getattribute__(item)
184        Undefined.__raise_access_error(item)
185
186    @staticmethod
187    def __raise_access_error(item: str) -> NoReturn:
188        msg = (
189            f"[UNDEFINED] You are referencing undefined object. Access to the attribute {item!r} is impossible. "
190            f"Ensure you populate the value before using any of its properties."
191        )
192        raise ValueError(msg)
193
194    @staticmethod
195    def __validate_correct_type[T](result: Any, expected_type: type[T]) -> T:  # noqa: ANN401 - Any is useful here
196        if not isinstance(result, expected_type):
197            msg = f"The __str__ method must return a string, got {type(result)} instead."
198            raise AssertionError(msg)  # noqa: TRY004 - It's a developer mistake, not a code error
199        return result
200
201    @classmethod
202    def __to_instance_identifier(cls, allowed_attributes: Collection[AllowedAttribute] = ()) -> frozenset[str]:
203        result = set()
204        for attribute in allowed_attributes:
205            if attribute.attribute in _ALWAYS_ALLOWED_ATTRIBUTES:
206                msg = f"Attribute {attribute!r} is reserved for the internal use."
207                raise ValueError(msg)
208            if attribute.attribute in result:
209                msg = f"Duplicate argument {attribute.attribute!r} in the allowed arguments."
210                raise ValueError(msg)
211            result |= {f"{attribute.attribute}|{attribute.alias}" if attribute.alias else attribute.attribute}
212        return frozenset(result)

A class to represent an undefined value.

Useful for scenarios where value will be defined later, but you need to define a variable on the initialization stage. For example, you want to define existing of some lazy entity, but you don't want to fetch it without a need.

This class is not a singleton, but restrict the number of instances to one per parameter set based on the exact usage of it. In the most strict case, this class will raise an attribute error on any attempt to access properties of the instance.

Note:

Python has different behaviour between calling of the magic methods and access to them using built-in functions. Basic implementation of this class guarantees correct behaviour for calling __str__, __bool__, and __repr__ based on specified configuration.

If you need to implement similar behaviour for another build-in calls, you need explicitly inherit from this class and override the necessary methods.

See details: https://docs.python.org/3/reference/datamodel.html#special-method-lookup

Arguments:
  • allowed_attributes (Collection[AllowedAttribute]): The list of allowed attributes for
  • the instance. It may be an attribute name, or a tuple with the attribute name and the callback to call.
Undefined( *allowed_attributes: AllowedAttribute)
 90    def __new__(cls, *allowed_attributes: AllowedAttribute) -> Self:
 91        """
 92        Return the same instance of the class per each set of allowed arguments.
 93
 94        Args:
 95            allowed_attributes (Collection[AllowedAttribute]): The list of allowed attributes for
 96            the instance. It may be an attribute name, or a tuple with the attribute name and the callback to call.
 97
 98        Returns:
 99            Self: The instance of the class with the defined callbacks for the allowed attributes.
100        """
101        instance_identifier = cls.__to_instance_identifier(allowed_attributes)
102        if instance_identifier not in cls.__instances:
103            instance = super().__new__(cls)
104            # __new__ and __init__ methods are final and must be compatible
105            # The only reason class itself is not final it a need of inheritance for the possible built-in methods
106            # workaround
107            instance.__init__(*allowed_attributes)  # type: ignore[misc]
108            cls.__instances[instance_identifier] = instance
109
110        return cls.__instances[instance_identifier]

Return the same instance of the class per each set of allowed arguments.

Arguments:
  • allowed_attributes (Collection[AllowedAttribute]): The list of allowed attributes for
  • the instance. It may be an attribute name, or a tuple with the attribute name and the callback to call.
Returns:

Self: The instance of the class with the defined callbacks for the allowed attributes.

def is_undefined(value: Any) -> TypeIs[Undefined]:
260def is_undefined(value: Any) -> TypeIs[Undefined]:  # noqa: ANN401 - Any is useful here
261    """
262    Check if the value is an undefined value.
263
264    Args:
265        value (Any): The value to check.
266
267    Returns:
268        bool: True if the value is an undefined value, False otherwise.
269    """
270    return isinstance(value, Undefined)

Check if the value is an undefined value.

Arguments:
  • value (Any): The value to check.
Returns:

bool: True if the value is an undefined value, False otherwise.