Skip to content

Commit 402841c

Browse files
committed
ScreenPyHQ#25 subclassing support for mypy prior to PEP 673 and python/mypy#11666
1 parent 08b4d07 commit 402841c

File tree

7 files changed

+73
-58
lines changed

7 files changed

+73
-58
lines changed

screenpy/actions/make_note.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
Make a quick note about the answer to a Question.
33
"""
44

5-
from typing import Any, Optional, Union
5+
from typing import Any, Optional, Type, TypeVar, Union
66

77
from screenpy import Actor, Director
88
from screenpy.exceptions import UnableToAct
99
from screenpy.pacing import aside, beat
1010
from screenpy.protocols import Answerable, ErrorKeeper
1111

12+
Self = TypeVar("Self", bound="MakeNote")
13+
1214

1315
class MakeNote:
1416
"""Make a note of a value or the answer to a Question.
@@ -29,7 +31,7 @@ class MakeNote:
2931
key: Optional[str]
3032

3133
@classmethod
32-
def of(cls, question: Union[Answerable, Any]) -> "MakeNote":
34+
def of(cls: Type[Self], question: Union[Answerable, Any]) -> Self:
3335
"""Supply the Question to answer and its arguments.
3436
3537
Aliases:
@@ -38,21 +40,21 @@ def of(cls, question: Union[Answerable, Any]) -> "MakeNote":
3840
return cls(question)
3941

4042
@classmethod
41-
def of_the(cls, question: Union[Answerable, Any]) -> "MakeNote":
43+
def of_the(cls: Type[Self], question: Union[Answerable, Any]) -> Self:
4244
"""Alias for :meth:`~screenpy.actions.MakeNote.of`."""
4345
return cls.of(question)
4446

45-
def as_(self, key: str) -> "MakeNote":
47+
def as_(self: Self, key: str) -> Self:
4648
"""Set the key to use to recall this noted value."""
4749
self.key = key
4850
return self
4951

50-
def describe(self) -> str:
52+
def describe(self: Self) -> str:
5153
"""Describe the Action in present tense."""
5254
return f"Make a note under {self.key}."
5355

5456
@beat('{} jots something down under "{key}".')
55-
def perform_as(self, the_actor: Actor) -> None:
57+
def perform_as(self: Self, the_actor: Actor) -> None:
5658
"""Direct the Actor to take a note."""
5759
if self.key is None:
5860
raise UnableToAct("No key was provided to name this note.")
@@ -70,7 +72,7 @@ def perform_as(self, the_actor: Actor) -> None:
7072
Director().notes(self.key, value)
7173

7274
def __init__(
73-
self, question: Union[Answerable, Any], key: Optional[str] = None
75+
self: Self, question: Union[Answerable, Any], key: Optional[str] = None
7476
) -> None:
7577
self.question = question
7678
self.key = key

screenpy/actions/pause.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from screenpy.exceptions import UnableToAct
1111
from screenpy.pacing import beat
1212

13-
T_pause = TypeVar("T_pause", bound="Pause")
13+
Self = TypeVar("Self", bound="Pause")
1414

1515

1616
class Pause:
@@ -36,11 +36,11 @@ class Pause:
3636
time: float
3737

3838
@classmethod
39-
def for_(cls: Type[T_pause], number: float) -> T_pause:
39+
def for_(cls: Type[Self], number: float) -> Self:
4040
"""Specify how many seconds or milliseconds to wait for."""
4141
return cls(number)
4242

43-
def seconds_because(self: T_pause, reason: str) -> T_pause:
43+
def seconds_because(self: Self, reason: str) -> Self:
4444
"""Use seconds and provide a reason for the pause.
4545
4646
Aliases:
@@ -50,23 +50,23 @@ def seconds_because(self: T_pause, reason: str) -> T_pause:
5050
self.reason = self._massage_reason(reason)
5151
return self
5252

