Merge pull request #25 from Maillol/user_can_add_tags_to_generated_oas

We can add custom tags to generated OPS
This commit is contained in:
MAILLOL Vincent 2021-08-22 15:03:04 +02:00 committed by GitHub
commit 1b10ebbcfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 173 additions and 40 deletions

View File

@ -269,6 +269,8 @@ Open Api Specification.
async def get(self) -> r200[List[Pet]]:
"""
Find all pets
Tags: pet
"""
pets = self.request.app["model"].list_pets()
return web.json_response([pet.dict() for pet in pets])
@ -277,6 +279,7 @@ Open Api Specification.
"""
Add a new pet to the store
Tags: pet
Status Codes:
201: The pet is created
"""
@ -289,6 +292,7 @@ Open Api Specification.
"""
Find a pet by ID
Tags: pet
Status Codes:
200: Successful operation
404: Pet not found
@ -300,6 +304,7 @@ Open Api Specification.
"""
Update an existing pet
Tags: pet
Status Codes:
200: successful operation
"""

View File

@ -4,7 +4,7 @@ Utility to extract extra OAS description from docstring.
import re
import textwrap
from typing import Dict
from typing import Dict, List
class LinesIterator:
@ -47,11 +47,10 @@ def _i_extract_block(lines: LinesIterator):
except StopIteration:
return
# Get the size of the indentation.
if (match := re.search("^ +", line)) is None:
return # No block to extract.
indent = match.group()
yield line[len(indent) :]
indent = re.fullmatch("( *).*", line).groups()[0]
indentation = len(indent)
start_of_other_block = re.compile(f" {{0,{indentation}}}[^ ].*")
yield line[indentation:]
# Yield lines until the indentation is the same or is greater than
# the first block line.
@ -59,8 +58,8 @@ def _i_extract_block(lines: LinesIterator):
line = next(lines)
except StopIteration:
return
while (is_empty := line.strip() == "") or line.startswith(indent):
yield "" if is_empty else line[len(indent) :]
while not start_of_other_block.fullmatch(line):
yield line[indentation:]
try:
line = next(lines)
except StopIteration:
@ -87,10 +86,13 @@ def status_code(docstring: str) -> Dict[int, str]:
iterator = LinesIterator(docstring)
for line in iterator:
if re.fullmatch("status\\s+codes?\\s*:", line, re.IGNORECASE):
iterator.rewind()
blocks = []
lines = []
for line_of_block in _i_extract_block(iterator):
if re.search("^\\d{3}\\s*:", line_of_block):
i_block = _i_extract_block(iterator)
next(i_block)
for line_of_block in i_block:
if re.search("^\\s*\\d{3}\\s*:", line_of_block):
if lines:
blocks.append("\n".join(lines))
lines = []
@ -105,6 +107,19 @@ def status_code(docstring: str) -> Dict[int, str]:
return {}
def tags(docstring: str) -> List[str]:
"""
Extract the "Tags:" block of the docstring.
"""
iterator = LinesIterator(docstring)
for line in iterator:
if re.fullmatch("tags\\s*:.*", line, re.IGNORECASE):
iterator.rewind()
lines = " ".join(_i_extract_block(iterator))
return [" ".join(e.split()) for e in re.split("[,;]", lines.split(":")[1])]
return []
def operation(docstring: str) -> str:
"""
Extract all docstring except the "Status Code:" block.
@ -112,7 +127,8 @@ def operation(docstring: str) -> str:
lines = LinesIterator(docstring)
ret = []
for line in lines:
if re.fullmatch("status\\s+codes?\\s*:", line, re.IGNORECASE):
if re.fullmatch("status\\s+codes?\\s*:|tags\\s*:.*", line, re.IGNORECASE):
lines.rewind()
for _ in _i_extract_block(lines):
pass
else:

View File

