Skip to content

Commit 4ffe1ec

Browse files
authored
Merge pull request #68 from mikofski/fields
implement a Field class similar to fields in Django, Marshmallow and DRF
2 parents 674557e + f9b396e commit 4ffe1ec

21 files changed

Lines changed: 848 additions & 733 deletions

carousel/contrib/readers.py

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import numpy as np
88
import h5py
99
from carousel.core.data_readers import DataReader
10-
from carousel.core import Q_ # UREG
11-
from django.db.models import AutoField
10+
from carousel.core.data_sources import DataParameter
11+
from carousel.core import Q_
1212
import logging
1313

1414
LOGGER = logging.getLogger(__name__)
@@ -17,11 +17,18 @@
1717

1818
def copy_model_instance(obj):
1919
"""
20-
https://djangosnippets.org/snippets/1040/
20+
Copy Django model instance as a dictionary excluding automatically created
21+
fields like an auto-generated sequence as a primary key or an auto-created
22+
many-to-one reverse relation.
23+
24+
:param obj: Django model object
25+
:return: copy of model instance as dictionary
2126
"""
22-
return {f.name: getattr(obj, f.name) for f in obj._meta.get_fields()
23-
if not isinstance(f, AutoField) and
24-
f not in obj._meta.parents.values()}
27+
meta = getattr(obj, '_meta') # make pycharm happy
28+
# dictionary of model values excluding auto created and related fields
29+
return {f.name: getattr(obj, f.name)
30+
for f in meta.get_fields(include_parents=False)
31+
if not f.auto_created}
2532

2633

