chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user