53-
def second_because(self: T_pause, reason: str) -> T_pause:
53+
def second_because(self: Self, reason: str) -> Self:
5454
"""Alias for :meth:`~screenpy.actions.Pause.seconds_because`."""
5555
return self.seconds_because(reason)
5656

57-
def milliseconds_because(self: T_pause, reason: str) -> T_pause:
57+
def milliseconds_because(self: Self, reason: str) -> Self:
5858
"""Use milliseconds and provide a reason for the pause."""
5959
self.unit = f"millisecond{'s' if self.number != 1 else ''}"
6060
self.time = self.time / 1000.0
6161
self.reason = self._massage_reason(reason)
6262
return self
6363

64-
def describe(self: T_pause) -> str:
64+
def describe(self: Self) -> str:
6565
"""Describe the Action in present tense."""
6666
return f"Pause for {self.number} {self.unit} {self.reason}."
6767

6868
@beat("{} pauses for {number} {unit} {reason}.")
69-
def perform_as(self: T_pause, _: Actor) -> None:
69+
def perform_as(self: Self, _: Actor) -> None:
7070
"""Direct the Actor to take their union-mandated break."""
7171
if not self.reason:
7272
raise UnableToAct(
@@ -76,7 +76,7 @@ def perform_as(self: T_pause, _: Actor) -> None:
7676

7777
sleep(self.time)
7878

79-
def _massage_reason(self: T_pause, reason: str) -> str:
79+
def _massage_reason(self: Self, reason: str) -> str:
8080
"""Apply some gentle massaging to the reason string."""
8181
if not reason.startswith("because"):
8282
reason = f"because {reason}"
@@ -85,7 +85,7 @@ def _massage_reason(self: T_pause, reason: str) -> str:
8585

8686
return reason
8787

88-
def __init__(self: T_pause, number: float) -> None:
88+
def __init__(self: Self, number: float) -> None:
8989
self.number = number
9090
self.time = number
9191
self.unit = f"second{'s' if self.number != 1 else ''}"

screenpy/actions/see.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Make an assertion using a Question and a Resolution.
33
"""
44

5-
from typing import Any, Union
5+
from typing import Any, Type, TypeVar, Union
66

77
from hamcrest import assert_that
88

@@ -12,6 +12,8 @@
1212
from screenpy.resolutions import BaseResolution
1313
from screenpy.speech_tools import get_additive_description
1414

15+
Self = TypeVar("Self", bound="See")
16+
1517

1618
class See:
1719
"""See if a value or the answer to a Question matches the Resolution.
@@ -32,16 +34,18 @@ class See:
3234
"""
3335

3436
@classmethod
35-
def the(cls, question: Union[Answerable, Any], resolution: BaseResolution) -> "See":
37+
def the(
38+
cls: Type[Self], question: Union[Answerable, Any], resolution: BaseResolution
39+
) -> Self:
3640
"""Supply the Question (or value) and Resolution to test."""
3741
return cls(question, resolution)
3842

39-
def describe(self) -> str:
43+
def describe(self: Self) -> str:
4044
"""Describe the Action in present tense."""
4145
return f"See if {self.question_to_log} is {self.resolution_to_log}."
4246

4347
@beat("{} sees if {question_to_log} is {resolution_to_log}.")
44-
def perform_as(self, the_actor: Actor) -> None:
48+
def perform_as(self: Self, the_actor: Actor) -> None:
4549
"""Direct the Actor to make an observation."""
4650
if isinstance(self.question, Answerable):
4751
value: object = self.question.answered_by(the_actor)
@@ -57,7 +61,7 @@ def perform_as(self, the_actor: Actor) -> None:
5761
assert_that(value, self.resolution, reason)
5862

5963
def __init__(
60-
self, question: Union[Answerable, Any], resolution: BaseResolution
64+
self: Self, question: Union[Answerable, Any], resolution: BaseResolution
6165
) -> None:
6266
self.question = question
6367
self.question_to_log = get_additive_description(question)

screenpy/actions/see_all_of.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
all of which are expected to be true.
44
"""
55

6-
from typing import Tuple
6+
from typing import Tuple, Type, TypeVar
77

88
from screenpy import Actor
99
from screenpy.exceptions import UnableToAct
@@ -13,6 +13,8 @@
1313

1414
from .see import See
1515

16+
Self = TypeVar("Self", bound="SeeAllOf")
17+
1618

1719
class SeeAllOf:
1820
"""See if all the provided values or Questions match their Resolutions.
@@ -38,21 +40,21 @@ class SeeAllOf:
3840
"""
3941

4042
@classmethod
41-
def the(cls, *tests: Tuple[Answerable, BaseResolution]) -> "SeeAllOf":
43+
def the(cls: Type[Self], *tests: Tuple[Answerable, BaseResolution]) -> Self:
4244
"""Supply any number of Question/value + Resolution tuples to test."""
4345
return cls(*tests)
4446

45-
def describe(self) -> str:
47+
def describe(self: Self) -> str:
4648
"""Describe the Action in present tense."""
4749
return f"See if all of {self.number_of_tests} tests pass."
4850

4951
@beat("{} sees if all of the following {number_of_tests} tests pass:")
50-
def perform_as(self, the_actor: Actor) -> None:
52+
def perform_as(self: Self, the_actor: Actor) -> None:
5153
"""Direct the Actor to make a series of observations."""
5254
for question, resolution in self.tests:
5355
the_actor.should(See.the(question, resolution))
5456

55-
def __init__(self, *tests: Tuple[Answerable, BaseResolution]) -> None:
57+
def __init__(self: Self, *tests: Tuple[Answerable, BaseResolution]) -> None:
5658
if len(tests) < 2:
5759
raise UnableToAct(
5860
"Must supply 2 or more tests for SeeAllOf."

screenpy/actions/see_any_of.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
at least one of which is expected to be true.
44
"""
55

6-
from typing import Tuple
6+
from typing import Tuple, Type, TypeVar
77

88
from screenpy import Actor
99
from screenpy.exceptions import UnableToAct
@@ -13,6 +13,8 @@
1313

1414
from .see import See
1515

16+
Self = TypeVar("Self", bound="SeeAnyOf")
17+
1618

1719
class SeeAnyOf:
1820
"""See if at least one value or Question matches its Resolution.
@@ -39,16 +41,16 @@ class SeeAnyOf:
3941
"""
4042

4143
@classmethod
42-
def the(cls, *tests: Tuple[Answerable, BaseResolution]) -> "SeeAnyOf":
44+
def the(cls: Type[Self], *tests: Tuple[Answerable, BaseResolution]) -> Self:
4345
"""Supply any number of Question/value + Resolution tuples to test."""
4446
return cls(*tests)
4547

46-
def describe(self) -> str:
48+
def describe(self: Self) -> str:
4749
"""Describe the Action in present tense."""
4850
return f"See if any of {self.number_of_tests} tests pass."
4951

5052
@beat("{} sees if any of the following {number_of_tests} tests pass:")
51-
def perform_as(self, the_actor: Actor) -> None:
53+
def perform_as(self: Self, the_actor: Actor) -> None:
5254
"""Direct the Actor to make a series of observations."""
5355
none_passed = True
5456
for question, resolution in self.tests:
@@ -61,7 +63,7 @@ def perform_as(self, the_actor: Actor) -> None:
6163
if none_passed:
6264
raise AssertionError(f"{the_actor} did not find any expected answers!")
6365

64-
def __init__(self, *tests: Tuple[Answerable, BaseResolution]) -> None:
66+
def __init__(self: Self, *tests: Tuple[Answerable, BaseResolution]) -> None:
6567
if len(tests) < 2:
6668
raise UnableToAct(
6769
"Must supply 2 or more tests for SeeAnyOf."

0 commit comments

Comments
 (0)