Skip to content

Commit b2c74c2

Browse files
committed
Add TypeForm support
Converts containers to use TypeForm instead of Type. This allows for type expressions to be used as component types.
1 parent 1ffd940 commit b2c74c2

File tree

6 files changed

+34
-16
lines changed

6 files changed

+34
-16
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Dropped support for Python 3.8 & 3.9.
1313

14+
### Fixed
15+
16+
- Component key type-hints now use `TypeForm`.
17+
Complex component types which already worked at runtime are now recognized by type linters.
18+
1419
## [5.4.1] - 2025-07-20
1520

1621
Maintenance release

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ dependencies = [
2222
"attrs >=23.1.0",
2323
"cattrs >=23.1.2",
2424
"sentinel-value >=1.0.0",
25-
"typing-extensions >=4.9.0",
25+
"typing-extensions >=4.13.1",
2626
]
2727

2828
[tool.setuptools_scm]
@@ -50,7 +50,8 @@ Source = "https://github.com/HexDecimal/python-tcod-ecs"
5050
files = "."
5151
exclude = ['^build/', '^\.']
5252
explicit_package_bases = true
53-
python_version = "3.10" # Type check Python version with EllipsisType
53+
python_version = "3.10"
54+
enable_incomplete_feature = ["TypeForm"]
5455
warn_unused_configs = true
5556
disallow_any_generics = true
5657
disallow_subclassing_any = true

tcod/ecs/entity.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import attrs
1717
from sentinel_value import sentinel
18-
from typing_extensions import Self, deprecated
18+
from typing_extensions import Self, TypeForm, deprecated
1919

2020
import tcod.ecs.callbacks
2121
import tcod.ecs.query
@@ -382,7 +382,7 @@ def _traverse_entities(start: Entity, traverse_parents: tuple[object, ...]) -> I
382382

383383

