tcod-ecs 5.5.2__tar.gz → 5.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tcod-ecs
3
- Version: 5.5.2
3
+ Version: 5.6.0
4
4
  Summary: A type-hinted Entity Component System based on Python dictionaries and sets.
5
5
  Author-email: Kyle Benesch <4b796c65+github@gmail.com>
6
6
  Requires-Python: >=3.10
@@ -14,8 +14,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
14
  Classifier: Typing :: Typed
15
15
  License-File: LICENSE
16
16
  Requires-Dist: attrs >=23.1.0
17
- Requires-Dist: cattrs >=23.1.2
18
- Requires-Dist: typing-extensions >=4.14.0
17
+ Requires-Dist: typing-extensions >=4.16.0
19
18
  Requires-Dist: attrs >=22.2.0 ; extra == "test"
20
19
  Requires-Dist: pytest >=7.2.0 ; extra == "test"
21
20
  Requires-Dist: pytest-cov >=4.0.0 ; extra == "test"
@@ -18,11 +18,7 @@ classifiers = [
18
18
  ]
19
19
  dynamic = ["version", "description"]
20
20
  requires-python = ">=3.10"
21
- dependencies = [
22
- "attrs >=23.1.0",
23
- "cattrs >=23.1.2",
24
- "typing-extensions >=4.14.0",
25
- ]
21
+ dependencies = ["attrs >=23.1.0", "typing-extensions >=4.16.0"]
26
22
 
27
23
  [tool.setuptools_scm]
28
24
  write_to = "tcod/ecs/_version.py"
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '5.5.2'
22
- __version_tuple__ = version_tuple = (5, 5, 2)
21
+ __version__ = version = '5.6.0'
22
+ __version_tuple__ = version_tuple = (5, 6, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g60f279cbb'
24
+ __commit_id__ = commit_id = 'ga15c18076'
@@ -9,7 +9,6 @@ from typing import (
9
9
  Final,
10
10
  Generic,
11
11
  TypeVar,
12
- cast,
13
12
  overload,
14
13
  )
15
14
  from weakref import WeakKeyDictionary, WeakValueDictionary
@@ -18,8 +17,8 @@ import attrs
18
17
  from typing_extensions import Self, Sentinel, TypeForm, deprecated
19
18
 
20
19
  import tcod.ecs.callbacks
21
- import tcod.ecs.query
22
20
  from tcod.ecs.constants import IsA
21
+ from tcod.ecs.traits import Component, Tag
23
22
  from tcod.ecs.typing import ComponentKey
24
23
 
25
24
  if TYPE_CHECKING:
@@ -362,7 +361,7 @@ def _traverse_entities(start: Entity, traverse_parents: tuple[object, ...]) -> I
362
361
  traverse_parents = traverse_parents[::-1]
363
362
  visited = {start}
364
363
  stack = [start]
365
- _relation_tags_by_entity = start.registry._relation_tags_by_entity
364
+ _relation_tags_by_entity = start.registry._any_by_key_by_entity_by_category["RelationTag"]
366
365
  while stack:
367
366
  entity = stack.pop()
368
367
  yield entity
@@ -418,7 +417,7 @@ class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any
418
417
  def __getitem__(self, key: ComponentKey[T]) -> T:
419
418
  """Return a component belonging to this entity, or an indirect parent."""
420
419
  assert self.__assert_key(key)
421
- _components_by_entity = self.entity.registry._components_by_entity
420
+ _components_by_entity = self.entity.registry._any_by_key_by_entity_by_category["Component"]
422
421
  for entity in _traverse_entities(self.entity, self.traverse):
423
422
  components = _components_by_entity.get(entity)
424
423
  if components is None:
@@ -433,13 +432,9 @@ class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any
433
432
  """Assign a component directly to an entity."""
434
433
  assert self.__assert_key(key)
435
434
 
436
- old_value = self.entity.registry._components_by_entity[self.entity].get(key)
435
+ old_value = self.entity.registry._any_by_key_by_entity_by_category["Component"].get(self.entity, {}).get(key)
437
436
 
438
- if old_value is None:
439
- tcod.ecs.query._touch_component(self.entity.registry, key) # Component added
440
-
441
- self.entity.registry._components_by_entity[self.entity][key] = value
442
- self.entity.registry._components_by_type[key][self.entity] = value
437
+ self.entity.registry._set_data(self.entity, Component.from_key(key), "Component", key, value)
443
438
 
444
439
  tcod.ecs.callbacks._on_component_changed(key, self.entity, old_value, value)
445
440
 
@@ -447,25 +442,16 @@ class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any
447
442
  """Delete a directly held component from an entity."""
448
443
  assert self.__assert_key(key)
449
444
 
450
- components = self.entity.registry._components_by_entity.get(self.entity)
451
- if components is None:
452
- raise KeyError(key)
453
- old_value = components.get(key)
454
-
455
- del components[key]
456
- if not components:
457
- del self.entity.registry._components_by_entity[self.entity]
445
+ old_value = self.entity.registry._any_by_key_by_entity_by_category["Component"].get(self.entity, {}).get(key)
458
446
 
459
- del self.entity.registry._components_by_type[key][self.entity]
460
- if not self.entity.registry._components_by_type[key]:
461
- del self.entity.registry._components_by_type[key]
447
+ if not self.entity.registry._discard_data(self.entity, Component.from_key(key), "Component", key):
448
+ raise KeyError(key)
462
449
 
463
- tcod.ecs.query._touch_component(self.entity.registry, key) # Component removed
464
450
  tcod.ecs.callbacks._on_component_changed(key, self.entity, old_value, None)
465
451
 
466
452
  def keys(self) -> AbstractSet[ComponentKey[object]]: # type: ignore[override]
467
453
  """Return the components held by this entity, including inherited components."""
468
- _components_by_entity = self.entity.registry._components_by_entity
454
+ _components_by_entity = self.entity.registry._any_by_key_by_entity_by_category["Component"]
469
455
  if not self.traverse:
470
456
  return _components_by_entity.get(self.entity, {}).keys()
471
457
  set_: set[ComponentKey[object]] = set()
@@ -475,7 +461,7 @@ class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any
475
461
 
476
462
  def __contains__(self, key: object) -> bool:
477
463
  """Return True if this entity has the provided component key."""
478
- _components_by_entity = self.entity.registry._components_by_entity
464
+ _components_by_entity = self.entity.registry._any_by_key_by_entity_by_category["Component"]
479
465
  return any(
480
466
  key in _components_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse)
481
467
  )
