Add type hints to python for class constructors

* We enforce default and copy constructors where possible, this isn't very
  pythonic but we can't use copy.deepcopy on our types.
* The structs that have specialised constructors e.g. FloatVector or Subresource
  also have those documented though we have no way to enforce it.
This commit is contained in:
baldurk
2026-08-13 17:46:47 +01:00
parent 950568fb8f
commit 5e462c5ebe
20 changed files with 1336 additions and 225 deletions
+63 -4
View File
@@ -390,15 +390,32 @@ def gen_class(file: Stream, class_obj: Type):
bases_string = ", ".join([b.__name__ for b in bases])
file.println(f"class {class_obj.__name__}({bases_string}):")
file.indent()
if class_obj.__doc__ is None:
raise ValueError("Unexpected None docstring")
lines = class_obj.__doc__.strip().splitlines()
constructors: List[List[Tuple[str, str]]] = []
while lines[0].strip().startswith(class_obj.__name__ + "("):
args = lines[0].strip()
start = args.find("(")
args = args[start + 1 : -1]
annot_split = lambda arg: (arg.split(":")[0].strip(), arg.split(":")[1].strip())
constructors.append([annot_split(arg) for arg in args.split(",") if arg != ""])
del lines[0]
if len(constructors) > 0:
file.println("from typing import overload")
file.println("")
class_doc = ("\n".join(lines)).strip()
file.println(f"class {class_obj.__name__}({bases_string}):")
file.indent()
file.println("# Original docstring")
file.println('"""')
file.printlines(class_obj.__doc__)
file.printlines(class_doc)
file.println('"""')
file.println("")
file.println("")
@@ -453,6 +470,47 @@ def gen_class(file: Stream, class_obj: Type):
file.println('"""')
file.println("")
else:
for ctor in constructors:
ctor_def = f"def __init__(self, "
for param, annot in ctor:
if annot != class_obj.__name__:
# a default value comes in with the annotation,
# we don't split it out otherwise so strip it here
type_str = annot.split("=")[0].strip()
add_dependencies(class_obj, deps, type_str)
ctor_def += f"{param}: {annot}, "
else:
# escape any self-references in ''s
ctor_def += f"{param}: '{annot}', "
ctor_def = ctor_def[:-2] + "):"
file.println("@overload")
file.println(ctor_def)
file.indent()
file.println('"""')
# copy constructors have only one parameter of our own type
if len(ctor) == 1 and ctor[0][1] == class_obj.__name__:
file.println(
f"Construct a new {class_obj.__name__} with a deep copy of the input."
)
# default constructors have no parameters
elif ctor == []:
file.println(
f"Construct a new default-initialised {class_obj.__name__}."
)
# more complex value constructor with parameters
else:
file.println(
f"Construct a new {class_obj.__name__} using provided values."
)
file.println('"""')
file.println("pass")
file.dedent()
file.println("")
if len(constructors) > 0:
file.println("")
for item_name in class_obj.__dict__.keys():
if item_name.startswith("__"):
continue
@@ -506,6 +564,7 @@ def gen_class(file: Stream, class_obj: Type):
file.println('"""')
file.printlines(doc)
file.println('"""')
file.println("")
else:
raise ValueError(
f"Unknown type of member {item_name} in {class_obj.__name__}"
+40 -11
View File
@@ -113,14 +113,14 @@ def make_c_typeval(ret: str, pattern: bool, typelist: List[str]):
elif ret == 'Tuple[str,str]': # special case
ret = 'rdcstrpair'
elif ret[0:9] == 'Callable[':
ret = '(std::function<void\(\)>|[A-Za-z_]+Callback)' if pattern else 'std::function/NamedCallback'
ret = r'(std::function<void\(\)>|[A-Za-z_]+Callback)' if pattern else 'std::function/NamedCallback'
elif ret[0:5] == 'List[':
inner = make_c_typeval(ret[5:-1], pattern, typelist)
ret = '(const )?rdcarray<{}> ?[&*]?'.format(inner) if pattern else 'rdcarray<{}>'.format(inner)
elif ret[0:6] == 'Tuple[':
inners = [make_c_typeval(i.strip(), pattern, typelist) for i in ret[6:-1].split(',')]
if pattern:
inner = ',\s*'.join(inners)
inner = r',\s*'.join(inners)
else:
inner = ', '.join(inners)
@@ -166,7 +166,7 @@ def check_function(parent_name, objname, obj, source, global_func, typelist):
default_val = ''
for p in params:
if len(funcargs[0]) > 0:
funcargs[0] += ',\s*'
funcargs[0] += r',\s*'
funcargs[1] += ', '
default_val = p[2].lstrip()
@@ -177,7 +177,7 @@ def check_function(parent_name, objname, obj, source, global_func, typelist):
funcargs[1] += make_c_typeval(p[0], False, typelist) + ' ' + p[1]
if default_val != "":
funcargs[0] += f"\s*=\s*{default_val}"
funcargs[0] += f"\\s*=\\s*{default_val}"
funcargs[1] += f" = {default_val}"
result = RTYPE_PATTERN.search(docstring)
@@ -190,7 +190,7 @@ def check_function(parent_name, objname, obj, source, global_func, typelist):
if global_func:
global_pattern = '(RENDERDOC_CC\s*RENDERDOC_)?'
pattern = '(?s){} ?{}{}\(\s*{}\)'.format(make_c_typeval(ret, True, typelist), global_pattern, objname, funcargs[0])
pattern = r'(?s){} ?{}{}\(\s*{}\)'.format(make_c_typeval(ret, True, typelist), global_pattern, objname, funcargs[0])
clean = '{} {}({})'.format(make_c_typeval(ret, False, typelist), objname, funcargs[1])
match = re.search(pattern, source, re.MULTILINE | re.DOTALL)
@@ -198,7 +198,7 @@ def check_function(parent_name, objname, obj, source, global_func, typelist):
pattern2 = None
# global functions returning strings can't return an rdcstr, they have to return const char *
if match is None and ret == 'str':
pattern2 = '(?s)const char \*{}{}\(\s*{}\)'.format(global_pattern, objname, funcargs[0])
pattern2 = r'(?s)const char \*{}{}\(\s*{}\)'.format(global_pattern, objname, funcargs[0])
match = re.search(pattern2, source, re.MULTILINE | re.DOTALL)
if match is None:
@@ -291,8 +291,8 @@ for mod_name in check_mods:
print("Checking class {}".format(qualname))
# Grab the source to just this class to search in
source = re.search('(struct|class|union) I?' + objname + '(\n|\s*:[^A-Za-z][\s:a-zA-Z]*\n)\{.*?^}', headers, re.MULTILINE | re.DOTALL)
source = re.search('(struct|class|union) I?' + objname + r'(\n|\s*:[^A-Za-z][\s:a-zA-Z]*\n)\{.*?^}', headers, re.MULTILINE | re.DOTALL)
namespace = None
if source is None and objname[0:2] in ['VK', 'GL']:
@@ -310,13 +310,19 @@ for mod_name in check_mods:
namespace = namespace.group(0)
if source is None and namespace is not None:
source = re.search('(struct|class|union) I?' + objname + '[^{]*\{.*?^}', namespace, re.MULTILINE | re.DOTALL)
source = re.search('(struct|class|union) I?' + objname + r'[^{]*\{.*?^}', namespace, re.MULTILINE | re.DOTALL)
source = source.group(0)
instance = None
copyable = False
try:
instance = obj()
try:
dupe_instance = obj(instance)
copyable = True
except NotImplementedError:
pass
except TypeError:
pass
@@ -328,6 +334,29 @@ for mod_name in check_mods:
instance_warned = False
# for types that we can create, we expect by default to
# see a default constructor and a copy constructor,
# unless we see a note that the type is not copyable
if instance is not None:
lines = docstring.strip().splitlines()
if lines[0].strip() != f"{obj.__name__}()":
count += 1
print(
f"Error {count:3}: {obj.__name__} can be created, "
"expect default constructor as first real line of its docstring."
)
elif (
copyable
and lines[1].strip() != f"{obj.__name__}(other: {obj.__name__})"
):
count += 1
print(
f"Error {count:3}: {obj.__name__} can be copied, "
"expect copy constructor as second entry in its docstring:\n"
f"Actual > {lines[1]}\n"
f"Expected > {obj.__name__}(other: {obj.__name__})"
)
for member_name in obj.__dict__.keys():
if '__' in member_name or member_name in ['this', 'thisown']:
continue
@@ -407,7 +436,7 @@ for mod_name in check_mods:
count += 1
print("Error {:3}: {}.{} is missing :type: declaration, should be {}".format(count, qualname, member_name, type_name))
else:
type_decl = re.sub('Tuple\[.*\]', 'tuple', type_decl)
type_decl = re.sub(r'Tuple\[.*\]', 'tuple', type_decl)
if type_decl != type_name:
count += 1
print("Error {:3}: {}.{} has wrong :type: declaration {}, should be {}".format(count, qualname, member_name, type_decl, type_name))