Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Doubly defined objects in the output specification #260 #245

Open
allComputableThings opened this issue May 3, 2024 · 2 comments
Open

Comments

@allComputableThings
Copy link

When we have:

@component
@dataclasses.dataclass
class Location:
    x: int
    y: int
@component
@dataclasses.dataclass
class UserProfile:
    name: str
    age: int
    email: str
    location: Location #Path

Location is define twice in the output schema. Once in components, once again within UserProfile.

This is deeply problematic for code generators which depend on generating a single class for both. ( openapi-generator-cli, for example, produces two Location classes and one cannot be used where the other is used).

UserProfile should have a ref to Location and Location should be added as a component whether or not it has the @component decorator:

{
   "$ref": f"#/components/schemas/{Location.__name__}"
}
import os
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

# from typing import List

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 Location:
    x: int
    y: int


@component
@dataclasses.dataclass
class UserProfile:
    name: str
    age: int
    email: str
    # locations: List[Location] #Path
    location: Location #Path

    @property
    def something(self) -> int:
        print("Oh no you didn't!")
        return "OK"




@app.post("/")
@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")

ref: sanic-org/sanic-openapi#260

@allComputableThings
Copy link
Author

Similarly: sanic-org/sanic-openapi#259

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.

@allComputableThings
Copy link
Author

I have a fix both, do you want a PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant