Skip to content

Commit 60294c9

Browse files
authored
Merge pull request #229 from dimitri-yatsenko/master
insert accepts relations. ERD looks up class names in context.
2 parents cee9aa4 + c886728 commit 60294c9

9 files changed

Lines changed: 110 additions & 73 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Release notes
2+
### 0.3.2.
3+
* Fixed issue #223: `insert` can insert relations without fetching
4+
* 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).
5+
* ERD.draw() no longer has the `prefix` argument: class names are shown as found in the context.

‎README.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ DataJoint for Python is a high-level programming interface for relational databa
1414

1515
DataJoint was initially developed in 2009 by Dimitri Yatsenko in Andreas Tolias' Lab for the distributed processing and management of large volumes of data streaming from regular experiments. Starting in 2011, DataJoint has been available as an open-source project adopted by other labs and improved through contributions from several developers.
1616

17-
1817
## Quick start guide
1918
The current pip version is ancient. We will update it as soon as datajoint release 1.0 is out.
2019
~~To install datajoint using `pip` just run:~~

‎datajoint/__init__.py‎

Lines changed: 2 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.0"
20-
__date__ = "July 14, 2016"
19+
__version__ = "0.3.2"
20+
__date__ = "July 18, 2016"
2121
__all__ = ['__author__', '__version__',
2222
'config', 'conn', 'kill', 'BaseRelation',
2323
'Connection', 'Heading', 'FreeRelation', 'Not', 'schema',

‎datajoint/base_relation.py‎

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ def heading(self):
3737
self._heading.init_from_database(self.connection, self.database, self.table_name)
3838
return self._heading
3939

40+
@property
41+
def context(self):
42+
return self._context
43+
4044
def declare(self):
4145
"""
4246
Loads the table heading. If the table is not declared, use self.definition to declare
@@ -98,16 +102,18 @@ def full_table_name(self):
98102
def insert1(self, row, **kwargs):
99103
"""
100104
Insert one data record or one Mapping (like a dict).
101-
:param row: Data record, a Mapping (like a dict), or a list or tuple with ordered values.
105+
:param row: a numpy record, a dict-like object, or an ordered sequence to be inserted as one row.
106+
For kwargs, see insert()
102107
"""
103108
self.insert((row,), **kwargs)
104109

105110
def insert(self, rows, replace=False, ignore_errors=False, skip_duplicates=False):
106111
"""
107-
Insert a collection of rows. Additional keyword arguments are passed to insert1.
112+
Insert a collection of rows.
108113
109-
:param rows: An iterable where an element is a valid arguments for insert1.
110-
:param replace: If True, replaces the matching data tuple in the table if it exists.
114+
:param rows: An iterable where an element is a numpy record, a dict-like object, or an ordered sequence.
115+
rows may also be another relation with the same heading.
116+
:param replace: If True, replaces the existing tuple.
111117
:param ignore_errors: If True, ignore errors: e.g. constraint violations.
112118
:param skip_duplicates: If True, silently skip duplicate inserts.
113119
@@ -116,6 +122,19 @@ def insert(self, rows, replace=False, ignore_errors=False, skip_duplicates=False
116122
>>> dict(subject_id=7, species="mouse", date_of_birth="2014-09-01"),
117123
>>> dict(subject_id=8, species="mouse", date_of_birth="2014-09-02")])
118124
"""
125+
126+
if isinstance(rows, RelationalOperand):
127+
# INSERT FROM SELECT
128+
if skip_duplicates:
129+
ignore_errors = True
130+
query = 'INSERT{ignore} INTO {table} ({fields}) {select}'.format(
131+
ignore=" IGNORE" if ignore_errors else "",
132+
table=self.full_table_name,
133+
fields='`'+'`,`'.join(rows.heading.names)+'`',
134+
select=rows.make_sql())
135+
self.connection.query(query)
136+
return
137+
119138
heading = self.heading
120139
field_list = None # ensures that all rows have the same attributes in the same order as the first row.
121140

@@ -336,30 +355,42 @@ def show_definition(self):
336355
do_include = False
337356
if attributes_thus_far.issuperset(primary_key):
338357
parents.pop(parent)
339-
definition += '-> ' + self.lookup_table_name(parent) + '\n'
358+
definition += '-> ' + lookup_class_name(self.context, parent) + '\n'
340359
if do_include:
341360
definition += '%-20s : %-28s # %s\n' % (
342-
attr.name if attr.default is None else '%s=%s' % (attr.name, attr.default),
361+
attr.name if attr.default is None else '%s = %s' % (attr.name, attr.default),
343362
'%s%s' % (attr.type, 'auto_increment' if attr.autoincrement else ''), attr.comment)
363+
print(definition)
344364
return definition
345365

346-
def lookup_table_name(self, name):
347-
"""
348-
given the name of another table in the form `database`.`table_name`, find its class in the context
349-
:param name: `database`.`table_name`
350-
:return: class name found in the context.
351-
"""
352-
def _lookup(context, name, level=0):
353-
for member_name, member in context.items():
354-
if inspect.isclass(member) and issubclass(member, BaseRelation) and member.full_table_name == name:
355-
return member_name
356-
if level < 5 and inspect.ismodule(member) and member.__name__ != 'datajoint':
357-
candidate_name = _lookup(dict(inspect.getmembers(member)), name, level+1)
358-
if candidate_name != name:
359-
return member_name + '.' + candidate_name
360-
return name
361366

362-
return _lookup(self._context, name)
367+
def lookup_class_name(context, name, levels=4):
368+
"""
369+
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
371+
: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.
374+
"""
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
363394

364395

365396
class FreeRelation(BaseRelation):

‎datajoint/erd.py‎

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
11
import networkx as nx
2-
import numpy as np
32
import re
3+
import numpy as np
44
from scipy.optimize import basinhopping
55
import itertools
66
import inspect
77
from . import Manual, Imported, Computed, Lookup, Part, DataJointError
8+
from .base_relation import lookup_class_name
89

910
user_relation_classes = (Manual, Lookup, Computed, Imported, Part)
1011

1112

12-
def _get_concrete_subclasses(class_list):
13-
for cls in class_list:
14-
for subclass in cls.__subclasses__():
15-
if not inspect.isabstract(subclass):
16-
yield subclass
17-
yield from _get_concrete_subclasses([subclass])
18-
19-
2013
def _get_tier(table_name):
2114
try:
2215
return next(tier for tier in user_relation_classes
@@ -30,64 +23,71 @@ class ERD(nx.DiGraph):
3023
Entity relationship diagram.
3124
3225
Usage:
33-
>>> erd = Erd(source)
34-
source can be a base relation object, a base relation class, a schema, or a module that has a schema
35-
or source can be a sequence of such objects.
26+
>>> erd = Erd(source, context=None)
27+
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
3630
3731
>>> erd.draw()
3832
draws the diagram using pyplot
3933
4034
erd1 + erd2 - combines the two ERDs.
4135
erd + n - adds n levels of successors
42-
erd - n - adds n levens of predecessors
36+
erd - n - adds n levels of predecessors
4337
Thus dj.ERD(schema.Table)+1-1 defines the diagram of immediate ancestors and descendants of schema.Table
4438
4539
Note that erd + 1 - 1 may differ from erd - 1 + 1 and so forth.
4640
Only those tables that are loaded in the connection object are displayed
4741
"""
48-
def __init__(self, source):
42+
def __init__(self, source, context=None):
4943

