1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-25 21:33:04 +04:00

Merge branch 4.x

This commit is contained in:
Alexander Smorkalov
2023-07-27 18:38:25 +03:00
151 changed files with 5199 additions and 1111 deletions
+57 -1
View File
@@ -5,6 +5,7 @@
#include "cv2_convert.hpp"
#include "cv2_numpy.hpp"
#include "cv2_util.hpp"
#include "opencv2/core/utils/logger.hpp"
PyTypeObject* pyopencv_Mat_TypePtr = nullptr;
@@ -24,6 +25,26 @@ static std::string pycv_dumpArray(const T* arr, int n)
return out.str();
}
static inline std::string getArrayTypeName(PyArrayObject* arr)
{
PyArray_Descr* dtype = PyArray_DESCR(arr);
PySafeObject dtype_str(PyObject_Str(reinterpret_cast<PyObject*>(dtype)));
if (!dtype_str)
{
// Fallback to typenum value
return cv::format("%d", PyArray_TYPE(arr));
}
std::string type_name;
if (!getUnicodeString(dtype_str, type_name))
{
// Failed to get string from bytes object - clear set TypeError and
// fallback to typenum value
PyErr_Clear();
return cv::format("%d", PyArray_TYPE(arr));
}
return type_name;
}
//======================================================================================================================
// --- Mat
@@ -80,6 +101,13 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info)
PyArrayObject* oarr = (PyArrayObject*) o;
if (info.outputarg && !PyArray_ISWRITEABLE(oarr))
{
failmsg("%s marked as output argument, but provided NumPy array "
"marked as readonly", info.name);
return false;
}
bool needcopy = false, needcast = false;
int typenum = PyArray_TYPE(oarr), new_typenum = typenum;
int type = typenum == NPY_UBYTE ? CV_8U :
@@ -102,7 +130,9 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info)
}
else
{
failmsg("%s data type = %d is not supported", info.name, typenum);
const std::string dtype_name = getArrayTypeName(oarr);
failmsg("%s data type = %s is not supported", info.name,
dtype_name.c_str());
return false;
}
}
@@ -728,6 +758,26 @@ PyObject* pyopencv_from(const Rect2d& r)
// --- RotatedRect
static inline bool convertToRotatedRect(PyObject* obj, RotatedRect& dst)
{
PyObject* type = PyObject_Type(obj);
if (getPyObjectAttr(type, "__module__") == MODULESTR &&
getPyObjectNameAttr(type) == "RotatedRect")
{
struct pyopencv_RotatedRect_t
{
PyObject_HEAD
cv::RotatedRect v;
};
dst = reinterpret_cast<pyopencv_RotatedRect_t*>(obj)->v;
Py_DECREF(type);
return true;
}
Py_DECREF(type);
return false;
}
template<>
bool pyopencv_to(PyObject* obj, RotatedRect& dst, const ArgInfo& info)
{
@@ -735,6 +785,12 @@ bool pyopencv_to(PyObject* obj, RotatedRect& dst, const ArgInfo& info)
{
return true;
}
// This is a workaround for compatibility with an initialization from tuple.
// Allows import RotatedRect as an object.
if (convertToRotatedRect(obj, dst))
{
return true;
}
if (!PySequence_Check(obj))
{
failmsg("Can't parse '%s' as RotatedRect."
+5 -1
View File
@@ -42,7 +42,7 @@ private:
/**
* Light weight RAII wrapper for `PyObject*` owning references.
* In comparisson to C++11 `std::unique_ptr` with custom deleter, it provides
* In comparison to C++11 `std::unique_ptr` with custom deleter, it provides
* implicit conversion functions that might be useful to initialize it with
* Python functions those returns owning references through the `PyObject**`
* e.g. `PyErr_Fetch` or directly pass it to functions those want to borrow
@@ -70,6 +70,10 @@ public:
return &obj_;
}
operator bool() {
return obj_ != nullptr;
}
PyObject* release()
{
PyObject* obj = obj_;
+7 -2
View File
@@ -231,6 +231,8 @@ simple_argtype_mapping = {
"c_string": ArgTypeInfo("char*", FormatStrings.string, '(char*)""'),
"string": ArgTypeInfo("std::string", FormatStrings.object, None, True),
"Stream": ArgTypeInfo("Stream", FormatStrings.object, 'Stream::Null()', True),
"cuda_Stream": ArgTypeInfo("cuda::Stream", FormatStrings.object, "cuda::Stream::Null()", True),
"cuda_GpuMat": ArgTypeInfo("cuda::GpuMat", FormatStrings.object, "cuda::GpuMat()", True),
"UMat": ArgTypeInfo("UMat", FormatStrings.object, 'UMat()', True), # FIXIT: switch to CV_EXPORTS_W_SIMPLE as UMat is already a some kind of smart pointer
}
@@ -509,14 +511,17 @@ class ArgInfo(object):
return self.enclosing_arg.name + '.' + self.name
def isbig(self):
return self.tp in ["Mat", "vector_Mat", "cuda::GpuMat", "GpuMat", "vector_GpuMat", "UMat", "vector_UMat"] # or self.tp.startswith("vector")
return self.tp in ["Mat", "vector_Mat",
"cuda::GpuMat", "cuda_GpuMat", "GpuMat",
"vector_GpuMat", "vector_cuda_GpuMat",
"UMat", "vector_UMat"] # or self.tp.startswith("vector")
def crepr(self):
return "ArgInfo(\"%s\", %d)" % (self.name, self.outputarg)
def find_argument_class_info(argument_type, function_namespace,
function_class_name, known_classes):
function_class_name, known_classes):
# type: (str, str, str, dict[str, ClassInfo]) -> ClassInfo | None
"""Tries to find corresponding class info for the provided argument type
+8 -2
View File
@@ -98,10 +98,10 @@ static inline bool getUnicodeString(PyObject * obj, std::string &str)
}
static inline
std::string getPyObjectNameAttr(PyObject* obj)
std::string getPyObjectAttr(PyObject* obj, const char* attrName)
{
std::string obj_name;
PyObject* cls_name_obj = PyObject_GetAttrString(obj, "__name__");
PyObject* cls_name_obj = PyObject_GetAttrString(obj, attrName);
if (cls_name_obj && !getUnicodeString(cls_name_obj, obj_name)) {
obj_name.clear();
}
@@ -117,6 +117,12 @@ std::string getPyObjectNameAttr(PyObject* obj)
return obj_name;
}
static inline
std::string getPyObjectNameAttr(PyObject* obj)
{
return getPyObjectAttr(obj, "__name__");
}
//==================================================================================================
#define CV_PY_FN_WITH_KW_(fn, flags) (PyCFunction)(void*)(PyCFunctionWithKeywords)(fn), (flags) | METH_VARARGS | METH_KEYWORDS
@@ -0,0 +1,337 @@
__all__ = [
"apply_manual_api_refinement"
]
from typing import cast, Sequence, Callable, Iterable
from .nodes import (NamespaceNode, FunctionNode, OptionalTypeNode, TypeNode,
ClassProperty, PrimitiveTypeNode, ASTNodeTypeNode,
AggregatedTypeNode, CallableTypeNode, AnyTypeNode,
TupleTypeNode, UnionTypeNode, ProtocolClassNode,
DictTypeNode, ClassTypeNode)
from .ast_utils import (find_function_node, SymbolName,
for_each_function_overload)
from .types_conversion import create_type_node
def apply_manual_api_refinement(root: NamespaceNode) -> None:
refine_highgui_module(root)
refine_cuda_module(root)
export_matrix_type_constants(root)
refine_dnn_module(root)
# Export OpenCV exception class
builtin_exception = root.add_class("Exception")
builtin_exception.is_exported = False
root.add_class("error", (builtin_exception, ), ERROR_CLASS_PROPERTIES)
for symbol_name, refine_symbol in NODES_TO_REFINE.items():
refine_symbol(root, symbol_name)
version_constant = root.add_constant("__version__", "<unused>")
version_constant._value_type = "str"
"""
def redirectError(
onError: Callable[[int, str, str, str, int], None] | None
) -> None: ...
"""
root.add_function("redirectError", [
FunctionNode.Arg(
"onError",
OptionalTypeNode(
CallableTypeNode(
"ErrorCallback",
[
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.str_(),
PrimitiveTypeNode.str_(),
PrimitiveTypeNode.str_(),
PrimitiveTypeNode.int_()
]
)
)
)
])
def export_matrix_type_constants(root: NamespaceNode) -> None:
MAX_PREDEFINED_CHANNELS = 4
depth_names = ("CV_8U", "CV_8S", "CV_16U", "CV_16S", "CV_32S",
"CV_32F", "CV_64F", "CV_16F")
for depth_value, depth_name in enumerate(depth_names):
# Export depth constants
root.add_constant(depth_name, str(depth_value))
# Export predefined types
for c in range(MAX_PREDEFINED_CHANNELS):
root.add_constant(f"{depth_name}C{c + 1}",
f"{depth_value + 8 * c}")
# Export type creation function
root.add_function(
f"{depth_name}C",
(FunctionNode.Arg("channels", PrimitiveTypeNode.int_()), ),
FunctionNode.RetType(PrimitiveTypeNode.int_())
)
# Export CV_MAKETYPE
root.add_function(
"CV_MAKETYPE",
(FunctionNode.Arg("depth", PrimitiveTypeNode.int_()),
FunctionNode.Arg("channels", PrimitiveTypeNode.int_())),
FunctionNode.RetType(PrimitiveTypeNode.int_())
)
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(
cast(TypeNode, overload.arguments[arg_idx].type_node)
)
return _make_optional_arg
def refine_cuda_module(root: NamespaceNode) -> None:
def fix_cudaoptflow_enums_names() -> None:
for class_name in ("NvidiaOpticalFlow_1_0", "NvidiaOpticalFlow_2_0"):
if class_name not in cuda_root.classes:
continue
opt_flow_class = cuda_root.classes[class_name]
_trim_class_name_from_argument_types(
for_each_function_overload(opt_flow_class), class_name
)
def fix_namespace_usage_scope(cuda_ns: NamespaceNode) -> None:
USED_TYPES = ("GpuMat", "Stream")
def fix_type_usage(type_node: TypeNode) -> None:
if isinstance(type_node, AggregatedTypeNode):
for item in type_node.items:
fix_type_usage(item)
if isinstance(type_node, ASTNodeTypeNode):
if type_node._typename in USED_TYPES:
type_node._typename = f"cuda_{type_node._typename}"
for overload in for_each_function_overload(cuda_ns):
if overload.return_type is not None:
fix_type_usage(overload.return_type.type_node)
for type_node in [arg.type_node for arg in overload.arguments
if arg.type_node is not None]:
fix_type_usage(type_node)
if "cuda" not in root.namespaces:
return
cuda_root = root.namespaces["cuda"]
fix_cudaoptflow_enums_names()
for ns in [ns for ns_name, ns in root.namespaces.items()
if ns_name.startswith("cuda")]:
fix_namespace_usage_scope(ns)
def refine_highgui_module(root: NamespaceNode) -> None:
# Check if library is built with enabled highgui module
if "destroyAllWindows" not in root.functions:
return
"""
def createTrackbar(trackbarName: str,
windowName: str,
value: int,
count: int,
onChange: Callable[[int], None]) -> None: ...
"""
root.add_function(
"createTrackbar",
[
FunctionNode.Arg("trackbarName", PrimitiveTypeNode.str_()),
FunctionNode.Arg("windowName", PrimitiveTypeNode.str_()),
FunctionNode.Arg("value", PrimitiveTypeNode.int_()),
FunctionNode.Arg("count", PrimitiveTypeNode.int_()),
FunctionNode.Arg("onChange",
CallableTypeNode("TrackbarCallback",
PrimitiveTypeNode.int_("int"))),
]
)
"""
def createButton(buttonName: str,
onChange: Callable[[tuple[int] | tuple[int, Any]], None],
userData: Any | None = ...,
buttonType: int = ...,
initialButtonState: int = ...) -> None: ...
"""
root.add_function(
"createButton",
[
FunctionNode.Arg("buttonName", PrimitiveTypeNode.str_()),
FunctionNode.Arg(
"onChange",
CallableTypeNode(
"ButtonCallback",
UnionTypeNode(
"onButtonChangeCallbackData",
[
TupleTypeNode("onButtonChangeCallbackData",
[PrimitiveTypeNode.int_(), ]),
TupleTypeNode("onButtonChangeCallbackData",
[PrimitiveTypeNode.int_(),
AnyTypeNode("void*")])
]
)
)),
FunctionNode.Arg("userData",
OptionalTypeNode(AnyTypeNode("void*")),
default_value="None"),
FunctionNode.Arg("buttonType", PrimitiveTypeNode.int_(),
default_value="0"),
FunctionNode.Arg("initialButtonState", PrimitiveTypeNode.int_(),
default_value="0")
]
)
"""
def setMouseCallback(
windowName: str,
onMouse: Callback[[int, int, int, int, Any | None], None],
param: Any | None = ...
) -> None: ...
"""
root.add_function(
"setMouseCallback",
[
FunctionNode.Arg("windowName", PrimitiveTypeNode.str_()),
FunctionNode.Arg(
"onMouse",
CallableTypeNode("MouseCallback", [
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.int_(),
OptionalTypeNode(AnyTypeNode("void*"))
])
),
FunctionNode.Arg("param", OptionalTypeNode(AnyTypeNode("void*")),
default_value="None")
]
)
def refine_dnn_module(root: NamespaceNode) -> None:
if "dnn" not in root.namespaces:
return
dnn_module = root.namespaces["dnn"]
"""
class LayerProtocol(Protocol):
def __init__(
self, params: dict[str, DictValue],
blobs: typing.Sequence[cv2.typing.MatLike]
) -> None: ...
def getMemoryShapes(
self, inputs: typing.Sequence[typing.Sequence[int]]
) -> typing.Sequence[typing.Sequence[int]]: ...
def forward(
self, inputs: typing.Sequence[cv2.typing.MatLike]
) -> typing.Sequence[cv2.typing.MatLike]: ...
"""
layer_proto = ProtocolClassNode("LayerProtocol", dnn_module)
layer_proto.add_function(
"__init__",
arguments=[
FunctionNode.Arg(
"params",
DictTypeNode(
"LayerParams", PrimitiveTypeNode.str_(),
create_type_node("cv::dnn::DictValue")
)
),
FunctionNode.Arg("blobs", create_type_node("vector<cv::Mat>"))
]
)
layer_proto.add_function(
"getMemoryShapes",
arguments=[
FunctionNode.Arg("inputs",
create_type_node("vector<vector<int>>"))
],
return_type=FunctionNode.RetType(
create_type_node("vector<vector<int>>")
)
)
layer_proto.add_function(
"forward",
arguments=[
FunctionNode.Arg("inputs", create_type_node("vector<cv::Mat>"))
],
return_type=FunctionNode.RetType(create_type_node("vector<cv::Mat>"))
)
"""
def dnn_registerLayer(layerTypeName: str,
layerClass: typing.Type[LayerProtocol]) -> None: ...
"""
root.add_function(
"dnn_registerLayer",
arguments=[
FunctionNode.Arg("layerTypeName", PrimitiveTypeNode.str_()),
FunctionNode.Arg(
"layerClass",
ClassTypeNode(ASTNodeTypeNode(
layer_proto.export_name, f"dnn.{layer_proto.export_name}"
))
)
]
)
"""
def dnn_unregisterLayer(layerTypeName: str) -> None: ...
"""
root.add_function(
"dnn_unregisterLayer",
arguments=[
FunctionNode.Arg("layerTypeName", PrimitiveTypeNode.str_())
]
)
def _trim_class_name_from_argument_types(
overloads: Iterable[FunctionNode.Overload],
class_name: str
) -> None:
separator = f"{class_name}_"
for overload in overloads:
for arg in [arg for arg in overload.arguments
if arg.type_node is not None]:
ast_node = cast(ASTNodeTypeNode, arg.type_node)
if class_name in ast_node.ctype_name:
fixed_name = ast_node._typename.split(separator)[-1]
ast_node._typename = fixed_name
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"),
}
ERROR_CLASS_PROPERTIES = (
ClassProperty("code", PrimitiveTypeNode.int_(), False),
ClassProperty("err", PrimitiveTypeNode.str_(), False),
ClassProperty("file", PrimitiveTypeNode.str_(), False),
ClassProperty("func", PrimitiveTypeNode.str_(), False),
ClassProperty("line", PrimitiveTypeNode.int_(), False),
ClassProperty("msg", PrimitiveTypeNode.str_(), False),
)
@@ -1,4 +1,5 @@
from typing import NamedTuple, Sequence, Tuple, Union, List, Dict
from typing import (NamedTuple, Sequence, Tuple, Union, List,
Dict, Callable, Optional, Generator, cast)
import keyword
from .nodes import (ASTNode, NamespaceNode, ClassNode, FunctionNode,
@@ -142,13 +143,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],
@@ -192,9 +204,7 @@ def create_function_node_in_scope(scope: Union[NamespaceNode, ClassNode],
outlist = variant.py_outlist
for _, argno in outlist:
assert argno >= 0, \
"Logic Error! Outlist contains function return type: {}".format(
outlist
)
f"Logic Error! Outlist contains function return type: {outlist}"
ret_types.append(create_type_node(variant.args[argno].tp))
@@ -323,12 +333,18 @@ def resolve_enum_scopes(root: NamespaceNode,
enum_node.parent = scope
def get_enclosing_namespace(node: ASTNode) -> NamespaceNode:
def get_enclosing_namespace(
node: ASTNode,
class_node_callback: Optional[Callable[[ClassNode], None]] = None
) -> NamespaceNode:
"""Traverses up nodes hierarchy to find closest enclosing namespace of the
passed node
Args:
node (ASTNode): Node to find a namespace for.
class_node_callback (Optional[Callable[[ClassNode], None]]): Optional
callable object invoked for each traversed class node in bottom-up
order. Defaults: None.
Returns:
NamespaceNode: Closest enclosing namespace of the provided node.
@@ -360,10 +376,61 @@ def get_enclosing_namespace(node: ASTNode) -> NamespaceNode:
"Can't find enclosing namespace for '{}' known as: '{}'".format(
node.full_export_name, node.native_name
)
if class_node_callback:
class_node_callback(cast(ClassNode, parent_node))
parent_node = parent_node.parent
return parent_node
def get_enum_module_and_export_name(enum_node: EnumerationNode) -> Tuple[str, str]:
"""Get export name of the enum node with its module name.
Note: Enumeration export names are prefixed with enclosing class names.
Args:
enum_node (EnumerationNode): Enumeration node to construct name for.
Returns:
Tuple[str, str]: a pair of enum export name and its full module name.
"""
enum_export_name = enum_node.export_name
def update_full_export_name(class_node: ClassNode) -> None:
nonlocal enum_export_name
enum_export_name = class_node.export_name + "_" + enum_export_name
namespace_node = get_enclosing_namespace(enum_node,
update_full_export_name)
return enum_export_name, namespace_node.full_export_name
def for_each_class(
node: Union[NamespaceNode, ClassNode]
) -> Generator[ClassNode, None, None]:
for cls in node.classes.values():
yield cls
if len(cls.classes):
yield from for_each_class(cls)
def for_each_function(
node: Union[NamespaceNode, ClassNode],
traverse_class_nodes: bool = True
) -> Generator[FunctionNode, None, None]:
yield from node.functions.values()
if traverse_class_nodes:
for cls in for_each_class(node):
yield from for_each_function(cls)
def for_each_function_overload(
node: Union[NamespaceNode, ClassNode],
traverse_class_nodes: bool = True
) -> Generator[FunctionNode.Overload, None, None]:
for func in for_each_function(node, traverse_class_nodes):
yield from func.overloads
if __name__ == '__main__':
import doctest
doctest.testmod()
@@ -2,19 +2,26 @@ __all__ = ("generate_typing_stubs", )
from io import StringIO
from pathlib import Path
from typing import (Generator, Type, Callable, NamedTuple, Union, Set, Dict,
Collection)
import re
from typing import (Callable, NamedTuple, Union, Set, Dict,
Collection, Tuple, List)
import warnings
from .ast_utils import get_enclosing_namespace
from .ast_utils import (get_enclosing_namespace,
get_enum_module_and_export_name,
for_each_function_overload,
for_each_class)
from .predefined_types import PREDEFINED_TYPES
from .api_refinement import apply_manual_api_refinement
from .nodes import (ASTNode, NamespaceNode, ClassNode, FunctionNode,
EnumerationNode, ConstantNode)
from .nodes import (ASTNode, ASTNodeType, NamespaceNode, ClassNode,
FunctionNode, EnumerationNode, ConstantNode,
ProtocolClassNode)
from .nodes.type_node import (TypeNode, AliasTypeNode, AliasRefTypeNode,
AggregatedTypeNode)
AggregatedTypeNode, ASTNodeTypeNode,
ConditionalAliasTypeNode, PrimitiveTypeNode)
def generate_typing_stubs(root: NamespaceNode, output_path: Path):
@@ -45,6 +52,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.
@@ -70,10 +89,11 @@ def generate_typing_stubs(root: NamespaceNode, output_path: Path):
# checked and at least 1 node is still unresolved.
root.resolve_type_nodes()
_generate_typing_module(root, output_path)
_populate_reexported_symbols(root)
_generate_typing_stubs(root, output_path)
def _generate_typing_stubs(root: NamespaceNode, output_path: Path):
def _generate_typing_stubs(root: NamespaceNode, output_path: Path) -> None:
output_path = Path(output_path) / root.export_name
output_path.mkdir(parents=True, exist_ok=True)
@@ -82,19 +102,24 @@ def _generate_typing_stubs(root: NamespaceNode, output_path: Path):
output_stream = StringIO()
# Add empty __all__ dunder on top of the module
output_stream.write("__all__: list[str] = []\n\n")
# Write required imports at the top of file
_write_required_imports(required_imports, output_stream)
# Write constants section, because constants don't impose any dependencies
_generate_section_stub(StubSection("# Constants", ConstantNode), root,
output_stream, 0)
_write_reexported_symbols_section(root, output_stream)
# NOTE: Enumerations require special handling, because all enumeration
# constants are exposed as module attributes
has_enums = _generate_section_stub(StubSection("# Enumerations", EnumerationNode),
root, output_stream, 0)
has_enums = _generate_section_stub(
StubSection("# Enumerations", ASTNodeType.Enumeration), root,
output_stream, 0
)
# Collect all enums from class level and export them to module level
for class_node in root.classes.values():
if _generate_enums_from_classes_tree(class_node, output_stream, indent=0):
if _generate_enums_from_classes_tree(class_node, output_stream,
indent=0):
has_enums = True
# 2 empty lines between enum and classes definitions
if has_enums:
@@ -112,14 +137,15 @@ def _generate_typing_stubs(root: NamespaceNode, output_path: Path):
class StubSection(NamedTuple):
name: str
node_type: Type[ASTNode]
node_type: ASTNodeType
STUB_SECTIONS = (
StubSection("# Constants", ConstantNode),
# StubSection("# Enumerations", EnumerationNode), # Skipped for now (special rules)
StubSection("# Classes", ClassNode),
StubSection("# Functions", FunctionNode)
StubSection("# Constants", ASTNodeType.Constant),
# Enumerations are skipped due to special handling rules
# StubSection("# Enumerations", ASTNodeType.Enumeration),
StubSection("# Classes", ASTNodeType.Class),
StubSection("# Functions", ASTNodeType.Function)
)
@@ -228,9 +254,9 @@ def _generate_class_stub(class_node: ClassNode, output_stream: StringIO,
else:
bases.append(base.export_name)
inheritance_str = "({})".format(
', '.join(bases)
)
inheritance_str = f"({', '.join(bases)})"
elif isinstance(class_node, ProtocolClassNode):
inheritance_str = "(Protocol)"
else:
inheritance_str = ""
@@ -269,7 +295,8 @@ def _generate_class_stub(class_node: ClassNode, output_stream: StringIO,
def _generate_constant_stub(constant_node: ConstantNode,
output_stream: StringIO, indent: int = 0,
extra_export_prefix: str = "") -> None:
extra_export_prefix: str = "",
generate_uppercase_version: bool = True) -> Tuple[str, ...]:
"""Generates stub for the provided constant node.
Args:
@@ -277,18 +304,36 @@ def _generate_constant_stub(constant_node: ConstantNode,
output_stream (StringIO): Output stream for constant stub.
indent (int, optional): Indent used for each line written to
`output_stream`. Defaults to 0.
extra_export_prefix (str, optional) Extra prefix added to the export
extra_export_prefix (str, optional): Extra prefix added to the export
constant name. Defaults to empty string.
generate_uppercase_version (bool, optional): Generate uppercase version
alongside the normal one. Defaults to True.
Returns:
Tuple[str, ...]: exported constants names.
"""
output_stream.write(
"{indent}{prefix}{name}: {value_type}\n".format(
prefix=extra_export_prefix,
name=constant_node.export_name,
value_type=constant_node.value_type,
indent=" " * indent
def write_constant_to_stream(export_name: str) -> None:
output_stream.write(
"{indent}{name}: {value_type}\n".format(
name=export_name,
value_type=constant_node.value_type,
indent=" " * indent
)
)
)
export_name = extra_export_prefix + constant_node.export_name
write_constant_to_stream(export_name)
if generate_uppercase_version:
# Handle Python "magic" constants like __version__
if re.match(r"^__.*__$", export_name) is not None:
return export_name,
uppercase_name = re.sub(r"([a-z])([A-Z])", r"\1_\2", export_name).upper()
if export_name != uppercase_name:
write_constant_to_stream(uppercase_name)
return export_name, uppercase_name
return export_name,
def _generate_enumeration_stub(enumeration_node: EnumerationNode,
@@ -353,18 +398,20 @@ def _generate_enumeration_stub(enumeration_node: EnumerationNode,
entries_extra_prefix = extra_export_prefix
if enumeration_node.is_scoped:
entries_extra_prefix += enumeration_node.export_name + "_"
generated_constants_entries: List[str] = []
for entry in enumeration_node.constants.values():
_generate_constant_stub(entry, output_stream, indent, entries_extra_prefix)
generated_constants_entries.extend(
_generate_constant_stub(entry, output_stream, indent, entries_extra_prefix)
)
# Unnamed enumerations are skipped as definition
if enumeration_node.export_name.endswith("<unnamed>"):
output_stream.write("\n")
return
output_stream.write(
"{indent}{export_prefix}{name} = int # One of [{entries}]\n\n".format(
'{indent}{export_prefix}{name} = int\n{indent}"""One of [{entries}]"""\n\n'.format(
export_prefix=extra_export_prefix,
name=enumeration_node.export_name,
entries=", ".join(entry.export_name
for entry in enumeration_node.constants.values()),
entries=", ".join(generated_constants_entries),
indent=" " * indent
)
)
@@ -500,35 +547,12 @@ def check_overload_presence(node: Union[NamespaceNode, ClassNode]) -> bool:
otherwise.
"""
for func_node in node.functions.values():
if len(func_node.overloads):
if len(func_node.overloads) > 1:
return True
return False
def _for_each_class(node: Union[NamespaceNode, ClassNode]) \
-> Generator[ClassNode, None, None]:
for cls in node.classes.values():
yield cls
if len(cls.classes):
yield from _for_each_class(cls)
def _for_each_function(node: Union[NamespaceNode, ClassNode]) \
-> Generator[FunctionNode, None, None]:
for func in node.functions.values():
yield func
for cls in node.classes.values():
yield from _for_each_function(cls)
def _for_each_function_overload(node: Union[NamespaceNode, ClassNode]) \
-> Generator[FunctionNode.Overload, None, None]:
for func in _for_each_function(node):
for overload in func.overloads:
yield overload
def _collect_required_imports(root: NamespaceNode) -> Set[str]:
def _collect_required_imports(root: NamespaceNode) -> Collection[str]:
"""Collects all imports required for classes and functions typing stubs
declarations.
@@ -536,8 +560,8 @@ def _collect_required_imports(root: NamespaceNode) -> Set[str]:
root (NamespaceNode): Namespace node to collect imports for
Returns:
Set[str]: Collection of unique `import smth` statements required for
classes and function declarations of `root` node.
Collection[str]: Collection of unique `import smth` statements required
for classes and function declarations of `root` node.
"""
def _add_required_usage_imports(type_node: TypeNode, imports: Set[str]):
@@ -550,7 +574,8 @@ def _collect_required_imports(root: NamespaceNode) -> Set[str]:
has_overload = check_overload_presence(root)
# if there is no module-level functions with overload, check its presence
# during class traversing, including their inner-classes
for cls in _for_each_class(root):
has_protocol = False
for cls in for_each_class(root):
if not has_overload and check_overload_presence(cls):
has_overload = True
required_imports.add("import typing")
@@ -564,12 +589,15 @@ def _collect_required_imports(root: NamespaceNode) -> Set[str]:
required_imports.add(
"import " + base_namespace.full_export_name
)
if isinstance(cls, ProtocolClassNode):
has_protocol = True
if has_overload:
required_imports.add("import typing")
# Importing modules required to resolve functions arguments
for overload in _for_each_function_overload(root):
for arg in filter(lambda a: a.type_node is not None, overload.arguments):
for overload in for_each_function_overload(root):
for arg in filter(lambda a: a.type_node is not None,
overload.arguments):
_add_required_usage_imports(arg.type_node, required_imports) # type: ignore
if overload.return_type is not None:
_add_required_usage_imports(overload.return_type.type_node,
@@ -579,7 +607,74 @@ def _collect_required_imports(root: NamespaceNode) -> Set[str]:
if root_import in required_imports:
required_imports.remove(root_import)
return required_imports
if has_protocol:
required_imports.add("import sys")
ordered_required_imports = sorted(required_imports)
# Protocol import always goes as last import statement
if has_protocol:
ordered_required_imports.append(
"""if sys.version_info >= (3, 8):
from typing import Protocol
else:
from typing_extensions import Protocol"""
)
return ordered_required_imports
def _populate_reexported_symbols(root: NamespaceNode) -> None:
# Re-export all submodules to allow referencing symbols in submodules
# without submodule import. Example:
# `cv2.aruco.ArucoDetector` should be accessible without `import cv2.aruco`
def _reexport_submodule(ns: NamespaceNode) -> None:
for submodule in ns.namespaces.values():
ns.reexported_submodules.append(submodule.export_name)
_reexport_submodule(submodule)
_reexport_submodule(root)
# Special cases, symbols defined in possible pure Python submodules
# should be
root.reexported_submodules_symbols["mat_wrapper"].append("Mat")
def _write_reexported_symbols_section(module: NamespaceNode,
output_stream: StringIO) -> None:
"""Write re-export section for the given module.
Re-export statements have from `from module_name import smth as smth`.
Example:
```python
from cv2 import aruco as aruco
from cv2 import cuda as cuda
from cv2 import ml as ml
from cv2.mat_wrapper import Mat as Mat
```
Args:
module (NamespaceNode): Module with re-exported symbols.
output_stream (StringIO): Output stream for re-export statements.
"""
parent_name = module.full_export_name
for submodule in sorted(module.reexported_submodules):
output_stream.write(
"from {0} import {1} as {1}\n".format(parent_name, submodule)
)
for submodule, symbols in sorted(module.reexported_submodules_symbols.items(),
key=lambda kv: kv[0]):
for symbol in symbols:
output_stream.write(
"from {0}.{1} import {2} as {2}\n".format(
parent_name, submodule, symbol
)
)
if len(module.reexported_submodules) or \
len(module.reexported_submodules_symbols):
output_stream.write("\n\n")
def _write_required_imports(required_imports: Collection[str],
@@ -592,7 +687,7 @@ def _write_required_imports(required_imports: Collection[str],
output_stream (StringIO): Output stream for import statements.
"""
for required_import in sorted(required_imports):
for required_import in required_imports:
output_stream.write(required_import)
output_stream.write("\n")
if len(required_imports):
@@ -614,31 +709,82 @@ def _generate_typing_module(root: NamespaceNode, output_path: Path) -> None:
f"Provided type node '{type_node.ctype_name}' is not an aggregated type"
for item in filter(lambda i: isinstance(i, AliasRefTypeNode), type_node):
register_alias(PREDEFINED_TYPES[item.ctype_name]) # type: ignore
type_node = PREDEFINED_TYPES[item.ctype_name]
if isinstance(type_node, AliasTypeNode):
register_alias(type_node)
elif isinstance(type_node, ConditionalAliasTypeNode):
conditional_type_nodes[type_node.ctype_name] = type_node
def create_alias_for_enum_node(enum_node_alias: AliasTypeNode) -> ConditionalAliasTypeNode:
"""Create conditional int alias corresponding to the given enum node.
Args:
enum_node (AliasTypeNode): Enumeration node to create conditional
int alias for.
Returns:
ConditionalAliasTypeNode: conditional int alias node with same
export name as enum.
"""
enum_node = enum_node_alias.ast_node
assert enum_node.node_type == ASTNodeType.Enumeration, \
f"{enum_node} has wrong node type. Expected type: Enumeration."
enum_export_name, enum_module_name = get_enum_module_and_export_name(
enum_node
)
return ConditionalAliasTypeNode(
enum_export_name,
"typing.TYPE_CHECKING",
positive_branch_type=enum_node_alias,
negative_branch_type=PrimitiveTypeNode.int_(enum_export_name),
condition_required_imports=("import typing", )
)
def register_alias(alias_node: AliasTypeNode) -> None:
typename = alias_node.typename
# Check if alias is already registered
if typename in aliases:
return
# Collect required imports for alias definition
for required_import in alias_node.required_definition_imports:
required_imports.add(required_import)
if isinstance(alias_node.value, AggregatedTypeNode):
# Check if collection contains a link to another alias
register_alias_links_from_aggregated_type(alias_node.value)
# Remove references to alias nodes
for i, item in enumerate(alias_node.value.items):
# Process enumerations only
if not isinstance(item, ASTNodeTypeNode) or item.ast_node is None:
continue
if item.ast_node.node_type != ASTNodeType.Enumeration:
continue
enum_node = create_alias_for_enum_node(item)
alias_node.value.items[i] = enum_node
conditional_type_nodes[enum_node.ctype_name] = enum_node
if isinstance(alias_node.value, ASTNodeTypeNode) \
and alias_node.value.ast_node == ASTNodeType.Enumeration:
enum_node = create_alias_for_enum_node(alias_node.ast_node)
conditional_type_nodes[enum_node.ctype_name] = enum_node
return
# Strip module prefix from aliased types
aliases[typename] = alias_node.value.full_typename.replace(
root.export_name + ".typing.", ""
)
if alias_node.doc is not None:
aliases[typename] += f'\n"""{alias_node.doc}"""'
for required_import in alias_node.required_definition_imports:
required_imports.add(required_import)
output_path = Path(output_path) / root.export_name / "typing"
output_path.mkdir(parents=True, exist_ok=True)
required_imports: Set[str] = set()
aliases: Dict[str, str] = {}
conditional_type_nodes: Dict[str, ConditionalAliasTypeNode] = {}
# Resolve each node and register aliases
TypeNode.compatible_to_runtime_usage = True
@@ -646,6 +792,12 @@ def _generate_typing_module(root: NamespaceNode, output_path: Path) -> None:
node.resolve(root)
if isinstance(node, AliasTypeNode):
register_alias(node)
elif isinstance(node, ConditionalAliasTypeNode):
conditional_type_nodes[node.ctype_name] = node
for node in conditional_type_nodes.values():
for required_import in node.required_definition_imports:
required_imports.add(required_import)
output_stream = StringIO()
output_stream.write("__all__ = [\n")
@@ -655,11 +807,14 @@ def _generate_typing_module(root: NamespaceNode, output_path: Path) -> None:
_write_required_imports(required_imports, output_stream)
# Add type checking time definitions as generated __init__.py content
for _, type_node in conditional_type_nodes.items():
output_stream.write(f"if {type_node.condition}:\n ")
output_stream.write(f"{type_node.typename} = {type_node.positive_branch_type.full_typename}\nelse:\n")
output_stream.write(f" {type_node.typename} = {type_node.negative_branch_type.full_typename}\n\n\n")
for alias_name, alias_type in aliases.items():
output_stream.write(alias_name)
output_stream.write(" = ")
output_stream.write(alias_type)
output_stream.write("\n")
output_stream.write(f"{alias_name} = {alias_type}\n")
TypeNode.compatible_to_runtime_usage = False
(output_path / "__init__.py").write_text(output_stream.getvalue())
@@ -669,8 +824,8 @@ StubGenerator = Callable[[ASTNode, StringIO, int], None]
NODE_TYPE_TO_STUB_GENERATOR = {
ClassNode: _generate_class_stub,
ConstantNode: _generate_constant_stub,
EnumerationNode: _generate_enumeration_stub,
FunctionNode: _generate_function_stub
ASTNodeType.Class: _generate_class_stub,
ASTNodeType.Constant: _generate_constant_stub,
ASTNodeType.Enumeration: _generate_enumeration_stub,
ASTNodeType.Function: _generate_function_stub
}
@@ -1,11 +1,12 @@
from .node import ASTNode
from .node import ASTNode, ASTNodeType
from .namespace_node import NamespaceNode
from .class_node import ClassNode, ClassProperty
from .class_node import ClassNode, ClassProperty, ProtocolClassNode
from .function_node import FunctionNode
from .enumeration_node import EnumerationNode
from .constant_node import ConstantNode
from .type_node import (
TypeNode, OptionalTypeNode, UnionTypeNode, NoneTypeNode, TupleTypeNode,
ASTNodeTypeNode, AliasTypeNode, SequenceTypeNode, AnyTypeNode,
AggregatedTypeNode, NDArrayTypeNode, AliasRefTypeNode,
AggregatedTypeNode, NDArrayTypeNode, AliasRefTypeNode, PrimitiveTypeNode,
CallableTypeNode, DictTypeNode, ClassTypeNode
)
@@ -63,8 +63,9 @@ class ClassNode(ASTNode):
return 1 + sum(base.weight for base in self.bases)
@property
def children_types(self) -> Tuple[Type[ASTNode], ...]:
return (ClassNode, FunctionNode, EnumerationNode, ConstantNode)
def children_types(self) -> Tuple[ASTNodeType, ...]:
return (ASTNodeType.Class, ASTNodeType.Function,
ASTNodeType.Enumeration, ASTNodeType.Constant)
@property
def node_type(self) -> ASTNodeType:
@@ -72,19 +73,19 @@ class ClassNode(ASTNode):
@property
def classes(self) -> Dict[str, "ClassNode"]:
return self._children[ClassNode]
return self._children[ASTNodeType.Class]
@property
def functions(self) -> Dict[str, FunctionNode]:
return self._children[FunctionNode]
return self._children[ASTNodeType.Function]
@property
def enumerations(self) -> Dict[str, EnumerationNode]:
return self._children[EnumerationNode]
return self._children[ASTNodeType.Enumeration]
@property
def constants(self) -> Dict[str, ConstantNode]:
return self._children[ConstantNode]
return self._children[ASTNodeType.Constant]
def add_class(self, name: str,
bases: Sequence["weakref.ProxyType[ClassNode]"] = (),
@@ -179,3 +180,11 @@ class ClassNode(ASTNode):
self.full_export_name, root.full_export_name, errors
)
)
class ProtocolClassNode(ClassNode):
def __init__(self, name: str, parent: Optional[ASTNode] = None,
export_name: Optional[str] = None,
properties: Sequence[ClassProperty] = ()) -> None:
super().__init__(name, parent, export_name, bases=(),
properties=properties)
@@ -1,4 +1,4 @@
from typing import Type, Optional, Tuple
from typing import Optional, Tuple
from .node import ASTNode, ASTNodeType
@@ -11,9 +11,10 @@ class ConstantNode(ASTNode):
export_name: Optional[str] = None) -> None:
super().__init__(name, parent, export_name)
self.value = value
self._value_type = "int"
@property
def children_types(self) -> Tuple[Type[ASTNode], ...]:
def children_types(self) -> Tuple[ASTNodeType, ...]:
return ()
@property
@@ -22,7 +23,7 @@ class ConstantNode(ASTNode):
@property
def value_type(self) -> str:
return 'int'
return self._value_type
def __str__(self) -> str:
return "Constant('{}' exported as '{}': {})".format(
@@ -18,8 +18,8 @@ class EnumerationNode(ASTNode):
self.is_scoped = is_scoped
@property
def children_types(self) -> Tuple[Type[ASTNode], ...]:
return (ConstantNode, )
def children_types(self) -> Tuple[ASTNodeType, ...]:
return (ASTNodeType.Constant, )
@property
def node_type(self) -> ASTNodeType:
@@ -27,7 +27,7 @@ class EnumerationNode(ASTNode):
@property
def constants(self) -> Dict[str, ConstantNode]:
return self._children[ConstantNode]
return self._children[ASTNodeType.Constant]
def add_constant(self, name: str, value: str) -> ConstantNode:
return self._add_child(ConstantNode, name, value=value)
@@ -1,4 +1,4 @@
from typing import NamedTuple, Sequence, Type, Optional, Tuple, List
from typing import NamedTuple, Sequence, Optional, Tuple, List
from .node import ASTNode, ASTNodeType
from .type_node import TypeNode, NoneTypeNode, TypeResolutionError
@@ -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
@@ -80,7 +98,7 @@ class FunctionNode(ASTNode):
return ASTNodeType.Function
@property
def children_types(self) -> Tuple[Type[ASTNode], ...]:
def children_types(self) -> Tuple[ASTNodeType, ...]:
return ()
def add_overload(self, arguments: Sequence["FunctionNode.Arg"] = (),
@@ -1,14 +1,13 @@
from typing import Type, Iterable, Sequence, Tuple, Optional, Dict
import itertools
import weakref
from .node import ASTNode, ASTNodeType
from collections import defaultdict
from typing import Dict, List, Optional, Sequence, Tuple
from .class_node import ClassNode, ClassProperty
from .function_node import FunctionNode
from .enumeration_node import EnumerationNode
from .constant_node import ConstantNode
from .enumeration_node import EnumerationNode
from .function_node import FunctionNode
from .node import ASTNode, ASTNodeType
from .type_node import TypeResolutionError
@@ -18,34 +17,45 @@ class NamespaceNode(ASTNode):
NamespaceNode can have other namespaces, classes, functions, enumerations
and global constants as its children nodes.
"""
def __init__(self, name: str, parent: Optional[ASTNode] = None,
export_name: Optional[str] = None) -> None:
super().__init__(name, parent, export_name)
self.reexported_submodules: List[str] = []
"""List of reexported submodules"""
self.reexported_submodules_symbols: Dict[str, List[str]] = defaultdict(list)
"""Mapping between submodules export names and their symbols re-exported
in this module"""
@property
def node_type(self) -> ASTNodeType:
return ASTNodeType.Namespace
@property
def children_types(self) -> Tuple[Type[ASTNode], ...]:
return (NamespaceNode, ClassNode, FunctionNode,
EnumerationNode, ConstantNode)
def children_types(self) -> Tuple[ASTNodeType, ...]:
return (ASTNodeType.Namespace, ASTNodeType.Class, ASTNodeType.Function,
ASTNodeType.Enumeration, ASTNodeType.Constant)
@property
def namespaces(self) -> Dict[str, "NamespaceNode"]:
return self._children[NamespaceNode]
return self._children[ASTNodeType.Namespace]
@property
def classes(self) -> Dict[str, ClassNode]:
return self._children[ClassNode]
return self._children[ASTNodeType.Class]
@property
def functions(self) -> Dict[str, FunctionNode]:
return self._children[FunctionNode]
return self._children[ASTNodeType.Function]
@property
def enumerations(self) -> Dict[str, EnumerationNode]:
return self._children[EnumerationNode]
return self._children[ASTNodeType.Enumeration]
@property
def constants(self) -> Dict[str, ConstantNode]:
return self._children[ConstantNode]
return self._children[ASTNodeType.Constant]
def add_namespace(self, name: str) -> "NamespaceNode":
return self._add_child(NamespaceNode, name)
@@ -1,7 +1,7 @@
import abc
import enum
import itertools
from typing import (Iterator, Type, TypeVar, Iterable, Dict,
from typing import (Iterator, Type, TypeVar, Dict,
Optional, Tuple, DefaultDict)
from collections import defaultdict
@@ -70,22 +70,22 @@ class ASTNode:
self._parent: Optional["ASTNode"] = None
self.parent = parent
self.is_exported = True
self._children: DefaultDict[NodeType, NameToNode] = defaultdict(dict)
self._children: DefaultDict[ASTNodeType, NameToNode] = defaultdict(dict)
def __str__(self) -> str:
return "{}('{}' exported as '{}')".format(
type(self).__name__.replace("Node", ""), self.name, self.export_name
self.node_type.name, self.name, self.export_name
)
def __repr__(self) -> str:
return str(self)
@abc.abstractproperty
def children_types(self) -> Tuple[Type["ASTNode"], ...]:
def children_types(self) -> Tuple[ASTNodeType, ...]:
"""Set of ASTNode types that are allowed to be children of this node
Returns:
Tuple[Type[ASTNode], ...]: Types of children nodes
Tuple[ASTNodeType, ...]: Types of children nodes
"""
pass
@@ -99,6 +99,9 @@ class ASTNode:
"""
pass
def node_type_name(self) -> str:
return f"{self.node_type.name}::{self.name}"
@property
def name(self) -> str:
return self.__name
@@ -126,11 +129,11 @@ class ASTNode:
"but got: {}".format(type(value))
if value is not None:
value.__check_child_before_add(type(self), self.name)
value.__check_child_before_add(self, self.name)
# Detach from previous parent
if self._parent is not None:
self._parent._children[type(self)].pop(self.name)
self._parent._children[self.node_type].pop(self.name)
if value is None:
self._parent = None
@@ -138,28 +141,26 @@ class ASTNode:
# Set a weak reference to a new parent and add self to its children
self._parent = weakref.proxy(value)
value._children[type(self)][self.name] = self
value._children[self.node_type][self.name] = self
def __check_child_before_add(self, child_type: Type[ASTNodeSubtype],
def __check_child_before_add(self, child: ASTNodeSubtype,
name: str) -> None:
assert len(self.children_types) > 0, \
"Trying to add child node '{}::{}' to node '{}::{}' " \
"that can't have children nodes".format(child_type.__name__, name,
type(self).__name__,
self.name)
assert len(self.children_types) > 0, (
f"Trying to add child node '{child.node_type_name}' to node "
f"'{self.node_type_name}' that can't have children nodes"
)
assert child_type in self.children_types, \
"Trying to add child node '{}::{}' to node '{}::{}' " \
assert child.node_type in self.children_types, \
"Trying to add child node '{}' to node '{}' " \
"that supports only ({}) as its children types".format(
child_type.__name__, name, type(self).__name__, self.name,
",".join(t.__name__ for t in self.children_types)
child.node_type_name, self.node_type_name,
",".join(t.name for t in self.children_types)
)
if self._find_child(child_type, name) is not None:
if self._find_child(child.node_type, name) is not None:
raise ValueError(
"Node '{}::{}' already has a child '{}::{}'".format(
type(self).__name__, self.name, child_type.__name__, name
)
f"Node '{self.node_type_name}' already has a "
f"child '{child.node_type_name}'"
)
def _add_child(self, child_type: Type[ASTNodeSubtype], name: str,
@@ -180,15 +181,14 @@ class ASTNode:
Returns:
ASTNodeSubtype: Created ASTNode
"""
self.__check_child_before_add(child_type, name)
return child_type(name, parent=self, **kwargs)
def _find_child(self, child_type: Type[ASTNodeSubtype],
def _find_child(self, child_type: ASTNodeType,
name: str) -> Optional[ASTNodeSubtype]:
"""Looks for child node with the given type and name.
Args:
child_type (Type[ASTNodeSubtype]): Type of the child node.
child_type (ASTNodeType): Type of the child node.
name (str): Name of the child node.
Returns:
@@ -307,14 +307,31 @@ class AliasTypeNode(TypeNode):
return cls(ctype_name, PrimitiveTypeNode.float_(), export_name, doc)
@classmethod
def array_(cls, ctype_name: str, shape: Optional[Tuple[int, ...]],
dtype: Optional[str] = None, export_name: Optional[str] = None,
doc: Optional[str] = None):
def array_ref_(cls, ctype_name: str, array_ref_name: str,
shape: Optional[Tuple[int, ...]],
dtype: Optional[str] = None,
export_name: Optional[str] = None,
doc: Optional[str] = None):
"""Create alias to array reference alias `array_ref_name`.
This is required to preserve backward compatibility with Python < 3.9
and NumPy 1.20, when NumPy module introduces generics support.
Args:
ctype_name (str): Name of the alias.
array_ref_name (str): Name of the conditional array alias.
shape (Optional[Tuple[int, ...]]): Array shape.
dtype (Optional[str], optional): Array type. Defaults to None.
export_name (Optional[str], optional): Alias export name.
Defaults to None.
doc (Optional[str], optional): Documentation string for alias.
Defaults to None.
"""
if doc is None:
doc = "Shape: " + str(shape)
doc = f"NDArray(shape={shape}, dtype={dtype})"
else:
doc += ". Shape: " + str(shape)
return cls(ctype_name, NDArrayTypeNode(ctype_name, shape, dtype),
doc += f". NDArray(shape={shape}, dtype={dtype})"
return cls(ctype_name, AliasRefTypeNode(array_ref_name),
export_name, doc)
@classmethod
@@ -376,24 +393,115 @@ class AliasTypeNode(TypeNode):
export_name, doc)
class NDArrayTypeNode(TypeNode):
"""Type node representing NumPy ndarray.
class ConditionalAliasTypeNode(TypeNode):
"""Type node representing an alias protected by condition checked in runtime.
For typing-related conditions, prefer using typing.TYPE_CHECKING. For a full explanation, see:
https://github.com/opencv/opencv/pull/23927#discussion_r1256326835
Example:
```python
if typing.TYPE_CHECKING
NumPyArray = numpy.ndarray[typing.Any, numpy.dtype[numpy.generic]]
else:
NumPyArray = numpy.ndarray
```
is defined as follows:
```python
ConditionalAliasTypeNode(
"NumPyArray",
'typing.TYPE_CHECKING',
NDArrayTypeNode("NumPyArray"),
NDArrayTypeNode("NumPyArray", use_numpy_generics=False),
condition_required_imports=("import typing",)
)
```
"""
def __init__(self, ctype_name: str, shape: Optional[Tuple[int, ...]] = None,
dtype: Optional[str] = None) -> None:
def __init__(self, ctype_name: str, condition: str,
positive_branch_type: TypeNode,
negative_branch_type: TypeNode,
export_name: Optional[str] = None,
condition_required_imports: Sequence[str] = ()) -> None:
super().__init__(ctype_name)
self.shape = shape
self.dtype = dtype
self.condition = condition
self.positive_branch_type = positive_branch_type
self.positive_branch_type.ctype_name = self.ctype_name
self.negative_branch_type = negative_branch_type
self.negative_branch_type.ctype_name = self.ctype_name
self._export_name = export_name
self._condition_required_imports = condition_required_imports
@property
def typename(self) -> str:
return "numpy.ndarray[{shape}, numpy.dtype[{dtype}]]".format(
# NOTE: Shape is not fully supported yet
# shape=self.shape if self.shape is not None else "typing.Any",
shape="typing.Any",
dtype=self.dtype if self.dtype is not None else "numpy.generic"
if self._export_name is not None:
return self._export_name
return self.ctype_name
@property
def full_typename(self) -> str:
return "cv2.typing." + self.typename
@property
def required_definition_imports(self) -> Generator[str, None, None]:
yield from self.positive_branch_type.required_usage_imports
yield from self.negative_branch_type.required_usage_imports
yield from self._condition_required_imports
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import cv2.typing"
@property
def is_resolved(self) -> bool:
return self.positive_branch_type.is_resolved \
and self.negative_branch_type.is_resolved
def resolve(self, root: ASTNode):
try:
self.positive_branch_type.resolve(root)
self.negative_branch_type.resolve(root)
except TypeResolutionError as e:
raise TypeResolutionError(
'Failed to resolve alias "{}" exposed as "{}"'.format(
self.ctype_name, self.typename
)
) from e
@classmethod
def numpy_array_(cls, ctype_name: str, export_name: Optional[str] = None,
shape: Optional[Tuple[int, ...]] = None,
dtype: Optional[str] = None):
"""Type subscription is not possible in python 3.8 and older numpy versions."""
return cls(
ctype_name,
"typing.TYPE_CHECKING",
NDArrayTypeNode(ctype_name, shape, dtype),
NDArrayTypeNode(ctype_name, shape, dtype,
use_numpy_generics=False),
condition_required_imports=("import typing",)
)
class NDArrayTypeNode(TypeNode):
"""Type node representing NumPy ndarray.
"""
def __init__(self, ctype_name: str,
shape: Optional[Tuple[int, ...]] = None,
dtype: Optional[str] = None,
use_numpy_generics: bool = True) -> None:
super().__init__(ctype_name)
self.shape = shape
self.dtype = dtype
self._use_numpy_generics = use_numpy_generics
@property
def typename(self) -> str:
if self._use_numpy_generics:
# NOTE: Shape is not fully supported yet
dtype = self.dtype if self.dtype is not None else "numpy.generic"
return f"numpy.ndarray[typing.Any, numpy.dtype[{dtype}]]"
return "numpy.ndarray"
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import numpy"
@@ -417,6 +525,10 @@ class ASTNodeTypeNode(TypeNode):
self._module_name = module_name
self._ast_node: Optional[weakref.ProxyType[ASTNode]] = None
@property
def ast_node(self):
return self._ast_node
@property
def typename(self) -> str:
if self._ast_node is None:
@@ -557,13 +669,14 @@ class ContainerTypeNode(AggregatedTypeNode):
@property
def required_definition_imports(self) -> Generator[str, None, None]:
yield "import typing"
return super().required_definition_imports
yield from super().required_definition_imports
@property
def required_usage_imports(self) -> Generator[str, None, None]:
if TypeNode.compatible_to_runtime_usage:
yield "import typing"
return super().required_usage_imports
yield from super().required_usage_imports
@abc.abstractproperty
def type_format(self) -> str:
pass
@@ -726,6 +839,21 @@ class CallableTypeNode(AggregatedTypeNode):
yield from super().required_usage_imports
class ClassTypeNode(ContainerTypeNode):
"""Type node representing types themselves (refer to typing.Type)
"""
def __init__(self, value: TypeNode) -> None:
super().__init__(value.ctype_name, (value,))
@property
def type_format(self) -> str:
return "typing.Type[{}]"
@property
def types_separator(self) -> str:
return ", "
def _resolve_symbol(root: Optional[ASTNode], full_symbol_name: str) -> Optional[ASTNode]:
"""Searches for a symbol with the given full export name in the AST
starting from the `root`.
@@ -1,7 +1,7 @@
from .nodes.type_node import (
AliasTypeNode, AliasRefTypeNode, PrimitiveTypeNode,
ASTNodeTypeNode, NDArrayTypeNode, NoneTypeNode, SequenceTypeNode,
TupleTypeNode, UnionTypeNode, AnyTypeNode
TupleTypeNode, UnionTypeNode, AnyTypeNode, ConditionalAliasTypeNode
)
# Set of predefined types used to cover cases when library doesn't
@@ -22,6 +22,10 @@ _PREDEFINED_TYPES = (
PrimitiveTypeNode.int_("uchar"),
PrimitiveTypeNode.int_("unsigned"),
PrimitiveTypeNode.int_("int64"),
PrimitiveTypeNode.int_("uint8_t"),
PrimitiveTypeNode.int_("int8_t"),
PrimitiveTypeNode.int_("int32_t"),
PrimitiveTypeNode.int_("uint32_t"),
PrimitiveTypeNode.int_("size_t"),
PrimitiveTypeNode.float_("float"),
PrimitiveTypeNode.float_("double"),
@@ -30,12 +34,15 @@ _PREDEFINED_TYPES = (
PrimitiveTypeNode.str_("char"),
PrimitiveTypeNode.str_("String"),
PrimitiveTypeNode.str_("c_string"),
ConditionalAliasTypeNode.numpy_array_("NumPyArrayGeneric"),
ConditionalAliasTypeNode.numpy_array_("NumPyArrayFloat32", dtype="numpy.float32"),
ConditionalAliasTypeNode.numpy_array_("NumPyArrayFloat64", dtype="numpy.float64"),
NoneTypeNode("void"),
AliasTypeNode.int_("void*", "IntPointer", "Represents an arbitrary pointer"),
AliasTypeNode.union_(
"Mat",
items=(ASTNodeTypeNode("Mat", module_name="cv2.mat_wrapper"),
NDArrayTypeNode("Mat")),
AliasRefTypeNode("NumPyArrayGeneric")),
export_name="MatLike"
),
AliasTypeNode.sequence_("MatShape", PrimitiveTypeNode.int_()),
@@ -137,10 +144,22 @@ _PREDEFINED_TYPES = (
ASTNodeTypeNode("gapi.wip.draw.Mosaic"),
ASTNodeTypeNode("gapi.wip.draw.Poly"))),
SequenceTypeNode("Prims", AliasRefTypeNode("Prim")),
AliasTypeNode.array_("Matx33f", (3, 3), "numpy.float32"),
AliasTypeNode.array_("Matx33d", (3, 3), "numpy.float64"),
AliasTypeNode.array_("Matx44f", (4, 4), "numpy.float32"),
AliasTypeNode.array_("Matx44d", (4, 4), "numpy.float64"),
AliasTypeNode.array_ref_("Matx33f",
array_ref_name="NumPyArrayFloat32",
shape=(3, 3),
dtype="numpy.float32"),
AliasTypeNode.array_ref_("Matx33d",
array_ref_name="NumPyArrayFloat64",
shape=(3, 3),
dtype="numpy.float64"),
AliasTypeNode.array_ref_("Matx44f",
array_ref_name="NumPyArrayFloat32",
shape=(4, 4),
dtype="numpy.float32"),
AliasTypeNode.array_ref_("Matx44d",
array_ref_name="NumPyArrayFloat64",
shape=(4, 4),
dtype="numpy.float64"),
NDArrayTypeNode("vector<uchar>", dtype="numpy.uint8"),
NDArrayTypeNode("vector_uchar", dtype="numpy.uint8"),
TupleTypeNode("GMat2", items=(ASTNodeTypeNode("GMat"),
+10 -3
View File
@@ -12,6 +12,7 @@ if sys.version_info >= (3, 6):
from contextlib import contextmanager
from typing import Dict, Set, Any, Sequence, Generator, Union
import traceback
from pathlib import Path
@@ -46,10 +47,12 @@ if sys.version_info >= (3, 6):
try:
ret_type = func(*args, **kwargs)
except Exception as e:
except Exception:
self.has_failure = True
warnings.warn(
'Typing stubs generation has failed. Reason: {}'.format(e)
"Typing stubs generation has failed.\n{}".format(
traceback.format_exc()
)
)
if ret_type_on_failure is None:
return None
@@ -120,7 +123,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):
+40
View File
@@ -219,6 +219,25 @@ class Arguments(NewOpenCVTests):
#res6 = cv.utils.dumpInputArray([a, b])
#self.assertEqual(res6, "InputArrayOfArrays: empty()=false kind=0x00050000 flags=0x01050000 total(-1)=2 dims(-1)=1 size(-1)=2x1 type(0)=CV_32FC1 dims(0)=4 size(0)=[2 3 4 5]")
def test_unsupported_numpy_data_types_string_description(self):
for dtype in (object, str, np.complex128):
test_array = np.zeros((4, 4, 3), dtype=dtype)
msg = ".*type = {} is not supported".format(test_array.dtype)
if sys.version_info[0] < 3:
self.assertRaisesRegexp(
Exception, msg, cv.utils.dumpInputArray, test_array
)
else:
self.assertRaisesRegex(
Exception, msg, cv.utils.dumpInputArray, test_array
)
def test_numpy_writeable_flag_is_preserved(self):
array = np.zeros((10, 10, 1), dtype=np.uint8)
array.setflags(write=False)
with self.assertRaises(Exception):
cv.rectangle(array, (0, 0), (5, 5), (255), 2)
def test_20968(self):
pixel = np.uint8([[[40, 50, 200]]])
_ = cv.cvtColor(pixel, cv.COLOR_RGB2BGR) # should not raise exception
@@ -482,6 +501,27 @@ class Arguments(NewOpenCVTests):
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_wrap_rotated_rect(self):
center = (34.5, 52.)
size = (565.0, 140.0)
angle = -177.5
rect1 = cv.RotatedRect(center, size, angle)
self.assertEqual(rect1.center, center)
self.assertEqual(rect1.size, size)
self.assertEqual(rect1.angle, angle)
pts = [[ 319.7845, -5.6109037],
[ 313.6778, 134.25586],
[-250.78448, 109.6109],
[-244.6778, -30.25586]]
self.assertLess(np.max(np.abs(rect1.points() - pts)), 1e-4)
rect2 = cv.RotatedRect(pts[0], pts[1], pts[2])
_, inter_pts = cv.rotatedRectangleIntersection(rect1, rect2)
self.assertLess(np.max(np.abs(inter_pts.reshape(-1, 2) - pts)), 1e-4)
def test_parse_to_rotated_rect_not_convertible(self):
for not_convertible in ([], (), np.array([]), (123, (45, 34), 1), {1: 2, 3: 4}, 123,
np.array([[123, 123, 14], [1, 3], 56], dtype=object), '123'):