Skip to content

Commit

Permalink
fix(router): support PEP 604 unions (pipe operator)
Browse files Browse the repository at this point in the history
  • Loading branch information
certator committed Nov 5, 2024
1 parent a09dfd5 commit f460291
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

- uses: actions/setup-python@v1
with:
python-version: 3.9
python-version: 3.11

- run: pip install pre-commit
- run: pre-commit run --all-files
Expand Down
20 changes: 10 additions & 10 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
default_language_version:
python: python3.9
python: python3.11

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: ^.*\.md$
Expand All @@ -19,37 +19,37 @@ repos:
- id: check-merge-conflict

- repo: https://github.com/jorisroovers/gitlint
rev: v0.17.0
rev: v0.19.1
hooks:
- id: gitlint

- repo: https://github.com/adrienverge/yamllint
rev: v1.26.3
rev: v1.35.1
hooks:
- id: yamllint

- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.30.0
rev: v0.42.0
hooks:
- id: markdownlint
language_version: system

- repo: https://github.com/PyCQA/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
# extra dependencies for config in pyproject.toml
additional_dependencies: ['toml', 'pyproject']
args: [ "--profile", "black" ]

- repo: https://github.com/psf/black
rev: 22.3.0
rev: 24.10.0
hooks:
- id: black
args: [--fast, --target-version=py39]

- repo: https://github.com/PyCQA/pylint
rev: v2.12.2
rev: v3.3.1
hooks:
- id: pylint
exclude: ^.*(docs/).*$
Expand All @@ -58,14 +58,14 @@ repos:
additional_dependencies: ['toml', 'pyproject']

- repo: https://github.com/PyCQA/pydocstyle
rev: 6.1.1
rev: 6.3.0
hooks:
- id: pydocstyle
exclude: ^.*(migrations/).*$
additional_dependencies: [ 'toml', 'pyproject' ]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.1.1
rev: v1.13.0
hooks:
- id: mypy
args: []
Expand Down
2 changes: 1 addition & 1 deletion flask_ninja/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class MediaType(BaseModel):
class ParameterBase(BaseModel):
description: Optional[str] = None
required: Optional[bool] = None
deprecated: Optional[bool] = None
deprecated: Union[Any, str, bool, None] = None
# Serialization rules for simple scenarios
style: Optional[str] = None
explode: Optional[bool] = None
Expand Down
4 changes: 2 additions & 2 deletions flask_ninja/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pydantic import BaseModel, ConfigDict, ValidationError