5044
if isinstance(source, ERD):
5145
# copy constructor
5246
self.nodes_to_show = set(source.nodes_to_show)
47+
self._context = source._context
5348
super().__init__(source)
5449
return
5550

56-
# if source is not a list, make it a list
51+
# find connection in the source
5752
try:
58-
source[0]
59-
except (TypeError, KeyError):
60-
source = [source]
61-
62-
# find connection in the first item in the list
63-
try:
64-
connection = source[0].connection
53+
connection = source.connection
6554
except AttributeError:
6655
try:
67-
connection = source[0].schema.connection
56+
connection = source.schema.connection
57+
except AttributeError:
58+
raise DataJointError('Could not find database connection in %s' % repr(source[0]))
59+
60+
if context is None:
61+
# find context in the source
62+
try:
63+
context = source.context
6864
except AttributeError:
69-
raise DataJointError('Could find database connection in %s' % repr(source[0]))
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
7071

7172
# initialize graph from dependencies
7273
connection.dependencies.load()
7374
super().__init__(connection.dependencies)
7475

7576
# Enumerate nodes from all the items in the list
7677
self.nodes_to_show = set()
77-
for source in source:
78+
try:
79+
self.nodes_to_show.add(source.full_table_name)
80+
except AttributeError:
7881
try:
79-
self.nodes_to_show.add(source.full_table_name)
82+
database = source.database
8083
except AttributeError:
8184
try:
82-
database = source.database
85+
database = source.schema.database
8386
except AttributeError:
84-
try:
85-
database = source.schema.database
86-
except AttributeError:
87-
raise DataJointError('Cannot plot ERD for %s' % repr(source))
88-
for node in self:
89-
if node.startswith('`%s`' % database):
90-
self.nodes_to_show.add(node)
87+
raise DataJointError('Cannot plot ERD for %s' % repr(source))
88+
for node in self:
89+
if node.startswith('`%s`' % database):
90+
self.nodes_to_show.add(node)
9191

