"""Tests for the stub generator.""" import unittest from generate_stubs import ( ParamOverrides, apply_overrides, collect_inherited_info, 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_inherited_info(structs) self.assertEqual(result["Node"].methods, set()) self.assertIn("is_registered_node_type", result["TextureNode"].methods) self.assertIn("is_registered_node_type", result["TextureNodeValToRGB"].methods) 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()