tinyecs 0.3.6__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2022 DickerDackel
3
+ Copyright (c) 2026 DickerDackel
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinyecs
3
- Version: 0.3.6
3
+ Version: 0.5.0
4
4
  Summary: The teeniest, tiniest ECS system
5
5
  Author-email: Michael Lamertz <michael.lamertz@gmail.com>
6
6
  License-Expression: MIT
@@ -13,7 +13,19 @@ Classifier: Topic :: Games/Entertainment
13
13
  Classifier: Topic :: Software Development :: Libraries :: pygame
14
14
  Description-Content-Type: text/x-rst
15
15
  License-File: LICENSE
16
- Requires-Dist: pgcooldown
16
+ Provides-Extra: base
17
+ Requires-Dist: tinyecs-compsys-base; extra == "base"
18
+ Provides-Extra: dynamics
19
+ Requires-Dist: tinyecs-compsys-dynamics; extra == "dynamics"
20
+ Provides-Extra: patternengine
21
+ Requires-Dist: tinyecs-compsys-patternengine; extra == "patternengine"
22
+ Provides-Extra: sdl2
23
+ Requires-Dist: tinyecs-compsys-sdl2; extra == "sdl2"
24
+ Provides-Extra: demos
25
+ Requires-Dist: tinyecs[base,dynamics,sdl2]; extra == "demos"
26
+ Requires-Dist: tinyecs-demos; extra == "demos"
27
+ Provides-Extra: all
28
+ Requires-Dist: tinyecs[base,demos,dynamics,patternengine,sdl2]; extra == "all"
17
29
  Dynamic: license-file
18
30
 
19
31
  Introduction
@@ -73,15 +85,6 @@ Issues can be opened on `github issues`_
73
85
 
74
86
  .. _github issues: https://github.com/dickerdackel/tinyecs/issues
75
87
 
76
- Please respect, that I don't want any contributions done with the assistance
77
- of AI. This is a hobby project with focus on the craft of programming. I
78
- have experimentally used AI to audit parts of the code and documentation, and
79
- while it found a lot of typos and 2 actual issues, without exception, the
80
- provided solutions were often besides the point or plain wrong.
81
-
82
- I have no possibility to enforce that request, the simple fact that I ask for
83
- it should be sufficient.
84
-
85
88
  License
86
89
  =======
87
90
 
@@ -55,15 +55,6 @@ Issues can be opened on `github issues`_
55
55
 
56
56
  .. _github issues: https://github.com/dickerdackel/tinyecs/issues
57
57
 
58
- Please respect, that I don't want any contributions done with the assistance
59
- of AI. This is a hobby project with focus on the craft of programming. I
60
- have experimentally used AI to audit parts of the code and documentation, and
61
- while it found a lot of typos and 2 actual issues, without exception, the
62
- provided solutions were often besides the point or plain wrong.
63
-
64
- I have no possibility to enforce that request, the simple fact that I ask for
65
- it should be sufficient.
66
-
67
58
  License
68
59
  =======
69
60
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "tinyecs"
3
3
  description = "The teeniest, tiniest ECS system"
4
- version = "0.3.6"
4
+ version = "0.5.0"
5
5
  readme = "README.rst"
6
6
 
