From 8a363ba36e3fc2db1c29583018793ee218a26d67 Mon Sep 17 00:00:00 2001 From: uttam12331 Date: Tue, 28 Jul 2026 10:33:08 +0530 Subject: [PATCH] Fix KeyError in get_components() when the smallest set is found last _get_components() scans the requested component types to iterate the smallest component set first, setting the other types aside for a membership check. When it found a new smallest set, it tried to move the previous smallest type into other_types using an index into component_types (component_types[len(other_types)]). That index is only correct while no type has already been set aside; once one has, it points at the wrong type and the real previous-smallest type is dropped. The dropped type is then never checked, so an entity that matches on the remaining types but lacks it slips through and raises a KeyError while building the result tuple. For example, querying three types where the final (smallest) set contains an entity missing the first type crashes instead of returning the correct matches. Track the previous smallest type directly instead of computing it from an index. Adds a regression test covering the size-ordering path. --- esper/__init__.py | 6 ++++-- tests/test_world.py | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/esper/__init__.py b/esper/__init__.py index 2a478ba..2b85689 100644 --- a/esper/__init__.py +++ b/esper/__init__.py @@ -493,6 +493,7 @@ def _get_components(*component_types: type[_C]) -> _Iterable[tuple[int, tuple[_C comp_db = _components min_set = None + min_type = None min_size = inf other_types = [] @@ -502,10 +503,11 @@ def _get_components(*component_types: type[_C]) -> _Iterable[tuple[int, tuple[_C return set_size = len(comp_set) if set_size < min_size: - if min_set is not None: - other_types.append(component_types[len(other_types)]) + if min_type is not None: + other_types.append(min_type) min_size = set_size min_set = comp_set + min_type = ct else: other_types.append(ct) diff --git a/tests/test_world.py b/tests/test_world.py index 9d8503e..52dfe8b 100644 --- a/tests/test_world.py +++ b/tests/test_world.py @@ -154,6 +154,32 @@ def test_get_three_components(): assert isinstance(e, ComponentE) +def test_get_components_smallest_set_missing_component(): + # Regression test: _get_components scans for the smallest component set + # first. When a type is set aside before a new smallest set is found, the + # previously-smallest type must be the one moved aside. Component sizes are + # arranged so ComponentB is set aside, then ComponentC becomes the new + # smallest -- ComponentA (the prior smallest) must not be dropped, otherwise + # an entity that matches on the remaining types but lacks ComponentA raises + # a KeyError while building the result tuple. + esper.create_entity(ComponentB(), ComponentC()) # matches B and C, not A + esper.create_entity(ComponentA()) + esper.create_entity(ComponentA()) + esper.create_entity(ComponentB()) + esper.create_entity(ComponentB()) + entity_all = esper.create_entity(ComponentA(), ComponentB(), ComponentC()) + + # sizes: A=3, B=4, C=2 -> C is discovered as the smallest set last + result = esper.get_components(ComponentA, ComponentB, ComponentC) + + assert len(result) == 1 + ent, (a, b, c) = result[0] + assert ent == entity_all + assert isinstance(a, ComponentA) + assert isinstance(b, ComponentB) + assert isinstance(c, ComponentC) + + def test_try_component(): entity1 = esper.create_entity(ComponentA(), ComponentB())