Source code for timetree.frontend

import contextlib
import types
from functools import wraps
from weakref import WeakValueDictionary

from .backend.base import BaseVersion

__all__ = [
    'use_version',
    'use_backend',
    'use_proxy_version',
    'make_persistent',
    'get_proxy_version',
    'get_proxy_backend',
    'branch', 'commit',
]

_global_version = None


# Version-setting context managers
@contextlib.contextmanager
def use_version(version):
    global _global_version

    if not isinstance(version, BaseVersion):
        raise TypeError('not a valid Version')

    old_version = _global_version
    _global_version = version

    yield

    _global_version = old_version


def use_backend(backend):
    return use_version(backend.branch())


def use_proxy_version(proxy):
    return use_version(get_proxy_version(proxy))


# Marker class
class TimetreeProxy:
    __slots__ = ()


def make_persistent(klass):
    class KlassTimetreeProxy(klass, TimetreeProxy):
        __slots__ = ('_timetree_vnode',)

        def __new__(cls, *args,
                    timetree_vnode=None,
                    timetree_version=None,
                    timetree_backend=None,
                    **kwargs):
            global _global_version

            if \
                    timetree_vnode is not None or\
                    timetree_version is not None or\
                    timetree_backend is not None or\
                    _global_version is not None:
                return object.__new__(cls)

            return klass(*args, **kwargs)

        def __init__(self, *args,
                     timetree_vnode=None,
                     timetree_version=None,
                     timetree_backend=None,
                     **kwargs):
            if timetree_vnode is not None:
                object.__setattr__(self, '_timetree_vnode', timetree_vnode)
                version = timetree_vnode.version
                proxy_set = timetree_vnode.get('_timetree_proxy_set')
                assert version not in proxy_set
                proxy_set[version] = self
                return

            if timetree_version is None:
                # Get the version
                if timetree_backend is not None:
                    timetree_version = timetree_backend.branch()
                elif _global_version is not None:
                    timetree_version = _global_version
                else:
                    assert False, "No version to use; __new__ should check that"

            vnode = timetree_version.new_node()
            object.__setattr__(self, '_timetree_vnode', vnode)
            vnode.set('_timetree_proxy_class', self.__class__)
            vnode.set('_timetree_proxy_set', WeakValueDictionary({
                timetree_version: self,
            }))

            with use_version(vnode.version):
                super().__init__(*args, **kwargs)

        def __getattribute__(self, name):
            vnode = object.__getattribute__(self, '_timetree_vnode')
            try:
                result = vnode.get(name)
                if vnode.backend.is_vnode(result):
                    result = _vnode_to_proxy(result)
                return result
            except KeyError:
                result = super().__getattribute__(name)

                # Wrap instance methods
                if isinstance(result, types.MethodType) \
                        and result.__self__ is self:
                    fn = result
                    version = vnode.version

                    @wraps(fn)
                    def wrapped_fn(*args, **kwargs):
                        with use_version(version):
                            return fn(*args, **kwargs)

                    result = wrapped_fn
                return result

        def __setattr__(self, name, value):
            vnode = object.__getattribute__(self, '_timetree_vnode')

            # Handle data descriptors, which get priority
            for cls in self.__class__.__mro__:
                if name in cls.__dict__:
                    obj = cls.__dict__[name]
                    if hasattr(obj, '__set__'):
                        return obj.__set__(self, value)

            if isinstance(value, TimetreeProxy):
                o_vnode = object.__getattribute__(value, '_timetree_vnode')
                if vnode.backend.is_vnode(o_vnode):
                    value = o_vnode

            return vnode.set(name, value)

        def __delattr__(self, name):
            vnode = object.__getattribute__(self, '_timetree_vnode')

            # Handle data descriptors, which get priority
            for cls in self.__class__.__mro__:
                if name in cls.__dict__:
                    obj = cls.__dict__[name]
                    if hasattr(obj, '__delete__'):
                        return obj.__delete__(self)

            return vnode.delete(name)

    KlassTimetreeProxy.__name__ = klass.__name__ + 'TimetreeProxy'

    return KlassTimetreeProxy


def _vnode_to_proxy(vnode):
    result = vnode.get('_timetree_proxy_set').get(vnode.version, None)
    if result is not None:
        return result
    return vnode.get('_timetree_proxy_class')(timetree_vnode=vnode)


def _proxy_to_vnode(proxy):
    if not isinstance(proxy, TimetreeProxy):
        raise TypeError("proxy is not a TimetreeProxy")
    return object.__getattribute__(proxy, '_timetree_vnode')


# Access the version or the backend
def get_proxy_version(proxy):
    return _proxy_to_vnode(proxy).version


def get_proxy_backend(proxy):
    return _proxy_to_vnode(proxy).backend


[docs]def branch(*args): """ Creates a new branch from a list of proxy objects Takes either a single iterable, or many objects as arguments. If no arguments are given, creates a new branch and return the version object. If a single proxy object is given as the argument, return a new proxy to the object. If an iterable is given, or at least two arguments are given, return a tuple of proxies to the new vnodes. """ return _create_version(args, is_branch=True)
[docs]def commit(*args): """ Creates a new commit from a list of proxy objects Takes either a single iterable, or many objects as arguments. If no arguments are given, creates a new commit and return the version object. If a single proxy object is given as the argument, return a new proxy to the object. If an iterable is given, or at least two arguments are given, return a tuple of proxies to the new vnodes. """ return _create_version(args, is_commit=True)
def _create_version(args, *, is_branch=False, is_commit=False): """ Internal implementation of branch and commit """ global _global_version assert int(is_branch) + int(is_commit) == 1,\ "Exactly one of is_branch and is_commit should be true" backend = get_proxy_backend(args[0]) if args else _global_version.backend make_version = backend.branch if is_branch else backend.commit if not args: return make_version() if len(args) == 1 and not isinstance(args[0], TimetreeProxy): # We were given an iterator is_iterator = True if not hasattr(args[0], '__iter__'): raise TypeError("Only argument was neither a TimetreeProxy nor an iterator") args = tuple(args[0]) else: is_iterator = False _, vnodes = make_version(_proxy_to_vnode(proxy) for proxy in args) vnodes = (_vnode_to_proxy(vnode) for vnode in vnodes) if is_iterator: return list(vnodes) elif len(args) == 1: return next(vnodes) else: return tuple(vnodes)