Source code for sentinel_value.sentinel_value

import inspect
import sys
import threading
from typing import Dict, Optional, Type


[docs]class SentinelValue: """Class for special unique placeholder objects, akin to :data:`None` and :data:`Ellipsis`. Useful for distinguishing "value is not set" and "value is set to None" cases as shown in this example:: >>> NOT_SET = SentinelValue(__name__, "NOT_SET") >>> value = getattr(object, "some_attribute", NOT_SET) >>> if value is NOT_SET: ... print('attribute is not set') attribute is not set If you need a separate type (for use with :mod:`typing` or :func:`functools.singledispatch`), then you can create a subclass:: >>> from typing import Union >>> class Missing(SentinelValue): ... pass >>> MISSING = Missing(__name__, "MISSING") # Here is how the Missing class can be used for type hinting. >>> value: Union[str, None, Missing] = getattr(object, "some_attribute", MISSING) >>> if value is MISSING: ... print("value is missing") value is missing """
[docs] def __init__(self, module_name: str, instance_name: str) -> None: """Initialize :class:`SentinelValue` object. :param module_name: name of Python module that hosts the sentinel value. In the majority of cases you should pass ``__name__`` here. :param instance_name: name of Python variable that points to the sentinel value. """ assert not ((module_name == self.__module__) and (instance_name == self.__class__.__name__)) self.instance_name = instance_name self.module_name = module_name self.qualified_name = self._compose_qualified_name(instance_name, module_name) super().__init__()
[docs] def __new__(cls, module_name, instance_name): """Create 1 instance of SentinelValue per name. Usually, when you call a class, you expect to get a new instance on each call. This is not true for the :class:`SentinelValue` class, that overrides :meth:`object.__new__` method to alter the way new instances are created. The overridden :meth:`SentinelValue.__new__` method constructs 1 unique instance per name, then saves it in the global registry called :data:`sentinel_value_instances`, and then next time it returns an already existing object. That is, if you call :class:`SentinelValue` multiple times with the same arguments, you get the *exactly same* instance, check this out:: >>> MISSING1 = SentinelValue(__name__, "MISSING") >>> MISSING2 = SentinelValue(__name__, "MISSING") >>> MISSING1 is MISSING2 True That guarantees that for each name, there is only 1 unique sentinel object. This is needed for 2 things: 1. :mod:`pickle` - when a sentinel value is pickled/un-pickled, you get the same instance (not a copy). 2. Hot code reloading Many IDEs can re-load individual modules on the fly, without re-starting the whole Python process. So this uniqueness trick allows to avoid duplicate instances that would appear if the module (containing sentinel value definition) is re-loaded. This is also nice when working with a Python shell. You can re-send ``SentinelValue()`` calls over and over again to the shell, and that wouldn't break your code (that already references the instance). """ qualified_name = cls._compose_qualified_name(instance_name, module_name) # The create-if-not-exists kind operation has to be protected with a lock. # # Otherwise, two (or more) concurrent threads will both see that a sentinel value # doesn't exist yet, both will create a new instance, and thus you get a duplicate. # # Well, of course, there is GIL (Global Interpreted Lock), that, in theory, # should protect us as long as we're not doing any I/O, but, there are no guarantees # about the GIL, and who knows what would # happen in the future. # # So let's just not rely on GIL, and just use 1 custom lock here, # that guarantees that a new object is really created only once. with sentinel_create_lock: # Check if the instance already exists in the global registry. existing_instance = sentinel_value_instances.get(qualified_name) if existing_instance is not None: # Change class on the fly. This is needed hot live code reloading features. # That is, if a subclass of SentinelValue was re-defined, # then we want to switch to the new class (but still use the old instance). # A bit hacky, but works. if existing_instance.__class__ is not cls: existing_instance.__class__ = cls return existing_instance # Ok, no existing instance. # Then create a new one, and put it to the global registry. new_instance = super().__new__(cls) sentinel_value_instances[qualified_name] = new_instance return new_instance
def __getnewargs__(self): # Get arguments for the __new__ method. # This is needed for pickle serialization. # In combination with magic in the overriden __new__() method above, # that allows to avoid constructing duplicates when un-pickling the object. return (self.module_name, self.instance_name) @staticmethod def _compose_qualified_name(instance_name: str, module_name: str) -> str: return module_name + "." + instance_name
[docs] def __repr__(self): """Provide :func:`repr` for :class:`SentinelValue`. By default, looks like this:: <MISSING> You're free to override it in a subclass if you want to customize it. """ return "<" + self.instance_name + ">"
[docs] @staticmethod def __bool__(): """Return False when :class:`SentinelValue` is treated as :func:`bool`. Sentinel values are always falsy. This is done because most sentinel objects are "no value" kind of objects (they're like ``None``, but just not the ``None`` object). So it is often handy to do ``if not value`` to check if there is no value (like if an attribute is set to ``None``, or not set at all):: >>> NOT_SET = SentinelValue(__name__, "NOT_SET") >>> value = getattr(object, "foobar", NOT_SET) # Is the value None, or empty, or not set at all? >>> if not value: ... print("no value") no value If this doesn't fit your case, you can override this method in a subclass. """ return False
sentinel_value_instances: Dict[str, SentinelValue] = {} """Dictionary that contains all instances of SentinelValue (and its subclasses). This dictionary looks like this:: { "package1.module1.MISSING": SentinelValue("package1.module1", "MISSING"), "package2.module2.MISSING": SentinelValue("package2.module2", "MISSING"), "package2.module2.ABSENT": SentinelValue("package2.module2", "ABSENT"), } When a :class:`SentinelValue` object is instanciated, it registers itself in this dictionary (and throws an error if already registered). This is needed to ensure that, for each name, there exists only 1 unique :class:`SentinelValue` object. """ sentinel_create_lock = threading.Lock() """A lock that prevents race conditions when creating new :class:`SentinelValue` objects. Problem: when you start multiple threads, they may try to create sentinel objects concurrently. If you're lucky enough, you get duplicate :class:`SentinelValue` instances, which is highly undesirable. This :data:`sentinel_create_lock` helps to protect against such race conditions. The lock is acquired whenever a new :class:`SentienlValue` object is created. So, when multiple threads try to create sentinel objects, then they're executed in sequence, and the 1st thread really creates a new instance, and other threads will get the already existing instance. """
[docs]def sentinel( instance_name: str, repr: Optional[str] = None, ) -> SentinelValue: """Create an unique sentinel object. Implementation of PEP 661 https://www.python.org/dev/peps/pep-0661/ >>> MISSING = sentinel("MISSING") >>> value = getattr(object, "value", MISSING) >>> if value is MISSING: ... print("value is not set") value is not set :param instance_name: Name of Python variable that points to the sentinel object. Needed for serialization (like :mod:`pickle`) and also nice :func:`repr`. :param repr: Any custom string that will be returned by func:`repr`. By default, composed as ``{module_name}.{instance_name}``. """ # pylint: disable=redefined-builtin module_name = _get_caller_module_name() assert module_name SentinelValueSubclass = _create_sentinel_value_subclass(module_name, instance_name) if repr: SentinelValueSubclass.__repr__ = lambda self: repr # type: ignore return SentinelValueSubclass(module_name, instance_name)
def _get_caller_module_name() -> Optional[str]: # Walk over the call stack, and stop as soon as we leave this (sentinel_value) module. frame = inspect.currentframe() while frame: module_name: str = frame.f_globals["__name__"] if module_name != __name__: return module_name frame = frame.f_back # Normally the code should never reach this point. # It may be only the case when stack inspectio is not available # (on some alternative Python implementation, like maybe Jython) return None def _create_sentinel_value_subclass(module_name: str, instance_name: str) -> Type[SentinelValue]: module = sys.modules[module_name] # Genarate class name from variable name. # E.g.: MISSING -> _sentinel_MISSING class_name = "_sentinel_" + instance_name.replace(".", "_") # Class should be created only once, so first check if it was already created. if hasattr(module, class_name): existing_class: Type[SentinelValue] = getattr(module, class_name) assert issubclass(existing_class, SentinelValue) return existing_class # Create a new subclass of SentinelValue. SentinelValueSubclass = type(class_name, (SentinelValue,), {}) # Bind class with the module. # That modifies module's globals, so the class really becomes a member of the module, # indistinguishable from classes that you define in the code. setattr(module, class_name, SentinelValueSubclass) SentinelValueSubclass.__module__ = module_name return SentinelValueSubclass