chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:06:04 +08:00
commit 86c9b1c39f
7743 changed files with 3316339 additions and 0 deletions
@@ -0,0 +1,35 @@
from .nodes import (
NamespaceNode,
ClassNode,
ClassProperty,
EnumerationNode,
FunctionNode,
ConstantNode,
TypeNode,
OptionalTypeNode,
TupleTypeNode,
AliasTypeNode,
SequenceTypeNode,
AnyTypeNode,
AggregatedTypeNode,
PathLikeTypeNode,
)
from .types_conversion import (
replace_template_parameters_with_placeholders,
get_template_instantiation_type,
create_type_node
)
from .ast_utils import (
SymbolName,
ScopeNotFoundError,
SymbolNotFoundError,
find_scope,
find_class_node,
create_class_node,
create_function_node,
resolve_enum_scopes
)
from .generation import generate_typing_stubs
@@ -0,0 +1,474 @@
__all__ = [
"apply_manual_api_refinement"
]
from typing import cast, Sequence, Callable, Iterable, Optional
from .nodes import (NamespaceNode, FunctionNode, OptionalTypeNode, TypeNode,
ClassProperty, PrimitiveTypeNode, ASTNodeTypeNode,
AggregatedTypeNode, CallableTypeNode, AnyTypeNode,
TupleTypeNode, UnionTypeNode, ProtocolClassNode,
DictTypeNode, ClassTypeNode, AliasRefTypeNode)
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"
convert_returned_scalar_to_tuple(root)
"""
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 make_optional_none_return(root_node: NamespaceNode,
function_symbol_name: SymbolName) -> None:
"""
Make return type Optional[MatLike],
for the functions that may return None.
"""
function = find_function_node(root_node, function_symbol_name)
for overload in function.overloads:
if overload.return_type is not None:
if not isinstance(overload.return_type.type_node, OptionalTypeNode):
overload.return_type.type_node = OptionalTypeNode(
overload.return_type.type_node
)
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_names: 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 arg_name in arg_names:
found_overload_with_arg = False
for overload in function.overloads:
arg_idx = _find_argument_index(overload.arguments, arg_name)
# skip overloads without this argument
if arg_idx is None:
continue
# 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)
)
found_overload_with_arg = True
if not found_overload_with_arg:
raise RuntimeError(
f"Failed to find argument with name: '{arg_name}'"
f" in '{function_symbol_name.name}' overloads"
)
return _make_optional_arg
def convert_returned_scalar_to_tuple(root: NamespaceNode) -> None:
"""Force `tuple[float, float, float, float]` usage instead of Scalar alias
for return types due to `pyopencv_from` specialization for Scalar type.
"""
float_4_tuple_node = TupleTypeNode(
"ScalarOutput",
items=(PrimitiveTypeNode.float_(),) * 4
)
def fix_scalar_return_type(fn: FunctionNode.Overload):
if fn.return_type is None:
return
if fn.return_type.type_node.typename == "Scalar":
fn.return_type.type_node = float_4_tuple_node
for overload in for_each_function_overload(root):
fix_scalar_return_type(overload)
for ns in root.namespaces.values():
for overload in for_each_function_overload(ns):
fix_scalar_return_type(overload)
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) -> Optional[int]:
for i, arg in enumerate(arguments):
if arg.name == name:
return i
return None
def make_matlike_or_scalar_arg(*arg_names: str) -> Callable[[NamespaceNode, SymbolName], None]:
"""Make arguments accept both MatLike and Scalar types.
This is used for functions like inRange where the C++ InputArray parameter
can accept both Mat objects and Scalar values (tuples, floats, etc.).
Example: cv2.inRange(img, (0, 0, 0), (255, 255, 255)) should be valid.
"""
def _make_matlike_or_scalar_arg(root_node: NamespaceNode,
function_symbol_name: SymbolName) -> None:
from .predefined_types import PREDEFINED_TYPES
function = find_function_node(root_node, function_symbol_name)
for arg_name in arg_names:
found_overload_with_arg = False
for overload in function.overloads:
arg_idx = _find_argument_index(overload.arguments, arg_name)
# skip overloads without this argument
if arg_idx is None:
continue
current_type = overload.arguments[arg_idx].type_node
# Check if it's already a union or if it already includes Scalar
if isinstance(current_type, UnionTypeNode):
# Check if Scalar is already in the union
has_scalar = any(
isinstance(item, AliasRefTypeNode) and item.typename == "Scalar"
for item in current_type.items
)
if has_scalar:
continue
# Add Scalar to existing union
scalar_ref = AliasRefTypeNode("Scalar")
current_type.items = current_type.items + (scalar_ref,)
else:
# Create a union of current type and Scalar
scalar_ref = AliasRefTypeNode("Scalar")
overload.arguments[arg_idx].type_node = UnionTypeNode(
f"{arg_name}_type",
(cast(TypeNode, current_type), scalar_ref)
)
found_overload_with_arg = True
if not found_overload_with_arg:
raise RuntimeError(
f"Failed to find argument with name: '{arg_name}'"
f" in '{function_symbol_name.name}' overloads"
)
return _make_matlike_or_scalar_arg
NODES_TO_REFINE = {
SymbolName(("cv", ), (), "resize"): make_optional_arg("dsize"),
SymbolName(("cv", ), (), "calcHist"): make_optional_arg("mask"),
SymbolName(("cv", ), (), "floodFill"): make_optional_arg("mask"),
SymbolName(("cv", ), ("Feature2D", ), "detectAndCompute"): make_optional_arg("mask"),
SymbolName(("cv", ), (), "findEssentialMat"): make_optional_arg(
"distCoeffs1", "distCoeffs2", "dist_coeff1", "dist_coeff2"
),
SymbolName(("cv", ), (), "drawFrameAxes"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "getOptimalNewCameraMatrix"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "initInverseRectificationMap"): make_optional_arg("distCoeffs", "R"),
SymbolName(("cv", ), (), "initUndistortRectifyMap"): make_optional_arg("distCoeffs", "R"),
SymbolName(("cv", ), (), "projectPoints"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solveP3P"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solvePnP"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solvePnPGeneric"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solvePnPRansac"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solvePnPRefineLM"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solvePnPRefineVVS"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "undistort"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "undistortPoints"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "calibrateCamera"): make_optional_arg("cameraMatrix", "distCoeffs"),
SymbolName(("cv", "fisheye"), (), "initUndistortRectifyMap"): make_optional_arg("D"),
SymbolName(("cv", ), (), "imread"): make_optional_none_return,
SymbolName(("cv", ), (), "imdecode"): make_optional_none_return,
SymbolName(("cv", ), (), "HoughCircles"): make_optional_none_return,
SymbolName(("cv", ), (), "HoughLines"): make_optional_none_return,
SymbolName(("cv", ), (), "HoughLinesP"): make_optional_none_return,
# Fix for issue #28534: inRange should accept Scalar for lowerb and upperb
SymbolName(("cv", ), (), "inRange"): make_matlike_or_scalar_arg("lowerb", "upperb"),
}
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),
)
@@ -0,0 +1,440 @@
from typing import (NamedTuple, Sequence, Tuple, Union, List,
Dict, Callable, Optional, Generator, cast)
import keyword
from .nodes import (ASTNode, NamespaceNode, ClassNode, FunctionNode,
EnumerationNode, ClassProperty, OptionalTypeNode,
TupleTypeNode, PathLikeTypeNode)
from .types_conversion import create_type_node
class ScopeNotFoundError(Exception):
pass
class SymbolNotFoundError(Exception):
pass
class SymbolName(NamedTuple):
namespaces: Tuple[str, ...]
classes: Tuple[str, ...]
name: str
def __str__(self) -> str:
return '(namespace="{}", classes="{}", name="{}")'.format(
'::'.join(self.namespaces),
'::'.join(self.classes),
self.name
)
def __repr__(self) -> str:
return str(self)
@classmethod
def parse(cls, full_symbol_name: str,
known_namespaces: Sequence[str],
symbol_parts_delimiter: str = '.') -> "SymbolName":
"""Performs contextual symbol name parsing into namespaces, classes
and "bare" symbol name.
Args:
full_symbol_name (str): Input string to parse symbol name from.
known_namespaces (Sequence[str]): Collection of namespace that was
met during C++ headers parsing.
symbol_parts_delimiter (str, optional): Delimiter string used to
split `full_symbol_name` string into chunks. Defaults to '.'.
Returns:
SymbolName: Parsed symbol name structure.
>>> SymbolName.parse('cv.ns.Feature', ('cv', 'cv.ns'))
(namespace="cv::ns", classes="", name="Feature")
>>> SymbolName.parse('cv.ns.Feature', ())
(namespace="", classes="cv::ns", name="Feature")
>>> SymbolName.parse('cv.ns.Feature.Params', ('cv', 'cv.ns'))
(namespace="cv::ns", classes="Feature", name="Params")
>>> SymbolName.parse('cv::ns::Feature::Params::serialize',
... known_namespaces=('cv', 'cv.ns'),
... symbol_parts_delimiter='::')
(namespace="cv::ns", classes="Feature::Params", name="serialize")
"""
chunks = full_symbol_name.split(symbol_parts_delimiter)
namespaces, name = chunks[:-1], chunks[-1]
classes: List[str] = []
while len(namespaces) > 0 and '.'.join(namespaces) not in known_namespaces:
classes.insert(0, namespaces.pop())
return SymbolName(tuple(namespaces), tuple(classes), name)
def find_scope(root: NamespaceNode, symbol_name: SymbolName,
create_missing_namespaces: bool = True) -> Union[NamespaceNode, ClassNode]:
"""Traverses down nodes hierarchy to the direct parent of the node referred
by `symbol_name`.
Args:
root (NamespaceNode): Root node of the hierarchy.
symbol_name (SymbolName): Full symbol name to find scope for.
create_missing_namespaces (bool, optional): Set to True to create missing
namespaces while traversing the hierarchy. Defaults to True.
Raises:
ScopeNotFoundError: If direct parent for the node referred by `symbol_name`
can't be found e.g. one of classes doesn't exist.
Returns:
Union[NamespaceNode, ClassNode]: Direct parent for the node referred by
`symbol_name`.
>>> root = NamespaceNode('cv')
>>> algorithm_node = root.add_class('Algorithm')
>>> find_scope(root, SymbolName(('cv', ), ('Algorithm',), 'Params')) == algorithm_node
True
>>> root = NamespaceNode('cv')
>>> scope = find_scope(root, SymbolName(('cv', 'gapi', 'detail'), (), 'function'))
>>> scope.full_export_name
'cv.gapi.detail'
>>> root = NamespaceNode('cv')
>>> scope = find_scope(root, SymbolName(('cv', 'gapi'), ('GOpaque',), 'function'))
Traceback (most recent call last):
...
ast_utils.ScopeNotFoundError: Can't find a scope for 'function', with \
'(namespace="cv::gapi", classes="GOpaque", name="function")', \
because 'GOpaque' class is not registered yet
"""
assert isinstance(root, NamespaceNode), \
'Wrong hierarchy root type: {}'.format(type(root))
assert symbol_name.namespaces[0] == root.name, \
"Trying to find scope for '{}' with root namespace different from: '{}'".format(
symbol_name, root.name
)
scope: Union[NamespaceNode, ClassNode] = root
for namespace in symbol_name.namespaces[1:]:
if namespace not in scope.namespaces: # type: ignore
if not create_missing_namespaces:
raise ScopeNotFoundError(
"Can't find a scope for '{}', with '{}', because namespace"
" '{}' is not created yet and `create_missing_namespaces`"
" flag is set to False".format(
symbol_name.name, symbol_name, namespace
)
)
scope = scope.add_namespace(namespace) # type: ignore
else:
scope = scope.namespaces[namespace] # type: ignore
for class_name in symbol_name.classes:
if class_name not in scope.classes:
raise ScopeNotFoundError(
"Can't find a scope for '{}', with '{}', because '{}' "
"class is not registered yet".format(
symbol_name.name, symbol_name, class_name
)
)
scope = scope.classes[class_name]
return scope
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],
func_info) -> FunctionNode:
def prepare_overload_arguments_and_return_type(variant):
arguments = [] # type: list[FunctionNode.Arg]
# Enumerate is required, because `argno` in `variant.py_arglist`
# refers to position of argument in C++ function interface,
# but `variant.py_noptargs` refers to position in `py_arglist`
for i, (_, argno) in enumerate(variant.py_arglist):
arg_info = variant.args[argno]
type_node = create_type_node(arg_info.tp)
# Special handling for string representation of the file system path
if arg_info.pathlike and type_node.typename == "str":
type_node = PathLikeTypeNode.string_or_pathlike_()
default_value = None
if len(arg_info.defval):
default_value = arg_info.defval
# If argument is optional and can be None - make its type optional
if variant.is_arg_optional(i):
# NOTE: should UMat be always mandatory for better type hints?
# otherwise overload won't be selected e.g. VideoCapture.read()
if arg_info.py_outputarg:
type_node = OptionalTypeNode(type_node)
default_value = "None"
elif arg_info.isbig() and "None" not in type_node.typename:
# but avoid duplication of the optioness
type_node = OptionalTypeNode(type_node)
arguments.append(
FunctionNode.Arg(arg_info.export_name, type_node=type_node,
default_value=default_value)
)
if func_info.isconstructor:
return arguments, None
# Function has more than 1 output argument, so its return type is a tuple
if len(variant.py_outlist) > 1:
ret_types = []
# Actual returned value of the function goes first
if variant.py_outlist[0][1] == -1:
ret_types.append(create_type_node(variant.rettype))
outlist = variant.py_outlist[1:]
else:
outlist = variant.py_outlist
for _, argno in outlist:
assert argno >= 0, \
f"Logic Error! Outlist contains function return type: {outlist}"
ret_types.append(create_type_node(variant.args[argno].tp))
return arguments, FunctionNode.RetType(
TupleTypeNode("return_type", ret_types)
)
# Function with 1 output argument in Python
if len(variant.py_outlist) == 1:
# Can be represented as a function with a non-void return type in C++
if variant.rettype:
return arguments, FunctionNode.RetType(
create_type_node(variant.rettype)
)
# or a function with void return type and output argument type
# such non-const reference
ret_type = variant.args[variant.py_outlist[0][1]].tp
return arguments, FunctionNode.RetType(
create_type_node(ret_type)
)
# Function without output types returns None in Python
return arguments, None
function_node = FunctionNode(func_info.name)
function_node.parent = scope
if func_info.isconstructor:
function_node.export_name = "__init__"
for variant in func_info.variants:
arguments, ret_type = prepare_overload_arguments_and_return_type(variant)
if isinstance(scope, ClassNode):
if func_info.is_static:
if ret_type is not None and ret_type.typename.endswith(scope.name):
function_node.is_classmethod = True
arguments.insert(0, FunctionNode.Arg("cls"))
else:
function_node.is_static = True
else:
arguments.insert(0, FunctionNode.Arg("self"))
function_node.add_overload(arguments, ret_type)
return function_node
def create_function_node(root: NamespaceNode, func_info) -> FunctionNode:
func_symbol_name = SymbolName(
func_info.namespace.split(".") if len(func_info.namespace) else (),
func_info.classname.split(".") if len(func_info.classname) else (),
func_info.name
)
return create_function_node_in_scope(find_scope(root, func_symbol_name),
func_info)
def create_class_node_in_scope(scope: Union[NamespaceNode, ClassNode],
symbol_name: SymbolName,
class_info) -> ClassNode:
properties = []
for property in class_info.props:
export_property_name = property.name
if keyword.iskeyword(export_property_name):
export_property_name += "_"
properties.append(
ClassProperty(
name=export_property_name,
type_node=create_type_node(property.tp),
is_readonly=property.readonly
)
)
class_node = scope.add_class(symbol_name.name,
properties=properties)
class_node.export_name = class_info.export_name
if class_info.constructor is not None:
create_function_node_in_scope(class_node, class_info.constructor)
for method in class_info.methods.values():
create_function_node_in_scope(class_node, method)
return class_node
def create_class_node(root: NamespaceNode, class_info,
namespaces: Sequence[str]) -> ClassNode:
symbol_name = SymbolName.parse(class_info.full_original_name, namespaces)
scope = find_scope(root, symbol_name)
return create_class_node_in_scope(scope, symbol_name, class_info)
def resolve_enum_scopes(root: NamespaceNode,
enums: Dict[SymbolName, EnumerationNode]):
"""Attaches all enumeration nodes to the appropriate classes and modules
If classes containing enumeration can't be found in the AST - they will
be created and marked as not exportable. This behavior is required to cover
cases, when enumeration is defined in base class, but only its derivatives
are used. Example:
```cpp
class CV_EXPORTS TermCriteria {
public:
enum Type { /* ... */ };
// ...
};
```
Args:
root (NamespaceNode): root of the reconstructed AST
enums (Dict[SymbolName, EnumerationNode]): Mapping between enumerations
symbol names and corresponding nodes without parents.
"""
for symbol_name, enum_node in enums.items():
if symbol_name.classes:
try:
scope = find_scope(root, symbol_name)
except ScopeNotFoundError:
# Scope can't be found if enumeration is a part of class
# that is not exported.
# Create class node, but mark it as not exported
for i, class_name in enumerate(symbol_name.classes):
scope = find_scope(root,
SymbolName(symbol_name.namespaces,
classes=symbol_name.classes[:i],
name=class_name))
if class_name in scope.classes:
continue
class_node = scope.add_class(class_name)
class_node.is_exported = False
scope = find_scope(root, symbol_name)
else:
scope = find_scope(root, symbol_name)
enum_node.parent = scope
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.
Raises:
AssertionError: if nodes hierarchy missing a namespace node.
>>> root = NamespaceNode('cv')
>>> feature_class = root.add_class("Feature")
>>> get_enclosing_namespace(feature_class) == root
True
>>> root = NamespaceNode('cv')
>>> feature_class = root.add_class("Feature")
>>> feature_params_class = feature_class.add_class("Params")
>>> serialize_params_func = feature_params_class.add_function("serialize")
>>> get_enclosing_namespace(serialize_params_func) == root
True
>>> root = NamespaceNode('cv')
>>> detail_ns = root.add_namespace('detail')
>>> flags_enum = detail_ns.add_enumeration('Flags')
>>> get_enclosing_namespace(flags_enum) == detail_ns
True
"""
parent_node = node.parent
while not isinstance(parent_node, NamespaceNode):
assert parent_node is not None, \
"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()
@@ -0,0 +1,864 @@
__all__ = ("generate_typing_stubs", )
from io import StringIO
from pathlib import Path
import re
import shutil
from typing import (Callable, NamedTuple, Union, Set, Dict,
Collection, Tuple, List)
import warnings
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, ASTNodeType, NamespaceNode, ClassNode,
FunctionNode, EnumerationNode, ConstantNode,
ProtocolClassNode)
from .nodes.type_node import (TypeNode, AliasTypeNode, AliasRefTypeNode,
AggregatedTypeNode, ASTNodeTypeNode,
ConditionalAliasTypeNode, PrimitiveTypeNode)
def _clean_stale_stubs_dirs(stubs_root: Path) -> None:
"""Remove all subdirectories under stubs_root.
During incremental builds, disabling a previously enabled module leaves
behind its typing stub directory (e.g. cv2/gapi/). Removing all
subdirectories before regeneration ensures only stubs for currently
enabled modules are present. Top-level files (py.typed, __init__.pyi)
are kept because they are managed separately.
"""
if not stubs_root.is_dir():
return
for item in stubs_root.iterdir():
if item.is_dir():
shutil.rmtree(item)
def generate_typing_stubs(root: NamespaceNode, output_path: Path):
"""Generates typing stubs for the AST with root `root` and outputs
created files tree to directory pointed by `output_path`.
Stubs generation consist from 4 steps:
1. Reconstruction of AST tree for header parser output.
2. "Lazy" AST nodes resolution (type nodes used as function arguments
and return types). Resolution procedure attaches every "lazy"
AST node to the corresponding node in the AST created during step 1.
3. Generation of the typing module content. Typing module doesn't exist
in library code, but is essential place to define aliases widely used
in stub files.
4. Generation of typing stubs from the reconstructed AST.
Every namespace corresponds to a Python module with the same name.
Generation procedure is recursive repetition of the following steps
for each namespace (module):
- Collect and write required imports for the module
- Write all module constants stubs
- Write all module enumerations stubs
- Write all module classes stubs, preserving correct declaration
order, when base classes go before their derivatives.
- Write all module functions stubs
- Repeat steps above for nested namespaces
Args:
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.
# Example:
# ```cpp
# namespace cv {
# enum AlgorithmType {
# // ...
# };
# namespace detail {
# struct Algorithm {
# static Ptr<Algorithm> create(AlgorithmType alg_type);
# };
# } // namespace detail
# } // namespace cv
# ```
# To resolve `alg_type` argument of function `create` having `AlgorithmType`
# type from above example the following steps are done:
# 1. Try to resolve against `cv::detail::Algorithm` - fail
# 2. Try to resolve against `cv::detail` - fail
# 3. Try to resolve against `cv` - success
# The whole process should fail !only! when all possible scopes are
# checked and at least 1 node is still unresolved.
root.resolve_type_nodes()
# Remove stale typing stub subdirectories from previous builds.
# In incremental builds, disabling a module (e.g. -DBUILD_opencv_gapi=OFF)
# no longer generates its stubs, but leftover directories from a previous
# build persist and propagate through the copy/install steps, causing
# type-checker errors for stubs referencing unavailable modules.
_clean_stale_stubs_dirs(Path(output_path) / root.export_name)
_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) -> None:
output_path = Path(output_path) / root.export_name
output_path.mkdir(parents=True, exist_ok=True)
# Collect all imports required for module items declaration
required_imports = _collect_required_imports(root)
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_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", 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):
has_enums = True
# 2 empty lines between enum and classes definitions
if has_enums:
output_stream.write("\n")
# Write the rest of module content - classes and functions
for section in STUB_SECTIONS:
_generate_section_stub(section, root, output_stream, 0)
# Dump content to the output file
(output_path / "__init__.pyi").write_text(output_stream.getvalue())
# Process nested namespaces
for ns in root.namespaces.values():
_generate_typing_stubs(ns, output_path)
class StubSection(NamedTuple):
name: str
node_type: ASTNodeType
STUB_SECTIONS = (
StubSection("# Constants", ASTNodeType.Constant),
# Enumerations are skipped due to special handling rules
# StubSection("# Enumerations", ASTNodeType.Enumeration),
StubSection("# Classes", ASTNodeType.Class),
StubSection("# Functions", ASTNodeType.Function)
)
def _generate_section_stub(section: StubSection, node: ASTNode,
output_stream: StringIO, indent: int) -> bool:
"""Generates stub for a single type of children nodes of the provided node.
Args:
section (StubSection): section identifier that carries section name and
type its nodes.
node (ASTNode): root node with children nodes used for
output_stream (StringIO): Output stream for all nodes stubs related to
the given section.
indent (int): Indent used for each line written to `output_stream`.
Returns:
bool: `True` if section has a content, `False` otherwise.
"""
if section.node_type not in node._children:
return False
children = node._children[section.node_type]
if len(children) == 0:
return False
output_stream.write(" " * indent)
output_stream.write(section.name)
output_stream.write("\n")
stub_generator = NODE_TYPE_TO_STUB_GENERATOR[section.node_type]
children = filter(lambda c: c.is_exported, children.values()) # type: ignore
if hasattr(section.node_type, "weight"):
children = sorted(children, key=lambda child: getattr(child, "weight")) # type: ignore
for child in children:
stub_generator(child, output_stream, indent) # type: ignore
output_stream.write("\n")
return True
def _generate_class_stub(class_node: ClassNode, output_stream: StringIO,
indent: int = 0) -> None:
"""Generates stub for the provided class node.
Rules:
- Read/write properties are converted to object attributes.
- Readonly properties are converted to functions decorated with `@property`.
- When return type of static functions matches class name - these functions
are treated as factory functions and annotated with `@classmethod`.
- In contrast to implicit `this` argument in C++ methods, in Python all
"normal" methods have explicit `self` as their first argument.
- Body of empty classes is replaced with `...`
Example:
```cpp
struct Object : public BaseObject {
struct InnerObject {
int param;
bool param2;
float readonlyParam();
};
Object(int param, bool param2 = false);
Object(InnerObject obj);
static Object create();
};
```
becomes
```python
class Object(BaseObject):
class InnerObject:
param: int
param2: bool
@property
def readonlyParam() -> float: ...
@typing.override
def __init__(self, param: int, param2: bool = ...) -> None: ...
@typing.override
def __init__(self, obj: "Object.InnerObject") -> None: ...
@classmethod
def create(cls) -> Object: ...
```
Args:
class_node (ClassNode): Class node to generate stub entry for.
output_stream (StringIO): Output stream for class stub.
indent (int, optional): Indent used for each line written to
`output_stream`. Defaults to 0.
"""
class_module = get_enclosing_namespace(class_node)
class_module_name = class_module.full_export_name
if len(class_node.bases) > 0:
bases = []
for base in class_node.bases:
base_module = get_enclosing_namespace(base) # type: ignore
if base_module != class_module:
bases.append(base.full_export_name)
else:
bases.append(base.export_name)
inheritance_str = f"({', '.join(bases)})"
elif isinstance(class_node, ProtocolClassNode):
inheritance_str = "(Protocol)"
else:
inheritance_str = ""
output_stream.write(
"{indent}class {name}{bases}:\n".format(
indent=" " * indent,
name=class_node.export_name,
bases=inheritance_str
)
)
has_content = len(class_node.properties) > 0
# Processing class properties
for property in class_node.properties:
if property.is_readonly:
template = "{indent}@property\n{indent}def {name}(self) -> {type}: ...\n"
else:
template = "{indent}{name}: {type}\n"
output_stream.write(
template.format(indent=" " * (indent + 4),
name=property.name,
type=property.relative_typename(class_module_name))
)
if len(class_node.properties) > 0:
output_stream.write("\n")
for section in STUB_SECTIONS:
if _generate_section_stub(section, class_node,
output_stream, indent + 4):
has_content = True
if not has_content:
output_stream.write(" " * (indent + 4))
output_stream.write("...\n\n")
def _generate_constant_stub(constant_node: ConstantNode,
output_stream: StringIO, indent: int = 0,
extra_export_prefix: str = "",
generate_uppercase_version: bool = True) -> Tuple[str, ...]:
"""Generates stub for the provided constant node.
Args:
constant_node (ConstantNode): Constant node to generate stub entry for.
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
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.
"""
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,
output_stream: StringIO, indent: int = 0,
extra_export_prefix: str = "") -> None:
"""Generates stub for the provided enumeration node. In contrast to the
Python `enum.Enum` class, C++ enumerations are exported as module-level
(or class-level) constants.
Example:
```cpp
enum Flags {
Flag1 = 0,
Flag2 = 1,
Flag3
};
```
becomes
```python
Flag1: int
Flag2: int
Flag3: int
Flags = int # One of [Flag1, Flag2, Flag3]
```
Unnamed enumerations don't export their names to Python:
```cpp
enum {
Flag1 = 0,
Flag2 = 1
};
```
becomes
```python
Flag1: int
Flag2: int
```
Scoped enumeration adds its name before each item name:
```cpp
enum struct ScopedEnum {
Flag1,
Flag2
};
```
becomes
```python
ScopedEnum_Flag1: int
ScopedEnum_Flag2: int
ScopedEnum = int # One of [ScopedEnum_Flag1, ScopedEnum_Flag2]
```
Args:
enumeration_node (EnumerationNode): Enumeration node to generate stub entry for.
output_stream (StringIO): Output stream for enumeration 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
enumeration name. Defaults to empty string.
"""
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():
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\n{indent}"""One of [{entries}]"""\n\n'.format(
export_prefix=extra_export_prefix,
name=enumeration_node.export_name,
entries=", ".join(generated_constants_entries),
indent=" " * indent
)
)
def _generate_function_stub(function_node: FunctionNode,
output_stream: StringIO, indent: int = 0) -> None:
"""Generates stub entry for the provided function node. Function node can
refer free function or class method.
Args:
function_node (FunctionNode): Function node to generate stub entry for.
output_stream (StringIO): Output stream for function stub.
indent (int, optional): Indent used for each line written to
`output_stream`. Defaults to 0.
"""
# Function is a stub without any arguments information
if not function_node.overloads:
warnings.warn(
'Function node "{}" exported as "{}" has no overloads'.format(
function_node.full_name, function_node.full_export_name
)
)
return
decorators = []
if function_node.is_classmethod:
decorators.append(" " * indent + "@classmethod")
elif function_node.is_static:
decorators.append(" " * indent + "@staticmethod")
if len(function_node.overloads) > 1:
decorators.append(" " * indent + "@_typing.overload")
function_module = get_enclosing_namespace(function_node)
function_module_name = function_module.full_export_name
for overload in function_node.overloads:
# Annotate every function argument
annotated_args = []
for arg in overload.arguments:
annotated_arg = arg.name
typename = arg.relative_typename(function_module_name)
if typename is not None:
annotated_arg += ": " + typename
if arg.default_value is not None:
annotated_arg += " = ..."
annotated_args.append(annotated_arg)
# And convert return type to the actual type
if overload.return_type is not None:
ret_type = overload.return_type.relative_typename(function_module_name)
else:
ret_type = "None"
output_stream.write(
"{decorators}"
"{indent}def {name}({args}) -> {ret_type}: ...\n".format(
decorators="\n".join(decorators) +
"\n" if len(decorators) > 0 else "",
name=function_node.export_name,
args=", ".join(annotated_args),
ret_type=ret_type,
indent=" " * indent
)
)
output_stream.write("\n")
def _generate_enums_from_classes_tree(class_node: ClassNode,
output_stream: StringIO,
indent: int = 0,
class_name_prefix: str = "") -> bool:
"""Recursively generates class-level enumerations on the module level
starting from the `class_node`.
NOTE: This function is required, because all enumerations are exported as
module-level constants.
Example:
```cpp
namespace cv {
struct TermCriteria {
enum Type {
COUNT = 1,
MAX_ITER = COUNT,
EPS = 2
};
};
} // namespace cv
```
is exported to `__init__.pyi` of `cv` module as as
```python
TermCriteria_COUNT: int
TermCriteria_MAX_ITER: int
TermCriteria_EPS: int
TermCriteria_Type = int # One of [COUNT, MAX_ITER, EPS]
```
Args:
class_node (ClassNode): Class node to generate enumerations stubs for.
output_stream (StringIO): Output stream for enumerations stub.
indent (int, optional): Indent used for each line written to
`output_stream`. Defaults to 0.
class_name_prefix (str, optional): Prefix used for enumerations and
constants names. Defaults to "".
Returns:
bool: `True` if classes tree declares at least 1 enum, `False` otherwise.
"""
class_name_prefix = class_node.export_name + "_" + class_name_prefix
has_content = len(class_node.enumerations) > 0
for enum_node in class_node.enumerations.values():
_generate_enumeration_stub(enum_node, output_stream, indent,
class_name_prefix)
for cls in class_node.classes.values():
if _generate_enums_from_classes_tree(cls, output_stream, indent,
class_name_prefix):
has_content = True
return has_content
def check_overload_presence(node: Union[NamespaceNode, ClassNode]) -> bool:
"""Checks that node has at least 1 function with overload.
Args:
node (Union[NamespaceNode, ClassNode]): Node to check for overload
presence.
Returns:
bool: True if input node has at least 1 function with overload, False
otherwise.
"""
for func_node in node.functions.values():
if len(func_node.overloads) > 1:
return True
return False
def _collect_required_imports(root: NamespaceNode) -> Collection[str]:
"""Collects all imports required for classes and functions typing stubs
declarations.
Args:
root (NamespaceNode): Namespace node to collect imports for
Returns:
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]):
for required_import in type_node.required_usage_imports:
imports.add(required_import)
required_imports: Set[str] = set()
# Check if typing module is required due to @overload decorator usage
# Looking for module-level function with at least 1 overload
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
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 as _typing")
# Add required imports for class properties
for prop in cls.properties:
_add_required_usage_imports(prop.type_node, required_imports)
# Add required imports for class bases
for base in cls.bases:
base_namespace = get_enclosing_namespace(base) # type: ignore
if base_namespace != root:
required_imports.add(
"import " + base_namespace.full_export_name
)
if isinstance(cls, ProtocolClassNode):
has_protocol = True
if has_overload:
required_imports.add("import typing as _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):
_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,
required_imports)
root_import = "import " + root.full_export_name
if root_import in required_imports:
required_imports.remove(root_import)
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)
root.reexported_submodules.append("typing")
# 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],
output_stream: StringIO) -> None:
"""Writes all entries of `required_imports` to the `output_stream`.
Args:
required_imports (Collection[str]): Imports to write into the output
stream.
output_stream (StringIO): Output stream for import statements.
"""
for required_import in required_imports:
output_stream.write(required_import)
output_stream.write("\n")
if len(required_imports):
output_stream.write("\n\n")
def _generate_typing_module(root: NamespaceNode, output_path: Path) -> None:
"""Generates stub file for typings module.
Actual module doesn't exist, but it is an appropriate place to define
all widely-used aliases.
Args:
root (NamespaceNode): AST root node used for type nodes resolution.
output_path (Path): Path to typing module directory, where __init__.pyi
will be written.
"""
def has_all_required_modules(type_node: TypeNode) -> bool:
return all(em in root.namespaces for em in type_node.required_modules)
def register_alias_links_from_aggregated_type(type_node: TypeNode) -> None:
assert isinstance(type_node, AggregatedTypeNode), \
f"Provided type node '{type_node.ctype_name}' is not an aggregated type"
for item in filter(lambda i: isinstance(i, AliasRefTypeNode), type_node):
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 as _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}"""'
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
for node in PREDEFINED_TYPES.values():
# if node does not have at least one required module skip it
# e.g. GArgs requires G-API module, so if build without G-API GArgs is not included
if not has_all_required_modules(node):
continue
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")
for alias_name in aliases:
output_stream.write(f' "{alias_name}",\n')
output_stream.write("]\n\n")
_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(f"{alias_name} = {alias_type}\n")
TypeNode.compatible_to_runtime_usage = False
(output_path / "__init__.py").write_text(output_stream.getvalue())
StubGenerator = Callable[[ASTNode, StringIO, int], None]
NODE_TYPE_TO_STUB_GENERATOR = {
ASTNodeType.Class: _generate_class_stub,
ASTNodeType.Constant: _generate_constant_stub,
ASTNodeType.Enumeration: _generate_enumeration_stub,
ASTNodeType.Function: _generate_function_stub
}
@@ -0,0 +1,12 @@
from .node import ASTNode, ASTNodeType
from .namespace_node import NamespaceNode
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, PrimitiveTypeNode,
CallableTypeNode, DictTypeNode, ClassTypeNode, PathLikeTypeNode
)
@@ -0,0 +1,190 @@
from typing import Type, Sequence, NamedTuple, Optional, Tuple, Dict
import itertools
import weakref
from .node import ASTNode, ASTNodeType
from .function_node import FunctionNode
from .enumeration_node import EnumerationNode
from .constant_node import ConstantNode
from .type_node import TypeNode, TypeResolutionError
class ClassProperty(NamedTuple):
name: str
type_node: TypeNode
is_readonly: bool
@property
def typename(self) -> str:
return self.type_node.full_typename
def resolve_type_nodes(self, root: ASTNode) -> None:
try:
self.type_node.resolve(root)
except TypeResolutionError as e:
raise TypeResolutionError(
'Failed to resolve "{}" property'.format(self.name)
) from e
def relative_typename(self, full_node_name: str) -> str:
"""Typename relative to the passed AST node name.
Args:
full_node_name (str): Full export name of the AST node
Returns:
str: typename relative to the passed AST node name
"""
return self.type_node.relative_typename(full_node_name)
class ClassNode(ASTNode):
"""Represents a C++ class that is also a class in Python.
ClassNode can have functions (methods), enumerations, constants and other
classes as its children nodes.
Class properties are not treated as a part of AST for simplicity and have
extra handling if required.
"""
def __init__(self, name: str, parent: Optional[ASTNode] = None,
export_name: Optional[str] = None,
bases: Sequence["weakref.ProxyType[ClassNode]"] = (),
properties: Sequence[ClassProperty] = ()) -> None:
super().__init__(name, parent, export_name)
self.bases = list(bases)
self.properties = properties
@property
def weight(self) -> int:
return 1 + sum(base.weight for base in self.bases)
@property
def children_types(self) -> Tuple[ASTNodeType, ...]:
return (ASTNodeType.Class, ASTNodeType.Function,
ASTNodeType.Enumeration, ASTNodeType.Constant)
@property
def node_type(self) -> ASTNodeType:
return ASTNodeType.Class
@property
def classes(self) -> Dict[str, "ClassNode"]:
return self._children[ASTNodeType.Class]
@property
def functions(self) -> Dict[str, FunctionNode]:
return self._children[ASTNodeType.Function]
@property
def enumerations(self) -> Dict[str, EnumerationNode]:
return self._children[ASTNodeType.Enumeration]
@property
def constants(self) -> Dict[str, ConstantNode]:
return self._children[ASTNodeType.Constant]
def add_class(self, name: str,
bases: Sequence["weakref.ProxyType[ClassNode]"] = (),
properties: Sequence[ClassProperty] = ()) -> "ClassNode":
return self._add_child(ClassNode, name, bases=bases,
properties=properties)
def add_function(self, name: str, arguments: Sequence[FunctionNode.Arg] = (),
return_type: Optional[FunctionNode.RetType] = None,
is_static: bool = False) -> FunctionNode:
"""Adds function as a child node of a class.
Function is classified in 3 categories:
1. Instance method.
If function is an instance method then `self` argument is
inserted at the beginning of its arguments list.
2. Class method (or factory method)
If `is_static` flag is `True` and typename of the function
return type matches name of the class then function is treated
as class method.
If function is a class method then `cls` argument is inserted
at the beginning of its arguments list.
3. Static method
Args:
name (str): Name of the function.
arguments (Sequence[FunctionNode.Arg], optional): Function arguments.
Defaults to ().
return_type (Optional[FunctionNode.RetType], optional): Function
return type. Defaults to None.
is_static (bool, optional): Flag whenever function is static or not.
Defaults to False.
Returns:
FunctionNode: created function node.
"""
arguments = list(arguments)
if return_type is not None:
is_classmethod = return_type.typename == self.name
else:
is_classmethod = False
if not is_static:
arguments.insert(0, FunctionNode.Arg("self"))
elif is_classmethod:
is_static = False
arguments.insert(0, FunctionNode.Arg("cls"))
return self._add_child(FunctionNode, name, arguments=arguments,
return_type=return_type, is_static=is_static,
is_classmethod=is_classmethod)
def add_enumeration(self, name: str) -> EnumerationNode:
return self._add_child(EnumerationNode, name)
def add_constant(self, name: str, value: str) -> ConstantNode:
return self._add_child(ConstantNode, name, value=value)
def add_base(self, base_class_node: "ClassNode") -> None:
self.bases.append(weakref.proxy(base_class_node))
def resolve_type_nodes(self, root: ASTNode) -> None:
"""Resolves type nodes for all inner-classes, methods and properties
in 2 steps:
1. Resolve against `self` as a tree root
2. Resolve against `root` as a tree root
Type resolution errors are postponed until all children nodes are
examined.
Args:
root (Optional[ASTNode], optional): Root of the AST sub-tree.
Defaults to None.
"""
errors = []
for child in itertools.chain(self.properties,
self.functions.values(),
self.classes.values()):
try:
try:
# Give priority to narrowest scope (class-level scope in this case)
child.resolve_type_nodes(self) # type: ignore
except TypeResolutionError:
child.resolve_type_nodes(root) # type: ignore
except TypeResolutionError as e:
errors.append(str(e))
if len(errors) > 0:
raise TypeResolutionError(
'Failed to resolve "{}" class against "{}". Errors: {}'.format(
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)
@@ -0,0 +1,31 @@
from typing import Optional, Tuple
from .node import ASTNode, ASTNodeType
class ConstantNode(ASTNode):
"""Represents C++ constant that is also a constant in Python.
"""
def __init__(self, name: str, value: str,
parent: Optional[ASTNode] = None,
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[ASTNodeType, ...]:
return ()
@property
def node_type(self) -> ASTNodeType:
return ASTNodeType.Constant
@property
def value_type(self) -> str:
return self._value_type
def __str__(self) -> str:
return "Constant('{}' exported as '{}': {})".format(
self.name, self.export_name, self.value
)
@@ -0,0 +1,33 @@
from typing import Type, Tuple, Optional, Dict
from .node import ASTNode, ASTNodeType
from .constant_node import ConstantNode
class EnumerationNode(ASTNode):
"""Represents C++ enumeration that treated as named set of constants in
Python.
EnumerationNode can have only constants as its children nodes.
"""
def __init__(self, name: str, is_scoped: bool = False,
parent: Optional[ASTNode] = None,
export_name: Optional[str] = None) -> None:
super().__init__(name, parent, export_name)
self.is_scoped = is_scoped
@property
def children_types(self) -> Tuple[ASTNodeType, ...]:
return (ASTNodeType.Constant, )
@property
def node_type(self) -> ASTNodeType:
return ASTNodeType.Enumeration
@property
def constants(self) -> Dict[str, ConstantNode]:
return self._children[ASTNodeType.Constant]
def add_constant(self, name: str, value: str) -> ConstantNode:
return self._add_child(ConstantNode, name, value=value)
@@ -0,0 +1,140 @@
from typing import NamedTuple, Sequence, Optional, Tuple, List
from .node import ASTNode, ASTNodeType
from .type_node import TypeNode, NoneTypeNode, TypeResolutionError
class FunctionNode(ASTNode):
"""Represents a function (or class method) in both C++ and Python.
This class defines an overload set rather then function itself, because
function without overloads is represented as FunctionNode with 1 overload.
"""
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]:
return getattr(self.type_node, "full_typename", None)
def relative_typename(self, root: str) -> Optional[str]:
if self.type_node is not None:
return self.type_node.relative_typename(root)
return None
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:
return self.type_node.full_typename
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
def __init__(self, name: str,
arguments: Optional[Sequence["FunctionNode.Arg"]] = None,
return_type: Optional["FunctionNode.RetType"] = None,
is_static: bool = False,
is_classmethod: bool = False,
parent: Optional[ASTNode] = None,
export_name: Optional[str] = None) -> None:
"""Function node initializer
Args:
name (str): Name of the function overload set
arguments (Optional[Sequence[FunctionNode.Arg]], optional): Function
arguments. If this argument is None, then no overloads are
added and node should be treated like a "function stub" rather
than function. This might be helpful if there is a knowledge
that function with the defined name exists, but information
about its interface is not available at that moment.
Defaults to None.
return_type (Optional[FunctionNode.RetType], optional): Function
return type. Defaults to None.
is_static (bool, optional): Flag pointing that function is
a static method of some class. Defaults to False.
is_classmethod (bool, optional): Flag pointing that function is
a class method of some class. Defaults to False.
parent (Optional[ASTNode], optional): Parent ASTNode of the function.
Can be class or namespace. Defaults to None.
export_name (Optional[str], optional): Export name of the function.
Defaults to None.
"""
super().__init__(name, parent, export_name)
self.overloads: List[FunctionNode.Overload] = []
self.is_static = is_static
self.is_classmethod = is_classmethod
if arguments is not None:
self.add_overload(arguments, return_type)
@property
def node_type(self) -> ASTNodeType:
return ASTNodeType.Function
@property
def children_types(self) -> Tuple[ASTNodeType, ...]:
return ()
def add_overload(self, arguments: Sequence["FunctionNode.Arg"] = (),
return_type: Optional["FunctionNode.RetType"] = None):
self.overloads.append(FunctionNode.Overload(arguments, return_type))
def resolve_type_nodes(self, root: ASTNode):
"""Resolves type nodes in all overloads against `root`
Type resolution errors are postponed until all type nodes are examined.
Args:
root (ASTNode): Root of AST sub-tree used for type nodes resolution.
"""
def has_unresolved_type_node(item) -> bool:
return item.type_node is not None and not item.type_node.is_resolved
errors = []
for overload in self.overloads:
for arg in filter(has_unresolved_type_node, overload.arguments):
try:
arg.type_node.resolve(root) # type: ignore
except TypeResolutionError as e:
errors.append(
'Failed to resolve "{}" argument: {}'.format(arg.name, e)
)
if overload.return_type is not None and \
has_unresolved_type_node(overload.return_type):
try:
overload.return_type.type_node.resolve(root)
except TypeResolutionError as e:
errors.append('Failed to resolve return type: {}'.format(e))
if len(errors) > 0:
raise TypeResolutionError(
'Failed to resolve "{}" function against "{}". Errors: {}'.format(
self.full_export_name, root.full_export_name,
", ".join("[{}]: {}".format(i, e) for i, e in enumerate(errors))
)
)
@@ -0,0 +1,113 @@
import itertools
import weakref
from collections import defaultdict
from typing import Dict, List, Optional, Sequence, Tuple
from .class_node import ClassNode, ClassProperty
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
class NamespaceNode(ASTNode):
"""Represents C++ namespace that treated as module in Python.
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[ASTNodeType, ...]:
return (ASTNodeType.Namespace, ASTNodeType.Class, ASTNodeType.Function,
ASTNodeType.Enumeration, ASTNodeType.Constant)
@property
def namespaces(self) -> Dict[str, "NamespaceNode"]:
return self._children[ASTNodeType.Namespace]
@property
def classes(self) -> Dict[str, ClassNode]:
return self._children[ASTNodeType.Class]
@property
def functions(self) -> Dict[str, FunctionNode]:
return self._children[ASTNodeType.Function]
@property
def enumerations(self) -> Dict[str, EnumerationNode]:
return self._children[ASTNodeType.Enumeration]
@property
def constants(self) -> Dict[str, ConstantNode]:
return self._children[ASTNodeType.Constant]
def add_namespace(self, name: str) -> "NamespaceNode":
return self._add_child(NamespaceNode, name)
def add_class(self, name: str,
bases: Sequence["weakref.ProxyType[ClassNode]"] = (),
properties: Sequence[ClassProperty] = ()) -> "ClassNode":
return self._add_child(ClassNode, name, bases=bases,
properties=properties)
def add_function(self, name: str, arguments: Sequence[FunctionNode.Arg] = (),
return_type: Optional[FunctionNode.RetType] = None) -> FunctionNode:
return self._add_child(FunctionNode, name, arguments=arguments,
return_type=return_type)
def add_enumeration(self, name: str) -> EnumerationNode:
return self._add_child(EnumerationNode, name)
def add_constant(self, name: str, value: str) -> ConstantNode:
return self._add_child(ConstantNode, name, value=value)
def resolve_type_nodes(self, root: Optional[ASTNode] = None) -> None:
"""Resolves type nodes for all children nodes in 2 steps:
1. Resolve against `self` as a tree root
2. Resolve against `root` as a tree root
Type resolution errors are postponed until all children nodes are
examined.
Args:
root (Optional[ASTNode], optional): Root of the AST sub-tree.
Defaults to None.
"""
errors = []
for child in itertools.chain(self.functions.values(),
self.classes.values(),
self.namespaces.values()):
try:
try:
child.resolve_type_nodes(self) # type: ignore
except TypeResolutionError:
if root is not None:
child.resolve_type_nodes(root) # type: ignore
else:
raise
except TypeResolutionError as e:
errors.append(str(e))
if len(errors) > 0:
raise TypeResolutionError(
'Failed to resolve "{}" namespace against "{}". '
'Errors: {}'.format(
self.full_export_name,
root if root is None else root.full_export_name,
errors
)
)
@@ -0,0 +1,233 @@
import abc
import enum
import itertools
from typing import (Iterator, Type, TypeVar, Dict,
Optional, Tuple, DefaultDict)
from collections import defaultdict
import weakref
ASTNodeSubtype = TypeVar("ASTNodeSubtype", bound="ASTNode")
NodeType = Type["ASTNode"]
NameToNode = Dict[str, ASTNodeSubtype]
class ASTNodeType(enum.Enum):
Namespace = enum.auto()
Class = enum.auto()
Function = enum.auto()
Enumeration = enum.auto()
Constant = enum.auto()
class ASTNode:
"""Represents an element of the Abstract Syntax Tree produced by parsing
public C++ headers.
NOTE: Every node manages a lifetime of its children nodes. Children nodes
contain only weak references to their direct parents, so there are no
circular dependencies.
"""
def __init__(self, name: str, parent: Optional["ASTNode"] = None,
export_name: Optional[str] = None) -> None:
"""ASTNode initializer
Args:
name (str): name of the node, should be unique inside enclosing
context (There can't be 2 classes with the same name defined
in the same namespace).
parent (ASTNode, optional): parent node expressing node context.
None corresponds to globally defined object e.g. root namespace
or function without namespace. Defaults to None.
export_name (str, optional): export name of the node used to resolve
issues in languages without proper overload resolution and
provide more meaningful naming. Defaults to None.
"""
FORBIDDEN_SYMBOLS = ";,*&#/|\\@!()[]^% "
for forbidden_symbol in FORBIDDEN_SYMBOLS:
assert forbidden_symbol not in name, \
"Invalid node identifier '{}' - contains 1 or more "\
"forbidden symbols: ({})".format(name, FORBIDDEN_SYMBOLS)
assert ":" not in name, \
"Name '{}' contains C++ scope symbols (':'). Convert the name to "\
"Python style and create appropriate parent nodes".format(name)
assert "." not in name, \
"Trying to create a node with '.' symbols in its name ({}). " \
"Dots are supposed to be a scope delimiters, so create all nodes in ('{}') " \
"and add '{}' as a last child node".format(
name,
"->".join(name.split('.')[:-1]),
name.rsplit('.', maxsplit=1)[-1]
)
self.__name = name
self.export_name = name if export_name is None else export_name
self._parent: Optional["ASTNode"] = None
self.parent = parent
self.is_exported = True
self._children: DefaultDict[ASTNodeType, NameToNode] = defaultdict(dict)
def __str__(self) -> str:
return "{}('{}' exported as '{}')".format(
self.node_type.name, self.name, self.export_name
)
def __repr__(self) -> str:
return str(self)
@abc.abstractproperty
def children_types(self) -> Tuple[ASTNodeType, ...]:
"""Set of ASTNode types that are allowed to be children of this node
Returns:
Tuple[ASTNodeType, ...]: Types of children nodes
"""
pass
@abc.abstractproperty
def node_type(self) -> ASTNodeType:
"""Type of the ASTNode that can be used to distinguish nodes without
importing all subclasses of ASTNode
Returns:
ASTNodeType: Current node type
"""
pass
def node_type_name(self) -> str:
return f"{self.node_type.name}::{self.name}"
@property
def name(self) -> str:
return self.__name
@property
def native_name(self) -> str:
return self.full_name.replace(".", "::")
@property
def full_name(self) -> str:
return self._construct_full_name("name")
@property
def full_export_name(self) -> str:
return self._construct_full_name("export_name")
@property
def parent(self) -> Optional["ASTNode"]:
return self._parent
@parent.setter
def parent(self, value: Optional["ASTNode"]) -> None:
assert value is None or isinstance(value, ASTNode), \
"ASTNode.parent should be None or another ASTNode, " \
"but got: {}".format(type(value))
if value is not None:
value.__check_child_before_add(self, self.name)
# Detach from previous parent
if self._parent is not None:
self._parent._children[self.node_type].pop(self.name)
if value is None:
self._parent = None
return
# Set a weak reference to a new parent and add self to its children
self._parent = weakref.proxy(value)
value._children[self.node_type][self.name] = self
def __check_child_before_add(self, child: ASTNodeSubtype,
name: str) -> None:
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.node_type in self.children_types, \
"Trying to add child node '{}' to node '{}' " \
"that supports only ({}) as its children types".format(
child.node_type_name, self.node_type_name,
",".join(t.name for t in self.children_types)
)
if self._find_child(child.node_type, name) is not None:
raise ValueError(
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,
**kwargs) -> ASTNodeSubtype:
"""Creates a child of the node with the given type and performs common
validation checks:
- Node can have children of the provided type
- Node doesn't have child with the same name
NOTE: Shouldn't be used directly by a user.
Args:
child_type (Type[ASTNodeSubtype]): Type of the child to create.
name (str): Name of the child.
**kwargs: Extra keyword arguments supplied to child_type.__init__
method.
Returns:
ASTNodeSubtype: Created ASTNode
"""
return child_type(name, parent=self, **kwargs)
def _find_child(self, child_type: ASTNodeType,
name: str) -> Optional[ASTNodeSubtype]:
"""Looks for child node with the given type and name.
Args:
child_type (ASTNodeType): Type of the child node.
name (str): Name of the child node.
Returns:
Optional[ASTNodeSubtype]: child node if it can be found, None
otherwise.
"""
if child_type not in self._children:
return None
return self._children[child_type].get(name, None)
def _construct_full_name(self, property_name: str) -> str:
"""Traverses nodes hierarchy upright to the root node and constructs a
full name of the node using original or export names depending on the
provided `property_name` argument.
Args:
property_name (str): Name of the property to quire from node to get
its name. Should be `name` or `export_name`.
Returns:
str: full node name where each node part is divided with a dot.
"""
def get_name(node: ASTNode) -> str:
return getattr(node, property_name)
assert property_name in ('name', 'export_name'), 'Invalid name property'
name_parts = [get_name(self), ]
parent = self.parent
while parent is not None:
name_parts.append(get_name(parent))
parent = parent.parent
return ".".join(reversed(name_parts))
def __iter__(self) -> Iterator["ASTNode"]:
return iter(itertools.chain.from_iterable(
node
# Iterate over mapping between node type and nodes dict
for children_nodes in self._children.values()
# Iterate over mapping between node name and node
for node in children_nodes.values()
))
@@ -0,0 +1,991 @@
from typing import Sequence, Generator, Tuple, Optional, Union
import weakref
import abc
from itertools import chain
from .node import ASTNode, ASTNodeType
class TypeResolutionError(Exception):
pass
class TypeNode(abc.ABC):
"""This class and its derivatives used for construction parts of AST that
otherwise can't be constructed from the information provided by header
parser, because this information is either not available at that moment of
time or not available at all:
- There is no possible way to derive correspondence between C++ type
and its Python equivalent if it is not exposed from library
e.g. `cv::Rect`.
- There is no information about types visibility (see `ASTNodeTypeNode`).
"""
compatible_to_runtime_usage = False
"""Class-wide property that switches exported type names for several nodes.
Example:
>>> node = OptionalTypeNode(ASTNodeTypeNode("Size"))
>>> node.typename # TypeNode.compatible_to_runtime_usage == False
"Size | None"
>>> TypeNode.compatible_to_runtime_usage = True
>>> node.typename
"typing.Optional[Size]"
"""
def __init__(self, ctype_name: str, required_modules: Tuple[str, ...] = ()) -> None:
self.ctype_name = ctype_name
self._required_modules = required_modules
@abc.abstractproperty
def typename(self) -> str:
"""Short name of the type node used that should be used in the same
module (or a file) where type is defined.
Returns:
str: short name of the type node.
"""
return ""
@property
def full_typename(self) -> str:
"""Full name of the type node including full module name starting from
the package.
Example: 'cv2.Algorithm', 'cv2.gapi.ie.PyParams'.
Returns:
str: full name of the type node.
"""
return self.typename
@property
def required_definition_imports(self) -> Generator[str, None, None]:
"""Generator filled with import statements required for type
node definition (especially used by `AliasTypeNode`).
Example:
```python
# Alias defined in the `cv2.typing.__init__.pyi`
Callback = typing.Callable[[cv2.GMat, float], None]
# alias definition
callback_alias = AliasTypeNode.callable_(
'Callback',
arg_types=(ASTNodeTypeNode('GMat'), PrimitiveTypeNode.float_())
)
# Required definition imports
for required_import in callback_alias.required_definition_imports:
print(required_import)
# Outputs:
# 'import typing'
# 'import cv2'
```
Yields:
Generator[str, None, None]: generator filled with import statements
required for type node definition.
"""
yield from ()
@property
def required_usage_imports(self) -> Generator[str, None, None]:
"""Generator filled with import statements required for type node
usage.
Example:
```python
# Alias defined in the `cv2.typing.__init__.pyi`
Callback = typing.Callable[[cv2.GMat, float], None]
# alias definition
callback_alias = AliasTypeNode.callable_(
'Callback',
arg_types=(ASTNodeTypeNode('GMat'), PrimitiveTypeNode.float_())
)
# Required usage imports
for required_import in callback_alias.required_usage_imports:
print(required_import)
# Outputs:
# 'import cv2.typing'
```
Yields:
Generator[str, None, None]: generator filled with import statements
required for type node definition.
"""
yield from ()
@property
def required_modules(self) -> Tuple[str, ...]:
return self._required_modules
@property
def is_resolved(self) -> bool:
return True
def relative_typename(self, module: str) -> str:
"""Type name relative to the provided module.
Args:
module (str): Full export name of the module to get relative name to.
Returns:
str: If module name of the type node doesn't match `module`, then
returns class scopes + `self.typename`, otherwise
`self.full_typename`.
"""
return self.full_typename
def resolve(self, root: ASTNode) -> None:
"""Resolves all references to AST nodes using a top-down search
for nodes with corresponding export names. See `_resolve_symbol` for
more details.
Args:
root (ASTNode): Node pointing to the root of a subtree in AST
representing search scope of the symbol.
Most of the symbols don't have full paths in their names, so
scopes should be examined in bottom-up manner starting
with narrowest one.
Raises:
TypeResolutionError: if at least 1 reference to AST node can't
be resolved in the subtree pointed by the root.
"""
pass
class NoneTypeNode(TypeNode):
"""Type node representing a None (or `void` in C++) type.
"""
@property
def typename(self) -> str:
return "None"
class AnyTypeNode(TypeNode):
"""Type node representing any type (most of the time it means unknown).
"""
@property
def typename(self) -> str:
return "_typing.Any"
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import typing as _typing"
class PrimitiveTypeNode(TypeNode):
"""Type node representing a primitive built-in types e.g. int, float, str.
"""
def __init__(self, ctype_name: str,
typename: Optional[str] = None,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, required_modules)
self._typename = typename if typename is not None else ctype_name
@property
def typename(self) -> str:
return self._typename
@classmethod
def int_(cls, ctype_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
if ctype_name is None:
ctype_name = "int"
return PrimitiveTypeNode(ctype_name, typename="int", required_modules=required_modules)
@classmethod
def float_(cls, ctype_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
if ctype_name is None:
ctype_name = "float"
return PrimitiveTypeNode(ctype_name, typename="float", required_modules=required_modules)
@classmethod
def bool_(cls, ctype_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
if ctype_name is None:
ctype_name = "bool"
return PrimitiveTypeNode(ctype_name, typename="bool", required_modules=required_modules)
@classmethod
def str_(cls, ctype_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
if ctype_name is None:
ctype_name = "string"
return PrimitiveTypeNode(ctype_name, "str", required_modules=required_modules)
class AliasRefTypeNode(TypeNode):
"""Type node representing an alias referencing another alias. Example:
```python
Point2i = tuple[int, int]
Point = Point2i
```
During typing stubs generation procedure above code section might be defined
as follows
```python
AliasTypeNode.tuple_("Point2i",
items=(
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.int_()
))
AliasTypeNode.ref_("Point", "Point2i")
```
"""
def __init__(self, alias_ctype_name: str,
alias_export_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
super().__init__(alias_ctype_name, required_modules)
if alias_export_name is None:
self.alias_export_name = alias_ctype_name
else:
self.alias_export_name = alias_export_name
@property
def typename(self) -> str:
return self.alias_export_name
@property
def full_typename(self) -> str:
return "cv2.typing." + self.typename
class AliasTypeNode(TypeNode):
"""Type node representing an alias to another type.
Example:
```python
Point2i = tuple[int, int]
```
can be defined as
```python
AliasTypeNode.tuple_("Point2i",
items=(
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.int_()
))
```
Under the hood it is implemented as a container of another type node.
"""
def __init__(self, ctype_name: str, value: TypeNode,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, required_modules)
self.value = value
# If alias is exported as is - use its ctype_name
if export_name is None:
forbidden_symbols = (":", "*", "&")
assert all(symbol not in ctype_name for symbol in forbidden_symbols), (
"Failed to create AliasTypeNode without export_name. "
f"'{ctype_name}' should not contain any of {forbidden_symbols}"
)
self._export_name = ctype_name
else:
self._export_name = export_name
self.doc = doc
@property
def typename(self) -> str:
return self._export_name
@property
def full_typename(self) -> str:
return "cv2.typing." + self.typename
@property
def required_definition_imports(self) -> Generator[str, None, None]:
return self.value.required_usage_imports
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import cv2.typing"
@property
def is_resolved(self) -> bool:
return self.value.is_resolved
def resolve(self, root: ASTNode):
try:
self.value.resolve(root)
except TypeResolutionError as e:
raise TypeResolutionError(
'Failed to resolve alias "{}" exposed as "{}"'.format(
self.ctype_name, self.typename
)
) from e
@classmethod
def int_(cls, ctype_name: str, export_name: Optional[str] = None,
doc: Optional[str] = None, required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, PrimitiveTypeNode.int_(), export_name, doc, required_modules)
@classmethod
def float_(cls, ctype_name: str, export_name: Optional[str] = None,
doc: Optional[str] = None, required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, PrimitiveTypeNode.float_(), export_name, doc, required_modules)
@classmethod
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,
required_modules: Tuple[str, ...] = ()):
"""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 = f"NDArray(shape={shape}, dtype={dtype})"
else:
doc += f". NDArray(shape={shape}, dtype={dtype})"
return cls(ctype_name, AliasRefTypeNode(array_ref_name),
export_name, doc, required_modules)
@classmethod
def union_(cls, ctype_name: str, items: Tuple[TypeNode, ...],
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, UnionTypeNode(ctype_name, items),
export_name, doc, required_modules)
@classmethod
def optional_(cls, ctype_name: str, item: TypeNode,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, OptionalTypeNode(item), export_name, doc, required_modules)
@classmethod
def sequence_(cls, ctype_name: str, item: TypeNode,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, SequenceTypeNode(ctype_name, item),
export_name, doc, required_modules)
@classmethod
def tuple_(cls, ctype_name: str, items: Tuple[TypeNode, ...],
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, TupleTypeNode(ctype_name, items),
export_name, doc, required_modules)
@classmethod
def class_(cls, ctype_name: str, class_name: str,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, ASTNodeTypeNode(class_name),
export_name, doc, required_modules)
@classmethod
def callable_(cls, ctype_name: str,
arg_types: Union[TypeNode, Sequence[TypeNode]],
ret_type: TypeNode = NoneTypeNode("void"),
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name,
CallableTypeNode(ctype_name, arg_types, ret_type),
export_name, doc, required_modules)
@classmethod
def ref_(cls, ctype_name: str, alias_ctype_name: str,
alias_export_name: Optional[str] = None,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name,
AliasRefTypeNode(alias_ctype_name, alias_export_name),
export_name, doc, required_modules)
@classmethod
def dict_(cls, ctype_name: str, key_type: TypeNode, value_type: TypeNode,
export_name: Optional[str] = None, doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, DictTypeNode(ctype_name, key_type, value_type),
export_name, doc, required_modules)
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, 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.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:
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 required_modules(self) -> Tuple[str, ...]:
return (*self.positive_branch_type.required_modules,
*self.negative_branch_type.required_modules)
@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 as _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"
# if self.shape is None:
yield "import typing as _typing"
class ASTNodeTypeNode(TypeNode):
"""Type node representing a lazy ASTNode corresponding to type of
function argument or its return type or type of class property.
Introduced laziness nature resolves the types visibility issue - all types
should be known during function declaration to select an appropriate node
from the AST. Such knowledge leads to evaluation of all preprocessor
directives (`#include` particularly) for each processed header and might be
too expensive and error prone.
"""
def __init__(self, ctype_name: str, typename: Optional[str] = None,
module_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, required_modules)
self._typename = typename if typename is not None else ctype_name
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:
return self._typename
typename = self._ast_node.export_name
if self._ast_node.node_type is not ASTNodeType.Enumeration:
return typename
# NOTE: Special handling for enums
parent = self._ast_node.parent
while parent.node_type is ASTNodeType.Class:
typename = parent.export_name + "_" + typename
parent = parent.parent
return typename
@property
def full_typename(self) -> str:
if self._ast_node is not None:
if self._ast_node.node_type is not ASTNodeType.Enumeration:
return self._ast_node.full_export_name
# NOTE: enumerations are exported to module scope
typename = self._ast_node.export_name
parent = self._ast_node.parent
while parent.node_type is ASTNodeType.Class:
typename = parent.export_name + "_" + typename
parent = parent.parent
return parent.full_export_name + "." + typename
if self._module_name is not None:
return self._module_name + "." + self._typename
return self._typename
@property
def required_usage_imports(self) -> Generator[str, None, None]:
if self._module_name is None:
assert self._ast_node is not None, \
"Can't find a module for class '{}' exported as '{}'".format(
self.ctype_name, self.typename,
)
module = self._ast_node.parent
while module.node_type is not ASTNodeType.Namespace:
module = module.parent
yield "import " + module.full_export_name
else:
yield "import " + self._module_name
@property
def is_resolved(self) -> bool:
return self._ast_node is not None or self._module_name is not None
def resolve(self, root: ASTNode):
if self.is_resolved:
return
node = _resolve_symbol(root, self.typename)
if node is None:
raise TypeResolutionError('Failed to resolve "{}" exposed as "{}"'.format(
self.ctype_name, self.typename
))
self._ast_node = weakref.proxy(node)
def relative_typename(self, module: str) -> str:
assert self._ast_node is not None or self._module_name is not None, \
"'{}' exported as '{}' is not resolved yet".format(self.ctype_name,
self.typename)
if self._module_name is None:
type_module = self._ast_node.parent # type: ignore
while type_module.node_type is not ASTNodeType.Namespace:
type_module = type_module.parent
module_name = type_module.full_export_name
else:
module_name = self._module_name
if module_name != module:
return self.full_typename
return self.full_typename[len(module_name) + 1:]
class AggregatedTypeNode(TypeNode):
"""Base type node for type nodes representing an aggregation of another
type nodes e.g. tuple, sequence or callable."""
def __init__(self, ctype_name: str, items: Sequence[TypeNode],
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, required_modules)
self.items = list(items)
@property
def is_resolved(self) -> bool:
return all(item.is_resolved for item in self.items)
@property
def required_modules(self) -> Tuple[str, ...]:
return (*chain.from_iterable(item.required_modules for item in self.items),
*self._required_modules)
def resolve(self, root: ASTNode) -> None:
errors = []
for item in filter(lambda item: not item.is_resolved, self):
try:
item.resolve(root)
except TypeResolutionError as e:
errors.append(str(e))
if len(errors) > 0:
raise TypeResolutionError(
'Failed to resolve one of "{}" items. Errors: {}'.format(
self.full_typename, errors
)
)
def __iter__(self):
return iter(self.items)
def __len__(self) -> int:
return len(self.items)
@property
def required_definition_imports(self) -> Generator[str, None, None]:
for item in self:
yield from item.required_definition_imports
@property
def required_usage_imports(self) -> Generator[str, None, None]:
for item in self:
yield from item.required_usage_imports
class ContainerTypeNode(AggregatedTypeNode):
"""Base type node for all type nodes representing a container type.
"""
@property
def typename(self) -> str:
return self.type_format.format(self.types_separator.join(
item.typename for item in self
))
@property
def full_typename(self) -> str:
return self.type_format.format(self.types_separator.join(
item.full_typename for item in self
))
def relative_typename(self, module: str) -> str:
return self.type_format.format(self.types_separator.join(
item.relative_typename(module) for item in self
))
@property
def required_definition_imports(self) -> Generator[str, None, None]:
yield "import typing as _typing"
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 as _typing"
yield from super().required_usage_imports
@abc.abstractproperty
def type_format(self) -> str:
return ""
@abc.abstractproperty
def types_separator(self) -> str:
return ""
class SequenceTypeNode(ContainerTypeNode):
"""Type node representing a homogeneous collection of elements with
possible unknown length.
"""
def __init__(self, ctype_name: str, item: TypeNode,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, (item, ), required_modules)
@property
def type_format(self) -> str:
return "_typing.Sequence[{}]"
@property
def types_separator(self) -> str:
return ", "
class TupleTypeNode(ContainerTypeNode):
"""Type node representing possibly heterogeneous collection of types with
possibly unspecified length.
"""
@property
def type_format(self) -> str:
if TypeNode.compatible_to_runtime_usage:
return "_typing.Tuple[{}]"
return "tuple[{}]"
@property
def types_separator(self) -> str:
return ", "
class UnionTypeNode(ContainerTypeNode):
"""Type node representing type that can be one of the predefined set of types.
"""
@property
def type_format(self) -> str:
if TypeNode.compatible_to_runtime_usage:
return "_typing.Union[{}]"
return "{}"
@property
def types_separator(self) -> str:
if TypeNode.compatible_to_runtime_usage:
return ", "
return " | "
class OptionalTypeNode(ContainerTypeNode):
"""Type node representing optional type which is effectively is a union
of value type node and None.
"""
def __init__(self, value: TypeNode,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(value.ctype_name, (value,), required_modules)
@property
def type_format(self) -> str:
if TypeNode.compatible_to_runtime_usage:
return "_typing.Optional[{}]"
return "{} | None"
@property
def types_separator(self) -> str:
return ", "
class DictTypeNode(ContainerTypeNode):
"""Type node representing a homogeneous key-value mapping.
"""
def __init__(self, ctype_name: str, key_type: TypeNode,
value_type: TypeNode,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, (key_type, value_type), required_modules)
@property
def key_type(self) -> TypeNode:
return self.items[0]
@property
def value_type(self) -> TypeNode:
return self.items[1]
@property
def type_format(self) -> str:
if TypeNode.compatible_to_runtime_usage:
return "_typing.Dict[{}]"
return "dict[{}]"
@property
def types_separator(self) -> str:
return ", "
class CallableTypeNode(AggregatedTypeNode):
"""Type node representing a callable type (most probably a function).
```python
CallableTypeNode(
'image_reading_callback',
arg_types=(ASTNodeTypeNode('Image'), PrimitiveTypeNode.float_())
)
```
defines a callable type node representing a function with the same
interface as the following
```python
def image_reading_callback(image: Image, timestamp: float) -> None: ...
```
"""
def __init__(self, ctype_name: str,
arg_types: Union[TypeNode, Sequence[TypeNode]],
ret_type: TypeNode = NoneTypeNode("void"),
required_modules: Tuple[str, ...] = ()) -> None:
if isinstance(arg_types, TypeNode):
super().__init__(ctype_name, (arg_types, ret_type), required_modules)
else:
super().__init__(ctype_name, (*arg_types, ret_type), required_modules)
@property
def arg_types(self) -> Sequence[TypeNode]:
return self.items[:-1]
@property
def ret_type(self) -> TypeNode:
return self.items[-1]
@property
def typename(self) -> str:
return '_typing.Callable[[{}], {}]'.format(
', '.join(arg.typename for arg in self.arg_types),
self.ret_type.typename
)
@property
def full_typename(self) -> str:
return '_typing.Callable[[{}], {}]'.format(
', '.join(arg.full_typename for arg in self.arg_types),
self.ret_type.full_typename
)
def relative_typename(self, module: str) -> str:
return '_typing.Callable[[{}], {}]'.format(
', '.join(arg.relative_typename(module) for arg in self.arg_types),
self.ret_type.relative_typename(module)
)
@property
def required_definition_imports(self) -> Generator[str, None, None]:
yield "import typing as _typing"
yield from super().required_definition_imports
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import typing as _typing"
yield from super().required_usage_imports
class ClassTypeNode(ContainerTypeNode):
"""Type node representing types themselves (refer to typing.Type)
"""
def __init__(self, value: TypeNode,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(value.ctype_name, (value,), required_modules)
@property
def type_format(self) -> str:
return "_typing.Type[{}]"
@property
def types_separator(self) -> str:
return ", "
class PathLikeTypeNode(TypeNode):
"""Type node representing a PathLike object.
"""
def __init__(self, ctype_name: str) -> None:
super().__init__(ctype_name)
@property
def typename(self) -> str:
return "os.PathLike[str]"
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import os"
@staticmethod
def string_or_pathlike_(ctype_name: str = "string") -> UnionTypeNode:
return UnionTypeNode(
ctype_name,
items=(
PrimitiveTypeNode.str_(ctype_name),
PathLikeTypeNode(ctype_name)
)
)
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`.
Args:
root (Optional[ASTNode]): Root of the examining AST.
full_symbol_name (str): Full export name of the symbol to find. Path
components can be divided by '.' or '_'.
Returns:
Optional[ASTNode]: ASTNode with full export name equal to
`full_symbol_name`, None otherwise.
>>> root = NamespaceNode('cv')
>>> cls = root.add_class('Algorithm').add_class('Params')
>>> _resolve_symbol(root, 'cv.Algorithm.Params') == cls
True
>>> root = NamespaceNode('cv')
>>> enum = root.add_namespace('detail').add_enumeration('AlgorithmType')
>>> _resolve_symbol(root, 'cv_detail_AlgorithmType') == enum
True
>>> root = NamespaceNode('cv')
>>> _resolve_symbol(root, 'cv.detail.Algorithm')
None
>>> root = NamespaceNode('cv')
>>> enum = root.add_namespace('detail').add_enumeration('AlgorithmType')
>>> _resolve_symbol(root, 'AlgorithmType')
None
"""
def search_down_symbol(scope: Optional[ASTNode],
scope_sep: str) -> Optional[ASTNode]:
parts = full_symbol_name.split(scope_sep, maxsplit=1)
while len(parts) == 2:
# Try to find narrow scope
scope = _resolve_symbol(scope, parts[0])
if scope is None:
return None
# and resolve symbol in it
node = _resolve_symbol(scope, parts[1])
if node is not None:
return node
# symbol is not found, but narrowed scope is valid - diving further
parts = parts[1].split(scope_sep, maxsplit=1)
return None
assert root is not None, \
"Can't resolve symbol '{}' from NONE root".format(full_symbol_name)
# Looking for exact symbol match
for attr in filter(lambda attr: hasattr(root, attr),
("namespaces", "classes", "enumerations")):
nodes_dict = getattr(root, attr)
node = nodes_dict.get(full_symbol_name, None)
if node is not None:
return node
# Symbol is not found, looking for more fine-grained scope if possible
for scope_sep in ("_", "."):
node = search_down_symbol(root, scope_sep)
if node is not None:
return node
return None
@@ -0,0 +1,273 @@
from .nodes.type_node import (
AliasTypeNode, AliasRefTypeNode, PrimitiveTypeNode,
ASTNodeTypeNode, NDArrayTypeNode, NoneTypeNode, SequenceTypeNode,
TupleTypeNode, UnionTypeNode, AnyTypeNode, ConditionalAliasTypeNode
)
# Set of predefined types used to cover cases when library doesn't
# directly exports a type and equivalent one should be used instead.
# Example: Instead of C++ `cv::Rect(1, 1, 5, 6)` in Python any sequence type
# with length 4 can be used: tuple `(1, 1, 5, 6)` or list `[1, 1, 5, 6]`.
# Predefined type might be:
# - alias - defines a Python synonym for a native type name.
# Example: `cv::Rect` and `cv::Size` are both `Sequence[int]` in Python, but
# with different length constraints (4 and 2 accordingly).
# - direct substitution - just a plain type replacement without any credits to
# native type. Example:
# * `std::vector<uchar>` is `np.ndarray` with `dtype == np.uint8` in Python
# * `double` is a Python `float`
# * `std::string` is a Python `str`
_PREDEFINED_TYPES = (
PrimitiveTypeNode.int_("int"),
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.int_("int64_t"),
PrimitiveTypeNode.int_("long long"),
PrimitiveTypeNode.float_("float"),
PrimitiveTypeNode.float_("double"),
PrimitiveTypeNode.bool_("bool"),
PrimitiveTypeNode.str_("string"),
PrimitiveTypeNode.str_("char"),
PrimitiveTypeNode.str_("String"),
PrimitiveTypeNode.str_("c_string"),
ConditionalAliasTypeNode.numpy_array_(
"NumPyArrayNumeric",
dtype="numpy.integer[_typing.Any] | numpy.floating[_typing.Any]"
),
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"),
AliasRefTypeNode("NumPyArrayNumeric")),
export_name="MatLike"
),
AliasTypeNode.sequence_("MatShape", PrimitiveTypeNode.int_()),
AliasTypeNode.sequence_("Size", PrimitiveTypeNode.int_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Size2f", PrimitiveTypeNode.float_(),
doc="Required length is 2"),
AliasTypeNode.union_(
"Scalar",
items=(SequenceTypeNode("Scalar", PrimitiveTypeNode.float_()),
PrimitiveTypeNode.float_()),
doc="Max sequence length is at most 4"
),
AliasTypeNode.sequence_("Point", PrimitiveTypeNode.int_(),
doc="Required length is 2"),
AliasTypeNode.ref_("Point2i", "Point"),
AliasTypeNode.sequence_("Point2f", PrimitiveTypeNode.float_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Point2d", PrimitiveTypeNode.float_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Point3i", PrimitiveTypeNode.int_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Point3f", PrimitiveTypeNode.float_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Point3d", PrimitiveTypeNode.float_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Range", PrimitiveTypeNode.int_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Rect", PrimitiveTypeNode.int_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Rect2i", PrimitiveTypeNode.int_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Rect2f", PrimitiveTypeNode.float_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Rect2d", PrimitiveTypeNode.float_(),
doc="Required length is 4"),
AliasTypeNode.dict_("Moments", PrimitiveTypeNode.str_("Moments::key"),
PrimitiveTypeNode.float_("Moments::value")),
AliasTypeNode.tuple_("RotatedRect",
items=(AliasRefTypeNode("Point2f"),
AliasRefTypeNode("Size2f"),
PrimitiveTypeNode.float_()),
doc="Any type providing sequence protocol is supported"),
AliasTypeNode.tuple_("TermCriteria",
items=(
ASTNodeTypeNode("TermCriteria.Type"),
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.float_()),
doc="Any type providing sequence protocol is supported"),
AliasTypeNode.sequence_("Vec2i", PrimitiveTypeNode.int_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Vec2f", PrimitiveTypeNode.float_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Vec2d", PrimitiveTypeNode.float_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Vec3i", PrimitiveTypeNode.int_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Vec3f", PrimitiveTypeNode.float_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Vec3d", PrimitiveTypeNode.float_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Vec4i", PrimitiveTypeNode.int_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Vec4f", PrimitiveTypeNode.float_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Vec4d", PrimitiveTypeNode.float_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Vec6f", PrimitiveTypeNode.float_(),
doc="Required length is 6"),
AliasTypeNode.class_("FeatureDetector", "Feature2D",
export_name="FeatureDetector"),
AliasTypeNode.class_("DescriptorExtractor", "Feature2D",
export_name="DescriptorExtractor"),
AliasTypeNode.class_("FeatureExtractor", "Feature2D",
export_name="FeatureExtractor"),
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"),
# DNN, optional
AliasTypeNode.class_("LayerId", "DictValue", required_modules=("dnn",)),
AliasTypeNode.dict_("LayerParams",
key_type=PrimitiveTypeNode.str_(),
value_type=UnionTypeNode("DictValue", items=(
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.float_(),
PrimitiveTypeNode.str_())
),
required_modules=("dnn",)),
# Flann, optional
PrimitiveTypeNode.int_("cvflann_flann_distance_t", required_modules=("flann",)),
PrimitiveTypeNode.int_("flann_flann_distance_t", required_modules=("flann",)),
PrimitiveTypeNode.int_("cvflann_flann_algorithm_t", required_modules=("flann",)),
PrimitiveTypeNode.int_("flann_flann_algorithm_t", required_modules=("flann",)),
AliasTypeNode.dict_("flann_IndexParams",
key_type=PrimitiveTypeNode.str_(),
value_type=UnionTypeNode("flann_IndexParams::value", items=(
PrimitiveTypeNode.bool_(),
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.float_(),
PrimitiveTypeNode.str_())
),
export_name="IndexParams",
required_modules=("flann",)),
AliasTypeNode.dict_("flann_SearchParams",
key_type=PrimitiveTypeNode.str_(),
value_type=UnionTypeNode("flann_IndexParams::value", items=(
PrimitiveTypeNode.bool_(),
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.float_(),
PrimitiveTypeNode.str_())
),
export_name="SearchParams",
required_modules=("flann",)),
AliasTypeNode.dict_("map_string_and_string",
PrimitiveTypeNode.str_("map_string_and_string::key"),
PrimitiveTypeNode.str_("map_string_and_string::value"),
required_modules=("flann",)),
AliasTypeNode.dict_("map_string_and_int",
PrimitiveTypeNode.str_("map_string_and_int::key"),
PrimitiveTypeNode.int_("map_string_and_int::value"),
required_modules=("flann",)),
AliasTypeNode.dict_("map_string_and_vector_size_t",
PrimitiveTypeNode.str_("map_string_and_vector_size_t::key"),
SequenceTypeNode("map_string_and_vector_size_t::value", PrimitiveTypeNode.int_("size_t")),
required_modules=("flann",)),
AliasTypeNode.dict_("map_string_and_vector_float",
PrimitiveTypeNode.str_("map_string_and_vector_float::key"),
SequenceTypeNode("map_string_and_vector_float::value", PrimitiveTypeNode.float_()),
required_modules=("flann",)),
AliasTypeNode.dict_("map_int_and_double",
PrimitiveTypeNode.int_("map_int_and_double::key"),
PrimitiveTypeNode.float_("map_int_and_double::value"),
required_modules=("flann",)),
# G-API, optional
AliasTypeNode.union_("GProtoArg",
items=(AliasRefTypeNode("Scalar"),
ASTNodeTypeNode("GMat"),
ASTNodeTypeNode("GOpaqueT"),
ASTNodeTypeNode("GArrayT")),
required_modules=("gapi",)),
SequenceTypeNode("GProtoArgs", AliasRefTypeNode("GProtoArg"), required_modules=("gapi",)),
AliasTypeNode.sequence_("GProtoInputArgs", AliasRefTypeNode("GProtoArg"), required_modules=("gapi",)),
AliasTypeNode.sequence_("GProtoOutputArgs", AliasRefTypeNode("GProtoArg"), required_modules=("gapi",)),
AliasTypeNode.union_(
"GRunArg",
items=(AliasRefTypeNode("Mat", "MatLike"),
AliasRefTypeNode("Scalar"),
ASTNodeTypeNode("GOpaqueT"),
ASTNodeTypeNode("GArrayT"),
SequenceTypeNode("GRunArg", AnyTypeNode("GRunArg")),
NoneTypeNode("GRunArg")),
required_modules=("gapi",)
),
AliasTypeNode.optional_("GOptRunArg", AliasRefTypeNode("GRunArg"), required_modules=("gapi",)),
AliasTypeNode.union_("GMetaArg",
items=(ASTNodeTypeNode("GMat"),
AliasRefTypeNode("Scalar"),
ASTNodeTypeNode("GOpaqueT"),
ASTNodeTypeNode("GArrayT")),
required_modules=("gapi",)),
AliasTypeNode.union_("Prim",
items=(ASTNodeTypeNode("gapi.wip.draw.Text"),
ASTNodeTypeNode("gapi.wip.draw.Circle"),
ASTNodeTypeNode("gapi.wip.draw.Image"),
ASTNodeTypeNode("gapi.wip.draw.Line"),
ASTNodeTypeNode("gapi.wip.draw.Rect"),
ASTNodeTypeNode("gapi.wip.draw.Mosaic"),
ASTNodeTypeNode("gapi.wip.draw.Poly")),
required_modules=("gapi",)),
SequenceTypeNode("Prims", AliasRefTypeNode("Prim"), required_modules=("gapi",)),
TupleTypeNode("GMat2", items=(ASTNodeTypeNode("GMat"),
ASTNodeTypeNode("GMat")), required_modules=("gapi",)),
ASTNodeTypeNode("GOpaque", "GOpaqueT", required_modules=("gapi",)),
ASTNodeTypeNode("GArray", "GArrayT", required_modules=("gapi",)),
AliasTypeNode.union_("GTypeInfo",
items=(ASTNodeTypeNode("GMat"),
AliasRefTypeNode("Scalar"),
ASTNodeTypeNode("GOpaqueT"),
ASTNodeTypeNode("GArrayT")),
required_modules=("gapi",)),
SequenceTypeNode("GCompileArgs", ASTNodeTypeNode("GCompileArg"), required_modules=("gapi",)),
SequenceTypeNode("GTypesInfo", AliasRefTypeNode("GTypeInfo"), required_modules=("gapi",)),
SequenceTypeNode("GRunArgs", AliasRefTypeNode("GRunArg"), required_modules=("gapi",)),
SequenceTypeNode("GMetaArgs", AliasRefTypeNode("GMetaArg"), required_modules=("gapi",)),
SequenceTypeNode("GOptRunArgs", AliasRefTypeNode("GOptRunArg"), required_modules=("gapi",)),
AliasTypeNode.callable_(
"detail_ExtractArgsCallback",
arg_types=SequenceTypeNode("GTypesInfo", AliasRefTypeNode("GTypeInfo")),
ret_type=SequenceTypeNode("GRunArgs", AliasRefTypeNode("GRunArg")),
export_name="ExtractArgsCallback",
required_modules=("gapi",)
),
AliasTypeNode.callable_(
"detail_ExtractMetaCallback",
arg_types=SequenceTypeNode("GTypesInfo", AliasRefTypeNode("GTypeInfo")),
ret_type=SequenceTypeNode("GMetaArgs", AliasRefTypeNode("GMetaArg")),
export_name="ExtractMetaCallback",
required_modules=("gapi",)
),
PrimitiveTypeNode("NativeByteArray", "bytes"),
)
PREDEFINED_TYPES = dict(
zip((t.ctype_name for t in _PREDEFINED_TYPES), _PREDEFINED_TYPES)
)
@@ -0,0 +1,333 @@
from typing import Tuple, List, Optional
from .predefined_types import PREDEFINED_TYPES
from .nodes.type_node import (
TypeNode, UnionTypeNode, SequenceTypeNode, ASTNodeTypeNode, TupleTypeNode
)
def replace_template_parameters_with_placeholders(string: str) \
-> Tuple[str, Tuple[str, ...]]:
"""Replaces template parameters with `format` placeholders for all template
instantiations in provided string.
Only outermost template parameters are replaced.
Args:
string (str): input string containing C++ template instantiations
Returns:
tuple[str, tuple[str, ...]]: string with '{}' placeholders template
instead of instantiation types and a tuple of extracted types.
>>> template_string, args = replace_template_parameters_with_placeholders(
... "std::vector<cv::Point<int>>, test<int>"
... )
>>> template_string.format(*args) == "std::vector<cv::Point<int>>, test<int>"
True
>>> replace_template_parameters_with_placeholders(
... "cv::util::variant<cv::GRunArgs, cv::GOptRunArgs>"
... )
('cv::util::variant<{}>', ('cv::GRunArgs, cv::GOptRunArgs',))
>>> replace_template_parameters_with_placeholders("vector<Point<int>>")
('vector<{}>', ('Point<int>',))
>>> replace_template_parameters_with_placeholders(
... "vector<Point<int>>, vector<float>"
... )
('vector<{}>, vector<{}>', ('Point<int>', 'float'))
>>> replace_template_parameters_with_placeholders("string without templates")
('string without templates', ())
"""
template_brackets_indices = []
template_instantiations_count = 0
template_start_index = 0
for i, c in enumerate(string):
if c == "<":
template_instantiations_count += 1
if template_instantiations_count == 1:
# + 1 - because left bound is included in substring range
template_start_index = i + 1
elif c == ">":
template_instantiations_count -= 1
assert template_instantiations_count >= 0, \
"Provided string is ill-formed. There are more '>' than '<'."
if template_instantiations_count == 0:
template_brackets_indices.append((template_start_index, i))
assert template_instantiations_count == 0, \
"Provided string is ill-formed. There are more '<' than '>'."
template_args: List[str] = []
# Reversed loop is required to preserve template start/end indices
for i, j in reversed(template_brackets_indices):
template_args.insert(0, string[i:j])
string = string[:i] + "{}" + string[j:]
return string, tuple(template_args)
def get_template_instantiation_type(typename: str) -> str:
"""Extracts outermost template instantiation type from provided string
Args:
typename (str): String containing C++ template instantiation.
Returns:
str: String containing template instantiation type
>>> get_template_instantiation_type("std::vector<cv::Point<int>>")
'cv::Point<int>'
>>> get_template_instantiation_type("std::vector<uchar>")
'uchar'
>>> get_template_instantiation_type("std::map<int, float>")
'int, float'
>>> get_template_instantiation_type("uchar")
Traceback (most recent call last):
...
ValueError: typename ('uchar') doesn't contain template instantiations
>>> get_template_instantiation_type("std::vector<int>, std::vector<float>")
Traceback (most recent call last):
...
ValueError: typename ('std::vector<int>, std::vector<float>') contains more than 1 template instantiation
"""
_, args = replace_template_parameters_with_placeholders(typename)
if len(args) == 0:
raise ValueError(
"typename ('{}') doesn't contain template instantiations".format(typename)
)
if len(args) > 1:
raise ValueError(
"typename ('{}') contains more than 1 template instantiation".format(typename)
)
return args[0]
def normalize_ctype_name(typename: str) -> str:
"""Normalizes C++ name by removing unnecessary namespace prefixes and possible
pointer/reference qualification. '::' are replaced with '_'.
NOTE: Pointer decay for 'void*' is not performed.
Args:
typename (str): Name of the C++ type for normalization
Returns:
str: Normalized C++ type name.
>>> normalize_ctype_name('std::vector<cv::Point2f>&')
'vector<cv_Point2f>'
>>> normalize_ctype_name('AKAZE::DescriptorType')
'AKAZE_DescriptorType'
>>> normalize_ctype_name('std::vector<Mat>')
'vector<Mat>'
>>> normalize_ctype_name('std::string')
'string'
>>> normalize_ctype_name('void*') # keep void* as is - special case
'void*'
>>> normalize_ctype_name('Ptr<AKAZE>')
'AKAZE'
>>> normalize_ctype_name('Algorithm_Ptr')
'Algorithm'
"""
for prefix_to_remove in ("cv", "std"):
if typename.startswith(prefix_to_remove):
typename = typename[len(prefix_to_remove):]
typename = typename.replace("::", "_").lstrip("_")
if typename.endswith('&'):
typename = typename[:-1]
typename = typename.strip()
if typename == 'void*':
return typename
if is_pointer_type(typename):
# Case for "type*", "type_Ptr", "typePtr"
for suffix in ("*", "_Ptr", "Ptr"):
if typename.endswith(suffix):
return typename[:-len(suffix)]
# Case Ptr<Type>
if _is_template_instantiation(typename):
return normalize_ctype_name(
get_template_instantiation_type(typename)
)
# Case Ptr_Type
return typename.split("_", maxsplit=1)[-1]
# special normalization for several G-API Types
if typename.startswith("GArray_") or typename.startswith("GArray<"):
return "GArrayT"
if typename.startswith("GOpaque_") or typename.startswith("GOpaque<"):
return "GOpaqueT"
if typename == "GStreamerPipeline" or typename.startswith("GStreamerSource"):
return "gst_" + typename
return typename
def is_tuple_type(typename: str) -> bool:
return typename.startswith("tuple") or typename.startswith("pair")
def is_sequence_type(typename: str) -> bool:
return typename.startswith("vector")
def is_pointer_type(typename: str) -> bool:
return typename.endswith("Ptr") or typename.endswith("*") \
or typename.startswith("Ptr")
def is_union_type(typename: str) -> bool:
return typename.startswith('util_variant')
def _is_template_instantiation(typename: str) -> bool:
"""Fast, but unreliable check whenever provided typename is a template
instantiation.
Args:
typename (str): typename to check against template instantiation.
Returns:
bool: True if provided `typename` contains template instantiation,
False otherwise
"""
if "<" in typename:
assert ">" in typename, \
"Wrong template class instantiation: {}. '>' is missing".format(typename)
return True
return False
def create_type_nodes_from_template_arguments(template_args_str: str) \
-> List[TypeNode]:
"""Creates a list of type nodes corresponding to the argument types
used for template instantiation.
This method correctly addresses the situation when arguments of the input
template are also templates.
Example:
if `create_type_node` is called with
`std::tuple<std::variant<int, Point2i>, int, std::vector<int>>`
this function will be called with
`std::variant<int, Point<int>>, int, std::vector<int>`
that produces the following order of types resolution
`std::variant` ~ `Union`
`std::variant<int, Point2i>` -> `int` ~ `int` -> `Union[int, Point2i]`
`Point2i` ~ `Point2i`
`int` -> `int`
`std::vector<int>` -> `std::vector` ~ `Sequence` -> `Sequence[int]`
`int` ~ `int`
Returns:
List[TypeNode]: set of type nodes used for template instantiation.
List is empty if input string doesn't contain template instantiation.
"""
type_nodes = []
template_args_str, templated_args_types = replace_template_parameters_with_placeholders(
template_args_str
)
template_index = 0
# For each template argument
for template_arg in template_args_str.split(","):
template_arg = template_arg.strip()
# Check if argument requires type substitution
if _is_template_instantiation(template_arg):
# Reconstruct the original type
template_arg = template_arg.format(templated_args_types[template_index])
template_index += 1
# create corresponding type node
type_nodes.append(create_type_node(template_arg))
return type_nodes
def create_type_node(typename: str,
original_ctype_name: Optional[str] = None) -> TypeNode:
"""Converts C++ type name to appropriate type used in Python library API.
Conversion procedure:
1. Normalize typename: remove redundant prefixes, unify name
components delimiters, remove reference qualifications.
2. Check whenever typename has a known predefined conversion or exported
as alias e.g.
- C++ `double` -> Python `float`
- C++ `cv::Rect` -> Python `Sequence[int]`
- C++ `std::vector<char>` -> Python `np.ndarray`
return TypeNode corresponding to the appropriate type.
3. Check whenever typename is a container of types e.g. variant,
sequence or tuple. If so, select appropriate Python container type
and perform arguments conversion.
4. Create a type node corresponding to the AST node passing normalized
typename as its name.
Args:
typename (str): C++ type name to convert.
original_ctype_name (Optional[str]): Original C++ name of the type.
`original_ctype_name` == `typename` if provided argument is None.
Default is None.
Returns:
TypeNode: type node that wraps C++ type exposed to Python
>>> create_type_node('Ptr<AKAZE>').typename
'AKAZE'
>>> create_type_node('std::vector<Ptr<cv::Algorithm>>').typename
'typing.Sequence[Algorithm]'
"""
if original_ctype_name is None:
original_ctype_name = typename
typename = normalize_ctype_name(typename.strip())
# if typename is a known alias or has explicitly defined substitution
type_node = PREDEFINED_TYPES.get(typename)
if type_node is not None:
type_node.ctype_name = original_ctype_name
return type_node
# If typename is a known exported alias name (e.g. IndexParams or SearchParams)
for alias in PREDEFINED_TYPES.values():
if alias.typename == typename:
return alias
if is_union_type(typename):
union_types = get_template_instantiation_type(typename)
return UnionTypeNode(
original_ctype_name,
items=create_type_nodes_from_template_arguments(union_types)
)
# if typename refers to a sequence type e.g. vector<int>
if is_sequence_type(typename):
# Recursively convert sequence element type
if _is_template_instantiation(typename):
inner_sequence_type = create_type_node(
get_template_instantiation_type(typename)
)
else:
# Handle vector_Type cases
# maxsplit=1 is required to handle sequence of sequence e.g:
# vector_vector_Mat -> Sequence[Sequence[Mat]]
inner_sequence_type = create_type_node(typename.split("_", 1)[-1])
return SequenceTypeNode(original_ctype_name, inner_sequence_type)
# If typename refers to a heterogeneous container
# (can contain elements of different types)
if is_tuple_type(typename):
tuple_types = get_template_instantiation_type(typename)
return TupleTypeNode(
original_ctype_name,
items=create_type_nodes_from_template_arguments(tuple_types)
)
# If everything else is False, it means that input typename refers to a
# class or enum of the library.
return ASTNodeTypeNode(original_ctype_name, typename)
if __name__ == "__main__":
import doctest
doctest.testmod()