esper 3.7__tar.gz → 3.8__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.
Files changed (33) hide show
  1. {esper-3.7 → esper-3.8}/.github/workflows/unit-tests.yml +2 -2
  2. {esper-3.7 → esper-3.8}/PKG-INFO +1 -1
  3. {esper-3.7 → esper-3.8}/RELEASE_NOTES +10 -0
  4. {esper-3.7 → esper-3.8}/esper/__init__.py +16 -8
  5. {esper-3.7 → esper-3.8}/examples/benchmark.py +8 -10
  6. {esper-3.7 → esper-3.8}/examples/benchmark_cache.py +7 -10
  7. {esper-3.7 → esper-3.8}/examples/pygame_example.py +7 -3
  8. {esper-3.7 → esper-3.8}/examples/pyglet_example.py +2 -1
  9. {esper-3.7 → esper-3.8}/examples/pysdl2_example.py +8 -4
  10. {esper-3.7 → esper-3.8}/tests/test_world.py +50 -0
  11. {esper-3.7 → esper-3.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {esper-3.7 → esper-3.8}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {esper-3.7 → esper-3.8}/.github/ISSUE_TEMPLATE/question-or-comment.md +0 -0
  14. {esper-3.7 → esper-3.8}/.github/workflows/type-checking.yml +0 -0
  15. {esper-3.7 → esper-3.8}/.gitignore +0 -0
  16. {esper-3.7 → esper-3.8}/.mypy.ini +0 -0
  17. {esper-3.7 → esper-3.8}/.readthedocs.yaml +0 -0
  18. {esper-3.7 → esper-3.8}/.ruff.toml +0 -0
  19. {esper-3.7 → esper-3.8}/LICENSE +0 -0
  20. {esper-3.7 → esper-3.8}/MANIFEST.in +0 -0
  21. {esper-3.7 → esper-3.8}/README.md +0 -0
  22. {esper-3.7 → esper-3.8}/docs/Makefile +0 -0
  23. {esper-3.7 → esper-3.8}/docs/conf.py +0 -0
  24. {esper-3.7 → esper-3.8}/docs/index.rst +0 -0
  25. {esper-3.7 → esper-3.8}/docs/make.bat +0 -0
  26. {esper-3.7 → esper-3.8}/esper/py.typed +0 -0
  27. {esper-3.7 → esper-3.8}/examples/bluesquare.png +0 -0
  28. {esper-3.7 → esper-3.8}/examples/headless_example.py +0 -0
  29. {esper-3.7 → esper-3.8}/examples/pythonista_ios_example.py +0 -0
  30. {esper-3.7 → esper-3.8}/examples/redsquare.png +0 -0
  31. {esper-3.7 → esper-3.8}/make.py +0 -0
  32. {esper-3.7 → esper-3.8}/pyproject.toml +0 -0
  33. {esper-3.7 → esper-3.8}/tests/__init__.py +0 -0
@@ -14,9 +14,9 @@ jobs:
14
14
  python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.10' ]
15
15
  steps:
16
16
  - name: Python ${{ matrix.python-version }} ${{ matrix.os }}
17
- uses: actions/checkout@v4
17
+ uses: actions/checkout@v7
18
18
  - name: Set up Python ${{ matrix.python-version }}
19
- uses: actions/setup-python@v5
19
+ uses: actions/setup-python@v6
20
20
  with:
21
21
  python-version: ${{ matrix.python-version }}
22
22
  - name: Install test dependencies
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esper
3
- Version: 3.7
3
+ Version: 3.8
4
4
  Summary: esper is a lightweight Entity System (ECS) for Python, with a focus on performance
5
5
  Author-email: Benjamin Moran <benmoran@protonmail.com>
6
6
  Requires-Python: >=3.8
@@ -1,3 +1,13 @@
1
+ esper 3.8
2
+ =========
3
+ Maintenance release
4
+
5
+ Changes
6
+ -------
7
+ - Fix incorrect return type hints for try_components. #120
8
+ - Fix bug where cached components could be lost when switching worlds. #121
9
+
10
+
1
11
  esper 3.7
2
12
  =========
3
13
  Maintenance release
@@ -22,7 +22,7 @@ from math import inf
22
22
 
23
23
  from itertools import count as _count
24
24
 
25
- __version__ = version = '3.7'
25
+ __version__ = version = '3.8'
26
26
 
27
27
 
28
28
  ###################
@@ -203,9 +203,6 @@ def switch_world(name: str) -> None:
203
203
 
204
204
  .. note:: At startup, a "default" World context is active.
205
205
  """
206
- if name not in _context_map:
207
- _context_map[name] = (_count(start=1), {}, {}, set(), {}, {}, [], {}, False, {}, {})
208
-
209
206
  global _current_world
210
207
  global _entity_count
211
208
  global _components
@@ -220,6 +217,17 @@ def switch_world(name: str) -> None:
220
217
  global event_registry
221
218
  global current_world
222
219
 
220
+ if _current_world is not None:
221
+ _context_map[_current_world] = (
222
+ _entity_count, _components, _entities, _dead_entities,
223
+ _get_component_cache, _get_components_cache,
224
+ _processors, _processors_dict, _cache_dirty,
225
+ process_times, event_registry,
226
+ )
227
+
228
+ if name not in _context_map:
229
+ _context_map[name] = (_count(start=1), {}, {}, set(), {}, {}, [], {}, False, {}, {})
230
+
223
231
  (_entity_count, _components, _entities, _dead_entities, _get_component_cache,
224
232
  _get_components_cache, _processors, _processors_dict, _cache_dirty,
225
233
  process_times, event_registry) = _context_map[name]
@@ -461,7 +469,7 @@ def try_remove_component(entity: int, component_type: type[_C]) -> _C | None:
461
469
  del _components[component_type]
462
470
 
463
471
  clear_cache()
464
- return _entities[entity].pop(component_type) # type: ignore[no-any-return]
472
+ return _entities[entity].pop(component_type, None) # type: ignore[no-any-return]
465
473
 
466
474
  return None
467
475
 
@@ -579,17 +587,17 @@ def try_component(entity: int, component_type: type[_C]) -> _C | None:
579
587
 
580
588
 
581
589
  @_overload
582
- def try_components(entity: int, __c1: type[_C], __c2: type[_C2]) -> tuple[_C, _C2]:
590
+ def try_components(entity: int, __c1: type[_C], __c2: type[_C2]) -> tuple[_C, _C2] | None:
583
591
  ...
584
592
 
585
593
 
586
594
  @_overload
587
- def try_components(entity: int, __c1: type[_C], __c2: type[_C2], __c3: type[_C3]) -> tuple[_C, _C2, _C3]:
595
+ def try_components(entity: int, __c1: type[_C], __c2: type[_C2], __c3: type[_C3]) -> tuple[_C, _C2, _C3] | None:
588
596
  ...
589
597
 
590
598
 
591
599
  @_overload
592
- def try_components(entity: int, __c1: type[_C], __c2: type[_C2], __c3: type[_C3], __c4: type[_C4]) -> tuple[_C, _C2, _C3, _C4]:
600
+ def try_components(entity: int, __c1: type[_C], __c2: type[_C2], __c3: type[_C3], __c4: type[_C4]) -> tuple[_C, _C2, _C3, _C4] | None:
593
601
  ...
594
602
 
595
603
 
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
2
 
4
3
  import gc
5
4
  import random
@@ -26,8 +25,7 @@ parser.add_option("-e", "--entities", dest="entities", action="store", default=5
26
25
 
27
26
  MAX_ENTITIES = options.entities
28
27
  if MAX_ENTITIES <= 500:
29
- print("The number of entities must be greater than 500.")
30
- sys.exit(1)
28
+ sys.exit("The number of entities must be greater than 500.")
31
29
 
32
30
  if options.walltime:
33
31
  print("Benchmarking wall clock time...\n")
@@ -35,6 +33,12 @@ if options.walltime:
35
33
  else:
36
34
  time_query = time.process_time
37
35
 
36
+ if options.plot:
37
+ try:
38
+ from matplotlib import pyplot as plt
39
+ except ImportError:
40
+ sys.exit("The matplotlib module is required for plotting results.")
41
+
38
42
 
39
43
  ##########################
40
44
  # Simple timing decorator:
@@ -264,13 +268,7 @@ for amount in range(500, MAX_ENTITIES, MAX_ENTITIES//50):
264
268
  if not options.plot:
265
269
  print("\nRun 'benchmark.py --help' for details on plotting this benchmark.")
266
270
 
267
- if options.plot:
268
- try:
269
- from matplotlib import pyplot as plt
270
- except ImportError:
271
- print("\nThe matplotlib module is required for plotting results.")
272
- sys.exit(1)
273
-
271
+ else:
274
272
  plt.figure(1)
275
273
  lines = []
276
274
  for num, result in results.items():
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
2
 
4
3
  import sys
5
4
  import time
@@ -9,12 +8,6 @@ from dataclasses import dataclass as component
9
8
 
10
9
  import esper
11
10
 
12
- try:
13
- from matplotlib import pyplot
14
- except ImportError:
15
- print("The matplotlib module is required for this benchmark.")
16
- raise Exception
17
-
18
11
  ######################
19
12
  # Commandline options:
20
13
  ######################
@@ -25,9 +18,13 @@ parser.add_option("-e", "--entities", dest="entities", action="store", default=5
25
18
  (options, arguments) = parser.parse_args()
26
19
 
27
20
  MAX_ENTITIES = options.entities
28
- if MAX_ENTITIES <= 50:
29
- print("The number of entities must be greater than 500.")
30
- sys.exit(1)
21
+ if MAX_ENTITIES <= 500:
22
+ sys.exit("The number of entities must be greater than 500.")
23
+
24
+ try:
25
+ from matplotlib import pyplot
26
+ except ImportError:
27
+ sys.exit("The matplotlib module is required for plotting results.")
31
28
 
32
29
 
33
30
  ##########################
@@ -1,11 +1,15 @@
1
- import pygame
1
+ #!/usr/bin/env python
2
+ from pathlib import Path
2
3
 
4
+ import pygame
3
5
  import esper
4
6
 
5
7
 
6
8
  FPS = 60
7
9
  RESOLUTION = 720, 480
8
10
 
11
+ # Parent dir of this script, to find the PNGs regardless of cwd
12
+ path = Path(__file__).parent
9
13
 
10
14
  ##################################
11
15
  # Define some Components:
@@ -81,10 +85,10 @@ def run():
81
85
  # Initialize Esper world, and create a "player" Entity with a few Components.
82
86
  player = esper.create_entity()
83
87
  esper.add_component(player, Velocity(x=0, y=0))
84
- esper.add_component(player, Renderable(image=pygame.image.load("redsquare.png"), posx=100, posy=100))
88
+ esper.add_component(player, Renderable(image=pygame.image.load(path / "redsquare.png"), posx=100, posy=100))
85
89
  # Another motionless Entity:
86
90
  enemy = esper.create_entity()
87
- esper.add_component(enemy, Renderable(image=pygame.image.load("bluesquare.png"), posx=400, posy=250))
91
+ esper.add_component(enemy, Renderable(image=pygame.image.load(path / "bluesquare.png"), posx=400, posy=250))
88
92
 
89
93
  # Create some Processor instances, and asign them to be processed.
90
94
  render_processor = RenderProcessor(window=window)
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env python
1
2
  import pyglet
2
3
  import esper
3
4
 
@@ -29,8 +30,8 @@ class MovementProcessor:
29
30
  def __init__(self, minx, maxx, miny, maxy):
30
31
  super().__init__()
31
32
  self.minx = minx
32
- self.miny = miny
33
33
  self.maxx = maxx
34
+ self.miny = miny
34
35
  self.maxy = maxy
35
36
 
36
37
  def process(self, dt):
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env python
2
+ from pathlib import Path
1
3
  from sdl2 import *
2
4
  import sdl2.ext as ext
3
5
  import esper
@@ -5,6 +7,8 @@ import esper
5
7
 
6
8
  RESOLUTION = 720, 480
7
9
 
10
+ # Parent dir of this script, to find the PNGs regardless of cwd
11
+ path = Path(__file__).parent
8
12
 
9
13
  ##################################
10
14
  # Define some Components:
@@ -66,7 +70,7 @@ class RenderProcessor:
66
70
  destination.y = int(rend.y)
67
71
  destination.w = rend.w
68
72
  destination.h = rend.h
69
- SDL_RenderCopy(self.renderer.renderer, rend.texture, None, destination)
73
+ SDL_RenderCopy(self.renderer.sdlrenderer, rend.texture, None, destination)
70
74
  self.renderer.present()
71
75
 
72
76
 
@@ -76,7 +80,7 @@ class RenderProcessor:
76
80
  def texture_from_image(renderer, image_name):
77
81
  """Create an SDL2 Texture from an image file"""
78
82
  soft_surface = ext.load_image(image_name)
79
- texture = SDL_CreateTextureFromSurface(renderer.renderer, soft_surface)
83
+ texture = SDL_CreateTextureFromSurface(renderer.sdlrenderer, soft_surface)
80
84
  SDL_FreeSurface(soft_surface)
81
85
  return texture
82
86
 
@@ -94,11 +98,11 @@ def run():
94
98
  # Initialize Esper world, and create a "player" Entity with a few Components.
95
99
  player = esper.create_entity()
96
100
  esper.add_component(player, Velocity(x=0, y=0))
97
- esper.add_component(player, Renderable(texture=texture_from_image(renderer, "redsquare.png"),
101
+ esper.add_component(player, Renderable(texture=texture_from_image(renderer, str(path / "redsquare.png")),
98
102
  width=64, height=64, posx=100, posy=100))
99
103
  # Another motionless Entity:
100
104
  enemy = esper.create_entity()
101
- esper.add_component(enemy, Renderable(texture=texture_from_image(renderer, "bluesquare.png"),
105
+ esper.add_component(enemy, Renderable(texture=texture_from_image(renderer, str(path / "bluesquare.png")),
102
106
  width=64, height=64, posx=400, posy=250))
103
107
 
104
108
  # Create some Processor instances, and asign them to be processed.
@@ -11,6 +11,7 @@ def _reset_to_zero():
11
11
  # Wipe out all world contexts
12
12
  # and re-create the default.
13
13
  esper._context_map.clear()
14
+ esper._current_world = None # type: ignore[assignment]
14
15
  esper.switch_world("default")
15
16
 
16
17
 
@@ -709,6 +710,55 @@ def test_delete_world():
709
710
  with pytest.raises(KeyError):
710
711
  esper.delete_world("non_existent")
711
712
 
713
+
714
+ def test_switch_world_cache_dirty():
715
+ """Verify that switching worlds preserves the cache dirty flag.
716
+
717
+ This test reproduces the bug from issue #121: get_component returns
718
+ stale data after switching to an already-existing world that was
719
+ modified by add_component.
720
+ """
721
+ esper.switch_world("issue121_test")
722
+
723
+ class Component:
724
+ pass
725
+
726
+ e = esper.create_entity()
727
+
728
+ c = esper.get_component(Component)
729
+ assert len(c) == 0
730
+
731
+ esper.add_component(e, Component())
732
+
733
+ esper.switch_world("issue121_test")
734
+
735
+ c = esper.get_component(Component)
736
+ assert len(c) == 1
737
+
738
+ esper.switch_world("default")
739
+ esper.delete_world("issue121_test")
740
+
741
+
742
+ def test_switch_world_same_entities_preserved():
743
+ """Verify that switching to a world and back preserves entities and components."""
744
+ esper.switch_world("world_a")
745
+ e = esper.create_entity()
746
+ esper.add_component(e, ComponentA())
747
+
748
+ esper.switch_world("world_b")
749
+ c = esper.get_component(ComponentA)
750
+ assert len(c) == 0
751
+
752
+ esper.switch_world("world_a")
753
+ c = esper.get_component(ComponentA)
754
+ assert len(c) == 1
755
+ assert c[0][1] is not None
756
+
757
+ esper.switch_world("default")
758
+ esper.delete_world("world_a")
759
+ esper.delete_world("world_b")
760
+
761
+
712
762
  ##################################################
713
763
  # Some helper functions and Component templates:
714
764
  ##################################################
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes