diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/DISCUSSION_TEMPLATE/questions.yml
similarity index 87%
rename from .github/ISSUE_TEMPLATE/question.yml
rename to .github/DISCUSSION_TEMPLATE/questions.yml
index 3b16b4ad0..3726b7d18 100644
--- a/.github/ISSUE_TEMPLATE/question.yml
+++ b/.github/DISCUSSION_TEMPLATE/questions.yml
@@ -1,5 +1,3 @@
-name: Question or Problem
-description: Ask a question or ask about a problem
labels: [question]
body:
- type: markdown
@@ -9,9 +7,9 @@ body:
Please follow these instructions, fill every question, and do every step. 🙏
- I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time.
+ I'm asking this because answering questions and solving problems in GitHub is what consumes most of the time.
- I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues.
+ I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling questions.
All that, on top of all the incredible help provided by a bunch of community members, the [FastAPI Experts](https://fastapi.tiangolo.com/fastapi-people/#experts), that give a lot of their time to come here and help others.
@@ -21,16 +19,16 @@ body:
And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎
- As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓
+ As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓
- type: checkboxes
id: checks
attributes:
label: First Check
description: Please confirm and check all the following options.
options:
- - label: I added a very descriptive title to this issue.
+ - label: I added a very descriptive title here.
required: true
- - label: I used the GitHub search to find a similar issue and didn't find it.
+ - label: I used the GitHub search to find a similar question and didn't find it.
required: true
- label: I searched the FastAPI documentation, with the integrated search.
required: true
@@ -38,7 +36,7 @@ body:
required: true
- label: I already read and followed all the tutorial in the docs and didn't find an answer.
required: true
- - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic).
+ - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/pydantic/pydantic).
required: true
- label: I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui).
required: true
@@ -51,9 +49,9 @@ body:
description: |
After submitting this, I commit to one of:
- * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there.
+ * Read open questions until I find 2 where I can help someone and add a comment to help there.
* I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.
- * Implement a Pull Request for a confirmed bug.
+ * Review one Pull Request by downloading the code and following [all the review process](https://fastapi.tiangolo.com/help-fastapi/#review-pull-requests).
options:
- label: I commit to help with one of those options 👆
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 55749398f..a8f4c4de2 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -2,3 +2,15 @@ blank_issues_enabled: false
contact_links:
- name: Security Contact
about: Please report security vulnerabilities to security@tiangolo.com
+ - name: Question or Problem
+ about: Ask a question or ask about a problem in GitHub Discussions.
+ url: https://github.com/tiangolo/fastapi/discussions/categories/questions
+ - name: Feature Request
+ about: To suggest an idea or ask about a feature, please start with a question saying what you would like to achieve. There might be a way to do it already.
+ url: https://github.com/tiangolo/fastapi/discussions/categories/questions
+ - name: Show and tell
+ about: Show what you built with FastAPI or to be used with FastAPI.
+ url: https://github.com/tiangolo/fastapi/discussions/categories/show-and-tell
+ - name: Translations
+ about: Coordinate translations in GitHub Discussions.
+ url: https://github.com/tiangolo/fastapi/discussions/categories/translations
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
deleted file mode 100644
index 322b6536a..000000000
--- a/.github/ISSUE_TEMPLATE/feature-request.yml
+++ /dev/null
@@ -1,181 +0,0 @@
-name: Feature Request
-description: Suggest an idea or ask for a feature that you would like to have in FastAPI
-labels: [enhancement]
-body:
- - type: markdown
- attributes:
- value: |
- Thanks for your interest in FastAPI! 🚀
-
- Please follow these instructions, fill every question, and do every step. 🙏
-
- I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time.
-
- I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues.
-
- All that, on top of all the incredible help provided by a bunch of community members, the [FastAPI Experts](https://fastapi.tiangolo.com/fastapi-people/#experts), that give a lot of their time to come here and help others.
-
- That's a lot of work they are doing, but if more FastAPI users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅).
-
- By asking questions in a structured way (following this) it will be much easier to help you.
-
- And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎
-
- As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓
- - type: checkboxes
- id: checks
- attributes:
- label: First Check
- description: Please confirm and check all the following options.
- options:
- - label: I added a very descriptive title to this issue.
- required: true
- - label: I used the GitHub search to find a similar issue and didn't find it.
- required: true
- - label: I searched the FastAPI documentation, with the integrated search.
- required: true
- - label: I already searched in Google "How to X in FastAPI" and didn't find any information.
- required: true
- - label: I already read and followed all the tutorial in the docs and didn't find an answer.
- required: true
- - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic).
- required: true
- - label: I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui).
- required: true
- - label: I already checked if it is not related to FastAPI but to [ReDoc](https://github.com/Redocly/redoc).
- required: true
- - type: checkboxes
- id: help
- attributes:
- label: Commit to Help
- description: |
- After submitting this, I commit to one of:
-
- * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there.
- * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.
- * Implement a Pull Request for a confirmed bug.
-
- options:
- - label: I commit to help with one of those options 👆
- required: true
- - type: textarea
- id: example
- attributes:
- label: Example Code
- description: |
- Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case.
-
- If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you.
-
- placeholder: |
- from fastapi import FastAPI
-
- app = FastAPI()
-
-
- @app.get("/")
- def read_root():
- return {"Hello": "World"}
- render: python
- validations:
- required: true
- - type: textarea
- id: description
- attributes:
- label: Description
- description: |
- What is your feature request?
-
- Write a short description telling me what you are trying to solve and what you are currently doing.
- placeholder: |
- * Open the browser and call the endpoint `/`.
- * It returns a JSON with `{"Hello": "World"}`.
- * I would like it to have an extra parameter to teleport me to the moon and back.
- validations:
- required: true
- - type: textarea
- id: wanted-solution
- attributes:
- label: Wanted Solution
- description: |
- Tell me what's the solution you would like.
- placeholder: |
- I would like it to have a `teleport_to_moon` parameter that defaults to `False`, and can be set to `True` to teleport me.
- validations:
- required: true
- - type: textarea
- id: wanted-code
- attributes:
- label: Wanted Code
- description: Show me an example of how you would want the code to look like.
- placeholder: |
- from fastapi import FastAPI
-
- app = FastAPI()
-
-
- @app.get("/", teleport_to_moon=True)
- def read_root():
- return {"Hello": "World"}
- render: python
- validations:
- required: true
- - type: textarea
- id: alternatives
- attributes:
- label: Alternatives
- description: |
- Tell me about alternatives you've considered.
- placeholder: |
- To wait for Space X moon travel plans to drop down long after they release them. But I would rather teleport.
- - type: dropdown
- id: os
- attributes:
- label: Operating System
- description: What operating system are you on?
- multiple: true
- options:
- - Linux
- - Windows
- - macOS
- - Other
- validations:
- required: true
- - type: textarea
- id: os-details
- attributes:
- label: Operating System Details
- description: You can add more details about your operating system here, in particular if you chose "Other".
- - type: input
- id: fastapi-version
- attributes:
- label: FastAPI Version
- description: |
- What FastAPI version are you using?
-
- You can find the FastAPI version with:
-
- ```bash
- python -c "import fastapi; print(fastapi.__version__)"
- ```
- validations:
- required: true
- - type: input
- id: python-version
- attributes:
- label: Python Version
- description: |
- What Python version are you using?
-
- You can find the Python version with:
-
- ```bash
- python --version
- ```
- validations:
- required: true
- - type: textarea
- id: context
- attributes:
- label: Additional Context
- description: Add any additional context information or screenshots you think are useful.
diff --git a/.github/ISSUE_TEMPLATE/privileged.yml b/.github/ISSUE_TEMPLATE/privileged.yml
new file mode 100644
index 000000000..c01e34b6d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/privileged.yml
@@ -0,0 +1,22 @@
+name: Privileged
+description: You are @tiangolo or he asked you directly to create an issue here. If not, check the other options. 👇
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for your interest in FastAPI! 🚀
+
+ If you are not @tiangolo or he didn't ask you directly to create an issue here, please start the conversation in a [Question in GitHub Discussions](https://github.com/tiangolo/fastapi/discussions/categories/questions) instead.
+ - type: checkboxes
+ id: privileged
+ attributes:
+ label: Privileged issue
+ description: Confirm that you are allowed to create an issue here.
+ options:
+ - label: I'm @tiangolo or he asked me directly to create an issue here.
+ required: true
+ - type: textarea
+ id: content
+ attributes:
+ label: Issue Content
+ description: Add the content of the issue here.
diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py
index 31756a5fc..05cbc71e5 100644
--- a/.github/actions/people/app/main.py
+++ b/.github/actions/people/app/main.py
@@ -4,7 +4,7 @@ import sys
from collections import Counter, defaultdict
from datetime import datetime, timedelta, timezone
from pathlib import Path
-from typing import Container, DefaultDict, Dict, List, Set, Union
+from typing import Any, Container, DefaultDict, Dict, List, Set, Union
import httpx
import yaml
@@ -12,6 +12,50 @@ from github import Github
from pydantic import BaseModel, BaseSettings, SecretStr
github_graphql_url = "https://api.github.com/graphql"
+questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0"
+
+discussions_query = """
+query Q($after: String, $category_id: ID) {
+ repository(name: "fastapi", owner: "tiangolo") {
+ discussions(first: 100, after: $after, categoryId: $category_id) {
+ edges {
+ cursor
+ node {
+ number
+ author {
+ login
+ avatarUrl
+ url
+ }
+ title
+ createdAt
+ comments(first: 100) {
+ nodes {
+ createdAt
+ author {
+ login
+ avatarUrl
+ url
+ }
+ isAnswer
+ replies(first: 10) {
+ nodes {
+ createdAt
+ author {
+ login
+ avatarUrl
+ url
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+"""
issues_query = """
query Q($after: String) {
@@ -131,15 +175,30 @@ class Author(BaseModel):
url: str
+# Issues and Discussions
+
+
class CommentsNode(BaseModel):
createdAt: datetime
author: Union[Author, None] = None
+class Replies(BaseModel):
+ nodes: List[CommentsNode]
+
+
+class DiscussionsCommentsNode(CommentsNode):
+ replies: Replies
+
+
class Comments(BaseModel):
nodes: List[CommentsNode]
+class DiscussionsComments(BaseModel):
+ nodes: List[DiscussionsCommentsNode]
+
+
class IssuesNode(BaseModel):
number: int
author: Union[Author, None] = None
@@ -149,27 +208,59 @@ class IssuesNode(BaseModel):
comments: Comments
+class DiscussionsNode(BaseModel):
+ number: int
+ author: Union[Author, None] = None
+ title: str
+ createdAt: datetime
+ comments: DiscussionsComments
+
+
class IssuesEdge(BaseModel):
cursor: str
node: IssuesNode
+class DiscussionsEdge(BaseModel):
+ cursor: str
+ node: DiscussionsNode
+
+
class Issues(BaseModel):
edges: List[IssuesEdge]
+class Discussions(BaseModel):
+ edges: List[DiscussionsEdge]
+
+
class IssuesRepository(BaseModel):
issues: Issues
+class DiscussionsRepository(BaseModel):
+ discussions: Discussions
+
+
class IssuesResponseData(BaseModel):
repository: IssuesRepository
+class DiscussionsResponseData(BaseModel):
+ repository: DiscussionsRepository
+
+
class IssuesResponse(BaseModel):
data: IssuesResponseData
+class DiscussionsResponse(BaseModel):
+ data: DiscussionsResponseData
+
+
+# PRs
+
+
class LabelNode(BaseModel):
name: str
@@ -219,6 +310,9 @@ class PRsResponse(BaseModel):
data: PRsResponseData
+# Sponsors
+
+
class SponsorEntity(BaseModel):
login: str
avatarUrl: str
@@ -264,10 +358,16 @@ class Settings(BaseSettings):
def get_graphql_response(
- *, settings: Settings, query: str, after: Union[str, None] = None
-):
+ *,
+ settings: Settings,
+ query: str,
+ after: Union[str, None] = None,
+ category_id: Union[str, None] = None,
+) -> Dict[str, Any]:
headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"}
- variables = {"after": after}
+ # category_id is only used by one query, but GraphQL allows unused variables, so
+ # keep it here for simplicity
+ variables = {"after": after, "category_id": category_id}
response = httpx.post(
github_graphql_url,
headers=headers,
@@ -275,7 +375,9 @@ def get_graphql_response(
json={"query": query, "variables": variables, "operationName": "Q"},
)
if response.status_code != 200:
- logging.error(f"Response was not 200, after: {after}")
+ logging.error(
+ f"Response was not 200, after: {after}, category_id: {category_id}"
+ )
logging.error(response.text)
raise RuntimeError(response.text)
data = response.json()
@@ -288,6 +390,21 @@ def get_graphql_issue_edges(*, settings: Settings, after: Union[str, None] = Non
return graphql_response.data.repository.issues.edges
+def get_graphql_question_discussion_edges(
+ *,
+ settings: Settings,
+ after: Union[str, None] = None,
+):
+ data = get_graphql_response(
+ settings=settings,
+ query=discussions_query,
+ after=after,
+ category_id=questions_category_id,
+ )
+ graphql_response = DiscussionsResponse.parse_obj(data)
+ return graphql_response.data.repository.discussions.edges
+
+
def get_graphql_pr_edges(*, settings: Settings, after: Union[str, None] = None):
data = get_graphql_response(settings=settings, query=prs_query, after=after)
graphql_response = PRsResponse.parse_obj(data)
@@ -300,7 +417,7 @@ def get_graphql_sponsor_edges(*, settings: Settings, after: Union[str, None] = N
return graphql_response.data.user.sponsorshipsAsMaintainer.edges
-def get_experts(settings: Settings):
+def get_issues_experts(settings: Settings):
issue_nodes: List[IssuesNode] = []
issue_edges = get_graphql_issue_edges(settings=settings)
@@ -326,13 +443,74 @@ def get_experts(settings: Settings):
for comment in issue.comments.nodes:
if comment.author:
authors[comment.author.login] = comment.author
- if comment.author.login == issue_author_name:
- continue
- issue_commentors.add(comment.author.login)
+ if comment.author.login != issue_author_name:
+ issue_commentors.add(comment.author.login)
for author_name in issue_commentors:
commentors[author_name] += 1
if issue.createdAt > one_month_ago:
last_month_commentors[author_name] += 1
+
+ return commentors, last_month_commentors, authors
+
+
+def get_discussions_experts(settings: Settings):
+ discussion_nodes: List[DiscussionsNode] = []
+ discussion_edges = get_graphql_question_discussion_edges(settings=settings)
+
+ while discussion_edges:
+ for discussion_edge in discussion_edges:
+ discussion_nodes.append(discussion_edge.node)
+ last_edge = discussion_edges[-1]
+ discussion_edges = get_graphql_question_discussion_edges(
+ settings=settings, after=last_edge.cursor
+ )
+
+ commentors = Counter()
+ last_month_commentors = Counter()
+ authors: Dict[str, Author] = {}
+
+ now = datetime.now(tz=timezone.utc)
+ one_month_ago = now - timedelta(days=30)
+
+ for discussion in discussion_nodes:
+ discussion_author_name = None
+ if discussion.author:
+ authors[discussion.author.login] = discussion.author
+ discussion_author_name = discussion.author.login
+ discussion_commentors = set()
+ for comment in discussion.comments.nodes:
+ if comment.author:
+ authors[comment.author.login] = comment.author
+ if comment.author.login != discussion_author_name:
+ discussion_commentors.add(comment.author.login)
+ for reply in comment.replies.nodes:
+ if reply.author:
+ authors[reply.author.login] = reply.author
+ if reply.author.login != discussion_author_name:
+ discussion_commentors.add(reply.author.login)
+ for author_name in discussion_commentors:
+ commentors[author_name] += 1
+ if discussion.createdAt > one_month_ago:
+ last_month_commentors[author_name] += 1
+ return commentors, last_month_commentors, authors
+
+
+def get_experts(settings: Settings):
+ (
+ issues_commentors,
+ issues_last_month_commentors,
+ issues_authors,
+ ) = get_issues_experts(settings=settings)
+ (
+ discussions_commentors,
+ discussions_last_month_commentors,
+ discussions_authors,
+ ) = get_discussions_experts(settings=settings)
+ commentors = issues_commentors + discussions_commentors
+ last_month_commentors = (
+ issues_last_month_commentors + discussions_last_month_commentors
+ )
+ authors = {**issues_authors, **discussions_authors}
return commentors, last_month_commentors, authors
@@ -425,13 +603,13 @@ if __name__ == "__main__":
logging.info(f"Using config: {settings.json()}")
g = Github(settings.input_standard_token.get_secret_value())
repo = g.get_repo(settings.github_repository)
- issue_commentors, issue_last_month_commentors, issue_authors = get_experts(
+ question_commentors, question_last_month_commentors, question_authors = get_experts(
settings=settings
)
contributors, pr_commentors, reviewers, pr_authors = get_contributors(
settings=settings
)
- authors = {**issue_authors, **pr_authors}
+ authors = {**question_authors, **pr_authors}
maintainers_logins = {"tiangolo"}
bot_names = {"codecov", "github-actions", "pre-commit-ci", "dependabot"}
maintainers = []
@@ -440,7 +618,7 @@ if __name__ == "__main__":
maintainers.append(
{
"login": login,
- "answers": issue_commentors[login],
+ "answers": question_commentors[login],
"prs": contributors[login],
"avatarUrl": user.avatarUrl,
"url": user.url,
@@ -453,13 +631,13 @@ if __name__ == "__main__":
min_count_reviewer = 4
skip_users = maintainers_logins | bot_names
experts = get_top_users(
- counter=issue_commentors,
+ counter=question_commentors,
min_count=min_count_expert,
authors=authors,
skip_users=skip_users,
)
last_month_active = get_top_users(
- counter=issue_last_month_commentors,
+ counter=question_last_month_commentors,
min_count=min_count_last_month,
authors=authors,
skip_users=skip_users,
diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
index b9bd521b3..68a180e38 100644
--- a/.github/workflows/build-docs.yml
+++ b/.github/workflows/build-docs.yml
@@ -7,7 +7,7 @@ on:
types: [opened, synchronize]
jobs:
build-docs:
- runs-on: ubuntu-18.04
+ runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
env:
@@ -17,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: "3.7"
+ python-version: "3.11"
- uses: actions/cache@v3
id: cache
with:
diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml
index 7d31a9c64..2af56e2bc 100644
--- a/.github/workflows/preview-docs.yml
+++ b/.github/workflows/preview-docs.yml
@@ -16,7 +16,7 @@ jobs:
rm -rf ./site
mkdir ./site
- name: Download Artifact Docs
- uses: dawidd6/action-download-artifact@v2.24.2
+ uses: dawidd6/action-download-artifact@v2.24.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: build-docs.yml
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 8ffb493a4..c2fdb8e17 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -31,7 +31,7 @@ jobs:
- name: Build distribution
run: python -m build
- name: Publish
- uses: pypa/gh-action-pypi-publish@v1.5.2
+ uses: pypa/gh-action-pypi-publish@v1.6.4
with:
password: ${{ secrets.PYPI_API_TOKEN }}
- name: Dump GitHub context
diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml
index 7559c24c0..d6206d697 100644
--- a/.github/workflows/smokeshow.yml
+++ b/.github/workflows/smokeshow.yml
@@ -20,7 +20,7 @@ jobs:
- run: pip install smokeshow
- - uses: dawidd6/action-download-artifact@v2.24.2
+ - uses: dawidd6/action-download-artifact@v2.24.3
with:
workflow: test.yml
commit: ${{ github.event.workflow_run.head_sha }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ddc43c942..1235516d3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -75,3 +75,19 @@ jobs:
with:
name: coverage-html
path: htmlcov
+
+ # https://github.com/marketplace/actions/alls-green#why
+ check: # This job does nothing and is only used for the branch protection
+
+ if: always()
+
+ needs:
+ - coverage-combine
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Decide whether the needed jobs succeeded or failed
+ uses: re-actors/alls-green@release/v1
+ with:
+ jobs: ${{ toJSON(needs) }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 96f097caa..01cd6ea0f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,7 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
+default_language_version:
+ python: python3.10
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
@@ -25,7 +27,7 @@ repos:
args:
- --fix
- repo: https://github.com/pycqa/isort
- rev: 5.10.1
+ rev: 5.12.0
hooks:
- id: isort
name: isort (python)
diff --git a/README.md b/README.md
index 7c4a6c4b4..39030ef52 100644
--- a/README.md
+++ b/README.md
@@ -49,14 +49,14 @@ The key features are:
-
-
+
+
@@ -102,6 +102,12 @@ The key features are:
---
+"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._"
+
+
get
işlemi kullanılarak
+
+
+!!! info "`@decorator` Bilgisi"
+ Python `@something` şeklinde ifadeleri "decorator" olarak adlandırır.
+
+ Decoratoru bir fonksiyonun üzerine koyarsınız. Dekoratif bir şapka gibi (Sanırım terim buradan gelmektedir).
+
+ Bir "decorator" fonksiyonu alır ve bazı işlemler gerçekleştir.
+
+ Bizim durumumzda decarator **FastAPI'ye** fonksiyonun bir `get` işlemi ile `/` pathine geldiğini söyler.
+
+ Bu **path işlem decoratordür**
+
+Ayrıca diğer işlemleri de kullanabilirsiniz:
+
+* `@app.post()`
+* `@app.put()`
+* `@app.delete()`
+
+Ve daha egzotik olanları:
+
+* `@app.options()`
+* `@app.head()`
+* `@app.patch()`
+* `@app.trace()`
+
+!!! tip
+ Her işlemi (HTTP method) istediğiniz gibi kullanmakta özgürsünüz.
+
+ **FastAPI** herhangi bir özel anlamı zorlamaz.
+
+ Buradaki bilgiler bir gereklilik değil, bir kılavuz olarak sunulmaktadır.
+
+ Örneğin, GraphQL kullanırkan normalde tüm işlemleri yalnızca `POST` işlemini kullanarak gerçekleştirirsiniz.
+
+### Adım 4: **path işlem fonksiyonunu** tanımlayın
+
+Aşağıdakiler bizim **path işlem fonksiyonlarımızdır**:
+
+* **path**: `/`
+* **işlem**: `get`
+* **function**: "decorator"ün altındaki fonksiyondur (`@app.get("/")` altında).
+
+```Python hl_lines="7"
+{!../../../docs_src/first_steps/tutorial001.py!}
+```
+
+Bu bir Python fonksiyonudur.
+
+Bir `GET` işlemi kullanarak "`/`" URL'sine bir istek geldiğinde **FastAPI** tarafından çağrılır.
+
+Bu durumda bir `async` fonksiyonudur.
+
+---
+
+Bunu `async def` yerine normal bir fonksiyon olarakta tanımlayabilirsiniz.
+
+```Python hl_lines="7"
+{!../../../docs_src/first_steps/tutorial003.py!}
+```
+
+!!! note
+
+ Eğer farkı bilmiyorsanız, [Async: *"Acelesi var?"*](../async.md#in-a-hurry){.internal-link target=_blank} kontrol edebilirsiniz.
+
+### Adım 5: İçeriği geri döndürün
+
+
+```Python hl_lines="8"
+{!../../../docs_src/first_steps/tutorial001.py!}
+```
+
+Bir `dict`, `list` döndürebilir veya `str`, `int` gibi tekil değerler döndürebilirsiniz.
+
+Ayrıca, Pydantic modellerini de döndürebilirsiniz. (Bununla ilgili daha sonra ayrıntılı bilgi göreceksiniz.)
+
+Otomatik olarak JSON'a dönüştürülecek(ORM'ler vb. dahil) başka birçok nesne ve model vardır. En beğendiklerinizi kullanmayı deneyin, yüksek ihtimalle destekleniyordur.
+
+## Özet
+
+* `FastAPI`'yi içe aktarın.
+* Bir `app` örneği oluşturun.
+* **path işlem decorator** yazın. (`@app.get("/")` gibi)
+* **path işlem fonksiyonu** yazın. (`def root(): ...` gibi)
+* Development sunucunuzu çalıştırın. (`uvicorn main:app --reload` gibi)
diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml
index e29d25936..54e198fef 100644
--- a/docs/tr/mkdocs.yml
+++ b/docs/tr/mkdocs.yml
@@ -61,6 +61,8 @@ nav:
- features.md
- fastapi-people.md
- python-types.md
+- Tutorial - User Guide:
+ - tutorial/first-steps.md
markdown_extensions:
- toc:
permalink: true
@@ -83,7 +85,7 @@ markdown_extensions:
extra:
analytics:
provider: google
- property: UA-133183413-1
+ property: G-YNEVN69SC3
social:
- icon: fontawesome/brands/github-alt
link: https://github.com/tiangolo/fastapi
diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml
index 711328771..05ff1a8b7 100644
--- a/docs/uk/mkdocs.yml
+++ b/docs/uk/mkdocs.yml
@@ -80,7 +80,7 @@ markdown_extensions:
extra:
analytics:
provider: google
- property: UA-133183413-1
+ property: G-YNEVN69SC3
social:
- icon: fontawesome/brands/github-alt
link: https://github.com/tiangolo/fastapi
diff --git a/docs/zh/docs/benchmarks.md b/docs/zh/docs/benchmarks.md
index 8991c72cd..71e8d4838 100644
--- a/docs/zh/docs/benchmarks.md
+++ b/docs/zh/docs/benchmarks.md
@@ -1,6 +1,6 @@
# 基准测试
-第三方机构 TechEmpower 的基准测试表明在 Uvicorn 下运行的 **FastAPI** 应用程序是 可用的最快的 Python 框架之一,仅次与 Starlette 和 Uvicorn 本身 (由 FastAPI 内部使用)。(*)
+第三方机构 TechEmpower 的基准测试表明在 Uvicorn 下运行的 **FastAPI** 应用程序是 可用的最快的 Python 框架之一,仅次于 Starlette 和 Uvicorn 本身 (由 FastAPI 内部使用)。(*)
但是在查看基准得分和对比时,请注意以下几点。
diff --git a/docs/zh/docs/index.md b/docs/zh/docs/index.md
index 7901e9c2c..4db3ef10c 100644
--- a/docs/zh/docs/index.md
+++ b/docs/zh/docs/index.md
@@ -28,7 +28,7 @@ FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框
关键特性:
-* **快速**:可与 **NodeJS** 和 **Go** 比肩的极高性能(归功于 Starlette 和 Pydantic)。[最快的 Python web 框架之一](#_11)。
+* **快速**:可与 **NodeJS** 和 **Go** 并肩的极高性能(归功于 Starlette 和 Pydantic)。[最快的 Python web 框架之一](#_11)。
* **高效编码**:提高功能开发速度约 200% 至 300%。*
* **更少 bug**:减少约 40% 的人为(开发者)导致错误。*
diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml
index f4c3c0ec1..3661c470e 100644
--- a/docs/zh/mkdocs.yml
+++ b/docs/zh/mkdocs.yml
@@ -137,7 +137,7 @@ markdown_extensions:
extra:
analytics:
provider: google
- property: UA-133183413-1
+ property: G-YNEVN69SC3
social:
- icon: fontawesome/brands/github-alt
link: https://github.com/tiangolo/fastapi
diff --git a/docs_src/app_testing/tutorial002.py b/docs_src/app_testing/tutorial002.py
index b4a9c0586..71c898b3c 100644
--- a/docs_src/app_testing/tutorial002.py
+++ b/docs_src/app_testing/tutorial002.py
@@ -10,7 +10,7 @@ async def read_main():
return {"msg": "Hello World"}
-@app.websocket_route("/ws")
+@app.websocket("/ws")
async def websocket(websocket: WebSocket):
await websocket.accept()
await websocket.send_json({"msg": "Hello WebSocket"})
diff --git a/docs_src/response_model/tutorial001.py b/docs_src/response_model/tutorial001.py
index 0f6e03e5b..fd1c902a5 100644
--- a/docs_src/response_model/tutorial001.py
+++ b/docs_src/response_model/tutorial001.py
@@ -1,4 +1,4 @@
-from typing import List, Union
+from typing import Any, List, Union
from fastapi import FastAPI
from pydantic import BaseModel
@@ -15,5 +15,13 @@ class Item(BaseModel):
@app.post("/items/", response_model=Item)
-async def create_item(item: Item):
+async def create_item(item: Item) -> Any:
return item
+
+
+@app.get("/items/", response_model=List[Item])
+async def read_items() -> Any:
+ return [
+ {"name": "Portal Gun", "price": 42.0},
+ {"name": "Plumbus", "price": 32.0},
+ ]
diff --git a/docs_src/response_model/tutorial001_01.py b/docs_src/response_model/tutorial001_01.py
new file mode 100644
index 000000000..98d30d540
--- /dev/null
+++ b/docs_src/response_model/tutorial001_01.py
@@ -0,0 +1,27 @@
+from typing import List, Union
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ description: Union[str, None] = None
+ price: float
+ tax: Union[float, None] = None
+ tags: List[str] = []
+
+
+@app.post("/items/")
+async def create_item(item: Item) -> Item:
+ return item
+
+
+@app.get("/items/")
+async def read_items() -> List[Item]:
+ return [
+ Item(name="Portal Gun", price=42.0),
+ Item(name="Plumbus", price=32.0),
+ ]
diff --git a/docs_src/response_model/tutorial001_01_py310.py b/docs_src/response_model/tutorial001_01_py310.py
new file mode 100644
index 000000000..7951c1076
--- /dev/null
+++ b/docs_src/response_model/tutorial001_01_py310.py
@@ -0,0 +1,25 @@
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ description: str | None = None
+ price: float
+ tax: float | None = None
+ tags: list[str] = []
+
+
+@app.post("/items/")
+async def create_item(item: Item) -> Item:
+ return item
+
+
+@app.get("/items/")
+async def read_items() -> list[Item]:
+ return [
+ Item(name="Portal Gun", price=42.0),
+ Item(name="Plumbus", price=32.0),
+ ]
diff --git a/docs_src/response_model/tutorial001_01_py39.py b/docs_src/response_model/tutorial001_01_py39.py
new file mode 100644
index 000000000..16c78aa3f
--- /dev/null
+++ b/docs_src/response_model/tutorial001_01_py39.py
@@ -0,0 +1,27 @@
+from typing import Union
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ description: Union[str, None] = None
+ price: float
+ tax: Union[float, None] = None
+ tags: list[str] = []
+
+
+@app.post("/items/")
+async def create_item(item: Item) -> Item:
+ return item
+
+
+@app.get("/items/")
+async def read_items() -> list[Item]:
+ return [
+ Item(name="Portal Gun", price=42.0),
+ Item(name="Plumbus", price=32.0),
+ ]
diff --git a/docs_src/response_model/tutorial001_py310.py b/docs_src/response_model/tutorial001_py310.py
index 59efecde4..f8a2aa9fc 100644
--- a/docs_src/response_model/tutorial001_py310.py
+++ b/docs_src/response_model/tutorial001_py310.py
@@ -1,3 +1,5 @@
+from typing import Any
+
from fastapi import FastAPI
from pydantic import BaseModel
@@ -13,5 +15,13 @@ class Item(BaseModel):
@app.post("/items/", response_model=Item)
-async def create_item(item: Item):
+async def create_item(item: Item) -> Any:
return item
+
+
+@app.get("/items/", response_model=list[Item])
+async def read_items() -> Any:
+ return [
+ {"name": "Portal Gun", "price": 42.0},
+ {"name": "Plumbus", "price": 32.0},
+ ]
diff --git a/docs_src/response_model/tutorial001_py39.py b/docs_src/response_model/tutorial001_py39.py
index cdcca39d2..261e252d0 100644
--- a/docs_src/response_model/tutorial001_py39.py
+++ b/docs_src/response_model/tutorial001_py39.py
@@ -1,4 +1,4 @@
-from typing import Union
+from typing import Any, Union
from fastapi import FastAPI
from pydantic import BaseModel
@@ -15,5 +15,13 @@ class Item(BaseModel):
@app.post("/items/", response_model=Item)
-async def create_item(item: Item):
+async def create_item(item: Item) -> Any:
return item
+
+
+@app.get("/items/", response_model=list[Item])
+async def read_items() -> Any:
+ return [
+ {"name": "Portal Gun", "price": 42.0},
+ {"name": "Plumbus", "price": 32.0},
+ ]
diff --git a/docs_src/response_model/tutorial002.py b/docs_src/response_model/tutorial002.py
index c68e8b138..a58668f9e 100644
--- a/docs_src/response_model/tutorial002.py
+++ b/docs_src/response_model/tutorial002.py
@@ -14,6 +14,6 @@ class UserIn(BaseModel):
# Don't do this in production!
-@app.post("/user/", response_model=UserIn)
-async def create_user(user: UserIn):
+@app.post("/user/")
+async def create_user(user: UserIn) -> UserIn:
return user
diff --git a/docs_src/response_model/tutorial002_py310.py b/docs_src/response_model/tutorial002_py310.py
index 29ab9c9d2..0a91a5967 100644
--- a/docs_src/response_model/tutorial002_py310.py
+++ b/docs_src/response_model/tutorial002_py310.py
@@ -12,6 +12,6 @@ class UserIn(BaseModel):
# Don't do this in production!
-@app.post("/user/", response_model=UserIn)
-async def create_user(user: UserIn):
+@app.post("/user/")
+async def create_user(user: UserIn) -> UserIn:
return user
diff --git a/docs_src/response_model/tutorial003.py b/docs_src/response_model/tutorial003.py
index 37e493dcb..c42dbc707 100644
--- a/docs_src/response_model/tutorial003.py
+++ b/docs_src/response_model/tutorial003.py
@@ -1,4 +1,4 @@
-from typing import Union
+from typing import Any, Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
@@ -20,5 +20,5 @@ class UserOut(BaseModel):
@app.post("/user/", response_model=UserOut)
-async def create_user(user: UserIn):
+async def create_user(user: UserIn) -> Any:
return user
diff --git a/docs_src/response_model/tutorial003_01.py b/docs_src/response_model/tutorial003_01.py
new file mode 100644
index 000000000..52694b551
--- /dev/null
+++ b/docs_src/response_model/tutorial003_01.py
@@ -0,0 +1,21 @@
+from typing import Union
+
+from fastapi import FastAPI
+from pydantic import BaseModel, EmailStr
+
+app = FastAPI()
+
+
+class BaseUser(BaseModel):
+ username: str
+ email: EmailStr
+ full_name: Union[str, None] = None
+
+
+class UserIn(BaseUser):
+ password: str
+
+
+@app.post("/user/")
+async def create_user(user: UserIn) -> BaseUser:
+ return user
diff --git a/docs_src/response_model/tutorial003_01_py310.py b/docs_src/response_model/tutorial003_01_py310.py
new file mode 100644
index 000000000..6ffddfd0a
--- /dev/null
+++ b/docs_src/response_model/tutorial003_01_py310.py
@@ -0,0 +1,19 @@
+from fastapi import FastAPI
+from pydantic import BaseModel, EmailStr
+
+app = FastAPI()
+
+
+class BaseUser(BaseModel):
+ username: str
+ email: EmailStr
+ full_name: str | None = None
+
+
+class UserIn(BaseUser):
+ password: str
+
+
+@app.post("/user/")
+async def create_user(user: UserIn) -> BaseUser:
+ return user
diff --git a/docs_src/response_model/tutorial003_02.py b/docs_src/response_model/tutorial003_02.py
new file mode 100644
index 000000000..df6a09646
--- /dev/null
+++ b/docs_src/response_model/tutorial003_02.py
@@ -0,0 +1,11 @@
+from fastapi import FastAPI, Response
+from fastapi.responses import JSONResponse, RedirectResponse
+
+app = FastAPI()
+
+
+@app.get("/portal")
+async def get_portal(teleport: bool = False) -> Response:
+ if teleport:
+ return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
+ return JSONResponse(content={"message": "Here's your interdimensional portal."})
diff --git a/docs_src/response_model/tutorial003_03.py b/docs_src/response_model/tutorial003_03.py
new file mode 100644
index 000000000..0d4bd8de5
--- /dev/null
+++ b/docs_src/response_model/tutorial003_03.py
@@ -0,0 +1,9 @@
+from fastapi import FastAPI
+from fastapi.responses import RedirectResponse
+
+app = FastAPI()
+
+
+@app.get("/teleport")
+async def get_teleport() -> RedirectResponse:
+ return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
diff --git a/docs_src/response_model/tutorial003_04.py b/docs_src/response_model/tutorial003_04.py
new file mode 100644
index 000000000..b13a92692
--- /dev/null
+++ b/docs_src/response_model/tutorial003_04.py
@@ -0,0 +1,13 @@
+from typing import Union
+
+from fastapi import FastAPI, Response
+from fastapi.responses import RedirectResponse
+
+app = FastAPI()
+
+
+@app.get("/portal")
+async def get_portal(teleport: bool = False) -> Union[Response, dict]:
+ if teleport:
+ return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
+ return {"message": "Here's your interdimensional portal."}
diff --git a/docs_src/response_model/tutorial003_04_py310.py b/docs_src/response_model/tutorial003_04_py310.py
new file mode 100644
index 000000000..cee49b83e
--- /dev/null
+++ b/docs_src/response_model/tutorial003_04_py310.py
@@ -0,0 +1,11 @@
+from fastapi import FastAPI, Response
+from fastapi.responses import RedirectResponse
+
+app = FastAPI()
+
+
+@app.get("/portal")
+async def get_portal(teleport: bool = False) -> Response | dict:
+ if teleport:
+ return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
+ return {"message": "Here's your interdimensional portal."}
diff --git a/docs_src/response_model/tutorial003_05.py b/docs_src/response_model/tutorial003_05.py
new file mode 100644
index 000000000..0962061a6
--- /dev/null
+++ b/docs_src/response_model/tutorial003_05.py
@@ -0,0 +1,13 @@
+from typing import Union
+
+from fastapi import FastAPI, Response
+from fastapi.responses import RedirectResponse
+
+app = FastAPI()
+
+
+@app.get("/portal", response_model=None)
+async def get_portal(teleport: bool = False) -> Union[Response, dict]:
+ if teleport:
+ return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
+ return {"message": "Here's your interdimensional portal."}
diff --git a/docs_src/response_model/tutorial003_05_py310.py b/docs_src/response_model/tutorial003_05_py310.py
new file mode 100644
index 000000000..f1c0f8e12
--- /dev/null
+++ b/docs_src/response_model/tutorial003_05_py310.py
@@ -0,0 +1,11 @@
+from fastapi import FastAPI, Response
+from fastapi.responses import RedirectResponse
+
+app = FastAPI()
+
+
+@app.get("/portal", response_model=None)
+async def get_portal(teleport: bool = False) -> Response | dict:
+ if teleport:
+ return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
+ return {"message": "Here's your interdimensional portal."}
diff --git a/docs_src/response_model/tutorial003_py310.py b/docs_src/response_model/tutorial003_py310.py
index fc9693e3c..3703bf888 100644
--- a/docs_src/response_model/tutorial003_py310.py
+++ b/docs_src/response_model/tutorial003_py310.py
@@ -1,3 +1,5 @@
+from typing import Any
+
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
@@ -18,5 +20,5 @@ class UserOut(BaseModel):
@app.post("/user/", response_model=UserOut)
-async def create_user(user: UserIn):
+async def create_user(user: UserIn) -> Any:
return user
diff --git a/fastapi/__init__.py b/fastapi/__init__.py
index 037d9804b..33875c7fa 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.88.0"
+__version__ = "0.92.0"
from starlette import status as status
diff --git a/fastapi/applications.py b/fastapi/applications.py
index 61d4582d2..204bd46b3 100644
--- a/fastapi/applications.py
+++ b/fastapi/applications.py
@@ -35,6 +35,7 @@ from starlette.applications import Starlette
from starlette.datastructures import State
from starlette.exceptions import HTTPException
from starlette.middleware import Middleware
+from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.exceptions import ExceptionMiddleware
from starlette.requests import Request
@@ -86,7 +87,7 @@ class FastAPI(Starlette):
),
**extra: Any,
) -> None:
- self._debug: bool = debug
+ self.debug = debug
self.title = title
self.description = description
self.version = version
@@ -143,7 +144,7 @@ class FastAPI(Starlette):
self.user_middleware: List[Middleware] = (
[] if middleware is None else list(middleware)
)
- self.middleware_stack: ASGIApp = self.build_middleware_stack()
+ self.middleware_stack: Union[ASGIApp, None] = None
self.setup()
def build_middleware_stack(self) -> ASGIApp:
@@ -274,7 +275,7 @@ class FastAPI(Starlette):
path: str,
endpoint: Callable[..., Coroutine[Any, Any, Response]],
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
@@ -332,7 +333,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
@@ -435,7 +436,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
@@ -490,7 +491,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
@@ -545,7 +546,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
@@ -600,7 +601,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
@@ -655,7 +656,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
@@ -710,7 +711,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
@@ -765,7 +766,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
@@ -820,7 +821,7 @@ class FastAPI(Starlette):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[Depends]] = None,
@@ -870,3 +871,35 @@ class FastAPI(Starlette):
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
+
+ def websocket_route(
+ self, path: str, name: Union[str, None] = None
+ ) -> Callable[[DecoratedCallable], DecoratedCallable]:
+ def decorator(func: DecoratedCallable) -> DecoratedCallable:
+ self.router.add_websocket_route(path, func, name=name)
+ return func
+
+ return decorator
+
+ def on_event(
+ self, event_type: str
+ ) -> Callable[[DecoratedCallable], DecoratedCallable]:
+ return self.router.on_event(event_type)
+
+ def middleware(
+ self, middleware_type: str
+ ) -> Callable[[DecoratedCallable], DecoratedCallable]:
+ def decorator(func: DecoratedCallable) -> DecoratedCallable:
+ self.add_middleware(BaseHTTPMiddleware, dispatch=func)
+ return func
+
+ return decorator
+
+ def exception_handler(
+ self, exc_class_or_status_code: Union[int, Type[Exception]]
+ ) -> Callable[[DecoratedCallable], DecoratedCallable]:
+ def decorator(func: DecoratedCallable) -> DecoratedCallable:
+ self.add_exception_handler(exc_class_or_status_code, func)
+ return func
+
+ return decorator
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 4c817d5d0..32e171f18 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -253,7 +253,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
name=param.name,
kind=param.kind,
default=param.default,
- annotation=get_typed_annotation(param, globalns),
+ annotation=get_typed_annotation(param.annotation, globalns),
)
for param in signature.parameters.values()
]
@@ -261,14 +261,24 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
return typed_signature
-def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
- annotation = param.annotation
+def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any:
if isinstance(annotation, str):
annotation = ForwardRef(annotation)
annotation = evaluate_forwardref(annotation, globalns, globalns)
return annotation
+def get_typed_return_annotation(call: Callable[..., Any]) -> Any:
+ signature = inspect.signature(call)
+ annotation = signature.return_annotation
+
+ if annotation is inspect.Signature.empty:
+ return None
+
+ globalns = getattr(call, "__globals__", {})
+ return get_typed_annotation(annotation, globalns)
+
+
def get_dependant(
*,
path: str,
diff --git a/fastapi/routing.py b/fastapi/routing.py
index 9a7d88efc..7ab6275b6 100644
--- a/fastapi/routing.py
+++ b/fastapi/routing.py
@@ -26,6 +26,7 @@ from fastapi.dependencies.utils import (
get_body_field,
get_dependant,
get_parameterless_sub_dependant,
+ get_typed_return_annotation,
solve_dependencies,
)
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
@@ -41,6 +42,7 @@ from fastapi.utils import (
from pydantic import BaseModel
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import ModelField, Undefined
+from pydantic.utils import lenient_issubclass
from starlette import routing
from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
@@ -323,7 +325,7 @@ class APIRoute(routing.Route):
path: str,
endpoint: Callable[..., Any],
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -354,6 +356,12 @@ class APIRoute(routing.Route):
) -> None:
self.path = path
self.endpoint = endpoint
+ if isinstance(response_model, DefaultPlaceholder):
+ return_annotation = get_typed_return_annotation(endpoint)
+ if lenient_issubclass(return_annotation, Response):
+ response_model = None
+ else:
+ response_model = return_annotation
self.response_model = response_model
self.summary = summary
self.response_description = response_description
@@ -514,12 +522,31 @@ class APIRouter(routing.Router):
self.default_response_class = default_response_class
self.generate_unique_id_function = generate_unique_id_function
+ def route(
+ self,
+ path: str,
+ methods: Optional[List[str]] = None,
+ name: Optional[str] = None,
+ include_in_schema: bool = True,
+ ) -> Callable[[DecoratedCallable], DecoratedCallable]:
+ def decorator(func: DecoratedCallable) -> DecoratedCallable:
+ self.add_route(
+ path,
+ func,
+ methods=methods,
+ name=name,
+ include_in_schema=include_in_schema,
+ )
+ return func
+
+ return decorator
+
def add_api_route(
self,
path: str,
endpoint: Callable[..., Any],
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -600,7 +627,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -678,6 +705,15 @@ class APIRouter(routing.Router):
return decorator
+ def websocket_route(
+ self, path: str, name: Union[str, None] = None
+ ) -> Callable[[DecoratedCallable], DecoratedCallable]:
+ def decorator(func: DecoratedCallable) -> DecoratedCallable:
+ self.add_websocket_route(path, func, name=name)
+ return func
+
+ return decorator
+
def include_router(
self,
router: "APIRouter",
@@ -795,7 +831,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -851,7 +887,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -907,7 +943,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -963,7 +999,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -1019,7 +1055,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -1075,7 +1111,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -1131,7 +1167,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -1187,7 +1223,7 @@ class APIRouter(routing.Router):
self,
path: str,
*,
- response_model: Any = None,
+ response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
@@ -1239,3 +1275,12 @@ class APIRouter(routing.Router):
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)
+
+ def on_event(
+ self, event_type: str
+ ) -> Callable[[DecoratedCallable], DecoratedCallable]:
+ def decorator(func: DecoratedCallable) -> DecoratedCallable:
+ self.add_event_handler(event_type, func)
+ return func
+
+ return decorator
diff --git a/fastapi/utils.py b/fastapi/utils.py
index b15f6a2cf..391c47d81 100644
--- a/fastapi/utils.py
+++ b/fastapi/utils.py
@@ -88,7 +88,13 @@ def create_response_field(
return response_field(field_info=field_info)
except RuntimeError:
raise fastapi.exceptions.FastAPIError(
- f"Invalid args for response field! Hint: check that {type_} is a valid pydantic field type"
+ "Invalid args for response field! Hint: "
+ f"check that {type_} is a valid Pydantic field type. "
+ "If you are using a return type annotation that is not a valid Pydantic "
+ "field (e.g. Union[Response, dict, None]) you can disable generating the "
+ "response model from the type annotation with the path operation decorator "
+ "parameter response_model=None. Read more: "
+ "https://fastapi.tiangolo.com/tutorial/response-model/"
) from None
diff --git a/pyproject.toml b/pyproject.toml
index 55856cf36..3e651ae36 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,7 +39,7 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP",
]
dependencies = [
- "starlette==0.22.0",
+ "starlette>=0.25.0,<0.26.0",
"pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0",
]
dynamic = ["version"]
@@ -51,7 +51,7 @@ Documentation = "https://fastapi.tiangolo.com/"
[project.optional-dependencies]
test = [
"pytest >=7.1.3,<8.0.0",
- "coverage[toml] >= 6.5.0,<7.0",
+ "coverage[toml] >= 6.5.0,< 8.0",
"mypy ==0.982",
"ruff ==0.0.138",
"black == 22.10.0",
@@ -73,7 +73,7 @@ test = [
"passlib[bcrypt] >=1.7.2,<2.0.0",
# types
- "types-ujson ==5.5.0",
+ "types-ujson ==5.6.0.0",
"types-orjson ==3.6.2",
]
doc = [
@@ -81,14 +81,13 @@ doc = [
"mkdocs-material >=8.1.4,<9.0.0",
"mdx-include >=1.4.1,<2.0.0",
"mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0",
- # TODO: upgrade and enable typer-cli once it supports Click 8.x.x
- # "typer-cli >=0.0.12,<0.0.13",
+ "typer-cli >=0.0.13,<0.0.14",
"typer[all] >=0.6.1,<0.8.0",
"pyyaml >=5.3.1,<7.0.0",
]
dev = [
"ruff ==0.0.138",
- "uvicorn[standard] >=0.12.0,<0.19.0",
+ "uvicorn[standard] >=0.12.0,<0.21.0",
"pre-commit >=2.17.0,<3.0.0",
]
all = [
@@ -155,6 +154,10 @@ source = [
"fastapi"
]
context = '${CONTEXT}'
+omit = [
+ "docs_src/response_model/tutorial003_04.py",
+ "docs_src/response_model/tutorial003_04_py310.py",
+]
[tool.ruff]
select = [
diff --git a/scripts/zip-docs.sh b/scripts/zip-docs.sh
index 69315f5dd..47c3b0977 100644
--- a/scripts/zip-docs.sh
+++ b/scripts/zip-docs.sh
@@ -1,4 +1,4 @@
-#! /usr/bin/env bash
+#!/usr/bin/env bash
set -x
set -e
diff --git a/tests/test_reponse_set_reponse_code_empty.py b/tests/test_reponse_set_reponse_code_empty.py
index 094d54a84..50ec753a0 100644
--- a/tests/test_reponse_set_reponse_code_empty.py
+++ b/tests/test_reponse_set_reponse_code_empty.py
@@ -9,6 +9,7 @@ app = FastAPI()
@app.delete(
"/{id}",
status_code=204,
+ response_model=None,
)
async def delete_deployment(
id: int,
diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py
new file mode 100644
index 000000000..e45364149
--- /dev/null
+++ b/tests/test_response_model_as_return_annotation.py
@@ -0,0 +1,1111 @@
+from typing import List, Union
+
+import pytest
+from fastapi import FastAPI
+from fastapi.exceptions import FastAPIError
+from fastapi.responses import JSONResponse, Response
+from fastapi.testclient import TestClient
+from pydantic import BaseModel, ValidationError
+
+
+class BaseUser(BaseModel):
+ name: str
+
+
+class User(BaseUser):
+ surname: str
+
+
+class DBUser(User):
+ password_hash: str
+
+
+class Item(BaseModel):
+ name: str
+ price: float
+
+
+app = FastAPI()
+
+
+@app.get("/no_response_model-no_annotation-return_model")
+def no_response_model_no_annotation_return_model():
+ return User(name="John", surname="Doe")
+
+
+@app.get("/no_response_model-no_annotation-return_dict")
+def no_response_model_no_annotation_return_dict():
+ return {"name": "John", "surname": "Doe"}
+
+
+@app.get("/response_model-no_annotation-return_same_model", response_model=User)
+def response_model_no_annotation_return_same_model():
+ return User(name="John", surname="Doe")
+
+
+@app.get("/response_model-no_annotation-return_exact_dict", response_model=User)
+def response_model_no_annotation_return_exact_dict():
+ return {"name": "John", "surname": "Doe"}
+
+
+@app.get("/response_model-no_annotation-return_invalid_dict", response_model=User)
+def response_model_no_annotation_return_invalid_dict():
+ return {"name": "John"}
+
+
+@app.get("/response_model-no_annotation-return_invalid_model", response_model=User)
+def response_model_no_annotation_return_invalid_model():
+ return Item(name="Foo", price=42.0)
+
+
+@app.get(
+ "/response_model-no_annotation-return_dict_with_extra_data", response_model=User
+)
+def response_model_no_annotation_return_dict_with_extra_data():
+ return {"name": "John", "surname": "Doe", "password_hash": "secret"}
+
+
+@app.get(
+ "/response_model-no_annotation-return_submodel_with_extra_data", response_model=User
+)
+def response_model_no_annotation_return_submodel_with_extra_data():
+ return DBUser(name="John", surname="Doe", password_hash="secret")
+
+
+@app.get("/no_response_model-annotation-return_same_model")
+def no_response_model_annotation_return_same_model() -> User:
+ return User(name="John", surname="Doe")
+
+
+@app.get("/no_response_model-annotation-return_exact_dict")
+def no_response_model_annotation_return_exact_dict() -> User:
+ return {"name": "John", "surname": "Doe"}
+
+
+@app.get("/no_response_model-annotation-return_invalid_dict")
+def no_response_model_annotation_return_invalid_dict() -> User:
+ return {"name": "John"}
+
+
+@app.get("/no_response_model-annotation-return_invalid_model")
+def no_response_model_annotation_return_invalid_model() -> User:
+ return Item(name="Foo", price=42.0)
+
+
+@app.get("/no_response_model-annotation-return_dict_with_extra_data")
+def no_response_model_annotation_return_dict_with_extra_data() -> User:
+ return {"name": "John", "surname": "Doe", "password_hash": "secret"}
+
+
+@app.get("/no_response_model-annotation-return_submodel_with_extra_data")
+def no_response_model_annotation_return_submodel_with_extra_data() -> User:
+ return DBUser(name="John", surname="Doe", password_hash="secret")
+
+
+@app.get("/response_model_none-annotation-return_same_model", response_model=None)
+def response_model_none_annotation_return_same_model() -> User:
+ return User(name="John", surname="Doe")
+
+
+@app.get("/response_model_none-annotation-return_exact_dict", response_model=None)
+def response_model_none_annotation_return_exact_dict() -> User:
+ return {"name": "John", "surname": "Doe"}
+
+
+@app.get("/response_model_none-annotation-return_invalid_dict", response_model=None)
+def response_model_none_annotation_return_invalid_dict() -> User:
+ return {"name": "John"}
+
+
+@app.get("/response_model_none-annotation-return_invalid_model", response_model=None)
+def response_model_none_annotation_return_invalid_model() -> User:
+ return Item(name="Foo", price=42.0)
+
+
+@app.get(
+ "/response_model_none-annotation-return_dict_with_extra_data", response_model=None
+)
+def response_model_none_annotation_return_dict_with_extra_data() -> User:
+ return {"name": "John", "surname": "Doe", "password_hash": "secret"}
+
+
+@app.get(
+ "/response_model_none-annotation-return_submodel_with_extra_data",
+ response_model=None,
+)
+def response_model_none_annotation_return_submodel_with_extra_data() -> User:
+ return DBUser(name="John", surname="Doe", password_hash="secret")
+
+
+@app.get(
+ "/response_model_model1-annotation_model2-return_same_model", response_model=User
+)
+def response_model_model1_annotation_model2_return_same_model() -> Item:
+ return User(name="John", surname="Doe")
+
+
+@app.get(
+ "/response_model_model1-annotation_model2-return_exact_dict", response_model=User
+)
+def response_model_model1_annotation_model2_return_exact_dict() -> Item:
+ return {"name": "John", "surname": "Doe"}
+
+
+@app.get(
+ "/response_model_model1-annotation_model2-return_invalid_dict", response_model=User
+)
+def response_model_model1_annotation_model2_return_invalid_dict() -> Item:
+ return {"name": "John"}
+
+
+@app.get(
+ "/response_model_model1-annotation_model2-return_invalid_model", response_model=User
+)
+def response_model_model1_annotation_model2_return_invalid_model() -> Item:
+ return Item(name="Foo", price=42.0)
+
+
+@app.get(
+ "/response_model_model1-annotation_model2-return_dict_with_extra_data",
+ response_model=User,
+)
+def response_model_model1_annotation_model2_return_dict_with_extra_data() -> Item:
+ return {"name": "John", "surname": "Doe", "password_hash": "secret"}
+
+
+@app.get(
+ "/response_model_model1-annotation_model2-return_submodel_with_extra_data",
+ response_model=User,
+)
+def response_model_model1_annotation_model2_return_submodel_with_extra_data() -> Item:
+ return DBUser(name="John", surname="Doe", password_hash="secret")
+
+
+@app.get(
+ "/response_model_filtering_model-annotation_submodel-return_submodel",
+ response_model=User,
+)
+def response_model_filtering_model_annotation_submodel_return_submodel() -> DBUser:
+ return DBUser(name="John", surname="Doe", password_hash="secret")
+
+
+@app.get("/response_model_list_of_model-no_annotation", response_model=List[User])
+def response_model_list_of_model_no_annotation():
+ return [
+ DBUser(name="John", surname="Doe", password_hash="secret"),
+ DBUser(name="Jane", surname="Does", password_hash="secret2"),
+ ]
+
+
+@app.get("/no_response_model-annotation_list_of_model")
+def no_response_model_annotation_list_of_model() -> List[User]:
+ return [
+ DBUser(name="John", surname="Doe", password_hash="secret"),
+ DBUser(name="Jane", surname="Does", password_hash="secret2"),
+ ]
+
+
+@app.get("/no_response_model-annotation_forward_ref_list_of_model")
+def no_response_model_annotation_forward_ref_list_of_model() -> "List[User]":
+ return [
+ DBUser(name="John", surname="Doe", password_hash="secret"),
+ DBUser(name="Jane", surname="Does", password_hash="secret2"),
+ ]
+
+
+@app.get(
+ "/response_model_union-no_annotation-return_model1",
+ response_model=Union[User, Item],
+)
+def response_model_union_no_annotation_return_model1():
+ return DBUser(name="John", surname="Doe", password_hash="secret")
+
+
+@app.get(
+ "/response_model_union-no_annotation-return_model2",
+ response_model=Union[User, Item],
+)
+def response_model_union_no_annotation_return_model2():
+ return Item(name="Foo", price=42.0)
+
+
+@app.get("/no_response_model-annotation_union-return_model1")
+def no_response_model_annotation_union_return_model1() -> Union[User, Item]:
+ return DBUser(name="John", surname="Doe", password_hash="secret")
+
+
+@app.get("/no_response_model-annotation_union-return_model2")
+def no_response_model_annotation_union_return_model2() -> Union[User, Item]:
+ return Item(name="Foo", price=42.0)
+
+
+@app.get("/no_response_model-annotation_response_class")
+def no_response_model_annotation_response_class() -> Response:
+ return Response(content="Foo")
+
+
+@app.get("/no_response_model-annotation_json_response_class")
+def no_response_model_annotation_json_response_class() -> JSONResponse:
+ return JSONResponse(content={"foo": "bar"})
+
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/no_response_model-no_annotation-return_model": {
+ "get": {
+ "summary": "No Response Model No Annotation Return Model",
+ "operationId": "no_response_model_no_annotation_return_model_no_response_model_no_annotation_return_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/no_response_model-no_annotation-return_dict": {
+ "get": {
+ "summary": "No Response Model No Annotation Return Dict",
+ "operationId": "no_response_model_no_annotation_return_dict_no_response_model_no_annotation_return_dict_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/response_model-no_annotation-return_same_model": {
+ "get": {
+ "summary": "Response Model No Annotation Return Same Model",
+ "operationId": "response_model_no_annotation_return_same_model_response_model_no_annotation_return_same_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model-no_annotation-return_exact_dict": {
+ "get": {
+ "summary": "Response Model No Annotation Return Exact Dict",
+ "operationId": "response_model_no_annotation_return_exact_dict_response_model_no_annotation_return_exact_dict_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model-no_annotation-return_invalid_dict": {
+ "get": {
+ "summary": "Response Model No Annotation Return Invalid Dict",
+ "operationId": "response_model_no_annotation_return_invalid_dict_response_model_no_annotation_return_invalid_dict_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model-no_annotation-return_invalid_model": {
+ "get": {
+ "summary": "Response Model No Annotation Return Invalid Model",
+ "operationId": "response_model_no_annotation_return_invalid_model_response_model_no_annotation_return_invalid_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model-no_annotation-return_dict_with_extra_data": {
+ "get": {
+ "summary": "Response Model No Annotation Return Dict With Extra Data",
+ "operationId": "response_model_no_annotation_return_dict_with_extra_data_response_model_no_annotation_return_dict_with_extra_data_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model-no_annotation-return_submodel_with_extra_data": {
+ "get": {
+ "summary": "Response Model No Annotation Return Submodel With Extra Data",
+ "operationId": "response_model_no_annotation_return_submodel_with_extra_data_response_model_no_annotation_return_submodel_with_extra_data_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation-return_same_model": {
+ "get": {
+ "summary": "No Response Model Annotation Return Same Model",
+ "operationId": "no_response_model_annotation_return_same_model_no_response_model_annotation_return_same_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation-return_exact_dict": {
+ "get": {
+ "summary": "No Response Model Annotation Return Exact Dict",
+ "operationId": "no_response_model_annotation_return_exact_dict_no_response_model_annotation_return_exact_dict_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation-return_invalid_dict": {
+ "get": {
+ "summary": "No Response Model Annotation Return Invalid Dict",
+ "operationId": "no_response_model_annotation_return_invalid_dict_no_response_model_annotation_return_invalid_dict_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation-return_invalid_model": {
+ "get": {
+ "summary": "No Response Model Annotation Return Invalid Model",
+ "operationId": "no_response_model_annotation_return_invalid_model_no_response_model_annotation_return_invalid_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation-return_dict_with_extra_data": {
+ "get": {
+ "summary": "No Response Model Annotation Return Dict With Extra Data",
+ "operationId": "no_response_model_annotation_return_dict_with_extra_data_no_response_model_annotation_return_dict_with_extra_data_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation-return_submodel_with_extra_data": {
+ "get": {
+ "summary": "No Response Model Annotation Return Submodel With Extra Data",
+ "operationId": "no_response_model_annotation_return_submodel_with_extra_data_no_response_model_annotation_return_submodel_with_extra_data_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model_none-annotation-return_same_model": {
+ "get": {
+ "summary": "Response Model None Annotation Return Same Model",
+ "operationId": "response_model_none_annotation_return_same_model_response_model_none_annotation_return_same_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/response_model_none-annotation-return_exact_dict": {
+ "get": {
+ "summary": "Response Model None Annotation Return Exact Dict",
+ "operationId": "response_model_none_annotation_return_exact_dict_response_model_none_annotation_return_exact_dict_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/response_model_none-annotation-return_invalid_dict": {
+ "get": {
+ "summary": "Response Model None Annotation Return Invalid Dict",
+ "operationId": "response_model_none_annotation_return_invalid_dict_response_model_none_annotation_return_invalid_dict_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/response_model_none-annotation-return_invalid_model": {
+ "get": {
+ "summary": "Response Model None Annotation Return Invalid Model",
+ "operationId": "response_model_none_annotation_return_invalid_model_response_model_none_annotation_return_invalid_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/response_model_none-annotation-return_dict_with_extra_data": {
+ "get": {
+ "summary": "Response Model None Annotation Return Dict With Extra Data",
+ "operationId": "response_model_none_annotation_return_dict_with_extra_data_response_model_none_annotation_return_dict_with_extra_data_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/response_model_none-annotation-return_submodel_with_extra_data": {
+ "get": {
+ "summary": "Response Model None Annotation Return Submodel With Extra Data",
+ "operationId": "response_model_none_annotation_return_submodel_with_extra_data_response_model_none_annotation_return_submodel_with_extra_data_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/response_model_model1-annotation_model2-return_same_model": {
+ "get": {
+ "summary": "Response Model Model1 Annotation Model2 Return Same Model",
+ "operationId": "response_model_model1_annotation_model2_return_same_model_response_model_model1_annotation_model2_return_same_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model_model1-annotation_model2-return_exact_dict": {
+ "get": {
+ "summary": "Response Model Model1 Annotation Model2 Return Exact Dict",
+ "operationId": "response_model_model1_annotation_model2_return_exact_dict_response_model_model1_annotation_model2_return_exact_dict_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model_model1-annotation_model2-return_invalid_dict": {
+ "get": {
+ "summary": "Response Model Model1 Annotation Model2 Return Invalid Dict",
+ "operationId": "response_model_model1_annotation_model2_return_invalid_dict_response_model_model1_annotation_model2_return_invalid_dict_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model_model1-annotation_model2-return_invalid_model": {
+ "get": {
+ "summary": "Response Model Model1 Annotation Model2 Return Invalid Model",
+ "operationId": "response_model_model1_annotation_model2_return_invalid_model_response_model_model1_annotation_model2_return_invalid_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model_model1-annotation_model2-return_dict_with_extra_data": {
+ "get": {
+ "summary": "Response Model Model1 Annotation Model2 Return Dict With Extra Data",
+ "operationId": "response_model_model1_annotation_model2_return_dict_with_extra_data_response_model_model1_annotation_model2_return_dict_with_extra_data_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model_model1-annotation_model2-return_submodel_with_extra_data": {
+ "get": {
+ "summary": "Response Model Model1 Annotation Model2 Return Submodel With Extra Data",
+ "operationId": "response_model_model1_annotation_model2_return_submodel_with_extra_data_response_model_model1_annotation_model2_return_submodel_with_extra_data_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model_filtering_model-annotation_submodel-return_submodel": {
+ "get": {
+ "summary": "Response Model Filtering Model Annotation Submodel Return Submodel",
+ "operationId": "response_model_filtering_model_annotation_submodel_return_submodel_response_model_filtering_model_annotation_submodel_return_submodel_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model_list_of_model-no_annotation": {
+ "get": {
+ "summary": "Response Model List Of Model No Annotation",
+ "operationId": "response_model_list_of_model_no_annotation_response_model_list_of_model_no_annotation_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Response Model List Of Model No Annotation Response Model List Of Model No Annotation Get",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/User"},
+ }
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation_list_of_model": {
+ "get": {
+ "summary": "No Response Model Annotation List Of Model",
+ "operationId": "no_response_model_annotation_list_of_model_no_response_model_annotation_list_of_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response No Response Model Annotation List Of Model No Response Model Annotation List Of Model Get",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/User"},
+ }
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation_forward_ref_list_of_model": {
+ "get": {
+ "summary": "No Response Model Annotation Forward Ref List Of Model",
+ "operationId": "no_response_model_annotation_forward_ref_list_of_model_no_response_model_annotation_forward_ref_list_of_model_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response No Response Model Annotation Forward Ref List Of Model No Response Model Annotation Forward Ref List Of Model Get",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/User"},
+ }
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model_union-no_annotation-return_model1": {
+ "get": {
+ "summary": "Response Model Union No Annotation Return Model1",
+ "operationId": "response_model_union_no_annotation_return_model1_response_model_union_no_annotation_return_model1_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Response Model Union No Annotation Return Model1 Response Model Union No Annotation Return Model1 Get",
+ "anyOf": [
+ {"$ref": "#/components/schemas/User"},
+ {"$ref": "#/components/schemas/Item"},
+ ],
+ }
+ }
+ },
+ }
+ },
+ }
+ },
+ "/response_model_union-no_annotation-return_model2": {
+ "get": {
+ "summary": "Response Model Union No Annotation Return Model2",
+ "operationId": "response_model_union_no_annotation_return_model2_response_model_union_no_annotation_return_model2_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Response Model Union No Annotation Return Model2 Response Model Union No Annotation Return Model2 Get",
+ "anyOf": [
+ {"$ref": "#/components/schemas/User"},
+ {"$ref": "#/components/schemas/Item"},
+ ],
+ }
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation_union-return_model1": {
+ "get": {
+ "summary": "No Response Model Annotation Union Return Model1",
+ "operationId": "no_response_model_annotation_union_return_model1_no_response_model_annotation_union_return_model1_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response No Response Model Annotation Union Return Model1 No Response Model Annotation Union Return Model1 Get",
+ "anyOf": [
+ {"$ref": "#/components/schemas/User"},
+ {"$ref": "#/components/schemas/Item"},
+ ],
+ }
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation_union-return_model2": {
+ "get": {
+ "summary": "No Response Model Annotation Union Return Model2",
+ "operationId": "no_response_model_annotation_union_return_model2_no_response_model_annotation_union_return_model2_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response No Response Model Annotation Union Return Model2 No Response Model Annotation Union Return Model2 Get",
+ "anyOf": [
+ {"$ref": "#/components/schemas/User"},
+ {"$ref": "#/components/schemas/Item"},
+ ],
+ }
+ }
+ },
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation_response_class": {
+ "get": {
+ "summary": "No Response Model Annotation Response Class",
+ "operationId": "no_response_model_annotation_response_class_no_response_model_annotation_response_class_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation_json_response_class": {
+ "get": {
+ "summary": "No Response Model Annotation Json Response Class",
+ "operationId": "no_response_model_annotation_json_response_class_no_response_model_annotation_json_response_class_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Item": {
+ "title": "Item",
+ "required": ["name", "price"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "price": {"title": "Price", "type": "number"},
+ },
+ },
+ "User": {
+ "title": "User",
+ "required": ["name", "surname"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "surname": {"title": "Surname", "type": "string"},
+ },
+ },
+ }
+ },
+}
+
+
+client = TestClient(app)
+
+
+def test_openapi_schema():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == openapi_schema
+
+
+def test_no_response_model_no_annotation_return_model():
+ response = client.get("/no_response_model-no_annotation-return_model")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_no_response_model_no_annotation_return_dict():
+ response = client.get("/no_response_model-no_annotation-return_dict")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_no_annotation_return_same_model():
+ response = client.get("/response_model-no_annotation-return_same_model")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_no_annotation_return_exact_dict():
+ response = client.get("/response_model-no_annotation-return_exact_dict")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_no_annotation_return_invalid_dict():
+ with pytest.raises(ValidationError):
+ client.get("/response_model-no_annotation-return_invalid_dict")
+
+
+def test_response_model_no_annotation_return_invalid_model():
+ with pytest.raises(ValidationError):
+ client.get("/response_model-no_annotation-return_invalid_model")
+
+
+def test_response_model_no_annotation_return_dict_with_extra_data():
+ response = client.get("/response_model-no_annotation-return_dict_with_extra_data")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_no_annotation_return_submodel_with_extra_data():
+ response = client.get(
+ "/response_model-no_annotation-return_submodel_with_extra_data"
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_no_response_model_annotation_return_same_model():
+ response = client.get("/no_response_model-annotation-return_same_model")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_no_response_model_annotation_return_exact_dict():
+ response = client.get("/no_response_model-annotation-return_exact_dict")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_no_response_model_annotation_return_invalid_dict():
+ with pytest.raises(ValidationError):
+ client.get("/no_response_model-annotation-return_invalid_dict")
+
+
+def test_no_response_model_annotation_return_invalid_model():
+ with pytest.raises(ValidationError):
+ client.get("/no_response_model-annotation-return_invalid_model")
+
+
+def test_no_response_model_annotation_return_dict_with_extra_data():
+ response = client.get("/no_response_model-annotation-return_dict_with_extra_data")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_no_response_model_annotation_return_submodel_with_extra_data():
+ response = client.get(
+ "/no_response_model-annotation-return_submodel_with_extra_data"
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_none_annotation_return_same_model():
+ response = client.get("/response_model_none-annotation-return_same_model")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_none_annotation_return_exact_dict():
+ response = client.get("/response_model_none-annotation-return_exact_dict")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_none_annotation_return_invalid_dict():
+ response = client.get("/response_model_none-annotation-return_invalid_dict")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John"}
+
+
+def test_response_model_none_annotation_return_invalid_model():
+ response = client.get("/response_model_none-annotation-return_invalid_model")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "Foo", "price": 42.0}
+
+
+def test_response_model_none_annotation_return_dict_with_extra_data():
+ response = client.get("/response_model_none-annotation-return_dict_with_extra_data")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "name": "John",
+ "surname": "Doe",
+ "password_hash": "secret",
+ }
+
+
+def test_response_model_none_annotation_return_submodel_with_extra_data():
+ response = client.get(
+ "/response_model_none-annotation-return_submodel_with_extra_data"
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "name": "John",
+ "surname": "Doe",
+ "password_hash": "secret",
+ }
+
+
+def test_response_model_model1_annotation_model2_return_same_model():
+ response = client.get("/response_model_model1-annotation_model2-return_same_model")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_model1_annotation_model2_return_exact_dict():
+ response = client.get("/response_model_model1-annotation_model2-return_exact_dict")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_model1_annotation_model2_return_invalid_dict():
+ with pytest.raises(ValidationError):
+ client.get("/response_model_model1-annotation_model2-return_invalid_dict")
+
+
+def test_response_model_model1_annotation_model2_return_invalid_model():
+ with pytest.raises(ValidationError):
+ client.get("/response_model_model1-annotation_model2-return_invalid_model")
+
+
+def test_response_model_model1_annotation_model2_return_dict_with_extra_data():
+ response = client.get(
+ "/response_model_model1-annotation_model2-return_dict_with_extra_data"
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_model1_annotation_model2_return_submodel_with_extra_data():
+ response = client.get(
+ "/response_model_model1-annotation_model2-return_submodel_with_extra_data"
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_filtering_model_annotation_submodel_return_submodel():
+ response = client.get(
+ "/response_model_filtering_model-annotation_submodel-return_submodel"
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_list_of_model_no_annotation():
+ response = client.get("/response_model_list_of_model-no_annotation")
+ assert response.status_code == 200, response.text
+ assert response.json() == [
+ {"name": "John", "surname": "Doe"},
+ {"name": "Jane", "surname": "Does"},
+ ]
+
+
+def test_no_response_model_annotation_list_of_model():
+ response = client.get("/no_response_model-annotation_list_of_model")
+ assert response.status_code == 200, response.text
+ assert response.json() == [
+ {"name": "John", "surname": "Doe"},
+ {"name": "Jane", "surname": "Does"},
+ ]
+
+
+def test_no_response_model_annotation_forward_ref_list_of_model():
+ response = client.get("/no_response_model-annotation_forward_ref_list_of_model")
+ assert response.status_code == 200, response.text
+ assert response.json() == [
+ {"name": "John", "surname": "Doe"},
+ {"name": "Jane", "surname": "Does"},
+ ]
+
+
+def test_response_model_union_no_annotation_return_model1():
+ response = client.get("/response_model_union-no_annotation-return_model1")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_response_model_union_no_annotation_return_model2():
+ response = client.get("/response_model_union-no_annotation-return_model2")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "Foo", "price": 42.0}
+
+
+def test_no_response_model_annotation_union_return_model1():
+ response = client.get("/no_response_model-annotation_union-return_model1")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "John", "surname": "Doe"}
+
+
+def test_no_response_model_annotation_union_return_model2():
+ response = client.get("/no_response_model-annotation_union-return_model2")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "Foo", "price": 42.0}
+
+
+def test_no_response_model_annotation_return_class():
+ response = client.get("/no_response_model-annotation_response_class")
+ assert response.status_code == 200, response.text
+ assert response.text == "Foo"
+
+
+def test_no_response_model_annotation_json_response_class():
+ response = client.get("/no_response_model-annotation_json_response_class")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"foo": "bar"}
+
+
+def test_invalid_response_model_field():
+ app = FastAPI()
+ with pytest.raises(FastAPIError) as e:
+
+ @app.get("/")
+ def read_root() -> Union[Response, None]:
+ return Response(content="Foo") # pragma: no cover
+
+ assert "valid Pydantic field type" in e.value.args[0]
+ assert "parameter response_model=None" in e.value.args[0]
diff --git a/tests/test_route_scope.py b/tests/test_route_scope.py
index a188e9a5f..2021c828f 100644
--- a/tests/test_route_scope.py
+++ b/tests/test_route_scope.py
@@ -46,5 +46,5 @@ def test_websocket():
def test_websocket_invalid_path_doesnt_match():
with pytest.raises(WebSocketDisconnect):
- with client.websocket_connect("/itemsx/portal-gun") as websocket:
- websocket.receive_json()
+ with client.websocket_connect("/itemsx/portal-gun"):
+ pass
diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py
index 34aeb0be5..f5597e30c 100644
--- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py
+++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py
@@ -54,7 +54,7 @@ def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
# TODO: remove this once Pydantic 1.9 is released
- # Ref: https://github.com/samuelcolvin/pydantic/pull/2557
+ # Ref: https://github.com/pydantic/pydantic/pull/2557
data = response.json()
alternative_data1 = deepcopy(data)
alternative_data2 = deepcopy(data)
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py
new file mode 100644
index 000000000..39a4734ed
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py
@@ -0,0 +1,120 @@
+from fastapi.testclient import TestClient
+
+from docs_src.response_model.tutorial003_01 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/user/": {
+ "post": {
+ "summary": "Create User",
+ "operationId": "create_user_user__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/UserIn"}
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/BaseUser"}
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "BaseUser": {
+ "title": "BaseUser",
+ "required": ["username", "email"],
+ "type": "object",
+ "properties": {
+ "username": {"title": "Username", "type": "string"},
+ "email": {"title": "Email", "type": "string", "format": "email"},
+ "full_name": {"title": "Full Name", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "UserIn": {
+ "title": "UserIn",
+ "required": ["username", "email", "password"],
+ "type": "object",
+ "properties": {
+ "username": {"title": "Username", "type": "string"},
+ "email": {"title": "Email", "type": "string", "format": "email"},
+ "full_name": {"title": "Full Name", "type": "string"},
+ "password": {"title": "Password", "type": "string"},
+ },
+ },
+ "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"},
+ },
+ },
+ }
+ },
+}
+
+
+def test_openapi_schema():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == openapi_schema
+
+
+def test_post_user():
+ response = client.post(
+ "/user/",
+ json={
+ "username": "foo",
+ "password": "fighter",
+ "email": "foo@example.com",
+ "full_name": "Grave Dohl",
+ },
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "username": "foo",
+ "email": "foo@example.com",
+ "full_name": "Grave Dohl",
+ }
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py
new file mode 100644
index 000000000..3a04db6bc
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py
@@ -0,0 +1,129 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/user/": {
+ "post": {
+ "summary": "Create User",
+ "operationId": "create_user_user__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/UserIn"}
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/BaseUser"}
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "BaseUser": {
+ "title": "BaseUser",
+ "required": ["username", "email"],
+ "type": "object",
+ "properties": {
+ "username": {"title": "Username", "type": "string"},
+ "email": {"title": "Email", "type": "string", "format": "email"},
+ "full_name": {"title": "Full Name", "type": "string"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "UserIn": {
+ "title": "UserIn",
+ "required": ["username", "email", "password"],
+ "type": "object",
+ "properties": {
+ "username": {"title": "Username", "type": "string"},
+ "email": {"title": "Email", "type": "string", "format": "email"},
+ "full_name": {"title": "Full Name", "type": "string"},
+ "password": {"title": "Password", "type": "string"},
+ },
+ },
+ "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"},
+ },
+ },
+ }
+ },
+}
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.response_model.tutorial003_01_py310 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == openapi_schema
+
+
+@needs_py310
+def test_post_user(client: TestClient):
+ response = client.post(
+ "/user/",
+ json={
+ "username": "foo",
+ "password": "fighter",
+ "email": "foo@example.com",
+ "full_name": "Grave Dohl",
+ },
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "username": "foo",
+ "email": "foo@example.com",
+ "full_name": "Grave Dohl",
+ }
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_02.py b/tests/test_tutorial/test_response_model/test_tutorial003_02.py
new file mode 100644
index 000000000..d933f871c
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_02.py
@@ -0,0 +1,93 @@
+from fastapi.testclient import TestClient
+
+from docs_src.response_model.tutorial003_02 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/portal": {
+ "get": {
+ "summary": "Get Portal",
+ "operationId": "get_portal_portal_get",
+ "parameters": [
+ {
+ "required": False,
+ "schema": {
+ "title": "Teleport",
+ "type": "boolean",
+ "default": False,
+ },
+ "name": "teleport",
+ "in": "query",
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "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"},
+ },
+ },
+ }
+ },
+}
+
+
+def test_openapi_schema():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == openapi_schema
+
+
+def test_get_portal():
+ response = client.get("/portal")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"message": "Here's your interdimensional portal."}
+
+
+def test_get_redirect():
+ response = client.get("/portal", params={"teleport": True}, follow_redirects=False)
+ assert response.status_code == 307, response.text
+ assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_03.py b/tests/test_tutorial/test_response_model/test_tutorial003_03.py
new file mode 100644
index 000000000..398eb4765
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_03.py
@@ -0,0 +1,36 @@
+from fastapi.testclient import TestClient
+
+from docs_src.response_model.tutorial003_03 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/teleport": {
+ "get": {
+ "summary": "Get Teleport",
+ "operationId": "get_teleport_teleport_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ }
+ },
+}
+
+
+def test_openapi_schema():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == openapi_schema
+
+
+def test_get_portal():
+ response = client.get("/teleport", follow_redirects=False)
+ assert response.status_code == 307, response.text
+ assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_04.py b/tests/test_tutorial/test_response_model/test_tutorial003_04.py
new file mode 100644
index 000000000..4aa80145a
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_04.py
@@ -0,0 +1,9 @@
+import pytest
+from fastapi.exceptions import FastAPIError
+
+
+def test_invalid_response_model():
+ with pytest.raises(FastAPIError):
+ from docs_src.response_model.tutorial003_04 import app
+
+ assert app # pragma: no cover
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py
new file mode 100644
index 000000000..b876facc8
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py
@@ -0,0 +1,12 @@
+import pytest
+from fastapi.exceptions import FastAPIError
+
+from ...utils import needs_py310
+
+
+@needs_py310
+def test_invalid_response_model():
+ with pytest.raises(FastAPIError):
+ from docs_src.response_model.tutorial003_04_py310 import app
+
+ assert app # pragma: no cover
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05.py b/tests/test_tutorial/test_response_model/test_tutorial003_05.py
new file mode 100644
index 000000000..27896d490
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_05.py
@@ -0,0 +1,93 @@
+from fastapi.testclient import TestClient
+
+from docs_src.response_model.tutorial003_05 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/portal": {
+ "get": {
+ "summary": "Get Portal",
+ "operationId": "get_portal_portal_get",
+ "parameters": [
+ {
+ "required": False,
+ "schema": {
+ "title": "Teleport",
+ "type": "boolean",
+ "default": False,
+ },
+ "name": "teleport",
+ "in": "query",
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "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"},
+ },
+ },
+ }
+ },
+}
+
+
+def test_openapi_schema():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == openapi_schema
+
+
+def test_get_portal():
+ response = client.get("/portal")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"message": "Here's your interdimensional portal."}
+
+
+def test_get_redirect():
+ response = client.get("/portal", params={"teleport": True}, follow_redirects=False)
+ assert response.status_code == 307, response.text
+ assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py
new file mode 100644
index 000000000..bf36c906b
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py
@@ -0,0 +1,103 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/portal": {
+ "get": {
+ "summary": "Get Portal",
+ "operationId": "get_portal_portal_get",
+ "parameters": [
+ {
+ "required": False,
+ "schema": {
+ "title": "Teleport",
+ "type": "boolean",
+ "default": False,
+ },
+ "name": "teleport",
+ "in": "query",
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "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"},
+ },
+ },
+ }
+ },
+}
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.response_model.tutorial003_05_py310 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == openapi_schema
+
+
+@needs_py310
+def test_get_portal(client: TestClient):
+ response = client.get("/portal")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"message": "Here's your interdimensional portal."}
+
+
+@needs_py310
+def test_get_redirect(client: TestClient):
+ response = client.get("/portal", params={"teleport": True}, follow_redirects=False)
+ assert response.status_code == 307, response.text
+ assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"