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

BaseSettings from pydantic-settings can't be used as a dependency #250

Open
peterlandry opened this issue Feb 18, 2025 · 13 comments
Open

BaseSettings from pydantic-settings can't be used as a dependency #250

peterlandry opened this issue Feb 18, 2025 · 13 comments

Comments

@peterlandry
Copy link

Describe the bug
change_version_of_annotation doesn't handle BaseSettings from pydantic-settings.

To Reproduce

from pydantic import Field
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    foo: str = Field(default="bar")

async def settings() -> Settings:
    return Settings()


SettingsDep = Annotated[Settings, Depends(settings)]



app = Cadwyn(versions=VersionBundle(HeadVersion(), Version("2000-01-01")))

router = VersionedAPIRouter()


@router.get("/")
def read_root(settings: SettingsDep):
    assert settings
    return {"foo": settings.foo}

app.generate_and_include_versioned_routers(router)

Expected behavior
The root endpoint should return {"foo": "bar"}, instead I get the following traceback:

Traceback (most recent call last):
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py", line 409, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        self.scope, self.receive, self.send
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/applications.py", line 185, in __call__
    self._cadwyn_initialize()
    ~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/applications.py", line 190, in _cadwyn_initialize
    generated_routers = generate_versioned_routers(
        self._latest_version_router,
        webhooks=self.webhooks,
        versions=self.versions,
    )
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/route_generation.py", line 77, in generate_versioned_routers
    return _EndpointTransformer(router, versions, webhooks).transform()
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/route_generation.py", line 132, in transform
    self.schema_generators[str(version.value)].annotation_transformer.migrate_router_to_version(router)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 447, in migrate_router_to_version
    self.migrate_route_to_version(route)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 459, in migrate_route_to_version
    route.endpoint = self.change_version_of_annotation(route.endpoint)
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 441, in change_version_of_annotation
    return self.change_versions_of_a_non_container_annotation(annotation)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 498, in _change_version_of_a_non_container_annotation
    return self._modify_callable_annotations(
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        annotation,
        ^^^^^^^^^^^
    ...<2 lines>...
        annotation_modifying_wrapper_factory=self._copy_function_through_class_based_wrapper,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 534, in _modify_callable_annotations
    annotation_modifying_wrapper.__annotations__ = modify_annotations(callable_annotations)
                                                   ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 496, in modifier
    return self.change_version_of_annotation(annotation)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 434, in change_version_of_annotation
    self.change_version_of_annotation(key): self.change_version_of_annotation(value)
                                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 441, in change_version_of_annotation
    return self.change_versions_of_a_non_container_annotation(annotation)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 468, in _change_version_of_a_non_container_annotation
    return get_origin(annotation)[tuple(self.change_version_of_annotation(arg) for arg in get_args(annotation))]
                                  ~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 468, in <genexpr>
    return get_origin(annotation)[tuple(self.change_version_of_annotation(arg) for arg in get_args(annotation))]
                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 441, in change_version_of_annotation
    return self.change_versions_of_a_non_container_annotation(annotation)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 488, in _change_version_of_a_non_container_annotation
    return self._change_version_of_type(annotation)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 509, in _change_version_of_type
    return self.generator[annotation]
           ~~~~~~~~~~~~~~^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 638, in __getitem__
    model_copy = wrapper.generate_model_copy(self)
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 360, in generate_model_copy
    tuple(generator[cast(type[BaseModel], base)] for base in self.cls.__bases__),
    ~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 360, in <genexpr>
    tuple(generator[cast(type[BaseModel], base)] for base in self.cls.__bases__),
          ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 635, in __getitem__
    wrapper = self._get_wrapper_for_model(model)
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 658, in _get_wrapper_for_model
    wrapper = _wrap_pydantic_model(model)
  File "/Users/peterlandry/repos/cadwyn-bug/.venv/lib/python3.13/site-packages/cadwyn/schema_generation.py", line 243, in _wrap_pydantic_model
    field_name: PydanticFieldWrapper(model.model_fields[field_name], model.__annotations__[field_name], field_name)
                                     ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
KeyError: 'model_config'

I've tried a few different ways of setting up the dependency, but every time cadwyn hits it I get a similar error.

@zmievsa
Copy link
Owner

zmievsa commented Feb 18, 2025

Thanks for reporting this! I'll try to fix it this Friday. Really nice minimal reproducible sample too!

@peterlandry
Copy link
Author

Only did a bit of a poke, but nothing jumped out at me as to a cause. Using a model_config on a standard BaseModel works fine.

If you point me at a theory or cause, I might have a chance to get a PR up for you to look at.

@zmievsa
Copy link
Owner

zmievsa commented Feb 18, 2025

I guess it's trying to wrap BaseSettings (to build its own wrapper around the model so that you could perform migrations over it) which Cadwyn is not supposed to do. _wrap_pydantic_model has special casing for BaseModel and I guess it should have the same special casing for BaseSettings. But this is just a theory.

@peterlandry
Copy link
Author

Minimal fix in cadwyn.schema_generation.SchemaGenerator is

    def __getitem__(self, model: type[_T_ANY_MODEL], /) -> type[_T_ANY_MODEL]:
        if (
            not isinstance(model, type)
            or not lenient_issubclass(model, BaseModel | Enum)
            or model in (BaseModel, RootModel, BaseSettings)
        ):
         ...

But this requires importing BaseSettings from pydantic_settings. Looking to see if there's another way to identify that cadwyn shouldn't wrap the model that isn't reliant on a third party type.

@zmievsa
Copy link
Owner

zmievsa commented Feb 18, 2025

How about an optional import? I.e. import in try-except block and if failed to import — BaseSettings=BaseModel

@zmievsa
Copy link
Owner

zmievsa commented Feb 18, 2025

May I also ask (just for personal statistics) where do you use Cadwyn and how well does it fit your use case? Anything missing or confusing?

@peterlandry
Copy link
Author

Evaluating it right now for our FastAPI app that badly needs a good versioning story. Tried throwing it right at our codebase and this is the first hurdle I found.

As for features, any thoughts about non-date version numbers? As long as they stay sortable.

@peterlandry
Copy link
Author

How about an optional import? I.e. import in try-except block and if failed to import — BaseSettings=BaseModel

Could do, will give that a try and a test case.

I think I'd like to understand why that special case is needed, though. BaseSettings does technically inherit from from BaseModel, so I'm curious what it's doing in its init that is causing trouble here with model_config

@zmievsa
Copy link
Owner

zmievsa commented Feb 18, 2025

Nice! Ping me if you need any features or help.

As for non-dates — I wanted to do this for a while too. I can implement it either this week or next week depending on my availability.

But note that they'll need to be fully lexicographically sortable. I.e. pure semantic versioning wouldn't fit in the first implementation. But I'm open to add that as well.

@peterlandry
Copy link
Author

First reason I thought is transitioning from our simple v1, etc, version markers. Would be great to be able to pull that into Cadwyn without having to force clients over to a date-scheme or do rewrites, etc.

@zmievsa
Copy link
Owner

zmievsa commented Feb 18, 2025

Sure! I can do that soon.

@zmievsa
Copy link
Owner

zmievsa commented Feb 20, 2025

I began working on the non-date versions but found a pretty bad case: "v10" < "v9". So we can't just handle them as regular strings. I'll do my best to add url-based versioning and v-prefixed numbers for versions.

@peterlandry
Copy link
Author

I began working on the non-date versions but found a pretty bad case: "v10" < "v9". So we can't just handle them as regular strings. I'll do my best to add url-based versioning and v-prefixed numbers for versions.

Can take discussion to a different issue if you'd like. If it helps understand our case, we could work with being able to mark one "legacy" version like v1, etc, that would not need to be sorted in-line with the date based ones. Not sure if that could make some kind of implementation easier?

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