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