@ -2,7 +2,7 @@
Utility to write Open Api Specifications using the Python language.
"""
from typing import Union
from typing import Union, List
class Info:
@ -157,7 +157,7 @@ class Responses:
self._spec = spec.setdefault("responses", {})
def __getitem__(self, status_code: Union[int, str]) -> Response:
if not (100 <= int(status_code) < 600):
if not 100 <= int(status_code) < 600:
raise ValueError("status_code must be between 100 and 599")
spec = self._spec.setdefault(str(status_code), {})
@ -196,6 +196,17 @@ class OperationObject:
def responses(self) -> Responses:
return Responses(self._spec)
@property
def tags(self) -> List[str]:
return self._spec.get("tags", [])[:]
@tags.setter
def tags(self, tags: List[str]):
if tags:
self._spec["tags"] = tags[:]
else:
self._spec.pop("tags", None)
class PathItem:
def __init__(self, spec: dict):

View File

@ -1,7 +1,7 @@
import typing
from inspect import getdoc
from itertools import count
from typing import List, Type, Optional, Dict
from typing import List, Type, Optional
from aiohttp.web import Response, json_response
from aiohttp.web_app import Application
@ -86,6 +86,7 @@ def _add_http_method_to_oas(
description = getdoc(handler)
if description:
oas_operation.description = docstring_parser.operation(description)
oas_operation.tags = docstring_parser.tags(description)
status_code_descriptions = docstring_parser.status_code(description)
else:
status_code_descriptions = {}

View File

@ -1,6 +1,6 @@
from functools import update_wrapper
from inspect import iscoroutinefunction
from typing import Any, Callable, Generator, Iterable, Set, ClassVar, Literal
from typing import Any, Callable, Generator, Iterable, Set, ClassVar
import warnings
from aiohttp.abc import AbstractView

View File

@ -20,7 +20,6 @@ class ArticleModels(BaseModel):
class ArticleView(PydanticView):
async def post(self, article: ArticleModel):
return web.json_response(article.dict())

View File

@ -1,5 +1,8 @@
from textwrap import dedent
from aiohttp_pydantic.oas.docstring_parser import (
status_code,
tags,
operation,
_i_extract_block,
LinesIterator,
@ -13,6 +16,13 @@ def web_handler():
bla bla bla
Tags: tag1, tag2
, tag3,
t a
g
4
Status Codes:
200: line 1
@ -36,6 +46,19 @@ def web_handler():
"""
def web_handler_2():
"""
bla bla bla
Tags: tag1
Status Codes:
200: line 1
bla bla
"""
def test_lines_iterator():
lines_iterator = LinesIterator("AAAA\nBBBB")
with pytest.raises(StopIteration):
@ -61,28 +84,72 @@ def test_status_code():
assert status_code(getdoc(web_handler)) == expected
def test_tags():
expected = ["tag1", "tag2", "tag3", "t a g 4"]
assert tags(getdoc(web_handler)) == expected
def test_operation():
expected = "bla bla bla\n\n\nbla bla"
assert operation(getdoc(web_handler)) == expected
assert operation(getdoc(web_handler_2)) == expected
def test_i_extract_block():
lines = LinesIterator(" aaaa\n\n bbbb\n\n cccc\n dddd")
blocks = dedent(
"""
aaaa:
bbbb
cccc
dddd
"""
)
lines = LinesIterator(blocks)
text = "\n".join(_i_extract_block(lines))
assert text == """aaaa\n\n bbbb\n\ncccc"""
assert text == """aaaa:\n\n bbbb\n\n cccc"""
blocks = dedent(
"""
aaaa:
bbbb
cccc
dddd
"""
)
lines = LinesIterator(blocks)
text = "\n".join(_i_extract_block(lines))
assert text == """aaaa:\n\n bbbb\n\n cccc\n"""
blocks = dedent(
"""
aaaa:
bbbb
cccc
"""
)
lines = LinesIterator(blocks)
text = "\n".join(_i_extract_block(lines))
assert text == """aaaa:\n\n bbbb\n\n cccc"""
lines = LinesIterator("")
text = "\n".join(_i_extract_block(lines))
assert text == ""
lines = LinesIterator("aaaa\n bbbb") # the indented block is cut by a new block
text = "\n".join(_i_extract_block(lines))
assert text == ""
lines = LinesIterator("\n")
text = "\n".join(_i_extract_block(lines))
assert text == ""
lines = LinesIterator(" aaaa\n bbbb")
lines = LinesIterator("aaaa:")
text = "\n".join(_i_extract_block(lines))
assert text == "aaaa\nbbbb"
assert text == "aaaa:"

View File

@ -119,6 +119,24 @@ def test_paths_operation_requestBody():
}
def test_paths_operation_tags():
oas = OpenApiSpec3()
operation = oas.paths["/users/{petId}"].get
assert operation.tags == []
operation.tags = ['pets']
assert oas.spec['paths']['/users/{petId}'] == {
'get': {
'tags': ['pets']
}
}
operation.tags = []
assert oas.spec['paths']['/users/{petId}'] == {
'get': {}
}
def test_paths_operation_responses():
oas = OpenApiSpec3()
response = oas.paths["/users/{petId}"].get.responses[200]

View File

@ -35,6 +35,7 @@ class PetCollectionView(PydanticView):
"""
Get a list of pets
Tags: pet
Status Codes:
200: Successful operation
"""
@ -46,7 +47,13 @@ class PetCollectionView(PydanticView):
class PetItemView(PydanticView):
async def get(self, id: int, /, size: Union[int, Literal['x', 'l', 's']], day: Union[int, Literal["now"]] = "now") -> Union[r200[Pet], r404]:
async def get(
self,
id: int,
/,
size: Union[int, Literal["x", "l", "s"]],
day: Union[int, Literal["now"]] = "now",
) -> Union[r200[Pet], r404]:
return web.json_response()
async def put(self, id: int, /, pet: Pet):
@ -120,6 +127,7 @@ async def test_generated_oas_should_have_pets_paths(generated_oas):
async def test_pets_route_should_have_get_method(generated_oas):
assert generated_oas["paths"]["/pets"]["get"] == {
"description": "Get a list of pets",
"tags": ["pet"],
"parameters": [
{
"in": "query",
@ -246,20 +254,28 @@ async def test_pets_id_route_should_have_get_method(generated_oas):
"required": True,
"schema": {"title": "id", "type": "integer"},
},
{'in': 'query',
'name': 'size',
'required': True,
'schema': {'anyOf': [{'type': 'integer'},
{'enum': ['x', 'l', 's'],
'type': 'string'}],
'title': 'size'}},
{'in': 'query',
'name': 'day',
'required': False,
'schema': {'anyOf': [{'type': 'integer'},
{'enum': ['now'], 'type': 'string'}],
'default': 'now',
'title': 'day'}}
{
"in": "query",
"name": "size",
"required": True,
"schema": {
"anyOf": [
{"type": "integer"},
{"enum": ["x", "l", "s"], "type": "string"},
],
"title": "size",
},
},
{
"in": "query",
"name": "day",
"required": False,
"schema": {
"anyOf": [{"type": "integer"}, {"enum": ["now"], "type": "string"}],
"default": "now",
"title": "day",
},
},
],
"responses": {
"200": {