Skip to content

Commit

Permalink
Merge #402
Browse files Browse the repository at this point in the history
402: Pull event name transformation into engine. r=astronouth7303 a=pathunstrom

Closes #390 

Co-authored-by: Piper Thunstrom <[email protected]>
  • Loading branch information
bors[bot] and pathunstrom authored Mar 29, 2020
2 parents 78df9c6 + c63319e commit 991680f
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 155 deletions.
60 changes: 45 additions & 15 deletions ppb/engine.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import time
from collections import defaultdict
from collections import deque
from contextlib import ExitStack
from itertools import chain
import time
from typing import Any
from typing import Callable
from typing import DefaultDict
Expand All @@ -11,19 +11,33 @@
from typing import Union

from ppb import events
from ppb.eventlib import EventMixin
from ppb.assetlib import AssetLoadingSystem
from ppb.errors import BadEventHandlerException
from ppb.systems import EventPoller
from ppb.systems import Renderer
from ppb.systems import Updater
from ppb.systems import SoundController
from ppb.assetlib import AssetLoadingSystem
from ppb.systems import Updater
from ppb.utils import LoggingMixin

from ppb.utils import camel_to_snake

_ellipsis = type(...)

_cached_handler_names = {}


class GameEngine(EventMixin, LoggingMixin):
def _get_handler_name(txt):
result = _cached_handler_names.get(txt)
if result is None:
result = "on_" + camel_to_snake(txt)
_cached_handler_names[txt] = result
return result


for x in events.__all__:
_get_handler_name(x)


class GameEngine(LoggingMixin):

