From 5859a531e5185248ab7f4d7ab2cc071f39a6e8c9 Mon Sep 17 00:00:00 2001 From: Vadim Levin Date: Wed, 14 Jun 2023 21:24:05 +0300 Subject: [PATCH 1/2] feat: manual refinement for Python API definition Mark `resize` and `calcHist` arguments as optional regardless of their C++ API optionality --- .../typing_stubs_generation/api_refinement.py | 44 +++++++++++++++++++ .../src2/typing_stubs_generation/ast_utils.py | 25 ++++++++--- .../typing_stubs_generation/generation.py | 13 ++++++ .../nodes/function_node.py | 30 ++++++++++--- modules/python/src2/typing_stubs_generator.py | 6 ++- 5 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 modules/python/src2/typing_stubs_generation/api_refinement.py diff --git a/modules/python/src2/typing_stubs_generation/api_refinement.py b/modules/python/src2/typing_stubs_generation/api_refinement.py new file mode 100644 index 0000000000..a3468c1567 --- /dev/null +++ b/modules/python/src2/typing_stubs_generation/api_refinement.py @@ -0,0 +1,44 @@ +__all__ = [ + "apply_manual_api_refinement" +] + +from typing import Sequence, Callable +from .nodes import NamespaceNode, FunctionNode, OptionalTypeNode +from .ast_utils import find_function_node, SymbolName + + +def apply_manual_api_refinement(root: NamespaceNode) -> None: + for symbol_name, refine_symbol in NODES_TO_REFINE.items(): + refine_symbol(root, symbol_name) + + +def make_optional_arg(arg_name: str) -> Callable[[NamespaceNode, SymbolName], None]: + def _make_optional_arg(root_node: NamespaceNode, + function_symbol_name: SymbolName) -> None: + function = find_function_node(root_node, function_symbol_name) + for overload in function.overloads: + arg_idx = _find_argument_index(overload.arguments, arg_name) + # Avoid multiplying optional qualification + if isinstance(overload.arguments[arg_idx].type_node, OptionalTypeNode): + continue + + overload.arguments[arg_idx].type_node = OptionalTypeNode( + overload.arguments[arg_idx].type_node + ) + + return _make_optional_arg + + +def _find_argument_index(arguments: Sequence[FunctionNode.Arg], name: str) -> int: + for i, arg in enumerate(arguments): + if arg.name == name: + return i + raise RuntimeError( + f"Failed to find argument with name: '{name}' in {arguments}" + ) + + +NODES_TO_REFINE = { + SymbolName(("cv", ), (), "resize"): make_optional_arg("dsize"), + SymbolName(("cv", ), (), "calcHist"): make_optional_arg("mask"), +} diff --git a/modules/python/src2/typing_stubs_generation/ast_utils.py b/modules/python/src2/typing_stubs_generation/ast_utils.py index 1f802f6da5..7001e1dd9c 100644 --- a/modules/python/src2/typing_stubs_generation/ast_utils.py +++ b/modules/python/src2/typing_stubs_generation/ast_utils.py @@ -142,13 +142,24 @@ because 'GOpaque' class is not registered yet return scope -def find_class_node(root: NamespaceNode, full_class_name: str, - namespaces: Sequence[str]) -> ClassNode: - symbol_name = SymbolName.parse(full_class_name, namespaces) - scope = find_scope(root, symbol_name) - if symbol_name.name not in scope.classes: - raise SymbolNotFoundError("Can't find {} in its scope".format(symbol_name)) - return scope.classes[symbol_name.name] +def find_class_node(root: NamespaceNode, class_symbol: SymbolName, + create_missing_namespaces: bool = False) -> ClassNode: + scope = find_scope(root, class_symbol, create_missing_namespaces) + if class_symbol.name not in scope.classes: + raise SymbolNotFoundError( + "Can't find {} in its scope".format(class_symbol) + ) + return scope.classes[class_symbol.name] + + +def find_function_node(root: NamespaceNode, function_symbol: SymbolName, + create_missing_namespaces: bool = False) -> FunctionNode: + scope = find_scope(root, function_symbol, create_missing_namespaces) + if function_symbol.name not in scope.functions: + raise SymbolNotFoundError( + "Can't find {} in its scope".format(function_symbol) + ) + return scope.functions[function_symbol.name] def create_function_node_in_scope(scope: Union[NamespaceNode, ClassNode], diff --git a/modules/python/src2/typing_stubs_generation/generation.py b/modules/python/src2/typing_stubs_generation/generation.py index 018c55414a..43fd82e797 100644 --- a/modules/python/src2/typing_stubs_generation/generation.py +++ b/modules/python/src2/typing_stubs_generation/generation.py @@ -9,6 +9,7 @@ import warnings from .ast_utils import get_enclosing_namespace from .predefined_types import PREDEFINED_TYPES +from .api_refinement import apply_manual_api_refinement from .nodes import (ASTNode, NamespaceNode, ClassNode, FunctionNode, EnumerationNode, ConstantNode) @@ -45,6 +46,18 @@ def generate_typing_stubs(root: NamespaceNode, output_path: Path): root (NamespaceNode): Root namespace node of the library AST. output_path (Path): Path to output directory. """ + # Perform special handling for function arguments that has some conventions + # not expressed in their API e.g. optionality of mutually exclusive arguments + # without default values: + # ```cxx + # cv::resize(cv::InputArray src, cv::OutputArray dst, cv::Size dsize, + # double fx = 0.0, double fy = 0.0, int interpolation); + # ``` + # should accept `None` as `dsize`: + # ```python + # cv2.resize(image, dsize=None, fx=0.5, fy=0.5) + # ``` + apply_manual_api_refinement(root) # Most of the time type nodes miss their full name (especially function # arguments and return types), so resolution should start from the narrowest # scope and gradually expanded. diff --git a/modules/python/src2/typing_stubs_generation/nodes/function_node.py b/modules/python/src2/typing_stubs_generation/nodes/function_node.py index a4e4f56561..4ebb90633e 100644 --- a/modules/python/src2/typing_stubs_generation/nodes/function_node.py +++ b/modules/python/src2/typing_stubs_generation/nodes/function_node.py @@ -10,10 +10,12 @@ class FunctionNode(ASTNode): This class defines an overload set rather then function itself, because function without overloads is represented as FunctionNode with 1 overload. """ - class Arg(NamedTuple): - name: str - type_node: Optional[TypeNode] = None - default_value: Optional[str] = None + class Arg: + def __init__(self, name: str, type_node: Optional[TypeNode] = None, + default_value: Optional[str] = None) -> None: + self.name = name + self.type_node = type_node + self.default_value = default_value @property def typename(self) -> Optional[str]: @@ -24,8 +26,18 @@ class FunctionNode(ASTNode): return self.type_node.relative_typename(root) return None - class RetType(NamedTuple): - type_node: TypeNode = NoneTypeNode("void") + def __str__(self) -> str: + return ( + f"Arg(name={self.name}, type_node={self.type_node}," + f" default_value={self.default_value})" + ) + + def __repr__(self) -> str: + return str(self) + + class RetType: + def __init__(self, type_node: TypeNode = NoneTypeNode("void")) -> None: + self.type_node = type_node @property def typename(self) -> str: @@ -34,6 +46,12 @@ class FunctionNode(ASTNode): def relative_typename(self, root: str) -> Optional[str]: return self.type_node.relative_typename(root) + def __str__(self) -> str: + return f"RetType(type_node={self.type_node})" + + def __repr__(self) -> str: + return str(self) + class Overload(NamedTuple): arguments: Sequence["FunctionNode.Arg"] = () return_type: Optional["FunctionNode.RetType"] = None diff --git a/modules/python/src2/typing_stubs_generator.py b/modules/python/src2/typing_stubs_generator.py index fa0b2b7a28..2ab19bfdf8 100644 --- a/modules/python/src2/typing_stubs_generator.py +++ b/modules/python/src2/typing_stubs_generator.py @@ -120,7 +120,11 @@ if sys.version_info >= (3, 6): @failures_wrapper.wrap_exceptions_as_warnings(ret_type_on_failure=ClassNodeStub) def find_class_node(self, class_info, namespaces): # type: (Any, Sequence[str]) -> ClassNode - return find_class_node(self.cv_root, class_info.full_original_name, namespaces) + return find_class_node( + self.cv_root, + SymbolName.parse(class_info.full_original_name, namespaces), + create_missing_namespaces=True + ) @failures_wrapper.wrap_exceptions_as_warnings(ret_type_on_failure=ClassNodeStub) def create_class_node(self, class_info, namespaces): From 69ebecc54f1dbc80e4cf8f0905eb79f6c1022247 Mon Sep 17 00:00:00 2001 From: Vadim Levin Date: Thu, 15 Jun 2023 23:10:10 +0300 Subject: [PATCH 2/2] feat: add OpenCV error class to cv2/__init__.pyi --- modules/python/src2/typing_stubs_generation/api_refinement.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/python/src2/typing_stubs_generation/api_refinement.py b/modules/python/src2/typing_stubs_generation/api_refinement.py index a3468c1567..f23167f914 100644 --- a/modules/python/src2/typing_stubs_generation/api_refinement.py +++ b/modules/python/src2/typing_stubs_generation/api_refinement.py @@ -8,6 +8,10 @@ from .ast_utils import find_function_node, SymbolName def apply_manual_api_refinement(root: NamespaceNode) -> None: + # Export OpenCV exception class + builtin_exception = root.add_class("Exception") + builtin_exception.is_exported = False + root.add_class("error", (builtin_exception, )) for symbol_name, refine_symbol in NODES_TO_REFINE.items(): refine_symbol(root, symbol_name)