Merge lp:~free.ekanayaka/storm/infoheritance into lp:storm
- infoheritance
- Merge into trunk
Status: | Rejected | ||||
---|---|---|---|---|---|
Rejected by: | Free Ekanayaka | ||||
Proposed branch: | lp:~free.ekanayaka/storm/infoheritance | ||||
Merge into: | lp:storm | ||||
Diff against target: |
575 lines (+402/-18) 6 files modified
NEWS (+45/-0) storm/references.py (+159/-16) tests/store/base.py (+187/-2) tests/store/mysql.py (+4/-0) tests/store/postgres.py (+3/-0) tests/store/sqlite.py (+4/-0) |
||||
To merge this branch: | bzr merge lp:~free.ekanayaka/storm/infoheritance | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Niemeyer | Disapprove | ||
Storm Developers | Pending | ||
Review via email: mp+80315@code.launchpad.net |
Commit message
Description of the change
This branch adds polymorphism support to References. See the NEWS file and the Reference docstring for an overview.
In a nutshell: if poly_type is passed to Reference.__init__, it will be treated as a column on the local object that holds an integer value identifying the type of the remote object. A Reference instance built with a non-None poly_type keeps a (lazily built) map of those integer values to the specific Relation objects to use for them.
Gustavo Niemeyer (niemeyer) wrote : | # |
Free Ekanayaka (free.ekanayaka) wrote : | # |
Just to clarify, what do you mean by simplifying the object model? Not using the infoheritance pattern at all?
Gustavo Niemeyer (niemeyer) wrote : | # |
I mean making things straightforward, non-fancy, no magic.
Free Ekanayaka (free.ekanayaka) wrote : | # |
I still don't understand your view. Specifically, I don't get whether you are suggesting to not use the infoheritance pattern at all by arranging your model objects differently, OR to use it but implement it with the non-fancy no-magic way described in the Storm website in the infohertiance page.
What I think is that if we are fine with the infoheritance pattern, or we don't have really sound alternatives (as it's the case with Landscape atm, since we are still using that pattern), then I believe there's nothing too bad in implementing support for it in Storm, which at the end of the day just move the very same non-fancy no-magic user code/logic into Storm, for convenience and sake of code re-use.
Gustavo Niemeyer (niemeyer) wrote : | # |
Yes, I mean making your model match the database content, which is what the "infoheritance" page
demonstrates. The model documented there is the outcome of precisely getting _away_ from internal
support in Storm for what you're adding here, since it was confusing and unnecessarily complex.
You're going backwards in history. Check out old revisions of Landscape and Storm and you'll
notice we've been there, and regretted.
Gustavo Niemeyer (niemeyer) : | # |
Free Ekanayaka (free.ekanayaka) wrote : | # |
Okay, thanks for explaining, I'm heading off for the day but will check the old revisions tomorrow. Not sure if it makes any difference, and maybe it goes without saying, but note that the branch implements exactly the infoheritance approach, where you have a separate "child" class referenced by the parent and holding extra information, and does *not* introduce polymorphism at the class-hierarchy level (the OOP-style), which is for instance the way this implemented in SQLAlchemy. So in practice I'd say it doesn't introduce further abstractions with respect to the infohertiance approach.
Thomas Herve (therve) wrote : | # |
I think there may be some value to extract the infoheritance pattern into some piece of code, for reusability/
Gustavo Niemeyer (niemeyer) wrote : | # |
Free, the beauty of the suggested pattern and the reason we have a page explaining it is precisely
that there's nothing special about it in terms of Storm itself. Storm offers simple building
blocks and it's up to people to put the pattern in place in a way that suits them, possibly with
variations that were not foreseen by us. Hacking that into Storm itself indeed feels like a bad idea.
Free Ekanayaka (free.ekanayaka) wrote : | # |
Thanks Gustavo and Thomas for the feedback. I understand your points they make sense to me. Maybe we could for composition like suggested by Thomas, something that doesn't modify the core of Storm at all, but simply plugs to it. It could be even an external package.
For now, I'm withdrawing the MP.
Unmerged revisions
- 420. By Free Ekanayaka
-
Add polymorphic support to Reference
Preview Diff
1 | === modified file 'NEWS' |
2 | --- NEWS 2011-10-17 18:30:27 +0000 |
3 | +++ NEWS 2011-10-25 10:34:20 +0000 |
4 | @@ -4,6 +4,51 @@ |
5 | Improvements |
6 | ------------ |
7 | |
8 | +- The Reference class now supports polymorphism, by linking to remote objects of |
9 | + different types and from different tables. Its new "poly_type" constructor |
10 | + parameter is used to indicate a column on the local object whose value |
11 | + identifies the type of remote object. This feature can be used to implement |
12 | + the infoheritance pattern, for example: |
13 | + |
14 | + class Person(object): |
15 | + |
16 | + __storm_table__ = "person" |
17 | + id = Int(allow_none=False, primary=True) |
18 | + name = Unicode(allow_none=False) |
19 | + info_type = Int(allow_none=False) |
20 | + info = Reference(id, "person_id", on_remote=True, poly_type=info_type) |
21 | + |
22 | + def __init__(self, name, info_class, **kwargs): |
23 | + self.name = name |
24 | + |
25 | + class PersonInfo(object): |
26 | + |
27 | + person_id = Int(allow_none=False, primary=True) |
28 | + person = Reference(person_id, Person.id) |
29 | + |
30 | + def __init__(self, person): |
31 | + self.person = person |
32 | + |
33 | + @poly_type(1, on=Person.info) |
34 | + class SecretAgent(PersonInfo): |
35 | + |
36 | + __storm_table__ = "secret_agent" |
37 | + passcode = Unicode(allow_none=False) |
38 | + |
39 | + def __init__(self, person, passcode=None): |
40 | + super(SecretAgent, self).__init__(person) |
41 | + self.passcode = passcode |
42 | + |
43 | + @poly_type(2, on=Person.info) |
44 | + class Teacher(PersonInfo): |
45 | + |
46 | + __storm_table__ = "teacher" |
47 | + school = Unicode(allow_none=False) |
48 | + |
49 | + def __init__(self, person, school=None): |
50 | + super(Teacher, self).__init__(person) |
51 | + self.school = school |
52 | + |
53 | Bug fixes |
54 | --------- |
55 | |
56 | |
57 | === modified file 'storm/references.py' |
58 | --- storm/references.py 2010-06-01 08:33:33 +0000 |
59 | +++ storm/references.py 2011-10-25 10:34:20 +0000 |
60 | @@ -111,7 +111,7 @@ |
61 | # warnings!). |
62 | _relation = LazyAttribute("_relation", "_build_relation") |
63 | |
64 | - def __init__(self, local_key, remote_key, on_remote=False): |
65 | + def __init__(self, local_key, remote_key, on_remote=False, poly_type=None): |
66 | """ |
67 | Create a Reference property. |
68 | |
69 | @@ -124,10 +124,15 @@ |
70 | @param on_remote: If specified, then the reference is |
71 | backwards: It is the C{remote_key} which is a foreign key |
72 | onto C{local_key}. |
73 | + @param poly_type: If specified, the sibling column that is used |
74 | + to determine the type of the referred-to object. In this case, |
75 | + the C{remote_key} parameter must be a C{str} which will resolve |
76 | + to the matching column of the referred-to type. |
77 | """ |
78 | self._local_key = local_key |
79 | self._remote_key = remote_key |
80 | self._on_remote = on_remote |
81 | + self._poly_type = poly_type |
82 | self._cls = None |
83 | |
84 | def __get__(self, local, cls=None): |
85 | @@ -141,27 +146,34 @@ |
86 | if local is None: |
87 | return self |
88 | |
89 | - remote = self._relation.get_remote(local) |
90 | + if isinstance(self._relation, Relation): |
91 | + relation = self._relation |
92 | + else: |
93 | + # This means the reference is polymorfic and we have to select |
94 | + # the relation according to the poly type |
95 | + relation = self._select_relation(local) |
96 | + |
97 | + remote = relation.get_remote(local) |
98 | if remote is not None: |
99 | return remote |
100 | |
101 | - if self._relation.local_variables_are_none(local): |
102 | + if relation.local_variables_are_none(local): |
103 | return None |
104 | |
105 | store = Store.of(local) |
106 | if store is None: |
107 | return None |
108 | |
109 | - if self._relation.remote_key_is_primary: |
110 | - remote = store.get(self._relation.remote_cls, |
111 | - self._relation.get_local_variables(local)) |
112 | + if relation.remote_key_is_primary: |
113 | + remote = store.get(relation.remote_cls, |
114 | + relation.get_local_variables(local)) |
115 | else: |
116 | - where = self._relation.get_where_for_remote(local) |
117 | - result = store.find(self._relation.remote_cls, where) |
118 | + where = relation.get_where_for_remote(local) |
119 | + result = store.find(relation.remote_cls, where) |
120 | remote = result.one() |
121 | |
122 | if remote is not None: |
123 | - self._relation.link(local, remote) |
124 | + relation.link(local, remote) |
125 | |
126 | return remote |
127 | |
128 | @@ -172,33 +184,97 @@ |
129 | if self._cls is None: |
130 | self._cls = _find_descriptor_class(local.__class__, self) |
131 | |
132 | + if isinstance(self._relation, Relation): |
133 | + relation = self._relation |
134 | + else: |
135 | + # This means the reference is polymorfic and we have to select |
136 | + # the relation according to the poly type |
137 | + relation = self._select_relation(local, remote) |
138 | + |
139 | if remote is None: |
140 | if self._on_remote: |
141 | remote = self.__get__(local) |
142 | if remote is None: |
143 | return |
144 | else: |
145 | - remote = self._relation.get_remote(local) |
146 | + remote = relation.get_remote(local) |
147 | if remote is None: |
148 | remote_info = None |
149 | else: |
150 | remote_info = get_obj_info(remote) |
151 | - self._relation.unlink(get_obj_info(local), remote_info, True) |
152 | + if self._poly_type: |
153 | + # We also need to reset the poly type |
154 | + local_info = get_obj_info(local) |
155 | + local_info.variables[self._poly_column].set(None) |
156 | + relation.unlink(get_obj_info(local), remote_info, True) |
157 | else: |
158 | # Don't use remote here, as it might be |
159 | # security proxied or something. |
160 | try: |
161 | remote = get_obj_info(remote).get_obj() |
162 | except ClassInfoError: |
163 | - pass # It might fail when remote is a tuple or a raw value. |
164 | - self._relation.link(local, remote, True) |
165 | + pass # It might fail when remote is a tuple or a raw value. |
166 | + relation.link(local, remote, True) |
167 | |
168 | def _build_relation(self): |
169 | resolver = PropertyResolver(self, self._cls) |
170 | self._local_key = resolver.resolve(self._local_key) |
171 | - self._remote_key = resolver.resolve(self._remote_key) |
172 | - self._relation = Relation(self._local_key, self._remote_key, |
173 | - False, self._on_remote) |
174 | + if self._poly_type is None: |
175 | + self._remote_key = resolver.resolve(self._remote_key) |
176 | + self._relation = Relation(self._local_key, self._remote_key, False, |
177 | + self._on_remote) |
178 | + else: |
179 | + # The reference is polymorfic, we will build relations lazily, |
180 | + # saving them in a poly_type -> poly_class mapping |
181 | + self._relation = {} |
182 | + if type(self._remote_key) is not tuple: |
183 | + self._remote_key = tuple((self._remote_key,)) |
184 | + self._poly_column = resolver.resolve_one(self._poly_type) |
185 | + |
186 | + def _select_relation(self, local, remote=None): |
187 | + """ |
188 | + Select the correct L{Relation} object for the polymorphic type at hand, |
189 | + building if it's the first time we hit this type. |
190 | + """ |
191 | + local_info = get_obj_info(local) |
192 | + local_poly_type = local_info.variables[self._poly_column].get() |
193 | + if remote is None: |
194 | + # We're being called from __get__, as poly type we use the value of |
195 | + # the given poly_type column on the local object |
196 | + poly_type = local_poly_type |
197 | + else: |
198 | + # We're being called from __set__, as the poly type we use the |
199 | + # value stored in the remote obect's class by register_poly_type |
200 | + remote_poly_type = getattr(remote.__class__, |
201 | + self._poly_column.name) |
202 | + # Set the poly_type column of the local object to the poly |
203 | + # type value found in the remote object's class |
204 | + local_info.variables[self._poly_column].set(remote_poly_type) |
205 | + poly_type = remote_poly_type |
206 | + |
207 | + relation = self._relation.get(poly_type) |
208 | + |
209 | + if relation is None: |
210 | + # The relation for the given poly type is not in the map, build |
211 | + # it now |
212 | + if remote is None: |
213 | + remote_class = self._poly_lookup[poly_type] |
214 | + else: |
215 | + remote_class = remote.__class__ |
216 | + |
217 | + if hasattr(remote_class, "__storm_table__"): |
218 | + # The remote object is a Storm object, so build a regular |
219 | + # relation |
220 | + resolver = PropertyResolver(self, self._cls) |
221 | + remote_key = resolver.resolve( |
222 | + tuple(getattr(remote_class, k) for k in self._remote_key)) |
223 | + relation = Relation(self._local_key, remote_key, False, |
224 | + self._on_remote) |
225 | + else: |
226 | + relation = RemoteFactory(remote_class, self._poly_column) |
227 | + |
228 | + self._relation[poly_type] = relation |
229 | + return relation |
230 | |
231 | def __eq__(self, other): |
232 | return self._relation.get_where_for_local(other) |
233 | @@ -892,6 +968,26 @@ |
234 | return self._r_to_l.setdefault(local_cls, map).get(remote_column) |
235 | |
236 | |
237 | +class RemoteFactory(object): |
238 | + """Factory for in-memory remote object references.""" |
239 | + |
240 | + def __init__(self, remote_class, poly_column): |
241 | + self._remote_class = remote_class |
242 | + self._local_attr = "__storm_poly_remote_%s_" % poly_column.name |
243 | + |
244 | + def get_remote(self, local): |
245 | + remote = getattr(local, self._local_attr, None) |
246 | + if remote is None: |
247 | + remote = self._remote_class.__new__(self._remote_class) |
248 | + self.link(local, remote, False) |
249 | + return remote |
250 | + |
251 | + def link(self, local, remote, setting): |
252 | + setattr(local, self._local_attr, remote) |
253 | + if not setting and getattr(remote, "link", None) is not None: |
254 | + remote.link(local) |
255 | + |
256 | + |
257 | class PropertyResolver(object): |
258 | """Transform strings and pure properties (non-columns) into columns.""" |
259 | |
260 | @@ -943,3 +1039,50 @@ |
261 | if _descr is descr: |
262 | return getattr(cls, attr) |
263 | raise RuntimeError("Reference used in an unknown class") |
264 | + |
265 | + |
266 | +def register_poly_type(cls, type, on): |
267 | + """Register a new type for a polymorfic reference. |
268 | + |
269 | + @param cls: The class of the new type to register. |
270 | + @param type: A unique identifier for the class, usually an integer value. |
271 | + @param on: The polymorfic L{Reference} to associated the type with. |
272 | + """ |
273 | + if getattr(on, "_poly_lookup", None) is None: |
274 | + on._poly_lookup = {} |
275 | + if type in on._poly_lookup: |
276 | + raise RuntimeError("poly type already registered") |
277 | + on._poly_lookup[type] = cls |
278 | + for attr, value in on._cls.__dict__.iteritems(): |
279 | + if value is on._poly_type: |
280 | + setattr(cls, attr, type) |
281 | + break |
282 | + else: |
283 | + raise RuntimeError("parent attr not found") |
284 | + |
285 | + |
286 | +def unregister_poly_type(cls, on): |
287 | + """Unregister a type of a polymorphic reference.""" |
288 | + for attr, value in on._cls.__dict__.iteritems(): |
289 | + if value is on._poly_type: |
290 | + del on._poly_lookup[getattr(cls, attr)] |
291 | + break |
292 | + else: |
293 | + raise RuntimeError("parent attr not found") |
294 | + |
295 | + |
296 | +def unregister_poly_types(on): |
297 | + """Unregister all types on the given polymorfic reference.""" |
298 | + on._poly_lookup.clear() |
299 | + |
300 | + |
301 | +def poly_type(type, on): |
302 | + """ |
303 | + Decorator used to register a class as a type for a polymorfic reference. |
304 | + """ |
305 | + |
306 | + def register(cls): |
307 | + register_poly_type(cls, type, on) |
308 | + return cls |
309 | + |
310 | + return register |
311 | |
312 | === modified file 'tests/store/base.py' |
313 | --- tests/store/base.py 2011-08-01 12:54:36 +0000 |
314 | +++ tests/store/base.py 2011-10-25 10:34:20 +0000 |
315 | @@ -27,7 +27,9 @@ |
316 | from uuid import uuid4 |
317 | import weakref |
318 | |
319 | -from storm.references import Reference, ReferenceSet, Proxy |
320 | +from storm.references import ( |
321 | + Reference, ReferenceSet, Proxy, poly_type, register_poly_type, |
322 | + unregister_poly_type) |
323 | from storm.database import Result, STATE_DISCONNECTED |
324 | from storm.properties import ( |
325 | Int, Float, RawStr, Unicode, Property, Pickle, UUID) |
326 | @@ -125,6 +127,32 @@ |
327 | foo = Reference(foo_id, Foo.id) |
328 | foo_title = Proxy(foo, Foo.title) |
329 | |
330 | + |
331 | +class Egg(object): |
332 | + __storm_table__ = "egg" |
333 | + id = Int(primary=True, default=AutoReload) |
334 | + info_type = Int() |
335 | + info = Reference(id, "egg_id", on_remote=True, poly_type=info_type) |
336 | + |
337 | + def __init__(self, info_class=None): |
338 | + if info_class is not None: |
339 | + self.info = info_class() |
340 | + |
341 | +@poly_type(1, on=Egg.info) |
342 | +class StoredEggInfo(object): |
343 | + __storm_table__ = "stored_egg_info" |
344 | + egg_id = Int(primary=True) |
345 | + egg = Reference(egg_id, Egg.id) |
346 | + |
347 | +@poly_type(2, on=Egg.info) |
348 | +class MemoryEggInfo(object): |
349 | + def __init__(self): |
350 | + self.created = True |
351 | + |
352 | + def link(self, egg): |
353 | + self.created = False |
354 | + self.egg = egg |
355 | + |
356 | class Money(object): |
357 | __storm_table__ = "money" |
358 | id = Int(primary=True) |
359 | @@ -267,7 +295,7 @@ |
360 | |
361 | def drop_tables(self): |
362 | for table in ["foo", "bar", "bin", "link", "money", "selfref", |
363 | - "foovalue", "unique_id"]: |
364 | + "foovalue", "unique_id", "egg", "stored_egg_info"]: |
365 | try: |
366 | self.connection.execute("DROP TABLE %s" % table) |
367 | self.connection.commit() |
368 | @@ -5756,6 +5784,163 @@ |
369 | self.assertEquals(bar.foo_id, 20) |
370 | self.assertEquals(link.foo_id, 20) |
371 | |
372 | + def test_poly_register(self): |
373 | + """ |
374 | + It's possible to register new polymorphic types using the C{poly_type} |
375 | + decorator. |
376 | + """ |
377 | + self.assertEqual(1, StoredEggInfo.info_type) |
378 | + self.assertEqual(2, MemoryEggInfo.info_type) |
379 | + |
380 | + def test_poly_register_with_duplicate_type(self): |
381 | + """ |
382 | + An error is raised when trying to register the same info type twice |
383 | + for the same Reference. |
384 | + """ |
385 | + |
386 | + class OtherEggInfo(object): |
387 | + pass |
388 | + |
389 | + self.assertRaises(RuntimeError, register_poly_type, OtherEggInfo, |
390 | + MemoryEggInfo.info_type, Egg.info) |
391 | + |
392 | + def test_poly_unregister(self): |
393 | + """ |
394 | + It's possible to unregister a poly type. |
395 | + """ |
396 | + |
397 | + class OtherEggInfo(object): |
398 | + pass |
399 | + |
400 | + register_poly_type(OtherEggInfo, 3, Egg.info) |
401 | + self.assertEqual(3, OtherEggInfo.info_type) |
402 | + self.assertIs(OtherEggInfo, Egg.info._poly_lookup[3]) |
403 | + unregister_poly_type(OtherEggInfo, Egg.info) |
404 | + self.assertIs(None, Egg.info._poly_lookup.get(3)) |
405 | + |
406 | + |
407 | + def test_poly_in_memory_init(self): |
408 | + """ |
409 | + In-memory remote objects are cached while the local object is alive, |
410 | + but when they're recreated, their C{__init__} should not be called |
411 | + again. |
412 | + """ |
413 | + egg = Egg() |
414 | + egg.info = MemoryEggInfo() |
415 | + self.store.add(egg) |
416 | + self.store.flush() |
417 | + egg_id = egg.id |
418 | + self.assertTrue(egg.info.created) |
419 | + self.store.invalidate(egg) |
420 | + del egg |
421 | + gc.collect() |
422 | + egg = self.store.get(Egg, egg_id) |
423 | + self.assertFalse(egg.info.created) |
424 | + self.assertIs(egg, egg.info.egg) |
425 | + |
426 | + def test_poly_wb_relations_are_built_lazily(self): |
427 | + """ |
428 | + The relations for the registered poly types are built lazily. |
429 | + """ |
430 | + Egg.info._relation.clear() |
431 | + egg = Egg() |
432 | + egg.info = StoredEggInfo() |
433 | + self.assertEqual([1], Egg.info._relation.keys()) |
434 | + egg.info = MemoryEggInfo() |
435 | + self.assertEqual(sorted([1, 2]), sorted(Egg.info._relation.keys())) |
436 | + |
437 | + def test_poly_with_type_override(self): |
438 | + """ |
439 | + When the local and remote poly type values don't match, the local one |
440 | + is overriden when setting the reference. |
441 | + """ |
442 | + egg = Egg() |
443 | + egg.info_type = 9999 |
444 | + egg.info = StoredEggInfo() |
445 | + self.assertEqual(StoredEggInfo.info_type, egg.info_type) |
446 | + |
447 | + def test_poly_create_in_memory_object(self): |
448 | + """ |
449 | + When setting the local reference to a new in-memory remote object, the |
450 | + C{link} method of the remote object is not invoked. |
451 | + """ |
452 | + egg = Egg(MemoryEggInfo) |
453 | + self.store.add(egg) |
454 | + self.store.flush() |
455 | + self.assertEqual(MemoryEggInfo.info_type, egg.info_type) |
456 | + self.assertTrue(egg.info.created) |
457 | + |
458 | + def test_poly_link_in_memory_remote(self): |
459 | + """ |
460 | + If the remote object is an in memory object, its C{link} method is |
461 | + invoked when it's linked to a Storm object loaded from the store. |
462 | + """ |
463 | + egg = Egg() |
464 | + egg.info_type = MemoryEggInfo.info_type |
465 | + self.store.add(egg) |
466 | + self.store.flush() |
467 | + self.assertFalse(egg.info.created) |
468 | + self.assertIs(egg, egg.info.egg) |
469 | + |
470 | + def test_poly_create_stored_remote(self): |
471 | + """ |
472 | + When a setting the local reference to a remote object, the remote |
473 | + key is set to the local key. |
474 | + """ |
475 | + egg = Egg() |
476 | + egg_info = StoredEggInfo() |
477 | + egg.info = egg_info |
478 | + self.store.add(egg) |
479 | + self.store.flush() |
480 | + self.assertEqual(StoredEggInfo.info_type, egg.info_type) |
481 | + self.assertEqual(egg.id, egg_info.egg_id) |
482 | + self.assertIs(egg, egg_info.egg) |
483 | + self.assertEqual(egg.id, egg_info.egg.id) |
484 | + |
485 | + def test_poly_link_stored_remote(self): |
486 | + """ |
487 | + When the remote object is a Storm object, the local key value is used |
488 | + to find the matching remote key. |
489 | + """ |
490 | + egg = Egg() |
491 | + egg.id = 1234 |
492 | + egg.info_type = StoredEggInfo.info_type |
493 | + egg_info1 = StoredEggInfo() |
494 | + egg_info1.egg_id = egg.id |
495 | + egg_info2 = StoredEggInfo() |
496 | + egg_info2.egg_id = 9999 |
497 | + self.store.add(egg) |
498 | + self.store.add(egg_info1) |
499 | + self.store.add(egg_info2) |
500 | + self.store.flush() |
501 | + self.assertIs(egg_info1, egg.info) |
502 | + |
503 | + def test_poly_link_with_change_type(self): |
504 | + """ |
505 | + When changing the type of the linked remote object, the poly type |
506 | + column on the local object gets changed accordingly. |
507 | + """ |
508 | + egg = Egg(StoredEggInfo) |
509 | + self.assertEqual(StoredEggInfo.info_type, egg.info_type) |
510 | + |
511 | + egg.info = MemoryEggInfo() |
512 | + self.assertEqual(MemoryEggInfo.info_type, egg.info_type) |
513 | + |
514 | + def test_poly_with_unlink_stored_remote(self): |
515 | + """ |
516 | + If the reference is linked to a remote object, when setting it to |
517 | + C{None} the poly type on the local object gets reset too. |
518 | + """ |
519 | + egg = Egg(StoredEggInfo) |
520 | + egg.id = 1234 |
521 | + egg_info = egg.info |
522 | + self.assertEqual(StoredEggInfo.info_type, egg.info_type) |
523 | + self.assertEqual(1234, egg_info.egg_id) |
524 | + |
525 | + egg.info = None |
526 | + self.assertEqual(None, egg.info_type) |
527 | + self.assertEqual(None, egg_info.egg_id) |
528 | + |
529 | def test_get_decimal_property(self): |
530 | money = self.store.get(Money, 10) |
531 | self.assertEquals(money.value, decimal.Decimal("12.3455")) |
532 | |
533 | === modified file 'tests/store/mysql.py' |
534 | --- tests/store/mysql.py 2011-02-14 12:17:54 +0000 |
535 | +++ tests/store/mysql.py 2011-10-25 10:34:20 +0000 |
536 | @@ -79,6 +79,10 @@ |
537 | connection.execute("CREATE TABLE unique_id " |
538 | "(id VARCHAR(36) PRIMARY KEY) " |
539 | "ENGINE=InnoDB") |
540 | + connection.execute("CREATE TABLE egg " |
541 | + "(id INT PRIMARY KEY AUTO_INCREMENT," |
542 | + " info_type INT)") |
543 | + connection.execute("CREATE TABLE stored_egg_info (egg_id INT)") |
544 | connection.commit() |
545 | |
546 | |
547 | |
548 | === modified file 'tests/store/postgres.py' |
549 | --- tests/store/postgres.py 2011-02-14 12:17:54 +0000 |
550 | +++ tests/store/postgres.py 2011-10-25 10:34:20 +0000 |
551 | @@ -93,6 +93,9 @@ |
552 | " value1 INTEGER, value2 INTEGER)") |
553 | connection.execute("CREATE TABLE unique_id " |
554 | "(id UUID PRIMARY KEY)") |
555 | + connection.execute("CREATE TABLE egg " |
556 | + "(id SERIAL PRIMARY KEY, info_type INTEGER)") |
557 | + connection.execute("CREATE TABLE stored_egg_info (egg_id INTEGER)") |
558 | connection.commit() |
559 | |
560 | def drop_tables(self): |
561 | |
562 | === modified file 'tests/store/sqlite.py' |
563 | --- tests/store/sqlite.py 2011-02-14 12:17:54 +0000 |
564 | +++ tests/store/sqlite.py 2011-10-25 10:34:20 +0000 |
565 | @@ -65,6 +65,10 @@ |
566 | " value1 INTEGER, value2 INTEGER)") |
567 | connection.execute("CREATE TABLE unique_id " |
568 | "(id VARCHAR PRIMARY KEY)") |
569 | + connection.execute("CREATE TABLE egg " |
570 | + "(id INTEGER PRIMARY KEY, info_type INTEGER)") |
571 | + connection.execute("CREATE TABLE stored_egg_info " |
572 | + "(egg_id INTEGER)") |
573 | connection.commit() |
574 | |
575 | def drop_tables(self): |
FWIW, we had something like this in the initial versions of Storm, before the
project was made public.
We actually used this pattern in Landscape itself for some time, and it created
code that is more involved and thus harder to understand, and we were able to
map the same model to a simpler and much cleaner version that was more explicit
without using that kind of trick, and decided to kill the feature because it
clearly encouraged unnecessary complexity.
I'd resist adding something like that, and would instead attempt to simplify
the object model.
I'll abstain from voting it, though, and will relay to others, but I have a
strong feeling you'll regret having invented this after you use it for some
time in real code.