chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:06:04 +08:00
commit 86c9b1c39f
7743 changed files with 3316339 additions and 0 deletions
@@ -0,0 +1,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