Skip to content

Commit aa844af

Browse files
authored
Make model specification and loading more robust (#47)
* Make model specification and loading more robust Right now, in relationships and parent table specifications, specifying the model name requires a fully-specified class name. Adding a registry so we don't have to load classes on the fly and instead register classes as they load, so we can specify a less-qualified class name and avoid having to read files to resolve a model class
1 parent c211db7 commit aa844af

6 files changed

Lines changed: 93 additions & 33 deletions

File tree

spanner_orm/admin/metadata.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
class SpannerMetadata(object):
3030
"""Gathers information about a table from Spanner."""
3131

32+
@classmethod
33+
def _class_name_from_table(cls, table_name):
34+
return 'table_{}_model'.format(table_name)
35+
3236
@classmethod
3337
def models(cls):
3438
"""Constructs model classes from Spanner table schema."""
@@ -39,24 +43,20 @@ def models(cls):
3943
for table_name, table_data in tables.items():
4044
primary_index = indexes[table_name][index.Index.PRIMARY_INDEX]
4145
primary_keys = set(primary_index.columns)
42-
klass = model.ModelBase('Model_{}'.format(table_name), (model.Model,),
43-
{})
46+
klass = model.ModelBase(
47+
cls._class_name_from_table(table_name), (model.Model,), {})
4448
for model_field in table_data['fields'].values():
4549
model_field._primary_key = model_field.name in primary_keys # pylint: disable=protected-access
4650

4751
klass.meta = model.Metadata(
4852
table=table_name,
4953
fields=table_data['fields'],
50-
interleaved=table_data['parent_table'],
54+
interleaved=cls._class_name_from_table(table_data['parent_table']),
5155
indexes=indexes[table_name],
5256
model_class=klass)
57+
klass.meta.finalize()
5358
models[table_name] = klass
5459

55-
for table_model in models.values():
56-
if table_model.meta.interleaved:
57-
table_model.meta.interleaved = models[table_model.meta.interleaved]
58-
table_model.meta.finalize()
59-
6060
return models
6161

6262
@classmethod
@@ -67,9 +67,9 @@ def model(cls, table_name):
6767
def tables(cls):
6868
"""Compiles table information from column schema."""
6969
column_data = collections.defaultdict(dict)
70-
columns = column.ColumnSchema.where(
71-
None, condition.equal_to('table_catalog', ''),
72-
condition.equal_to('table_schema', ''))
70+
columns = column.ColumnSchema.where(None,
71+
condition.equal_to('table_catalog', ''),
72+
condition.equal_to('table_schema', ''))
7373
for column_row in columns:
7474
new_field = field.Field(
7575
column_row.field_type(), nullable=column_row.nullable())
@@ -78,9 +78,9 @@ def tables(cls):
7878
column_data[column_row.table_name][column_row.column_name] = new_field
7979

8080
table_data = collections.defaultdict(dict)
81-
tables = table.TableSchema.where(
82-
None, condition.equal_to('table_catalog', ''),
83-
condition.equal_to('table_schema', ''))
81+
tables = table.TableSchema.where(None,
82+
condition.equal_to('table_catalog', ''),
83+
condition.equal_to('table_schema', ''))
8484
for table_row in tables:
8585
name = table_row.table_name
8686
table_data[name]['parent_table'] = table_row.parent_table_name

spanner_orm/model.py

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616

1717
import collections
1818
import copy
19-
import importlib
2019

2120
from spanner_orm import api
2221
from spanner_orm import condition
2322
from spanner_orm import error
2423
from spanner_orm import field
2524
from spanner_orm import index
2625
from spanner_orm import query
26+
from spanner_orm import registry
2727
from spanner_orm import relationship
2828

2929
from google.cloud import spanner
@@ -69,6 +69,7 @@ def finalize(self):
6969

7070
for _, relation in self.relations.items():
7171
relation.origin = self.model_class
72+
registry.model_registry().register(self.model_class)
7273
self._finalized = True
7374

7475
def add_metadata(self, metadata):
@@ -153,9 +154,9 @@ def indexes(cls):
153154

154155
@property
155156
def interleaved(cls):
156-
if cls.meta.interleaved and not isinstance(cls.meta.interleaved, ModelBase):
157-
cls.meta.interleaved = load_model(cls.meta.interleaved)
158-
return cls.meta.interleaved
157+
if cls.meta.interleaved:
158+
return registry.model_registry().get(cls.meta.interleaved)
159+
return None
159160

160161
@property
161162
def primary_keys(cls):
@@ -426,16 +427,3 @@ def save(self, transaction=None):
426427
self._metaclass.create(transaction, **self.values)
427428
self._persisted = True
428429
return self
429-
430-
431-
def load_model(model_handle):
432-
if isinstance(model_handle, Model):
433-
return model_handle
434-
parts = model_handle.split('.')
435-
path = '.'.join(parts[:-1])
436-
module = importlib.import_module(path)
437-
klass = getattr(module, parts[-1])
438-
if not issubclass(klass, Model):
439-
raise error.SpannerError(
440-
'{model} is not a Model'.format(model=model_handle))
441-
return klass

spanner_orm/registry.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# python3
2+
# Copyright 2019 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Registers Model classes so they can be referenced elsewhere."""
16+
17+
from __future__ import annotations
18+
19+
from typing import Any, Dict, List, Type, Union
20+
21+
import dataclasses
22+
from spanner_orm import error
23+
24+
25+
@dataclasses.dataclass
26+
class RegistryComponent:
27+
references: List[Type[Any]] = dataclasses.field(default_factory=list)
28+
29+
def add(self, reference: Type[Any]) -> None:
30+
self.references.append(reference)
31+
32+
33+
class Registry(object):
34+
35+
def __init__(self):
36+
self._registered = {} # type: Dict[str, RegistryComponent]
37+
38+
def _name_from_class(self, klass: Type[Any]) -> str:
39+
return '{}.{}'.format(klass.__module__, klass.__name__)
40+
41+
def register(self, to_register: Type[Any]) -> None:
42+
name_components = reversed(self._name_from_class(to_register).split('.'))
43+
name = None
44+
for component in name_components:
45+
name = name = '{}.{}'.format(component, name) if name else component
46+
if name not in self._registered:
47+
self._registered[name] = RegistryComponent()
48+
self._registered[name].add(to_register)
49+
50+
def get(self, name: Union[Type[Any], str]) -> Type[Any]:
51+
if isinstance(name, type):
52+
name = self._name_from_class(name)
53+
54+
if name not in self._registered:
55+
raise error.SpannerError(
56+
'{} was not found, verify it has been imported'.format(name))
57+
if len(self._registered[name].references) > 1:
58+
raise error.SpannerError(
59+
'Multiple classes match {}, add more specificity'.format(name))
60+
return self._registered[name].references[0]
61+
62+
63+
_registry = Registry()
64+
65+
66+
def model_registry():
67+
return _registry

spanner_orm/relationship.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import dataclasses
2222
from spanner_orm import error
2323
from spanner_orm import model
24+
from spanner_orm import registry
2425

2526

2627
@dataclasses.dataclass
@@ -67,7 +68,8 @@ def constraints(self) -> List[RelationshipConstraint]:
6768
@property
6869
def destination(self) -> Type[model.Model]:
6970
if not self._destination:
70-
self._destination = model.load_model(self._destination_handle)
71+
self._destination = registry.model_registry().get(
72+
self._destination_handle)
7173
return self._destination
7274

7375
@property

spanner_orm/tests/model_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ def test_relation_get_error_on_unretrieved(self):
124124
with self.assertRaises(AttributeError):
125125
_ = test_model.parent
126126

127+
def test_interleaved(self):
128+
self.assertEqual(models.ChildTestModel.interleaved, models.SmallTestModel)
129+
127130
@mock.patch('spanner_orm.model.ModelMeta.find')
128131
def test_reload(self, find):
129132
values = {'key': 'key', 'value_1': 'value_1'}

spanner_orm/tests/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class ChildTestModel(model.Model):
3333
"""Model class for testing interleaved tables."""
3434

3535
__table__ = 'ChildTestModel'
36-
__interleaved__ = SmallTestModel
36+
__interleaved__ = 'SmallTestModel'
3737

3838
key = field.Field(field.String, primary_key=True)
3939
child_key = field.Field(field.String, primary_key=True)

0 commit comments

Comments
 (0)