1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-03-20 06:50:23 +03:00

Refactor HelpDoc class to support function-based help documentation and improve return type handling

This commit is contained in:
Adolfo Gómez García 2025-02-08 20:07:15 +01:00
parent f9a0026d5d
commit 94249decfb
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
3 changed files with 133 additions and 40 deletions

View File

@ -88,21 +88,6 @@ class Dispatcher(View):
# # Guess content type from content type header (post) or ".xxx" to method
content_type: str = request.META.get('CONTENT_TYPE', 'application/json').split(';')[0]
# while path:
# clean_path = path[0]
# # Skip empty path elements, so /x/y == /x////y for example (due to some bugs detected on some clients)
# if not clean_path:
# path = path[1:]
# continue
# if clean_path in service.children: # if we have a node for this path, walk down
# service = service.children[clean_path]
# full_path_lst.append(path[0]) # Add this path to full path
# path = path[1:] # Remove first part of path
# else:
# break # If we don't have a node for this path, we are done
# full_path = '/'.join(full_path_lst)
handler_node = Dispatcher.base_handler_node.find_path(path)
if not handler_node:
return http.HttpResponseNotFound('Service not found', content_type="text/plain")

View File

@ -72,7 +72,7 @@ class TypedResponse(abc.ABC):
list: '<list>',
typing.Any: '<any>',
}
def _as_help(obj: typing.Any) -> typing.Union[str, dict[str, typing.Any]]:
if dataclasses.is_dataclass(obj):
return {field.name: _as_help(field.type) for field in dataclasses.fields(obj)}
@ -170,19 +170,19 @@ class HelpDoc:
"""
Help helper class
"""
@dataclasses.dataclass
class ArgumentInfo:
name: str
type: str
description: str
path: str
description: str
arguments: list[ArgumentInfo] = dataclasses.field(default_factory=list)
# Result is always a json ressponse, so we can describe it as a dict
# Note that this dict can be nested
returns: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
returns: typing.Any = None
def __init__(
self,
@ -218,15 +218,19 @@ class HelpDoc:
"""
self.description = ''
self.arguments = []
self.returns = {}
self.returns = None
match = API_RE.search(help)
if match:
self.description = help[: match.start()].strip()
if annotations:
if 'return' in annotations and issubclass(annotations['return'], TypedResponse):
self.returns = annotations['return'].as_help()
if 'return' in annotations:
t = annotations['return']
if isinstance(t, collections.abc.Iterable):
pass
# if issubclass(annotations['return'], TypedResponse):
# self.returns = annotations['return'].as_help()
@staticmethod
def from_typed_response(path: str, help: str, TR: type[TypedResponse]) -> 'HelpDoc':
@ -239,6 +243,28 @@ class HelpDoc:
returns=TR.as_help(),
)
@staticmethod
def from_fnc(path: str, help: str, fnc: typing.Callable[..., typing.Any]) -> 'HelpDoc|None':
"""
Returns a HelpDoc from a function that returns a list of TypedResponses
"""
return_type: typing.Any = fnc.__annotations__.get('return')
if isinstance(return_type, TypedResponse):
return HelpDoc.from_typed_response(path, help, typing.cast(type[TypedResponse], return_type))
elif (
isinstance(return_type, collections.abc.Iterable)
and len(typing.cast(typing.Any, return_type).__args__) == 1
and issubclass(typing.cast(typing.Any, return_type).__args__[0], TypedResponse)
):
hd = HelpDoc.from_typed_response(
path, help, typing.cast(type[TypedResponse], typing.cast(typing.Any, return_type).__args__[0])
)
hd.returns = [hd.returns] # We need to return a list of returns
return hd
return None
@dataclasses.dataclass(frozen=True)
class HelpNode:

View File

@ -30,7 +30,10 @@
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import dataclasses
import collections.abc
import logging
import typing
from unittest import TestCase
from uds.core.types import rest
@ -81,25 +84,27 @@ class TestHelpDoc(TestCase):
self.assertEqual(h.description, 'help_text')
self.assertEqual(h.arguments, arguments)
self.assertEqual(h.returns, returns)
def test_help_doc_from_typed_response(self) -> None:
@dataclasses.dataclass
class TestResponse(rest.TypedResponse):
name: str = 'test_name'
age: int = 0
money: float = 0.0
h = rest.HelpDoc.from_typed_response('path', 'help', TestResponse)
self.assertEqual(h.path, 'path')
self.assertEqual(h.description, 'help')
self.assertEqual(h.arguments, [])
self.assertEqual(h.returns, {
'name': '<string>',
'age': '<integer>',
'money': '<float>',
})
self.assertEqual(
h.returns,
{
'name': '<string>',
'age': '<integer>',
'money': '<float>',
},
)
def test_help_doc_from_typed_response_nested_dataclass(self) -> None:
@dataclasses.dataclass
@ -107,26 +112,103 @@ class TestHelpDoc(TestCase):
name: str = 'test_name'
age: int = 0
money: float = 0.0
@dataclasses.dataclass
class TestResponse2(rest.TypedResponse):
name: str
age: int
money: float
nested: TestResponse
h = rest.HelpDoc.from_typed_response('path', 'help', TestResponse2)
self.assertEqual(h.path, 'path')
self.assertEqual(h.description, 'help')
self.assertEqual(h.arguments, [])
self.assertEqual(h.returns, {
'name': '<string>',
'age': '<integer>',
'money': '<float>',
'nested': {
self.assertEqual(
h.returns,
{
'name': '<string>',
'age': '<integer>',
'money': '<float>',
}
})
'nested': {
'name': '<string>',
'age': '<integer>',
'money': '<float>',
},
},
)
def test_help_doc_from_fnc(self) -> None:
@dataclasses.dataclass
class TestResponse(rest.TypedResponse):
name: str = 'test_name'
age: int = 0
money: float = 0.0
def testing_fnc() -> TestResponse:
"""
This is a test function
"""
return []
h = rest.HelpDoc.from_fnc('path', 'help', testing_fnc)
if h is None:
self.fail('HelpDoc is None')
self.assertEqual(h.path, 'path')
self.assertEqual(h.description, 'help')
self.assertEqual(h.arguments, [])
self.assertEqual(
h.returns,
{
'name': '<string>',
'age': '<integer>',
'money': '<float>',
},
)
def test_help_doc_from_non_typed_response(self) -> None:
def testing_fnc() -> dict[str, typing.Any]:
"""
This is a test function
"""
return {}
h = rest.HelpDoc.from_fnc('path', 'help', testing_fnc)
self.assertIsNone(h)
def test_help_doc_from_fnc_list(self) -> None:
@dataclasses.dataclass
class TestResponse(rest.TypedResponse):
name: str = 'test_name'
age: int = 0
money: float = 0.0
def testing_fnc() -> list[TestResponse]:
"""
This is a test function
"""
return []
h = rest.HelpDoc.from_fnc('path', 'help', testing_fnc)
if h is None:
self.fail('HelpDoc is None')
self.assertEqual(h.path, 'path')
self.assertEqual(h.description, 'help')
self.assertEqual(h.arguments, [])
self.assertEqual(
h.returns,
[
{
'name': '<string>',
'age': '<integer>',
'money': '<float>',
}
],
)