9292
def __add__(self, arg):
9393
"""
@@ -131,31 +131,25 @@ def __mul__(self, arg):
131131
self.nodes_to_show.intersection_update(arg.nodes_to_show)
132132
return self
133133

134-
def _make_graph(self, prefix):
134+
def _make_graph(self):
135135
"""
136136
Make the self.graph - a graph object ready for drawing
137137
"""
138138
graph = nx.DiGraph(self).subgraph(self.nodes_to_show)
139139
nx.set_node_attributes(graph, 'node_type', {n: _get_tier(n.split('`')[-2]) for n in graph})
140140
# relabel nodes to class names
141-
class_list = list(cls for cls in _get_concrete_subclasses(user_relation_classes))
142-
mapping = {
143-
cls.full_table_name:
144-
(cls._context['__name__'] + '.'
145-
if (prefix and cls._context['__name__'] != '__main__') else '') +
146-
(cls._master.__name__+'.' if issubclass(cls, Part) else '') + cls.__name__
147-
for cls in class_list if cls.full_table_name in graph}
141+
mapping = {node: lookup_class_name(self._context, node) for node in graph.nodes()}
148142
new_names = [mapping.values()]
149143
if len(new_names) > len(set(new_names)):
150144
raise DataJointError('Some classes have identical names. The ERD cannot be plotted.')
151145
nx.relabel_nodes(graph, mapping, copy=False)
152146
return graph
153147

154-
def draw(self, pos=None, layout=None, prefix=True, font_scale=1.2, **layout_options):
148+
def draw(self, pos=None, layout=None, font_scale=1.2, **layout_options):
155149
if not self.nodes_to_show:
156150
print('There is nothing to plot')
157151
return
158-
graph = self._make_graph(prefix)
152+
graph = self._make_graph()
159153
if pos is None:
160154
pos = (layout if layout else self._layout)(graph, **layout_options)
161155
import matplotlib.pyplot as plt

‎datajoint/heading.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ def init_from_database(self, conn, database, table_name):
183183
if attr['string'] and attr['default'] is not None and attr['default'] not in sql_literals:
184184
attr['default'] = '"%s"' % attr['default']
185185

186+
if attr['nullable']: # nullable fields always default to null
187+
attr['default'] = 'null'
188+
186189
attr['sql_expression'] = None
187190
if not (attr['numeric'] or attr['string'] or attr['is_blob']):
188191
raise DataJointError('Unsupported field type {field} in `{database}`.`{table_name}`'.format(

‎datajoint/schema.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from . import conn, DataJointError, config
77
from .heading import Heading
88
from .utils import user_choice, to_camel_case
9-
from .user_relations import UserRelation, Part, Computed, Imported, Manual, Lookup
9+
from .user_relations import Part, Computed, Imported, Manual, Lookup
1010

1111
logger = logging.getLogger(__name__)
1212

@@ -117,7 +117,6 @@ def exists(self):
117117
cur = self.connection.query("SHOW DATABASES LIKE '{database}'".format(database=self.database))
118118
return cur.rowcount > 0
119119

120-
121120
def process_relation_class(self, relation_class, context, assert_declared=False):
122121
"""
123122
assign schema properties to the relation class and declare the table
@@ -141,7 +140,6 @@ def process_relation_class(self, relation_class, context, assert_declared=False)
141140
else:
142141
instance.insert(contents, skip_duplicates=True)
143142

144-
145143
def __call__(self, cls):
146144
"""
147145
Binds the passed in class object to a database. This is intended to be used as a decorator.
@@ -170,3 +168,4 @@ def jobs(self):
170168
:return: jobs relation
171169
"""
172170
return self.connection.jobs[self.database]
171+

‎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(prefix=False)
35+
graph = erd._make_graph()
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()))

‎tests/test_relation.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ def test_wrong_arguments_insert(self):
8484
def test_wrong_insert_type(self):
8585
self.user.insert1(3)
8686

87+
def test_insert_select(self):
88+
original_length = len(self.subject)
89+
self.subject.insert(self.subject.proj(
90+
'real_id', 'date_of_birth', 'subject_notes', subject_id='subject_id+1000', species='"human"'))
91+
assert_equal(len(self.subject), 2*original_length)
92+
8793
def test_replace(self):
8894
"""
8995
Test replacing or ignoring duplicate entries

0 commit comments

Comments
 (0)