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

feat: pass examples given in the FieldDefinition to the OpenAPIMediaType #3222

Draft
wants to merge 25 commits into
base: v3.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f39864e
feat: Support `schema_extra` in `Parameter` and `Body` (#3204)
tuukkamustonen Mar 19, 2024
085c88a
feat: Added precedence of CLI parameters over envs (#3190)
kedod Mar 17, 2024
4544ed7
fix: pass examples given in the FieldDefinition to the OpenAPIMediaTy…
robswc Feb 23, 2024
eb95a86
fix: move imports into type-checking block
robswc Feb 23, 2024
48a1556
fix: ruff-format
robswc Feb 23, 2024
1cba655
fix: add test and instance checking.
robswc Feb 23, 2024
3426fe3
fix: use `get_formatted_examples` to format examples.
robswc Feb 24, 2024
d4e7401
fix: pass examples given in the FieldDefinition to the OpenAPIMediaTy…
robswc Feb 23, 2024
5288f5e
fix: use `get_formatted_examples` to format examples.
robswc Feb 24, 2024
95b473c
fix: improve example type-checking
robswc Mar 17, 2024
9882ea7
fix: remove breaking type checking.
robswc Mar 17, 2024
ac67c60
fix: remove unused import
robswc Mar 17, 2024
3b5cbb8
feat: Added precedence of CLI parameters over envs (#3190)
kedod Mar 17, 2024
1d54273
fix: F811 Redefinition of unused `test_run_command_arguments_preceden…
robswc Mar 18, 2024
8eddfaa
Trigger documentation build
JacobCoffee Mar 27, 2024
61388f1
Trigger documentation build
JacobCoffee Mar 27, 2024
e166797
feat: Add LITESTAR_ prefix before WEB_CONCURRENCY env option (#3227)
kedod Mar 27, 2024
1c41cd1
feat: Support `schema_extra` in `Parameter` and `Body` (#3204)
tuukkamustonen Mar 19, 2024
cceefda
Trigger documentation build
JacobCoffee Mar 27, 2024
8b8ac98
Trigger documentation build
JacobCoffee Mar 27, 2024
1204e80
fix: add test and instance checking.
robswc Feb 23, 2024
e3daa83
feat: Support `schema_extra` in `Parameter` and `Body` (#3204)
tuukkamustonen Mar 19, 2024
c01f993
Trigger documentation build
JacobCoffee Mar 27, 2024
a8e9977
Trigger documentation build
JacobCoffee Mar 27, 2024
58108c0
fix: import `Any` in test_request_body.py
robswc Apr 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions litestar/_openapi/request_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@

__all__ = ("create_request_body",)


if TYPE_CHECKING:
from litestar._openapi.datastructures import OpenAPIContext
from litestar.dto import AbstractDTO
from litestar.openapi.spec import Example, Reference
from litestar.typing import FieldDefinition


Expand Down Expand Up @@ -48,4 +48,11 @@ def create_request_body(
else:
schema = schema_creator.for_field_definition(data_field)

return RequestBody(required=True, content={media_type: OpenAPIMediaType(schema=schema)})
examples: dict[str, Example | Reference] | None = None
if isinstance(data_field.kwarg_definition, BodyKwarg) and data_field.kwarg_definition.examples:
examples = {}
for example in data_field.kwarg_definition.examples:
if isinstance(example.summary, str) and isinstance(example.value, dict):
examples[example.summary] = example

return RequestBody(required=True, content={media_type: OpenAPIMediaType(schema=schema, examples=examples)})
16 changes: 16 additions & 0 deletions litestar/_openapi/schema_generation/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,22 @@
)
setattr(schema, schema_key, value)

if isinstance(field.kwarg_definition, KwargDefinition) and (extra := field.kwarg_definition.schema_extra):
for schema_key, value in extra.items():
if not hasattr(schema, schema_key):
raise ValueError(

Check warning on line 583 in litestar/_openapi/schema_generation/schema.py

View check run for this annotation

Codecov / codecov/patch

litestar/_openapi/schema_generation/schema.py#L583

Added line #L583 was not covered by tests
f"`schema_extra` declares key `{schema_key}` which does not exist in `Schema` object"
)
setattr(schema, schema_key, value)

if isinstance(field.kwarg_definition, KwargDefinition) and (extra := field.kwarg_definition.schema_extra):
for schema_key, value in extra.items():
if not hasattr(schema, schema_key):
raise ValueError(

Check warning on line 591 in litestar/_openapi/schema_generation/schema.py

View check run for this annotation

Codecov / codecov/patch

litestar/_openapi/schema_generation/schema.py#L591

Added line #L591 was not covered by tests
f"`schema_extra` declares key `{schema_key}` which does not exist in `Schema` object"
)
setattr(schema, schema_key, value)

if schema.default is None and field.default is not Empty:
schema.default = field.default

Expand Down
109 changes: 109 additions & 0 deletions tests/unit/test_cli/test_core_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,115 @@ def test_run_command_arguments_precedence(
assert expected in mock_subprocess_run.call_args_list[0].args[0]


@pytest.mark.parametrize(
"cli, env, expected",
(
(
("--reload", True),
("LITESTAR_RELOAD", False),
"--reload",
),
(
("--reload-dir", [".", "../somewhere_else"]),
("LITESTAR_RELOAD_DIRS", ["../somewhere_else3", "../somewhere_else2"]),
["--reload-dir=.", "--reload-dir=../somewhere_else"],
),
(
("--reload-include", ["*.rst", "*.yml"]),
("LITESTAR_RELOAD_INCLUDES", ["*.rst2", "*.yml2"]),
["--reload-include=*.rst", "--reload-include=*.yml"],
),
(
("--reload-exclude", ["*.rst", "*.yml"]),
("LITESTAR_RELOAD_EXCLUDES", ["*.rst2", "*.yml2"]),
["--reload-exclude=*.rst", "--reload-exclude=*.yml"],
),
(
("--wc", 2),
("LITESTAR_WEB_CONCURRENCY", 4),
"--workers=2",
),
(
("--fd", 0),
("LITESTAR_FILE_DESCRIPTOR", 1),
"--fd=0",
),
(
("--uds", "/run/uvicorn/litestar_test.sock"),
("LITESTAR_UNIX_DOMAIN_SOCKET", "/run/uvicorn/litestar_test2.sock"),
"--uds=/run/uvicorn/litestar_test.sock",
),
(
("-d", True),
("LITESTAR_DEBUG", False),
("LITESTAR_DEBUG", "1"),
),
(
("--pdb", True),
("LITESTAR_PDB", False),
("LITESTAR_PDB", "1"),
),
),
)
def test_run_command_arguments_precedence(
cli: Tuple[str, Union[Literal[True], List[str], str]],
env: Tuple[str, Union[Literal[True], List[str], str]],
expected: str,
runner: CliRunner,
monkeypatch: MonkeyPatch,
mock_subprocess_run: MagicMock,
tmp_project_dir: Path,
create_app_file: CreateAppFileFixture,
mock_uvicorn_run: MagicMock,
) -> None:
args = []
args.extend(["--app", f"{Path('my_app.py').stem}:app"])
args.extend(["--app-dir", str(Path(tmp_project_dir / "custom_subfolder"))])
args.extend(["run"])
create_app_file("my_app.py", directory="custom_subfolder")

env_name, env_value = env
cli_name, cli_value = cli

if env_name:
if isinstance(env_value, list):
monkeypatch.setenv(env_name, "".join(env_value))
else:
monkeypatch.setenv(env_name, env_value) # type: ignore[arg-type] # pyright: ignore (reportGeneralTypeIssues)

if cli_name:
if cli_value is True:
args.append(cli_name)
elif isinstance(cli_value, list):
for value in cli_value:
args.extend([cli_name, value])
else:
args.extend([cli_name, cli_value])

result = runner.invoke(cli_command, args)

assert result.exception is None
assert result.exit_code == 0

if cli_name in ["--fd", "--uds"]:
mock_subprocess_run.assert_not_called()
if isinstance(expected, list): # type: ignore[unreachable]
assert all(_ in mock_uvicorn_run.call_args_list[0].args[0] for _ in expected) # type: ignore[unreachable]
else:
assert mock_uvicorn_run.call_args_list[0].kwargs.get(cli_name.strip("--")) == cli_value

elif cli_name in ["-d", "--pdb"]:
assert os.environ.get(expected[0]) == expected[1]

else:
mock_subprocess_run.assert_called_once()

if isinstance(expected, list): # type: ignore[unreachable]
assert all(_ in mock_subprocess_run.call_args_list[0].args[0] for _ in expected) # type: ignore[unreachable]
else:
assert expected in mock_subprocess_run.call_args_list[0].args[0]


@pytest.fixture()
def unset_env() -> Generator[None, None, None]:
initial_env = {**os.environ}
Expand Down
29 changes: 16 additions & 13 deletions tests/unit/test_openapi/test_request_body.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Type
from unittest.mock import ANY, MagicMock

import pytest
from typing_extensions import Annotated
Expand All @@ -9,11 +8,10 @@
from litestar._openapi.datastructures import OpenAPIContext
from litestar._openapi.request_body import create_request_body
from litestar.datastructures.upload_file import UploadFile
from litestar.dto import AbstractDTO
from litestar.enums import RequestEncodingType
from litestar.handlers import BaseRouteHandler
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.spec import RequestBody
from litestar.openapi.spec import Example, RequestBody
from litestar.params import Body
from litestar.typing import FieldDefinition

Expand Down Expand Up @@ -168,16 +166,21 @@ async def handle_form_upload(
}


def test_request_body_generation_with_dto(create_request: RequestBodyFactory) -> None:
mock_dto = MagicMock(spec=AbstractDTO)
def test_example_in_request_body_schema_generation() -> None:
@dataclass
class SampleClass:
name: str
age: int

@post(path="/form-upload", dto=mock_dto) # pyright: ignore
async def handler(data: Dict[str, Any]) -> None:
@post(path="/example")
async def handler(
data: Annotated[SampleClass, Body(examples=[Example(summary="example", value={"name": "John", "age": 30})])],
) -> None:
return None

Litestar(route_handlers=[handler])
field_definition = FieldDefinition.from_annotation(Dict[str, Any])
create_request(handler, field_definition)
mock_dto.create_openapi_schema.assert_called_once_with(
field_definition=field_definition, handler_id=handler.handler_id, schema_creator=ANY
)
app = Litestar([handler])
schema = app.openapi_schema.to_schema()

assert schema["paths"]["/example"]["post"]["requestBody"]["content"]["application/json"]["examples"] == {
"example": {"summary": "example", "value": {"name": "John", "age": 30}}
}
Loading