tcod-ecs 5.5.0__tar.gz → 5.5.2__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.0
3
+ Version: 5.5.2
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
@@ -92,6 +92,7 @@ line-length = 120
92
92
  select = ["ALL"]
93
93
  ignore = [
94
94
  "A005", # stdlib-module-shadowing, workaround VSCode treating all modules as local
95
+ "CPY001", # missing-copyright-notice
95
96
  "COM", # flake8-commas, handled by formatter
96
97
  "E501", # line-too-long
97
98
  "S101", # assert
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '5.5.2'
22
+ __version_tuple__ = version_tuple = (5, 5, 2)
23
+
24
+ __commit_id__ = commit_id = 'g60f279cbb'
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing_extensions import Never
6
+
5
7
 
6
8
  class _IgnoreSetState(type):
7
9
  def __setstate__(cls, _state: object) -> None:
@@ -11,9 +13,9 @@ class _IgnoreSetState(type):
11
13
  class IsA(metaclass=_IgnoreSetState):
12
14
  """The default is-a relationship tag used for entity inheritance."""
13
15
 
14
- def __new__(cls: type[IsA], *_args: object) -> type[IsA]: # type: ignore[misc]
15
- """Return own type instead of instance, for outdated sentinel-value pickle data."""
16
- return cls
16
+ def __new__(cls: type[IsA], *_: object) -> Never: # noqa: D102
17
+ # Return own type instead of instance, for outdated sentinel-value pickle data.
18
+ return cls # type: ignore[misc]
17
19
 
18
20
 
19
21
  _sentinel_IsA = IsA # Compatibility with sentinel-value, deprecated since 5.4 # noqa: N816
@@ -9,6 +9,7 @@ from typing import (
9
9
  Final,
10
10
  Generic,
11
11
  TypeVar,
12
+ cast,
12
13
  overload,
13
14
  )
14
15
  from weakref import WeakKeyDictionary, WeakValueDictionary
@@ -419,9 +420,12 @@ class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any
419
420
  assert self.__assert_key(key)
420
421
  _components_by_entity = self.entity.registry._components_by_entity
421
422
  for entity in _traverse_entities(self.entity, self.traverse):
423
+ components = _components_by_entity.get(entity)
424
+ if components is None:
425
+ continue
422
426
  try:
423
- return _components_by_entity[entity][key] # type: ignore[no-any-return]
424
- except KeyError: # noqa: PERF203
427
+ return components[key] # type: ignore[no-any-return]
428
+ except KeyError:
425
429
  pass
426
430
  raise KeyError(key)
427
431
 
@@ -443,10 +447,13 @@ class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any
443
447
  """Delete a directly held component from an entity."""
444
448
  assert self.__assert_key(key)
445
449
 
446
- old_value = self.entity.registry._components_by_entity[self.entity].get(key)
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)
447
454
 
448
- del self.entity.registry._components_by_entity[self.entity][key]
449
- if not self.entity.registry._components_by_entity[self.entity]:
455
+ del components[key]
456
+ if not components:
450
457
  del self.entity.registry._components_by_entity[self.entity]
451
458
 
452
459
  del self.entity.registry._components_by_type[key][self.entity]
@@ -588,8 +595,12 @@ class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any
588
595
  def clear(self) -> None:
589
596
  """Remove any components stored directly in this entity."""
590
597
  if self.traverse:
591
- return self(traverse=()).clear()
592
- return super().clear()
598
+ self(traverse=()).clear()
599
+ return
600
+ 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)
593
604
 
594
605
 
595
606
  @attrs.define(eq=False, frozen=True, weakref_slot=False)
@@ -602,6 +613,11 @@ class EntityTags(MutableSet[Any]):
602
613
  entity: Entity
603
614
  traverse: tuple[object, ...]
604
615
 
616
+ @classmethod
617
+ def _from_iterable(cls, it: Iterable[_T1]) -> set[_T1]:
618
+ """Return an independent set for the inherited set operations."""
619
+ return set(it)
620
+
605
621
  def __call__(self, *, traverse: Iterable[object]) -> Self:
