Files
geMoldInsight/venv/lib/python3.11/site-packages/vtkmodules/generate_pyi.py
T
2026-02-12 23:27:48 +08:00

663 lines
22 KiB
Python

#!/usr/bin/env python
"""
This program will generate .pyi files for all the VTK modules
in the "vtkmodules" package (or whichever package you specify).
These files are used for type checking and autocompletion in
some Python IDEs.
The VTK modules must be in Python's path when you run this script.
Options are as follows:
-p PACKAGE The package to generate .pyi files for [vtkmodules]
-o OUTPUT The output directory [default is the package directory]
-e EXT The file suffix [.pyi]
-i IMPORTER The static module importer (for static builds only)
-h HELP
With no arguments, the script runs with the defaults (the .pyi files
are put inside the existing vtkmodules package). This is equivalent
to the following:
python -m vtkmodules.generate_pyi -p vtkmodules
To put the pyi files somewhere else, perhaps with a different suffix:
python -m vtkmodules.generate_pyi -o /path/to/vtkmodules -e .pyi
To generate pyi files for just one or two modules:
python -m vtkmodules.generate_pyi -p vtkmodules vtkCommonCore vtkCommonDataModel
To generate pyi files for your own modules in your own package:
python -m vtkmodules.generate_pyi -p mypackage mymodule [mymodule2 ...]
"""
from vtkmodules.vtkCommonCore import vtkObjectBase, vtkSOADataArrayTemplate
from keyword import iskeyword
import sys
import os
import re
import ast
import argparse
import builtins
import inspect
import importlib.util
# ==== Cancel any module overrides ====
import vtkmodules
vtkmodules.MODULE_MAPPER = {}
# ==== For type inspection ====
# list expected non-vtk type names
types = set()
for m,o in builtins.__dict__.items():
if isinstance(o, type):
types.add(m)
for m in ['Any', 'Buffer', 'Callback', 'None', 'Pointer', 'Template', 'Union']:
types.add(m)
# basic type checking methods
ismethod = inspect.isroutine
isclass = inspect.isclass
# VTK methods have a special type
vtkmethod = type(vtkObjectBase.IsA)
template = type(vtkSOADataArrayTemplate)
def isvtkmethod(m):
"""Check for VTK's custom method descriptor"""
return (type(m) == vtkmethod)
def isnamespace(m):
"""Check for namespaces within a module"""
# until vtkmodules.vtkCommonCore.namespace is directly accessible
return (str(type(m)) == "<class 'vtkmodules.vtkCommonCore.namespace'>")
def isenum(m):
"""Check for enums (currently derived from int)"""
return (isclass(m) and issubclass(m, int))
def typename(o):
"""Generate a typename that can be used for annotation."""
if o is None:
return "None"
elif type(o) == template:
return "Template"
else:
return type(o).__name__
def typename_forward(o):
"""Generate a typename, or if necessary, a forward reference."""
name = typename(o)
if name not in types:
# do forward reference by adding quotes
name = '\'' + name + '\''
return name
# ==== For the topological sort ====
class Graph:
"""A graph for topological sorting."""
def __init__(self):
self.nodes = {}
def __getitem__(self, name):
return self.nodes[name]
def __setitem__(self, name, node):
self.nodes[name] = node
class Node:
"""A node for the graph."""
def __init__(self, o, d):
self.obj = o
self.deps = d
def build_graph(d):
"""Build a graph from a module's dictionary."""
graph = Graph()
items = sorted(d.items())
for m,o in items:
if isclass(o):
if m == o.__name__:
# a class definition
bases = [b.__name__ for b in o.__bases__]
graph[m] = Node(o, bases)
else:
# a class alias
graph[m] = Node(o, [o.__name__])
elif ismethod(o):
graph[m] = Node(o, [])
else:
graph[m] = Node(o, [typename(o)])
return graph
def sorted_graph_helper(graph, m, visited, items):
"""Helper for topological sorting."""
visited.add(m)
try:
node = graph[m]
except KeyError:
return
for dep in node.deps:
if dep not in visited:
sorted_graph_helper(graph, dep, visited, items)
items.append((m, node.obj))
def sorted_graph(graph):
"""Sort a graph and return the sorted items."""
items = []
visited = set()
for m in graph.nodes:
if m not in visited:
sorted_graph_helper(graph, m, visited, items)
return items
def topologically_sorted_items(d):
"""Return the items from a module's dictionary, topologically sorted."""
return sorted_graph(build_graph(d))
# ==== For parsing docstrings ====
# regular expressions for parsing
string = re.compile(r"""("([^\\"]|\\.)*"|'([^\\']|\\.)*')""")
identifier = re.compile(r"""([A-Za-z_]([A-Za-z0-9_]|[.][A-Za-z_])*)""")
indent = re.compile(r"[ \t]+(?=\S)")
has_self = re.compile(r"[(]self[,)]")
# important characters for rapidly parsing code
keychar = re.compile(r"[\'\"{}\[\]()\n]")
def parse_error(message, text, begin, pos):
"""Print a parse error, syntax or otherwise.
"""
end = text.find('\n', pos)
if end == -1:
end = len(text)
sys.stderr.write("Error: " + message + ":\n")
sys.stderr.write(text[begin:end] + "\n");
sys.stderr.write('-' * (pos - begin) + "^\n")
def annotation_text(a, text, is_return):
"""Return the new text to be used for an annotation.
"""
if isinstance(a, ast.Name):
name = a.id
if name not in types:
# quote the type, in case it isn't yet defined
text = '\'' + name + '\''
elif isinstance(a, (ast.Tuple, ast.List)):
size = len(a.elts)
e = a.elts[0]
offset = a.col_offset
old_name = text[e.col_offset - offset:e.end_col_offset - offset]
name = annotation_text(e, old_name, is_return)
if is_return:
# use concrete types for return values
if isinstance(a, ast.Tuple):
text = 'Tuple[' + ', '.join([name]*size) + ']'
else:
text = 'List[' + name + ']'
else:
# use generic sequence types for args
if isinstance(a, ast.Tuple):
text = 'Sequence[' + name + ']'
else:
text = 'MutableSequence[' + name + ']'
return text
def fix_annotations(signature):
"""Fix the annotations in a method definition.
The signature must be a single-line function def, no decorators.
"""
# get the FunctionDef object from the parse tree
definition = ast.parse(signature).body[0]
annotations = [arg.annotation for arg in definition.args.args]
return_i = len(annotations) # index of annotation for return
annotations.append(definition.returns)
# create a list of changes to apply to the annotations
changes = []
for i,a in enumerate(annotations):
if a is not None:
old_text = signature[a.col_offset:a.end_col_offset]
text = annotation_text(a, old_text, (i == return_i))
if text != old_text:
changes.append((a.col_offset, a.end_col_offset, text))
# apply changes to generate a new signature
if changes:
newsig = ""
lastpos = 0
for begin,end,text in changes:
newsig += signature[lastpos:begin]
newsig += text
lastpos = end
newsig += signature[lastpos:]
signature = newsig
return signature
def push_signature(o, l, signature):
"""Process a method signature and add it to the list.
"""
# eliminate newlines and indents
signature = re.sub(r"\s+", " ", signature)
# no space after opening delimiter or ':' or '='
signature = re.sub(r"([({\[:=]) ", "\\1", signature)
if signature.startswith('C++:'):
# the C++ method signatures are unused
pass
elif signature.startswith(o.__name__ + "("):
# make it into a python method definition
signature = "def " + signature + ': ...'
if sys.hexversion >= 0x3080000:
# XXX(Python 3.8) uses ast features from 3.8
signature = fix_annotations(signature)
if signature not in l:
l.append(signature)
def get_signatures(o):
"""Return a list of method signatures found in the docstring.
"""
doc = o.__doc__
signatures = [] # output method signatures
if doc is None:
return signatures
# variables used for parsing the docstrings
begin = 0 # beginning of current signature
pos = 0 # current position in docstring
delim_stack = [] # keep track of bracket depth
# loop through docstring using longest strides possible
# (this will go line-by-line or until first ( ) { } [ ] " ')
while pos < len(doc):
# look for the next "character of interest" in docstring
match = keychar.search(doc, pos)
# did we find a match before the end of docstring?
if match:
# get new position
pos,end = match.span()
# take different action, depending on char
c = match.group()
if c in '\"\'':
# skip over a string literal
m = string.match(doc, pos)
if m:
pos,end = m.span()
else:
parse_error("Unterminated string", doc, begin, pos)
break
elif c in '{[(':
# descend into a bracketed expression (push stack)
delim_stack.append({'{':'}','[':']','(':')'}[c])
elif c in '}])':
# ascend out of a bracketed expression (pop stack)
if not delim_stack or c != delim_stack.pop():
parse_error("Unmatched bracket", doc, begin, pos)
break
elif c == '\n' and not (delim_stack or indent.match(doc, end)):
# a newline not followed by an indent marks end of signature,
# except for within brackets
signature = doc[begin:pos].strip()
if signature:
push_signature(o, signatures, signature)
begin = end
else:
# blank line means no more signatures in docstring
break
else:
# reached the end of the docstring
end = len(doc)
if not delim_stack:
signature = doc[begin:pos].strip()
if signature:
push_signature(o, signatures, signature)
else:
parse_error("Unmatched bracket", doc, begin, pos)
break
# advance position within docstring and return to head of loop
pos = end
return signatures
def get_constructors(c):
"""Get constructors from the class documentation.
"""
constructors = []
name = c.__name__
doc = c.__doc__
if not doc or not doc.startswith(name + "("):
return constructors
signatures = get_signatures(c)
for signature in signatures:
if signature.startswith("def " + name + "("):
signature = re.sub("-> \'?" + name + "\'?", "-> None", signature)
if signature.startswith("def " + name + "()"):
constructors.append(re.sub(name + r"\(", "__init__(self", signature, count=1))
else:
constructors.append(re.sub(name + r"\(", "__init__(self, ", signature, count=1))
return constructors
def handle_static(o, signature):
"""If method has no "self", add @static decorator."""
if isvtkmethod(o) and not has_self.search(signature):
return "@staticmethod\n" + signature
else:
return signature
def add_indent(s, indent):
"""Add the given indent before every line in the string.
"""
return indent + re.sub(r"\n(?=([^\n]))", "\n" + indent, s)
def namespace_pyi(c, mod):
"""Fake a namespace by creating a dummy class.
"""
base = "namespace"
if mod.__name__ != 'vtkmodules.vtkCommonCore':
base = 'vtkmodules.vtkCommonCore.' + base
out = "class " + c.__name__ + "(" + base + "):\n"
count = 0
# do all nested classes (these will be enum types)
items = topologically_sorted_items(c.__dict__)
others = []
for m,o in items:
if isenum(o) and m == o.__name__:
out += add_indent(class_pyi(o), " ")
count += 1
else:
others.append((m, o))
# do all constants
items = others
others = []
for m,o in items:
if not m.startswith("__") and not ismethod(o) and not isclass(o):
out += " " + m + ":" + typename_forward(o) + "\n"
count += 1
else:
others.append((m,o))
if count == 0:
out = out[0:-1] + " ...\n"
return out
def class_pyi(c):
"""Generate all the method stubs for a class.
"""
bases = []
for b in c.__bases__:
if b.__module__ in (c.__module__, 'builtins'):
bases.append(b.__name__)
else:
bases.append(b.__module__ + "." + b.__name__)
out = "class " + c.__name__ + "(" + ", ".join(bases) + "):\n"
count = 0
# do all nested classes (these are usually enum types)
items = topologically_sorted_items(c.__dict__)
others = []
for m,o in items:
if isclass(o) and m == o.__name__:
out += add_indent(class_pyi(o), " ")
count += 1
else:
others.append((m, o))
# do all constants
items = others
others = []
for m,o in items:
if not m.startswith("__") and not ismethod(o) and not isclass(o) and not iskeyword(m):
out += " " + m + ":" + typename_forward(o) + "\n"
count += 1
else:
others.append((m,o))
# do the __init__ methods
constructors = get_constructors(c)
if len(constructors) == 0:
if hasattr(c, "__init__") and issubclass(c, vtkObjectBase):
out += " def __init__(self, **properties:Any) -> None: ...\n"
count += 1
else:
count += 1
if len(constructors) == 1:
out += add_indent(constructors[0], " ") + "\n"
else:
for overload in constructors:
out += add_indent("@overload\n" + overload, " ") + "\n"
# do the methods
items = others
others = []
for m,o in items:
if ismethod(o):
signatures = get_signatures(o)
if len(signatures) == 0:
continue
count += 1
if len(signatures) == 1:
signature = handle_static(o, signatures[0])
out += add_indent(signature, " ") + "\n"
continue
for overload in signatures:
signature = handle_static(o, overload)
out += add_indent("@overload\n" + signature, " ") + "\n"
else:
others.append((m, o))
if count == 0:
out = out[0:-1] + " ...\n"
return out
def module_pyi(mod, output):
"""Generate the contents of a .pyi file for a VTK module.
"""
# needed stuff from typing module
output.write("from typing import overload, Any, Callable, TypeVar, Union\n")
output.write("from typing import Tuple, List, Sequence, MutableSequence\n")
output.write("\n")
output.write("Callback = Union[Callable[..., None], None]\n")
output.write("Buffer = TypeVar('Buffer')\n")
output.write("Pointer = TypeVar('Pointer')\n")
output.write("Template = TypeVar('Template')\n")
output.write("\n")
if mod.__name__ == 'vtkmodules.vtkCommonCore':
# dummy superclass for namespaces
output.write("class namespace: pass\n")
output.write("\n")
# all the modules this module depends on
depends = set(['vtkmodules.vtkCommonCore'])
for m,o in mod.__dict__.items():
if isclass(o) and m == o.__name__:
for base in o.__bases__:
depends.add(base.__module__)
depends.discard(mod.__name__)
depends.discard("builtins")
for depend in sorted(depends):
output.write("import " + depend + "\n")
if depends:
output.write("\n")
# sort the dict according to dependency
items = topologically_sorted_items(mod.__dict__)
# do all namespaces
others = []
for m,o in items:
if isnamespace(o) and m == o.__name__:
output.write(namespace_pyi(o, mod))
output.write("\n")
else:
others.append((m, o))
# do all enum types
items = others
others = []
for m,o in items:
if isenum(o) and m == o.__name__:
output.write(class_pyi(o))
output.write("\n")
else:
others.append((m, o))
# do all enum aliases
items = others
others = []
for m,o in items:
if isenum(o) and m != o.__name__:
output.write(m + " = " + o.__name__ + "\n")
else:
others.append((m, o))
# do all constants
items = others
others = []
for m,o in items:
if not m.startswith("__") and not ismethod(o) and not isclass(o):
output.write(m + ":" + typename_forward(o) + "\n")
else:
others.append((m,o))
if len(items) > len(others):
output.write("\n")
# do all classes
items = others
others = []
for m,o in items:
if isclass(o) and m == o.__name__:
output.write(class_pyi(o))
output.write("\n")
else:
others.append((m, o))
# do all class aliases
items = others
others = []
for m,o in items:
if isclass(o) and m != o.__name__:
output.write(m + " = " + o.__name__ + "\n")
else:
others.append((m, o))
def main(argv=sys.argv):
# for error messages etcetera
progname = os.path.basename(argv[0])
# parse the program arguments
parser = argparse.ArgumentParser(
prog=argv[0],
usage="python " + progname + " [-p package] [-o output_dir]",
description="A .pyi generator for the VTK python wrappers.")
parser.add_argument('-p', '--package', type=str, default="vtkmodules",
help="Package name [vtkmodules].")
parser.add_argument('-i', '--importer', type=str,
help="Static module importer [].")
parser.add_argument('-o', '--output', type=str,
help="Output directory [package directory].")
parser.add_argument('-e', '--ext', type=str, default=".pyi",
help="Output file suffix [.pyi].")
parser.add_argument('--test', action='count', default=0,
help="Test .pyi files instead of creating them.")
parser.add_argument('modules', type=str, nargs='*',
help="Modules to process [all].")
args = parser.parse_args(argv[1:])
# for convenience
packagename = args.package
modules = args.modules
basedir = args.output
ext = args.ext
# if static module importer is needed, it must be handled first
if args.importer:
if len(modules) == 0:
sys.stderr.write(progname + ": when using '-i', all modules " +
"in the package must be listed on the command line.\n")
return 1
# check that the modules aren't already present as builtins
# (we replace '.' separators with underscores for static importers)
module_exemplar = (packagename + '.' + modules[0]).replace('.', '_')
if module_exemplar not in sys.builtin_module_names:
importlib.import_module(args.importer)
# get information about the package
if basedir is None or len(modules) == 0:
mod = importlib.import_module(packagename)
if basedir is None:
filename = getattr(mod, '__file__', None)
if filename is None or os.path.basename(filename) != '__init__.py':
sys.stderr.write(progname + ": " + packagename + " has no __init__.py\n")
return 1
basedir = os.path.dirname(filename)
if len(modules) == 0:
for modname in mod.__all__:
# only generate .pyi files for the extension modules in __all__
try:
spec = importlib.util.find_spec(packagename + "." + modname)
except ValueError:
spec = None
if not errflag:
errflag = True
sys.stderr.write(progname + ": couldn't get loader for " + modname + "\n")
if spec is None:
continue
if not isinstance(spec.loader, importlib.machinery.ExtensionFileLoader):
continue
# the module is definitely an extension module
modules.append(modname)
# Give all PATH environment variable entries to add_dll_directory on Windows
# This enable third-party libraries like OpenXR loader's DLL to be found easily.
if os.name == "nt":
for p in os.environ.get("PATH").split(';'):
try:
os.add_dll_directory(p)
except Exception as e:
print(f"Warning: Failed to add {p} as DLL search directory: ${e}")
# iterate through the modules in the package
errflag = False
for modname in modules:
pyifile = os.path.join(basedir, modname + ext)
if args.test:
# test the syntax of the .pyi file
flags = ast.PyCF_TYPE_COMMENTS if sys.hexversion >= 0x3080000 else 0
with open(pyifile, 'r') as f:
compile(f.read(), pyifile, 'exec', flags)
else:
# generate the .pyi file for the module
mod = importlib.import_module(packagename + "." + modname)
with open(pyifile, "w") as f:
module_pyi(mod, f)
# add 'py.typed' to the package
with open(os.path.join(basedir, 'py.typed'), 'w') as f:
pass
if __name__ == '__main__':
result = main(sys.argv)
if result is not None:
sys.exit(result)