Skip to content

Commit cd241a8

Browse files
authored
Merge pull request #230 from dimitri-yatsenko/master
Improved ERD
2 parents 60294c9 + 54f0a15 commit cd241a8

6 files changed

Lines changed: 77 additions & 50 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,9 @@
33
* Fixed issue #223: `insert` can insert relations without fetching
44
* ERD() now takes the `context` argument, which specifies in which context to look for classes. The default is taken from the argument (schema or relation).
55
* ERD.draw() no longer has the `prefix` argument: class names are shown as found in the context.
6+
7+
### 0.3.3
8+
* Suppressed warnings (redirected them to logging). Previoiusly, scipy would throw warnings in ERD, for example.
9+
* Added ERD.from_sequence as a shortcut to combining the ERDs of multiple sources
10+
* ERD() no longer text the context argument.
11+
* ERD.draw() now takes an optional context argument. By default uses the caller's locals.

‎datajoint/__init__.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
import os
1717

1818
__author__ = "Dimitri Yatsenko, Edgar Walker, and Fabian Sinz at Baylor College of Medicine"
19-
__version__ = "0.3.2"
20-
__date__ = "July 18, 2016"
19+
__version__ = "0.3.3"
20+
__date__ = "July 20, 2016"
2121
__all__ = ['__author__', '__version__',
2222
'config', 'conn', 'kill', 'BaseRelation',
2323
'Connection', 'Heading', 'FreeRelation', 'Not', 'schema',
@@ -27,6 +27,8 @@
2727

2828
print('DataJoint', __version__, '('+__date__+')')
2929

30+
logging.captureWarnings(True)
31+
3032

3133
class key:
3234
"""

‎datajoint/base_relation.py‎

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ def show_definition(self):
355355
do_include = False
356356
if attributes_thus_far.issuperset(primary_key):
357357
parents.pop(parent)
358-
definition += '-> ' + lookup_class_name(self.context, parent) + '\n'
358+
definition += '-> ' + lookup_class_name(parent, self.context) + '\n'
359359
if do_include:
360360
definition += '%-20s : %-28s # %s\n' % (
361361
attr.name if attr.default is None else '%s = %s' % (attr.name, attr.default),
@@ -364,33 +364,39 @@ def show_definition(self):
364364
return definition
365365

366366

367-
def lookup_class_name(context, name, levels=4):
367+
def lookup_class_name(name, context, depth=3):
368368
"""
369369
given the name of another table in the form `database`.`table_name`, find its class in the context
370-
:param context: dictionary representing the namespace
371370
:param name: `database`.`table_name`
372-
:param levels: the limit on how deep to go into imported modules, helps avoid infinite recursion.
373-
:return: class name found in the context.
371+
:param context: dictionary representing the namespace
372+
:param depth: search depth into imported modules, helps avoid infinite recursion.
373+
:return: class name found in the context or original name if not found
374374
"""
375-
for member_name, member in context.items():
376-
if inspect.isclass(member) and issubclass(member, BaseRelation):
377-
if member.full_table_name == name:
378-
return member_name
379-
# check part tables
380-
try:
381-
parts = member._ordered_class_members
382-
except AttributeError:
383-
pass # not a UserRelation
384-
else:
385-
for part in parts:
386-
part = getattr(member, part)
387-
if inspect.isclass(part) and issubclass(part, BaseRelation) and part.full_table_name == name:
388-
return member_name + '.' + part.__name__
389-
elif levels and inspect.ismodule(member) and member.__name__ != 'datajoint':
390-
candidate_name = lookup_class_name(dict(inspect.getmembers(member)), name, levels-1)
391-
if candidate_name != name:
392-
return member_name + '.' + candidate_name
393-
return name
375+
# breadth-first search
376+
nodes = [dict(context=context, context_name='', depth=depth)]
377+
while nodes:
378+
node = nodes.pop(0)
379+
for member_name, member in node['context'].items():
380+
if inspect.isclass(member) and issubclass(member, BaseRelation):
381+
if member.full_table_name == name: # found it!
382+
return '.'.join([node['context_name'], member_name]).lstrip('.')
383+
try: # look for part tables
384+
parts = member._ordered_class_members
385+
except AttributeError:
386+
pass # not a UserRelation -- cannot have part tables.
387+
else:
388+
for part in (getattr(member, p) for p in parts):
389+
if inspect.isclass(part) and issubclass(part, BaseRelation) and part.full_table_name == name:
390+
return '.'.join([node['context_name'], member_name, part.__name__]).lstrip('.')
391+
elif node['depth'] > 0 and inspect.ismodule(member) and member.__name__ != 'datajoint':
392+
try:
393+
nodes.append(
394+
dict(context=dict(inspect.getmembers(member)),
395+
context_name=node['context_name'] + '.' + member_name,
396+
depth=node['depth']-1))
397+
except ImportError:
398+
pass # could not import, so do not attempt
399+
return name # return original name if class is not found
394400

395401

396402
class FreeRelation(BaseRelation):

‎datajoint/connection.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def __init__(self, host, user, passwd, init_fun=None):
6262
port = config['database.port']
6363
self.conn_info = dict(host=host, port=port, user=user, passwd=passwd)
6464
self.init_fun = init_fun
65+
print("Connecting {user}@{host}:{port}".format(**self.conn_info))
6566
self.connect()
6667
if self.is_connected:
6768
logger.info("Connected {user}@{host}:{port}".format(**self.conn_info))

‎datajoint/erd.py‎

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import networkx as nx
22
import re
33
import numpy as np
4+
import functools
45
from scipy.optimize import basinhopping
56
import itertools
6-
import inspect
77
from . import Manual, Imported, Computed, Lookup, Part, DataJointError
88
from .base_relation import lookup_class_name
99

10+
1011
user_relation_classes = (Manual, Lookup, Computed, Imported, Part)
1112

1213

@@ -23,10 +24,8 @@ class ERD(nx.DiGraph):
2324
Entity relationship diagram.
2425
2526
Usage:
26-
>>> erd = Erd(source, context=None)
27+
>>> erd = Erd(source)
2728
source can be a base relation object, a base relation class, a schema, or a module that has a schema.
28-
context is the namespace in which to search for classes. If None (default), looks
29-
in source.context or source.schema.context
3029
3130
>>> erd.draw()
3231
draws the diagram using pyplot
@@ -39,12 +38,11 @@ class ERD(nx.DiGraph):
3938
Note that erd + 1 - 1 may differ from erd - 1 + 1 and so forth.
4039
Only those tables that are loaded in the connection object are displayed
4140
"""
42-
def __init__(self, source, context=None):
41+
def __init__(self, source):
4342

4443
if isinstance(source, ERD):
4544
# copy constructor
4645
self.nodes_to_show = set(source.nodes_to_show)
47-
self._context = source._context
4846
super().__init__(source)
4947
return
5048

@@ -57,18 +55,6 @@ def __init__(self, source, context=None):
5755
except AttributeError:
5856
raise DataJointError('Could not find database connection in %s' % repr(source[0]))
5957

60-
if context is None:
61-
# find context in the source
62-
try:
63-
context = source.context
64-
except AttributeError:
65-
try:
66-
context = source.schema.context
67-
except AttributeError:
68-
raise DataJointError('Could not find context in %s' % repr(source[0]))
69-
70-
self._context = context
71-
7258
# initialize graph from dependencies
7359
connection.dependencies.load()
7460
super().__init__(connection.dependencies)
@@ -89,6 +75,15 @@ def __init__(self, source, context=None):
8975
if node.startswith('`%s`' % database):
9076
self.nodes_to_show.add(node)
9177

78+
@classmethod
79+
def from_sequence(cls, sequence):
80+
"""
81+
The join ERD for all objects in sequence
82+
:param sequence: a sequence (e.g. list, tuple)
83+
:return: ERD(arg1) + ... + ERD(argn)
84+
"""
85+
return functools.reduce(lambda x, y: x+y, map(ERD, sequence))
86+
9287
def __add__(self, arg):
9388
"""
9489
:param arg: either another ERD or a positive integer.
@@ -131,25 +126,42 @@ def __mul__(self, arg):
131126
self.nodes_to_show.intersection_update(arg.nodes_to_show)
132127
return self
133128

134-
def _make_graph(self):
129+
def _make_graph(self, context):
135130
"""
136131
Make the self.graph - a graph object ready for drawing
137132
"""
138133
graph = nx.DiGraph(self).subgraph(self.nodes_to_show)
139134
nx.set_node_attributes(graph, 'node_type', {n: _get_tier(n.split('`')[-2]) for n in graph})
140135
# relabel nodes to class names
141-
mapping = {node: lookup_class_name(self._context, node) for node in graph.nodes()}
136+
mapping = {node: lookup_class_name(node, context) for node in graph.nodes()}
142137
new_names = [mapping.values()]
143138
if len(new_names) > len(set(new_names)):
144139
raise DataJointError('Some classes have identical names. The ERD cannot be plotted.')
145140
nx.relabel_nodes(graph, mapping, copy=False)
146141
return graph
147142

148-
def draw(self, pos=None, layout=None, font_scale=1.2, **layout_options):
143+
def draw(self, pos=None, layout=None, context=None, font_scale=1.2, **layout_options):
144+
"""
145+
Draws the graph of dependencies.
146+
:param pos: dict with positions for every node. If None, then layout is called.
147+
:param layout: the graph layout function. If None, then self._layout is used.
148+
:param context: the context in which to look for the class names. If None, the caller's context is used.
149+
:param font_scale: the scalar used to scale all the fonts.
150+
:param layout_options: kwargs passed into the layout function.
151+
"""
149152
if not self.nodes_to_show:
150153
print('There is nothing to plot')
151154
return
152-
graph = self._make_graph()
155+
if context is None:
156+
# get the caller's locals()
157+
import inspect
158+
frame = inspect.currentframe()
159+
try:
160+
context = frame.f_back.f_locals
161+
finally:
162+
del frame
163+
164+
graph = self._make_graph(context)
153165
if pos is None:
154166
pos = (layout if layout else self._layout)(graph, **layout_options)
155167
import matplotlib.pyplot as plt
@@ -158,7 +170,7 @@ def draw(self, pos=None, layout=None, font_scale=1.2, **layout_options):
158170
edge_styles = ['solid' if e[2]['primary'] else 'dashed' for e in edge_list]
159171
nx.draw_networkx_edges(graph, pos=pos, edgelist=edge_list, style=edge_styles, alpha=0.2)
160172

161-
label_props = { # http://matplotlib.org/examples/color/named_colors.html
173+
label_props = { # http://matplotlib.org/examples/color/named_colors.html
162174
None: dict(bbox=dict(boxstyle='round,pad=0.1', facecolor='yellow', alpha=0.3), size=round(font_scale*8)),
163175
Manual: dict(bbox=dict(boxstyle='round,pad=0.1', edgecolor='white', facecolor='darkgreen', alpha=0.3), size=round(font_scale*10)),
164176
Lookup: dict(bbox=dict(boxstyle='round,pad=0.1', edgecolor='white', facecolor='gray', alpha=0.2), size=round(font_scale*8)),

‎tests/test_erd.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def test_dependencies():
3232
@staticmethod
3333
def test_erd():
3434
erd = dj.ERD(schema)
35-
graph = erd._make_graph()
35+
graph = erd._make_graph(schema.context)
3636
assert_true(set(cls.__name__ for cls in (A, B, D, E, L)).issubset(graph.nodes()))
3737
pos = erd._layout(graph)
3838
assert_true(set(cls.__name__ for cls in (A, B, D, E, L)).issubset(pos.keys()))

0 commit comments

Comments
 (0)