Source code for mincepy.builtins

"""Module for all the built in container and other types"""

from abc import ABCMeta, abstractmethod
import collections
from pathlib import Path
import shutil
from typing import BinaryIO, Optional
import uuid

from . import base_savable
from . import refs
from .utils import sync
from . import types

__all__ = ('List', 'LiveList', 'LiveRefList', 'RefList', 'Str', 'Dict', 'RefDict', 'LiveDict',
           'LiveRefDict', 'BaseFile', 'File')


class _UserType(base_savable.SimpleSavable, metaclass=ABCMeta):
    """Mixin for helping user types to be compatible with the historian.
    These typically have a .data member that stores the actual data (list, dict, str, etc)"""
    ATTRS = ('data',)
    data = None  # placeholder
    # This is an optional type for the data member.  See save_instance_state type for information
    # on when it might be useful
    DATA_TYPE = None  # type: type

    def save_instance_state(self, saver):
        # This is a convenient way of storing primitive data types directly as the state
        # rather than having to be a 'data' member of a dictionary.  This makes it much
        # easier to search these types
        if self.DATA_TYPE is not None and \
                issubclass(self.DATA_TYPE, saver.get_historian().primitives):
            self._on_save(saver)  # Call this here are we aren't going to call up the hierarchy
            return self.data

        return super().save_instance_state(saver)

    def load_instance_state(self, saved_state, loader):
        # See save_instance_state
        if self.DATA_TYPE is not None and \
                issubclass(self.DATA_TYPE, loader.get_historian().primitives):
            self._on_load(loader)  # Call this here are we aren't going to call up the hierarchy
            self.data = saved_state
        else:
            super(_UserType, self).load_instance_state(saved_state, loader)


class ObjProxy(_UserType):
    """A simple proxy for any object/data type which can also be a primitive"""
    TYPE_ID = uuid.UUID('d43c2db5-1e8c-428f-988f-8b198accde47')

    def __init__(self, data=None):
        super(ObjProxy, self).__init__()
        self.data = data

    def __call__(self):
        return self.data

    def assign(self, value):
        self.data = value


