Skip to content

Commit

Permalink
Split unit tests and type checking into different workflows.
Browse files Browse the repository at this point in the history
  • Loading branch information
benmoran56 committed Dec 21, 2023
1 parent e5d7c90 commit 900c817
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 32 deletions.
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ Design

* World Context

Esper uses the concept of "World" contexts. When you import esper, a default context is active.
You create Entities, assign Components, register Processesors, etc., by calling functions
Esper uses the concept of "World" contexts. When you first `import esper`, a default context is
active. You create Entities, assign Components, register Processesors, etc., by calling functions
on the `esper` module. Entities, Components and Processors can be created, assigned, or deleted
while your game is running. A simple call to `esper.process()` is all that's needed for each
iteration of your game loop. Advanced users can switch contexts, which can be useful for
Expand Down Expand Up @@ -184,19 +184,20 @@ General Usage
World Contexts
--------------
Esper has the capability of supporting multiple "World" contexts. On import, a "default" World is
active. All creation of Entities, assignment of Processors, and all operations exist within the
confines of a World. For advanced use cases Esper allows you to switch between multiple Worlds,
which are completely isolated from each other. This can be useful when different scenes in your
game have different Entities and Processor requirements. World context operations are done with
the following functions::

active. All creation of Entities, assignment of Processors, and all other operations occur within
the confines of the active World. In other words, the World contexts are completely isolated from
each other. For basic games and designs, you may not need to bother with this functionality. A
single default World context can often be enough. For advanced use cases, such as when different
scenes in your game have different Entities and Processor requirements, this functionality can be
quite useful. World context operations are done with the following functions::
*
* esper.list_worlds()
* esper.switch_world(name)
* esper.delete_world(name)

When switching Worlds, be careful of the `name`. If a World doesn't exist, it will be created.
You can delete old Worlds which are no longer needed, but you cannot delete the currently active
World.
When switching Worlds, be mindful of the `name`. If a World doesn't exist, it will be created when
you first switch to it. You can delete old Worlds if they are no longer needed, but you can not
delete the currently active World.

Adding and Removing Processors
------------------------------
Expand Down Expand Up @@ -390,8 +391,8 @@ Contributions to Esper are always welcome, but there are some specific project g

- Pure Python code only: no binary extensions, Cython, etc.
- Try to target all non-EOL Python versions. Exceptions can be made if there is a compelling reason.
- Avoid bloat as much as possible. New features will be considered if they are commonly useful. Generally speaking, we don't want to add functionality that is better handled in another module or library.
- Performance is preferrable to readability.
- Avoid bloat as much as possible. New features will be considered if they are commonly useful. Generally speaking, we don't want to add functionality that is better served by another module or library.
- Performance is preferrable to readability. The public API should remain clean, but ugly internal code is acceptable if it provides a performance benefit. Every cycle counts!

If you have any questions before contributing, feel free to [open an issue].

Expand Down
10 changes: 10 additions & 0 deletions RELEASE_NOTES
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
esper 3.2
=========
Maintenance release

Changes
-------
- Add `esper.current_world` property to easily check the current World context.
- Made some minor docstring corrections, and added some programmer notes.


esper 3.1
=========
Maintenance release
Expand Down
40 changes: 23 additions & 17 deletions esper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from itertools import count as _count


version = '3.1'
version = '3.2'
__version__ = version


