blob: e02bbc3b4fdb6bb7da79a6301897a2079c898c6f [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
"""Common utilities for the `tvm-ffi-stubgen` tool."""
from __future__ import annotations
import dataclasses
from io import StringIO
from typing import Callable
from tvm_ffi.core import TypeInfo, TypeSchema
from . import consts as C
@dataclasses.dataclass
class InitConfig:
"""Configuration for generating new stubs.
Examples
--------
If we are generating type stubs for Python package `my-ffi-extension`,
and the CMake target that generates the shared library is `my_ffi_extension_shared`,
then we can run the following command to generate the stubs:
--init-pypkg my-ffi-extension --init-lib my_ffi_extension_shared --init-prefix my_ffi_extension.
"""
pkg: str
"""Name of the Python package to generate stubs for, e.g. apache-tvm-ffi (instead of tvm_ffi)"""
shared_target: str
"""Name of CMake target that generates the shared library, e.g. tvm_ffi_shared
This is used to determine the name of the shared library file.
- macOS: lib{shared_target}.dylib or lib{shared_target}.so
- Linux: lib{shared_target}.so
- Windows: {shared_target}.dll
"""
prefix: str
"""Only generate stubs for global function and objects with the given prefix, e.g. `tvm_ffi.`"""
@dataclasses.dataclass
class Options:
"""Command line options for stub generation."""
imports: list[str] = dataclasses.field(default_factory=list)
dlls: list[str] = dataclasses.field(default_factory=list)
init: InitConfig | None = None
indent: int = 4
files: list[str] = dataclasses.field(default_factory=list)
verbose: bool = False
dry_run: bool = False
@dataclasses.dataclass(frozen=True, eq=True)
class ImportItem:
"""An import statement item."""
mod: str
name: str
type_checking_only: bool = False
alias: str | None = None
def __init__(
self,
name: str,
type_checking_only: bool = False,
alias: str | None = None,
) -> None:
"""Initialize an `ImportItem` with the given module name and optional alias."""
if "." in name:
mod, name = name.rsplit(".", 1)
for mod_prefix, mod_replacement in C.MOD_MAP.items():
if mod.startswith(mod_prefix):
mod = mod.replace(mod_prefix, mod_replacement, 1)
break
else:
mod = ""
object.__setattr__(self, "mod", mod)
object.__setattr__(self, "name", name)
object.__setattr__(self, "type_checking_only", type_checking_only)
object.__setattr__(self, "alias", alias)
@property
def name_with_alias(self) -> str:
"""Generate a string of the form `name as alias` if an alias is set, otherwise just `name`."""
return f"{self.name} as {self.alias}" if self.alias else self.name
@property
def full_name(self) -> str:
"""Generate a string of the form `mod.name` or `name` if no module is set."""
return f"{self.mod}.{self.name}" if self.mod else self.name
def __repr__(self) -> str:
"""Generate an import statement string for this item."""
return str(self)
def __str__(self) -> str:
"""Generate an import statement string for this item."""
if self.mod:
ret = f"from {self.mod} import {self.name_with_alias}"
else:
ret = f"import {self.name_with_alias}"
return ret
@dataclasses.dataclass(init=False)
class NamedTypeSchema(TypeSchema):
"""A type schema with an associated name."""
name: str
def __init__(self, name: str, schema: TypeSchema) -> None:
"""Initialize a `NamedTypeSchema` with the given name and type schema."""
super().__init__(origin=schema.origin, args=schema.args)
self.name = name
@dataclasses.dataclass
class FuncInfo:
"""Information of a function."""
schema: NamedTypeSchema
is_member: bool
@staticmethod
def from_schema(name: str, schema: TypeSchema, *, is_member: bool = False) -> FuncInfo:
"""Construct a `FuncInfo` from a name and its type schema."""
return FuncInfo(schema=NamedTypeSchema(name=name, schema=schema), is_member=is_member)
def gen(self, ty_map: Callable[[str], str], indent: int) -> str:
"""Generate a function signature string for this function."""
try:
_, func_name = self.schema.name.rsplit(".", 1)
except ValueError:
func_name = self.schema.name
buf = StringIO()
buf.write(" " * indent)
buf.write(f"def {func_name}(")
if self.schema.origin != "Callable":
raise ValueError(f"Expected Callable type schema, but got: {self.schema}")
if not self.schema.args:
ty_map("Any")
buf.write("*args: Any) -> Any: ...")
return buf.getvalue()
arg_ret = self.schema.args[0]
arg_args = self.schema.args[1:]
for i, arg in enumerate(arg_args):
if self.is_member and i == 0:
buf.write("self, ")
else:
buf.write(f"_{i}: ")
buf.write(arg.repr(ty_map))
buf.write(", ")
if arg_args:
buf.write("/")
buf.write(") -> ")
buf.write(arg_ret.repr(ty_map))
buf.write(": ...")
return buf.getvalue()
@dataclasses.dataclass
class ObjectInfo:
"""Information of an object type, including its fields and methods."""
fields: list[NamedTypeSchema]
methods: list[FuncInfo]
type_key: str | None = None
parent_type_key: str | None = None
@staticmethod
def from_type_info(type_info: TypeInfo) -> ObjectInfo:
"""Construct an `ObjectInfo` from a `TypeInfo` instance."""
parent_type_key: str | None = None
if type_info.parent_type_info is not None:
parent_type_key = type_info.parent_type_info.type_key
return ObjectInfo(
fields=[
NamedTypeSchema(
name=field.name,
schema=TypeSchema.from_json_str(field.metadata["type_schema"]),
)
for field in type_info.fields
],
methods=[
FuncInfo(
schema=NamedTypeSchema(
name=C.FN_NAME_MAP.get(method.name, method.name),
schema=TypeSchema.from_json_str(method.metadata["type_schema"]),
),
is_member=not method.is_static,
)
for method in type_info.methods
],
type_key=type_info.type_key,
parent_type_key=parent_type_key,
)
def gen_fields(self, ty_map: Callable[[str], str], indent: int) -> list[str]:
"""Generate field definitions for this object."""
indent_str = " " * indent
return [f"{indent_str}{field.name}: {field.repr(ty_map)}" for field in self.fields]
def gen_methods(self, ty_map: Callable[[str], str], indent: int) -> list[str]:
"""Generate method definitions for this object."""
indent_str = " " * indent
ret = []
for method in self.methods:
if not method.is_member:
ret.append(f"{indent_str}@staticmethod")
ret.append(method.gen(ty_map, indent))
return ret