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
|
ffmpeg.exe
|
||||||
ffprobe.exe
|
ffprobe.exe
|
||||||
|
|
||||||
|
tools/docgen/out
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
import sys
|
import sys
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Type
|
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from ytdl_sub.script.types.resolvable import BuiltInFunctionType
|
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 IncompatibleFunctionArguments
|
||||||
from ytdl_sub.script.utils.exceptions import UserException
|
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 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
|
from ytdl_sub.script.utils.type_checking import is_union
|
||||||
|
|
||||||
TUserException = TypeVar("TUserException", bound=UserException)
|
TUserException = TypeVar("TUserException", bound=UserException)
|
||||||
|
|
@ -103,28 +98,10 @@ class FunctionArgumentsExceptionFormatter:
|
||||||
input_spec: FunctionSpec,
|
input_spec: FunctionSpec,
|
||||||
function_instance: BuiltInFunctionType,
|
function_instance: BuiltInFunctionType,
|
||||||
):
|
):
|
||||||
self._args = input_spec.args
|
self._input_spec = input_spec
|
||||||
self._varargs = input_spec.varargs
|
|
||||||
self._name = function_instance.name
|
self._name = function_instance.name
|
||||||
self._input_args = function_instance.args
|
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:
|
def _received_args_str(self) -> str:
|
||||||
received_type_names: List[str] = []
|
received_type_names: List[str] = []
|
||||||
for arg in self._input_args:
|
for arg in self._input_args:
|
||||||
|
|
@ -149,5 +126,6 @@ class FunctionArgumentsExceptionFormatter:
|
||||||
"""
|
"""
|
||||||
return IncompatibleFunctionArguments(
|
return IncompatibleFunctionArguments(
|
||||||
f"Incompatible arguments passed to function {self._name}.\n"
|
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)
|
@dataclass(frozen=True)
|
||||||
class FunctionSpec:
|
class FunctionSpec:
|
||||||
return_type: Type[Resolvable]
|
return_type: Type[Resolvable]
|
||||||
|
arg_names: List[str]
|
||||||
args: Optional[List[Type[Resolvable | Optional[Resolvable]]]] = None
|
args: Optional[List[Type[Resolvable | Optional[Resolvable]]]] = None
|
||||||
varargs: Optional[Type[Resolvable]] = None
|
varargs: Optional[Type[Resolvable]] = None
|
||||||
|
|
||||||
|
|
@ -223,6 +224,42 @@ class FunctionSpec:
|
||||||
return l_type
|
return l_type
|
||||||
return None
|
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
|
@classmethod
|
||||||
def from_callable(cls, callable_ref: Callable[..., Resolvable]) -> "FunctionSpec":
|
def from_callable(cls, callable_ref: Callable[..., Resolvable]) -> "FunctionSpec":
|
||||||
"""
|
"""
|
||||||
|
|
@ -234,10 +271,12 @@ class FunctionSpec:
|
||||||
if arg_spec.varargs:
|
if arg_spec.varargs:
|
||||||
return FunctionSpec(
|
return FunctionSpec(
|
||||||
return_type=arg_spec.annotations["return"],
|
return_type=arg_spec.annotations["return"],
|
||||||
|
arg_names=[arg_spec.varargs],
|
||||||
varargs=arg_spec.annotations[arg_spec.varargs],
|
varargs=arg_spec.annotations[arg_spec.varargs],
|
||||||
)
|
)
|
||||||
|
|
||||||
return FunctionSpec(
|
return FunctionSpec(
|
||||||
return_type=arg_spec.annotations["return"],
|
return_type=arg_spec.annotations["return"],
|
||||||
|
arg_names=arg_spec.args,
|
||||||
args=[arg_spec.annotations[arg_name] for arg_name in 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(
|
with pytest.raises(
|
||||||
IncompatibleFunctionArguments,
|
IncompatibleFunctionArguments,
|
||||||
match=_incompatible_arguments_match(
|
match=_incompatible_arguments_match(
|
||||||
expected="Map, AnyArgument, Optional[AnyArgument]",
|
expected="mapping: Map, key: AnyArgument, default: Optional[AnyArgument]",
|
||||||
recieved="%if(...)->Union[Array, Map], String",
|
recieved="%if(...)->Union[Array, Map], String",
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
|
|
@ -49,11 +49,11 @@ class TestFunction:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"function_str, expected_types, received_types",
|
"function_str, expected_types, received_types",
|
||||||
[
|
[
|
||||||
("{%array_at({'a': 'dict?'}, 1)}", "Array, Integer", "Map, Integer"),
|
("{%array_at({'a': 'dict?'}, 1)}", "array: Array, idx: Integer", "Map, Integer"),
|
||||||
("{%array_extend('not', 'array')}", "Array, ...", "String, String"),
|
("{%array_extend('not', 'array')}", "arrays: Array, ...", "String, String"),
|
||||||
(
|
(
|
||||||
"{%replace('hi mom', 'mom', 'dad', 1, 0)}",
|
"{%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",
|
"String, String, String, Integer, Integer",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
|
import inspect
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from tools.docgen.utils import camel_case_to_human
|
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 section
|
||||||
from tools.docgen.utils import static_methods
|
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.entries.script.custom_functions import CustomFunctions
|
||||||
from ytdl_sub.script.functions import Functions
|
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]:
|
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__)
|
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:
|
def generate_function_docs() -> str:
|
||||||
docs = section("Scripting Functions", level=0)
|
docs = section("Scripting Functions", level=0)
|
||||||
|
|
||||||
|
|
@ -38,14 +64,14 @@ def generate_function_docs() -> str:
|
||||||
|
|
||||||
for function_name in static_methods(parent_objs[name]):
|
for function_name in static_methods(parent_objs[name]):
|
||||||
if display_function_name := maybe_get_function_name(function_name):
|
if display_function_name := maybe_get_function_name(function_name):
|
||||||
docs += get_function_docs(
|
docs += get_function_docstring(
|
||||||
function_name=function_name,
|
function_name=function_name,
|
||||||
display_function_name=display_function_name,
|
display_function_name=display_function_name,
|
||||||
obj=parent_objs[name],
|
function=getattr(parent_objs[name], function_name),
|
||||||
level=2,
|
level=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
return docs
|
return docs
|
||||||
|
|
||||||
|
|
||||||
print(generate_function_docs())
|
to_out_dir(name="scripting_functions.rst", docs=generate_function_docs())
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import inspect
|
import inspect
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
@ -42,3 +43,8 @@ def get_function_docs(
|
||||||
docs += inspect.cleandoc(getattr(obj, function_name).__doc__)
|
docs += inspect.cleandoc(getattr(obj, function_name).__doc__)
|
||||||
docs += "\n"
|
docs += "\n"
|
||||||
return docs
|
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