@@ -583,12 +569,12 @@ class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any
583
569
  ...
584
570
  KeyError: <class 'str'>
585
571
  """
586
- _components = self.entity.registry._components_by_entity.get(self.entity, {})
587
- if __key not in _components:
572
+ data_by_key = self.entity.registry._any_by_key_by_entity_by_category["Component"].get(self.entity, {})
573
+ if __key not in data_by_key:
588
574
  if default is _raise:
589
575
  raise KeyError(__key)
590
576
  return default
591
- value: T | _T1 = _components[__key]
577
+ value: T | _T1 = data_by_key[__key]
592
578
  del self[__key]
593
579
  return value
594
580
 
@@ -598,9 +584,6 @@ class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any
598
584
  self(traverse=()).clear()
599
585
  return
600
586
  super().clear()
601
- # MutableMapping.clear() only calls __delitem__ for existing components,
602
- # so it leaves an already empty registry row behind.
603
- self.entity.registry._components_by_entity.pop(self.entity, None)
604
587
 
605
588
 
606
589
  @attrs.define(eq=False, frozen=True, weakref_slot=False)
@@ -627,49 +610,30 @@ class EntityTags(MutableSet[Any]):
627
610
 
628
611
  def add(self, tag: object) -> None:
629
612
  """Add a tag to the entity."""
630
- if tag in self.entity.registry._tags_by_entity[self.entity]:
631
- return # Already has tag
632
- tcod.ecs.query._touch_tag(self.entity.registry, tag) # Tag added
633
-
634
- self.entity.registry._tags_by_entity[self.entity].add(tag)
635
- self.entity.registry._tags_by_key[tag].add(self.entity)
613
+ self.entity.registry._set_data(self.entity, Tag(tag), "Tag", tag, True) # noqa: FBT003
636
614
 
637
615
  def discard(self, tag: object) -> None:
638
616
  """Discard a tag directly held by an entity."""
639
- if tag not in self.entity.registry._tags_by_entity.get(self.entity, ()):
640
- return # Already doesn't have tag
641
- tcod.ecs.query._touch_tag(self.entity.registry, tag) # Tag removed
642
-
643
- self.entity.registry._tags_by_entity[self.entity].discard(tag)
644
- if not self.entity.registry._tags_by_entity[self.entity]:
645
- del self.entity.registry._tags_by_entity[self.entity]
646
-
647
- self.entity.registry._tags_by_key[tag].discard(self.entity)
648
- if not self.entity.registry._tags_by_key[tag]:
649
- del self.entity.registry._tags_by_key[tag]
617
+ self.entity.registry._discard_data(self.entity, Tag(tag), "Tag", tag)
650
618
 
651
619
  def remove(self, tag: object) -> None:
652
620
  """Remove a tag directly held by an entity."""
653
- tags = self.entity.registry._tags_by_entity.get(self.entity)
654
- if tags is None or tag not in tags:
621
+ if not self.entity.registry._discard_data(self.entity, Tag(tag), "Tag", tag):
655
622
  raise KeyError(tag)
656
- self.discard(tag)
657
623
 
658
624
  def clear(self) -> None:
659
625
  """Remove all tags directly in this entity."""
660
- _tags_by_entity = self.entity.registry._tags_by_entity
661
- while tags := _tags_by_entity.get(self.entity):
626
+ while tags := self.entity.registry._any_by_key_by_entity_by_category["Tag"].get(self.entity):
662
627
  self.discard(next(iter(tags)))
663
- _tags_by_entity.pop(self.entity, None)
664
628
 
665
629
  def __contains__(self, x: object) -> bool:
666
630
  """Return True if this entity has the given tag."""
667
- _tags_by_entity = self.entity.registry._tags_by_entity
631
+ _tags_by_entity = self.entity.registry._any_by_key_by_entity_by_category["Tag"]
668
632
  return any(x in _tags_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse))
669
633
 
670
634
  def _as_set(self) -> set[object]:
671
635
  """Return all tags inherited by traversal rules into a single set with no duplicates."""
672
- _tags_by_entity = self.entity.registry._tags_by_entity
636
+ _tags_by_entity = self.entity.registry._any_by_key_by_entity_by_category["Tag"]
673
637
  return set().union(
674
638
  *(_tags_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse))
675
639
  )
@@ -701,42 +665,6 @@ class EntityTags(MutableSet[Any]):
701
665
  return self
702
666
 
703
667
 
704
- def _relations_lookup_add(registry: Registry, origin: Entity, tag: object, target: Entity) -> None:
705
- """Add a relation tag/component to the lookup table and handle side effects."""
706
- registry._relations_lookup[(tag, target)].add(origin)
707
- registry._relations_lookup[(tag, ...)].add(origin)
708
- registry._relations_lookup[(origin, tag, None)].add(target)
709
- registry._relations_lookup[(..., tag, None)].add(target)
710
- tcod.ecs.query._touch_relations(registry, ((tag, target), (tag, ...), (origin, tag, None), (..., tag, None)))
711
-
712
-
713
- def _relations_lookup_discard(registry: Registry, origin: Entity, tag: object, target: Entity) -> None:
714
- """Discard a relation tag/component from the lookup table and handle side effects."""
715
- tags = registry._relation_tags_by_entity.get(origin)
716
- if tags is not None and target in tags.get(tag, ()):
717
- return # A tag relation still owns this index entry.
718
- components = registry._relation_components_by_entity.get(origin)
719
- if components is not None and target in components.get(cast("ComponentKey[object]", tag), ()):
720
- return # A component relation still owns this index entry.
721
- registry._relations_lookup[(tag, target)].discard(origin)
722
- if not registry._relations_lookup[(tag, target)]:
723
- del registry._relations_lookup[(tag, target)]
724
-
725
- registry._relations_lookup[(..., tag, None)].discard(target)
726
- if not registry._relations_lookup[(..., tag, None)]:
727
- del registry._relations_lookup[(..., tag, None)]
728
-
729
- registry._relations_lookup[(origin, tag, None)].discard(target)
730
- if not registry._relations_lookup[(origin, tag, None)]:
731
- del registry._relations_lookup[(origin, tag, None)]
732
-
733
- registry._relations_lookup[(tag, ...)].discard(origin)
734
- if not registry._relations_lookup[(tag, ...)]:
735
- del registry._relations_lookup[(tag, ...)]
736
-
737
- tcod.ecs.query._touch_relations(registry, ((tag, target), (tag, ...), (origin, tag, None), (..., tag, None)))
738
-
739
-
740
668
  @attrs.define(eq=False, frozen=True, weakref_slot=False)
741
669
  class EntityRelationsMapping(MutableSet[Entity]):
742
670
  """A proxy attribute to access entity relation targets like a set.
