From 5cf2745c64239b47f288856bea607513edae80f2 Mon Sep 17 00:00:00 2001 From: baldurk Date: Mon, 14 Sep 2026 11:58:52 +0100 Subject: [PATCH] Add a script to make sure that python stubs are sensible --- docs/check_stubs.py | 132 +++++++++++++++++++++++++++++++++++++++ docs/regenerate_stubs.py | 51 ++++++++------- docs/stubgen.py | 37 ++++++----- 3 files changed, 183 insertions(+), 37 deletions(-) create mode 100644 docs/check_stubs.py diff --git a/docs/check_stubs.py b/docs/check_stubs.py new file mode 100644 index 000000000..0e7d59ce4 --- /dev/null +++ b/docs/check_stubs.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +import os +import sys +import struct +from types import ModuleType + +if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [path/to/tmp/folder]") + sys.exit(1) + +destpath = os.path.join(os.path.realpath(sys.argv[1]), "rdstubs") + +os.makedirs(destpath, exist_ok=True) + +# do everything relative to this script +docsdir = os.path.realpath(os.path.dirname(__file__)) + +# path to module libraries for windows +if struct.calcsize("P") == 8: + binpath = os.path.abspath(os.path.join(docsdir, "../x64/")) +else: + binpath = os.path.abspath(os.path.join(docsdir, "../Win32/")) + +# Prioritise release over development builds +sys.path.insert(0, os.path.abspath(binpath + "/Development/pymodules")) +sys.path.insert(0, os.path.abspath(binpath + "/Release/pymodules")) + +# Add the build paths to PATH so renderdoc.dll can be located +os.environ["PATH"] += os.pathsep + os.path.abspath(binpath + "/Development/") +os.environ["PATH"] += os.pathsep + os.path.abspath(binpath + "/Release/") + +if sys.platform == "win32" and sys.version_info[1] >= 8: + if os.path.exists(binpath + "/Release/"): + os.add_dll_directory(binpath + "/Release/") + if os.path.exists(binpath + "/Development/"): + os.add_dll_directory(binpath + "/Development/") + +# path to module libraries for linux +sys.path.insert(0, os.path.abspath(os.path.join(docsdir, "../build/lib"))) + +import shutil + +shutil.rmtree(destpath) + +import renderdoc as rd_module +import qrenderdoc as qrd_module +import stubgen + +stubgen.gen(rd_module, destpath) +stubgen.gen(qrd_module, destpath) + +sys.path.insert(0, os.path.dirname(destpath)) + +from rdstubs import renderdoc as rd_stub # type: ignore +from rdstubs import qrenderdoc as qrd_stub # type: ignore + +import inspect, enum + +for mod, stub in [(rd_module, rd_stub), (qrd_module, qrd_stub)]: + stub: ModuleType + + # we enforce in naming checks that top level items like classes and functions do not contain a _ + mod_items = [x for x in dir(mod) if "_" not in x] + stub_items = [x for x in dir(stub) if "_" not in x] + + missing = set(mod_items) - set(stub_items) + + if len(missing) > 0: + raise RuntimeError( + f"Some module items do not have a corresponding stub item: {list(missing)}" + ) + + for name, mod_item, stub_item in [ + (x, getattr(mod, x), getattr(stub, x)) for x in mod_items + ]: + if inspect.isclass(mod_item): + if issubclass(mod_item, enum.Enum): + members = mod_item.__members__ + for val in members.keys(): + if not hasattr(stub_item, val): + raise RuntimeError( + f"{mod.__name__}.{name} stub for enum is missing {val}" + ) + if getattr(stub_item, val) != members[val]: + raise RuntimeError( + f"{mod.__name__}.{name} stub for enum has wrong value: {members[val]} vs {getattr(stub_item, val)}" + ) + else: + swig_vars = [ + "disown", + "acquire", + "own", + "append", + "next", + "this", + "thisown", + ] + members = [ + x for x in dir(mod_item) if x[0] != "_" and x not in swig_vars + ] + + for member_name in members: + if not hasattr(stub_item, member_name): + raise RuntimeError( + f"{mod.__name__}.{name} stub for class is missing {member_name}" + ) + + mod_member = getattr(mod_item, member_name) + stub_member = getattr(stub_item, member_name) + + if inspect.ismethoddescriptor(mod_member) or inspect.isbuiltin( + mod_member + ): + if not inspect.isfunction(stub_member): + raise RuntimeError( + f"{mod.__name__}.{name}.{member_name} stub is not a member function" + ) + elif inspect.isgetsetdescriptor(mod_member): + if not isinstance(stub_member, property): + raise RuntimeError( + f"{mod.__name__}.{name}.{member_name} stub is not a property" + ) + elif isinstance(mod_member, int): + if mod_member != stub_member: + raise RuntimeError( + f"{mod.__name__}.{name}.{member_name} stub has wrong value: {mod_member} vs {stub_member}" + ) + +shutil.rmtree(destpath) + +print("Generated stubs look OK") \ No newline at end of file diff --git a/docs/regenerate_stubs.py b/docs/regenerate_stubs.py index e3a34150b..bf5710ec8 100644 --- a/docs/regenerate_stubs.py +++ b/docs/regenerate_stubs.py @@ -1,48 +1,57 @@ #!/usr/bin/env python3 +import argparse import os import sys import struct -if len(sys.argv) < 2: - print(f"Usage: {sys.argv[0]} [path/to/stubs/folder]") - sys.exit(1) +parser = argparse.ArgumentParser() +parser.add_argument("stubspath", help="path/to/stubs/folder") +parser.add_argument( + "-v", + "--verbose", + help="Enable debugger mode, exceptions are not caught by the framework.", + action="store_true", +) +parser.epilog +args = parser.parse_args() -destpath = os.path.realpath(sys.argv[1]) +destpath = os.path.realpath(args.stubspath) -print(f"Writing python stubs to {destpath}") +if args.verbose: + print(f"Writing python stubs to {destpath}") os.makedirs(destpath, exist_ok=True) - + # do everything relative to this script docsdir = os.path.realpath(os.path.dirname(__file__)) - + # path to module libraries for windows if struct.calcsize("P") == 8: - binpath = os.path.abspath(os.path.join(docsdir, '../x64/')) + binpath = os.path.abspath(os.path.join(docsdir, "../x64/")) else: - binpath = os.path.abspath(os.path.join(docsdir, '../Win32/')) + binpath = os.path.abspath(os.path.join(docsdir, "../Win32/")) # Prioritise release over development builds -sys.path.insert(0, os.path.abspath(binpath + '/Development/pymodules')) -sys.path.insert(0, os.path.abspath(binpath + '/Release/pymodules')) +sys.path.insert(0, os.path.abspath(binpath + "/Development/pymodules")) +sys.path.insert(0, os.path.abspath(binpath + "/Release/pymodules")) # Add the build paths to PATH so renderdoc.dll can be located -os.environ["PATH"] += os.pathsep + os.path.abspath(binpath + '/Development/') -os.environ["PATH"] += os.pathsep + os.path.abspath(binpath + '/Release/') +os.environ["PATH"] += os.pathsep + os.path.abspath(binpath + "/Development/") +os.environ["PATH"] += os.pathsep + os.path.abspath(binpath + "/Release/") -if sys.platform == 'win32' and sys.version_info[1] >= 8: - if os.path.exists(binpath + '/Release/'): - os.add_dll_directory(binpath + '/Release/') - if os.path.exists(binpath + '/Development/'): - os.add_dll_directory(binpath + '/Development/') +if sys.platform == "win32" and sys.version_info[1] >= 8: + if os.path.exists(binpath + "/Release/"): + os.add_dll_directory(binpath + "/Release/") + if os.path.exists(binpath + "/Development/"): + os.add_dll_directory(binpath + "/Development/") # path to module libraries for linux -sys.path.insert(0, os.path.abspath(os.path.join(docsdir, '../build/lib'))) +sys.path.insert(0, os.path.abspath(os.path.join(docsdir, "../build/lib"))) import renderdoc import qrenderdoc import stubgen -stubgen.gen(renderdoc, destpath) -stubgen.gen(qrenderdoc, destpath) +stubgen.gen(renderdoc, destpath, verbose=args.verbose) +stubgen.gen(qrenderdoc, destpath, verbose=args.verbose) diff --git a/docs/stubgen.py b/docs/stubgen.py index 9f9e139f3..edb1d5902 100644 --- a/docs/stubgen.py +++ b/docs/stubgen.py @@ -17,6 +17,15 @@ if sys.platform == "msys": nl = "\r\n" +verbose_prints = False + + +def log(s: str): + global verbose_prints + if verbose_prints: + print(s) + + # RenderDoc/swig specific filtering def shouldskip(name) -> bool: if name.startswith("Swig") or name.startswith("SWIG"): @@ -355,12 +364,8 @@ def gen_function(file: Stream, class_parent: Any, func: Callable): file.indent() file.println('"""') docstring = func.__doc__.strip() - i = 0 + # trim any auto-generated function signatures while docstring.startswith(f"{func.__name__}("): - i += 1 - if i > 100: - print(docstring) - # trim any auto-generated function signatures try: docstring = docstring[docstring.index("\n") :].strip() except ValueError: @@ -622,8 +627,10 @@ def gen_class(file: Stream, class_obj: Type): dependencies[class_obj.__name__] = deps -def gen(module: types.ModuleType, destpath: str): - global dependencies, dummy_types, fwd_ref +def gen(module: types.ModuleType, destpath: str, verbose=False): + global dependencies, dummy_types, fwd_ref, verbose_prints + + verbose_prints = verbose begin = time.time() @@ -633,11 +640,11 @@ def gen(module: types.ModuleType, destpath: str): output_basepath = os.path.join(destpath, module.__name__) if __file__ in dir(module): - print( + log( f"Generating stubs for {module.__name__} from {module.__file__}, writing to {output_basepath}" ) else: - print(f"Generating stubs for {module.__name__}, writing to {output_basepath}") + log(f"Generating stubs for {module.__name__}, writing to {output_basepath}") shutil.rmtree(output_basepath, ignore_errors=True) os.makedirs(output_basepath, exist_ok=True) @@ -763,7 +770,7 @@ def gen(module: types.ModuleType, destpath: str): if len(group) > 1: group.sort() group_list = ", ".join(group) - print(f"Circular dependency detected: {group_list}") + log(f"Circular dependency detected: {group_list}") filename = f"{module.__name__}_circular{circular}" circular += 1 @@ -802,10 +809,8 @@ def gen(module: types.ModuleType, destpath: str): dummy_types.append(local) if len(dummy_types) > 0: - print( - f"WARNING: Some types could not be found, and were mapped to dummy types:" - ) - print(" " + ", ".join(list(dummy_types))) + log(f"WARNING: Some types could not be found, and were mapped to dummy types:") + log(" " + ", ".join(list(dummy_types))) for dummy in dummy_types: dependencies[dummy] = set() @@ -937,5 +942,5 @@ def gen(module: types.ModuleType, destpath: str): init.merge(stream) end = time.time() - print(f"Generated in {int((end-begin)*1000)} ms") - print("") + log(f"Generated in {int((end-begin)*1000)} ms") + log("")