From 77bc77fe35905dddcc1b1ce8434de89322a28db6 Mon Sep 17 00:00:00 2001 From: dimitri-yatsenko Date: Sun, 20 Dec 2015 00:51:06 -0600 Subject: [PATCH 1/6] converted table_name to classproperty in user relations --- datajoint/user_relations.py | 52 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/datajoint/user_relations.py b/datajoint/user_relations.py index 4f5875bac..8f8e08d5e 100644 --- a/datajoint/user_relations.py +++ b/datajoint/user_relations.py @@ -2,14 +2,22 @@ Hosts the table tiers, user relations should be derived from. """ -import abc from .base_relation import BaseRelation from .autopopulate import AutoPopulate from .utils import from_camel_case from . import DataJointError -class Part(BaseRelation, metaclass=abc.ABCMeta): +class classproperty: + + def __init__(self, f): + self.f = f + + def __get__(self, obj, owner): + return self.f(owner) + + +class Part(BaseRelation): """ Inherit from this class if the table's values are details of an entry in another relation and if this table is populated by this relation. For example, the entries inheriting from @@ -17,44 +25,44 @@ class Part(BaseRelation, metaclass=abc.ABCMeta): Part relations are implemented as classes inside classes. """ - @property - def master(self): - if not hasattr(self, '_master'): + @classproperty + def master(cls): + if not hasattr(cls, '_master'): raise DataJointError( 'Part relations must be declared inside a base relation class') - return self._master + return cls._master @property def table_name(self): return self.master().table_name + '__' + from_camel_case(self.__class__.__name__) -class Manual(BaseRelation, metaclass=abc.ABCMeta): +class Manual(BaseRelation): """ Inherit from this class if the table's values are entered manually. """ - @property - def table_name(self): + @classproperty + def table_name(cls): """ :returns: the table name of the table formatted for mysql. """ - return from_camel_case(self.__class__.__name__) + return from_camel_case(cls.__name__) -class Lookup(BaseRelation, metaclass=abc.ABCMeta): +class Lookup(BaseRelation): """ Inherit from this class if the table's values are for lookup. This is currently equivalent to defining the table as Manual and serves semantic purposes only. """ - @property - def table_name(self): + @classproperty + def table_name(cls): """ :returns: the table name of the table formatted for mysql. """ - return '#' + from_camel_case(self.__class__.__name__) + return '#' + from_camel_case(cls.__name__) def _prepare(self): """ @@ -64,29 +72,29 @@ def _prepare(self): self.insert(self.contents, skip_duplicates=True) -class Imported(BaseRelation, AutoPopulate, metaclass=abc.ABCMeta): +class Imported(BaseRelation, AutoPopulate): """ Inherit from this class if the table's values are imported from external data sources. The inherited class must at least provide the function `_make_tuples`. """ - @property - def table_name(self): + @classproperty + def table_name(cls): """ :returns: the table name of the table formatted for mysql. """ - return "_" + from_camel_case(self.__class__.__name__) + return "_" + from_camel_case(cls.__name__) -class Computed(BaseRelation, AutoPopulate, metaclass=abc.ABCMeta): +class Computed(BaseRelation, AutoPopulate): """ Inherit from this class if the table's values are computed from other relations in the schema. The inherited class must at least provide the function `_make_tuples`. """ - @property - def table_name(self): + @classproperty + def table_name(cls): """ :returns: the table name of the table formatted for mysql. """ - return "__" + from_camel_case(self.__class__.__name__) + return "__" + from_camel_case(cls.__name__) From 4c40b0b19cea61a0c128870ab7ad524cd65a359b Mon Sep 17 00:00:00 2001 From: dimitri-yatsenko Date: Sun, 20 Dec 2015 01:17:38 -0600 Subject: [PATCH 2/6] made full_table_name a class property in UserRelation --- datajoint/schema.py | 19 +++++++++++-------- datajoint/user_relations.py | 27 ++++++++++++++++----------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/datajoint/schema.py b/datajoint/schema.py index 985e038b4..44be4c1a5 100644 --- a/datajoint/schema.py +++ b/datajoint/schema.py @@ -87,15 +87,18 @@ def process_relation_class(relation_class, context): process_relation_class(cls, context=self.context) - # Process subordinate relations - parts = list() - is_part = lambda x: inspect.isclass(x) and issubclass(x, Part) + # Process part relations + def is_part(x): + return inspect.isclass(x) and issubclass(x, Part) - for var, part in inspect.getmembers(cls, is_part): - parts.append(part) - part._master = cls - # TODO: look into local namespace for the subclasses - process_relation_class(part, context=dict(self.context, **{cls.__name__: cls})) + parts = list() + for part in dir(cls): + if part[0].isupper(): + part = getattr(cls, part) + if is_part(part): + parts.append(part) + part._master = cls + process_relation_class(part, context=dict(self.context, **{cls.__name__: cls})) # invoke Relation._prepare() on class and its part relations. cls()._prepare() diff --git a/datajoint/user_relations.py b/datajoint/user_relations.py index 8f8e08d5e..a70f253f6 100644 --- a/datajoint/user_relations.py +++ b/datajoint/user_relations.py @@ -17,27 +17,32 @@ def __get__(self, obj, owner): return self.f(owner) -class Part(BaseRelation): +class UserRelation(BaseRelation): + + @classproperty + def full_table_name(cls): + return r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name) + + +class Part(UserRelation): """ Inherit from this class if the table's values are details of an entry in another relation and if this table is populated by this relation. For example, the entries inheriting from dj.Part could be single entries of a matrix, while the parent table refers to the entire matrix. Part relations are implemented as classes inside classes. """ + _master = None @classproperty def master(cls): - if not hasattr(cls, '_master'): - raise DataJointError( - 'Part relations must be declared inside a base relation class') return cls._master - @property - def table_name(self): - return self.master().table_name + '__' + from_camel_case(self.__class__.__name__) + @classproperty + def table_name(cls): + return cls.master.table_name + '__' + from_camel_case(cls.__name__) -class Manual(BaseRelation): +class Manual(UserRelation): """ Inherit from this class if the table's values are entered manually. """ @@ -50,7 +55,7 @@ def table_name(cls): return from_camel_case(cls.__name__) -class Lookup(BaseRelation): +class Lookup(UserRelation): """ Inherit from this class if the table's values are for lookup. This is currently equivalent to defining the table as Manual and serves semantic @@ -72,7 +77,7 @@ def _prepare(self): self.insert(self.contents, skip_duplicates=True) -class Imported(BaseRelation, AutoPopulate): +class Imported(UserRelation, AutoPopulate): """ Inherit from this class if the table's values are imported from external data sources. The inherited class must at least provide the function `_make_tuples`. @@ -86,7 +91,7 @@ def table_name(cls): return "_" + from_camel_case(cls.__name__) -class Computed(BaseRelation, AutoPopulate): +class Computed(UserRelation, AutoPopulate): """ Inherit from this class if the table's values are computed from other relations in the schema. The inherited class must at least provide the function `_make_tuples`. From 4d1990a3fa65a11dfe14097290eea0c8157e9e85 Mon Sep 17 00:00:00 2001 From: dimitri-yatsenko Date: Sun, 20 Dec 2015 01:27:09 -0600 Subject: [PATCH 3/6] simplified ERD for UserRelations --- datajoint/erd.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/datajoint/erd.py b/datajoint/erd.py index fefbc8031..a0cbe4df1 100644 --- a/datajoint/erd.py +++ b/datajoint/erd.py @@ -21,34 +21,24 @@ import matplotlib.pyplot as plt from inspect import isabstract from .base_relation import BaseRelation +from .user_relations import UserRelation logger = logging.getLogger(__name__) -def get_concrete_descendants(cls): +def get_concrete_subclasses(cls): desc = [] child= cls.__subclasses__() for c in child: if not isabstract(c): desc.append(c) - desc.extend(get_concrete_descendants(c)) + desc.extend(get_concrete_subclasses(c)) return desc -def parse_base_relations(rels): - name_map = {} - for r in rels: - try: - name_map[r().full_table_name] = '{module}.{cls}'.format(module=r.__module__, cls=r.__name__) - except TypeError: - # skip if failed to instantiate BaseRelation derivative - pass - return name_map - - def get_table_relation_name_map(): - rels = get_concrete_descendants(BaseRelation) - return parse_base_relations(rels) + return {rel.full_table_name: '{module}.{cls}'.format(module=rel.__module__, cls=rel.__name__) + for rel in get_concrete_subclasses(UserRelation)} class ERD(DiGraph): From b184ade2214bd035a55efef598ca1ba5d5d39e04 Mon Sep 17 00:00:00 2001 From: dimitri-yatsenko Date: Sun, 20 Dec 2015 01:43:34 -0600 Subject: [PATCH 4/6] cleanup in erd.py --- datajoint/erd.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/datajoint/erd.py b/datajoint/erd.py index a0cbe4df1..c0ca03143 100644 --- a/datajoint/erd.py +++ b/datajoint/erd.py @@ -36,11 +36,6 @@ def get_concrete_subclasses(cls): return desc -def get_table_relation_name_map(): - return {rel.full_table_name: '{module}.{cls}'.format(module=rel.__module__, cls=rel.__name__) - for rel in get_concrete_subclasses(UserRelation)} - - class ERD(DiGraph): """ A directed graph representing dependencies between Relations within and across @@ -55,15 +50,14 @@ def node_labels(self): """ :return: dictionary of key : label pairs for plotting """ - name_map = get_table_relation_name_map() + name_map = {rel.full_table_name: '{module}.{cls}'.format(module=rel.__module__, cls=rel.__name__) + for rel in get_concrete_subclasses(UserRelation)} return {k: self.get_label(k, name_map) for k in self.nodes()} def get_label(self, node, name_map=None): label = self.node[node].get('label', '') if label.strip(): return label - - # it's not efficient to recreate name-map on every call! if name_map is not None and node in name_map: return name_map[node] # no other name exists, so just use full table now From d17a97da390ae8b4640c829b21ef2de564d5e8ca Mon Sep 17 00:00:00 2001 From: dimitri-yatsenko Date: Sun, 20 Dec 2015 01:52:20 -0600 Subject: [PATCH 5/6] fixed issue #178 --- datajoint/erd.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/datajoint/erd.py b/datajoint/erd.py index c0ca03143..14858d530 100644 --- a/datajoint/erd.py +++ b/datajoint/erd.py @@ -4,14 +4,11 @@ import logging from collections import defaultdict -import pyparsing as pp import networkx as nx from networkx import DiGraph from functools import cmp_to_key import operator -from collections import OrderedDict - # use pygraphviz if available try: from networkx import pygraphviz_layout @@ -20,8 +17,7 @@ import matplotlib.pyplot as plt from inspect import isabstract -from .base_relation import BaseRelation -from .user_relations import UserRelation +from .user_relations import UserRelation, Part logger = logging.getLogger(__name__) @@ -50,8 +46,18 @@ def node_labels(self): """ :return: dictionary of key : label pairs for plotting """ - name_map = {rel.full_table_name: '{module}.{cls}'.format(module=rel.__module__, cls=rel.__name__) - for rel in get_concrete_subclasses(UserRelation)} + def full_class_name(user_class): + if issubclass(user_class, Part): + return '{module}.{master}.{cls}'.format( + module=user_class.__module__, + master=user_class.master.__name__, + cls=user_class.__name__) + else: + return '{module}.{cls}'.format( + module=user_class.__module__, + cls=user_class.__name__) + + name_map = {rel.full_table_name: full_class_name(rel) for rel in get_concrete_subclasses(UserRelation)} return {k: self.get_label(k, name_map) for k in self.nodes()} def get_label(self, node, name_map=None): From 6c5fab5e1ff1b6c59aa7c5a60a34118be9f435a7 Mon Sep 17 00:00:00 2001 From: dimitri-yatsenko Date: Sun, 20 Dec 2015 02:44:06 -0600 Subject: [PATCH 6/6] minor cleanup --- datajoint/user_relations.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/datajoint/user_relations.py b/datajoint/user_relations.py index a70f253f6..9ffe75045 100644 --- a/datajoint/user_relations.py +++ b/datajoint/user_relations.py @@ -18,6 +18,17 @@ def __get__(self, obj, owner): class UserRelation(BaseRelation): + """ + A subclass of UserRelation defines is a dedicated class interfacing a base relation. + UserRelation is initialized by the decorator generated by schema(). + """ + _connection = None + _context = None + _heading = None + + @classproperty + def connection(cls): + return cls._connection @classproperty def full_table_name(cls):