mirror of
https://github.com/long2ice/fastapi-cache.git
synced 2026-03-24 20:47:54 +00:00
Merge pull request #133 from mjpieters/correct_json_decode_return
Clean up type annotations
This commit is contained in:
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -2,12 +2,17 @@ name: ci
|
||||
on: [ push, pull_request ]
|
||||
jobs:
|
||||
ci:
|
||||
strategy:
|
||||
matrix:
|
||||
python: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
|
||||
name: "Test on Python ${{ matrix.python }}"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.x"
|
||||
python-version: "${{ matrix.python }}"
|
||||
- name: Install and configure Poetry
|
||||
run: |
|
||||
pip install -U pip poetry
|
||||
|
||||
2
Makefile
2
Makefile
@@ -14,6 +14,8 @@ style: deps
|
||||
check: deps
|
||||
@black $(checkfiles) || (echo "Please run 'make style' to auto-fix style issues" && false)
|
||||
@flake8 $(checkfiles)
|
||||
@mypy ${checkfiles}
|
||||
@pyright ${checkfiles}
|
||||
|
||||
test: deps
|
||||
$(py_warn) pytest
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
from typing import Dict, Optional
|
||||
|
||||
import pendulum
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
@@ -84,9 +87,9 @@ app.get("/method")(cache(namespace="test")(instance.handler_method))
|
||||
# cache a Pydantic model instance; the return type annotation is required in this case
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
description: str | None = None
|
||||
description: Optional[str] = None
|
||||
price: float
|
||||
tax: float | None = None
|
||||
tax: Optional[float] = None
|
||||
|
||||
|
||||
@app.get("/pydantic_instance")
|
||||
@@ -110,7 +113,7 @@ async def uncached_put():
|
||||
@cache(namespace="test", expire=5, injected_dependency_namespace="monty_python")
|
||||
def namespaced_injection(
|
||||
__fastapi_cache_request: int = 42, __fastapi_cache_response: int = 17
|
||||
) -> dict[str, int]:
|
||||
) -> Dict[str, int]:
|
||||
return {
|
||||
"__fastapi_cache_request": __fastapi_cache_request,
|
||||
"__fastapi_cache_response": __fastapi_cache_response,
|
||||
@@ -123,4 +126,4 @@ async def startup():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("main:app", debug=True, reload=True)
|
||||
uvicorn.run("main:app", reload=True)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
import time
|
||||
|
||||
import pendulum
|
||||
@@ -87,4 +88,4 @@ async def startup():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("main:app", debug=True, reload=True)
|
||||
uvicorn.run("main:app", reload=True)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import datetime
|
||||
from typing import Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
from aiobotocore.client import AioBaseClient
|
||||
from aiobotocore.session import get_session, AioSession
|
||||
from aiobotocore.session import AioSession, get_session
|
||||
|
||||
from fastapi_cache.backends import Backend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types_aiobotocore_dynamodb import DynamoDBClient
|
||||
else:
|
||||
DynamoDBClient = AioBaseClient
|
||||
|
||||
|
||||
class DynamoBackend(Backend):
|
||||
"""
|
||||
@@ -25,14 +30,18 @@ class DynamoBackend(Backend):
|
||||
>> FastAPICache.init(dynamodb)
|
||||
"""
|
||||
|
||||
client: DynamoDBClient
|
||||
session: AioSession
|
||||
table_name: str
|
||||
region: Optional[str]
|
||||
|
||||
def __init__(self, table_name: str, region: Optional[str] = None) -> None:
|
||||
self.session: AioSession = get_session()
|
||||
self.client: Optional[AioBaseClient] = None # Needs async init
|
||||
self.table_name = table_name
|
||||
self.region = region
|
||||
|
||||
async def init(self) -> None:
|
||||
self.client = await self.session.create_client(
|
||||
self.client = await self.session.create_client( # pyright: ignore[reportUnknownMemberType]
|
||||
"dynamodb", region_name=self.region
|
||||
).__aenter__()
|
||||
|
||||
@@ -60,6 +69,7 @@ class DynamoBackend(Backend):
|
||||
response = await self.client.get_item(TableName=self.table_name, Key={"key": {"S": key}})
|
||||
if "Item" in response:
|
||||
return response["Item"].get("value", {}).get("B")
|
||||
return None
|
||||
|
||||
async def set(self, key: str, value: bytes, expire: Optional[int] = None) -> None:
|
||||
ttl = (
|
||||
|
||||
@@ -13,18 +13,18 @@ class RedisBackend(Backend):
|
||||
|
||||
async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:
|
||||
async with self.redis.pipeline(transaction=not self.is_cluster) as pipe:
|
||||
return await pipe.ttl(key).get(key).execute()
|
||||
return await pipe.ttl(key).get(key).execute() # type: ignore[union-attr,no-any-return]
|
||||
|
||||
async def get(self, key: str) -> Optional[bytes]:
|
||||
return await self.redis.get(key)
|
||||
return await self.redis.get(key) # type: ignore[union-attr]
|
||||
|
||||
async def set(self, key: str, value: bytes, expire: Optional[int] = None) -> None:
|
||||
return await self.redis.set(key, value, ex=expire)
|
||||
await self.redis.set(key, value, ex=expire) # type: ignore[union-attr]
|
||||
|
||||
async def clear(self, namespace: Optional[str] = None, key: Optional[str] = None) -> int:
|
||||
if namespace:
|
||||
lua = f"for i, name in ipairs(redis.call('KEYS', '{namespace}:*')) do redis.call('DEL', name); end"
|
||||
return await self.redis.eval(lua, numkeys=0)
|
||||
return await self.redis.eval(lua, numkeys=0) # type: ignore[union-attr,no-any-return]
|
||||
elif key:
|
||||
return await self.redis.delete(key)
|
||||
return await self.redis.delete(key) # type: ignore[union-attr]
|
||||
return 0
|
||||
|
||||
@@ -8,28 +8,31 @@ import pendulum
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseConfig, ValidationError, fields
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.templating import _TemplateResponse as TemplateResponse
|
||||
from starlette.templating import (
|
||||
_TemplateResponse as TemplateResponse, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_T = TypeVar("_T", bound=type)
|
||||
|
||||
|
||||
CONVERTERS: dict[str, Callable[[str], Any]] = {
|
||||
"date": lambda x: pendulum.parse(x, exact=True),
|
||||
"datetime": lambda x: pendulum.parse(x, exact=True),
|
||||
CONVERTERS: Dict[str, Callable[[str], Any]] = {
|
||||
# Pendulum 3.0.0 adds parse to __all__, at which point these ignores can be removed
|
||||
"date": lambda x: pendulum.parse(x, exact=True), # type: ignore[attr-defined]
|
||||
"datetime": lambda x: pendulum.parse(x, exact=True), # type: ignore[attr-defined]
|
||||
"decimal": Decimal,
|
||||
}
|
||||
|
||||
|
||||
class JsonEncoder(json.JSONEncoder):
|
||||
def default(self, obj: Any) -> Any:
|
||||
if isinstance(obj, datetime.datetime):
|
||||
return {"val": str(obj), "_spec_type": "datetime"}
|
||||
elif isinstance(obj, datetime.date):
|
||||
return {"val": str(obj), "_spec_type": "date"}
|
||||
elif isinstance(obj, Decimal):
|
||||
return {"val": str(obj), "_spec_type": "decimal"}
|
||||
def default(self, o: Any) -> Any:
|
||||
if isinstance(o, datetime.datetime):
|
||||
return {"val": str(o), "_spec_type": "datetime"}
|
||||
elif isinstance(o, datetime.date):
|
||||
return {"val": str(o), "_spec_type": "date"}
|
||||
elif isinstance(o, Decimal):
|
||||
return {"val": str(o), "_spec_type": "decimal"}
|
||||
else:
|
||||
return jsonable_encoder(obj)
|
||||
return jsonable_encoder(o)
|
||||
|
||||
|
||||
def object_hook(obj: Any) -> Any:
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import sys
|
||||
from functools import wraps
|
||||
from inspect import Parameter, Signature, isawaitable, iscoroutinefunction
|
||||
from typing import Awaitable, Callable, Optional, Type, TypeVar
|
||||
from typing import Awaitable, Callable, List, Optional, Type, TypeVar, Union, cast
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import ParamSpec
|
||||
@@ -29,14 +29,14 @@ def _augment_signature(signature: Signature, *extra: Parameter) -> Signature:
|
||||
return signature
|
||||
|
||||
parameters = list(signature.parameters.values())
|
||||
variadic_keyword_params = []
|
||||
variadic_keyword_params: List[Parameter] = []
|
||||
while parameters and parameters[-1].kind is Parameter.VAR_KEYWORD:
|
||||
variadic_keyword_params.append(parameters.pop())
|
||||
|
||||
return signature.replace(parameters=[*parameters, *extra, *variadic_keyword_params])
|
||||
|
||||
|
||||
def _locate_param(sig: Signature, dep: Parameter, to_inject: list[Parameter]) -> Parameter:
|
||||
def _locate_param(sig: Signature, dep: Parameter, to_inject: List[Parameter]) -> Parameter:
|
||||
"""Locate an existing parameter in the decorated endpoint
|
||||
|
||||
If not found, returns the injectable parameter, and adds it to the to_inject list.
|
||||
@@ -56,9 +56,9 @@ def cache(
|
||||
expire: Optional[int] = None,
|
||||
coder: Optional[Type[Coder]] = None,
|
||||
key_builder: Optional[KeyBuilder] = None,
|
||||
namespace: Optional[str] = "",
|
||||
namespace: str = "",
|
||||
injected_dependency_namespace: str = "__fastapi_cache",
|
||||
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
|
||||
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[Union[R, Response]]]]:
|
||||
"""
|
||||
cache all function
|
||||
:param namespace:
|
||||
@@ -80,16 +80,16 @@ def cache(
|
||||
kind=Parameter.KEYWORD_ONLY,
|
||||
)
|
||||
|
||||
def wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
||||
def wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[Union[R, Response]]]:
|
||||
# get_typed_signature ensures that any forward references are resolved first
|
||||
wrapped_signature = get_typed_signature(func)
|
||||
to_inject: list[Parameter] = []
|
||||
to_inject: List[Parameter] = []
|
||||
request_param = _locate_param(wrapped_signature, injected_request, to_inject)
|
||||
response_param = _locate_param(wrapped_signature, injected_response, to_inject)
|
||||
return_type = get_typed_return_annotation(func)
|
||||
|
||||
@wraps(func)
|
||||
async def inner(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
async def inner(*args: P.args, **kwargs: P.kwargs) -> Union[R, Response]:
|
||||
nonlocal coder
|
||||
nonlocal expire
|
||||
nonlocal key_builder
|
||||
@@ -111,11 +111,11 @@ def cache(
|
||||
else:
|
||||
# sync, wrap in thread and return async
|
||||
# see above why we have to await even although caller also awaits.
|
||||
return await run_in_threadpool(func, *args, **kwargs)
|
||||
return await run_in_threadpool(func, *args, **kwargs) # type: ignore[arg-type]
|
||||
|
||||
copy_kwargs = kwargs.copy()
|
||||
request: Optional[Request] = copy_kwargs.pop(request_param.name, None)
|
||||
response: Optional[Response] = copy_kwargs.pop(response_param.name, None)
|
||||
request: Optional[Request] = copy_kwargs.pop(request_param.name, None) # type: ignore[assignment]
|
||||
response: Optional[Response] = copy_kwargs.pop(response_param.name, None) # type: ignore[assignment]
|
||||
if (
|
||||
request and request.headers.get("Cache-Control") in ("no-store", "no-cache")
|
||||
) or not FastAPICache.get_enable():
|
||||
@@ -137,17 +137,18 @@ def cache(
|
||||
)
|
||||
if isawaitable(cache_key):
|
||||
cache_key = await cache_key
|
||||
assert isinstance(cache_key, str)
|
||||
|
||||
try:
|
||||
ttl, ret = await backend.get_with_ttl(cache_key)
|
||||
ttl, cached = await backend.get_with_ttl(cache_key)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
f"Error retrieving cache key '{cache_key}' from backend:", exc_info=True
|
||||
)
|
||||
ttl, ret = 0, None
|
||||
ttl, cached = 0, None
|
||||
if not request:
|
||||
if ret is not None:
|
||||
return coder.decode_as_type(ret, type_=return_type)
|
||||
if cached is not None:
|
||||
return cast(R, coder.decode_as_type(cached, type_=return_type))
|
||||
ret = await ensure_async_func(*args, **kwargs)
|
||||
try:
|
||||
await backend.set(cache_key, coder.encode(ret), expire)
|
||||
@@ -161,15 +162,15 @@ def cache(
|
||||
return await ensure_async_func(*args, **kwargs)
|
||||
|
||||
if_none_match = request.headers.get("if-none-match")
|
||||
if ret is not None:
|
||||
if cached is not None:
|
||||
if response:
|
||||
response.headers["Cache-Control"] = f"max-age={ttl}"
|
||||
etag = f"W/{hash(ret)}"
|
||||
etag = f"W/{hash(cached)}"
|
||||
if if_none_match == etag:
|
||||
response.status_code = 304
|
||||
return response
|
||||
response.headers["ETag"] = etag
|
||||
return coder.decode_as_type(ret, type_=return_type)
|
||||
return cast(R, coder.decode_as_type(cached, type_=return_type))
|
||||
|
||||
ret = await ensure_async_func(*args, **kwargs)
|
||||
encoded_ret = coder.encode(ret)
|
||||
@@ -179,12 +180,13 @@ def cache(
|
||||
except Exception:
|
||||
logger.warning(f"Error setting cache key '{cache_key}' in backend:", exc_info=True)
|
||||
|
||||
response.headers["Cache-Control"] = f"max-age={expire}"
|
||||
etag = f"W/{hash(encoded_ret)}"
|
||||
response.headers["ETag"] = etag
|
||||
if response:
|
||||
response.headers["Cache-Control"] = f"max-age={expire}"
|
||||
etag = f"W/{hash(encoded_ret)}"
|
||||
response.headers["ETag"] = etag
|
||||
return ret
|
||||
|
||||
inner.__signature__ = _augment_signature(wrapped_signature, *to_inject)
|
||||
inner.__signature__ = _augment_signature(wrapped_signature, *to_inject) # type: ignore[attr-defined]
|
||||
return inner
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import hashlib
|
||||
from typing import Any, Callable, Optional
|
||||
from typing import Any, Callable, Dict, Optional, Tuple
|
||||
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
@@ -8,10 +8,11 @@ from starlette.responses import Response
|
||||
def default_key_builder(
|
||||
func: Callable[..., Any],
|
||||
namespace: str = "",
|
||||
*,
|
||||
request: Optional[Request] = None,
|
||||
response: Optional[Response] = None,
|
||||
args: Optional[tuple[Any, ...]] = None,
|
||||
kwargs: Optional[dict[str, Any]] = None,
|
||||
args: Tuple[Any, ...],
|
||||
kwargs: Dict[str, Any],
|
||||
) -> str:
|
||||
cache_key = hashlib.md5( # nosec:B303
|
||||
f"{func.__module__}:{func.__name__}:{args}:{kwargs}".encode()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from typing import Any, Awaitable, Callable, Optional, Protocol, Union
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional, Tuple, Union
|
||||
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from typing_extensions import Protocol
|
||||
|
||||
|
||||
_Func = Callable[..., Any]
|
||||
@@ -10,12 +11,12 @@ _Func = Callable[..., Any]
|
||||
class KeyBuilder(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
_function: _Func,
|
||||
_namespace: str = ...,
|
||||
__function: _Func,
|
||||
__namespace: str = ...,
|
||||
*,
|
||||
request: Optional[Request] = ...,
|
||||
response: Optional[Response] = ...,
|
||||
args: tuple[Any, ...] = ...,
|
||||
kwargs: dict[str, Any] = ...,
|
||||
args: Tuple[Any, ...],
|
||||
kwargs: Dict[str, Any],
|
||||
) -> Union[Awaitable[str], str]:
|
||||
...
|
||||
|
||||
691
poetry.lock
generated
691
poetry.lock
generated
File diff suppressed because one or more lines are too long
@@ -22,7 +22,7 @@ redis = { version = "^4.2.0rc1", optional = true }
|
||||
aiomcache = { version = "*", optional = true }
|
||||
pendulum = "*"
|
||||
aiobotocore = { version = "^1.4.1", optional = true }
|
||||
typing-extensions = { version = ">=4.1.0", markers = "python_version < \"3.10\"" }
|
||||
typing-extensions = { version = ">=4.1.0" }
|
||||
aiohttp = { version = ">=3.8.3", markers = "python_version >= \"3.11\"" }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
@@ -33,6 +33,10 @@ pytest = "*"
|
||||
requests = "*"
|
||||
coverage = "^6.5.0"
|
||||
httpx = "*"
|
||||
mypy = "^1.2.0"
|
||||
types-redis = "^4.5.4.2"
|
||||
pyright = "^1.1.306"
|
||||
types-aiobotocore = { extras = ["dynamodb"], version = "^2.5.0.post2" }
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
@@ -47,3 +51,31 @@ all = ["redis", "aiomcache", "aiobotocore"]
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py36', 'py37', 'py38', 'py39']
|
||||
|
||||
[tool.mypy]
|
||||
files = ["fastapi_cache", "examples", "tests"]
|
||||
python_version = "3.7"
|
||||
# equivalent of --strict
|
||||
warn_unused_configs = true
|
||||
disallow_any_generics = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
check_untyped_defs = true
|
||||
disallow_untyped_decorators = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
warn_return_any = true
|
||||
no_implicit_reexport = true
|
||||
strict_equality = true
|
||||
strict_concatenate = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "examples.*.main"
|
||||
ignore_errors = true
|
||||
|
||||
[tool.pyright]
|
||||
include = ["fastapi_cache", "tests", "examples"]
|
||||
strict = ["fastapi_cache", "tests"]
|
||||
pythonVersion = "3.7"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Tuple, Type
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, ValidationError
|
||||
@@ -46,14 +46,14 @@ def test_pickle_coder(value: Any) -> None:
|
||||
[
|
||||
(1, None),
|
||||
("some_string", None),
|
||||
((1, 2), tuple[int, int]),
|
||||
((1, 2), Tuple[int, int]),
|
||||
([1, 2, 3], None),
|
||||
({"some_key": 1, "other_key": 2}, None),
|
||||
(DCItem(name="foo", price=42.0, description="some dataclass item", tax=0.2), DCItem),
|
||||
(PDItem(name="foo", price=42.0, description="some pydantic item", tax=0.2), PDItem),
|
||||
],
|
||||
)
|
||||
def test_json_coder(value: Any, return_type) -> None:
|
||||
def test_json_coder(value: Any, return_type: Type[Any]) -> None:
|
||||
encoded_value = JsonCoder.encode(value)
|
||||
assert isinstance(encoded_value, bytes)
|
||||
decoded_value = JsonCoder.decode_as_type(encoded_value, type_=return_type)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import time
|
||||
from typing import Generator
|
||||
from typing import Generator, Any
|
||||
|
||||
import pendulum
|
||||
import pytest
|
||||
@@ -11,7 +11,7 @@ from fastapi_cache.backends.inmemory import InMemoryBackend
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def init_cache() -> Generator:
|
||||
def init_cache() -> Generator[Any, Any, None]:
|
||||
FastAPICache.init(InMemoryBackend())
|
||||
yield
|
||||
FastAPICache.reset()
|
||||
@@ -21,34 +21,34 @@ def test_datetime() -> None:
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/datetime")
|
||||
now = response.json().get("now")
|
||||
now_ = pendulum.now().replace(microsecond=0)
|
||||
assert pendulum.parse(now).replace(microsecond=0) == now_
|
||||
now_ = pendulum.now().replace(microsecond=0) # type: ignore[no-untyped-call]
|
||||
assert pendulum.parse(now).replace(microsecond=0) == now_ # type: ignore[attr-defined]
|
||||
response = client.get("/datetime")
|
||||
now = response.json().get("now")
|
||||
assert pendulum.parse(now).replace(microsecond=0) == now_
|
||||
assert pendulum.parse(now).replace(microsecond=0) == now_ # type: ignore[attr-defined]
|
||||
time.sleep(3)
|
||||
response = client.get("/datetime")
|
||||
now = response.json().get("now")
|
||||
now = pendulum.parse(now).replace(microsecond=0)
|
||||
now = pendulum.parse(now).replace(microsecond=0) # type: ignore[attr-defined]
|
||||
assert now != now_
|
||||
assert now == pendulum.now().replace(microsecond=0)
|
||||
assert now == pendulum.now().replace(microsecond=0) # type: ignore[no-untyped-call]
|
||||
|
||||
|
||||
def test_date() -> None:
|
||||
"""Test path function without request or response arguments."""
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/date")
|
||||
assert pendulum.parse(response.json()) == pendulum.today()
|
||||
assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined]
|
||||
|
||||
# do it again to test cache
|
||||
response = client.get("/date")
|
||||
assert pendulum.parse(response.json()) == pendulum.today()
|
||||
assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined]
|
||||
|
||||
# now test with cache disabled, as that's a separate code path
|
||||
FastAPICache._enable = False
|
||||
FastAPICache._enable = False # pyright: ignore[reportPrivateUsage]
|
||||
response = client.get("/date")
|
||||
assert pendulum.parse(response.json()) == pendulum.today()
|
||||
FastAPICache._enable = True
|
||||
assert pendulum.parse(response.json()) == pendulum.today() # type: ignore[attr-defined]
|
||||
FastAPICache._enable = True # pyright: ignore[reportPrivateUsage]
|
||||
|
||||
|
||||
def test_sync() -> None:
|
||||
|
||||
Reference in New Issue
Block a user