2734
# TODO: make parameters consistent for all readers
@@ -58,8 +65,10 @@ def load_data(self, *args, **kwargs):
5865
"""
5966
# get positional argument names from parameters and apply them to args
6067
# update data with additional kwargs
61-
argpos = {v['argpos']: k for k, v in self.parameters.iteritems()
62-
if 'argpos' in v}
68+
argpos = {
69+
v['extras']['argpos']: k for k, v in self.parameters.iteritems()
70+
if 'argpos' in v['extras']
71+
}
6372
data = dict(
6473
{argpos[n]: a for n, a in enumerate(args)}, **kwargs
6574
)
@@ -91,30 +100,32 @@ def __init__(self, parameters=None, meta=None):
91100
raise AttributeError('model not specified in Meta class')
92101
#: Django model
93102
self.model = meta.model
94-
all_field_names = [f.name for f in self.model._meta.get_fields()]
103+
model_meta = getattr(self.model, '_meta') # make pycharm happy
104+
# model fields excluding AutoFields and related fields like one-to-many
105+
all_model_fields = [
106+
f for f in model_meta.get_fields(include_parents=False)
107+
if not f.auto_created
108+
]
109+
all_field_names = [f.name for f in all_model_fields] # field names
110+
# use all fields if no parameters given
95111
if parameters is None:
96-
parameters = dict.fromkeys(
112+
parameters = DataParameter.fromkeys(
97113
all_field_names, {}
98114
)
99-
fields = getattr(meta, 'fields', all_field_names)
115+
fields = getattr(meta, 'fields', all_field_names) # specified fields
100116
LOGGER.debug('fields:\n%r', fields)
101-
exclude = getattr(meta, 'exclude', [])
102-
model_meta_parents_values = self.model._meta.parents.values()
103-
for f in self.model._meta.fields:
104-
# pop and skip any AutoFields or parents
105-
if isinstance(f, AutoField) or f in model_meta_parents_values:
106-
parameters.pop(f.name, None)
107-
continue
117+
exclude = getattr(meta, 'exclude', []) # specifically excluded fields
118+
for f in all_model_fields:
108119
# skip any fields not specified in data source
109120
if f.name not in fields or f.name in exclude:
110121
LOGGER.debug('skipping %s', f.name)
111122
continue
112123
# add field to parameters or update parameters with field type
113124
param_dict = {'ftype': f.get_internal_type()}
114125
if f.name in parameters:
115-
parameters[f.name].update(param_dict)
126+
parameters[f.name]['extras'].update(param_dict)
116127
else:
117-
parameters[f.name] = param_dict
128+
parameters[f.name] = DataParameter(**param_dict)
118129
super(DjangoModelReader, self).__init__(parameters)
119130

120131
def load_data(self, model_instance, *args, **kwargs):
@@ -138,8 +149,9 @@ def load_data(self, h5file, *args, **kwargs):
138149
h5data = dict.fromkeys(self.parameters)
139150
for param, attrs in self.parameters.iteritems():
140151
LOGGER.debug('parameter:\n%r', param)
141-
node = attrs['node'] # full name of node
142-
member = attrs.get('member') # composite datatype member
152+
node = attrs['extras']['node'] # full name of node
153+
# composite datatype member
154+
member = attrs['extras'].get('member')
143155
if member is not None:
144156
# if node is a table then get column/field/description
145157
h5data[param] = np.asarray(h5f[node][member]) # copy member

carousel/contrib/tests/test_data_readers.py

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from carousel.contrib.readers import (
77
ArgumentReader, DjangoModelReader, HDF5Reader
88
)
9-
from carousel.core.data_sources import DataSource
9+
from carousel.core.data_sources import DataSource, DataParameter
1010
from datetime import datetime
1111
from carousel.core import UREG
1212
from django.db import models
@@ -91,7 +91,8 @@ class MyApp(AppConfig):
9191

9292
class MyModel(models.Model):
9393
"""
94-
Django model for testing :class:`~carousel.contrib.readers.DjangoModelReader`.
94+
Django model for testing
95+
:class:`~carousel.contrib.readers.DjangoModelReader`.
9596
"""
9697
air_temp = models.FloatField()
9798
latitude = models.FloatField()
@@ -108,8 +109,8 @@ class Meta:
108109

109110
def test_arg_reader():
110111
"""
111-
Test :class:`~carousel.contrib.readers.ArgumentReader` is instantiated and can
112-
load argument data units and values correctly.
112+
Test :class:`~carousel.contrib.readers.ArgumentReader` is instantiated and
113+
can load argument data units and values correctly.
113114
114115
:return: arg reader and data
115116
:raises: AssertionError
@@ -122,11 +123,11 @@ def test_arg_reader():
122123
air_temp = TAIR
123124
location = {'latitude': LAT, 'longitude': LON, 'timezone': TZ}
124125
parameters = {
125-
'pvmodule': {'argpos': 0},
126-
'air_temp': {'units': 'celsius', 'argpos': 1},
127-
'latitude': {'units': 'degrees'},
128-
'longitude': {'units': 'degrees'},
129-
'timezone': {'units': 'hours'}
126+
'pvmodule': {'extras': {'argpos': 0}},
127+
'air_temp': {'units': 'celsius', 'extras': {'argpos': 1}},
128+
'latitude': {'units': 'degrees', 'extras': {}},
129+
'longitude': {'units': 'degrees', 'extras': {}},
130+
'timezone': {'units': 'hours', 'extras': {}}
130131
}
131132
arg_reader = ArgumentReader(parameters)
132133
assert isinstance(arg_reader, DataReader) # instance of ArgumentReader
@@ -156,10 +157,10 @@ def test_arg_data_src():
156157
class ArgSrcTest(DataSource):
157158
data_reader = ArgumentReader
158159
data_cache_enabled = False
159-
air_temp = {'units': 'celsius', 'argpos': 0}
160-
latitude = {'units': 'degrees', 'isconstant': True}
161-
longitude = {'units': 'degrees', 'isconstant': True}
162-
timezone = {'units': 'hours'}
160+
air_temp = DataParameter(**{'units': 'celsius', 'argpos': 0})
161+
latitude = DataParameter(**{'units': 'degrees', 'isconstant': True})
162+
longitude = DataParameter(**{'units': 'degrees', 'isconstant': True})
163+
timezone = DataParameter(**{'units': 'hours'})
163164

164165
def __prepare_data__(self):
165166
pass
@@ -180,13 +181,13 @@ def __prepare_data__(self):
180181

181182
def test_django_reader():
182183
"""
183-
Test :class:`~carousel.contrib.readers.DjangoModelReader` is instantiated and
184-
can load argument data units and values correctly.
184+
Test :class:`~carousel.contrib.readers.DjangoModelReader` is instantiated
185+
and can load argument data units and values correctly.
185186
186187
:return: django reader and data
187188
:raises: AssertionError
188189
"""
189-
params = {'air_temp': {'units': 'celsius'}}
190+
params = {'air_temp': {'units': 'celsius', 'extras': {}}}
190191
meta = type('Meta', (), {'model': MyModel})
191192
django_reader = DjangoModelReader(params, meta)
192193
assert isinstance(django_reader, (DataReader, ArgumentReader))
@@ -216,9 +217,9 @@ class DjangoSrcTest1(DataSource):
216217
data_reader = DjangoModelReader
217218
data_cache_enabled = False
218219
# parameters
219-
air_temp = {'units': 'celsius'}
220-
latitude = {'units': 'degrees'}
221-
longitude = {'units': 'degrees'}
220+
air_temp = DataParameter(**{'units': 'celsius'})
221+
latitude = DataParameter(**{'units': 'degrees'})
222+
longitude = DataParameter(**{'units': 'degrees'})
222223

