785 lines
24 KiB
Python
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()
|