from .constants import NOT_SET, ApiConfigError, ParamType
from .model_field import FieldMapping, ModelField, Undefined
from .model_field import FieldMapping, ModelField, Undefined, UnionType
from .models import MediaType
from .models import Operation as OAPIOperation
from .models import (
Expand Down Expand Up @@ -158,7 +158,7 @@ def _sanitize_responses(

# Check if for each returned type there is implicitly or explicitly defined response model and code
if func_return_type:
if get_origin(func_return_type) == Union:
if get_origin(func_return_type) in [Union, UnionType]:
for ret_type in func_return_type.__args__:
if not any(resp.type_ != ret_type for resp in responses.values()):
raise ApiConfigError(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "flask-ninja"
version = "1.3.0"
version = "1.3.1"
description = "Flask Ninja is a web framework for building APIs with Flask and Python 3.9+ type hints."
readme = "README.md"
authors = ["Michal Korbela <[email protected]>"]
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/snapshots/test_api/test_get_schema/api_schema
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"openapi": "3.1.0", "info": {"title": "", "description": "", "version": "1.0.0"}, "paths": {"/some_endpoint/{param}": {"get": {"summary": "", "description": "", "parameters": [{"required": true, "schema": {"type": "integer"}, "name": "param", "in": "path"}], "requestBody": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Server"}}}, "required": true}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "integer"}}}}}, "security": [{"bearerTokenAuth": []}]}}}, "components": {"schemas": {"Server": {"additionalProperties": true, "properties": {"url": {"anyOf": [{"format": "uri", "minLength": 1, "type": "string"}, {"type": "string"}], "title": "Url"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Description"}, "variables": {"anyOf": [{"additionalProperties": {"$ref": "#/components/schemas/ServerVariable"}, "type": "object"}, {"type": "null"}], "default": null, "title": "Variables"}}, "required": ["url"], "title": "Server", "type": "object"}, "ServerVariable": {"additionalProperties": true, "properties": {"enum": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "default": null, "title": "Enum"}, "default": {"title": "Default", "type": "string"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Description"}}, "required": ["default"], "title": "ServerVariable", "type": "object"}}, "securitySchemes": {"bearerTokenAuth": {"type": "http", "scheme": "bearer"}}}}
{"openapi": "3.1.0", "info": {"title": "", "description": "", "version": "1.0.0"}, "paths": {"/some_endpoint/{param}": {"get": {"summary": "", "description": "", "parameters": [{"required": true, "schema": {"type": "integer"}, "name": "param", "in": "path"}], "requestBody": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Server"}}}, "required": true}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "integer"}}}}}, "security": [{"bearerTokenAuth": []}]}}}, "components": {"schemas": {"Server": {"additionalProperties": true, "properties": {"url": {"anyOf": [{"format": "uri", "minLength": 1, "type": "string"}, {"type": "string"}], "title": "Url"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Description"}, "variables": {"anyOf": [{"additionalProperties": {"$ref": "#/components/schemas/ServerVariable"}, "type": "object"}, {"type": "null"}], "default": null, "title": "Variables"}}, "required": ["url"], "title": "Server", "type": "object"}, "ServerVariable": {"additionalProperties": true, "properties": {"enum": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "default": null, "title": "Enum"}, "default": {"type": "string", "title": "Default"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Description"}}, "required": ["default"], "title": "ServerVariable", "type": "object"}}, "securitySchemes": {"bearerTokenAuth": {"type": "http", "scheme": "bearer"}}}}
48 changes: 48 additions & 0 deletions tests/unit/test_router.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import Union

import pytest
from pydantic import BaseModel

from flask_ninja import Query
from flask_ninja.operation import Callback, Operation
Expand Down Expand Up @@ -43,6 +46,51 @@ def sample_method():
assert router.operations[0].params == [param]


def test_add_route_union():
router = Router()
callback = Callback(
name="some_name",
url="some_url",
method="some_callback_method",
response_codes={},
)

param = create_model_field(name="some_param", type_=int, field_info=Query())

class Response200(BaseModel):
status: str

class Response400(BaseModel):
error: str

@router.add_route(
"GET",
"/foo",
responses={200: Response200, 400: Response400},
auth="some_auth",
summary="some_summary",
description="some_description",
params=[param],
callbacks=[callback],
)
def sample_method() -> Union[Response200, Response400]:
return Response200(status="foo")

assert len(router.operations) == 1
assert router.operations[0].path == "/foo"
assert router.operations[0].method == "GET"
assert str(router.operations[0].responses) == str(
{
200: create_model_field(name="Response 200", type_=Response200),
400: create_model_field(name="Response 400", type_=Response400),
}
)
assert router.operations[0].callbacks == [callback]
assert router.operations[0].summary == "some_summary"
assert router.operations[0].description == "some_description"
assert router.operations[0].params == [param]


def test_add_route_no_params():
router = Router()

Expand Down
51 changes: 51 additions & 0 deletions tests/unit/test_router_py3_10.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from pydantic import BaseModel

from flask_ninja import Query
from flask_ninja.operation import Callback
from flask_ninja.router import Router
from flask_ninja.utils import create_model_field


def test_add_route_union():
router = Router()
callback = Callback(
name="some_name",
url="some_url",
method="some_callback_method",
response_codes={},
)

param = create_model_field(name="some_param", type_=int, field_info=Query())

class Response200(BaseModel):
status: str

class Response400(BaseModel):
error: str

@router.add_route(
"GET",
"/foo",
responses={200: Response200, 400: Response400},
auth="some_auth",
summary="some_summary",
description="some_description",
params=[param],
callbacks=[callback],
)
def sample_method() -> Response200 | Response400:
return Response200(status="foo")

assert len(router.operations) == 1
assert router.operations[0].path == "/foo"
assert router.operations[0].method == "GET"
assert str(router.operations[0].responses) == str(
{
200: create_model_field(name="Response 200", type_=Response200),
400: create_model_field(name="Response 400", type_=Response400),
}
)
assert router.operations[0].callbacks == [callback]
assert router.operations[0].summary == "some_summary"
assert router.operations[0].description == "some_description"
assert router.operations[0].params == [param]
9 changes: 8 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py3.9, py3.10, py3.11
envlist = py39, py310, py311
isolated_build = True

[testenv]
Expand All @@ -9,3 +9,10 @@ deps = pytest
commands =
# NOTE: you can run any command line tool here - not just tests
pytest

[testenv:py39]
deps = pytest
pytest-snapshot
commands =
# NOTE: test modules with _py3_10 suffix contain py3.9-incompatible syntax
pytest --ignore-glob='*_py3_10.py'

0 comments on commit f460291

Please sign in to comment.