From 7ff5da8bf2b8bfe895e4621f630e9fc3f2c434f4 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 6 Aug 2024 14:46:39 +0100 Subject: [PATCH 01/89] edit middleware docs code sample to use perf_counter as a timer --- docs_src/middleware/tutorial001.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs_src/middleware/tutorial001.py b/docs_src/middleware/tutorial001.py index 6bab3410a6548..e65a7dade1791 100644 --- a/docs_src/middleware/tutorial001.py +++ b/docs_src/middleware/tutorial001.py @@ -7,8 +7,8 @@ @app.middleware("http") async def add_process_time_header(request: Request, call_next): - start_time = time.time() + start_time = time.perf_counter() response = await call_next(request) - process_time = time.time() - start_time + process_time = time.perf_counter() - start_time response.headers["X-Process-Time"] = str(process_time) return response From 5827b922c3ebf999eae867dbb855c0bf2aa07a7e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 31 Aug 2024 03:23:14 +0000 Subject: [PATCH 02/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2f7fb0cbdbabb..d1ac98ed14b1a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Tweak middleware code sample `time.time()` to `time.perf_counter()`. PR [#11957](https://github.com/fastapi/fastapi/pull/11957) by [@domdent](https://github.com/domdent). * 🔧 Update sponsors: Coherence. PR [#12093](https://github.com/fastapi/fastapi/pull/12093) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix async test example not to trigger DeprecationWarning. PR [#12084](https://github.com/fastapi/fastapi/pull/12084) by [@marcinsulikowski](https://github.com/marcinsulikowski). * 📝 Update `docs_src/path_params_numeric_validations/tutorial006.py`. PR [#11478](https://github.com/fastapi/fastapi/pull/11478) by [@MuhammadAshiqAmeer](https://github.com/MuhammadAshiqAmeer). From c37f2c976dfe71ac46ccc082b551d41cb7b6122f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 31 Aug 2024 12:15:50 +0200 Subject: [PATCH 03/89] =?UTF-8?q?=F0=9F=93=9D=20Add=20note=20about=20`time?= =?UTF-8?q?.perf=5Fcounter()`=20in=20middlewares=20(#12095)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/middleware.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/docs/tutorial/middleware.md b/docs/en/docs/tutorial/middleware.md index 06fb3f504eca6..199b593d3eea0 100644 --- a/docs/en/docs/tutorial/middleware.md +++ b/docs/en/docs/tutorial/middleware.md @@ -63,6 +63,12 @@ For example, you could add a custom header `X-Process-Time` containing the time {!../../../docs_src/middleware/tutorial001.py!} ``` +/// tip + +Here we use `time.perf_counter()` instead of `time.time()` because it can be more precise for these use cases. 🤓 + +/// + ## Other middlewares You can later read more about other middlewares in the [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank}. From 6c8a205db106835c2cd4af5d0b5110f562c2a92f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 31 Aug 2024 10:16:12 +0000 Subject: [PATCH 04/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d1ac98ed14b1a..e0e9192bda8dd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add note about `time.perf_counter()` in middlewares. PR [#12095](https://github.com/fastapi/fastapi/pull/12095) by [@tiangolo](https://github.com/tiangolo). * 📝 Tweak middleware code sample `time.time()` to `time.perf_counter()`. PR [#11957](https://github.com/fastapi/fastapi/pull/11957) by [@domdent](https://github.com/domdent). * 🔧 Update sponsors: Coherence. PR [#12093](https://github.com/fastapi/fastapi/pull/12093) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix async test example not to trigger DeprecationWarning. PR [#12084](https://github.com/fastapi/fastapi/pull/12084) by [@marcinsulikowski](https://github.com/marcinsulikowski). From 0077af97195ec77ab2fcc5a3cf1536c7ed6125d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 31 Aug 2024 12:18:37 +0200 Subject: [PATCH 05/89] =?UTF-8?q?=F0=9F=94=A7=20Update=20labeler=20config?= =?UTF-8?q?=20to=20handle=20sponsorships=20data=20(#12096)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index 1d49a24116570..c5b1f84f3d942 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -7,6 +7,8 @@ docs: - all-globs-to-all-files: - '!fastapi/**' - '!pyproject.toml' + - '!docs/en/data/sponsors.yml' + - '!docs/en/overrides/main.html' lang-all: - all: @@ -28,6 +30,8 @@ internal: - .pre-commit-config.yaml - pdm_build.py - requirements*.txt + - docs/en/data/sponsors.yml + - docs/en/overrides/main.html - all-globs-to-all-files: - '!docs/*/docs/**' - '!fastapi/**' From 83422b1923b070da891bfacec707f4d2c796a32c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 31 Aug 2024 10:20:40 +0000 Subject: [PATCH 06/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e0e9192bda8dd..a87ce21d4ca70 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -27,6 +27,7 @@ hide: ### Internal +* 🔧 Update labeler config to handle sponsorships data. PR [#12096](https://github.com/fastapi/fastapi/pull/12096) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Kong. PR [#12085](https://github.com/fastapi/fastapi/pull/12085) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12076](https://github.com/fastapi/fastapi/pull/12076) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 👷 Update `latest-changes` GitHub Action. PR [#12073](https://github.com/fastapi/fastapi/pull/12073) by [@tiangolo](https://github.com/tiangolo). From eebc6c3d54f96cfd056c0a6fe4de7c3b0064ac42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 31 Aug 2024 17:35:58 +0200 Subject: [PATCH 07/89] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors=20link:?= =?UTF-8?q?=20Coherence=20(#12097)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/overrides/main.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 851f8e8959e99..47e46c4bf29be 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -59,7 +59,7 @@
- + From 47b3351be9c268a3b79faf9b41bf08921dbc8a8b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 31 Aug 2024 15:36:24 +0000 Subject: [PATCH 08/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a87ce21d4ca70..855338742a5c8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -27,6 +27,7 @@ hide: ### Internal +* 🔧 Update sponsors link: Coherence. PR [#12097](https://github.com/fastapi/fastapi/pull/12097) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update labeler config to handle sponsorships data. PR [#12096](https://github.com/fastapi/fastapi/pull/12096) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Kong. PR [#12085](https://github.com/fastapi/fastapi/pull/12085) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12076](https://github.com/fastapi/fastapi/pull/12076) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 581aacc4a9f3bbb872b082ee55535fe60655de2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 31 Aug 2024 22:19:30 +0200 Subject: [PATCH 09/89] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20and=20sim?= =?UTF-8?q?plify=20dependencies=20data=20structures=20with=20dataclasses?= =?UTF-8?q?=20(#12098)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/models.py | 75 ++++++++++++---------------------- fastapi/dependencies/utils.py | 2 +- 2 files changed, 28 insertions(+), 49 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 61ef006387781..418c117259aa3 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -1,58 +1,37 @@ -from typing import Any, Callable, List, Optional, Sequence +from dataclasses import dataclass, field +from typing import Any, Callable, List, Optional, Sequence, Tuple from fastapi._compat import ModelField from fastapi.security.base import SecurityBase +@dataclass class SecurityRequirement: - def __init__( - self, security_scheme: SecurityBase, scopes: Optional[Sequence[str]] = None - ): - self.security_scheme = security_scheme - self.scopes = scopes + security_scheme: SecurityBase + scopes: Optional[Sequence[str]] = None +@dataclass class Dependant: - def __init__( - self, - *, - path_params: Optional[List[ModelField]] = None, - query_params: Optional[List[ModelField]] = None, - header_params: Optional[List[ModelField]] = None, - cookie_params: Optional[List[ModelField]] = None, - body_params: Optional[List[ModelField]] = None, - dependencies: Optional[List["Dependant"]] = None, - security_schemes: Optional[List[SecurityRequirement]] = None, - name: Optional[str] = None, - call: Optional[Callable[..., Any]] = None, - request_param_name: Optional[str] = None, - websocket_param_name: Optional[str] = None, - http_connection_param_name: Optional[str] = None, - response_param_name: Optional[str] = None, - background_tasks_param_name: Optional[str] = None, - security_scopes_param_name: Optional[str] = None, - security_scopes: Optional[List[str]] = None, - use_cache: bool = True, - path: Optional[str] = None, - ) -> None: - self.path_params = path_params or [] - self.query_params = query_params or [] - self.header_params = header_params or [] - self.cookie_params = cookie_params or [] - self.body_params = body_params or [] - self.dependencies = dependencies or [] - self.security_requirements = security_schemes or [] - self.request_param_name = request_param_name - self.websocket_param_name = websocket_param_name - self.http_connection_param_name = http_connection_param_name - self.response_param_name = response_param_name - self.background_tasks_param_name = background_tasks_param_name - self.security_scopes = security_scopes - self.security_scopes_param_name = security_scopes_param_name - self.name = name - self.call = call - self.use_cache = use_cache - # Store the path to be able to re-generate a dependable from it in overrides - self.path = path - # Save the cache key at creation to optimize performance + path_params: List[ModelField] = field(default_factory=list) + query_params: List[ModelField] = field(default_factory=list) + header_params: List[ModelField] = field(default_factory=list) + cookie_params: List[ModelField] = field(default_factory=list) + body_params: List[ModelField] = field(default_factory=list) + dependencies: List["Dependant"] = field(default_factory=list) + security_requirements: List[SecurityRequirement] = field(default_factory=list) + name: Optional[str] = None + call: Optional[Callable[..., Any]] = None + request_param_name: Optional[str] = None + websocket_param_name: Optional[str] = None + http_connection_param_name: Optional[str] = None + response_param_name: Optional[str] = None + background_tasks_param_name: Optional[str] = None + security_scopes_param_name: Optional[str] = None + security_scopes: Optional[List[str]] = None + use_cache: bool = True + path: Optional[str] = None + cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False) + + def __post_init__(self) -> None: self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or [])))) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 3e8e7b4101e0f..85703c9e96983 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -175,7 +175,7 @@ def get_flat_dependant( header_params=dependant.header_params.copy(), cookie_params=dependant.cookie_params.copy(), body_params=dependant.body_params.copy(), - security_schemes=dependant.security_requirements.copy(), + security_requirements=dependant.security_requirements.copy(), use_cache=dependant.use_cache, path=dependant.path, ) From 75c4e7fc44fda0c63d5627af65cb0ba7919fd334 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 31 Aug 2024 20:19:51 +0000 Subject: [PATCH 10/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 855338742a5c8..a02b722f6322d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ♻️ Refactor and simplify dependencies data structures with dataclasses. PR [#12098](https://github.com/fastapi/fastapi/pull/12098) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Add note about `time.perf_counter()` in middlewares. PR [#12095](https://github.com/fastapi/fastapi/pull/12095) by [@tiangolo](https://github.com/tiangolo). From 08547e1d571df8e8cf71d8b15a767acbc38aea62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 31 Aug 2024 22:27:44 +0200 Subject: [PATCH 11/89] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20and=20sim?= =?UTF-8?q?plify=20internal=20`analyze=5Fparam()`=20to=20structure=20data?= =?UTF-8?q?=20with=20dataclasses=20instead=20of=20tuple=20(#12099)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 85703c9e96983..5ebdddaf658ce 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,6 +1,7 @@ import inspect from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy +from dataclasses import dataclass from typing import ( Any, Callable, @@ -258,16 +259,16 @@ def get_dependant( ) for param_name, param in signature_params.items(): is_path_param = param_name in path_param_names - type_annotation, depends, param_field = analyze_param( + param_details = analyze_param( param_name=param_name, annotation=param.annotation, value=param.default, is_path_param=is_path_param, ) - if depends is not None: + if param_details.depends is not None: sub_dependant = get_param_sub_dependant( param_name=param_name, - depends=depends, + depends=param_details.depends, path=path, security_scopes=security_scopes, ) @@ -275,18 +276,18 @@ def get_dependant( continue if add_non_field_param_to_dependency( param_name=param_name, - type_annotation=type_annotation, + type_annotation=param_details.type_annotation, dependant=dependant, ): assert ( - param_field is None + param_details.field is None ), f"Cannot specify multiple FastAPI annotations for {param_name!r}" continue - assert param_field is not None - if is_body_param(param_field=param_field, is_path_param=is_path_param): - dependant.body_params.append(param_field) + assert param_details.field is not None + if is_body_param(param_field=param_details.field, is_path_param=is_path_param): + dependant.body_params.append(param_details.field) else: - add_param_to_fields(field=param_field, dependant=dependant) + add_param_to_fields(field=param_details.field, dependant=dependant) return dependant @@ -314,13 +315,20 @@ def add_non_field_param_to_dependency( return None +@dataclass +class ParamDetails: + type_annotation: Any + depends: Optional[params.Depends] + field: Optional[ModelField] + + def analyze_param( *, param_name: str, annotation: Any, value: Any, is_path_param: bool, -) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]: +) -> ParamDetails: field_info = None depends = None type_annotation: Any = Any @@ -450,7 +458,7 @@ def analyze_param( field_info=field_info, ) - return type_annotation, depends, field + return ParamDetails(type_annotation=type_annotation, depends=depends, field=field) def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool: From 8d7d89e8c603ca13043ce037503c66cc6a662a48 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 31 Aug 2024 20:28:07 +0000 Subject: [PATCH 12/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a02b722f6322d..677816f40c10e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ♻️ Refactor and simplify internal `analyze_param()` to structure data with dataclasses instead of tuple. PR [#12099](https://github.com/fastapi/fastapi/pull/12099) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and simplify dependencies data structures with dataclasses. PR [#12098](https://github.com/fastapi/fastapi/pull/12098) by [@tiangolo](https://github.com/tiangolo). ### Docs From 5b7fa3900e3156dcb93f496516740bc06903d7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 31 Aug 2024 22:52:06 +0200 Subject: [PATCH 13/89] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20and=20sim?= =?UTF-8?q?plify=20internal=20data=20from=20`solve=5Fdependencies()`=20usi?= =?UTF-8?q?ng=20dataclasses=20(#12100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 45 +++++++++++++++++++---------------- fastapi/routing.py | 33 +++++++++++++++---------- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 5ebdddaf658ce..ed03df88bd110 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -529,6 +529,15 @@ async def solve_generator( return await stack.enter_async_context(cm) +@dataclass +class SolvedDependency: + values: Dict[str, Any] + errors: List[Any] + background_tasks: Optional[StarletteBackgroundTasks] + response: Response + dependency_cache: Dict[Tuple[Callable[..., Any], Tuple[str]], Any] + + async def solve_dependencies( *, request: Union[Request, WebSocket], @@ -539,13 +548,7 @@ async def solve_dependencies( dependency_overrides_provider: Optional[Any] = None, dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, async_exit_stack: AsyncExitStack, -) -> Tuple[ - Dict[str, Any], - List[Any], - Optional[StarletteBackgroundTasks], - Response, - Dict[Tuple[Callable[..., Any], Tuple[str]], Any], -]: +) -> SolvedDependency: values: Dict[str, Any] = {} errors: List[Any] = [] if response is None: @@ -587,27 +590,21 @@ async def solve_dependencies( dependency_cache=dependency_cache, async_exit_stack=async_exit_stack, ) - ( - sub_values, - sub_errors, - background_tasks, - _, # the subdependency returns the same response we have - sub_dependency_cache, - ) = solved_result - dependency_cache.update(sub_dependency_cache) - if sub_errors: - errors.extend(sub_errors) + background_tasks = solved_result.background_tasks + dependency_cache.update(solved_result.dependency_cache) + if solved_result.errors: + errors.extend(solved_result.errors) continue if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache: solved = dependency_cache[sub_dependant.cache_key] elif is_gen_callable(call) or is_async_gen_callable(call): solved = await solve_generator( - call=call, stack=async_exit_stack, sub_values=sub_values + call=call, stack=async_exit_stack, sub_values=solved_result.values ) elif is_coroutine_callable(call): - solved = await call(**sub_values) + solved = await call(**solved_result.values) else: - solved = await run_in_threadpool(call, **sub_values) + solved = await run_in_threadpool(call, **solved_result.values) if sub_dependant.name is not None: values[sub_dependant.name] = solved if sub_dependant.cache_key not in dependency_cache: @@ -654,7 +651,13 @@ async def solve_dependencies( values[dependant.security_scopes_param_name] = SecurityScopes( scopes=dependant.security_scopes ) - return values, errors, background_tasks, response, dependency_cache + return SolvedDependency( + values=values, + errors=errors, + background_tasks=background_tasks, + response=response, + dependency_cache=dependency_cache, + ) def request_params_to_args( diff --git a/fastapi/routing.py b/fastapi/routing.py index 49f1b60138f3d..c46772017d684 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -292,26 +292,34 @@ async def app(request: Request) -> Response: dependency_overrides_provider=dependency_overrides_provider, async_exit_stack=async_exit_stack, ) - values, errors, background_tasks, sub_response, _ = solved_result + errors = solved_result.errors if not errors: raw_response = await run_endpoint_function( - dependant=dependant, values=values, is_coroutine=is_coroutine + dependant=dependant, + values=solved_result.values, + is_coroutine=is_coroutine, ) if isinstance(raw_response, Response): if raw_response.background is None: - raw_response.background = background_tasks + raw_response.background = solved_result.background_tasks response = raw_response else: - response_args: Dict[str, Any] = {"background": background_tasks} + response_args: Dict[str, Any] = { + "background": solved_result.background_tasks + } # If status_code was set, use it, otherwise use the default from the # response class, in the case of redirect it's 307 current_status_code = ( - status_code if status_code else sub_response.status_code + status_code + if status_code + else solved_result.response.status_code ) if current_status_code is not None: response_args["status_code"] = current_status_code - if sub_response.status_code: - response_args["status_code"] = sub_response.status_code + if solved_result.response.status_code: + response_args["status_code"] = ( + solved_result.response.status_code + ) content = await serialize_response( field=response_field, response_content=raw_response, @@ -326,7 +334,7 @@ async def app(request: Request) -> Response: response = actual_response_class(content, **response_args) if not is_body_allowed_for_status_code(response.status_code): response.body = b"" - response.headers.raw.extend(sub_response.headers.raw) + response.headers.raw.extend(solved_result.response.headers.raw) if errors: validation_error = RequestValidationError( _normalize_errors(errors), body=body @@ -360,11 +368,12 @@ async def app(websocket: WebSocket) -> None: dependency_overrides_provider=dependency_overrides_provider, async_exit_stack=async_exit_stack, ) - values, errors, _, _2, _3 = solved_result - if errors: - raise WebSocketRequestValidationError(_normalize_errors(errors)) + if solved_result.errors: + raise WebSocketRequestValidationError( + _normalize_errors(solved_result.errors) + ) assert dependant.call is not None, "dependant.call must be a function" - await dependant.call(**values) + await dependant.call(**solved_result.values) return app From 3660c7a063ddc605269b0c204acd4724ccf2d69c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 31 Aug 2024 20:52:29 +0000 Subject: [PATCH 14/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 677816f40c10e..3fc659cbbdeed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ♻️ Refactor and simplify internal data from `solve_dependencies()` using dataclasses. PR [#12100](https://github.com/fastapi/fastapi/pull/12100) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and simplify internal `analyze_param()` to structure data with dataclasses instead of tuple. PR [#12099](https://github.com/fastapi/fastapi/pull/12099) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and simplify dependencies data structures with dataclasses. PR [#12098](https://github.com/fastapi/fastapi/pull/12098) by [@tiangolo](https://github.com/tiangolo). From d08b95ea57fa5740c8c04da554f2b6e259f4dea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 1 Sep 2024 01:46:03 +0200 Subject: [PATCH 15/89] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20internal=20?= =?UTF-8?q?`create=5Fresponse=5Ffield()`=20to=20`create=5Fmodel=5Ffield()`?= =?UTF-8?q?=20as=20it's=20used=20for=20more=20than=20response=20models=20(?= =?UTF-8?q?#12103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 6 +++--- fastapi/routing.py | 6 +++--- fastapi/utils.py | 9 +++------ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index ed03df88bd110..c5ed709f76ab9 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -55,7 +55,7 @@ from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect -from fastapi.utils import create_response_field, get_path_param_names +from fastapi.utils import create_model_field, get_path_param_names from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool @@ -449,7 +449,7 @@ def analyze_param( else: alias = field_info.alias or param_name field_info.alias = alias - field = create_response_field( + field = create_model_field( name=param_name, type_=use_annotation_from_field_info, default=field_info.default, @@ -818,7 +818,7 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: ] if len(set(body_param_media_types)) == 1: BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0] - final_field = create_response_field( + final_field = create_model_field( name="body", type_=BodyModel, required=required, diff --git a/fastapi/routing.py b/fastapi/routing.py index c46772017d684..61a112fc47ea6 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -49,7 +49,7 @@ from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import ( create_cloned_field, - create_response_field, + create_model_field, generate_unique_id, get_value_or_default, is_body_allowed_for_status_code, @@ -497,7 +497,7 @@ def __init__( status_code ), f"Status code {status_code} must not have a response body" response_name = "Response_" + self.unique_id - self.response_field = create_response_field( + self.response_field = create_model_field( name=response_name, type_=self.response_model, mode="serialization", @@ -530,7 +530,7 @@ def __init__( additional_status_code ), f"Status code {additional_status_code} must not have a response body" response_name = f"Response_{additional_status_code}_{self.unique_id}" - response_field = create_response_field(name=response_name, type_=model) + response_field = create_model_field(name=response_name, type_=model) response_fields[additional_status_code] = response_field if response_fields: self.response_fields: Dict[Union[int, str], ModelField] = response_fields diff --git a/fastapi/utils.py b/fastapi/utils.py index 5c2538facaa93..4c7350fea9e5e 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -60,9 +60,9 @@ def get_path_param_names(path: str) -> Set[str]: return set(re.findall("{(.*?)}", path)) -def create_response_field( +def create_model_field( name: str, - type_: Type[Any], + type_: Any, class_validators: Optional[Dict[str, Validator]] = None, default: Optional[Any] = Undefined, required: Union[bool, UndefinedType] = Undefined, @@ -71,9 +71,6 @@ def create_response_field( alias: Optional[str] = None, mode: Literal["validation", "serialization"] = "validation", ) -> ModelField: - """ - Create a new response field. Raises if type_ is invalid. - """ class_validators = class_validators or {} if PYDANTIC_V2: field_info = field_info or FieldInfo( @@ -135,7 +132,7 @@ def create_cloned_field( use_type.__fields__[f.name] = create_cloned_field( f, cloned_types=cloned_types ) - new_field = create_response_field(name=field.name, type_=use_type) + new_field = create_model_field(name=field.name, type_=use_type) new_field.has_alias = field.has_alias # type: ignore[attr-defined] new_field.alias = field.alias # type: ignore[misc] new_field.class_validators = field.class_validators # type: ignore[attr-defined] From d5c6cf8122cd8de3d4fcd37b616fbfa2ade1542b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 31 Aug 2024 23:46:26 +0000 Subject: [PATCH 16/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3fc659cbbdeed..d7b278dbe1aae 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ♻️ Rename internal `create_response_field()` to `create_model_field()` as it's used for more than response models. PR [#12103](https://github.com/fastapi/fastapi/pull/12103) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and simplify internal data from `solve_dependencies()` using dataclasses. PR [#12100](https://github.com/fastapi/fastapi/pull/12100) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and simplify internal `analyze_param()` to structure data with dataclasses instead of tuple. PR [#12099](https://github.com/fastapi/fastapi/pull/12099) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and simplify dependencies data structures with dataclasses. PR [#12098](https://github.com/fastapi/fastapi/pull/12098) by [@tiangolo](https://github.com/tiangolo). From 23bda0ffeb26e906b5dcf58423522ab4166669ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 1 Sep 2024 21:39:25 +0200 Subject: [PATCH 17/89] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20internal?= =?UTF-8?q?=20`check=5Ffile=5Ffield()`,=20rename=20to=20`ensure=5Fmultipar?= =?UTF-8?q?t=5Fis=5Finstalled()`=20to=20clarify=20its=20purpose=20(#12106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 47 ++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index c5ed709f76ab9..0dcba62f130a9 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -80,25 +80,23 @@ ) -def check_file_field(field: ModelField) -> None: - field_info = field.field_info - if isinstance(field_info, params.Form): +def ensure_multipart_is_installed() -> None: + try: + # __version__ is available in both multiparts, and can be mocked + from multipart import __version__ # type: ignore + + assert __version__ try: - # __version__ is available in both multiparts, and can be mocked - from multipart import __version__ # type: ignore - - assert __version__ - try: - # parse_options_header is only available in the right multipart - from multipart.multipart import parse_options_header # type: ignore - - assert parse_options_header - except ImportError: - logger.error(multipart_incorrect_install_error) - raise RuntimeError(multipart_incorrect_install_error) from None + # parse_options_header is only available in the right multipart + from multipart.multipart import parse_options_header # type: ignore + + assert parse_options_header except ImportError: - logger.error(multipart_not_installed_error) - raise RuntimeError(multipart_not_installed_error) from None + logger.error(multipart_incorrect_install_error) + raise RuntimeError(multipart_incorrect_install_error) from None + except ImportError: + logger.error(multipart_not_installed_error) + raise RuntimeError(multipart_not_installed_error) from None def get_param_sub_dependant( @@ -336,6 +334,7 @@ def analyze_param( if annotation is not inspect.Signature.empty: use_annotation = annotation type_annotation = annotation + # Extract Annotated info if get_origin(use_annotation) is Annotated: annotated_args = get_args(annotation) type_annotation = annotated_args[0] @@ -355,6 +354,7 @@ def analyze_param( ) else: fastapi_annotation = None + # Set default for Annotated FieldInfo if isinstance(fastapi_annotation, FieldInfo): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( @@ -369,9 +369,10 @@ def analyze_param( field_info.default = value else: field_info.default = Required + # Get Annotated Depends elif isinstance(fastapi_annotation, params.Depends): depends = fastapi_annotation - + # Get Depends from default value if isinstance(value, params.Depends): assert depends is None, ( "Cannot specify `Depends` in `Annotated` and default value" @@ -382,6 +383,7 @@ def analyze_param( f" default value together for {param_name!r}" ) depends = value + # Get FieldInfo from default value elif isinstance(value, FieldInfo): assert field_info is None, ( "Cannot specify FastAPI annotations in `Annotated` and default value" @@ -391,11 +393,13 @@ def analyze_param( if PYDANTIC_V2: field_info.annotation = type_annotation + # Get Depends from type annotation if depends is not None and depends.dependency is None: # Copy `depends` before mutating it depends = copy(depends) depends.dependency = type_annotation + # Handle non-param type annotations like Request if lenient_issubclass( type_annotation, ( @@ -411,6 +415,7 @@ def analyze_param( assert ( field_info is None ), f"Cannot specify FastAPI annotation for type {type_annotation!r}" + # Handle default assignations, neither field_info nor depends was not found in Annotated nor default value elif field_info is None and depends is None: default_value = value if value is not inspect.Signature.empty else Required if is_path_param: @@ -428,7 +433,9 @@ def analyze_param( field_info = params.Query(annotation=use_annotation, default=default_value) field = None + # It's a field_info, not a dependency if field_info is not None: + # Handle field_info.in_ if is_path_param: assert isinstance(field_info, params.Path), ( f"Cannot use `{field_info.__class__.__name__}` for path param" @@ -444,6 +451,8 @@ def analyze_param( field_info, param_name, ) + if isinstance(field_info, params.Form): + ensure_multipart_is_installed() if not field_info.alias and getattr(field_info, "convert_underscores", None): alias = param_name.replace("_", "-") else: @@ -786,7 +795,6 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: embed = getattr(field_info, "embed", None) body_param_names_set = {param.name for param in flat_dependant.body_params} if len(body_param_names_set) == 1 and not embed: - check_file_field(first_param) return first_param # If one field requires to embed, all have to be embedded # in case a sub-dependency is evaluated with a single unique body field @@ -825,5 +833,4 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: alias="body", field_info=BodyFieldInfo(**BodyFieldInfo_kwargs), ) - check_file_field(final_field) return final_field From b203d7a15fb5b49635bd81811e09ad94700f68a6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 1 Sep 2024 19:39:46 +0000 Subject: [PATCH 18/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d7b278dbe1aae..c3e7c35903c65 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* ♻️ Refactor internal `check_file_field()`, rename to `ensure_multipart_is_installed()` to clarify its purpose. PR [#12106](https://github.com/fastapi/fastapi/pull/12106) by [@tiangolo](https://github.com/tiangolo). * ♻️ Rename internal `create_response_field()` to `create_model_field()` as it's used for more than response models. PR [#12103](https://github.com/fastapi/fastapi/pull/12103) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and simplify internal data from `solve_dependencies()` using dataclasses. PR [#12100](https://github.com/fastapi/fastapi/pull/12100) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor and simplify internal `analyze_param()` to structure data with dataclasses instead of tuple. PR [#12099](https://github.com/fastapi/fastapi/pull/12099) by [@tiangolo](https://github.com/tiangolo). From 92bdfbc7bac466e502872a1ddf6e7cae069fd068 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:36:52 +0200 Subject: [PATCH 19/89] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pypi-pu?= =?UTF-8?q?blish=20from=201.9.0=20to=201.10.0=20(#12112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.9.0 to 1.10.0. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.9.0...v1.10.0) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 591df634b201b..03f87d17272b8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.0 - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} From 17f1f7b5bde8a75197c3aef7d4d24b04f8438083 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 2 Sep 2024 19:37:19 +0000 Subject: [PATCH 20/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c3e7c35903c65..bc431dfac5f86 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -35,6 +35,7 @@ hide: ### Internal +* ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.0. PR [#12112](https://github.com/fastapi/fastapi/pull/12112) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors link: Coherence. PR [#12097](https://github.com/fastapi/fastapi/pull/12097) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update labeler config to handle sponsorships data. PR [#12096](https://github.com/fastapi/fastapi/pull/12096) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Kong. PR [#12085](https://github.com/fastapi/fastapi/pull/12085) by [@tiangolo](https://github.com/tiangolo). From b63b4189eedc9e586fb51705a0a29ace8fa6a6d1 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Mon, 2 Sep 2024 21:53:53 +0200 Subject: [PATCH 21/89] =?UTF-8?q?=F0=9F=92=9A=20Set=20`include-hidden-file?= =?UTF-8?q?s`=20to=20`True`=20when=20using=20the=20`upload-artifact`=20GH?= =?UTF-8?q?=20action=20(#12118)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * include-hidden-files when uploading coverage files * include-hidden-files when building docs --- .github/workflows/build-docs.yml | 1 + .github/workflows/test.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index e46629e9b40d2..52c34a49ea6ba 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -113,6 +113,7 @@ jobs: with: name: docs-site-${{ matrix.lang }} path: ./site/** + include-hidden-files: true # https://github.com/marketplace/actions/alls-green#why docs-all-green: # This job does nothing and is only used for the branch protection diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0458f83ffe208..e9db49b51fd30 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,6 +91,7 @@ jobs: with: name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage + include-hidden-files: true coverage-combine: needs: [test] @@ -123,6 +124,7 @@ jobs: with: name: coverage-html path: htmlcov + include-hidden-files: true # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection From a6ad088183d860c8f985a5e14f916efe77ff5011 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 2 Sep 2024 19:54:19 +0000 Subject: [PATCH 22/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bc431dfac5f86..4774f8af92f28 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -35,6 +35,7 @@ hide: ### Internal +* 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#12118](https://github.com/fastapi/fastapi/pull/12118) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.0. PR [#12112](https://github.com/fastapi/fastapi/pull/12112) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors link: Coherence. PR [#12097](https://github.com/fastapi/fastapi/pull/12097) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update labeler config to handle sponsorships data. PR [#12096](https://github.com/fastapi/fastapi/pull/12096) by [@tiangolo](https://github.com/tiangolo). From 6b3d1c6d4e84447e310584ee62eaa231636a63d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:15:17 +0200 Subject: [PATCH 23/89] =?UTF-8?q?=E2=AC=86=20Bump=20pillow=20from=2010.3.0?= =?UTF-8?q?=20to=2010.4.0=20(#12105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.3.0 to 10.4.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/10.3.0...10.4.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index ab2b0165be0b8..332fd1857eddb 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -8,7 +8,7 @@ pyyaml >=5.3.1,<7.0.0 # For Material for MkDocs, Chinese search jieba==0.42.1 # For image processing by Material for MkDocs -pillow==10.3.0 +pillow==10.4.0 # For image processing by Material for MkDocs cairosvg==2.7.1 mkdocstrings[python]==0.25.1 From 7537bac43f1bcbf1edde837422cd4b720317c26b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Sep 2024 13:15:41 +0000 Subject: [PATCH 24/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4774f8af92f28..27900f2696802 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -35,6 +35,7 @@ hide: ### Internal +* ⬆ Bump pillow from 10.3.0 to 10.4.0. PR [#12105](https://github.com/fastapi/fastapi/pull/12105) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#12118](https://github.com/fastapi/fastapi/pull/12118) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.0. PR [#12112](https://github.com/fastapi/fastapi/pull/12112) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors link: Coherence. PR [#12097](https://github.com/fastapi/fastapi/pull/12097) by [@tiangolo](https://github.com/tiangolo). From c1c57336b04d17077a142ec39724e9fb1a1d8bec Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Tue, 3 Sep 2024 10:43:56 -0300 Subject: [PATCH 25/89] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20transla?= =?UTF-8?q?tion=20for=20`docs/pt/docs/advanced/security/index.md`=20(#1211?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/advanced/security/index.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/pt/docs/advanced/security/index.md diff --git a/docs/pt/docs/advanced/security/index.md b/docs/pt/docs/advanced/security/index.md new file mode 100644 index 0000000000000..ae63f1c96d12e --- /dev/null +++ b/docs/pt/docs/advanced/security/index.md @@ -0,0 +1,19 @@ +# Segurança Avançada + +## Funcionalidades Adicionais + +Existem algumas funcionalidades adicionais para lidar com segurança além das cobertas em [Tutorial - Guia de Usuário: Segurança](../../tutorial/security/index.md){.internal-link target=_blank}. + +/// tip | "Dica" + +As próximas seções **não são necessariamente "avançadas"**. + +E é possível que para o seu caso de uso, a solução está em uma delas. + +/// + +## Leia o Tutorial primeiro + +As próximas seções pressupõem que você já leu o principal [Tutorial - Guia de Usuário: Segurança](../../tutorial/security/index.md){.internal-link target=_blank}. + +Todas elas são baseadas nos mesmos conceitos, mas permitem algumas funcionalidades extras. From e26229ed98f8c1e9ccfbf4274157c554e5cd80f3 Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Tue, 3 Sep 2024 10:44:35 -0300 Subject: [PATCH 26/89] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20transla?= =?UTF-8?q?tion=20for=20`docs/pt/docs/advanced/testing-events.md`=20(#1210?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/advanced/testing-events.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/pt/docs/advanced/testing-events.md diff --git a/docs/pt/docs/advanced/testing-events.md b/docs/pt/docs/advanced/testing-events.md new file mode 100644 index 0000000000000..392fb741cac08 --- /dev/null +++ b/docs/pt/docs/advanced/testing-events.md @@ -0,0 +1,7 @@ +# Testando Eventos: inicialização - encerramento + +Quando você precisa que os seus manipuladores de eventos (`startup` e `shutdown`) sejam executados em seus testes, você pode utilizar o `TestClient` usando a instrução `with`: + +```Python hl_lines="9-12 20-24" +{!../../../docs_src/app_testing/tutorial003.py!} +``` From 56cfecc1bfef14506d50eb46b676dc832b85b914 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Sep 2024 13:44:55 +0000 Subject: [PATCH 27/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 27900f2696802..2f871a5c47d00 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Translations +* 🌐 Add Portuguese translation for `docs/pt/docs/advanced/security/index.md`. PR [#12114](https://github.com/fastapi/fastapi/pull/12114) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Dutch translation for `docs/nl/docs/index.md`. PR [#12042](https://github.com/fastapi/fastapi/pull/12042) by [@svlandeg](https://github.com/svlandeg). * 🌐 Update Chinese translation for `docs/zh/docs/how-to/index.md`. PR [#12070](https://github.com/fastapi/fastapi/pull/12070) by [@synthpop123](https://github.com/synthpop123). From 7d69943a22aada657b2326cec43ac6a3023d8203 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Sep 2024 13:45:21 +0000 Subject: [PATCH 28/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2f871a5c47d00..a9acd0278e25c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Translations +* 🌐 Add Portuguese translation for `docs/pt/docs/advanced/testing-events.md`. PR [#12108](https://github.com/fastapi/fastapi/pull/12108) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Portuguese translation for `docs/pt/docs/advanced/security/index.md`. PR [#12114](https://github.com/fastapi/fastapi/pull/12114) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Dutch translation for `docs/nl/docs/index.md`. PR [#12042](https://github.com/fastapi/fastapi/pull/12042) by [@svlandeg](https://github.com/svlandeg). * 🌐 Update Chinese translation for `docs/zh/docs/how-to/index.md`. PR [#12070](https://github.com/fastapi/fastapi/pull/12070) by [@synthpop123](https://github.com/synthpop123). From 7eae92544351036aa1ab0c70e7dea8e53eae97c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:47:08 +0200 Subject: [PATCH 29/89] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pypi-pu?= =?UTF-8?q?blish=20from=201.10.0=20to=201.10.1=20(#12120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.10.0 to 1.10.1. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.10.0...v1.10.1) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 03f87d17272b8..5004b94ddb5a6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.10.0 + uses: pypa/gh-action-pypi-publish@v1.10.1 - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} From 560c43269dbd4b3c2964a69cfb1487567dfcb80e Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Sep 2024 13:49:34 +0000 Subject: [PATCH 30/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a9acd0278e25c..a82965aa82414 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -37,6 +37,7 @@ hide: ### Internal +* ⬆ Bump pypa/gh-action-pypi-publish from 1.10.0 to 1.10.1. PR [#12120](https://github.com/fastapi/fastapi/pull/12120) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 10.3.0 to 10.4.0. PR [#12105](https://github.com/fastapi/fastapi/pull/12105) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#12118](https://github.com/fastapi/fastapi/pull/12118) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.0. PR [#12112](https://github.com/fastapi/fastapi/pull/12112) by [@dependabot[bot]](https://github.com/apps/dependabot). From 3feed9dd8c826a11354377c7a81b4d95382413d0 Mon Sep 17 00:00:00 2001 From: Max Scheijen <47034840+maxscheijen@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:50:38 +0200 Subject: [PATCH 31/89] =?UTF-8?q?=F0=9F=8C=90=20=20Add=20Dutch=20translati?= =?UTF-8?q?on=20for=20`docs/nl/docs/features.md`=20(#12101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/nl/docs/features.md | 201 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 docs/nl/docs/features.md diff --git a/docs/nl/docs/features.md b/docs/nl/docs/features.md new file mode 100644 index 0000000000000..848b155eced6a --- /dev/null +++ b/docs/nl/docs/features.md @@ -0,0 +1,201 @@ +# Functionaliteit + +## FastAPI functionaliteit + +**FastAPI** biedt je het volgende: + +### Gebaseerd op open standaarden + +* OpenAPI voor het maken van API's, inclusief declaraties van padbewerkingen, parameters, request bodies, beveiliging, enz. +* Automatische datamodel documentatie met JSON Schema (aangezien OpenAPI zelf is gebaseerd op JSON Schema). +* Ontworpen op basis van deze standaarden, na zorgvuldig onderzoek. In plaats van achteraf deze laag er bovenop te bouwen. +* Dit maakt het ook mogelijk om automatisch **clientcode te genereren** in verschillende programmeertalen. + +### Automatische documentatie + +Interactieve API-documentatie en verkenning van webgebruikersinterfaces. Aangezien dit framework is gebaseerd op OpenAPI, zijn er meerdere documentatie opties mogelijk, waarvan er standaard 2 zijn inbegrepen. + +* Swagger UI, met interactieve interface, maakt het mogelijk je API rechtstreeks vanuit de browser aan te roepen en te testen. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Alternatieve API-documentatie met ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Gewoon Moderne Python + +Het is allemaal gebaseerd op standaard **Python type** declaraties (dankzij Pydantic). Je hoeft dus geen nieuwe syntax te leren. Het is gewoon standaard moderne Python. + +Als je een opfriscursus van 2 minuten nodig hebt over het gebruik van Python types (zelfs als je FastAPI niet gebruikt), bekijk dan deze korte tutorial: [Python Types](python-types.md){.internal-link target=_blank}. + +Je schrijft gewoon standaard Python met types: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Declareer een variabele als een str +# en krijg editorondersteuning in de functie +def main(user_id: str): + return user_id + + +# Een Pydantic model +class User(BaseModel): + id: int + name: str + joined: date +``` + +Vervolgens kan je het op deze manier gebruiken: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +/// info + +`**second_user_data` betekent: + +Geef de sleutels (keys) en waarden (values) van de `second_user_data` dict direct door als sleutel-waarden argumenten, gelijk aan: `User(id=4, name=“Mary”, joined=“2018-11-30”)` + +/// + +### Editor-ondersteuning + +Het gehele framework is ontworpen om eenvoudig en intuïtief te zijn in gebruik. Alle beslissingen zijn getest op meerdere code-editors nog voordat het daadwerkelijke ontwikkelen begon, om zo de beste ontwikkelervaring te garanderen. + +Uit enquêtes onder Python ontwikkelaars blijkt maar al te duidelijk dat "(automatische) code aanvulling" een van de meest gebruikte functionaliteiten is. + +Het hele **FastAPI** framework is daarop gebaseerd. Automatische code aanvulling werkt overal. + +Je hoeft zelden terug te vallen op de documentatie. + +Zo kan je editor je helpen: + +* in Visual Studio Code: + +![editor ondersteuning](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* in PyCharm: + +![editor ondersteuning](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +Je krijgt autocomletion die je voorheen misschien zelfs voor onmogelijk had gehouden. Zoals bijvoorbeeld de `price` key in een JSON body (die genest had kunnen zijn) die afkomstig is van een request. + +Je hoeft niet langer de verkeerde keys in te typen, op en neer te gaan tussen de documentatie, of heen en weer te scrollen om te checken of je `username` of toch `user_name` had gebruikt. + +### Kort + +Dit framework heeft voor alles verstandige **standaardinstellingen**, met overal optionele configuraties. Alle parameters kunnen worden verfijnd zodat het past bij wat je nodig hebt, om zo de API te kunnen definiëren die jij nodig hebt. + +Maar standaard werkt alles **“gewoon”**. + +### Validatie + +* Validatie voor de meeste (of misschien wel alle?) Python **datatypes**, inclusief: + * JSON objecten (`dict`). + * JSON array (`list`) die itemtypes definiëren. + * String (`str`) velden, die min en max lengtes hebben. + * Getallen (`int`, `float`) met min en max waarden, enz. + +* Validatie voor meer exotische typen, zoals: + * URL. + * E-mail. + * UUID. + * ...en anderen. + +Alle validatie wordt uitgevoerd door het beproefde en robuuste **Pydantic**. + +### Beveiliging en authenticatie + +Beveiliging en authenticatie is geïntegreerd. Zonder compromissen te doen naar databases of datamodellen. + +Alle beveiligingsschema's gedefinieerd in OpenAPI, inclusief: + +* HTTP Basic. +* **OAuth2** (ook met **JWT tokens**). Bekijk de tutorial over [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* API keys in: + * Headers. + * Query parameters. + * Cookies, enz. + +Plus alle beveiligingsfuncties van Starlette (inclusief **sessiecookies**). + +Gebouwd als een herbruikbare tool met componenten die makkelijk te integreren zijn in en met je systemen, datastores, relationele en NoSQL databases, enz. + +### Dependency Injection + +FastAPI bevat een uiterst eenvoudig, maar uiterst krachtig Dependency Injection systeem. + +* Zelfs dependencies kunnen dependencies hebben, waardoor een hiërarchie of **“graph” van dependencies** ontstaat. +* Allemaal **automatisch afgehandeld** door het framework. +* Alle dependencies kunnen data nodig hebben van request, de vereiste **padoperaties veranderen** en automatische documentatie verstrekken. +* **Automatische validatie** zelfs voor *padoperatie* parameters gedefinieerd in dependencies. +* Ondersteuning voor complexe gebruikersauthenticatiesystemen, **databaseverbindingen**, enz. +* **Geen compromisen** met databases, gebruikersinterfaces, enz. Maar eenvoudige integratie met ze allemaal. + +### Ongelimiteerde "plug-ins" + +Of anders gezegd, je hebt ze niet nodig, importeer en gebruik de code die je nodig hebt. + +Elke integratie is ontworpen om eenvoudig te gebruiken (met afhankelijkheden), zodat je een “plug-in" kunt maken in 2 regels code, met dezelfde structuur en syntax die wordt gebruikt voor je *padbewerkingen*. + +### Getest + +* 100% van de code is getest. +* 100% type geannoteerde codebase. +* Wordt gebruikt in productietoepassingen. + +## Starlette functies + +**FastAPI** is volledig verenigbaar met (en gebaseerd op) Starlette. + +`FastAPI` is eigenlijk een subklasse van `Starlette`. Dus als je Starlette al kent of gebruikt, zal de meeste functionaliteit op dezelfde manier werken. + +Met **FastAPI** krijg je alle functies van **Starlette** (FastAPI is gewoon Starlette op steroïden): + +* Zeer indrukwekkende prestaties. Het is een van de snelste Python frameworks, vergelijkbaar met **NodeJS** en **Go**. +* **WebSocket** ondersteuning. +* Taken in de achtergrond tijdens het proces. +* Opstart- en afsluit events. +* Test client gebouwd op HTTPX. +* **CORS**, GZip, Statische bestanden, Streaming reacties. +* **Sessie en Cookie** ondersteuning. +* 100% van de code is getest. +* 100% type geannoteerde codebase. + +## Pydantic functionaliteit + +**FastAPI** is volledig verenigbaar met (en gebaseerd op) Pydantic. Dus alle extra Pydantic code die je nog hebt werkt ook. + +Inclusief externe pakketten die ook gebaseerd zijn op Pydantic, zoals ORMs, ODMs voor databases. + +Dit betekent ook dat je in veel gevallen het object dat je van een request krijgt **direct naar je database** kunt sturen, omdat alles automatisch wordt gevalideerd. + +Hetzelfde geldt ook andersom, in veel gevallen kun je dus het object dat je krijgt van de database **direct doorgeven aan de client**. + +Met **FastAPI** krijg je alle functionaliteit van **Pydantic** (omdat FastAPI is gebaseerd op Pydantic voor alle dataverwerking): + +* **Geen brainfucks**: + * Je hoeft geen nieuwe microtaal voor schemadefinities te leren. + * Als je bekend bent Python types, weet je hoe je Pydantic moet gebruiken. +* Werkt goed samen met je **IDE/linter/hersenen**: + * Doordat pydantic's datastructuren enkel instanties zijn van klassen, die je definieert, werkt automatische aanvulling, linting, mypy en je intuïtie allemaal goed met je gevalideerde data. +* Valideer **complexe structuren**: + * Gebruik van hiërarchische Pydantic modellen, Python `typing`'s `List` en `Dict`, enz. + * Met validators kunnen complexe dataschema's duidelijk en eenvoudig worden gedefinieerd, gecontroleerd en gedocumenteerd als JSON Schema. + * Je kunt diep **geneste JSON** objecten laten valideren en annoteren. +* **Uitbreidbaar**: + * Met Pydantic kunnen op maat gemaakte datatypen worden gedefinieerd of je kunt validatie uitbreiden met methoden op een model dat is ingericht met de decorator validator. +* 100% van de code is getest. From cbdc58b1b75a7e94c78d3dca39f0564bd071d190 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Sep 2024 13:54:00 +0000 Subject: [PATCH 32/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a82965aa82414..df49277529343 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -30,6 +30,7 @@ hide: ### Translations +* 🌐 Add Dutch translation for `docs/nl/docs/features.md`. PR [#12101](https://github.com/fastapi/fastapi/pull/12101) by [@maxscheijen](https://github.com/maxscheijen). * 🌐 Add Portuguese translation for `docs/pt/docs/advanced/testing-events.md`. PR [#12108](https://github.com/fastapi/fastapi/pull/12108) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Portuguese translation for `docs/pt/docs/advanced/security/index.md`. PR [#12114](https://github.com/fastapi/fastapi/pull/12114) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Dutch translation for `docs/nl/docs/index.md`. PR [#12042](https://github.com/fastapi/fastapi/pull/12042) by [@svlandeg](https://github.com/svlandeg). From f42fd9aac2529c1bda6b81fe9cecce0986dadbf3 Mon Sep 17 00:00:00 2001 From: Shubhendra Kushwaha Date: Tue, 3 Sep 2024 21:35:19 +0530 Subject: [PATCH 33/89] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:=20T?= =?UTF-8?q?echniques=20and=20applications=20of=20SQLAlchemy=20global=20fil?= =?UTF-8?q?ters=20in=20FastAPI=20(#12109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/external_links.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index 15f6169eef815..63fd3d0cf8618 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -264,6 +264,14 @@ Articles: author_link: https://devonray.com link: https://devonray.com/blog/deploying-a-fastapi-project-using-aws-lambda-aurora-cdk title: Deployment using Docker, Lambda, Aurora, CDK & GH Actions + - author: Shubhendra Kushwaha + author_link: https://www.linkedin.com/in/theshubhendra/ + link: https://theshubhendra.medium.com/mastering-soft-delete-advanced-sqlalchemy-techniques-4678f4738947 + title: 'Mastering Soft Delete: Advanced SQLAlchemy Techniques' + - author: Shubhendra Kushwaha + author_link: https://www.linkedin.com/in/theshubhendra/ + link: https://theshubhendra.medium.com/role-based-row-filtering-advanced-sqlalchemy-techniques-733e6b1328f6 + title: 'Role based row filtering: Advanced SQLAlchemy Techniques' German: - author: Marcel Sander (actidoo) author_link: https://www.actidoo.com From 9b2a9333b3b9820082deb35605c0619bd578baee Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Sep 2024 16:05:42 +0000 Subject: [PATCH 34/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index df49277529343..70dbce5397d43 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Docs +* 📝 Add External Link: Techniques and applications of SQLAlchemy global filters in FastAPI. PR [#12109](https://github.com/fastapi/fastapi/pull/12109) by [@TheShubhendra](https://github.com/TheShubhendra). * 📝 Add note about `time.perf_counter()` in middlewares. PR [#12095](https://github.com/fastapi/fastapi/pull/12095) by [@tiangolo](https://github.com/tiangolo). * 📝 Tweak middleware code sample `time.time()` to `time.perf_counter()`. PR [#11957](https://github.com/fastapi/fastapi/pull/11957) by [@domdent](https://github.com/domdent). * 🔧 Update sponsors: Coherence. PR [#12093](https://github.com/fastapi/fastapi/pull/12093) by [@tiangolo](https://github.com/tiangolo). From 1f64a1bb551829b57f0b8403ce4aab641a6ee11d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:07:32 +0200 Subject: [PATCH 35/89] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commit?= =?UTF-8?q?=20autoupdate=20(#12115)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⬆ [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.2 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.2...v0.6.3) * bump ruff as well --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem Co-authored-by: svlandeg --- .pre-commit-config.yaml | 2 +- requirements-tests.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3175140622488..7e58afd4b9362 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.2 + rev: v0.6.3 hooks: - id: ruff args: diff --git a/requirements-tests.txt b/requirements-tests.txt index 08561d23a9ac0..de5fdb8a2a5f8 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.8.0 -ruff ==0.6.1 +ruff ==0.6.3 dirty-equals ==0.6.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy # probably when including SQLModel From 68e5ef6968f8a2799d9db927b5db5b8e86d8b5f0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Sep 2024 09:07:55 +0000 Subject: [PATCH 36/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 70dbce5397d43..c54971b73941c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -39,6 +39,7 @@ hide: ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12115](https://github.com/fastapi/fastapi/pull/12115) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump pypa/gh-action-pypi-publish from 1.10.0 to 1.10.1. PR [#12120](https://github.com/fastapi/fastapi/pull/12120) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 10.3.0 to 10.4.0. PR [#12105](https://github.com/fastapi/fastapi/pull/12105) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#12118](https://github.com/fastapi/fastapi/pull/12118) by [@svlandeg](https://github.com/svlandeg). From 7213d421f5bf0a4b9b8815e69a141550b4fc3f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 11:13:32 +0200 Subject: [PATCH 37/89] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.112.?= =?UTF-8?q?3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ fastapi/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c54971b73941c..cdd6cdc904e73 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +## 0.112.3 + +This release is mainly internal refactors, it shouldn't affect apps using FastAPI in any way. You don't even have to upgrade to this version yet. There are a few bigger releases coming right after. 🚀 + ### Refactors * ♻️ Refactor internal `check_file_field()`, rename to `ensure_multipart_is_installed()` to clarify its purpose. PR [#12106](https://github.com/fastapi/fastapi/pull/12106) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index ac2508d89d919..1bc1bfd829dd7 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.112.2" +__version__ = "0.112.3" from starlette import status as status From aa21814a89853c17c139054a5c51f0bb1ea68a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 13:24:36 +0200 Subject: [PATCH 38/89] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20deciding?= =?UTF-8?q?=20if=20`embed`=20body=20fields,=20do=20not=20overwrite=20field?= =?UTF-8?q?s,=20compute=20once=20per=20router,=20refactor=20internals=20in?= =?UTF-8?q?=20preparation=20for=20Pydantic=20models=20in=20`Form`,=20`Quer?= =?UTF-8?q?y`=20and=20others=20(#12117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat.py | 15 ++ fastapi/dependencies/utils.py | 300 ++++++++++++++++++------------- fastapi/param_functions.py | 4 +- fastapi/params.py | 3 +- fastapi/routing.py | 26 ++- tests/test_compat.py | 13 +- tests/test_forms_single_param.py | 99 ++++++++++ 7 files changed, 324 insertions(+), 136 deletions(-) create mode 100644 tests/test_forms_single_param.py diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 06b847b4f3818..f940d65973223 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -279,6 +279,12 @@ def create_body_model( BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] return BodyModel + def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return [ + ModelField(field_info=field_info, name=name) + for name, field_info in model.model_fields.items() + ] + else: from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX from pydantic import AnyUrl as Url # noqa: F401 @@ -513,6 +519,9 @@ def create_body_model( BodyModel.__fields__[f.name] = f # type: ignore[index] return BodyModel + def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return list(model.__fields__.values()) # type: ignore[attr-defined] + def _regenerate_error_with_loc( *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...] @@ -532,6 +541,12 @@ def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if field_annotation_is_sequence(arg): + return True + return False return _annotation_is_sequence(annotation) or _annotation_is_sequence( get_origin(annotation) ) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 0dcba62f130a9..7ac18d941cd85 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -59,7 +59,13 @@ from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool -from starlette.datastructures import FormData, Headers, QueryParams, UploadFile +from starlette.datastructures import ( + FormData, + Headers, + ImmutableMultiDict, + QueryParams, + UploadFile, +) from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket @@ -282,7 +288,7 @@ def get_dependant( ), f"Cannot specify multiple FastAPI annotations for {param_name!r}" continue assert param_details.field is not None - if is_body_param(param_field=param_details.field, is_path_param=is_path_param): + if isinstance(param_details.field.field_info, params.Body): dependant.body_params.append(param_details.field) else: add_param_to_fields(field=param_details.field, dependant=dependant) @@ -466,29 +472,16 @@ def analyze_param( required=field_info.default in (Required, Undefined), field_info=field_info, ) + if is_path_param: + assert is_scalar_field( + field=field + ), "Path params must be of one of the supported types" + elif isinstance(field_info, params.Query): + assert is_scalar_field(field) or is_scalar_sequence_field(field) return ParamDetails(type_annotation=type_annotation, depends=depends, field=field) -def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool: - if is_path_param: - assert is_scalar_field( - field=param_field - ), "Path params must be of one of the supported types" - return False - elif is_scalar_field(field=param_field): - return False - elif isinstance( - param_field.field_info, (params.Query, params.Header) - ) and is_scalar_sequence_field(param_field): - return False - else: - assert isinstance( - param_field.field_info, params.Body - ), f"Param: {param_field.name} can only be a request body, using Body()" - return True - - def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: field_info = field.field_info field_info_in = getattr(field_info, "in_", None) @@ -557,6 +550,7 @@ async def solve_dependencies( dependency_overrides_provider: Optional[Any] = None, dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, async_exit_stack: AsyncExitStack, + embed_body_fields: bool, ) -> SolvedDependency: values: Dict[str, Any] = {} errors: List[Any] = [] @@ -598,6 +592,7 @@ async def solve_dependencies( dependency_overrides_provider=dependency_overrides_provider, dependency_cache=dependency_cache, async_exit_stack=async_exit_stack, + embed_body_fields=embed_body_fields, ) background_tasks = solved_result.background_tasks dependency_cache.update(solved_result.dependency_cache) @@ -640,7 +635,9 @@ async def solve_dependencies( body_values, body_errors, ) = await request_body_to_args( # body_params checked above - required_params=dependant.body_params, received_body=body + body_fields=dependant.body_params, + received_body=body, + embed_body_fields=embed_body_fields, ) values.update(body_values) errors.extend(body_errors) @@ -669,138 +666,185 @@ async def solve_dependencies( ) +def _validate_value_with_model_field( + *, field: ModelField, value: Any, values: Dict[str, Any], loc: Tuple[str, ...] +) -> Tuple[Any, List[Any]]: + if value is None: + if field.required: + return None, [get_missing_field_error(loc=loc)] + else: + return deepcopy(field.default), [] + v_, errors_ = field.validate(value, values, loc=loc) + if isinstance(errors_, ErrorWrapper): + return None, [errors_] + elif isinstance(errors_, list): + new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) + return None, new_errors + else: + return v_, [] + + +def _get_multidict_value(field: ModelField, values: Mapping[str, Any]) -> Any: + if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)): + value = values.getlist(field.alias) + else: + value = values.get(field.alias, None) + if ( + value is None + or ( + isinstance(field.field_info, params.Form) + and isinstance(value, str) # For type checks + and value == "" + ) + or (is_sequence_field(field) and len(value) == 0) + ): + if field.required: + return + else: + return deepcopy(field.default) + return value + + def request_params_to_args( - required_params: Sequence[ModelField], + fields: Sequence[ModelField], received_params: Union[Mapping[str, Any], QueryParams, Headers], ) -> Tuple[Dict[str, Any], List[Any]]: - values = {} + values: Dict[str, Any] = {} errors = [] - for field in required_params: - if is_scalar_sequence_field(field) and isinstance( - received_params, (QueryParams, Headers) - ): - value = received_params.getlist(field.alias) or field.default - else: - value = received_params.get(field.alias) + for field in fields: + value = _get_multidict_value(field, received_params) field_info = field.field_info assert isinstance( field_info, params.Param ), "Params must be subclasses of Param" loc = (field_info.in_.value, field.alias) - if value is None: - if field.required: - errors.append(get_missing_field_error(loc=loc)) - else: - values[field.name] = deepcopy(field.default) - continue - v_, errors_ = field.validate(value, values, loc=loc) - if isinstance(errors_, ErrorWrapper): - errors.append(errors_) - elif isinstance(errors_, list): - new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) - errors.extend(new_errors) + v_, errors_ = _validate_value_with_model_field( + field=field, value=value, values=values, loc=loc + ) + if errors_: + errors.extend(errors_) else: values[field.name] = v_ return values, errors +def _should_embed_body_fields(fields: List[ModelField]) -> bool: + if not fields: + return False + # More than one dependency could have the same field, it would show up as multiple + # fields but it's the same one, so count them by name + body_param_names_set = {field.name for field in fields} + # A top level field has to be a single field, not multiple + if len(body_param_names_set) > 1: + return True + first_field = fields[0] + # If it explicitly specifies it is embedded, it has to be embedded + if getattr(first_field.field_info, "embed", None): + return True + # If it's a Form (or File) field, it has to be a BaseModel to be top level + # otherwise it has to be embedded, so that the key value pair can be extracted + if isinstance(first_field.field_info, params.Form): + return True + return False + + +async def _extract_form_body( + body_fields: List[ModelField], + received_body: FormData, +) -> Dict[str, Any]: + values = {} + first_field = body_fields[0] + first_field_info = first_field.field_info + + for field in body_fields: + value = _get_multidict_value(field, received_body) + if ( + isinstance(first_field_info, params.File) + and is_bytes_field(field) + and isinstance(value, UploadFile) + ): + value = await value.read() + elif ( + is_bytes_sequence_field(field) + and isinstance(first_field_info, params.File) + and value_is_sequence(value) + ): + # For types + assert isinstance(value, sequence_types) # type: ignore[arg-type] + results: List[Union[bytes, str]] = [] + + async def process_fn( + fn: Callable[[], Coroutine[Any, Any, Any]], + ) -> None: + result = await fn() + results.append(result) # noqa: B023 + + async with anyio.create_task_group() as tg: + for sub_value in value: + tg.start_soon(process_fn, sub_value.read) + value = serialize_sequence_value(field=field, value=results) + values[field.name] = value + return values + + async def request_body_to_args( - required_params: List[ModelField], + body_fields: List[ModelField], received_body: Optional[Union[Dict[str, Any], FormData]], + embed_body_fields: bool, ) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: - values = {} + values: Dict[str, Any] = {} errors: List[Dict[str, Any]] = [] - if required_params: - field = required_params[0] - field_info = field.field_info - embed = getattr(field_info, "embed", None) - field_alias_omitted = len(required_params) == 1 and not embed - if field_alias_omitted: - received_body = {field.alias: received_body} - - for field in required_params: - loc: Tuple[str, ...] - if field_alias_omitted: - loc = ("body",) - else: - loc = ("body", field.alias) - - value: Optional[Any] = None - if received_body is not None: - if (is_sequence_field(field)) and isinstance(received_body, FormData): - value = received_body.getlist(field.alias) - else: - try: - value = received_body.get(field.alias) - except AttributeError: - errors.append(get_missing_field_error(loc)) - continue - if ( - value is None - or (isinstance(field_info, params.Form) and value == "") - or ( - isinstance(field_info, params.Form) - and is_sequence_field(field) - and len(value) == 0 - ) - ): - if field.required: - errors.append(get_missing_field_error(loc)) - else: - values[field.name] = deepcopy(field.default) + assert body_fields, "request_body_to_args() should be called with fields" + single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields + first_field = body_fields[0] + body_to_process = received_body + if isinstance(received_body, FormData): + body_to_process = await _extract_form_body(body_fields, received_body) + + if single_not_embedded_field: + loc: Tuple[str, ...] = ("body",) + v_, errors_ = _validate_value_with_model_field( + field=first_field, value=body_to_process, values=values, loc=loc + ) + return {first_field.name: v_}, errors_ + for field in body_fields: + loc = ("body", field.alias) + value: Optional[Any] = None + if body_to_process is not None: + try: + value = body_to_process.get(field.alias) + # If the received body is a list, not a dict + except AttributeError: + errors.append(get_missing_field_error(loc)) continue - if ( - isinstance(field_info, params.File) - and is_bytes_field(field) - and isinstance(value, UploadFile) - ): - value = await value.read() - elif ( - is_bytes_sequence_field(field) - and isinstance(field_info, params.File) - and value_is_sequence(value) - ): - # For types - assert isinstance(value, sequence_types) # type: ignore[arg-type] - results: List[Union[bytes, str]] = [] - - async def process_fn( - fn: Callable[[], Coroutine[Any, Any, Any]], - ) -> None: - result = await fn() - results.append(result) # noqa: B023 - - async with anyio.create_task_group() as tg: - for sub_value in value: - tg.start_soon(process_fn, sub_value.read) - value = serialize_sequence_value(field=field, value=results) - - v_, errors_ = field.validate(value, values, loc=loc) - - if isinstance(errors_, list): - errors.extend(errors_) - elif errors_: - errors.append(errors_) - else: - values[field.name] = v_ + v_, errors_ = _validate_value_with_model_field( + field=field, value=value, values=values, loc=loc + ) + if errors_: + errors.extend(errors_) + else: + values[field.name] = v_ return values, errors -def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: - flat_dependant = get_flat_dependant(dependant) +def get_body_field( + *, flat_dependant: Dependant, name: str, embed_body_fields: bool +) -> Optional[ModelField]: + """ + Get a ModelField representing the request body for a path operation, combining + all body parameters into a single field if necessary. + + Used to check if it's form data (with `isinstance(body_field, params.Form)`) + or JSON and to generate the JSON Schema for a request body. + + This is **not** used to validate/parse the request body, that's done with each + individual body parameter. + """ if not flat_dependant.body_params: return None first_param = flat_dependant.body_params[0] - field_info = first_param.field_info - embed = getattr(field_info, "embed", None) - body_param_names_set = {param.name for param in flat_dependant.body_params} - if len(body_param_names_set) == 1 and not embed: + if not embed_body_fields: return first_param - # If one field requires to embed, all have to be embedded - # in case a sub-dependency is evaluated with a single unique body field - # That is combined (embedded) with other body fields - for param in flat_dependant.body_params: - setattr(param.field_info, "embed", True) # noqa: B010 model_name = "Body_" + name BodyModel = create_body_model( fields=flat_dependant.body_params, model_name=model_name diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 0d5f27af48a74..7ddaace25a936 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1282,7 +1282,7 @@ def Body( # noqa: N802 ), ] = _Unset, embed: Annotated[ - bool, + Union[bool, None], Doc( """ When `embed` is `True`, the parameter will be expected in a JSON body as a @@ -1294,7 +1294,7 @@ def Body( # noqa: N802 [FastAPI docs for Body - Multiple Parameters](https://fastapi.tiangolo.com/tutorial/body-multiple-params/#embed-a-single-body-parameter). """ ), - ] = False, + ] = None, media_type: Annotated[ str, Doc( diff --git a/fastapi/params.py b/fastapi/params.py index cc2a5c13c0261..3dfa5a1a381e5 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -479,7 +479,7 @@ def __init__( *, default_factory: Union[Callable[[], Any], None] = _Unset, annotation: Optional[Any] = None, - embed: bool = False, + embed: Union[bool, None] = None, media_type: str = "application/json", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, @@ -642,7 +642,6 @@ def __init__( default=default, default_factory=default_factory, annotation=annotation, - embed=True, media_type=media_type, alias=alias, alias_priority=alias_priority, diff --git a/fastapi/routing.py b/fastapi/routing.py index 61a112fc47ea6..86e30360216cb 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -33,8 +33,10 @@ from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.dependencies.models import Dependant from fastapi.dependencies.utils import ( + _should_embed_body_fields, get_body_field, get_dependant, + get_flat_dependant, get_parameterless_sub_dependant, get_typed_return_annotation, solve_dependencies, @@ -225,6 +227,7 @@ def get_request_handler( response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, dependency_overrides_provider: Optional[Any] = None, + embed_body_fields: bool = False, ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" is_coroutine = asyncio.iscoroutinefunction(dependant.call) @@ -291,6 +294,7 @@ async def app(request: Request) -> Response: body=body, dependency_overrides_provider=dependency_overrides_provider, async_exit_stack=async_exit_stack, + embed_body_fields=embed_body_fields, ) errors = solved_result.errors if not errors: @@ -354,7 +358,9 @@ async def app(request: Request) -> Response: def get_websocket_app( - dependant: Dependant, dependency_overrides_provider: Optional[Any] = None + dependant: Dependant, + dependency_overrides_provider: Optional[Any] = None, + embed_body_fields: bool = False, ) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]: async def app(websocket: WebSocket) -> None: async with AsyncExitStack() as async_exit_stack: @@ -367,6 +373,7 @@ async def app(websocket: WebSocket) -> None: dependant=dependant, dependency_overrides_provider=dependency_overrides_provider, async_exit_stack=async_exit_stack, + embed_body_fields=embed_body_fields, ) if solved_result.errors: raise WebSocketRequestValidationError( @@ -399,11 +406,15 @@ def __init__( 0, get_parameterless_sub_dependant(depends=depends, path=self.path_format), ) - + self._flat_dependant = get_flat_dependant(self.dependant) + self._embed_body_fields = _should_embed_body_fields( + self._flat_dependant.body_params + ) self.app = websocket_session( get_websocket_app( dependant=self.dependant, dependency_overrides_provider=dependency_overrides_provider, + embed_body_fields=self._embed_body_fields, ) ) @@ -544,7 +555,15 @@ def __init__( 0, get_parameterless_sub_dependant(depends=depends, path=self.path_format), ) - self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id) + self._flat_dependant = get_flat_dependant(self.dependant) + self._embed_body_fields = _should_embed_body_fields( + self._flat_dependant.body_params + ) + self.body_field = get_body_field( + flat_dependant=self._flat_dependant, + name=self.unique_id, + embed_body_fields=self._embed_body_fields, + ) self.app = request_response(self.get_route_handler()) def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]: @@ -561,6 +580,7 @@ def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response] response_model_exclude_defaults=self.response_model_exclude_defaults, response_model_exclude_none=self.response_model_exclude_none, dependency_overrides_provider=self.dependency_overrides_provider, + embed_body_fields=self._embed_body_fields, ) def matches(self, scope: Scope) -> Tuple[Match, Scope]: diff --git a/tests/test_compat.py b/tests/test_compat.py index bf268b860b1dc..270475bf3a42a 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,11 +1,13 @@ -from typing import List, Union +from typing import Any, Dict, List, Union from fastapi import FastAPI, UploadFile from fastapi._compat import ( ModelField, Undefined, _get_model_config, + get_model_fields, is_bytes_sequence_annotation, + is_scalar_field, is_uploadfile_sequence_annotation, ) from fastapi.testclient import TestClient @@ -91,3 +93,12 @@ def test_is_uploadfile_sequence_annotation(): # and other types, but I'm not even sure it's a good idea to support it as a first # class "feature" assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) + + +def test_is_pv1_scalar_field(): + # For coverage + class Model(BaseModel): + foo: Union[str, Dict[str, Any]] + + fields = get_model_fields(Model) + assert not is_scalar_field(fields[0]) diff --git a/tests/test_forms_single_param.py b/tests/test_forms_single_param.py new file mode 100644 index 0000000000000..3bb951441f52c --- /dev/null +++ b/tests/test_forms_single_param.py @@ -0,0 +1,99 @@ +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/form/") +def post_form(username: Annotated[str, Form()]): + return username + + +client = TestClient(app) + + +def test_single_form_field(): + response = client.post("/form/", data={"username": "Rick"}) + assert response.status_code == 200, response.text + assert response.json() == "Rick" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/form/": { + "post": { + "summary": "Post Form", + "operationId": "post_form_form__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_post_form_form__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "Body_post_form_form__post": { + "properties": {"username": {"type": "string", "title": "Username"}}, + "type": "object", + "required": ["username"], + "title": "Body_post_form_form__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } From 832e634fd4b8c4e1a5714b5ad73f2cdc04e05e43 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Sep 2024 11:25:02 +0000 Subject: [PATCH 39/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index cdd6cdc904e73..2fe884615f3d4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ♻️ Refactor deciding if `embed` body fields, do not overwrite fields, compute once per router, refactor internals in preparation for Pydantic models in `Form`, `Query` and others. PR [#12117](https://github.com/fastapi/fastapi/pull/12117) by [@tiangolo](https://github.com/tiangolo). + ## 0.112.3 This release is mainly internal refactors, it shouldn't affect apps using FastAPI in any way. You don't even have to upgrade to this version yet. There are a few bigger releases coming right after. 🚀 From 0f3e65b00712a59d763cf9c7715cde353bb94b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 16:40:48 +0200 Subject: [PATCH 40/89] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Pydanti?= =?UTF-8?q?c=20models=20in=20`Form`=20parameters=20(#12127)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/request-form-models/image01.png | Bin 0 -> 44487 bytes docs/en/docs/tutorial/request-form-models.md | 65 +++++ docs/en/mkdocs.yml | 1 + docs_src/request_form_models/tutorial001.py | 14 + .../request_form_models/tutorial001_an.py | 15 ++ .../tutorial001_an_py39.py | 16 ++ fastapi/dependencies/utils.py | 17 +- .../playwright/request_form_models/image01.py | 36 +++ tests/test_forms_single_model.py | 129 ++++++++++ .../test_request_form_models/__init__.py | 0 .../test_tutorial001.py | 232 +++++++++++++++++ .../test_tutorial001_an.py | 232 +++++++++++++++++ .../test_tutorial001_an_py39.py | 240 ++++++++++++++++++ 13 files changed, 994 insertions(+), 3 deletions(-) create mode 100644 docs/en/docs/img/tutorial/request-form-models/image01.png create mode 100644 docs/en/docs/tutorial/request-form-models.md create mode 100644 docs_src/request_form_models/tutorial001.py create mode 100644 docs_src/request_form_models/tutorial001_an.py create mode 100644 docs_src/request_form_models/tutorial001_an_py39.py create mode 100644 scripts/playwright/request_form_models/image01.py create mode 100644 tests/test_forms_single_model.py create mode 100644 tests/test_tutorial/test_request_form_models/__init__.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py diff --git a/docs/en/docs/img/tutorial/request-form-models/image01.png b/docs/en/docs/img/tutorial/request-form-models/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..3fe32c03d589e76abec5e9c6b71374ebd4e8cd2c GIT binary patch literal 44487 zcmeFZcT|(j_b-b2Dz8{DDj*>68l^Ys(k&DL0RgF@_uhL8ib&{6RjSe3GerJ&n@emd;U6qoOQFZ);yV+XJ+p`lX>?3?7g3`_bT#tDCj82$jI&} zyp>TWBfCz#ygKyvRbugyN%0D?xZ(r=sS4^yATZ0)ghX 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 CMaG{1 literal 0 HcmV?d00001 diff --git a/docs/en/docs/tutorial/request-form-models.md b/docs/en/docs/tutorial/request-form-models.md new file mode 100644 index 0000000000000..8bb1ffb1fc2cb --- /dev/null +++ b/docs/en/docs/tutorial/request-form-models.md @@ -0,0 +1,65 @@ +# Form Models + +You can use Pydantic models to declare form fields in FastAPI. + +/// info + +To use forms, first install `python-multipart`. + +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: + +```console +$ pip install python-multipart +``` + +/// + +/// note + +This is supported since FastAPI version `0.113.0`. 🤓 + +/// + +## Pydantic Models for Forms + +You just need to declare a Pydantic model with the fields you want to receive as form fields, and then declare the parameter as `Form`: + +//// tab | Python 3.9+ + +```Python hl_lines="9-11 15" +{!> ../../../docs_src/request_form_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="8-10 14" +{!> ../../../docs_src/request_form_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-9 13" +{!> ../../../docs_src/request_form_models/tutorial001.py!} +``` + +//// + +FastAPI will extract the data for each field from the form data in the request and give you the Pydantic model you defined. + +## Check the Docs + +You can verify it in the docs UI at `/docs`: + +
+ +
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 528c80b8e6b31..7c810c2d7c212 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -129,6 +129,7 @@ nav: - tutorial/extra-models.md - tutorial/response-status-code.md - tutorial/request-forms.md + - tutorial/request-form-models.md - tutorial/request-files.md - tutorial/request-forms-and-files.md - tutorial/handling-errors.md diff --git a/docs_src/request_form_models/tutorial001.py b/docs_src/request_form_models/tutorial001.py new file mode 100644 index 0000000000000..98feff0b9f4e8 --- /dev/null +++ b/docs_src/request_form_models/tutorial001.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: FormData = Form()): + return data diff --git a/docs_src/request_form_models/tutorial001_an.py b/docs_src/request_form_models/tutorial001_an.py new file mode 100644 index 0000000000000..30483d44555f8 --- /dev/null +++ b/docs_src/request_form_models/tutorial001_an.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/docs_src/request_form_models/tutorial001_an_py39.py b/docs_src/request_form_models/tutorial001_an_py39.py new file mode 100644 index 0000000000000..7cc81aae9586d --- /dev/null +++ b/docs_src/request_form_models/tutorial001_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 7ac18d941cd85..98ce17b55d9ba 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -33,6 +33,7 @@ field_annotation_is_scalar, get_annotation_from_field_info, get_missing_field_error, + get_model_fields, is_bytes_field, is_bytes_sequence_field, is_scalar_field, @@ -56,6 +57,7 @@ from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.utils import create_model_field, get_path_param_names +from pydantic import BaseModel from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool @@ -743,7 +745,9 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool: return True # If it's a Form (or File) field, it has to be a BaseModel to be top level # otherwise it has to be embedded, so that the key value pair can be extracted - if isinstance(first_field.field_info, params.Form): + if isinstance(first_field.field_info, params.Form) and not lenient_issubclass( + first_field.type_, BaseModel + ): return True return False @@ -783,7 +787,8 @@ async def process_fn( for sub_value in value: tg.start_soon(process_fn, sub_value.read) value = serialize_sequence_value(field=field, value=results) - values[field.name] = value + if value is not None: + values[field.name] = value return values @@ -798,8 +803,14 @@ async def request_body_to_args( single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields first_field = body_fields[0] body_to_process = received_body + + fields_to_extract: List[ModelField] = body_fields + + if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel): + fields_to_extract = get_model_fields(first_field.type_) + if isinstance(received_body, FormData): - body_to_process = await _extract_form_body(body_fields, received_body) + body_to_process = await _extract_form_body(fields_to_extract, received_body) if single_not_embedded_field: loc: Tuple[str, ...] = ("body",) diff --git a/scripts/playwright/request_form_models/image01.py b/scripts/playwright/request_form_models/image01.py new file mode 100644 index 0000000000000..15bd3858c52c8 --- /dev/null +++ b/scripts/playwright/request_form_models/image01.py @@ -0,0 +1,36 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context() + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="POST /login/ Login").click() + page.get_by_role("button", name="Try it out").click() + page.screenshot(path="docs/en/docs/img/tutorial/request-form-models/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/request_form_models/tutorial001.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py new file mode 100644 index 0000000000000..7ed3ba3a2df55 --- /dev/null +++ b/tests/test_forms_single_model.py @@ -0,0 +1,129 @@ +from typing import List, Optional + +from dirty_equals import IsDict +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class FormModel(BaseModel): + username: str + lastname: str + age: Optional[int] = None + tags: List[str] = ["foo", "bar"] + + +@app.post("/form/") +def post_form(user: Annotated[FormModel, Form()]): + return user + + +client = TestClient(app) + + +def test_send_all_data(): + response = client.post( + "/form/", + data={ + "username": "Rick", + "lastname": "Sanchez", + "age": "70", + "tags": ["plumbus", "citadel"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "Rick", + "lastname": "Sanchez", + "age": 70, + "tags": ["plumbus", "citadel"], + } + + +def test_defaults(): + response = client.post("/form/", data={"username": "Rick", "lastname": "Sanchez"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "Rick", + "lastname": "Sanchez", + "age": None, + "tags": ["foo", "bar"], + } + + +def test_invalid_data(): + response = client.post( + "/form/", + data={ + "username": "Rick", + "lastname": "Sanchez", + "age": "seventy", + "tags": ["plumbus", "citadel"], + }, + ) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "age"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "seventy", + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "age"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_no_data(): + response = client.post("/form/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"]}, + }, + { + "type": "missing", + "loc": ["body", "lastname"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"]}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "lastname"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) diff --git a/tests/test_tutorial/test_request_form_models/__init__.py b/tests/test_tutorial/test_request_form_models/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py new file mode 100644 index 0000000000000..46c130ee8c167 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -0,0 +1,232 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001 import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py new file mode 100644 index 0000000000000..4e14d89c84f26 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py @@ -0,0 +1,232 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001_an import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py new file mode 100644 index 0000000000000..2e6426aa78165 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py @@ -0,0 +1,240 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from tests.utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_py39 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } From ccb19c4c3506d7cb05218076d0e3527cb21eed81 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Sep 2024 14:41:11 +0000 Subject: [PATCH 41/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2fe884615f3d4..8fe8be6a7f56e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Add support for Pydantic models in `Form` parameters. PR [#12127](https://github.com/fastapi/fastapi/pull/12127) by [@tiangolo](https://github.com/tiangolo). + ### Refactors * ♻️ Refactor deciding if `embed` body fields, do not overwrite fields, compute once per router, refactor internals in preparation for Pydantic models in `Form`, `Query` and others. PR [#12117](https://github.com/fastapi/fastapi/pull/12117) by [@tiangolo](https://github.com/tiangolo). From 8e6cf9ee9c9d87b6b658cc240146121c80f71476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 16:55:44 +0200 Subject: [PATCH 42/89] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20Temporarily=20revert?= =?UTF-8?q?=20"=E2=9C=A8=20Add=20support=20for=20Pydantic=20models=20in=20?= =?UTF-8?q?`Form`=20parameters"=20to=20make=20a=20checkpoint=20release=20(?= =?UTF-8?q?#12128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "✨ Add support for Pydantic models in `Form` parameters (#12127)" This reverts commit 0f3e65b00712a59d763cf9c7715cde353bb94b02. --- .../tutorial/request-form-models/image01.png | Bin 44487 -> 0 bytes docs/en/docs/tutorial/request-form-models.md | 65 ----- docs/en/mkdocs.yml | 1 - docs_src/request_form_models/tutorial001.py | 14 - .../request_form_models/tutorial001_an.py | 15 -- .../tutorial001_an_py39.py | 16 -- fastapi/dependencies/utils.py | 17 +- .../playwright/request_form_models/image01.py | 36 --- tests/test_forms_single_model.py | 129 ---------- .../test_request_form_models/__init__.py | 0 .../test_tutorial001.py | 232 ----------------- .../test_tutorial001_an.py | 232 ----------------- .../test_tutorial001_an_py39.py | 240 ------------------ 13 files changed, 3 insertions(+), 994 deletions(-) delete mode 100644 docs/en/docs/img/tutorial/request-form-models/image01.png delete mode 100644 docs/en/docs/tutorial/request-form-models.md delete mode 100644 docs_src/request_form_models/tutorial001.py delete mode 100644 docs_src/request_form_models/tutorial001_an.py delete mode 100644 docs_src/request_form_models/tutorial001_an_py39.py delete mode 100644 scripts/playwright/request_form_models/image01.py delete mode 100644 tests/test_forms_single_model.py delete mode 100644 tests/test_tutorial/test_request_form_models/__init__.py delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001.py delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001_an.py delete mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py diff --git a/docs/en/docs/img/tutorial/request-form-models/image01.png b/docs/en/docs/img/tutorial/request-form-models/image01.png deleted file mode 100644 index 3fe32c03d589e76abec5e9c6b71374ebd4e8cd2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44487 zcmeFZcT|(j_b-b2Dz8{DDj*>68l^Ys(k&DL0RgF@_uhL8ib&{6RjSe3GerJ&n@emd;U6qoOQFZ);yV+XJ+p`lX>?3?7g3`_bT#tDCj82$jI&} zyp>TWBfCz#ygKyvRbugyN%0D?xZ(r=sS4^yATZ0)ghX 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 CMaG{1 diff --git a/docs/en/docs/tutorial/request-form-models.md b/docs/en/docs/tutorial/request-form-models.md deleted file mode 100644 index 8bb1ffb1fc2cb..0000000000000 --- a/docs/en/docs/tutorial/request-form-models.md +++ /dev/null @@ -1,65 +0,0 @@ -# Form Models - -You can use Pydantic models to declare form fields in FastAPI. - -/// info - -To use forms, first install `python-multipart`. - -Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: - -```console -$ pip install python-multipart -``` - -/// - -/// note - -This is supported since FastAPI version `0.113.0`. 🤓 - -/// - -## Pydantic Models for Forms - -You just need to declare a Pydantic model with the fields you want to receive as form fields, and then declare the parameter as `Form`: - -//// tab | Python 3.9+ - -```Python hl_lines="9-11 15" -{!> ../../../docs_src/request_form_models/tutorial001_an_py39.py!} -``` - -//// - -//// tab | Python 3.8+ - -```Python hl_lines="8-10 14" -{!> ../../../docs_src/request_form_models/tutorial001_an.py!} -``` - -//// - -//// tab | Python 3.8+ non-Annotated - -/// tip - -Prefer to use the `Annotated` version if possible. - -/// - -```Python hl_lines="7-9 13" -{!> ../../../docs_src/request_form_models/tutorial001.py!} -``` - -//// - -FastAPI will extract the data for each field from the form data in the request and give you the Pydantic model you defined. - -## Check the Docs - -You can verify it in the docs UI at `/docs`: - -
- -
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 7c810c2d7c212..528c80b8e6b31 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -129,7 +129,6 @@ nav: - tutorial/extra-models.md - tutorial/response-status-code.md - tutorial/request-forms.md - - tutorial/request-form-models.md - tutorial/request-files.md - tutorial/request-forms-and-files.md - tutorial/handling-errors.md diff --git a/docs_src/request_form_models/tutorial001.py b/docs_src/request_form_models/tutorial001.py deleted file mode 100644 index 98feff0b9f4e8..0000000000000 --- a/docs_src/request_form_models/tutorial001.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI, Form -from pydantic import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - -@app.post("/login/") -async def login(data: FormData = Form()): - return data diff --git a/docs_src/request_form_models/tutorial001_an.py b/docs_src/request_form_models/tutorial001_an.py deleted file mode 100644 index 30483d44555f8..0000000000000 --- a/docs_src/request_form_models/tutorial001_an.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI, Form -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/docs_src/request_form_models/tutorial001_an_py39.py b/docs_src/request_form_models/tutorial001_an_py39.py deleted file mode 100644 index 7cc81aae9586d..0000000000000 --- a/docs_src/request_form_models/tutorial001_an_py39.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Form -from pydantic import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 98ce17b55d9ba..7ac18d941cd85 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -33,7 +33,6 @@ field_annotation_is_scalar, get_annotation_from_field_info, get_missing_field_error, - get_model_fields, is_bytes_field, is_bytes_sequence_field, is_scalar_field, @@ -57,7 +56,6 @@ from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.utils import create_model_field, get_path_param_names -from pydantic import BaseModel from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool @@ -745,9 +743,7 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool: return True # If it's a Form (or File) field, it has to be a BaseModel to be top level # otherwise it has to be embedded, so that the key value pair can be extracted - if isinstance(first_field.field_info, params.Form) and not lenient_issubclass( - first_field.type_, BaseModel - ): + if isinstance(first_field.field_info, params.Form): return True return False @@ -787,8 +783,7 @@ async def process_fn( for sub_value in value: tg.start_soon(process_fn, sub_value.read) value = serialize_sequence_value(field=field, value=results) - if value is not None: - values[field.name] = value + values[field.name] = value return values @@ -803,14 +798,8 @@ async def request_body_to_args( single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields first_field = body_fields[0] body_to_process = received_body - - fields_to_extract: List[ModelField] = body_fields - - if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel): - fields_to_extract = get_model_fields(first_field.type_) - if isinstance(received_body, FormData): - body_to_process = await _extract_form_body(fields_to_extract, received_body) + body_to_process = await _extract_form_body(body_fields, received_body) if single_not_embedded_field: loc: Tuple[str, ...] = ("body",) diff --git a/scripts/playwright/request_form_models/image01.py b/scripts/playwright/request_form_models/image01.py deleted file mode 100644 index 15bd3858c52c8..0000000000000 --- a/scripts/playwright/request_form_models/image01.py +++ /dev/null @@ -1,36 +0,0 @@ -import subprocess -import time - -import httpx -from playwright.sync_api import Playwright, sync_playwright - - -# Run playwright codegen to generate the code below, copy paste the sections in run() -def run(playwright: Playwright) -> None: - browser = playwright.chromium.launch(headless=False) - context = browser.new_context() - page = context.new_page() - page.goto("http://localhost:8000/docs") - page.get_by_role("button", name="POST /login/ Login").click() - page.get_by_role("button", name="Try it out").click() - page.screenshot(path="docs/en/docs/img/tutorial/request-form-models/image01.png") - - # --------------------- - context.close() - browser.close() - - -process = subprocess.Popen( - ["fastapi", "run", "docs_src/request_form_models/tutorial001.py"] -) -try: - for _ in range(3): - try: - response = httpx.get("http://localhost:8000/docs") - except httpx.ConnectError: - time.sleep(1) - break - with sync_playwright() as playwright: - run(playwright) -finally: - process.terminate() diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py deleted file mode 100644 index 7ed3ba3a2df55..0000000000000 --- a/tests/test_forms_single_model.py +++ /dev/null @@ -1,129 +0,0 @@ -from typing import List, Optional - -from dirty_equals import IsDict -from fastapi import FastAPI, Form -from fastapi.testclient import TestClient -from pydantic import BaseModel -from typing_extensions import Annotated - -app = FastAPI() - - -class FormModel(BaseModel): - username: str - lastname: str - age: Optional[int] = None - tags: List[str] = ["foo", "bar"] - - -@app.post("/form/") -def post_form(user: Annotated[FormModel, Form()]): - return user - - -client = TestClient(app) - - -def test_send_all_data(): - response = client.post( - "/form/", - data={ - "username": "Rick", - "lastname": "Sanchez", - "age": "70", - "tags": ["plumbus", "citadel"], - }, - ) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "Rick", - "lastname": "Sanchez", - "age": 70, - "tags": ["plumbus", "citadel"], - } - - -def test_defaults(): - response = client.post("/form/", data={"username": "Rick", "lastname": "Sanchez"}) - assert response.status_code == 200, response.text - assert response.json() == { - "username": "Rick", - "lastname": "Sanchez", - "age": None, - "tags": ["foo", "bar"], - } - - -def test_invalid_data(): - response = client.post( - "/form/", - data={ - "username": "Rick", - "lastname": "Sanchez", - "age": "seventy", - "tags": ["plumbus", "citadel"], - }, - ) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "int_parsing", - "loc": ["body", "age"], - "msg": "Input should be a valid integer, unable to parse string as an integer", - "input": "seventy", - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "age"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } - ) - - -def test_no_data(): - response = client.post("/form/") - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"tags": ["foo", "bar"]}, - }, - { - "type": "missing", - "loc": ["body", "lastname"], - "msg": "Field required", - "input": {"tags": ["foo", "bar"]}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "lastname"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) diff --git a/tests/test_tutorial/test_request_form_models/__init__.py b/tests/test_tutorial/test_request_form_models/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py deleted file mode 100644 index 46c130ee8c167..0000000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001.py +++ /dev/null @@ -1,232 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial001 import app - - client = TestClient(app) - return client - - -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py deleted file mode 100644 index 4e14d89c84f26..0000000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py +++ /dev/null @@ -1,232 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial001_an import app - - client = TestClient(app) - return client - - -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py deleted file mode 100644 index 2e6426aa78165..0000000000000 --- a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py +++ /dev/null @@ -1,240 +0,0 @@ -import pytest -from dirty_equals import IsDict -from fastapi.testclient import TestClient - -from tests.utils import needs_py39 - - -@pytest.fixture(name="client") -def get_client(): - from docs_src.request_form_models.tutorial001_an_py39 import app - - client = TestClient(app) - return client - - -@needs_py39 -def test_post_body_form(client: TestClient): - response = client.post("/login/", data={"username": "Foo", "password": "secret"}) - assert response.status_code == 200 - assert response.json() == {"username": "Foo", "password": "secret"} - - -@needs_py39 -def test_post_body_form_no_password(client: TestClient): - response = client.post("/login/", data={"username": "Foo"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {"username": "Foo"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_body_form_no_username(client: TestClient): - response = client.post("/login/", data={"password": "secret"}) - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {"password": "secret"}, - } - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } - ) - - -@needs_py39 -def test_post_body_form_no_data(client: TestClient): - response = client.post("/login/") - assert response.status_code == 422 - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_post_body_json(client: TestClient): - response = client.post("/login/", json={"username": "Foo", "password": "secret"}) - assert response.status_code == 422, response.text - assert response.json() == IsDict( - { - "detail": [ - { - "type": "missing", - "loc": ["body", "username"], - "msg": "Field required", - "input": {}, - }, - { - "type": "missing", - "loc": ["body", "password"], - "msg": "Field required", - "input": {}, - }, - ] - } - ) | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } - ) - - -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": {"$ref": "#/components/schemas/FormData"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "FormData": { - "properties": { - "username": {"type": "string", "title": "Username"}, - "password": {"type": "string", "title": "Password"}, - }, - "type": "object", - "required": ["username", "password"], - "title": "FormData", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, - } From b69e8b24af305ddceaa9c63c8d2eebf80672caed Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Sep 2024 14:56:10 +0000 Subject: [PATCH 43/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 8fe8be6a7f56e..3c5fbb7319aa0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,10 @@ hide: * ♻️ Refactor deciding if `embed` body fields, do not overwrite fields, compute once per router, refactor internals in preparation for Pydantic models in `Form`, `Query` and others. PR [#12117](https://github.com/fastapi/fastapi/pull/12117) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* ⏪️ Temporarily revert "✨ Add support for Pydantic models in `Form` parameters" to make a checkpoint release. PR [#12128](https://github.com/fastapi/fastapi/pull/12128) by [@tiangolo](https://github.com/tiangolo). + ## 0.112.3 This release is mainly internal refactors, it shouldn't affect apps using FastAPI in any way. You don't even have to upgrade to this version yet. There are a few bigger releases coming right after. 🚀 From 8224addd8f31325ad465af994da8421a69f494ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 16:57:57 +0200 Subject: [PATCH 44/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3c5fbb7319aa0..22a224a5a502d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,7 +9,7 @@ hide: ### Features -* ✨ Add support for Pydantic models in `Form` parameters. PR [#12127](https://github.com/fastapi/fastapi/pull/12127) by [@tiangolo](https://github.com/tiangolo). +## 0.112.4 ### Refactors @@ -18,6 +18,7 @@ hide: ### Internal * ⏪️ Temporarily revert "✨ Add support for Pydantic models in `Form` parameters" to make a checkpoint release. PR [#12128](https://github.com/fastapi/fastapi/pull/12128) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add support for Pydantic models in `Form` parameters. PR [#12127](https://github.com/fastapi/fastapi/pull/12127) by [@tiangolo](https://github.com/tiangolo). Reverted to make a checkpoint release with only refactors. ## 0.112.3 From 96c7e7e0f34730e6f6333ced9476bfd62f384cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 17:00:13 +0200 Subject: [PATCH 45/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 22a224a5a502d..43ce86c995521 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,10 +7,12 @@ hide: ## Latest Changes -### Features - ## 0.112.4 +This release is mainly a big internal refactor to enable adding support for Pydantic models for `Form` fields, but that feature comes in the next release. + +This release shouldn't affect apps using FastAPI in any way. You don't even have to upgrade to this version yet. It's just a checkpoint. 🤓 + ### Refactors * ♻️ Refactor deciding if `embed` body fields, do not overwrite fields, compute once per router, refactor internals in preparation for Pydantic models in `Form`, `Query` and others. PR [#12117](https://github.com/fastapi/fastapi/pull/12117) by [@tiangolo](https://github.com/tiangolo). From 999eeb6c76ff37f94612dd140ce8091932f56c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 17:00:33 +0200 Subject: [PATCH 46/89] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.112.?= =?UTF-8?q?4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 1bc1bfd829dd7..1e10bf5576a4e 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.112.3" +__version__ = "0.112.4" from starlette import status as status From 965fc8301e8fa7a7228bee33873387f4852a30df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 17:09:31 +0200 Subject: [PATCH 47/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 43ce86c995521..9b44bc9a8e108 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,8 +19,8 @@ This release shouldn't affect apps using FastAPI in any way. You don't even have ### Internal -* ⏪️ Temporarily revert "✨ Add support for Pydantic models in `Form` parameters" to make a checkpoint release. PR [#12128](https://github.com/fastapi/fastapi/pull/12128) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add support for Pydantic models in `Form` parameters. PR [#12127](https://github.com/fastapi/fastapi/pull/12127) by [@tiangolo](https://github.com/tiangolo). Reverted to make a checkpoint release with only refactors. +* ⏪️ Temporarily revert "✨ Add support for Pydantic models in `Form` parameters" to make a checkpoint release. PR [#12128](https://github.com/fastapi/fastapi/pull/12128) by [@tiangolo](https://github.com/tiangolo). Restored by PR [#12129](https://github.com/fastapi/fastapi/pull/12129). +* ✨ Add support for Pydantic models in `Form` parameters. PR [#12127](https://github.com/fastapi/fastapi/pull/12127) by [@tiangolo](https://github.com/tiangolo). Reverted by PR [#12128](https://github.com/fastapi/fastapi/pull/12128) to make a checkpoint release with only refactors. Restored by PR [#12129](https://github.com/fastapi/fastapi/pull/12129). ## 0.112.3 From 7bad7c09757f8a06cf62cc0838082a766065883e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 17:16:50 +0200 Subject: [PATCH 48/89] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Pydanti?= =?UTF-8?q?c=20models=20in=20`Form`=20parameters=20(#12129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "⏪️ Temporarily revert "✨ Add support for Pydantic models in `Form` pa…" This reverts commit 8e6cf9ee9c9d87b6b658cc240146121c80f71476. --- .../tutorial/request-form-models/image01.png | Bin 0 -> 44487 bytes docs/en/docs/tutorial/request-form-models.md | 65 +++++ docs/en/mkdocs.yml | 1 + docs_src/request_form_models/tutorial001.py | 14 + .../request_form_models/tutorial001_an.py | 15 ++ .../tutorial001_an_py39.py | 16 ++ fastapi/dependencies/utils.py | 17 +- .../playwright/request_form_models/image01.py | 36 +++ tests/test_forms_single_model.py | 129 ++++++++++ .../test_request_form_models/__init__.py | 0 .../test_tutorial001.py | 232 +++++++++++++++++ .../test_tutorial001_an.py | 232 +++++++++++++++++ .../test_tutorial001_an_py39.py | 240 ++++++++++++++++++ 13 files changed, 994 insertions(+), 3 deletions(-) create mode 100644 docs/en/docs/img/tutorial/request-form-models/image01.png create mode 100644 docs/en/docs/tutorial/request-form-models.md create mode 100644 docs_src/request_form_models/tutorial001.py create mode 100644 docs_src/request_form_models/tutorial001_an.py create mode 100644 docs_src/request_form_models/tutorial001_an_py39.py create mode 100644 scripts/playwright/request_form_models/image01.py create mode 100644 tests/test_forms_single_model.py create mode 100644 tests/test_tutorial/test_request_form_models/__init__.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py diff --git a/docs/en/docs/img/tutorial/request-form-models/image01.png b/docs/en/docs/img/tutorial/request-form-models/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..3fe32c03d589e76abec5e9c6b71374ebd4e8cd2c GIT binary patch literal 44487 zcmeFZcT|(j_b-b2Dz8{DDj*>68l^Ys(k&DL0RgF@_uhL8ib&{6RjSe3GerJ&n@emd;U6qoOQFZ);yV+XJ+p`lX>?3?7g3`_bT#tDCj82$jI&} zyp>TWBfCz#ygKyvRbugyN%0D?xZ(r=sS4^yATZ0)ghX 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 CMaG{1 literal 0 HcmV?d00001 diff --git a/docs/en/docs/tutorial/request-form-models.md b/docs/en/docs/tutorial/request-form-models.md new file mode 100644 index 0000000000000..8bb1ffb1fc2cb --- /dev/null +++ b/docs/en/docs/tutorial/request-form-models.md @@ -0,0 +1,65 @@ +# Form Models + +You can use Pydantic models to declare form fields in FastAPI. + +/// info + +To use forms, first install `python-multipart`. + +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: + +```console +$ pip install python-multipart +``` + +/// + +/// note + +This is supported since FastAPI version `0.113.0`. 🤓 + +/// + +## Pydantic Models for Forms + +You just need to declare a Pydantic model with the fields you want to receive as form fields, and then declare the parameter as `Form`: + +//// tab | Python 3.9+ + +```Python hl_lines="9-11 15" +{!> ../../../docs_src/request_form_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="8-10 14" +{!> ../../../docs_src/request_form_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-9 13" +{!> ../../../docs_src/request_form_models/tutorial001.py!} +``` + +//// + +FastAPI will extract the data for each field from the form data in the request and give you the Pydantic model you defined. + +## Check the Docs + +You can verify it in the docs UI at `/docs`: + +
+ +
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 528c80b8e6b31..7c810c2d7c212 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -129,6 +129,7 @@ nav: - tutorial/extra-models.md - tutorial/response-status-code.md - tutorial/request-forms.md + - tutorial/request-form-models.md - tutorial/request-files.md - tutorial/request-forms-and-files.md - tutorial/handling-errors.md diff --git a/docs_src/request_form_models/tutorial001.py b/docs_src/request_form_models/tutorial001.py new file mode 100644 index 0000000000000..98feff0b9f4e8 --- /dev/null +++ b/docs_src/request_form_models/tutorial001.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: FormData = Form()): + return data diff --git a/docs_src/request_form_models/tutorial001_an.py b/docs_src/request_form_models/tutorial001_an.py new file mode 100644 index 0000000000000..30483d44555f8 --- /dev/null +++ b/docs_src/request_form_models/tutorial001_an.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/docs_src/request_form_models/tutorial001_an_py39.py b/docs_src/request_form_models/tutorial001_an_py39.py new file mode 100644 index 0000000000000..7cc81aae9586d --- /dev/null +++ b/docs_src/request_form_models/tutorial001_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 7ac18d941cd85..98ce17b55d9ba 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -33,6 +33,7 @@ field_annotation_is_scalar, get_annotation_from_field_info, get_missing_field_error, + get_model_fields, is_bytes_field, is_bytes_sequence_field, is_scalar_field, @@ -56,6 +57,7 @@ from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.utils import create_model_field, get_path_param_names +from pydantic import BaseModel from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool @@ -743,7 +745,9 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool: return True # If it's a Form (or File) field, it has to be a BaseModel to be top level # otherwise it has to be embedded, so that the key value pair can be extracted - if isinstance(first_field.field_info, params.Form): + if isinstance(first_field.field_info, params.Form) and not lenient_issubclass( + first_field.type_, BaseModel + ): return True return False @@ -783,7 +787,8 @@ async def process_fn( for sub_value in value: tg.start_soon(process_fn, sub_value.read) value = serialize_sequence_value(field=field, value=results) - values[field.name] = value + if value is not None: + values[field.name] = value return values @@ -798,8 +803,14 @@ async def request_body_to_args( single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields first_field = body_fields[0] body_to_process = received_body + + fields_to_extract: List[ModelField] = body_fields + + if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel): + fields_to_extract = get_model_fields(first_field.type_) + if isinstance(received_body, FormData): - body_to_process = await _extract_form_body(body_fields, received_body) + body_to_process = await _extract_form_body(fields_to_extract, received_body) if single_not_embedded_field: loc: Tuple[str, ...] = ("body",) diff --git a/scripts/playwright/request_form_models/image01.py b/scripts/playwright/request_form_models/image01.py new file mode 100644 index 0000000000000..15bd3858c52c8 --- /dev/null +++ b/scripts/playwright/request_form_models/image01.py @@ -0,0 +1,36 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context() + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="POST /login/ Login").click() + page.get_by_role("button", name="Try it out").click() + page.screenshot(path="docs/en/docs/img/tutorial/request-form-models/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/request_form_models/tutorial001.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py new file mode 100644 index 0000000000000..7ed3ba3a2df55 --- /dev/null +++ b/tests/test_forms_single_model.py @@ -0,0 +1,129 @@ +from typing import List, Optional + +from dirty_equals import IsDict +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class FormModel(BaseModel): + username: str + lastname: str + age: Optional[int] = None + tags: List[str] = ["foo", "bar"] + + +@app.post("/form/") +def post_form(user: Annotated[FormModel, Form()]): + return user + + +client = TestClient(app) + + +def test_send_all_data(): + response = client.post( + "/form/", + data={ + "username": "Rick", + "lastname": "Sanchez", + "age": "70", + "tags": ["plumbus", "citadel"], + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "Rick", + "lastname": "Sanchez", + "age": 70, + "tags": ["plumbus", "citadel"], + } + + +def test_defaults(): + response = client.post("/form/", data={"username": "Rick", "lastname": "Sanchez"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "Rick", + "lastname": "Sanchez", + "age": None, + "tags": ["foo", "bar"], + } + + +def test_invalid_data(): + response = client.post( + "/form/", + data={ + "username": "Rick", + "lastname": "Sanchez", + "age": "seventy", + "tags": ["plumbus", "citadel"], + }, + ) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "age"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "seventy", + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "age"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_no_data(): + response = client.post("/form/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"]}, + }, + { + "type": "missing", + "loc": ["body", "lastname"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"]}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "lastname"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) diff --git a/tests/test_tutorial/test_request_form_models/__init__.py b/tests/test_tutorial/test_request_form_models/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py new file mode 100644 index 0000000000000..46c130ee8c167 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -0,0 +1,232 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001 import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py new file mode 100644 index 0000000000000..4e14d89c84f26 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py @@ -0,0 +1,232 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001_an import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py new file mode 100644 index 0000000000000..2e6426aa78165 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py @@ -0,0 +1,240 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from tests.utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_py39 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } From e787f854ddbe12d08ae6b13298b6d5eda7e20928 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Sep 2024 15:17:13 +0000 Subject: [PATCH 49/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9b44bc9a8e108..c6cbc76581b46 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Add support for Pydantic models in `Form` parameters. PR [#12129](https://github.com/fastapi/fastapi/pull/12129) by [@tiangolo](https://github.com/tiangolo). + ## 0.112.4 This release is mainly a big internal refactor to enable adding support for Pydantic models for `Form` fields, but that feature comes in the next release. From afdda4e50ba002c951233f5bedcc64068d59d212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 17:21:35 +0200 Subject: [PATCH 50/89] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20Coher?= =?UTF-8?q?ence=20link=20(#12130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/data/sponsors.yml | 2 +- docs/en/docs/deployment/cloud.md | 2 +- docs/en/overrides/main.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5554f71d48da7..3b01b713a2f4c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ The key features are: - + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 3a767b6b18c4c..d96646fb39c6e 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -17,7 +17,7 @@ gold: - url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge title: Auth, user management and more for your B2B product img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png - - url: https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs + - url: https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website title: Coherence img: https://fastapi.tiangolo.com/img/sponsors/coherence.png - url: https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md index 3ea5087f8c967..41ada859d4fc2 100644 --- a/docs/en/docs/deployment/cloud.md +++ b/docs/en/docs/deployment/cloud.md @@ -14,4 +14,4 @@ You might want to try their services and follow their guides: * Platform.sh * Porter -* Coherence +* Coherence diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 47e46c4bf29be..463c5af3b6fc7 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -59,7 +59,7 @@
- + From 179f838c366b1d9ac74e114949c5c6cfe713ec03 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Sep 2024 15:23:05 +0000 Subject: [PATCH 51/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c6cbc76581b46..acf53e3de9e71 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ✨ Add support for Pydantic models in `Form` parameters. PR [#12129](https://github.com/fastapi/fastapi/pull/12129) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* 🔧 Update sponsors: Coherence link. PR [#12130](https://github.com/fastapi/fastapi/pull/12130) by [@tiangolo](https://github.com/tiangolo). + ## 0.112.4 This release is mainly a big internal refactor to enable adding support for Pydantic models for `Form` fields, but that feature comes in the next release. From d86f6603029def91e0798ca42f5fd12eff13c87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 5 Sep 2024 17:25:29 +0200 Subject: [PATCH 52/89] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.113.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 25 +++++++++++++++++++++++++ fastapi/__init__.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index acf53e3de9e71..0571523bfe399 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,31 @@ hide: ## Latest Changes +## 0.113.0 + +Now you can declare form fields with Pydantic models: + +```python +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data +``` + +Read the new docs: [Form Models](https://fastapi.tiangolo.com/tutorial/request-form-models/). + ### Features * ✨ Add support for Pydantic models in `Form` parameters. PR [#12129](https://github.com/fastapi/fastapi/pull/12129) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 1e10bf5576a4e..f785f81cd6274 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.112.4" +__version__ = "0.113.0" from starlette import status as status From c411b81c29f0e8365e0710baf951b4a42039a2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Sep 2024 17:57:43 +0200 Subject: [PATCH 53/89] =?UTF-8?q?=E2=9C=85=20Update=20internal=20tests=20f?= =?UTF-8?q?or=20latest=20Pydantic,=20including=20CI=20tweaks=20to=20instal?= =?UTF-8?q?l=20the=20latest=20Pydantic=20(#12147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 ++-- tests/test_openapi_examples.py | 27 ++++++++++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e9db49b51fd30..fb4b083c4e0c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt - name: Install Pydantic v2 - run: pip install "pydantic>=2.0.2,<3.0.0" + run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" - name: Lint run: bash scripts/lint.sh @@ -79,7 +79,7 @@ jobs: run: pip install "pydantic>=1.10.0,<2.0.0" - name: Install Pydantic v2 if: matrix.pydantic-version == 'pydantic-v2' - run: pip install "pydantic>=2.0.2,<3.0.0" + run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" - run: mkdir coverage - name: Test run: bash scripts/test.sh diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py index 6597e5058bfed..b3f83ae237ff8 100644 --- a/tests/test_openapi_examples.py +++ b/tests/test_openapi_examples.py @@ -155,13 +155,26 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - {"data": "Data in Body examples, example1"} - ], - }, + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"} + ], + } + ) + | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + {"data": "Data in Body examples, example1"} + ], + } + ), "examples": { "Example One": { "summary": "Example One Summary", From 1b06b532677c91006efe01e4bd09b5d91f5df261 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 6 Sep 2024 15:58:05 +0000 Subject: [PATCH 54/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0571523bfe399..2b9bd3e871e33 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* ✅ Update internal tests for latest Pydantic, including CI tweaks to install the latest Pydantic. PR [#12147](https://github.com/fastapi/fastapi/pull/12147) by [@tiangolo](https://github.com/tiangolo). + ## 0.113.0 Now you can declare form fields with Pydantic models: From 4633b1bca933e68dac5c3bcce797ff5963debe2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Sep 2024 19:31:18 +0200 Subject: [PATCH 55/89] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20forbidd?= =?UTF-8?q?ing=20extra=20form=20fields=20with=20Pydantic=20models=20(#1213?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem --- docs/en/docs/tutorial/request-form-models.md | 75 ++++++- docs_src/request_form_models/tutorial002.py | 15 ++ .../request_form_models/tutorial002_an.py | 16 ++ .../tutorial002_an_py39.py | 17 ++ .../request_form_models/tutorial002_pv1.py | 17 ++ .../request_form_models/tutorial002_pv1_an.py | 18 ++ .../tutorial002_pv1_an_py39.py | 19 ++ fastapi/dependencies/utils.py | 3 + .../test_tutorial002.py | 196 +++++++++++++++++ .../test_tutorial002_an.py | 196 +++++++++++++++++ .../test_tutorial002_an_py39.py | 203 ++++++++++++++++++ .../test_tutorial002_pv1.py | 189 ++++++++++++++++ .../test_tutorial002_pv1_an.py | 196 +++++++++++++++++ .../test_tutorial002_pv1_an_p39.py | 203 ++++++++++++++++++ 14 files changed, 1360 insertions(+), 3 deletions(-) create mode 100644 docs_src/request_form_models/tutorial002.py create mode 100644 docs_src/request_form_models/tutorial002_an.py create mode 100644 docs_src/request_form_models/tutorial002_an_py39.py create mode 100644 docs_src/request_form_models/tutorial002_pv1.py create mode 100644 docs_src/request_form_models/tutorial002_pv1_an.py create mode 100644 docs_src/request_form_models/tutorial002_pv1_an_py39.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py create mode 100644 tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py diff --git a/docs/en/docs/tutorial/request-form-models.md b/docs/en/docs/tutorial/request-form-models.md index 8bb1ffb1fc2cb..a317ee14d097b 100644 --- a/docs/en/docs/tutorial/request-form-models.md +++ b/docs/en/docs/tutorial/request-form-models.md @@ -1,6 +1,6 @@ # Form Models -You can use Pydantic models to declare form fields in FastAPI. +You can use **Pydantic models** to declare **form fields** in FastAPI. /// info @@ -22,7 +22,7 @@ This is supported since FastAPI version `0.113.0`. 🤓 ## Pydantic Models for Forms -You just need to declare a Pydantic model with the fields you want to receive as form fields, and then declare the parameter as `Form`: +You just need to declare a **Pydantic model** with the fields you want to receive as **form fields**, and then declare the parameter as `Form`: //// tab | Python 3.9+ @@ -54,7 +54,7 @@ Prefer to use the `Annotated` version if possible. //// -FastAPI will extract the data for each field from the form data in the request and give you the Pydantic model you defined. +**FastAPI** will **extract** the data for **each field** from the **form data** in the request and give you the Pydantic model you defined. ## Check the Docs @@ -63,3 +63,72 @@ You can verify it in the docs UI at `/docs`:
+ +## Restrict Extra Form Fields + +In some special use cases (probably not very common), you might want to **restrict** the form fields to only those declared in the Pydantic model. And **forbid** any **extra** fields. + +/// note + +This is supported since FastAPI version `0.114.0`. 🤓 + +/// + +You can use Pydantic's model configuration to `forbid` any `extra` fields: + +//// tab | Python 3.9+ + +```Python hl_lines="12" +{!> ../../../docs_src/request_form_models/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11" +{!> ../../../docs_src/request_form_models/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/request_form_models/tutorial002.py!} +``` + +//// + +If a client tries to send some extra data, they will receive an **error** response. + +For example, if the client tries to send the form fields: + +* `username`: `Rick` +* `password`: `Portal Gun` +* `extra`: `Mr. Poopybutthole` + +They will receive an error response telling them that the field `extra` is not allowed: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "Mr. Poopybutthole" + } + ] +} +``` + +## Summary + +You can use Pydantic models to declare form fields in FastAPI. 😎 diff --git a/docs_src/request_form_models/tutorial002.py b/docs_src/request_form_models/tutorial002.py new file mode 100644 index 0000000000000..59b329e8d8955 --- /dev/null +++ b/docs_src/request_form_models/tutorial002.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + model_config = {"extra": "forbid"} + + +@app.post("/login/") +async def login(data: FormData = Form()): + return data diff --git a/docs_src/request_form_models/tutorial002_an.py b/docs_src/request_form_models/tutorial002_an.py new file mode 100644 index 0000000000000..bcb0227959b47 --- /dev/null +++ b/docs_src/request_form_models/tutorial002_an.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + model_config = {"extra": "forbid"} + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/docs_src/request_form_models/tutorial002_an_py39.py b/docs_src/request_form_models/tutorial002_an_py39.py new file mode 100644 index 0000000000000..3004e085243c2 --- /dev/null +++ b/docs_src/request_form_models/tutorial002_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + model_config = {"extra": "forbid"} + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/docs_src/request_form_models/tutorial002_pv1.py b/docs_src/request_form_models/tutorial002_pv1.py new file mode 100644 index 0000000000000..d5f7db2a67ba4 --- /dev/null +++ b/docs_src/request_form_models/tutorial002_pv1.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + class Config: + extra = "forbid" + + +@app.post("/login/") +async def login(data: FormData = Form()): + return data diff --git a/docs_src/request_form_models/tutorial002_pv1_an.py b/docs_src/request_form_models/tutorial002_pv1_an.py new file mode 100644 index 0000000000000..fe9dbc344ba95 --- /dev/null +++ b/docs_src/request_form_models/tutorial002_pv1_an.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + class Config: + extra = "forbid" + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/docs_src/request_form_models/tutorial002_pv1_an_py39.py b/docs_src/request_form_models/tutorial002_pv1_an_py39.py new file mode 100644 index 0000000000000..942d5d4118af9 --- /dev/null +++ b/docs_src/request_form_models/tutorial002_pv1_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + class Config: + extra = "forbid" + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 98ce17b55d9ba..6083b73195fc5 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -789,6 +789,9 @@ async def process_fn( value = serialize_sequence_value(field=field, value=results) if value is not None: values[field.name] = value + for key, value in received_body.items(): + if key not in values: + values[key] = value return values diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002.py b/tests/test_tutorial/test_request_form_models/test_tutorial002.py new file mode 100644 index 0000000000000..76f4800016fb8 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002.py @@ -0,0 +1,196 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002 import app + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_pydanticv2 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "extra", + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py new file mode 100644 index 0000000000000..179b2977d6695 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py @@ -0,0 +1,196 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002_an import app + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_pydanticv2 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "extra", + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py new file mode 100644 index 0000000000000..510ad9d7c022a --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py @@ -0,0 +1,203 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_py39, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002_an_py39 import app + + client = TestClient(app) + return client + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "extra", + } + ] + } + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py new file mode 100644 index 0000000000000..249b9379df726 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py @@ -0,0 +1,189 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002_pv1 import app + + client = TestClient(app) + return client + + +@needs_pydanticv1 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_pydanticv1 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.extra", + "loc": ["body", "extra"], + "msg": "extra fields not permitted", + } + ] + } + + +@needs_pydanticv1 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + } + ] + } + + +@needs_pydanticv1 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + } + ] + } + + +@needs_pydanticv1 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +@needs_pydanticv1 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py new file mode 100644 index 0000000000000..44cb3c32b63f8 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py @@ -0,0 +1,196 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002_pv1_an import app + + client = TestClient(app) + return client + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.extra", + "loc": ["body", "extra"], + "msg": "extra fields not permitted", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py new file mode 100644 index 0000000000000..899549e406647 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py @@ -0,0 +1,203 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_py39, needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002_pv1_an_py39 import app + + client = TestClient(app) + return client + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.extra", + "loc": ["body", "extra"], + "msg": "extra fields not permitted", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } From a11e392f5f0ae8b50f92252f811764d48929466f Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 6 Sep 2024 17:31:44 +0000 Subject: [PATCH 56/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2b9bd3e871e33..7f4353cfe9fde 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Add support for forbidding extra form fields with Pydantic models. PR [#12134](https://github.com/fastapi/fastapi/pull/12134) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ✅ Update internal tests for latest Pydantic, including CI tweaks to install the latest Pydantic. PR [#12147](https://github.com/fastapi/fastapi/pull/12147) by [@tiangolo](https://github.com/tiangolo). From 4ff22a0c4167e5fe5dc039b29531329398d67ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Sep 2024 19:38:23 +0200 Subject: [PATCH 57/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs,=20Form=20Mo?= =?UTF-8?q?dels=20section=20title,=20to=20match=20config=20name=20(#12152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/request-form-models.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/request-form-models.md b/docs/en/docs/tutorial/request-form-models.md index a317ee14d097b..1440d17b8b983 100644 --- a/docs/en/docs/tutorial/request-form-models.md +++ b/docs/en/docs/tutorial/request-form-models.md @@ -64,7 +64,7 @@ You can verify it in the docs UI at `/docs`:
-## Restrict Extra Form Fields +## Forbid Extra Form Fields In some special use cases (probably not very common), you might want to **restrict** the form fields to only those declared in the Pydantic model. And **forbid** any **extra** fields. From e68d8c60fbe22609b6e3c3a652474088eeba18e6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 6 Sep 2024 17:38:50 +0000 Subject: [PATCH 58/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7f4353cfe9fde..b7db0f7806cd0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ✨ Add support for forbidding extra form fields with Pydantic models. PR [#12134](https://github.com/fastapi/fastapi/pull/12134) by [@tiangolo](https://github.com/tiangolo). +### Docs + +* 📝 Update docs, Form Models section title, to match config name. PR [#12152](https://github.com/fastapi/fastapi/pull/12152) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ✅ Update internal tests for latest Pydantic, including CI tweaks to install the latest Pydantic. PR [#12147](https://github.com/fastapi/fastapi/pull/12147) by [@tiangolo](https://github.com/tiangolo). From 74842f0a604f9e90e6ffb71c352186389060b1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Sep 2024 19:40:27 +0200 Subject: [PATCH 59/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b7db0f7806cd0..94f494375ea73 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,30 @@ hide: ## Latest Changes +You can restrict form fields to only include those declared in a Pydantic model and forbid any extra field sent in the request using Pydantic's `model_config = {"extra": "forbid"}`: + +```python +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + model_config = {"extra": "forbid"} + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data +``` + +Read the new docs: [Form Models - Forbid Extra Form Fields](https://fastapi.tiangolo.com/tutorial/request-form-models/#forbid-extra-form-fields). + ### Features * ✨ Add support for forbidding extra form fields with Pydantic models. PR [#12134](https://github.com/fastapi/fastapi/pull/12134) by [@tiangolo](https://github.com/tiangolo). From bde12faea20313e4570f7cb896c201058c26e546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 6 Sep 2024 19:41:13 +0200 Subject: [PATCH 60/89] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.114.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 94f494375ea73..557498278daf4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.114.0 + You can restrict form fields to only include those declared in a Pydantic model and forbid any extra field sent in the request using Pydantic's `model_config = {"extra": "forbid"}`: ```python diff --git a/fastapi/__init__.py b/fastapi/__init__.py index f785f81cd6274..dce17360f9397 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.113.0" +__version__ = "0.114.0" from starlette import status as status From b60d36e7533e0ae299cdff0d72b078d1f036ac67 Mon Sep 17 00:00:00 2001 From: Vaibhav <35167042+surreal30@users.noreply.github.com> Date: Fri, 6 Sep 2024 23:36:20 +0530 Subject: [PATCH 61/89] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20`f?= =?UTF-8?q?astapi/params.py`=20(#12143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/params.py b/fastapi/params.py index 3dfa5a1a381e5..90ca7cb010e6c 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -556,7 +556,7 @@ def __init__( kwargs["examples"] = examples if regex is not None: warnings.warn( - "`regex` has been depreacated, please use `pattern` instead", + "`regex` has been deprecated, please use `pattern` instead", category=DeprecationWarning, stacklevel=4, ) From 4b9e5b3a7433f13dcb1ca6d284326b1753231af2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 6 Sep 2024 18:06:45 +0000 Subject: [PATCH 62/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 557498278daf4..23bb2d9d18d34 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* ✏️ Fix typo in `fastapi/params.py`. PR [#12143](https://github.com/fastapi/fastapi/pull/12143) by [@surreal30](https://github.com/surreal30). + ## 0.114.0 You can restrict form fields to only include those declared in a Pydantic model and forbid any extra field sent in the request using Pydantic's `model_config = {"extra": "forbid"}`: From edb584199f3341b205da5d7e1686c54d8719b82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 7 Sep 2024 17:21:14 +0200 Subject: [PATCH 63/89] =?UTF-8?q?=F0=9F=91=B7=20Update=20`issue-manager.ym?= =?UTF-8?q?l`=20(#12159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index d5b947a9c52a2..fbb856792037b 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,7 +2,7 @@ name: Issue Manager on: schedule: - - cron: "10 3 * * *" + - cron: "13 22 * * *" issue_comment: types: - created @@ -16,6 +16,7 @@ on: permissions: issues: write + pull-requests: write jobs: issue-manager: @@ -35,8 +36,8 @@ jobs: "delay": 864000, "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." }, - "changes-requested": { + "waiting": { "delay": 2628000, - "message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." + "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." } } From b501fc6dafbef19a9d17b8484469ca81426c8e9d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Sep 2024 15:24:06 +0000 Subject: [PATCH 64/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 23bb2d9d18d34..16bd6e52695eb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 👷 Update `issue-manager.yml`. PR [#12159](https://github.com/fastapi/fastapi/pull/12159) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in `fastapi/params.py`. PR [#12143](https://github.com/fastapi/fastapi/pull/12143) by [@surreal30](https://github.com/surreal30). ## 0.114.0 From ec2a50829202ab98f166f581053d82b74d3f1130 Mon Sep 17 00:00:00 2001 From: BORA <88664069+BORA040126@users.noreply.github.com> Date: Sun, 8 Sep 2024 08:35:43 +0900 Subject: [PATCH 65/89] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20translation?= =?UTF-8?q?=20for=20`docs/ko/docs/project-generation.md`=20(#12157)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/project-generation.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/ko/docs/project-generation.md diff --git a/docs/ko/docs/project-generation.md b/docs/ko/docs/project-generation.md new file mode 100644 index 0000000000000..019919f776c87 --- /dev/null +++ b/docs/ko/docs/project-generation.md @@ -0,0 +1,28 @@ +# Full Stack FastAPI 템플릿 + +템플릿은 일반적으로 특정 설정과 함께 제공되지만, 유연하고 커스터마이징이 가능하게 디자인 되었습니다. 이 특성들은 여러분이 프로젝트의 요구사항에 맞춰 수정, 적용을 할 수 있게 해주고, 템플릿이 완벽한 시작점이 되게 해줍니다. 🏁 + +많은 초기 설정, 보안, 데이터베이스 및 일부 API 엔드포인트가 이미 준비되어 있으므로, 여러분은 이 템플릿을 (프로젝트를) 시작하는 데 사용할 수 있습니다. + +GitHub 저장소: Full Stack FastAPI 템플릿 + +## Full Stack FastAPI 템플릿 - 기술 스택과 기능들 + +- ⚡ [**FastAPI**](https://fastapi.tiangolo.com): Python 백엔드 API. + - 🧰 [SQLModel](https://sqlmodel.tiangolo.com): Python SQL 데이터 상호작용을 위한 (ORM). + - 🔍 [Pydantic](https://docs.pydantic.dev): FastAPI에 의해 사용되는, 데이터 검증과 설정관리. + - 💾 [PostgreSQL](https://www.postgresql.org): SQL 데이터베이스. +- 🚀 [React](https://react.dev): 프론트엔드. + - 💃 TypeScript, hooks, Vite 및 기타 현대적인 프론트엔드 스택을 사용. + - 🎨 [Chakra UI](https://chakra-ui.com): 프론트엔드 컴포넌트. + - 🤖 자동으로 생성된 프론트엔드 클라이언트. + - 🧪 E2E 테스트를 위한 Playwright. + - 🦇 다크 모드 지원. +- 🐋 [Docker Compose](https://www.docker.com): 개발 환경과 프로덕션(운영). +- 🔒 기본으로 지원되는 안전한 비밀번호 해싱. +- 🔑 JWT 토큰 인증. +- 📫 이메일 기반 비밀번호 복구. +- ✅ [Pytest]를 이용한 테스트(https://pytest.org). +- 📞 [Traefik](https://traefik.io): 리버스 프록시 / 로드 밸런서. +- 🚢 Docker Compose를 이용한 배포 지침: 자동 HTTPS 인증서를 처리하기 위한 프론트엔드 Traefik 프록시 설정 방법을 포함. +- 🏭 GitHub Actions를 기반으로 CI (지속적인 통합) 및 CD (지속적인 배포). From 3a4431b6feb50a86a60ce034580cf9fbacee9d32 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Sep 2024 23:36:05 +0000 Subject: [PATCH 66/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 16bd6e52695eb..e09eb57b62d45 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Add Korean translation for `docs/ko/docs/project-generation.md`. PR [#12157](https://github.com/fastapi/fastapi/pull/12157) by [@BORA040126](https://github.com/BORA040126). + ### Internal * 👷 Update `issue-manager.yml`. PR [#12159](https://github.com/fastapi/fastapi/pull/12159) by [@tiangolo](https://github.com/tiangolo). From 270aef71c47694ca349afeba95d13ade195185d2 Mon Sep 17 00:00:00 2001 From: Guillaume Fassot <97948781+prometek@users.noreply.github.com> Date: Sun, 8 Sep 2024 22:36:53 +0200 Subject: [PATCH 67/89] =?UTF-8?q?=F0=9F=93=9D=20Remove=20duplicate=20line?= =?UTF-8?q?=20in=20docs=20for=20`docs/en/docs/environment-variables.md`=20?= =?UTF-8?q?(#12169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/environment-variables.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/en/docs/environment-variables.md b/docs/en/docs/environment-variables.md index 78e82d5af65a4..43dd06add3d56 100644 --- a/docs/en/docs/environment-variables.md +++ b/docs/en/docs/environment-variables.md @@ -243,8 +243,6 @@ This way, when you type `python` in the terminal, the system will find the Pytho //// -This way, when you type `python` in the terminal, the system will find the Python program in `/opt/custompython/bin` (the last directory) and use that one. - So, if you type:
From c49c4e7df8eef1ab4ed5baacdf02df3a10aaaae1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 8 Sep 2024 20:37:14 +0000 Subject: [PATCH 68/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e09eb57b62d45..6e84911e95d13 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Remove duplicate line in docs for `docs/en/docs/environment-variables.md`. PR [#12169](https://github.com/fastapi/fastapi/pull/12169) by [@prometek](https://github.com/prometek). + ### Translations * 🌐 Add Korean translation for `docs/ko/docs/project-generation.md`. PR [#12157](https://github.com/fastapi/fastapi/pull/12157) by [@BORA040126](https://github.com/BORA040126). From a67167dce3f3b33ef1789c0eeb7a2dcdf2cc4314 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:35:50 +0200 Subject: [PATCH 69/89] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commit?= =?UTF-8?q?=20autoupdate=20(#12176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⬆ [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.3 → v0.6.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.3...v0.6.4) * bump ruff in tests requirements as well --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: svlandeg --- .pre-commit-config.yaml | 2 +- requirements-tests.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e58afd4b9362..f74816f12352c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.3 + rev: v0.6.4 hooks: - id: ruff args: diff --git a/requirements-tests.txt b/requirements-tests.txt index de5fdb8a2a5f8..809a19c0ceff9 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.8.0 -ruff ==0.6.3 +ruff ==0.6.4 dirty-equals ==0.6.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy # probably when including SQLModel From da4670cf775ff7c2ef98b7157a71a91fe980816e Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 9 Sep 2024 18:36:15 +0000 Subject: [PATCH 70/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6e84911e95d13..3af1a5ade66b0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12176](https://github.com/fastapi/fastapi/pull/12176) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 👷 Update `issue-manager.yml`. PR [#12159](https://github.com/fastapi/fastapi/pull/12159) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in `fastapi/params.py`. PR [#12143](https://github.com/fastapi/fastapi/pull/12143) by [@surreal30](https://github.com/surreal30). From fc601bcb4b2dd97e3c7918c8f59f97a62df06abc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:07:46 +0200 Subject: [PATCH 71/89] =?UTF-8?q?=E2=AC=86=20Bump=20tiangolo/issue-manager?= =?UTF-8?q?=20from=200.5.0=20to=200.5.1=20(#12173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tiangolo/issue-manager](https://github.com/tiangolo/issue-manager) from 0.5.0 to 0.5.1. - [Release notes](https://github.com/tiangolo/issue-manager/releases) - [Commits](https://github.com/tiangolo/issue-manager/compare/0.5.0...0.5.1) --- updated-dependencies: - dependency-name: tiangolo/issue-manager dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue-manager.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index fbb856792037b..439084434dd0b 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -27,7 +27,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: tiangolo/issue-manager@0.5.0 + - uses: tiangolo/issue-manager@0.5.1 with: token: ${{ secrets.GITHUB_TOKEN }} config: > From bc715d55bc1ee3aedf4d429ca4e08ae39e0bbb90 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Sep 2024 09:08:09 +0000 Subject: [PATCH 72/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3af1a5ade66b0..b9eaed65dbfc4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Internal +* ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#12173](https://github.com/fastapi/fastapi/pull/12173) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12176](https://github.com/fastapi/fastapi/pull/12176) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 👷 Update `issue-manager.yml`. PR [#12159](https://github.com/fastapi/fastapi/pull/12159) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in `fastapi/params.py`. PR [#12143](https://github.com/fastapi/fastapi/pull/12143) by [@surreal30](https://github.com/surreal30). From 80e2cd12747df2de38b4ab3ee1ee1cb889ee242b Mon Sep 17 00:00:00 2001 From: marcelomarkus Date: Tue, 10 Sep 2024 07:34:25 -0300 Subject: [PATCH 73/89] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20transla?= =?UTF-8?q?tion=20for=20`docs/pt/docs/tutorial/debugging.md`=20(#12165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/tutorial/debugging.md | 115 +++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/pt/docs/tutorial/debugging.md diff --git a/docs/pt/docs/tutorial/debugging.md b/docs/pt/docs/tutorial/debugging.md new file mode 100644 index 0000000000000..54582fcbcb073 --- /dev/null +++ b/docs/pt/docs/tutorial/debugging.md @@ -0,0 +1,115 @@ +# Depuração + +Você pode conectar o depurador no seu editor, por exemplo, com o Visual Studio Code ou PyCharm. + +## Chamar `uvicorn` + +Em seu aplicativo FastAPI, importe e execute `uvicorn` diretamente: + +```Python hl_lines="1 15" +{!../../../docs_src/debugging/tutorial001.py!} +``` + +### Sobre `__name__ == "__main__"` + +O objetivo principal de `__name__ == "__main__"` é ter algum código que seja executado quando seu arquivo for chamado com: + +
+ +```console +$ python myapp.py +``` + +
+ +mas não é chamado quando outro arquivo o importa, como em: + +```Python +from myapp import app +``` + +#### Mais detalhes + +Digamos que seu arquivo se chama `myapp.py`. + +Se você executá-lo com: + +
+ +```console +$ python myapp.py +``` + +
+ +então a variável interna `__name__` no seu arquivo, criada automaticamente pelo Python, terá como valor a string `"__main__"`. + +Então, a seção: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +vai executar. + +--- + +Isso não acontecerá se você importar esse módulo (arquivo). + +Então, se você tiver outro arquivo `importer.py` com: + +```Python +from myapp import app + +# Mais um pouco de código +``` + +nesse caso, a variável criada automaticamente dentro de `myapp.py` não terá a variável `__name__` com o valor `"__main__"`. + +Então, a linha: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +não será executada. + +/// info | "Informação" + +Para mais informações, consulte a documentação oficial do Python. + +/// + +## Execute seu código com seu depurador + +Como você está executando o servidor Uvicorn diretamente do seu código, você pode chamar seu programa Python (seu aplicativo FastAPI) diretamente do depurador. + +--- + +Por exemplo, no Visual Studio Code, você pode: + +* Ir para o painel "Debug". +* "Add configuration...". +* Selecionar "Python" +* Executar o depurador com a opção "`Python: Current File (Integrated Terminal)`". + +Em seguida, ele iniciará o servidor com seu código **FastAPI**, parará em seus pontos de interrupção, etc. + +Veja como pode parecer: + + + +--- + +Se você usar o Pycharm, você pode: + +* Abrir o menu "Executar". +* Selecionar a opção "Depurar...". +* Então um menu de contexto aparece. +* Selecionar o arquivo para depurar (neste caso, `main.py`). + +Em seguida, ele iniciará o servidor com seu código **FastAPI**, parará em seus pontos de interrupção, etc. + +Veja como pode parecer: + + From 73d4f347df83c8e59ab55c9dfdbd974351e6efc5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Sep 2024 10:34:46 +0000 Subject: [PATCH 74/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b9eaed65dbfc4..7492242a43979 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/debugging.md`. PR [#12165](https://github.com/fastapi/fastapi/pull/12165) by [@marcelomarkus](https://github.com/marcelomarkus). * 🌐 Add Korean translation for `docs/ko/docs/project-generation.md`. PR [#12157](https://github.com/fastapi/fastapi/pull/12157) by [@BORA040126](https://github.com/BORA040126). ### Internal From a4a7925045e42030528c6f1fdeaa059e811455c0 Mon Sep 17 00:00:00 2001 From: marcelomarkus Date: Tue, 10 Sep 2024 07:35:14 -0300 Subject: [PATCH 75/89] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20transla?= =?UTF-8?q?tion=20for=20`docs/pt/docs/tutorial/testing.md`=20(#12164)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/tutorial/testing.md | 249 +++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 docs/pt/docs/tutorial/testing.md diff --git a/docs/pt/docs/tutorial/testing.md b/docs/pt/docs/tutorial/testing.md new file mode 100644 index 0000000000000..f734a7d9a5d22 --- /dev/null +++ b/docs/pt/docs/tutorial/testing.md @@ -0,0 +1,249 @@ +# Testando + +Graças ao Starlette, testar aplicativos **FastAPI** é fácil e agradável. + +Ele é baseado no HTTPX, que por sua vez é projetado com base em Requests, por isso é muito familiar e intuitivo. + +Com ele, você pode usar o pytest diretamente com **FastAPI**. + +## Usando `TestClient` + +/// info | "Informação" + +Para usar o `TestClient`, primeiro instale o `httpx`. + +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e instalá-lo, por exemplo: + +```console +$ pip install httpx +``` + +/// + +Importe `TestClient`. + +Crie um `TestClient` passando seu aplicativo **FastAPI** para ele. + +Crie funções com um nome que comece com `test_` (essa é a convenção padrão do `pytest`). + +Use o objeto `TestClient` da mesma forma que você faz com `httpx`. + +Escreva instruções `assert` simples com as expressões Python padrão que você precisa verificar (novamente, `pytest` padrão). + +```Python hl_lines="2 12 15-18" +{!../../../docs_src/app_testing/tutorial001.py!} +``` + +/// tip | "Dica" + +Observe que as funções de teste são `def` normais, não `async def`. + +E as chamadas para o cliente também são chamadas normais, não usando `await`. + +Isso permite que você use `pytest` diretamente sem complicações. + +/// + +/// note | "Detalhes técnicos" + +Você também pode usar `from starlette.testclient import TestClient`. + +**FastAPI** fornece o mesmo `starlette.testclient` que `fastapi.testclient` apenas como uma conveniência para você, o desenvolvedor. Mas ele vem diretamente da Starlette. + +/// + +/// tip | "Dica" + +Se você quiser chamar funções `async` em seus testes além de enviar solicitações ao seu aplicativo FastAPI (por exemplo, funções de banco de dados assíncronas), dê uma olhada em [Testes assíncronos](../advanced/async-tests.md){.internal-link target=_blank} no tutorial avançado. + +/// + +## Separando testes + +Em uma aplicação real, você provavelmente teria seus testes em um arquivo diferente. + +E seu aplicativo **FastAPI** também pode ser composto de vários arquivos/módulos, etc. + +### Arquivo do aplicativo **FastAPI** + +Digamos que você tenha uma estrutura de arquivo conforme descrito em [Aplicativos maiores](bigger-applications.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` + +No arquivo `main.py` você tem seu aplicativo **FastAPI**: + + +```Python +{!../../../docs_src/app_testing/main.py!} +``` + +### Arquivo de teste + +Então você poderia ter um arquivo `test_main.py` com seus testes. Ele poderia estar no mesmo pacote Python (o mesmo diretório com um arquivo `__init__.py`): + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Como esse arquivo está no mesmo pacote, você pode usar importações relativas para importar o objeto `app` do módulo `main` (`main.py`): + +```Python hl_lines="3" +{!../../../docs_src/app_testing/test_main.py!} +``` + +...e ter o código para os testes como antes. + +## Testando: exemplo estendido + +Agora vamos estender este exemplo e adicionar mais detalhes para ver como testar diferentes partes. + +### Arquivo de aplicativo **FastAPI** estendido + +Vamos continuar com a mesma estrutura de arquivo de antes: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Digamos que agora o arquivo `main.py` com seu aplicativo **FastAPI** tenha algumas outras **operações de rotas**. + +Ele tem uma operação `GET` que pode retornar um erro. + +Ele tem uma operação `POST` que pode retornar vários erros. + +Ambas as *operações de rotas* requerem um cabeçalho `X-Token`. + +//// tab | Python 3.10+ + +```Python +{!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python +{!> ../../../docs_src/app_testing/app_b_an/main.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip | "Dica" + +Prefira usar a versão `Annotated` se possível. + +/// + +```Python +{!> ../../../docs_src/app_testing/app_b_py310/main.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | "Dica" + +Prefira usar a versão `Annotated` se possível. + +/// + +```Python +{!> ../../../docs_src/app_testing/app_b/main.py!} +``` + +//// + +### Arquivo de teste estendido + +Você pode então atualizar `test_main.py` com os testes estendidos: + +```Python +{!> ../../../docs_src/app_testing/app_b/test_main.py!} +``` + +Sempre que você precisar que o cliente passe informações na requisição e não souber como, você pode pesquisar (no Google) como fazer isso no `httpx`, ou até mesmo como fazer isso com `requests`, já que o design do HTTPX é baseado no design do Requests. + +Depois é só fazer o mesmo nos seus testes. + +Por exemplo: + +* Para passar um parâmetro *path* ou *query*, adicione-o à própria URL. +* Para passar um corpo JSON, passe um objeto Python (por exemplo, um `dict`) para o parâmetro `json`. +* Se você precisar enviar *Dados de Formulário* em vez de JSON, use o parâmetro `data`. +* Para passar *headers*, use um `dict` no parâmetro `headers`. +* Para *cookies*, um `dict` no parâmetro `cookies`. + +Para mais informações sobre como passar dados para o backend (usando `httpx` ou `TestClient`), consulte a documentação do HTTPX. + +/// info | "Informação" + +Observe que o `TestClient` recebe dados que podem ser convertidos para JSON, não para modelos Pydantic. + +Se você tiver um modelo Pydantic em seu teste e quiser enviar seus dados para o aplicativo durante o teste, poderá usar o `jsonable_encoder` descrito em [Codificador compatível com JSON](encoder.md){.internal-link target=_blank}. + +/// + +## Execute-o + +Depois disso, você só precisa instalar o `pytest`. + +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e instalá-lo, por exemplo: + +
+ +```console +$ pip install pytest + +---> 100% +``` + +
+ +Ele detectará os arquivos e os testes automaticamente, os executará e informará os resultados para você. + +Execute os testes com: + +
+ +```console +$ pytest + +================ test session starts ================ +platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +rootdir: /home/user/code/superawesome-cli/app +plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 +collected 6 items + +---> 100% + +test_main.py ...... [100%] + +================= 1 passed in 0.03s ================= +``` + +
From e69ba263861b440fce06b55bedf83bd08646452b Mon Sep 17 00:00:00 2001 From: marcelomarkus Date: Tue, 10 Sep 2024 07:36:42 -0300 Subject: [PATCH 76/89] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20transla?= =?UTF-8?q?tion=20for=20`docs/pt/docs/environment-variables.md`=20(#12162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/environment-variables.md | 298 ++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 docs/pt/docs/environment-variables.md diff --git a/docs/pt/docs/environment-variables.md b/docs/pt/docs/environment-variables.md new file mode 100644 index 0000000000000..360d1c496bd49 --- /dev/null +++ b/docs/pt/docs/environment-variables.md @@ -0,0 +1,298 @@ +# Variáveis de Ambiente + +/// tip | "Dica" + +Se você já sabe o que são "variáveis de ambiente" e como usá-las, pode pular esta seção. + +/// + +Uma variável de ambiente (também conhecida como "**env var**") é uma variável que existe **fora** do código Python, no **sistema operacional**, e pode ser lida pelo seu código Python (ou por outros programas também). + +Variáveis de ambiente podem ser úteis para lidar com **configurações** do aplicativo, como parte da **instalação** do Python, etc. + +## Criar e Usar Variáveis de Ambiente + +Você pode **criar** e usar variáveis de ambiente no **shell (terminal)**, sem precisar do Python: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// Você pode criar uma variável de ambiente MY_NAME com +$ export MY_NAME="Wade Wilson" + +// Então você pode usá-la com outros programas, como +$ echo "Hello $MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Criar uma variável de ambiente MY_NAME +$ $Env:MY_NAME = "Wade Wilson" + +// Usá-la com outros programas, como +$ echo "Hello $Env:MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +## Ler Variáveis de Ambiente no Python + +Você também pode criar variáveis de ambiente **fora** do Python, no terminal (ou com qualquer outro método) e depois **lê-las no Python**. + +Por exemplo, você poderia ter um arquivo `main.py` com: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip | "Dica" + +O segundo argumento para `os.getenv()` é o valor padrão a ser retornado. + +Se não for fornecido, é `None` por padrão, Aqui fornecemos `"World"` como o valor padrão a ser usado. + +/// + +Então você poderia chamar esse programa Python: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// Aqui ainda não definimos a variável de ambiente +$ python main.py + +// Como não definimos a variável de ambiente, obtemos o valor padrão + +Hello World from Python + +// Mas se criarmos uma variável de ambiente primeiro +$ export MY_NAME="Wade Wilson" + +// E então chamar o programa novamente +$ python main.py + +// Agora ele pode ler a variável de ambiente + +Hello Wade Wilson from Python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Aqui ainda não definimos a variável de ambiente +$ python main.py + +// Como não definimos a variável de ambiente, obtemos o valor padrão + +Hello World from Python + +// Mas se criarmos uma variável de ambiente primeiro +$ $Env:MY_NAME = "Wade Wilson" + +// E então chamar o programa novamente +$ python main.py + +// Agora ele pode ler a variável de ambiente + +Hello Wade Wilson from Python +``` + +
+ +//// + +Como as variáveis de ambiente podem ser definidas fora do código, mas podem ser lidas pelo código e não precisam ser armazenadas (com versão no `git`) com o restante dos arquivos, é comum usá-las para configurações ou **definições**. + +Você também pode criar uma variável de ambiente apenas para uma **invocação específica do programa**, que só está disponível para aquele programa e apenas pela duração dele. + +Para fazer isso, crie-a na mesma linha, antes do próprio programa: + +
+ +```console +// Criar uma variável de ambiente MY_NAME para esta chamada de programa +$ MY_NAME="Wade Wilson" python main.py + +// Agora ele pode ler a variável de ambiente + +Hello Wade Wilson from Python + +// A variável de ambiente não existe mais depois +$ python main.py + +Hello World from Python +``` + +
+ +/// tip | "Dica" + +Você pode ler mais sobre isso em The Twelve-Factor App: Config. + +/// + +## Tipos e Validação + +Essas variáveis de ambiente só podem lidar com **strings de texto**, pois são externas ao Python e precisam ser compatíveis com outros programas e com o resto do sistema (e até mesmo com diferentes sistemas operacionais, como Linux, Windows, macOS). + +Isso significa que **qualquer valor** lido em Python de uma variável de ambiente **será uma `str`**, e qualquer conversão para um tipo diferente ou qualquer validação precisa ser feita no código. + +Você aprenderá mais sobre como usar variáveis de ambiente para lidar com **configurações do aplicativo** no [Guia do Usuário Avançado - Configurações e Variáveis de Ambiente](./advanced/settings.md){.internal-link target=_blank}. + +## Variável de Ambiente `PATH` + +Existe uma variável de ambiente **especial** chamada **`PATH`** que é usada pelos sistemas operacionais (Linux, macOS, Windows) para encontrar programas para executar. + +O valor da variável `PATH` é uma longa string composta por diretórios separados por dois pontos `:` no Linux e macOS, e por ponto e vírgula `;` no Windows. + +Por exemplo, a variável de ambiente `PATH` poderia ter esta aparência: + +//// tab | Linux, macOS + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +Isso significa que o sistema deve procurar programas nos diretórios: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +Isso significa que o sistema deve procurar programas nos diretórios: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +Quando você digita um **comando** no terminal, o sistema operacional **procura** o programa em **cada um dos diretórios** listados na variável de ambiente `PATH`. + +Por exemplo, quando você digita `python` no terminal, o sistema operacional procura um programa chamado `python` no **primeiro diretório** dessa lista. + +Se ele o encontrar, então ele o **usará**. Caso contrário, ele continua procurando nos **outros diretórios**. + +### Instalando o Python e Atualizando o `PATH` + +Durante a instalação do Python, você pode ser questionado sobre a atualização da variável de ambiente `PATH`. + +//// tab | Linux, macOS + +Vamos supor que você instale o Python e ele fique em um diretório `/opt/custompython/bin`. + +Se você concordar em atualizar a variável de ambiente `PATH`, o instalador adicionará `/opt/custompython/bin` para a variável de ambiente `PATH`. + +Poderia parecer assim: + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +Dessa forma, ao digitar `python` no terminal, o sistema encontrará o programa Python em `/opt/custompython/bin` (último diretório) e o utilizará. + +//// + +//// tab | Windows + +Digamos que você instala o Python e ele acaba em um diretório `C:\opt\custompython\bin`. + +Se você disser sim para atualizar a variável de ambiente `PATH`, o instalador adicionará `C:\opt\custompython\bin` à variável de ambiente `PATH`. + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +Dessa forma, quando você digitar `python` no terminal, o sistema encontrará o programa Python em `C:\opt\custompython\bin` (o último diretório) e o utilizará. + +//// + +Então, se você digitar: + +
+ +```console +$ python +``` + +
+ +//// tab | Linux, macOS + +O sistema **encontrará** o programa `python` em `/opt/custompython/bin` e o executará. + +Seria aproximadamente equivalente a digitar: + +
+ +```console +$ /opt/custompython/bin/python +``` + +
+ +//// + +//// tab | Windows + +O sistema **encontrará** o programa `python` em `C:\opt\custompython\bin\python` e o executará. + +Seria aproximadamente equivalente a digitar: + +
+ +```console +$ C:\opt\custompython\bin\python +``` + +
+ +//// + +Essas informações serão úteis ao aprender sobre [Ambientes Virtuais](virtual-environments.md){.internal-link target=_blank}. + +## Conclusão + +Com isso, você deve ter uma compreensão básica do que são **variáveis ​​de ambiente** e como usá-las em Python. + +Você também pode ler mais sobre elas na Wikipedia para Variáveis ​​de Ambiente. + +Em muitos casos, não é muito óbvio como as variáveis ​​de ambiente seriam úteis e aplicáveis ​​imediatamente. Mas elas continuam aparecendo em muitos cenários diferentes quando você está desenvolvendo, então é bom saber sobre elas. + +Por exemplo, você precisará dessas informações na próxima seção, sobre [Ambientes Virtuais](virtual-environments.md). From 944b6e507e326f986061e99936c80aa565da669f Mon Sep 17 00:00:00 2001 From: marcelomarkus Date: Tue, 10 Sep 2024 07:37:13 -0300 Subject: [PATCH 77/89] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20transla?= =?UTF-8?q?tion=20for=20`docs/pt/docs/virtual-environments.md`=20(#12163)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/virtual-environments.md | 844 +++++++++++++++++++++++++++ 1 file changed, 844 insertions(+) create mode 100644 docs/pt/docs/virtual-environments.md diff --git a/docs/pt/docs/virtual-environments.md b/docs/pt/docs/virtual-environments.md new file mode 100644 index 0000000000000..863c8d65e6714 --- /dev/null +++ b/docs/pt/docs/virtual-environments.md @@ -0,0 +1,844 @@ +# Ambientes Virtuais + +Ao trabalhar em projetos Python, você provavelmente deve usar um **ambiente virtual** (ou um mecanismo similar) para isolar os pacotes que você instala para cada projeto. + +/// info | "Informação" + +Se você já sabe sobre ambientes virtuais, como criá-los e usá-los, talvez seja melhor pular esta seção. 🤓 + +/// + +/// tip | "Dica" + +Um **ambiente virtual** é diferente de uma **variável de ambiente**. + +Uma **variável de ambiente** é uma variável no sistema que pode ser usada por programas. + +Um **ambiente virtual** é um diretório com alguns arquivos. + +/// + +/// info | "Informação" + +Esta página lhe ensinará como usar **ambientes virtuais** e como eles funcionam. + +Se você estiver pronto para adotar uma **ferramenta que gerencia tudo** para você (incluindo a instalação do Python), experimente uv. + +/// + +## Criar um Projeto + +Primeiro, crie um diretório para seu projeto. + +O que normalmente faço é criar um diretório chamado `code` dentro do meu diretório home/user. + +E dentro disso eu crio um diretório por projeto. + +
+ +```console +// Vá para o diretório inicial +$ cd +// Crie um diretório para todos os seus projetos de código +$ mkdir code +// Entre nesse diretório de código +$ cd code +// Crie um diretório para este projeto +$ mkdir awesome-project +// Entre no diretório do projeto +$ cd awesome-project +``` + +
+ +## Crie um ambiente virtual + +Ao começar a trabalhar em um projeto Python **pela primeira vez**, crie um ambiente virtual **dentro do seu projeto**. + +/// tip | "Dica" + +Você só precisa fazer isso **uma vez por projeto**, não toda vez que trabalhar. + +/// + +//// tab | `venv` + +Para criar um ambiente virtual, você pode usar o módulo `venv` que vem com o Python. + +
+ +```console +$ python -m venv .venv +``` + +
+ +/// details | O que esse comando significa + +* `python`: usa o programa chamado `python` +* `-m`: chama um módulo como um script, nós diremos a ele qual módulo vem em seguida +* `venv`: usa o módulo chamado `venv` que normalmente vem instalado com o Python +* `.venv`: cria o ambiente virtual no novo diretório `.venv` + +/// + +//// + +//// tab | `uv` + +Se você tiver o `uv` instalado, poderá usá-lo para criar um ambiente virtual. + +
+ +```console +$ uv venv +``` + +
+ +/// tip | "Dica" + +Por padrão, `uv` criará um ambiente virtual em um diretório chamado `.venv`. + +Mas você pode personalizá-lo passando um argumento adicional com o nome do diretório. + +/// + +//// + +Esse comando cria um novo ambiente virtual em um diretório chamado `.venv`. + +/// details | `.venv` ou outro nome + +Você pode criar o ambiente virtual em um diretório diferente, mas há uma convenção para chamá-lo de `.venv`. + +/// + +## Ative o ambiente virtual + +Ative o novo ambiente virtual para que qualquer comando Python que você executar ou pacote que você instalar o utilize. + +/// tip | "Dica" + +Faça isso **toda vez** que iniciar uma **nova sessão de terminal** para trabalhar no projeto. + +/// + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Ou se você usa o Bash para Windows (por exemplo, Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +/// tip | "Dica" + +Toda vez que você instalar um **novo pacote** naquele ambiente, **ative** o ambiente novamente. + +Isso garante que, se você usar um **programa de terminal (CLI)** instalado por esse pacote, você usará aquele do seu ambiente virtual e não qualquer outro que possa ser instalado globalmente, provavelmente com uma versão diferente do que você precisa. + +/// + +## Verifique se o ambiente virtual está ativo + +Verifique se o ambiente virtual está ativo (o comando anterior funcionou). + +/// tip | "Dica" + +Isso é **opcional**, mas é uma boa maneira de **verificar** se tudo está funcionando conforme o esperado e se você está usando o ambiente virtual pretendido. + +/// + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +Se ele mostrar o binário `python` em `.venv/bin/python`, dentro do seu projeto (neste caso `awesome-project`), então funcionou. 🎉 + +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +Se ele mostrar o binário `python` em `.venv\Scripts\python`, dentro do seu projeto (neste caso `awesome-project`), então funcionou. 🎉 + +//// + +## Atualizar `pip` + +/// tip | "Dica" + +Se você usar `uv`, você o usará para instalar coisas em vez do `pip`, então não precisará atualizar o `pip`. 😎 + +/// + +Se você estiver usando `pip` para instalar pacotes (ele vem por padrão com o Python), você deve **atualizá-lo** para a versão mais recente. + +Muitos erros exóticos durante a instalação de um pacote são resolvidos apenas atualizando o `pip` primeiro. + +/// tip | "Dica" + +Normalmente, você faria isso **uma vez**, logo após criar o ambiente virtual. + +/// + +Certifique-se de que o ambiente virtual esteja ativo (com o comando acima) e execute: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +## Adicionar `.gitignore` + +Se você estiver usando **Git** (você deveria), adicione um arquivo `.gitignore` para excluir tudo em seu `.venv` do Git. + +/// tip | "Dica" + +Se você usou `uv` para criar o ambiente virtual, ele já fez isso para você, você pode pular esta etapa. 😎 + +/// + +/// tip | "Dica" + +Faça isso **uma vez**, logo após criar o ambiente virtual. + +/// + +
+ +```console +$ echo "*" > .venv/.gitignore +``` + +
+ +/// details | O que esse comando significa + +* `echo "*"`: irá "imprimir" o texto `*` no terminal (a próxima parte muda isso um pouco) +* `>`: qualquer coisa impressa no terminal pelo comando à esquerda de `>` não deve ser impressa, mas sim escrita no arquivo que vai à direita de `>` +* `.gitignore`: o nome do arquivo onde o texto deve ser escrito + +E `*` para Git significa "tudo". Então, ele ignorará tudo no diretório `.venv`. + +Esse comando criará um arquivo `.gitignore` com o conteúdo: + +```gitignore +* +``` + +/// + +## Instalar Pacotes + +Após ativar o ambiente, você pode instalar pacotes nele. + +/// tip | "Dica" + +Faça isso **uma vez** ao instalar ou atualizar os pacotes que seu projeto precisa. + +Se precisar atualizar uma versão ou adicionar um novo pacote, você **fará isso novamente**. + +/// + +### Instalar pacotes diretamente + +Se estiver com pressa e não quiser usar um arquivo para declarar os requisitos de pacote do seu projeto, você pode instalá-los diretamente. + +/// tip | "Dica" + +É uma (muito) boa ideia colocar os pacotes e versões que seu programa precisa em um arquivo (por exemplo `requirements.txt` ou `pyproject.toml`). + +/// + +//// tab | `pip` + +
+ +```console +$ pip install "fastapi[standard]" + +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +Se você tem o `uv`: + +
+ +```console +$ uv pip install "fastapi[standard]" +---> 100% +``` + +
+ +//// + +### Instalar a partir de `requirements.txt` + +Se você tiver um `requirements.txt`, agora poderá usá-lo para instalar seus pacotes. + +//// tab | `pip` + +
+ +```console +$ pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +Se você tem o `uv`: + +
+ +```console +$ uv pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +/// details | `requirements.txt` + +Um `requirements.txt` com alguns pacotes poderia se parecer com: + +```requirements.txt +fastapi[standard]==0.113.0 +pydantic==2.8.0 +``` + +/// + +## Execute seu programa + +Depois de ativar o ambiente virtual, você pode executar seu programa, e ele usará o Python dentro do seu ambiente virtual com os pacotes que você instalou lá. + +
+ +```console +$ python main.py + +Hello World +``` + +
+ +## Configure seu editor + +Você provavelmente usaria um editor. Certifique-se de configurá-lo para usar o mesmo ambiente virtual que você criou (ele provavelmente o detectará automaticamente) para que você possa obter erros de preenchimento automático e em linha. + +Por exemplo: + +* VS Code +* PyCharm + +/// tip | "Dica" + +Normalmente, você só precisa fazer isso **uma vez**, ao criar o ambiente virtual. + +/// + +## Desativar o ambiente virtual + +Quando terminar de trabalhar no seu projeto, você pode **desativar** o ambiente virtual. + +
+ +```console +$ deactivate +``` + +
+ +Dessa forma, quando você executar `python`, ele não tentará executá-lo naquele ambiente virtual com os pacotes instalados nele. + +## Pronto para trabalhar + +Agora você está pronto para começar a trabalhar no seu projeto. + + + +/// tip | "Dica" + +Você quer entender o que é tudo isso acima? + +Continue lendo. 👇🤓 + +/// + +## Por que ambientes virtuais + +Para trabalhar com o FastAPI, você precisa instalar o Python. + +Depois disso, você precisará **instalar** o FastAPI e quaisquer outros **pacotes** que queira usar. + +Para instalar pacotes, você normalmente usaria o comando `pip` que vem com o Python (ou alternativas semelhantes). + +No entanto, se você usar `pip` diretamente, os pacotes serão instalados no seu **ambiente Python global** (a instalação global do Python). + +### O Problema + +Então, qual é o problema em instalar pacotes no ambiente global do Python? + +Em algum momento, você provavelmente acabará escrevendo muitos programas diferentes que dependem de **pacotes diferentes**. E alguns desses projetos em que você trabalha dependerão de **versões diferentes** do mesmo pacote. 😱 + +Por exemplo, você pode criar um projeto chamado `philosophers-stone`, este programa depende de outro pacote chamado **`harry`, usando a versão `1`**. Então, você precisa instalar `harry`. + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` + +Então, em algum momento depois, você cria outro projeto chamado `prisoner-of-azkaban`, e esse projeto também depende de `harry`, mas esse projeto precisa do **`harry` versão `3`**. + +```mermaid +flowchart LR + azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] +``` + +Mas agora o problema é que, se você instalar os pacotes globalmente (no ambiente global) em vez de em um **ambiente virtual** local, você terá que escolher qual versão do `harry` instalar. + +Se você quiser executar `philosophers-stone`, precisará primeiro instalar `harry` versão `1`, por exemplo com: + +
+ +```console +$ pip install "harry==1" +``` + +
+ +E então você acabaria com `harry` versão `1` instalado em seu ambiente Python global. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -->|requires| harry-1 + end +``` + +Mas se você quiser executar `prisoner-of-azkaban`, você precisará desinstalar `harry` versão `1` e instalar `harry` versão `3` (ou apenas instalar a versão `3` desinstalaria automaticamente a versão `1`). + +
+ +```console +$ pip install "harry==3" +``` + +
+ +E então você acabaria com `harry` versão `3` instalado em seu ambiente Python global. + +E se você tentar executar `philosophers-stone` novamente, há uma chance de que **não funcione** porque ele precisa de `harry` versão `1`. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + style harry-1 fill:#ccc,stroke-dasharray: 5 5 + harry-3[harry v3] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -.-x|⛔️| harry-1 + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --> |requires| harry-3 + end +``` + +/// tip | "Dica" + +É muito comum em pacotes Python tentar ao máximo **evitar alterações drásticas** em **novas versões**, mas é melhor prevenir do que remediar e instalar versões mais recentes intencionalmente e, quando possível, executar os testes para verificar se tudo está funcionando corretamente. + +/// + +Agora, imagine isso com **muitos** outros **pacotes** dos quais todos os seus **projetos dependem**. Isso é muito difícil de gerenciar. E você provavelmente acabaria executando alguns projetos com algumas **versões incompatíveis** dos pacotes, e não saberia por que algo não está funcionando. + +Além disso, dependendo do seu sistema operacional (por exemplo, Linux, Windows, macOS), ele pode ter vindo com o Python já instalado. E, nesse caso, provavelmente tinha alguns pacotes pré-instalados com algumas versões específicas **necessárias para o seu sistema**. Se você instalar pacotes no ambiente global do Python, poderá acabar **quebrando** alguns dos programas que vieram com seu sistema operacional. + +## Onde os pacotes são instalados + +Quando você instala o Python, ele cria alguns diretórios com alguns arquivos no seu computador. + +Alguns desses diretórios são os responsáveis ​​por ter todos os pacotes que você instala. + +Quando você executa: + +
+ +```console +// Não execute isso agora, é apenas um exemplo 🤓 +$ pip install "fastapi[standard]" +---> 100% +``` + +
+ +Isso fará o download de um arquivo compactado com o código FastAPI, normalmente do PyPI. + +Ele também fará o **download** de arquivos para outros pacotes dos quais o FastAPI depende. + +Em seguida, ele **extrairá** todos esses arquivos e os colocará em um diretório no seu computador. + +Por padrão, ele colocará os arquivos baixados e extraídos no diretório que vem com a instalação do Python, que é o **ambiente global**. + +## O que são ambientes virtuais + +A solução para os problemas de ter todos os pacotes no ambiente global é usar um **ambiente virtual para cada projeto** em que você trabalha. + +Um ambiente virtual é um **diretório**, muito semelhante ao global, onde você pode instalar os pacotes para um projeto. + +Dessa forma, cada projeto terá seu próprio ambiente virtual (diretório `.venv`) com seus próprios pacotes. + +```mermaid +flowchart TB + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) --->|requires| harry-1 + subgraph venv1[.venv] + harry-1[harry v1] + end + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --->|requires| harry-3 + subgraph venv2[.venv] + harry-3[harry v3] + end + end + stone-project ~~~ azkaban-project +``` + +## O que significa ativar um ambiente virtual + +Quando você ativa um ambiente virtual, por exemplo com: + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Ou se você usa o Bash para Windows (por exemplo, Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +Esse comando criará ou modificará algumas [variáveis ​​de ambiente](environment-variables.md){.internal-link target=_blank} que estarão disponíveis para os próximos comandos. + +Uma dessas variáveis ​​é a variável `PATH`. + +/// tip | "Dica" + +Você pode aprender mais sobre a variável de ambiente `PATH` na seção [Variáveis ​​de ambiente](environment-variables.md#path-environment-variable){.internal-link target=_blank}. + +/// + +A ativação de um ambiente virtual adiciona seu caminho `.venv/bin` (no Linux e macOS) ou `.venv\Scripts` (no Windows) à variável de ambiente `PATH`. + +Digamos que antes de ativar o ambiente, a variável `PATH` estava assim: + +//// tab | Linux, macOS + +```plaintext +/usr/bin:/bin:/usr/sbin:/sbin +``` + +Isso significa que o sistema procuraria programas em: + +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Windows\System32 +``` + +Isso significa que o sistema procuraria programas em: + +* `C:\Windows\System32` + +//// + +Após ativar o ambiente virtual, a variável `PATH` ficaria mais ou menos assim: + +//// tab | Linux, macOS + +```plaintext +/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +Isso significa que o sistema agora começará a procurar primeiro por programas em: + +```plaintext +/home/user/code/awesome-project/.venv/bin +``` + +antes de procurar nos outros diretórios. + +Então, quando você digita `python` no terminal, o sistema encontrará o programa Python em + +```plaintext +/home/user/code/awesome-project/.venv/bin/python +``` + +e usa esse. + +//// + +//// tab | Windows + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 +``` + +Isso significa que o sistema agora começará a procurar primeiro por programas em: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts +``` + +antes de procurar nos outros diretórios. + +Então, quando você digita `python` no terminal, o sistema encontrará o programa Python em + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +e usa esse. + +//// + +Um detalhe importante é que ele colocará o caminho do ambiente virtual no **início** da variável `PATH`. O sistema o encontrará **antes** de encontrar qualquer outro Python disponível. Dessa forma, quando você executar `python`, ele usará o Python **do ambiente virtual** em vez de qualquer outro `python` (por exemplo, um `python` de um ambiente global). + +Ativar um ambiente virtual também muda algumas outras coisas, mas esta é uma das mais importantes. + +## Verificando um ambiente virtual + +Ao verificar se um ambiente virtual está ativo, por exemplo com: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +//// + +Isso significa que o programa `python` que será usado é aquele **no ambiente virtual**. + +você usa `which` no Linux e macOS e `Get-Command` no Windows PowerShell. + +A maneira como esse comando funciona é que ele vai e verifica na variável de ambiente `PATH`, passando por **cada caminho em ordem**, procurando pelo programa chamado `python`. Uma vez que ele o encontre, ele **mostrará o caminho** para esse programa. + +A parte mais importante é que quando você chama ``python`, esse é exatamente o "`python`" que será executado. + +Assim, você pode confirmar se está no ambiente virtual correto. + +/// tip | "Dica" + +É fácil ativar um ambiente virtual, obter um Python e então **ir para outro projeto**. + +E o segundo projeto **não funcionaria** porque você está usando o **Python incorreto**, de um ambiente virtual para outro projeto. + +É útil poder verificar qual `python` está sendo usado. 🤓 + +/// + +## Por que desativar um ambiente virtual + +Por exemplo, você pode estar trabalhando em um projeto `philosophers-stone`, **ativar esse ambiente virtual**, instalar pacotes e trabalhar com esse ambiente. + +E então você quer trabalhar em **outro projeto** `prisoner-of-azkaban`. + +Você vai para aquele projeto: + +
+ +```console +$ cd ~/code/prisoner-of-azkaban +``` + +
+ +Se você não desativar o ambiente virtual para `philosophers-stone`, quando você executar `python` no terminal, ele tentará usar o Python de `philosophers-stone`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +$ python main.py + +// Erro ao importar o Sirius, ele não está instalado 😱 +Traceback (most recent call last): + File "main.py", line 1, in + import sirius +``` + +
+ +Mas se você desativar o ambiente virtual e ativar o novo para `prisoner-of-askaban`, quando você executar `python`, ele usará o Python do ambiente virtual em `prisoner-of-azkaban`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +// Você não precisa estar no diretório antigo para desativar, você pode fazer isso de onde estiver, mesmo depois de ir para o outro projeto 😎 +$ deactivate + +// Ative o ambiente virtual em prisoner-of-azkaban/.venv 🚀 +$ source .venv/bin/activate + +// Agora, quando você executar o python, ele encontrará o pacote sirius instalado neste ambiente virtual ✨ +$ python main.py + +Eu juro solenemente 🐺 +``` + +
+ +## Alternativas + +Este é um guia simples para você começar e lhe ensinar como tudo funciona **por baixo**. + +Existem muitas **alternativas** para gerenciar ambientes virtuais, dependências de pacotes (requisitos) e projetos. + +Quando estiver pronto e quiser usar uma ferramenta para **gerenciar todo o projeto**, dependências de pacotes, ambientes virtuais, etc., sugiro que você experimente o uv. + +`uv` pode fazer muitas coisas, ele pode: + +* **Instalar o Python** para você, incluindo versões diferentes +* Gerenciar o **ambiente virtual** para seus projetos +* Instalar **pacotes** +* Gerenciar **dependências e versões** de pacotes para seu projeto +* Certifique-se de ter um conjunto **exato** de pacotes e versões para instalar, incluindo suas dependências, para que você possa ter certeza de que pode executar seu projeto em produção exatamente da mesma forma que em seu computador durante o desenvolvimento, isso é chamado de **bloqueio** +* E muitas outras coisas + +## Conclusão + +Se você leu e entendeu tudo isso, agora **você sabe muito mais** sobre ambientes virtuais do que muitos desenvolvedores por aí. 🤓 + +Saber esses detalhes provavelmente será útil no futuro, quando você estiver depurando algo que parece complexo, mas você saberá **como tudo funciona**. 😎 From eb45bade63972dec674b83524e010e19ebdcd457 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Sep 2024 10:37:36 +0000 Subject: [PATCH 78/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7492242a43979..114841f2d66c5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/testing.md`. PR [#12164](https://github.com/fastapi/fastapi/pull/12164) by [@marcelomarkus](https://github.com/marcelomarkus). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/debugging.md`. PR [#12165](https://github.com/fastapi/fastapi/pull/12165) by [@marcelomarkus](https://github.com/marcelomarkus). * 🌐 Add Korean translation for `docs/ko/docs/project-generation.md`. PR [#12157](https://github.com/fastapi/fastapi/pull/12157) by [@BORA040126](https://github.com/BORA040126). From a4c5f7f62fbb2fbfc3daefd3ddcefa8b65e103d8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Sep 2024 10:38:58 +0000 Subject: [PATCH 79/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 114841f2d66c5..11289cfe8ac0d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Add Portuguese translation for `docs/pt/docs/environment-variables.md`. PR [#12162](https://github.com/fastapi/fastapi/pull/12162) by [@marcelomarkus](https://github.com/marcelomarkus). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/testing.md`. PR [#12164](https://github.com/fastapi/fastapi/pull/12164) by [@marcelomarkus](https://github.com/marcelomarkus). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/debugging.md`. PR [#12165](https://github.com/fastapi/fastapi/pull/12165) by [@marcelomarkus](https://github.com/marcelomarkus). * 🌐 Add Korean translation for `docs/ko/docs/project-generation.md`. PR [#12157](https://github.com/fastapi/fastapi/pull/12157) by [@BORA040126](https://github.com/BORA040126). From 74451189f6f243833674fc22a1fe57dfb21f9831 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Sep 2024 10:40:52 +0000 Subject: [PATCH 80/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 11289cfe8ac0d..a72775416d4fc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Translations +* 🌐 Add Portuguese translation for `docs/pt/docs/virtual-environments.md`. PR [#12163](https://github.com/fastapi/fastapi/pull/12163) by [@marcelomarkus](https://github.com/marcelomarkus). * 🌐 Add Portuguese translation for `docs/pt/docs/environment-variables.md`. PR [#12162](https://github.com/fastapi/fastapi/pull/12162) by [@marcelomarkus](https://github.com/marcelomarkus). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/testing.md`. PR [#12164](https://github.com/fastapi/fastapi/pull/12164) by [@marcelomarkus](https://github.com/marcelomarkus). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/debugging.md`. PR [#12165](https://github.com/fastapi/fastapi/pull/12165) by [@marcelomarkus](https://github.com/marcelomarkus). From b0eedbb5804a6ac32e4ee8d029d462d950ff8848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 11 Sep 2024 09:45:30 +0200 Subject: [PATCH 81/89] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Improve=20performanc?= =?UTF-8?q?e=20in=20request=20body=20parsing=20with=20a=20cache=20for=20in?= =?UTF-8?q?ternal=20model=20fields=20(#12184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat.py | 6 ++++++ fastapi/dependencies/utils.py | 4 ++-- tests/test_compat.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index f940d65973223..4b07b44fa5829 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -2,6 +2,7 @@ from copy import copy from dataclasses import dataclass, is_dataclass from enum import Enum +from functools import lru_cache from typing import ( Any, Callable, @@ -649,3 +650,8 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) for sub_annotation in get_args(annotation) ) + + +@lru_cache +def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return get_model_fields(model) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 6083b73195fc5..f18eace9d430e 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -32,8 +32,8 @@ evaluate_forwardref, field_annotation_is_scalar, get_annotation_from_field_info, + get_cached_model_fields, get_missing_field_error, - get_model_fields, is_bytes_field, is_bytes_sequence_field, is_scalar_field, @@ -810,7 +810,7 @@ async def request_body_to_args( fields_to_extract: List[ModelField] = body_fields if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel): - fields_to_extract = get_model_fields(first_field.type_) + fields_to_extract = get_cached_model_fields(first_field.type_) if isinstance(received_body, FormData): body_to_process = await _extract_form_body(fields_to_extract, received_body) diff --git a/tests/test_compat.py b/tests/test_compat.py index 270475bf3a42a..f4a3093c5ee4f 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -5,6 +5,7 @@ ModelField, Undefined, _get_model_config, + get_cached_model_fields, get_model_fields, is_bytes_sequence_annotation, is_scalar_field, @@ -102,3 +103,18 @@ class Model(BaseModel): fields = get_model_fields(Model) assert not is_scalar_field(fields[0]) + + +def test_get_model_fields_cached(): + class Model(BaseModel): + foo: str + + non_cached_fields = get_model_fields(Model) + non_cached_fields2 = get_model_fields(Model) + cached_fields = get_cached_model_fields(Model) + cached_fields2 = get_cached_model_fields(Model) + for f1, f2 in zip(cached_fields, cached_fields2): + assert f1 is f2 + + assert non_cached_fields is not non_cached_fields2 + assert cached_fields is cached_fields2 From 8dc882f75121414eb44db590efae83fbddf43f72 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 11 Sep 2024 07:45:49 +0000 Subject: [PATCH 82/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a72775416d4fc..647b51b1979ab 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ⚡️ Improve performance in request body parsing with a cache for internal model fields. PR [#12184](https://github.com/fastapi/fastapi/pull/12184) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Remove duplicate line in docs for `docs/en/docs/environment-variables.md`. PR [#12169](https://github.com/fastapi/fastapi/pull/12169) by [@prometek](https://github.com/prometek). From 212fd5e247279073dceaba346fd4afc52f627232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 11 Sep 2024 09:46:34 +0200 Subject: [PATCH 83/89] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.114.?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 647b51b1979ab..97f472815245d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.114.1 + ### Refactors * ⚡️ Improve performance in request body parsing with a cache for internal model fields. PR [#12184](https://github.com/fastapi/fastapi/pull/12184) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index dce17360f9397..c2ed4859a77bb 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.114.0" +__version__ = "0.114.1" from starlette import status as status From 24b8f2668beb773895a93040a2ae284898dc58b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 12 Sep 2024 00:49:55 +0200 Subject: [PATCH 84/89] =?UTF-8?q?=E2=9E=95=20Add=20inline-snapshot=20for?= =?UTF-8?q?=20tests=20(#12189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++++ requirements-tests.txt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bb87be470e375..1be2817a1f32f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -241,3 +241,7 @@ known-third-party = ["fastapi", "pydantic", "starlette"] [tool.ruff.lint.pyupgrade] # Preserve types, even if a file imports `from __future__ import annotations`. keep-runtime-typing = true + +[tool.inline-snapshot] +# default-flags=["fix"] +# default-flags=["create"] diff --git a/requirements-tests.txt b/requirements-tests.txt index 809a19c0ceff9..2f2576dd503e1 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -14,7 +14,7 @@ anyio[trio] >=3.2.1,<4.0.0 PyJWT==2.8.0 pyyaml >=5.3.1,<7.0.0 passlib[bcrypt] >=1.7.2,<2.0.0 - +inline-snapshot==0.13.0 # types types-ujson ==5.7.0.1 types-orjson ==3.6.2 From ba0bb6212e553e779f75f973d07a4db112b43cf0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 11 Sep 2024 22:50:18 +0000 Subject: [PATCH 85/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 97f472815245d..01c9fb22510b6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* ➕ Add inline-snapshot for tests. PR [#12189](https://github.com/fastapi/fastapi/pull/12189) by [@tiangolo](https://github.com/tiangolo). + ## 0.114.1 ### Refactors From c8e644d19e688e00e51cdca2c1bb15d274d70801 Mon Sep 17 00:00:00 2001 From: Max Scheijen <47034840+maxscheijen@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:00:36 +0200 Subject: [PATCH 86/89] =?UTF-8?q?=F0=9F=8C=90=20Add=20Dutch=20translation?= =?UTF-8?q?=20for=20`docs/nl/docs/python-types.md`=20(#12158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/nl/docs/python-types.md | 597 +++++++++++++++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 docs/nl/docs/python-types.md diff --git a/docs/nl/docs/python-types.md b/docs/nl/docs/python-types.md new file mode 100644 index 0000000000000..a5562b7950a04 --- /dev/null +++ b/docs/nl/docs/python-types.md @@ -0,0 +1,597 @@ +# Introductie tot Python Types + +Python biedt ondersteuning voor optionele "type hints" (ook wel "type annotaties" genoemd). + +Deze **"type hints"** of annotaties zijn een speciale syntax waarmee het type van een variabele kan worden gedeclareerd. + +Door types voor je variabelen te declareren, kunnen editors en hulpmiddelen je beter ondersteunen. + +Dit is slechts een **korte tutorial/opfrisser** over Python type hints. Het behandelt enkel het minimum dat nodig is om ze te gebruiken met **FastAPI**... en dat is relatief weinig. + +**FastAPI** is helemaal gebaseerd op deze type hints, ze geven veel voordelen. + +Maar zelfs als je **FastAPI** nooit gebruikt, heb je er baat bij om er iets over te leren. + +/// note + +Als je een Python expert bent en alles al weet over type hints, sla dan dit hoofdstuk over. + +/// + +## Motivatie + +Laten we beginnen met een eenvoudig voorbeeld: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +Het aanroepen van dit programma leidt tot het volgende resultaat: + +``` +John Doe +``` + +De functie voert het volgende uit: + +* Neem een `first_name` en een `last_name` +* Converteer de eerste letter van elk naar een hoofdletter met `title()`. +`` +* Voeg samen met een spatie in het midden. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### Bewerk het + +Dit is een heel eenvoudig programma. + +Maar stel je nu voor dat je het vanaf nul zou moeten maken. + +Op een gegeven moment zou je aan de definitie van de functie zijn begonnen, je had de parameters klaar... + +Maar dan moet je “die methode die de eerste letter naar hoofdletters converteert” aanroepen. + +Was het `upper`? Was het `uppercase`? `first_uppercase`? `capitalize`? + +Dan roep je de hulp in van je oude programmeursvriend, (automatische) code aanvulling in je editor. + +Je typt de eerste parameter van de functie, `first_name`, dan een punt (`.`) en drukt dan op `Ctrl+Spatie` om de aanvulling te activeren. + +Maar helaas krijg je niets bruikbaars: + + + +### Types toevoegen + +Laten we een enkele regel uit de vorige versie aanpassen. + +We zullen precies dit fragment, de parameters van de functie, wijzigen van: + +```Python + first_name, last_name +``` + +naar: + +```Python + first_name: str, last_name: str +``` + +Dat is alles. + +Dat zijn de "type hints": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +Dit is niet hetzelfde als het declareren van standaardwaarden zoals bij: + +```Python + first_name="john", last_name="doe" +``` + +Het is iets anders. + +We gebruiken dubbele punten (`:`), geen gelijkheidstekens (`=`). + +Het toevoegen van type hints verandert normaal gesproken niet wat er gebeurt in je programma t.o.v. wat er zonder type hints zou gebeuren. + +Maar stel je voor dat je weer bezig bent met het maken van een functie, maar deze keer met type hints. + +Op hetzelfde moment probeer je de automatische aanvulling te activeren met `Ctrl+Spatie` en je ziet: + + + +Nu kun je de opties bekijken en er doorheen scrollen totdat je de optie vindt die “een belletje doet rinkelen”: + + + +### Meer motivatie + +Bekijk deze functie, deze heeft al type hints: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +Omdat de editor de types van de variabelen kent, krijgt u niet alleen aanvulling, maar ook controles op fouten: + + + +Nu weet je hoe je het moet oplossen, converteer `age` naar een string met `str(age)`: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## Types declareren + +Je hebt net de belangrijkste plek om type hints te declareren gezien. Namelijk als functieparameters. + +Dit is ook de belangrijkste plek waar je ze gebruikt met **FastAPI**. + +### Eenvoudige types + +Je kunt alle standaard Python types declareren, niet alleen `str`. + +Je kunt bijvoorbeeld het volgende gebruiken: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### Generieke types met typeparameters + +Er zijn enkele datastructuren die andere waarden kunnen bevatten, zoals `dict`, `list`, `set` en `tuple` en waar ook de interne waarden hun eigen type kunnen hebben. + +Deze types die interne types hebben worden “**generieke**” types genoemd. Het is mogelijk om ze te declareren, zelfs met hun interne types. + +Om deze types en de interne types te declareren, kun je de standaard Python module `typing` gebruiken. Deze module is speciaal gemaakt om deze type hints te ondersteunen. + +#### Nieuwere versies van Python + +De syntax met `typing` is **verenigbaar** met alle versies, van Python 3.6 tot aan de nieuwste, inclusief Python 3.9, Python 3.10, enz. + +Naarmate Python zich ontwikkelt, worden **nieuwere versies**, met verbeterde ondersteuning voor deze type annotaties, beschikbaar. In veel gevallen hoef je niet eens de `typing` module te importeren en te gebruiken om de type annotaties te declareren. + +Als je een recentere versie van Python kunt kiezen voor je project, kun je profiteren van die extra eenvoud. + +In alle documentatie staan voorbeelden die compatibel zijn met elke versie van Python (als er een verschil is). + +Bijvoorbeeld “**Python 3.6+**” betekent dat het compatibel is met Python 3.6 of hoger (inclusief 3.7, 3.8, 3.9, 3.10, etc). En “**Python 3.9+**” betekent dat het compatibel is met Python 3.9 of hoger (inclusief 3.10, etc). + +Als je de **laatste versies van Python** kunt gebruiken, gebruik dan de voorbeelden voor de laatste versie, die hebben de **beste en eenvoudigste syntax**, bijvoorbeeld “**Python 3.10+**”. + +#### List + +Laten we bijvoorbeeld een variabele definiëren als een `list` van `str`. + +//// tab | Python 3.9+ + +Declareer de variabele met dezelfde dubbele punt (`:`) syntax. + +Als type, vul `list` in. + +Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes: + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial006_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +Van `typing`, importeer `List` (met een hoofdletter `L`): + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial006.py!} +``` + +Declareer de variabele met dezelfde dubbele punt (`:`) syntax. + +Zet als type de `List` die je hebt geïmporteerd uit `typing`. + +Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes: + +```Python hl_lines="4" +{!> ../../../docs_src/python_types/tutorial006.py!} +``` + +//// + +/// info + +De interne types tussen vierkante haakjes worden “typeparameters” genoemd. + +In dit geval is `str` de typeparameter die wordt doorgegeven aan `List` (of `list` in Python 3.9 en hoger). + +/// + +Dat betekent: “de variabele `items` is een `list`, en elk van de items in deze list is een `str`”. + +/// tip + +Als je Python 3.9 of hoger gebruikt, hoef je `List` niet te importeren uit `typing`, je kunt in plaats daarvan hetzelfde reguliere `list` type gebruiken. + +/// + +Door dat te doen, kan je editor ondersteuning bieden, zelfs tijdens het verwerken van items uit de list: + + + +Zonder types is dat bijna onmogelijk om te bereiken. + +Merk op dat de variabele `item` een van de elementen is in de lijst `items`. + +Toch weet de editor dat het een `str` is, en biedt daar vervolgens ondersteuning voor aan. + +#### Tuple en Set + +Je kunt hetzelfde doen om `tuple`s en `set`s te declareren: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial007_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial007.py!} +``` + +//// + +Dit betekent: + +* De variabele `items_t` is een `tuple` met 3 items, een `int`, nog een `int`, en een `str`. +* De variabele `items_s` is een `set`, en elk van de items is van het type `bytes`. + +#### Dict + +Om een `dict` te definiëren, geef je 2 typeparameters door, gescheiden door komma's. + +De eerste typeparameter is voor de sleutels (keys) van de `dict`. + +De tweede typeparameter is voor de waarden (values) van het `dict`: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial008_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial008.py!} +``` + +//// + +Dit betekent: + +* De variabele `prices` is een `dict`: + * De sleutels van dit `dict` zijn van het type `str` (bijvoorbeeld de naam van elk item). + * De waarden van dit `dict` zijn van het type `float` (bijvoorbeeld de prijs van elk item). + +#### Union + +Je kunt een variable declareren die van **verschillende types** kan zijn, bijvoorbeeld een `int` of een `str`. + +In Python 3.6 en hoger (inclusief Python 3.10) kun je het `Union`-type van `typing` gebruiken en de mogelijke types die je wilt accepteren, tussen de vierkante haakjes zetten. + +In Python 3.10 is er ook een **nieuwe syntax** waarin je de mogelijke types kunt scheiden door een verticale balk (`|`). + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial008b_py310.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial008b.py!} +``` + +//// + +In beide gevallen betekent dit dat `item` een `int` of een `str` kan zijn. + +#### Mogelijk `None` + +Je kunt declareren dat een waarde een type kan hebben, zoals `str`, maar dat het ook `None` kan zijn. + +In Python 3.6 en hoger (inclusief Python 3.10) kun je het declareren door `Optional` te importeren en te gebruiken vanuit de `typing`-module. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +Door `Optional[str]` te gebruiken in plaats van alleen `str`, kan de editor je helpen fouten te detecteren waarbij je ervan uit zou kunnen gaan dat een waarde altijd een `str` is, terwijl het in werkelijkheid ook `None` zou kunnen zijn. + +`Optional[EenType]` is eigenlijk een snelkoppeling voor `Union[EenType, None]`, ze zijn equivalent. + +Dit betekent ook dat je in Python 3.10 `EenType | None` kunt gebruiken: + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial009.py!} +``` + +//// + +//// tab | Python 3.8+ alternative + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial009b.py!} +``` + +//// + +#### Gebruik van `Union` of `Optional` + +Als je een Python versie lager dan 3.10 gebruikt, is dit een tip vanuit mijn **subjectieve** standpunt: + +* 🚨 Vermijd het gebruik van `Optional[EenType]`. +* Gebruik in plaats daarvan **`Union[EenType, None]`** ✨. + +Beide zijn gelijkwaardig en onderliggend zijn ze hetzelfde, maar ik zou `Union` aanraden in plaats van `Optional` omdat het woord “**optional**” lijkt te impliceren dat de waarde optioneel is, en het eigenlijk betekent “het kan `None` zijn”, zelfs als het niet optioneel is en nog steeds vereist is. + +Ik denk dat `Union[SomeType, None]` explicieter is over wat het betekent. + +Het gaat alleen om de woorden en naamgeving. Maar die naamgeving kan invloed hebben op hoe jij en je teamgenoten over de code denken. + +Laten we als voorbeeld deze functie nemen: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c.py!} +``` + +De parameter `name` is gedefinieerd als `Optional[str]`, maar is **niet optioneel**, je kunt de functie niet aanroepen zonder de parameter: + +```Python +say_hi() # Oh, nee, dit geeft een foutmelding! 😱 +``` + +De `name` parameter is **nog steeds vereist** (niet *optioneel*) omdat het geen standaardwaarde heeft. Toch accepteert `name` `None` als waarde: + +```Python +say_hi(name=None) # Dit werkt, None is geldig 🎉 +``` + +Het goede nieuws is dat als je eenmaal Python 3.10 gebruikt, je je daar geen zorgen meer over hoeft te maken, omdat je dan gewoon `|` kunt gebruiken om unions van types te definiëren: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c_py310.py!} +``` + +Dan hoef je je geen zorgen te maken over namen als `Optional` en `Union`. 😎 + +#### Generieke typen + +De types die typeparameters in vierkante haakjes gebruiken, worden **Generieke types** of **Generics** genoemd, bijvoorbeeld: + +//// tab | Python 3.10+ + +Je kunt dezelfde ingebouwde types gebruiken als generics (met vierkante haakjes en types erin): + +* `list` +* `tuple` +* `set` +* `dict` + +Hetzelfde als bij Python 3.8, uit de `typing`-module: + +* `Union` +* `Optional` (hetzelfde als bij Python 3.8) +* ...en anderen. + +In Python 3.10 kun je , als alternatief voor de generieke `Union` en `Optional`, de verticale lijn (`|`) gebruiken om unions van typen te voorzien, dat is veel beter en eenvoudiger. + +//// + +//// tab | Python 3.9+ + +Je kunt dezelfde ingebouwde types gebruiken als generieke types (met vierkante haakjes en types erin): + +* `list` +* `tuple` +* `set` +* `dict` + +En hetzelfde als met Python 3.8, vanuit de `typing`-module: + +* `Union` +* `Optional` +* ...en anderen. + +//// + +//// tab | Python 3.8+ + +* `List` +* `Tuple` +* `Set` +* `Dict` +* `Union` +* `Optional` +* ...en anderen. + +//// + +### Klassen als types + +Je kunt een klasse ook declareren als het type van een variabele. + +Stel dat je een klasse `Person` hebt, met een naam: + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Vervolgens kun je een variabele van het type `Persoon` declareren: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Dan krijg je ook nog eens volledige editorondersteuning: + + + +Merk op dat dit betekent dat "`one_person` een **instantie** is van de klasse `Person`". + +Dit betekent niet dat `one_person` de **klasse** is met de naam `Person`. + +## Pydantic modellen + +Pydantic is een Python-pakket voor het uitvoeren van datavalidatie. + +Je declareert de "vorm" van de data als klassen met attributen. + +Elk attribuut heeft een type. + +Vervolgens maak je een instantie van die klasse met een aantal waarden en het valideert de waarden, converteert ze naar het juiste type (als dat het geval is) en geeft je een object met alle data terug. + +Daarnaast krijg je volledige editorondersteuning met dat resulterende object. + +Een voorbeeld uit de officiële Pydantic-documentatie: + +//// tab | Python 3.10+ + +```Python +{!> ../../../docs_src/python_types/tutorial011_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!> ../../../docs_src/python_types/tutorial011_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python +{!> ../../../docs_src/python_types/tutorial011.py!} +``` + +//// + +/// info + +Om meer te leren over Pydantic, bekijk de documentatie. + +/// + +**FastAPI** is volledig gebaseerd op Pydantic. + +Je zult veel meer van dit alles in de praktijk zien in de [Tutorial - Gebruikershandleiding](tutorial/index.md){.internal-link target=_blank}. + +/// tip + +Pydantic heeft een speciaal gedrag wanneer je `Optional` of `Union[EenType, None]` gebruikt zonder een standaardwaarde, je kunt er meer over lezen in de Pydantic-documentatie over Verplichte optionele velden. + +/// + +## Type Hints met Metadata Annotaties + +Python heeft ook een functie waarmee je **extra metadata** in deze type hints kunt toevoegen met behulp van `Annotated`. + +//// tab | Python 3.9+ + +In Python 3.9 is `Annotated` onderdeel van de standaardpakket, dus je kunt het importeren vanuit `typing`. + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial013_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +In versies lager dan Python 3.9 importeer je `Annotated` vanuit `typing_extensions`. + +Het wordt al geïnstalleerd met **FastAPI**. + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial013.py!} +``` + +//// + +Python zelf doet niets met deze `Annotated` en voor editors en andere hulpmiddelen is het type nog steeds een `str`. + +Maar je kunt deze ruimte in `Annotated` gebruiken om **FastAPI** te voorzien van extra metadata over hoe je wilt dat je applicatie zich gedraagt. + +Het belangrijkste om te onthouden is dat **de eerste *typeparameter*** die je doorgeeft aan `Annotated` het **werkelijke type** is. De rest is gewoon metadata voor andere hulpmiddelen. + +Voor nu hoef je alleen te weten dat `Annotated` bestaat en dat het standaard Python is. 😎 + +Later zul je zien hoe **krachtig** het kan zijn. + +/// tip + +Het feit dat dit **standaard Python** is, betekent dat je nog steeds de **best mogelijke ontwikkelaarservaring** krijgt in je editor, met de hulpmiddelen die je gebruikt om je code te analyseren en te refactoren, enz. ✨ + +Daarnaast betekent het ook dat je code zeer verenigbaar zal zijn met veel andere Python-hulpmiddelen en -pakketten. 🚀 + +/// + +## Type hints in **FastAPI** + +**FastAPI** maakt gebruik van type hints om verschillende dingen te doen. + +Met **FastAPI** declareer je parameters met type hints en krijg je: + +* **Editor ondersteuning**. +* **Type checks**. + +...en **FastAPI** gebruikt dezelfde declaraties om: + +* **Vereisten te definïeren **: van request pad parameters, query parameters, headers, bodies, dependencies, enz. +* **Data te converteren**: van de request naar het vereiste type. +* **Data te valideren**: afkomstig van elke request: + * **Automatische foutmeldingen** te genereren die naar de client worden geretourneerd wanneer de data ongeldig is. +* De API met OpenAPI te **documenteren**: + * die vervolgens wordt gebruikt door de automatische interactieve documentatie gebruikersinterfaces. + +Dit klinkt misschien allemaal abstract. Maak je geen zorgen. Je ziet dit allemaal in actie in de [Tutorial - Gebruikershandleiding](tutorial/index.md){.internal-link target=_blank}. + +Het belangrijkste is dat door standaard Python types te gebruiken, op één plek (in plaats van meer klassen, decorators, enz. toe te voegen), **FastAPI** een groot deel van het werk voor je doet. + +/// info + +Als je de hele tutorial al hebt doorgenomen en terug bent gekomen om meer te weten te komen over types, is een goede bron het "cheat sheet" van `mypy`. + +/// From 492943fdb1f726800ab7a42ead08e297813c8e68 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 Sep 2024 17:01:01 +0000 Subject: [PATCH 87/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 01c9fb22510b6..c3bf2bb8d5f33 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Add Dutch translation for `docs/nl/docs/python-types.md`. PR [#12158](https://github.com/fastapi/fastapi/pull/12158) by [@maxscheijen](https://github.com/maxscheijen). + ### Internal * ➕ Add inline-snapshot for tests. PR [#12189](https://github.com/fastapi/fastapi/pull/12189) by [@tiangolo](https://github.com/tiangolo). From 4a94fe3c8249e2c13999964ac9f707ab0ca069ee Mon Sep 17 00:00:00 2001 From: Waket Zheng Date: Fri, 13 Sep 2024 01:01:54 +0800 Subject: [PATCH 88/89] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translatio?= =?UTF-8?q?n=20for=20`docs/zh/docs/project-generation.md`=20(#12170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/project-generation.md | 112 ++++++++--------------------- 1 file changed, 28 insertions(+), 84 deletions(-) diff --git a/docs/zh/docs/project-generation.md b/docs/zh/docs/project-generation.md index 0655cb0a9cf57..9b37355398de7 100644 --- a/docs/zh/docs/project-generation.md +++ b/docs/zh/docs/project-generation.md @@ -1,84 +1,28 @@ -# 项目生成 - 模板 - -项目生成器一般都会提供很多初始设置、安全措施、数据库,甚至还准备好了第一个 API 端点,能帮助您快速上手。 - -项目生成器的设置通常都很主观,您可以按需更新或修改,但对于您的项目来说,它是非常好的起点。 - -## 全栈 FastAPI + PostgreSQL - -GitHub:https://github.com/tiangolo/full-stack-fastapi-postgresql - -### 全栈 FastAPI + PostgreSQL - 功能 - -* 完整的 **Docker** 集成(基于 Docker) -* Docker Swarm 开发模式 -* **Docker Compose** 本地开发集成与优化 -* **生产可用**的 Python 网络服务器,使用 Uvicorn 或 Gunicorn -* Python **FastAPI** 后端: -* * **速度快**:可与 **NodeJS** 和 **Go** 比肩的极高性能(归功于 Starlette 和 Pydantic) - * **直观**:强大的编辑器支持,处处皆可自动补全,减少调试时间 - * **简单**:易学、易用,阅读文档所需时间更短 - * **简短**:代码重复最小化,每次参数声明都可以实现多个功能 - * **健壮**: 生产级别的代码,还有自动交互文档 - * **基于标准**:完全兼容并基于 API 开放标准:OpenAPIJSON Schema - * **更多功能**包括自动验证、序列化、交互文档、OAuth2 JWT 令牌身份验证等 -* **安全密码**,默认使用密码哈希 -* **JWT 令牌**身份验证 -* **SQLAlchemy** 模型(独立于 Flask 扩展,可直接用于 Celery Worker) -* 基础的用户模型(可按需修改或删除) -* **Alembic** 迁移 -* **CORS**(跨域资源共享) -* **Celery** Worker 可从后端其它部分有选择地导入并使用模型和代码 -* REST 后端测试基于 Pytest,并与 Docker 集成,可独立于数据库实现完整的 API 交互测试。因为是在 Docker 中运行,每次都可从头构建新的数据存储(使用 ElasticSearch、MongoDB、CouchDB 等数据库,仅测试 API 运行) -* Python 与 **Jupyter Kernels** 集成,用于远程或 Docker 容器内部开发,使用 Atom Hydrogen 或 Visual Studio Code 的 Jupyter 插件 -* **Vue** 前端: - * 由 Vue CLI 生成 - * **JWT 身份验证**处理 - * 登录视图 - * 登录后显示主仪表盘视图 - * 主仪表盘支持用户创建与编辑 - * 用户信息编辑 - * **Vuex** - * **Vue-router** - * **Vuetify** 美化组件 - * **TypeScript** - * 基于 **Nginx** 的 Docker 服务器(优化了 Vue-router 配置) - * Docker 多阶段构建,无需保存或提交编译的代码 - * 在构建时运行前端测试(可禁用) - * 尽量模块化,开箱即用,但仍可使用 Vue CLI 重新生成或创建所需项目,或复用所需内容 -* 使用 **PGAdmin** 管理 PostgreSQL 数据库,可轻松替换为 PHPMyAdmin 或 MySQL -* 使用 **Flower** 监控 Celery 任务 -* 使用 **Traefik** 处理前后端负载平衡,可把前后端放在同一个域下,按路径分隔,但在不同容器中提供服务 -* Traefik 集成,包括自动生成 Let's Encrypt **HTTPS** 凭证 -* GitLab **CI**(持续集成),包括前后端测试 - -## 全栈 FastAPI + Couchbase - -GitHub:https://github.com/tiangolo/full-stack-fastapi-couchbase - -⚠️ **警告** ⚠️ - -如果您想从头开始创建新项目,建议使用以下备选方案。 - -例如,项目生成器全栈 FastAPI + PostgreSQL 会更适用,这个项目的维护积极,用的人也多,还包括了所有新功能和改进内容。 - -当然,您也可以放心使用这个基于 Couchbase 的生成器,它也能正常使用。就算用它生成项目也没有任何问题(为了更好地满足需求,您可以自行更新这个项目)。 - -详见资源仓库中的文档。 - -## 全栈 FastAPI + MongoDB - -……敬请期待,得看我有没有时间做这个项目。😅 🎉 - -## FastAPI + spaCy 机器学习模型 - -GitHub:https://github.com/microsoft/cookiecutter-spacy-fastapi - -### FastAPI + spaCy 机器学习模型 - 功能 - -* 集成 **spaCy** NER 模型 -* 内置 **Azure 认知搜索**请求格式 -* **生产可用**的 Python 网络服务器,使用 Uvicorn 与 Gunicorn -* 内置 **Azure DevOps** Kubernetes (AKS) CI/CD 开发 -* **多语**支持,可在项目设置时选择 spaCy 内置的语言 -* 不仅局限于 spaCy,可**轻松扩展**至其它模型框架(Pytorch、TensorFlow) +# FastAPI全栈模板 + +模板通常带有特定的设置,而且被设计为灵活和可定制的。这允许您根据项目的需求修改和调整它们,使它们成为一个很好的起点。🏁 + +您可以使用此模板开始,因为它包含了许多已经为您完成的初始设置、安全性、数据库和一些API端点。 + +代码仓: Full Stack FastAPI Template + +## FastAPI全栈模板 - 技术栈和特性 + +- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) 用于Python后端API. + - 🧰 [SQLModel](https://sqlmodel.tiangolo.com) 用于Python和SQL数据库的集成(ORM)。 + - 🔍 [Pydantic](https://docs.pydantic.dev) FastAPI的依赖项之一,用于数据验证和配置管理。 + - 💾 [PostgreSQL](https://www.postgresql.org) 作为SQL数据库。 +- 🚀 [React](https://react.dev) 用于前端。 + - 💃 使用了TypeScript、hooks、Vite和其他一些现代化的前端技术栈。 + - 🎨 [Chakra UI](https://chakra-ui.com) 用于前端组件。 + - 🤖 一个自动化生成的前端客户端。 + - 🧪 Playwright用于端到端测试。 + - 🦇 支持暗黑主题(Dark mode)。 +- 🐋 [Docker Compose](https://www.docker.com) 用于开发环境和生产环境。 +- 🔒 默认使用密码哈希来保证安全。 +- 🔑 JWT令牌用于权限验证。 +- 📫 使用邮箱来进行密码恢复。 +- ✅ 单元测试用了[Pytest](https://pytest.org). +- 📞 [Traefik](https://traefik.io) 用于反向代理和负载均衡。 +- 🚢 部署指南(Docker Compose)包含了如何起一个Traefik前端代理来自动化HTTPS认证。 +- 🏭 CI(持续集成)和 CD(持续部署)基于GitHub Actions。 From 93e50e373b0651c22a6743a4e907dafbadc8d27e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 Sep 2024 17:03:26 +0000 Subject: [PATCH 89/89] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c3bf2bb8d5f33..ac23987596510 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/project-generation.md`. PR [#12170](https://github.com/fastapi/fastapi/pull/12170) by [@waketzheng](https://github.com/waketzheng). * 🌐 Add Dutch translation for `docs/nl/docs/python-types.md`. PR [#12158](https://github.com/fastapi/fastapi/pull/12158) by [@maxscheijen](https://github.com/maxscheijen). ### Internal