diff --git a/screenpy/actions/make_note.py b/screenpy/actions/make_note.py index a1eac100..617afc7f 100644 --- a/screenpy/actions/make_note.py +++ b/screenpy/actions/make_note.py @@ -9,6 +9,7 @@ from screenpy.exceptions import UnableToAct from screenpy.pacing import aside, beat from screenpy.protocols import Answerable, ErrorKeeper +from screenpy.speech_tools import represent_prop SelfMakeNote = TypeVar("SelfMakeNote", bound="MakeNote") T_Q = Union[Answerable, object] @@ -54,9 +55,9 @@ def as_(self: SelfMakeNote, key: str) -> SelfMakeNote: def describe(self: SelfMakeNote) -> str: """Describe the Action in present tense.""" - return f"Make a note under {self.key}." + return f"Make a note under {represent_prop(self.key)}." - @beat('{} jots something down under "{key}".') + @beat("{} jots something down under {key_to_log}.") def perform_as(self: SelfMakeNote, the_actor: Actor) -> None: """Direct the Actor to take a note.""" if self.key is None: @@ -81,3 +82,4 @@ def __init__( ) -> None: self.question = question self.key = key + self.key_to_log = represent_prop(key) diff --git a/screenpy/actions/see.py b/screenpy/actions/see.py index fb4a75c0..ba03e4ed 100644 --- a/screenpy/actions/see.py +++ b/screenpy/actions/see.py @@ -9,7 +9,7 @@ from screenpy.actor import Actor from screenpy.pacing import aside, beat from screenpy.protocols import Answerable, ErrorKeeper, Resolvable -from screenpy.speech_tools import get_additive_description +from screenpy.speech_tools import get_additive_description, represent_prop SelfSee = TypeVar("SelfSee", bound="See") T_Q = Union[Answerable, object] @@ -56,7 +56,7 @@ def perform_as(self: SelfSee, the_actor: Actor) -> None: else: # must be a value instead of a question! value = self.question - aside(f"the actual value is: {value}") + aside(f"the actual value is: {represent_prop(value)}") reason = "" if isinstance(self.question, ErrorKeeper): diff --git a/screenpy/pacing.py b/screenpy/pacing.py index d0b8a29a..cdc585fc 100644 --- a/screenpy/pacing.py +++ b/screenpy/pacing.py @@ -9,6 +9,7 @@ from typing import Any, Callable, Optional from screenpy.narration import Narrator, StdOutAdapter +from screenpy.speech_tools import represent_prop Function = Callable[..., Any] the_narrator: Narrator = Narrator(adapters=[StdOutAdapter()]) @@ -88,7 +89,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: with the_narrator.stating_a_beat(func, completed_line, gravitas) as n_func: retval = n_func(*args, **kwargs) if retval is not None: - aside(f"=> {retval}") + aside(f"=> {represent_prop(retval)}") return retval return wrapper diff --git a/screenpy/resolutions/contains_the_entry.py b/screenpy/resolutions/contains_the_entry.py index 2dc12553..7fac3ade 100644 --- a/screenpy/resolutions/contains_the_entry.py +++ b/screenpy/resolutions/contains_the_entry.py @@ -9,6 +9,7 @@ from screenpy.exceptions import UnableToFormResolution from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop K = TypeVar("K", bound=Hashable) V = TypeVar("V") @@ -78,4 +79,6 @@ def __init__(self, *kv_args: Any, **kv_kwargs: Any) -> None: ] self.entries = dict(pairs, **kv_kwargs) self.entry_plural = "entries" if len(self.entries) != 1 else "entry" - self.entries_to_log = ", ".join(f"{k}->{v}" for k, v in self.entries.items()) + self.entries_to_log = ", ".join( + f"{represent_prop(k)}->{represent_prop(v)}" for k, v in self.entries.items() + ) diff --git a/screenpy/resolutions/contains_the_item.py b/screenpy/resolutions/contains_the_item.py index c9c3f548..29362b4b 100644 --- a/screenpy/resolutions/contains_the_item.py +++ b/screenpy/resolutions/contains_the_item.py @@ -8,6 +8,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop T = TypeVar("T") @@ -24,12 +25,13 @@ class ContainsTheItem: def describe(self) -> str: """Describe the Resolution's expectation.""" - return f'A sequence containing "{self.item}".' + return f"A sequence containing {self.item_to_log}." - @beat('... hoping it contains "{item}".') + @beat("... hoping it contains {item_to_log}.") def resolve(self) -> Matcher[Sequence[T]]: """Produce the Matcher to make the assertion.""" return has_item(self.item) def __init__(self, item: T) -> None: self.item = item + self.item_to_log = represent_prop(item) diff --git a/screenpy/resolutions/contains_the_key.py b/screenpy/resolutions/contains_the_key.py index e5194c6b..946e59c4 100644 --- a/screenpy/resolutions/contains_the_key.py +++ b/screenpy/resolutions/contains_the_key.py @@ -8,6 +8,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop K = TypeVar("K", bound=Hashable) @@ -22,12 +23,13 @@ class ContainsTheKey(Generic[K]): def describe(self) -> str: """Describe the Resolution's expectation.""" - return f'Containing the key "{self.key}".' + return f"Containing the key {self.key_to_log}." - @beat('... hoping it\'s a dict containing the key "{key}".') + @beat("... hoping it's a dict containing the key {key_to_log}.") def resolve(self) -> Matcher[Mapping[K, Any]]: """Produce the Matcher to make the assertion.""" return has_key(self.key) def __init__(self, key: K) -> None: self.key = key + self.key_to_log = represent_prop(key) diff --git a/screenpy/resolutions/contains_the_text.py b/screenpy/resolutions/contains_the_text.py index cd874ba4..6114ba42 100644 --- a/screenpy/resolutions/contains_the_text.py +++ b/screenpy/resolutions/contains_the_text.py @@ -6,6 +6,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop class ContainsTheText: @@ -20,12 +21,13 @@ class ContainsTheText: def describe(self) -> str: """Describe the Resolution's expectation.""" - return f'Containing the text "{self.text}".' + return f"Containing the text {self.text_to_log}." - @beat('... hoping it contains "{text}".') + @beat("... hoping it contains {text_to_log}.") def resolve(self) -> Matcher[str]: """Produce the Matcher to make the assertion.""" return contains_string(self.text) def __init__(self, text: str) -> None: self.text = text + self.text_to_log = represent_prop(text) diff --git a/screenpy/resolutions/contains_the_value.py b/screenpy/resolutions/contains_the_value.py index c8d3b63b..909dbc94 100644 --- a/screenpy/resolutions/contains_the_value.py +++ b/screenpy/resolutions/contains_the_value.py @@ -8,6 +8,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop V = TypeVar("V") @@ -24,12 +25,13 @@ class ContainsTheValue(Generic[V]): def describe(self) -> str: """Describe the Resolution's expectation.""" - return f'Containing the value "{self.value}".' + return f"Containing the value {self.value_to_log}." - @beat('... hoping it contains the value "{value}".') + @beat("... hoping it contains the value {value_to_log}.") def resolve(self) -> Matcher[Mapping[Any, V]]: """Produce the Matcher to form the assertion.""" return has_value(self.value) def __init__(self, value: V) -> None: self.value = value + self.value_to_log = represent_prop(value) diff --git a/screenpy/resolutions/custom_matchers/sequence_containing_pattern.py b/screenpy/resolutions/custom_matchers/sequence_containing_pattern.py index afd575b6..dc2fb783 100644 --- a/screenpy/resolutions/custom_matchers/sequence_containing_pattern.py +++ b/screenpy/resolutions/custom_matchers/sequence_containing_pattern.py @@ -28,7 +28,7 @@ def _matches(self, item: Sequence[str]) -> bool: def describe_to(self, description: Description) -> None: """Describe the passing case.""" description.append_text( - f"a sequence containing an element which matches {self.pattern}" + f'a sequence containing an element which matches r"{self.pattern}"' ) def describe_match(self, _: Sequence[str], match_description: Description) -> None: @@ -43,7 +43,7 @@ def describe_mismatch( mismatch_description.append_text("was not a sequence") return mismatch_description.append_text( - f"did not contain an item matching {self.pattern}" + f'did not contain an item matching r"{self.pattern}"' ) diff --git a/screenpy/resolutions/ends_with.py b/screenpy/resolutions/ends_with.py index 00b1996f..e865df60 100644 --- a/screenpy/resolutions/ends_with.py +++ b/screenpy/resolutions/ends_with.py @@ -6,6 +6,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop class EndsWith: @@ -20,12 +21,13 @@ class EndsWith: def describe(self) -> str: """Describe the Resolution's expectation.""" - return f'Ending with "{self.postfix}".' + return f"Ending with {self.postfix_to_log}." - @beat('... hoping it ends with "{postfix}".') + @beat("... hoping it ends with {postfix_to_log}.") def resolve(self) -> Matcher[str]: """Produce the Matcher to make the assertion.""" return ends_with(self.postfix) def __init__(self, postfix: str) -> None: self.postfix = postfix + self.postfix_to_log = represent_prop(postfix) diff --git a/screenpy/resolutions/is_equal_to.py b/screenpy/resolutions/is_equal_to.py index e83e6a37..72c8aa0b 100644 --- a/screenpy/resolutions/is_equal_to.py +++ b/screenpy/resolutions/is_equal_to.py @@ -8,6 +8,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop class IsEqualTo: @@ -22,12 +23,13 @@ class IsEqualTo: def describe(self) -> str: """Describe the Resolution's expectation.""" - return f"Equal to {self.expected}." + return f"Equal to {self.expected_to_log}." - @beat("... hoping it's equal to {expected}.") + @beat("... hoping it's equal to {expected_to_log}.") def resolve(self) -> Matcher[Any]: """Produce the Matcher to make the assertion.""" return equal_to(self.expected) def __init__(self, obj: Any) -> None: self.expected = obj + self.expected_to_log = represent_prop(obj) diff --git a/screenpy/resolutions/is_greater_than.py b/screenpy/resolutions/is_greater_than.py index 9b4a396f..8d669ad3 100644 --- a/screenpy/resolutions/is_greater_than.py +++ b/screenpy/resolutions/is_greater_than.py @@ -8,6 +8,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop class IsGreaterThan: @@ -20,12 +21,13 @@ class IsGreaterThan: def describe(self) -> str: """Describe the Resolution's expectation.""" - return f"Greater than {self.number}." + return f"Greater than {self.number_to_log}." - @beat("... hoping it's greater than {number}.") + @beat("... hoping it's greater than {number_to_log}.") def resolve(self) -> Matcher[Any]: """Produce the Matcher to make the assertion.""" return greater_than(self.number) def __init__(self, number: float) -> None: self.number = number + self.number_to_log = represent_prop(number) diff --git a/screenpy/resolutions/is_greater_than_or_equal_to.py b/screenpy/resolutions/is_greater_than_or_equal_to.py index 533dbf05..13555f9d 100644 --- a/screenpy/resolutions/is_greater_than_or_equal_to.py +++ b/screenpy/resolutions/is_greater_than_or_equal_to.py @@ -8,6 +8,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop class IsGreaterThanOrEqualTo: @@ -22,12 +23,13 @@ class IsGreaterThanOrEqualTo: def describe(self) -> str: """Describe the Resolution's expectation.""" - return f"Greater than or equal to {self.number}." + return f"Greater than or equal to {self.number_to_log}." - @beat("... hoping it's greater than or equal to {number}.") + @beat("... hoping it's greater than or equal to {number_to_log}.") def resolve(self) -> Matcher[Any]: """Produce the Matcher to make the assertion.""" return greater_than_or_equal_to(self.number) def __init__(self, number: float) -> None: self.number = number + self.number_to_log = represent_prop(number) diff --git a/screenpy/resolutions/is_less_than.py b/screenpy/resolutions/is_less_than.py index 1bf5c058..9a5bd7c1 100644 --- a/screenpy/resolutions/is_less_than.py +++ b/screenpy/resolutions/is_less_than.py @@ -6,6 +6,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop class IsLessThan: @@ -20,12 +21,13 @@ class IsLessThan: def describe(self) -> str: """Describe the Resolution's expectation.""" - return f"Less than {self.number}." + return f"Less than {self.number_to_log}." - @beat("... hoping it's less than {number}.") + @beat("... hoping it's less than {number_to_log}.") def resolve(self) -> Matcher[float]: """Produce the Matcher to make the assertion.""" return less_than(self.number) def __init__(self, number: float) -> None: self.number = number + self.number_to_log = represent_prop(number) diff --git a/screenpy/resolutions/is_less_than_or_equal_to.py b/screenpy/resolutions/is_less_than_or_equal_to.py index 37951f55..d58bde82 100644 --- a/screenpy/resolutions/is_less_than_or_equal_to.py +++ b/screenpy/resolutions/is_less_than_or_equal_to.py @@ -6,6 +6,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop class IsLessThanOrEqualTo: @@ -20,12 +21,13 @@ class IsLessThanOrEqualTo: def describe(self) -> str: """Describe the Resolution's expectation.""" - return f"Less than or equal to {self.number}." + return f"Less than or equal to {self.number_to_log}." - @beat("... hoping it's less than or equal to {number}.") + @beat("... hoping it's less than or equal to {number_to_log}.") def resolve(self) -> Matcher[float]: """Produce the Matcher to make the assertion.""" return less_than_or_equal_to(self.number) def __init__(self, number: float) -> None: self.number = number + self.number_to_log = represent_prop(number) diff --git a/screenpy/resolutions/reads_exactly.py b/screenpy/resolutions/reads_exactly.py index 310da5fc..fe2bf377 100644 --- a/screenpy/resolutions/reads_exactly.py +++ b/screenpy/resolutions/reads_exactly.py @@ -6,6 +6,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop class ReadsExactly: @@ -20,12 +21,13 @@ class ReadsExactly: def describe(self) -> str: """Describe the Resolution's expectation.""" - return f'"{self.text}", verbatim.' + return f"{self.text_to_log}, verbatim." - @beat('... hoping it\'s "{text}", verbatim.') + @beat("... hoping it's {text_to_log}, verbatim.") def resolve(self) -> Matcher[object]: """Produce the Matcher to make the assertion.""" return has_string(self.text) def __init__(self, text: str) -> None: self.text = text + self.text_to_log = represent_prop(text) diff --git a/screenpy/resolutions/starts_with.py b/screenpy/resolutions/starts_with.py index 290080e2..b57960a0 100644 --- a/screenpy/resolutions/starts_with.py +++ b/screenpy/resolutions/starts_with.py @@ -6,6 +6,7 @@ from hamcrest.core.matcher import Matcher from screenpy.pacing import beat +from screenpy.speech_tools import represent_prop class StartsWith: @@ -20,12 +21,13 @@ class StartsWith: def describe(self) -> str: """Describe the Resolution's expectation.""" - return f'Starting with "{self.prefix}".' + return f"Starting with {self.prefix_to_log}." - @beat('... hoping it starts with "{prefix}".') + @beat("... hoping it starts with {prefix_to_log}.") def resolve(self) -> Matcher[str]: """Produce the Matcher to make the assertion.""" return starts_with(self.prefix) def __init__(self, prefix: str) -> None: self.prefix = prefix + self.prefix_to_log = represent_prop(prefix) diff --git a/screenpy/speech_tools.py b/screenpy/speech_tools.py index 83fefd1a..d3352760 100644 --- a/screenpy/speech_tools.py +++ b/screenpy/speech_tools.py @@ -3,12 +3,17 @@ """ import re -from typing import Any, Union +from typing import TypeVar, Union, overload + +from hamcrest.core.helpers.hasmethod import hasmethod +from hamcrest.core.helpers.ismock import ismock from screenpy.protocols import Answerable, Describable, Performable, Resolvable +T = TypeVar("T") + -def get_additive_description(describable: Union[Describable, Any]) -> str: +def get_additive_description(describable: Union[Describable, T]) -> str: """Extract a description that can be placed within a sentence. The ``describe`` method of Describables will provide a description, @@ -44,3 +49,27 @@ class name: replace each capital letter with a space and a lower-case description = f"the {describable.__class__.__name__}" return description + + +@overload +def represent_prop(item: str) -> str: + ... + + +@overload +def represent_prop(item: T) -> T: + ... + + +def represent_prop(item: Union[str, T]) -> Union[str, T]: + """represent items in a manner suitable for the audience (logging)""" + if not ismock(item) and hasmethod(item, "describe_to"): + return f"{item}" + if isinstance(item, str): + return repr(item) + + description = str(item) + if description[:1] == "<" and description[-1:] == ">": + return item + + return f"<{item}>" diff --git a/tests/test_actions.py b/tests/test_actions.py index 56ef0b8e..a6b55f47 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -134,7 +134,6 @@ def test_describe(self) -> None: class TestEventually: - settings_path = "screenpy.actions.eventually.settings" def test_can_be_instantiated(self) -> None: @@ -311,7 +310,7 @@ def test_mention_multiple_errors_once(self, mocked_time, Tester): Eventually(See(mock_question, IsEqualTo(False))).perform_as(Tester) assert str(actual_exception.value) == ( - "Tester tried to Eventually see if returns bool is equal to False 3 times " + "Tester tried to Eventually see if returns bool is equal to 3 times " "over 20.0 seconds, but got:\n" " AssertionError: \n" "Expected: \n" @@ -417,7 +416,7 @@ def test_using_note_immediately_raises_with_docs(self, Tester) -> None: assert "screenpy-docs.readthedocs.io" in str(exc.value) def test_describe(self) -> None: - assert MakeNote(None).as_("blah").describe() == "Make a note under blah." + assert MakeNote(None).as_("blah").describe() == "Make a note under 'blah'." @mock.patch("screenpy.actions.make_note.aside", autospec=True) def test_caught_exception_noted(self, mock_aside: mock.Mock, Tester) -> None: @@ -560,6 +559,36 @@ def test_describe(self) -> None: == "See if can you speak is only this sentence." ) + def test_log_passes(self, Tester, caplog) -> None: + with caplog.at_level(logging.INFO): + See(SimpleQuestion(), IsEqualTo(True)).perform_as(Tester) + + assert [r.msg for r in caplog.records] == [ + "Tester sees if simpleQuestion is equal to .", + " Tester examines SimpleQuestion", + " => ", + " ... hoping it's equal to .", + " => ", + ] + + def test_log_fails(self, Tester, caplog) -> None: + with caplog.at_level(logging.INFO), pytest.raises(AssertionError): + See(SimpleQuestion(), IsEqualTo(False)).perform_as(Tester) + + assert [r.msg for r in caplog.records] == [ + "Tester sees if simpleQuestion is equal to .", + " Tester examines SimpleQuestion", + " => ", + " ... hoping it's equal to .", + " => ", + # don't be fooled! the next few lines do not have commas on purpose + " ***ERROR***\n" + "\n" + "AssertionError: \n" + "Expected: \n" + " but: was \n", + ] + class TestSeeAllOf: def test_can_be_instantiated(self) -> None: @@ -610,9 +639,11 @@ def test_raises_assertionerror_if_one_fails(self, Tester) -> None: (FakeQuestion(), IsEqualTo(True)), ).perform_as(Tester) - def test_stops_at_first_failure(self, Tester) -> None: + def test_log_first_failure(self, Tester, caplog) -> None: mock_question = FakeQuestion() + caplog.set_level(logging.INFO) + with pytest.raises(AssertionError): SeeAllOf( (mock_question, IsEqualTo(True)), @@ -622,8 +653,24 @@ def test_stops_at_first_failure(self, Tester) -> None: ).perform_as(Tester) assert mock_question.answered_by.call_count == 2 + assert [r.msg for r in caplog.records] == [ + "Tester sees if all of 4 tests pass:", + " Tester sees if fakeQuestion is equal to .", + " ... hoping it's equal to .", + " => ", + " Tester sees if fakeQuestion is equal to .", + " ... hoping it's equal to .", + " => ", + # don't be fooled! the next few lines do not have commas on purpose + " ***ERROR***\n" + "\n" + "AssertionError: \n" + "Expected: \n" + " but: was \n", + ] - def test_passes_if_all_pass(self, Tester) -> None: + def test_log_all_pass(self, Tester, caplog) -> None: + caplog.set_level(logging.INFO) # test passes if no exception is raised SeeAllOf( (FakeQuestion(), IsEqualTo(True)), @@ -632,6 +679,22 @@ def test_passes_if_all_pass(self, Tester) -> None: (FakeQuestion(), IsEqualTo(True)), ).perform_as(Tester) + assert [r.msg for r in caplog.records] == [ + "Tester sees if all of 4 tests pass:", + " Tester sees if fakeQuestion is equal to .", + " ... hoping it's equal to .", + " => ", + " Tester sees if fakeQuestion is equal to .", + " ... hoping it's equal to .", + " => ", + " Tester sees if fakeQuestion is equal to .", + " ... hoping it's equal to .", + " => ", + " Tester sees if fakeQuestion is equal to .", + " ... hoping it's equal to .", + " => ", + ] + def test_describe(self) -> None: test = (FakeQuestion(), IsEqualTo(True)) tests = ( @@ -697,17 +760,34 @@ def test_raises_assertionerror_if_none_pass(self, Tester) -> None: assert "did not find any expected answers" in str(actual_exception) - def test_stops_at_first_pass(self, Tester) -> None: + def test_log_first_pass(self, Tester, caplog) -> None: mock_question = FakeQuestion() + caplog.set_level(logging.INFO) + SeeAnyOf( (mock_question, IsEqualTo(False)), - (mock_question, IsEqualTo(True)), # <-- + (mock_question, IsEqualTo(True)), (mock_question, IsEqualTo(True)), (mock_question, IsEqualTo(True)), ).perform_as(Tester) assert mock_question.answered_by.call_count == 2 + assert [r.msg for r in caplog.records] == [ + "Tester sees if any of 4 tests pass:", + " Tester sees if fakeQuestion is equal to .", + " ... hoping it's equal to .", + " => ", + # don't be fooled! the next few lines do not have commas on purpose + " ***ERROR***\n" + "\n" + "AssertionError: \n" + "Expected: \n" + " but: was \n", + " Tester sees if fakeQuestion is equal to .", + " ... hoping it's equal to .", + " => ", + ] def test_passes_with_one_pass(self, Tester) -> None: # test passes if no exception is raised @@ -917,10 +997,10 @@ def test_unabridged_set_outside_silently(self, Tester, caplog) -> None: assert [r.msg for r in caplog.records] == [ "Tester tries to Action2", - " Tester sees if simpleQuestion is equal to True.", + " Tester sees if simpleQuestion is equal to .", " Tester examines SimpleQuestion", - " => True", - " ... hoping it's equal to True.", + " => ", + " ... hoping it's equal to .", " => ", ] @@ -937,6 +1017,7 @@ class Action3: The results of this test show the strange behavior. """ + @beat("{} tries to Action3") def perform_as(self, the_actor: Actor) -> None: settings.UNABRIDGED_NARRATION = True @@ -956,10 +1037,10 @@ def perform_as(self, the_actor: Actor) -> None: "Tester tries to Action3", # you'd think this wouldn't be here! " Tester tries to Action1", " Tester tries to Action2", - " Tester sees if simpleQuestion is equal to True.", + " Tester sees if simpleQuestion is equal to .", " Tester examines SimpleQuestion", - " => True", - " ... hoping it's equal to True.", + " => ", + " ... hoping it's equal to .", " => ", ] @@ -976,6 +1057,7 @@ class Action4: The results of this test show the strange behavior. """ + @beat("{} tries to Action4") def perform_as(self, the_actor: Actor) -> None: settings.UNABRIDGED_NARRATION = True @@ -1017,9 +1099,9 @@ def test_describe(self) -> None: mock_action2 = FakeAction() mock_action2.describe.return_value = "produce stuff!" - + t = Either(mock_action1).or_(mock_action2) - assert (t.describe() == "Either do thing or produce stuff") + assert t.describe() == "Either do thing or produce stuff" def test_multi_action_describe(self) -> None: mock_action1 = FakeAction() @@ -1032,13 +1114,13 @@ def test_multi_action_describe(self) -> None: mock_action4.describe.return_value = "PerformBar." t = Either(mock_action1, mock_action2).or_(mock_action3, mock_action4) - assert (t.describe() == "Either doThing, doStuff or performFoo, performBar") + assert t.describe() == "Either doThing, doStuff or performFoo, performBar" def test_first_action_passes(self, Tester, mocker: MockerFixture) -> None: mock_clear = mocker.spy(the_narrator, "clear_backup") mock_flush = mocker.spy(the_narrator, "flush_backup") mock_kink = mocker.spy(the_narrator, "mic_cable_kinked") - + action1 = FakeAction() action2 = FakeAction() Either(action1).or_(action2).perform_as(Tester) @@ -1053,12 +1135,12 @@ def test_first_action_fails(self, Tester, mocker: MockerFixture) -> None: mock_clear = mocker.spy(the_narrator, "clear_backup") mock_flush = mocker.spy(the_narrator, "flush_backup") mock_kink = mocker.spy(the_narrator, "mic_cable_kinked") - + exc = AssertionError("Wrong!") action1 = FakeAction() action2 = FakeAction() action1.perform_as.side_effect = exc - + Either(action1).or_(action2).perform_as(Tester) assert action1.perform_as.call_count == 1 @@ -1067,7 +1149,9 @@ def test_first_action_fails(self, Tester, mocker: MockerFixture) -> None: assert mock_clear.call_count == 2 assert mock_flush.call_count == 1 - def test_first_action_fails_with_custom_exception(self, Tester, mocker: MockerFixture) -> None: + def test_first_action_fails_with_custom_exception( + self, Tester, mocker: MockerFixture + ) -> None: mock_clear = mocker.spy(the_narrator, "clear_backup") mock_flush = mocker.spy(the_narrator, "flush_backup") mock_kink = mocker.spy(the_narrator, "mic_cable_kinked") @@ -1089,7 +1173,6 @@ class CustomException(Exception): assert mock_flush.call_count == 1 def test_output_first_fails(self, Tester, caplog): - class FakeActionFail(Performable): @beat("{} tries to FakeActionFail") def perform_as(self, actor: Actor): @@ -1118,7 +1201,7 @@ def perform_as(self, actor: Actor): caplog.set_level(logging.INFO) mock_settings = ScreenPySettings(UNABRIDGED_NARRATION=True) - + with mock.patch(self.settings_path, mock_settings): Either(FakeActionFail()).or_(FakeActionPass()).perform_as(Tester) diff --git a/tests/test_narration_integration.py b/tests/test_narration_integration.py index bf1f7353..f859ee50 100644 --- a/tests/test_narration_integration.py +++ b/tests/test_narration_integration.py @@ -43,7 +43,7 @@ def _assert_stdout_correct(caplog) -> None: assert caplog.messages[1] == f"Scene: {TEST_SCENE.title()}" assert caplog.messages[2] == TEST_BEAT assert caplog.messages[3] == f"{INDENT}{TEST_ASIDE}" - assert caplog.messages[4] == f"{INDENT}=> {TEST_RETVAL}" + assert caplog.messages[4] == f"{INDENT}=> {TEST_RETVAL!r}" class TestNarrateToStdOut: diff --git a/tests/test_resolutions.py b/tests/test_resolutions.py index e9d9c6ca..9537aefb 100644 --- a/tests/test_resolutions.py +++ b/tests/test_resolutions.py @@ -132,9 +132,7 @@ def test_description(self) -> None: cim = ContainsItemMatching(test_pattern) - expected_description = ( - f'A sequence with an item matching the pattern r"{test_pattern}".' - ) + expected_description = 'A sequence with an item matching the pattern r".*".' assert cim.describe() == expected_description @@ -150,11 +148,11 @@ def test_the_test(self) -> None: """Matches dictionaries containing the entry(/ies)""" cte_single = ContainsTheEntry(key="value").resolve() cte_multiple = ContainsTheEntry(key1="value1", key2="value2").resolve() - cte_alt2 = ContainsTheEntry({"key2": "something2"}).resolve() - cte_alt3 = ContainsTheEntry("key3", "something3").resolve() + cte_alt2 = ContainsTheEntry({"key2": 12345}).resolve() + cte_alt3 = ContainsTheEntry("key3", False).resolve() - assert cte_alt2.matches({"key2": "something2"}) - assert cte_alt3.matches({"key3": "something3"}) + assert cte_alt2.matches({"key2": 12345}) + assert cte_alt3.matches({"key3": False}) assert cte_single.matches({"key": "value"}) assert cte_single.matches({"key": "value", "play": "Hamlet"}) assert not cte_single.matches({"play": "Hamlet"}) @@ -166,15 +164,14 @@ def test_the_test(self) -> None: def test_description(self) -> None: test_entry = {"spam": "eggs"} - test_entries = {"tree": "larch", "spam": "eggs"} + test_entries = {"number": 1234, "spam": "eggs"} cte_single = ContainsTheEntry(**test_entry) cte_multiple = ContainsTheEntry(**test_entries) - expected_description_single = "A mapping with the entry spam->eggs." + expected_description_single = "A mapping with the entry 'spam'->'eggs'." expected_description_multiple = ( - "A mapping with the entries" - f" {', '.join(f'{k}->{v}' for k, v in test_entries.items())}." + "A mapping with the entries 'number'-><1234>, 'spam'->'eggs'." ) assert cte_single.describe() == expected_description_single assert cte_multiple.describe() == expected_description_multiple @@ -193,13 +190,13 @@ def test_the_test(self) -> None: assert cti.matches(range(0, 10)) assert not cti.matches({0, 3, 5}) - def test_description(self) -> None: - test_item = 1 - - cti = ContainsTheItem(test_item) - - expected_description = f'A sequence containing "{test_item}".' - assert cti.describe() == expected_description + @pytest.mark.parametrize( + ("arg", "expected"), + ((1, "A sequence containing <1>."), ("1", "A sequence containing '1'.")), + ) + def test_description_uses_represent_prop(self, arg: object, expected: str) -> None: + cti = ContainsTheItem(arg) + assert cti.describe() == expected class TestContainsTheKey: @@ -221,7 +218,7 @@ def test_description(self) -> None: ctk = ContainsTheKey(test_key) - expected_description = f'Containing the key "{test_key}".' + expected_description = "Containing the key 'spam'." assert ctk.describe() == expected_description @@ -243,7 +240,7 @@ def test_description(self) -> None: ctt = ContainsTheText(test_text) - expected_description = f'Containing the text "{test_text}".' + expected_description = "Containing the text 'Wenn ist das Nunstück git und Slotermeyer?'." assert ctt.describe() == expected_description @@ -261,13 +258,14 @@ def test_the_test(self) -> None: assert ctv.matches({"key": "value", "play": "Hamlet"}) assert not ctv.matches({"play": "Hamlet"}) - def test_description(self) -> None: - test_value = 42 - - ctv = ContainsTheValue(test_value) - expected_description = f'Containing the value "{test_value}".' - assert ctv.describe() == expected_description + @pytest.mark.parametrize( + ("arg", "expected"), + ((42, "Containing the value <42>."), ("42", "Containing the value '42'.")), + ) + def test_description_uses_represent_prop(self, arg: object, expected: str) -> None: + ctv = ContainsTheValue(arg) + assert ctv.describe() == expected class TestEmpty: @@ -306,7 +304,7 @@ def test_description(self) -> None: ew = EndsWith(test_postfix) - expected_description = f'Ending with "{test_postfix}".' + expected_description = "Ending with 'got better.'." assert ew.describe() == expected_description @@ -330,7 +328,7 @@ def test_description(self) -> None: hl5 = HasLength(test_length) expected_description1 = "1 item long." - expected_description5 = f"{test_length} items long." + expected_description5 = "5 items long." assert hl1.describe() == expected_description1 assert hl5.describe() == expected_description5 @@ -372,13 +370,13 @@ def test_the_test(self) -> None: assert ie.matches(1) assert not ie.matches(2) - def test_description(self) -> None: - test_object = "my Schwartz" - - ie = IsEqualTo(test_object) - - expected_description = f"Equal to {test_object}." - assert ie.describe() == expected_description + @pytest.mark.parametrize( + ("arg", "expected"), + ((8675, "Equal to <8675>."), ("8675", "Equal to '8675'.")), + ) + def test_description_uses_represent_prop(self, arg: object, expected: str) -> None: + ie = IsEqualTo(arg) + assert ie.describe() == expected class TestIsGreaterThan: @@ -400,7 +398,7 @@ def test_description(self) -> None: igt = IsGreaterThan(test_num) - expected_description = f"Greater than {test_num}." + expected_description = "Greater than <41>." assert igt.describe() == expected_description @@ -423,7 +421,7 @@ def test_description(self) -> None: igtoet = IsGreaterThanOrEqualTo(test_num) - expected_description = f"Greater than or equal to {test_num}." + expected_description = "Greater than or equal to <1337>." assert igtoet.describe() == expected_description @@ -497,7 +495,7 @@ def test_description(self) -> None: ilt = IsLessThan(test_num) - expected_description = f"Less than {test_num}." + expected_description = "Less than <43>." assert ilt.describe() == expected_description @@ -520,7 +518,7 @@ def test_description(self) -> None: iltoet = IsLessThanOrEqualTo(test_num) - expected_description = f"Less than or equal to {test_num}." + expected_description = "Less than or equal to <1337>." assert iltoet.describe() == expected_description @@ -563,7 +561,7 @@ def test_description(self) -> None: m = Matches(test_match) - expected_description = f'Text matching the pattern r"{test_match}".' + expected_description = 'Text matching the pattern r"(spam)+".' assert m.describe() == expected_description @@ -585,7 +583,9 @@ def test_description(self) -> None: re_ = ReadsExactly(test_text) - expected_description = f'"{test_text}", verbatim.' + expected_description = ( + "'I will not buy this record, it is scratched.', verbatim." + ) assert re_.describe() == expected_description @@ -606,5 +606,5 @@ def test_description(self) -> None: sw = StartsWith(test_prefix) - expected_description = f'Starting with "{test_prefix}".' + expected_description = "Starting with 'It was the best of times,'." assert sw.describe() == expected_description diff --git a/tests/test_speech_tools.py b/tests/test_speech_tools.py index e3ca076d..5963aa08 100644 --- a/tests/test_speech_tools.py +++ b/tests/test_speech_tools.py @@ -1,6 +1,6 @@ import pytest -from screenpy.speech_tools import get_additive_description +from screenpy.speech_tools import get_additive_description, represent_prop class ThisIsADescribableWithADescribe: @@ -52,3 +52,15 @@ def test_indescribable(self) -> None: description = get_additive_description(Indescribable()) assert description == "something indescribable" + + +class TestRepresentProp: + def test_str(self): + val = "hello\nworld!" + + assert represent_prop(val) == "'hello\\nworld!'" + + def test_int(self): + val = 1234 + + assert represent_prop(val) == "<1234>" diff --git a/tests/useful_mocks.py b/tests/useful_mocks.py index d1f6f328..03f66ecc 100644 --- a/tests/useful_mocks.py +++ b/tests/useful_mocks.py @@ -8,7 +8,7 @@ def get_mock_action_class() -> Any: class FakeAction(Action): def __new__(cls, *args, **kwargs): rt = mock.create_autospec(FakeAction, instance=True) - rt.describe.return_value = None + rt.describe.return_value = "FakeAction" return rt return FakeAction @@ -18,7 +18,7 @@ def get_mock_question_class() -> Any: class FakeQuestion(Question): def __new__(cls, *args, **kwargs): rt = mock.create_autospec(FakeQuestion, instance=True) - rt.describe.return_value = None + rt.describe.return_value = "FakeQuestion" rt.answered_by.return_value = True return rt @@ -30,7 +30,7 @@ class FakeResolution(Resolution): def __new__(cls, *args, **kwargs): rt = mock.create_autospec(FakeResolution, instance=True) rt.resolve.return_value = rt - rt.describe.return_value = None + rt.describe.return_value = "FakeResolution" return rt return FakeResolution