Repository: sanic-org/sanic-ext (python)

Issue:
[Bug] Pydantic's SecretStr type is not supported
**Describe the bug**
Trying to return a pydantic model with an attribute `SecretStr` yields the following error:

```
Traceback (most recent call last):
  File "handle_request", line 96, in handle_request
  File "/tmp/test_sanic/.venv/lib/python3.10/site-packages/sanic_ext/extensions/openapi/openapi.py", line 232, in handler
    retval = await retval
  File "/tmp/test_sanic/.venv/lib/python3.10/site-packages/sanic_ext/extras/validation/decorator.py", line 75, in decorated_function
    retval = await retval
  File "/tmp/test_sanic/server.py", line 25, in user
    return json(body)
  File "/tmp/test_sanic/.venv/lib/python3.10/site-packages/sanic/response/convenience.py", line 50, in json
    return JSONResponse(
  File "/tmp/test_sanic/.venv/lib/python3.10/site-packages/sanic/response/types.py", line 248, in __init__
    self._encode_body(self._use_dumps(body, **self._use_dumps_kwargs)),
TypeError: User(username='string', password=SecretStr('**********')) is not JSON serializable
```

Maybe it is not intended to support this but I supposed that I get a representation containing `*` marks.
The [pydantic documentation](https://docs.pydantic.dev/usage/types/#secret-types) describes that it will be formatted using `*` on serializing to json.

If I uncomment the response part of the openapi decorator I get the following error message:

```
  ...
  File "/usr/lib/python3.10/typing.py", line 694, in _evaluate
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
NameError: name 'unicode' is not defined
```

**To Reproduce**

```
from sanic import Sanic, json
from sanic_ext import openapi, validate

from pydantic import BaseModel, SecretStr

class User(BaseModel):
    username: str
    password: SecretStr

app = Sanic("MyHelloWorldApp")

@app.post("/user")
@openapi.definition(
    body={'application/json': User.schema()},
    # response=[
    #     openapi.definitions.Response(
    #         content={"application/json": User},
    #         description="...",
    #         status=200,
    #     )
    # ]
)
@validate(json=User)
async def user(request, body: User):
    return json(body)
```

**Expected behavior**
To get a json representation consisting '*' marks.

**Environment (please complete the following information):**
 - OS: Manjaro
 - Browser: firefox
 

Pull request:
Do not fail on cython objects
Closes #186 

Patch:
diff --git a/sanic_ext/extensions/http/methods.py b/sanic_ext/extensions/http/methods.py
index 176167d..19ffbc4 100644
--- a/sanic_ext/extensions/http/methods.py
+++ b/sanic_ext/extensions/http/methods.py
@@ -125,20 +125,20 @@ def add_auto_handlers(
                         )
                     except StopIteration:
                         base_route = group[0]
-                    for host in hosts:
-                        name = f"{base_route.name}_options"
-                        handler = openapi.definition(
-                            summary=clean_route_name(base_route.name).title(),
-                            description="Retrieve OPTIONS details",
-                        )(partial(options_handler, methods=group.methods))
-                        handler.__auto_handler__ = True
-                        app.add_route(
-                            handler=handler,
-                            uri=group.uri,
-                            methods=["OPTIONS"],
-                            strict_slashes=group.strict,
-                            name=name,
-                            host=host,
-                            unquote=group.unquote,
-                        )
+
+                    name = f"{base_route.name}_options"
+                    handler = openapi.definition(
+                        summary=clean_route_name(base_route.name).title(),
+                        description="Retrieve OPTIONS details",
+                    )(partial(options_handler, methods=group.methods))
+                    handler.__auto_handler__ = True
+                    app.add_route(
+                        handler=handler,
+                        uri=group.uri,
+                        methods=["OPTIONS"],
+                        strict_slashes=group.strict,
+                        name=name,
+                        host=hosts,
+                        unquote=group.unquote,
+                    )
             app.finalize()
diff --git a/sanic_ext/extensions/openapi/types.py b/sanic_ext/extensions/openapi/types.py
index 3df27f3..c789b0d 100644
--- a/sanic_ext/extensions/openapi/types.py
+++ b/sanic_ext/extensions/openapi/types.py
@@ -22,7 +22,7 @@ from sanic_ext.utils.typing import is_attrs, is_generic, is_pydantic
 try:
     import attrs
 
-    NOTHING = attrs.NOTHING
+    NOTHING: Any = attrs.NOTHING
 except ImportError:
     NOTHING = object()
 
@@ -365,19 +365,32 @@ def _properties(value: object) -> Dict:
     cls = value if callable(value) else value.__class__
     extra = value if isinstance(value, dict) else {}
     try:
-        return {
+        annotations = get_type_hints(cls)
+    except NameError:
+        if hasattr(value, "__annotations__"):
+            annotations = value.__annotations__
+        else:
+            annotations = {}
+    annotations.pop("return", None)
+    try:
+        output = {
             k: v
-            for k, v in {**fields, **get_type_hints(cls), **extra}.items()
+            for k, v in {**fields, **annotations, **extra}.items()
             if not k.startswith("_")
             and not (
                 isclass(v)
                 and isclass(cls)
-                and v.__qualname__.endswith(f"{cls.__name__}.{v.__name__}")
+                and v.__qualname__.endswith(
+                    f"{getattr(cls, '__name__', '<unknown>')}."
+                    f"{getattr(v, '__name__', '<unknown>')}"
+                )
             )
         }
     except TypeError:
         return {}
 
+    return output
+
 
 def _extract(item):
     if isinstance(item, property):


Test patch:
diff --git a/tests/extensions/http/test_methods.py b/tests/extensions/http/test_methods.py
index 7d8e207..7f46f54 100644
--- a/tests/extensions/http/test_methods.py
+++ b/tests/extensions/http/test_methods.py
@@ -65,11 +65,11 @@ def test_auto_trace(bare_app: Sanic):
 
 
 def test_auto_head_with_vhosts(app: Sanic, get_docs):
-    @app.get("/foo", host="one.com")
+    @app.get("/foo", host="one.com", name="one")
     async def foo_handler_one(_):
         return text(".")
 
-    @app.get("/foo", host="two.com")
+    @app.get("/foo", host="two.com", name="two")
     async def foo_handler_two(_):
         return text("..")
 
@@ -88,6 +88,29 @@ def test_auto_head_with_vhosts(app: Sanic, get_docs):
     assert "get" in schema["paths"]["/foo"]
 
 
+def test_auto_options_with_vhosts(app: Sanic, get_docs):
+    @app.post("/foo", host="one.com", name="one")
+    async def foo_handler_one(_):
+        return text(".")
+
+    @app.post("/foo", host="two.com", name="two")
+    async def foo_handler_two(_):
+        return text("..")
+
+    assert app.config.HTTP_AUTO_OPTIONS
+    _, response = app.test_client.options("/foo", headers={"host": "one.com"})
+    assert response.status == 204
+    assert len(response.body) == 0
+    assert "POST" in response.headers["allow"]
+    assert "OPTIONS" in response.headers["allow"]
+
+    _, response = app.test_client.options("/foo", headers={"host": "two.com"})
+    assert response.status == 204
+    assert len(response.body) == 0
+    assert "POST" in response.headers["allow"]
+    assert "OPTIONS" in response.headers["allow"]
+
+
 # This test also appears in Core Sanic tests but is added here as well
 # because of: https://github.com/sanic-org/sanic-ext/issues/148
 @pytest.mark.parametrize("unquote", [True, False, None])