def __init__(self, first_scene: Type, *,
basic_systems=(Renderer, Updater, EventPoller, SoundController, AssetLoadingSystem),
Expand Down Expand Up @@ -127,17 +141,26 @@ def publish(self):
scene = self.current_scene
event.scene = scene
extensions = chain(self.event_extensions[type(event)], self.event_extensions[...])

# Hydrating extensions.
for callback in extensions:
callback(event)
self.__event__(event, self.signal)
for system in self.systems:
system.__event__(event, self.signal)
# Required for if we publish with no current scene.
# Should only happen when the last scene stops via event.
if scene is not None:
scene.__event__(event, self.signal)
for game_object in scene:
game_object.__event__(event, self.signal)

event_handler_name = _get_handler_name(type(event).__name__)
for obj in self.walk():
method = getattr(obj, event_handler_name, None)
if callable(method):
try:
method(event, self.signal)
except TypeError as ex:
from inspect import signature
sig = signature(method)
try:
sig.bind(event, self.signal)
except TypeError:
raise BadEventHandlerException(obj, event_handler_name, event) from ex
else:
raise

def on_start_scene(self, event: events.StartScene, signal: Callable[[Any], None]):
"""
Expand Down Expand Up @@ -212,3 +235,10 @@ def flush_events(self):
the wrong scene.
"""
self.events = deque()

def walk(self):
yield self
yield from self.systems
yield self.current_scene
if self.current_scene is not None:
yield from self.current_scene
25 changes: 25 additions & 0 deletions ppb/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class BadEventHandlerException(TypeError):

def __init__(self, instance, method, event):
object_type = type(instance)
event_type = type(event)
o_name = object_type.__name__
e_name = event_type.__name__
article = ['a', 'an'][int(e_name.lower()[0] in "aeiou")]

message = f"""
{o_name}.{method}() signature incorrect, it should accept {article} {e_name} object and a signal function.
{e_name} is a dataclass that represents an event. Its attributes
tell you about the event.
The signal function is a function you can call that accepts an event instance
as its only parameter. Call it to add an event to the queue. You don't have to
use it, but it is a mandatory argument provided by ppb.
It should look like this:
def {method}({e_name.lower()}_event: {e_name}, signal_function):
(Your code goes here.)
"""
super().__init__(message)
67 changes: 0 additions & 67 deletions ppb/eventlib.py

This file was deleted.

3 changes: 1 addition & 2 deletions ppb/features/twophase.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""
from dataclasses import dataclass
from ppb.systemslib import System
from ppb.eventlib import EventMixin

__all__ = 'Commit',

Expand All @@ -24,7 +23,7 @@ def on_update(self, event, signal):
signal(Commit())


class TwoPhaseMixin(EventMixin):
class TwoPhaseMixin:
"""
Mixin to apply to objects to handle two phase updates.
"""
Expand Down
3 changes: 1 addition & 2 deletions ppb/scenes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing import Type

from ppb.camera import Camera
from ppb.eventlib import EventMixin


class GameObjectCollection(Collection):
Expand Down Expand Up @@ -96,7 +95,7 @@ def remove(self, game_object: Hashable) -> None:
s.discard(game_object)


class BaseScene(EventMixin):
class BaseScene:
# Background color, in RGB, each channel is 0-255
background_color: Sequence[int] = (0, 0, 100)
container_class: Type = GameObjectCollection
Expand Down
3 changes: 1 addition & 2 deletions ppb/sprites.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from ppb_vector import Vector

import ppb
from ppb.eventlib import EventMixin
from ppb.utils import FauxFloat

__all__ = (
Expand All @@ -35,7 +34,7 @@
side_attribute_error_message = error_message.format


class BaseSprite(EventMixin):
class BaseSprite:
"""
The base Sprite class. All sprites should inherit from this (directly or
indirectly).
Expand Down
5 changes: 1 addition & 4 deletions ppb/systemslib.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
from ppb import eventlib


class System(eventlib.EventMixin):
class System:

def __init__(self, **_):
pass
Expand Down
16 changes: 13 additions & 3 deletions ppb/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
import sys
import numbers
import math
import numbers
import re
import sys

__all__ = 'LoggingMixin', 'FauxFloat',
__all__ = 'LoggingMixin', 'FauxFloat', 'camel_to_snake'


# Dictionary mapping file names -> module names
Expand All @@ -22,6 +23,15 @@ def _build_index():
}


_boundaries_finder = re.compile('(.)([A-Z][a-z]+)')
_boundaries_finder_2 = re.compile('([a-z0-9])([A-Z])')


def camel_to_snake(txt):
s1 = _boundaries_finder.sub(r'\1_\2', txt)
return _boundaries_finder_2.sub(r'\1_\2', s1).lower()


def _get_module(file_name):
"""
Find the module name for the given file name, or raise KeyError if it's
Expand Down
58 changes: 2 additions & 56 deletions tests/test_events.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,8 @@
from pytest import mark
from pytest import raises

from ppb.eventlib import BadEventHandlerException
from ppb.eventlib import camel_to_snake
from ppb.eventlib import EventMixin


def test_eventmixin():
passed_bag = None
passed_fire = None

class Spam:
pass

class Eventable(EventMixin):
def on_spam(self, bag, fire_event):
nonlocal passed_bag, passed_fire
passed_fire = fire_event
passed_bag = bag

bag = Spam()
fire_event = lambda: None

e = Eventable()

e.__event__(bag, fire_event)
assert bag is passed_bag
assert fire_event is passed_fire


def test_event_mixin_with_bad_signature():

class BadSpam:
pass


class Spam:
pass


class Eventable(EventMixin):
def on_spam(self, spam_event):
pass

def on_bad_spam(self, bad_spam_event, signal):
raise TypeError

e = Eventable()

with raises(BadEventHandlerException):
e.__event__(Spam(), lambda x: None)

with raises(TypeError) as exception_info:
e.__event__(BadSpam(), lambda x: None)

exec = exception_info.value
assert not isinstance(exec, BadEventHandlerException)

from ppb.errors import BadEventHandlerException
from ppb.utils import camel_to_snake

@mark.parametrize("text,expected", [
("CamelCase", "camel_case"),
Expand Down
8 changes: 4 additions & 4 deletions tests/test_testutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
from ppb.events import Quit


@mark.parametrize("loop_count", range(1, 6))
@mark.parametrize("loop_count", list(range(1, 6)))
def test_quitter(loop_count):
quitter = testutil.Quitter(loop_count=loop_count)
signal_mock = Mock()
for i in range(loop_count):
quitter.__event__(Idle(.01), signal_mock)
quitter.on_idle(Idle(.01), signal_mock)
signal_mock.assert_called_once()
assert len(signal_mock.call_args[0]) == 1
assert len(signal_mock.call_args[1]) == 0
Expand All @@ -25,7 +25,7 @@ def test_failer_immediate():
failer = testutil.Failer(fail=lambda e: True, message="Expected failure.", engine=None)

with raises(AssertionError):
failer.__event__(Idle(0.0), lambda x: None)
failer.on_idle(Idle(0.0), lambda x: None)


def test_failer_timed():
Expand All @@ -35,7 +35,7 @@ def test_failer_timed():

while True:
try:
failer.__event__(Idle(0.0), lambda x: None)
failer.on_idle(Idle(0.0), lambda x: None)
except AssertionError as e:
if e.args[0] == "Test ran too long.":
end_time = monotonic()
Expand Down

0 comments on commit 991680f

Please sign in to comment.