Initial commit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
"""Tests for the Blender downloader module."""
|
||||
|
||||
import unittest
|
||||
|
||||
from blender_downloader import (
|
||||
get_archive_extension,
|
||||
get_download_url,
|
||||
get_extracted_dir_name,
|
||||
)
|
||||
|
||||
|
||||
class TestGetDownloadUrl(unittest.TestCase):
|
||||
def test_builds_correct_url(self) -> None:
|
||||
url = get_download_url("5.0.1")
|
||||
self.assertIn("https://download.blender.org/release/Blender5.0/", url)
|
||||
self.assertIn("blender-5.0.1-", url)
|
||||
self.assertTrue(url.endswith(get_archive_extension()))
|
||||
|
||||
def test_major_minor_extraction(self) -> None:
|
||||
url = get_download_url("4.3.2")
|
||||
self.assertIn("/Blender4.3/", url)
|
||||
self.assertIn("blender-4.3.2-", url)
|
||||
|
||||
|
||||
class TestGetExtractedDirName(unittest.TestCase):
|
||||
def test_linux_dir_name(self) -> None:
|
||||
name = get_extracted_dir_name("5.0.1")
|
||||
self.assertEqual(name, "blender-5.0.1-linux-x64")
|
||||
|
||||
def test_different_version(self) -> None:
|
||||
name = get_extracted_dir_name("4.3.2")
|
||||
self.assertEqual(name, "blender-4.3.2-linux-x64")
|
||||
|
||||
|
||||
class TestGetArchiveExtension(unittest.TestCase):
|
||||
def test_linux_extension(self) -> None:
|
||||
ext = get_archive_extension()
|
||||
self.assertEqual(ext, ".tar.xz")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,784 @@
|
||||
"""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()
|
||||
@@ -0,0 +1,531 @@
|
||||
"""Tests for the introspection module."""
|
||||
|
||||
import unittest
|
||||
from typing import cast
|
||||
|
||||
from introspect import (
|
||||
FunctionData,
|
||||
infer_getter_return_types,
|
||||
parse_rst_function_sig,
|
||||
infer_context_member_type,
|
||||
introspect_callable,
|
||||
refine_types_by_context,
|
||||
clean_docstring,
|
||||
clean_type_str,
|
||||
parse_docstring_types,
|
||||
python_type_name,
|
||||
)
|
||||
|
||||
|
||||
class TestCleanTypeStr(unittest.TestCase):
|
||||
def test_class_markup_removal(self) -> None:
|
||||
self.assertEqual(
|
||||
clean_type_str(":class:`bpy.types.Library`"), "bpy.types.Library"
|
||||
)
|
||||
|
||||
def test_plain_type_passthrough(self) -> None:
|
||||
self.assertEqual(clean_type_str("str | bytes"), "str | bytes")
|
||||
|
||||
def test_generic_type(self) -> None:
|
||||
self.assertEqual(
|
||||
clean_type_str("list[tuple[str, str]]"), "list[tuple[str, str]]"
|
||||
)
|
||||
|
||||
def test_sequence_type(self) -> None:
|
||||
self.assertEqual(clean_type_str("Sequence[str]"), "Sequence[str]")
|
||||
|
||||
def test_whitespace_collapse(self) -> None:
|
||||
self.assertEqual(clean_type_str("str | bytes"), "str | bytes")
|
||||
|
||||
def test_multiline_collapse(self) -> None:
|
||||
self.assertEqual(clean_type_str("str\n | bytes"), "str | bytes")
|
||||
|
||||
|
||||
class TestParseDocstringTypes(unittest.TestCase):
|
||||
def test_simple_type(self) -> None:
|
||||
doc = ":type name: str\n:rtype: str"
|
||||
param_types, return_type = parse_docstring_types(doc)
|
||||
self.assertEqual(param_types, {"name": "str"})
|
||||
self.assertEqual(return_type, "str")
|
||||
|
||||
def test_union_type(self) -> None:
|
||||
doc = ":type path: str | bytes\n:rtype: str"
|
||||
param_types, return_type = parse_docstring_types(doc)
|
||||
self.assertEqual(param_types, {"path": "str | bytes"})
|
||||
self.assertEqual(return_type, "str")
|
||||
|
||||
def test_class_reference(self) -> None:
|
||||
doc = ":type library: :class:`bpy.types.Library`\n:rtype: str"
|
||||
param_types, _return_type = parse_docstring_types(doc)
|
||||
self.assertEqual(param_types, {"library": "bpy.types.Library"})
|
||||
|
||||
def test_generic_return(self) -> None:
|
||||
doc = ":rtype: list[tuple[str, str]]"
|
||||
_param_types, return_type = parse_docstring_types(doc)
|
||||
self.assertEqual(return_type, "list[tuple[str, str]]")
|
||||
|
||||
def test_empty_docstring(self) -> None:
|
||||
param_types, return_type = parse_docstring_types("")
|
||||
self.assertEqual(param_types, {})
|
||||
self.assertIsNone(return_type)
|
||||
|
||||
def test_no_type_annotations(self) -> None:
|
||||
doc = "Just a description with no type info."
|
||||
param_types, return_type = parse_docstring_types(doc)
|
||||
self.assertEqual(param_types, {})
|
||||
self.assertIsNone(return_type)
|
||||
|
||||
def test_multiple_params(self) -> None:
|
||||
doc = (
|
||||
":arg start: Relative to this path.\n"
|
||||
":type start: str | bytes\n"
|
||||
":arg library: The library.\n"
|
||||
":type library: :class:`bpy.types.Library`\n"
|
||||
":rtype: str"
|
||||
)
|
||||
param_types, return_type = parse_docstring_types(doc)
|
||||
self.assertEqual(
|
||||
param_types,
|
||||
{
|
||||
"start": "str | bytes",
|
||||
"library": "bpy.types.Library",
|
||||
},
|
||||
)
|
||||
self.assertEqual(return_type, "str")
|
||||
|
||||
def test_literal_inferred_from_arg_enum_values(self) -> None:
|
||||
doc = (
|
||||
":arg mode: The blend mode.\n"
|
||||
" * ``NONE`` No blending.\n"
|
||||
" * ``ALPHA`` Alpha blend.\n"
|
||||
" * ``ADDITIVE`` Additive.\n"
|
||||
":type mode: str"
|
||||
)
|
||||
param_types, _ = parse_docstring_types(doc)
|
||||
self.assertEqual(
|
||||
param_types["mode"],
|
||||
'Literal["NONE", "ALPHA", "ADDITIVE"]',
|
||||
)
|
||||
|
||||
def test_literal_not_inferred_when_few_values(self) -> None:
|
||||
doc = ":arg path: The ``PATH`` to use.\n:type path: str"
|
||||
param_types, _ = parse_docstring_types(doc)
|
||||
self.assertEqual(param_types["path"], "str")
|
||||
|
||||
def test_lowercase_generator(self) -> None:
|
||||
self.assertEqual(
|
||||
clean_type_str("generator"),
|
||||
"Generator[object, None, None]",
|
||||
)
|
||||
|
||||
def test_lowercase_sequence(self) -> None:
|
||||
self.assertEqual(clean_type_str("sequence"), "Sequence[object]")
|
||||
|
||||
def test_bare_set_gets_parameterized(self) -> None:
|
||||
self.assertEqual(clean_type_str("set"), "set[object]")
|
||||
|
||||
def test_bare_frozenset_gets_parameterized(self) -> None:
|
||||
self.assertEqual(clean_type_str("frozenset"), "frozenset[object]")
|
||||
|
||||
def test_module_maps_to_types_moduletype(self) -> None:
|
||||
self.assertEqual(clean_type_str("Module"), "types.ModuleType")
|
||||
self.assertEqual(clean_type_str("Module | None"), "types.ModuleType | None")
|
||||
|
||||
def test_nonetype_maps_to_none(self) -> None:
|
||||
self.assertEqual(clean_type_str("NoneType"), "None")
|
||||
self.assertEqual(clean_type_str("str | NoneType"), "str | None")
|
||||
|
||||
def test_undefined_maps_to_object(self) -> None:
|
||||
self.assertEqual(clean_type_str("Undefined"), "object")
|
||||
|
||||
def test_uint_maps_to_int(self) -> None:
|
||||
self.assertEqual(clean_type_str("uint"), "int")
|
||||
self.assertEqual(clean_type_str("uint | None"), "int | None")
|
||||
|
||||
def test_type_sequence_pattern(self) -> None:
|
||||
self.assertEqual(clean_type_str("int or int sequence"), "int | Sequence[int]")
|
||||
self.assertEqual(
|
||||
clean_type_str("float or float sequence"), "float | Sequence[float]"
|
||||
)
|
||||
|
||||
def test_plural_bools(self) -> None:
|
||||
self.assertEqual(clean_type_str("Sequence of bools"), "Sequence[bool]")
|
||||
|
||||
def test_plural_numbers(self) -> None:
|
||||
self.assertEqual(clean_type_str("Sequence of numbers"), "Sequence[float]")
|
||||
|
||||
def test_plural_vectors(self) -> None:
|
||||
self.assertEqual(clean_type_str("vectors"), "mathutils.Vector")
|
||||
|
||||
def test_plural_matrices(self) -> None:
|
||||
self.assertEqual(clean_type_str("matrices"), "mathutils.Matrix")
|
||||
|
||||
def test_sequence_multi_arg_to_tuple(self) -> None:
|
||||
self.assertEqual(
|
||||
clean_type_str("Sequence[int, int]"), "Sequence[tuple[int, int]]"
|
||||
)
|
||||
self.assertEqual(
|
||||
clean_type_str("Sequence[Sequence[int, int]]"),
|
||||
"Sequence[Sequence[tuple[int, int]]]",
|
||||
)
|
||||
|
||||
def test_plural_tuples(self) -> None:
|
||||
self.assertEqual(clean_type_str("list[tuples]"), "list[tuple[object, ...]]")
|
||||
|
||||
def test_sequence_of_n_type_skips_number(self) -> None:
|
||||
self.assertEqual(clean_type_str("Sequence of 3 float"), "Sequence[float]")
|
||||
self.assertEqual(clean_type_str("Iterable of 4 int"), "Iterable[int]")
|
||||
|
||||
def test_sequence_of_n_or_m_type_skips_dimensions(self) -> None:
|
||||
self.assertEqual(clean_type_str("Sequence of 3 or 4 float"), "Sequence[float]")
|
||||
self.assertEqual(clean_type_str("Sequence of 1 to 4 float"), "Sequence[float]")
|
||||
|
||||
def test_numeric_type_arg_stripped(self) -> None:
|
||||
# "Sequence[3]" from malformed docstring -> bare Sequence -> parameterized
|
||||
self.assertEqual(clean_type_str("Sequence[3]"), "Sequence[object]")
|
||||
|
||||
def test_numeric_union_parts_stripped(self) -> None:
|
||||
# "Sequence[1] | 2 | 3" from dimension descriptions
|
||||
result = clean_type_str("Sequence[1] | 2 | 3 | object")
|
||||
self.assertNotIn("2", result)
|
||||
self.assertNotIn("3", result)
|
||||
|
||||
def test_empty_brackets_treated_as_bare(self) -> None:
|
||||
self.assertEqual(clean_type_str("dict[]"), "dict[str, object]")
|
||||
self.assertEqual(clean_type_str("list[]"), "list[object]")
|
||||
|
||||
def test_callable_empty_params_preserved(self) -> None:
|
||||
self.assertEqual(clean_type_str("Callable[[], None]"), "Callable[[], None]")
|
||||
|
||||
def test_bare_generic_no_false_match(self) -> None:
|
||||
# \bSequence\b should not match inside SequenceEntry
|
||||
self.assertEqual(clean_type_str("SequenceEntry"), "SequenceEntry")
|
||||
# \bset\b should not match inside SettingsGroup
|
||||
self.assertEqual(clean_type_str("SettingsGroup"), "SettingsGroup")
|
||||
|
||||
|
||||
class TestRefineTypesByContext(unittest.TestCase):
|
||||
def test_bool_vector_default(self) -> None:
|
||||
param_types = {"default": "Sequence"}
|
||||
param_types, _ = refine_types_by_context(
|
||||
"BoolVectorProperty", param_types, None
|
||||
)
|
||||
self.assertEqual(param_types["default"], "Sequence[bool]")
|
||||
|
||||
def test_float_vector_default(self) -> None:
|
||||
param_types = {"default": "Sequence[object]"}
|
||||
param_types, _ = refine_types_by_context(
|
||||
"FloatVectorProperty", param_types, None
|
||||
)
|
||||
self.assertEqual(param_types["default"], "Sequence[float]")
|
||||
|
||||
def test_int_vector_default(self) -> None:
|
||||
param_types = {"default": "Sequence[object]"}
|
||||
param_types, _ = refine_types_by_context("IntVectorProperty", param_types, None)
|
||||
self.assertEqual(param_types["default"], "Sequence[int]")
|
||||
|
||||
def test_property_options_set_str(self) -> None:
|
||||
param_types = {"options": "set[object]", "override": "set[object]"}
|
||||
param_types, _ = refine_types_by_context("BoolProperty", param_types, None)
|
||||
self.assertEqual(param_types["options"], "set[str]")
|
||||
self.assertEqual(param_types["override"], "set[str]")
|
||||
|
||||
def test_generator_return_refined(self) -> None:
|
||||
_, return_type = refine_types_by_context("app_template_paths", {}, "Generator")
|
||||
self.assertEqual(return_type, "Generator[str, None, None]")
|
||||
|
||||
def test_non_property_set_unchanged(self) -> None:
|
||||
param_types = {"options": "set[object]"}
|
||||
param_types, _ = refine_types_by_context("some_function", param_types, None)
|
||||
self.assertEqual(param_types["options"], "set[object]")
|
||||
|
||||
|
||||
class TestPythonTypeName(unittest.TestCase):
|
||||
def test_set_with_string_contents(self) -> None:
|
||||
obj: object = {"a", "b"}
|
||||
self.assertEqual(python_type_name(obj), "set[str]")
|
||||
|
||||
def test_empty_set_defaults_to_str(self) -> None:
|
||||
obj = cast(object, set())
|
||||
self.assertEqual(python_type_name(obj), "set[str]")
|
||||
|
||||
def test_list_with_int_contents(self) -> None:
|
||||
obj: object = [1, 2, 3]
|
||||
self.assertEqual(python_type_name(obj), "list[int]")
|
||||
|
||||
def test_empty_list_defaults_to_object(self) -> None:
|
||||
obj: object = []
|
||||
self.assertEqual(python_type_name(obj), "list[object]")
|
||||
|
||||
def test_frozenset_with_contents(self) -> None:
|
||||
obj: object = frozenset({"x"})
|
||||
self.assertEqual(python_type_name(obj), "frozenset[str]")
|
||||
|
||||
def test_empty_frozenset_defaults_to_str(self) -> None:
|
||||
obj = cast(object, frozenset())
|
||||
self.assertEqual(python_type_name(obj), "frozenset[str]")
|
||||
|
||||
|
||||
class TestParseRstFunctionSig(unittest.TestCase):
|
||||
def test_keyword_only_with_defaults(self) -> None:
|
||||
doc = ".. function:: blend_paths(*, absolute=False, packed=False)"
|
||||
result = parse_rst_function_sig(doc)
|
||||
self.assertEqual(result["absolute"], ("False", "KEYWORD_ONLY"))
|
||||
self.assertEqual(result["packed"], ("False", "KEYWORD_ONLY"))
|
||||
|
||||
def test_positional_and_keyword(self) -> None:
|
||||
doc = ".. function:: foo(path, *, create=True)"
|
||||
result = parse_rst_function_sig(doc)
|
||||
self.assertEqual(result["path"], (None, "POSITIONAL_OR_KEYWORD"))
|
||||
self.assertEqual(result["create"], ("True", "KEYWORD_ONLY"))
|
||||
|
||||
def test_no_rst_directive(self) -> None:
|
||||
doc = "Just a plain docstring."
|
||||
result = parse_rst_function_sig(doc)
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_class_directive(self) -> None:
|
||||
doc = ".. class:: GPUBatch(type, buf, elem=None)"
|
||||
result = parse_rst_function_sig(doc)
|
||||
self.assertEqual(result["type"], (None, "POSITIONAL_OR_KEYWORD"))
|
||||
self.assertEqual(result["buf"], (None, "POSITIONAL_OR_KEYWORD"))
|
||||
self.assertEqual(result["elem"], ("None", "POSITIONAL_OR_KEYWORD"))
|
||||
|
||||
def test_positional_only_separator(self) -> None:
|
||||
doc = ".. class:: Color(/, rgb=(0.0, 0.0, 0.0))"
|
||||
result = parse_rst_function_sig(doc)
|
||||
self.assertNotIn("/", result)
|
||||
self.assertEqual(result["rgb"], ("...", "POSITIONAL_OR_KEYWORD"))
|
||||
|
||||
def test_rst_optional_brackets(self) -> None:
|
||||
doc = ".. method:: write(data[, position])"
|
||||
result = parse_rst_function_sig(doc)
|
||||
self.assertEqual(result["data"], (None, "POSITIONAL_OR_KEYWORD"))
|
||||
self.assertEqual(result["position"], (None, "POSITIONAL_OR_KEYWORD"))
|
||||
|
||||
def test_rst_nested_optional_brackets(self) -> None:
|
||||
doc = ".. method:: bar(a[, b[, c]])"
|
||||
result = parse_rst_function_sig(doc)
|
||||
self.assertIn("a", result)
|
||||
self.assertIn("b", result)
|
||||
self.assertIn("c", result)
|
||||
|
||||
def test_positional_only_with_named_params(self) -> None:
|
||||
doc = ".. function:: foo(a, b, /, c, *, d=True)"
|
||||
result = parse_rst_function_sig(doc)
|
||||
self.assertEqual(result["a"], (None, "POSITIONAL_ONLY"))
|
||||
self.assertEqual(result["b"], (None, "POSITIONAL_ONLY"))
|
||||
self.assertEqual(result["c"], (None, "POSITIONAL_OR_KEYWORD"))
|
||||
self.assertEqual(result["d"], ("True", "KEYWORD_ONLY"))
|
||||
|
||||
|
||||
class _NoSigCallable:
|
||||
"""Callable that raises ValueError on inspect.signature (like C builtins)."""
|
||||
|
||||
def __init__(self, doc: str) -> None:
|
||||
self.__doc__ = doc
|
||||
|
||||
def __call__(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def __signature__(self) -> None:
|
||||
raise ValueError("no signature")
|
||||
|
||||
|
||||
class TestRstOnlyParams(unittest.TestCase):
|
||||
def test_param_from_rst_sig_without_type_directive(self) -> None:
|
||||
"""Params in RST signature but without :type: should still appear."""
|
||||
func = _NoSigCallable(
|
||||
".. method:: foreach_get(seq)\n\nFast access to array data."
|
||||
)
|
||||
result = introspect_callable(func, "foreach_get")
|
||||
assert result is not None
|
||||
self.assertEqual(len(result["params"]), 1)
|
||||
self.assertEqual(result["params"][0]["name"], "seq")
|
||||
self.assertIsNone(result["params"][0]["type"])
|
||||
|
||||
def test_rst_params_merged_with_typed_params(self) -> None:
|
||||
"""RST params without :type: are added after typed params."""
|
||||
func = _NoSigCallable(".. method:: foo(a, b)\n\n:type a: str")
|
||||
result = introspect_callable(func, "foo")
|
||||
assert result is not None
|
||||
self.assertEqual(len(result["params"]), 2)
|
||||
self.assertEqual(result["params"][0]["name"], "a")
|
||||
self.assertEqual(result["params"][0]["type"], "str")
|
||||
self.assertEqual(result["params"][1]["name"], "b")
|
||||
self.assertIsNone(result["params"][1]["type"])
|
||||
|
||||
|
||||
class TestParamNameMismatch(unittest.TestCase):
|
||||
def test_docstring_name_used_when_sig_has_generic(self) -> None:
|
||||
"""When __text_signature__ uses 'object' but docstring uses 'string'."""
|
||||
|
||||
def fake_func(object: object) -> str: # noqa: A002
|
||||
""":type string: str
|
||||
:rtype: str"""
|
||||
return ""
|
||||
|
||||
result = introspect_callable(fake_func, "escape_identifier")
|
||||
assert result is not None
|
||||
self.assertEqual(result["params"][0]["name"], "string")
|
||||
self.assertEqual(result["params"][0]["type"], "str")
|
||||
|
||||
def test_matching_names_work_normally(self) -> None:
|
||||
def fake_func(path: str) -> str:
|
||||
""":type path: str
|
||||
:rtype: str"""
|
||||
return ""
|
||||
|
||||
result = introspect_callable(fake_func, "basename")
|
||||
assert result is not None
|
||||
self.assertEqual(result["params"][0]["name"], "path")
|
||||
self.assertEqual(result["params"][0]["type"], "str")
|
||||
|
||||
|
||||
class TestInferContextMemberType(unittest.TestCase):
|
||||
def test_objects_suffix(self) -> None:
|
||||
self.assertEqual(
|
||||
infer_context_member_type("selected_objects"), "Sequence[Object]"
|
||||
)
|
||||
self.assertEqual(
|
||||
infer_context_member_type("visible_objects"), "Sequence[Object]"
|
||||
)
|
||||
|
||||
def test_object_suffix(self) -> None:
|
||||
self.assertEqual(infer_context_member_type("active_object"), "Object")
|
||||
self.assertEqual(infer_context_member_type("edit_object"), "Object")
|
||||
|
||||
def test_bones_suffix(self) -> None:
|
||||
self.assertEqual(
|
||||
infer_context_member_type("selected_bones"), "Sequence[EditBone]"
|
||||
)
|
||||
|
||||
def test_bone_suffix(self) -> None:
|
||||
self.assertEqual(infer_context_member_type("active_bone"), "EditBone")
|
||||
|
||||
def test_fcurves_suffix(self) -> None:
|
||||
self.assertEqual(
|
||||
infer_context_member_type("visible_fcurves"), "Sequence[FCurve]"
|
||||
)
|
||||
|
||||
def test_strips_suffix(self) -> None:
|
||||
self.assertEqual(
|
||||
infer_context_member_type("selected_strips"), "Sequence[NlaStrip]"
|
||||
)
|
||||
|
||||
def test_unknown_returns_none(self) -> None:
|
||||
self.assertIsNone(infer_context_member_type("some_random_thing"))
|
||||
self.assertIsNone(infer_context_member_type("property"))
|
||||
|
||||
|
||||
class TestCleanDocstring(unittest.TestCase):
|
||||
def test_strips_arg_directives(self) -> None:
|
||||
doc = "Description of function.\n\n:arg name: The name.\n:type name: str"
|
||||
result = clean_docstring(doc)
|
||||
self.assertEqual(result, "Description of function.")
|
||||
|
||||
def test_preserves_multiline_description(self) -> None:
|
||||
doc = "First line.\nSecond line.\n\n:arg x: something"
|
||||
result = clean_docstring(doc)
|
||||
self.assertEqual(result, "First line.\nSecond line.")
|
||||
|
||||
def test_empty(self) -> None:
|
||||
self.assertEqual(clean_docstring(""), "")
|
||||
|
||||
def test_no_directives(self) -> None:
|
||||
doc = "Just a plain description."
|
||||
self.assertEqual(clean_docstring(doc), "Just a plain description.")
|
||||
|
||||
|
||||
class TestInferGetterReturnTypes(unittest.TestCase):
|
||||
def test_getter_inferred_from_setter(self) -> None:
|
||||
functions: list[FunctionData] = [
|
||||
{
|
||||
"name": "blend_get",
|
||||
"doc": "",
|
||||
"params": [],
|
||||
"return_type": None,
|
||||
"is_classmethod": False,
|
||||
},
|
||||
{
|
||||
"name": "blend_set",
|
||||
"doc": "",
|
||||
"params": [
|
||||
{
|
||||
"name": "mode",
|
||||
"type": "Literal['NONE', 'ALPHA']",
|
||||
"default": None,
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
}
|
||||
],
|
||||
"return_type": "None",
|
||||
"is_classmethod": False,
|
||||
},
|
||||
]
|
||||
infer_getter_return_types(functions)
|
||||
self.assertEqual(functions[0]["return_type"], "Literal['NONE', 'ALPHA']")
|
||||
|
||||
def test_getter_not_overwritten_when_typed(self) -> None:
|
||||
functions: list[FunctionData] = [
|
||||
{
|
||||
"name": "depth_mask_get",
|
||||
"doc": "",
|
||||
"params": [],
|
||||
"return_type": "bool",
|
||||
"is_classmethod": False,
|
||||
},
|
||||
{
|
||||
"name": "depth_mask_set",
|
||||
"doc": "",
|
||||
"params": [
|
||||
{
|
||||
"name": "value",
|
||||
"type": "bool",
|
||||
"default": None,
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
}
|
||||
],
|
||||
"return_type": "None",
|
||||
"is_classmethod": False,
|
||||
},
|
||||
]
|
||||
infer_getter_return_types(functions)
|
||||
self.assertEqual(functions[0]["return_type"], "bool")
|
||||
|
||||
def test_setter_with_multiple_params_ignored(self) -> None:
|
||||
functions: list[FunctionData] = [
|
||||
{
|
||||
"name": "scissor_get",
|
||||
"doc": "",
|
||||
"params": [],
|
||||
"return_type": None,
|
||||
"is_classmethod": False,
|
||||
},
|
||||
{
|
||||
"name": "scissor_set",
|
||||
"doc": "",
|
||||
"params": [
|
||||
{
|
||||
"name": "x",
|
||||
"type": "int",
|
||||
"default": None,
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
},
|
||||
{
|
||||
"name": "y",
|
||||
"type": "int",
|
||||
"default": None,
|
||||
"kind": "POSITIONAL_OR_KEYWORD",
|
||||
},
|
||||
],
|
||||
"return_type": "None",
|
||||
"is_classmethod": False,
|
||||
},
|
||||
]
|
||||
infer_getter_return_types(functions)
|
||||
self.assertIsNone(functions[0]["return_type"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user