sanic-openapi
sanic-openapi copied to clipboard
openapi.component needlessly inspects properties, gets stuck
import dataclasses
from sanic_ext.extensions.openapi import openapi
@dataclasses.dataclass
class Path:
x: [float]
y: [float]
@property
def reversed(self) -> "Path": # Recurses
return Path(
x=self.x[::-1],
y=self.y[::-1],
)
openapi.component(Path) # Infinite loop
...
File "/home/sir/venv/py3/lib/python3.10/site-packages/sanic_ext/extensions/openapi/types.py", line 446, in _extract
hints = get_type_hints(item.fget)
File "/usr/lib/python3.10/typing.py", line 1871, in get_type_hints
value = _eval_type(value, globalns, localns)
File "/usr/lib/python3.10/typing.py", line 327, in _eval_type
return t._evaluate(globalns, localns, recursive_guard)
File "/usr/lib/python3.10/typing.py", line 694, in _evaluate
eval(self.__forward_code__, globalns, localns),
File "<string>", line 1, in <module>
RecursionError: maximum recursion depth exceeded while calling a Python object
This is failing due to:
#openapi/types.py
return cls(
{
k: Schema.make(v, **extra.get(k, {}))
for k, v in _properties(value).items() # Does not need to inspect properties for dataclass or Pydantic objects
},
**kwargs,
)
It similarly fails without recursion:
import dataclasses
from typing import Tuple, List
from sanic_ext.extensions.openapi import openapi
@dataclasses.dataclass
class Path:
x: [float]
y: [float]
@property
def points(self) -> List[Tuple[float, float]]:
pass
openapi.component(Path)
...
Traceback (most recent call last):
File "/home/sir/venv/py3/lib/python3.10/site-packages/sanic_ext/extensions/openapi/types.py", line 417, in _properties
annotations = get_type_hints(cls)
File "/usr/lib/python3.10/typing.py", line 1856, in get_type_hints
raise TypeError('{!r} is not a module, class, method, '
TypeError: typing.Tuple[float, float] is not a module, class, method, or function.
There's really no need for sanic to inspect property return types to discover model attributes. Dataclasses, Pydantic or type annotations on the class constructor all provide what is needed.
In the following, the "something" property is added to the schema, for both classes (this is a mistake, I think).
from multiprocessing import freeze_support
import dataclasses
from sanic import Sanic, text, Request
from sanic_ext.extensions.openapi import openapi
from sanic_ext.extensions.openapi.definitions import Response, RequestBody
from sanic_ext.extensions.openapi.openapi import component
app = Sanic("app")
@component
@dataclasses.dataclass
class MyBody:
email: str
@property
def something(self) -> int:
print("Oh no you didn't!")
return "OK2"
@component
@dataclasses.dataclass
class UserProfile:
name: str
age: int
email: str
@property
def something(self) -> int:
print("Oh no you didn't!")
return "OK"
@app.route("/foo")
def foo(request):
url = app.url_for("handler", _external=True)
return text(f"URL: {url}")
@app.get("/")
@openapi.definition(
body=RequestBody({"application/json": {
"$ref": f"#/components/schemas/{MyBody.__name__}"
}}, required=True), # if body else None,
# body=RequestBody(Body, required=True),
summary="User profile update",
# tag="one",
# description=openapi.description(textwrap.dedent(func.__doc__)) if func.__doc__ else None,
response=[ # Success,
Response({
"application/json": {
"schema": {
"$ref": f"#/components/schemas/{UserProfile.__name__}"
}
}
}, status=200)
# Response(Failure, status=400)
],
)
def root(req: Request, *args, **kw): # body:UserProfile):
"""
Short description
Long Description
"""
return text("Hello")
if __name__ == '__main__':
freeze_support()
app.run(host='0.0.0.0', port=8000,
# access_log=True
)
...
# http://localhost:8000/docs/openapi.json
{"openapi":"3.0.3","info":{"title":"API","version":"1.0.0","contact":{}},"paths":{"/foo":{"get":{"operationId":"get~foo","summary":"Foo","responses":{"default":{"description":"OK"}}}},"/":{"get":{"operationId":"get~root","summary":"User profile update","description":"Long Description","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfile"}}},"description":"Default Response"}},"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyBody"}}},"required":true}}}},"tags":[],"servers":[],"security":[],"components":{"schemas":{"MyBody":{"type":"object","properties":{"something":{"type":"integer","format":"int32"},"email":{"type":"string","title":"Email"}}},"UserProfile":{"type":"object","properties":{"something":{"type":"integer","format":"int32"},"name":{"type":"string","title":"Name"},"age":{"type":"integer","format":"int32","title":"Age"},"email":{"type":"string","title":"Email"}}}}}}