更新时间:2023-12-01 22:14:46
在 models.py
中给出以下代码:
class Member(ndb.Model):
name = ndb.StringProperty()
$ b def remove_duplicates(prop,value):
举例Exception('Duplicate')
class Club1(ndb.Model):
members = ndb.StructuredProperty(Member,repeated = True ,validator = remove_duplicates)
我可以创建一个 Member
instance
> m = Member(name ='Alice')
创建一个 Club1
这个成员
实例会触发验证:
> c1 = models.Club1(members = [m])
$ p $然而,创建一个空的
Traceback(最近一次调用最后一次):
< snip>
文件models.py,第60行,在remove_duplicates中
举例异常('重复')
异常:重复Club1
实例,然后附加一个Member
>不会:这实际上是您的测试用例。> c1 = models.Club1()
> c1.members.append(m)
> c1.put()
Key('Club1',6682831673622528)code> ndb.StructuredProperty 并将验证放在子类中:
class MembersStructuredProperty ndb.StructuredProperty):
$ b $ def _validate(self,value):
异常('重复')
类Club2(ndb.Model ):
members = MembersStructuredProperty(Member,repeated = True)创建一个
Club2
实例与一个Member
会像之前一样触发验证:> c2 = models.Club2(members = [m])
Traceback(最近的最后一次调用):
< snip>
在_validate
中引用models.py,第56行,引发异常('重复')
例外:重复现在如此追加一个
Member
,然后尝试写入数据存储区:> c2 = models.Club2()
> c2.members.append(m)
> c2.put()
Traceback(最近一次调用最后一次):
< snip>
在_validate
中引用models.py,第56行,引发异常('重复')
例外:重复因此,子类化
ndb.StructuredProperty
应该允许您的测试通过。strong>编辑:
我不知道为什么ndb的属性验证行为如此,可以说这是一个错误,或者至少是没有记录的行为。正如@DanCornilescu在评论中指出的,这是 SDK中的已知错误
Here is my ndb Model
from google.appengine.ext import ndb from mainsite.rainbow.models.CFCSocialUser import CFCSocialUser class CFCSocialGroup(ndb.Model): def remove_duplicate(self, value): raise Exception("Duplicate user detected") name = ndb.StringProperty(required=True) created_on = ndb.DateTimeProperty(auto_now_add=True) updated_on = ndb.DateTimeProperty(auto_now=True) created_by = ndb.StructuredProperty(CFCSocialUser) members = ndb.StructuredProperty(CFCSocialUser, repeated=True, validator=remove_duplicate) @staticmethod def create_group(name): """Create a new group""" group = CFCSocialGroup(name=name) return group def add_member(self, social_user): """Add a member to the local group""" self.members.append(social_user)
I am trying to ensure that I do not add the same user to a given group. So I trying to validate the value of members property (StructuredProperty).
My tests is
from unittest import TestCase from mainsite.rainbow.models.CFCSocialGroup import CFCSocialGroup from tests.test_CFCSocialUser import create_user from tests.cfcsocialtests.testbase import CFCTestBase_NDB from nose.tools import * from nose.plugins.attrib import attr class TestCFCSocialGroup(CFCTestBase_NDB): @attr("CRUD") @raises(Exception) def test_duplicate_addition(self): """Test to detect duplicate users in groups""" user1 = create_user() user2 = create_user() group = CFCSocialGroup.create_group('Group1') group.add_member(user1) group.add_member(user2)
The test fails to raise an exception.
Here is the debug code
FAILED (errors=1) MacBook-Pro:tests vinay$ nosetests -v test_CFCSocialGroup.py Test to detect duplicate users in groups ... FAIL ====================================================================== FAIL: Test to detect duplicate users in groups ---------------------------------------------------------------------- Traceback (most recent call last): File "/Library/Python/2.7/site-packages/nose/tools/nontrivial.py", line 67, in newfunc raise AssertionError(message) AssertionError: test_duplicate_addition() did not raise Exception -------------------- >> begin captured logging << -------------------- root: DEBUG: Using threading.local root: WARNING: No ssl package found. urlfetch will not be able to validate SSL certificates. root: DEBUG: all_pending: add <Future 10d0d2e90 created by _put_async(model.py:3467) for tasklet put(context.py:787); pending> root: DEBUG: nowevent: _help_tasklet_along root: DEBUG: Sending None to initial generator put(context.py:787) root: DEBUG: all_pending: add <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_set_tasklet).add(('NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw', 0), ('set', 32, '', None)); pending> root: DEBUG: AutoBatcher(_memcache_set_tasklet): creating new queue for ('set', 32, '', None) root: DEBUG: initial generator put(context.py:787) yielded <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_set_tasklet).add(('NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw', 0), ('set', 32, '', None)); pending> root: DEBUG: <Future 10d0d2e90 created by _put_async(model.py:3467) for tasklet put(context.py:787) suspended generator put(context.py:810); pending> is now blocked waiting for <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_set_tasklet).add(('NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw', 0), ('set', 32, '', None)); pending> root: DEBUG: idler: _on_idle root: DEBUG: AutoBatcher(_memcache_set_tasklet): 1 items root: DEBUG: all_pending: add <Future 10d0ec250 created by run_queue(context.py:185) for tasklet _memcache_set_tasklet(context.py:1111); pending> root: DEBUG: nowevent: _help_tasklet_along root: DEBUG: Sending None to initial generator _memcache_set_tasklet(context.py:1111) root: DEBUG: initial generator _memcache_set_tasklet(context.py:1111) yielded <google.appengine.api.apiproxy_stub_map.UserRPC object at 0x10d0ec490> root: DEBUG: idler: _on_idle root: DEBUG: idler _on_idle removed root: DEBUG: rpc: memcache.Set root: DEBUG: Sending {'NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw': 1} to suspended generator _memcache_set_tasklet(context.py:1122) root: DEBUG: all_pending: success: remove <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_set_tasklet).add(('NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw', 0), ('set', 32, '', None)); result True> root: DEBUG: suspended generator _memcache_set_tasklet(context.py:1122) returned None root: DEBUG: all_pending: success: remove <Future 10d0ec250 created by run_queue(context.py:185) for tasklet _memcache_set_tasklet(context.py:1111); result None> root: DEBUG: nowevent: _on_future_completion root: DEBUG: <Future 10d0d2e90 created by _put_async(model.py:3467) for tasklet put(context.py:787); pending> is no longer blocked waiting for <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_set_tasklet).add(('NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw', 0), ('set', 32, '', None)); result True> root: DEBUG: Sending True to suspended generator put(context.py:810) root: DEBUG: all_pending: add <Future 10d0ec510 created by add(context.py:211) for AutoBatcher(_put_tasklet).add(CFCSocialUser(key=Key('CFCSocialUser', 'Vinay Joseph'), date_of_birth=datetime.date(1900, 3, 2), email='vinay@vinayjoseph.com', username='Vinay Joseph'), None); pending> root: DEBUG: AutoBatcher(_put_tasklet): creating new queue for None root: DEBUG: suspended generator put(context.py:810) yielded <Future 10d0ec510 created by add(context.py:211) for AutoBatcher(_put_tasklet).add(CFCSocialUser(key=Key('CFCSocialUser', 'Vinay Joseph'), date_of_birth=datetime.date(1900, 3, 2), email='vinay@vinayjoseph.com', username='Vinay Joseph'), None); pending> root: DEBUG: <Future 10d0d2e90 created by _put_async(model.py:3467) for tasklet put(context.py:787) suspended generator put(context.py:824); pending> is now blocked waiting for <Future 10d0ec510 created by add(context.py:211) for AutoBatcher(_put_tasklet).add(CFCSocialUser(key=Key('CFCSocialUser', 'Vinay Joseph'), date_of_birth=datetime.date(1900, 3, 2), email='vinay@vinayjoseph.com', username='Vinay Joseph'), None); pending> root: DEBUG: nowevent: _finished_callback root: DEBUG: idler: _on_idle root: DEBUG: AutoBatcher(_put_tasklet): 1 items root: DEBUG: all_pending: add <Future 10d0ec610 created by run_queue(context.py:185) for tasklet _put_tasklet(context.py:348); pending> root: DEBUG: nowevent: _help_tasklet_along root: DEBUG: Sending None to initial generator _put_tasklet(context.py:348) root: DEBUG: initial generator _put_tasklet(context.py:348) yielded <google.appengine.api.apiproxy_stub_map.UserRPC object at 0x10d0ec890> root: DEBUG: idler: _on_idle root: DEBUG: idler _on_idle removed root: DEBUG: rpc: datastore_v3.Put root: DEBUG: Sending [Key('CFCSocialUser', 'Vinay Joseph')] to suspended generator _put_tasklet(context.py:358) root: DEBUG: all_pending: success: remove <Future 10d0ec510 created by add(context.py:211) for AutoBatcher(_put_tasklet).add(CFCSocialUser(key=Key('CFCSocialUser', 'Vinay Joseph'), date_of_birth=datetime.date(1900, 3, 2), email='vinay@vinayjoseph.com', username='Vinay Joseph'), None); result Key('CFCSocialUser', 'Vinay Joseph')> root: DEBUG: suspended generator _put_tasklet(context.py:358) returned None root: DEBUG: all_pending: success: remove <Future 10d0ec610 created by run_queue(context.py:185) for tasklet _put_tasklet(context.py:348); result None> root: DEBUG: nowevent: _on_future_completion root: DEBUG: <Future 10d0d2e90 created by _put_async(model.py:3467) for tasklet put(context.py:787); pending> is no longer blocked waiting for <Future 10d0ec510 created by add(context.py:211) for AutoBatcher(_put_tasklet).add(CFCSocialUser(key=Key('CFCSocialUser', 'Vinay Joseph'), date_of_birth=datetime.date(1900, 3, 2), email='vinay@vinayjoseph.com', username='Vinay Joseph'), None); result Key('CFCSocialUser', 'Vinay Joseph')> root: DEBUG: Sending Key('CFCSocialUser', 'Vinay Joseph') to suspended generator put(context.py:824) root: DEBUG: all_pending: add <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_del_tasklet).add(NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw, (0, '', None)); pending> root: DEBUG: AutoBatcher(_memcache_del_tasklet): creating new queue for (0, '', None) root: DEBUG: suspended generator put(context.py:824) yielded <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_del_tasklet).add(NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw, (0, '', None)); pending> root: DEBUG: <Future 10d0d2e90 created by _put_async(model.py:3467) for tasklet put(context.py:787) suspended generator put(context.py:833); pending> is now blocked waiting for <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_del_tasklet).add(NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw, (0, '', None)); pending> root: DEBUG: nowevent: _finished_callback root: DEBUG: idler: _on_idle root: DEBUG: AutoBatcher(_memcache_del_tasklet): 1 items root: DEBUG: all_pending: add <Future 10d0ec850 created by run_queue(context.py:185) for tasklet _memcache_del_tasklet(context.py:1130); pending> root: DEBUG: nowevent: _help_tasklet_along root: DEBUG: Sending None to initial generator _memcache_del_tasklet(context.py:1130) root: DEBUG: initial generator _memcache_del_tasklet(context.py:1130) yielded <google.appengine.api.apiproxy_stub_map.UserRPC object at 0x10d0ec210> root: DEBUG: idler: _on_idle root: DEBUG: idler _on_idle removed root: DEBUG: rpc: memcache.Delete root: DEBUG: Sending [2] to suspended generator _memcache_del_tasklet(context.py:1141) root: DEBUG: all_pending: success: remove <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_del_tasklet).add(NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw, (0, '', None)); result 2> root: DEBUG: suspended generator _memcache_del_tasklet(context.py:1141) returned None root: DEBUG: all_pending: success: remove <Future 10d0ec850 created by run_queue(context.py:185) for tasklet _memcache_del_tasklet(context.py:1130); result None> root: DEBUG: nowevent: _on_future_completion root: DEBUG: <Future 10d0d2e90 created by _put_async(model.py:3467) for tasklet put(context.py:787); pending> is no longer blocked waiting for <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_del_tasklet).add(NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw, (0, '', None)); result 2> root: DEBUG: Sending 2 to suspended generator put(context.py:833) root: DEBUG: suspended generator put(context.py:833) returned Key('CFCSocialUser', 'Vinay Joseph') root: DEBUG: all_pending: success: remove <Future 10d0d2e90 created by _put_async(model.py:3467) for tasklet put(context.py:787); result Key('CFCSocialUser', 'Vinay Joseph')> root: DEBUG: all_pending: add <Future 10d0ec150 created by _put_async(model.py:3467) for tasklet put(context.py:787); pending> root: DEBUG: nowevent: _finished_callback root: DEBUG: nowevent: _help_tasklet_along root: DEBUG: Sending None to initial generator put(context.py:787) root: DEBUG: all_pending: add <Future 10d0ec990 created by add(context.py:211) for AutoBatcher(_memcache_set_tasklet).add(('NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw', 0), ('set', 32, '', None)); pending> root: DEBUG: AutoBatcher(_memcache_set_tasklet): creating new queue for ('set', 32, '', None) root: DEBUG: initial generator put(context.py:787) yielded <Future 10d0ec990 created by add(context.py:211) for AutoBatcher(_memcache_set_tasklet).add(('NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw', 0), ('set', 32, '', None)); pending> root: DEBUG: <Future 10d0ec150 created by _put_async(model.py:3467) for tasklet put(context.py:787) suspended generator put(context.py:810); pending> is now blocked waiting for <Future 10d0ec990 created by add(context.py:211) for AutoBatcher(_memcache_set_tasklet).add(('NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw', 0), ('set', 32, '', None)); pending> root: DEBUG: idler: _on_idle root: DEBUG: AutoBatcher(_memcache_set_tasklet): 1 items root: DEBUG: all_pending: add <Future 10d0ecd10 created by run_queue(context.py:185) for tasklet _memcache_set_tasklet(context.py:1111); pending> root: DEBUG: nowevent: _help_tasklet_along root: DEBUG: Sending None to initial generator _memcache_set_tasklet(context.py:1111) root: DEBUG: initial generator _memcache_set_tasklet(context.py:1111) yielded <google.appengine.api.apiproxy_stub_map.UserRPC object at 0x10d0ecf50> root: DEBUG: idler: _on_idle root: DEBUG: idler _on_idle removed root: DEBUG: rpc: memcache.Set root: DEBUG: Sending {'NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw': 1} to suspended generator _memcache_set_tasklet(context.py:1122) root: DEBUG: all_pending: success: remove <Future 10d0ec990 created by add(context.py:211) for AutoBatcher(_memcache_set_tasklet).add(('NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw', 0), ('set', 32, '', None)); result True> root: DEBUG: suspended generator _memcache_set_tasklet(context.py:1122) returned None root: DEBUG: all_pending: success: remove <Future 10d0ecd10 created by run_queue(context.py:185) for tasklet _memcache_set_tasklet(context.py:1111); result None> root: DEBUG: nowevent: _on_future_completion root: DEBUG: <Future 10d0ec150 created by _put_async(model.py:3467) for tasklet put(context.py:787); pending> is no longer blocked waiting for <Future 10d0ec990 created by add(context.py:211) for AutoBatcher(_memcache_set_tasklet).add(('NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw', 0), ('set', 32, '', None)); result True> root: DEBUG: Sending True to suspended generator put(context.py:810) root: DEBUG: all_pending: add <Future 10d0ecfd0 created by add(context.py:211) for AutoBatcher(_put_tasklet).add(CFCSocialUser(key=Key('CFCSocialUser', 'Vinay Joseph'), date_of_birth=datetime.date(1900, 3, 2), email='vinay@vinayjoseph.com', username='Vinay Joseph'), None); pending> root: DEBUG: AutoBatcher(_put_tasklet): creating new queue for None root: DEBUG: suspended generator put(context.py:810) yielded <Future 10d0ecfd0 created by add(context.py:211) for AutoBatcher(_put_tasklet).add(CFCSocialUser(key=Key('CFCSocialUser', 'Vinay Joseph'), date_of_birth=datetime.date(1900, 3, 2), email='vinay@vinayjoseph.com', username='Vinay Joseph'), None); pending> root: DEBUG: <Future 10d0ec150 created by _put_async(model.py:3467) for tasklet put(context.py:787) suspended generator put(context.py:824); pending> is now blocked waiting for <Future 10d0ecfd0 created by add(context.py:211) for AutoBatcher(_put_tasklet).add(CFCSocialUser(key=Key('CFCSocialUser', 'Vinay Joseph'), date_of_birth=datetime.date(1900, 3, 2), email='vinay@vinayjoseph.com', username='Vinay Joseph'), None); pending> root: DEBUG: nowevent: _finished_callback root: DEBUG: idler: _on_idle root: DEBUG: AutoBatcher(_put_tasklet): 1 items root: DEBUG: all_pending: add <Future 10d132110 created by run_queue(context.py:185) for tasklet _put_tasklet(context.py:348); pending> root: DEBUG: nowevent: _help_tasklet_along root: DEBUG: Sending None to initial generator _put_tasklet(context.py:348) root: DEBUG: initial generator _put_tasklet(context.py:348) yielded <google.appengine.api.apiproxy_stub_map.UserRPC object at 0x10d132450> root: DEBUG: idler: _on_idle root: DEBUG: idler _on_idle removed root: DEBUG: rpc: datastore_v3.Put root: DEBUG: Sending [Key('CFCSocialUser', 'Vinay Joseph')] to suspended generator _put_tasklet(context.py:358) root: DEBUG: all_pending: success: remove <Future 10d0ecfd0 created by add(context.py:211) for AutoBatcher(_put_tasklet).add(CFCSocialUser(key=Key('CFCSocialUser', 'Vinay Joseph'), date_of_birth=datetime.date(1900, 3, 2), email='vinay@vinayjoseph.com', username='Vinay Joseph'), None); result Key('CFCSocialUser', 'Vinay Joseph')> root: DEBUG: suspended generator _put_tasklet(context.py:358) returned None root: DEBUG: all_pending: success: remove <Future 10d132110 created by run_queue(context.py:185) for tasklet _put_tasklet(context.py:348); result None> root: DEBUG: nowevent: _on_future_completion root: DEBUG: <Future 10d0ec150 created by _put_async(model.py:3467) for tasklet put(context.py:787); pending> is no longer blocked waiting for <Future 10d0ecfd0 created by add(context.py:211) for AutoBatcher(_put_tasklet).add(CFCSocialUser(key=Key('CFCSocialUser', 'Vinay Joseph'), date_of_birth=datetime.date(1900, 3, 2), email='vinay@vinayjoseph.com', username='Vinay Joseph'), None); result Key('CFCSocialUser', 'Vinay Joseph')> root: DEBUG: Sending Key('CFCSocialUser', 'Vinay Joseph') to suspended generator put(context.py:824) root: DEBUG: all_pending: add <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_del_tasklet).add(NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw, (0, '', None)); pending> root: DEBUG: AutoBatcher(_memcache_del_tasklet): creating new queue for (0, '', None) root: DEBUG: suspended generator put(context.py:824) yielded <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_del_tasklet).add(NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw, (0, '', None)); pending> root: DEBUG: <Future 10d0ec150 created by _put_async(model.py:3467) for tasklet put(context.py:787) suspended generator put(context.py:833); pending> is now blocked waiting for <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_del_tasklet).add(NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw, (0, '', None)); pending> root: DEBUG: nowevent: _finished_callback root: DEBUG: idler: _on_idle root: DEBUG: AutoBatcher(_memcache_del_tasklet): 1 items root: DEBUG: all_pending: add <Future 10d0eca10 created by run_queue(context.py:185) for tasklet _memcache_del_tasklet(context.py:1130); pending> root: DEBUG: nowevent: _help_tasklet_along root: DEBUG: Sending None to initial generator _memcache_del_tasklet(context.py:1130) root: DEBUG: initial generator _memcache_del_tasklet(context.py:1130) yielded <google.appengine.api.apiproxy_stub_map.UserRPC object at 0x10d0ece10> root: DEBUG: idler: _on_idle root: DEBUG: idler _on_idle removed root: DEBUG: rpc: memcache.Delete root: DEBUG: Sending [2] to suspended generator _memcache_del_tasklet(context.py:1141) root: DEBUG: all_pending: success: remove <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_del_tasklet).add(NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw, (0, '', None)); result 2> root: DEBUG: suspended generator _memcache_del_tasklet(context.py:1141) returned None root: DEBUG: all_pending: success: remove <Future 10d0eca10 created by run_queue(context.py:185) for tasklet _memcache_del_tasklet(context.py:1130); result None> root: DEBUG: nowevent: _on_future_completion root: DEBUG: <Future 10d0ec150 created by _put_async(model.py:3467) for tasklet put(context.py:787); pending> is no longer blocked waiting for <Future 10b0ae1d0 created by add(context.py:211) for AutoBatcher(_memcache_del_tasklet).add(NDB9:agx0ZXN0YmVkLXRlc3RyHwsSDUNGQ1NvY2lhbFVzZXIiDFZpbmF5IEpvc2VwaAw, (0, '', None)); result 2> root: DEBUG: Sending 2 to suspended generator put(context.py:833) root: DEBUG: suspended generator put(context.py:833) returned Key('CFCSocialUser', 'Vinay Joseph') root: DEBUG: all_pending: success: remove <Future 10d0ec150 created by _put_async(model.py:3467) for tasklet put(context.py:787); result Key('CFCSocialUser', 'Vinay Joseph')> --------------------- >> end captured logging << --------------------- ---------------------------------------------------------------------- Ran 1 test in 0.035s FAILED (failures=1)
Given this code in
models.py
:class Member(ndb.Model): name = ndb.StringProperty() def remove_duplicates(prop, value): raise Exception('Duplicate') class Club1(ndb.Model): members = ndb.StructuredProperty(Member, repeated=True, validator=remove_duplicates)
I can create a
Member
instance
> m = Member(name='Alice')
creating a
Club1
instance with thisMember
instance triggers the validation:> c1 = models.Club1(members=[m]) Traceback (most recent call last): <snip> File "models.py", line 60, in remove_duplicates raise Exception('Duplicate') Exception: Duplicate
However, creating an empty
Club1
instance and then appending aMember
does not: this is effectively your test case.> c1 = models.Club1() > c1.members.append(m) > c1.put() Key('Club1', 6682831673622528)
We can subclass
ndb.StructuredProperty
and put the validation in the subclass:class MembersStructuredProperty(ndb.StructuredProperty): def _validate(self, value): raise Exception('Duplicate') class Club2(ndb.Model): members = MembersStructuredProperty(Member, repeated=True)
Creating a
Club2
instance with aMember
triggers the validation as before:> c2 = models.Club2(members=[m]) Traceback (most recent call last): <snip> File "models.py", line 56, in _validate raise Exception('Duplicate') Exception: Duplicate
And now so does appending a
Member
and then trying to write to the Datastore:> c2 = models.Club2() > c2.members.append(m) > c2.put() Traceback (most recent call last): <snip> File "models.py", line 56, in _validate raise Exception('Duplicate') Exception: Duplicate
So subclassing
ndb.StructuredProperty
should allow your test to pass.
I don't know why ndb's property validation behaves like this, arguably it's a bug, or at least undocumented behaviour.EDIT:
As @DanCornilescu observes in the comments, this is a known bug in the SDK