yuval9313 / FastApi-RESTful

Quicker way to develop FastApi
MIT License
180 stars 25 forks source link

[BUG] Annotated is not supported in class based views #177

Open mattmess1221 opened 1 year ago

mattmess1221 commented 1 year ago

The new way to declare a dependency in FastAPI 0.95 is to use arg: Annotated[type, Depends(func)] = default. Using this on a cbv decorated class attribute does not behave as expected. The cbv seems to treat the attribute as if it wasn't annotated at all.

Example:

def my_dependency():
  return "Dependency"

@cbv(router)
class MyView:
  dep: Annotated[str, Depends(my_dependency)
  # behaves as if
  dep: str
yuval9313 commented 1 year ago

I would look into it soon

yuval9313 commented 1 year ago

I've explored this and really didn't encounter any errors, have you updated Fastapi to the latest version available? can you share a gist?

jgentil commented 1 year ago

It's kinda weird, sometimes it works just fine, but other times it really hates it.

Given this little database manager wrapper class:

class DBManager:
    engine: sqlalchemy.ext.asyncio.AsyncEngine
    sessionmaker: sqlalchemy.ext.asyncio.async_sessionmaker[sqlalchemy.ext.asyncio.AsyncSession]

    @classmethod
    def db_init(cls, create_async_engine=sqlalchemy.ext.asyncio.create_async_engine,
                async_sessionmaker=sqlalchemy.ext.asyncio.async_sessionmaker):
        """ Initialize the manager with it's singleton properties
        """
        settings = get_settings()  # just returns a Pydantic settings object

        cls.engine = create_async_engine(
            settings.database_url,
            pool_pre_ping=True,
            echo=settings.debug,
        )
        cls.sessionmaker = async_sessionmaker(
            bind=cls.engine,
            autoflush=False,
            future=True,
            expire_on_commit=False,
        )

    @classmethod
    def get_sessionmaker(cls) -> sqlalchemy.ext.asyncio.async_sessionmaker[sqlalchemy.ext.asyncio.AsyncSession]:
        return cls.sessionmaker

This seems fine:

router = InferringRouter(prefix="/thing", tags=["thing"])

@cbv(router)
class ThingService:
    sessionmaker: async_sessionmaker[AsyncSession] = Depends(DBManager.get_sessionmaker)

but this does not:

router = InferringRouter(prefix="/thing", tags=["thing"])
Sessionmaker = Annotated[async_sessionmaker[AsyncSession], Depends(DBManager.get_sessionmaker)]

@cbv(router)
class ThingService:
    sessionmaker: Sessionmaker

Just to make sure it works with base FastAPI, I tried this and it works fine:

router = APIRouter(prefix="/thing")
Sessionmaker = Annotated[async_sessionmaker[AsyncSession], Depends(DBManager.get_sessionmaker)]

@wallet_router.get("/{thing_id}")
async def test_route(sessionmaker: Sessionmaker, thing_id: int) -> str:
    return f"test: {thing_id}"

The traceback I get when trying the middle one that breaks is:

Traceback (most recent call last):
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/uvicorn/main.py", line 404, in main
    run(
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/uvicorn/main.py", line 569, in run
    server.run()
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/uvicorn/server.py", line 60, in run
    return asyncio.run(self.serve(sockets=sockets))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "uvloop/loop.pyx", line 1517, in uvloop.loop.Loop.run_until_complete
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/uvicorn/server.py", line 67, in serve
    config.load()
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/uvicorn/config.py", line 477, in load
    self.loaded_app = import_from_string(self.app)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/uvicorn/importer.py", line 21, in import_from_string
    module = importlib.import_module(module_str)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1206, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1178, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1149, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/Users/jpg/repos/test-proj/service.py", line 82, in <module>
    @cbv(wallet_router)
     ^^^^^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi_restful/cbv.py", line 30, in decorator
    return _cbv(router, cls, *urls)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi_restful/cbv.py", line 41, in _cbv
    _register_endpoints(router, cls, *urls)
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi_restful/cbv.py", line 117, in _register_endpoints
    router.include_router(cbv_router)
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi/routing.py", line 806, in include_router
    self.add_api_route(
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi/routing.py", line 608, in add_api_route
    route = route_class(
            ^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi/routing.py", line 454, in __init__
    self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi/dependencies/utils.py", line 310, in get_dependant
    sub_dependant = get_param_sub_dependant(
                    ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi/dependencies/utils.py", line 123, in get_param_sub_dependant
    return get_sub_dependant(
           ^^^^^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi/dependencies/utils.py", line 159, in get_sub_dependant
    sub_dependant = get_dependant(
                    ^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi/dependencies/utils.py", line 303, in get_dependant
    type_annotation, depends, param_field = analyze_param(
                                            ^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi/dependencies/utils.py", line 462, in analyze_param
    field = create_response_field(
            ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jpg/Library/Caches/pypoetry/virtualenvs/test-proj-RbGL24dd-py3.11/lib/python3.11/site-packages/fastapi/utils.py", line 92, in create_response_field
    return ModelField(
           ^^^^^^^^^^^
  File "pydantic/fields.py", line 436, in pydantic.fields.ModelField.__init__
  File "pydantic/fields.py", line 552, in pydantic.fields.ModelField.prepare
  File "pydantic/fields.py", line 755, in pydantic.fields.ModelField._type_analysis
TypeError: Fields of type "<class 'sqlalchemy.ext.asyncio.session.async_sessionmaker'>" are not supported.
jgentil commented 1 year ago

This doesn't work either, but it doesn't cause a traceback:

class GlobalSettings(BaseSettings):
    debug: bool

def get_settings() -> BaseSettings:
    return GlobalSettings()

Settings = Annotated[GlobalSettings, Depends(get_settings)]
router = InferringRouter(prefix="/thing", tags=["thing"])

@cbv(router)
class ThingService:
    settings: Settings

Instead it weirdly changes all of the methods registered in ThingService to expect a GlobalSettings instance as it's input parameter. The generated OpenAPI documentation is very weird suddenly.

At first I thought it worked fine but it does not.

lelit commented 9 months ago

I'm facing this, trying to simplify some type signatures using Annotated.

Any news?