Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull event name transformation into engine. #402

Merged
merged 1 commit into from
Mar 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Odd phrasing?

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():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of losing this test, but I also can't think of a good way to handle it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I think I need to puzzle on how to get it. It's a lot heavier of a test now.


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