Add custom typing to config and ctx (#2785)
This commit is contained in:
@@ -293,7 +293,7 @@ def test_handle_request_with_nested_sanic_exception(
|
||||
|
||||
|
||||
def test_app_name_required():
|
||||
with pytest.raises(SanicException):
|
||||
with pytest.raises(TypeError):
|
||||
Sanic()
|
||||
|
||||
|
||||
|
||||
@@ -310,3 +310,29 @@ def test_request_idempotent(method, idempotent):
|
||||
def test_request_cacheable(method, cacheable):
|
||||
request = Request(b"/", {}, None, method, None, None)
|
||||
assert request.is_cacheable is cacheable
|
||||
|
||||
|
||||
def test_custom_ctx():
|
||||
class CustomContext:
|
||||
FOO = "foo"
|
||||
|
||||
class CustomRequest(Request[Sanic, CustomContext]):
|
||||
@staticmethod
|
||||
def make_context() -> CustomContext:
|
||||
return CustomContext()
|
||||
|
||||
app = Sanic("Test", request_class=CustomRequest)
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request: CustomRequest):
|
||||
return response.json(
|
||||
[
|
||||
isinstance(request, CustomRequest),
|
||||
isinstance(request.ctx, CustomContext),
|
||||
request.ctx.FOO,
|
||||
]
|
||||
)
|
||||
|
||||
_, resp = app.test_client.get("/")
|
||||
|
||||
assert resp.json == [True, True, "foo"]
|
||||
|
||||
10
tests/typing/samples/app_custom_config.py
Normal file
10
tests/typing/samples/app_custom_config.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from sanic import Sanic
|
||||
from sanic.config import Config
|
||||
|
||||
|
||||
class CustomConfig(Config):
|
||||
pass
|
||||
|
||||
|
||||
app = Sanic("test", config=CustomConfig())
|
||||
reveal_type(app)
|
||||
9
tests/typing/samples/app_custom_ctx.py
Normal file
9
tests/typing/samples/app_custom_ctx.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
|
||||
app = Sanic("test", ctx=Foo())
|
||||
reveal_type(app)
|
||||
5
tests/typing/samples/app_default.py
Normal file
5
tests/typing/samples/app_default.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
app = Sanic("test")
|
||||
reveal_type(app)
|
||||
14
tests/typing/samples/app_fully_custom.py
Normal file
14
tests/typing/samples/app_fully_custom.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from sanic import Sanic
|
||||
from sanic.config import Config
|
||||
|
||||
|
||||
class CustomConfig(Config):
|
||||
pass
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
|
||||
app = Sanic("test", config=CustomConfig(), ctx=Foo())
|
||||
reveal_type(app)
|
||||
17
tests/typing/samples/request_custom_ctx.py
Normal file
17
tests/typing/samples/request_custom_ctx.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sanic import Request, Sanic
|
||||
from sanic.config import Config
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
|
||||
app = Sanic("test")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request: Request[Sanic[Config, SimpleNamespace], Foo]):
|
||||
reveal_type(request.ctx)
|
||||
reveal_type(request.app)
|
||||
19
tests/typing/samples/request_custom_sanic.py
Normal file
19
tests/typing/samples/request_custom_sanic.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sanic import Request, Sanic
|
||||
from sanic.config import Config
|
||||
|
||||
|
||||
class CustomConfig(Config):
|
||||
pass
|
||||
|
||||
|
||||
app = Sanic("test", config=CustomConfig())
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def handler(
|
||||
request: Request[Sanic[CustomConfig, SimpleNamespace], SimpleNamespace]
|
||||
):
|
||||
reveal_type(request.ctx)
|
||||
reveal_type(request.app)
|
||||
34
tests/typing/samples/request_fully_custom.py
Normal file
34
tests/typing/samples/request_fully_custom.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from sanic import Request, Sanic
|
||||
from sanic.config import Config
|
||||
|
||||
|
||||
class CustomConfig(Config):
|
||||
pass
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
|
||||
class RequestContext:
|
||||
foo: Foo
|
||||
|
||||
|
||||
class CustomRequest(Request[Sanic[CustomConfig, Foo], RequestContext]):
|
||||
@staticmethod
|
||||
def make_context() -> RequestContext:
|
||||
ctx = RequestContext()
|
||||
ctx.foo = Foo()
|
||||
return ctx
|
||||
|
||||
|
||||
app = Sanic(
|
||||
"test", config=CustomConfig(), ctx=Foo(), request_class=CustomRequest
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def handler(request: CustomRequest):
|
||||
reveal_type(request)
|
||||
reveal_type(request.ctx)
|
||||
reveal_type(request.app)
|
||||
127
tests/typing/test_typing.py
Normal file
127
tests/typing/test_typing.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# flake8: noqa: E501
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
CURRENT_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def run_check(path_location: str) -> str:
|
||||
"""Use mypy to check the given path location and return the output."""
|
||||
|
||||
mypy_path = "mypy"
|
||||
path = CURRENT_DIR / path_location
|
||||
command = [mypy_path, path.resolve().as_posix()]
|
||||
|
||||
process = subprocess.run(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
output = process.stdout + process.stderr
|
||||
return output
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path_location,expected",
|
||||
(
|
||||
(
|
||||
"app_default.py",
|
||||
[
|
||||
(
|
||||
"sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]",
|
||||
5,
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
"app_custom_config.py",
|
||||
[
|
||||
(
|
||||
"sanic.app.Sanic[app_custom_config.CustomConfig, types.SimpleNamespace]",
|
||||
10,
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
"app_custom_ctx.py",
|
||||
[("sanic.app.Sanic[sanic.config.Config, app_custom_ctx.Foo]", 9)],
|
||||
),
|
||||
(
|
||||
"app_fully_custom.py",
|
||||
[
|
||||
(
|
||||
"sanic.app.Sanic[app_fully_custom.CustomConfig, app_fully_custom.Foo]",
|
||||
14,
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
"request_custom_sanic.py",
|
||||
[
|
||||
("types.SimpleNamespace", 18),
|
||||
(
|
||||
"sanic.app.Sanic[request_custom_sanic.CustomConfig, types.SimpleNamespace]",
|
||||
19,
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
"request_custom_ctx.py",
|
||||
[
|
||||
("request_custom_ctx.Foo", 16),
|
||||
(
|
||||
"sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]",
|
||||
17,
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
"request_fully_custom.py",
|
||||
[
|
||||
("request_fully_custom.CustomRequest", 32),
|
||||
("request_fully_custom.RequestContext", 33),
|
||||
(
|
||||
"sanic.app.Sanic[request_fully_custom.CustomConfig, request_fully_custom.Foo]",
|
||||
34,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_check_app_default(
|
||||
path_location: str, expected: List[Tuple[str, int]]
|
||||
) -> None:
|
||||
output = run_check(f"samples/{path_location}")
|
||||
|
||||
for text, number in expected:
|
||||
current = CURRENT_DIR / f"samples/{path_location}"
|
||||
path = current.relative_to(CURRENT_DIR.parent)
|
||||
|
||||
target = Path.cwd()
|
||||
while True:
|
||||
note = _text_from_path(current, path, target, number, text)
|
||||
try:
|
||||
assert note in output, output
|
||||
except AssertionError:
|
||||
target = target.parent
|
||||
if not target.exists():
|
||||
raise
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
def _text_from_path(
|
||||
base: Path, path: Path, target: Path, number: int, text: str
|
||||
) -> str:
|
||||
relative_to_cwd = base.relative_to(target)
|
||||
prefix = ".".join(relative_to_cwd.parts[:-1])
|
||||
text = text.replace(path.stem, f"{prefix}.{path.stem}")
|
||||
return f'{path}:{number}: note: Revealed type is "{text}"'
|
||||
Reference in New Issue
Block a user