606
622
  """Update this view with alternative parameters, such as a specific traversal relation.
607
623
 
@@ -620,7 +636,7 @@ class EntityTags(MutableSet[Any]):
620
636
 
621
637
  def discard(self, tag: object) -> None:
622
638
  """Discard a tag directly held by an entity."""
623
- if tag not in self.entity.registry._tags_by_entity[self.entity]:
639
+ if tag not in self.entity.registry._tags_by_entity.get(self.entity, ()):
624
640
  return # Already doesn't have tag
625
641
  tcod.ecs.query._touch_tag(self.entity.registry, tag) # Tag removed
626
642
 
@@ -642,8 +658,9 @@ class EntityTags(MutableSet[Any]):
642
658
  def clear(self) -> None:
643
659
  """Remove all tags directly in this entity."""
644
660
  _tags_by_entity = self.entity.registry._tags_by_entity
645
- while _tags_by_entity[self.entity]:
646
- self.discard(next(iter(_tags_by_entity[self.entity])))
661
+ while tags := _tags_by_entity.get(self.entity):
662
+ self.discard(next(iter(tags)))
663
+ _tags_by_entity.pop(self.entity, None)
647
664
 
648
665
  def __contains__(self, x: object) -> bool:
649
666
  """Return True if this entity has the given tag."""
@@ -695,6 +712,12 @@ def _relations_lookup_add(registry: Registry, origin: Entity, tag: object, targe
695
712
 
696
713
  def _relations_lookup_discard(registry: Registry, origin: Entity, tag: object, target: Entity) -> None:
697
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.
698
721
  registry._relations_lookup[(tag, target)].discard(origin)
699
722
  if not registry._relations_lookup[(tag, target)]:
700
723
  del registry._relations_lookup[(tag, target)]
@@ -725,6 +748,11 @@ class EntityRelationsMapping(MutableSet[Entity]):
725
748
  key: object
726
749
  traverse: tuple[object, ...]
727
750
 
751
+ @classmethod
752
+ def _from_iterable(cls, it: Iterable[_T1]) -> set[_T1]:
753
+ """Return an independent set for the inherited set operations."""
754
+ return set(it)
755
+
728
756
  if __debug__:
729
757
 
730
758
  def __attrs_post_init__(self) -> None:
@@ -826,6 +854,7 @@ class EntityRelations(MutableMapping[object, EntityRelationsMapping]):
826
854
  def __setitem__(self, key: object, values: Iterable[Entity]) -> None:
827
855
  """Overwrite the targets of a relation tag with the new values."""
828
856
  assert not isinstance(values, Entity), "Did you mean `entity.relations[key] = (target,)`?"
857
+ values = tuple(values) # Values may be a live view of the mapping being replaced.
829
858
  mapping = EntityRelationsMapping(self.entity, key, self.traverse)
830
859
  mapping.clear()
831
860
  for v in values:
@@ -978,10 +1007,16 @@ class EntityComponentRelationMapping(MutableMapping[Entity, T], Generic[T]):
978
1007
  def __delitem__(self, target: Entity) -> None:
979
1008
  """Delete a component assigned to the target entity."""
980
1009
  registry = self.entity.registry
981
- del registry._relation_components_by_entity[self.entity][self.key][target]
982
- if not registry._relation_components_by_entity[self.entity][self.key]:
983
- del registry._relation_components_by_entity[self.entity][self.key]
984
- if not registry._relation_components_by_entity[self.entity]:
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:
1015
+ raise KeyError(target)
1016
+ del by_key[target]
1017
+ if not by_key:
1018
+ del by_entity[self.key]
1019
+ if not by_entity:
985
1020
  del registry._relation_components_by_entity[self.entity]
986
1021
 
987
1022
  _relations_lookup_discard(registry, self.entity, self.key, target)
@@ -1047,11 +1082,16 @@ class EntityComponentRelations(MutableMapping[ComponentKey[Any], EntityComponent
1047
1082
 
1048
1083
  ..versionadded:: 4.2.0
1049
1084
  """
1050
- if isinstance(__values, EntityComponentRelationMapping) and __values.entity is self.entity:
1085
+ if (
1086
+ isinstance(__values, EntityComponentRelationMapping)
1087
+ and __values.entity is self.entity
1088
+ and __values.key == __key
1089
+ ):
1051
1090
  return
1091
+ items = tuple(__values.items()) # The input may inherit values from the mapping being replaced.
1052
1092
  mapping: EntityComponentRelationMapping[T] = self[__key]
1053
1093
  mapping.clear()
1054
- for target, component in __values.items():
1094
+ for target, component in items:
1055
1095
  mapping[target] = component
1056
1096
 
1057
1097
  def __delitem__(self, key: ComponentKey[object]) -> None:
@@ -6,7 +6,7 @@ import itertools
6
6
  import warnings
7
7
  from collections import defaultdict
8
8
  from typing import TYPE_CHECKING, Any, Protocol, TypeVar, overload
9
- from weakref import WeakKeyDictionary, WeakSet
9
+ from weakref import WeakSet
10
10
 
11
11
  import attrs
12
12
  from typing_extensions import Self, deprecated
@@ -29,10 +29,6 @@ _T4 = TypeVar("_T4")
29
29
  _T5 = TypeVar("_T5")
30
30
 
31
31
 
32
- _query_caches: WeakKeyDictionary[Registry, _QueryCache] = WeakKeyDictionary()
33
- """The master table of cached queries."""
34
-
35
-
36
32
  @attrs.define
37
33
  class _QueryCache:
38
34
  """Main data structure for the query cache."""
@@ -59,7 +55,7 @@ def _drop_cached_query(cache: _QueryCache, query: _Query) -> None:
59
55
  """Drop a cached query and all of its dependant queries."""
60
56
  cache.queries.pop(query, None)
61
57
  for sub_registry, sub_query in cache.dependencies.pop(query, ()):
62
- _drop_cached_query(_query_caches[sub_registry], sub_query)
58
+ _drop_cached_query(_get_query_cache(sub_registry), sub_query)
63
59
 
64
60
 