Expand Down Expand Up @@ -106,23 +106,19 @@ class Processor:
Processor instances must contain a `process` method, but you are otherwise
free to define the class any way you wish. Processors should be instantiated,
and then added to a :py:class:`esper.World` instance by calling
:py:func:`esper.World.add_processor`. For example::
my_world = World()
and then added to the current World context by calling :py:func:`esper.add_processor`.
For example::
my_processor_instance = MyProcessor()
my_world.add_processor(my_processor_instance)
esper.add_processor(my_processor_instance)
After adding your Processors to a :py:class:`esper.World`, Processor.world
will be set to the World it is in. This allows easy access to the World and
it's methods from your Processor methods. All Processors in a World will have
their `process` methods called by a single call to :py:func:`esper.World.process`,
so you will generally want to iterate over entities with one (or more) calls to
the appropriate world methods::
All the Processors that have been added to the World context will have their
:py:meth:`esper.Processor.process` methods called by a single call to
:py:func:`esper.process`. Inside the `process` method is generally where you
should iterate over Entities with one (or more) calls to the appropriate methods::
def process(self):
for ent, (rend, vel) in self.world.get_components(Renderable, Velocity):
for ent, (rend, vel) in esper.get_components(Renderable, Velocity):
your_code_here()
"""

Expand Down Expand Up @@ -506,9 +502,9 @@ def process(*args: _Any, **kwargs: _Any) -> None:
def timed_process(*args: _Any, **kwargs: _Any) -> None:
"""Track Processor execution time for benchmarking.
This function is identical to :py:func:`esper.process`,
but it will additionally record the elapsed time of each
processor call in the `esper.process_times` dictionary.
This function is identical to :py:func:`esper.process`, but
it additionally records the elapsed time of each processor
call (in milliseconds) in the`esper.process_times` dictionary.
"""
clear_dead_entities()
for processor in _processors:
Expand Down Expand Up @@ -554,7 +550,7 @@ def switch_world(name: str) -> None:
already active.
"""
if name not in _context_map:
# Create a new
# Create a new context if the name does not already exist:
_context_map[name] = (_count(start=1), {}, {}, set(), {}, {}, [], {}, {})

global _current_context
Expand All @@ -568,6 +564,16 @@ def switch_world(name: str) -> None:
global process_times
global event_registry

# switch the references to the objects in the named context_map:
(_entity_count, _components, _entities, _dead_entities, _get_component_cache,
_get_components_cache, _processors, process_times, event_registry) = _context_map[name]
_current_context = name


@property
def current_world() -> str:
"""The currently active World context.
To switch World contexts, see :py:func:`esper.switch_world`.
"""
return _current_context
36 changes: 34 additions & 2 deletions tests/test_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,14 +275,24 @@ def test_clear_dead_entities():


def test_switch_world():
# The `create_entities` helper will add <number>/2 of
# 'ComponentA' to the World context. Make a new
# "left" context, and confirm this is True:
esper.switch_world("left")
assert len(esper.get_component(ComponentA)) == 0
create_entities(200)
assert len(esper.get_component(ComponentA)) == 100

# Switching to a new "right" World context, no
# 'ComponentA' Components should yet exist.
esper.switch_world("right")
assert len(esper.get_component(ComponentA)) == 0
create_entities(300)
assert len(esper.get_component(ComponentA)) == 150

# Switching back to the original "left" context,
# the original 100 Components should still exist.
# From there, 200 more should be added:
esper.switch_world("left")
assert len(esper.get_component(ComponentA)) == 100
create_entities(400)
Expand All @@ -293,6 +303,16 @@ def test_switch_world():
# Some helper functions and Component templates:
##################################################
def create_entities(number):
"""This function will create X number of entities.
The entities are created with a mix of Components,
so the World context will see an addition of
ComponentA * number * 1
ComponentB * number * 1
ComponentC * number * 2
ComponentD * number * 1
ComponentE * number * 1
"""
for _ in range(number // 2):
esper.create_entity(ComponentA(), ComponentB(), ComponentC())
esper.create_entity(ComponentC(), ComponentD(), ComponentE())
Expand Down Expand Up @@ -459,17 +479,29 @@ def test_event_handler_switch_world():
def handler():
nonlocal called
called += 1

# Switch to a new "left" World context, and register
# an event handler. Confirm that it is being called
# by checking that the 'called' variable is incremented.
esper.switch_world("left")
esper.set_handler("foo", handler)
assert called == 0
esper.dispatch_event("foo")
assert called == 1

# Here we switch to a new "right" World context.
# The handler is registered to the "left" context only,
# so dispatching the event should have no effect. The
# handler is not attached, and so the 'called' value
# should not be incremented further.
esper.switch_world("right")
assert called == 1
esper.dispatch_event("foo")
assert called == 1

# Switching back to the "left" context and dispatching
# the event, the handler should still be registered and
# the 'called' variable should be incremented by 1.
esper.switch_world("left")
assert called == 1
esper.dispatch_event("foo")
assert called == 2

Expand Down

0 comments on commit 900c817

Please sign in to comment.