223224
class Meta:
224225
model = MyModel
@@ -244,9 +245,9 @@ class DjangoSrcTest2(DataSource):
244245
data_reader = DjangoModelReader
245246
data_cache_enabled = False
246247
# parameters
247-
air_temp = {'units': 'celsius'}
248-
latitude = {'units': 'degrees'}
249-
longitude = {'units': 'degrees'}
248+
air_temp = DataParameter(**{'units': 'celsius'})
249+
latitude = DataParameter(**{'units': 'degrees'})
250+
longitude = DataParameter(**{'units': 'degrees'})
250251

251252
class Meta:
252253
model = MyModel
@@ -298,9 +299,9 @@ def test_hdf5_reader():
298299
setup_hdf5_test_data()
299300
# test 1: load data from hdf5 dataset array by node
300301
params = {
301-
'GHI': {'units': 'W/m**2', 'node': '/data/GHI'},
302-
'DNI': {'units': 'W/m**2', 'node': '/data/DNI'},
303-
'Tdry': {'units': 'degC', 'node': '/data/Tdry'}
302+
'GHI': {'units': 'W/m**2', 'extras': {'node': '/data/GHI'}},
303+
'DNI': {'units': 'W/m**2', 'extras': {'node': '/data/DNI'}},
304+
'Tdry': {'units': 'degC', 'extras': {'node': '/data/Tdry'}}
304305
}
305306
reader1 = HDF5Reader(params)
306307
assert isinstance(reader1, DataReader)
@@ -312,12 +313,12 @@ def test_hdf5_reader():
312313
assert np.allclose(data1['Tdry'], H5TABLE['DryBulbTemperature'])
313314
assert data1['Tdry'].units == UREG.degC
314315
# test 2: load data from hdf5 dataset table by node and member name
315-
params['GHI']['node'] = 'data'
316-
params['GHI']['member'] = 'GlobalHorizontalRadiation'
317-
params['DNI']['node'] = 'data'
318-
params['DNI']['member'] = 'DirectNormalRadiation'
319-
params['Tdry']['node'] = 'data'
320-
params['Tdry']['member'] = 'DryBulbTemperature'
316+
params['GHI']['extras']['node'] = 'data'
317+
params['GHI']['extras']['member'] = 'GlobalHorizontalRadiation'
318+
params['DNI']['extras']['node'] = 'data'
319+
params['DNI']['extras']['member'] = 'DirectNormalRadiation'
320+
params['Tdry']['extras']['node'] = 'data'
321+
params['Tdry']['extras']['member'] = 'DryBulbTemperature'
321322
reader2 = HDF5Reader(params)
322323
assert isinstance(reader1, DataReader)
323324
data2 = reader2.load_data(H5TEST2)
@@ -329,3 +330,11 @@ def test_hdf5_reader():
329330
assert data1['Tdry'].units == UREG.degC
330331
teardown_hdf5_test_data()
331332
return reader1, data1, reader2, data2
333+
334+
335+
if __name__ == '__main__':
336+
ar, d1 = test_arg_reader()
337+
a = test_arg_data_src()
338+
dr, d2 = test_django_reader()
339+
test_django_data_src()
340+
h5r1, h5d1, h5r2, h5d2 = test_hdf5_reader()