65
61
  def _touch_component(registry: Registry, component: ComponentKey[object]) -> None:
@@ -126,11 +122,8 @@ def _fetch_relation_table(registry: Registry, relation: _RelationQuery) -> Abstr
126
122
 
127
123
 
128
124
  def _get_query_cache(registry: Registry) -> _QueryCache:
129
- """Return the global cache for the given registry, creating it if it does not exist."""
130
- cache = _query_caches.get(registry)
131
- if cache is None:
132
- cache = _query_caches[registry] = _QueryCache()
133
- return cache
125
+ """Return the cache owned by the given registry."""
126
+ return registry._query_cache
134
127
 
135
128
 
136
129
  def _get_query(registry: Registry, query: _Query) -> AbstractSet[Entity]:
@@ -237,10 +230,6 @@ class _QueryLogicalAnd:
237
230
  _all_of: frozenset[_Query] = frozenset()
238
231
  _none_of: frozenset[_Query] = frozenset()
239
232
 
240
- def __attrs_post_init__(self) -> None:
241
- """Verify the current state."""
242
- assert self._all_of.isdisjoint(self._none_of)
243
-
244
233
  def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None:
245
234
  for dependency in itertools.chain(self._all_of, self._none_of):
246
235
  cache.dependencies[dependency].add((registry, self))
@@ -465,13 +454,16 @@ class BoundQuery:
465
454
  Entity = tcod.ecs.entity.Entity # noqa: N806
466
455
 
467
456
  entities = list(self.all_of(components=set(key) - {Entity}).get_entities())
468
- entity_components = []
457
+ entity_components: list[list[Any]] = []
469
458
  for component_key in key:
470
459
  if component_key is Entity:
471
460
  entity_components.append(entities)
472
461
  continue
473
462
  registry_components = self.registry._components_by_type[component_key]
474
- entity_components.append([registry_components[entity] for entity in entities])
463
+ try:
464
+ entity_components.append([registry_components[entity] for entity in entities])
465
+ except KeyError: # Some matching entities inherit this component.
466
+ entity_components.append([entity.components[component_key] for entity in entities])
475
467
  return zip(*entity_components, strict=True)
476
468
 
477
469
 
@@ -13,6 +13,7 @@ from typing_extensions import deprecated
13
13
  import tcod.ecs._converter
14
14
  import tcod.ecs.query
15
15
  from tcod.ecs.entity import Entity
16
+ from tcod.ecs.query import _QueryCache
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from tcod.ecs.typing import ComponentKey, _RelationTargetLookup
@@ -71,7 +72,7 @@ def _relations_lookup_from(
71
72
  for component_key, target_components in components.items():
72
73
  for target in target_components:
73
74
  relations_lookup[(component_key, ...)].add(origin)
74
- relations_lookup[(component_key, origin)].add(origin)
75
+ relations_lookup[(component_key, target)].add(origin)
75
76
  relations_lookup[(origin, component_key, None)].add(target)
76
77
  relations_lookup[(..., component_key, None)].add(target)
77
78
 
@@ -152,6 +153,9 @@ class Registry:
152
153
  dict[Entity] = entities_name
153
154
  """
154
155
 
156
+ _query_cache: _QueryCache = attrs.field(init=False, factory=_QueryCache, repr=False)
157
+ """Transient query cache, owned here so cached entities do not keep registries alive globally."""
158
+
155
159
  @property
156
160
  @deprecated(
157
161
  "The 'registry.global_' attribute has been deprecated. Use 'registry[None]' to access this entity.",
@@ -178,6 +182,7 @@ class Registry:
178
182
 
179
183
  def __setstate__(self, state: dict[str, Any]) -> None:
180
184
  """Unpickle this object and handle state migration."""
185
+ self._query_cache = _QueryCache()
181
186
  global_: Entity | None = state.pop("global_", None) # Migrate from version <=1.2.0
182
187
 
183
188
  # These attributes contain redundant data and will be removed
@@ -1,34 +0,0 @@
1
- # file generated by setuptools-scm
2
- # don't change, don't track in version control
3
-
4
- __all__ = [
5
- "__version__",
6
- "__version_tuple__",
7
- "version",
8
- "version_tuple",
9
- "__commit_id__",
10
- "commit_id",
11
- ]
12
-
13
- TYPE_CHECKING = False
14
- if TYPE_CHECKING:
15
- from typing import Tuple
16
- from typing import Union
17
-
18
- VERSION_TUPLE = Tuple[Union[int, str], ...]
19
- COMMIT_ID = Union[str, None]
20
- else:
21
- VERSION_TUPLE = object
22
- COMMIT_ID = object
23
-
24
- version: str
25
- __version__: str
26
- __version_tuple__: VERSION_TUPLE
27
- version_tuple: VERSION_TUPLE
28
- commit_id: COMMIT_ID
29
- __commit_id__: COMMIT_ID
30
-
31
- __version__ = version = '5.5.0'
32
- __version_tuple__ = version_tuple = (5, 5, 0)
33
-
34
- __commit_id__ = commit_id = 'gf2ee28b92'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes