Skip to content

Commit f71fc7b

Browse files
authored
Merge pull request #281 from dimitri-yatsenko/master
Fix issues #280 and #252
2 parents c4a6174 + 8f2ffa7 commit f71fc7b

12 files changed

Lines changed: 105 additions & 64 deletions

‎.travis.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
sudo: required
22
language: python
33
env:
4-
- DJ_TEST_HOST="127.0.0.1" DJ_TEST_USER="datajoint" DJ_TEST_PASSWORD="datajoint" DJ_HOST="127.0.0.1" DJ_USER="datajoint" DJ_PASSWORD="datajoint"
4+
- DJ_TEST_HOST="127.0.0.1" DJ_TEST_USER="datajoint" DJ_TEST_PASSWORD="datajoint" DJ_HOST="127.0.0.1" DJ_USER="datajoint" DJ_PASS="datajoint"
55
python:
66
- "3.4"
77
- "3.5"

‎datajoint/__init__.py‎

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

1818
__author__ = "Dimitri Yatsenko, Edgar Y. Walker, and Fabian Sinz at Baylor College of Medicine"
19-
__version__ = "0.4.6"
20-
__date__ = "December 22, 2016"
19+
__version__ = "0.4.7"
20+
__date__ = "January 24, 2017"
2121
__all__ = ['__author__', '__version__',
2222
'config', 'conn', 'kill', 'BaseRelation',
2323
'Connection', 'Heading', 'FreeRelation', 'Not', 'schema',
@@ -47,23 +47,25 @@ class DataJointError(Exception):
4747
from .settings import Config, LOCALCONFIG, GLOBALCONFIG, logger, log_levels
4848
config = Config()
4949

50-
51-
if os.getenv('DJ_HOST') is not None and os.getenv('DJ_USER') is not None and os.getenv('DJ_PASS') is not None: # pragma: no cover
52-
print("Loading local settings from environment variables")
53-
config['database.host'] = os.getenv('DJ_HOST')
54-
config['database.user'] = os.getenv('DJ_USER')
55-
config['database.password'] = os.getenv('DJ_PASS')
56-
elif os.path.exists(LOCALCONFIG): # pragma: no cover
57-
local_config_file = os.path.expanduser(LOCALCONFIG)
58-
print("Loading local settings from {0:s}".format(local_config_file))
59-
logger.log(logging.INFO, "Loading local settings from {0:s}".format(local_config_file))
60-
config.load(local_config_file)
61-
elif os.path.exists(os.path.expanduser('~/') + GLOBALCONFIG): # pragma: no cover
62-
local_config_file = os.path.expanduser('~/') + GLOBALCONFIG
50+
config_files = (os.path.expanduser(n) for n in (LOCALCONFIG, '~/' + GLOBALCONFIG))
51+
try:
52+
local_config_file = next(n for n in config_files if os.path.exists(n))
53+
except StopIteration:
54+
local_config_file = None
55+
else:
6356
print("Loading local settings from {0:s}".format(local_config_file))
6457
logger.log(logging.INFO, "Loading local settings from {0:s}".format(local_config_file))
6558
config.load(local_config_file)
66-
else:
59+
60+
# override login credentials with environment variables
61+
mapping = {k: v for k, v in zip(
62+
('database.host', 'database.user', 'database.password'),
63+
map(os.getenv, ('DJ_HOST', 'DJ_USER', 'DJ_PASS')))
64+
if v is not None}
65+
config.update(mapping)
66+
67+
# create local config file if neither local nor global file exists
68+
if not mapping and local_config_file is None:
6769
print("""Cannot find configuration settings. Using default configuration. To change that, either
6870
* modify the local copy of %s that datajoint just saved for you
6971
* put a file named %s with the same configuration format in your home

‎datajoint/base_relation.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,9 @@ def size_on_disk(self):
348348
database=self.database, table=self.table_name), as_dict=True).fetchone()
349349
return ret['Data_length'] + ret['Index_length']
350350

351+
def describe(self):
352+
return self.show_definition()
353+
351354
def show_definition(self):
352355
"""
353356
:return: the definition string for the relation using DataJoint DDL.

‎datajoint/connection.py‎

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from contextlib import contextmanager
77
import pymysql as client
88
import logging
9+
from getpass import getpass
910

1011
from . import config
1112
from . import DataJointError
@@ -16,7 +17,7 @@
1617
logger = logging.getLogger(__name__)
1718

1819

19-
def conn(host=None, user=None, passwd=None, init_fun=None, reset=False):
20+
def conn(host=None, user=None, password=None, init_fun=None, reset=False):
2021
"""
2122
Returns a persistent connection object to be shared by multiple modules.
2223
If the connection is not yet established or reset=True, a new connection is set up.
@@ -26,18 +27,20 @@ def conn(host=None, user=None, passwd=None, init_fun=None, reset=False):
2627
2728
:param host: hostname
2829
:param user: mysql user
29-
:param passwd: mysql password
30+
:param password: mysql password
3031
:param init_fun: initialization function
31-
:param reset: whether the connection should be reseted or not
32+
:param reset: whether the connection should be reset or not
3233
"""
3334
if not hasattr(conn, 'connection') or reset:
3435
host = host if host is not None else config['database.host']
3536
user = user if user is not None else config['database.user']
36-
passwd = passwd if passwd is not None else config['database.password']
37-
if passwd is None: # pragma: no cover
38-
passwd = input("Please enter database password: ")
37+
password = password if password is not None else config['database.password']
38+
if user is None: # pragma: no cover
39+
user = input("Please enter DataJoint username: ")
40+
if password is None: # pragma: no cover
41+
password = getpass(prompt="Please enter DataJoint password: ")
3942
init_fun = init_fun if init_fun is not None else config['connection.init_function']
40-
conn.connection = Connection(host, user, passwd, init_fun)
43+
conn.connection = Connection(host, user, password, init_fun)
4144
return conn.connection
4245

4346

@@ -50,19 +53,20 @@ class Connection:
5053
5154
:param host: host name
5255
:param user: user name
53-
:param passwd: password
54-
:param init_fun: initialization function
56+
:param password: password
57+
:param init_fun: connection initialization function (SQL)
5558
"""
5659

57-
def __init__(self, host, user, passwd, init_fun=None):
60+
def __init__(self, host, user, password, init_fun=None):
5861
if ':' in host:
5962
host, port = host.split(':')
6063
port = int(port)
6164
else:
6265
port = config['database.port']
63-
self.conn_info = dict(host=host, port=port, user=user, passwd=passwd)
66+
self.conn_info = dict(host=host, port=port, user=user, passwd=password)
6467
self.init_fun = init_fun
6568
print("Connecting {user}@{host}:{port}".format(**self.conn_info))
69+
self._conn = None
6670
self.connect()
6771
if self.is_connected:
6872
logger.info("Connected {user}@{host}:{port}".format(**self.conn_info))
@@ -82,11 +86,9 @@ def __repr__(self):
8286
return "DataJoint connection ({connected}) {user}@{host}:{port}".format(
8387
connected=connected, **self.conn_info)
8488

85-
def connect(self, init_fun=None):
89+
def connect(self):
8690
"""
8791
Connects to the database server.
88-
89-
:param init_fun: initialization function passed to pymysql
9092
"""
9193
self._conn = client.connect(init_command=self.init_fun, **self.conn_info)
9294

@@ -198,6 +200,3 @@ def transaction(self):
198200
raise
199201
else:
200202
self.commit_transaction()
201-
202-
203-

‎datajoint/dependencies.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,14 @@ def add_table(self, table_name):
5656
except pp.ParseException:
5757
pass
5858
else:
59+
referencing_attributes = [r.strip('` ') for r in result.attributes.split(',')]
60+
referenced_attributes = [r.strip('` ') for r in result.referenced_attributes.split(',')]
5961
self.add_edge(result.referenced_table, table_name,
60-
primary=all(r.strip('` ') in primary_key for r in result.attributes.split(',')))
62+
primary=all(a in primary_key for a in referencing_attributes),
63+
referenced_attributes=referenced_attributes,
64+
referencing_attributes=referencing_attributes,
65+
aliased=any(a != b for a, b in zip(referenced_attributes, referenced_attributes)),
66+
multi=any(a not in primary_key for a in referencing_attributes))
6167

6268
def load(self, target=None):
6369
"""
@@ -82,4 +88,4 @@ def descendants(self, full_table_name):
8288
:return: all dependent tables sorted in topological order. Self is included.
8389
"""
8490
nodes = nx.algorithms.dag.descendants(self, full_table_name)
85-
return [full_table_name] + nx.algorithms.dag.topological_sort(self, nodes)
91+
return [full_table_name] + nx.algorithms.dag.topological_sort(self, nodes)

‎datajoint/heading.py‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,23 +217,29 @@ def project(self, attribute_list, named_attributes=None, force_primary_key=None)
217217
"""
218218
derive a new heading by selecting, renaming, or computing attributes.
219219
In relational algebra these operators are known as project, rename, and extend.
220+
:param attribute_list: the full list of existing attributes to include
221+
:param force_primary_key: attributes to force to be converted to primary
222+
:param named_attributes: dictionary of renamed attributes
220223
"""
221224
try: # check for missing attributes
222225
raise DataJointError('Attribute `%s` is not found' % next(a for a in attribute_list if a not in self.names))
223226
except StopIteration:
224227
if named_attributes is None:
225228
named_attributes = {}
229+
if force_primary_key is None:
230+
force_primary_key = set()
226231
return Heading(
227-
[dict(v.todict(), **dict(
228-
() if force_primary_key is None else [('in_key', k in force_primary_key)]))
229-
for k, v in self.attributes.items() if k in attribute_list] +
230-
[dict( # rename attribute
232+
[dict( # copied attributes
233+
self.attributes[k].todict(),
234+
in_key=self.attributes[k].in_key or k in force_primary_key)
235+
for k in attribute_list] +
236+
[dict( # renamed attributes
231237
self.attributes[sql_expression].todict(),
232238
name=new_name,
233-
sql_expression='`' + sql_expression + '`',
234-
**dict(() if force_primary_key is None else [('in_key', sql_expression in force_primary_key)]))
239+
sql_expression='`%s`' % sql_expression,
240+
in_key=self.attributes[sql_expression].in_key or sql_expression in force_primary_key)
235241
if sql_expression in self.names else
236-
dict( # compute attribute
242+
dict( # computed attributes
237243
default_attribute_properties,
238244
name=new_name,
239245
sql_expression=sql_expression)

‎datajoint/settings.py‎

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,13 @@
3737

3838
default = OrderedDict({
3939
'database.host': 'localhost',
40-
'database.password': 'datajoint',
41-
'database.user': 'datajoint',
40+
'database.password': None,
41+
'database.user': None,
4242
'database.port': 3306,
43-
'database.reconnect': False,
44-
#
4543
'connection.init_function': None,
46-
#
44+
'database.reconnect': False,
4745
'loglevel': 'INFO',
48-
#
4946
'safemode': True,
50-
#
5147
'display.limit': 7,
5248
'display.width': 14
5349
})
@@ -63,7 +59,6 @@
6359
}
6460

6561

66-
6762
class Config(collections.MutableMapping):
6863

6964
instance = None
@@ -98,7 +93,6 @@ def __iter__(self):
9893
def __len__(self):
9994
return len(self.instance._conf)
10095

101-
10296
@contextmanager
10397
def __call__(self, **kwargs):
10498
"""
@@ -107,17 +101,15 @@ def __call__(self, **kwargs):
107101
double underscore '__'. The context manager yields the changed config object.
108102
109103
Example:
110-
111104
>>> import datajoint as dj
112-
>>> with dj.config(safe__mode=False) as cfg:
105+
>>> with dj.config(safemode=False, database__host="localhost") as cfg:
113106
>>> # do dangerous stuff here
114-
115107
"""
116108

117109
try:
118110
backup = self.instance
119111
self.instance = Config.__Config(self.instance._conf)
120-
new = {k.replace('__','.'):v for k,v in kwargs.items()}
112+
new = {k.replace('__', '.'): v for k, v in kwargs.items()}
121113
self.instance._conf.update(new)
122114
yield self
123115
except:
@@ -126,15 +118,13 @@ def __call__(self, **kwargs):
126118
else:
127119
self.instance = backup
128120

129-
130121
class __Config:
131122
"""
132123
Stores datajoint settings. Behaves like a dictionary, but applies validator functions
133124
when certain keys are set.
134125
135126
The default parameters are stored in datajoint.settings.default . If a local config file
136127
exists, the settings specified in this file override the default settings.
137-
138128
"""
139129

140130
def __init__(self, *args, **kwargs):
@@ -166,8 +156,7 @@ def save(self, filename=None):
166156
def load(self, filename):
167157
"""
168158
Updates the setting from config file in JSON format.
169-
170-
:param filename=None: filename of the local JSON settings file. If None, the local config file is used.
159+
:param filename: filename of the local JSON settings file. If None, the local config file is used.
171160
"""
172161
if filename is None:
173162
filename = LOCALCONFIG

‎tests/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
CONN_INFO = dict(
2323
host=environ.get('DJ_TEST_HOST', 'localhost'),
2424
user=environ.get('DJ_TEST_USER', 'datajoint'),
25-
passwd=environ.get('DJ_TEST_PASSWORD', 'datajoint'))
25+
password=environ.get('DJ_TEST_PASSWORD', 'datajoint'))
2626

2727
# Prefix for all databases used during testing
2828
PREFIX = environ.get('DJ_TEST_DB_PREFIX', 'djtest')

‎tests/schema.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
@schema
1515
class Test(dj.Computed):
16-
definition="""
16+
definition = """
1717
key : int # key
1818
---
1919
value : int # value

‎tests/schema_simple.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,24 @@
1111
schema = dj.schema(PREFIX + '_relational', locals(), connection=dj.conn(**CONN_INFO))
1212

1313

14+
@schema
15+
class IJ(dj.Manual):
16+
definition = """ # tests restrictions
17+
i : int
18+
j : int
19+
"""
20+
contents = list(dict(i=i, j=j+2) for i in range(3) for j in range(3))
21+
22+
23+
@schema
24+
class JI(dj.Manual):
25+
definition = """ # tests restrictions by relations when attributes are reordered
26+
j : int
27+
i : int
28+
"""
29+
contents = list(dict(i=i+1, j=j) for i in range(3) for j in range(3))
30+
31+
1432
@schema
1533
class A(dj.Lookup):
1634
definition = """

0 commit comments

Comments
 (0)