Basicco
.. image:: https://github.com/brunonicko/basicco/workflows/MyPy/badge.svg
:target: https://github.com/brunonicko/basicco/actions?query=workflow%3AMyPy
.. image:: https://github.com/brunonicko/basicco/workflows/Lint/badge.svg
:target: https://github.com/brunonicko/basicco/actions?query=workflow%3ALint
.. image:: https://github.com/brunonicko/basicco/workflows/Tests/badge.svg
:target: https://github.com/brunonicko/basicco/actions?query=workflow%3ATests
.. image:: https://readthedocs.org/projects/basicco/badge/?version=stable
:target: https://basicco.readthedocs.io/en/stable/
.. image:: https://img.shields.io/github/license/brunonicko/basicco?color=light-green
:target: https://github.com/brunonicko/basicco/blob/master/LICENSE
.. image:: https://static.pepy.tech/personalized-badge/basicco?period=total&units=international_system&left_color=grey&right_color=brightgreen&left_text=Downloads
:target: https://pepy.tech/project/basicco
.. image:: https://img.shields.io/pypi/pyversions/basicco?color=light-green&style=flat
:target: https://pypi.org/project/basicco/
Overview
Base Classes
_ and Utilities
_ for compatibility, features and validation.
Motivation
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.
Base Classes
CompatBase
^^^^^^^^^^
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 <https://docs.python.org/3/library/abc.html#abc.abstractproperty>
:
Better abstractmethod
decorator support for property-like descriptors.
See also abstract_class
.PEP 487 <https://peps.python.org/pep-0487/>
:
Support for __init_subclass__
and __set_name__
.
See also init_subclass
and set_name
_.object.__dir__ <https://docs.python.org/3/reference/datamodel.html#object.__dir__>
:
Base __dir__
method.
See also default_dir
.__eq__ override <https://docs.python.org/3/reference/datamodel.html#object.__hash__>
:
Overriding __eq__
will set __hash__
to None. See also implicit_hash
.PEP 307 <https://peps.python.org/pep-0307/>
:
Support for pickling objects with __slots__
.
See also obj_state
.PEP 3155 <https://peps.python.org/pep-03155/>
:
Qualified name __qualname__
for nested classes.
See also qualname
.__ne__ behavior <https://docs.python.org/3.0/whatsnew/3.0.html#operators-and-special-methods>
:
By default, __ne__
should negate the result of __eq__
.
See also safe_not_equals
PEP 0560 <https://peps.python.org/pep-0560/>
:
Better handling of Generic classes.
See also tippo <https://github.com/brunonicko/tippo#generic-fixes>
.
Base
^^^^
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
.
SlottedBase
^^^^^^^^^^^
The SlottedBase
class and the SlottedBaseMeta
metaclass offer all features from
Base
and BaseMeta
plus implicit __slots__
declaration.
See slotted <https://github.com/brunonicko/slotted>
_ for more information.
Utilities
Apart from the features integrated into the base classes, basicco
provides many
general utility modules.
abstract_class
^^^^^^^^^^^^^^
Better support for
abstract classes <https://docs.python.org/3/library/abc.html#abc.abstractmethod>
_.
Provides abstract decorators that can be used directly on methods but also on
property getters, classmethods, and staticmethods (even in Python 2.7).
.. code:: python
>>> 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...
caller_module
^^^^^^^^^^^^^
Retrieve the caller's module name.
.. code:: python
>>> from basicco.caller_module import caller_module
>>> def do_something():
... return "I was called by {}".format(caller_module())
...
>>> do_something()
'I was called by __main__'
context_vars
^^^^^^^^^^^^
Backport of the contextvars
module for Python 2.7, based on
MagicStack/contextvars <https://github.com/MagicStack/contextvars>
_.
When imported from Python 3, it simply redirects to the native
contextvars <https://docs.python.org/3/library/contextvars.html>
_ module.
.. code:: python
>>> from basicco.context_vars import ContextVar
>>> my_var = ContextVar("my_var", default="bar")
>>> token = my_var.set("foo")
>>> my_var.get()
'foo'
>>> my_var.reset(token)
>>> my_var.get()
'bar'
custom_repr
^^^^^^^^^^^
Custom representation functions for mappings, items, and iterables.
.. code:: python
>>> from basicco.custom_repr import mapping_repr
>>> dct = {"a": 1, "b": 2}
>>> mapping_repr(
... dct,
... prefix="<",
... suffix=">",
... template="{key}={value}",
... sorting=True
... )
"<'a'=1, 'b'=2>"
.. code:: python
>>> from basicco.custom_repr import mapping_repr
>>> items = [("a", 1), ("b", 2)]
>>> mapping_repr(
... items,
... prefix="[", suffix="]",
... template=(lambda i, key, value: key + " -> " + value),
... )
"['a' -> 1, 'b' -> 2]"
.. code:: python
>>> from basicco.custom_repr import iterable_repr
>>> tup = ("a", "b", "c", 1, 2, 3)
>>> iterable_repr(tup, prefix="<", suffix=">", value_repr=str)
'<a, b, c, 1, 2, 3>'
default_dir
^^^^^^^^^^^
Backport of Python 3's implementation of
object.__dir__ <https://docs.python.org/3/reference/datamodel.html#object.__dir__>
_.
This allows for calling super().__dir__()
from a subclass to leverage the default
implementation.
.. code:: python
>>> from six import with_metaclass
>>> from basicco.default_dir import DefaultDir
>>> class Class(DefaultDir):
... def __dir__(self):
... return super(Class, self).__dir__()
...
>>> obj = Class()
>>> dir(obj)
[...]
descriptors
^^^^^^^^^^^
Configurable descriptors.
.. code:: python
>>> 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'
dynamic_class
^^^^^^^^^^^^^
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.
.. code:: python
>>> from basicco import Base
>>> from basicco.dynamic_class import make_cls
>>> class MyClass(object):
... DynClass = make_cls("MyClass.DynClass", bases=(Base,), dct={"foo": "bar"})
...
>>> repr(MyClass.DynClass)
"<class '__main__.MyClass.DynClass'>"
dynamic_code
^^^^^^^^^^^^
Generate debuggable code on the fly that supports line numbers on tracebacks.
.. code:: python
>>> 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'
explicit_hash
^^^^^^^^^^^^^
Metaclass that forces __hash__
to be declared whenever __eq__
is declared.
.. code:: python
>>> from six import with_metaclass
>>> from basicco.explicit_hash import ExplicitHashMeta
>>> class Asset(with_metaclass(ExplicitHashMeta, object)):
... def __eq__(self, other):
... pass
...
Traceback (most recent call last):
TypeError: declared '__eq__' in 'Asset' but didn't declare '__hash__'
fabricate_value
^^^^^^^^^^^^^^^
Run a value through a callable factory (or None).
.. code:: python
>>> from basicco.fabricate_value import fabricate_value
>>> fabricate_value(None, 3) # no factory, value passthrough
3
>>> fabricate_value(str, 3) # callable factory
'3'
>>> fabricate_value("str", 3) # use an import path
'3'
>>> fabricate_value(int) # no input value, just the factory itself
0
func_tools
^^^^^^^^^^
Backport of functools.cache
, functools.lru_cache
, and functools.update_wrapper
for Python 2.7.
.. code:: python
>>> from basicco.func_tools import cache
>>> @cache
... def calculate(a, b):
... print("calculating...")
... return a ** b
...
>>> calculate(2, 2)
calculating...
4
>>> calculate(2, 2)
4
get_mro
^^^^^^^
Get consistent MRO amongst different python versions. This works even with generic
classes in Python 2.7.
.. code:: python
>>> 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']
hash_cache_wrapper
^^^^^^^^^^^^^^^^^^
An integer subclass that pickles/copies as None. This can be used to avoid serializing
a cached hash value.
.. code:: python
>>> from copy import copy
>>> from basicco.hash_cache_wrapper import HashCacheWrapper
>>> hash_cache = HashCacheWrapper(12345)
>>> print(hash_cache)
12345
>>> print(copy(hash_cache))
None
implicit_hash
^^^^^^^^^^^^^
Metaclass that forces __hash__
to None when __eq__
is declared.
This is a backport of the default behavior in Python 3.
.. code:: python
>>> from six import with_metaclass
>>> from basicco.implicit_hash import ImplicitHashMeta
>>> class Asset(with_metaclass(ImplicitHashMeta, object)):
... def __eq__(self, other):
... pass
...
>>> Asset.__hash__ is None
True
import_path
^^^^^^^^^^^
Generate importable dot paths and import from them.
.. code:: python
>>> import itertools
>>> from basicco.import_path import get_path, import_path
>>> get_path(itertools.chain)
'itertools.chain'
>>> import_path("itertools.chain")
<... 'itertools.chain'>
.. code:: python
>>> from basicco.import_path import extract_generic_paths
>>> extract_generic_paths("Tuple[int, str]")
('Tuple', ('int', 'str'))
init_subclass
^^^^^^^^^^^^^
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).
.. code:: python
>>> from basicco.init_subclass import InitSubclass
>>> class Foo(InitSubclass):
... def __init_subclass__(cls, foo=None, **kwargs):
... cls.foo = foo
...
>>> class Bar(Foo):
... __kwargs__ = {"foo": "bar"} # you can specify cls kwargs on py2 like this
...
>>> Bar.foo
'bar'
lazy_tuple
^^^^^^^^^^
Lazily-evaluated tuple-like structure.
.. code:: python
>>> from basicco.lazy_tuple import LazyTuple
>>> def expensive_generator():
... for i in range(100):
... yield i
...
>>> lazy_tuple = LazyTuple(expensive_generator())
>>> lazy_tuple[4]
4
locked_class
^^^^^^^^^^^^
Prevents changing public class attributes.
.. code:: python
>>> from six import with_metaclass
>>> from basicco.locked_class import LockedClassMeta
>>> class Foo(with_metaclass(LockedClassMeta, object)):
... bar = "foo"
...
>>> Foo.bar = "bar"
Traceback (most recent call last):
AttributeError: can't set read-only class attribute 'bar'
mangling
^^^^^^^^
Functions to mangle/unmangle/extract private names.
.. code:: python
>>> from basicco.mangling import mangle, unmangle, extract
>>> mangle("__member", "Foo")
'_Foo__member'
>>> unmangle("_Foo__member", "Foo")
'__member'
>>> extract("_Foo__member")
('__member', 'Foo')
mapping_proxy
^^^^^^^^^^^^^
Mapping Proxy type (read-only dictionary) for older Python versions.
.. code:: python
>>> from basicco.mapping_proxy import MappingProxyType
>>> internal_dict = {"foo": "bar"}
>>> proxy_dict = MappingProxyType(internal_dict)
>>> proxy_dict["foo"]
'bar'
namespace
^^^^^^^^^
Wraps a dictionary/mapping and provides attribute-style access to it.
.. code:: python
>>> from basicco.namespace import Namespace
>>> ns = Namespace({"bar": "foo"})
>>> ns.bar
'foo'
.. code:: python
>>> from basicco.namespace import MutableNamespace
>>> ns = MutableNamespace({"bar": "foo"})
>>> ns.foo = "bar"
>>> ns.foo
'bar'
>>> ns.bar
'foo'
Also provides a NamespacedMeta
metaclass that adds a __namespace
protected class
attribute that is unique to each class.
.. code:: python
>>> 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'
null_context
^^^^^^^^^^^^
Backport of contextlib.nullcontext
for Python 2.7.
.. code:: python
>>> 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
...
obj_state
^^^^^^^^^
Get/update the state of an object, slotted or not (works even in Python 2.7).
.. code:: python
>>> from basicco.obj_state import get_state
>>> class Slotted(object):
... __slots__ = ("foo", "bar")
... def __init__(self, foo, bar):
... self.foo = foo
... self.bar = bar
...
>>> slotted = Slotted("a", "b")
>>> sorted(get_state(slotted).items())
[('bar', 'b'), ('foo', 'a')]
Also provides a ReducibleMeta
metaclass that allows for pickling instances of slotted
classes in Python 2.7.
qualname
^^^^^^^^
Python 2.7 compatible way of getting the qualified name. Based on
wbolster/qualname <https://github.com/wbolster/qualname>
_.
Also provides a QualnamedMeta
metaclass with a __qualname__
class property for
Python 2.7.
recursive_repr
^^^^^^^^^^^^^^
Decorator that prevents infinite recursion for __repr__
methods.
.. code:: python
>>> from basicco.recursive_repr import recursive_repr
>>> class MyClass(object):
...
... @recursive_repr
... def __repr__(self):
... return "MyClass<{!r}>".format(self)
...
>>> my_obj = MyClass()
>>> repr(my_obj)
'MyClass<...>'
runtime_final
^^^^^^^^^^^^^
Runtime-checked version of the
typing.final <https://docs.python.org/3/library/typing.html#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:
.. code:: python
>>> 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'
.. code:: python
>>> 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'
.. code:: python
>>> 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'
safe_not_equals
^^^^^^^^^^^^^^^
Backport of the default Python 3 behavior of __ne__
behavior for Python 2.7.
.. code:: python
>>> 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)
safe_repr
^^^^^^^^^
Decorator that prevents __repr__
methods from raising exceptions and return a default
representation instead.
.. code:: python
>>> from basicco.safe_repr import safe_repr
>>> class Class(object):
... @safe_repr
... def __repr__(self):
... raise RuntimeError("oh oh")
...
>>> obj = Class()
>>> repr(obj)
"<__main__.Class object at ...; repr failed due to 'RuntimeError: oh oh'>"
sentinel
^^^^^^^^
Easily define singleton sentinel values and their type (for type hinting).
.. code:: python
>>> from basicco.sentinel import SentinelType
>>> class MissingType(SentinelType):
... def __repr__(self):
... return "MISSING"
...
>>> MISSING = MissingType()
>>> MISSING
MISSING
>>> MissingType() is MISSING
True
>>> isinstance(MISSING, MissingType)
True
set_name
^^^^^^^^
Backport of the functionality of __set_name__
from PEP 487 to Python 2.7.
.. code:: python
>>> from basicco.set_name import SetName
>>> class Attribute(object):
... def __set_name__(self, owner, name):
... self.owner = owner
... self.name = name
...
>>> class Collection(SetName):
... foo = Attribute()
...
>>> Collection.foo.owner is Collection
True
>>> Collection.foo.name
'foo'
suppress_exception
^^^^^^^^^^^^^^^^^^
Backport of contextlib.suppress
for Python 2.7.
See null_context
_ for an example usage.
type_checking
^^^^^^^^^^^^^
Runtime type checking with support for import paths and type hints.
.. code:: python
>>> 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
unique_iterator
^^^^^^^^^^^^^^^
Iterator that yields unique values.
.. code:: python
>>> from basicco.unique_iterator import unique_iterator
>>> list(unique_iterator([1, 2, 3, 3, 4, 4, 5]))
[1, 2, 3, 4, 5]