Remove legacy string enum comparisons from InputState/Key/MouseButton, closes #306

Removed custom __eq__/__ne__ that allowed comparing enums to legacy string
names (e.g., Key.ESCAPE == "Escape"). Removed _legacy_names dicts and
to_legacy_string() functions. Kept from_legacy_string() in PyKey.cpp as
it's used by C++ event dispatch. Updated ~50 Python test/demo/cookbook
files to use enum members instead of string comparisons. Also updates
grid.position -> grid.pos in files that had both types of changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-04-09 22:19:02 -04:00
commit 6d5e99a114
52 changed files with 372 additions and 533 deletions

View file

@ -4,24 +4,19 @@
// Static storage for cached enum class reference
PyObject* PyInputState::input_state_enum_class = nullptr;
// InputState entries - maps enum name to value and legacy string
// InputState entries - maps enum name to value
struct InputStateEntry {
const char* name; // Python enum name (UPPER_SNAKE_CASE)
int value; // Integer value
const char* legacy; // Legacy string name for backwards compatibility
};
static const InputStateEntry input_state_table[] = {
{"PRESSED", 0, "start"},
{"RELEASED", 1, "end"},
{"PRESSED", 0},
{"RELEASED", 1},
};
static const int NUM_INPUT_STATE_ENTRIES = sizeof(input_state_table) / sizeof(input_state_table[0]);
const char* PyInputState::to_legacy_string(bool pressed) {
return pressed ? "start" : "end";
}
PyObject* PyInputState::create_enum_class(PyObject* module) {
// Build the enum definition dynamically from the table
std::ostringstream code;
@ -31,13 +26,8 @@ PyObject* PyInputState::create_enum_class(PyObject* module) {
code << " \"\"\"Enum representing input event states (pressed/released).\n";
code << " \n";
code << " Values:\n";
code << " PRESSED: Key or button was pressed (legacy: 'start')\n";
code << " RELEASED: Key or button was released (legacy: 'end')\n";
code << " \n";
code << " These enum values compare equal to their legacy string equivalents\n";
code << " for backwards compatibility:\n";
code << " InputState.PRESSED == 'start' # True\n";
code << " InputState.RELEASED == 'end' # True\n";
code << " PRESSED: Key or button was pressed\n";
code << " RELEASED: Key or button was released\n";
code << " \"\"\"\n";
// Add enum members
@ -45,42 +35,10 @@ PyObject* PyInputState::create_enum_class(PyObject* module) {
code << " " << input_state_table[i].name << " = " << input_state_table[i].value << "\n";
}
// Add legacy names and custom methods AFTER class creation
// (IntEnum doesn't allow dict attributes during class definition)
code << "\n# Add legacy name mapping after class creation\n";
code << "InputState._legacy_names = {\n";
for (int i = 0; i < NUM_INPUT_STATE_ENTRIES; i++) {
code << " " << input_state_table[i].value << ": \"" << input_state_table[i].legacy << "\",\n";
}
code << "}\n\n";
code << R"(
def _InputState_eq(self, other):
if isinstance(other, str):
# Check enum name match (e.g., "PRESSED")
if self.name == other:
return True
# Check legacy name match (e.g., "start")
legacy = type(self)._legacy_names.get(self.value)
if legacy and legacy == other:
return True
return False
# Fall back to int comparison for IntEnum
return int.__eq__(int(self), other)
InputState.__eq__ = _InputState_eq
def _InputState_ne(self, other):
result = type(self).__eq__(self, other)
if result is NotImplemented:
return result
return not result
InputState.__ne__ = _InputState_ne
InputState.__hash__ = lambda self: hash(int(self))
InputState.__repr__ = lambda self: f"{type(self).__name__}.{self.name}"
InputState.__str__ = lambda self: self.name
)";
code << "\n";
code << "InputState.__hash__ = lambda self: hash(int(self))\n";
code << "InputState.__repr__ = lambda self: f\"{type(self).__name__}.{self.name}\"\n";
code << "InputState.__str__ = lambda self: self.name\n";
std::string code_str = code.str();
@ -167,25 +125,22 @@ int PyInputState::from_arg(PyObject* arg, bool* out_pressed) {
return 0;
}
// Accept string (both new and legacy names)
// Accept string (enum name only)
if (PyUnicode_Check(arg)) {
const char* name = PyUnicode_AsUTF8(arg);
if (!name) {
return 0;
}
// Check all entries for both name and legacy match
for (int i = 0; i < NUM_INPUT_STATE_ENTRIES; i++) {
if (strcmp(name, input_state_table[i].name) == 0 ||
strcmp(name, input_state_table[i].legacy) == 0) {
if (strcmp(name, input_state_table[i].name) == 0) {
*out_pressed = (input_state_table[i].value == 0);
return 1;
}
}
PyErr_Format(PyExc_ValueError,
"Unknown InputState: '%s'. Use InputState.PRESSED, InputState.RELEASED, "
"or legacy strings 'start', 'end'.", name);
"Unknown InputState: '%s'. Use InputState.PRESSED or InputState.RELEASED.", name);
return 0;
}

View file

@ -6,10 +6,8 @@
// Stored as a module attribute: mcrfpy.InputState
//
// Values:
// PRESSED = 0 (corresponds to "start" in legacy API)
// RELEASED = 1 (corresponds to "end" in legacy API)
//
// The enum compares equal to both its name ("PRESSED") and legacy string ("start")
// PRESSED = 0
// RELEASED = 1
class PyInputState {
public:
@ -18,14 +16,11 @@ public:
static PyObject* create_enum_class(PyObject* module);
// Helper to extract input state from Python arg
// Accepts InputState enum, string (for backwards compatibility), int, or None
// Accepts InputState enum, string (enum name), or int
// Returns 1 on success, 0 on error (with exception set)
// out_pressed is set to true for PRESSED/start, false for RELEASED/end
// out_pressed is set to true for PRESSED, false for RELEASED
static int from_arg(PyObject* arg, bool* out_pressed);
// Convert bool to legacy string name (for passing to callbacks)
static const char* to_legacy_string(bool pressed);
// Cached reference to the InputState enum class for fast type checking
static PyObject* input_state_enum_class;

View file

@ -143,15 +143,6 @@ static const KeyEntry key_table[] = {
static const int NUM_KEY_ENTRIES = sizeof(key_table) / sizeof(key_table[0]);
const char* PyKey::to_legacy_string(sf::Keyboard::Key key) {
for (int i = 0; i < NUM_KEY_ENTRIES; i++) {
if (key_table[i].value == static_cast<int>(key)) {
return key_table[i].legacy;
}
}
return "Unknown";
}
sf::Keyboard::Key PyKey::from_legacy_string(const char* name) {
for (int i = 0; i < NUM_KEY_ENTRIES; i++) {
if (strcmp(key_table[i].legacy, name) == 0 ||
@ -181,11 +172,6 @@ PyObject* PyKey::create_enum_class(PyObject* module) {
code << " Navigation: LEFT, RIGHT, UP, DOWN, HOME, END, PAGE_UP, PAGE_DOWN\n";
code << " Editing: ENTER, BACKSPACE, DELETE, INSERT, TAB, SPACE\n";
code << " Symbols: COMMA, PERIOD, SLASH, SEMICOLON, etc.\n";
code << " \n";
code << " These enum values compare equal to their legacy string equivalents\n";
code << " for backwards compatibility:\n";
code << " Key.ESCAPE == 'Escape' # True\n";
code << " Key.LEFT_SHIFT == 'LShift' # True\n";
code << " \"\"\"\n";
// Add enum members
@ -193,42 +179,10 @@ PyObject* PyKey::create_enum_class(PyObject* module) {
code << " " << key_table[i].name << " = " << key_table[i].value << "\n";
}
// Add legacy names and custom methods AFTER class creation
// (IntEnum doesn't allow dict attributes during class definition)
code << "\n# Add legacy name mapping after class creation\n";
code << "Key._legacy_names = {\n";
for (int i = 0; i < NUM_KEY_ENTRIES; i++) {
code << " " << key_table[i].value << ": \"" << key_table[i].legacy << "\",\n";
}
code << "}\n\n";
code << R"(
def _Key_eq(self, other):
if isinstance(other, str):
# Check enum name match (e.g., "ESCAPE")
if self.name == other:
return True
# Check legacy name match (e.g., "Escape")
legacy = type(self)._legacy_names.get(self.value)
if legacy and legacy == other:
return True
return False
# Fall back to int comparison for IntEnum
return int.__eq__(int(self), other)
Key.__eq__ = _Key_eq
def _Key_ne(self, other):
result = type(self).__eq__(self, other)
if result is NotImplemented:
return result
return not result
Key.__ne__ = _Key_ne
Key.__hash__ = lambda self: hash(int(self))
Key.__repr__ = lambda self: f"{type(self).__name__}.{self.name}"
Key.__str__ = lambda self: self.name
)";
code << "\n";
code << "Key.__hash__ = lambda self: hash(int(self))\n";
code << "Key.__repr__ = lambda self: f\"{type(self).__name__}.{self.name}\"\n";
code << "Key.__str__ = lambda self: self.name\n";
std::string code_str = code.str();
@ -315,25 +269,22 @@ int PyKey::from_arg(PyObject* arg, sf::Keyboard::Key* out_key) {
return 0;
}
// Accept string (both new and legacy names)
// Accept string (enum name only)
if (PyUnicode_Check(arg)) {
const char* name = PyUnicode_AsUTF8(arg);
if (!name) {
return 0;
}
// Check all entries for both name and legacy match
for (int i = 0; i < NUM_KEY_ENTRIES; i++) {
if (strcmp(name, key_table[i].name) == 0 ||
strcmp(name, key_table[i].legacy) == 0) {
if (strcmp(name, key_table[i].name) == 0) {
*out_key = static_cast<sf::Keyboard::Key>(key_table[i].value);
return 1;
}
}
PyErr_Format(PyExc_ValueError,
"Unknown Key: '%s'. Use Key enum members (e.g., Key.ESCAPE, Key.A) "
"or legacy strings (e.g., 'Escape', 'A').", name);
"Unknown Key: '%s'. Use Key enum members (e.g., Key.ESCAPE, Key.A).", name);
return 0;
}

View file

@ -6,7 +6,6 @@
// Stored as a module attribute: mcrfpy.Key
//
// Values map to sf::Keyboard::Key enum values.
// The enum compares equal to both its name ("ESCAPE") and legacy string ("Escape")
//
// Naming convention:
// - Letters: A, B, C, ... Z
@ -24,14 +23,11 @@ public:
static PyObject* create_enum_class(PyObject* module);
// Helper to extract key from Python arg
// Accepts Key enum, string (for backwards compatibility), int, or None
// Accepts Key enum, string (enum name), or int
// Returns 1 on success, 0 on error (with exception set)
static int from_arg(PyObject* arg, sf::Keyboard::Key* out_key);
// Convert sf::Keyboard::Key to legacy string name (for passing to callbacks)
static const char* to_legacy_string(sf::Keyboard::Key key);
// Convert legacy string to sf::Keyboard::Key
// Convert string name to sf::Keyboard::Key (used by C++ event dispatch)
// Returns sf::Keyboard::Unknown if not found
static sf::Keyboard::Key from_legacy_string(const char* name);

View file

@ -4,11 +4,10 @@
// Static storage for cached enum class reference
PyObject* PyMouseButton::mouse_button_enum_class = nullptr;
// MouseButton entries - maps enum name to value and legacy string
// MouseButton entries - maps enum name to value
struct MouseButtonEntry {
const char* name; // Python enum name (UPPER_SNAKE_CASE)
int value; // Integer value (matches sf::Mouse::Button)
const char* legacy; // Legacy string name for backwards compatibility
};
// Custom values for scroll wheel (beyond sf::Mouse::Button range)
@ -16,26 +15,17 @@ static const int SCROLL_UP_VALUE = 10;
static const int SCROLL_DOWN_VALUE = 11;
static const MouseButtonEntry mouse_button_table[] = {
{"LEFT", sf::Mouse::Left, "left"},
{"RIGHT", sf::Mouse::Right, "right"},
{"MIDDLE", sf::Mouse::Middle, "middle"},
{"X1", sf::Mouse::XButton1, "x1"},
{"X2", sf::Mouse::XButton2, "x2"},
{"SCROLL_UP", SCROLL_UP_VALUE, "wheel_up"},
{"SCROLL_DOWN", SCROLL_DOWN_VALUE, "wheel_down"},
{"LEFT", sf::Mouse::Left},
{"RIGHT", sf::Mouse::Right},
{"MIDDLE", sf::Mouse::Middle},
{"X1", sf::Mouse::XButton1},
{"X2", sf::Mouse::XButton2},
{"SCROLL_UP", SCROLL_UP_VALUE},
{"SCROLL_DOWN", SCROLL_DOWN_VALUE},
};
static const int NUM_MOUSE_BUTTON_ENTRIES = sizeof(mouse_button_table) / sizeof(mouse_button_table[0]);
const char* PyMouseButton::to_legacy_string(sf::Mouse::Button button) {
for (int i = 0; i < NUM_MOUSE_BUTTON_ENTRIES; i++) {
if (mouse_button_table[i].value == static_cast<int>(button)) {
return mouse_button_table[i].legacy;
}
}
return "left"; // Default fallback
}
PyObject* PyMouseButton::create_enum_class(PyObject* module) {
// Build the enum definition dynamically from the table
std::ostringstream code;
@ -45,19 +35,13 @@ PyObject* PyMouseButton::create_enum_class(PyObject* module) {
code << " \"\"\"Enum representing mouse buttons and scroll wheel.\n";
code << " \n";
code << " Values:\n";
code << " LEFT: Left mouse button (legacy: 'left')\n";
code << " RIGHT: Right mouse button (legacy: 'right')\n";
code << " MIDDLE: Middle mouse button / scroll wheel click (legacy: 'middle')\n";
code << " X1: Extra mouse button 1 (legacy: 'x1')\n";
code << " X2: Extra mouse button 2 (legacy: 'x2')\n";
code << " SCROLL_UP: Scroll wheel up (legacy: 'wheel_up')\n";
code << " SCROLL_DOWN: Scroll wheel down (legacy: 'wheel_down')\n";
code << " \n";
code << " These enum values compare equal to their legacy string equivalents\n";
code << " for backwards compatibility:\n";
code << " MouseButton.LEFT == 'left' # True\n";
code << " MouseButton.RIGHT == 'right' # True\n";
code << " MouseButton.SCROLL_UP == 'wheel_up' # True\n";
code << " LEFT: Left mouse button\n";
code << " RIGHT: Right mouse button\n";
code << " MIDDLE: Middle mouse button / scroll wheel click\n";
code << " X1: Extra mouse button 1\n";
code << " X2: Extra mouse button 2\n";
code << " SCROLL_UP: Scroll wheel up\n";
code << " SCROLL_DOWN: Scroll wheel down\n";
code << " \"\"\"\n";
// Add enum members
@ -65,42 +49,10 @@ PyObject* PyMouseButton::create_enum_class(PyObject* module) {
code << " " << mouse_button_table[i].name << " = " << mouse_button_table[i].value << "\n";
}
// Add legacy names and custom methods AFTER class creation
// (IntEnum doesn't allow dict attributes during class definition)
code << "\n# Add legacy name mapping after class creation\n";
code << "MouseButton._legacy_names = {\n";
for (int i = 0; i < NUM_MOUSE_BUTTON_ENTRIES; i++) {
code << " " << mouse_button_table[i].value << ": \"" << mouse_button_table[i].legacy << "\",\n";
}
code << "}\n\n";
code << R"(
def _MouseButton_eq(self, other):
if isinstance(other, str):
# Check enum name match (e.g., "LEFT")
if self.name == other:
return True
# Check legacy name match (e.g., "left")
legacy = type(self)._legacy_names.get(self.value)
if legacy and legacy == other:
return True
return False
# Fall back to int comparison for IntEnum
return int.__eq__(int(self), other)
MouseButton.__eq__ = _MouseButton_eq
def _MouseButton_ne(self, other):
result = type(self).__eq__(self, other)
if result is NotImplemented:
return result
return not result
MouseButton.__ne__ = _MouseButton_ne
MouseButton.__hash__ = lambda self: hash(int(self))
MouseButton.__repr__ = lambda self: f"{type(self).__name__}.{self.name}"
MouseButton.__str__ = lambda self: self.name
)";
code << "\n";
code << "MouseButton.__hash__ = lambda self: hash(int(self))\n";
code << "MouseButton.__repr__ = lambda self: f\"{type(self).__name__}.{self.name}\"\n";
code << "MouseButton.__str__ = lambda self: self.name\n";
std::string code_str = code.str();
@ -195,17 +147,15 @@ int PyMouseButton::from_arg(PyObject* arg, sf::Mouse::Button* out_button) {
return 0;
}
// Accept string (both new and legacy names)
// Accept string (enum name only)
if (PyUnicode_Check(arg)) {
const char* name = PyUnicode_AsUTF8(arg);
if (!name) {
return 0;
}
// Check all entries for both name and legacy match
for (int i = 0; i < NUM_MOUSE_BUTTON_ENTRIES; i++) {
if (strcmp(name, mouse_button_table[i].name) == 0 ||
strcmp(name, mouse_button_table[i].legacy) == 0) {
if (strcmp(name, mouse_button_table[i].name) == 0) {
*out_button = static_cast<sf::Mouse::Button>(mouse_button_table[i].value);
return 1;
}
@ -213,8 +163,7 @@ int PyMouseButton::from_arg(PyObject* arg, sf::Mouse::Button* out_button) {
PyErr_Format(PyExc_ValueError,
"Unknown MouseButton: '%s'. Use MouseButton.LEFT, MouseButton.RIGHT, "
"MouseButton.MIDDLE, MouseButton.X1, MouseButton.X2, "
"or legacy strings 'left', 'right', 'middle', 'x1', 'x2'.", name);
"MouseButton.MIDDLE, MouseButton.X1, MouseButton.X2.", name);
return 0;
}

View file

@ -6,13 +6,11 @@
// Stored as a module attribute: mcrfpy.MouseButton
//
// Values map to sf::Mouse::Button:
// LEFT = 0 (corresponds to "left" in legacy API)
// RIGHT = 1 (corresponds to "right" in legacy API)
// MIDDLE = 2 (corresponds to "middle" in legacy API)
// X1 = 3 (extra button 1)
// X2 = 4 (extra button 2)
//
// The enum compares equal to both its name ("LEFT") and legacy string ("left")
// LEFT = 0
// RIGHT = 1
// MIDDLE = 2
// X1 = 3
// X2 = 4
class PyMouseButton {
public:
@ -21,13 +19,10 @@ public:
static PyObject* create_enum_class(PyObject* module);
// Helper to extract mouse button from Python arg
// Accepts MouseButton enum, string (for backwards compatibility), int, or None
// Accepts MouseButton enum, string (enum name), or int
// Returns 1 on success, 0 on error (with exception set)
static int from_arg(PyObject* arg, sf::Mouse::Button* out_button);
// Convert sf::Mouse::Button to legacy string name (for passing to callbacks)
static const char* to_legacy_string(sf::Mouse::Button button);
// Cached reference to the MouseButton enum class for fast type checking
static PyObject* mouse_button_enum_class;