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