mirror of
https://github.com/ModOrganizer2/pystubs-generation.git
synced 2026-07-27 14:07:13 -07:00
548 lines
16 KiB
Python
548 lines
16 KiB
Python
# -*- encoding: utf-8 -*-
|
|
|
|
import inspect
|
|
import re
|
|
|
|
from collections import defaultdict, OrderedDict
|
|
|
|
from typing import List, Tuple, Optional, Dict, Union
|
|
|
|
from .register import MobaseRegister
|
|
from .mtypes import Type, CType, Class, Enum, Arg, Method, Constant, Property, Function
|
|
from . import logger
|
|
|
|
|
|
def magic_split(value: str, sep=",", open="(<", close=")>"):
|
|
""" Split the value according to the given separator, but keeps together elements
|
|
within the given separator. Useful to split C++ signature function since type names
|
|
can contain special characters...
|
|
|
|
Examples:
|
|
- magic_split("a,b,c", sep=",") -> ["a", "b", "c"]
|
|
- magic_split("a<b,c>,d(e,<k,c>),p) -> ["a<b,c>", "d(e,<k,c>)", "p"]
|
|
|
|
Args:
|
|
value: String to split.
|
|
sep: Separator to use.
|
|
open: List of opening characters.
|
|
close: List of closing characters. Order must match open.
|
|
|
|
Returns: The list of split parts from value.
|
|
"""
|
|
i, j = 0, 0
|
|
s: List[str] = []
|
|
r = []
|
|
while i < len(value):
|
|
j = i + 1
|
|
while j < len(value):
|
|
c = value[j]
|
|
|
|
# Separator found and the stack is empty:
|
|
if c == sep and not s:
|
|
break
|
|
|
|
# Check close/open:
|
|
if c in open:
|
|
s.append(open.index(c))
|
|
elif c in close:
|
|
# The stack might be empty if the separator is also an opening element:
|
|
if not s and sep in open and j + 1 == len(value):
|
|
pass
|
|
else:
|
|
t = s.pop()
|
|
if t != close.index(c):
|
|
raise ValueError(
|
|
"Found closing element {} for opening element {}.".format(
|
|
c, open[t]
|
|
)
|
|
)
|
|
j += 1
|
|
r.append(value[i:j])
|
|
i = j + 1
|
|
|
|
return r
|
|
|
|
|
|
def parse_ctype(s: str) -> CType:
|
|
""" Parse a C++ type from the given string.
|
|
|
|
Args:
|
|
s: String to parse.
|
|
|
|
Returns: A C++ type parsed from the given string.
|
|
"""
|
|
|
|
# List of strings that can be removed from the names:
|
|
for d in [
|
|
"__64",
|
|
"__cdecl",
|
|
"__ptr64",
|
|
"{lvalue}",
|
|
"class",
|
|
"struct",
|
|
"enum",
|
|
"unsigned",
|
|
]:
|
|
s = s.replace(d, "")
|
|
|
|
# Remove the namespace remaing:
|
|
for d in ["MOBase", "boost::python"]:
|
|
s = s.replace(d + "::", "")
|
|
|
|
# Specific replacement:
|
|
s = s.replace("__int64", "int")
|
|
s = s.replace(" const &", "")
|
|
s = s.replace("&", "")
|
|
|
|
return CType(s.strip())
|
|
|
|
|
|
def parse_carg(s: str, has_default: bool) -> Arg:
|
|
""" Parse the given C++ argument.
|
|
|
|
Args:
|
|
s: The string to parse.
|
|
has_default: Indicates if this argument as a default.
|
|
|
|
Returns: An argument parsed from the given string.
|
|
"""
|
|
v, d = s, None
|
|
if s.find("=") != -1:
|
|
v, d = [x.strip() for x in s.split("=")]
|
|
|
|
if d is None and has_default:
|
|
d = Arg.DEFAULT_NONE
|
|
|
|
return Arg(parse_ctype(v), d)
|
|
|
|
|
|
def parse_csig(s, name) -> Tuple[CType, List[Arg]]:
|
|
""" Parse a boost::python C++ signature.
|
|
|
|
Args:
|
|
s: The signature to parse.
|
|
name: Name of the function, or "" if the signature correspond to a type.
|
|
|
|
Returns: (RType, Args) where RType is a CType object, and Args is a list of Arg
|
|
objects containing CType.
|
|
"""
|
|
# Remove the [ and ] which specifies default arguments but are useless since
|
|
# we already have = to tell us - The replacement is weird because the way boost
|
|
# present these is weird, and to avoid breaking default argument such as = []:
|
|
c = s.count("[,")
|
|
s = s.replace("[,", ",")
|
|
s = s.replace("]" * c, "")
|
|
|
|
# Split return type/arguments:
|
|
if name:
|
|
rtype_s, args_s = s.split(name)
|
|
|
|
# Remove the ( and ).
|
|
args_s = args_s.strip()[1:-1]
|
|
else:
|
|
rtype_s, args_s = magic_split(s, "(", "(<", ")>")
|
|
|
|
# Only remove the last ) because the first one is removed by magic_split:
|
|
args_s = args_s.strip()[:-1]
|
|
|
|
# Parse return type:
|
|
rtype = parse_ctype(rtype_s.strip())
|
|
|
|
# Parse arguments:
|
|
|
|
# Strip spaces and remove the first and last ():
|
|
args_s = args_s.strip()
|
|
args_ss = magic_split(args_s, ",", "(<", ")>")
|
|
args = [parse_carg(v, i > len(args_ss) - c - 1) for i, v in enumerate(args_ss)]
|
|
|
|
return rtype, args
|
|
|
|
|
|
def parse_psig(s: str, name: str) -> Tuple[Type, List[Arg]]:
|
|
""" Parse a boost::python python signature.
|
|
|
|
Args:
|
|
s: The signature to parse.
|
|
name: Name of the function.
|
|
|
|
Returns: (RType, Args) where RType is a Type object, and Args is a list of Arg
|
|
objects containing Type.
|
|
"""
|
|
|
|
c = s.count("[,")
|
|
s = s.replace("[,", ",")
|
|
s = s.replace("]" * c, "")
|
|
|
|
# This is pretty brutal way of extracting stuff... But most things can be
|
|
# retrieve from the C++ signature, here we are mainly interested in extracting
|
|
# the python type if possible:
|
|
m: re.Match[str] = re.search(
|
|
r"{}\((.*)\)\s*->\s*([^\s]+)\s*:".format(name), s
|
|
) # type: ignore
|
|
pargs = []
|
|
args = list(filter(bool, m.group(1).strip().split(",")))
|
|
for i, pa in enumerate(args):
|
|
pa = pa.strip()
|
|
t = pa[1 : pa.find(")")]
|
|
d: Optional[str] = None
|
|
if pa.find("=") != -1:
|
|
d = pa.split("=")[-1].strip()
|
|
elif i > len(args) - c - 1:
|
|
d = Arg.DEFAULT_NONE
|
|
pargs.append(Arg(Type(t), d))
|
|
return Type(m.group(2)), pargs
|
|
|
|
|
|
def find_best_type(ptype: Type, ctype: CType) -> Type:
|
|
""" Find the best type from the given python and C++ type.
|
|
|
|
|
|
Args:
|
|
ptype: The python type.
|
|
ctype: The C++ type.
|
|
|
|
Returns: The best of the two types.
|
|
"""
|
|
from .register import MOBASE_REGISTER
|
|
|
|
if ptype.name == ctype.name:
|
|
return ptype
|
|
elif ptype.is_none() and ctype.is_none():
|
|
return ptype
|
|
|
|
assert ptype.is_none() == ctype.is_none()
|
|
|
|
MOBASE_REGISTER.register_type(ptype, ctype)
|
|
|
|
if ptype.is_object():
|
|
if ctype.is_object():
|
|
return ptype
|
|
return ctype
|
|
|
|
# Returned pointer are treated differently because they can often be null:
|
|
if ctype.is_pointer():
|
|
return ctype
|
|
|
|
return ptype
|
|
|
|
|
|
def find_best_value(pvalue: str, cvalue: str) -> str:
|
|
""" Find the best value (default value) from the given python and C++ one.
|
|
|
|
WARNING: This currently always return pvalue and only warns the user if
|
|
the two values are not identical.
|
|
|
|
Args:
|
|
pvalue: Python default value.
|
|
cvalue: C++ default value.
|
|
|
|
Returns: The best of the two values.
|
|
"""
|
|
if pvalue != cvalue:
|
|
logger.warning("Mismatch default value: {} {}.".format(pvalue, cvalue))
|
|
return pvalue
|
|
|
|
|
|
def is_enum(e: type) -> bool:
|
|
""" Check if the given class is an enumeration.
|
|
|
|
Args:
|
|
e: The class object to check.
|
|
|
|
Returns: True if the object is an enumeration (boost::python enumeration, not
|
|
python) False otherwize.
|
|
"""
|
|
# Yet to find a better way...
|
|
if not isinstance(e, type):
|
|
return False
|
|
return any(
|
|
"{}.{}".format(c.__module__, c.__name__) == "Boost.Python.enum"
|
|
for c in inspect.getmro(e)
|
|
)
|
|
|
|
|
|
def make_enum(name: str, e: type) -> Enum:
|
|
""" Construct a Enum object from the given class.
|
|
|
|
Args:
|
|
name: Fully qualified name of the enumeration.
|
|
e: The class representing a boost::python enumeration.
|
|
|
|
Returns: An Enum object representing the given enumeration.
|
|
"""
|
|
# All boost enums have a .values attributes:
|
|
values = e.values # type: ignore
|
|
return Enum(
|
|
e.__name__, OrderedDict((values[k].name, k) for k in sorted(values.keys()))
|
|
)
|
|
|
|
|
|
class Overload:
|
|
|
|
""" Small class to avoid mypy issues... """
|
|
|
|
rtype: Type
|
|
args: List[Arg]
|
|
|
|
def __init__(self, rtype, args):
|
|
self.rtype = rtype
|
|
self.args = args
|
|
|
|
|
|
def parse_bpy_function_docstring(e) -> List[Overload]:
|
|
""" Parse the docstring of the given element.
|
|
|
|
Args:
|
|
e: The function to "parse".
|
|
|
|
Returns: A list of overloads for the given function, where each overload is
|
|
a dictionary with a "rtype" entry containing the return type and a "args"
|
|
entry containing the list of arguments.
|
|
"""
|
|
lines = e.__doc__.split("\n")
|
|
|
|
# Find the various overloads:
|
|
so = [i for i, line in enumerate(lines) if line.strip().startswith(e.__name__)]
|
|
so.append(len(lines))
|
|
|
|
# We are going to parse the python and C++ signature, and try to merge
|
|
# them...
|
|
overloads: List[Overload] = []
|
|
for i, j in zip(so[:-1], so[1:]):
|
|
|
|
psig = lines[i].strip()
|
|
for k in range(i, j):
|
|
if lines[k].strip().startswith("C++ signature"):
|
|
csig = lines[k + 1].strip()
|
|
|
|
prtype, pargs = parse_psig(psig, e.__name__)
|
|
crtype, cargs = parse_csig(csig, e.__name__)
|
|
|
|
# Currently there is no way to automatically check so we add [optional]
|
|
# in the doc:
|
|
if e.__doc__.find("[optional]") != -1:
|
|
crtype._optional = True
|
|
|
|
assert len(pargs) == len(cargs)
|
|
|
|
# Now we need to find the "best" type from both signatures:
|
|
rtype = find_best_type(prtype, crtype)
|
|
args = []
|
|
for parg, carg in zip(pargs, cargs):
|
|
args.append(
|
|
Arg(
|
|
find_best_type(parg.type, carg.type), # type: ignore
|
|
find_best_value(parg.value, carg.value), # type: ignore
|
|
)
|
|
) # type: ignore
|
|
|
|
overloads.append(Overload(rtype=rtype, args=args))
|
|
|
|
return overloads
|
|
|
|
|
|
def make_functions(name: str, e) -> List[Function]:
|
|
overloads = parse_bpy_function_docstring(e)
|
|
|
|
return [
|
|
Function(e.__name__, ovld.rtype, ovld.args, has_overloads=len(overloads) > 1,)
|
|
for ovld in overloads
|
|
]
|
|
|
|
|
|
def make_class(fullname: str, e: type, register: MobaseRegister) -> Class:
|
|
""" Constructs a Class objecgt from the given python class.
|
|
|
|
Args:
|
|
fullname: Name of the class (might be different from __name__ for inner
|
|
classes).
|
|
e: The python class (created from boost) to construct an object for.
|
|
class_register:
|
|
|
|
Returns: A Class object corresponding to the given class.
|
|
"""
|
|
|
|
base_classes_s: List[str] = []
|
|
|
|
# Kind of ugly, but...:
|
|
for c in inspect.getmro(e):
|
|
if c != e and c.__module__ == "mobase":
|
|
base_classes_s.append(c.__name__)
|
|
if c.__module__ == "Boost.Python":
|
|
break
|
|
|
|
# Keep as a comment but this is/should be fixed in the actual C++ code:
|
|
# Lots of class exposed do not inherit IPlugin while they should:
|
|
# if "IPlugin" not in base_classes_s and e.__name__.startswith("IPlugin") \
|
|
# and e.__name__ != "IPlugin":
|
|
# base_classes_s.append("IPlugin")
|
|
|
|
# This contains ALL the parent classes, not the direct ones:
|
|
base_classes = [register.make_object(name) for name in base_classes_s]
|
|
|
|
# Retrieve all the attributes... The hasattr is required but I don't know why:
|
|
all_attrs = [(n, getattr(e, n)) for n in dir(e) if hasattr(e, n)]
|
|
|
|
# Some exclusions:
|
|
EXCLUDED_MEMBERS = [
|
|
"__weakref__",
|
|
"__dict__",
|
|
"__doc__",
|
|
"__instance_size__",
|
|
"__module__",
|
|
"__getattr__",
|
|
]
|
|
all_attrs = [
|
|
a
|
|
for a in all_attrs
|
|
# Using getattr() here since some attribute do not have name (e.g. constants):
|
|
if a[0] not in EXCLUDED_MEMBERS
|
|
]
|
|
|
|
# Fetch all attributes from the base classes:
|
|
base_attrs: Dict[str, List[Union[Constant, Property, Method, Class]]] = defaultdict(
|
|
list
|
|
)
|
|
for bc in base_classes:
|
|
# Thanks mypy for the naming...
|
|
for a1 in bc.constants:
|
|
base_attrs[a1.name].append(a1)
|
|
for a2 in bc.methods:
|
|
base_attrs[a2.name].append(a2)
|
|
for a3 in bc.properties:
|
|
base_attrs[a3.name].append(a3)
|
|
for a4 in bc.inner_classes:
|
|
base_attrs[a4.name].append(a4)
|
|
|
|
# Find the methods:
|
|
methods = [m[1] for m in all_attrs if callable(m[1])]
|
|
methods = sorted(methods, key=lambda m: m.__name__)
|
|
|
|
# Filter out methods not provided or implemented:
|
|
methods = [
|
|
m
|
|
for m in methods
|
|
if m.__doc__ is not None and m.__doc__.find("C++ signature") != -1
|
|
]
|
|
|
|
# List of methods that must return bool:
|
|
BOOL_METHODS = ["__eq__", "__lt__", "__le__", "__ne__", "__gt__", "__ge__"]
|
|
|
|
pmethods = []
|
|
for method in methods:
|
|
if method.__doc__ is None:
|
|
continue
|
|
overloads = parse_bpy_function_docstring(method)
|
|
|
|
# __eq__ must accept an object in python, so we need to add an overload:
|
|
if method.__name__ in ["__eq__", "__ne__"]:
|
|
overloads.append(
|
|
Overload(
|
|
rtype=Type("bool"),
|
|
args=[Arg(Type(e.__name__)), Arg(Type("object"))],
|
|
)
|
|
)
|
|
|
|
cmethods = []
|
|
for ovld in overloads:
|
|
args = ovld.args
|
|
|
|
# This is a very heuristic way of checking if the method is static but I did
|
|
# not find anything better yet...
|
|
static = False
|
|
if len(args) == 0:
|
|
static = True
|
|
elif method.__name__.startswith("__"): # Special method cannot be static
|
|
static = False
|
|
else:
|
|
arg0_name = args[0].type.name
|
|
if arg0_name in register.cpp2py:
|
|
arg0_name = register.cpp2py[arg0_name].name
|
|
arg0_name = (
|
|
arg0_name.replace("*", "")
|
|
.replace("&", "")
|
|
.replace("const", "")
|
|
.strip()
|
|
)
|
|
|
|
static = (
|
|
arg0_name
|
|
not in [e.__name__, e.__name__ + "Wrapper"] + base_classes_s
|
|
)
|
|
|
|
pmethod = Method(
|
|
e.__name__,
|
|
method.__name__,
|
|
ovld.rtype,
|
|
ovld.args,
|
|
static=static,
|
|
has_overloads=len(overloads) > 1,
|
|
)
|
|
|
|
if method.__name__ in BOOL_METHODS:
|
|
pmethod.rtype = Type("bool")
|
|
|
|
cmethods.append(pmethod)
|
|
|
|
pmethods.extend(cmethods)
|
|
|
|
# Retrieve the enumerations and classes:
|
|
inner_classes = [
|
|
ic[1]
|
|
for ic in all_attrs
|
|
if isinstance(ic[1], type)
|
|
and ic[1].__name__ != "class"
|
|
and ic[0] not in base_attrs
|
|
]
|
|
pinner_classes = [
|
|
register.make_object("{}.{}".format(fullname, ic.__name__), ic)
|
|
for ic in inner_classes
|
|
]
|
|
|
|
# Retrieve the attributes:
|
|
constants = []
|
|
properties = []
|
|
for name, attr in all_attrs:
|
|
if callable(attr) or isinstance(attr, type):
|
|
continue
|
|
|
|
# Maybe we should check an override here (e.g., different value for a constant):
|
|
if name in base_attrs:
|
|
continue
|
|
|
|
if isinstance(attr, property):
|
|
properties.append(Property(name, Type("Any"), attr.fset is None))
|
|
elif not hasattr(attr, "__name__"):
|
|
constants.append(Constant(name, Type(type(attr).__name__), attr))
|
|
|
|
direct_bases_s = []
|
|
for c in e.__bases__:
|
|
if c.__module__ != "Boost.Python":
|
|
direct_bases_s.append(c.__name__)
|
|
|
|
# Forcing QWidget base for XWidget classes since these do not show up
|
|
# and we use a trick:
|
|
if e.__name__.endswith("Widget"):
|
|
logger.info(
|
|
"Forcing base {} for class {}.".format(
|
|
"PyQt5.QtWidgets.QWidget", e.__name__
|
|
)
|
|
)
|
|
direct_bases_s.append("PyQt5.QtWidgets.QWidget")
|
|
|
|
# Check if this is an inner class:
|
|
parts = fullname.split(".")
|
|
outer_class: Optional[str] = None
|
|
if len(parts) > 1:
|
|
outer_class = parts[-2]
|
|
|
|
return Class(
|
|
e.__name__,
|
|
direct_bases_s,
|
|
pmethods,
|
|
inner_classes=pinner_classes,
|
|
outer_class=outer_class,
|
|
properties=properties,
|
|
constants=constants,
|
|
)
|