carousel/core/__init__.py

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -77,24 +77,28 @@ class Registry(dict):
7777
calling the :func:`super` built-in function.
7878
7979
By default there are no meta attributes, only the register method.
80-
To set meta attributes, in a subclass, add them in the constructor::
80+
To set meta attributes, in a subclass, set the ``meta_names`` class
81+
attribute in the subclass::
8182
82-
def __init__(self):
83-
self.meta1 = {}
84-
self.meta2 = {}
85-
...
83+
class MyRegistry(Registry):
84+
meta_names = ['meta1', 'meta2', ...]
85+
86+
The ``Registry`` superclass will check that the meta names are not already
87+
attributes and then set instance attributes as empty dictionaries in the
88+
subclass. To document them, use the class docstring or document them in the
89+
documentation API.
8690
"""
91+
meta_names = []
92+
8793
def __init__(self):
88-
if hasattr(self, 'meta_names'):
89-
self.meta_names = _listify(self.meta_names)
90-
if [m for m in self.meta_names if m.startswith('_')]:
91-
raise AttributeError('No underscores in meta names.')
92-
for m in self.meta_names:
93-
# check for m in cls and bases
94-
if m in dir(Registry):
95-
msg = ('Class %s already has %s member.' %
96-
(self.__class__.__name__, m))
97-
raise AttributeError(msg)
94+
self.meta_names = _listify(self.meta_names) # convert to list
95+
for m in self.meta_names:
96+
# check for m in cls and bases
97+
if m in dir(Registry):
98+
msg = ('Class %s already has %s member.' %
99+
(self.__class__.__name__, m))
100+
raise AttributeError(msg)
101+
setattr(self, m, {}) # create instance attribute and set to dict()
98102
super(Registry, self).__init__()
99103

100104
def register(self, newitems, *args, **kwargs):
@@ -105,13 +109,10 @@ def register(self, newitems, *args, **kwargs):
105109
items, keys are not allowed to override existing keys in the
106110
registry.
107111
:type newitems: mapping
108-
:param args: Key-value pairs of meta-data. The key is the meta-name,
109-
and the value is a map of the corresponding meta-data for new
110-
item-keys. Each set of meta-keys must be a subset of new item-keys.
111-
:type args: tuple or list
112+
:param args: Positional arguments with meta data corresponding to order
113+
of meta names class attributes
112114
:param kwargs: Maps of corresponding meta for new keys. Each set of
113115
meta keys must be a subset of the new item keys.
114-
:type kwargs: mapping
115116
:raises:
116117
:exc:`~carousel.core.exceptions.DuplicateRegItemError`,
117118
:exc:`~carousel.core.exceptions.MismatchRegMetaKeysError`
@@ -121,19 +122,13 @@ def register(self, newitems, *args, **kwargs):
121122
raise DuplicateRegItemError(self.viewkeys() & newkeys)
122123
self.update(newitems) # register new item
123124
# update meta fields
124-
if any(isinstance(_, dict) for _ in args):
125-
# don't allow kwargs to passed as args!
126-
raise TypeError('*args should be all named tuples.')
127-
# combine the meta args and kwargs together
128-
kwargs.update(args) # doesn't work for combo of dicts and tuples
125+
kwargs.update(zip(self.meta_names, args))
129126
for k, v in kwargs.iteritems():
130127
meta = getattr(self, k) # get the meta attribute
131128
if v:
132129
if not v.viewkeys() <= newkeys:
133130
raise MismatchRegMetaKeysError(newkeys - v.viewkeys())
134131
meta.update(v) # register meta
135-
# TODO: default "tag" meta field for all registries?
136-
# TODO: append "meta" to all meta fields, so they're easier to find?
137132

138133
def unregister(self, items):
139134
"""
@@ -258,7 +253,7 @@ def set_param_file_or_parameters(mcs, attr):
258253
attr['param_file'] = os.path.join(cls_path, cls_file)
259254
else:
260255
attr['parameters'] = dict.fromkeys(
261-
k for k in attr if not k.startswith('_')
256+
k for k, v in attr.iteritems() if isinstance(v, Parameter)
262257
)
263258
for k in attr['parameters']:
264259
attr['parameters'][k] = attr.pop(k)
@@ -277,3 +272,24 @@ def get_parents(bases, parent):
277272
:rtype: list
278273
"""
279274
return [b for b in bases if isinstance(b, parent)]
275+
276+
277+
class Parameter(dict):
278+
_attrs = []
279+
280+
def __init__(self, *args, **kwargs):
281+
items = dict(zip(self._attrs, args))
282+
extras = {}
283+
for key, val in kwargs.iteritems():
284+
if key in self._attrs:
285+
items[key] = val
286+
else:
287+
extras[key] = val
288+
LOGGER.warning('This key: "%s" is not an attribute.', key)
289+
super(Parameter, self).__init__(items, extras=extras)
290+
291+
def __repr__(self):
292+
fmt = ('<%s(' % self.__class__.__name__)
293+
fmt += ', '.join('%s=%r' % (k, v) for k, v in self.iteritems())
294+
fmt += ')>'
295+
return fmt

0 commit comments

Comments
 (0)