BeanieODM / beanie

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

[BUG] AttributeError: get_motor_collection when setting fetch_links=True #1056

Open tdoan2010 opened 1 month ago

tdoan2010 commented 1 month ago

Describe the bug AttributeError: get_motor_collection when setting fetch_links=True.

To Reproduce I'm using Beanie with FastAPI. For example, my Beanie models look like this:

class Project(Document):
    id: PydanticObjectId = Field(default_factory=PydanticObjectId)
    items: list[Link[ItemDB]] | None = None

class ItemDB(Document):
    creator: Indexed(str)

When getting a Project by ID, I also want to prefetch its related items. So, my endpoint looks like this:

@router.get('/{project_id}', status_code=status.HTTP_200_OK, summary='Get a project',
            response_model_exclude_none=True)
async def get_project(project_id: str) -> Project:
    project = await Project.get(project_id, fetch_links=True)
    if not project:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
    return project

But I got the error:

ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "my-project/.venv/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py", line 419, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/fastapi/applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "my-project/.venv/lib/python3.12/site-packages/starlette/applications.py", line 113, in __call__
    await self.middleware_stack(scope, receive, send)
  File "my-project/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 187, in __call__
    raise exc
  File "my-project/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 165, in __call__
    await self.app(scope, receive, _send)
  File "my-project/.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "my-project/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 62, in wrapped_app
    raise exc
  File "my-project/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 51, in wrapped_app
    await app(scope, receive, sender)
  File "my-project/.venv/lib/python3.12/site-packages/starlette/routing.py", line 715, in __call__
    await self.middleware_stack(scope, receive, send)
  File "my-project/.venv/lib/python3.12/site-packages/starlette/routing.py", line 735, in app
    await route.handle(scope, receive, send)
  File "my-project/.venv/lib/python3.12/site-packages/starlette/routing.py", line 288, in handle
    await self.app(scope, receive, send)
  File "my-project/.venv/lib/python3.12/site-packages/starlette/routing.py", line 76, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "my-project/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 62, in wrapped_app
    raise exc
  File "my-project/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 51, in wrapped_app
    await app(scope, receive, sender)
  File "my-project/.venv/lib/python3.12/site-packages/starlette/routing.py", line 73, in app
    response = await f(request)
               ^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 301, in app
    raw_response = await run_endpoint_function(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 212, in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "my-project/app/routers/project.py", line 46, in get_project
    project = await Project.get(project_id, fetch_links=True)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/beanie/odm/documents.py", line 276, in get
    return await cls.find_one(
           ^^^^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/beanie/odm/queries/find.py", line 1024, in __await__
    document = yield from self._find_one().__await__()  # type: ignore
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/beanie/odm/queries/find.py", line 982, in _find_one
    return await self.document_model.find_many(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/beanie/odm/queries/find.py", line 689, in first_or_none
    res = await self.limit(1).to_list()
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/beanie/odm/queries/cursor.py", line 67, in to_list
    cursor = self.motor_cursor
             ^^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/beanie/odm/queries/find.py", line 661, in motor_cursor
    self.build_aggregation_pipeline()
  File "my-project/.venv/lib/python3.12/site-packages/beanie/odm/queries/find.py", line 610, in build_aggregation_pipeline
    construct_lookup_queries(
  File "my-project/.venv/lib/python3.12/site-packages/beanie/odm/utils/find.py", line 29, in construct_lookup_queries
    construct_query(
  File "my-project/.venv/lib/python3.12/site-packages/beanie/odm/utils/find.py", line 271, in construct_query
    "from": link_info.document_class.get_motor_collection().name,  # type: ignore
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "my-project/.venv/lib/python3.12/site-packages/pydantic/_internal/_model_construction.py", line 242, in __getattr__
    raise AttributeError(item)
AttributeError: get_motor_collection

If I remove fetch_links=True and later on call

await project.fetch_all_links()

then it works fine.

I'm sure that the database was initialized properly because everything else works normally, only this fetch_links has problem. Just to be sure, here is how the database was initialized:

async def init_db(db_url: str):
    client = AsyncIOMotorClient(host=db_url, tz_aware=True)
    await init_beanie(
        database=client.get_default_database(default='myDB'),
        document_models=[Project, ItemDB],
        allow_index_dropping=True
    )

This function is then called in the startup part of FastAPI:

@asynccontextmanager
async def lifespan(app: FastAPI):
    await init_db(settings.db_url)
    yield

app = FastAPI(
    lifespan=lifespan
)

So, did I miss something? Or it's a bug of Beanie? I'm using FastAPI 0.115.2 and Beanie 1.27.0.

Expected behavior Document.get(id, fetch_links=True) should work without error.

github-actions[bot] commented 8 hours ago

This issue is stale because it has been open 30 days with no activity.