From c3b849dcfcfa1b0db513c2fae34124e7b0cc6585 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 20 Oct 2023 17:19:16 -0700 Subject: [PATCH 01/83] Prepare 5.0.0 release Last second additions with tests --- CHANGELOG.md | 2 ++ tcod/ecs/entity.py | 27 ++++++++++++++++++++++++--- tests/test_traversal.py | 5 +++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d683175..6615706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [5.0.0] - 2023-10-20 ### Added - Added the `tcod.ecs.IsA` sentinel value. - Entities will automatically inherit components/tags/relations from entities they have an `IsA` relationship with. https://github.com/HexDecimal/python-tcod-ecs/pull/15 diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 018960b..dbf2f43 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -163,7 +163,7 @@ def instantiate(self) -> Self: >>> parent.components[Tuple[str, ...]] ('foo',) - .. versionadded:: Unreleased + .. versionadded:: 5.0 """ new_entity = self.__class__(self.world, object()) new_entity.relation_tag[IsA] = self @@ -381,7 +381,7 @@ class EntityComponents(MutableMapping[Union[Type[Any], Tuple[object, Type[Any]]] def __call__(self, *, traverse: Iterable[object]) -> Self: """Update this view with alternative parameters, such as a specific traversal relation. - .. versionadded:: Unreleased + .. versionadded:: 5.0 """ return self.__class__(self.entity, tuple(traverse)) @@ -538,7 +538,7 @@ class EntityTags(MutableSet[Any]): def __call__(self, *, traverse: Iterable[object]) -> Self: """Update this view with alternative parameters, such as a specific traversal relation. - .. versionadded:: Unreleased + .. versionadded:: 5.0 """ return self.__class__(self.entity, tuple(traverse)) @@ -739,6 +739,13 @@ class EntityRelations(MutableMapping[object, EntityRelationsMapping]): entity: Entity traverse: tuple[object, ...] + def __call__(self, *, traverse: Iterable[object]) -> Self: + """Update this view with alternative parameters, such as a specific traversal relation. + + .. versionadded:: 5.0 + """ + return self.__class__(self.entity, tuple(traverse)) + def __getitem__(self, key: object) -> EntityRelationsMapping: """Return the relation mapping for a tag.""" return EntityRelationsMapping(self.entity, key, self.traverse) @@ -789,6 +796,13 @@ class EntityRelationsExclusive(MutableMapping[object, Entity]): entity: Entity traverse: tuple[object, ...] + def __call__(self, *, traverse: Iterable[object]) -> Self: + """Update this view with alternative parameters, such as a specific traversal relation. + + .. versionadded:: 5.0 + """ + return self.__class__(self.entity, tuple(traverse)) + def __getitem__(self, key: object) -> Entity: """Return the relation target for a key. @@ -931,6 +945,13 @@ def __attrs_post_init__(self) -> None: """Validate attributes.""" assert isinstance(self.entity, Entity), self.entity + def __call__(self, *, traverse: Iterable[object]) -> Self: + """Update this view with alternative parameters, such as a specific traversal relation. + + .. versionadded:: 5.0 + """ + return self.__class__(self.entity, tuple(traverse)) + def __getitem__(self, key: ComponentKey[T]) -> EntityComponentRelationMapping[T]: """Access relations for this component key as a `{target: component}` dict-like object.""" return EntityComponentRelationMapping(self.entity, key, self.traverse) diff --git a/tests/test_traversal.py b/tests/test_traversal.py index e7b798c..d72319b 100644 --- a/tests/test_traversal.py +++ b/tests/test_traversal.py @@ -175,10 +175,14 @@ def test_relation_traversal() -> None: world["A"].relation_tag["test"] = world["foo"] world["B"].relation_tag["test"] = world["bar"] + assert set(world["C"].relation_tags_many["test"]) == {world["foo"], world["bar"]} assert len(world["C"].relation_tags_many["test"]) == 2 # noqa: PLR2004 assert world["C"].relation_tag["test"] == world["bar"] assert world.Q.all_of(relations=[("test", ...)]).get_entities() == {world["A"], world["B"], world["C"]} + assert not set(world["C"].relation_tags_many(traverse=())["test"]) + with pytest.raises(KeyError): + world["C"].relation_tag(traverse=())["test"] del world["B"].relation_tag["test"] assert set(world["C"].relation_tags_many["test"]) == {world["foo"]} @@ -189,6 +193,7 @@ def test_relation_traversal() -> None: world["A"].relation_components[str][world["foo"]] = "foo" assert world.Q.all_of(relations=[(str, ...)]).get_entities() == {world["A"], world["B"], world["C"]} + assert not set(world["C"].relation_components(traverse=())) world["B"].relation_components[str][world["bar"]] = "bar" world["C"].relation_components[str][world["bar"]] = "replaced" From 44a36dd61db2d9e5f57909aaff9c35da6fc5cf9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 21:09:48 +0000 Subject: [PATCH 02/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.9.1 → 23.10.1](https://github.com/psf/black/compare/23.9.1...23.10.1) - [github.com/astral-sh/ruff-pre-commit: v0.0.292 → v0.1.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.292...v0.1.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e2de62..e83fb2a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,11 +15,11 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.292 + rev: v0.1.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From c86037ded3d7504f121b7da4dddcaacfd7554b55 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 24 Oct 2023 22:28:28 -0700 Subject: [PATCH 03/83] Update formatters to use Ruff --- .github/workflows/python-package.yml | 25 +++++-------------------- .pre-commit-config.yaml | 11 ++--------- .vscode/extensions.json | 9 +++++++++ .vscode/settings.json | 3 +-- pyproject.toml | 2 ++ 5 files changed, 19 insertions(+), 31 deletions(-) create mode 100644 .vscode/extensions.json diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 6d20db5..1fbac88 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -13,32 +13,17 @@ defaults: shell: bash jobs: - black: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install Black - run: pip install black - - name: Run Black - run: black --check --diff tcod/ - - isort: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install isort - run: pip install isort - - name: isort - run: isort tcod/ --check --diff - ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Ruff run: pip install ruff - - name: Ruff - run: ruff . + - name: Ruff Check + run: ruff check . --output-format=github + - name: Ruff Format + run: ruff format . --check + test: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e83fb2a..91b04ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,18 +14,11 @@ repos: - id: debug-statements - id: fix-byte-order-marker - id: detect-private-key - - repo: https://github.com/psf/black - rev: 23.10.1 - hooks: - - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.1 + rev: v0.1.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort + - id: ruff-format default_language_version: python: python3.11 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..6333cdc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "charliermarsh.ruff", + "ms-python.mypy-type-checker", + "ms-python.vscode-pylance", + "ms-python.python", + "streetsidesoftware.code-spell-checker" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 73adb4a..f0e546a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -54,7 +54,6 @@ "WAXD", "WINDOWLEAVE" ], - "python.formatting.provider": "none", "editor.codeActionsOnSave": { "source.fixAll": true, }, @@ -64,6 +63,6 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" + "editor.defaultFormatter": "charliermarsh.ruff" }, } diff --git a/pyproject.toml b/pyproject.toml index a61887c..9048b1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,8 @@ ignore = [ "S101", # assert "ANN101", # missing-type-self "ANN102", # missing-type-cls + "D206", # indent-with-spaces + "W191", # tab-indentation ] line-length = 120 From e74de9f0a360046e5c200fae296e2a1407e07188 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 20:49:43 +0000 Subject: [PATCH 04/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.2 → v0.1.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.2...v0.1.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91b04ce..58352e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.2 + rev: v0.1.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 7a74c335c88d525dbbebf2498e48873cb9471994 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 20:46:12 +0000 Subject: [PATCH 05/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.3 → v0.1.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.3...v0.1.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 58352e7..cc5b93d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.3 + rev: v0.1.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From e510540c74ebe45ec811df78b8655a0315a72304 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:18:23 +0000 Subject: [PATCH 06/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.4 → v0.1.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.4...v0.1.5) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc5b93d..ed54b41 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.4 + rev: v0.1.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From dd27b64cf54aeb799e0bd198f36f3c4f317234cb Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 10 Nov 2023 14:42:10 -0800 Subject: [PATCH 07/83] Remove type ignores from complex tuple code New version of Mypy --- tcod/ecs/query.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index fa820f4..b405b06 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -108,16 +108,15 @@ def _fetch_relation_table(world: World, relation: _RelationQuery) -> Set[Entity] For advanced cases `WorldQuery` this returns the subset of entities following the query condition. """ if len(relation) == 2: # noqa: PLR2004 - tag, target = relation # type: ignore[misc] # https://github.com/python/mypy/issues/1178 + tag, target = relation if not isinstance(target, WorldQuery): return world._relations_lookup.get((tag, target), frozenset()) world = target.world return set().union(*(world._relations_lookup.get((tag, entity), ()) for entity in target)) - - origin, tag, target = relation # type: ignore[misc] # https://github.com/python/mypy/issues/1178 + origin, tag, target_none = relation if not isinstance(origin, WorldQuery): - return world._relations_lookup.get((origin, tag, target), frozenset()) + return world._relations_lookup.get((origin, tag, target_none), frozenset()) world = origin.world return set().union(*(world._relations_lookup.get((entity, tag, None), ()) for entity in origin)) @@ -151,11 +150,11 @@ def _normalize_query_relation(relation: _RelationQuery) -> _RelationQuery: This adds the inverse lookup to the sub-query so that this only matches entities which have a relation. """ if len(relation) == 2: # noqa: PLR2004 - tag, targets = relation # type: ignore[misc] # https://github.com/python/mypy/issues/1178 + tag, targets = relation if isinstance(targets, WorldQuery): # (tag, targets) return tag, targets.all_of(relations=[(..., tag, None)]) return relation - origin, tag, _ = relation # type: ignore[misc] # https://github.com/python/mypy/issues/1178 + origin, tag, _ = relation if isinstance(origin, WorldQuery): # (origins, tag, None) return origin.all_of(relations=[(tag, ...)]), tag, None return relation From bc82ec3fc87ad4cc84def6e910b0a90ad8cc499e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 20:39:24 +0000 Subject: [PATCH 08/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.5 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.5...v0.1.6) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed54b41..650b516 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.5 + rev: v0.1.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From b50d7401c387f4f39c06c350f0483e4a7e9ed1a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 21:12:06 +0000 Subject: [PATCH 09/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.1.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.6...v0.1.7) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 650b516..74f2dc7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.1.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 0af6f2beddba728ad20fe1a9672af1fee86a501e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 21:12:17 +0000 Subject: [PATCH 10/83] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tcod/ecs/callbacks.py | 3 ++- tcod/ecs/world.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tcod/ecs/callbacks.py b/tcod/ecs/callbacks.py index e051216..d0bbfe1 100644 --- a/tcod/ecs/callbacks.py +++ b/tcod/ecs/callbacks.py @@ -19,7 +19,8 @@ def register_component_changed( - *, component: ComponentKey[Any] + *, + component: ComponentKey[Any], ) -> Callable[[_OnComponentChangedFuncT], _OnComponentChangedFuncT]: """Return a decorator to register on-component-changed callback functions. diff --git a/tcod/ecs/world.py b/tcod/ecs/world.py index 4da3e43..9d5134e 100644 --- a/tcod/ecs/world.py +++ b/tcod/ecs/world.py @@ -28,7 +28,7 @@ def _defaultdict_of_dict() -> defaultdict[_T1, dict[_T2, _T3]]: def _components_by_entity_from( - by_type: defaultdict[ComponentKey[object], dict[Entity, Any]] + by_type: defaultdict[ComponentKey[object], dict[Entity, Any]], ) -> defaultdict[Entity, dict[ComponentKey[object], Any]]: """Return the component lookup table from the components sparse-set.""" by_entity: defaultdict[Entity, dict[ComponentKey[object], Any]] = defaultdict(dict) From 7f19c75cebee449611fd9113dd523971abfc9f1d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 21:20:18 +0000 Subject: [PATCH 11/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.7 → v0.1.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.7...v0.1.8) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74f2dc7..894c658 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.7 + rev: v0.1.8 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 9023ba18727b134891029389000736ac87319d9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 21:01:32 +0000 Subject: [PATCH 12/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.8 → v0.1.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.8...v0.1.9) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 894c658..e4a0285 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.8 + rev: v0.1.9 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From f7cf834b718c94d2f45d73b6bbf687df185e18b3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 21:00:35 +0000 Subject: [PATCH 13/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.9 → v0.1.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.9...v0.1.11) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4a0285..8a011f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.1.11 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From c15c66a48398f2ac540c6cd88ef203a11b28de9d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 20:27:14 +0000 Subject: [PATCH 14/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.11 → v0.1.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.11...v0.1.13) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a011f5..39b717c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.11 + rev: v0.1.13 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 32735e2f0b8f4e269fb9205b52fe5190bd66529f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 20:34:59 +0000 Subject: [PATCH 15/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.13 → v0.1.14](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.13...v0.1.14) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39b717c..351522c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.13 + rev: v0.1.14 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From c680ed3417afa26ddb5e04f325643ff2cf751c02 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:46:02 +0000 Subject: [PATCH 16/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.14 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.14...v0.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 351522c..8229531 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.14 + rev: v0.2.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 2d66e9ab65ac7c18c31eeadc5569ff9f75106219 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 00:26:19 +0000 Subject: [PATCH 17/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8229531..0a5d1a5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 + rev: v0.2.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 24dc860e599539d9574bf472814c97d8390f5c13 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 12 Feb 2024 20:15:33 -0800 Subject: [PATCH 18/83] Remove default language version Can be too much of an issue if the specific version isn't installed. Any modern installations should be relatively up-to-date. --- .pre-commit-config.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a5d1a5..af18a76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,5 +20,3 @@ repos: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format -default_language_version: - python: python3.11 From 89b476c2243a125d340b298dbe45c11abe7166c5 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 12 Feb 2024 20:16:52 -0800 Subject: [PATCH 19/83] Replace deprecated VSCode config option --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f0e546a..c46f319 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -55,7 +55,7 @@ "WINDOWLEAVE" ], "editor.codeActionsOnSave": { - "source.fixAll": true, + "source.fixAll": "always" }, "editor.rulers": [ 120 From e01d20b1475959cbc3b2f73de6fd83d2a49255c6 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 12 Feb 2024 20:21:19 -0800 Subject: [PATCH 20/83] Change type union to type overloads Previous hint was triggering the 'join vs union' issue and was failing valid code. --- CHANGELOG.md | 2 ++ tcod/ecs/entity.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6615706..86a0161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- Updated `EntityComponents.__ior__` type hints which were causing false positives. ## [5.0.0] - 2023-10-20 ### Added diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index dbf2f43..b4e1bd3 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -17,6 +17,7 @@ Type, TypeVar, Union, + overload, ) from weakref import WeakKeyDictionary, WeakValueDictionary @@ -499,6 +500,14 @@ def by_name_type(self, name_type: type[_T1], component_type: type[_T2]) -> Itera if key_component is component_type and isinstance(key_name, name_type): yield key_name, key_component + @overload + def __ior__(self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any]) -> Self: + ... + + @overload + def __ior__(self, value: Iterable[tuple[ComponentKey[Any], Any]]) -> Self: + ... + def __ior__( self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any] | Iterable[tuple[ComponentKey[Any], Any]] ) -> Self: From ac86c443fa4f9d5bf199b6d2bbf059981444199b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 12 Feb 2024 20:24:21 -0800 Subject: [PATCH 21/83] Reformat markdown files --- CHANGELOG.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 17 +++++++++-------- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a0161..537d793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,134 +6,185 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + ### Fixed + - Updated `EntityComponents.__ior__` type hints which were causing false positives. ## [5.0.0] - 2023-10-20 + ### Added + - Added the `tcod.ecs.IsA` sentinel value. - Entities will automatically inherit components/tags/relations from entities they have an `IsA` relationship with. https://github.com/HexDecimal/python-tcod-ecs/pull/15 - Entities can be used as prefabs, use `Entity.instantiate()` to make a new entities inheriting the base entities components/tags/relations. ### Removed + - `tcod.ecs.query.Query` removed due to a refactor. - `abstract_component` decorator removed. ### Fixed + - Fix for `x in Entity.relation_tags_many` not checking the correct values. ## [4.4.0] - 2023-08-11 + ### Added + - Added `WorldQuery.get_entities` for returning query results as a set. ### Fixed + - Removed an optimization which would check the equality of component values, since this would fail when comparing some types such as NumPy arrays. - Removed unintentional iteration behavior from `World`. https://github.com/HexDecimal/python-tcod-ecs/issues/8 ## [4.3.1] - 2023-08-02 + ### Fixed + - Relation component lookup tables were replacing previous entries instead of adding to them. - Relation ellipsis lookup tables were discarding entities which still had a relevant relation. ## [4.3.0] - 2023-08-01 + ### Added + - `tcod.ecs.typing.ComponentKey` is now stable. - Can now register a callback to be called on component changes. ### Fixed + - Fixed stale caches for relation components. ## [4.2.1] - 2023-07-28 + ### Fixed + - Unpickled worlds had reversed relations from what were saved. ## [4.2.0] - 2023-07-28 + ### Added + - `Entity.relation_components` now has `MutableMapping` functionality. - You can now set the value of `Entity.relation_components[component_key] = {target: component}`. - Added the `Entity.clear` method which effectively deletes an entity by removing its components/tags/relations. This does not delete relations targeting the cleared entity. ## [4.1.0] - 2023-07-28 + ### Added + - Now supports giving a query to another relation query, allowing conditional and chained relation queries. https://github.com/HexDecimal/python-tcod-ecs/issues/1 ## [4.0.0] - 2023-07-26 + ### Changed + - The type returned by `World.Q` has been renamed from `tcod.ecs.Query` to `tcod.ecs.query.WorldQuery`. - Serialization format updated. ### Performance + - Added a simple query cache. A query is automatically cached until any of the components/tags/relations it depends on have changed. ## [3.5.0] - 2023-07-23 + ### Changed + - Serialization format updated, older versions will not be able to unpickle this version. - Reduced the size of the pickled World. ### Fixed + - Missing components in `Entity.components` now returns the missing key in the KeyError exception instead of the entity. - Backwards relations for querying were not cleared on relation deletions. ## [3.4.0] - 2023-07-12 + ### Added + - `Entity.components` now supports the `|=` assignment operator. ## [3.3.0] - 2023-07-12 + ### Added + - `Entity.tags` now supports the `|=` and `-=` assignment operators. ## [3.2.0] - 2023-07-02 + ### Changed + - Warn if a string is passed directly as a tags parameter, which might cause unexpected behavior. - `Entity.relation_tags` has been renamed to `Entity.relation_tag`. ### Deprecated + - Deprecated the renamed attribute `Entity.relation_tag`. ## [3.1.0] - 2023-06-10 + ### Changed + - `World.new_entity` can now take a `Mapping` as the `components` parameter. ### Deprecated + - Implicit keys for components have been deprecated in all places. - The names feature has been deprecated. - `Entity.components.by_name_type` has been deprecated. ## [3.0.1] - 2023-06-04 + ### Deprecated + - `World.global_` has been deprecated since `world[None]` is simpler and less redundant. ## [3.0.0] - 2023-05-29 + ### Added + - `Entity.components.by_name_type(name_type, component_type)` to iterate over named components with names of a specific type. ### Changed + - Remap `World.global_` to `uid=None`. ## [2.0.0] - 2023-05-26 + ### Added + - You can now use custom identifiers for entity objects. You can access these from World instances with `entity = world[uid]`. ### Removed + - Dropped support for unpickling v1.0 World objects. ## [1.2.0] - 2023-04-26 + ### Added + - Allow `Entity` instances to be referenced weakly. ### Fixed + - Added missing typing marker. - Corrected the type-hinting of `Entity.component.get` and `Entity.component.setdefault`. ## [1.1.0] - 2023-04-21 + ### Added + - World's now have a globally accessible entity accessed with `World.global_`. ### Changed + - You can now quickly lookup the relation tags and relation component keys of an entity. ## [1.0.0] - 2023-04-11 + First stable release. diff --git a/README.md b/README.md index 3d5e6d2..4b83f9d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ A lightweight version which implements only the entity-component framework exist # Installation Use pip to install this library: + ``` pip install tcod-ecs ``` @@ -246,11 +247,11 @@ True You can use the following table to help with constructing relation queries. `tag` is a component key if you are querying for a component relation. -| Includes | Syntax | -| --------------------------------------------------------------------- | :----: | -| Entities with a relation tag to the given target | `(tag, target_entity)` | -| Entities with a relation tag to any target | `(tag, ...)` (Literal dot-dot-dot) | -| Entities with a relation tag to the targets in the given query | `(tag, world.Q.all_of(...))` | -| The target entities of a relation of a given entity | `(origin_entity, tag, None)` | -| The target entities of any entity with the given relation tag | `(..., tag, None)` (Literal dot-dot-dot) | -| The target entities of the queried entities with the given relation | `(tag, world.Q.all_of(...))` | +| Includes | Syntax | +| ------------------------------------------------------------------- | :--------------------------------------: | +| Entities with a relation tag to the given target | `(tag, target_entity)` | +| Entities with a relation tag to any target | `(tag, ...)` (Literal dot-dot-dot) | +| Entities with a relation tag to the targets in the given query | `(tag, world.Q.all_of(...))` | +| The target entities of a relation of a given entity | `(origin_entity, tag, None)` | +| The target entities of any entity with the given relation tag | `(..., tag, None)` (Literal dot-dot-dot) | +| The target entities of the queried entities with the given relation | `(tag, world.Q.all_of(...))` | From 4a110be26d98eea60a0cba4ab22c098fdda75a22 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 12 Feb 2024 21:32:38 -0800 Subject: [PATCH 22/83] Update VSCode linting config, prefer local Mypy over bundled version --- .vscode/settings.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c46f319..e55f8f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,12 +1,4 @@ { - "python.linting.enabled": true, - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": false, - "python.linting.mypyEnabled": true, - "python.linting.mypyArgs": [ - "--follow-imports=silent", - "--show-column-numbers" - ], "editor.formatOnSave": true, "files.trimFinalNewlines": true, "files.insertFinalNewline": true, @@ -65,4 +57,5 @@ "[python]": { "editor.defaultFormatter": "charliermarsh.ruff" }, + "mypy-type-checker.importStrategy": "fromEnvironment" } From 8d43f6940e59ede68bd87b5ff1f4aa5f5e80b445 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 12 Feb 2024 21:52:56 -0800 Subject: [PATCH 23/83] Rename World to Registry --- CHANGELOG.md | 8 +++++ conftest.py | 2 +- tcod/ecs/__init__.py | 4 ++- tcod/ecs/entity.py | 14 ++++---- tcod/ecs/query.py | 48 +++++++++++++------------- tcod/ecs/{world.py => registry.py} | 4 +-- tests/test_benchmarks.py | 12 +++---- tests/test_ecs.py | 54 ++++++++++++++++-------------- tests/test_relations.py | 14 ++++---- tests/test_traversal.py | 14 ++++---- 10 files changed, 93 insertions(+), 81 deletions(-) rename tcod/ecs/{world.py => registry.py} (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 537d793..60d060b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Renamed `World` to the more standard name `Registry`. + +### Deprecated + +- `World` is deprecated and has been renamed to `Registry`. + ### Fixed - Updated `EntityComponents.__ior__` type hints which were causing false positives. diff --git a/conftest.py b/conftest.py index fe2d585..9d2f2d5 100644 --- a/conftest.py +++ b/conftest.py @@ -9,7 +9,7 @@ @pytest.fixture(autouse=True) def _add_world_entity(doctest_namespace: Dict[str, Any]) -> None: """Add world and entity objects to all doctests.""" - world = tcod.ecs.World() + world = tcod.ecs.Registry() entity = world["entity"] other_entity = world["other"] doctest_namespace.update( diff --git a/tcod/ecs/__init__.py b/tcod/ecs/__init__.py index 946df61..7b0371d 100644 --- a/tcod/ecs/__init__.py +++ b/tcod/ecs/__init__.py @@ -7,12 +7,14 @@ from tcod.ecs.constants import IsA from tcod.ecs.entity import Entity -from tcod.ecs.world import World +from tcod.ecs.registry import Registry +from tcod.ecs.registry import Registry as World __all__ = ( "__version__", "Entity", "IsA", + "Registry", "World", ) diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index b4e1bd3..2422aea 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -32,14 +32,14 @@ if TYPE_CHECKING: from _typeshed import SupportsKeysAndGetItem - from tcod.ecs.world import World + from tcod.ecs.registry import Registry T = TypeVar("T") _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") -_entity_table: WeakKeyDictionary[World, WeakValueDictionary[object, Entity]] = WeakKeyDictionary() +_entity_table: WeakKeyDictionary[Registry, WeakValueDictionary[object, Entity]] = WeakKeyDictionary() """A weak table of worlds and unique identifiers to entity objects. This table is used to that non-unique Entity's won't create a new object and thus will always share identities. @@ -63,12 +63,12 @@ class Entity: __slots__ = ("world", "uid", "__weakref__") - world: Final[World] # type:ignore[misc] # https://github.com/python/mypy/issues/5774 + world: Final[Registry] # type:ignore[misc] # https://github.com/python/mypy/issues/5774 """The :any:`World` this entity belongs to.""" uid: Final[object] # type:ignore[misc] """This entities unique identifier.""" - def __new__(cls, world: World, uid: object = object) -> Entity: + def __new__(cls, world: Registry, uid: object = object) -> Entity: """Return a unique entity for the given `world` and `uid`. If an entity already exists with a matching `world` and `uid` then that entity is returned. @@ -329,7 +329,7 @@ def __repr__(self) -> str: items = [self.__class__.__name__, f"name={name!r}"] return f"<{' '.join(items)}>" - def __reduce__(self) -> tuple[type[Entity], tuple[World, object]]: + def __reduce__(self) -> tuple[type[Entity], tuple[Registry, object]]: """Pickle this Entity. Note that any pickled entity will include the world it belongs to and all the entities of that world. @@ -620,7 +620,7 @@ def __isub__(self, other: Set[Any]) -> Self: return self -def _relations_lookup_add(world: World, origin: Entity, tag: object, target: Entity) -> None: +def _relations_lookup_add(world: Registry, origin: Entity, tag: object, target: Entity) -> None: """Add a relation tag/component to the lookup table and handle side effects.""" world._relations_lookup[(tag, target)].add(origin) world._relations_lookup[(tag, ...)].add(origin) @@ -629,7 +629,7 @@ def _relations_lookup_add(world: World, origin: Entity, tag: object, target: Ent tcod.ecs.query._touch_relations(world, ((tag, target), (tag, ...), (origin, tag, None), (..., tag, None))) -def _relations_lookup_discard(world: World, origin: Entity, tag: object, target: Entity) -> None: +def _relations_lookup_discard(world: Registry, origin: Entity, tag: object, target: Entity) -> None: """Discard a relation tag/component from the lookup table and handle side effects.""" world._relations_lookup[(tag, target)].discard(origin) if not world._relations_lookup[(tag, target)]: diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index b405b06..6039ce9 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from tcod.ecs.entity import Entity - from tcod.ecs.world import World + from tcod.ecs.registry import Registry _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") @@ -26,7 +26,7 @@ _T5 = TypeVar("_T5") -_query_caches: WeakKeyDictionary[World, _QueryCache] = WeakKeyDictionary() +_query_caches: WeakKeyDictionary[Registry, _QueryCache] = WeakKeyDictionary() """The master table of cached queries.""" @@ -45,7 +45,7 @@ class _QueryCache: by_relations: defaultdict[_RelationQuery, WeakSet[_Query]] = attrs.field(factory=lambda: defaultdict(WeakSet)) """Which queries depend on which relations.""" - dependencies: dict[_Query, set[tuple[World, _Query]]] = attrs.field(factory=lambda: defaultdict(set)) + dependencies: dict[_Query, set[tuple[Registry, _Query]]] = attrs.field(factory=lambda: defaultdict(set)) """Tracks which queries depend on the queries of the current world. `dependencies[dependency] = {dependant}` @@ -59,7 +59,7 @@ def _drop_cached_query(cache: _QueryCache, query: _Query) -> None: _drop_cached_query(_query_caches[sub_world], sub_query) -def _touch_component(world: World, component: ComponentKey[object]) -> None: +def _touch_component(world: Registry, component: ComponentKey[object]) -> None: """Drop cached queries if a component change has invalidated them.""" cache = _get_query_cache(world) if component not in cache.by_components: @@ -68,7 +68,7 @@ def _touch_component(world: World, component: ComponentKey[object]) -> None: _drop_cached_query(cache, touched_query) -def _touch_tag(world: World, tag: object) -> None: +def _touch_tag(world: Registry, tag: object) -> None: """Drop cached queries if a tag change has invalidated them.""" cache = _get_query_cache(world) if tag not in cache.by_tags: @@ -77,7 +77,7 @@ def _touch_tag(world: World, tag: object) -> None: _drop_cached_query(cache, touched_query) -def _touch_relations(world: World, relations: Iterable[_RelationQuery]) -> None: +def _touch_relations(world: Registry, relations: Iterable[_RelationQuery]) -> None: """Drop cached queries if a relation change has invalidated them.""" cache = _get_query_cache(world) for relation in relations: @@ -100,7 +100,7 @@ def _check_suspicious_tags(tags: Iterable[object], stacklevel: int = 2) -> None: ) -def _fetch_relation_table(world: World, relation: _RelationQuery) -> Set[Entity]: +def _fetch_relation_table(world: Registry, relation: _RelationQuery) -> Set[Entity]: """Get the entity table for this relation. For simple cases where target/origin is `Entity | ...` this returns the set directly from the lookup table. @@ -122,7 +122,7 @@ def _fetch_relation_table(world: World, relation: _RelationQuery) -> Set[Entity] return set().union(*(world._relations_lookup.get((entity, tag, None), ()) for entity in origin)) -def _get_query_cache(world: World) -> _QueryCache: +def _get_query_cache(world: Registry) -> _QueryCache: """Return the global cache for the given world, creating it if it does not exist.""" cache = _query_caches.get(world) if cache is None: @@ -130,7 +130,7 @@ def _get_query_cache(world: World) -> _QueryCache: return cache -def _get_query(world: World, query: _Query) -> Set[Entity]: +def _get_query(world: Registry, query: _Query) -> Set[Entity]: """Return the entities for the given query and world.""" cache = _get_query_cache(world) if cache is not None: @@ -163,11 +163,11 @@ def _normalize_query_relation(relation: _RelationQuery) -> _RelationQuery: class _Query(Protocol): """Abstract query class.""" - def _add_to_cache(self, world: World, cache: _QueryCache) -> None: + def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: """Add this query to the local cache.""" ... - def _compile(self, world: World, cache: _QueryCache) -> Set[Entity]: + def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: """Compile the entities of this query, returning a set which must not be modified.""" ... @@ -178,10 +178,10 @@ class _QueryComponent: _component: ComponentKey[object] - def _add_to_cache(self, world: World, cache: _QueryCache) -> None: + def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: cache.by_components[self._component].add(self) - def _compile(self, world: World, cache: _QueryCache) -> Set[Entity]: + def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: return world._components_by_type.get(self._component, {}).keys() @@ -191,10 +191,10 @@ class _QueryTag: _tag: object - def _add_to_cache(self, world: World, cache: _QueryCache) -> None: + def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: cache.by_tags[self._tag].add(self) - def _compile(self, world: World, cache: _QueryCache) -> Set[Entity]: + def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: return world._tags_by_key.get(self._tag, set()) @@ -204,7 +204,7 @@ class _QueryRelation: _relation: _RelationQuery = attrs.field(converter=_normalize_query_relation) - def _add_to_cache(self, world: World, cache: _QueryCache) -> None: + def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: """Add this query to the cache and mark it dependant on a world query if the relation uses one.""" def _get_world_query() -> WorldQuery | None: @@ -220,7 +220,7 @@ def _get_world_query() -> WorldQuery | None: if w_query is not None: _get_query_cache(w_query.world).dependencies[w_query._query].add((world, self)) - def _compile(self, world: World, cache: _QueryCache) -> Set[Entity]: + def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: return _fetch_relation_table(world, self._relation) @@ -238,11 +238,11 @@ def __attrs_post_init__(self) -> None: """Verify the current state.""" assert self._all_of.isdisjoint(self._none_of) - def _add_to_cache(self, world: World, cache: _QueryCache) -> None: + def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: for dependency in itertools.chain(self._all_of, self._none_of): cache.dependencies[dependency].add((world, self)) - def _compile(self, world: World, cache: _QueryCache) -> Set[Entity]: + def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: if len(self._all_of) == 1 and not self._none_of: # Only one sub-query, simply return the results of it return _get_query(world, next(iter(self._all_of))) # Avoids an extra copy of a set requires = sorted( # Place the smallest sets first to speed up intersections @@ -267,11 +267,11 @@ class _QueryLogicalOr: _any_of: frozenset[_Query] = frozenset() - def _add_to_cache(self, world: World, cache: _QueryCache) -> None: + def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: for dependency in self._any_of: cache.dependencies[dependency].add((world, self)) - def _compile(self, world: World, cache: _QueryCache) -> Set[Entity]: + def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: if len(self._any_of) == 1: # If there is only one sub-query then simply return the results of it return _get_query(world, next(iter(self._any_of))) # Avoids an extra copy of a set entities: set[Entity] = set() @@ -296,11 +296,11 @@ def _get_traverse_query(self) -> _QueryLogicalOr: any_of=frozenset(_QueryRelation((..., traverse_key, None)) for traverse_key in self._traverse_keys) ) - def _add_to_cache(self, world: World, cache: _QueryCache) -> None: + def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: cache.dependencies[self._sub_query].add((world, self)) cache.dependencies[self._get_traverse_query()].add((world, self)) - def _compile(self, world: World, cache: _QueryCache) -> Set[Entity]: + def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: cumulative_set = set(_get_query(world, self._sub_query)) # All entities touched by this traversal relations_set = _get_query(world, self._get_traverse_query()) # The subset of entities which can propagate from unchecked_set = cumulative_set & relations_set # Most recently touched entities which can propagate farther @@ -322,7 +322,7 @@ def _compile(self, world: World, cache: _QueryCache) -> Set[Entity]: class WorldQuery: """Collect a set of entities with the provided conditions.""" - world: World + world: Registry _query: _Query = attrs.field(factory=_QueryLogicalAnd) def get_entities(self) -> Set[Entity]: diff --git a/tcod/ecs/world.py b/tcod/ecs/registry.py similarity index 99% rename from tcod/ecs/world.py rename to tcod/ecs/registry.py index 9d5134e..71bfc5a 100644 --- a/tcod/ecs/world.py +++ b/tcod/ecs/registry.py @@ -74,7 +74,7 @@ def _relations_lookup_from( @attrs.define(eq=False) -class World: +class Registry: """A container for entities and components.""" _components_by_entity: defaultdict[Entity, dict[ComponentKey[object], Any]] = attrs.field( @@ -242,7 +242,7 @@ def __getitem__(self, uid: object) -> Entity: Example:: - >>> world = World() + >>> world = Registry() >>> foo = world["foo"] # Referencing a new entity returns a new empty entity >>> foo is world["foo"] True diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py index 0b2ee07..5b944e9 100644 --- a/tests/test_benchmarks.py +++ b/tests/test_benchmarks.py @@ -9,12 +9,12 @@ def test_component_missing(benchmark: Any) -> None: - entity = tcod.ecs.World().new_entity() + entity = tcod.ecs.Registry().new_entity() benchmark(lambda: entity.components.get(str)) def test_component_assign(benchmark: Any) -> None: - entity = tcod.ecs.World().new_entity() + entity = tcod.ecs.Registry().new_entity() @benchmark # type: ignore[misc] def _() -> None: @@ -22,21 +22,21 @@ def _() -> None: def test_component_found(benchmark: Any) -> None: - entity = tcod.ecs.World().new_entity() + entity = tcod.ecs.Registry().new_entity() entity.components[str] = "value" benchmark(lambda: entity.components[str]) def test_tag_missing(benchmark: Any) -> None: - entity = tcod.ecs.World().new_entity() + entity = tcod.ecs.Registry().new_entity() benchmark(lambda: "value" in entity.tags) def test_tag_assign(benchmark: Any) -> None: - entity = tcod.ecs.World().new_entity() + entity = tcod.ecs.Registry().new_entity() benchmark(lambda: entity.tags.add("value")) def test_tag_found(benchmark: Any) -> None: - entity = tcod.ecs.World().new_entity() + entity = tcod.ecs.Registry().new_entity() benchmark(lambda: "value" in entity.tags) diff --git a/tests/test_ecs.py b/tests/test_ecs.py index 6dbe7bf..147031a 100644 --- a/tests/test_ecs.py +++ b/tests/test_ecs.py @@ -15,7 +15,7 @@ def test_world() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() with pytest.warns(): entity = world.new_entity([1, "test"]) assert entity.components[int] == 1 @@ -33,7 +33,7 @@ def test_world() -> None: def test_component_names() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() entity = world.new_entity() entity.components[("name", str)] = "name" entity.components[("foo", str)] = "foo" @@ -44,7 +44,7 @@ def test_component_names() -> None: def test_naming() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() with pytest.warns(): entity_a = world.new_entity(name="A") assert entity_a.name == "A" @@ -61,9 +61,9 @@ def test_naming() -> None: assert repr(world.new_entity(name="foo")) == "" -def sample_world_v1() -> tcod.ecs.World: +def sample_world_v1() -> tcod.ecs.Registry: """Return a sample world.""" - world = tcod.ecs.World() + world = tcod.ecs.Registry() with pytest.warns(): entity = world.new_entity(name="A") entity.components[str] = "str" @@ -76,7 +76,7 @@ def sample_world_v1() -> tcod.ecs.World: return world -def check_world_v1(world: tcod.ecs.World) -> None: +def check_world_v1(world: tcod.ecs.Registry) -> None: """Assert a sample world is as expected.""" entity = world.named["A"] assert entity.components[str] == "str" @@ -92,9 +92,9 @@ def check_world_v1(world: tcod.ecs.World) -> None: assert not world[None].components -def sample_world_v2() -> tcod.ecs.World: +def sample_world_v2() -> tcod.ecs.Registry: """Return a sample world.""" - world = tcod.ecs.World() + world = tcod.ecs.Registry() with pytest.warns(): world["A"].name = "A" assert world.named["A"] is world["A"] @@ -107,7 +107,7 @@ def sample_world_v2() -> tcod.ecs.World: return world -def check_world_v2(world: tcod.ecs.World) -> None: +def check_world_v2(world: tcod.ecs.Registry) -> None: """Assert a sample world is as expected.""" original = sample_world_v2() assert not world["X"].components @@ -121,13 +121,15 @@ def check_world_v2(world: tcod.ecs.World) -> None: PICKLED_SAMPLES = { "v2": { - "latest": b'\x80\x04\x95R\x01\x00\x00\x00\x00\x00\x00\x8c\x0etcod.ecs.world\x94\x8c\x05World\x94\x93\x94)\x81\x94}\x94(\x8c\x13_components_by_type\x94}\x94(\x8c\x08builtins\x94\x8c\x03str\x94\x93\x94}\x94\x8c\x0ftcod.ecs.entity\x94\x8c\x06Entity\x94\x93\x94h\x03\x8c\x01A\x94\x86\x94R\x94\x8c\x03str\x94s\x8c\x03foo\x94h\t\x86\x94}\x94h\x10h\x12sh\x07\x8c\x04bool\x94\x93\x94}\x94h\rh\x03N\x86\x94R\x94\x88su\x8c\x0f_tags_by_entity\x94}\x94h\x10\x8f\x94(\x8c\x03tag\x94\x90s\x8c\x18_relation_tags_by_entity\x94}\x94h\rh\x03\x8c\x01B\x94\x86\x94R\x94}\x94\x8c\x07ChildOf\x94\x8f\x94(h\x10\x90ss\x8c\x1e_relation_components_by_entity\x94}\x94h"}\x94h\t}\x94h\x10h\x11sss\x8c\x0e_names_by_name\x94}\x94h\x0eh\x10sub.', # cspell: disable-line + "latest": b'\x80\x04\x95X\x01\x00\x00\x00\x00\x00\x00\x8c\x11tcod.ecs.registry\x94\x8c\x08Registry\x94\x93\x94)\x81\x94}\x94(\x8c\x13_components_by_type\x94}\x94(\x8c\x08builtins\x94\x8c\x03str\x94\x93\x94}\x94\x8c\x0ftcod.ecs.entity\x94\x8c\x06Entity\x94\x93\x94h\x03\x8c\x01A\x94\x86\x94R\x94\x8c\x03str\x94s\x8c\x03foo\x94h\t\x86\x94}\x94h\x10h\x12sh\x07\x8c\x04bool\x94\x93\x94}\x94h\rh\x03N\x86\x94R\x94\x88su\x8c\x0f_tags_by_entity\x94}\x94h\x10\x8f\x94(\x8c\x03tag\x94\x90s\x8c\x18_relation_tags_by_entity\x94}\x94h\rh\x03\x8c\x01B\x94\x86\x94R\x94}\x94\x8c\x07ChildOf\x94\x8f\x94(h\x10\x90ss\x8c\x1e_relation_components_by_entity\x94}\x94h"}\x94h\t}\x94h\x10h\x11sss\x8c\x0e_names_by_name\x94}\x94h\x0eh\x10sub.', # cspell: disable-line + "5.0.0": b'\x80\x04\x95R\x01\x00\x00\x00\x00\x00\x00\x8c\x0etcod.ecs.world\x94\x8c\x05World\x94\x93\x94)\x81\x94}\x94(\x8c\x13_components_by_type\x94}\x94(\x8c\x08builtins\x94\x8c\x03str\x94\x93\x94}\x94\x8c\x0ftcod.ecs.entity\x94\x8c\x06Entity\x94\x93\x94h\x03\x8c\x01A\x94\x86\x94R\x94\x8c\x03str\x94s\x8c\x03foo\x94h\t\x86\x94}\x94h\x10h\x12sh\x07\x8c\x04bool\x94\x93\x94}\x94h\rh\x03N\x86\x94R\x94\x88su\x8c\x0f_tags_by_entity\x94}\x94h\x10\x8f\x94(\x8c\x03tag\x94\x90s\x8c\x18_relation_tags_by_entity\x94}\x94h\rh\x03\x8c\x01B\x94\x86\x94R\x94}\x94\x8c\x07ChildOf\x94\x8f\x94(h\x10\x90ss\x8c\x1e_relation_components_by_entity\x94}\x94h"}\x94h\t}\x94h\x10h\x11sss\x8c\x0e_names_by_name\x94}\x94h\x0eh\x10sub.', # cspell: disable-line "3.5.0": b"\x80\x04\x95<\x01\x00\x00\x00\x00\x00\x00\x8c\x08tcod.ecs\x94\x8c\x05World\x94\x93\x94)\x81\x94}\x94(\x8c\x13_components_by_type\x94}\x94(\x8c\x08builtins\x94\x8c\x03str\x94\x93\x94}\x94h\x00\x8c\x06Entity\x94\x93\x94h\x03\x8c\x01A\x94\x86\x94R\x94\x8c\x03str\x94s\x8c\x03foo\x94h\t\x86\x94}\x94h\x0fh\x11sh\x07\x8c\x04bool\x94\x93\x94}\x94h\x0ch\x03N\x86\x94R\x94\x88su\x8c\x0f_tags_by_entity\x94}\x94h\x0f\x8f\x94(\x8c\x03tag\x94\x90s\x8c\x18_relation_tags_by_entity\x94}\x94h\x0ch\x03\x8c\x01B\x94\x86\x94R\x94}\x94\x8c\x07ChildOf\x94\x8f\x94(h\x0f\x90ss\x8c\x1e_relation_components_by_entity\x94}\x94h!}\x94h\t}\x94h\x0fh\x10sss\x8c\x0e_names_by_name\x94}\x94h\rh\x0fsub.", # cspell: disable-line "3.4.0": b"\x80\x04\x95\xbb\x02\x00\x00\x00\x00\x00\x00\x8c\x08tcod.ecs\x94\x8c\x05World\x94\x93\x94)\x81\x94}\x94(\x8c\x13_components_by_type\x94\x8c\x0bcollections\x94\x8c\x0bdefaultdict\x94\x93\x94\x8c\x08builtins\x94\x8c\x04dict\x94\x93\x94\x85\x94R\x94(h\t\x8c\x03str\x94\x93\x94}\x94h\x00\x8c\x06Entity\x94\x93\x94h\x03\x8c\x01A\x94\x86\x94R\x94\x8c\x03str\x94s\x8c\x03foo\x94h\x0f\x86\x94}\x94h\x15h\x17sh\t\x8c\x04bool\x94\x93\x94}\x94h\x12h\x03N\x86\x94R\x94\x88su\x8c\x15_components_by_entity\x94h\x08h\t\x8c\x03set\x94\x93\x94\x85\x94R\x94(h\x15\x8f\x94(h\x18h\x0f\x90h\x1e\x8f\x94(h\x1b\x90u\x8c\x0c_tags_by_key\x94h\x08h!\x85\x94R\x94\x8c\x03tag\x94\x8f\x94(h\x15\x90s\x8c\x0f_tags_by_entity\x94h\x08h!\x85\x94R\x94h\x15\x8f\x94(h)\x90s\x8c\x18_relation_tags_by_entity\x94h\x08h\x00\x8c\x13_defaultdict_of_set\x94\x93\x94\x85\x94R\x94h\x12h\x03\x8c\x01B\x94\x86\x94R\x94h\x08h!\x85\x94R\x94\x8c\x07ChildOf\x94\x8f\x94(h\x15\x90ss\x8c\x1e_relation_components_by_entity\x94h\x08h\x00\x8c\x14_defaultdict_of_dict\x94\x93\x94\x85\x94R\x94h6h\x08h\x0b\x85\x94R\x94h\x0f}\x94h\x15h\x16sss\x8c\x11_relations_lookup\x94h\x08h!\x85\x94R\x94(h9h\x15\x86\x94\x8f\x94(h6\x90h9h\t\x8c\x08Ellipsis\x94\x93\x94\x86\x94\x8f\x94(h6\x90h6h9N\x87\x94\x8f\x94(h\x15\x90hIh9N\x87\x94\x8f\x94(h\x15\x90h\x0fh\x15\x86\x94\x8f\x94(h6\x90h\x0fhI\x86\x94\x8f\x94(h6\x90h6h\x0fN\x87\x94\x8f\x94(h\x15\x90hIh\x0fN\x87\x94\x8f\x94(h\x15\x90u\x8c\x0e_names_by_name\x94}\x94h\x13h\x15s\x8c\x10_names_by_entity\x94}\x94h\x15h\x13sub.", # cspell: disable-line "3.0.0": b"\x80\x04\x95\xc6\x02\x00\x00\x00\x00\x00\x00\x8c\x08tcod.ecs\x94\x8c\x05World\x94\x93\x94)\x81\x94}\x94(\x8c\x13_components_by_type\x94\x8c\x0bcollections\x94\x8c\x0bdefaultdict\x94\x93\x94\x8c\x08builtins\x94\x8c\x04dict\x94\x93\x94\x85\x94R\x94(h\t\x8c\x03str\x94\x93\x94}\x94h\x00\x8c\x06Entity\x94\x93\x94h\x03\x8c\x01A\x94\x86\x94R\x94\x8c\x03str\x94s\x8c\x03foo\x94h\x0f\x86\x94}\x94h\x15h\x17sh\t\x8c\x04bool\x94\x93\x94}\x94h\x12h\x03N\x86\x94R\x94\x88su\x8c\x15_components_by_entity\x94h\x08h\t\x8c\x03set\x94\x93\x94\x85\x94R\x94(h\x15\x8f\x94(h\x18h\x0f\x90h\x1e\x8f\x94(h\x1b\x90u\x8c\x0c_tags_by_key\x94h\x08h!\x85\x94R\x94\x8c\x03tag\x94\x8f\x94(h\x15\x90s\x8c\x0f_tags_by_entity\x94h\x08h!\x85\x94R\x94h\x15\x8f\x94(h)\x90s\x8c\x18_relation_tags_by_entity\x94h\x08\x8c\tfunctools\x94\x8c\x07partial\x94\x93\x94h\x08\x85\x94R\x94(h\x08h!\x85\x94}\x94Nt\x94b\x85\x94R\x94h\x12h\x03\x8c\x01B\x94\x86\x94R\x94h\x08h!\x85\x94R\x94\x8c\x07ChildOf\x94\x8f\x94(h\x15\x90ss\x8c\x1e_relation_components_by_entity\x94h\x08h2h\x08\x85\x94R\x94(h\x08h\x0b\x85\x94}\x94Nt\x94b\x85\x94R\x94h str: @pytest.mark.parametrize("sample_version", PICKLED_SAMPLES.keys()) def test_pickle(sample_version: str) -> None: """Test that pickled worlds are stable.""" - sample_world: Callable[[], tcod.ecs.World] = globals()[f"sample_world_{sample_version}"] - check_world: Callable[[tcod.ecs.World], None] = globals()[f"check_world_{sample_version}"] + sample_world: Callable[[], tcod.ecs.Registry] = globals()[f"sample_world_{sample_version}"] + check_world: Callable[[tcod.ecs.Registry], None] = globals()[f"check_world_{sample_version}"] sample_data = PICKLED_SAMPLES[sample_version]["latest"] pickled = pickle.dumps(sample_world(), protocol=4) print(pickled) assert pickle_disassemble(pickled) == pickle_disassemble(sample_data), "Check if data format has changed" - unpickled: tcod.ecs.World = pickle.loads(pickled) # noqa: S301 + unpickled: tcod.ecs.Registry = pickle.loads(pickled) # noqa: S301 check_world(unpickled) @@ -173,15 +175,15 @@ def test_pickle(sample_version: str) -> None: def test_unpickle(sample_version: str, ecs_version: str) -> None: """Test that pickled worlds are stable.""" globals()[f"sample_world_{sample_version}"] - check_world: Callable[[tcod.ecs.World], None] = globals()[f"check_world_{sample_version}"] + check_world: Callable[[tcod.ecs.Registry], None] = globals()[f"check_world_{sample_version}"] sample_data = PICKLED_SAMPLES[sample_version][ecs_version] - unpickled: tcod.ecs.World = pickle.loads(sample_data) # noqa: S301 + unpickled: tcod.ecs.Registry = pickle.loads(sample_data) # noqa: S301 check_world(unpickled) def test_global() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() with pytest.warns(match=r"world\[None\]"): world.global_.components[int] = 1 with pytest.warns(match=r"world\[None\]"): @@ -189,7 +191,7 @@ def test_global() -> None: def test_by_name_type() -> None: - entity = tcod.ecs.World()[None] + entity = tcod.ecs.Registry()[None] with pytest.warns(): assert list(entity.components.by_name_type(int, int)) == [] entity.components[int] = 0 @@ -202,17 +204,17 @@ def test_by_name_type() -> None: def test_suspicious_tags() -> None: with pytest.warns(match=r"The tags parameter was given a str type"): - tcod.ecs.World().Q.all_of(tags="Tags") + tcod.ecs.Registry().Q.all_of(tags="Tags") def test_component_setdefault() -> None: - entity = tcod.ecs.World()[None] + entity = tcod.ecs.Registry()[None] assert entity.components.setdefault(int, 1) == 1 assert entity.components.setdefault(int, 2) == 1 def test_query_exclude_components() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() world["A"].components[int] = 0 world["A"].components[str] = "" world["B"].components[int] = 0 @@ -220,14 +222,14 @@ def test_query_exclude_components() -> None: def test_query_exclude_tags() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() world["A"].tags |= set("AB") world["B"].tags |= set("B") assert set(world.Q.all_of(tags=["B"]).none_of(tags=["A"])) == {world["B"]} def test_query_exclude_relations() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() world["B"].relation_tag["ChildOf"] = world["A"] world["C"].relation_tags_many["ChildOf"] = {world["A"], world["B"]} assert set(world.Q.all_of(relations=[("ChildOf", ...)]).none_of(relations=[("ChildOf", world["B"])])) == { @@ -236,7 +238,7 @@ def test_query_exclude_relations() -> None: def test_tag_query() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() assert not set(world.Q.all_of(tags=["A"])) world["A"].tags.add("A") assert set(world.Q.all_of(tags=["A"])) == {world["A"]} @@ -248,7 +250,7 @@ def test_tag_query() -> None: def test_entity_clear() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() entity = world["entity"] other = world["other"] entity.components[int] = 0 @@ -271,4 +273,4 @@ def test_entity_clear() -> None: def test_world_iter() -> None: with pytest.raises(TypeError, match=r"is not iterable"): - iter(tcod.ecs.World()) # Not iterable for now, maybe later + iter(tcod.ecs.Registry()) # Not iterable for now, maybe later diff --git a/tests/test_relations.py b/tests/test_relations.py index 4f414b3..d6cb2bf 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -13,7 +13,7 @@ def test_relations() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() entity_a = world["A"] entity_b = world["B"] entity_b.relation_tag[ChildOf] = entity_a @@ -37,13 +37,13 @@ def test_relations() -> None: def test_relations_old() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() with pytest.warns(FutureWarning): world[None].relation_tags["Foo"] = world[None] def test_relations_many() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() entity_a = world["A"] entity_b = world["B"] entity_c = world["C"] @@ -60,7 +60,7 @@ def test_relations_many() -> None: def test_relation_components() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() entity_a = world["A"] entity_b = world["B"] @@ -91,7 +91,7 @@ def test_relation_components() -> None: def test_conditional_relations() -> None: - world = tcod.ecs.World() + world = tcod.ecs.Registry() world["A"].relation_tag[ChildOf] = world["B"] world["C"].components[int] = 42 has_int_query = world.Q.all_of(components=[int]) @@ -106,7 +106,7 @@ def test_conditional_relations() -> None: def test_relation_component_tables() -> None: - w = tcod.ecs.World() + w = tcod.ecs.Registry() e1 = w["e1"] e2 = w["e2"] e3 = w["e3"] @@ -138,7 +138,7 @@ def test_relation_component_tables() -> None: def test_relation_tag_tables() -> None: - w = tcod.ecs.World() + w = tcod.ecs.Registry() e1 = w["e1"] e2 = w["e2"] e3 = w["e3"] diff --git a/tests/test_traversal.py b/tests/test_traversal.py index d72319b..edfa9cc 100644 --- a/tests/test_traversal.py +++ b/tests/test_traversal.py @@ -3,13 +3,13 @@ import pytest -from tcod.ecs import IsA, World +from tcod.ecs import IsA, Registry # ruff: noqa: D103 def test_component_traversal() -> None: - world = World() + world = Registry() assert not world.Q.all_of(components=[str]).get_entities() world["derived"].relation_tag[IsA] = world["base"] world["instance"].relation_tag[IsA] = world["derived"] @@ -54,7 +54,7 @@ def test_component_traversal() -> None: def test_component_traversal_alternate() -> None: - world = World() + world = Registry() world["base"].components[str] = "base" world["alt"].components[str] = "alt" world["derived"].relation_tag[IsA] = world["base"] @@ -81,7 +81,7 @@ def test_component_traversal_alternate() -> None: def test_multiple_inheritance() -> None: - world = World() + world = Registry() ViaA: Final = object() ViaC: Final = object() world["A"].components[str] = "A" @@ -110,7 +110,7 @@ def test_multiple_inheritance() -> None: def test_cyclic_inheritance() -> None: - world = World() + world = Registry() world["A"].relation_tag[IsA] = world["D"] world["B"].relation_tag[IsA] = world["A"] world["C"].relation_tag[IsA] = world["B"] @@ -127,7 +127,7 @@ def test_cyclic_inheritance() -> None: def test_tag_traversal() -> None: - world = World() + world = Registry() world["B"].relation_tag[IsA] = world["A"] world["C"].relation_tag[IsA] = world["B"] @@ -165,7 +165,7 @@ def test_tag_traversal() -> None: def test_relation_traversal() -> None: - world = World() + world = Registry() world["B"].relation_tag[IsA] = world["A"] world["C"].relation_tag[IsA] = world["B"] assert set(world["C"].relation_tags_many[IsA]) == {world["A"], world["B"]} From 798ad387a15b4432b6699c82c5a914961b9e1b91 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 12 Feb 2024 21:54:46 -0800 Subject: [PATCH 24/83] Keep backwards compatibility with World Added as a separate commit so that Git can tell that the file was moved. --- tcod/ecs/world.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tcod/ecs/world.py diff --git a/tcod/ecs/world.py b/tcod/ecs/world.py new file mode 100644 index 0000000..f0bac7d --- /dev/null +++ b/tcod/ecs/world.py @@ -0,0 +1,6 @@ +# noqa: D100 +__all__ = ("World",) + +from tcod.ecs.registry import Registry as World + +# This modules existence keeps backwards compatibly with tcod-ecs version <= 5.0.0 From a81967cf35df92eb77ee94d2391646e2831b3d59 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 13 Feb 2024 00:32:36 -0800 Subject: [PATCH 25/83] Rename world to registry across project --- CHANGELOG.md | 6 +- README.md | 86 +++++++------- conftest.py | 12 +- docs/api.rst | 4 +- tcod/ecs/entity.py | 271 ++++++++++++++++++++++--------------------- tcod/ecs/query.py | 169 +++++++++++++++------------ tcod/ecs/registry.py | 36 +++--- tcod/ecs/typing.py | 6 +- tests/test_ecs.py | 4 +- 9 files changed, 314 insertions(+), 280 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60d060b..0ea1871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Renamed `World` to the more standard name `Registry`. +- Renamed `World` to the more standard name `Registry` in multiple places. ### Deprecated -- `World` is deprecated and has been renamed to `Registry`. +- `World` is now `Registry` +- `WorldQuery` is now `BoundQuery` +- `.world` attributes of `Entity` and `BoundQuery` are now `.registry` ### Fixed diff --git a/README.md b/README.md index 4b83f9d..641db2c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The following features are currently implemented: - Entities can have one instance of a type, or multiple instances of a type using a hashable tag to differentiate them. - Entity relationships are supported, either as many-to-many or many-to-one relationships. - ECS Queries can be made to fetch entities having a combination of components/tags/relations or excluding such. -- The ECS World object can be serialized with Python's pickle module for easy storage. +- The ECS Registry object can be serialized with Python's pickle module for easy storage. A lightweight version which implements only the entity-component framework exists called [tcod-ec](https://pypi.org/project/tcod-ec/). `tcod-ec` was geared towards a dynamic-typed-dict style of syntax and is missing a lot of important features such as queries and named components. @@ -34,57 +34,59 @@ Remove or update `tcod` to fix this issue. # Examples -## World +## Registry + +The ECS Registry is used to create and store entities and their components. ```py >>> import tcod.ecs ->>> world = tcod.ecs.World() # New empty world +>>> registry = tcod.ecs.Registry() # New empty registry ``` ## Entity -Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `world` it belongs. -New unique entities can be created with `World.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desireable. -An entity always knows about its assigned world, which can be access with the `Entity.world` property from any Entity instance. -Worlds only know about their entities once the entity is assigned a name, component, tag, or relation. +Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `registry` it belongs. +New unique entities can be created with `Registry.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desireable. +An entity always knows about its assigned registry, which can be accessed with the `Entity.registry` property from any Entity instance. +Registries only know about their entities once the entity is assigned a name, component, tag, or relation. ```py ->>> entity = world.new_entity() # Creates a unique entity using `object()` as the uid +>>> entity = registry.new_entity() # Creates a unique entity using `object()` as the uid >>> entity ->>> entity.world is world # Worlds can always be accessed from their entity +>>> entity.registry is registry # Registry can always be accessed from their entity True ->>> world[entity.uid] is entity # Entities with the same world/uid are compared using `is` +>>> registry[entity.uid] is entity # Entities with the same registry/uid are compared using `is` True # Reference an entity with the given uid, can be any hashable object: ->>> entity = world["MyEntity"] +>>> entity = registry["MyEntity"] >>> entity ->>> world["MyEntity"] is entity # Matching entities ALWAYS share a single identity +>>> registry["MyEntity"] is entity # Matching entities ALWAYS share a single identity True ``` -Use `World.new_entity` to create unique entities and use `World[x]` to reference a global entity or relation with an id. -`World[None]` is recommend for use as a global entity when you want to store components in the world itself. +Use `Registry.new_entity` to create unique entities and use `Registry[x]` to reference a global entity or relation with an id. +`registry[None]` is recommend for use as a global entity when you want to store components in the registry itself. -Do not save the `uid`'s of entities to be used later with `World[uid]`, this process is slower than holding onto the Entity instance. +Do not save the `uid`'s of entities to be used later with `registry[uid]`, this process is slower than holding onto the Entity instance. ## Serialization -Worlds are normal Python objects and can be pickled as long as all stored components are pickleable. +Registries are normal Python objects and can be pickled as long as all stored components are pickleable. ```py >>> import pickle ->>> pickled_data: bytes = pickle.dumps(world) ->>> world = pickle.loads(pickled_data) +>>> pickled_data: bytes = pickle.dumps(registry) +>>> registry = pickle.loads(pickled_data) ``` Stability is a priority but changes may still break older saves. -Backwards compatibility is not a priority, pickled worlds should not be unpickled with an older version of the library. +Backwards compatibility is not a priority, pickled registries should not be unpickled with an older version of the library. This project follows [Semantic Versioning](https://semver.org/), major version increments will break the API, the save format or both, minor version increments may break backwards compatibility. Check the [changelog](https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.md) to be aware of format changes and breaks. There should always be a transition period before a format break, so keeping up with the latest version is a good idea. @@ -98,7 +100,7 @@ The types used can be custom classes or standard Python types. ```py >>> import attrs ->>> entity = world.new_entity() +>>> entity = registry.new_entity() >>> entity.components[int] = 42 >>> entity.components[int] 42 @@ -124,20 +126,20 @@ Vector2(x=1, y=2) >>> entity.components[Vector2] Vector2(x=0, y=0) -# Queries can be made on all entities of a world with matching components ->>> for e in world.Q.all_of(components=[Vector2]): +# Queries can be made on all entities of a registry with matching components +>>> for e in registry.Q.all_of(components=[Vector2]): ... e.components[Vector2].x += 10 >>> entity.components[Vector2] Vector2(x=10, y=0) # You can match components and iterate over them at the same time. This can be combined with the above ->>> for pos, i in world.Q[Vector2, int]: +>>> for pos, i in registry.Q[Vector2, int]: ... print((pos, i)) (Vector2(x=10, y=0), 11) # You can include `Entity` to iterate over entities with their components # This always iterates over the entity itself instead of an Entity component ->>> for e, pos, i in world.Q[tcod.ecs.Entity, Vector2, int]: +>>> for e, pos, i in registry.Q[tcod.ecs.Entity, Vector2, int]: ... print((e, pos, i)) (, Vector2(x=10, y=0), 11) @@ -152,7 +154,7 @@ The syntax `[type]` and `[(name, type)]` can be used interchangeably in all plac Queries on components access named components with the same syntax and must use names explicitly. ```py ->>> entity = world.new_entity() +>>> entity = registry.new_entity() >>> entity.components[Vector2] = Vector2(0, 0) >>> entity.components[("velocity", Vector2)] = Vector2(1, 1) >>> entity.components[("velocity", Vector2)] @@ -175,11 +177,11 @@ Vector2(x=1, y=1) 'empty' # Queries can be made on all named components with the same syntax as normal ones ->>> for e in world.Q.all_of(components=[("hp", int), ("max_hp", int)]): +>>> for e in registry.Q.all_of(components=[("hp", int), ("max_hp", int)]): ... e.components[("hp", int)] = e.components[("max_hp", int)] >>> entity.components[("hp", int)] 12 ->>> for e, pos, delta in world.Q[tcod.ecs.Entity, Vector2, ("velocity", Vector2)]: +>>> for e, pos, delta in registry.Q[tcod.ecs.Entity, Vector2, ("velocity", Vector2)]: ... e.components[Vector2] = Vector2(pos.x + delta.x, pos.y + delta.y) >>> entity.components[Vector2] Vector2(x=1, y=1) @@ -192,13 +194,13 @@ Tags are hashable objects stored in the set-like `Entity.tags`. These are useful as flags or to group entities together. ```py ->>> entity = world.new_entity() +>>> entity = registry.new_entity() >>> entity.tags.add("player") # Works well for groups >>> "player" in entity.tags True >>> entity.tags.add(("eats", "fruit")) >>> entity.tags.add(("eats", "meat")) ->>> set(world.Q.all_of(tags=["player"])) == {entity} +>>> set(registry.Q.all_of(tags=["player"])) == {entity} True ``` @@ -218,26 +220,26 @@ Relations are unidirectional, but you can query either end of a relation. ... class OrbitOf: # OrbitOf component ... dist: int >>> LandedOn = "LandedOn" # LandedOn tag ->>> star = world.new_entity() ->>> planet = world.new_entity() ->>> moon = world.new_entity() ->>> ship = world.new_entity() ->>> player = world.new_entity() ->>> moon_rock = world.new_entity() +>>> star = registry.new_entity() +>>> planet = registry.new_entity() +>>> moon = registry.new_entity() +>>> ship = registry.new_entity() +>>> player = registry.new_entity() +>>> moon_rock = registry.new_entity() >>> planet.relation_components[OrbitOf][star] = OrbitOf(dist=1000) >>> moon.relation_components[OrbitOf][planet] = OrbitOf(dist=10) >>> ship.relation_tag[LandedOn] = moon >>> moon_rock.relation_tag[LandedOn] = moon >>> player.relation_tag[LandedOn] = moon_rock ->>> set(world.Q.all_of(relations=[(OrbitOf, planet)])) == {moon} +>>> set(registry.Q.all_of(relations=[(OrbitOf, planet)])) == {moon} True ->>> set(world.Q.all_of(relations=[(OrbitOf, ...)])) == {planet, moon} # Get objects in an orbit +>>> set(registry.Q.all_of(relations=[(OrbitOf, ...)])) == {planet, moon} # Get objects in an orbit True ->>> set(world.Q.all_of(relations=[(..., OrbitOf, None)])) == {star, planet} # Get objects being orbited +>>> set(registry.Q.all_of(relations=[(..., OrbitOf, None)])) == {star, planet} # Get objects being orbited True ->>> set(world.Q.all_of(relations=[(LandedOn, ...)])) == {ship, moon_rock, player} +>>> set(registry.Q.all_of(relations=[(LandedOn, ...)])) == {ship, moon_rock, player} True ->>> set(world.Q.all_of(relations=[(LandedOn, ...)]).none_of(relations=[(LandedOn, moon)])) == {player} +>>> set(registry.Q.all_of(relations=[(LandedOn, ...)]).none_of(relations=[(LandedOn, moon)])) == {player} True ``` @@ -251,7 +253,7 @@ You can use the following table to help with constructing relation queries. | ------------------------------------------------------------------- | :--------------------------------------: | | Entities with a relation tag to the given target | `(tag, target_entity)` | | Entities with a relation tag to any target | `(tag, ...)` (Literal dot-dot-dot) | -| Entities with a relation tag to the targets in the given query | `(tag, world.Q.all_of(...))` | +| Entities with a relation tag to the targets in the given query | `(tag, registry.Q.all_of(...))` | | The target entities of a relation of a given entity | `(origin_entity, tag, None)` | | The target entities of any entity with the given relation tag | `(..., tag, None)` (Literal dot-dot-dot) | -| The target entities of the queried entities with the given relation | `(tag, world.Q.all_of(...))` | +| The target entities of the queried entities with the given relation | `(tag, registry.Q.all_of(...))` | diff --git a/conftest.py b/conftest.py index 9d2f2d5..247b69f 100644 --- a/conftest.py +++ b/conftest.py @@ -7,15 +7,15 @@ @pytest.fixture(autouse=True) -def _add_world_entity(doctest_namespace: Dict[str, Any]) -> None: - """Add world and entity objects to all doctests.""" - world = tcod.ecs.Registry() - entity = world["entity"] - other_entity = world["other"] +def _add_registry_entity(doctest_namespace: Dict[str, Any]) -> None: + """Add registry and entity objects to all doctests.""" + registry = tcod.ecs.Registry() + entity = registry["entity"] + other_entity = registry["other"] doctest_namespace.update( { "tcod": tcod, - "world": world, + "registry": registry, "entity": entity, "other_entity": other_entity, } diff --git a/docs/api.rst b/docs/api.rst index f0c6c83..5f6450b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5,9 +5,9 @@ API reference :members: :undoc-members: :show-inheritance: - :exclude-members: World, Entity + :exclude-members: Registry, World, Entity -.. automodule:: tcod.ecs.world +.. automodule:: tcod.ecs.registry :members: :undoc-members: :show-inheritance: diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 2422aea..4c37809 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -40,67 +40,78 @@ _T2 = TypeVar("_T2") _entity_table: WeakKeyDictionary[Registry, WeakValueDictionary[object, Entity]] = WeakKeyDictionary() -"""A weak table of worlds and unique identifiers to entity objects. +"""A weak table of registries and unique identifiers to entity objects. This table is used to that non-unique Entity's won't create a new object and thus will always share identities. -_entity_table[world][uid] = entity +_entity_table[registry][uid] = entity """ class Entity: - """A unique entity in a world. + """A unique entity in a registry. Example:: >>> import tcod.ecs - >>> world = tcod.ecs.World() # Create a new world - >>> world.new_entity() # Create a new entity + >>> registry = tcod.ecs.Registry() # Create a new registry + >>> registry.new_entity() # Create a new entity - >>> entity = world["entity"] # Get an entity from a specific identifier - >>> other_entity = world["other"] + >>> entity = registry["entity"] # Get an entity from a specific identifier + >>> other_entity = registry["other"] """ # Changes here should be reflected in conftest.py - __slots__ = ("world", "uid", "__weakref__") + __slots__ = ("registry", "uid", "__weakref__") - world: Final[Registry] # type:ignore[misc] # https://github.com/python/mypy/issues/5774 - """The :any:`World` this entity belongs to.""" + registry: Final[Registry] # type:ignore[misc] # https://github.com/python/mypy/issues/5774 + """The :any:`Registry` this entity belongs to.""" uid: Final[object] # type:ignore[misc] """This entities unique identifier.""" - def __new__(cls, world: Registry, uid: object = object) -> Entity: - """Return a unique entity for the given `world` and `uid`. + @property + def world(self) -> Registry: + """Deprecated alias for registry. + + .. deprecated:: Unreleased + Use :any:`registry` instead. + """ + if __debug__: + warnings.warn("Use '.registry' instead of '.world'", DeprecationWarning, stacklevel=2) + return self.registry + + def __new__(cls, registry: Registry, uid: object = object) -> Entity: + """Return a unique entity for the given `registry` and `uid`. - If an entity already exists with a matching `world` and `uid` then that entity is returned. + If an entity already exists with a matching `registry` and `uid` then that entity is returned. The `uid` default of `object` will create an instance of :any:`object` as the `uid`. An entity created this way will never match or collide with an existing entity. Example:: - >>> world = tcod.ecs.World() - >>> Entity(world, "foo") + >>> registry = tcod.ecs.Registry() + >>> Entity(registry, "foo") - >>> Entity(world, "foo") is Entity(world, "foo") + >>> Entity(registry, "foo") is Entity(registry, "foo") True - >>> Entity(world) is Entity(world) + >>> Entity(registry) is Entity(registry) False """ if uid is object: uid = object() try: - table = _entity_table[world] + table = _entity_table[registry] except KeyError: table = WeakValueDictionary() - _entity_table[world] = table + _entity_table[registry] = table try: return table[uid] except KeyError: pass self = super().__new__(cls) - self.world = world # type:ignore[misc] # https://github.com/python/mypy/issues/5774 + self.registry = registry # type:ignore[misc] # https://github.com/python/mypy/issues/5774 self.uid = uid # type:ignore[misc] - _entity_table[world][uid] = self + _entity_table[registry][uid] = self return self def clear(self) -> None: @@ -125,12 +136,12 @@ def instantiate(self) -> Self: # 'child = entity.instantiate()' is equivalent to the following: >>> from tcod.ecs import IsA - >>> child = world[object()] # New unique entity + >>> child = registry[object()] # New unique entity >>> child.relation_tag[IsA] = entity # Configure IsA relation Example:: - >>> parent = world.new_entity() + >>> parent = registry.new_entity() >>> parent.components[str] = "baz" >>> child = parent.instantiate() >>> child.components[str] # Inherits components from parent @@ -166,7 +177,7 @@ def instantiate(self) -> Self: .. versionadded:: 5.0 """ - new_entity = self.__class__(self.world, object()) + new_entity = self.__class__(self.registry, object()) new_entity.relation_tag[IsA] = self return new_entity @@ -187,9 +198,9 @@ def components(self) -> EntityComponents: True >>> {str, ("name", str)}.issubset(entity.components.keys()) True - >>> list(world.Q.all_of(components=[str])) # Query components + >>> list(registry.Q.all_of(components=[str])) # Query components [] - >>> list(world.Q[tcod.ecs.Entity, str, ("name", str)]) # Query zip components + >>> list(registry.Q[tcod.ecs.Entity, str, ("name", str)]) # Query zip components [(, 'foo', 'my_name')] """ return EntityComponents(self, (IsA,)) @@ -207,7 +218,7 @@ def tags(self) -> EntityTags: >>> entity.tags.add("tag") # Add tag >>> "tag" in entity.tags # Check tag True - >>> list(world.Q.all_of(tags=["tag"])) # Query tags + >>> list(registry.Q.all_of(tags=["tag"])) # Query tags [] >>> entity.tags.discard("tag") >>> entity.tags |= {"IsPortable", "CanBurn", "OnFire"} # Supports in-place syntax @@ -233,13 +244,13 @@ def relation_components(self) -> EntityComponentRelations: >>> entity.relation_components[("distance", int)][other_entity] = 42 # Also works for named components >>> other_entity in entity.relation_components[str] True - >>> list(world.Q.all_of(relations=[(str, other_entity)])) + >>> list(registry.Q.all_of(relations=[(str, other_entity)])) [] - >>> list(world.Q.all_of(relations=[(str, ...)])) + >>> list(registry.Q.all_of(relations=[(str, ...)])) [] - >>> list(world.Q.all_of(relations=[(entity, str, None)])) + >>> list(registry.Q.all_of(relations=[(entity, str, None)])) [] - >>> list(world.Q.all_of(relations=[(..., str, None)])) + >>> list(registry.Q.all_of(relations=[(..., str, None)])) [] """ return EntityComponentRelations(self, (IsA,)) @@ -251,9 +262,9 @@ def relation_tag(self) -> EntityRelationsExclusive: Example:: >>> entity.relation_tag["ChildOf"] = other_entity # Assign relation - >>> list(world.Q.all_of(relations=[("ChildOf", other_entity)])) # Get children of other_entity + >>> list(registry.Q.all_of(relations=[("ChildOf", other_entity)])) # Get children of other_entity [] - >>> list(world.Q.all_of(relations=[(entity, "ChildOf", None)])) # Get parents of entity + >>> list(registry.Q.all_of(relations=[(entity, "ChildOf", None)])) # Get parents of entity [] >>> del entity.relation_tag["ChildOf"] """ @@ -287,26 +298,26 @@ def _set_name(self, value: object, stacklevel: int = 1) -> None: ) old_name = self.name if old_name is not None: # Remove self from names - del self.world._names_by_name[old_name] - del self.world._names_by_entity[self] + del self.registry._names_by_name[old_name] + del self.registry._names_by_entity[self] if value is not None: # Add self to names - old_entity = self.world._names_by_name.get(value) + old_entity = self.registry._names_by_name.get(value) if old_entity is not None: # Remove entity with old name, name will be overwritten - del self.world._names_by_entity[old_entity] - self.world._names_by_name[value] = self - self.world._names_by_entity[self] = value + del self.registry._names_by_entity[old_entity] + self.registry._names_by_name[value] = self + self.registry._names_by_entity[self] = value @property def name(self) -> object: """The unique name of this entity or None. - You may assign a new name, but if an entity of the world already has that name then it will lose it. + You may assign a new name, but if an entity of the registry already has that name then it will lose it. .. deprecated:: 3.1 This feature has been deprecated. """ - return self.world._names_by_entity.get(self) + return self.registry._names_by_entity.get(self) @name.setter def name(self, value: object) -> None: @@ -317,9 +328,9 @@ def __repr__(self) -> str: Example:: - >>> world.new_entity() + >>> registry.new_entity() - >>> world["foo"] + >>> registry["foo"] """ uid_str = f"object at 0x{id(self.uid):X}" if self.uid.__class__ == object else repr(self.uid) @@ -332,13 +343,13 @@ def __repr__(self) -> str: def __reduce__(self) -> tuple[type[Entity], tuple[Registry, object]]: """Pickle this Entity. - Note that any pickled entity will include the world it belongs to and all the entities of that world. + Note that any pickled entity will include the registry it belongs to and all the entities of that registry. """ - return self.__class__, (self.world, self.uid) + return self.__class__, (self.registry, self.uid) def _force_remap(self, new_uid: object) -> None: """Remap this Entity to a new uid, both and old and new uid's will use this entity.""" - _entity_table[self.world][new_uid] = self + _entity_table[self.registry][new_uid] = self self.uid = new_uid # type: ignore[misc] @@ -350,7 +361,7 @@ def _traverse_entities(start: Entity, traverse_parents: tuple[object, ...]) -> I traverse_parents = traverse_parents[::-1] visited = {start} stack = [start] - _relation_tags_by_entity = start.world._relation_tags_by_entity + _relation_tags_by_entity = start.registry._relation_tags_by_entity while stack: entity = stack.pop() yield entity @@ -410,7 +421,7 @@ def __assert_key(key: ComponentKey[Any]) -> bool: def __getitem__(self, key: ComponentKey[T]) -> T: """Return a component belonging to this entity, or an indirect parent.""" assert self.__assert_key(key) - _components_by_entity = self.entity.world._components_by_entity + _components_by_entity = self.entity.registry._components_by_entity for entity in _traverse_entities(self.entity, self.traverse): try: return _components_by_entity[entity][key] # type: ignore[no-any-return] @@ -422,13 +433,13 @@ def __setitem__(self, key: ComponentKey[T], value: T) -> None: """Assign a component directly to an entity.""" assert self.__assert_key(key) - old_value = self.entity.world._components_by_entity[self.entity].get(key) + old_value = self.entity.registry._components_by_entity[self.entity].get(key) if old_value is None: - tcod.ecs.query._touch_component(self.entity.world, key) # Component added + tcod.ecs.query._touch_component(self.entity.registry, key) # Component added - self.entity.world._components_by_entity[self.entity][key] = value - self.entity.world._components_by_type[key][self.entity] = value + self.entity.registry._components_by_entity[self.entity][key] = value + self.entity.registry._components_by_type[key][self.entity] = value tcod.ecs.callbacks._on_component_changed(key, self.entity, old_value, value) @@ -436,22 +447,22 @@ def __delitem__(self, key: type[object] | tuple[object, type[object]]) -> None: """Delete a directly held component from an entity.""" assert self.__assert_key(key) - old_value = self.entity.world._components_by_entity[self.entity].get(key) + old_value = self.entity.registry._components_by_entity[self.entity].get(key) - del self.entity.world._components_by_entity[self.entity][key] - if not self.entity.world._components_by_entity[self.entity]: - del self.entity.world._components_by_entity[self.entity] + del self.entity.registry._components_by_entity[self.entity][key] + if not self.entity.registry._components_by_entity[self.entity]: + del self.entity.registry._components_by_entity[self.entity] - del self.entity.world._components_by_type[key][self.entity] - if not self.entity.world._components_by_type[key]: - del self.entity.world._components_by_type[key] + del self.entity.registry._components_by_type[key][self.entity] + if not self.entity.registry._components_by_type[key]: + del self.entity.registry._components_by_type[key] - tcod.ecs.query._touch_component(self.entity.world, key) # Component removed + tcod.ecs.query._touch_component(self.entity.registry, key) # Component removed tcod.ecs.callbacks._on_component_changed(key, self.entity, old_value, None) def keys(self) -> Set[ComponentKey[object]]: # type: ignore[override] """Return the components held by this entity, including inherited components.""" - _components_by_entity = self.entity.world._components_by_entity + _components_by_entity = self.entity.registry._components_by_entity if not self.traverse: return _components_by_entity.get(self.entity, {}).keys() set_: set[ComponentKey[object]] = set() @@ -461,7 +472,7 @@ def keys(self) -> Set[ComponentKey[object]]: # type: ignore[override] def __contains__(self, key: ComponentKey[object]) -> bool: # type: ignore[override] """Return True if this entity has the provided component.""" - _components_by_entity = self.entity.world._components_by_entity + _components_by_entity = self.entity.registry._components_by_entity return any( key in _components_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse) ) @@ -553,42 +564,42 @@ def __call__(self, *, traverse: Iterable[object]) -> Self: def add(self, tag: object) -> None: """Add a tag to the entity.""" - if tag in self.entity.world._tags_by_entity[self.entity]: + if tag in self.entity.registry._tags_by_entity[self.entity]: return # Already has tag - tcod.ecs.query._touch_tag(self.entity.world, tag) # Tag added + tcod.ecs.query._touch_tag(self.entity.registry, tag) # Tag added - self.entity.world._tags_by_entity[self.entity].add(tag) - self.entity.world._tags_by_key[tag].add(self.entity) + self.entity.registry._tags_by_entity[self.entity].add(tag) + self.entity.registry._tags_by_key[tag].add(self.entity) def discard(self, tag: object) -> None: """Discard a tag directly held by an entity.""" - if tag not in self.entity.world._tags_by_entity[self.entity]: + if tag not in self.entity.registry._tags_by_entity[self.entity]: return # Already doesn't have tag - tcod.ecs.query._touch_tag(self.entity.world, tag) # Tag removed + tcod.ecs.query._touch_tag(self.entity.registry, tag) # Tag removed - self.entity.world._tags_by_entity[self.entity].discard(tag) - if not self.entity.world._tags_by_entity[self.entity]: - del self.entity.world._tags_by_entity[self.entity] + self.entity.registry._tags_by_entity[self.entity].discard(tag) + if not self.entity.registry._tags_by_entity[self.entity]: + del self.entity.registry._tags_by_entity[self.entity] - self.entity.world._tags_by_key[tag].discard(self.entity) - if not self.entity.world._tags_by_key[tag]: - del self.entity.world._tags_by_key[tag] + self.entity.registry._tags_by_key[tag].discard(self.entity) + if not self.entity.registry._tags_by_key[tag]: + del self.entity.registry._tags_by_key[tag] def remove(self, tag: object) -> None: """Remove a tag directly held by an entity.""" - tags = self.entity.world._tags_by_entity.get(self.entity) + tags = self.entity.registry._tags_by_entity.get(self.entity) if tags is None or tag not in tags: raise KeyError(tag) self.discard(tag) def __contains__(self, x: object) -> bool: """Return True if this entity has the given tag.""" - _tags_by_entity = self.entity.world._tags_by_entity + _tags_by_entity = self.entity.registry._tags_by_entity return any(x in _tags_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse)) def _as_set(self) -> set[object]: """Return all tags inherited by traversal rules into a single set with no duplicates.""" - _tags_by_entity = self.entity.world._tags_by_entity + _tags_by_entity = self.entity.registry._tags_by_entity return set().union( *(_tags_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse)) ) @@ -620,34 +631,34 @@ def __isub__(self, other: Set[Any]) -> Self: return self -def _relations_lookup_add(world: Registry, origin: Entity, tag: object, target: Entity) -> None: +def _relations_lookup_add(registry: Registry, origin: Entity, tag: object, target: Entity) -> None: """Add a relation tag/component to the lookup table and handle side effects.""" - world._relations_lookup[(tag, target)].add(origin) - world._relations_lookup[(tag, ...)].add(origin) - world._relations_lookup[(origin, tag, None)].add(target) - world._relations_lookup[(..., tag, None)].add(target) - tcod.ecs.query._touch_relations(world, ((tag, target), (tag, ...), (origin, tag, None), (..., tag, None))) + registry._relations_lookup[(tag, target)].add(origin) + registry._relations_lookup[(tag, ...)].add(origin) + registry._relations_lookup[(origin, tag, None)].add(target) + registry._relations_lookup[(..., tag, None)].add(target) + tcod.ecs.query._touch_relations(registry, ((tag, target), (tag, ...), (origin, tag, None), (..., tag, None))) -def _relations_lookup_discard(world: Registry, origin: Entity, tag: object, target: Entity) -> None: +def _relations_lookup_discard(registry: Registry, origin: Entity, tag: object, target: Entity) -> None: """Discard a relation tag/component from the lookup table and handle side effects.""" - world._relations_lookup[(tag, target)].discard(origin) - if not world._relations_lookup[(tag, target)]: - del world._relations_lookup[(tag, target)] + registry._relations_lookup[(tag, target)].discard(origin) + if not registry._relations_lookup[(tag, target)]: + del registry._relations_lookup[(tag, target)] - world._relations_lookup[(..., tag, None)].discard(target) - if not world._relations_lookup[(..., tag, None)]: - del world._relations_lookup[(..., tag, None)] + registry._relations_lookup[(..., tag, None)].discard(target) + if not registry._relations_lookup[(..., tag, None)]: + del registry._relations_lookup[(..., tag, None)] - world._relations_lookup[(origin, tag, None)].discard(target) - if not world._relations_lookup[(origin, tag, None)]: - del world._relations_lookup[(origin, tag, None)] + registry._relations_lookup[(origin, tag, None)].discard(target) + if not registry._relations_lookup[(origin, tag, None)]: + del registry._relations_lookup[(origin, tag, None)] - world._relations_lookup[(tag, ...)].discard(origin) - if not world._relations_lookup[(tag, ...)]: - del world._relations_lookup[(tag, ...)] + registry._relations_lookup[(tag, ...)].discard(origin) + if not registry._relations_lookup[(tag, ...)]: + del registry._relations_lookup[(tag, ...)] - tcod.ecs.query._touch_relations(world, ((tag, target), (tag, ...), (origin, tag, None), (..., tag, None))) + tcod.ecs.query._touch_relations(registry, ((tag, target), (tag, ...), (origin, tag, None), (..., tag, None))) @attrs.define(eq=False, frozen=True, weakref_slot=False) @@ -669,29 +680,29 @@ def __attrs_post_init__(self) -> None: def add(self, target: Entity) -> None: """Add a relation target to this tag.""" - world = self.entity.world - world._relation_tags_by_entity[self.entity][self.key].add(target) + registry = self.entity.registry + registry._relation_tags_by_entity[self.entity][self.key].add(target) - _relations_lookup_add(world, self.entity, self.key, target) + _relations_lookup_add(registry, self.entity, self.key, target) def discard(self, target: Entity) -> None: """Discard a directly held relation target from this tag.""" - world = self.entity.world + registry = self.entity.registry - world._relation_tags_by_entity[self.entity][self.key].discard(target) - if not world._relation_tags_by_entity[self.entity][self.key]: - del world._relation_tags_by_entity[self.entity][self.key] - if not world._relation_tags_by_entity[self.entity]: - del world._relation_tags_by_entity[self.entity] + registry._relation_tags_by_entity[self.entity][self.key].discard(target) + if not registry._relation_tags_by_entity[self.entity][self.key]: + del registry._relation_tags_by_entity[self.entity][self.key] + if not registry._relation_tags_by_entity[self.entity]: + del registry._relation_tags_by_entity[self.entity] - _relations_lookup_discard(world, self.entity, self.key, target) + _relations_lookup_discard(registry, self.entity, self.key, target) def remove(self, target: Entity) -> None: """Remove a directly held relation target from this tag. This will raise KeyError of only an indirect relation target exists. """ - relations = self.entity.world._relation_tags_by_entity.get(self.entity) + relations = self.entity.registry._relation_tags_by_entity.get(self.entity) if relations is None: raise KeyError(target) targets = relations.get(self.key) @@ -701,7 +712,7 @@ def remove(self, target: Entity) -> None: def __contains__(self, target: Entity) -> bool: # type: ignore[override] """Return True if this relation contains the given value.""" - _relation_tags_by_entity = self.entity.world._relation_tags_by_entity + _relation_tags_by_entity = self.entity.registry._relation_tags_by_entity for entity in _traverse_entities(self.entity, self.traverse): by_entity = _relation_tags_by_entity.get(entity) if by_entity is None: @@ -712,7 +723,7 @@ def __contains__(self, target: Entity) -> bool: # type: ignore[override] def _as_set(self) -> set[Entity]: """Return the combined targets of this mapping via traversal with duplicates removed.""" - _relation_tags_by_entity = self.entity.world._relation_tags_by_entity + _relation_tags_by_entity = self.entity.registry._relation_tags_by_entity results: set[Entity] = set() for entity in _traverse_entities(self.entity, self.traverse): by_entity = _relation_tags_by_entity.get(entity) @@ -731,7 +742,7 @@ def __len__(self) -> int: def clear(self) -> None: """Discard all targets for this tag relation.""" - by_entity = self.entity.world._relation_tags_by_entity.get(self.entity) + by_entity = self.entity.registry._relation_tags_by_entity.get(self.entity) if by_entity is None: return for key in list(by_entity.get(self.key, ())): @@ -776,7 +787,7 @@ def __delitem__(self, key: object) -> None: def __iter__(self) -> Iterator[Any]: """Iterate over the unique relation tags of this entity.""" - _relation_tags_by_entity = self.entity.world._relation_tags_by_entity + _relation_tags_by_entity = self.entity.registry._relation_tags_by_entity EMPTY_DICT: dict[object, set[Entity]] = {} yield from set().union( *( @@ -791,7 +802,7 @@ def __len__(self) -> int: def clear(self) -> None: """Discard all tag relations from an entity.""" - for key in list(self.entity.world._relation_tags_by_entity.get(self.entity, ())): + for key in list(self.entity.registry._relation_tags_by_entity.get(self.entity, ())): del self[key] @@ -818,7 +829,7 @@ def __getitem__(self, key: object) -> Entity: If the relation has no target then raises KeyError. If the relation is not exclusive then raises ValueError. """ - _relation_tags_by_entity = self.entity.world._relation_tags_by_entity + _relation_tags_by_entity = self.entity.registry._relation_tags_by_entity for entity in _traverse_entities(self.entity, self.traverse): by_entity = _relation_tags_by_entity.get(entity) if by_entity is None: @@ -877,7 +888,7 @@ def __attrs_post_init__(self) -> None: def __getitem__(self, target: Entity) -> T: """Return the component related to a target entity.""" - _relation_components_by_entity = self.entity.world._relation_components_by_entity + _relation_components_by_entity = self.entity.registry._relation_components_by_entity for entity in _traverse_entities(self.entity, self.traverse): by_entity = _relation_components_by_entity.get(entity) if by_entity is None: @@ -892,32 +903,32 @@ def __getitem__(self, target: Entity) -> T: def __setitem__(self, target: Entity, component: T) -> None: """Assign a component to the target entity.""" - world = self.entity.world + registry = self.entity.registry - old_value = world._relation_components_by_entity[self.entity][self.key].get(target) + old_value = registry._relation_components_by_entity[self.entity][self.key].get(target) if old_value is None: # Relation added tcod.ecs.query._touch_relations( - world, ((self.key, target), (self.key, ...), (self.entity, self.key, None), (..., self.key, None)) + registry, ((self.key, target), (self.key, ...), (self.entity, self.key, None), (..., self.key, None)) ) - world._relation_components_by_entity[self.entity][self.key][target] = component + registry._relation_components_by_entity[self.entity][self.key][target] = component - _relations_lookup_add(world, self.entity, self.key, target) + _relations_lookup_add(registry, self.entity, self.key, target) def __delitem__(self, target: Entity) -> None: """Delete a component assigned to the target entity.""" - world = self.entity.world - del world._relation_components_by_entity[self.entity][self.key][target] - if not world._relation_components_by_entity[self.entity][self.key]: - del world._relation_components_by_entity[self.entity][self.key] - if not world._relation_components_by_entity[self.entity]: - del world._relation_components_by_entity[self.entity] + registry = self.entity.registry + del registry._relation_components_by_entity[self.entity][self.key][target] + if not registry._relation_components_by_entity[self.entity][self.key]: + del registry._relation_components_by_entity[self.entity][self.key] + if not registry._relation_components_by_entity[self.entity]: + del registry._relation_components_by_entity[self.entity] - _relations_lookup_discard(world, self.entity, self.key, target) + _relations_lookup_discard(registry, self.entity, self.key, target) def keys(self) -> Set[Entity]: # type: ignore[override] """Return all entities with an associated component value.""" - _relation_components_by_entity = self.entity.world._relation_components_by_entity + _relation_components_by_entity = self.entity.registry._relation_components_by_entity result: set[Entity] = set() for entity in _traverse_entities(self.entity, self.traverse): by_entity = _relation_components_by_entity.get(entity) @@ -990,12 +1001,12 @@ def clear(self) -> None: Does not clear relations targeting this entity. """ - for component_key in list(self.entity.world._relation_components_by_entity.get(self.entity, ())): + for component_key in list(self.entity.registry._relation_components_by_entity.get(self.entity, ())): self[component_key].clear() def keys(self) -> Set[ComponentKey[object]]: # type: ignore[override] """Returns the components keys this entity has relations for.""" - _relation_components_by_entity = self.entity.world._relation_components_by_entity + _relation_components_by_entity = self.entity.registry._relation_components_by_entity return set().union( *( _relation_components_by_entity.get(entity, ()) diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index 6039ce9..b8d4f36 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -1,4 +1,4 @@ -"""Tools for querying World objects.""" +"""Tools for querying Registry objects.""" from __future__ import annotations import itertools @@ -46,7 +46,7 @@ class _QueryCache: """Which queries depend on which relations.""" dependencies: dict[_Query, set[tuple[Registry, _Query]]] = attrs.field(factory=lambda: defaultdict(set)) - """Tracks which queries depend on the queries of the current world. + """Tracks which queries depend on the queries of the current registry. `dependencies[dependency] = {dependant}` """ @@ -55,31 +55,31 @@ class _QueryCache: def _drop_cached_query(cache: _QueryCache, query: _Query) -> None: """Drop a cached query and all of its dependant queries.""" cache.queries.pop(query, None) - for sub_world, sub_query in cache.dependencies.pop(query, ()): - _drop_cached_query(_query_caches[sub_world], sub_query) + for sub_registry, sub_query in cache.dependencies.pop(query, ()): + _drop_cached_query(_query_caches[sub_registry], sub_query) -def _touch_component(world: Registry, component: ComponentKey[object]) -> None: +def _touch_component(registry: Registry, component: ComponentKey[object]) -> None: """Drop cached queries if a component change has invalidated them.""" - cache = _get_query_cache(world) + cache = _get_query_cache(registry) if component not in cache.by_components: return for touched_query in cache.by_components.pop(component, ()): _drop_cached_query(cache, touched_query) -def _touch_tag(world: Registry, tag: object) -> None: +def _touch_tag(registry: Registry, tag: object) -> None: """Drop cached queries if a tag change has invalidated them.""" - cache = _get_query_cache(world) + cache = _get_query_cache(registry) if tag not in cache.by_tags: return for touched_query in cache.by_tags.pop(tag, ()): _drop_cached_query(cache, touched_query) -def _touch_relations(world: Registry, relations: Iterable[_RelationQuery]) -> None: +def _touch_relations(registry: Registry, relations: Iterable[_RelationQuery]) -> None: """Drop cached queries if a relation change has invalidated them.""" - cache = _get_query_cache(world) + cache = _get_query_cache(registry) for relation in relations: if relation not in cache.by_relations: continue @@ -100,47 +100,47 @@ def _check_suspicious_tags(tags: Iterable[object], stacklevel: int = 2) -> None: ) -def _fetch_relation_table(world: Registry, relation: _RelationQuery) -> Set[Entity]: +def _fetch_relation_table(registry: Registry, relation: _RelationQuery) -> Set[Entity]: """Get the entity table for this relation. For simple cases where target/origin is `Entity | ...` this returns the set directly from the lookup table. - For advanced cases `WorldQuery` this returns the subset of entities following the query condition. + For advanced cases `BoundQuery` this returns the subset of entities following the query condition. """ if len(relation) == 2: # noqa: PLR2004 tag, target = relation - if not isinstance(target, WorldQuery): - return world._relations_lookup.get((tag, target), frozenset()) + if not isinstance(target, BoundQuery): + return registry._relations_lookup.get((tag, target), frozenset()) - world = target.world - return set().union(*(world._relations_lookup.get((tag, entity), ()) for entity in target)) + registry = target.registry + return set().union(*(registry._relations_lookup.get((tag, entity), ()) for entity in target)) origin, tag, target_none = relation - if not isinstance(origin, WorldQuery): - return world._relations_lookup.get((origin, tag, target_none), frozenset()) + if not isinstance(origin, BoundQuery): + return registry._relations_lookup.get((origin, tag, target_none), frozenset()) - world = origin.world - return set().union(*(world._relations_lookup.get((entity, tag, None), ()) for entity in origin)) + registry = origin.registry + return set().union(*(registry._relations_lookup.get((entity, tag, None), ()) for entity in origin)) -def _get_query_cache(world: Registry) -> _QueryCache: - """Return the global cache for the given world, creating it if it does not exist.""" - cache = _query_caches.get(world) +def _get_query_cache(registry: Registry) -> _QueryCache: + """Return the global cache for the given registry, creating it if it does not exist.""" + cache = _query_caches.get(registry) if cache is None: - cache = _query_caches[world] = _QueryCache() + cache = _query_caches[registry] = _QueryCache() return cache -def _get_query(world: Registry, query: _Query) -> Set[Entity]: - """Return the entities for the given query and world.""" - cache = _get_query_cache(world) +def _get_query(registry: Registry, query: _Query) -> Set[Entity]: + """Return the entities for the given query and registry.""" + cache = _get_query_cache(registry) if cache is not None: cached_entities = cache.queries.get(query) if cached_entities is not None: return cached_entities # Found a cached query # Not in cache, build the cache and return the results - cache.queries[query] = entities = query._compile(world, cache) - query._add_to_cache(world, cache) + cache.queries[query] = entities = query._compile(registry, cache) + query._add_to_cache(registry, cache) return entities @@ -151,11 +151,11 @@ def _normalize_query_relation(relation: _RelationQuery) -> _RelationQuery: """ if len(relation) == 2: # noqa: PLR2004 tag, targets = relation - if isinstance(targets, WorldQuery): # (tag, targets) + if isinstance(targets, BoundQuery): # (tag, targets) return tag, targets.all_of(relations=[(..., tag, None)]) return relation origin, tag, _ = relation - if isinstance(origin, WorldQuery): # (origins, tag, None) + if isinstance(origin, BoundQuery): # (origins, tag, None) return origin.all_of(relations=[(tag, ...)]), tag, None return relation @@ -163,11 +163,11 @@ def _normalize_query_relation(relation: _RelationQuery) -> _RelationQuery: class _Query(Protocol): """Abstract query class.""" - def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: + def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: """Add this query to the local cache.""" ... - def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: + def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: """Compile the entities of this query, returning a set which must not be modified.""" ... @@ -178,11 +178,11 @@ class _QueryComponent: _component: ComponentKey[object] - def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: + def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: cache.by_components[self._component].add(self) - def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: - return world._components_by_type.get(self._component, {}).keys() + def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + return registry._components_by_type.get(self._component, {}).keys() @attrs.define(frozen=True) @@ -191,11 +191,11 @@ class _QueryTag: _tag: object - def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: + def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: cache.by_tags[self._tag].add(self) - def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: - return world._tags_by_key.get(self._tag, set()) + def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + return registry._tags_by_key.get(self._tag, set()) @attrs.define(frozen=True) @@ -204,24 +204,24 @@ class _QueryRelation: _relation: _RelationQuery = attrs.field(converter=_normalize_query_relation) - def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: - """Add this query to the cache and mark it dependant on a world query if the relation uses one.""" + def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: + """Add this query to the cache and mark it dependant on a registry query if the relation uses one.""" - def _get_world_query() -> WorldQuery | None: - """Return the world query of a relation if it exists.""" - if isinstance(self._relation[0], WorldQuery): + def _get_registry_query() -> BoundQuery | None: + """Return the bound query of a relation if it exists.""" + if isinstance(self._relation[0], BoundQuery): return self._relation[0] - if isinstance(self._relation[-1], WorldQuery): + if isinstance(self._relation[-1], BoundQuery): return self._relation[-1] return None cache.by_relations[self._relation].add(self) - w_query = _get_world_query() + w_query = _get_registry_query() if w_query is not None: - _get_query_cache(w_query.world).dependencies[w_query._query].add((world, self)) + _get_query_cache(w_query.registry).dependencies[w_query._query].add((registry, self)) - def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: - return _fetch_relation_table(world, self._relation) + def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + return _fetch_relation_table(registry, self._relation) @attrs.define(frozen=True) @@ -238,21 +238,21 @@ def __attrs_post_init__(self) -> None: """Verify the current state.""" assert self._all_of.isdisjoint(self._none_of) - def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: + def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: for dependency in itertools.chain(self._all_of, self._none_of): - cache.dependencies[dependency].add((world, self)) + cache.dependencies[dependency].add((registry, self)) - def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: + def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: if len(self._all_of) == 1 and not self._none_of: # Only one sub-query, simply return the results of it - return _get_query(world, next(iter(self._all_of))) # Avoids an extra copy of a set + return _get_query(registry, next(iter(self._all_of))) # Avoids an extra copy of a set requires = sorted( # Place the smallest sets first to speed up intersections - (_get_query(world, q) for q in self._all_of), key=len + (_get_query(registry, q) for q in self._all_of), key=len ) entities = set(requires[0]) for required_set in requires[1:]: entities.intersection_update(required_set) for excluded_query in self._none_of: - entities.difference_update(_get_query(world, excluded_query)) + entities.difference_update(_get_query(registry, excluded_query)) return entities def __and__(self, other: _Query) -> Self: @@ -267,15 +267,15 @@ class _QueryLogicalOr: _any_of: frozenset[_Query] = frozenset() - def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: + def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: for dependency in self._any_of: - cache.dependencies[dependency].add((world, self)) + cache.dependencies[dependency].add((registry, self)) - def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: + def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: if len(self._any_of) == 1: # If there is only one sub-query then simply return the results of it - return _get_query(world, next(iter(self._any_of))) # Avoids an extra copy of a set + return _get_query(registry, next(iter(self._any_of))) # Avoids an extra copy of a set entities: set[Entity] = set() - entities.update(*(_get_query(world, q) for q in self._any_of)) + entities.update(*(_get_query(registry, q) for q in self._any_of)) return entities @@ -296,13 +296,15 @@ def _get_traverse_query(self) -> _QueryLogicalOr: any_of=frozenset(_QueryRelation((..., traverse_key, None)) for traverse_key in self._traverse_keys) ) - def _add_to_cache(self, world: Registry, cache: _QueryCache) -> None: - cache.dependencies[self._sub_query].add((world, self)) - cache.dependencies[self._get_traverse_query()].add((world, self)) + def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: + cache.dependencies[self._sub_query].add((registry, self)) + cache.dependencies[self._get_traverse_query()].add((registry, self)) - def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: - cumulative_set = set(_get_query(world, self._sub_query)) # All entities touched by this traversal - relations_set = _get_query(world, self._get_traverse_query()) # The subset of entities which can propagate from + def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + cumulative_set = set(_get_query(registry, self._sub_query)) # All entities touched by this traversal + relations_set = _get_query( + registry, self._get_traverse_query() + ) # The subset of entities which can propagate from unchecked_set = cumulative_set & relations_set # Most recently touched entities which can propagate farther depth = 0 while unchecked_set and (self._max_depth is None or depth < self._max_depth): @@ -311,7 +313,7 @@ def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: empty_set: frozenset[Entity] = frozenset() for traverse_key in self._traverse_keys: for unchecked in unchecked_set: - new_set |= world._relations_lookup.get((traverse_key, unchecked), empty_set) + new_set |= registry._relations_lookup.get((traverse_key, unchecked), empty_set) new_set -= cumulative_set cumulative_set |= new_set unchecked_set = new_set @@ -319,12 +321,26 @@ def _compile(self, world: Registry, cache: _QueryCache) -> Set[Entity]: @attrs.define(frozen=True) -class WorldQuery: - """Collect a set of entities with the provided conditions.""" +class BoundQuery: + """Collect a set of entities with the provided conditions. - world: Registry + This query is bound to a specific registry. + """ + + registry: Registry _query: _Query = attrs.field(factory=_QueryLogicalAnd) + @property + def world(self) -> Registry: + """Deprecated alias for registry. + + .. deprecated:: Unreleased + Use :any:`registry` instead. + """ + if __debug__: + warnings.warn("Use '.registry' instead of '.world'", DeprecationWarning, stacklevel=2) + return self.registry + def get_entities(self) -> Set[Entity]: """Return entities matching the current query as a read-only set. @@ -332,7 +348,7 @@ def get_entities(self) -> Set[Entity]: .. versionadded:: 4.4 """ - return _get_query(self.world, self._query) + return _get_query(self.registry, self._query) @staticmethod def __as_queries( @@ -360,7 +376,7 @@ def all_of( # noqa: PLR0913 """Filter entities based on having all of the provided elements.""" _check_suspicious_tags(tags, stacklevel=2) return self.__class__( - self.world, + self.registry, _QueryLogicalAnd(all_of=frozenset(self.__as_queries(components, tags, relations, traverse, depth))) & self._query, ) @@ -377,7 +393,7 @@ def none_of( # noqa: PLR0913 """Filter entities based on having none of the provided elements.""" _check_suspicious_tags(tags, stacklevel=2) return self.__class__( - self.world, + self.registry, _QueryLogicalAnd(none_of=frozenset(self.__as_queries(components, tags, relations, traverse, depth))) & self._query, ) @@ -430,6 +446,9 @@ def __getitem__(self, key: tuple[ComponentKey[object], ...]) -> Iterable[tuple[A if component_key is Entity: entity_components.append(entities) continue - world_components = self.world._components_by_type[component_key] - entity_components.append([world_components[entity] for entity in entities]) + registry_components = self.registry._components_by_type[component_key] + entity_components.append([registry_components[entity] for entity in entities]) return zip(*entity_components) + + +WorldQuery = BoundQuery diff --git a/tcod/ecs/registry.py b/tcod/ecs/registry.py index 71bfc5a..82260ae 100644 --- a/tcod/ecs/registry.py +++ b/tcod/ecs/registry.py @@ -1,4 +1,4 @@ -"""World management tools.""" +"""Registry management tools.""" from __future__ import annotations import warnings @@ -151,7 +151,7 @@ class Registry: def global_(self) -> Entity: """A unique globally accessible entity. - This can be used to store globally accessible components in the world itself without any extra boilerplate. + This can be used to store globally accessible components in the registry itself without any extra boilerplate. Otherwise this entity is not special and will show up with other entities in queries, etc. This entity has a `uid` of `None` and may be accessed that way. @@ -161,12 +161,12 @@ def global_(self) -> Entity: Example:: - >>> world[None].components[("turn", int)] = 0 - >>> world[None].components[("turn", int)] + >>> registry[None].components[("turn", int)] = 0 + >>> registry[None].components[("turn", int)] 0 """ warnings.warn( - "The 'world.global_' attribute has been deprecated. Use 'world[None]' to access this entity.", + "The 'registry.global_' attribute has been deprecated. Use 'registry[None]' to access this entity.", FutureWarning, stacklevel=2, ) @@ -242,25 +242,25 @@ def __getitem__(self, uid: object) -> Entity: Example:: - >>> world = Registry() - >>> foo = world["foo"] # Referencing a new entity returns a new empty entity - >>> foo is world["foo"] + >>> registry = Registry() + >>> foo = registry["foo"] # Referencing a new entity returns a new empty entity + >>> foo is registry["foo"] True - >>> entity = world.new_entity() - >>> world[entity.uid] is entity # Anonymous entities can be referred to by their uid + >>> entity = registry.new_entity() + >>> registry[entity.uid] is entity # Anonymous entities can be referred to by their uid True """ assert uid is not object, "This is reserved." return Entity(self, uid) def __iter__(self) -> NoReturn: - """Raises TypeError, :any:`World` is not iterable.""" - msg = "'World' object is not iterable." + """Raises TypeError, :any:`Registry` is not iterable.""" + msg = "'Registry' object is not iterable." raise TypeError(msg) @property def named(self) -> Mapping[object, Entity]: - """A view into this worlds named entities. + """A view into this registries named entities. .. deprecated:: 3.1 This feature has been deprecated. @@ -281,7 +281,7 @@ def new_entity( Example:: - >>> entity = world.new_entity( + >>> entity = registry.new_entity( ... components={ ... ("name", str): "my name", ... ("hp", int): 10, @@ -306,9 +306,9 @@ def new_entity( return entity @property - def Q(self) -> tcod.ecs.query.WorldQuery: - """Start a new Query for this world. + def Q(self) -> tcod.ecs.query.BoundQuery: + """Start a new Query for this registry. - Alias for ``tcod.ecs.Query(world)``. + Alias for ``tcod.ecs.Query(registry)``. """ - return tcod.ecs.query.WorldQuery(self) + return tcod.ecs.query.BoundQuery(self) diff --git a/tcod/ecs/typing.py b/tcod/ecs/typing.py index f0c58ad..7c6b6af 100644 --- a/tcod/ecs/typing.py +++ b/tcod/ecs/typing.py @@ -9,10 +9,10 @@ if TYPE_CHECKING: from tcod.ecs.entity import Entity - from tcod.ecs.query import WorldQuery + from tcod.ecs.query import BoundQuery else: Entity = Any - WorldQuery = Any + BoundQuery = Any if sys.version_info >= (3, 10): # pragma: no cover EllipsisType: TypeAlias = types.EllipsisType @@ -27,7 +27,7 @@ _RelationTargetLookup: TypeAlias = Union[Entity, EllipsisType] """Possible target for stored relations.""" -_RelationQueryTarget: TypeAlias = Union[_RelationTargetLookup, WorldQuery] +_RelationQueryTarget: TypeAlias = Union[_RelationTargetLookup, BoundQuery] """Possible target for relation queries.""" _RelationQuery: TypeAlias = Union[Tuple[object, _RelationQueryTarget], Tuple[_RelationQueryTarget, object, None]] diff --git a/tests/test_ecs.py b/tests/test_ecs.py index 147031a..5161732 100644 --- a/tests/test_ecs.py +++ b/tests/test_ecs.py @@ -184,9 +184,9 @@ def test_unpickle(sample_version: str, ecs_version: str) -> None: def test_global() -> None: world = tcod.ecs.Registry() - with pytest.warns(match=r"world\[None\]"): + with pytest.warns(match=r"registry\[None\]"): world.global_.components[int] = 1 - with pytest.warns(match=r"world\[None\]"): + with pytest.warns(match=r"registry\[None\]"): assert set(world.Q[tcod.ecs.Entity, int]) == {(world.global_, 1)} From 833e1e326d4e9b13f0586c4415adfe57f3c2b159 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 13 Feb 2024 05:48:35 -0800 Subject: [PATCH 26/83] Prepare 5.1.0 release --- CHANGELOG.md | 2 ++ LICENSE | 2 +- docs/conf.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ea1871..2ac5db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.1.0] - 2024-02-13 + ### Changed - Renamed `World` to the more standard name `Registry` in multiple places. diff --git a/LICENSE b/LICENSE index 79dc0f7..22c0179 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2023 Kyle Benesch +Copyright (c) 2023-2024 Kyle Benesch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/conf.py b/docs/conf.py index 74e1656..6691aa2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ import importlib.metadata project = "tcod-ecs" -copyright = "2023, Kyle Benesch" +copyright = "2023-2024, Kyle Benesch" author = "Kyle Benesch" release = importlib.metadata.version(project) version = ".".join(release.split(".")[:2]) From d057ae89f1b5fba8dcf97384969f04dd09d9e365 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:43:00 +0000 Subject: [PATCH 27/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.1 → v0.2.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.1...v0.2.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af18a76..b05cc87 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.1 + rev: v0.2.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 2eddfcaedc1083d6ce28151a23a7613329c11f34 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 00:56:47 +0000 Subject: [PATCH 28/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.2 → v0.3.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.2...v0.3.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b05cc87..6cf360e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.2 + rev: v0.3.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 378847d361d21da18c4aa573172ffef96710de2b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 00:57:02 +0000 Subject: [PATCH 29/83] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/get_release_description.py | 1 + tcod/ecs/__init__.py | 1 + tcod/ecs/callbacks.py | 1 + tcod/ecs/constants.py | 1 + tcod/ecs/entity.py | 7 +++---- tcod/ecs/query.py | 19 +++++++------------ tcod/ecs/registry.py | 7 ++++--- tcod/ecs/typing.py | 1 + tests/test_benchmarks.py | 1 + tests/test_ecs.py | 1 + tests/test_relations.py | 1 + tests/test_traversal.py | 1 + 12 files changed, 23 insertions(+), 19 deletions(-) diff --git a/scripts/get_release_description.py b/scripts/get_release_description.py index 7d47d9f..c106885 100755 --- a/scripts/get_release_description.py +++ b/scripts/get_release_description.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Print the description used for GitHub Releases.""" + from __future__ import annotations import re diff --git a/tcod/ecs/__init__.py b/tcod/ecs/__init__.py index 7b0371d..f8e9376 100644 --- a/tcod/ecs/__init__.py +++ b/tcod/ecs/__init__.py @@ -1,4 +1,5 @@ """A type-hinted Entity Component System based on Python dictionaries and sets.""" + from __future__ import annotations import importlib.metadata diff --git a/tcod/ecs/callbacks.py b/tcod/ecs/callbacks.py index d0bbfe1..1a005ee 100644 --- a/tcod/ecs/callbacks.py +++ b/tcod/ecs/callbacks.py @@ -1,4 +1,5 @@ """ECS callback management.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union diff --git a/tcod/ecs/constants.py b/tcod/ecs/constants.py index 75ca669..ec366a6 100644 --- a/tcod/ecs/constants.py +++ b/tcod/ecs/constants.py @@ -1,4 +1,5 @@ """Special constants and sentinel values.""" + from typing import Final from sentinel_value import sentinel diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 4c37809..76af417 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -1,4 +1,5 @@ """Entity management and interface tools.""" + from __future__ import annotations import warnings @@ -512,12 +513,10 @@ def by_name_type(self, name_type: type[_T1], component_type: type[_T2]) -> Itera yield key_name, key_component @overload - def __ior__(self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any]) -> Self: - ... + def __ior__(self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any]) -> Self: ... @overload - def __ior__(self, value: Iterable[tuple[ComponentKey[Any], Any]]) -> Self: - ... + def __ior__(self, value: Iterable[tuple[ComponentKey[Any], Any]]) -> Self: ... def __ior__( self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any] | Iterable[tuple[ComponentKey[Any], Any]] diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index b8d4f36..c4f6ea1 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -1,4 +1,5 @@ """Tools for querying Registry objects.""" + from __future__ import annotations import itertools @@ -403,35 +404,29 @@ def __iter__(self) -> Iterator[Entity]: return iter(self.get_entities()) @overload - def __getitem__(self, key: tuple[ComponentKey[_T1]]) -> Iterable[tuple[_T1]]: - ... + def __getitem__(self, key: tuple[ComponentKey[_T1]]) -> Iterable[tuple[_T1]]: ... @overload - def __getitem__(self, key: tuple[ComponentKey[_T1], ComponentKey[_T2]]) -> Iterable[tuple[_T1, _T2]]: - ... + def __getitem__(self, key: tuple[ComponentKey[_T1], ComponentKey[_T2]]) -> Iterable[tuple[_T1, _T2]]: ... @overload def __getitem__( self, key: tuple[ComponentKey[_T1], ComponentKey[_T2], ComponentKey[_T3]] - ) -> Iterable[tuple[_T1, _T2, _T3]]: - ... + ) -> Iterable[tuple[_T1, _T2, _T3]]: ... @overload def __getitem__( self, key: tuple[ComponentKey[_T1], ComponentKey[_T2], ComponentKey[_T3], ComponentKey[_T4]] - ) -> Iterable[tuple[_T1, _T2, _T3, _T4]]: - ... + ) -> Iterable[tuple[_T1, _T2, _T3, _T4]]: ... @overload def __getitem__( self, key: tuple[ComponentKey[_T1], ComponentKey[_T2], ComponentKey[_T3], ComponentKey[_T4], ComponentKey[_T5]], - ) -> Iterable[tuple[_T1, _T2, _T3, _T4, _T5]]: - ... + ) -> Iterable[tuple[_T1, _T2, _T3, _T4, _T5]]: ... @overload - def __getitem__(self, key: tuple[ComponentKey[object], ...]) -> Iterable[tuple[Any, ...]]: - ... + def __getitem__(self, key: tuple[ComponentKey[object], ...]) -> Iterable[tuple[Any, ...]]: ... def __getitem__(self, key: tuple[ComponentKey[object], ...]) -> Iterable[tuple[Any, ...]]: """Collect components from a query.""" diff --git a/tcod/ecs/registry.py b/tcod/ecs/registry.py index 82260ae..2841f84 100644 --- a/tcod/ecs/registry.py +++ b/tcod/ecs/registry.py @@ -1,4 +1,5 @@ """Registry management tools.""" + from __future__ import annotations import warnings @@ -110,9 +111,9 @@ class Registry: dict[entity][tag] = {target_entities} """ - _relation_components_by_entity: defaultdict[ - Entity, defaultdict[ComponentKey[object], dict[Entity, Any]] - ] = attrs.field(init=False, factory=lambda: defaultdict(_defaultdict_of_dict)) + _relation_components_by_entity: defaultdict[Entity, defaultdict[ComponentKey[object], dict[Entity, Any]]] = ( + attrs.field(init=False, factory=lambda: defaultdict(_defaultdict_of_dict)) + ) """Random access relations owning components. dict[entity][ComponentKey][target_entity] = component diff --git a/tcod/ecs/typing.py b/tcod/ecs/typing.py index 7c6b6af..bb578ec 100644 --- a/tcod/ecs/typing.py +++ b/tcod/ecs/typing.py @@ -1,4 +1,5 @@ """Common type-hints for tcod.ecs.""" + from __future__ import annotations import sys diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py index 5b944e9..950b11f 100644 --- a/tests/test_benchmarks.py +++ b/tests/test_benchmarks.py @@ -1,4 +1,5 @@ """Benchmarking tests.""" + from __future__ import annotations from typing import Any diff --git a/tests/test_ecs.py b/tests/test_ecs.py index 5161732..e5daddc 100644 --- a/tests/test_ecs.py +++ b/tests/test_ecs.py @@ -1,4 +1,5 @@ """Tests for tcod-ecs.""" + from __future__ import annotations import io diff --git a/tests/test_relations.py b/tests/test_relations.py index d6cb2bf..a758eeb 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,4 +1,5 @@ """Tests for entity relations.""" + from __future__ import annotations from typing import Final diff --git a/tests/test_traversal.py b/tests/test_traversal.py index edfa9cc..0cc555b 100644 --- a/tests/test_traversal.py +++ b/tests/test_traversal.py @@ -1,4 +1,5 @@ """Inheritance tests.""" + from typing import Final import pytest From 542e4be7249cf534ae048a3590ac4d57ed5cda4e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 22:32:16 +0000 Subject: [PATCH 30/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.2 → v0.3.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.2...v0.3.3) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6cf360e..8e4db31 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.2 + rev: v0.3.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 6d62dbaaf02048e63bea3356c25e39126c550f41 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 22:13:05 +0000 Subject: [PATCH 31/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.3 → v0.3.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.3...v0.3.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e4db31..1d7a509 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.3 + rev: v0.3.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 01a67af6844969e54e83d76a3f002efadb5b5133 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 22:32:59 +0000 Subject: [PATCH 32/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.3.5) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1d7a509..02a0264 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.3.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From b819dbb5e93c935deb13669f0e78b635bc31b237 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 22:57:00 +0000 Subject: [PATCH 33/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 02a0264..e9d8573 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From a026e6fb92c9cd8be1ad2e2f8435420ab6b01eaf Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 9 Jul 2024 05:44:06 -0700 Subject: [PATCH 34/83] Update and apply linters --- .pre-commit-config.yaml | 4 ++- conftest.py | 6 ++-- docs/conf.py | 6 ++-- pyproject.toml | 61 ++++++++++------------------------------- tcod/ecs/_converter.py | 2 +- tcod/ecs/callbacks.py | 3 +- tcod/ecs/constants.py | 2 ++ tcod/ecs/entity.py | 23 ++++++++-------- tcod/ecs/query.py | 33 +++++++++++----------- tcod/ecs/registry.py | 12 ++++---- tcod/ecs/typing.py | 2 +- tcod/ecs/world.py | 2 ++ tests/test_traversal.py | 6 ++-- 13 files changed, 73 insertions(+), 89 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9d8573..3543878 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,7 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +ci: + autoupdate_schedule: quarterly repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 @@ -15,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.5 + rev: v0.5.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/conftest.py b/conftest.py index 247b69f..3d791ef 100644 --- a/conftest.py +++ b/conftest.py @@ -1,5 +1,7 @@ # ruff: noqa: D100 D103 ANN401 -from typing import Any, Dict +from __future__ import annotations + +from typing import Any import pytest @@ -7,7 +9,7 @@ @pytest.fixture(autouse=True) -def _add_registry_entity(doctest_namespace: Dict[str, Any]) -> None: +def _add_registry_entity(doctest_namespace: dict[str, Any]) -> None: """Add registry and entity objects to all doctests.""" registry = tcod.ecs.Registry() entity = registry["entity"] diff --git a/docs/conf.py b/docs/conf.py index 6691aa2..bc08cb5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,10 +6,12 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +from __future__ import annotations + import importlib.metadata project = "tcod-ecs" -copyright = "2023-2024, Kyle Benesch" +copyright = "2023-2024, Kyle Benesch" # noqa: A001 author = "Kyle Benesch" release = importlib.metadata.version(project) version = ".".join(release.split(".")[:2]) @@ -32,7 +34,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "furo" -# html_static_path = ["_static"] +# html_static_path = ["_static"] # noqa: ERA001 intersphinx_mapping = { diff --git a/pyproject.toml b/pyproject.toml index 9048b1c..0597f43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,16 +46,6 @@ Changelog = "https://github.com/HexDecimal/python-tcod-ecs/blob/main/CHANGELOG.m Documentation = "https://python-tcod-ecs.readthedocs.io" Source = "https://github.com/HexDecimal/python-tcod-ecs" -[tool.black] # https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file -target-version = ["py38"] -line-length = 120 - -[tool.isort] # https://pycqa.github.io/isort/docs/configuration/options.html -py_version = "38" -line_length = 120 -profile = "black" -skip_gitignore = true - [tool.mypy] # https://mypy.readthedocs.io/en/stable/config_file.html files = "." exclude = ['^build/', '^\.'] @@ -86,44 +76,23 @@ testpaths = ["."] exclude_lines = ['^\s*\.\.\.', "if TYPE_CHECKING:", "# pragma: no cover"] [tool.ruff] -# https://beta.ruff.rs/docs/rules/ -select = [ - "C90", # mccabe - "E", # pycodestyle - "W", # pycodestyle - "F", # Pyflakes - "I", # isort - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "S", # flake8-bandit - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "EM", # flake8-errmsg - "EXE", # flake8-executable - "RET", # flake8-return - "ICN", # flake8-import-conventions - "PIE", # flake8-pie - "PT", # flake8-pytest-style - "SIM", # flake8-simplify - "PTH", # flake8-use-pathlib - "PL", # Pylint - "TRY", # tryceratops - "RUF", # NumPy-specific rules - "G", # flake8-logging-format - "D", # pydocstyle -] +line-length = 120 + +[tool.ruff.lint] # https://docs.astral.sh/ruff/rules/ +select = ["ALL"] ignore = [ - "E501", # line-too-long - "S101", # assert "ANN101", # missing-type-self "ANN102", # missing-type-cls - "D206", # indent-with-spaces - "W191", # tab-indentation + "COM", # flake8-commas, handled by formatter + "E501", # line-too-long + "S101", # assert + "SLF001", # private-member-access + "T10", # flake8-debugger + "T20", # flake8-print ] -line-length = 120 -[tool.ruff.pydocstyle] -# Use Google-style docstrings. -convention = "google" +[tool.ruff.lint.isort] +required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint.pydocstyle] +convention = "google" # Use Google-style docstrings. diff --git a/tcod/ecs/_converter.py b/tcod/ecs/_converter.py index 8e7a2ff..4ca5c9f 100644 --- a/tcod/ecs/_converter.py +++ b/tcod/ecs/_converter.py @@ -12,7 +12,7 @@ def _is_defaultdict_type(type_hint: object) -> bool: return get_origin(type_hint) is defaultdict -def _setup_defaultdict_factory(type_hint: type[defaultdict[Any, Any]] | type[object]) -> Callable[[], Any]: +def _setup_defaultdict_factory(type_hint: type[defaultdict[Any, Any] | object]) -> Callable[[], Any]: """Return the factory value for a defaultdict given its value type-hint.""" assert type_hint is not Any if get_origin(type_hint) is not defaultdict: diff --git a/tcod/ecs/callbacks.py b/tcod/ecs/callbacks.py index 1a005ee..d3d93ad 100644 --- a/tcod/ecs/callbacks.py +++ b/tcod/ecs/callbacks.py @@ -6,10 +6,9 @@ from typing_extensions import TypeAlias -from tcod.ecs.typing import ComponentKey - if TYPE_CHECKING: from tcod.ecs.entity import Entity + from tcod.ecs.typing import ComponentKey _T = TypeVar("_T") diff --git a/tcod/ecs/constants.py b/tcod/ecs/constants.py index ec366a6..6c7980d 100644 --- a/tcod/ecs/constants.py +++ b/tcod/ecs/constants.py @@ -1,5 +1,7 @@ """Special constants and sentinel values.""" +from __future__ import annotations + from typing import Final from sentinel_value import sentinel diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 76af417..a611b15 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -3,7 +3,6 @@ from __future__ import annotations import warnings -from collections.abc import Set from typing import ( TYPE_CHECKING, Any, @@ -31,6 +30,8 @@ from tcod.ecs.typing import ComponentKey if TYPE_CHECKING: + from collections.abc import Set as AbstractSet + from _typeshed import SupportsKeysAndGetItem from tcod.ecs.registry import Registry @@ -80,7 +81,7 @@ def world(self) -> Registry: warnings.warn("Use '.registry' instead of '.world'", DeprecationWarning, stacklevel=2) return self.registry - def __new__(cls, registry: Registry, uid: object = object) -> Entity: + def __new__(cls, registry: Registry, uid: object = object) -> Entity: # noqa: PYI034 """Return a unique entity for the given `registry` and `uid`. If an entity already exists with a matching `registry` and `uid` then that entity is returned. @@ -334,7 +335,7 @@ def __repr__(self) -> str: >>> registry["foo"] """ - uid_str = f"object at 0x{id(self.uid):X}" if self.uid.__class__ == object else repr(self.uid) + uid_str = f"object at 0x{id(self.uid):X}" if self.uid.__class__ is object else repr(self.uid) items = [f"{self.__class__.__name__}(uid={uid_str})"] name = self.name if name is not None: # Switch to older style @@ -426,7 +427,7 @@ def __getitem__(self, key: ComponentKey[T]) -> T: for entity in _traverse_entities(self.entity, self.traverse): try: return _components_by_entity[entity][key] # type: ignore[no-any-return] - except KeyError: + except KeyError: # noqa: PERF203 pass raise KeyError(key) @@ -461,7 +462,7 @@ def __delitem__(self, key: type[object] | tuple[object, type[object]]) -> None: tcod.ecs.query._touch_component(self.entity.registry, key) # Component removed tcod.ecs.callbacks._on_component_changed(key, self.entity, old_value, None) - def keys(self) -> Set[ComponentKey[object]]: # type: ignore[override] + def keys(self) -> AbstractSet[ComponentKey[object]]: # type: ignore[override] """Return the components held by this entity, including inherited components.""" _components_by_entity = self.entity.registry._components_by_entity if not self.traverse: @@ -611,7 +612,7 @@ def __len__(self) -> int: """Return the number of tags this entity has.""" return len(self._as_set()) - def __ior__(self, other: Set[object]) -> Self: + def __ior__(self, other: AbstractSet[object]) -> Self: """Add tags in-place. .. versionadded:: 3.3 @@ -620,7 +621,7 @@ def __ior__(self, other: Set[object]) -> Self: self.add(to_add) return self - def __isub__(self, other: Set[Any]) -> Self: + def __isub__(self, other: AbstractSet[Any]) -> Self: """Remove tags in-place. .. versionadded:: 3.3 @@ -787,10 +788,10 @@ def __delitem__(self, key: object) -> None: def __iter__(self) -> Iterator[Any]: """Iterate over the unique relation tags of this entity.""" _relation_tags_by_entity = self.entity.registry._relation_tags_by_entity - EMPTY_DICT: dict[object, set[Entity]] = {} + empty_dict: dict[object, set[Entity]] = {} yield from set().union( *( - _relation_tags_by_entity.get(entity, EMPTY_DICT).keys() + _relation_tags_by_entity.get(entity, empty_dict).keys() for entity in _traverse_entities(self.entity, self.traverse) ) ) @@ -925,7 +926,7 @@ def __delitem__(self, target: Entity) -> None: _relations_lookup_discard(registry, self.entity, self.key, target) - def keys(self) -> Set[Entity]: # type: ignore[override] + def keys(self) -> AbstractSet[Entity]: # type: ignore[override] """Return all entities with an associated component value.""" _relation_components_by_entity = self.entity.registry._relation_components_by_entity result: set[Entity] = set() @@ -1003,7 +1004,7 @@ def clear(self) -> None: for component_key in list(self.entity.registry._relation_components_by_entity.get(self.entity, ())): self[component_key].clear() - def keys(self) -> Set[ComponentKey[object]]: # type: ignore[override] + def keys(self) -> AbstractSet[ComponentKey[object]]: # type: ignore[override] """Returns the components keys this entity has relations for.""" _relation_components_by_entity = self.entity.registry._relation_components_by_entity return set().union( diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index c4f6ea1..a072c76 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -5,7 +5,6 @@ import itertools import warnings from collections import defaultdict -from collections.abc import Set from typing import TYPE_CHECKING, Any, Iterable, Iterator, Protocol, TypeVar, overload from weakref import WeakKeyDictionary, WeakSet @@ -14,11 +13,13 @@ import tcod.ecs.entity from tcod.ecs.constants import IsA -from tcod.ecs.typing import ComponentKey, _RelationQuery if TYPE_CHECKING: + from collections.abc import Set as AbstractSet + from tcod.ecs.entity import Entity from tcod.ecs.registry import Registry + from tcod.ecs.typing import ComponentKey, _RelationQuery _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") @@ -35,7 +36,7 @@ class _QueryCache: """Main data structure for the query cache.""" - queries: dict[_Query, Set[Entity]] = attrs.field(factory=dict) + queries: dict[_Query, AbstractSet[Entity]] = attrs.field(factory=dict) """Table of cached queries.""" by_components: defaultdict[ComponentKey[object], WeakSet[_Query]] = attrs.field( factory=lambda: defaultdict(WeakSet) @@ -101,7 +102,7 @@ def _check_suspicious_tags(tags: Iterable[object], stacklevel: int = 2) -> None: ) -def _fetch_relation_table(registry: Registry, relation: _RelationQuery) -> Set[Entity]: +def _fetch_relation_table(registry: Registry, relation: _RelationQuery) -> AbstractSet[Entity]: """Get the entity table for this relation. For simple cases where target/origin is `Entity | ...` this returns the set directly from the lookup table. @@ -131,7 +132,7 @@ def _get_query_cache(registry: Registry) -> _QueryCache: return cache -def _get_query(registry: Registry, query: _Query) -> Set[Entity]: +def _get_query(registry: Registry, query: _Query) -> AbstractSet[Entity]: """Return the entities for the given query and registry.""" cache = _get_query_cache(registry) if cache is not None: @@ -168,7 +169,7 @@ def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: """Add this query to the local cache.""" ... - def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: """Compile the entities of this query, returning a set which must not be modified.""" ... @@ -179,10 +180,10 @@ class _QueryComponent: _component: ComponentKey[object] - def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: + def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: # noqa: ARG002 cache.by_components[self._component].add(self) - def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002 return registry._components_by_type.get(self._component, {}).keys() @@ -192,10 +193,10 @@ class _QueryTag: _tag: object - def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: + def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: # noqa: ARG002 cache.by_tags[self._tag].add(self) - def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002 return registry._tags_by_key.get(self._tag, set()) @@ -221,7 +222,7 @@ def _get_registry_query() -> BoundQuery | None: if w_query is not None: _get_query_cache(w_query.registry).dependencies[w_query._query].add((registry, self)) - def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002 return _fetch_relation_table(registry, self._relation) @@ -243,7 +244,7 @@ def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: for dependency in itertools.chain(self._all_of, self._none_of): cache.dependencies[dependency].add((registry, self)) - def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002 if len(self._all_of) == 1 and not self._none_of: # Only one sub-query, simply return the results of it return _get_query(registry, next(iter(self._all_of))) # Avoids an extra copy of a set requires = sorted( # Place the smallest sets first to speed up intersections @@ -272,7 +273,7 @@ def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: for dependency in self._any_of: cache.dependencies[dependency].add((registry, self)) - def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002 if len(self._any_of) == 1: # If there is only one sub-query then simply return the results of it return _get_query(registry, next(iter(self._any_of))) # Avoids an extra copy of a set entities: set[Entity] = set() @@ -301,7 +302,7 @@ def _add_to_cache(self, registry: Registry, cache: _QueryCache) -> None: cache.dependencies[self._sub_query].add((registry, self)) cache.dependencies[self._get_traverse_query()].add((registry, self)) - def _compile(self, registry: Registry, cache: _QueryCache) -> Set[Entity]: + def _compile(self, registry: Registry, cache: _QueryCache) -> AbstractSet[Entity]: # noqa: ARG002 cumulative_set = set(_get_query(registry, self._sub_query)) # All entities touched by this traversal relations_set = _get_query( registry, self._get_traverse_query() @@ -342,7 +343,7 @@ def world(self) -> Registry: warnings.warn("Use '.registry' instead of '.world'", DeprecationWarning, stacklevel=2) return self.registry - def get_entities(self) -> Set[Entity]: + def get_entities(self) -> AbstractSet[Entity]: """Return entities matching the current query as a read-only set. This is useful for post-processing the results of a query using set operations. @@ -433,7 +434,7 @@ def __getitem__(self, key: tuple[ComponentKey[object], ...]) -> Iterable[tuple[A assert key is not None assert isinstance(key, tuple) - Entity = tcod.ecs.entity.Entity + Entity = tcod.ecs.entity.Entity # noqa: N806 entities = list(self.all_of(components=set(key) - {Entity}).get_entities()) entity_components = [] diff --git a/tcod/ecs/registry.py b/tcod/ecs/registry.py index 2841f84..eab2ecd 100644 --- a/tcod/ecs/registry.py +++ b/tcod/ecs/registry.py @@ -4,14 +4,16 @@ import warnings from collections import defaultdict -from typing import Any, DefaultDict, Dict, Iterable, Mapping, NoReturn, Set, TypeVar +from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Final, Iterable, Mapping, NoReturn, Set, TypeVar import attrs import tcod.ecs._converter import tcod.ecs.query from tcod.ecs.entity import Entity -from tcod.ecs.typing import ComponentKey, _RelationTargetLookup + +if TYPE_CHECKING: + from tcod.ecs.typing import ComponentKey, _RelationTargetLookup _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") @@ -178,7 +180,7 @@ def __setstate__(self, state: dict[str, Any]) -> None: global_: Entity | None = state.pop("global_", None) # Migrate from version <=1.2.0 # These attributes contain redundant data and will be removed - REDUNDANT_ATTRIBUTES = frozenset( + redundant_attributes: Final = frozenset( { "_components_by_entity", # <=3.4.0 "_tags_by_key", # <=3.4.0 @@ -186,7 +188,7 @@ def __setstate__(self, state: dict[str, Any]) -> None: "_names_by_entity", # <=3.4.0 } ) - for ignored in REDUNDANT_ATTRIBUTES: + for ignored in redundant_attributes: state.pop(ignored, None) converter = tcod.ecs._converter._get_converter() @@ -307,7 +309,7 @@ def new_entity( return entity @property - def Q(self) -> tcod.ecs.query.BoundQuery: + def Q(self) -> tcod.ecs.query.BoundQuery: # noqa: N802 """Start a new Query for this registry. Alias for ``tcod.ecs.Query(registry)``. diff --git a/tcod/ecs/typing.py b/tcod/ecs/typing.py index bb578ec..6a52595 100644 --- a/tcod/ecs/typing.py +++ b/tcod/ecs/typing.py @@ -31,7 +31,7 @@ _RelationQueryTarget: TypeAlias = Union[_RelationTargetLookup, BoundQuery] """Possible target for relation queries.""" -_RelationQuery: TypeAlias = Union[Tuple[object, _RelationQueryTarget], Tuple[_RelationQueryTarget, object, None]] +_RelationQuery: TypeAlias = Union[Tuple[object, _RelationQueryTarget], Tuple[_RelationQueryTarget, object, None]] # noqa: PYI047 """Query format for relations. One of 4 formats: diff --git a/tcod/ecs/world.py b/tcod/ecs/world.py index f0bac7d..efd45b7 100644 --- a/tcod/ecs/world.py +++ b/tcod/ecs/world.py @@ -1,4 +1,6 @@ # noqa: D100 +from __future__ import annotations + __all__ = ("World",) from tcod.ecs.registry import Registry as World diff --git a/tests/test_traversal.py b/tests/test_traversal.py index 0cc555b..70b4b42 100644 --- a/tests/test_traversal.py +++ b/tests/test_traversal.py @@ -1,5 +1,7 @@ """Inheritance tests.""" +from __future__ import annotations + from typing import Final import pytest @@ -83,8 +85,8 @@ def test_component_traversal_alternate() -> None: def test_multiple_inheritance() -> None: world = Registry() - ViaA: Final = object() - ViaC: Final = object() + ViaA: Final = object() # noqa: N806 + ViaC: Final = object() # noqa: N806 world["A"].components[str] = "A" world["B"].components[str] = "B" world["B"].components[int] = 0 From 2e24ccc4c5abd2bf0a3ed5e536f85fc08f40aa8b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 18 Jul 2024 10:30:35 -0700 Subject: [PATCH 35/83] Update relation query explanations and examples --- .vscode/settings.json | 1 + README.md | 58 ++++++++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e55f8f2..bb00495 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "libtcod", "liskin", "maxdepth", + "Mertens", "modindex", "Mypy", "Numpad", diff --git a/README.md b/README.md index 641db2c..8397e73 100644 --- a/README.md +++ b/README.md @@ -207,13 +207,45 @@ True ## Relations -Use `Entity.relation_components[component_key][target] = component` to associate a target entity with a component. -Use `Entity.relation_tag[tag] = target` to associate a tag exclusively with a target entity. -Use `Entity.relation_tags_many[tag].add(target)` to associate a tag with multiple targets. +Entity relations are unidirectional from an origin entity to possibly multiple target entities. + +- Use `origin.relation_tag[tag] = target` to associate an origins tag exclusively with the target entity. + This uses standard assignment and is useful for tags which would not make sense with multiple targets. + Reading `origin.relation_tag[tag]` returns a single target while enforcing the invariant of only having one target. +- Use `origin.relation_tags_many[tag].add(target)` to associate a tag with multiple targets. + This supports `set`-like syntax such as adding or removing multiple targets at once. + This allows for many-to-many relations. +- Use `origin.relation_components[component_key][target] = component` to associate a target entity with a component. + This allows storing data along with a relation. + This supports `dict`-like syntax. + The `component_key` can be queried like a normal tag. -Relation queries are a little more complex than other queries. -Relation tags and relation components share the same space then queried, so 'normal' tags should not be in the format of a component key. -Relations are unidirectional, but you can query either end of a relation. +### Relation queries + +Relations are queried with `registry.Q.all_of(relations=[...])`. +This expects 2-item or 3-item tuples following these rules: + +- Use `(tag, target)` to match the origin entities with the relation `tag` to `target`. +- If `tag` is a component key then component relations are also matched. + This means you should be careful with tags which look like component keys. +- `target` can be a specific entity. This means only entities relating to that specific entity will be matched. +- `target` can be query itself. This means only entities relating to a match from the sub-query are matched. +- `target` can be `...` which means an entity with a relation to any entity is matched. +- To reverse the direction use a 3-item tuple `(origin, tag, None)`. `origin` can be anything a `target` could be. + +Relations using sub-queries may be chained together. +See [Sander Mertens - Why it is time to start thinking of games as databases](https://ajmmertens.medium.com/why-it-is-time-to-start-thinking-of-games-as-databases-e7971da33ac3) to understand the repercussion of this. + +You can use the following table to help with constructing relation queries: + +| Matches | Syntax | +| ------------------------------------------------------------------- | :--------------------------------------: | +| Origins with a relation `tag` to `target_entity` | `(tag, target_entity)` | +| Origins with a relation `tag` to any target entity | `(tag, ...)` (Literal dot-dot-dot) | +| Origins with a relation `tag` to any targets matching a sub-query | `(tag, registry.Q.all_of(...))` | +| Targets of the relation `tag` from `origin_entity` | `(origin_entity, tag, None)` | +| Targets of the relation `tag` from any origin entity | `(..., tag, None)` (Literal dot-dot-dot) | +| Targets of the relation `tag` from any origins matching a sub-query | `(registry.Q.all_of(...), tag, None)` | ```py >>> @attrs.define @@ -243,17 +275,3 @@ True True ``` - -### Relation queries - -You can use the following table to help with constructing relation queries. -`tag` is a component key if you are querying for a component relation. - -| Includes | Syntax | -| ------------------------------------------------------------------- | :--------------------------------------: | -| Entities with a relation tag to the given target | `(tag, target_entity)` | -| Entities with a relation tag to any target | `(tag, ...)` (Literal dot-dot-dot) | -| Entities with a relation tag to the targets in the given query | `(tag, registry.Q.all_of(...))` | -| The target entities of a relation of a given entity | `(origin_entity, tag, None)` | -| The target entities of any entity with the given relation tag | `(..., tag, None)` (Literal dot-dot-dot) | -| The target entities of the queried entities with the given relation | `(tag, registry.Q.all_of(...))` | From 42c43312d5f00b7ad9a3e3091322b4af962f2cca Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 18 Jul 2024 11:16:46 -0700 Subject: [PATCH 36/83] Update pre-commit --- .pre-commit-config.yaml | 2 +- tcod/ecs/query.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3543878..9675e42 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.1 + rev: v0.5.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index a072c76..1251edc 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -366,7 +366,7 @@ def __as_queries( yield from (_QueryTraversalPropagation(_QueryTag(tag), traverse, depth) for tag in tags) yield from (_QueryTraversalPropagation(_QueryRelation(relations), traverse, depth) for relations in relations) - def all_of( # noqa: PLR0913 + def all_of( self, components: Iterable[ComponentKey[object]] = (), *, @@ -383,7 +383,7 @@ def all_of( # noqa: PLR0913 & self._query, ) - def none_of( # noqa: PLR0913 + def none_of( self, components: Iterable[ComponentKey[object]] = (), *, From 654ed037faaa385302cae4ed403e818af60977fc Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 Jul 2024 22:27:21 -0700 Subject: [PATCH 37/83] Add `__bool__` to queries --- CHANGELOG.md | 4 ++++ tcod/ecs/query.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ac5db5..a5d9db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Queries are now truthy if they match any entity. + ## [5.1.0] - 2024-02-13 ### Changed diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index 1251edc..58f2012 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -352,6 +352,10 @@ def get_entities(self) -> AbstractSet[Entity]: """ return _get_query(self.registry, self._query) + def __bool__(self) -> bool: + """Return True if any entity matches this query.""" + return bool(self.get_entities()) + @staticmethod def __as_queries( components: Iterable[ComponentKey[object]] = (), From 728f2104c9412fb0f5e84a0d58d59df26504132b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 Jul 2024 22:30:06 -0700 Subject: [PATCH 38/83] pre-commit update --- .pre-commit-config.yaml | 2 +- .vscode/settings.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9675e42..7c27b32 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.3 + rev: v0.5.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/.vscode/settings.json b/.vscode/settings.json index bb00495..44cd0a7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "files.trimTrailingWhitespace": true, "cSpell.words": [ "automodule", + "autoupdate", "autouse", "Benesch", "cattrs", From 00fb7f4673c36ad9991bd6f1cd9bd94a9dc9dd2b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 Jul 2024 22:33:41 -0700 Subject: [PATCH 39/83] Ignore hint on assert --- tcod/ecs/_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/ecs/_converter.py b/tcod/ecs/_converter.py index 4ca5c9f..aea85dd 100644 --- a/tcod/ecs/_converter.py +++ b/tcod/ecs/_converter.py @@ -14,7 +14,7 @@ def _is_defaultdict_type(type_hint: object) -> bool: def _setup_defaultdict_factory(type_hint: type[defaultdict[Any, Any] | object]) -> Callable[[], Any]: """Return the factory value for a defaultdict given its value type-hint.""" - assert type_hint is not Any + assert type_hint is not Any # type: ignore[comparison-overlap] if get_origin(type_hint) is not defaultdict: return get_origin(type_hint) or type_hint return functools.partial(defaultdict, _setup_defaultdict_factory(get_args(type_hint)[1])) From 7111d82b528d4fe0625150ebf4c6a840e4753cd4 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 Jul 2024 22:47:17 -0700 Subject: [PATCH 40/83] Update and cleanup workflow --- .github/workflows/python-package.yml | 52 +++++++++++++++++++--------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 1fbac88..1f190ce 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,6 +5,10 @@ name: Package on: push: + branches: + - "*" + tags: + - "*.*.*" pull_request: types: [opened, reopened] @@ -16,7 +20,7 @@ jobs: ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Ruff run: pip install ruff - name: Ruff Check @@ -24,70 +28,86 @@ jobs: - name: Ruff Format run: ruff format . --check - - test: + mypy: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.12"] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 name: Setup Python ${{ matrix.python-version }} with: python-version: ${{ matrix.python-version }} - name: Install package run: pip install -e ".[test]" - name: Mypy - uses: liskin/gh-problem-matcher-wrap@v2 + uses: liskin/gh-problem-matcher-wrap@v3 with: linters: mypy run: mypy --show-column-numbers --python-version ${{ matrix.python-version }} + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13-dev"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + name: Setup Python ${{ matrix.python-version }} + with: + python-version: ${{ matrix.python-version }} + - name: Install package + run: pip install -e ".[test]" - name: Run tests run: pytest --cov-report=xml - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} build-dist: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install build run: pip install build - name: Build package run: python -m build - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: python-dist path: dist/* if-no-files-found: error retention-days: 1 + compression-level: 0 deploy: needs: [build-dist] - if: startsWith(github.ref, 'refs/tags/') + if: github.ref_type == 'tag' runs-on: ubuntu-latest environment: - name: release - url: https://pypi.org/p/tcod-ecs + name: pypi + url: https://pypi.org/project/tcod-ecs/${{ github.ref_name }}/ permissions: id-token: write steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: python-dist path: dist/ - uses: pypa/gh-action-pypi-publish@release/v1 release: - if: startsWith(github.ref, 'refs/tags/') + if: github.ref_type == 'tag' name: Create Release runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Generate body run: scripts/get_release_description.py | tee release_body.md - name: Create Release From 795d76d38278f806daa20b7a51473e91bc2eaba6 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 Jul 2024 22:53:15 -0700 Subject: [PATCH 41/83] Test query truthiness --- tests/test_ecs.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_ecs.py b/tests/test_ecs.py index e5daddc..9ecd372 100644 --- a/tests/test_ecs.py +++ b/tests/test_ecs.py @@ -275,3 +275,10 @@ def test_entity_clear() -> None: def test_world_iter() -> None: with pytest.raises(TypeError, match=r"is not iterable"): iter(tcod.ecs.Registry()) # Not iterable for now, maybe later + + +def test_world_query_bool() -> None: + world = tcod.ecs.Registry() + assert not world.Q.all_of(tags=["Foo"]) + world[None].tags.add("Foo") + assert world.Q.all_of(tags=["Foo"]) From 31b4f2c364712cc985d99b02bc052820b8150b3f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 Jul 2024 23:06:22 -0700 Subject: [PATCH 42/83] Fix deprecated VSCode launch settings --- .vscode/launch.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ce2253e..8be899d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,14 +6,14 @@ "configurations": [ { "name": "Python: Current File", - "type": "python", + "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", }, { "name": "Python: Run tests", - "type": "python", + "type": "debugpy", "request": "launch", "module": "pytest", "preLaunchTask": "develop tcod-ecs", From 8f1e90c0fabcc98278587ebad40b905eb5623b5e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 Jul 2024 23:07:18 -0700 Subject: [PATCH 43/83] Prepare 5.2.0 release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5d9db0..4cd9621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.2.0] - 2024-07-22 + ### Changed - Queries are now truthy if they match any entity. From b471eca0afaf1cde3d3f2c0f64d5fb9e9423bda7 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 Jul 2024 23:16:57 -0700 Subject: [PATCH 44/83] Fix type of get default Was returning None for non-None defaults. --- CHANGELOG.md | 4 ++++ tcod/ecs/entity.py | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cd9621..a51a432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fixed type of default parameter for `EntityComponents.get`. + ## [5.2.0] - 2024-07-22 ### Changed diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index a611b15..73bbb7e 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -529,12 +529,17 @@ def __ior__( self.update(value) return self - def get(self, __key: ComponentKey[T], __default: T | None = None) -> T | None: + @overload + def get(self, __key: ComponentKey[T]) -> T | None: ... + @overload + def get(self, __key: ComponentKey[T], __default: _T1) -> T | _T1: ... + + def get(self, __key: ComponentKey[T], __default: _T1 | None = None) -> T | _T1: """Return a component, returns None or a default value when the component is missing.""" try: return self[__key] except KeyError: - return __default + return __default # type: ignore[return-value] # https://github.com/python/mypy/issues/3737 def setdefault(self, __key: ComponentKey[T], __default: T) -> T: # type: ignore[override] """Assign a default value if a component is missing, then returns the current value.""" From 6f4cd366efb1abca1174acabfa413223e4064825 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 30 Jul 2024 15:54:50 -0700 Subject: [PATCH 45/83] Prepare 5.2.1 release Remove dunders from `__all__`. Update pre-commit --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 2 ++ tcod/ecs/__init__.py | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7c27b32..4e50cea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.4 + rev: v0.5.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/CHANGELOG.md b/CHANGELOG.md index a51a432..30b730d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.2.1] - 2024-07-30 + ### Fixed - Fixed type of default parameter for `EntityComponents.get`. diff --git a/tcod/ecs/__init__.py b/tcod/ecs/__init__.py index f8e9376..e2146a3 100644 --- a/tcod/ecs/__init__.py +++ b/tcod/ecs/__init__.py @@ -12,7 +12,6 @@ from tcod.ecs.registry import Registry as World __all__ = ( - "__version__", "Entity", "IsA", "Registry", From a08541024ef081537c2ffd80e356357033286149 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 2 Aug 2024 12:05:46 -0700 Subject: [PATCH 46/83] Fix EntityComponents.pop Update docs for instantiate --- CHANGELOG.md | 4 +++ tcod/ecs/entity.py | 65 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b730d..0a14d06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `EntityComponents.pop` now correctly returns defaults when the components are inherited instead of local. + ## [5.2.1] - 2024-07-30 ### Fixed diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 73bbb7e..5f4d509 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -22,6 +22,7 @@ from weakref import WeakKeyDictionary, WeakValueDictionary import attrs +from sentinel_value import sentinel from typing_extensions import Self import tcod.ecs.callbacks @@ -41,6 +42,8 @@ _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") +_raise: Final = sentinel("_raise") + _entity_table: WeakKeyDictionary[Registry, WeakValueDictionary[object, Entity]] = WeakKeyDictionary() """A weak table of registries and unique identifiers to entity objects. @@ -134,6 +137,10 @@ def instantiate(self) -> Self: This creates a new unique entity and assigns an :any:`IsA` relationship with `self` to the new entity. The :any:`IsA` relation is the only data this new entity directly holds. + Immutable components inherited from the parent are always copy-on-write for its child instances. + + Keep in mind that components/tags/relations inherited from the parent are not removable from the child instance. + Example:: # 'child = entity.instantiate()' is equivalent to the following: @@ -149,14 +156,18 @@ def instantiate(self) -> Self: >>> child.components[str] # Inherits components from parent 'baz' >>> parent.components[str] = "foo" - >>> child.components[str] # Changes in parent and reflected in children + >>> child.components[str] # Changes in parent are reflected in children 'foo' >>> child.components[str] += "bar" # In-place assignment operators will copy-on-write immutable objects >>> child.components[str] 'foobar' >>> parent.components[str] 'foo' - >>> del child.components[str] + >>> child.components.pop(str, None) # Revert the component to the inherited value + 'foobar' + >>> child.components[str] + 'foo' + >>> child.components.pop(str, None) # Safe to call .pop with default when the value isn't set on the child >>> child.components[str] 'foo' @@ -530,16 +541,16 @@ def __ior__( return self @overload - def get(self, __key: ComponentKey[T]) -> T | None: ... + def get(self, __key: ComponentKey[T], /) -> T | None: ... @overload - def get(self, __key: ComponentKey[T], __default: _T1) -> T | _T1: ... + def get(self, __key: ComponentKey[T], /, default: _T1) -> T | _T1: ... - def get(self, __key: ComponentKey[T], __default: _T1 | None = None) -> T | _T1: + def get(self, __key: ComponentKey[T], /, default: _T1 | None = None) -> T | _T1: """Return a component, returns None or a default value when the component is missing.""" try: return self[__key] except KeyError: - return __default # type: ignore[return-value] # https://github.com/python/mypy/issues/3737 + return default # type: ignore[return-value] # https://github.com/python/mypy/issues/3737 def setdefault(self, __key: ComponentKey[T], __default: T) -> T: # type: ignore[override] """Assign a default value if a component is missing, then returns the current value.""" @@ -549,6 +560,48 @@ def setdefault(self, __key: ComponentKey[T], __default: T) -> T: # type: ignore self[__key] = __default return __default + @overload + def pop(self, __key: ComponentKey[T], /) -> T | None: ... + @overload + def pop(self, __key: ComponentKey[T], /, default: _T1) -> T | _T1: ... + + def pop( + self, + __key: ComponentKey[T], + /, + default: _T1 = _raise, # type: ignore[assignment] # https://github.com/python/mypy/issues/3737 + ) -> T | _T1: + """Remove a component directly from this entity. + + Returns the removed value. + If the value is missing returns `default`. + If `default` is unset then raises :any:`KeyError` instead. + + Operates directly on the entity without traversal. + + >>> parent = registry[object()] + >>> parent.components[str] = "foo" + >>> child = parent.instantiate() + >>> child.components[str] = "bar" + >>> child.components.pop(str, None) + 'bar' + >>> child.components.pop(str, None) + >>> child.components[str] + 'foo' + >>> child.components.pop(str) + Traceback (most recent call last): + ... + KeyError: + """ + _components = self.entity.registry._components_by_entity.get(self.entity, {}) + if __key not in _components: + if default is _raise: + raise KeyError(__key) + return default + value: T | _T1 = _components[__key] + del self[__key] + return value + @attrs.define(eq=False, frozen=True, weakref_slot=False) class EntityTags(MutableSet[Any]): From f479955443d90e7fcc247fdd90d485310c851b9a Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 2 Aug 2024 12:07:21 -0700 Subject: [PATCH 47/83] pre-commit update --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4e50cea..d86ddee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.5 + rev: v0.5.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 60e92962f870ec5882371a352ce59221f18e3d27 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 3 Aug 2024 11:34:26 -0700 Subject: [PATCH 48/83] Prepare 5.2.2 release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a14d06..805ef08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.2.2] - 2024-08-03 + ### Fixed - `EntityComponents.pop` now correctly returns defaults when the components are inherited instead of local. From 13d43bb69092cb929688fe73887194e2f2d2c1d9 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 19 Aug 2024 19:31:00 -0700 Subject: [PATCH 49/83] Fix tag clear hang --- CHANGELOG.md | 4 ++++ tcod/ecs/entity.py | 6 ++++++ tests/test_traversal.py | 10 ++++++++++ 3 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 805ef08..e59a6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Clearing an entity with inherited tags no longer hangs. + ## [5.2.2] - 2024-08-03 ### Fixed diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 5f4d509..75ba3c4 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -650,6 +650,12 @@ def remove(self, tag: object) -> None: raise KeyError(tag) self.discard(tag) + def clear(self) -> None: + """Remove all tags directly in this entity.""" + _tags_by_entity = self.entity.registry._tags_by_entity + while _tags_by_entity[self.entity]: + self.discard(next(iter(_tags_by_entity[self.entity]))) + def __contains__(self, x: object) -> bool: """Return True if this entity has the given tag.""" _tags_by_entity = self.entity.registry._tags_by_entity diff --git a/tests/test_traversal.py b/tests/test_traversal.py index 70b4b42..eccbe28 100644 --- a/tests/test_traversal.py +++ b/tests/test_traversal.py @@ -205,3 +205,13 @@ def test_relation_traversal() -> None: assert len(world["C"].relation_components[str]) == 2 # noqa: PLR2004 world["C"].relation_components[int][world["foo"]] = 0 assert set(world["C"].relation_components) == {str, int} + + +def test_inherited_clear() -> None: + world = Registry() + world["A"].components[int] = 1 + world["A"].tags.add("foo") + world["A"].relation_components[str][world["B"]] = "bar" + world["A"].relation_tags["baz"] = world["B"] + child = world["A"].instantiate() + child.clear() # Could hang if broken From a56445a1449977add85e651d6b17a3b519a029d8 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 20 Aug 2024 14:19:14 -0700 Subject: [PATCH 50/83] Prepare 5.2.3 release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e59a6ec..f041d24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.2.3] - 2024-08-20 + ### Fixed - Clearing an entity with inherited tags no longer hangs. From 81b801b3f3fc40ee43f4c1e2861c9d7095649d02 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 7 Mar 2025 09:49:32 -0800 Subject: [PATCH 51/83] General cleanup Ignore Pylance type errors which conflict with Mypy Refine types to be more correct --- .pre-commit-config.yaml | 4 ++-- .vscode/settings.json | 13 +++++++++++++ pyproject.toml | 10 ++++++++-- tcod/ecs/entity.py | 8 ++++---- tcod/ecs/typing.py | 2 +- tests/test_traversal.py | 2 ++ 6 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d86ddee..e913327 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: autoupdate_schedule: quarterly repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.9.10 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/.vscode/settings.json b/.vscode/settings.json index 44cd0a7..ae122bd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,12 +4,16 @@ "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "cSpell.words": [ + "addopts", "automodule", "autoupdate", "autouse", "Benesch", + "buildapi", "cattrs", "codecov", + "docstrings", + "doctest", "doctests", "dtype", "furo", @@ -22,19 +26,28 @@ "liskin", "maxdepth", "Mertens", + "minversion", "modindex", "Mypy", + "ncipollo", "Numpad", "PAGEDOWN", "PAGEUP", "pickleable", + "pydocstyle", + "pypa", + "pypi", + "pyright", "pytest", "quickstart", "RMASK", "rtype", "scancode", "setdefault", + "setuptools", + "subclassing", "tcod", + "testpaths", "toctree", "Traceback", "typehints", diff --git a/pyproject.toml b/pyproject.toml index 0597f43..f1bf5ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,14 @@ warn_return_any = true no_implicit_reexport = true strict_equality = true +[tool.pyright] +reportInconsistentOverload = false +reportIncompatibleMethodOverride = false +reportAssignmentType = false +reportCallIssue = false +reportInvalidTypeVarUse = false +reportArgumentType = false + [tool.pytest.ini_options] minversion = "6.0" required_plugins = ["pytest-cov>=4.0.0", "pytest-benchmark>=4.0.0"] @@ -81,8 +89,6 @@ line-length = 120 [tool.ruff.lint] # https://docs.astral.sh/ruff/rules/ select = ["ALL"] ignore = [ - "ANN101", # missing-type-self - "ANN102", # missing-type-cls "COM", # flake8-commas, handled by formatter "E501", # line-too-long "S101", # assert diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 75ba3c4..b380289 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -66,7 +66,7 @@ class Entity: >>> other_entity = registry["other"] """ # Changes here should be reflected in conftest.py - __slots__ = ("registry", "uid", "__weakref__") + __slots__ = ("__weakref__", "registry", "uid") registry: Final[Registry] # type:ignore[misc] # https://github.com/python/mypy/issues/5774 """The :any:`Registry` this entity belongs to.""" @@ -394,7 +394,7 @@ def _traverse_entities(start: Entity, traverse_parents: tuple[object, ...]) -> I @attrs.define(eq=False, frozen=True, weakref_slot=False) -class EntityComponents(MutableMapping[Union[Type[Any], Tuple[object, Type[Any]]], Any]): +class EntityComponents(MutableMapping[Union[Type[Any], Tuple[object, Type[Any]]], object]): """A proxy attribute to access an entities components like a dictionary. See :any:`Entity.components`. @@ -552,7 +552,7 @@ def get(self, __key: ComponentKey[T], /, default: _T1 | None = None) -> T | _T1: except KeyError: return default # type: ignore[return-value] # https://github.com/python/mypy/issues/3737 - def setdefault(self, __key: ComponentKey[T], __default: T) -> T: # type: ignore[override] + def setdefault(self, __key: ComponentKey[T], __default: T, /) -> T: """Assign a default value if a component is missing, then returns the current value.""" try: return self[__key] @@ -1040,7 +1040,7 @@ def __getitem__(self, key: ComponentKey[T]) -> EntityComponentRelationMapping[T] """Access relations for this component key as a `{target: component}` dict-like object.""" return EntityComponentRelationMapping(self.entity, key, self.traverse) - def __setitem__(self, __key: ComponentKey[T], __values: Mapping[Entity, object]) -> None: + def __setitem__(self, __key: ComponentKey[T], __values: Mapping[Entity, object], /) -> None: """Redefine the component relations for this entity. ..versionadded:: 4.2.0 diff --git a/tcod/ecs/typing.py b/tcod/ecs/typing.py index 6a52595..50139a6 100644 --- a/tcod/ecs/typing.py +++ b/tcod/ecs/typing.py @@ -1,4 +1,4 @@ -"""Common type-hints for tcod.ecs.""" +"""Common type-hints for tcod.ecs.""" # noqa: A005 from __future__ import annotations diff --git a/tests/test_traversal.py b/tests/test_traversal.py index eccbe28..193c0cd 100644 --- a/tests/test_traversal.py +++ b/tests/test_traversal.py @@ -215,3 +215,5 @@ def test_inherited_clear() -> None: world["A"].relation_tags["baz"] = world["B"] child = world["A"].instantiate() child.clear() # Could hang if broken + x = child.components + x[int] = "asd" From e438d903d96b06b2fc96cc3185dcd8d4a3fb9ca8 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 7 Mar 2025 09:52:19 -0800 Subject: [PATCH 52/83] Don't traverse attributes on Entity.clear This causes confusion for the clear method and causes data to be missed instead of removed. --- CHANGELOG.md | 4 ++++ tcod/ecs/entity.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f041d24..ac89e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Clearing an entity with inherited components no longer leaves the entity with missed components. + ## [5.2.3] - 2024-08-20 ### Fixed diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index b380289..81aa86e 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -126,10 +126,10 @@ def clear(self) -> None: .. versionadded:: 4.2.0 """ - self.components.clear() - self.tags.clear() - self.relation_tags_many.clear() - self.relation_components.clear() + self.components(traverse=()).clear() + self.tags(traverse=()).clear() + self.relation_tags_many(traverse=()).clear() + self.relation_components(traverse=()).clear() def instantiate(self) -> Self: """Return a new entity which inherits the components, tags, and relations of this entity. From 3e36748bf5964152bd15071406f075d74cfc0928 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 7 Mar 2025 09:55:17 -0800 Subject: [PATCH 53/83] Prepare 5.2.4 release --- CHANGELOG.md | 2 ++ LICENSE | 2 +- README.md | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac89e66..7f2fd3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.2.4] - 2025-03-07 + ### Fixed - Clearing an entity with inherited components no longer leaves the entity with missed components. diff --git a/LICENSE b/LICENSE index 22c0179..a65ed3f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2023-2024 Kyle Benesch +Copyright (c) 2023-2025 Kyle Benesch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8397e73..451bcf5 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The ECS Registry is used to create and store entities and their components. ## Entity Each Entity is identified by its unique id (`uid`) which can be any hashable object combined with the `registry` it belongs. -New unique entities can be created with `Registry.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desireable. +New unique entities can be created with `Registry.new_entity` which uses a new `object()` as the `uid`, this guarantees uniqueness which is not always desirable. An entity always knows about its assigned registry, which can be accessed with the `Entity.registry` property from any Entity instance. Registries only know about their entities once the entity is assigned a name, component, tag, or relation. From 3339ef3ba90447880cad33203c03d5ae09ab39b6 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 8 Mar 2025 09:58:20 -0800 Subject: [PATCH 54/83] Add query any_of method --- CHANGELOG.md | 4 ++++ tcod/ecs/query.py | 24 ++++++++++++++++++++++++ tests/test_ecs.py | 8 ++++++++ 3 files changed, 36 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f2fd3f..4513b16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- New query `.any_of` method. This was possible before but it is easier with this method. + ## [5.2.4] - 2025-03-07 ### Fixed diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index 58f2012..8e0391f 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -404,6 +404,30 @@ def none_of( & self._query, ) + def any_of( + self, + components: Iterable[ComponentKey[object]] = (), + *, + tags: Iterable[object] = (), + relations: Iterable[_RelationQuery] = (), + traverse: Iterable[object] = (IsA,), + depth: int | None = None, + ) -> Self: + """Filter entities based on having at least one of the provided elements. + + .. versionadded:: Unreleased + """ + _check_suspicious_tags(tags, stacklevel=2) + return self.__class__( + self.registry, + _QueryLogicalAnd( + all_of=frozenset( + [_QueryLogicalOr(any_of=frozenset(self.__as_queries(components, tags, relations, traverse, depth)))] + ) + ) + & self._query, + ) + def __iter__(self) -> Iterator[Entity]: """Iterate over the matching entities.""" return iter(self.get_entities()) diff --git a/tests/test_ecs.py b/tests/test_ecs.py index 9ecd372..8f288a8 100644 --- a/tests/test_ecs.py +++ b/tests/test_ecs.py @@ -282,3 +282,11 @@ def test_world_query_bool() -> None: assert not world.Q.all_of(tags=["Foo"]) world[None].tags.add("Foo") assert world.Q.all_of(tags=["Foo"]) + + +def test_any_of() -> None: + world = tcod.ecs.Registry() + world[None].tags.add("foo") + assert world.Q.any_of(tags=["foo"]) + assert world.Q.any_of(tags=["foo", "bar"]) + assert not world.Q.any_of(tags=["bar"]) From 08b2b21259339e22f086c1478c0ca84505bc0527 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 8 Mar 2025 11:09:07 -0800 Subject: [PATCH 55/83] Prepare 5.3.0 release --- CHANGELOG.md | 2 ++ tcod/ecs/entity.py | 2 +- tcod/ecs/query.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4513b16..aba6d8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.3.0] - 2025-03-08 + ### Added - New query `.any_of` method. This was possible before but it is easier with this method. diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 81aa86e..ba2a135 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -77,7 +77,7 @@ class Entity: def world(self) -> Registry: """Deprecated alias for registry. - .. deprecated:: Unreleased + .. deprecated:: 5.1 Use :any:`registry` instead. """ if __debug__: diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index 8e0391f..6641a4b 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -336,7 +336,7 @@ class BoundQuery: def world(self) -> Registry: """Deprecated alias for registry. - .. deprecated:: Unreleased + .. deprecated:: 5.1 Use :any:`registry` instead. """ if __debug__: @@ -415,7 +415,7 @@ def any_of( ) -> Self: """Filter entities based on having at least one of the provided elements. - .. versionadded:: Unreleased + .. versionadded:: 5.3 """ _check_suspicious_tags(tags, stacklevel=2) return self.__class__( From 7a559b94827abf4b0447008c77f71722caab436b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 18 Mar 2025 12:13:34 -0700 Subject: [PATCH 56/83] Update deprecations to use PEP 702 --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- tcod/ecs/entity.py | 34 +++++++++++++--------------------- tcod/ecs/query.py | 5 ++--- tcod/ecs/registry.py | 14 +++++++------- tests/test_traversal.py | 3 ++- 6 files changed, 29 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aba6d8b..2b6d084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Updated deprecations to use [PEP 702](https://peps.python.org/pep-0702/). + ## [5.3.0] - 2025-03-08 ### Added diff --git a/pyproject.toml b/pyproject.toml index f1bf5ec..429d8fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "attrs >=23.1.0", "cattrs >=23.1.2", "sentinel-value >=1.0.0", - "typing-extensions >=4.4.0", + "typing-extensions >=4.9.0", ] [tool.setuptools_scm] diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index ba2a135..1eca96b 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -2,7 +2,6 @@ from __future__ import annotations -import warnings from typing import ( TYPE_CHECKING, Any, @@ -23,7 +22,7 @@ import attrs from sentinel_value import sentinel -from typing_extensions import Self +from typing_extensions import Self, deprecated import tcod.ecs.callbacks import tcod.ecs.query @@ -74,14 +73,13 @@ class Entity: """This entities unique identifier.""" @property + @deprecated("Use '.registry' instead of '.world'") def world(self) -> Registry: """Deprecated alias for registry. .. deprecated:: 5.1 Use :any:`registry` instead. """ - if __debug__: - warnings.warn("Use '.registry' instead of '.world'", DeprecationWarning, stacklevel=2) return self.registry def __new__(cls, registry: Registry, uid: object = object) -> Entity: # noqa: PYI034 @@ -284,13 +282,13 @@ def relation_tag(self) -> EntityRelationsExclusive: return EntityRelationsExclusive(self, (IsA,)) @property + @deprecated("The '.relation_tags' attribute has been renamed to '.relation_tag'", category=FutureWarning) def relation_tags(self) -> EntityRelationsExclusive: """Access an entities exclusive relations. .. deprecated:: 3.2 This attribute was renamed to :any:`relation_tag`. """ - warnings.warn("The '.relation_tags' attribute has been renamed to '.relation_tag'", FutureWarning, stacklevel=2) return EntityRelationsExclusive(self, (IsA,)) @property @@ -303,12 +301,8 @@ def relation_tags_many(self) -> EntityRelations: """ return EntityRelations(self, (IsA,)) - def _set_name(self, value: object, stacklevel: int = 1) -> None: - warnings.warn( - "The name feature has been deprecated and will be removed.", - FutureWarning, - stacklevel=stacklevel + 1, - ) + @deprecated("The name feature has been deprecated and will be removed.", category=FutureWarning) + def _set_name(self, value: object) -> None: old_name = self.name if old_name is not None: # Remove self from names del self.registry._names_by_name[old_name] @@ -333,8 +327,9 @@ def name(self) -> object: return self.registry._names_by_entity.get(self) @name.setter + @deprecated("The name feature has been deprecated and will be removed.", category=FutureWarning) def name(self, value: object) -> None: - self._set_name(value, stacklevel=2) + self._set_name(value) def __repr__(self) -> str: """Return a representation of this entity. @@ -410,17 +405,13 @@ def __call__(self, *, traverse: Iterable[object]) -> Self: """ return self.__class__(self.entity, tuple(traverse)) - def set(self, value: object, *, _stacklevel: int = 1) -> None: + @deprecated("Setting values without an explicit key has been deprecated.", category=FutureWarning) + def set(self, value: object) -> None: """Assign or overwrite a component, automatically deriving the key. .. deprecated:: 3.1 Setting values without an explicit key has been deprecated. """ - warnings.warn( - "Setting values without an explicit key has been deprecated.", - FutureWarning, - stacklevel=_stacklevel + 1, - ) key = value.__class__ self[key] = value @@ -498,15 +489,17 @@ def __len__(self) -> int: """Return the number of components belonging to this entity.""" return len(self.keys()) - def update_values(self, values: Iterable[object], *, _stacklevel: int = 1) -> None: + @deprecated("Setting values without an explicit key has been deprecated.", category=FutureWarning) + def update_values(self, values: Iterable[object]) -> None: """Add or overwrite multiple components inplace, deriving the keys from the values. .. deprecated:: 3.1 Setting values without an explicit key has been deprecated. """ for value in values: - self.set(value, _stacklevel=_stacklevel + 1) + self.set(value) + @deprecated("This method has been deprecated. Iterate over items instead.", category=FutureWarning) def by_name_type(self, name_type: type[_T1], component_type: type[_T2]) -> Iterator[tuple[_T1, type[_T2]]]: """Iterate over all of an entities component keys with a specific (name_type, component_type) combination. @@ -515,7 +508,6 @@ def by_name_type(self, name_type: type[_T1], component_type: type[_T2]) -> Itera .. deprecated:: 3.1 This method has been deprecated. Iterate over items instead. """ - warnings.warn("This method has been deprecated. Iterate over items instead.", FutureWarning, stacklevel=2) # Naive implementation until I feel like optimizing it for key in self: if not isinstance(key, tuple): diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index 6641a4b..0f8c381 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -9,7 +9,7 @@ from weakref import WeakKeyDictionary, WeakSet import attrs -from typing_extensions import Self +from typing_extensions import Self, deprecated import tcod.ecs.entity from tcod.ecs.constants import IsA @@ -333,14 +333,13 @@ class BoundQuery: _query: _Query = attrs.field(factory=_QueryLogicalAnd) @property + @deprecated("Use '.registry' instead of '.world'") def world(self) -> Registry: """Deprecated alias for registry. .. deprecated:: 5.1 Use :any:`registry` instead. """ - if __debug__: - warnings.warn("Use '.registry' instead of '.world'", DeprecationWarning, stacklevel=2) return self.registry def get_entities(self) -> AbstractSet[Entity]: diff --git a/tcod/ecs/registry.py b/tcod/ecs/registry.py index eab2ecd..d0a7d28 100644 --- a/tcod/ecs/registry.py +++ b/tcod/ecs/registry.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Final, Iterable, Mapping, NoReturn, Set, TypeVar import attrs +from typing_extensions import deprecated import tcod.ecs._converter import tcod.ecs.query @@ -151,6 +152,10 @@ class Registry: """ @property + @deprecated( + "The 'registry.global_' attribute has been deprecated. Use 'registry[None]' to access this entity.", + category=FutureWarning, + ) def global_(self) -> Entity: """A unique globally accessible entity. @@ -168,11 +173,6 @@ def global_(self) -> Entity: >>> registry[None].components[("turn", int)] 0 """ - warnings.warn( - "The 'registry.global_' attribute has been deprecated. Use 'registry[None]' to access this entity.", - FutureWarning, - stacklevel=2, - ) return Entity(self, None) def __setstate__(self, state: dict[str, Any]) -> None: @@ -300,12 +300,12 @@ def new_entity( if isinstance(components, Mapping): entity.components.update(components) elif components: - entity.components.update_values(components, _stacklevel=2) + entity.components.update_values(components) entity_tags = entity.tags for tag in tags: entity_tags.add(tag) if name is not None: - entity._set_name(name, stacklevel=2) + entity._set_name(name) return entity @property diff --git a/tests/test_traversal.py b/tests/test_traversal.py index 193c0cd..950a98f 100644 --- a/tests/test_traversal.py +++ b/tests/test_traversal.py @@ -212,7 +212,8 @@ def test_inherited_clear() -> None: world["A"].components[int] = 1 world["A"].tags.add("foo") world["A"].relation_components[str][world["B"]] = "bar" - world["A"].relation_tags["baz"] = world["B"] + with pytest.warns(): + world["A"].relation_tags["baz"] = world["B"] child = world["A"].instantiate() child.clear() # Could hang if broken x = child.components From aadcce4768da5cc3f10d9f16088d2d0bd7a0e181 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 18 Mar 2025 12:28:15 -0700 Subject: [PATCH 57/83] Update Ruff and pre-commit --- .github/workflows/python-package.yml | 9 ++------- .pre-commit-config.yaml | 2 +- .vscode/settings.json | 1 + pyproject.toml | 1 + tcod/ecs/typing.py | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 1f190ce..b935ab2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -17,16 +17,11 @@ defaults: shell: bash jobs: - ruff: + pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install Ruff - run: pip install ruff - - name: Ruff Check - run: ruff check . --output-format=github - - name: Ruff Format - run: ruff format . --check + - uses: pre-commit/action@v3.0.1 mypy: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e913327..d5f9b2f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.10 + rev: v0.11.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/.vscode/settings.json b/.vscode/settings.json index ae122bd..445fcc1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,6 +45,7 @@ "scancode", "setdefault", "setuptools", + "stdlib", "subclassing", "tcod", "testpaths", diff --git a/pyproject.toml b/pyproject.toml index 429d8fe..d522698 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ line-length = 120 [tool.ruff.lint] # https://docs.astral.sh/ruff/rules/ select = ["ALL"] ignore = [ + "A005", # stdlib-module-shadowing, workaround VSCode treating all modules as local "COM", # flake8-commas, handled by formatter "E501", # line-too-long "S101", # assert diff --git a/tcod/ecs/typing.py b/tcod/ecs/typing.py index 50139a6..6a52595 100644 --- a/tcod/ecs/typing.py +++ b/tcod/ecs/typing.py @@ -1,4 +1,4 @@ -"""Common type-hints for tcod.ecs.""" # noqa: A005 +"""Common type-hints for tcod.ecs.""" from __future__ import annotations From 3172128ddf0616357693997a95ff1a95b611c549 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 20:57:28 +0000 Subject: [PATCH 58/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.0...v0.11.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d5f9b2f..0f68af5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.0 + rev: v0.11.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From ea6d028edd7991d9b431564599a6a4dd5a5f3ad9 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 9 Apr 2025 13:44:01 -0700 Subject: [PATCH 59/83] Fix incorrect override for pop method This method could never return None because a missing key would raise instead, which is the expected behavior of the base class. --- tcod/ecs/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 1eca96b..981f5b0 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -553,7 +553,7 @@ def setdefault(self, __key: ComponentKey[T], __default: T, /) -> T: return __default @overload - def pop(self, __key: ComponentKey[T], /) -> T | None: ... + def pop(self, __key: ComponentKey[T], /) -> T: ... @overload def pop(self, __key: ComponentKey[T], /, default: _T1) -> T | _T1: ... From d48de23f6b775b6489235b280d6fc203a0a23935 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 9 Apr 2025 14:01:38 -0700 Subject: [PATCH 60/83] Fix type-hint lint warning wanting a Self return This function explicitly returns Entity which prevents a Self return, but it still allows Self to be added as a union to the return type. --- tcod/ecs/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 981f5b0..6e28126 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -82,7 +82,7 @@ def world(self) -> Registry: """ return self.registry - def __new__(cls, registry: Registry, uid: object = object) -> Entity: # noqa: PYI034 + def __new__(cls, registry: Registry, uid: object = object) -> Self | Entity: """Return a unique entity for the given `registry` and `uid`. If an entity already exists with a matching `registry` and `uid` then that entity is returned. From 098f9a17ab8293d125caa8092b402ce38deb5016 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 10 Apr 2025 19:47:27 -0700 Subject: [PATCH 61/83] Add and test clear methods for attributes --- CHANGELOG.md | 8 ++++++++ tcod/ecs/entity.py | 27 ++++++++++++++++++++++---- tests/test_relations.py | 43 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b6d084..49771c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Allow setting the `traverse` state of an `entity.component_tags[type][entity](traverse=...)` attribute. + ### Changed - Updated deprecations to use [PEP 702](https://peps.python.org/pep-0702/). +### Fixed + +- Fixed `.clear` methods for `entity.components` and `entity.component_relations`. + ## [5.3.0] - 2025-03-08 ### Added diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 6e28126..7575688 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -124,10 +124,10 @@ def clear(self) -> None: .. versionadded:: 4.2.0 """ - self.components(traverse=()).clear() - self.tags(traverse=()).clear() - self.relation_tags_many(traverse=()).clear() - self.relation_components(traverse=()).clear() + self.components.clear() + self.tags.clear() + self.relation_tags_many.clear() + self.relation_components.clear() def instantiate(self) -> Self: """Return a new entity which inherits the components, tags, and relations of this entity. @@ -594,6 +594,12 @@ def pop( del self[__key] return value + def clear(self) -> None: + """Remove any components stored directly in this entity.""" + if self.traverse: + return self(traverse=()).clear() + return super().clear() + @attrs.define(eq=False, frozen=True, weakref_slot=False) class EntityTags(MutableSet[Any]): @@ -942,6 +948,13 @@ def __attrs_post_init__(self) -> None: """Validate attributes.""" assert isinstance(self.entity, Entity), self.entity + def __call__(self, *, traverse: Iterable[object]) -> Self: + """Update this view with alternative parameters, such as a specific traversal relation. + + .. versionadded:: Unreleased + """ + return self.__class__(self.entity, self.key, tuple(traverse)) + def __getitem__(self, target: Entity) -> T: """Return the component related to a target entity.""" _relation_components_by_entity = self.entity.registry._relation_components_by_entity @@ -1001,6 +1014,12 @@ def __len__(self) -> int: """Return the count of targets for this component relation.""" return len(self.keys()) + def clear(self) -> None: + """Remove any components stored directly in this entity relation.""" + if self.traverse: + return self(traverse=()).clear() + return super().clear() + @attrs.define(eq=False, frozen=True, weakref_slot=False) class EntityComponentRelations(MutableMapping[ComponentKey[Any], EntityComponentRelationMapping[Any]]): diff --git a/tests/test_relations.py b/tests/test_relations.py index a758eeb..b0df2ea 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -168,3 +168,46 @@ def test_relation_tag_tables() -> None: assert set(w.Q.all_of(relations=[(..., "tag", None)])) == {e2} assert set(w.Q.all_of(relations=[(e1, "tag", None)])) == {e2} assert not set(w.Q.all_of(relations=[(e3, "tag", None)])) + + +def test_clear_with_relation() -> None: + world = tcod.ecs.Registry() + parent = world["parent"] + child = parent.instantiate() + entity_other = world[object()] + for i in range(5): + parent.components[(i, int)] = i + parent.tags.add(i) + parent.relation_components[(i, int)][entity_other] = i + parent.relation_tag[i] = entity_other + + for i in range(10): + child.components[(i, int)] = i + child.tags.add(i) + child.relation_components[(i, int)][entity_other] = i + child.relation_tag[i] = entity_other + + components_all = {(i, int) for i in range(10)} + components_after = {(i, int) for i in range(5)} + tags_all = set(range(10)) + tags_after = set(range(5)) + + assert child.components.keys() == components_all + assert child.relation_components.keys() == components_all + assert set(child.tags) == tags_all + assert set(child.relation_tag.keys()) == tags_all | {tcod.ecs.IsA} + + # Clear direct values, keeping values from parent as long as the IsA relation exists + child.components.clear() + assert child.components.keys() == components_after + child.relation_components.clear() + assert child.relation_components.keys() == components_after + child.tags.clear() + assert set(child.tags) == tags_after + + # Clearing relation_tags means breaking the IsA link to the parent + child.relation_tag.clear() + assert not child.relation_tag.keys() + assert not child.components.keys() + assert not child.tags + assert not child.relation_components.keys() From 0bbf97bd71372e547f0c05bb562517d736486d2e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 10 Apr 2025 20:07:46 -0700 Subject: [PATCH 62/83] Prepare 5.4.0 release --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 2 ++ tcod/ecs/entity.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f68af5..abddfd1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.4 + rev: v0.11.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/CHANGELOG.md b/CHANGELOG.md index 49771c1..f02f927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.4.0] - 2025-04-10 + ### Added - Allow setting the `traverse` state of an `entity.component_tags[type][entity](traverse=...)` attribute. diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 7575688..8e7c0f9 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -951,7 +951,7 @@ def __attrs_post_init__(self) -> None: def __call__(self, *, traverse: Iterable[object]) -> Self: """Update this view with alternative parameters, such as a specific traversal relation. - .. versionadded:: Unreleased + .. versionadded:: 5.4 """ return self.__class__(self.entity, self.key, tuple(traverse)) From 92cc6ab6c75c7ae2c8541fb1821c2f78aa9cf280 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 21 Apr 2025 16:32:20 -0700 Subject: [PATCH 63/83] Deprecate old World name at typing level --- tcod/ecs/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tcod/ecs/__init__.py b/tcod/ecs/__init__.py index e2146a3..5661153 100644 --- a/tcod/ecs/__init__.py +++ b/tcod/ecs/__init__.py @@ -4,12 +4,21 @@ import importlib.metadata from collections import defaultdict -from typing import TypeVar +from typing import TYPE_CHECKING, TypeVar from tcod.ecs.constants import IsA from tcod.ecs.entity import Entity from tcod.ecs.registry import Registry -from tcod.ecs.registry import Registry as World + +if TYPE_CHECKING: + from typing_extensions import deprecated + + @deprecated("Renamed to Registry.") + class World(Registry): # noqa: D101 + pass + +else: + from tcod.ecs.registry import Registry as World __all__ = ( "Entity", From fa77284cac00c41649c52472dd58f97e10dd4a17 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 20 Jul 2025 07:28:29 -0700 Subject: [PATCH 64/83] Pre-commit update --- .pre-commit-config.yaml | 4 ++-- conftest.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index abddfd1..c11409b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,8 +17,8 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.5 + rev: v0.12.4 hooks: - - id: ruff + - id: ruff-check args: [--fix, --exit-non-zero-on-fix] - id: ruff-format diff --git a/conftest.py b/conftest.py index 3d791ef..2ff6934 100644 --- a/conftest.py +++ b/conftest.py @@ -1,4 +1,4 @@ -# ruff: noqa: D100 D103 ANN401 +# ruff: noqa: D100 from __future__ import annotations from typing import Any From 4e540dd68ba81d8aa793afaf68ec470469eb59de Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 20 Jul 2025 07:39:19 -0700 Subject: [PATCH 65/83] Update type ignores for latest Mypy Requires Python 3.9 to run latest Mypy --- .github/workflows/python-package.yml | 5 +++-- tcod/ecs/_converter.py | 2 +- tcod/ecs/entity.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index b935ab2..f61b866 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.12"] + python-version: ["3.9", "3.12"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -46,7 +46,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13-dev"] + python-version: + ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/tcod/ecs/_converter.py b/tcod/ecs/_converter.py index aea85dd..4ca5c9f 100644 --- a/tcod/ecs/_converter.py +++ b/tcod/ecs/_converter.py @@ -14,7 +14,7 @@ def _is_defaultdict_type(type_hint: object) -> bool: def _setup_defaultdict_factory(type_hint: type[defaultdict[Any, Any] | object]) -> Callable[[], Any]: """Return the factory value for a defaultdict given its value type-hint.""" - assert type_hint is not Any # type: ignore[comparison-overlap] + assert type_hint is not Any if get_origin(type_hint) is not defaultdict: return get_origin(type_hint) or type_hint return functools.partial(defaultdict, _setup_defaultdict_factory(get_args(type_hint)[1])) diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 8e7c0f9..f7e4b9a 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -544,7 +544,7 @@ def get(self, __key: ComponentKey[T], /, default: _T1 | None = None) -> T | _T1: except KeyError: return default # type: ignore[return-value] # https://github.com/python/mypy/issues/3737 - def setdefault(self, __key: ComponentKey[T], __default: T, /) -> T: + def setdefault(self, __key: ComponentKey[T], __default: T, /) -> T: # type: ignore[override] # Disallows default None """Assign a default value if a component is missing, then returns the current value.""" try: return self[__key] From bb07cde7a5a1069fbaa969c4a7442861d6ca88d9 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 20 Jul 2025 08:13:36 -0700 Subject: [PATCH 66/83] Prepare 5.4.1 release --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f02f927..f1d8de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.4.1] - 2025-07-20 + +Maintenance release + ## [5.4.0] - 2025-04-10 ### Added From ef3698c43b792583d1da532d47e617720563afb8 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 20 Jul 2025 09:33:49 -0700 Subject: [PATCH 67/83] Drop Python 3.8 and 3.9 support Update code and type hints to use Python 3.10 --- .github/workflows/python-package.yml | 5 ++--- CHANGELOG.md | 4 ++++ pyproject.toml | 4 ++-- tcod/ecs/_converter.py | 5 ++++- tcod/ecs/callbacks.py | 7 +++---- tcod/ecs/entity.py | 11 ++--------- tcod/ecs/query.py | 5 +++-- tcod/ecs/registry.py | 19 ++++++++++--------- tcod/ecs/typing.py | 19 ++++++------------- tests/test_ecs.py | 2 +- 10 files changed, 37 insertions(+), 44 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f61b866..f69ecb7 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.12"] + python-version: ["3.10", "3.13"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -46,8 +46,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: - ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14-dev"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/CHANGELOG.md b/CHANGELOG.md index f1d8de6..194f317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- Dropped support for Python 3.8 & 3.9. + ## [5.4.1] - 2025-07-20 Maintenance release diff --git a/pyproject.toml b/pyproject.toml index d522698..658578f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ "Typing :: Typed", ] dynamic = ["version", "description"] -requires-python = ">=3.8" +requires-python = ">=3.10" dependencies = [ "attrs >=23.1.0", "cattrs >=23.1.2", @@ -34,7 +34,7 @@ test = [ "pytest >=7.2.0", "pytest-cov >=4.0.0", "pytest-benchmark >=4.0.0", - "mypy >=1.1.1", + "mypy >=1.17.0", ] [tool.flit.module] diff --git a/tcod/ecs/_converter.py b/tcod/ecs/_converter.py index 4ca5c9f..55fcf11 100644 --- a/tcod/ecs/_converter.py +++ b/tcod/ecs/_converter.py @@ -2,10 +2,13 @@ import functools from collections import defaultdict -from typing import Any, Callable, get_args, get_origin +from typing import TYPE_CHECKING, Any, get_args, get_origin import cattrs +if TYPE_CHECKING: + from collections.abc import Callable + def _is_defaultdict_type(type_hint: object) -> bool: """Return True if `type_hint` is a defaultdict type-hint.""" diff --git a/tcod/ecs/callbacks.py b/tcod/ecs/callbacks.py index d3d93ad..34e51e3 100644 --- a/tcod/ecs/callbacks.py +++ b/tcod/ecs/callbacks.py @@ -2,9 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union - -from typing_extensions import TypeAlias +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar if TYPE_CHECKING: from tcod.ecs.entity import Entity @@ -12,7 +11,7 @@ _T = TypeVar("_T") -_OnComponentChangedFunc: TypeAlias = Callable[["Entity", Union[_T, None], Union[_T, None]], None] +_OnComponentChangedFunc: TypeAlias = Callable[["Entity", _T | None, _T | None], None] _OnComponentChangedFuncT = TypeVar("_OnComponentChangedFuncT", bound=_OnComponentChangedFunc[Any]) _on_component_changed_callbacks: dict[ComponentKey[Any], list[_OnComponentChangedFunc[Any]]] = {} diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index f7e4b9a..46fe2a9 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -2,20 +2,13 @@ from __future__ import annotations +from collections.abc import Iterable, Iterator, Mapping, MutableMapping, MutableSet from typing import ( TYPE_CHECKING, Any, Final, Generic, - Iterable, - Iterator, - Mapping, - MutableMapping, - MutableSet, - Tuple, - Type, TypeVar, - Union, overload, ) from weakref import WeakKeyDictionary, WeakValueDictionary @@ -389,7 +382,7 @@ def _traverse_entities(start: Entity, traverse_parents: tuple[object, ...]) -> I @attrs.define(eq=False, frozen=True, weakref_slot=False) -class EntityComponents(MutableMapping[Union[Type[Any], Tuple[object, Type[Any]]], object]): +class EntityComponents(MutableMapping[type[Any] | tuple[object, type[Any]], object]): """A proxy attribute to access an entities components like a dictionary. See :any:`Entity.components`. diff --git a/tcod/ecs/query.py b/tcod/ecs/query.py index 0f8c381..cd4be36 100644 --- a/tcod/ecs/query.py +++ b/tcod/ecs/query.py @@ -5,7 +5,7 @@ import itertools import warnings from collections import defaultdict -from typing import TYPE_CHECKING, Any, Iterable, Iterator, Protocol, TypeVar, overload +from typing import TYPE_CHECKING, Any, Protocol, TypeVar, overload from weakref import WeakKeyDictionary, WeakSet import attrs @@ -15,6 +15,7 @@ from tcod.ecs.constants import IsA if TYPE_CHECKING: + from collections.abc import Iterable, Iterator from collections.abc import Set as AbstractSet from tcod.ecs.entity import Entity @@ -471,7 +472,7 @@ def __getitem__(self, key: tuple[ComponentKey[object], ...]) -> Iterable[tuple[A continue registry_components = self.registry._components_by_type[component_key] entity_components.append([registry_components[entity] for entity in entities]) - return zip(*entity_components) + return zip(*entity_components, strict=True) WorldQuery = BoundQuery diff --git a/tcod/ecs/registry.py b/tcod/ecs/registry.py index d0a7d28..2d3db74 100644 --- a/tcod/ecs/registry.py +++ b/tcod/ecs/registry.py @@ -4,7 +4,8 @@ import warnings from collections import defaultdict -from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Final, Iterable, Mapping, NoReturn, Set, TypeVar +from collections.abc import Iterable, Mapping +from typing import TYPE_CHECKING, Any, Final, NoReturn, TypeVar import attrs from typing_extensions import deprecated @@ -195,23 +196,23 @@ def __setstate__(self, state: dict[str, Any]) -> None: # Apply defaultdict types to unpickled dictionaries self._components_by_type = converter.structure( state.pop("_components_by_type"), - DefaultDict[Any, Dict[Any, Any]], + defaultdict[Any, dict[Any, Any]], ) self._components_by_entity = _components_by_entity_from(self._components_by_type) self._tags_by_entity = converter.structure( state.pop("_tags_by_entity"), - DefaultDict[Any, Set[Any]], + defaultdict[Any, set[Any]], ) self._tags_by_key = _tags_by_key_from_tags_by_entity(self._tags_by_entity) self._relation_tags_by_entity = converter.structure( state.pop("_relation_tags_by_entity"), - DefaultDict[Any, DefaultDict[Any, Set[Any]]], + defaultdict[Any, defaultdict[Any, set[Any]]], ) self._relation_components_by_entity = converter.structure( state.pop("_relation_components_by_entity"), - DefaultDict[Any, DefaultDict[Any, Dict[Any, Any]]], + defaultdict[Any, defaultdict[Any, dict[Any, Any]]], ) self._relations_lookup = _relations_lookup_from( self._relation_tags_by_entity, self._relation_components_by_entity @@ -231,11 +232,11 @@ def __getstate__(self) -> dict[str, Any]: converter = tcod.ecs._converter._get_converter() # Replace defaultdict types with plain dict when saving return { - "_components_by_type": converter.structure(self._components_by_type, Dict[Any, Dict[Any, Any]]), - "_tags_by_entity": converter.structure(self._tags_by_entity, Dict[Any, Any]), - "_relation_tags_by_entity": converter.structure(self._relation_tags_by_entity, Dict[Any, Dict[Any, Any]]), + "_components_by_type": converter.structure(self._components_by_type, dict[Any, dict[Any, Any]]), + "_tags_by_entity": converter.structure(self._tags_by_entity, dict[Any, Any]), + "_relation_tags_by_entity": converter.structure(self._relation_tags_by_entity, dict[Any, dict[Any, Any]]), "_relation_components_by_entity": converter.structure( - self._relation_components_by_entity, Dict[Any, Dict[Any, Any]] + self._relation_components_by_entity, dict[Any, dict[Any, Any]] ), "_names_by_name": self._names_by_name, } diff --git a/tcod/ecs/typing.py b/tcod/ecs/typing.py index 6a52595..4949a2f 100644 --- a/tcod/ecs/typing.py +++ b/tcod/ecs/typing.py @@ -2,11 +2,8 @@ from __future__ import annotations -import sys -import types -from typing import TYPE_CHECKING, Any, Tuple, Type, TypeVar, Union - -from typing_extensions import TypeAlias +from types import EllipsisType +from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar if TYPE_CHECKING: from tcod.ecs.entity import Entity @@ -15,23 +12,19 @@ Entity = Any BoundQuery = Any -if sys.version_info >= (3, 10): # pragma: no cover - EllipsisType: TypeAlias = types.EllipsisType -else: # pragma: no cover - EllipsisType = Any _T = TypeVar("_T") -ComponentKey: TypeAlias = Union[Type[_T], Tuple[object, Type[_T]]] +ComponentKey: TypeAlias = type[_T] | tuple[object, type[_T]] """ComponentKey is plain `type` or tuple `(tag, type)`.""" -_RelationTargetLookup: TypeAlias = Union[Entity, EllipsisType] +_RelationTargetLookup: TypeAlias = Entity | EllipsisType """Possible target for stored relations.""" -_RelationQueryTarget: TypeAlias = Union[_RelationTargetLookup, BoundQuery] +_RelationQueryTarget: TypeAlias = _RelationTargetLookup | BoundQuery """Possible target for relation queries.""" -_RelationQuery: TypeAlias = Union[Tuple[object, _RelationQueryTarget], Tuple[_RelationQueryTarget, object, None]] # noqa: PYI047 +_RelationQuery: TypeAlias = tuple[object, _RelationQueryTarget] | tuple[_RelationQueryTarget, object, None] # noqa: PYI047 """Query format for relations. One of 4 formats: diff --git a/tests/test_ecs.py b/tests/test_ecs.py index 8f288a8..c7e4412 100644 --- a/tests/test_ecs.py +++ b/tests/test_ecs.py @@ -6,7 +6,7 @@ import pickle import pickletools import sys -from typing import Callable, Iterator +from collections.abc import Callable, Iterator # noqa: TC003 import pytest From ecd1f988ade56580783b7ee152855c3a52edfb2e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Nov 2025 11:48:22 -0800 Subject: [PATCH 68/83] Update linters Cleanup minor Mypy and Ruff warnings --- .pre-commit-config.yaml | 4 ++-- pyproject.toml | 2 ++ tcod/ecs/entity.py | 2 +- tests/test_benchmarks.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c11409b..da481ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: autoupdate_schedule: quarterly repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.4 + rev: v0.14.6 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] diff --git a/pyproject.toml b/pyproject.toml index 658578f..2b0b4c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,8 @@ warn_unused_ignores = true warn_return_any = true no_implicit_reexport = true strict_equality = true +strict_bytes = true +extra_checks = true [tool.pyright] reportInconsistentOverload = false diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 46fe2a9..cc89c6f 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -925,7 +925,7 @@ def clear(self) -> None: @attrs.define(eq=False, frozen=True, weakref_slot=False) -class EntityComponentRelationMapping(Generic[T], MutableMapping[Entity, T]): +class EntityComponentRelationMapping(MutableMapping[Entity, T], Generic[T]): """An entity-component mapping to access the relation target component objects. See :any:`Entity.relation_components`. diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py index 950b11f..34a05e1 100644 --- a/tests/test_benchmarks.py +++ b/tests/test_benchmarks.py @@ -17,7 +17,7 @@ def test_component_missing(benchmark: Any) -> None: def test_component_assign(benchmark: Any) -> None: entity = tcod.ecs.Registry().new_entity() - @benchmark # type: ignore[misc] + @benchmark # type: ignore[untyped-decorator] def _() -> None: entity.components[str] = "value" From 596ef525a874de7fad84cd3cbd281a92ae5d67dd Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Nov 2025 11:56:14 -0800 Subject: [PATCH 69/83] Update workflows Test on latest Python versions Update actions versions Mypy no longer needs to be tested in multiple Python versions since 3.9 was dropped --- .github/workflows/python-package.yml | 30 +++++++++++----------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f69ecb7..dd34d66 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -20,36 +20,30 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: pre-commit/action@v3.0.1 mypy: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10", "3.13"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - name: Setup Python ${{ matrix.python-version }} - with: - python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 - name: Install package run: pip install -e ".[test]" - name: Mypy uses: liskin/gh-problem-matcher-wrap@v3 with: linters: mypy - run: mypy --show-column-numbers --python-version ${{ matrix.python-version }} + run: mypy --show-column-numbers test: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14-dev"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15-dev"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 name: Setup Python ${{ matrix.python-version }} with: python-version: ${{ matrix.python-version }} @@ -58,19 +52,19 @@ jobs: - name: Run tests run: pytest --cov-report=xml - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} build-dist: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install build run: pip install build - name: Build package run: python -m build - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: python-dist path: dist/* @@ -88,7 +82,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v6 with: name: python-dist path: dist/ @@ -102,7 +96,7 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Generate body run: scripts/get_release_description.py | tee release_body.md - name: Create Release From 1ffd9404923d74808de0800041d11d1179ec5eb0 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Nov 2025 11:58:17 -0800 Subject: [PATCH 70/83] Add dependabot config --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..be006de --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly From b2c74c243072831c156459ef803deccd8825ec08 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 9 Apr 2025 14:17:47 -0700 Subject: [PATCH 71/83] Add TypeForm support Converts containers to use TypeForm instead of Type. This allows for type expressions to be used as component types. --- CHANGELOG.md | 5 +++++ pyproject.toml | 5 +++-- tcod/ecs/entity.py | 19 +++++++++---------- tcod/ecs/registry.py | 6 +++--- tcod/ecs/typing.py | 4 +++- tests/test_ecs.py | 11 +++++++++++ 6 files changed, 34 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 194f317..315869e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dropped support for Python 3.8 & 3.9. +### Fixed + +- Component key type-hints now use `TypeForm`. + Complex component types which already worked at runtime are now recognized by type linters. + ## [5.4.1] - 2025-07-20 Maintenance release diff --git a/pyproject.toml b/pyproject.toml index 2b0b4c9..0b84935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "attrs >=23.1.0", "cattrs >=23.1.2", "sentinel-value >=1.0.0", - "typing-extensions >=4.9.0", + "typing-extensions >=4.13.1", ] [tool.setuptools_scm] @@ -50,7 +50,8 @@ Source = "https://github.com/HexDecimal/python-tcod-ecs" files = "." exclude = ['^build/', '^\.'] explicit_package_bases = true -python_version = "3.10" # Type check Python version with EllipsisType +python_version = "3.10" +enable_incomplete_feature = ["TypeForm"] warn_unused_configs = true disallow_any_generics = true disallow_subclassing_any = true diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index cc89c6f..6013bee 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -15,7 +15,7 @@ import attrs from sentinel_value import sentinel -from typing_extensions import Self, deprecated +from typing_extensions import Self, TypeForm, deprecated import tcod.ecs.callbacks import tcod.ecs.query @@ -382,7 +382,7 @@ def _traverse_entities(start: Entity, traverse_parents: tuple[object, ...]) -> I @attrs.define(eq=False, frozen=True, weakref_slot=False) -class EntityComponents(MutableMapping[type[Any] | tuple[object, type[Any]], object]): +class EntityComponents(MutableMapping[TypeForm[Any] | tuple[object, TypeForm[Any]], object]): """A proxy attribute to access an entities components like a dictionary. See :any:`Entity.components`. @@ -440,7 +440,7 @@ def __setitem__(self, key: ComponentKey[T], value: T) -> None: tcod.ecs.callbacks._on_component_changed(key, self.entity, old_value, value) - def __delitem__(self, key: type[object] | tuple[object, type[object]]) -> None: + def __delitem__(self, key: TypeForm[object] | tuple[object, TypeForm[object]]) -> None: """Delete a directly held component from an entity.""" assert self.__assert_key(key) @@ -467,8 +467,8 @@ def keys(self) -> AbstractSet[ComponentKey[object]]: # type: ignore[override] *(_components_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse)) ) - def __contains__(self, key: ComponentKey[object]) -> bool: # type: ignore[override] - """Return True if this entity has the provided component.""" + def __contains__(self, key: object) -> bool: + """Return True if this entity has the provided component key.""" _components_by_entity = self.entity.registry._components_by_entity return any( key in _components_by_entity.get(entity, ()) for entity in _traverse_entities(self.entity, self.traverse) @@ -493,7 +493,7 @@ def update_values(self, values: Iterable[object]) -> None: self.set(value) @deprecated("This method has been deprecated. Iterate over items instead.", category=FutureWarning) - def by_name_type(self, name_type: type[_T1], component_type: type[_T2]) -> Iterator[tuple[_T1, type[_T2]]]: + def by_name_type(self, name_type: type[_T1], component_type: TypeForm[_T2]) -> Iterator[tuple[_T1, TypeForm[_T2]]]: """Iterate over all of an entities component keys with a specific (name_type, component_type) combination. .. versionadded:: 3.0 @@ -501,13 +501,12 @@ def by_name_type(self, name_type: type[_T1], component_type: type[_T2]) -> Itera .. deprecated:: 3.1 This method has been deprecated. Iterate over items instead. """ - # Naive implementation until I feel like optimizing it for key in self: if not isinstance(key, tuple): continue key_name, key_component = key if key_component is component_type and isinstance(key_name, name_type): - yield key_name, key_component + yield key_name, key_component # type: ignore[unused-ignore] # Too complex for PyLance, deprecated anyways @overload def __ior__(self, value: SupportsKeysAndGetItem[ComponentKey[Any], Any]) -> Self: ... @@ -1044,14 +1043,14 @@ def __getitem__(self, key: ComponentKey[T]) -> EntityComponentRelationMapping[T] """Access relations for this component key as a `{target: component}` dict-like object.""" return EntityComponentRelationMapping(self.entity, key, self.traverse) - def __setitem__(self, __key: ComponentKey[T], __values: Mapping[Entity, object], /) -> None: + def __setitem__(self, __key: ComponentKey[T], __values: Mapping[Entity, T], /) -> None: """Redefine the component relations for this entity. ..versionadded:: 4.2.0 """ if isinstance(__values, EntityComponentRelationMapping) and __values.entity is self.entity: return - mapping: EntityComponentRelationMapping[object] = self[__key] + mapping: EntityComponentRelationMapping[T] = self[__key] mapping.clear() for target, component in __values.items(): mapping[target] = component diff --git a/tcod/ecs/registry.py b/tcod/ecs/registry.py index 2d3db74..a2927f1 100644 --- a/tcod/ecs/registry.py +++ b/tcod/ecs/registry.py @@ -33,10 +33,10 @@ def _defaultdict_of_dict() -> defaultdict[_T1, dict[_T2, _T3]]: def _components_by_entity_from( - by_type: defaultdict[ComponentKey[object], dict[Entity, Any]], -) -> defaultdict[Entity, dict[ComponentKey[object], Any]]: + by_type: defaultdict[ComponentKey[_T1], dict[Entity, _T1]], +) -> defaultdict[Entity, dict[ComponentKey[_T1], _T1]]: """Return the component lookup table from the components sparse-set.""" - by_entity: defaultdict[Entity, dict[ComponentKey[object], Any]] = defaultdict(dict) + by_entity: defaultdict[Entity, dict[ComponentKey[_T1], _T1]] = defaultdict(dict) for component_key, components in by_type.items(): for entity, component in components.items(): by_entity[entity][component_key] = component diff --git a/tcod/ecs/typing.py b/tcod/ecs/typing.py index 4949a2f..d262677 100644 --- a/tcod/ecs/typing.py +++ b/tcod/ecs/typing.py @@ -5,6 +5,8 @@ from types import EllipsisType from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar +from typing_extensions import TypeForm + if TYPE_CHECKING: from tcod.ecs.entity import Entity from tcod.ecs.query import BoundQuery @@ -15,7 +17,7 @@ _T = TypeVar("_T") -ComponentKey: TypeAlias = type[_T] | tuple[object, type[_T]] +ComponentKey: TypeAlias = TypeForm[_T] | tuple[object, TypeForm[_T]] """ComponentKey is plain `type` or tuple `(tag, type)`.""" _RelationTargetLookup: TypeAlias = Entity | EllipsisType diff --git a/tests/test_ecs.py b/tests/test_ecs.py index c7e4412..a245f24 100644 --- a/tests/test_ecs.py +++ b/tests/test_ecs.py @@ -7,6 +7,7 @@ import pickletools import sys from collections.abc import Callable, Iterator # noqa: TC003 +from typing import Final import pytest @@ -290,3 +291,13 @@ def test_any_of() -> None: assert world.Q.any_of(tags=["foo"]) assert world.Q.any_of(tags=["foo", "bar"]) assert not world.Q.any_of(tags=["bar"]) + + +def test_type_form() -> None: + world = tcod.ecs.Registry() + TupleKey: Final = ("TupleKey", tuple[int, int]) # noqa: N806 + + # tuple layout is forgotten when TypeForm support is missing + world[None].components[TupleKey] = (1, 2) + x, y = world[None].components[TupleKey] + assert (x, y) == (1, 2) From 99e849adff3f78ae75f55443a4cf1aa533f4cd17 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Nov 2025 14:03:58 -0800 Subject: [PATCH 72/83] Prepare 5.4.2 release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 315869e..d95f21d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.4.2] - 2025-11-28 + ### Removed - Dropped support for Python 3.8 & 3.9. From 8cf25a242863b9df07c0700c9b502e96b00ef96d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:07:13 +0000 Subject: [PATCH 73/83] Bump the github-actions group with 2 updates Bumps the github-actions group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/upload-artifact` from 5 to 6 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) Updates `actions/download-artifact` from 6 to 7 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index dd34d66..f057176 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -64,7 +64,7 @@ jobs: run: pip install build - name: Build package run: python -m build - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: python-dist path: dist/* @@ -82,7 +82,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: python-dist path: dist/ From 57c7027a94caee5b1e7fdd37276b58661895242d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:35:41 +0000 Subject: [PATCH 74/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.6 → v0.14.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.6...v0.14.10) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da481ac..55464e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.6 + rev: v0.14.10 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] From 9913c439b334c40fc0a5cc9a98d8a7410b083b21 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 14 Jan 2026 14:26:41 -0800 Subject: [PATCH 75/83] Remove sentinel-value dependency I did not like this how this library doesn't use pickle singletons I'll use classes for now since those can switch to something better later Typing-extensions can be used for `_raise` because that doesn't need to support pickle --- CHANGELOG.md | 4 ++++ pyproject.toml | 3 +-- tcod/ecs/constants.py | 17 +++++++++++++---- tcod/ecs/entity.py | 5 ++--- tests/test_ecs.py | 7 +++++++ 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d95f21d..f7a77aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Removed dependency on `sentinel-value`. + ## [5.4.2] - 2025-11-28 ### Removed diff --git a/pyproject.toml b/pyproject.toml index 0b84935..84cea06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,7 @@ requires-python = ">=3.10" dependencies = [ "attrs >=23.1.0", "cattrs >=23.1.2", - "sentinel-value >=1.0.0", - "typing-extensions >=4.13.1", + "typing-extensions >=4.14.0", ] [tool.setuptools_scm] diff --git a/tcod/ecs/constants.py b/tcod/ecs/constants.py index 6c7980d..adbd11c 100644 --- a/tcod/ecs/constants.py +++ b/tcod/ecs/constants.py @@ -2,9 +2,18 @@ from __future__ import annotations -from typing import Final -from sentinel_value import sentinel +class _IgnoreSetState(type): + def __setstate__(cls, _state: object) -> None: + """Ignore setstate on outdated sentinel-value pickle data.""" -IsA: Final = sentinel("IsA") -"""The default is-a relationship tag used for entity inheritance.""" + +class IsA(metaclass=_IgnoreSetState): + """The default is-a relationship tag used for entity inheritance.""" + + def __new__(cls: type[IsA], *_args: object) -> type[IsA]: # type: ignore[misc] + """Return own type instead of instance, for outdated sentinel-value pickle data.""" + return cls + + +_sentinel_IsA = IsA # Compatibility with sentinel-value, deprecated since 5.4 # noqa: N816 diff --git a/tcod/ecs/entity.py b/tcod/ecs/entity.py index 6013bee..3f5d1dc 100644 --- a/tcod/ecs/entity.py +++ b/tcod/ecs/entity.py @@ -14,8 +14,7 @@ from weakref import WeakKeyDictionary, WeakValueDictionary import attrs -from sentinel_value import sentinel -from typing_extensions import Self, TypeForm, deprecated +from typing_extensions import Self, Sentinel, TypeForm, deprecated import tcod.ecs.callbacks import tcod.ecs.query @@ -34,7 +33,7 @@ _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") -_raise: Final = sentinel("_raise") +_raise: Final = Sentinel("_raise") _entity_table: WeakKeyDictionary[Registry, WeakValueDictionary[object, Entity]] = WeakKeyDictionary() """A weak table of registries and unique identifiers to entity objects. diff --git a/tests/test_ecs.py b/tests/test_ecs.py index a245f24..95c334c 100644 --- a/tests/test_ecs.py +++ b/tests/test_ecs.py @@ -301,3 +301,10 @@ def test_type_form() -> None: world[None].components[TupleKey] = (1, 2) x, y = world[None].components[TupleKey] assert (x, y) == (1, 2) + + +PICKLED_ISA = b"\x80\x03ctcod.ecs.constants\n_sentinel_IsA\nq\x00X\x12\x00\x00\x00tcod.ecs.constantsq\x01X\x03\x00\x00\x00IsAq\x02\x86q\x03\x81q\x04}q\x05(X\r\x00\x00\x00instance_nameq\x06h\x02X\x0b\x00\x00\x00module_nameq\x07h\x01X\x0e\x00\x00\x00qualified_nameq\x08X\x16\x00\x00\x00tcod.ecs.constants.IsAq\tub." # cspell: disable-line + + +def test_unpickle_is_a() -> None: + assert pickle.loads(PICKLED_ISA) is tcod.ecs.IsA # noqa: S301 From f2ee28b92ae677623f59fb2eabda7e3ab61c0ef8 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 14 Jan 2026 14:40:18 -0800 Subject: [PATCH 76/83] Prepare 5.5.0 release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a77aa..4f14484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.5.0] - 2026-01-14 + ### Changed - Removed dependency on `sentinel-value`. From 419e5dc2510818df70c31276afa0e5d6c86be540 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 14 Jan 2026 15:07:09 -0800 Subject: [PATCH 77/83] Fix documentation Attempt to make sentinel workarounds look as nice as possible --- docs/api.rst | 2 +- tcod/ecs/constants.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 5f6450b..d1e60e9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5,7 +5,7 @@ API reference :members: :undoc-members: :show-inheritance: - :exclude-members: Registry, World, Entity + :exclude-members: Registry, World, Entity, IsA .. automodule:: tcod.ecs.registry :members: diff --git a/tcod/ecs/constants.py b/tcod/ecs/constants.py index adbd11c..a7a122a 100644 --- a/tcod/ecs/constants.py +++ b/tcod/ecs/constants.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing_extensions import Never + class _IgnoreSetState(type): def __setstate__(cls, _state: object) -> None: @@ -11,9 +13,9 @@ def __setstate__(cls, _state: object) -> None: class IsA(metaclass=_IgnoreSetState): """The default is-a relationship tag used for entity inheritance.""" - def __new__(cls: type[IsA], *_args: object) -> type[IsA]: # type: ignore[misc] - """Return own type instead of instance, for outdated sentinel-value pickle data.""" - return cls + def __new__(cls: type[IsA], *_: object) -> Never: # noqa: D102 + # Return own type instead of instance, for outdated sentinel-value pickle data. + return cls # type: ignore[misc] _sentinel_IsA = IsA # Compatibility with sentinel-value, deprecated since 5.4 # noqa: N816 From 55d029877814c66f33de6c131a72d0f7b7bef6b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:04:47 +0000 Subject: [PATCH 78/83] Bump the github-actions group with 2 updates Bumps the github-actions group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/upload-artifact` from 6 to 7 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) Updates `actions/download-artifact` from 7 to 8 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f057176..ee781c8 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -64,7 +64,7 @@ jobs: run: pip install build - name: Build package run: python -m build - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: python-dist path: dist/* @@ -82,7 +82,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: python-dist path: dist/ From c5788a2fc3b062bf474024373bef4fda29e6aa98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:12:40 +0000 Subject: [PATCH 79/83] Bump codecov/codecov-action from 5 to 6 in the github-actions group Bumps the github-actions group with 1 update: [codecov/codecov-action](https://github.com/codecov/codecov-action). Updates `codecov/codecov-action` from 5 to 6 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5...v6) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index ee781c8..d281493 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -52,7 +52,7 @@ jobs: - name: Run tests run: pytest --cov-report=xml - name: Upload coverage - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} From dc300c52f353e2a31bb88f8449e69b61503255a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:05:29 +0000 Subject: [PATCH 80/83] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.10 → v0.15.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.10...v0.15.9) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55464e5..0c6f16e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.10 + rev: v0.15.9 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] From 2a5d957c687bc9a931c96b3d6649f3a3cf6fc05b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 27 May 2026 19:15:59 -0700 Subject: [PATCH 81/83] Add Zizmor linter Apply fixes for new rules --- .github/dependabot.yml | 4 +++- .github/workflows/python-package.yml | 23 ++++++++++++++++++----- .github/zizmor.yaml | 9 +++++++++ .pre-commit-config.yaml | 6 +++++- .vscode/settings.json | 4 +++- 5 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 .github/zizmor.yaml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index be006de..2b2eb8c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,8 @@ updates: groups: github-actions: patterns: - - "*" # Group all Actions updates into a single larger pull request + - "*" # Group all Actions updates into a single larger pull request schedule: interval: weekly + cooldown: + default-days: 7 diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index d281493..e873d39 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,17 +16,25 @@ defaults: run: shell: bash +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + with: + persist-credentials: false - uses: pre-commit/action@v3.0.1 mypy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + with: + persist-credentials: false - uses: actions/setup-python@v6 - name: Install package run: pip install -e ".[test]" @@ -43,6 +51,8 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15-dev"] steps: - uses: actions/checkout@v6 + with: + persist-credentials: false - uses: actions/setup-python@v6 name: Setup Python ${{ matrix.python-version }} with: @@ -60,6 +70,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Install build run: pip install build - name: Build package @@ -80,7 +92,7 @@ jobs: name: pypi url: https://pypi.org/project/tcod-ecs/${{ github.ref_name }}/ permissions: - id-token: write + id-token: write # Attestation steps: - uses: actions/download-artifact@v8 with: @@ -93,14 +105,15 @@ jobs: name: Create Release runs-on: ubuntu-latest permissions: - contents: write + contents: write # Publish GitHub Releases steps: - name: Checkout code uses: actions/checkout@v6 + with: + persist-credentials: false - name: Generate body run: scripts/get_release_description.py | tee release_body.md - name: Create Release id: create_release - uses: ncipollo/release-action@v1 - with: - bodyFile: release_body.md + # https://cli.github.com/manual/gh_release_create + run: gh release create "${GITHUB_REF_NAME}" --verify-tag --notes-file release_body.md diff --git a/.github/zizmor.yaml b/.github/zizmor.yaml new file mode 100644 index 0000000..d9e822c --- /dev/null +++ b/.github/zizmor.yaml @@ -0,0 +1,9 @@ +rules: + anonymous-definition: + disable: true + cache-poisoning: + disable: true + excessive-permissions: + disable: true + unpinned-uses: + disable: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c6f16e..524851c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,8 +17,12 @@ repos: - id: fix-byte-order-marker - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.9 + rev: v0.15.14 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] - id: ruff-format + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.25.2 + hooks: + - id: zizmor diff --git a/.vscode/settings.json b/.vscode/settings.json index 445fcc1..c0fdc09 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "buildapi", "cattrs", "codecov", + "cooldown", "docstrings", "doctest", "doctests", @@ -60,7 +61,8 @@ "unstructure", "WASD", "WAXD", - "WINDOWLEAVE" + "WINDOWLEAVE", + "zizmor" ], "editor.codeActionsOnSave": { "source.fixAll": "always" From 8272dc7a5294eb14e6c7573ba14fbce480c7e484 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 27 May 2026 19:20:50 -0700 Subject: [PATCH 82/83] Remove liskin/gh-problem-matcher-wrap The better aesthetics are not worth an extra dependency --- .github/workflows/python-package.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e873d39..8262b4b 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -39,10 +39,7 @@ jobs: - name: Install package run: pip install -e ".[test]" - name: Mypy - uses: liskin/gh-problem-matcher-wrap@v3 - with: - linters: mypy - run: mypy --show-column-numbers + run: mypy --show-column-numbers test: runs-on: ubuntu-latest From dd8407449d0ec5e3f3f7a93979bce2d6f6f21869 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 3 Jun 2026 20:50:12 -0700 Subject: [PATCH 83/83] Add missing token for GitHub CLI --- .github/workflows/python-package.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8262b4b..a91c4d8 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -114,3 +114,5 @@ jobs: id: create_release # https://cli.github.com/manual/gh_release_create run: gh release create "${GITHUB_REF_NAME}" --verify-tag --notes-file release_body.md + env: + GH_TOKEN: ${{ github.token }}