Skip to main content

Bases and utilities for compatibility, features and validation.

Project description

https://github.com/brunonicko/basicco/workflows/MyPy/badge.svg https://github.com/brunonicko/basicco/workflows/Lint/badge.svg https://github.com/brunonicko/basicco/workflows/Tests/badge.svg https://readthedocs.org/projects/basicco/badge/?version=stable https://img.shields.io/github/license/brunonicko/basicco?color=light-green https://static.pepy.tech/personalized-badge/basicco?period=total&units=international_system&left_color=grey&right_color=brightgreen&left_text=Downloads https://img.shields.io/pypi/pyversions/basicco?color=light-green&style=flat

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:

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 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.

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...

caller_module

Retrieve the caller’s module name.

>>> 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.

When imported from Python 3, it simply redirects to the native contextvars module.

>>> 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.

>>> 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>"
>>> 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]"
>>> 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__.

This allows for calling super().__dir__() from a subclass to leverage the default implementation.

>>> 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.

>>> 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.

>>> 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.

>>> 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.

>>> 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).

>>> 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.

>>> 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.

>>> 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.

>>> 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.

>>> 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.

>>> import itertools
>>> from basicco.import_path import get_path, import_path
>>> get_path(itertools.chain)
'itertools.chain'
>>> import_path("itertools.chain")
<... 'itertools.chain'>
>>> 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).

>>> 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.

>>> 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.

>>> 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.

>>> 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.

>>> 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.

>>> from basicco.namespace import Namespace
>>> ns = Namespace({"bar": "foo"})
>>> ns.bar
'foo'
>>> 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.

>>> 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.

>>> 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).

>>> 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. Also provides a QualnamedMeta metaclass with a __qualname__ class property for Python 2.7.

recursive_repr

Decorator that prevents infinite recursion for __repr__ methods.

>>> 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 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'

safe_not_equals

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)

safe_repr

Decorator that prevents __repr__ methods from raising exceptions and return a default representation instead.

>>> 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).

>>> 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.

>>> 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.

>>> 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.

>>> from basicco.unique_iterator import unique_iterator
>>> list(unique_iterator([1, 2, 3, 3, 4, 4, 5]))
[1, 2, 3, 4, 5]

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

basicco-10.5.8.tar.gz (68.8 kB view hashes)

Uploaded Source

Built Distribution

basicco-10.5.8-py2.py3-none-any.whl (55.2 kB view hashes)

Uploaded Python 2 Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page