blender-python-stubs/tests/test_generate_stubs.py
Joseph HENRY 852a5de700 Initial commit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:00:51 +01:00

785 lines
24 KiB
Python

"""Tests for the stub generator."""
import unittest
from generate_stubs import (
ParamOverrides,
apply_overrides,
collect_all_methods,
generate_function_stub,
generate_method_stub,
generate_property_stub,
generate_struct_stub,
generate_types_stub,
generate_variable_stub,
generate_module_stub,
collect_imports,
map_type,
topological_sort_structs,
)
from introspect import (
FunctionData,
ModuleData,
PropertyData,
StructData,
VariableData,
)
class TestMapType(unittest.TestCase):
def test_frozenset_gets_type_param(self) -> None:
self.assertEqual(map_type("frozenset"), "frozenset[str]")
def test_unknown_passthrough(self) -> None:
self.assertEqual(map_type("list[str]"), "list[str]")
class TestGenerateFunctionStub(unittest.TestCase):
def test_simple_function(self) -> None:
func: FunctionData = {
"name": "basename",
"doc": "Get basename.",
"params": [
{
"name": "path",
"type": "str",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
],
"return_type": "str",
"is_classmethod": False,
}
result = generate_function_stub(func)
self.assertIn("def basename(path: str) -> str:", result)
self.assertIn('"""Get basename."""', result)
def test_keyword_only_params(self) -> None:
func: FunctionData = {
"name": "abspath",
"doc": "",
"params": [
{
"name": "path",
"type": "str | bytes",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
{
"name": "start",
"type": "str | bytes | None",
"default": "None",
"kind": "KEYWORD_ONLY",
},
{
"name": "library",
"type": "bpy.types.Library | None",
"default": "None",
"kind": "KEYWORD_ONLY",
},
],
"return_type": "str",
"is_classmethod": False,
}
result = generate_function_stub(func)
self.assertEqual(
result,
"def abspath(path: str | bytes, *, start: str | bytes | None = None, library: bpy.types.Library | None = None) -> str: ...\n",
)
def test_no_return_type(self) -> None:
func: FunctionData = {
"name": "do_thing",
"doc": "",
"params": [],
"return_type": None,
"is_classmethod": False,
}
result = generate_function_stub(func)
self.assertEqual(result, "def do_thing() -> None: ...\n")
def test_no_param_type_gets_object(self) -> None:
func: FunctionData = {
"name": "foo",
"doc": "",
"params": [
{
"name": "x",
"type": None,
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
],
"return_type": None,
"is_classmethod": False,
}
result = generate_function_stub(func)
self.assertEqual(result, "def foo(x: object) -> None: ...\n")
def test_no_star_separator_with_only_var_keyword(self) -> None:
"""Don't insert * when the only keyword param is **kwargs."""
func: FunctionData = {
"name": "foo",
"doc": "",
"params": [
{
"name": "x",
"type": "str",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
{
"name": "kwargs",
"type": "object",
"default": None,
"kind": "VAR_KEYWORD",
},
],
"return_type": None,
"is_classmethod": False,
}
result = generate_function_stub(func)
self.assertNotIn("*, ", result)
self.assertIn("**kwargs", result)
def test_default_before_nondefault_reordered(self) -> None:
"""Non-default params must come before default params."""
func: FunctionData = {
"name": "bake",
"doc": "",
"params": [
{
"name": "start",
"type": "int",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
{
"name": "step",
"type": "float",
"default": "1.0",
"kind": "POSITIONAL_OR_KEYWORD",
},
{
"name": "remove",
"type": "str",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
],
"return_type": None,
"is_classmethod": False,
}
result = generate_function_stub(func)
# remove (no default) should come before step (has default)
self.assertIn(
"def bake(start: int, remove: str, step: float = 1.0) -> None: ...",
result,
)
def test_star_separator_with_named_keyword(self) -> None:
"""Insert * when there are named keyword-only params."""
func: FunctionData = {
"name": "bar",
"doc": "",
"params": [
{
"name": "x",
"type": "str",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
{
"name": "flag",
"type": "bool",
"default": "False",
"kind": "KEYWORD_ONLY",
},
],
"return_type": None,
"is_classmethod": False,
}
result = generate_function_stub(func)
self.assertIn("*, flag", result)
def test_default_without_type_gets_object(self) -> None:
func: FunctionData = {
"name": "foo",
"doc": "",
"params": [
{
"name": "x",
"type": None,
"default": "42",
"kind": "POSITIONAL_OR_KEYWORD",
},
],
"return_type": None,
"is_classmethod": False,
}
result = generate_function_stub(func)
self.assertEqual(result, "def foo(x: object = 42) -> None: ...\n")
class TestGenerateVariableStub(unittest.TestCase):
def test_frozenset_variable(self) -> None:
var: VariableData = {
"name": "extensions_image",
"type": "frozenset",
"value": "frozenset({'.png', '.jpg'})",
}
result = generate_variable_stub(var)
self.assertEqual(result, "extensions_image: frozenset[str]\n")
class TestCollectImports(unittest.TestCase):
def test_sequence_import(self) -> None:
module_data: ModuleData = {
"module": "test",
"doc": "",
"functions": [
{
"name": "f",
"doc": "",
"params": [
{
"name": "x",
"type": "Sequence[str]",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
}
],
"return_type": None,
"is_classmethod": False,
},
],
"variables": [],
"structs": [],
}
imports = collect_imports(module_data)
self.assertIn("from collections.abc import Sequence", imports)
def test_bpy_types_import(self) -> None:
module_data: ModuleData = {
"module": "test",
"doc": "",
"functions": [
{
"name": "f",
"doc": "",
"params": [
{
"name": "x",
"type": "bpy.types.Library | None",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
}
],
"return_type": None,
"is_classmethod": False,
},
],
"variables": [],
"structs": [],
}
imports = collect_imports(module_data)
self.assertIn("import bpy.types", imports)
def test_types_module_import(self) -> None:
module_data: ModuleData = {
"module": "test",
"doc": "",
"functions": [
{
"name": "f",
"doc": "",
"params": [
{
"name": "mod",
"type": "types.ModuleType",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
}
],
"return_type": None,
"is_classmethod": False,
},
],
"variables": [],
"structs": [],
}
imports = collect_imports(module_data)
self.assertIn("import types", imports)
def test_no_imports_needed(self) -> None:
module_data: ModuleData = {
"module": "test",
"doc": "",
"functions": [
{
"name": "f",
"doc": "",
"params": [
{
"name": "x",
"type": "str",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
}
],
"return_type": "str",
"is_classmethod": False,
},
],
"variables": [],
"structs": [],
}
imports = collect_imports(module_data)
self.assertEqual(imports, set())
class TestGenerateModuleStub(unittest.TestCase):
def test_full_module(self) -> None:
module_data: ModuleData = {
"module": "bpy.path",
"doc": "Path utilities.",
"functions": [
{
"name": "basename",
"doc": "Get basename.",
"params": [
{
"name": "path",
"type": "str",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
],
"return_type": "str",
"is_classmethod": False,
},
],
"variables": [
{
"name": "extensions_image",
"type": "frozenset",
"value": "frozenset({'.png'})",
},
],
"structs": [],
}
result = generate_module_stub(module_data)
self.assertIn('"""Path utilities."""', result)
self.assertIn("extensions_image: frozenset[str]", result)
self.assertIn("def basename(path: str) -> str:", result)
self.assertIn('"""Get basename."""', result)
class TestGeneratePropertyStub(unittest.TestCase):
def test_simple_property(self) -> None:
prop: PropertyData = {
"name": "filepath",
"type": "str",
"is_readonly": False,
"description": "Path to the library.",
}
result = generate_property_stub(prop)
self.assertIn("filepath: str", result)
self.assertIn('"""Path to the library."""', result)
def test_property_no_description(self) -> None:
prop: PropertyData = {
"name": "x",
"type": "float",
"is_readonly": False,
"description": "",
}
result = generate_property_stub(prop)
self.assertEqual(result, " x: float\n")
def test_readonly_property(self) -> None:
prop: PropertyData = {
"name": "name",
"type": "str",
"is_readonly": True,
"description": "Unique name used in the code and scripting.",
}
result = generate_property_stub(prop)
self.assertIn("@property", result)
self.assertIn("def name(self) -> str:", result)
self.assertIn('"""Unique name used in the code and scripting."""', result)
def test_readonly_no_description(self) -> None:
prop: PropertyData = {
"name": "type",
"type": "str",
"is_readonly": True,
"description": "",
}
result = generate_property_stub(prop)
self.assertIn("@property", result)
self.assertIn("def type(self) -> str:", result)
self.assertIn("...", result)
class TestGenerateMethodStub(unittest.TestCase):
def test_simple_method(self) -> None:
func: FunctionData = {
"name": "select_get",
"doc": "Test if selected.",
"params": [
{
"name": "view_layer",
"type": "ViewLayer",
"default": "None",
"kind": "POSITIONAL_OR_KEYWORD",
},
],
"return_type": "bool",
"is_classmethod": False,
}
result = generate_method_stub(func)
self.assertIn(
"def select_get(self, view_layer: ViewLayer = None) -> bool:", result
)
self.assertIn('"""Test if selected."""', result)
def test_classmethod(self) -> None:
func: FunctionData = {
"name": "is_running",
"doc": "",
"params": [
{
"name": "context",
"type": "Context",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
],
"return_type": "bool",
"is_classmethod": True,
}
result = generate_method_stub(func)
self.assertIn("@classmethod", result)
self.assertIn("def is_running(cls, context: Context) -> bool: ...", result)
def test_override_decorator(self) -> None:
func: FunctionData = {
"name": "is_registered_node_type",
"doc": "",
"params": [],
"return_type": "bool",
"is_classmethod": True,
}
result = generate_method_stub(func, is_override=True)
self.assertIn("@override", result)
self.assertIn("@classmethod", result)
lines = result.strip().split("\n")
self.assertEqual(lines[0].strip(), "@override")
self.assertEqual(lines[1].strip(), "@classmethod")
def test_positional_only_params(self) -> None:
"""Positional-only params get a / separator."""
func: FunctionData = {
"name": "__init__",
"doc": "",
"params": [
{
"name": "rgb",
"type": "Sequence[float]",
"default": "...",
"kind": "POSITIONAL_ONLY",
},
],
"return_type": "None",
"is_classmethod": False,
}
result = generate_method_stub(func)
self.assertIn(
"def __init__(self, rgb: Sequence[float] = ..., /) -> None: ...", result
)
def test_keyword_only_method_params(self) -> None:
"""Keyword-only params get a * separator."""
func: FunctionData = {
"name": "__init__",
"doc": "",
"params": [
{
"name": "width",
"type": "int",
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
{
"name": "format",
"type": "str",
"default": "'RGBA8'",
"kind": "KEYWORD_ONLY",
},
],
"return_type": "None",
"is_classmethod": False,
}
result = generate_method_stub(func)
self.assertIn(
"def __init__(self, width: int, *, format: str = 'RGBA8') -> None: ...",
result,
)
class TestGenerateStructStub(unittest.TestCase):
def test_with_base(self) -> None:
struct: StructData = {
"name": "Library",
"doc": "Library data-block.",
"base": "ID",
"properties": [
{
"name": "filepath",
"type": "str",
"is_readonly": False,
"description": "",
},
],
"methods": [],
}
result = generate_struct_stub(struct)
self.assertIn("class Library(ID):", result)
self.assertIn(" filepath: str", result)
def test_without_base(self) -> None:
struct: StructData = {
"name": "ID",
"doc": "Base type.",
"base": None,
"properties": [],
"methods": [],
}
result = generate_struct_stub(struct)
self.assertIn("class ID:", result)
self.assertIn('"""Base type."""', result)
def test_with_methods(self) -> None:
struct: StructData = {
"name": "Object",
"doc": "",
"base": "ID",
"properties": [],
"methods": [
{
"name": "select_get",
"doc": "",
"params": [],
"return_type": "bool",
"is_classmethod": False,
},
],
}
result = generate_struct_stub(struct)
self.assertIn("def select_get(self) -> bool: ...", result)
def test_method_no_return_gets_none(self) -> None:
struct: StructData = {
"name": "Object",
"doc": "",
"base": None,
"properties": [],
"methods": [
{
"name": "do_thing",
"doc": "",
"params": [],
"return_type": None,
"is_classmethod": False,
},
],
}
result = generate_struct_stub(struct)
self.assertIn("def do_thing(self) -> None: ...", result)
class TestTopologicalSort(unittest.TestCase):
def test_base_before_child(self) -> None:
structs: list[StructData] = [
{
"name": "Library",
"doc": "",
"base": "ID",
"properties": [],
"methods": [],
},
{
"name": "ID",
"doc": "",
"base": None,
"properties": [],
"methods": [],
},
]
result = topological_sort_structs(structs)
names = [s["name"] for s in result]
self.assertEqual(names, ["ID", "Library"])
def test_circular_deps_dont_loop(self) -> None:
structs: list[StructData] = [
{
"name": "A",
"doc": "",
"base": "B",
"properties": [],
"methods": [],
},
{
"name": "B",
"doc": "",
"base": "A",
"properties": [],
"methods": [],
},
]
result = topological_sort_structs(structs)
self.assertEqual(len(result), 2)
class TestCollectAllMethods(unittest.TestCase):
def test_inherits_from_base(self) -> None:
method: FunctionData = {
"name": "is_registered_node_type",
"doc": "",
"params": [],
"return_type": "bool",
"is_classmethod": True,
}
structs: list[StructData] = [
{
"name": "Node",
"doc": "",
"base": None,
"properties": [],
"methods": [method],
},
{
"name": "TextureNode",
"doc": "",
"base": "Node",
"properties": [],
"methods": [method],
},
{
"name": "TextureNodeValToRGB",
"doc": "",
"base": "TextureNode",
"properties": [],
"methods": [method],
},
]
result = collect_all_methods(structs)
self.assertEqual(result["Node"], set())
self.assertIn("is_registered_node_type", result["TextureNode"])
self.assertIn("is_registered_node_type", result["TextureNodeValToRGB"])
class TestGenerateTypesStub(unittest.TestCase):
def test_includes_header(self) -> None:
structs: list[StructData] = [
{
"name": "ID",
"doc": "",
"base": None,
"properties": [],
"methods": [],
},
]
result = generate_types_stub(structs)
self.assertIn('_T = TypeVar("_T")', result)
self.assertIn("class ID:", result)
class TestApplyOverrides(unittest.TestCase):
def test_overrides_param_type(self) -> None:
module_data: ModuleData = {
"module": "test",
"doc": "",
"functions": [
{
"name": "abspath",
"doc": "",
"params": [
{
"name": "path",
"type": None,
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
},
],
"return_type": "str",
"is_classmethod": False,
},
],
"variables": [],
"structs": [],
}
overrides: dict[str, ParamOverrides] = {
"abspath": {
"params": {"path": "str | bytes"},
},
}
result = apply_overrides(module_data, overrides)
self.assertEqual(result["functions"][0]["params"][0]["type"], "str | bytes")
def test_overrides_return_type(self) -> None:
module_data: ModuleData = {
"module": "test",
"doc": "",
"functions": [
{
"name": "foo",
"doc": "",
"params": [],
"return_type": None,
"is_classmethod": False,
},
],
"variables": [],
"structs": [],
}
overrides: dict[str, ParamOverrides] = {
"foo": {"return_type": "int"},
}
result = apply_overrides(module_data, overrides)
self.assertEqual(result["functions"][0]["return_type"], "int")
def test_no_matching_override(self) -> None:
module_data: ModuleData = {
"module": "test",
"doc": "",
"functions": [
{
"name": "bar",
"doc": "",
"params": [
{
"name": "x",
"type": None,
"default": None,
"kind": "POSITIONAL_OR_KEYWORD",
}
],
"return_type": None,
"is_classmethod": False,
},
],
"variables": [],
"structs": [],
}
overrides: dict[str, ParamOverrides] = {
"foo": {"params": {"x": "int"}},
}
result = apply_overrides(module_data, overrides)
self.assertIsNone(result["functions"][0]["params"][0]["type"])
if __name__ == "__main__":
unittest.main()