BeanieODM / beanie

Asynchronous Python ODM for MongoDB
http://beanie-odm.dev/
Apache License 2.0
1.94k stars 203 forks source link

[BUG]: Link fields interference/contamination #433

Closed svaraborut closed 1 year ago

svaraborut commented 1 year ago

Describe the bug When parent documents are implemented and shared among multiple documents, if all documents are initialized, the _link_fields variable of the various documents gets improperly initialized and documents gets contaminated with links from other documents. This has been ovserved in all latest versions.

To Reproduce

import pytest
from beanie import Document, Link

class RootDocument(Document):
    name: str
    link_root: Link[Document]

class ADocument(RootDocument):
    surname: str
    link_a: Link[Document]

    class Settings:
        name = 'B'

class BDocument(RootDocument):
    email: str
    link_b: Link[Document]

    class Settings:
        name = 'B'

# >>> FIX

DOCS = [
    RootDocument,
    ADocument,
    BDocument,
]

@pytest.fixture(autouse=True)
async def configure():
    # todo : DRY
    import motor.motor_asyncio
    from beanie import init_beanie
    from api.seed.env import Environ

    client = motor.motor_asyncio.AsyncIOMotorClient(Environ.MONGO_URL, tz_aware=True)
    db = client.get_default_database()
    await init_beanie(database=db, document_models=DOCS)

@pytest.fixture(autouse=True, scope='function')
async def clear(configure):
    for doc in DOCS:
        await doc.delete_all()

# >>> TEST

async def test_query_composition():
    SYS = {'id', 'revision_id'}

    # Simple fields are initialized using the pydantic __fields__ internal property
    # such fields are properly isolated when multi inheritance is involved.
    assert set(RootDocument.__fields__.keys()) == SYS | {'name', 'link_root'}
    assert set(ADocument.__fields__.keys()) == SYS | {'name', 'link_root', 'surname', 'link_a'}
    assert set(BDocument.__fields__.keys()) == SYS | {'name', 'link_root', 'email', 'link_b'}

    # Where Document.init_fields() has a bug that prevents proper link inheritance when parent
    # documents are initialized. Furthermore, some-why BDocument._link_fields are not deterministic
    assert set(RootDocument._link_fields.keys()) == {'link_root'}
    assert set(ADocument._link_fields.keys()) == {'link_root', 'link_a'}
    assert set(BDocument._link_fields.keys()) == {'link_root', 'link_b'}

Expected behavior Each document should know/use only own and inherited links.

Additional context We have implemented different "base documents" that gets then mixed and matched to compose new documents, this alow us to structure documents and better isolate functionalities. When beanie is initialized we initialize all of them, to better power queries.

Here RootDocument is not a real document as it lacks of any collection name, but is shared among multiple documents. This document is initialized to trigger the internal field initialization process and allow us to implement reusable queries that use directly the root document field RootDocument.name == 'beanie', or implement root functions:

class RootDocument(Document):
    ...

    @classmethod
    def find_by_name(cls, name):
        return cls.find(cls.name == name)

Our current fixes are:

# Fix for 1.11.9
from beanie import Document as _Document
class Document(_Document):
    @classmethod
    def init_fields(cls) -> None:
        cls._link_fields = {}
        super().init_fields()

# Fix for 1.15.4
from beanie.odm.utils.init import Initializer
old = Initializer.init_document_fields
def patch(x, cls):
    cls._link_fields = {}
    old(cls)
Initializer.init_document_fields = patch

But why is this check performed?

roman-right commented 1 year ago

Hey,

Thank you for the issue. I'll try to fix it asap.

roman-right commented 1 year ago

Hey @svaraborut , Please try 1.16.2