From afcb54d9fea6b9173eda71377c44d5c970079183 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Wed, 26 Nov 2025 08:08:43 -0500 Subject: [PATCH] fix: Make UICollection/EntityCollection match Python list semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking change: UICollection.remove() now takes a value (element) instead of an index, matching Python's list.remove() behavior. New methods added to both UICollection and EntityCollection: - pop([index]) -> element: Remove and return element at index (default: last) - insert(index, element): Insert element at position Semantic fixes: - remove(element): Now removes first occurrence of element (was: remove by index) - All methods now have docstrings documenting behavior Note on z_index sorting: The collections are sorted by z_index before each render. Using index-based operations (pop, insert) with non-default z_index values may produce unexpected results. Use name-based .find() for stable element access when z_index sorting is in use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/UICollection.cpp | 186 +++++++++++++-- src/UICollection.h | 2 + src/UIGrid.cpp | 142 ++++++++++- src/UIGrid.h | 2 + tests/unit/collection_list_methods_test.py | 263 +++++++++++++++++++++ 5 files changed, 564 insertions(+), 31 deletions(-) create mode 100644 tests/unit/collection_list_methods_test.py diff --git a/src/UICollection.cpp b/src/UICollection.cpp index 36b2e64..b29d22c 100644 --- a/src/UICollection.cpp +++ b/src/UICollection.cpp @@ -790,30 +790,151 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable) PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o) { - if (!PyLong_Check(o)) - { - PyErr_SetString(PyExc_TypeError, "UICollection.remove requires an integer index to remove"); - return NULL; - } - long index = PyLong_AsLong(o); - - // Handle negative indexing - while (index < 0) index += self->data->size(); - - if (index >= self->data->size()) - { - PyErr_SetString(PyExc_ValueError, "Index out of range"); + auto vec = self->data.get(); + if (!vec) { + PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); return NULL; } - // release the shared pointer at self->data[index]; - self->data->erase(self->data->begin() + index); - - // Mark scene as needing resort after removing element + // Type checking - must be a UIDrawable subclass + if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Drawable"))) { + PyErr_SetString(PyExc_TypeError, + "UICollection.remove requires a UI element (Frame, Caption, Sprite, Grid)"); + return NULL; + } + + // Get the C++ object from the Python object + std::shared_ptr search_drawable = nullptr; + + if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { + search_drawable = ((PyUIFrameObject*)o)->data; + } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { + search_drawable = ((PyUICaptionObject*)o)->data; + } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { + search_drawable = ((PyUISpriteObject*)o)->data; + } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { + search_drawable = ((PyUIGridObject*)o)->data; + } + + if (!search_drawable) { + PyErr_SetString(PyExc_TypeError, + "UICollection.remove requires a UI element (Frame, Caption, Sprite, Grid)"); + return NULL; + } + + // Search for the object and remove first occurrence + for (auto it = vec->begin(); it != vec->end(); ++it) { + if (it->get() == search_drawable.get()) { + vec->erase(it); + McRFPy_API::markSceneNeedsSort(); + Py_RETURN_NONE; + } + } + + PyErr_SetString(PyExc_ValueError, "element not in UICollection"); + return NULL; +} + +PyObject* UICollection::pop(PyUICollectionObject* self, PyObject* args) +{ + Py_ssize_t index = -1; // Default to last element + + if (!PyArg_ParseTuple(args, "|n", &index)) { + return NULL; + } + + auto vec = self->data.get(); + if (!vec) { + PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); + return NULL; + } + + if (vec->empty()) { + PyErr_SetString(PyExc_IndexError, "pop from empty UICollection"); + return NULL; + } + + // Handle negative indexing + Py_ssize_t size = static_cast(vec->size()); + if (index < 0) { + index += size; + } + + if (index < 0 || index >= size) { + PyErr_SetString(PyExc_IndexError, "pop index out of range"); + return NULL; + } + + // Get the element before removing + std::shared_ptr drawable = (*vec)[index]; + + // Remove from vector + vec->erase(vec->begin() + index); + McRFPy_API::markSceneNeedsSort(); - - Py_INCREF(Py_None); - return Py_None; + + // Convert to Python object and return + return convertDrawableToPython(drawable); +} + +PyObject* UICollection::insert(PyUICollectionObject* self, PyObject* args) +{ + Py_ssize_t index; + PyObject* o; + + if (!PyArg_ParseTuple(args, "nO", &index, &o)) { + return NULL; + } + + auto vec = self->data.get(); + if (!vec) { + PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); + return NULL; + } + + // Type checking - must be a UIDrawable subclass + if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Drawable"))) { + PyErr_SetString(PyExc_TypeError, + "UICollection.insert requires a UI element (Frame, Caption, Sprite, Grid)"); + return NULL; + } + + // Get the C++ object from the Python object + std::shared_ptr drawable = nullptr; + + if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { + drawable = ((PyUIFrameObject*)o)->data; + } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { + drawable = ((PyUICaptionObject*)o)->data; + } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { + drawable = ((PyUISpriteObject*)o)->data; + } else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { + drawable = ((PyUIGridObject*)o)->data; + } + + if (!drawable) { + PyErr_SetString(PyExc_TypeError, + "UICollection.insert requires a UI element (Frame, Caption, Sprite, Grid)"); + return NULL; + } + + // Handle negative indexing and clamping (Python list.insert behavior) + Py_ssize_t size = static_cast(vec->size()); + if (index < 0) { + index += size; + if (index < 0) { + index = 0; + } + } else if (index > size) { + index = size; + } + + // Insert at position + vec->insert(vec->begin() + index, drawable); + + McRFPy_API::markSceneNeedsSort(); + + Py_RETURN_NONE; } PyObject* UICollection::index_method(PyUICollectionObject* self, PyObject* value) { @@ -1036,15 +1157,30 @@ PyObject* UICollection::find(PyUICollectionObject* self, PyObject* args, PyObjec PyMethodDef UICollection::methods[] = { {"append", (PyCFunction)UICollection::append, METH_O, - "Add an element to the end of the collection"}, + "append(element)\n\n" + "Add an element to the end of the collection."}, {"extend", (PyCFunction)UICollection::extend, METH_O, - "Add all elements from an iterable to the collection"}, + "extend(iterable)\n\n" + "Add all elements from an iterable to the collection."}, + {"insert", (PyCFunction)UICollection::insert, METH_VARARGS, + "insert(index, element)\n\n" + "Insert element at index. Like list.insert(), indices past the end append.\n\n" + "Note: If using z_index for sorting, insertion order may not persist after\n" + "the next render. Use name-based .find() for stable element access."}, {"remove", (PyCFunction)UICollection::remove, METH_O, - "Remove element at the given index"}, + "remove(element)\n\n" + "Remove first occurrence of element. Raises ValueError if not found."}, + {"pop", (PyCFunction)UICollection::pop, METH_VARARGS, + "pop([index]) -> element\n\n" + "Remove and return element at index (default: last element).\n\n" + "Note: If using z_index for sorting, indices may shift after render.\n" + "Use name-based .find() for stable element access."}, {"index", (PyCFunction)UICollection::index_method, METH_O, - "Return the index of an element in the collection"}, + "index(element) -> int\n\n" + "Return index of first occurrence of element. Raises ValueError if not found."}, {"count", (PyCFunction)UICollection::count, METH_O, - "Count occurrences of an element in the collection"}, + "count(element) -> int\n\n" + "Count occurrences of element in the collection."}, {"find", (PyCFunction)UICollection::find, METH_VARARGS | METH_KEYWORDS, "find(name, recursive=False) -> element or list\n\n" "Find elements by name.\n\n" diff --git a/src/UICollection.h b/src/UICollection.h index b70fcf2..a026ea9 100644 --- a/src/UICollection.h +++ b/src/UICollection.h @@ -30,6 +30,8 @@ public: static PyObject* append(PyUICollectionObject* self, PyObject* o); static PyObject* extend(PyUICollectionObject* self, PyObject* iterable); static PyObject* remove(PyUICollectionObject* self, PyObject* o); + static PyObject* pop(PyUICollectionObject* self, PyObject* args); + static PyObject* insert(PyUICollectionObject* self, PyObject* args); static PyObject* index_method(PyUICollectionObject* self, PyObject* value); static PyObject* count(PyUICollectionObject* self, PyObject* value); static PyObject* find(PyUICollectionObject* self, PyObject* args, PyObject* kwds); diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 56c3197..8c57a3c 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -1953,13 +1953,132 @@ PyObject* UIEntityCollection::extend(PyUIEntityCollectionObject* self, PyObject* return Py_None; } +PyObject* UIEntityCollection::pop(PyUIEntityCollectionObject* self, PyObject* args) +{ + Py_ssize_t index = -1; // Default to last element + + if (!PyArg_ParseTuple(args, "|n", &index)) { + return NULL; + } + + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); + return NULL; + } + + if (list->empty()) { + PyErr_SetString(PyExc_IndexError, "pop from empty EntityCollection"); + return NULL; + } + + // Handle negative indexing + Py_ssize_t size = static_cast(list->size()); + if (index < 0) { + index += size; + } + + if (index < 0 || index >= size) { + PyErr_SetString(PyExc_IndexError, "pop index out of range"); + return NULL; + } + + // Navigate to the element (std::list requires iteration) + auto it = list->begin(); + std::advance(it, index); + + // Get the entity before removing + std::shared_ptr entity = *it; + + // Clear grid reference and remove from list + entity->grid = nullptr; + list->erase(it); + + // Create Python object for the entity + PyTypeObject* entityType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); + if (!entityType) { + PyErr_SetString(PyExc_RuntimeError, "Could not find Entity type"); + return NULL; + } + + PyUIEntityObject* py_entity = (PyUIEntityObject*)entityType->tp_alloc(entityType, 0); + Py_DECREF(entityType); + + if (!py_entity) { + return NULL; + } + + py_entity->data = entity; + py_entity->weakreflist = NULL; + + return (PyObject*)py_entity; +} + +PyObject* UIEntityCollection::insert(PyUIEntityCollectionObject* self, PyObject* args) +{ + Py_ssize_t index; + PyObject* o; + + if (!PyArg_ParseTuple(args, "nO", &index, &o)) { + return NULL; + } + + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "Collection data is null"); + return NULL; + } + + // Type checking - must be an Entity + if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { + PyErr_SetString(PyExc_TypeError, "EntityCollection.insert requires an Entity object"); + return NULL; + } + + PyUIEntityObject* entity = (PyUIEntityObject*)o; + if (!entity->data) { + PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object"); + return NULL; + } + + // Handle negative indexing and clamping (Python list.insert behavior) + Py_ssize_t size = static_cast(list->size()); + if (index < 0) { + index += size; + if (index < 0) { + index = 0; + } + } else if (index > size) { + index = size; + } + + // Navigate to insert position + auto it = list->begin(); + std::advance(it, index); + + // Insert and set grid reference + list->insert(it, entity->data); + entity->data->grid = self->grid; + + // Initialize gridstate if needed + if (entity->data->gridstate.size() == 0 && self->grid) { + entity->data->gridstate.resize(self->grid->grid_x * self->grid->grid_y); + for (auto& state : entity->data->gridstate) { + state.visible = false; + state.discovered = false; + } + } + + Py_RETURN_NONE; +} + PyObject* UIEntityCollection::index_method(PyUIEntityCollectionObject* self, PyObject* value) { auto list = self->data.get(); if (!list) { PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); return NULL; } - + // Type checking - must be an Entity if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { PyErr_SetString(PyExc_TypeError, "EntityCollection.index requires an Entity object"); @@ -2317,15 +2436,26 @@ PyObject* UIEntityCollection::find(PyUIEntityCollectionObject* self, PyObject* a PyMethodDef UIEntityCollection::methods[] = { {"append", (PyCFunction)UIEntityCollection::append, METH_O, - "Add an entity to the collection"}, + "append(entity)\n\n" + "Add an entity to the end of the collection."}, {"extend", (PyCFunction)UIEntityCollection::extend, METH_O, - "Add all entities from an iterable"}, + "extend(iterable)\n\n" + "Add all entities from an iterable to the collection."}, + {"insert", (PyCFunction)UIEntityCollection::insert, METH_VARARGS, + "insert(index, entity)\n\n" + "Insert entity at index. Like list.insert(), indices past the end append."}, {"remove", (PyCFunction)UIEntityCollection::remove, METH_O, - "Remove an entity from the collection"}, + "remove(entity)\n\n" + "Remove first occurrence of entity. Raises ValueError if not found."}, + {"pop", (PyCFunction)UIEntityCollection::pop, METH_VARARGS, + "pop([index]) -> entity\n\n" + "Remove and return entity at index (default: last entity)."}, {"index", (PyCFunction)UIEntityCollection::index_method, METH_O, - "Return the index of an entity"}, + "index(entity) -> int\n\n" + "Return index of first occurrence of entity. Raises ValueError if not found."}, {"count", (PyCFunction)UIEntityCollection::count, METH_O, - "Count occurrences of an entity"}, + "count(entity) -> int\n\n" + "Count occurrences of entity in the collection."}, {"find", (PyCFunction)UIEntityCollection::find, METH_VARARGS | METH_KEYWORDS, "find(name) -> entity or list\n\n" "Find entities by name.\n\n" diff --git a/src/UIGrid.h b/src/UIGrid.h index f2c9633..c3835cf 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -141,6 +141,8 @@ public: static PyObject* append(PyUIEntityCollectionObject* self, PyObject* o); static PyObject* extend(PyUIEntityCollectionObject* self, PyObject* o); static PyObject* remove(PyUIEntityCollectionObject* self, PyObject* o); + static PyObject* pop(PyUIEntityCollectionObject* self, PyObject* args); + static PyObject* insert(PyUIEntityCollectionObject* self, PyObject* args); static PyObject* index_method(PyUIEntityCollectionObject* self, PyObject* value); static PyObject* count(PyUIEntityCollectionObject* self, PyObject* value); static PyObject* find(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds); diff --git a/tests/unit/collection_list_methods_test.py b/tests/unit/collection_list_methods_test.py new file mode 100644 index 0000000..7035099 --- /dev/null +++ b/tests/unit/collection_list_methods_test.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +"""Test for Python list-like methods on UICollection and EntityCollection. + +Tests that remove(), pop(), insert(), index(), count() match Python list semantics. +""" + +import mcrfpy +import sys + + +def test_uicollection_remove(): + """Test UICollection.remove() takes a value, not an index.""" + print("Testing UICollection.remove()...") + + mcrfpy.createScene("test_remove") + ui = mcrfpy.sceneUI("test_remove") + + frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100)) + frame3 = mcrfpy.Frame(pos=(200, 0), size=(100, 100)) + + ui.append(frame1) + ui.append(frame2) + ui.append(frame3) + + assert len(ui) == 3 + + # Remove by value (like Python list) + ui.remove(frame2) + assert len(ui) == 2 + print(" [PASS] remove(element) works") + + # Verify frame2 is gone, but frame1 and frame3 remain + assert ui[0] is not None + assert ui[1] is not None + + # Try to remove something not in the list + try: + frame4 = mcrfpy.Frame(pos=(300, 0), size=(100, 100)) + ui.remove(frame4) + assert False, "Should have raised ValueError" + except ValueError as e: + assert "not in" in str(e).lower() + print(" [PASS] remove() raises ValueError when not found") + + # Try to pass an integer (should fail - no longer takes index) + try: + ui.remove(0) + assert False, "Should have raised TypeError" + except TypeError: + print(" [PASS] remove(int) raises TypeError (correct - takes element, not index)") + + print("UICollection.remove() tests passed!") + return True + + +def test_uicollection_pop(): + """Test UICollection.pop() removes and returns element at index.""" + print("\nTesting UICollection.pop()...") + + mcrfpy.createScene("test_pop") + ui = mcrfpy.sceneUI("test_pop") + + frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + frame1.name = "first" + frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100)) + frame2.name = "second" + frame3 = mcrfpy.Frame(pos=(200, 0), size=(100, 100)) + frame3.name = "third" + + ui.append(frame1) + ui.append(frame2) + ui.append(frame3) + + # pop() with no args removes last + popped = ui.pop() + assert popped.name == "third", f"Expected 'third', got '{popped.name}'" + assert len(ui) == 2 + print(" [PASS] pop() removes last element") + + # pop(0) removes first + popped = ui.pop(0) + assert popped.name == "first", f"Expected 'first', got '{popped.name}'" + assert len(ui) == 1 + print(" [PASS] pop(0) removes first element") + + # pop(-1) is same as pop() + ui.append(mcrfpy.Frame(pos=(0, 0), size=(10, 10))) + ui[-1].name = "new_last" + popped = ui.pop(-1) + assert popped.name == "new_last" + print(" [PASS] pop(-1) removes last element") + + # pop from empty collection + ui.pop() # Remove last remaining element + try: + ui.pop() + assert False, "Should have raised IndexError" + except IndexError as e: + assert "empty" in str(e).lower() + print(" [PASS] pop() from empty raises IndexError") + + print("UICollection.pop() tests passed!") + return True + + +def test_uicollection_insert(): + """Test UICollection.insert() inserts at given index.""" + print("\nTesting UICollection.insert()...") + + mcrfpy.createScene("test_insert") + ui = mcrfpy.sceneUI("test_insert") + + frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + frame1.name = "first" + frame3 = mcrfpy.Frame(pos=(200, 0), size=(100, 100)) + frame3.name = "third" + + ui.append(frame1) + ui.append(frame3) + + # Insert in middle + frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100)) + frame2.name = "second" + ui.insert(1, frame2) + + assert len(ui) == 3 + assert ui[0].name == "first" + assert ui[1].name == "second" + assert ui[2].name == "third" + print(" [PASS] insert(1, element) inserts at index 1") + + # Insert at beginning + frame0 = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) + frame0.name = "zero" + ui.insert(0, frame0) + assert ui[0].name == "zero" + print(" [PASS] insert(0, element) inserts at beginning") + + # Insert at end (index > len) + frame_end = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) + frame_end.name = "end" + ui.insert(100, frame_end) # Way past end + assert ui[-1].name == "end" + print(" [PASS] insert(100, element) appends when index > len") + + # Negative index + frame_neg = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) + frame_neg.name = "negative" + current_len = len(ui) + ui.insert(-1, frame_neg) # Insert before last + assert ui[-2].name == "negative" + print(" [PASS] insert(-1, element) inserts before last") + + print("UICollection.insert() tests passed!") + return True + + +def test_entitycollection_pop_insert(): + """Test EntityCollection.pop() and insert().""" + print("\nTesting EntityCollection.pop() and insert()...") + + mcrfpy.createScene("test_entity_pop") + ui = mcrfpy.sceneUI("test_entity_pop") + + grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(400, 400)) + ui.append(grid) + + e1 = mcrfpy.Entity(grid_pos=(1, 1)) + e1.name = "first" + e2 = mcrfpy.Entity(grid_pos=(2, 2)) + e2.name = "second" + e3 = mcrfpy.Entity(grid_pos=(3, 3)) + e3.name = "third" + + grid.entities.append(e1) + grid.entities.append(e2) + grid.entities.append(e3) + + # Test pop() + popped = grid.entities.pop() + assert popped.name == "third" + assert len(grid.entities) == 2 + print(" [PASS] EntityCollection.pop() works") + + # Test pop(0) + popped = grid.entities.pop(0) + assert popped.name == "first" + assert len(grid.entities) == 1 + print(" [PASS] EntityCollection.pop(0) works") + + # Test insert + e_new = mcrfpy.Entity(grid_pos=(5, 5)) + e_new.name = "new" + grid.entities.insert(0, e_new) + assert grid.entities[0].name == "new" + print(" [PASS] EntityCollection.insert() works") + + print("EntityCollection pop/insert tests passed!") + return True + + +def test_index_and_count(): + """Test index() and count() methods.""" + print("\nTesting index() and count()...") + + mcrfpy.createScene("test_index_count") + ui = mcrfpy.sceneUI("test_index_count") + + frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100)) + + ui.append(frame1) + ui.append(frame2) + + # index() returns integer + idx = ui.index(frame1) + assert idx == 0, f"Expected 0, got {idx}" + assert isinstance(idx, int) + print(" [PASS] index() returns integer") + + idx = ui.index(frame2) + assert idx == 1 + print(" [PASS] index() finds correct position") + + # count() returns integer + cnt = ui.count(frame1) + assert cnt == 1 + assert isinstance(cnt, int) + print(" [PASS] count() returns integer") + + # count of element not in collection + frame3 = mcrfpy.Frame(pos=(200, 0), size=(100, 100)) + cnt = ui.count(frame3) + assert cnt == 0 + print(" [PASS] count() returns 0 for element not in collection") + + print("index() and count() tests passed!") + return True + + +if __name__ == "__main__": + try: + all_passed = True + all_passed &= test_uicollection_remove() + all_passed &= test_uicollection_pop() + all_passed &= test_uicollection_insert() + all_passed &= test_entitycollection_pop_insert() + all_passed &= test_index_and_count() + + if all_passed: + print("\n" + "="*50) + print("All list-like method tests PASSED!") + print("="*50) + sys.exit(0) + else: + print("\nSome tests FAILED!") + sys.exit(1) + except Exception as e: + print(f"\nTest failed with exception: {e}") + import traceback + traceback.print_exc() + sys.exit(1)