Skip to content
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
12 changes: 8 additions & 4 deletions screenpy/actions/make_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

from screenpy import Actor, Director
from screenpy.exceptions import UnableToAct
from screenpy.pacing import beat
from screenpy.protocols import Answerable
from screenpy.pacing import aside, beat
from screenpy.protocols import Answerable, ErrorKeeper


class MakeNote:
Expand Down Expand Up @@ -57,12 +57,16 @@ def perform_as(self, the_actor: Actor) -> None:
if self.key is None:
raise UnableToAct("No key was provided to name this note.")

if hasattr(self.question, "answered_by"):
value = self.question.answered_by(the_actor)
if isinstance(self.question, Answerable):
value: object = self.question.answered_by(the_actor)
Comment on lines +60 to +61
Copy link
Member

Choose a reason for hiding this comment

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

Seriously, i love how this looks now. Really great addition!

else:
# must be a value instead of a question!
value = self.question

if isinstance(self.question, ErrorKeeper):
aside(f"Making note of {self.question}...")
aside(f"Caught Exception: {self.question.caught_exception}")
Comment on lines +67 to +68
Copy link
Member

Choose a reason for hiding this comment

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

What does this look like in the logs? I'm particularly curious about the "Making note of..." part.

Copy link
Contributor

Choose a reason for hiding this comment

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

11:36:01   INFO Tester jots something down under "test".
11:36:01   INFO     Making note of <Mock id='4553053312'>...
11:36:01   INFO     Caught Exception: <Mock name='mock.caught_exception' id='4553054368'>


Director().notes(self.key, value)

