From a7f4686220c9ae0fa1c0333eeb3df7d1cc776f4e Mon Sep 17 00:00:00 2001 From: Marcel Wilson Date: Fri, 16 Sep 2022 16:35:23 -0500 Subject: [PATCH] #25 subclassing support for mypy prior to PEP 673 and https://github.com/python/mypy/pull/11666 --- screenpy/actions/make_note.py | 16 ++++++----- screenpy/actions/pause.py | 18 ++++++------- screenpy/actions/see.py | 14 ++++++---- screenpy/actions/see_all_of.py | 12 +++++---- screenpy/actions/see_any_of.py | 12 +++++---- screenpy/actor.py | 49 +++++++++++++++++----------------- screenpy/director.py | 10 ++++--- 7 files changed, 73 insertions(+), 58 deletions(-) diff --git a/screenpy/actions/make_note.py b/screenpy/actions/make_note.py index f10a1bdc..fecc57fd 100644 --- a/screenpy/actions/make_note.py +++ b/screenpy/actions/make_note.py @@ -2,13 +2,15 @@ Make a quick note about the answer to a Question. """ -from typing import Any, Optional, Union +from typing import Any, Optional, Type, TypeVar, Union from screenpy import Actor, Director from screenpy.exceptions import UnableToAct from screenpy.pacing import aside, beat from screenpy.protocols import Answerable, ErrorKeeper +Self = TypeVar("Self", bound="MakeNote") + class MakeNote: """Make a note of a value or the answer to a Question. @@ -29,7 +31,7 @@ class MakeNote: key: Optional[str] @classmethod - def of(cls, question: Union[Answerable, Any]) -> "MakeNote": + def of(cls: Type[Self], question: Union[Answerable, Any]) -> Self: """Supply the Question to answer and its arguments. Aliases: @@ -38,21 +40,21 @@ def of(cls, question: Union[Answerable, Any]) -> "MakeNote": return cls(question) @classmethod - def of_the(cls, question: Union[Answerable, Any]) -> "MakeNote": + def of_the(cls: Type[Self], question: Union[Answerable, Any]) -> Self: """Alias for :meth:`~screenpy.actions.MakeNote.of`.""" return cls.of(question) - def as_(self, key: str) -> "MakeNote": + def as_(self: Self, key: str) -> Self: """Set the key to use to recall this noted value.""" self.key = key return self - def describe(self) -> str: + def describe(self: Self) -> str: """Describe the Action in present tense.""" return f"Make a note under {self.key}." @beat('{} jots something down under "{key}".') - def perform_as(self, the_actor: Actor) -> None: + def perform_as(self: Self, the_actor: Actor) -> None: """Direct the Actor to take a note.""" if self.key is None: raise UnableToAct("No key was provided to name this note.") @@ -70,7 +72,7 @@ def perform_as(self, the_actor: Actor) -> None: Director().notes(self.key, value) def __init__( - self, question: Union[Answerable, Any], key: Optional[str] = None + self: Self, question: Union[Answerable, Any], key: Optional[str] = None ) -> None: self.question = question self.key = key diff --git a/screenpy/actions/pause.py b/screenpy/actions/pause.py index 72333544..60fce044 100644 --- a/screenpy/actions/pause.py +++ b/screenpy/actions/pause.py @@ -10,7 +10,7 @@ from screenpy.exceptions import UnableToAct from screenpy.pacing import beat -T_pause = TypeVar("T_pause", bound="Pause") +Self = TypeVar("Self", bound="Pause") class Pause: @@ -36,11 +36,11 @@ class Pause: time: float @classmethod - def for_(cls: Type[T_pause], number: float) -> T_pause: + def for_(cls: Type[Self], number: float) -> Self: """Specify how many seconds or milliseconds to wait for.""" return cls(number) - def seconds_because(self: T_pause, reason: str) -> T_pause: + def seconds_because(self: Self, reason: str) -> Self: """Use seconds and provide a reason for the pause. Aliases: @@ -50,23 +50,23 @@ def seconds_because(self: T_pause, reason: str) -> T_pause: self.reason = self._massage_reason(reason) return self - def second_because(self: T_pause, reason: str) -> T_pause: + def second_because(self: Self, reason: str) -> Self: """Alias for :meth:`~screenpy.actions.Pause.seconds_because`.""" return self.seconds_because(reason) - def milliseconds_because(self: T_pause, reason: str) -> T_pause: + def milliseconds_because(self: Self, reason: str) -> Self: """Use milliseconds and provide a reason for the pause.""" self.unit = f"millisecond{'s' if self.number != 1 else ''}" self.time = self.time / 1000.0 self.reason = self._massage_reason(reason) return self - def describe(self: T_pause) -> str: + def describe(self: Self) -> str: """Describe the Action in present tense.""" return f"Pause for {self.number} {self.unit} {self.reason}." @beat("{} pauses for {number} {unit} {reason}.") - def perform_as(self: T_pause, _: Actor) -> None: + def perform_as(self: Self, _: Actor) -> None: """Direct the Actor to take their union-mandated break.""" if not self.reason: raise UnableToAct( @@ -76,7 +76,7 @@ def perform_as(self: T_pause, _: Actor) -> None: sleep(self.time) - def _massage_reason(self: T_pause, reason: str) -> str: + def _massage_reason(self: Self, reason: str) -> str: """Apply some gentle massaging to the reason string.""" if not reason.startswith("because"): reason = f"because {reason}" @@ -85,7 +85,7 @@ def _massage_reason(self: T_pause, reason: str) -> str: return reason - def __init__(self: T_pause, number: float) -> None: + def __init__(self: Self, number: float) -> None: self.number = number self.time = number self.unit = f"second{'s' if self.number != 1 else ''}" diff --git a/screenpy/actions/see.py b/screenpy/actions/see.py index 81aaa88d..5062a33b 100644 --- a/screenpy/actions/see.py +++ b/screenpy/actions/see.py @@ -2,7 +2,7 @@ Make an assertion using a Question and a Resolution. """ -from typing import Any, Union +from typing import Any, Type, TypeVar, Union from hamcrest import assert_that @@ -12,6 +12,8 @@ from screenpy.resolutions import BaseResolution from screenpy.speech_tools import get_additive_description +Self = TypeVar("Self", bound="See") + class See: """See if a value or the answer to a Question matches the Resolution. @@ -32,16 +34,18 @@ class See: """ @classmethod - def the(cls, question: Union[Answerable, Any], resolution: BaseResolution) -> "See": + def the( + cls: Type[Self], question: Union[Answerable, Any], resolution: BaseResolution + ) -> Self: """Supply the Question (or value) and Resolution to test.""" return cls(question, resolution) - def describe(self) -> str: + def describe(self: Self) -> str: """Describe the Action in present tense.""" return f"See if {self.question_to_log} is {self.resolution_to_log}." @beat("{} sees if {question_to_log} is {resolution_to_log}.") - def perform_as(self, the_actor: Actor) -> None: + def perform_as(self: Self, the_actor: Actor) -> None: """Direct the Actor to make an observation.""" if isinstance(self.question, Answerable): value: object = self.question.answered_by(the_actor) @@ -57,7 +61,7 @@ def perform_as(self, the_actor: Actor) -> None: assert_that(value, self.resolution, reason) def __init__( - self, question: Union[Answerable, Any], resolution: BaseResolution + self: Self, question: Union[Answerable, Any], resolution: BaseResolution ) -> None: self.question = question self.question_to_log = get_additive_description(question) diff --git a/screenpy/actions/see_all_of.py b/screenpy/actions/see_all_of.py index 2c695b6f..42cd4e6c 100644 --- a/screenpy/actions/see_all_of.py +++ b/screenpy/actions/see_all_of.py @@ -3,7 +3,7 @@ all of which are expected to be true. """ -from typing import Tuple +from typing import Tuple, Type, TypeVar from screenpy import Actor from screenpy.exceptions import UnableToAct @@ -13,6 +13,8 @@ from .see import See +Self = TypeVar("Self", bound="SeeAllOf") + class SeeAllOf: """See if all the provided values or Questions match their Resolutions. @@ -38,21 +40,21 @@ class SeeAllOf: """ @classmethod - def the(cls, *tests: Tuple[Answerable, BaseResolution]) -> "SeeAllOf": + def the(cls: Type[Self], *tests: Tuple[Answerable, BaseResolution]) -> Self: """Supply any number of Question/value + Resolution tuples to test.""" return cls(*tests) - def describe(self) -> str: + def describe(self: Self) -> str: """Describe the Action in present tense.""" return f"See if all of {self.number_of_tests} tests pass." @beat("{} sees if all of the following {number_of_tests} tests pass:") - def perform_as(self, the_actor: Actor) -> None: + def perform_as(self: Self, the_actor: Actor) -> None: """Direct the Actor to make a series of observations.""" for question, resolution in self.tests: the_actor.should(See.the(question, resolution)) - def __init__(self, *tests: Tuple[Answerable, BaseResolution]) -> None: + def __init__(self: Self, *tests: Tuple[Answerable, BaseResolution]) -> None: if len(tests) < 2: raise UnableToAct( "Must supply 2 or more tests for SeeAllOf." diff --git a/screenpy/actions/see_any_of.py b/screenpy/actions/see_any_of.py index 32c3bd0c..c1b3f18b 100644 --- a/screenpy/actions/see_any_of.py +++ b/screenpy/actions/see_any_of.py @@ -3,7 +3,7 @@ at least one of which is expected to be true. """ -from typing import Tuple +from typing import Tuple, Type, TypeVar from screenpy import Actor from screenpy.exceptions import UnableToAct @@ -13,6 +13,8 @@ from .see import See +Self = TypeVar("Self", bound="SeeAnyOf") + class SeeAnyOf: """See if at least one value or Question matches its Resolution. @@ -39,16 +41,16 @@ class SeeAnyOf: """ @classmethod - def the(cls, *tests: Tuple[Answerable, BaseResolution]) -> "SeeAnyOf": + def the(cls: Type[Self], *tests: Tuple[Answerable, BaseResolution]) -> Self: """Supply any number of Question/value + Resolution tuples to test.""" return cls(*tests) - def describe(self) -> str: + def describe(self: Self) -> str: """Describe the Action in present tense.""" return f"See if any of {self.number_of_tests} tests pass." @beat("{} sees if any of the following {number_of_tests} tests pass:") - def perform_as(self, the_actor: Actor) -> None: + def perform_as(self: Self, the_actor: Actor) -> None: """Direct the Actor to make a series of observations.""" none_passed = True for question, resolution in self.tests: @@ -61,7 +63,7 @@ def perform_as(self, the_actor: Actor) -> None: if none_passed: raise AssertionError(f"{the_actor} did not find any expected answers!") - def __init__(self, *tests: Tuple[Answerable, BaseResolution]) -> None: + def __init__(self: Self, *tests: Tuple[Answerable, BaseResolution]) -> None: if len(tests) < 2: raise UnableToAct( "Must supply 2 or more tests for SeeAnyOf." diff --git a/screenpy/actor.py b/screenpy/actor.py index c3d08892..0239daa3 100644 --- a/screenpy/actor.py +++ b/screenpy/actor.py @@ -34,6 +34,7 @@ ] T = TypeVar("T") +Self = TypeVar("Self", bound="Actor") class Actor: @@ -52,12 +53,12 @@ class Actor: independent_cleanup_tasks: List[Performable] @classmethod - def named(cls, name: Text) -> "Actor": + def named(cls: Type[Self], name: Text) -> Self: """Give a name to this Actor.""" aside(choice(ENTRANCE_DIRECTIONS).format(actor=name)) return cls(name) - def who_can(self, *abilities: Forgettable) -> "Actor": + def who_can(self: Self, *abilities: Forgettable) -> Self: """Add one or more Abilities to this Actor. Aliases: @@ -66,11 +67,11 @@ def who_can(self, *abilities: Forgettable) -> "Actor": self.abilities.extend(abilities) return self - def can(self, *abilities: Forgettable) -> "Actor": + def can(self: Self, *abilities: Forgettable) -> Self: """Alias for :meth:`~screenpy.actor.Actor.who_can`.""" return self.who_can(*abilities) - def has_cleanup_tasks(self, *tasks: Performable) -> "Actor": + def has_cleanup_tasks(self: Self, *tasks: Performable) -> Self: """Assign one or more tasks to the Actor to perform when exiting.""" warnings.warn( "This method is deprecated and will be removed in ScreenPy 4.2.0." @@ -80,7 +81,7 @@ def has_cleanup_tasks(self, *tasks: Performable) -> "Actor": ) return self.has_ordered_cleanup_tasks(*tasks) - def has_ordered_cleanup_tasks(self, *tasks: Performable) -> "Actor": + def has_ordered_cleanup_tasks(self: Self, *tasks: Performable) -> Self: """Assign one or more tasks for the Actor to perform when exiting. The tasks given to this method must be performed successfully in @@ -93,11 +94,11 @@ def has_ordered_cleanup_tasks(self, *tasks: Performable) -> "Actor": self.ordered_cleanup_tasks.extend(tasks) return self - def with_ordered_cleanup_tasks(self, *tasks: Performable) -> "Actor": + def with_ordered_cleanup_tasks(self: Self, *tasks: Performable) -> Self: """Alias for :meth:`~screenpy.actor.Actor.has_ordered_cleanup_tasks`.""" return self.has_ordered_cleanup_tasks(*tasks) - def has_independent_cleanup_tasks(self, *tasks: Performable) -> "Actor": + def has_independent_cleanup_tasks(self: Self, *tasks: Performable) -> Self: """Assign one or more tasks for the Actor to perform when exiting. The tasks included through this method are assumed to be independent; @@ -110,11 +111,11 @@ def has_independent_cleanup_tasks(self, *tasks: Performable) -> "Actor": self.independent_cleanup_tasks.extend(tasks) return self - def with_independent_cleanup_tasks(self, *tasks: Performable) -> "Actor": + def with_independent_cleanup_tasks(self: Self, *tasks: Performable) -> Self: """Alias for :meth:`~screenpy.actor.Actor.has_independent_cleanup_tasks`.""" return self.has_independent_cleanup_tasks(*tasks) - def uses_ability_to(self, ability: Type[T]) -> T: + def uses_ability_to(self: Self, ability: Type[T]) -> T: """Find the Ability referenced and return it, if the Actor is capable. Raises: @@ -129,11 +130,11 @@ def uses_ability_to(self, ability: Type[T]) -> T: raise UnableToPerform(f"{self} does not have the Ability to {ability}") - def ability_to(self, ability: Type[T]) -> T: + def ability_to(self: Self, ability: Type[T]) -> T: """Alias for :meth:`~screenpy.actor.Actor.uses_ability_to`.""" return self.uses_ability_to(ability) - def has_ability_to(self, ability: Type[Forgettable]) -> bool: + def has_ability_to(self: Self, ability: Type[Forgettable]) -> bool: """Ask whether the Actor has the Ability to do something.""" try: self.ability_to(ability) @@ -141,7 +142,7 @@ def has_ability_to(self, ability: Type[Forgettable]) -> bool: except UnableToPerform: return False - def attempts_to(self, *actions: Performable) -> None: + def attempts_to(self: Self, *actions: Performable) -> None: """Perform a list of Actions, one after the other. Aliases: @@ -151,19 +152,19 @@ def attempts_to(self, *actions: Performable) -> None: for action in actions: self.perform(action) - def was_able_to(self, *actions: Performable) -> None: + def was_able_to(self: Self, *actions: Performable) -> None: """Alias for :meth:`~screenpy.actor.Actor.attempts_to`, for test setup.""" return self.attempts_to(*actions) - def should(self, *actions: Performable) -> None: + def should(self: Self, *actions: Performable) -> None: """Alias for :meth:`~screenpy.actor.Actor.attempts_to`, for test assertions.""" return self.attempts_to(*actions) - def perform(self, action: Performable) -> None: + def perform(self: Self, action: Performable) -> None: """Perform an Action.""" action.perform_as(self) - def cleans_up_ordered_tasks(self) -> None: + def cleans_up_ordered_tasks(self: Self) -> None: """Perform ordered clean-up tasks.""" try: for task in self.ordered_cleanup_tasks: @@ -171,7 +172,7 @@ def cleans_up_ordered_tasks(self) -> None: finally: self.ordered_cleanup_tasks = [] - def cleans_up_independent_tasks(self) -> None: + def cleans_up_independent_tasks(self: Self) -> None: """Perform independent clean-up tasks.""" for task in self.independent_cleanup_tasks: try: @@ -186,12 +187,12 @@ def cleans_up_independent_tasks(self) -> None: self.independent_cleanup_tasks = [] - def cleans_up(self) -> None: + def cleans_up(self: Self) -> None: """Perform any scheduled clean-up tasks.""" self.cleans_up_independent_tasks() self.cleans_up_ordered_tasks() - def exit(self) -> None: + def exit(self: Self) -> None: """Direct the Actor to forget all their Abilities. Aliases: @@ -204,22 +205,22 @@ def exit(self) -> None: ability.forget() self.abilities = [] - def exit_stage_left(self) -> None: + def exit_stage_left(self: Self) -> None: """Alias for :meth:`~screenpy.actor.Actor.exit`.""" return self.exit() - def exit_stage_right(self) -> None: + def exit_stage_right(self: Self) -> None: """Alias for :meth:`~screenpy.actor.Actor.exit`.""" return self.exit() - def exit_through_vomitorium(self) -> None: + def exit_through_vomitorium(self: Self) -> None: """Alias for :meth:`~screenpy.actor.Actor.exit`.""" return self.exit() - def __repr__(self) -> str: + def __repr__(self: Self) -> str: return self.name - def __init__(self, name: str) -> None: + def __init__(self: Self, name: str) -> None: self.name = name self.abilities = [] self.ordered_cleanup_tasks = [] diff --git a/screenpy/director.py b/screenpy/director.py index 6c4cfe63..58108e89 100644 --- a/screenpy/director.py +++ b/screenpy/director.py @@ -2,6 +2,10 @@ Directors handle the meta information that it takes to run a screenplay. There is only one of them, so you'll always have access to the same information. """ +from typing import Type, TypeVar + +Self = TypeVar("Self", bound="Director") +T = TypeVar("T") class Director: @@ -15,16 +19,16 @@ class Director: _instance = None - def __new__(cls) -> "Director": + def __new__(cls: Type[Self]) -> Self: if cls._instance is None: cls._instance = super().__new__(cls) cls._instance.notebook = {} return cls._instance - def notes(self, key: str, value: object) -> None: + def notes(self: Self, key: str, value: T) -> None: """Note down a value under the given key.""" self.notebook[key] = value - def looks_up(self, key: str) -> object: + def looks_up(self: Self, key: str) -> T: """Look up a noted value by its key.""" return self.notebook[key]