Base Classes and Utilities for compatibility, features and validation.
While developing Python software for Visual Effects pipelines, I found myself having to write the same boiler-plate code over and over again, as well as struggling with compatibility issues and feature gaps between Python 2.7 and Python 3.7+.
So I decided to implement solutions for those issues at the Base, and basicco was born.
The goal with the CompatBaseMeta metaclass and the CompatBase class is to bridge some of the feature gaps between Python 2.7 and Python 3.7+.
- This includes adding Python 2.7 workarounds for:
-
- Abstract properties: Better abstractmethod decorator support for property-like descriptors. See also abstract_class.
- PEP 487: Support for __init_subclass__ and __set_name__. See also init_subclass and set_name.
- object.__dir__: Base __dir__ method. See also default_dir.
- __eq__ override: Overriding __eq__ will set __hash__ to None. See also implicit_hash.
- PEP 307: Support for pickling objects with __slots__. See also obj_state.
- PEP 3155: Qualified name __qualname__ for nested classes. See also qualname.
- __ne__ behavior: By default, __ne__ should negate the result of __eq__. See also safe_not_equals
- PEP 0560: Better handling of Generic classes. See also tippo.
In addition to the compatibility solutions, the goal with the BaseMeta metaclass and the Base class is to add useful low-level features that hopefully yield better code readability and validation.
- This includes:
-
- __weakref__ slot: Added by default.
- locked_class: Public class attributes are read-only by default.
- explicit_hash: Overriding __eq__ without overriding __hash__ will error.
- namespace: Adds a protected __namespace unique to each class.
- runtime_final: Runtime checking for classes and methods decorated with final.
The SlottedBase class and the SlottedBaseMeta metaclass offer all features from Base and BaseMeta plus implicit __slots__ declaration. See slotted for more information.
Apart from the features integrated into the base classes, basicco provides many general utility modules.
Better support for abstract classes.
Provides abstract decorators that can be used directly on methods but also on property getters, classmethods, and staticmethods (even in Python 2.7).
>>> from six import with_metaclass
>>> from basicco.abstract_class import AbstractMeta, abstract
>>> class Asset(with_metaclass(AbstractMeta, object)):
... @abstract
... def method(self):
... pass
...
... @property
... @abstract
... def prop(self):
... return None
...
>>> Asset()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Asset...
Retrieve the caller's module name.
Backport of the contextvars module for Python 2.7, based on MagicStack/contextvars.
When imported from Python 3, it simply redirects to the native contextvars module.
Custom representation functions for mappings, items, and iterables.
Backport of Python 3's implementation of object.__dir__.
This allows for calling super().__dir__() from a subclass to leverage the default implementation.
Configurable descriptors.
>>> from six import with_metaclass
>>> from basicco.descriptors import REMOVE, Descriptor, Owner
>>> class SlotDescriptor(Descriptor):
... def __get_required_slots__(self):
... return (self.name,) # request a slot with the same name as this
... def __get_replacement__(self):
... return REMOVE # remove this descriptor from the class body
...
>>> class PropDescriptor(Descriptor):
... __slots__ = ("_slot_desc",)
... def __init__(self, slot_desc):
... super(PropDescriptor, self).__init__()
... self._slot_desc = slot_desc
... def __get__(self, instance, owner):
... if instance is not None:
... return getattr(instance, self._slot_desc.name)
... return self
... def __set__(self, instance, value):
... setattr(instance, self._slot_desc.name, value)
...
>>> class Stuff(Owner):
... _foo = SlotDescriptor()
... _bar = SlotDescriptor()
... foo = PropDescriptor(_foo)
... bar = PropDescriptor(_bar)
...
>>> stuff = Stuff()
>>> stuff.foo = "foo"
>>> stuff.bar = "bar"
>>> stuff.foo
'foo'
>>> stuff.bar
'bar'
Easily generate classes on the fly. This works best with a Base class. If provided a valid qualified name and module (uses caller_module by default), the class will be pickable/importable.
Generate debuggable code on the fly that supports line numbers on tracebacks.
>>> from basicco.dynamic_code import make_function, generate_unique_filename
>>> class MyClass(object):
... pass
...
>>> bar = 'bar'
>>> # Prepare the script and necessary data.
>>> script = "\n".join(
... (
... "def __init__(self):",
... " self.foo = 'bar'",
... )
... )
>>> # Gather information.
>>> name = "__init__"
>>> owner_name = MyClass.__name__
>>> module = MyClass.__module__
>>> filename = generate_unique_filename(name, module, owner_name)
>>> globs = {"bar": bar}
>>> # Make function and attach it as a method.
>>> MyClass.__init__ = make_function(name, script, globs, filename, module)
>>> obj = MyClass()
>>> obj.foo
'bar'
Metaclass that forces __hash__ to be declared whenever __eq__ is declared.
Run a value through a callable factory (or None).
Backport of functools.cache, functools.lru_cache, and functools.update_wrapper for Python 2.7.
Get consistent MRO amongst different python versions. This works even with generic classes in Python 2.7.
>>> from six import with_metaclass
>>> from tippo import Generic, TypeVar
>>> from basicco.get_mro import get_mro
>>> T = TypeVar("T")
>>> class MyGeneric(Generic[T]):
... pass
...
>>> class SubClass(MyGeneric[T]):
... pass
...
>>> class Mixed(SubClass[T], MyGeneric[T]):
... pass
...
>>> [c.__name__ for c in get_mro(Mixed)]
['Mixed', 'SubClass', 'MyGeneric', 'Generic', 'object']
An integer subclass that pickles/copies as None. This can be used to avoid serializing a cached hash value.
Metaclass that forces __hash__ to None when __eq__ is declared. This is a backport of the default behavior in Python 3.
Generate importable dot paths and import from them.
Backport of the functionality of __init_subclass__ from PEP 487 to Python 2.7. This works for both Python 2 (using __kwargs__) and 3 (using the new class parameters).
Lazily-evaluated tuple-like structure.
Prevents changing public class attributes.
Functions to mangle/unmangle/extract private names.
Mapping Proxy type (read-only dictionary) for older Python versions.
Wraps a dictionary/mapping and provides attribute-style access to it.
Also provides a NamespacedMeta metaclass that adds a __namespace protected class attribute that is unique to each class.
>>> from six import with_metaclass
>>> from basicco.namespace import NamespacedMeta
>>> class Asset(with_metaclass(NamespacedMeta, object)):
... @classmethod
... def set_class_value(cls, value):
... cls.__namespace.value = value
...
... @classmethod
... def get_class_value(cls):
... return cls.__namespace.value
...
>>> Asset.set_class_value("foobar")
>>> Asset.get_class_value()
'foobar'
Backport of contextlib.nullcontext for Python 2.7.
>>> from basicco.null_context import null_context
>>> from basicco.suppress_exception import suppress_exception
>>> def myfunction(arg, ignore_exceptions=False):
... if ignore_exceptions:
... # Use suppress_exception to ignore all exceptions.
... cm = suppress_exception(Exception)
... else:
... # Do not ignore any exceptions, cm has no effect.
... cm = null_context()
... with cm:
... pass # Do something
...
Get/update the state of an object, slotted or not (works even in Python 2.7).
Also provides a ReducibleMeta metaclass that allows for pickling instances of slotted classes in Python 2.7.
Python 2.7 compatible way of getting the qualified name. Based on wbolster/qualname. Also provides a QualnamedMeta metaclass with a __qualname__ class property for Python 2.7.
Decorator that prevents infinite recursion for __repr__ methods.
Runtime-checked version of the typing.final decorator.
Can be used on methods, properties, classmethods, staticmethods, and classes that have RuntimeFinalMeta as a metaclass. It is also recognized by static type checkers and prevents subclassing and/or member overriding during runtime:
>>> from six import with_metaclass
>>> from basicco.runtime_final import RuntimeFinalMeta, final
>>> @final
... class Asset(with_metaclass(RuntimeFinalMeta, object)):
... pass
...
>>> class SubAsset(Asset):
... pass
...
Traceback (most recent call last):
TypeError: can't subclass final class 'Asset'
>>> from six import with_metaclass
>>> from basicco.runtime_final import RuntimeFinalMeta, final
>>> class Asset(with_metaclass(RuntimeFinalMeta, object)):
... @final
... def method(self):
... pass
...
>>> class SubAsset(Asset):
... def method(self):
... pass
Traceback (most recent call last):
TypeError: 'SubAsset' overrides final member 'method' defined by 'Asset'
>>> from six import with_metaclass
>>> from basicco.runtime_final import RuntimeFinalMeta, final
>>> class Asset(with_metaclass(RuntimeFinalMeta, object)):
... @property
... @final
... def prop(self):
... pass
...
>>> class SubAsset(Asset):
... @property
... def prop(self):
... pass
Traceback (most recent call last):
TypeError: 'SubAsset' overrides final member 'prop' defined by 'Asset'
Backport of the default Python 3 behavior of __ne__ behavior for Python 2.7.
>>> from six import with_metaclass
>>> from basicco.safe_not_equals import SafeNotEqualsMeta
>>> class Class(with_metaclass(SafeNotEqualsMeta, object)):
... pass
...
>>> obj_a = Class()
>>> obj_b = Class()
>>> assert (obj_a == obj_a) is not (obj_a != obj_a)
>>> assert (obj_b == obj_b) is not (obj_b != obj_b)
>>> assert (obj_a == obj_b) is not (obj_a != obj_b)
Decorator that prevents __repr__ methods from raising exceptions and return a default representation instead.
Easily define singleton sentinel values and their type (for type hinting).
Backport of the functionality of __set_name__ from PEP 487 to Python 2.7.
Backport of contextlib.suppress for Python 2.7. See null_context for an example usage.
Runtime type checking with support for import paths and type hints.
>>> from tippo import Mapping, Literal
>>> from itertools import chain
>>> from basicco.type_checking import is_instance
>>> class SubChain(chain):
... pass
...
>>> is_instance(3, int)
True
>>> is_instance(3, (chain, int))
True
>>> is_instance(3, ())
False
>>> is_instance(SubChain(), "itertools.chain")
True
>>> is_instance(chain(), "itertools.chain", subtypes=False)
True
>>> is_instance(SubChain(), "itertools.chain", subtypes=False)
False
>>> is_instance({"a": 1, "b": 2}, Mapping[str, int])
True
>>> is_instance("PRE", Literal["PRE", "POST"])
True
Iterator that yields unique values.