镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

google-cloud-ndb: Expando dynamic property updates are not persisted by put() #18204

Open

Description

@samuel100u

google-cloud-ndb==2.3.0, Python 3.11.

If I assign a dynamic property on an Expando and put(), the first write sticks. Changing that same property later and put() again looks fine (no error; in-process it even reads as the new value) but a refetch from datastore still has the old number.

Declared properties on the class (e.g. FloatProperty) update correctly. This also worked on the old App Engine ndb.

from google.cloud import ndb

class Item(ndb.Expando):
    name = ndb.StringProperty()

# ndb client context already active
ent = Item(name="x")
ent.extra = 2.00
key = ent.put()

ent = key.get()
ent.extra = 9.99
ent.put()

ent = key.get()
print(ent.extra)  # 2.00, expected 9.99

Expando.__setattr__ looks like the cause. If the name is already in _properties it calls super().__setattr__. For a class-level Property that hits the descriptor and updates _values. For a dynamic name there is no descriptor on the class, so it lands in __dict__ and put() serializes the stale _values.

Is this expected? I couldn't find it documented. The old runtime always went through _set_value for non-class names.

Opened here because https://github.com/googleapis/python-ndb is archived.

Activity

  1. CaptainAni187 commented on Sep 8, 2026

    @CaptainAni187

    Your read of Expando.__setattr__ is right. Opened #18301.

    The specific culprit is the third condition — it delegates to object.__setattr__ whenever the name is already in _properties, but delegating only works for names with a class level descriptor, since the descriptor is what writes _values. A dynamic name has none, so it goes to __dict__ and _values keeps the old value.

    That's also why it read back correctly in process: __dict__ shadows __getattr__, so the attribute looked updated while put() was still serializing the original.

    Reproduces without Datastore:

    e.extra = 2.0
    e.extra = 9.99
    e.extra            # 9.99
    e._values['extra'] # 2.0

    The fix narrows that check to class level descriptors, matching what __delattr__ just below already does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions