Skip to content

Commit 72321a6

Browse files
authored
/docs return 404 after adding api_version_default_value #268 (#269)
#268
1 parent 6eeaeab commit 72321a6

File tree

12 files changed

+113
-15
lines changed

12 files changed

+113
-15
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ Please follow [the Keep a Changelog standard](https://keepachangelog.com/en/1.0.
55

66
## [Unreleased]
77

8+
## [5.2.1]
9+
10+
### Fixed
11+
12+
* [#268](https://github.com/zmievsa/cadwyn/issues/268) A bug where we received 404 for all unversioned routes when a default api version was passed to Cadwyn at initialization
13+
814
## [5.2.0]
915

1016
### Removed

cadwyn/_internal/__init__.py

Whitespace-only changes.

cadwyn/_internal/context_vars.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from contextvars import ContextVar
2+
3+
from typing_extensions import Literal
4+
5+
DEFAULT_API_VERSION_VAR: "ContextVar[str | None]" = ContextVar("cadwyn_default_api_version")
6+
CURRENT_DEPENDENCY_SOLVER_OPTIONS = Literal["cadwyn", "fastapi"]
7+
CURRENT_DEPENDENCY_SOLVER_VAR: ContextVar[CURRENT_DEPENDENCY_SOLVER_OPTIONS] = ContextVar(
8+
"cadwyn_dependencies_current_dependency_solver"
9+
)

cadwyn/applications.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ def __init__(
133133
stacklevel=2,
134134
)
135135
api_version_parameter_name = api_version_header_name
136+
if api_version_default_value is not None and api_version_location == "path":
137+
raise CadwynStructureError(
138+
"You tried to pass an api_version_default_value while putting the API version in Path. "
139+
"This is not currently supported by Cadwyn. "
140+
"Please, open an issue on our github if you'd like to have it."
141+
)
136142

137143
super().__init__(
138144
debug=debug,
@@ -231,8 +237,8 @@ def __init__(
231237
versioning_middleware_class,
232238
api_version_parameter_name=api_version_parameter_name,
233239
api_version_manager=self._api_version_manager,
234-
api_version_default_value=api_version_default_value,
235240
api_version_var=self.versions.api_version_var,
241+
api_version_default_value=api_version_default_value,
236242
)
237243
if self.api_version_format == "date" and (
238244
sorted(self.versions.versions, key=lambda v: v.value, reverse=True) != list(self.versions.versions)

cadwyn/dependencies.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from cadwyn.structure.versions import _CURRENT_DEPENDENCY_SOLVER_OPTIONS, _CURRENT_DEPENDENCY_SOLVER_VAR
1+
from cadwyn._internal.context_vars import CURRENT_DEPENDENCY_SOLVER_OPTIONS, CURRENT_DEPENDENCY_SOLVER_VAR
22

33

4-
async def current_dependency_solver() -> _CURRENT_DEPENDENCY_SOLVER_OPTIONS:
5-
return _CURRENT_DEPENDENCY_SOLVER_VAR.get("fastapi")
4+
async def current_dependency_solver() -> CURRENT_DEPENDENCY_SOLVER_OPTIONS:
5+
return CURRENT_DEPENDENCY_SOLVER_VAR.get("fastapi")

cadwyn/middleware.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
from contextvars import ContextVar
88
from typing import Annotated, Any, Literal, Protocol, Union
99

10+
import fastapi
1011
from fastapi import Request
1112
from starlette.middleware.base import BaseHTTPMiddleware, DispatchFunction, RequestResponseEndpoint
1213
from starlette.types import ASGIApp
1314

15+
from cadwyn._internal.context_vars import DEFAULT_API_VERSION_VAR
1416
from cadwyn.structure.common import VersionType
1517

1618

@@ -69,6 +71,8 @@ def api_version_dependency(**kwargs: Any):
6971
annotation=Annotated[
7072
validation_data_type, fastapi_depends_class(openapi_examples={"default": {"value": default_value}})
7173
],
74+
# Path-based parameters do not support a default value in FastAPI :(
75+
default=default_value if fastapi_depends_class != fastapi.Path else inspect.Signature.empty,
7276
),
7377
],
7478
)
@@ -103,10 +107,11 @@ async def dispatch(
103107
api_version = self._api_version_manager.get(request)
104108

105109
if api_version is None:
106-
if callable(self.api_version_default_value): # pragma: no cover # TODO
110+
if callable(self.api_version_default_value):
107111
api_version = await self.api_version_default_value(request)
108112
else:
109113
api_version = self.api_version_default_value
114+
DEFAULT_API_VERSION_VAR.set(api_version)
110115

111116
self.api_version_var.set(api_version)
112117
response = await call_next(request)

cadwyn/routing.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from starlette.routing import BaseRoute, Match
1212
from starlette.types import Receive, Scope, Send
1313

14+
from cadwyn._internal.context_vars import DEFAULT_API_VERSION_VAR
1415
from cadwyn._utils import same_definition_as_in
1516
from cadwyn.middleware import APIVersionFormat
1617
from cadwyn.structure.common import VersionType
@@ -70,8 +71,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
7071
if scope["type"] == "lifespan":
7172
await self.lifespan(scope, receive, send)
7273
return
73-
7474
version = self.api_version_var.get(None)
75+
default_version_that_was_picked = DEFAULT_API_VERSION_VAR.get(None)
7576

7677
# if version is None, then it's an unversioned request and we need to use the unversioned routes
7778
# if there will be a value, we search for the most suitable version
@@ -81,6 +82,14 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
8182
routes = self.versioned_routers[version].routes
8283
else:
8384
routes = await self._get_routes_from_closest_suitable_version(version)
85+
if default_version_that_was_picked:
86+
# We add unversioned routes to versioned routes because otherwise unversioned routes
87+
# will be completely unavailable when a default version is passed. So routes such as
88+
# /docs will not be accessible at all.
89+
90+
# We use this order because if versioned routes go first and there is a versioned route that is
91+
# the same as an unversioned route -- the unversioned one becomes impossible to match.
92+
routes = self.unversioned_routes + routes
8493
await self.process_request(scope=scope, receive=receive, send=send, routes=routes)
8594

8695
@cached_property

cadwyn/structure/versions.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
from pydantic import BaseModel
2525
from pydantic_core import PydanticUndefined
2626
from starlette._utils import is_async_callable
27-
from typing_extensions import Any, Literal, ParamSpec, TypeAlias, TypeVar, assert_never, deprecated, get_args
27+
from typing_extensions import Any, ParamSpec, TypeAlias, TypeVar, assert_never, deprecated, get_args
2828

29+
from cadwyn._internal.context_vars import CURRENT_DEPENDENCY_SOLVER_VAR
2930
from cadwyn._utils import classproperty
3031
from cadwyn.exceptions import (
3132
CadwynError,
@@ -52,10 +53,6 @@
5253
_CADWYN_RESPONSE_PARAM_NAME = "cadwyn_response_param"
5354
_P = ParamSpec("_P")
5455
_R = TypeVar("_R")
55-
_CURRENT_DEPENDENCY_SOLVER_OPTIONS = Literal["cadwyn", "fastapi"]
56-
_CURRENT_DEPENDENCY_SOLVER_VAR: ContextVar[_CURRENT_DEPENDENCY_SOLVER_OPTIONS] = ContextVar(
57-
"cadwyn_dependencies_dry_run"
58-
)
5956

6057
PossibleInstructions: TypeAlias = Union[
6158
AlterSchemaSubInstruction, AlterEndpointSubInstruction, AlterEnumSubInstruction, SchemaHadInstruction, staticmethod
@@ -387,7 +384,7 @@ async def _migrate_request(
387384
request.scope["headers"] = tuple((key.encode(), value.encode()) for key, value in request_info.headers.items())
388385
del request._headers
389386
# This gives us the ability to tell the user whether cadwyn is running its dependencies or FastAPI
390-
_CURRENT_DEPENDENCY_SOLVER_VAR.set("cadwyn")
387+
CURRENT_DEPENDENCY_SOLVER_VAR.set("cadwyn")
391388
# Remember this: if len(body_params) == 1, then route.body_schema == route.dependant.body_params[0]
392389
result = await solve_dependencies(
393390
request=request,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "cadwyn"
3-
version = "5.2.0"
3+
version = "5.2.1"
44
description = "Production-ready community-driven modern Stripe-like API versioning in FastAPI"
55
authors = [{ name = "Stanislav Zmiev", email = "zmievsa@gmail.com" }]
66
license = "MIT"

tests/test_applications.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pydantic import BaseModel
99

1010
from cadwyn import Cadwyn
11+
from cadwyn.exceptions import CadwynStructureError
1112
from cadwyn.route_generation import VersionedAPIRouter
1213
from cadwyn.structure.endpoints import endpoint
1314
from cadwyn.structure.schemas import schema
@@ -405,3 +406,15 @@ def test__api_version_header_name_is_deprecated_and_translates_to_api_version_pa
405406
with pytest.warns(DeprecationWarning):
406407
cadwyn = Cadwyn(api_version_header_name="x-api-version", versions=VersionBundle(Version("2022-11-16")))
407408
assert cadwyn.api_version_parameter_name == "x-api-version"
409+
410+
411+
def test__api_version_default_value_with_path_location__should_raise_error():
412+
with pytest.raises(
413+
CadwynStructureError,
414+
match="You tried to pass an api_version_default_value while putting the API version in Path",
415+
):
416+
Cadwyn(
417+
versions=VersionBundle(HeadVersion(), Version("2022-11-16")),
418+
api_version_default_value="2022-11-16",
419+
api_version_location="path",
420+
)

0 commit comments

Comments
 (0)