def __init__(
Expand Down
12 changes: 8 additions & 4 deletions screenpy/actions/see.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from screenpy import Actor
from screenpy.pacing import aside, beat
from screenpy.protocols import Answerable
from screenpy.protocols import Answerable, ErrorKeeper
from screenpy.resolutions import BaseResolution
from screenpy.speech_tools import get_additive_description

Expand Down Expand Up @@ -43,14 +43,18 @@ def describe(self) -> str:
@beat("{} sees if {question_to_log} is {resolution_to_log}.")
def perform_as(self, the_actor: Actor) -> None:
"""Direct the Actor to make an observation."""
if hasattr(self.question, "answered_by"):
value = self.question.answered_by(the_actor)
if isinstance(self.question, Answerable):
value: object = self.question.answered_by(the_actor)
else:
# must be a value instead of a question!
value = self.question
aside(f"the actual value is: {value}")

assert_that(value, self.resolution)
reason = ""
if isinstance(self.question, ErrorKeeper):
reason = f"{self.question.caught_exception}"

assert_that(value, self.resolution, reason)

def __init__(
self, question: Union[Answerable, Any], resolution: BaseResolution
Expand Down
21 changes: 20 additions & 1 deletion screenpy/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@

from typing import TYPE_CHECKING, Any, Callable, Generator, Optional

from typing_extensions import Protocol
from typing_extensions import Protocol, runtime_checkable

if TYPE_CHECKING:
from .actor import Actor

# pylint: disable=unused-argument


@runtime_checkable
class Answerable(Protocol):
"""Questions are Answerable"""

Expand All @@ -35,6 +36,7 @@ def answered_by(self, the_actor: "Actor") -> Any:
...


@runtime_checkable
class Forgettable(Protocol):
"""Abilities are Forgettable"""

Expand All @@ -46,6 +48,7 @@ def forget(self) -> None:
...


@runtime_checkable
class Performable(Protocol):
"""Actions that can be performed are Performable"""

Expand All @@ -59,6 +62,22 @@ def perform_as(self, the_actor: "Actor") -> None:
...


@runtime_checkable
class ErrorKeeper(Protocol):
Copy link
Member

Choose a reason for hiding this comment

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

I do think ErrorKeeper is the best name for it, even though Exceptional still has a place in my heart. <3

"""Classes that save exceptions for later are ErrorKeeper(s)"""

caught_exception: Optional[Exception]


@runtime_checkable
class Describable(Protocol):
"""Classes that describe themselves are Describable"""

def describe(self) -> str:
...


@runtime_checkable
class Adapter(Protocol):
"""Required functions for an adapter to the Narrator's microphone.

Expand Down
12 changes: 7 additions & 5 deletions screenpy/speech_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
"""

import re
from typing import Union
from typing import Any, Union

from screenpy.protocols import Answerable, Any, Performable
from screenpy.protocols import Answerable, Describable, Performable


def get_additive_description(describable: Union[Performable, Answerable, Any]) -> str:
def get_additive_description(
describable: Union[Performable, Answerable, Describable, Any]
) -> str:
"""Extract a description that can be placed within a sentence.

The ``describe`` method of Performables and Answerables will provide a
Expand All @@ -24,11 +26,11 @@ class name by replacing each capital letter with a space and a lower-case
stick a "the" in front of the class name. This should make it read like
"the list" or "the str".
"""
if hasattr(describable, "describe"):
if isinstance(describable, Describable):
description = describable.describe() # type: ignore # see PEP 544
description = description[0].lower() + description[1:]
description = re.sub(r"[.,?!;:]*$", r"", description)
elif hasattr(describable, "perform_as") or hasattr(describable, "answered_by"):
elif isinstance(describable, (Answerable, Performable)):
Copy link
Member

Choose a reason for hiding this comment

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

Ugh, it's beautiful.

# No describe method, so fabricate a description from the class name.
description = describable.__class__.__name__
description = re.sub(r"(?<!^)([A-Z])", r" \1", description).lower()
Expand Down
64 changes: 62 additions & 2 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
from screenpy.directions import noted_under
from screenpy.director import Director
from screenpy.exceptions import DeliveryError, UnableToAct, UnableToDirect
from screenpy.protocols import (
Describable,
Performable,
)
from screenpy.resolutions import IsEqualTo
from tests.conftest import mock_settings

Expand All @@ -27,6 +31,11 @@ def test_can_be_instantiated(self):

assert isinstance(atf, AttachTheFile)

def test_implements_protocol(self):
atf = AttachTheFile("")
assert isinstance(atf, Performable)
assert isinstance(atf, Describable)

def test_divines_filename(self):
filename = "thisisonlyatest.png"
filepath = os.sep.join(["this", "is", "a", "test", filename])
Expand Down Expand Up @@ -54,6 +63,11 @@ def test_can_be_instantiated(self):

assert isinstance(d, Debug)

def test_implements_protocol(self):
d = Debug()
assert isinstance(d, Performable)
assert isinstance(d, Describable)

@mock.patch("screenpy.actions.debug.breakpoint")
def test_calls_breakpoint(self, mocked_breakpoint, Tester):
Debug().perform_as(Tester)
Expand Down Expand Up @@ -96,6 +110,11 @@ def test_can_be_instantiated(self):
assert isinstance(e7, Eventually)
assert isinstance(e8, Eventually)

def test_implements_protocol(self):
t = Eventually(None)
assert isinstance(t, Performable)
assert isinstance(t, Describable)

def test_uses_timeframe_builder(self):
ev = Eventually(None).trying_for(1)

Expand Down Expand Up @@ -212,6 +231,11 @@ def test_can_be_instantiated(self):
assert isinstance(mn2, MakeNote)
assert isinstance(mn3, MakeNote)

def test_implements_protocol(self):
m = MakeNote("")
assert isinstance(m, Performable)
assert isinstance(m, Describable)

def test_key_value_set(self):
test_question = "Do I feel lucky?"
test_key = "Well, do you, punk?"
Expand Down Expand Up @@ -262,6 +286,21 @@ def test_using_note_immediately_raises_with_docs(self, Tester):

assert "screenpy-docs.readthedocs.io" in str(exc.value)

@mock.patch("screenpy.actions.make_note.aside")
def test_caught_exception_noted(self, mock_aside: mock.Mock, Tester):
key = "key"
value = "note"
MockQuestion = mock.Mock()
MockQuestion.answered_by.return_value = value
MockQuestion.caught_exception = ValueError("Failure msg")

MakeNote.of_the(MockQuestion).as_(key).perform_as(Tester)
mock_aside.assert_has_calls((
mock.call(f"Making note of {MockQuestion}..."),
mock.call(f"Caught Exception: {MockQuestion.caught_exception}"))
)
return


class TestPause:
def test_can_be_instantiated(self):
Expand All @@ -273,6 +312,11 @@ def test_can_be_instantiated(self):
assert isinstance(p2, Pause)
assert isinstance(p3, Pause)

def test_implements_protocol(self):
p = Pause(1)
assert isinstance(p, Performable)
assert isinstance(p, Describable)

def test_seconds(self):
"""Choosing seconds stores the correct time"""
duration = 20
Expand Down Expand Up @@ -329,18 +373,24 @@ def test_can_be_instantiated(self):
assert isinstance(s1, See)
assert isinstance(s2, See)

def test_implements_protocol(self):
s = See(None, mock.Mock())
assert isinstance(s, Performable)
assert isinstance(s, Describable)

@mock.patch("screenpy.actions.see.assert_that")
def test_calls_assert_that_with_answered_question(self, mocked_assert_that, Tester):
mock_question = mock.Mock()
mock_question.describe.return_value = "What was your mother?"
mock_question.caught_exception = ValueError("Failure msg")
mock_resolution = mock.Mock()
mock_resolution.describe.return_value = "A hamster!"

See.the(mock_question, mock_resolution).perform_as(Tester)

mock_question.answered_by.assert_called_once_with(Tester)
mocked_assert_that.assert_called_once_with(
mock_question.answered_by.return_value, mock_resolution
mock_question.answered_by.return_value, mock_resolution, str(mock_question.caught_exception)
)

@mock.patch("screenpy.actions.see.assert_that")
Expand All @@ -351,7 +401,7 @@ def test_calls_assert_that_with_value(self, mocked_assert_that, Tester):

See.the(test_value, mock_resolution).perform_as(Tester)

mocked_assert_that.assert_called_once_with(test_value, mock_resolution)
mocked_assert_that.assert_called_once_with(test_value, mock_resolution, "")


class TestSeeAllOf:
Expand All @@ -362,6 +412,11 @@ def test_can_be_instantiated(self):
assert isinstance(sao1, SeeAllOf)
assert isinstance(sao2, SeeAllOf)

def test_implements_protocol(self):
s = SeeAllOf(None, None)
assert isinstance(s, Performable)
assert isinstance(s, Describable)

def test_raises_exception_for_too_few_tests(self):
with pytest.raises(UnableToAct):
SeeAllOf(None)
Expand Down Expand Up @@ -407,6 +462,11 @@ def test_can_be_instantiated(self):
assert isinstance(sao1, SeeAnyOf)
assert isinstance(sao2, SeeAnyOf)

def test_implements_protocol(self):
s = SeeAnyOf(None, None)
assert isinstance(s, Performable)
assert isinstance(s, Describable)

def test_raises_exception_for_too_few_tests(self):
with pytest.raises(UnableToAct):
SeeAnyOf(None)
Expand Down
13 changes: 13 additions & 0 deletions tests/test_adapters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging

from screenpy.protocols import Adapter
from screenpy.narration.adapters.stdout_adapter import StdOutAdapter, StdOutManager


Expand All @@ -9,6 +10,10 @@ def prop():


class TestStdOutManager:
def test_instantiate(self):
m = StdOutManager()
assert isinstance(m, StdOutManager)

def test__outdent(self):
manager = StdOutManager()
manager.depth = []
Expand Down Expand Up @@ -38,6 +43,14 @@ def test_step(self, caplog):


class TestStdOutAdapter:
def test_instantiate(self):
a = StdOutAdapter()
assert isinstance(a, StdOutAdapter)

def test_implements_protocol(self):
a = StdOutAdapter()
assert isinstance(a, Adapter)

def test_act(self, caplog):
adapter = StdOutAdapter()
act_name = "test act"
Expand Down