mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
Tests: add tests for sub-classing & calling operators from Python
Add two tests, one to check that sub-classing works as expected, another for the operators callable types. Written to validate fixes for #155928 & #155996 and ensure there are no further regressions. Ref !155994
This commit is contained in:
parent
c1878785d6
commit
2edae0c5e0
3 changed files with 839 additions and 0 deletions
|
|
@ -320,6 +320,16 @@ add_blender_test(
|
|||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_pyapi_bmesh.py
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
operator_function_py_api
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_operator_function_py_api.py
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
operator_wrap_py_api
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_operator_wrap_py_api.py
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
script_pyapi_grease_pencil
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_pyapi_grease_pencil.py
|
||||
|
|
|
|||
140
tests/python/bl_operator_function_py_api.py
Normal file
140
tests/python/bl_operator_function_py_api.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Tests for `source/blender/python/intern/bpy_operator_function.cc`
|
||||
# Python calling into the operator system (using a callable function like type).
|
||||
#
|
||||
# ./blender.bin --background --python tests/python/bl_operator_function_py_api.py -- --verbose
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import unittest
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
class TestOperatorFunctionAccess(unittest.TestCase):
|
||||
|
||||
def test_access_operator(self):
|
||||
op = bpy.ops.object.select_all
|
||||
self.assertTrue(callable(op))
|
||||
|
||||
|
||||
class TestOperatorCall(unittest.TestCase):
|
||||
|
||||
def test_call_clear_recent_files(self):
|
||||
self.assertEqual(bpy.ops.wm.clear_recent_files(), {'FINISHED'})
|
||||
|
||||
|
||||
class TestOperatorCallPositionalArgs(unittest.TestCase):
|
||||
|
||||
def test_exec_default(self):
|
||||
self.assertEqual(bpy.ops.wm.clear_recent_files('EXEC_DEFAULT'), {'FINISHED'})
|
||||
|
||||
def test_invoke_default(self):
|
||||
self.assertEqual(bpy.ops.wm.clear_recent_files('INVOKE_DEFAULT'), {'FINISHED'})
|
||||
|
||||
def test_context_and_undo(self):
|
||||
self.assertEqual(bpy.ops.wm.clear_recent_files('EXEC_DEFAULT', True), {'FINISHED'})
|
||||
|
||||
def test_context_and_undo_int(self):
|
||||
self.assertEqual(bpy.ops.wm.clear_recent_files('EXEC_DEFAULT', 1), {'FINISHED'})
|
||||
|
||||
|
||||
class TestOperatorCallKeywordArgs(unittest.TestCase):
|
||||
|
||||
def test_enum_keyword(self):
|
||||
self.assertEqual(bpy.ops.object.select_all(action='TOGGLE'), {'FINISHED'})
|
||||
|
||||
def test_keyword_with_context(self):
|
||||
self.assertEqual(
|
||||
bpy.ops.object.select_all('EXEC_DEFAULT', action='TOGGLE'), {'FINISHED'})
|
||||
|
||||
def test_keyword_with_context_and_undo(self):
|
||||
self.assertEqual(
|
||||
bpy.ops.object.select_all('EXEC_DEFAULT', True, action='TOGGLE'), {'FINISHED'})
|
||||
|
||||
def test_multiple_keywords(self):
|
||||
self.assertEqual(
|
||||
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(0, 0, 0)), {'FINISHED'})
|
||||
|
||||
|
||||
class TestOperatorCallKeywordArgsInvalid(unittest.TestCase):
|
||||
|
||||
def test_invalid_enum_value(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.object.select_all(action='INVALID')
|
||||
|
||||
def test_wrong_type_for_enum(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.object.select_all(action=123)
|
||||
|
||||
def test_wrong_type_for_float(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.mesh.primitive_cube_add(size='big')
|
||||
|
||||
def test_wrong_type_for_bool(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.mesh.primitive_cube_add(align='WORLD', calc_uvs='yes')
|
||||
|
||||
def test_none_for_enum(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.object.select_all(action=None)
|
||||
|
||||
def test_none_for_float(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.mesh.primitive_cube_add(size=None)
|
||||
|
||||
def test_list_for_float(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.mesh.primitive_cube_add(size=[1.0])
|
||||
|
||||
def test_unknown_keyword(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.object.select_all(nonexistent=True)
|
||||
|
||||
def test_multiple_unknown_keywords(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.object.select_all(foo=1, bar=2)
|
||||
|
||||
def test_valid_and_unknown_keyword(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.object.select_all(action='TOGGLE', nonexistent=True)
|
||||
|
||||
def test_empty_string_for_enum(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.object.select_all(action='')
|
||||
|
||||
def test_non_string_keywords(self):
|
||||
for key in (1, None, (1, 2)):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.object.select_all(**{key: 'TOGGLE'})
|
||||
|
||||
|
||||
class TestOperatorCallInvalid(unittest.TestCase):
|
||||
|
||||
def test_invalid_context_string(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.wm.clear_recent_files('INVALID_CONTEXT')
|
||||
|
||||
def test_invalid_keyword_argument(self):
|
||||
with self.assertRaises(TypeError):
|
||||
bpy.ops.wm.clear_recent_files(nonexistent_prop=True)
|
||||
|
||||
def test_poll_failure(self):
|
||||
# Edit-mode operator cannot run without an active mesh in edit-mode.
|
||||
with self.assertRaises(RuntimeError):
|
||||
bpy.ops.mesh.subdivide()
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
|
||||
unittest.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
689
tests/python/bl_operator_wrap_py_api.py
Normal file
689
tests/python/bl_operator_wrap_py_api.py
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Tests for `source/blender/python/intern/bpy_operator_wrap.cc`
|
||||
# Python wrapping the operator system (using subclasses).
|
||||
#
|
||||
# ./blender.bin --background --python tests/python/bl_operator_wrap_py_api.py -- --verbose
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import contextlib
|
||||
import unittest
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def registered_operator(cls):
|
||||
bpy.utils.register_class(cls)
|
||||
try:
|
||||
yield cls
|
||||
finally:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
|
||||
class TestOperatorRegister(unittest.TestCase):
|
||||
|
||||
def test_register_and_unregister(self):
|
||||
class TESTING_OT_stub(bpy.types.Operator):
|
||||
bl_idname = "testing.stub"
|
||||
bl_label = "Stub"
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.utils.register_class(TESTING_OT_stub)
|
||||
self.assertTrue(bpy.ops.testing.stub.poll())
|
||||
bpy.utils.unregister_class(TESTING_OT_stub)
|
||||
with self.assertRaises(AttributeError):
|
||||
bpy.ops.testing.stub.poll()
|
||||
|
||||
def test_execute(self):
|
||||
class TESTING_OT_exec(bpy.types.Operator):
|
||||
bl_idname = "testing.exec"
|
||||
bl_label = "Exec"
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with registered_operator(TESTING_OT_exec):
|
||||
self.assertEqual(bpy.ops.testing.exec(), {'FINISHED'})
|
||||
|
||||
def test_execute_cancelled(self):
|
||||
class TESTING_OT_cancel(bpy.types.Operator):
|
||||
bl_idname = "testing.cancel"
|
||||
bl_label = "Cancel"
|
||||
|
||||
def execute(self, context):
|
||||
return {'CANCELLED'}
|
||||
|
||||
with registered_operator(TESTING_OT_cancel):
|
||||
self.assertEqual(bpy.ops.testing.cancel(), {'CANCELLED'})
|
||||
|
||||
def test_poll(self):
|
||||
class TESTING_OT_poll_pass(bpy.types.Operator):
|
||||
bl_idname = "testing.poll_pass"
|
||||
bl_label = "Poll Pass"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with registered_operator(TESTING_OT_poll_pass):
|
||||
self.assertTrue(bpy.ops.testing.poll_pass.poll())
|
||||
self.assertEqual(bpy.ops.testing.poll_pass(), {'FINISHED'})
|
||||
|
||||
def test_poll_fails(self):
|
||||
class TESTING_OT_poll_fail(bpy.types.Operator):
|
||||
bl_idname = "testing.poll_fail"
|
||||
bl_label = "Poll Fail"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return False
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with registered_operator(TESTING_OT_poll_fail):
|
||||
self.assertFalse(bpy.ops.testing.poll_fail.poll())
|
||||
with self.assertRaises(RuntimeError):
|
||||
bpy.ops.testing.poll_fail()
|
||||
|
||||
|
||||
class TestOperatorRegisterInvalid(unittest.TestCase):
|
||||
|
||||
def test_missing_bl_idname(self):
|
||||
class TESTING_OT_no_id(bpy.types.Operator):
|
||||
bl_label = "No ID"
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(RuntimeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_no_id)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
"Error: Registering operator class 'TESTING_OT_no_id', "
|
||||
"invalid bl_idname 'TESTING_OT_no_id', Invalid character at position 0\n",
|
||||
)
|
||||
|
||||
def test_missing_bl_label(self):
|
||||
class TESTING_OT_no_label(bpy.types.Operator):
|
||||
bl_idname = "testing.no_label"
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(AttributeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_no_label)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_no_label class to have an "bl_label" attribute',
|
||||
)
|
||||
|
||||
def test_double_register(self):
|
||||
class TESTING_OT_double(bpy.types.Operator):
|
||||
bl_idname = "testing.double"
|
||||
bl_label = "Double"
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.utils.register_class(TESTING_OT_double)
|
||||
self.addCleanup(bpy.utils.unregister_class, TESTING_OT_double)
|
||||
with self.assertRaises(ValueError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_double)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
"register_class(...): already registered as a subclass 'TESTING_OT_double'",
|
||||
)
|
||||
|
||||
def test_unregister_not_registered(self):
|
||||
class TESTING_OT_not_reg(bpy.types.Operator):
|
||||
bl_idname = "testing.not_reg"
|
||||
bl_label = "Not Reg"
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(RuntimeError) as ex:
|
||||
bpy.utils.unregister_class(TESTING_OT_not_reg)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
"unregister_class(...):, missing bl_rna attribute from '_RNAMeta' instance (may not be registered)",
|
||||
)
|
||||
|
||||
def test_bl_idname_no_dot(self):
|
||||
class TESTING_OT_nodot(bpy.types.Operator):
|
||||
bl_idname = "testingnodot"
|
||||
bl_label = "No Dot"
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(RuntimeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_nodot)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
"Error: Registering operator class 'TESTING_OT_nodot', "
|
||||
"invalid bl_idname 'testingnodot', Must contain 1 '.' character\n",
|
||||
)
|
||||
|
||||
def test_bl_idname_empty(self):
|
||||
class TESTING_OT_empty(bpy.types.Operator):
|
||||
bl_idname = ""
|
||||
bl_label = "Empty"
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(RuntimeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_empty)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
"Error: Registering operator class 'TESTING_OT_empty', invalid bl_idname '', Must contain 1 '.' character\n",
|
||||
)
|
||||
|
||||
def test_bl_idname_uppercase(self):
|
||||
class TESTING_OT_upper(bpy.types.Operator):
|
||||
bl_idname = "testing.Upper"
|
||||
bl_label = "Upper"
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(RuntimeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_upper)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
"Error: Registering operator class 'TESTING_OT_upper', "
|
||||
"invalid bl_idname 'testing.Upper', Invalid character at position 8\n",
|
||||
)
|
||||
|
||||
def test_bl_idname_spaces(self):
|
||||
class TESTING_OT_spaces(bpy.types.Operator):
|
||||
bl_idname = "testing.has spaces"
|
||||
bl_label = "Spaces"
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(RuntimeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_spaces)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
"Error: Registering operator class 'TESTING_OT_spaces', "
|
||||
"invalid bl_idname 'testing.has spaces', Invalid character at position 11\n",
|
||||
)
|
||||
|
||||
def test_bl_label_wrong_type(self):
|
||||
class TESTING_OT_intlabel(bpy.types.Operator):
|
||||
bl_idname = "testing.intlabel"
|
||||
bl_label = 123
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(TypeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_intlabel)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
"validating class: Operator.bl_label expected a string type, not int",
|
||||
)
|
||||
|
||||
|
||||
class TestOperatorRegisterCallbackSignature(unittest.TestCase):
|
||||
|
||||
def test_execute_missing_arg(self):
|
||||
class TESTING_OT_exec_missing(bpy.types.Operator):
|
||||
bl_idname = "testing.exec_missing"
|
||||
bl_label = "Exec Missing"
|
||||
|
||||
def execute(self):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(ValueError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_exec_missing)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_exec_missing class "execute" function to have 2 args, found 1',
|
||||
)
|
||||
|
||||
def test_execute_extra_arg(self):
|
||||
class TESTING_OT_exec_extra(bpy.types.Operator):
|
||||
bl_idname = "testing.exec_extra"
|
||||
bl_label = "Exec Extra"
|
||||
|
||||
def execute(self, context, extra):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(ValueError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_exec_extra)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_exec_extra class "execute" function to have 2 args, found 3',
|
||||
)
|
||||
|
||||
def test_invoke_missing_arg(self):
|
||||
class TESTING_OT_invoke_missing(bpy.types.Operator):
|
||||
bl_idname = "testing.invoke_missing"
|
||||
bl_label = "Invoke Missing"
|
||||
|
||||
def invoke(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(ValueError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_invoke_missing)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_invoke_missing class "invoke" function to have 3 args, found 2',
|
||||
)
|
||||
|
||||
def test_draw_missing_arg(self):
|
||||
class TESTING_OT_draw_missing(bpy.types.Operator):
|
||||
bl_idname = "testing.draw_missing"
|
||||
bl_label = "Draw Missing"
|
||||
|
||||
def draw(self):
|
||||
pass
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(ValueError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_draw_missing)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_draw_missing class "draw" function to have 2 args, found 1',
|
||||
)
|
||||
|
||||
def test_poll_extra_arg(self):
|
||||
class TESTING_OT_poll_extra(bpy.types.Operator):
|
||||
bl_idname = "testing.poll_extra"
|
||||
bl_label = "Poll Extra"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context, extra):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(ValueError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_poll_extra)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_poll_extra class "poll" function to have 2 args, found 3',
|
||||
)
|
||||
|
||||
|
||||
class TestOperatorRegisterCallbackWrongType(unittest.TestCase):
|
||||
|
||||
def test_execute_as_classmethod(self):
|
||||
class TESTING_OT_exec_cls(bpy.types.Operator):
|
||||
bl_idname = "testing.exec_cls"
|
||||
bl_label = "Exec Cls"
|
||||
|
||||
@classmethod # Wrong: must be a regular `method`.
|
||||
def execute(cls, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(TypeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_exec_cls)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_exec_cls class "execute" attribute to be a function, not a method',
|
||||
)
|
||||
|
||||
def test_execute_as_staticmethod(self):
|
||||
class TESTING_OT_exec_static(bpy.types.Operator):
|
||||
bl_idname = "testing.exec_static"
|
||||
bl_label = "Exec Static"
|
||||
|
||||
@staticmethod # Wrong: must be a regular `method`.
|
||||
def execute(context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(ValueError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_exec_static)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_exec_static class "execute" function to have 2 args, found 1',
|
||||
)
|
||||
|
||||
def test_execute_as_property(self):
|
||||
class TESTING_OT_exec_prop(bpy.types.Operator):
|
||||
bl_idname = "testing.exec_prop"
|
||||
bl_label = "Exec Prop"
|
||||
|
||||
@property # Wrong: must be a regular `method`.
|
||||
def execute(self):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(TypeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_exec_prop)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_exec_prop class "execute" attribute to be a function, not a property',
|
||||
)
|
||||
|
||||
def test_invoke_as_classmethod(self):
|
||||
class TESTING_OT_invoke_cls(bpy.types.Operator):
|
||||
bl_idname = "testing.invoke_cls"
|
||||
bl_label = "Invoke Cls"
|
||||
|
||||
@classmethod # Wrong: must be a regular `method`.
|
||||
def invoke(cls, context, event):
|
||||
return {'FINISHED'}
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(TypeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_invoke_cls)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_invoke_cls class "invoke" attribute to be a function, not a method',
|
||||
)
|
||||
|
||||
def test_poll_as_staticmethod(self):
|
||||
class TESTING_OT_poll_static(bpy.types.Operator):
|
||||
bl_idname = "testing.poll_static"
|
||||
bl_label = "Poll Static"
|
||||
|
||||
@staticmethod # Wrong: must be a `@classmethod`.
|
||||
def poll(context):
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(TypeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_poll_static)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_poll_static class "poll" '
|
||||
'attribute to be a static/class method, not a function',
|
||||
)
|
||||
|
||||
def test_poll_as_regular_method(self):
|
||||
class TESTING_OT_poll_regular(bpy.types.Operator):
|
||||
bl_idname = "testing.poll_regular"
|
||||
bl_label = "Poll Regular"
|
||||
|
||||
def poll(self, context): # Wrong: must be a `@classmethod`.
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(TypeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_poll_regular)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_poll_regular class "poll" '
|
||||
'attribute to be a static/class method, not a function',
|
||||
)
|
||||
|
||||
def test_draw_as_classmethod(self):
|
||||
class TESTING_OT_draw_cls(bpy.types.Operator):
|
||||
bl_idname = "testing.draw_cls"
|
||||
bl_label = "Draw Cls"
|
||||
|
||||
@classmethod # Wrong: must be a regular `method`.
|
||||
def draw(cls, context):
|
||||
pass
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
with self.assertRaises(TypeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_draw_cls)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_draw_cls class "draw" attribute to be a function, not a method',
|
||||
)
|
||||
|
||||
def test_execute_as_string(self):
|
||||
class TESTING_OT_exec_str(bpy.types.Operator):
|
||||
bl_idname = "testing.exec_str"
|
||||
bl_label = "Exec Str"
|
||||
execute = "not_a_function" # Wrong: must be a `callable`.
|
||||
|
||||
with self.assertRaises(TypeError) as ex:
|
||||
bpy.utils.register_class(TESTING_OT_exec_str)
|
||||
self.assertEqual(
|
||||
str(ex.exception),
|
||||
'expected Operator, TESTING_OT_exec_str class "execute" attribute to be a function, not a str',
|
||||
)
|
||||
|
||||
|
||||
class TestOperatorRegisterComplete(unittest.TestCase):
|
||||
"""Operator using every supported flag and callback."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
class TESTING_OT_complete(bpy.types.Operator):
|
||||
bl_idname = "testing.complete"
|
||||
bl_label = "Complete"
|
||||
bl_description = "A complete operator"
|
||||
bl_translation_context = "*"
|
||||
bl_undo_group = ""
|
||||
bl_options = {item.identifier for item in bpy.types.Operator.bl_rna.properties['bl_options'].enum_items}
|
||||
bl_cursor_pending = 'DEFAULT'
|
||||
|
||||
value: bpy.props.IntProperty(name="Value", default=0)
|
||||
last_value = None
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def invoke(self, context, event):
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
type(self).last_value = self.value
|
||||
return {'FINISHED'}
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.prop(self, 'value')
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
return "Complete operator description"
|
||||
|
||||
def cancel(self, context):
|
||||
pass
|
||||
|
||||
def modal(self, context, event):
|
||||
return {'FINISHED'}
|
||||
|
||||
def check(self, context):
|
||||
return False
|
||||
|
||||
cls._op_class = TESTING_OT_complete
|
||||
bpy.utils.register_class(TESTING_OT_complete)
|
||||
cls.op = bpy.ops.testing.complete
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
bpy.utils.unregister_class(cls._op_class)
|
||||
|
||||
def test_all_callbacks_implemented(self):
|
||||
"""Ensure the operator implements every registered callback from RNA."""
|
||||
registered_callbacks = {
|
||||
fn_name for fn_name, fn in bpy.types.Operator.bl_rna.functions.items()
|
||||
if fn.is_registered or fn.is_registered_optional
|
||||
}
|
||||
implemented_callbacks = {
|
||||
fn_name for fn_name in registered_callbacks
|
||||
if fn_name in self._op_class.__dict__
|
||||
}
|
||||
self.assertEqual(registered_callbacks, implemented_callbacks)
|
||||
|
||||
def test_all_properties_implemented(self):
|
||||
"""Ensure the operator implements every registered property from RNA."""
|
||||
registered_properties = {
|
||||
prop_name for prop_name, prop in bpy.types.Operator.bl_rna.properties.items()
|
||||
if prop_name != "rna_type" and (prop.is_registered or prop.is_registered_optional)
|
||||
}
|
||||
implemented_properties = {
|
||||
prop_name for prop_name in registered_properties
|
||||
if prop_name in self._op_class.__dict__
|
||||
}
|
||||
self.assertEqual(registered_properties, implemented_properties)
|
||||
|
||||
def test_poll(self):
|
||||
self.assertTrue(self.op.poll())
|
||||
|
||||
def test_execute(self):
|
||||
self.assertEqual(self.op(value=99), {'FINISHED'})
|
||||
self.assertEqual(self._op_class.last_value, 99)
|
||||
|
||||
def test_invoke(self):
|
||||
self.assertEqual(self.op('INVOKE_DEFAULT', value=7), {'FINISHED'})
|
||||
self.assertEqual(self._op_class.last_value, 7)
|
||||
|
||||
def test_idname(self):
|
||||
self.assertEqual(self.op.idname(), 'TESTING_OT_complete')
|
||||
|
||||
def test_idname_py(self):
|
||||
self.assertEqual(self.op.idname_py(), 'testing.complete')
|
||||
|
||||
def test_get_rna_type(self):
|
||||
rna = self.op.get_rna_type()
|
||||
self.assertEqual(rna.identifier, 'TESTING_OT_complete')
|
||||
|
||||
def test_bl_options(self):
|
||||
all_options = {item.identifier for item in bpy.types.Operator.bl_rna.properties['bl_options'].enum_items}
|
||||
self.assertEqual(self.op.bl_options, all_options)
|
||||
|
||||
def test_doc(self):
|
||||
self.assertEqual(self.op.__doc__, "bpy.ops.testing.complete(value=0)\nA complete operator")
|
||||
|
||||
def test_str(self):
|
||||
s = str(self.op)
|
||||
self.assertTrue(s.startswith("<function bpy.ops.testing.complete at 0x"))
|
||||
self.assertTrue(s.endswith(">"))
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual(repr(self.op), "bpy.ops.testing.complete(value=0)")
|
||||
|
||||
|
||||
class TestOperatorWrapProperties(unittest.TestCase):
|
||||
|
||||
def _make_operator(self, bl_idname, prop_type):
|
||||
"""Create an operator class that captures `value` on execute."""
|
||||
|
||||
class _OT(bpy.types.Operator):
|
||||
bl_label = "Test"
|
||||
last_value = None
|
||||
|
||||
def execute(self, context):
|
||||
v = self.value
|
||||
type(self).last_value = tuple(v) if hasattr(v, "__len__") and not isinstance(v, (str, bytes)) else v
|
||||
return {'FINISHED'}
|
||||
|
||||
_OT.bl_idname = bl_idname
|
||||
_OT.__name__ = "TESTING_OT_" + bl_idname.split(".")[1]
|
||||
_OT.__annotations__ = {"value": prop_type}
|
||||
return _OT
|
||||
|
||||
def _run_property_test(self, *, bl_idname, prop_type, value, expected=None):
|
||||
if expected is None:
|
||||
expected = value
|
||||
op_cls = self._make_operator(bl_idname, prop_type)
|
||||
with registered_operator(op_cls):
|
||||
getattr(bpy.ops.testing, bl_idname.split(".")[1])(value=value)
|
||||
result = op_cls.last_value
|
||||
if isinstance(expected, float):
|
||||
self.assertAlmostEqual(result, expected, places=2)
|
||||
elif isinstance(expected, (list, tuple)):
|
||||
self.assertEqual(list(result), list(expected))
|
||||
else:
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_string_property(self):
|
||||
self._run_property_test(
|
||||
bl_idname="testing.str_prop",
|
||||
prop_type=bpy.props.StringProperty(),
|
||||
value="hello",
|
||||
)
|
||||
|
||||
def test_int_property(self):
|
||||
self._run_property_test(
|
||||
bl_idname="testing.int_prop",
|
||||
prop_type=bpy.props.IntProperty(),
|
||||
value=42,
|
||||
)
|
||||
|
||||
def test_float_property(self):
|
||||
self._run_property_test(
|
||||
bl_idname="testing.float_prop",
|
||||
prop_type=bpy.props.FloatProperty(),
|
||||
value=3.14,
|
||||
)
|
||||
|
||||
def test_bool_property(self):
|
||||
self._run_property_test(
|
||||
bl_idname="testing.bool_prop",
|
||||
prop_type=bpy.props.BoolProperty(),
|
||||
value=True,
|
||||
)
|
||||
|
||||
def test_enum_property(self):
|
||||
self._run_property_test(
|
||||
bl_idname="testing.enum_prop",
|
||||
prop_type=bpy.props.EnumProperty(items=[('A', "A", ""), ('B', "B", "")]),
|
||||
value='B',
|
||||
)
|
||||
|
||||
def test_byte_string_property(self):
|
||||
self._run_property_test(
|
||||
bl_idname="testing.byte_str_prop",
|
||||
prop_type=bpy.props.StringProperty(subtype='BYTE_STRING'),
|
||||
value=b"hello",
|
||||
)
|
||||
|
||||
def test_int_vector_property(self):
|
||||
self._run_property_test(
|
||||
bl_idname="testing.int_vec_prop",
|
||||
prop_type=bpy.props.IntVectorProperty(size=3),
|
||||
value=(1, 2, 3),
|
||||
)
|
||||
|
||||
def test_float_vector_property(self):
|
||||
self._run_property_test(
|
||||
bl_idname="testing.float_vec_prop",
|
||||
prop_type=bpy.props.FloatVectorProperty(size=3),
|
||||
value=(1.5, 2.5, 3.5),
|
||||
)
|
||||
|
||||
def test_bool_vector_property(self):
|
||||
self._run_property_test(
|
||||
bl_idname="testing.bool_vec_prop",
|
||||
prop_type=bpy.props.BoolVectorProperty(size=3),
|
||||
value=(True, False, True),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
|
||||
unittest.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue