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)
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.
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.
Combined instance of STRINGABLE_UNDEFINED and FALSEY_UNDEFINED.
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.
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.
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.
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.
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.
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.