From 6134869371cf4e7ae79515690960a563fd0db40e Mon Sep 17 00:00:00 2001 From: John McCardle Date: Thu, 3 Jul 2025 20:41:03 -0400 Subject: [PATCH] Add validation to keypressScene() for non-callable arguments Added PyCallable_Check validation to ensure keypressScene() only accepts callable objects. Now properly raises TypeError with a clear error message when passed non-callable arguments like strings, numbers, None, or dicts. --- src/McRFPy_API.cpp | 8 +++ tests/keypress_scene_validation_test.py | 93 +++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 tests/keypress_scene_validation_test.py diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 54bbcf3..531ec5d 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -603,6 +603,13 @@ PyObject* McRFPy_API::_createScene(PyObject* self, PyObject* args) { PyObject* McRFPy_API::_keypressScene(PyObject* self, PyObject* args) { PyObject* callable; if (!PyArg_ParseTuple(args, "O", &callable)) return NULL; + + // Validate that the argument is callable + if (!PyCallable_Check(callable)) { + PyErr_SetString(PyExc_TypeError, "keypressScene() argument must be callable"); + return NULL; + } + /* if (game->currentScene()->key_callable != NULL and game->currentScene()->key_callable != Py_None) { @@ -613,6 +620,7 @@ PyObject* McRFPy_API::_keypressScene(PyObject* self, PyObject* args) { Py_INCREF(Py_None); */ game->currentScene()->key_callable = std::make_unique(callable); + Py_INCREF(Py_None); return Py_None; } diff --git a/tests/keypress_scene_validation_test.py b/tests/keypress_scene_validation_test.py new file mode 100644 index 0000000..4bd2982 --- /dev/null +++ b/tests/keypress_scene_validation_test.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +""" +Test for keypressScene() validation - should reject non-callable arguments +""" + +def test_keypress_validation(timer_name): + """Test that keypressScene validates its argument is callable""" + import mcrfpy + import sys + + print("Testing keypressScene() validation...") + + # Create test scene + mcrfpy.createScene("test") + mcrfpy.setScene("test") + + # Test 1: Valid callable (function) + def key_handler(key, action): + print(f"Key pressed: {key}, action: {action}") + + try: + mcrfpy.keypressScene(key_handler) + print("✓ Accepted valid function as key handler") + except Exception as e: + print(f"✗ Rejected valid function: {e}") + raise + + # Test 2: Valid callable (lambda) + try: + mcrfpy.keypressScene(lambda k, a: None) + print("✓ Accepted valid lambda as key handler") + except Exception as e: + print(f"✗ Rejected valid lambda: {e}") + raise + + # Test 3: Invalid - string + try: + mcrfpy.keypressScene("not callable") + print("✗ Should have rejected string as key handler") + except TypeError as e: + print(f"✓ Correctly rejected string: {e}") + except Exception as e: + print(f"✗ Wrong exception type for string: {e}") + raise + + # Test 4: Invalid - number + try: + mcrfpy.keypressScene(42) + print("✗ Should have rejected number as key handler") + except TypeError as e: + print(f"✓ Correctly rejected number: {e}") + except Exception as e: + print(f"✗ Wrong exception type for number: {e}") + raise + + # Test 5: Invalid - None + try: + mcrfpy.keypressScene(None) + print("✗ Should have rejected None as key handler") + except TypeError as e: + print(f"✓ Correctly rejected None: {e}") + except Exception as e: + print(f"✗ Wrong exception type for None: {e}") + raise + + # Test 6: Invalid - dict + try: + mcrfpy.keypressScene({"not": "callable"}) + print("✗ Should have rejected dict as key handler") + except TypeError as e: + print(f"✓ Correctly rejected dict: {e}") + except Exception as e: + print(f"✗ Wrong exception type for dict: {e}") + raise + + # Test 7: Valid callable class instance + class KeyHandler: + def __call__(self, key, action): + print(f"Class handler: {key}, {action}") + + try: + mcrfpy.keypressScene(KeyHandler()) + print("✓ Accepted valid callable class instance") + except Exception as e: + print(f"✗ Rejected valid callable class: {e}") + raise + + print("\n✅ keypressScene() validation test PASSED") + sys.exit(0) + +# Execute the test after a short delay +import mcrfpy +mcrfpy.setTimer("test", test_keypress_validation, 100) \ No newline at end of file