blob: a82c076033db4706e98ddd4a32e1c09864e89a9f [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.
from __future__ import annotations
import itertools
import typing
from pathlib import Path
import pytest
import tvm_ffi.stub.cli as stub_cli
from tvm_ffi import Object, method
from tvm_ffi.core import TypeSchema
from tvm_ffi.dataclasses import py_class
from tvm_ffi.stub import consts as C
from tvm_ffi.stub.cli import _stage_2, _stage_3
from tvm_ffi.stub.file_utils import CodeBlock, FileInfo
from tvm_ffi.stub.generator import get_generator
from tvm_ffi.stub.python_generator import consts as PC
from tvm_ffi.stub.python_generator.codegen import (
generate_python_all,
generate_python_export,
generate_python_ffi_api,
generate_python_global_funcs,
generate_python_import_section,
generate_python_init,
generate_python_object,
render_func_signature,
render_object_ffi_init,
render_object_fields,
render_object_init,
render_object_methods,
)
from tvm_ffi.stub.python_generator.utils import ImportItem
from tvm_ffi.stub.utils import (
FuncInfo,
InitConfig,
InitFieldInfo,
NamedTypeSchema,
ObjectInfo,
Options,
)
_counter = itertools.count()
def _identity_ty_map(name: str) -> str:
return name
def _unique_type_key(base: str) -> str:
return f"testing.stubgen.{base}_{next(_counter)}"
def _default_ty_map() -> dict[str, str]:
return PC.TY_MAP_DEFAULTS.copy()
def _type_suffix(name: str) -> str:
return PC.TY_MAP_DEFAULTS.get(name, name).rsplit(".", 1)[-1]
def _input_type_suffix(name: str) -> str:
return PC.TY_MAP_INPUT_DEFAULTS.get(name, PC.TY_MAP_DEFAULTS.get(name, name)).rsplit(".", 1)[-1]
def test_codeblock_from_begin_line_variants() -> None:
cases = [
(f"{C.PYTHON_SYNTAX.begin} global/demo", "global", ("demo", "")),
(f"{C.PYTHON_SYNTAX.begin} global/demo@.registry", "global", ("demo", ".registry")),
(f"{C.PYTHON_SYNTAX.begin} object/demo.TypeBase", "object", "demo.TypeBase"),
(f"{C.PYTHON_SYNTAX.begin} ty-map/custom", "ty-map", "custom"),
(f"{C.PYTHON_SYNTAX.begin} import-section", "import-section", ""),
]
for lineno, (line, kind, param) in enumerate(cases, start=1):
block = CodeBlock.from_begin_line(lineno, line, C.PYTHON_SYNTAX)
assert block.kind == kind
assert block.param == param
assert block.lineno_start == lineno
assert block.lineno_end is None
assert block.lines == []
def test_codeblock_from_begin_line_ty_map_and_unknown() -> None:
line = f"{C.PYTHON_SYNTAX.ty_map} custom -> mapped"
block = CodeBlock.from_begin_line(5, line, C.PYTHON_SYNTAX)
assert block.kind == "ty-map"
assert block.param == "custom -> mapped"
assert block.lineno_start == 5
assert block.lineno_end == 5
with pytest.raises(ValueError):
CodeBlock.from_begin_line(1, f"{C.PYTHON_SYNTAX.begin} unsupported/kind", C.PYTHON_SYNTAX)
def test_fileinfo_from_file_skip_and_missing_markers(tmp_path: Path) -> None:
skip = tmp_path / "skip.py"
skip.write_text(f"print('hi')\n{C.PYTHON_SYNTAX.skip_file}\n", encoding="utf-8")
assert FileInfo.from_file(skip) is None
plain = tmp_path / "plain.py"
plain.write_text("print('plain')\n", encoding="utf-8")
assert FileInfo.from_file(plain) is None
def test_fileinfo_from_file_parses_blocks(tmp_path: Path) -> None:
content = "\n".join(
[
"first = 1",
f"{C.PYTHON_SYNTAX.begin} global/demo.func",
"in_stub = True",
C.PYTHON_SYNTAX.end,
f"{C.PYTHON_SYNTAX.ty_map} x -> y",
]
)
path = tmp_path / "demo.py"
path.write_text(content, encoding="utf-8")
info = FileInfo.from_file(path)
assert info is not None
assert info.path == path.resolve()
assert len(info.code_blocks) == 3
first, stub, ty_map = info.code_blocks
assert first.kind is None and first.lines == ["first = 1"]
assert stub.kind == "global"
assert stub.param == ("demo.func", "")
assert stub.lineno_start == 2
assert stub.lineno_end == 4
assert stub.lines == [
f"{C.PYTHON_SYNTAX.begin} global/demo.func",
"in_stub = True",
C.PYTHON_SYNTAX.end,
]
assert ty_map.kind == "ty-map"
assert ty_map.param == "x -> y"
assert ty_map.lineno_start == ty_map.lineno_end == 5
assert ty_map.lines == [f"{C.PYTHON_SYNTAX.ty_map} x -> y"]
def test_fileinfo_from_file_error_paths(tmp_path: Path) -> None:
nested = tmp_path / "nested.py"
nested.write_text(
"\n".join(
[
f"{C.PYTHON_SYNTAX.begin} global/outer",
f"{C.PYTHON_SYNTAX.begin} global/inner",
]
),
encoding="utf-8",
)
with pytest.raises(ValueError, match="Nested stub not permitted"):
FileInfo.from_file(nested)
unmatched_end = tmp_path / "unmatched.py"
unmatched_end.write_text(C.PYTHON_SYNTAX.end + "\n", encoding="utf-8")
with pytest.raises(ValueError, match="Unmatched"):
FileInfo.from_file(unmatched_end)
unclosed = tmp_path / "unclosed.py"
unclosed.write_text(f"{C.PYTHON_SYNTAX.begin} global/method\n", encoding="utf-8")
with pytest.raises(ValueError, match="Unclosed stub block"):
FileInfo.from_file(unclosed)
def test_funcinfo_gen_variants() -> None:
called: list[str] = []
def ty_map(name: str) -> str:
called.append(name)
return name
schema_no_args = NamedTypeSchema("demo.no_args", TypeSchema("Callable", ()))
func = FuncInfo(schema=schema_no_args, is_member=False)
assert render_func_signature(func, ty_map, indent=2) == " def no_args(*args: Any) -> Any: ..."
assert called == ["Any"]
schema_member = NamedTypeSchema(
"pkg.Class.method",
TypeSchema(
"Callable",
(
TypeSchema("str"),
TypeSchema("int"),
TypeSchema("float"),
),
),
)
member_func = FuncInfo(schema=schema_member, is_member=True)
assert (
render_func_signature(member_func, _identity_ty_map, indent=0)
== "def method(self, _1: float, /) -> str: ..."
)
schema_bad = NamedTypeSchema("bad", TypeSchema("int"))
with pytest.raises(ValueError):
render_func_signature(
FuncInfo(schema=schema_bad, is_member=False), _identity_ty_map, indent=0
)
def test_objectinfo_gen_fields_and_methods() -> None:
ty_calls: list[str] = []
def ty_map(name: str) -> str:
ty_calls.append(name)
return {"list": "Sequence", "dict": "Mapping"}.get(name, name)
info = ObjectInfo(
fields=[
NamedTypeSchema("field_a", TypeSchema("list", (TypeSchema("int"),))),
NamedTypeSchema(
"field_b", TypeSchema("dict", (TypeSchema("str"), TypeSchema("float")))
),
],
methods=[
FuncInfo(
schema=NamedTypeSchema("demo.static", TypeSchema("Callable", (TypeSchema("int"),))),
is_member=False,
),
FuncInfo(
schema=NamedTypeSchema(
"demo.member",
TypeSchema("Callable", (TypeSchema("str"), TypeSchema("bytes"))),
),
is_member=True,
),
],
)
assert render_object_fields(info, ty_map, indent=2) == [
" field_a: Sequence[int]",
" field_b: Mapping[str, float]",
]
assert ty_calls.count("list") == 1 and ty_calls.count("dict") == 1
methods = render_object_methods(info, _identity_ty_map, indent=2)
assert methods == [
" @staticmethod",
" def static() -> int: ...",
" def member(self, /) -> str: ...",
]
def test_type_schema_container_origins() -> None:
"""Test that Array/List/Map/Dict origins are distinct and validated correctly."""
# Array and List: 0 or 1 arg, default to (Any,)
for origin in ("Array", "List"):
s = TypeSchema(origin)
assert s.args == (TypeSchema("Any"),), f"{origin} should default to (Any,)"
s = TypeSchema(origin, (TypeSchema("int"),))
assert s.repr() == f"{origin}[int]"
# Map and Dict: 0 or 2 args, default to (Any, Any)
for origin in ("Map", "Dict"):
s = TypeSchema(origin)
assert s.args == (TypeSchema("Any"), TypeSchema("Any")), (
f"{origin} should default to (Any, Any)"
)
s = TypeSchema(origin, (TypeSchema("str"), TypeSchema("float")))
assert s.repr() == f"{origin}[str, float]"
# from_json_str round-trip through _TYPE_SCHEMA_ORIGIN_CONVERTER
s = TypeSchema.from_json_str('{"type":"ffi.Array","args":[{"type":"int"}]}')
assert s.origin == "Array"
assert s.repr() == "Array[int]"
s = TypeSchema.from_json_str('{"type":"ffi.List","args":[{"type":"str"}]}')
assert s.origin == "List"
assert s.repr() == "List[str]"
s = TypeSchema.from_json_str('{"type":"ffi.Map","args":[{"type":"str"},{"type":"int"}]}')
assert s.origin == "Map"
assert s.repr() == "Map[str, int]"
s = TypeSchema.from_json_str('{"type":"ffi.Dict","args":[{"type":"str"},{"type":"float"}]}')
assert s.origin == "Dict"
assert s.repr() == "Dict[str, float]"
# Backward compat: "list" and "dict" origins still work
s = TypeSchema("list", (TypeSchema("int"),))
assert s.repr() == "list[int]"
s = TypeSchema("dict", (TypeSchema("str"), TypeSchema("int")))
assert s.repr() == "dict[str, int]"
def test_objectinfo_gen_fields_container_types() -> None:
"""Test that ObjectInfo fields render distinct container annotations."""
info = ObjectInfo(
fields=[
NamedTypeSchema("arr", TypeSchema("Array", (TypeSchema("int"),))),
NamedTypeSchema("lst", TypeSchema("List", (TypeSchema("str"),))),
NamedTypeSchema("mp", TypeSchema("Map", (TypeSchema("str"), TypeSchema("int")))),
NamedTypeSchema("dt", TypeSchema("Dict", (TypeSchema("str"), TypeSchema("float")))),
],
methods=[],
)
assert render_object_fields(info, _type_suffix, indent=0) == [
"arr: Sequence[int]",
"lst: MutableSequence[str]",
"mp: Mapping[str, int]",
"dt: MutableMapping[str, float]",
]
def test_funcinfo_gen_uses_input_annotations_for_parameters() -> None:
info = FuncInfo(
schema=NamedTypeSchema(
"demo.echo_list",
TypeSchema(
"Callable",
(
TypeSchema("List", (TypeSchema("int"),)),
TypeSchema("List", (TypeSchema("int"),)),
),
),
),
is_member=False,
)
assert (
render_func_signature(info, _type_suffix, indent=0, input_ty_map=_input_type_suffix)
== "def echo_list(_0: Sequence[int], /) -> MutableSequence[int]: ..."
)
def test_generate_global_funcs_populates_input_defaults_for_partial_ty_map() -> None:
code = CodeBlock(
kind="global",
param=("demo", "mockpkg"),
lineno_start=1,
lineno_end=2,
lines=[f"{C.PYTHON_SYNTAX.begin} global/demo@mockpkg", C.PYTHON_SYNTAX.end],
)
funcs = [
FuncInfo(
schema=NamedTypeSchema(
"demo.echo_list",
TypeSchema(
"Callable",
(
TypeSchema("List", (TypeSchema("int"),)),
TypeSchema("List", (TypeSchema("int"),)),
),
),
),
is_member=False,
)
]
imports: list[ImportItem] = []
generate_python_global_funcs(
code, funcs, {"List": "collections.abc.MutableSequence"}, imports, Options()
)
assert code.lines == [
f"{C.PYTHON_SYNTAX.begin} global/demo@mockpkg",
"# fmt: off",
'_FFI_INIT_FUNC("demo", __name__)',
"if TYPE_CHECKING:",
" def echo_list(_0: Sequence[int], /) -> MutableSequence[int]: ...",
"# fmt: on",
C.PYTHON_SYNTAX.end,
]
def test_objectinfo_gen_init_uses_input_annotations() -> None:
info = ObjectInfo(
fields=[NamedTypeSchema("items", TypeSchema("List", (TypeSchema("int"),)))],
methods=[],
init_fields=[
InitFieldInfo(
name="items",
schema=NamedTypeSchema("items", TypeSchema("List", (TypeSchema("int"),))),
kw_only=False,
has_default=False,
)
],
has_init=True,
)
assert render_object_fields(info, _type_suffix, indent=0) == ["items: MutableSequence[int]"]
assert render_object_init(info, _type_suffix, indent=0, input_ty_map=_input_type_suffix) == [
"def __init__(self, items: Sequence[int]) -> None: ..."
]
assert render_object_ffi_init(
info, _type_suffix, indent=0, input_ty_map=_input_type_suffix
) == [
"def __ffi_init__(self, items: Sequence[int]) -> None: ... # ty: "
"ignore[invalid-method-override]"
]
def test_py_class_method_metadata_renders_stub_signature() -> None:
@py_class(_unique_type_key("MethodMetadata"))
class MethodMetadata(Object):
value: int
@method
def describe(self, values: typing.List[int], prefix: str) -> str: # noqa: UP006
return f"{prefix}:{self.value}:{len(values)}"
@method
@staticmethod
def normalize(values: typing.List[int]) -> typing.List[int]: # noqa: UP006
return values
info = ObjectInfo.from_type_info(MethodMetadata.__tvm_ffi_type_info__) # ty: ignore[unresolved-attribute]
methods = {method.schema.name: method for method in info.methods}
describe_schema = methods["describe"].schema
assert describe_schema.origin == "Callable"
assert [arg.origin for arg in describe_schema.args] == [
"str",
MethodMetadata.__tvm_ffi_type_info__.type_key, # ty: ignore[unresolved-attribute]
"List",
"str",
]
assert render_object_methods(info, _type_suffix, indent=0, input_ty_map=_input_type_suffix) == [
"def describe(self, _1: Sequence[int], _2: str, /) -> str: ...",
"@staticmethod",
"def normalize(_0: Sequence[int], /) -> MutableSequence[int]: ...",
]
def test_generate_global_funcs_updates_block() -> None:
code = CodeBlock(
kind="global",
param=("demo", "mockpkg"),
lineno_start=1,
lineno_end=2,
lines=[f"{C.PYTHON_SYNTAX.begin} global/demo@mockpkg", C.PYTHON_SYNTAX.end],
)
funcs = [
FuncInfo(
schema=NamedTypeSchema(
"demo.add_one",
TypeSchema("Callable", (TypeSchema("int"), TypeSchema("int"))),
),
is_member=False,
)
]
opts = Options(indent=2)
imports: list[ImportItem] = []
generate_python_global_funcs(code, funcs, _default_ty_map(), imports, opts)
assert imports == [
ImportItem("mockpkg.init_ffi_api", alias="_FFI_INIT_FUNC"),
ImportItem("typing.TYPE_CHECKING"),
]
assert code.lines == [
f"{C.PYTHON_SYNTAX.begin} global/demo@mockpkg",
"# fmt: off",
'_FFI_INIT_FUNC("demo", __name__)',
"if TYPE_CHECKING:",
" def add_one(_0: int, /) -> int: ...",
"# fmt: on",
C.PYTHON_SYNTAX.end,
]
def test_generate_global_funcs_noop_on_empty_list() -> None:
code = CodeBlock(
kind="global",
param=("empty", ""),
lineno_start=1,
lineno_end=2,
lines=[f"{C.PYTHON_SYNTAX.begin} global/empty", C.PYTHON_SYNTAX.end],
)
imports: list[ImportItem] = []
generate_python_global_funcs(code, [], _default_ty_map(), imports, Options())
assert code.lines == [f"{C.PYTHON_SYNTAX.begin} global/empty", C.PYTHON_SYNTAX.end]
assert imports == []
def test_generate_global_funcs_respects_custom_import_from() -> None:
code = CodeBlock(
kind="global",
param=("demo", "custom.mod"),
lineno_start=1,
lineno_end=2,
lines=[f"{C.PYTHON_SYNTAX.begin} global/demo@custom.mod", C.PYTHON_SYNTAX.end],
)
funcs = [
FuncInfo(
schema=NamedTypeSchema(
"demo.add_one",
TypeSchema("Callable", (TypeSchema("int"), TypeSchema("int"))),
),
is_member=False,
)
]
imports: list[ImportItem] = []
generate_python_global_funcs(code, funcs, _default_ty_map(), imports, Options(indent=0))
assert ImportItem("custom.mod.init_ffi_api", alias="_FFI_INIT_FUNC") in imports
def test_generate_global_funcs_aliases_colliding_type() -> None:
"""When a function name matches a type name, the type import gets an alias."""
code = CodeBlock(
kind="global",
param=("demo", "mockpkg"),
lineno_start=1,
lineno_end=2,
lines=[f"{C.PYTHON_SYNTAX.begin} global/demo@mockpkg", C.PYTHON_SYNTAX.end],
)
# Function "demo.Foo" returns type "demo.Foo" — name collision
funcs = [
FuncInfo(
schema=NamedTypeSchema(
"demo.Foo",
TypeSchema("Callable", (TypeSchema("demo.Foo"), TypeSchema("Any"))),
),
is_member=False,
)
]
ty_map = _default_ty_map()
ty_map["demo.Foo"] = "somepkg.Foo"
imports: list[ImportItem] = []
generate_python_global_funcs(code, funcs, ty_map, imports, Options(indent=4))
# The type import should use an alias to avoid shadowing the function
assert ImportItem("somepkg.Foo", type_checking_only=True, alias="_Foo") in imports
# The function annotation should use the alias
assert any("-> _Foo:" in line for line in code.lines)
def test_generate_object_fields_only_block() -> None:
code = CodeBlock(
kind="object",
param="demo.TypeDerived",
lineno_start=1,
lineno_end=2,
lines=[f"{C.PYTHON_SYNTAX.begin} object/demo.TypeDerived", C.PYTHON_SYNTAX.end],
)
opts = Options(indent=4)
imports: list[ImportItem] = []
info = ObjectInfo(
fields=[
NamedTypeSchema("field_a", TypeSchema("int")),
NamedTypeSchema("field_b", TypeSchema("float")),
],
methods=[],
type_key="demo.TypeDerived",
parent_type_key="demo.Parent",
)
generate_python_object(
code,
_default_ty_map(),
imports,
opts,
info,
)
assert imports == []
expected = [
f"{C.PYTHON_SYNTAX.begin} object/demo.TypeDerived",
" " * code.indent + "# fmt: off",
*[
(" " * code.indent) + line
for line in render_object_fields(info, _type_suffix, indent=0)
],
" " * code.indent + "# fmt: on",
C.PYTHON_SYNTAX.end,
]
assert code.lines == expected
def test_generate_object_with_methods() -> None:
code = CodeBlock(
kind="object",
param="demo.IntPair",
lineno_start=1,
lineno_end=2,
lines=[f"{C.PYTHON_SYNTAX.begin} object/demo.IntPair", C.PYTHON_SYNTAX.end],
)
opts = Options(indent=4)
imports: list[ImportItem] = []
info = ObjectInfo(
fields=[],
methods=[
FuncInfo.from_schema(
"demo.IntPair.__ffi_init__",
TypeSchema("Callable", (TypeSchema("None"), TypeSchema("int"), TypeSchema("int"))),
is_member=True,
),
FuncInfo.from_schema(
"demo.IntPair.sum",
TypeSchema("Callable", (TypeSchema("int"),)),
is_member=True,
),
],
type_key="demo.IntPair",
parent_type_key="demo.Parent",
)
generate_python_object(code, _default_ty_map(), imports, opts, info)
assert set(imports) == {ImportItem("typing.TYPE_CHECKING")}
assert code.lines[0] == f"{C.PYTHON_SYNTAX.begin} object/demo.IntPair"
assert code.lines[-1] == C.PYTHON_SYNTAX.end
assert "# fmt: off" in code.lines[1]
assert any("if TYPE_CHECKING:" in line for line in code.lines)
method_lines = [line for line in code.lines if "def __ffi_init__" in line or "def sum" in line]
# __ffi_init__ from TypeMethod is rendered as an instance method (self, ...) -> None
assert any(line.strip().startswith("def __ffi_init__(self") for line in method_lines)
assert any(line.strip().startswith("def sum") for line in method_lines)
def test_import_item_mod_map_prefix_rewrite() -> None:
# MOD_MAP rewrites must respect module-path boundaries.
assert ImportItem("ffi.Object").mod == "tvm_ffi"
assert ImportItem("testing.TestIntPair").mod == "tvm_ffi.testing"
assert ImportItem("testing.sub.Thing").mod == "tvm_ffi.testing.sub"
# A module that merely starts with a mapped prefix is NOT rewritten.
assert ImportItem("testingfoo.Thing").mod == "testingfoo"
assert ImportItem("ffi2.Thing").mod == "ffi2"
def test_generate_import_section_groups_modules() -> None:
code = CodeBlock(
kind="import-section",
param="",
lineno_start=1,
lineno_end=2,
lines=[f"{C.PYTHON_SYNTAX.begin} import", C.PYTHON_SYNTAX.end],
)
imports = [
ImportItem("typing.Any", type_checking_only=True),
ImportItem("demo_pkg.Tensor", type_checking_only=True),
ImportItem("demo.TestObjectBase", type_checking_only=True),
ImportItem("custom.mod.Type", type_checking_only=True),
]
opts = Options(indent=4)
generate_python_import_section(code, imports, opts)
expected_prefix = [
f"{C.PYTHON_SYNTAX.begin} import",
"# fmt: off",
"# isort: off",
"from __future__ import annotations",
"from typing import TYPE_CHECKING",
"if TYPE_CHECKING:",
]
assert code.lines[: len(expected_prefix)] == expected_prefix
assert " from demo import TestObjectBase" in code.lines
assert " from demo_pkg import Tensor" in code.lines
assert " from custom.mod import Type" in code.lines
assert " from typing import Any" in code.lines
assert code.lines[-2:] == ["# fmt: on", C.PYTHON_SYNTAX.end]
def test_generate_import_section_no_imports_noop() -> None:
code = CodeBlock(
kind="import-section",
param="",
lineno_start=1,
lineno_end=2,
lines=[f"{C.PYTHON_SYNTAX.begin} import", C.PYTHON_SYNTAX.end],
)
before = list(code.lines)
generate_python_import_section(code, [], Options())
assert code.lines == before
def test_generate_all_builds_sorted_and_deduped_list() -> None:
code = CodeBlock(
kind="global",
param="all",
lineno_start=1,
lineno_end=2,
lines=[" " + C.PYTHON_SYNTAX.begin + " global/all", C.PYTHON_SYNTAX.end],
)
generate_python_all(
code,
names={"tvm_ffi.foo", "bar", "pkg.baz", "bar"}, # duplicates stripped
opt=Options(indent=2),
)
assert code.lines == [
" " + C.PYTHON_SYNTAX.begin + " global/all",
' "bar",',
' "baz",',
' "foo",',
C.PYTHON_SYNTAX.end,
]
def test_generate_all_noop_on_empty_names() -> None:
code = CodeBlock(
kind="global",
param="all-empty",
lineno_start=1,
lineno_end=2,
lines=[C.PYTHON_SYNTAX.begin + " global/all-empty", C.PYTHON_SYNTAX.end],
)
before = list(code.lines)
generate_python_all(code, names=set(), opt=Options())
assert code.lines == before
def test_generate_all_uses_isort_style_ordering() -> None:
code = CodeBlock(
kind="global",
param="all-mixed",
lineno_start=1,
lineno_end=2,
lines=[C.PYTHON_SYNTAX.begin + " global/all-mixed", C.PYTHON_SYNTAX.end],
)
names = {"foo", "Bar", "LIB", "baz", "Alpha", "CONST"}
generate_python_all(code, names=names, opt=Options(indent=0))
assert code.lines == [
C.PYTHON_SYNTAX.begin + " global/all-mixed",
'"CONST",',
'"LIB",',
'"Alpha",',
'"Bar",',
'"baz",',
'"foo",',
C.PYTHON_SYNTAX.end,
]
def test_stage_3_adds_LIB_when_load_lib_imported(tmp_path: Path) -> None:
path = tmp_path / "demo.py"
global_block = CodeBlock(
kind="global",
param=("testing", ""),
lineno_start=2,
lineno_end=3,
lines=[f"{C.PYTHON_SYNTAX.begin} global/testing", C.PYTHON_SYNTAX.end],
)
import_obj_block = CodeBlock(
kind="import-object",
param=("tvm_ffi.libinfo.load_lib_module", "False", "_FFI_LOAD_LIB"),
lineno_start=1,
lineno_end=1,
lines=[
f"{C.PYTHON_SYNTAX.import_object} tvm_ffi.libinfo.load_lib_module;False;_FFI_LOAD_LIB"
],
)
all_block = CodeBlock(
kind="__all__",
param="",
lineno_start=4,
lineno_end=5,
lines=[f"{C.PYTHON_SYNTAX.begin} __all__", C.PYTHON_SYNTAX.end],
)
file_info = FileInfo(
path=path,
lines=tuple(
line for block in (import_obj_block, global_block, all_block) for line in block.lines
),
code_blocks=[import_obj_block, global_block, all_block],
syntax=C.PYTHON_SYNTAX,
)
funcs = [
FuncInfo.from_schema(
"testing.add_one",
TypeSchema("Callable", (TypeSchema("int"), TypeSchema("int"))),
)
]
_stage_3(
file_info,
Options(dry_run=True),
_default_ty_map(),
{"testing": funcs},
get_generator("python"),
)
lib_lines = [line for line in all_block.lines if "LIB" in line]
assert any("LIB" in line for line in lib_lines)
def test_generate_export_builds_all_extension() -> None:
code = CodeBlock(
kind="export",
param="ffi_api",
lineno_start=1,
lineno_end=2,
lines=[f"{C.PYTHON_SYNTAX.begin} export/ffi_api", C.PYTHON_SYNTAX.end],
)
generate_python_export(code)
full_text = "\n".join(code.lines)
assert "from .ffi_api import *" in full_text
assert "ffi_api__all__" in full_text
def test_generate_init_with_and_without_existing_export_block() -> None:
code_no_blocks = generate_python_init([], "demo", "_ffi_api", C.PYTHON_SYNTAX)
assert "Package demo." in code_no_blocks
assert f"{C.PYTHON_SYNTAX.begin} export/_ffi_api" in code_no_blocks
code_with_export = generate_python_init(
[
CodeBlock(
kind="export",
param="_ffi_api",
lineno_start=1,
lineno_end=2,
lines=["", ""],
)
],
"demo",
"_ffi_api",
C.PYTHON_SYNTAX,
)
assert code_with_export == ""
def test_generate_ffi_api_without_objects_includes_sections() -> None:
init_cfg = InitConfig(pkg="pkg", shared_target="pkg_shared", prefix="pkg.")
code = generate_python_ffi_api(
[],
_default_ty_map(),
"demo.mod",
[],
init_cfg,
is_root=False,
syntax=C.PYTHON_SYNTAX,
)
assert f"{C.PYTHON_SYNTAX.begin} import-section" in code
assert f"{C.PYTHON_SYNTAX.begin} global/demo.mod" in code
assert C.PYTHON_SYNTAX.begin + " __all__" in code
assert "LIB =" not in code
def test_generate_ffi_api_with_objects_imports_parents() -> None:
init_cfg = InitConfig(pkg="pkg", shared_target="pkg_shared", prefix="pkg.")
obj_info = ObjectInfo(
fields=[],
methods=[],
type_key="demo.TypeDerived",
parent_type_key="demo.Parent",
)
parent_key = obj_info.parent_type_key
code = generate_python_ffi_api(
[],
_default_ty_map(),
"demo",
[obj_info],
init_cfg,
is_root=False,
syntax=C.PYTHON_SYNTAX,
)
assert C.PYTHON_SYNTAX.import_object in code # register_object prompt
assert f"{C.PYTHON_SYNTAX.begin} object/{obj_info.type_key}" in code
assert parent_key is not None
parent_import_prompt = (
f"{C.PYTHON_SYNTAX.import_object} {parent_key};False;_{parent_key.replace('.', '_')}"
)
assert parent_import_prompt in code
def test_stage_2_filters_prefix_and_marks_root(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
prefixes: dict[str, list[FuncInfo]] = {"demo.sub": [], "demo": [], "other": []}
monkeypatch.setattr(stub_cli, "collect_type_keys", lambda: prefixes)
monkeypatch.setattr(stub_cli, "toposort_objects", lambda objs: [])
global_funcs = {
"demo.sub": [
FuncInfo.from_schema(
"demo.sub.add_one",
TypeSchema("Callable", (TypeSchema("int"), TypeSchema("int"))),
)
],
"demo": [
FuncInfo.from_schema(
"demo.add_one",
TypeSchema("Callable", (TypeSchema("int"), TypeSchema("int"))),
)
],
"other": [
FuncInfo.from_schema(
"other.add_one",
TypeSchema("Callable", (TypeSchema("int"), TypeSchema("int"))),
)
],
}
_stage_2(
files=[],
ty_map=_default_ty_map(),
init_cfg=InitConfig(pkg="demo-pkg", shared_target="demo_shared", prefix="demo."),
init_path=tmp_path,
global_funcs=global_funcs,
generator=get_generator("python"),
)
root_api = tmp_path / "demo" / "_ffi_api.py"
sub_api = tmp_path / "demo" / "sub" / "_ffi_api.py"
other_api = tmp_path / "other" / "_ffi_api.py"
assert root_api.exists()
assert sub_api.exists()
assert not other_api.exists()
root_text = root_api.read_text(encoding="utf-8")
sub_text = sub_api.read_text(encoding="utf-8")
assert 'LIB = _FFI_LOAD_LIB("demo-pkg", "demo_shared")' in root_text
assert "LIB =" not in sub_text