且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

重复的ndb.StructuredProperty验证程序无法触发

更新时间: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])
Traceback(最近一次调用最后一次):
< snip>
文件models.py,第60行,在remove_duplicates中
举例异常('重复')
异常:重复
$ p $然而,创建一个空的 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 会像之前一样触发验证:

 &GT; 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 应该允许您的测试通过。



我不知道为什么ndb的属性验证行为如此,可以说这是一个错误,或者至少是没有记录的行为。

strong>编辑:

正如@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 this Member 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 a Member 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 a Member 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