@@ -761,39 +689,23 @@ class EntityRelationsMapping(MutableSet[Entity]):
761
689
 
762
690
  def add(self, target: Entity) -> None:
763
691
  """Add a relation target to this tag."""
764
- registry = self.entity.registry
765
- registry._relation_tags_by_entity[self.entity][self.key].add(target)
766
-
767
- _relations_lookup_add(registry, self.entity, self.key, target)
692
+ self.entity.registry._set_relation_data(self.entity, Tag(self.key), "RelationTag", self.key, target, value=True)
768
693
 
769
694
  def discard(self, target: Entity) -> None:
770
695
  """Discard a directly held relation target from this tag."""
771
- registry = self.entity.registry
772
-
773
- registry._relation_tags_by_entity[self.entity][self.key].discard(target)
774
- if not registry._relation_tags_by_entity[self.entity][self.key]:
775
- del registry._relation_tags_by_entity[self.entity][self.key]
776
- if not registry._relation_tags_by_entity[self.entity]:
777
- del registry._relation_tags_by_entity[self.entity]
778
-
779
- _relations_lookup_discard(registry, self.entity, self.key, target)
696
+ self.entity.registry._discard_relation_data(self.entity, Tag(self.key), "RelationTag", self.key, target)
780
697
 
781
698
  def remove(self, target: Entity) -> None:
782
699
  """Remove a directly held relation target from this tag.
783
700
 
784
- This will raise KeyError of only an indirect relation target exists.
701
+ This will raise KeyError if only an indirect relation target exists.
785
702
  """
786
- relations = self.entity.registry._relation_tags_by_entity.get(self.entity)
787
- if relations is None:
788
- raise KeyError(target)
789
- targets = relations.get(self.key)
790
- if targets is None or target not in targets:
703
+ if not self.entity.registry._discard_relation_data(self.entity, Tag(self.key), "RelationTag", self.key, target):
791
704
  raise KeyError(target)
792
- self.discard(target)
793
705
 
794
706
  def __contains__(self, target: Entity) -> bool: # type: ignore[override]
795
707
  """Return True if this relation contains the given value."""
796
- _relation_tags_by_entity = self.entity.registry._relation_tags_by_entity
708
+ _relation_tags_by_entity = self.entity.registry._any_by_key_by_entity_by_category["RelationTag"]
797
709
  for entity in _traverse_entities(self.entity, self.traverse):
798
710
  by_entity = _relation_tags_by_entity.get(entity)
799
711
  if by_entity is None:
@@ -804,7 +716,7 @@ class EntityRelationsMapping(MutableSet[Entity]):
804
716
 
805
717
  def _as_set(self) -> set[Entity]:
806
718
  """Return the combined targets of this mapping via traversal with duplicates removed."""
807
- _relation_tags_by_entity = self.entity.registry._relation_tags_by_entity
719
+ _relation_tags_by_entity = self.entity.registry._any_by_key_by_entity_by_category["RelationTag"]
808
720
  results: set[Entity] = set()
809
721
  for entity in _traverse_entities(self.entity, self.traverse):
810
722
  by_entity = _relation_tags_by_entity.get(entity)
@@ -823,7 +735,7 @@ class EntityRelationsMapping(MutableSet[Entity]):
823
735
 
824
736
  def clear(self) -> None:
825
737
  """Discard all targets for this tag relation."""
826
- by_entity = self.entity.registry._relation_tags_by_entity.get(self.entity)
738
+ by_entity = self.entity.registry._any_by_key_by_entity_by_category["RelationTag"].get(self.entity)
827
739
  if by_entity is None:
828
740
  return
829
741
  for key in list(by_entity.get(self.key, ())):
@@ -869,7 +781,7 @@ class EntityRelations(MutableMapping[object, EntityRelationsMapping]):
869
781
 
870
782
  def __iter__(self) -> Iterator[Any]:
871
783
  """Iterate over the unique relation tags of this entity."""
872
- _relation_tags_by_entity = self.entity.registry._relation_tags_by_entity
784
+ _relation_tags_by_entity = self.entity.registry._any_by_key_by_entity_by_category["RelationTag"]
873
785
  empty_dict: dict[object, set[Entity]] = {}
874
786
  yield from set().union(
875
787
  *(
@@ -884,7 +796,7 @@ class EntityRelations(MutableMapping[object, EntityRelationsMapping]):
884
796
 
885
797
  def clear(self) -> None:
886
798
  """Discard all tag relations from an entity."""
887
- for key in list(self.entity.registry._relation_tags_by_entity.get(self.entity, ())):
799
+ for key in list(self.entity.registry._any_by_key_by_entity_by_category["RelationTag"].get(self.entity, ())):
888
800
  del self[key]
889
801
 
890
802
 
@@ -911,7 +823,7 @@ class EntityRelationsExclusive(MutableMapping[object, Entity]):
911
823
  If the relation has no target then raises KeyError.
912
824
  If the relation is not exclusive then raises ValueError.
913
825
  """
914
- _relation_tags_by_entity = self.entity.registry._relation_tags_by_entity
826
+ _relation_tags_by_entity = self.entity.registry._any_by_key_by_entity_by_category["RelationTag"]
915
827
  for entity in _traverse_entities(self.entity, self.traverse):
916
828
  by_entity = _relation_tags_by_entity.get(entity)
917
829
  if by_entity is None:
@@ -977,7 +889,7 @@ class EntityComponentRelationMapping(MutableMapping[Entity, T], Generic[T]):
977
889
 
978
890
  def __getitem__(self, target: Entity) -> T:
979
891
  """Return the component related to a target entity."""
980
- _relation_components_by_entity = self.entity.registry._relation_components_by_entity
892
+ _relation_components_by_entity = self.entity.registry._any_by_key_by_entity_by_category["RelationComponent"]
981
893
  for entity in _traverse_entities(self.entity, self.traverse):
982
894
  by_entity = _relation_components_by_entity.get(entity)
983
895
  if by_entity is None:
@@ -992,38 +904,20 @@ class EntityComponentRelationMapping(MutableMapping[Entity, T], Generic[T]):
992
904
 
993
905
  def __setitem__(self, target: Entity, component: T) -> None:
994
906
  """Assign a component to the target entity."""
995
- registry = self.entity.registry
996
-
997
- old_value = registry._relation_components_by_entity[self.entity][self.key].get(target)
998
- if old_value is None: # Relation added
999
- tcod.ecs.query._touch_relations(
1000
- registry, ((self.key, target), (self.key, ...), (self.entity, self.key, None), (..., self.key, None))
1001
- )
1002
-
1003
- registry._relation_components_by_entity[self.entity][self.key][target] = component
1004
-
1005
- _relations_lookup_add(registry, self.entity, self.key, target)
907
+ self.entity.registry._set_relation_data(
908
+ self.entity, Component.from_key(self.key), "RelationComponent", self.key, target, component
909
+ )
1006
910
 
1007
911
  def __delitem__(self, target: Entity) -> None:
1008
912
  """Delete a component assigned to the target entity."""
1009
- registry = self.entity.registry
1010
- by_entity = registry._relation_components_by_entity.get(self.entity)
1011
- if by_entity is None:
1012
- raise KeyError(target)
1013
- by_key = by_entity.get(self.key)
1014
- if by_key is None:
913
+ if not self.entity.registry._discard_relation_data(
914
+ self.entity, Component.from_key(self.key), "RelationComponent", self.key, target
915
+ ):
1015
916
  raise KeyError(target)
1016
- del by_key[target]
1017
- if not by_key:
1018
- del by_entity[self.key]
1019
- if not by_entity:
1020
- del registry._relation_components_by_entity[self.entity]
1021
-
1022
- _relations_lookup_discard(registry, self.entity, self.key, target)
1023
917
 
1024
918
  def keys(self) -> AbstractSet[Entity]: # type: ignore[override]
1025
919
  """Return all entities with an associated component value."""
1026
- _relation_components_by_entity = self.entity.registry._relation_components_by_entity
920
+ _relation_components_by_entity = self.entity.registry._any_by_key_by_entity_by_category["RelationComponent"]
1027
921
  result: set[Entity] = set()
1028
922
  for entity in _traverse_entities(self.entity, self.traverse):
1029
923
  by_entity = _relation_components_by_entity.get(entity)
@@ -1107,12 +1001,14 @@ class EntityComponentRelations(MutableMapping[ComponentKey[Any], EntityComponent
1107
1001
 
1108
1002
  Does not clear relations targeting this entity.
1109
1003
  """
1110
- for component_key in list(self.entity.registry._relation_components_by_entity.get(self.entity, ())):
1004
+ for component_key in list(
1005
+ self.entity.registry._any_by_key_by_entity_by_category["RelationComponent"].get(self.entity, ())
1006
+ ):
1111
1007
  self[component_key].clear()
1112
1008
 
1113
1009
  def keys(self) -> AbstractSet[ComponentKey[object]]: # type: ignore[override]
1114
1010
  """Returns the components keys this entity has relations for."""
1115
- _relation_components_by_entity = self.entity.registry._relation_components_by_entity
1011
+ _relation_components_by_entity = self.entity.registry._any_by_key_by_entity_by_category["RelationComponent"]
1116
1012
  return set().union(
1117
1013
  *(
1118
1014
  _relation_components_by_entity.get(entity, ())
@@ -13,6 +13,7 @@ from typing_extensions import Self, deprecated
13
13
 
14
14
  import tcod.ecs.entity
15
15
  from tcod.ecs.constants import IsA
16
+ from tcod.ecs.traits import Component, Relation, traits_from_query
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from collections.abc import Iterable, Iterator
@@ -20,7 +21,7 @@ if TYPE_CHECKING:
20
21
 
21
22
  from tcod.ecs.entity import Entity
22
23
  from tcod.ecs.registry import Registry
23
- from tcod.ecs.typing import ComponentKey, _RelationQuery
24
+ from tcod.ecs.typing import ComponentKey, _RelationQuery, _Trait
24
25
 
25
26
  _T1 = TypeVar("_T1")
26
27
  _T2 = TypeVar("_T2")
@@ -35,14 +36,8 @@ class _QueryCache:
35
36
 
36
37
  queries: dict[_Query, AbstractSet[Entity]] = attrs.field(factory=dict)
37
38
  """Table of cached queries."""
38
- by_components: defaultdict[ComponentKey[object], WeakSet[_Query]] = attrs.field(
39
- factory=lambda: defaultdict(WeakSet)
40
- )
41
- """Which queries depend on which components."""
42
- by_tags: defaultdict[object, WeakSet[_Query]] = attrs.field(factory=lambda: defaultdict(WeakSet))
43
- """Which queries depend on which tags."""
44
- by_relations: defaultdict[_RelationQuery, WeakSet[_Query]] = attrs.field(factory=lambda: defaultdict(WeakSet))
45
- """Which queries depend on which relations."""
39
+ by_traits: defaultdict[_Trait, WeakSet[_Query]] = attrs.field(factory=lambda: defaultdict(WeakSet))
40
+ """Which queries depend on which traits."""
46
41
 
47
42
  dependencies: dict[_Query, set[tuple[Registry, _Query]]] = attrs.field(factory=lambda: defaultdict(set))
48
43
  """Tracks which queries depend on the queries of the current registry.
@@ -50,40 +45,18 @@ class _QueryCache:
50
45
  `dependencies[dependency] = {dependant}`
51
46
  """
52
47
 
48
+ def _drop_cached_query(self, query: _Query) -> None:
49
+ """Drop a cached query and all of its dependant queries."""
50
+ self.queries.pop(query, None)
51
+ for sub_registry, sub_query in self.dependencies.pop(query, ()):
52
+ sub_registry._query_cache._drop_cached_query(sub_query)
53
53
 
54
- def _drop_cached_query(cache: _QueryCache, query: _Query) -> None:
55
- """Drop a cached query and all of its dependant queries."""
56
- cache.queries.pop(query, None)
57
- for sub_registry, sub_query in cache.dependencies.pop(query, ()):
58
- _drop_cached_query(_get_query_cache(sub_registry), sub_query)
59
-
60
-
61
- def _touch_component(registry: Registry, component: ComponentKey[object]) -> None:
62
- """Drop cached queries if a component change has invalidated them."""
63
- cache = _get_query_cache(registry)
64
- if component not in cache.by_components:
65
- return
66
- for touched_query in cache.by_components.pop(component, ()):
67
- _drop_cached_query(cache, touched_query)
68
-
69
-
70
- def _touch_tag(registry: Registry, tag: object) -> None:
71
- """Drop cached queries if a tag change has invalidated them."""
72
- cache = _get_query_cache(registry)
73
- if tag not in cache.by_tags:
74
- return
75
- for touched_query in cache.by_tags.pop(tag, ()):
76
- _drop_cached_query(cache, touched_query)
77
-
78
-
79
- def _touch_relations(registry: Registry, relations: Iterable[_RelationQuery]) -> None:
80
- """Drop cached queries if a relation change has invalidated them."""
81
- cache = _get_query_cache(registry)
82
- for relation in relations:
83
- if relation not in cache.by_relations:
84
- continue
85
- for touched_query in cache.by_relations.pop(relation, ()):
86
- _drop_cached_query(cache, touched_query)
54
+ def _touch_trait(self, trait: _Trait) -> None:
55
+ """Drop cached queries if a trait change has invalidated them."""
56
+ if trait not in self.by_traits:
57
+ return
58
+ for touched_query in self.by_traits.pop(trait, ()):
59
+ self._drop_cached_query(touched_query)
87
60
 
88
61
 
89
62
  def _check_suspicious_tags(tags: Iterable[object], stacklevel: int = 2) -> None:
@@ -106,29 +79,20 @@ def _fetch_relation_table(registry: Registry, relation: _RelationQuery) -> Abstr
106
79
 
107
80
  For advanced cases `BoundQuery` this returns the subset of entities following the query condition.
108
81
  """
109
- if len(relation) == 2: # noqa: PLR2004
110
- tag, target = relation
111
- if not isinstance(target, BoundQuery):
112
- return registry._relations_lookup.get((tag, target), frozenset())
113
-
114
- registry = target.registry
115
- return set().union(*(registry._relations_lookup.get((tag, entity), ()) for entity in target))
116
- origin, tag, target_none = relation
117
- if not isinstance(origin, BoundQuery):
118
- return registry._relations_lookup.get((origin, tag, target_none), frozenset())
119
-
120
- registry = origin.registry
121
- return set().union(*(registry._relations_lookup.get((entity, tag, None), ()) for entity in origin))
122
-
123
-
124
- def _get_query_cache(registry: Registry) -> _QueryCache:
125
- """Return the cache owned by the given registry."""
126
- return registry._query_cache
82
+ trait: Relation[Any] = Relation._from_query(relation)
83
+ if not isinstance(trait.target, BoundQuery):
84
+ return registry._any_by_entity_by_trait.get(trait, {}).keys()
85
+ return set().union(
86
+ *(
87
+ registry._any_by_entity_by_trait.get(Relation(trait.trait, entity, trait.inverse), ())
88
+ for entity in trait.target
89
+ )
90
+ )
127
91
 
128
92
 
129
93
  def _get_query(registry: Registry, query: _Query) -> AbstractSet[Entity]:
130
94
  """Return the entities for the given query and registry."""
131
- cache = _get_query_cache(registry)
95
+ cache = registry._query_cache
132
96
  if cache is not None:
133
97
  cached_entities = cache.queries.get(query)
134
98
  if cached_entities is not None:
@@ -164,34 +128,21 @@ class _Query(Protocol):
164
128
  ...
165
129
 
166
130
  def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]:
167
- """Compile the entities of this query, returning a set which must not be modified."""
131
+ """Compile the entities of this query, returning a set which must not be modified by the caller."""
168
132
  ...
169
133
 
170
134
 
171
135
  @attrs.define(frozen=True)
172
- class _QueryComponent:
173
- """Query all entities with the given component."""
174
-
175
- _component: ComponentKey[object]
176
-
177
- def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: # noqa: ARG002
178
- cache.by_components[self._component].add(self)
179
-
180
- def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002
181
- return registry._components_by_type.get(self._component, {}).keys()
182
-
183
-
184
- @attrs.define(frozen=True)
185
- class _QueryTag:
186
- """Query all entities with the given tag."""
136
+ class _QueryTrait:
137
+ """Query all entities with the given trait."""
187
138
 
188
- _tag: object
139
+ trait: _Trait
189
140
 
190
141
  def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: # noqa: ARG002
191
- cache.by_tags[self._tag].add(self)
142
+ cache.by_traits[self.trait].add(self)
192
143
 
193
144
  def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002
194
- return registry._tags_by_key.get(self._tag, set())
145
+ return registry._any_by_entity_by_trait.get(self.trait, {}).keys()
195
146
 
196
147
 
197
148
  @attrs.define(frozen=True)
@@ -202,19 +153,13 @@ class _QueryRelation:
202
153
 
203
154
  def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None:
204
155
  """Add this query to the cache and mark it dependant on a registry query if the relation uses one."""
156
+ trait: Relation[Any] = Relation._from_query(self._relation)
205
157
 
206
- def _get_registry_query() -> BoundQuery | None:
207
- """Return the bound query of a relation if it exists."""
208
- if isinstance(self._relation[0], BoundQuery):
209
- return self._relation[0]
210
- if isinstance(self._relation[-1], BoundQuery):
211
- return self._relation[-1]
212
- return None
213
-
214
- cache.by_relations[self._relation].add(self)
215
- w_query = _get_registry_query()
216
- if w_query is not None:
217
- _get_query_cache(w_query.registry).dependencies[w_query._query].add((registry, self))
158
+ target_query = trait.target
159
+ if isinstance(target_query, BoundQuery):
160
+ target_query.registry._query_cache.dependencies[target_query._query].add((registry, self))
161
+ else:
162
+ cache.by_traits[trait].add(self)
218
163
 
219
164
  def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002
220
165
  return _fetch_relation_table(registry, self._relation)
@@ -302,10 +247,9 @@ class _QueryTraversalPropagation:
302
247
  while unchecked_set and (self._max_depth is None or depth < self._max_depth):
303
248
  depth += 1
304
249
  new_set: set[Entity] = set()
305
- empty_set: frozenset[Entity] = frozenset()
306
250
  for traverse_key in self._traverse_keys:
307
251
  for unchecked in unchecked_set:
308
- new_set |= registry._relations_lookup.get((traverse_key, unchecked), empty_set)
252
+ new_set |= registry._any_by_entity_by_trait.get(Relation(traverse_key, unchecked), {}).keys()
309
253
  new_set -= cumulative_set
310
254
  cumulative_set |= new_set
311
255
  unchecked_set = new_set
@@ -355,8 +299,10 @@ class BoundQuery:
355
299
  ) -> Iterator[_Query]:
356
300
  """Convert parameters into queries."""
357
301
  traverse = tuple(traverse)
358
- yield from (_QueryTraversalPropagation(_QueryComponent(component), traverse, depth) for component in components)
359
- yield from (_QueryTraversalPropagation(_QueryTag(tag), traverse, depth) for tag in tags)
302
+ yield from (
303
+ _QueryTraversalPropagation(_QueryTrait(trait), traverse, depth)
304
+ for trait in traits_from_query(components=components, tags=tags)
305
+ )
360
306
  yield from (_QueryTraversalPropagation(_QueryRelation(relations), traverse, depth) for relations in relations)
361
307
 
362
308
  def all_of(
@@ -459,7 +405,7 @@ class BoundQuery:
459
405
  if component_key is Entity:
460
406
  entity_components.append(entities)
461
407
  continue
462
- registry_components = self.registry._components_by_type[component_key]
408
+ registry_components = self.registry._any_by_entity_by_trait.get(Component.from_key(component_key), {})
463
409
  try:
464
410
  entity_components.append([registry_components[entity] for entity in entities])
465
411
  except KeyError: # Some matching entities inherit this component.