384384
@attrs.define(eq=False, frozen=True, weakref_slot=False)
385-
class EntityComponents(MutableMapping[type[Any] | tuple[object, type[Any]], object]):
385+
class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any]], object]):
386386
"""A proxy attribute to access an entities components like a dictionary.
387387
388388
See :any:`Entity.components`.
@@ -440,7 +440,7 @@ def __setitem__(self, key: ComponentKey[T], value: T) -> None:
440440

441441
tcod.ecs.callbacks._on_component_changed(key, self.entity, old_value, value)
442442

443-
def __delitem__(self, key: type[object] | tuple[object, type[object]]) -> None:
443+
def __delitem__(self, key: TypeForm[object] | tuple[object, TypeForm[object]]) -> None:
444444
"""Delete a directly held component from an entity."""
445445
assert self.__assert_key(key)
446446

@@ -467,8 +467,8 @@ def keys(self) -> AbstractSet[ComponentKey[object]]: # type: ignore[override]
467467
*(_components_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse))
468468
)
469469

470-
def __contains__(self, key: ComponentKey[object]) -> bool: # type: ignore[override]
471-
"""Return True if this entity has the provided component."""
470+
def __contains__(self, key: object) -> bool:
471+
"""Return True if this entity has the provided component key."""
472472
_components_by_entity = self.entity.registry._components_by_entity
473473
return any(
474474
key in _components_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse)
@@ -493,21 +493,20 @@ def update_values(self, values: Iterable[object]) -> None:
493493
self.set(value)
494494

495495
@deprecated("This method has been deprecated. Iterate over items instead.", category=FutureWarning)
496-
def by_name_type(self, name_type: type[_T1], component_type: type[_T2]) -> Iterator[tuple[_T1, type[_T2]]]:
496+
def by_name_type(self, name_type: type[_T1], component_type: TypeForm[_T2]) -> Iterator[tuple[_T1, TypeForm[_T2]]]:
497497
"""Iterate over all of an entities component keys with a specific (name_type, component_type) combination.
498498
499499
.. versionadded:: 3.0
500500
501501
.. deprecated:: 3.1
502502
This method has been deprecated. Iterate over items instead.
503503
"""
504-
# Naive implementation until I feel like optimizing it
505504
for key in self:
506505
if not isinstance(key, tuple):
507506
continue
508507
key_name, key_component = key
509508
if key_component is component_type and isinstance(key_name, name_type):
510-
yield key_name, key_component
509+
yield key_name, key_component # type: ignore[unused-ignore] # Too complex for PyLance, deprecated anyways
511510

512511
@overload
513512
def __ior__(self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any]) -> Self: ...
@@ -1044,14 +1043,14 @@ def __getitem__(self, key: ComponentKey[T]) -> EntityComponentRelationMapping[T]
10441043
"""Access relations for this component key as a `{target: component}` dict-like object."""
10451044
return EntityComponentRelationMapping(self.entity, key, self.traverse)
10461045

1047-
def __setitem__(self, __key: ComponentKey[T], __values: Mapping[Entity, object], /) -> None:
1046+
def __setitem__(self, __key: ComponentKey[T], __values: Mapping[Entity, T], /) -> None:
10481047
"""Redefine the component relations for this entity.
10491048
10501049
..versionadded:: 4.2.0
10511050
"""
10521051
if isinstance(__values, EntityComponentRelationMapping) and __values.entity is self.entity:
10531052
return
1054-
mapping: EntityComponentRelationMapping[object] = self[__key]
1053+
mapping: EntityComponentRelationMapping[T] = self[__key]
10551054
mapping.clear()
10561055
for target, component in __values.items():
10571056
mapping[target] = component

tcod/ecs/registry.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ def _defaultdict_of_dict() -> defaultdict[_T1, dict[_T2, _T3]]:
3333

3434

3535
def _components_by_entity_from(
36-
by_type: defaultdict[ComponentKey[object], dict[Entity, Any]],
37-
) -> defaultdict[Entity, dict[ComponentKey[object], Any]]:
36+
by_type: defaultdict[ComponentKey[_T1], dict[Entity, _T1]],
37+
) -> defaultdict[Entity, dict[ComponentKey[_T1], _T1]]:
3838
"""Return the component lookup table from the components sparse-set."""
39-
by_entity: defaultdict[Entity, dict[ComponentKey[object], Any]] = defaultdict(dict)
39+
by_entity: defaultdict[Entity, dict[ComponentKey[_T1], _T1]] = defaultdict(dict)
4040
for component_key, components in by_type.items():
4141
for entity, component in components.items():
4242
by_entity[entity][component_key] = component

tcod/ecs/typing.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from types import EllipsisType
66
from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
77

8+
from typing_extensions import TypeForm
9+
810
if TYPE_CHECKING:
911
from tcod.ecs.entity import Entity
1012
from tcod.ecs.query import BoundQuery
@@ -15,7 +17,7 @@
1517

1618
_T = TypeVar("_T")
1719

18-
ComponentKey: TypeAlias = type[_T] | tuple[object, type[_T]]
20+
ComponentKey: TypeAlias = TypeForm[_T] | tuple[object, TypeForm[_T]]
1921
"""ComponentKey is plain `type` or tuple `(tag, type)`."""
2022

2123
_RelationTargetLookup: TypeAlias = Entity | EllipsisType

tests/test_ecs.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pickletools
88
import sys
99
from collections.abc import Callable, Iterator # noqa: TC003
10+
from typing import Final
1011

1112
import pytest
1213

@@ -290,3 +291,13 @@ def test_any_of() -> None:
290291
assert world.Q.any_of(tags=["foo"])
291292
assert world.Q.any_of(tags=["foo", "bar"])
292293
assert not world.Q.any_of(tags=["bar"])
294+
295+
296+
def test_type_form() -> None:
297+
world = tcod.ecs.Registry()
298+
TupleKey: Final = ("TupleKey", tuple[int, int]) # noqa: N806
299+
300+
# tuple layout is forgotten when TypeForm support is missing
301+
world[None].components[TupleKey] = (1, 2)
302+
x, y = world[None].components[TupleKey]
303+
assert (x, y) == (1, 2)

0 commit comments

Comments
 (0)