Revision aeb0ab472296c0298c2b007c30af2705a75a89f8 authored by ST John on 18 June 2019, 09:46:26 UTC, committed by ST John on 18 June 2019, 09:48:10 UTC
1 parent 4ad6260
Raw File
parentable.py
# Copyright 2017 Artem Artemev @awav
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import abc
import uuid

from .. import misc


class Parentable:
    """
    Very simple class for organizing GPflow objects in a tree.
    Each node contains a reference to the its parent. Links to children
    established implicitly via python object attributes.

    The Parentable class stores static variable which is used for assigning unique
    prefix name identification for each newly created object inherited from Parentable.

    Name is not required at `Parentable` object creation.
    But in this case, unique index identifier will be attached to the class name.

    :param name: String name.
    """

    __global_index = 0

    def __init__(self, name=None):
        self._parent = None
        index = None
        if name is None:
            name = self._define_name(name)
            index = self._gen_index()
        self._name = name
        self._index = index

    @property
    def root(self):
        """
        Top of the parentable tree.
        :return: Reference to top parentable object.
        """
        if self._parent is None:
            return self
        return self._parent.root

    @property
    def parent(self):
        """
        Parent for this node.
        :return: Reference to parent object.
        """
        if self._parent is None:
            return self
        return self._parent

    @property
    def name(self):
        """
        The name assigned to node at creation time. It can be referred also
        as original name.

        :return: Given name.
        """
        return self._name

    @property
    def pathname(self):
        """
        Path name is a recursive representation parent path name plus the name
        which was assigned to this object by its parent. In other words, it is
        stack of parent name where top is always parent's original name:
        `parent.pathname + parent.childname` and stop condition is root's
        name.

        For example, the pathname of an instance with the two parents may look
        like `parent0/parent1/childname_at_parent1`.
        Top parent's name equals its original name `parent0.name == parent0.pathname`.
        """
        if self.parent is self:
            return self.name
        parent = self._parent
        return misc.tensor_name(parent.pathname, parent.childname(self))
    
    @property
    def index(self):
        return self._index

    @abc.abstractproperty
    def _children(self):
        """
        Abstract property for getting pairs of names and children, respectively.

        :return: Dictionary where keys are names and values are Parentable objects.
        """
        pass

    @abc.abstractmethod
    def _store_child(self, name, child):
        """
        Abstract method for saving association between child and its name in
        parent's specific storage.
        """
        pass


    @abc.abstractmethod
    def _remove_child(self, name, child):
        """
        Abstract method for removing an association between child and its name in
        parent's specific storage.
        """
        pass

    @property
    def _descendants(self):
        """
        Scans full list of node descendants.

        :return: Generator of nodes.
        """
        children = self._children
        if children is not None:
            for child in children.values():
                yield from child._descendants
                yield child
    
    def _contains(self, node):
        """
        Checks either node already exist somewhere among descendants.

        :return: Boolean value.
        """
        return node in list(self._descendants)

    def childname(self, child):
        if not isinstance(child, Parentable):
            raise ValueError('Parentable object expected, {child} passed.'.format(child=child))
        name = [ch[0] for ch in self._children.items() if ch[1] is child]
        if not name:
            raise KeyError('Parent {parent} does not have child {child}'.format(parent=self, child=child))
        return name[0]

    def _set_child(self, name, child):
        """
        Set child.

        :param name: Child name.
        :param child: Parentable object.
        """
        if not isinstance(child, Parentable):
            raise ValueError('Parentable child object expected, not {child}'.format(child=child))
        child._set_parent(self)
        self._store_child(name, child)
    
    def _unset_child(self, name, child):
        """
        Untie child from parent.

        :param name: Child name.
        :param child: Parentable object.
        """
        if name not in self._children or self._children[name] is not child:
            msg = 'Child {child} with name "{name}" is not found'
            raise ValueError(msg.format(child=child, name=name))
        child._set_parent(None)
        self._remove_child(name, child)


    def _set_parent(self, parent=None):
        """
        Set parent.

        :param parent: Parentable object.
        :raises ValueError: Self-reference object passed.
        :raises ValueError: Non-Parentable object passed.
        """
        if parent is not None:
            if not isinstance(parent, Parentable):
                raise ValueError('Parent object must implement Parentable interface.')
            if parent is self or parent._contains(self):
                raise ValueError('Self references are not allowed.')
        self._parent = parent if parent is not None else None
    
    def reset_name(self, name=None):
        self._name = self._define_name(name=name)
        self._index = self._gen_index()

    def _define_name(self, name=None):
        if name is not None:
            return name
        cls_name = self.__class__.__name__
        return cls_name

    def _gen_index(self):
        internal_index = Parentable._read_index()
        uuid_index = str(uuid.uuid4())[:8]
        pattern = '{uuid_index}-{internal_index}'
        return pattern.format(uuid_index=uuid_index, internal_index=internal_index)

    @classmethod
    def _read_index(cls):
        """
        Reads index number and increments it. No thread-safety guarantees.

        # TODO(@awav): index can grow indefinetly,
        # check boundaries or make another solution.
        """
        index = cls.__global_index
        cls.__global_index += 1
        return index
back to top