[docs]class Str(collections.UserString, _UserType): TYPE_ID = uuid.UUID('350f3634-4a6f-4d35-b229-71238ce9727d') DATA_TYPE = str def __init__(self, seq): collections.UserString.__init__(self, seq) _UserType.__init__(self)
class Reffer: """Mixin for types that want to be able to create references non-primitive types""" # pylint: disable=too-few-public-methods, no-self-use def _ref(self, obj): """Create a reference for a non-primitive, otherwise use the value""" if types.is_primitive(obj): return obj return self._create_ref(obj) def _deref(self, obj): """Dereference a non-primitive, otherwise return the value""" if types.is_primitive(obj): return obj return obj() def _create_ref(self, obj): """Create a reference for a given object""" return refs.ObjRef(obj) # region lists
[docs]class List(collections.UserList, _UserType): TYPE_ID = uuid.UUID('2b033f70-168f-4412-99ea-d1f131e3a25a') DATA_TYPE = list def __init__(self, initlist=None): collections.UserList.__init__(self, initlist) _UserType.__init__(self)
[docs]class RefList(collections.abc.MutableSequence, Reffer, _UserType): """A list that stores all entries as references in the database except primitives""" TYPE_ID = uuid.UUID('091efff5-136d-4ac2-bd59-28f50f151263') DATA_TYPE = list def __init__(self, init_list=None): super().__init__() init_list = init_list or [] self.data = self.DATA_TYPE(self._ref(item) for item in init_list) def __str__(self): return str(self.data) def __repr__(self): return repr(self.data) def __getitem__(self, item): return self._deref(self.data[item]) def __setitem__(self, key, value): self.data[key] = self._ref(value) def __delitem__(self, key): self.data.__delitem__(key) def __len__(self): return self.data.__len__()
[docs] def insert(self, index, value): self.data.insert(index, self._ref(value))
[docs]class LiveList(collections.abc.MutableSequence, _UserType): """A list that is always in sync with the database""" TYPE_ID = uuid.UUID('c83e6206-cd29-4fda-bf76-11fce1681cd9') def __init__(self, init_list=None): super(LiveList, self).__init__() init_list = init_list or [] self.data = RefList(self._create_proxy(item) for item in init_list) @sync() def __getitem__(self, item): proxy = self.data[item] # type: ObjProxy if self.is_saved(): proxy.sync() return proxy() @sync(save=True) def __setitem__(self, key, value): proxy = self.data[key] # type: ObjProxy proxy.assign(value) @sync(save=True) def __delitem__(self, key): proxy = self.data[key] # type: ObjProxy del self.data[key] if self._historian is not None: self._historian.delete(proxy) @sync() def __len__(self): return len(self.data)
[docs] @sync(save=True) def insert(self, index, value): proxy = self._create_proxy(value) self.data.insert(index, proxy)
[docs] @sync(save=True) def append(self, value): proxy = self._create_proxy(value) self.data.append(proxy)
def _create_proxy(self, obj): # pylint: disable=no-self-use return ObjProxy(obj)
[docs]class LiveRefList(Reffer, LiveList): """A live list that uses references to store objects""" TYPE_ID = uuid.UUID('98454806-c587-4fcc-a514-65fdefb0180d') @sync() def __getitem__(self, item): proxy = self.data[item] # type: ObjProxy if self.is_saved(): proxy.sync() ref = proxy() return self._deref(ref) @sync(save=True) def __setitem__(self, key, value): proxy = self.data[key] # type: ObjProxy proxy.assign(self._ref(value)) def _create_proxy(self, obj): return ObjProxy(self._ref(obj))
# endregion # region Dicts
[docs]class Dict(collections.UserDict, _UserType): TYPE_ID = uuid.UUID('a7584078-95b6-4e00-bb8a-b077852ca510') DATA_TYPE = dict def __init__(self, *args, **kwarg): collections.UserDict.__init__(self, *args, **kwarg) _UserType.__init__(self)
[docs]class RefDict(collections.MutableMapping, Reffer, _UserType): """A dictionary that stores all values as references in the database.""" TYPE_ID = uuid.UUID('c95f4c4e-766b-4dda-a43c-5fca4fd7bdd0') DATA_TYPE = dict def __init__(self, *args, **kwargs): super().__init__() initial = dict(*args, **kwargs) self.data = self.DATA_TYPE({key: self._ref(value) for key, value in initial.items()}) def __str__(self): return str(self.data) def __repr__(self): return repr(self.data) def __getitem__(self, item): return self._deref(self.data[item]) def __setitem__(self, key, value): self.data[key] = self._ref(value) def __delitem__(self, key): self.data.__delitem__(key) def __iter__(self): return self.data.__iter__() def __len__(self): return self.data.__len__()
[docs]class LiveDict(collections.MutableMapping, _UserType): TYPE_ID = uuid.UUID('740cc832-721c-4f85-9628-706257eb55b9') DATA_TYPE = RefDict def __init__(self, *args, **kwargs): super().__init__() initial = dict(*args, **kwargs) self.data = RefDict({key: self._create_proxy(value) for key, value in initial.items()}) @sync() def __getitem__(self, item): proxy = self.data[item] # type: ObjProxy proxy.sync() return proxy() @sync() def __setitem__(self, key, value): if key in self.data: # Can simply update the proxy proxy = self.data[key] # type: ObjProxy proxy.assign(value) if proxy.is_saved(): proxy.save() else: proxy = self._create_proxy(value) self.data[key] = proxy if self.is_saved(): self.save() # This will cause the proxy to be saved as well @sync(save=True) def __delitem__(self, key): proxy = self.data[key] del self.data[key] self._historian.delete(proxy) @sync() def __iter__(self): return self.data.__iter__() @sync() def __len__(self): return len(self.data) def _create_proxy(self, value): # pylint: disable=no-self-use return ObjProxy(value)
[docs]class LiveRefDict(Reffer, LiveDict): """A live dictionary that uses references to refer to contained objects""" TYPE_ID = uuid.UUID('16e7e814-8268-46e0-8d8e-6f34132366b9') def __init__(self, *args, **kwargs): super().__init__() initial = dict(*args, **kwargs) self.data = RefDict({key: self._create_proxy(value) for key, value in initial.items()}) @sync() def __getitem__(self, item): proxy = self.data[item] # type: ObjProxy proxy.sync() ref = proxy() return self._deref(ref) @sync() def __setitem__(self, key, value): if key in self.data: # Can simply update the proxy proxy = self.data[key] # type: ObjProxy proxy.assign(self._ref(value)) if proxy.is_saved(): proxy.save() else: proxy = self._create_proxy(value) self.data[key] = proxy if self.is_saved(): self.save() # This will cause the proxy to be saved as well def _create_proxy(self, value): return ObjProxy(self._ref(value))
# endregion
[docs]class File(base_savable.SimpleSavable, metaclass=ABCMeta): ATTRS = ('_filename', '_encoding') READ_SIZE = 256 # The number of bytes to read at a time def __init__(self, filename: str = None, encoding=None): super().__init__() self._filename = filename self._encoding = encoding @property def filename(self) -> Optional[str]: return self._filename @property def encoding(self) -> Optional[str]: return self._encoding
[docs] @abstractmethod def open(self, mode='r', **kwargs) -> BinaryIO: """Open returning a file like object that supports close() and read()"""
[docs] def from_disk(self, path): """Copy the contents of a disk file to this file""" with open(str(path), 'r', encoding=self.encoding) as disk_file: with self.open('w') as this: shutil.copyfileobj(disk_file, this)
[docs] def to_disk(self, folder: [str, Path]): """Copy the contents of this file to a file on disk in the given folder""" file_path = Path(str(folder)) / self.filename with open(str(file_path), 'w', encoding=self._encoding) as disk_file: with self.open('r') as this: shutil.copyfileobj(this, disk_file)
def write_text(self, text: str, encoding=None): encoding = encoding or self._encoding with self.open('w', encoding=encoding) as fileobj: fileobj.write(text)
[docs] def read_text(self, encoding=None) -> str: """Read the contents of the file as text. This function is named as to mirror pathlib.Path""" encoding = encoding or self._encoding with self.open('r', encoding=encoding) as fileobj: return fileobj.read()
def __str__(self): contents = [str(self._filename)] if self._encoding is not None: contents.append("({})".format(self._encoding)) return " ".join(contents) def __eq__(self, other) -> bool: """Compare the contents of two files If both files do not exist they are considered equal. """ if not isinstance(other, BaseFile) or self.filename != other.filename: return False try: with self.open() as my_file: try: with other.open() as other_file: while True: my_line = my_file.readline(self.READ_SIZE) other_line = other_file.readline(self.READ_SIZE) if my_line != other_line: return False if my_line == '' and other_line == '': return True except FileNotFoundError: return False except FileNotFoundError: # Our file doesn't exist, make sure the other doesn't either try: with other.open(): return False except FileNotFoundError: return True
[docs] def yield_hashables(self, hasher): """Hash the contents of the file""" try: with self.open('rb') as opened: while True: line = opened.read(self.READ_SIZE) if line == b'': return yield line except FileNotFoundError: yield from hasher.yield_hashables(None)
BaseFile = File HISTORIAN_TYPES = (Str, List, RefList, LiveList, LiveRefList, Dict, RefDict, LiveDict, LiveRefDict, ObjProxy)