Files
paddlepaddle--paddle/tools/gen_tensor_stub.py
T
2026-07-13 12:40:42 +08:00

643 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import argparse
import importlib
import inspect
import logging
import re
import sys
import traceback
from dataclasses import dataclass
from functools import cached_property, lru_cache
from typing import TYPE_CHECKING, Any, Literal, TypeAlias
from typing_extensions import get_overloads
if TYPE_CHECKING:
from collections.abc import Callable
from types import ModuleType
logging.basicConfig(style="{", format="{message}", level=logging.INFO)
logger = logging.getLogger("Generating stub file for paddle.Tensor")
logger.setLevel(logging.INFO)
INDENT_SIZE = 4
INDENT = " " * INDENT_SIZE
MEANLESS_INDEX = -1
MemberType: TypeAlias = Literal[
"doc",
"attribute",
"method",
]
@dataclass
class Member:
id: int
name: str
type: MemberType
aliases: list[str]
decorators: list[str]
signature: str
doc: str | None
def add_alias(self, alias: str):
self.aliases.append(alias)
@lru_cache
def _slot_pattern(slot_name: str) -> re.Pattern:
return re.compile(
r"(?P<indent> *)#\s*annotation:\s*\$\{" + slot_name + r"\}"
)
@lru_cache
def create_builtin_annotation_renamer():
# NOTE(ooooo-create): Rename built-in types to avoid naming conflicts
builtin_types = ["int", "bool", "str", "float", "complex", "bytes"]
regex_string = "|".join([rf"\b{t}\b" for t in builtin_types])
regex = re.compile(regex_string)
def renamer(annotations):
if annotations is inspect.Signature.empty:
return annotations
return regex.sub(lambda m: f"_{m.group(0)}", annotations)
return renamer
def rename_builtin_annotation(annotation):
renamer = create_builtin_annotation_renamer()
return renamer(annotation)
class TensorGen:
def __init__(self, template: str = '', prefix: str = 'tensor'):
self._template = template
self._template_codes: list[tuple[int, int, str]] = []
self._prefix = prefix
def find_annotation_slot(self, slot_name: str) -> tuple[str, int, int]:
pattern = _slot_pattern(slot_name)
slot = []
for mo in pattern.finditer(self._template):
_indent = mo.group('indent')
_start_index, _end_index = mo.span()
slot.append((_indent, _start_index, _end_index))
assert len(slot) == 1, self._template
return slot[0]
@property
def tensor_docstring(self):
return self.find_annotation_slot(f'{self._prefix}_docstring')
@property
def tensor_attributes(self):
return self.find_annotation_slot(f'{self._prefix}_attributes')
@property
def tensor_methods(self):
return self.find_annotation_slot(f'{self._prefix}_methods')
@property
def tensor_alias(self):
return self.find_annotation_slot(f'{self._prefix}_alias')
@property
def template_codes(self) -> list[tuple[int, int, str]]:
return self._template_codes
@property
def template_begin(self) -> int:
return self._template.find(f'{self._prefix}_begin')
@property
def template_end(self) -> int:
return self._template.find(f'{self._prefix}_end')
def find_apis(self, api_name: str) -> list[dict[str, tuple[str, int, int]]]:
if self.template_begin < 0 or self.template_end < 0:
return []
pattern = re.compile(
r"(?P<indent> *)(?P<def_api>def "
+ api_name
+ r")(?P<signature>\(.*?\).*?:)(?P<docstring>.*?)(?P<ellipsis>\.{3})(?P<comment>[^\n]*#[^\n]*\n)?",
re.DOTALL,
)
api = []
for mo in pattern.finditer(self._template):
_start_index, _end_index = mo.span()
if not (
self.template_begin < _start_index < self.template_end
and self.template_begin < _end_index < self.template_end
):
continue
_indent = mo.group('indent')
_signature = mo.group('signature')
_docstring = mo.group('docstring')
_ellipsis = mo.group('ellipsis')
_comment = mo.group('comment')
_comment = '' if _comment is None else _comment
_start_indent, _ = mo.span('indent')
_start_docstring, _ = mo.span('docstring')
_, _end_ellipsis = mo.span('ellipsis')
_start_comment = _end_ellipsis
_end_comment = _start_comment + len(_comment)
assert _start_index == _start_indent
assert _end_comment == _end_index
_api = {
'indent': (_indent, MEANLESS_INDEX, MEANLESS_INDEX),
'signature': (_signature, MEANLESS_INDEX, MEANLESS_INDEX),
'docstring': (_docstring, _start_docstring, _end_comment),
'ellipsis': (_ellipsis, MEANLESS_INDEX, MEANLESS_INDEX),
'comment': (_comment, MEANLESS_INDEX, MEANLESS_INDEX),
}
api.append(_api)
return api
def insert_template(self, code: str, start: int, end: int) -> None:
if start != MEANLESS_INDEX and end != MEANLESS_INDEX:
self._template_codes.append((start, end, code))
def add_method(self, func: Member):
"""
1. insert docstring: tensor.prototype.pyi define the method without docstring
2. insert method: tensor.prototype.pyi NOT define the method
"""
methods = self.find_apis(func.name)
if methods:
# only use the last method
method = methods[-1]
# insert docstring if necessary
if not method['docstring'][0].strip():
doc = func.doc
if doc:
comment = method['comment'][0]
doc_start = method['docstring'][1]
doc_end = method['docstring'][2]
api_indent = method['indent'][0]
assert len(api_indent) % INDENT_SIZE == 0
_indent = api_indent + INDENT
_doc = '\n' # new line
_doc += f'{_indent}r"""\n'
_doc += with_indent(doc, len(_indent) // INDENT_SIZE)
_doc += "\n"
_doc += f'{_indent}"""\n'
_doc += f'{_indent}...\n'
_doc += f'{_indent}\n'
self.insert_template(comment + _doc, doc_start, doc_end)
else:
method_code = '\n'
for decorator in func.decorators:
method_code += f"@{decorator}\n"
method_code += f"def {func.signature}:\n"
# do NOT insert docs from overload methods,
# because we always add a plain method
if func.doc and func.decorators != ["overload"]:
method_code += f'{INDENT}r"""\n'
method_code += with_indent(func.doc, 1)
method_code += "\n"
method_code += f'{INDENT}"""\n'
method_code += f"{INDENT}...\n"
_indent, _, _end_index = self.tensor_methods
method_code = with_indent(method_code, len(_indent) // INDENT_SIZE)
self.insert_template(method_code, _end_index, _end_index)
def add_alias(self, alias: str, target: str):
_indent, _, _end_index = self.tensor_alias
aliases_code = "\n"
aliases_code += f"{_indent}{alias} = {target}"
self.insert_template(aliases_code, _end_index, _end_index)
def add_attribute(self, name: str, type_: str):
_indent, _, _end_index = self.tensor_attributes
attr_code = "\n"
attr_code += f"{_indent}{name}: {type_}"
self.insert_template(attr_code, _end_index, _end_index)
def add_doc(self, doc: str):
_indent, _, _end_index = self.tensor_docstring
docstring = "\n"
docstring += 'r"""\n'
docstring += doc
docstring += "\n"
docstring += '"""\n'
docstring = with_indent(docstring, len(_indent) // INDENT_SIZE)
self.insert_template(docstring, _end_index, _end_index)
@classmethod
def codegen(
cls, template: str, template_codes: list[tuple[int, int, str]]
) -> str:
header = (
'# This file is auto generated by `tools/gen_tensor_stub.py`.\n\n'
)
_template = []
start = 0
end = 0
for _start, _end, code in sorted(template_codes):
end = _start
_template.extend(
[
template[start:end],
code,
]
)
start = _end
_template.append(template[start:])
_content = header + ''.join(_template)
return _content
def is_inherited_member(name: str, cls: type) -> bool:
"""Check if the member is inherited from parent class"""
# keep magic methods
if name.startswith("__") and name.endswith("__"):
return False
if name in cls.__dict__:
return False
for base in cls.__bases__:
if name in base.__dict__:
return True
return any(is_inherited_member(name, base) for base in cls.__bases__)
def is_property(member: Any) -> bool:
"""Check if the member is a property"""
return isinstance(member, (property, cached_property))
def is_staticmethod(member: Any) -> bool:
"""Check if the member is a staticmethod"""
return isinstance(member, staticmethod)
def is_classmethod(member: Any) -> bool:
"""Check if the member is a classmethod"""
return isinstance(member, classmethod)
def process_lines(code: str, callback: Callable[[str], str]) -> str:
lines = code.splitlines()
end_with_newline = code.endswith("\n")
processed_lines: list[str] = []
for line in lines:
processed_lines.append(callback(line))
processed_code = "\n".join(processed_lines)
if end_with_newline:
processed_code += "\n"
return processed_code
def with_indent(code: str, level: int) -> str:
def add_indent_line(line: str) -> str:
if not line:
return line
return INDENT + line
def remove_indent_line(line: str) -> str:
if not line:
return line
elif line.startswith(INDENT):
return line[len(INDENT) :]
else:
return line
if level == 0:
return code
elif level > 0:
if level == 1:
return process_lines(code, add_indent_line)
code = process_lines(code, add_indent_line)
return with_indent(code, level - 1)
else:
if level == -1:
return process_lines(code, remove_indent_line)
return with_indent(code, level - 1)
def func_sig_to_method_sig(func_sig: str) -> str:
regex_func_sig = re.compile(
r"^(?P<method_name>[_a-zA-Z0-9]+)\((?P<arg0>[^,*)]+(:.+)?)?(?P<rest_args>.*)?\)",
re.DOTALL,
)
matched = regex_func_sig.search(func_sig)
if matched is None:
# TODO: resolve this case
logging.warning(f"Cannot parse function signature: {func_sig}")
return "_(self)"
if matched.group('rest_args').startswith('*'):
method_sig = regex_func_sig.sub(
r"\g<method_name>(self, \g<rest_args>)", func_sig
)
else:
method_sig = regex_func_sig.sub(
r"\g<method_name>(self\g<rest_args>)", func_sig
)
return method_sig
def func_doc_to_method_doc(func_doc: str) -> str:
# Iterate every line, insert the indent and remove document of the first argument
method_doc = ""
is_first_arg = False
first_arg_offset = 0
for line in func_doc.splitlines():
current_line_offset = len(line) - len(line.lstrip())
# Remove the first argument (self in Tensor method) from docstring
if is_first_arg:
if current_line_offset <= first_arg_offset:
is_first_arg = False
if not first_arg_offset:
first_arg_offset = current_line_offset
if is_first_arg:
continue
method_doc += f"{line}\n" if line else "\n"
if line.lstrip().startswith("Args:"):
is_first_arg = True
return method_doc
def try_import_paddle() -> ModuleType | None:
try:
return importlib.import_module('paddle')
except ModuleNotFoundError:
traceback.print_exc(file=sys.stderr)
sys.stderr.write(
'''
ERROR: Can NOT import paddle from `tools/gen_tensor_stub.py` before installation.
So the stub file `python/paddle/tensor/tensor.pyi` of `paddle.Tensor` may be lost.
We COULD import paddle without installation with all libs (.dll or .so) copied into dir `paddle/libs`,
or path already been set for the system. Try the following steps to locate the problem.
1. Build with `SKIP_STUB_GEN=ON make -j$(nproc)`.
2. Install the wheel from `build/python/dist`.
3. Try to `import paddle` and check the problems.
'''
)
return None
def get_tensor_members(module: str = 'paddle.Tensor') -> dict[int, Member]:
paddle = try_import_paddle()
if not paddle:
raise (
ModuleNotFoundError(
'Can NOT import paddle from tools/gen_tensor_stub.py.'
)
)
tensor_class = eval(module)
members: dict[int, Member] = {}
for name, member in inspect.getmembers(tensor_class):
member_id = id(member)
member_doc = inspect.getdoc(member)
member_doc_cleaned = (
func_doc_to_method_doc(inspect.cleandoc(member_doc))
if member_doc is not None
else None
)
try:
sig = inspect.signature(member)
sig = sig.replace(
parameters=[
p.replace(
annotation=rename_builtin_annotation(p.annotation)
)
for p in sig.parameters.values()
],
return_annotation=rename_builtin_annotation(
sig.return_annotation
),
)
# TODO: classmethod
member_signature = f"{name}{sig}"
except (TypeError, ValueError):
member_signature = f"{name}()"
if is_inherited_member(name, tensor_class):
continue
# Filter out private members except magic methods
if name.startswith("_") and not (
name.startswith("__") and name.endswith("__")
):
continue
if member_id in members:
members[member_id].add_alias(name)
continue
if name == '__doc__':
members[member_id] = Member(
member_id,
name,
"doc",
[],
[],
"__doc__",
member,
)
elif is_property(member) or inspect.isdatadescriptor(member):
members[member_id] = Member(
member_id,
name,
"method",
[],
["property"],
f"{name}(self)",
member_doc_cleaned,
)
elif is_classmethod(member):
members[member_id] = Member(
member_id,
name,
"method",
[],
["classmethod"],
member_signature,
member_doc_cleaned,
)
elif is_staticmethod(member):
members[member_id] = Member(
member_id,
name,
"method",
[],
["staticmethod"],
member_signature,
member_doc_cleaned,
)
elif inspect.isfunction(member) or inspect.ismethod(member):
# `all_signatures` list[[member id, decorators, signature]]
# with atleast an original method
all_signatures = [[member_id, [], member_signature]]
# try to get overloads
_overloads = get_overloads(member)
for f in _overloads:
_sig = inspect.signature(f)
_sig = _sig.replace(
parameters=[
p.replace(
annotation=rename_builtin_annotation(p.annotation)
)
for p in _sig.parameters.values()
],
return_annotation=rename_builtin_annotation(
_sig.return_annotation
),
)
all_signatures.append(
[
id(f),
["overload"],
f"{name}{_sig}".replace("Ellipsis", "..."),
]
)
for _member_id, _decorators, _sig in all_signatures:
members[_member_id] = Member(
_member_id,
name,
"method",
[],
_decorators,
func_sig_to_method_sig(_sig),
member_doc_cleaned,
)
elif inspect.ismethoddescriptor(member):
members[member_id] = Member(
member_id,
name,
"method",
[],
[],
func_sig_to_method_sig(member_signature),
member_doc_cleaned,
)
else:
logging.debug(f"Skip unknown type of member: {name}, {member}")
return members
def get_tensor_template(path: str) -> str:
with open(path) as f:
return ''.join(f.readlines())
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"-i",
"--input-file",
type=str,
default="python/paddle/tensor/tensor.prototype.pyi",
)
parser.add_argument(
"-o",
"--output-file",
type=str,
default="python/paddle/tensor/tensor.pyi",
)
args = parser.parse_args()
return args
def generate_stub_file(input_file=None, output_file=None):
# Get tensor template
tensor_template = get_tensor_template(input_file)
all_members: set[int] = set()
template_codes: list[tuple[int, int, str]] = []
# Get members of Tensor
for module, prefix in [
['paddle.Tensor', 'tensor'],
['paddle.base.framework.EagerParamBase', 'eager_param_base'],
]:
tensor_members = get_tensor_members(module)
logging.debug(f'total members in {module}: {len(tensor_members)}')
# Generate the Tensor stub
tensor_gen = TensorGen(tensor_template, prefix)
for member_id, member in tensor_members.items():
if member_id in all_members:
continue
if member.type == "method":
tensor_gen.add_method(member)
for alias in member.aliases:
tensor_gen.add_alias(alias, member.name)
elif member.type == "attribute":
tensor_gen.add_attribute(member.name, "Any")
elif member.type == "doc":
tensor_gen.add_doc(member.doc)
all_members |= tensor_members.keys()
template_codes += tensor_gen.template_codes
# Write to target file
with open(output_file, "w", encoding="utf-8") as f:
f.write(TensorGen.codegen(tensor_template, template_codes))
def main():
args = parse_args()
generate_stub_file(args.input_file, args.output_file)
if __name__ == "__main__":
main()