7
7
  authors = [
@@ -18,14 +18,18 @@ classifiers = [
18
18
  license = "MIT"
19
19
  license-files = ["LICENSE"]
20
20
 
21
- dependencies = [
22
- "pgcooldown"
23
- ]
21
+ dependencies = [ ]
24
22
 
25
23
  [project.optional-dependencies]
26
-
27
- [project.scripts]
28
- tinyecs-demo = 'tinyecs.demo:main'
24
+ base = ["tinyecs-compsys-base"]
25
+ dynamics = ["tinyecs-compsys-dynamics"]
26
+ patternengine = ["tinyecs-compsys-patternengine"]
27
+ sdl2 = ["tinyecs-compsys-sdl2"]
28
+ demos = [
29
+ "tinyecs[base,dynamics,sdl2]",
30
+ "tinyecs-demos",
31
+ ]
32
+ all = ["tinyecs[base,dynamics,sdl2,patternengine,demos]"]
29
33
 
30
34
  [project.urls]
31
35
  homepage = "https://github.com/dickerdackel/tinyecs"
@@ -3,20 +3,21 @@ from collections.abc import Hashable, Iterable
3
3
  from typing import Any, Protocol
4
4
  from uuid import uuid4
5
5
 
6
- __all__ = ['Component', 'ComponentID', 'DomainID', 'EntityID', 'Property',
7
- 'RegistryError', 'UnknownArchetypeError', 'UnknownComponentError',
8
- 'UnknownEntityError', 'UnknownSystemError', 'add_component',
9
- 'add_components', 'add_system', 'add_system_to_domain',
10
- 'add_to_archetype', 'cid_of_comp', 'cids_of_eid',
11
- 'clear_properties', 'comp_of_eid', 'comps_of_archetype',
12
- 'comps_of_eid', 'create_archetype', 'create_entity', 'eid_has',
13
- 'eid_of_comp', 'eids_by_cids', 'eids_by_property', 'has',
14
- 'has_property', 'healthcheck', 'purge_by_property',
15
- 'remove_archetype', 'remove_component', 'remove_entity',
16
- 'remove_from_archetype', 'remove_property', 'remove_system',
17
- 'remove_system_from_domain', 'reset', 'run_all_systems',
18
- 'run_domain', 'run_system', 'set_properties', 'set_property',
19
- 'update_component',]
6
+ __all__ = ['Component', 'ComponentID', 'DomainID', 'EntityID',
7
+ 'EntityComponentsBundle', 'Property', 'RegistryError',
8
+ 'SystemFunction', 'BulkSystemFunction', 'UnknownArchetypeError',
9
+ 'UnknownComponentError', 'UnknownEntityError',
10
+ 'UnknownSystemError', 'add_component', 'add_components',
11
+ 'add_system', 'add_system_to_domain', 'add_to_archetype',
12
+ 'cid_of_comp', 'cids_of_eid', 'clear_properties', 'comp_of_eid',
13
+ 'comps_of_archetype', 'comps_of_eid', 'create_archetype',
14
+ 'create_entity', 'eid_has', 'eid_of_comp', 'eids_by_cids',
15
+ 'eids_by_property', 'has', 'has_property', 'healthcheck',
16
+ 'purge_by_property', 'remove_archetype', 'remove_component',
17
+ 'remove_entity', 'remove_from_archetype', 'remove_property',
18
+ 'remove_system', 'remove_system_from_domain', 'reset',
19
+ 'run_all_systems', 'run_bulk_system', 'run_domain', 'run_system',
20
+ 'set_properties', 'set_property', 'update_component',]
20
21
 
21
22
  type EntityID = Hashable
22
23
  type ComponentID = Hashable
@@ -24,7 +25,7 @@ type DomainID = Hashable
24
25
  type Property = Hashable
25
26
  type Component = object
26
27
 
27
- type _EntityComponentsBundle = tuple[EntityID, list[Component]]
28
+ type EntityComponentsBundle = tuple[EntityID, list[Component]]
28
29
  type _OptionalProperties = Iterable[Property] | None
29
30
  type _RunSystemResult = dict[EntityID, Any]
30
31
 
@@ -32,7 +33,11 @@ type _RunSystemResult = dict[EntityID, Any]
32
33
  # The typehinting system can't work with functions using *args, **kwargs.
33
34
  # This solution comes from https://docs.python.org/3/library/typing.html#annotating-callable-objects
34
35
  class SystemFunction(Protocol):
35
- def __call__(self, dt: float, eid: EntityID, *cids: ComponentID, **kwargs: dict[str, object]) -> Any: ...
36
+ def __call__(self, eid: EntityID, *cids: ComponentID, **kwargs: dict[str, Any]) -> Any: ...
37
+
38
+
39
+ class BulkSystemFunction(Protocol):
40
+ def __call__(self, call_list: list[tuple[EntityID, *tuple[Component, ...]]], **kwargs: dict[str, Any]) -> Any: ...
36
41
 
37
42
 
38
43
  eidx = {} # entity index
@@ -71,14 +76,14 @@ class RegistryError(Exception):
71
76
  :param eid: The entity ID of the defective registry entry
72
77
  :param cid: The component ID of the defective registry entry
73
78
  :param component: The component object of the defective registry entry
74
- :param eid: The properties of the defective registry entry
79
+ :param other: Component in cidx when eidx and cidx mismatch
75
80
 
76
81
  Contains an error message and the registry objects that have been
77
82
  identified as inconsistent.
78
83
  """
79
84
 
80
85
  def __init__(self, error, *, eid, cid=None, component=None, other=None, properties=None):
81
- self.error = error
86
+ super().__init__(error)
82
87
  self.eid = eid
83
88
  self.cid = cid
84
89
  self.component = component
@@ -149,7 +154,7 @@ def create_entity(tag: EntityID = None,
149
154
  :return: The entity ID. Same as given if one is passed, otherwise a uuid4
150
155
  """
151
156
 
152
- eid = tag if tag else str(uuid4())
157
+ eid = tag if tag is not None else str(uuid4())
153
158
  if eid not in eidx:
154
159
  eidx[eid] = {}
155
160
 
@@ -208,6 +213,11 @@ def add_component(eid: EntityID, cid: ComponentID, comp: Component) -> Component
208
213
  .. note:: Technically, there is no reason a comp object couldn't
209
214
  have methods, but by concept, functionality is reserved for the System
210
215
  working on the components, not the component itself.
216
+
217
+ .. note:: This function is also aliased to `update_component` just to be
218
+ able to make it clear in the user code, that a component will be
219
+ replaced. Consider it semantic sugar.
220
+
211
221
  """
212
222
 
213
223
  if eid not in eidx:
@@ -219,12 +229,16 @@ def add_component(eid: EntityID, cid: ComponentID, comp: Component) -> Component
219
229
  # Make sure, when replacing a component, the the old one is removed from
220
230
  # oidx for this entity.
221
231
  try:
222
- old_comp = id(eidx[eid][cid])
223
- oidx[old_comp].discard(eid)
224
- if not oidx[old_comp]:
225
- del oidx[old_comp]
232
+ old_comp = eidx[eid][cid]
226
233
  except KeyError:
227
234
  pass
235
+ else:
236
+ old_comp_id = id(old_comp)
237
+ oidx[old_comp_id].discard(eid)
238
+ if not oidx[old_comp_id]:
239
+ del oidx[old_comp_id]
240
+ if hasattr(old_comp, 'shutdown_'):
241
+ old_comp.shutdown_()
228
242
 
229
243
  cidx[cid][eid] = comp
230
244
  eidx[eid][cid] = comp
@@ -242,7 +256,7 @@ def add_components(eid: EntityID, components: dict[ComponentID, Component]) -> l
242
256
  """A convenience wrapper around add_component.
243
257
 
244
258
  :param components: A dict with component IDs as keys and components as values (see add_component)
245
- :return: None
259
+ :return: list of added component IDs
246
260
  """
247
261
 
248
262
  return [add_component(eid, cid, comp) for cid, comp in components.items()]
@@ -255,8 +269,8 @@ def remove_component(eid: EntityID, *cids: ComponentID) -> None:
255
269
  :param cids: The component ids to remove
256
270
  :return: None
257
271
 
258
- If the component has a shutdown\_ attribute, it is assumed to be a list of
259
- zero parameter functions to be called in order.
272
+ If the component has a shutdown\_ attribute, it is assumed to be a
273
+ zero parameter function that is called after the component removal.
260
274
 
261
275
  .. note:: If the entity no longer exists, the error is silently ignored.
262
276
 
@@ -291,11 +305,10 @@ def add_system(fn: SystemFunction, *cids: ComponentID) -> None:
291
305
 
292
306
  The prototype for the function is::
293
307
 
294
- fn(delta_time, eid, *comps)
308
+ fn(eid, *comps, **kwargs)
295
309
 
296
- where delta_time is e.g. the miliseconds from a pygame tick. eid is the id
297
- of the entity that matches, and \*comps are all requested components for
298
- this specific entity.
310
+ eid is the id of the entity that matches, and \*comps are all requested
311
+ components for this specific entity.
299
312
 
300
313
  This function is called for every entity that matches all specified
301
314
  component ids.
@@ -340,10 +353,10 @@ def add_system_to_domain(domain: DomainID, system: SystemFunction) -> None:
340
353
  .. important:: The system must first be registered with add_system.
341
354
  """
342
355
 
343
- if domain not in didx:
344
- didx[domain] = set()
345
356
  if system not in sidx:
346
357
  raise UnknownSystemError(f'system {system} is not registered')
358
+ if domain not in didx:
359
+ didx[domain] = set()
347
360
  didx[domain].add(system)
348
361
 
349
362
 
@@ -387,9 +400,14 @@ def eid_has(eid: EntityID, *cids: ComponentID) -> bool:
387
400
 
388
401
  :param cids: All component ids that need to match
389
402
  :return: True if all given cids are available for the specified eid
403
+ :raises UnknownEntityError: If the entity is not registered (anymore).
390
404
  """
391
405
 
392
- e = eidx[eid]
406
+ try:
407
+ e = eidx[eid]
408
+ except KeyError:
409
+ raise UnknownEntityError
410
+
393
411
  for cid in cids:
394
412
  if cid not in e:
395
413
  return False
@@ -401,7 +419,7 @@ def eids_by_cids(*cids: ComponentID, has_properties: _OptionalProperties = None)
401
419
  """Get eids that match all specified cids.
402
420
 
403
421
  :param cids: All component ids that need to match
404
- :param as_properties: Optional set/tuple/list of required properties
422
+ :param has_properties: Optional set/tuple/list of required properties
405
423
  :return: A list of tuples of entity IDs and components
406
424
  """
407
425
 
@@ -454,7 +472,7 @@ def comps_of_eid(eid: EntityID, *cids: ComponentID) -> list[Component]:
454
472
  While cids_of_eid gets component IDs, this function now gets the actual
455
473
  components containing the data.
456
474
 
457
- .. note:: If cids is empty or None, returns all components of eid.
475
+ .. note:: If no cids are given, returns all components of eid.
458
476
  """
459
477
 
460
478
  if eid not in eidx:
@@ -480,14 +498,14 @@ def comp_of_eid(eid: EntityID, cid: ComponentID) -> Component:
480
498
  return comps_of_eid(eid, cid)[0]
481
499
 
482
500
 
483
- def eid_of_comp(comp: Component) -> set(EntityID):
501
+ def eid_of_comp(comp: Component) -> set[EntityID]:
484
502
  """Find the entity id for object comp.
485
503
 
486
504
  :param comp: The component to find the entity of
487
- :return: The entity_id the given component belongs to
505
+ :return: A set of all entity IDs that contain the given component.
488
506
  """
489
507
 
490
- return oidx[id(comp)]
508
+ return oidx.get(id(comp), set())
491
509
 
492
510
 
493
511
  def cid_of_comp(eid: EntityID, comp: Component) -> ComponentID:
@@ -538,14 +556,12 @@ def _create_call_list(cids: tuple[ComponentID],
538
556
  return call_list
539
557
 
540
558
 
541
- def run_system(dt: float,
542
- fn: SystemFunction,
559
+ def run_system(fn: SystemFunction,
543
560
  *cids: ComponentID,
544
561
  has_properties: _OptionalProperties = None,
545
562
  **kwargs: dict[str, Any]) -> _RunSystemResult:
546
563
  """Run the system for the matching cids.
547
564
 
548
- :param dt: delta time since the last frame (miliseconds)
549
565
  :param fn: the actual system function
550
566
  :param cids: the components to run on
551
567
  :param has_properties: set of required properties
@@ -554,7 +570,7 @@ def run_system(dt: float,
554
570
  For every entity that has all the listed components, ``fn`` is run. The
555
571
  function prototype of ``fn`` is::
556
572
 
557
- def callback(dt: float, eid: EntityID, *comps: Component) -> Any
573
+ def callback(eid: EntityID, *comps: Component) -> Any
558
574
 
559
575
  The list can further be narrowed down my filtering for given properties.
560
576
 
@@ -563,56 +579,62 @@ def run_system(dt: float,
563
579
  """
564
580
 
565
581
  call_list = _create_call_list(cids, has_properties)
566
- return {eid: fn(dt, eid, *parms, **kwargs) for eid, *parms in call_list}
582
+ return {eid: fn(eid, *parms, **kwargs) for eid, *parms in call_list}
567
583
 
568
584
 
569
- def run_bulk_system(dt: float,
570
- fn: SystemFunction,
585
+ def run_bulk_system(fn: BulkSystemFunction,
571
586
  *cids: ComponentID,
572
587
  has_properties: _OptionalProperties = None,
573
588
  **kwargs: dict[str, Any]) -> Any:
574
589
  """Run the bulk system for all entities with matching cids.
575
590
 
576
- :param dt: delta time since the last frame (miliseconds)
577
591
  :param fn: the actual system function
578
592
  :param cids: the components to run on
579
593
  :param has_properties: set of required properties
580
- :return: A dictionary with entity IDs as key and the function result as value
594
+ :return: Returns whatever the given callback returns or None if no
595
+ matching entities have been found
581
596
 
582
- The fn is called with a list of entity ID and components tuples of all
597
+ The fn is called with a list of `(eid, comp...)` tuples of all
583
598
  entities that contain the listed components. The list can further be
584
599
  narrowed down my filtering for given properties.
585
600
 
586
- ``fn`` is called given a list of ``(EntityID, (components,...))``. In
601
+ ``fn`` is called given a list of ``(EntityID, components,...)``. In
587
602
  contrast to ``run_system``, the user is responsible to loop over the list.
588
603
 
604
+ If no entities are found, `fn` is not called.
605
+
589
606
  The prototype of ``fn`` is::
590
607
 
591
- def callback(dt: call_list: list[tuple[EntityID, list[Component]]]) -> Any
608
+ def callback(call_list: list[tuple[EntityID, *tuple[Component, ...]]], **kwargs) -> Any
609
+
610
+ Example::
611
+
612
+ def sys_bulk_render(entities_and_comps):
613
+ for eid, pos, sprite in entities_and_comps:
614
+ ...
615
+
592
616
  """
593
617
 
594
618
  call_list = _create_call_list(cids, has_properties)
595
- return fn(dt, call_list, **kwargs)
619
+ return fn(call_list, **kwargs) if call_list else None
596
620
 
597
621
 
598
- def run_all_systems(dt: float) -> dict[SystemFunction, _RunSystemResult]:
622
+ def run_all_systems(**kwargs) -> dict[SystemFunction, _RunSystemResult]:
599
623
  """Run all registered systems.
600
624
 
601
- :param dt: Delta time
602
625
  :return: A dict of function as key, and the result of run_system as value
603
626
 
604
627
  This calls above run_system for all registered systems with their
605
628
  appropriate components.
606
629
  """
607
630
 
608
- return {fn: run_system(dt, fn, *comps)
631
+ return {fn: run_system(fn, *comps, **kwargs)
609
632
  for fn, comps in sidx.items()}
610
633
 
611
634
 
612
- def run_domain(dt: float, domain: DomainID) -> dict[SystemFunction, _RunSystemResult]:
635
+ def run_domain(domain: DomainID, **kwargs) -> dict[SystemFunction, _RunSystemResult]:
613
636
  """Run all systems within domain.
614
637
 
615
- :param dt: Delta time
616
638
  :return: A dict of function as key, and the result of run_system as value
617
639
 
618
640
  This is the same as run_all_systems, but limited to a specific domain.
@@ -621,7 +643,7 @@ def run_domain(dt: float, domain: DomainID) -> dict[SystemFunction, _RunSystemRe
621
643
  if domain not in didx:
622
644
  return {}
623
645
 
624
- return {fn: run_system(dt, fn, *sidx[fn])
646
+ return {fn: run_system(fn, *sidx[fn], **kwargs)
625
647
  for fn in didx[domain]}
626
648
 
627
649
 
@@ -660,20 +682,23 @@ def create_archetype(*cids: ComponentID) -> None:
660
682
 
661
683
 
662
684
  def remove_archetype(cids: Iterable[ComponentID]) -> None:
663
- """Remove an archetype from the system. (See `add_archetype`).
685
+ """Remove an archetype from the system. (See `create_archetype`).
664
686
 
665
687
  :param cids: The list of component IDs that construct the archetype.
666
688
  :return: None
667
689
  """
668
690
 
669
691
  at = tuple(cids)
670
- del archetype[at]
692
+ try:
693
+ del archetype[at]
694
+ except KeyError:
695
+ pass
671
696
 
672
697
 
673
698
  def add_to_archetype(eid: EntityID) -> None:
674
699
  """Make sure, eid is registered with all appropriate archetypes.
675
700
 
676
- :param eids: The entity ID to add to the archetype
701
+ :param eid: The entity ID to add to the archetype
677
702
 
678
703
  There should be no need to call this function manually. It's called
679
704
  internally, when a new component is added to an entity which makes this
@@ -693,7 +718,7 @@ def add_to_archetype(eid: EntityID) -> None:
693
718
  def remove_from_archetype(eid: EntityID, cid: ComponentID | None = None) -> None:
694
719
  """Make sure, eid is only registered with appropriate archetypes.
695
720
 
696
- :param eids: The entity ID to remove from the archetype
721
+ :param eid: The entity ID to remove from the archetype
697
722
  :param cid: An optional component ID.
698
723
 
699
724
  There should be no need to call this function manually. It's called
@@ -706,7 +731,7 @@ def remove_from_archetype(eid: EntityID, cid: ComponentID | None = None) -> None
706
731
  del adict[eid]
707
732
 
708
733
 
709
- def comps_of_archetype(*cids: ComponentID, has_properties: _OptionalProperties = None) -> list[_EntityComponentsBundle]:
734
+ def comps_of_archetype(*cids: ComponentID, has_properties: _OptionalProperties = None) -> list[EntityComponentsBundle]:
710
735
  """Return the given archetype.
711
736
 
712
737
  :param cids: The cids that define the archetype.
@@ -732,11 +757,11 @@ def comps_of_archetype(*cids: ComponentID, has_properties: _OptionalProperties =
732
757
  return [(e, comps) for e, comps in archetype[at].items()]
733
758
 
734
759
 
735
- def set_property(eid: EntityID, property: Property) -> None:
760
+ def set_property(eid: EntityID, prop: Property) -> None:
736
761
  """Add a property to the given entity.
737
762
 
738
763
  :param eid: The entity id to add the component to
739
- :param property: A flag or tag that can be filtered, e.g. 'is_drawable'
764
+ :param prop: A flag or tag that can be filtered, e.g. 'is_drawable'
740
765
  :return: None
741
766
  :raises UnknownEntityError: If the entity is not registered (anymore).
742
767
  """
@@ -744,14 +769,14 @@ def set_property(eid: EntityID, property: Property) -> None:
744
769
  if eid not in eidx:
745
770
  raise UnknownEntityError(f'Entity {eid} is not registered')
746
771
 
747
- plist[eid].add(property)
772
+ plist[eid].add(prop)
748
773
 
749
774
 
750
775
  def set_properties(eid: EntityID, properties: Iterable[Property]) -> None:
751
776
  """Add a list of properties to the given entity.
752
777
 
753
778
  :param eid: The entity ID to add the properties to.
754
- :param property: The list of properties to add.
779
+ :param properties: The list of properties to add.
755
780
 
756
781
  This is just a convenience wrapper around `set_property`.
757
782
  """
@@ -763,7 +788,7 @@ def has_property(eid: EntityID, prop: Property) -> bool:
763
788
  """Checks if the given entity has that property.
764
789
 
765
790
  :param eid: The entity id to add the component to
766
- :param property: The property to check for
791
+ :param prop: The property to check for
767
792
  :return: True if the property is set for the given entity
768
793
  :raises UnknownEntityError: If the entity is not registered (anymore).
769
794
  """
@@ -778,7 +803,7 @@ def remove_property(eid: EntityID, prop: Property) -> None:
778
803
  """Removes a property from the given entity.
779
804
 
780
805
  :param eid: The entity id to add the component to
781
- :param property: The property to remove
806
+ :param prop: The property to remove
782
807
  :return: None
783
808
  :raises UnknownEntityError: If the entity is not registered (anymore).
784
809
  """
@@ -786,7 +811,10 @@ def remove_property(eid: EntityID, prop: Property) -> None:
786
811
  if eid not in eidx:
787
812
  raise UnknownEntityError(f'Entity {eid} is not registered')
788
813
 
789
- plist[eid].remove(prop)
814
+ try:
815
+ plist[eid].remove(prop)
816
+ except KeyError:
817
+ pass
790
818
 
791
819
 
792
820
  def clear_properties(eid: EntityID) -> None: