function type hinting
This commit is contained in:
parent
1b2c449331
commit
fe6bcadc0d
6 changed files with 84 additions and 33 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -149,3 +149,5 @@ docker/testing/volumes
|
|||
|
||||
ffmpeg.exe
|
||||
ffprobe.exe
|
||||
|
||||
tools/docgen/out
|
||||
|
|
@ -1,16 +1,11 @@
|
|||
import sys
|
||||
from typing import List
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
from ytdl_sub.script.types.resolvable import BuiltInFunctionType
|
||||
from ytdl_sub.script.types.resolvable import NamedType
|
||||
from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments
|
||||
from ytdl_sub.script.utils.exceptions import UserException
|
||||
from ytdl_sub.script.utils.type_checking import FunctionSpec
|
||||
from ytdl_sub.script.utils.type_checking import get_optional_type
|
||||
from ytdl_sub.script.utils.type_checking import is_optional
|
||||
from ytdl_sub.script.utils.type_checking import is_union
|
||||
|
||||
TUserException = TypeVar("TUserException", bound=UserException)
|
||||
|
|
@ -103,28 +98,10 @@ class FunctionArgumentsExceptionFormatter:
|
|||
input_spec: FunctionSpec,
|
||||
function_instance: BuiltInFunctionType,
|
||||
):
|
||||
self._args = input_spec.args
|
||||
self._varargs = input_spec.varargs
|
||||
self._input_spec = input_spec
|
||||
self._name = function_instance.name
|
||||
self._input_args = function_instance.args
|
||||
|
||||
@classmethod
|
||||
def _to_human_readable_name(cls, python_type: Type[NamedType] | Type[Union[NamedType]]) -> str:
|
||||
if is_optional(python_type):
|
||||
return f"Optional[{cls._to_human_readable_name(get_optional_type(python_type))}]"
|
||||
if is_union(python_type):
|
||||
return ", ".join(
|
||||
sorted(cls._to_human_readable_name(arg) for arg in python_type.__args__)
|
||||
)
|
||||
return python_type.type_name()
|
||||
|
||||
def _expected_args_str(self) -> str:
|
||||
if self._args is not None:
|
||||
return f"({', '.join([self._to_human_readable_name(type_) for type_ in self._args])})"
|
||||
if self._varargs is not None:
|
||||
return f"({self._to_human_readable_name(self._varargs)}, ...)"
|
||||
return "()"
|
||||
|
||||
def _received_args_str(self) -> str:
|
||||
received_type_names: List[str] = []
|
||||
for arg in self._input_args:
|
||||
|
|
@ -149,5 +126,6 @@ class FunctionArgumentsExceptionFormatter:
|
|||
"""
|
||||
return IncompatibleFunctionArguments(
|
||||
f"Incompatible arguments passed to function {self._name}.\n"
|
||||
f"Expected {self._expected_args_str()}\nReceived {self._received_args_str()}"
|
||||
f"Expected {self._input_spec.human_readable_input_args()}\n"
|
||||
f"Received {self._received_args_str()}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ def is_type_compatible(
|
|||
@dataclass(frozen=True)
|
||||
class FunctionSpec:
|
||||
return_type: Type[Resolvable]
|
||||
arg_names: List[str]
|
||||
args: Optional[List[Type[Resolvable | Optional[Resolvable]]]] = None
|
||||
varargs: Optional[Type[Resolvable]] = None
|
||||
|
||||
|
|
@ -223,6 +224,42 @@ class FunctionSpec:
|
|||
return l_type
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _to_human_readable_name(cls, python_type: Type[NamedType] | Type[Union[NamedType]]) -> str:
|
||||
if is_optional(python_type):
|
||||
return f"Optional[{cls._to_human_readable_name(get_optional_type(python_type))}]"
|
||||
if is_union(python_type):
|
||||
args = ", ".join(
|
||||
sorted(cls._to_human_readable_name(arg) for arg in python_type.__args__)
|
||||
)
|
||||
return f"Union[{args}]"
|
||||
return python_type.type_name()
|
||||
|
||||
def human_readable_input_args(self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
input arg string in human-readable format
|
||||
"""
|
||||
if self.args is not None:
|
||||
args = ", ".join(
|
||||
f"{name}: {self._to_human_readable_name(type_)}"
|
||||
for name, type_ in zip(self.arg_names, self.args)
|
||||
)
|
||||
elif self.varargs is not None:
|
||||
args = f"{self.arg_names[0]}: {self._to_human_readable_name(self.varargs)}, ..."
|
||||
else:
|
||||
args = ""
|
||||
return f"({args})"
|
||||
|
||||
def human_readable_output_type(self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
output type string in human-readable format
|
||||
"""
|
||||
return self._to_human_readable_name(self.return_type)
|
||||
|
||||
@classmethod
|
||||
def from_callable(cls, callable_ref: Callable[..., Resolvable]) -> "FunctionSpec":
|
||||
"""
|
||||
|
|
@ -234,10 +271,12 @@ class FunctionSpec:
|
|||
if arg_spec.varargs:
|
||||
return FunctionSpec(
|
||||
return_type=arg_spec.annotations["return"],
|
||||
arg_names=[arg_spec.varargs],
|
||||
varargs=arg_spec.annotations[arg_spec.varargs],
|
||||
)
|
||||
|
||||
return FunctionSpec(
|
||||
return_type=arg_spec.annotations["return"],
|
||||
arg_names=arg_spec.args,
|
||||
args=[arg_spec.annotations[arg_name] for arg_name in arg_spec.args],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class TestFunction:
|
|||
with pytest.raises(
|
||||
IncompatibleFunctionArguments,
|
||||
match=_incompatible_arguments_match(
|
||||
expected="Map, AnyArgument, Optional[AnyArgument]",
|
||||
expected="mapping: Map, key: AnyArgument, default: Optional[AnyArgument]",
|
||||
recieved="%if(...)->Union[Array, Map], String",
|
||||
),
|
||||
):
|
||||
|
|
@ -49,11 +49,11 @@ class TestFunction:
|
|||
@pytest.mark.parametrize(
|
||||
"function_str, expected_types, received_types",
|
||||
[
|
||||
("{%array_at({'a': 'dict?'}, 1)}", "Array, Integer", "Map, Integer"),
|
||||
("{%array_extend('not', 'array')}", "Array, ...", "String, String"),
|
||||
("{%array_at({'a': 'dict?'}, 1)}", "array: Array, idx: Integer", "Map, Integer"),
|
||||
("{%array_extend('not', 'array')}", "arrays: Array, ...", "String, String"),
|
||||
(
|
||||
"{%replace('hi mom', 'mom', 'dad', 1, 0)}",
|
||||
"String, String, String, Optional[Integer]",
|
||||
"string: String, old: String, new: String, count: Optional[Integer]",
|
||||
"String, String, String, Integer, Integer",
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import inspect
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
|
||||
from tools.docgen.utils import camel_case_to_human
|
||||
from tools.docgen.utils import get_function_docs
|
||||
from tools.docgen.utils import section
|
||||
from tools.docgen.utils import static_methods
|
||||
from tools.docgen.utils import to_out_dir
|
||||
from ytdl_sub.entries.script.custom_functions import CustomFunctions
|
||||
from ytdl_sub.script.functions import Functions
|
||||
from ytdl_sub.script.utils.type_checking import FunctionSpec
|
||||
|
||||
|
||||
def maybe_get_function_name(function_name: str) -> Optional[str]:
|
||||
|
|
@ -25,6 +27,30 @@ def function_class_to_name(obj: Type[Any]) -> str:
|
|||
return camel_case_to_human(obj.__name__)
|
||||
|
||||
|
||||
def function_type_hinting(display_function_name: str, function: Any) -> str:
|
||||
spec = FunctionSpec.from_callable(function)
|
||||
out = "``"
|
||||
out += display_function_name
|
||||
out += spec.human_readable_input_args()
|
||||
out += " -> "
|
||||
out += spec.human_readable_output_type()
|
||||
out += "``\n\n"
|
||||
return out
|
||||
|
||||
|
||||
def get_function_docstring(
|
||||
function_name: str, function: Any, level: int, display_function_name: Optional[str] = None
|
||||
) -> str:
|
||||
display_function_name = display_function_name if display_function_name else function_name
|
||||
|
||||
docs = section(display_function_name, level=level)
|
||||
|
||||
docs += function_type_hinting(display_function_name=display_function_name, function=function)
|
||||
docs += inspect.cleandoc(function.__doc__)
|
||||
docs += "\n"
|
||||
return docs
|
||||
|
||||
|
||||
def generate_function_docs() -> str:
|
||||
docs = section("Scripting Functions", level=0)
|
||||
|
||||
|
|
@ -38,14 +64,14 @@ def generate_function_docs() -> str:
|
|||
|
||||
for function_name in static_methods(parent_objs[name]):
|
||||
if display_function_name := maybe_get_function_name(function_name):
|
||||
docs += get_function_docs(
|
||||
docs += get_function_docstring(
|
||||
function_name=function_name,
|
||||
display_function_name=display_function_name,
|
||||
obj=parent_objs[name],
|
||||
function=getattr(parent_objs[name], function_name),
|
||||
level=2,
|
||||
)
|
||||
|
||||
return docs
|
||||
|
||||
|
||||
print(generate_function_docs())
|
||||
to_out_dir(name="scripting_functions.rst", docs=generate_function_docs())
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import inspect
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
|
@ -42,3 +43,8 @@ def get_function_docs(
|
|||
docs += inspect.cleandoc(getattr(obj, function_name).__doc__)
|
||||
docs += "\n"
|
||||
return docs
|
||||
|
||||
|
||||
def to_out_dir(name: str, docs: str) -> None:
|
||||
with open(Path("tools") / "docgen" / "out" / name, "w", encoding="utf-8") as out:
|
||||
out.write(docs)
|
||||
|
|
|
|||
Loading…
Reference in a new issue