Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Inconsistent before_event trigger behavior #1103

Open
paulpage opened this issue Jan 3, 2025 · 1 comment
Open

[BUG] Inconsistent before_event trigger behavior #1103

paulpage opened this issue Jan 3, 2025 · 1 comment

Comments

@paulpage
Copy link

paulpage commented Jan 3, 2025

Describe the bug
I have a before_event annotation to update a field on a document every time it is updated. The behavior the event handling is inconsistent based on what update method I use. I have a full reproduction that shows updating 4 documents in 4 different ways, with differing results:

  1. find_one(...).update(...) - Not triggered, tag field NOT set in db
  2. find_one(...); result.update(...) - Triggered ONCE, tag field NOT set in db
  3. find_one(...); result.set(...) - Triggered ONCE, tag field NOT set in db
  4. find_one(...); result.field = val; result.save() - Triggered TWICE; tag field SET in db

To Reproduce

import asyncio

from beanie import Document, Replace, Save, SaveChanges, Update, before_event
from beanie import init_beanie
from motor.motor_asyncio import AsyncIOMotorClient
from beanie.odm.operators.update.general import Set
from beanie.odm.queries.update import UpdateResponse


class Person(Document):
    name: str
    tag: str | None = None

    @before_event(Save, Update, Replace, SaveChanges)
    def set_tag(self):
        print("    before_event TRIGGERED")
        self.tag = "updated"


async def main():
    client = AsyncIOMotorClient("mongodb://localhost:27017")
    client.get_io_loop = asyncio.get_running_loop
    await init_beanie(
        client["mydb"],
        document_models=[
            Person,
        ],
    )

    print("=== create")
    await Person(name="Alice").insert()
    await Person(name="Bob").insert()
    await Person(name="Charlie").insert()
    await Person(name="Dan").insert()

    print("=== find_one.update")
    result = await Person.find_one(Person.name == "Alice").update(Set({"name": "Alicia"}), response_type=UpdateResponse.NEW_DOCUMENT)
    print(f"    result: {result}")

    print("=== find_one; update")
    result = await Person.find_one(Person.name == "Bob")
    result = await result.update(Set({"name": "Bobby"}))
    print(f"    result: {result}")

    print("=== find_one; set")
    result = await Person.find_one(Person.name == "Charlie")
    result = await result.set({"name": "Charles"})
    print(f"    result: {result}")

    print("=== find_one; save")
    result = await Person.find_one(Person.name == "Dan")
    result.name = "Daniel"
    await result.save()
    print(f"    result: {result}")

    print("=== close")
    client.close()

if __name__ == "__main__":
    asyncio.run(main())

Expected behavior
I'm unsure of whether or not the before_event should be triggered twice during the 4th case (find, update, save()), but for all 4 cases I would expect the before_event to get triggered at least once, and I would expect the final value in the DB to have a "tag": "updated" value.

Additional context
I'm particularly interested in the behavior of the first case, where .update() is called directly on the result of find_one() - I would like to use this pattern with a before_event annotation to automatically set an updatedAt field on my documents.

The repro code can be run with beanie installed and an empty mongodb instance (I used the mongo:5 docker image, but I suspect a local instance would work as well).

Python version: 3.10.10
Beanie version: 1.28.0

@Tatayoyoh
Copy link

Hey there !

I'm facing the same issue than the first use case previously mentionned find_one(...).update(...), with delete action.

I share some more code to test this behavior.

main.py

from fastapi import FastAPI
from contextlib import asynccontextmanager
from beanie import Document, Delete, after_event, init_beanie
from motor.motor_asyncio import AsyncIOMotorClient

class Item(Document):
    name:str = ''

    @after_event(Delete)
    def remove_action(self):
        print('Do something')

@asynccontextmanager
async def init_database_client(app: FastAPI):
    client = AsyncIOMotorClient('mongodb://localhost:27017')
    await init_beanie(
        database = client['after_event_db'], 
        document_models = [Item]
    )
    yield

app = FastAPI(lifespan=init_database_client)

@app.get('/add/{name}')
async def add_item(name:str) -> Item:
    return await Item(name=name).save()

@app.get('/not_working/{name}')
async def not_working(name:str):
    await Item.find_one(Item.name == name).delete()   # ❌ @after_event is NOT triggered
    return {'success':True}

@app.get('/working/{name}')
async def working(name:str):
    item:Item = await Item.find_one(Item.name == name)
    if not item:
        return {'success':False}
    await item.delete()                # ✅ @after_event is triggered
    return {'success':True}

requirements.txt

annotated-types==0.7.0
anyio==4.8.0
beanie==1.29.0
click==8.1.8
dnspython==2.7.0
fastapi==0.115.8
h11==0.14.0
idna==3.10
lazy-model==0.2.0
motor==3.7.0
pydantic==2.10.6
pydantic_core==2.27.2
pymongo==4.11
sniffio==1.3.1
starlette==0.45.3
toml==0.10.2
typing_extensions==4.12.2
uvicorn==0.34.0

Test it

uvicorn main:app --host 0.0.0.0 --port 8123 --reload
# Not working case
curl 'http://localhost:8123/add/myitem'
curl 'http://localhost:8123/not_working/myitem'
# Working case
curl 'http://localhost:8123/add/myitem'
curl 'http://localhost:8123/working/myitem'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants