This is my proposed path regarding this:
Full implementation is ~200 lines long, but this sums it up.
# sentinel.py
class Singleton:
"""Base singleton class, which later can be replaced by `C` implementation and standardise things for `C` level sentinels as well"""
class SentinelMeta(type):
def __new__(mcs, type_name, bases, ...):
bases += (Singleton,)
return type.__new__(mcs, type_name, bases, namespace, **kwds)
# API ----
class SentinelGetAttrMeta(type):
def __getattr__(cls, name):
...
return SentinelMeta(f'{name}Type', ...)()
class Sentinel(metaclass=SentinelGetAttrMeta):
_registry = coll.defaultdict(dict)
def __new__(cls, name, bases=None, /, **attrs):
...
return SentinelMeta(f'{name}Type', bases or (), attrs)()
Usage:
# mymodule.py
from pickle import dumps, loads
from singleton import Sentinel
# With defaults
Null = Sentinel.Null
type(Null) # mymodule.NullType
type(Null)() is Null # True (same as None)
bool(Null) # True
# Serialises both sentinels and types
loads(dumps(Null)) is Null # True
loads(dumps(type(Null))) is type(Null) # True
Null = Sentinel.Null
# TypeError mymodule.NullType in _sentinel_type_registry
# With custom methods and attributes
# Some shorthands can be provided for method definition
NULL = Sentinel('NULL', bases=(int,), __repr__='MyNULLSentinel', __bool__=False, __contains__=lambda obj: False)
repr(NULL) # 'MyNULLSentinel'
bool(NULL) # False
1 in NULL # False
type(NULL).__mro__ # (mymodule.NullType, int, singleton.Singleton, object)
# registry per module
Sentinel._registry
defaultdict(dict,
{'mymodule': {
'NullType': mymodule.NullType,
'NULLType': mymodule.NULLType
}})
For anything more complex, one can just use Singleton
directly:
class MySentinelType(Singleton):
def __str__(self):
return type(self).__name__ + str(bool(self))
...
MySentinel = MySentinelType()
This would be fully in line with existing sentinels and support full customisation.
Also, C
stuff of Singletonobject.c and unification of `singletons` and `singlenels` would nicely fit into this.
If new implementation is provided I think it would only be worth it if it checked all the boxes and would be in line with existing standards. In other words, it would be a final stop. Otherwise if it doesn’t provide all features and its behaviour is divergent from existing standards people will just use object()
and not bother. Or stop using it after needing to look for alternative solutions after some new attribute for sentinel is needed or find out that it can not do things like isinstance(obj, (int, type(NULL))
. Implementation is fairly small, thus it is not difficult to get it all right.