From cc6ef720169024b9232224f1bb2179ff1ead0844 Mon Sep 17 00:00:00 2001 From: Count Infinity Date: Sun, 2 Nov 2025 23:41:24 -0700 Subject: [PATCH 1/8] Optimize ANSIString --- evennia/utils/ansi.py | 45 +++++++--- evennia/utils/tests/test_ansi.py | 147 ++++++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 16 deletions(-) diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index 38bea3fcb1..f7b7a16218 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -703,12 +703,17 @@ def _transform(func_name): def wrapped(self, *args, **kwargs): replacement_string = _query_super(func_name)(self, *args, **kwargs) + + # Convert to sets for O(1) membership testing + code_indexes_set = set(self._code_indexes) + char_indexes_set = set(self._char_indexes) + to_string = [] char_counter = 0 for index in range(0, len(self._raw_string)): - if index in self._code_indexes: + if index in code_indexes_set: to_string.append(self._raw_string[index]) - elif index in self._char_indexes: + elif index in char_indexes_set: to_string.append(replacement_string[char_counter]) char_counter += 1 return ANSIString( @@ -1028,10 +1033,12 @@ class ANSIString(str, metaclass=ANSIMeta): return ANSIString("") last_mark = slice_indexes[0] # Check between the slice intervals for escape sequences. + # Convert to set for O(1) membership testing + code_indexes_set = set(self._code_indexes) i = None for i in slice_indexes[1:]: for index in range(last_mark, i): - if index in self._code_indexes: + if index in code_indexes_set: string += self._raw_string[index] last_mark = i try: @@ -1065,15 +1072,18 @@ class ANSIString(str, metaclass=ANSIMeta): append_tail = self._get_interleving(item + 1) else: append_tail = "" - item = self._char_indexes[item] - clean = self._raw_string[item] - result = "" - # Get the character they're after, and replay all escape sequences - # previous to it. - for index in range(0, item + 1): - if index in self._code_indexes: - result += self._raw_string[index] + char_pos = self._char_indexes[item] + clean = self._raw_string[char_pos] + + code_indexes_set = set(self._code_indexes) + + result_chars = [ + self._raw_string[index] for index in range(0, char_pos + 1) if index in code_indexes_set + ] + + result = "".join(result_chars) + return ANSIString(result + clean + append_tail, decoded=True) def clean(self): @@ -1153,7 +1163,9 @@ class ANSIString(str, metaclass=ANSIMeta): # Plain string, no ANSI codes. return code_indexes, list(range(0, len(self._raw_string))) # all indexes not occupied by ansi codes are normal characters - char_indexes = [i for i in range(len(self._raw_string)) if i not in code_indexes] + code_indexes_set = set(code_indexes) + char_indexes = [i for i in range(len(self._raw_string)) if i not in code_indexes_set] + return code_indexes, char_indexes def _get_interleving(self, index): @@ -1166,12 +1178,17 @@ class ANSIString(str, metaclass=ANSIMeta): index = self._char_indexes[index - 1] except IndexError: return "" + + # Convert to sets for O(1) membership testing + char_indexes_set = set(self._char_indexes) + code_indexes_set = set(self._code_indexes) + s = "" while True: index += 1 - if index in self._char_indexes: + if index in char_indexes_set: break - elif index in self._code_indexes: + elif index in code_indexes_set: s += self._raw_string[index] else: break diff --git a/evennia/utils/tests/test_ansi.py b/evennia/utils/tests/test_ansi.py index 4ff9d468c6..91a4ea0247 100644 --- a/evennia/utils/tests/test_ansi.py +++ b/evennia/utils/tests/test_ansi.py @@ -8,7 +8,16 @@ Test of the ANSI parsing and ANSIStrings. from django.test import TestCase -from evennia.utils.ansi import ANSIString as AN +from evennia.utils.ansi import ( + ANSIString as AN, + ANSI_RED, + ANSI_CYAN, + ANSI_YELLOW, + ANSI_GREEN, + ANSI_BLUE, + ANSI_HILITE, + ANSI_NORMAL, +) class TestANSIString(TestCase): @@ -20,7 +29,9 @@ class TestANSIString(TestCase): self.example_raw = "|relectric |cboogaloo|n" self.example_ansi = AN(self.example_raw) self.example_str = "electric boogaloo" - self.example_output = "\x1b[1m\x1b[31melectric \x1b[1m\x1b[36mboogaloo\x1b[0m" + self.example_output = ( + f"{ANSI_HILITE}{ANSI_RED}electric {ANSI_HILITE}{ANSI_CYAN}boogaloo{ANSI_NORMAL}" + ) def test_length(self): self.assertEqual(len(self.example_ansi), 17) @@ -52,3 +63,135 @@ class TestANSIString(TestCase): self.assertEqual(split2, split3, "Split 2 and 3 differ") self.assertEqual(split1, split2, "Split 1 and 2 differ") self.assertEqual(split1, split3, "Split 1 and 3 differ") + + def test_getitem_index_access(self): + """Test individual character access via indexing (tests optimized __getitem__)""" + # Test accessing individual characters + self.assertEqual(self.example_ansi[0].clean(), "e") + self.assertEqual(self.example_ansi[9].clean(), "b") + self.assertEqual(self.example_ansi[-1].clean(), "o") + self.assertEqual(self.example_ansi[-2].clean(), "o") + + # Verify ANSI codes are preserved when accessing characters + first_char = self.example_ansi[0] + self.assertTrue(isinstance(first_char, AN)) + # First character should have red color code + self.assertIn(ANSI_RED, first_char.raw()) + + # Test character at color boundary (first character after color change) + ninth_char = self.example_ansi[9] + self.assertEqual(ninth_char.clean(), "b") + # Should have cyan color code + self.assertIn(ANSI_CYAN, ninth_char.raw()) + + def test_getitem_slice_access(self): + """Test slice access (tests optimized __getitem__ via _slice)""" + # Test basic slicing + substring = self.example_ansi[0:8] + self.assertEqual(substring.clean(), "electric") + self.assertTrue(isinstance(substring, AN)) + + # Test slicing with step + substring2 = self.example_ansi[9:17] + self.assertEqual(substring2.clean(), "boogaloo") + + # Test negative indices + last_three = self.example_ansi[-3:] + self.assertEqual(last_three.clean(), "loo") + + # Verify ANSI codes are preserved in slices + first_word = self.example_ansi[0:8] + self.assertIn(ANSI_RED, first_word.raw()) + + def test_getitem_edge_cases(self): + """Test edge cases for indexing""" + # Test with string with no ANSI codes + plain = AN("plain text") + self.assertEqual(plain[0].clean(), "p") + self.assertEqual(plain[6].clean(), "t") + + # Test with single character + single = AN("|rX|n") + self.assertEqual(len(single), 1) + self.assertEqual(single[0].clean(), "X") + + # Test IndexError + with self.assertRaises(IndexError): + _ = self.example_ansi[100] + + def test_upper_method(self): + """Test upper() method (uses optimized _transform)""" + # Test basic upper with ANSI codes + result = self.example_ansi.upper() + self.assertEqual(result.clean(), "ELECTRIC BOOGALOO") + self.assertTrue(isinstance(result, AN)) + + # Verify ANSI codes are preserved + self.assertIn(ANSI_RED, result.raw()) + self.assertIn(ANSI_CYAN, result.raw()) + + # Test with mixed case + mixed = AN("|rHeLLo |cWoRLd|n") + self.assertEqual(mixed.upper().clean(), "HELLO WORLD") + + def test_lower_method(self): + """Test lower() method (uses optimized _transform)""" + # Test basic lower with ANSI codes + upper_ansi = AN("|rELECTRIC |cBOOGALOO|n") + result = upper_ansi.lower() + self.assertEqual(result.clean(), "electric boogaloo") + self.assertTrue(isinstance(result, AN)) + + # Verify ANSI codes are preserved + self.assertIn(ANSI_RED, result.raw()) + self.assertIn(ANSI_CYAN, result.raw()) + + def test_capitalize_method(self): + """Test capitalize() method (uses optimized _transform)""" + # Test basic capitalize with ANSI codes + lower_ansi = AN("|relectric |cboogaloo|n") + result = lower_ansi.capitalize() + self.assertEqual(result.clean(), "Electric boogaloo") + self.assertTrue(isinstance(result, AN)) + + # Verify ANSI codes are preserved + self.assertIn(ANSI_RED, result.raw()) + + def test_swapcase_method(self): + """Test swapcase() method (uses optimized _transform)""" + # Test basic swapcase with ANSI codes + mixed = AN("|rElEcTrIc |cBoOgAlOo|n") + result = mixed.swapcase() + self.assertEqual(result.clean(), "eLeCtRiC bOoGaLoO") + self.assertTrue(isinstance(result, AN)) + + # Verify ANSI codes are preserved + self.assertIn(ANSI_RED, result.raw()) + self.assertIn(ANSI_CYAN, result.raw()) + + def test_transform_with_dense_ansi(self): + """Test string transformation with ANSI codes between every character""" + # Simulate rainbow text with ANSI between each character + dense = AN("|rh|ce|yl|gl|bo|n") + self.assertEqual(dense.clean(), "hello") + + # Test upper preserves all ANSI codes + upper_dense = dense.upper() + self.assertEqual(upper_dense.clean(), "HELLO") + self.assertTrue(isinstance(upper_dense, AN)) + + # Verify all color codes are still present + raw = upper_dense.raw() + self.assertIn(ANSI_RED, raw) + self.assertIn(ANSI_CYAN, raw) + self.assertIn(ANSI_YELLOW, raw) + self.assertIn(ANSI_GREEN, raw) + self.assertIn(ANSI_BLUE, raw) + + def test_transform_without_ansi(self): + """Test string transformation on plain strings""" + plain = AN("hello world") + + self.assertEqual(plain.upper().clean(), "HELLO WORLD") + self.assertEqual(plain.lower().clean(), "hello world") + self.assertEqual(plain.capitalize().clean(), "Hello world") From 2ab37d39880c477204f6acbbbab83200c9347e91 Mon Sep 17 00:00:00 2001 From: Count Infinity Date: Fri, 7 Nov 2025 22:23:08 -0700 Subject: [PATCH 2/8] Fix typo in prototype --- evennia/prototypes/prototypes.py | 2 +- evennia/prototypes/tests.py | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index c369e4fa89..a413a5f57b 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -187,7 +187,7 @@ def homogenize_prototype(prototype, custom_keys=None): "prototype-{}".format(hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7]), ) homogenized["prototype_tags"] = homogenized.get("prototype_tags", []) - homogenized["prototype_locks"] = homogenized.get("prototype_lock", _PROTOTYPE_FALLBACK_LOCK) + homogenized["prototype_locks"] = homogenized.get("prototype_locks", _PROTOTYPE_FALLBACK_LOCK) homogenized["prototype_desc"] = homogenized.get("prototype_desc", "") if "typeclass" not in prototype and "prototype_parent" not in prototype: homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 4a0b1d1a1b..c2c8d223e3 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -402,6 +402,39 @@ class TestProtLib(BaseEvenniaTest): match = protlib.search_prototype(self.prot["prototype_key"].upper()) self.assertEqual(match, [self.prot]) + def test_homogenize_prototype_locks_preserved(self): + """Test that homogenize_prototype preserves custom prototype_locks. (Bug 3828)""" + from evennia.prototypes.prototypes import _PROTOTYPE_FALLBACK_LOCK + + prot_with_locks = { + "prototype_key": "test_prot_with_locks", + "typeclass": "evennia.objects.objects.DefaultObject", + "prototype_locks": "spawn:perm(Builder);edit:perm(Admin)", + } + homogenized = protlib.homogenize_prototype(prot_with_locks) + self.assertEqual( + homogenized["prototype_locks"], + "spawn:perm(Builder);edit:perm(Admin)", + ) + self.assertNotEqual( + homogenized["prototype_locks"], + _PROTOTYPE_FALLBACK_LOCK, + ) + + def test_homogenize_prototype_locks_default_fallback(self): + """Test that homogenize_prototype uses default when prototype_locks not provided.""" + from evennia.prototypes.prototypes import _PROTOTYPE_FALLBACK_LOCK + + prot_without_locks = { + "prototype_key": "test_prot_without_locks", + "typeclass": "evennia.objects.objects.DefaultObject", + } + homogenized = protlib.homogenize_prototype(prot_without_locks) + self.assertEqual( + homogenized["prototype_locks"], + _PROTOTYPE_FALLBACK_LOCK, + ) + class TestProtFuncs(BaseEvenniaTest): @override_settings(PROT_FUNC_MODULES=["evennia.prototypes.protfuncs"]) From 3b45b2dfae6f5bc2b8d52d4f2cb18958dc64ceb5 Mon Sep 17 00:00:00 2001 From: Count Infinity Date: Fri, 7 Nov 2025 22:28:51 -0700 Subject: [PATCH 3/8] Better unit test text --- evennia/utils/tests/test_ansi.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/evennia/utils/tests/test_ansi.py b/evennia/utils/tests/test_ansi.py index 91a4ea0247..d8b0cac5bf 100644 --- a/evennia/utils/tests/test_ansi.py +++ b/evennia/utils/tests/test_ansi.py @@ -65,7 +65,7 @@ class TestANSIString(TestCase): self.assertEqual(split1, split3, "Split 1 and 3 differ") def test_getitem_index_access(self): - """Test individual character access via indexing (tests optimized __getitem__)""" + """Test individual character access via indexing""" # Test accessing individual characters self.assertEqual(self.example_ansi[0].clean(), "e") self.assertEqual(self.example_ansi[9].clean(), "b") @@ -85,7 +85,7 @@ class TestANSIString(TestCase): self.assertIn(ANSI_CYAN, ninth_char.raw()) def test_getitem_slice_access(self): - """Test slice access (tests optimized __getitem__ via _slice)""" + """Test slice access""" # Test basic slicing substring = self.example_ansi[0:8] self.assertEqual(substring.clean(), "electric") @@ -120,7 +120,7 @@ class TestANSIString(TestCase): _ = self.example_ansi[100] def test_upper_method(self): - """Test upper() method (uses optimized _transform)""" + """Test upper() method""" # Test basic upper with ANSI codes result = self.example_ansi.upper() self.assertEqual(result.clean(), "ELECTRIC BOOGALOO") @@ -135,7 +135,7 @@ class TestANSIString(TestCase): self.assertEqual(mixed.upper().clean(), "HELLO WORLD") def test_lower_method(self): - """Test lower() method (uses optimized _transform)""" + """Test lower() method""" # Test basic lower with ANSI codes upper_ansi = AN("|rELECTRIC |cBOOGALOO|n") result = upper_ansi.lower() @@ -147,7 +147,7 @@ class TestANSIString(TestCase): self.assertIn(ANSI_CYAN, result.raw()) def test_capitalize_method(self): - """Test capitalize() method (uses optimized _transform)""" + """Test capitalize() method""" # Test basic capitalize with ANSI codes lower_ansi = AN("|relectric |cboogaloo|n") result = lower_ansi.capitalize() @@ -158,7 +158,7 @@ class TestANSIString(TestCase): self.assertIn(ANSI_RED, result.raw()) def test_swapcase_method(self): - """Test swapcase() method (uses optimized _transform)""" + """Test swapcase() method""" # Test basic swapcase with ANSI codes mixed = AN("|rElEcTrIc |cBoOgAlOo|n") result = mixed.swapcase() From 1c99e89f1ca70dab5389a7f67af97ec4baa3469e Mon Sep 17 00:00:00 2001 From: Count Infinity Date: Mon, 10 Nov 2025 22:29:19 -0700 Subject: [PATCH 4/8] Fix Unexpected keyword argument 'prototype' error when spawning --- evennia/prototypes/tests.py | 39 +++++++++++++++++++++++++++++++++++++ evennia/utils/funcparser.py | 4 ++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 4a0b1d1a1b..1422c58d40 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -1045,6 +1045,45 @@ class TestIssue2908(BaseEvenniaTest): self.assertEqual(obj[0].location, self.room1) +class TestIssue3824(BaseEvenniaTest): + """ + Test that $obj, $objlist, and $search callables work correctly when spawning prototypes. + + """ + + def test_spawn_with_search_callables(self): + """Test spawning prototype with $obj, $objlist, and $search callables.""" + # Setup: tag some objects for searching + self.room1.tags.add("test_location", category="zone") + self.room2.tags.add("test_location", category="zone") + self.obj1.tags.add("test_item", category="item_type") + + # Create prototype using all three search callables + prot = { + "prototype_key": "test_search_callables", + "typeclass": "evennia.objects.objects.DefaultObject", + "key": "test object", + "attr_obj": f"$obj({self.obj1.dbref})", + "attr_search": "$search(Char)", + "attr_objlist": "$objlist(test_location, category=zone, type=tag)", + } + + # This should not raise TypeError about 'prototype' kwarg + objs = spawner.spawn(prot, caller=self.char1) + + self.assertEqual(len(objs), 1) + obj = objs[0] + + # Verify all search callables worked correctly + self.assertEqual(obj.db.attr_obj, self.obj1) + self.assertEqual(obj.db.attr_search, self.char1) + # attr_objlist should be a list or list-like object with 2 rooms + objlist = obj.db.attr_objlist + self.assertEqual(len(objlist), 2) + self.assertIn(self.room1, objlist) + self.assertIn(self.room2, objlist) + + class TestIssue3101(EvenniaCommandTest): """ Spawning and using create_object should store the same `typeclass_path` if using diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index 2ed3243004..64aa25c503 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -1131,12 +1131,12 @@ def funcparser_callable_search(*args, caller=None, access="control", **kwargs): - "$search(beach, category=outdoors, type=tag) """ - # clean out funcparser-specific kwargs so we can use the kwargs for + # clean out funcparser and protfunc_parser-specific kwargs so we can use the kwargs for # searching search_kwargs = { key: value for key, value in kwargs.items() - if key not in ("funcparser", "raise_errors", "type", "return_list") + if key not in ("funcparser", "raise_errors", "type", "return_list", "prototype") } return_list = str(kwargs.pop("return_list", "false")).lower() == "true" From 2efe981ddf469a473b84f73eb6ec949f324b8415 Mon Sep 17 00:00:00 2001 From: electroglyph Date: Mon, 10 Nov 2025 22:22:24 -0800 Subject: [PATCH 5/8] change 0xDEADFED5 -> electroglyph --- CHANGELOG.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 619fc3facd..8dc07fdb79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,10 +51,10 @@ This upgrade requires running `evennia migrate` on your existing database - Feat (backwards incompatible): RUN MIGRATIONS (`evennia migrate`): Now requiring Django 5.1 (Griatch) - Feat (backwards incompatible): Drop support and testing for Python 3.10 (Griatch) -- [Feat][pull3719]: Support Python 3.13. (0xDEADFED5) +- [Feat][pull3719]: Support Python 3.13. (electroglyph) - [Feat][pull3633]: Default object's default descs are now taken from a `default_description` class variable instead of the `desc` Attribute always being set (count-infinity) -- [Feat][pull3718]: Remove twistd.bat creation for Windows, should not be needed anymore (0xDEADFED5) +- [Feat][pull3718]: Remove twistd.bat creation for Windows, should not be needed anymore (electroglyph) - [Feat][pull3756]: Updated German translation (JohnFi) - [Feat][pull3757]: Add more i18n strings to `DefaultObject` for easier translation (JohnFi) - [Feat][pull3783]: Support users of `ruff` linter by adding compatible config in `pyproject.toml` (jaborsh) @@ -70,8 +70,8 @@ This upgrade requires running `evennia migrate` on your existing database - [Fix][pull3690]: In searches, allow special 'here' and 'me' keywords only be valid queries unless current location and/or caller is in valid search candidates respectively (InspectorCaracal) - [Fix][pull3694]: Funcparser swallowing rest of line after a `\`-escape (count-infinity) -- [Fix][pull3705]: Properly serialize `IntFlag` enum types (0xDEADFED5) -- [Fix][pull3707]: Correct links in `about` command (0xDEADFED5) +- [Fix][pull3705]: Properly serialize `IntFlag` enum types (electroglyph) +- [Fix][pull3707]: Correct links in `about` command (electroglyph) - [Fix][pull3710]: Clean reduntant session clearin in `at_server_cold_start` (InspectorCaracal) - [Fix][pull3711]: Usability improvements in the Discord integration (InspectorCaracal) - [Fix][pull3721]: Avoid loading cmdsets that don't need to be checked, avoiding @@ -87,7 +87,7 @@ This upgrade requires running `evennia migrate` on your existing database - [Fix][pull3743]: Log full stack trace on failed object creation (aMiss-aWry) - [Fix][pull3747]: TutorialWorld bridge-room didn't correctly randomize weather effects (SpyrosRoum) - [Fix][pull3765]: Storing TickerHandler `store_key` in a db attribute would not - work correctly (0xDEADFED5) + work correctly (electroglyph) - [Fix][pull3753]: Make sure `AttributeProperty`s are initialized with default values also in parent class (JohnFi) - [Fix][pull3751]: The `access` and `inventory` commands would traceback if run on a character without an Account (EliasWatson) - [Fix][pull3768]: Make sure the `CmdCopy` command copies object categories, @@ -102,7 +102,7 @@ This upgrade requires running `evennia migrate` on your existing database it caused an OnDemandHandler save error on reload. Will now clean up on save. (Griatch) used as the task's category (Griatch) - Fix: Correct aws contrib's use of legacy django string utils (Griatch) -- [Docs]: Fixes from InspectorCaracal, Griatch, ChrisLR, JohnFi, 0xDEADFED5, jaborsh, Problematic, BlaneWins +- [Docs]: Fixes from InspectorCaracal, Griatch, ChrisLR, JohnFi, electroglyph, jaborsh, Problematic, BlaneWins [pull3633]: https://github.com/evennia/evennia/pull/3633 [pull3677]: https://github.com/evennia/evennia/pull/3677 @@ -224,7 +224,7 @@ Sep 29, 2024 - Feat: Support `scripts key:typeclass` to create global scripts with dynamic keys (rather than just relying on typeclass' key) (Griatch) -- [Feat][pull3595]: Tweak Sqlite3 PRAGMAs for better performance (0xDEADFED5) +- [Feat][pull3595]: Tweak Sqlite3 PRAGMAs for better performance (electroglyph) - Feat: Make Sqlite3 PRAGMAs configurable via settings (Griatch) - [Feat][pull3592]: Revised German locationlization ('Du' instead of 'Sie', cleanup) (Drakon72) @@ -233,7 +233,7 @@ with dynamic keys (rather than just relying on typeclass' key) (Griatch) - [Feat][pull3588]: New `DefaultObject` hooks: `at_object_post_creation`, called once after first creation but after any prototypes have been applied, and `at_object_post_spawn(prototype)`, called only after creation/update with a prototype (InspectorCaracal) -- [Fix][pull3594]: Update/clean some Evennia dependencies (0xDEADFED5) +- [Fix][pull3594]: Update/clean some Evennia dependencies (electroglyph) - [Fix][issue3556]: Better error if trying to treat ObjectDB as a typeclass (Griatch) - [Fix][issue3590]: Make `examine` command properly show `strattr` type Attribute values (Griatch) @@ -247,7 +247,7 @@ did not add it to the handler's object (Griatch) - [Fix][pull3605]: Correctly pass node kwargs through `@list_node` decorated evmenu nodes (InspectorCaracal) - [Fix][pull3597]: Address timing issue for testing `new_task_waiting_input `on - Windows (0xDEADFED5) + Windows (electroglyph) - [Fix][pull3611]: Fix and update for Reports contrib (InspectorCaracal) - [Fix][pull3625]: Lycanthropy tutorial page had some issues (feyrkh) - [Fix][pull3622]: Fix for examine command tracebacking with strvalue error @@ -293,10 +293,10 @@ Aug 11, 2024 - [Feat][pull3531]: New contrib; `in-game reports` for handling user reports, bugs etc in-game (InspectorCaracal) - [Feat][pull3586]: Add ANSI color support `|U`, `|I`, `|i`, `|s`, `|S` for -underline reset, italic/reset and strikethrough/reset (0xDEADFED5) +underline reset, italic/reset and strikethrough/reset (electroglyph) - Feat: Add `Trait.traithandler` back-reference so custom Traits from the Traits contrib can find and reference other Traits. (Griatch) -- [Feat][pull3582]: Add true-color parsing/fallback for ANSIString (0xDEADFED5) +- [Feat][pull3582]: Add true-color parsing/fallback for ANSIString (electroglyph) - [Fix][pull3571]: Better visual display of partial multimatch search results (InspectorCaracal) - [Fix][issue3378]: Prototype 'alias' key was not properly homogenized to a list @@ -306,8 +306,8 @@ underline reset, italic/reset and strikethrough/reset (0xDEADFED5) - [Fix][pull3585]: `TagCmd.switch_options` was misnamed (erratic-pattern) - [Fix][pull3580]: Fix typo that made `find/loc` show the wrong dbref in result (erratic-pattern) - [Fix][pull3589]: Fix regex escaping in `utils.py` for future Python versions (hhsiao) -- [Docs]: Add True-color description for Colors documentation (0xDEADFED5) -- [Docs]: Doc fixes (Griatch, InspectorCaracal, 0xDEADFED5) +- [Docs]: Add True-color description for Colors documentation (electroglyph) +- [Docs]: Doc fixes (Griatch, InspectorCaracal, electroglyph) [pull3585]: https://github.com/evennia/evennia/pull/3585 [pull3580]: https://github.com/evennia/evennia/pull/3580 From 8e607f71851d17dcd61b9afd1cbfd6a8c211e0bf Mon Sep 17 00:00:00 2001 From: Count Infinity Date: Tue, 11 Nov 2025 21:32:49 -0700 Subject: [PATCH 6/8] Fix error when creating object with None key --- evennia/objects/manager.py | 5 +++++ evennia/objects/tests.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index 2982daa1d7..88b836d6b5 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -685,6 +685,11 @@ class ObjectDBManager(TypedObjectManager): "or the setting is malformed." ) + # db_key has NOT NULL constraint, convert None to empty string. + # at_first_save() will convert empty string to #dbref + if key is None: + key = "" + # create new instance new_object = typeclass( db_key=key, diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index 3d5997127d..a0dbb96d98 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -244,6 +244,20 @@ class DefaultObjectTest(BaseEvenniaTest): class TestObjectManager(BaseEvenniaTest): "Test object manager methods" + def test_create_object_with_none_key(self): + """Test that create_object() handles key=None and key="" correctly.""" + # Test with key=None - should convert to "" and then to #dbref + obj_none = ObjectDB.objects.create_object(key=None, location=self.room1) + self.assertIsNotNone(obj_none) + self.assertEqual(obj_none.key, f"#{obj_none.id}") + obj_none.delete() + + # Test with key="" - should convert to #dbref + obj_empty = ObjectDB.objects.create_object(key="", location=self.room1) + self.assertIsNotNone(obj_empty) + self.assertEqual(obj_empty.key, f"#{obj_empty.id}") + obj_empty.delete() + def test_get_object_with_account(self): query = ObjectDB.objects.get_object_with_account("TestAccount").first() self.assertEqual(query, self.char1) From cf936f2e6dc617d979e3957ed1b6f0f62bb20bfc Mon Sep 17 00:00:00 2001 From: Hasna Boubakry Date: Fri, 14 Nov 2025 22:08:55 +0100 Subject: [PATCH 7/8] docs: clarify Windows py launcher usage during installation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d373a2b511..35f766ae5a 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ with [discussion forums][group] and a [discord server][chat] to help and support pip install evennia (windows users once: py -m evennia) + (note: Windows users with multiple Python versions should prefer `py -3.11` instead of `python` when creating virtual environments) evennia --init mygame cd mygame evennia migrate From e834e34fd155ceaec733caedb89d2b772b999cce Mon Sep 17 00:00:00 2001 From: Count Infinity Date: Wed, 19 Nov 2025 00:18:31 -0700 Subject: [PATCH 8/8] Add searchable --- evennia/prototypes/tests.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 1422c58d40..362ef8bcf1 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -1047,18 +1047,19 @@ class TestIssue2908(BaseEvenniaTest): class TestIssue3824(BaseEvenniaTest): """ - Test that $obj, $objlist, and $search callables work correctly when spawning prototypes. + Test that $obj, $objlist, $search, and $dbref callables work correctly when spawning prototypes. + Regression test for bug where 'prototype' kwarg was passed to search functions causing TypeError. """ def test_spawn_with_search_callables(self): - """Test spawning prototype with $obj, $objlist, and $search callables.""" + """Test spawning prototype with $obj, $objlist, $search, and $dbref callables.""" # Setup: tag some objects for searching self.room1.tags.add("test_location", category="zone") self.room2.tags.add("test_location", category="zone") self.obj1.tags.add("test_item", category="item_type") - # Create prototype using all three search callables + # Create prototype using all search callables prot = { "prototype_key": "test_search_callables", "typeclass": "evennia.objects.objects.DefaultObject", @@ -1066,6 +1067,7 @@ class TestIssue3824(BaseEvenniaTest): "attr_obj": f"$obj({self.obj1.dbref})", "attr_search": "$search(Char)", "attr_objlist": "$objlist(test_location, category=zone, type=tag)", + "attr_dbref": f"$dbref({self.obj1.dbref})", } # This should not raise TypeError about 'prototype' kwarg @@ -1077,13 +1079,13 @@ class TestIssue3824(BaseEvenniaTest): # Verify all search callables worked correctly self.assertEqual(obj.db.attr_obj, self.obj1) self.assertEqual(obj.db.attr_search, self.char1) + self.assertEqual(obj.db.attr_dbref, self.obj1) # attr_objlist should be a list or list-like object with 2 rooms objlist = obj.db.attr_objlist self.assertEqual(len(objlist), 2) self.assertIn(self.room1, objlist) self.assertIn(self.room2, objlist) - class TestIssue3101(EvenniaCommandTest): """ Spawning and using create_object should store the same `typeclass_path` if using