Skip to content

Commit dffc08c

Browse files
committed
refactor!: Retry argument attributes
Mypy doesn't like that the retry argument attributes are defined dynamically when a class is instantiated, so it'll complain that they don't exist when a subclass developer is trying to modify them when overriding/extending the parser. If instead of having separate attributes per registered stage, we just have a `dict`` where the keys are the stage names, mypy is happy. Note that this is a breaking change, as it changes how subclass developers customize the parser.
1 parent 1431129 commit dffc08c

10 files changed

+84
-92
lines changed

doc/source/examples.rst

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,6 @@ by adding the following to the ``MyScript`` class:
7676
:lines: 27-39
7777
:caption: ``example/ex_1_removing_the_retry_arguments.py``
7878

79-
.. note::
80-
81-
An upcoming release will refactor the retry argument attributes so
82-
`mypy`_ will be happy with them. For now, just use the ``type:
83-
ignore[attr-defined]`` comments.
84-
85-
.. _mypy: https://mypy-lang.org/
86-
8779
Now when we look at the ``--help`` text, we see:
8880

8981
.. command-output:: python3 ../../example/ex_1_removing_the_retry_arguments.py --help

example/ex_1_removing_the_retry_arguments.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ def parser(self) -> ArgumentParser:
3131
my_parser.description = "Demonstrate removing the retry arguments."
3232
self.retry_arg_group.title = argparse.SUPPRESS
3333
self.retry_arg_group.description = argparse.SUPPRESS
34-
self.hello_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
35-
self.hello_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
36-
self.hello_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
37-
self.goodbye_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
38-
self.goodbye_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
39-
self.goodbye_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
34+
self.retry_attempts_arg["hello"].help = argparse.SUPPRESS
35+
self.retry_delay_arg["hello"].help = argparse.SUPPRESS
36+
self.retry_timeout_arg["hello"].help = argparse.SUPPRESS
37+
self.retry_attempts_arg["goodbye"].help = argparse.SUPPRESS
38+
self.retry_delay_arg["goodbye"].help = argparse.SUPPRESS
39+
self.retry_timeout_arg["goodbye"].help = argparse.SUPPRESS
4040
return my_parser
4141

4242
def main(self, argv: List[str]) -> None:

example/ex_2_running_certain_stages_by_default.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ def parser(self) -> ArgumentParser:
3333
)
3434
self.retry_arg_group.title = argparse.SUPPRESS
3535
self.retry_arg_group.description = argparse.SUPPRESS
36-
self.hello_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
37-
self.hello_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
38-
self.hello_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
39-
self.goodbye_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
40-
self.goodbye_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
41-
self.goodbye_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
36+
self.retry_attempts_arg["hello"].help = argparse.SUPPRESS
37+
self.retry_delay_arg["hello"].help = argparse.SUPPRESS
38+
self.retry_timeout_arg["hello"].help = argparse.SUPPRESS
39+
self.retry_attempts_arg["goodbye"].help = argparse.SUPPRESS
40+
self.retry_delay_arg["goodbye"].help = argparse.SUPPRESS
41+
self.retry_timeout_arg["goodbye"].help = argparse.SUPPRESS
4242
my_parser.set_defaults(stage=list(self.stages))
4343
return my_parser
4444

example/ex_3_adding_arguments.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ def parser(self) -> ArgumentParser:
3636
my_parser.description = "Demonstrate adding arguments to the parser."
3737
self.retry_arg_group.title = argparse.SUPPRESS
3838
self.retry_arg_group.description = argparse.SUPPRESS
39-
self.hello_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
40-
self.hello_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
41-
self.hello_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
42-
self.goodbye_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
43-
self.goodbye_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
44-
self.goodbye_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
39+
self.retry_attempts_arg["hello"].help = argparse.SUPPRESS
40+
self.retry_delay_arg["hello"].help = argparse.SUPPRESS
41+
self.retry_timeout_arg["hello"].help = argparse.SUPPRESS
42+
self.retry_attempts_arg["goodbye"].help = argparse.SUPPRESS
43+
self.retry_delay_arg["goodbye"].help = argparse.SUPPRESS
44+
self.retry_timeout_arg["goodbye"].help = argparse.SUPPRESS
4545
my_parser.set_defaults(stage=list(self.stages))
4646
my_parser.add_argument(
4747
"--some-file",

example/ex_4_customizing_stage_behavior.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ def parser(self) -> ArgumentParser:
3636
my_parser.description = "Demonstrate adding arguments to the parser."
3737
self.retry_arg_group.title = argparse.SUPPRESS
3838
self.retry_arg_group.description = argparse.SUPPRESS
39-
self.hello_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
40-
self.hello_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
41-
self.hello_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
42-
self.goodbye_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
43-
self.goodbye_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
44-
self.goodbye_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
39+
self.retry_attempts_arg["hello"].help = argparse.SUPPRESS
40+
self.retry_delay_arg["hello"].help = argparse.SUPPRESS
41+
self.retry_timeout_arg["hello"].help = argparse.SUPPRESS
42+
self.retry_attempts_arg["goodbye"].help = argparse.SUPPRESS
43+
self.retry_delay_arg["goodbye"].help = argparse.SUPPRESS
44+
self.retry_timeout_arg["goodbye"].help = argparse.SUPPRESS
4545
my_parser.set_defaults(stage=list(self.stages))
4646
my_parser.add_argument(
4747
"--some-file",

example/ex_5_customizing_individual_stages.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ def parser(self) -> ArgumentParser:
3636
my_parser.description = "Demonstrate adding arguments to the parser."
3737
self.retry_arg_group.title = argparse.SUPPRESS
3838
self.retry_arg_group.description = argparse.SUPPRESS
39-
self.hello_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
40-
self.hello_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
41-
self.hello_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
42-
self.goodbye_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
43-
self.goodbye_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
44-
self.goodbye_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
39+
self.retry_attempts_arg["hello"].help = argparse.SUPPRESS
40+
self.retry_delay_arg["hello"].help = argparse.SUPPRESS
41+
self.retry_timeout_arg["hello"].help = argparse.SUPPRESS
42+
self.retry_attempts_arg["goodbye"].help = argparse.SUPPRESS
43+
self.retry_delay_arg["goodbye"].help = argparse.SUPPRESS
44+
self.retry_timeout_arg["goodbye"].help = argparse.SUPPRESS
4545
my_parser.set_defaults(stage=list(self.stages))
4646
my_parser.add_argument(
4747
"--some-file",

example/ex_6_creating_retryable_stages.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ def say_goodbye(self) -> None:
6161
def parser(self) -> ArgumentParser:
6262
my_parser = super().parser
6363
my_parser.description = "Demonstrate adding arguments to the parser."
64-
self.hello_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
65-
self.hello_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
66-
self.hello_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
67-
self.goodbye_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
68-
self.goodbye_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
69-
self.goodbye_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
64+
self.retry_attempts_arg["hello"].help = argparse.SUPPRESS
65+
self.retry_delay_arg["hello"].help = argparse.SUPPRESS
66+
self.retry_timeout_arg["hello"].help = argparse.SUPPRESS
67+
self.retry_attempts_arg["goodbye"].help = argparse.SUPPRESS
68+
self.retry_delay_arg["goodbye"].help = argparse.SUPPRESS
69+
self.retry_timeout_arg["goodbye"].help = argparse.SUPPRESS
7070
my_parser.set_defaults(
7171
stage=list(self.stages),
7272
flaky_retry_attempts=5,

example/ex_7_customizing_the_summary.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ def say_goodbye(self) -> None:
6363
def parser(self) -> ArgumentParser:
6464
my_parser = super().parser
6565
my_parser.description = "Demonstrate adding arguments to the parser."
66-
self.hello_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
67-
self.hello_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
68-
self.hello_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
69-
self.goodbye_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
70-
self.goodbye_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
71-
self.goodbye_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
66+
self.retry_attempts_arg["hello"].help = argparse.SUPPRESS
67+
self.retry_delay_arg["hello"].help = argparse.SUPPRESS
68+
self.retry_timeout_arg["hello"].help = argparse.SUPPRESS
69+
self.retry_attempts_arg["goodbye"].help = argparse.SUPPRESS
70+
self.retry_delay_arg["goodbye"].help = argparse.SUPPRESS
71+
self.retry_timeout_arg["goodbye"].help = argparse.SUPPRESS
7272
my_parser.set_defaults(
7373
stage=list(self.stages),
7474
flaky_retry_attempts=5,

staged_script/staged_script.py

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import shlex
1717
import subprocess
1818
from argparse import (
19+
Action,
1920
ArgumentDefaultsHelpFormatter,
2021
ArgumentParser,
2122
Namespace,
@@ -74,6 +75,23 @@ class StagedScript:
7475
retry_arg_group (argparse._ArgumentGroup): A container within
7576
the :class:`ArgumentParser` holding all the arguments
7677
associated with retrying stages.
78+
retry_attempts (Dict[str, int]): A mapping from stage names to
79+
the number of times to attempt retrying each stage.
80+
retry_attempts_arg (Dict[str, argparse.Action]): The
81+
corresponding arguments in the :class:`ArgumentParser`, so
82+
subclass developers can modify them if needed.
83+
retry_delay (Dict[str, float]): A mapping from stage names to
84+
how long to wait (in seconds) before attempting to retry a
85+
stage.
86+
retry_delay_arg (Dict[str, argparse.Action]): The
87+
corresponding arguments in the :class:`ArgumentParser`, so
88+
subclass developers can modify them if needed.
89+
retry_timeout (Dict[str, int]): A mapping from stage names to
90+
how long to wait (in seconds) before giving up on retrying
91+
the stage.
92+
retry_timeout_arg (Dict[str, argparse.Action]): The
93+
corresponding arguments in the :class:`ArgumentParser`, so
94+
subclass developers can modify them if needed.
7795
script_name (str): The name of the script (the
7896
:class:`StagedScript` subclass) being run.
7997
script_stem (str): Same as :attr:`script_name`, but without the
@@ -90,28 +108,6 @@ class StagedScript:
90108
the user via the command line arguments.
91109
start_time (datetime): The time at which this object was
92110
initialized.
93-
94-
Note that additional attributes are automatically generated for each
95-
``stage`` registered for a subclass object:
96-
97-
``STAGE_NAME_retry_attempts`` (int)
98-
The number of times to attempt retrying the ``STAGE_NAME``
99-
stage.
100-
``STAGE_NAME_retry_attempts_arg`` (argparse.Action)
101-
The corresponding argument in the :class:`ArgumentParser`, so
102-
subclass developers can modify it if needed.
103-
``STAGE_NAME_retry_delay`` (float)
104-
How long to wait (in seconds) before attempting to retry the
105-
``STAGE_NAME`` stage.
106-
``STAGE_NAME_retry_delay_arg`` (argparse.Action)
107-
The corresponding argument in the :class:`ArgumentParser`, so
108-
subclass developers can modify it if needed.
109-
``STAGE_NAME_retry_timeout`` (int)
110-
How long to wait (in seconds) before giving up on retrying the
111-
``STAGE_NAME`` stage.
112-
``STAGE_NAME_retry_timeout_arg`` (argparse.Action)
113-
The corresponding argument in the :class:`ArgumentParser`, so
114-
subclass developers can modify it if needed.
115111
"""
116112

117113
def __init__(
@@ -155,6 +151,12 @@ def __init__(
155151
self.dry_run = False
156152
self.durations: List[StageDuration] = []
157153
self.print_commands = print_commands
154+
self.retry_attempts: Dict[str, int] = {}
155+
self.retry_attempts_arg: Dict[str, Action] = {}
156+
self.retry_delay: Dict[str, float] = {}
157+
self.retry_delay_arg: Dict[str, Action] = {}
158+
self.retry_timeout: Dict[str, int] = {}
159+
self.retry_timeout_arg: Dict[str, Action] = {}
158160
self.script_name = Path(__main__.__file__).name
159161
self.script_stem = Path(__main__.__file__).stem
160162
self.script_success = True
@@ -320,9 +322,9 @@ def wrapper(self, *args, **kwargs) -> None:
320322
"""
321323
self.current_stage = stage_name
322324
get_phase_method(self, "_run_pre_stage_actions")()
323-
timeout = getattr(self, f"{stage_name}_retry_timeout")
324-
attempts = getattr(self, f"{stage_name}_retry_attempts")
325-
delay = getattr(self, f"{stage_name}_retry_delay")
325+
timeout = self.retry_timeout[stage_name]
326+
attempts = self.retry_attempts[stage_name]
327+
delay = self.retry_delay[stage_name]
326328
stop_after_timeout = stop_after_delay(timeout)
327329
stop_after_max_attempts = stop_after_attempt(attempts + 1)
328330
retry = Retrying(
@@ -590,10 +592,7 @@ def _handle_stage_retry_error_test(
590592
retry: The :class:`Retrying` controller, which contains
591593
information about the retrying that was done.
592594
"""
593-
retry_attempts = getattr(
594-
self, f"{self.current_stage}_retry_attempts", 0
595-
)
596-
if retry_attempts > 0:
595+
if self.retry_attempts[self.current_stage] > 0:
597596
stage_time = timedelta(
598597
seconds=retry.statistics["delay_since_first_attempt"]
599598
)
@@ -660,9 +659,9 @@ def parser(self) -> ArgumentParser:
660659
661660
.. code-block:: python
662661
663-
self.foo_retry_attempts_arg.help = argparse.SUPPRESS
664-
self.foo_retry_delay_arg.help = argparse.SUPPRESS
665-
self.foo_retry_timeout_arg.help = argparse.SUPPRESS
662+
self.retry_attempts_arg["foo"].help = argparse.SUPPRESS
663+
self.retry_delay_arg["foo"].help = argparse.SUPPRESS
664+
self.retry_timeout_arg["foo"].help = argparse.SUPPRESS
666665
667666
And if you want to remove the title for the retry group
668667
altogether, you can do so with:
@@ -706,23 +705,23 @@ def parser(self) -> ArgumentParser:
706705
type=int,
707706
help=f"How many times to retry the {stage!r} stage.",
708707
)
709-
setattr(self, f"{stage}_retry_attempts_arg", retry_attempts)
708+
self.retry_attempts_arg[stage] = retry_attempts
710709
retry_delay = self.retry_arg_group.add_argument(
711710
f"--{stage}-retry-delay",
712711
default=0,
713712
type=float,
714713
help="How long to wait (in seconds) before retrying the "
715714
f"{stage!r} stage.",
716715
)
717-
setattr(self, f"{stage}_retry_delay_arg", retry_delay)
716+
self.retry_delay_arg[stage] = retry_delay
718717
retry_timeout = self.retry_arg_group.add_argument(
719718
f"--{stage}-retry-timeout",
720719
default=60,
721720
type=int,
722721
help="How long to wait (in seconds) before giving up on "
723722
f"retrying the {stage!r} stage.",
724723
)
725-
setattr(self, f"{stage}_retry_timeout_arg", retry_timeout)
724+
self.retry_timeout_arg[stage] = retry_timeout
726725
return my_parser
727726

728727
def parse_args(self, argv: List[str]) -> None:
@@ -755,12 +754,13 @@ def parse_args(self, argv: List[str]) -> None:
755754
set(self.args.stage) if self.args.stage is not None else set()
756755
)
757756
for stage in self.stages:
758-
for retry_arg in [
759-
f"{stage}_retry_attempts",
760-
f"{stage}_retry_delay",
761-
f"{stage}_retry_timeout",
762-
]:
763-
setattr(self, retry_arg, getattr(self.args, retry_arg, None))
757+
for arg in ["attempts", "delay", "timeout"]:
758+
retry_arg = getattr(self, f"retry_{arg}")
759+
retry_arg[stage] = getattr(
760+
self.args,
761+
f"{stage}_retry_{arg}",
762+
None,
763+
)
764764

765765
def raise_parser_error(self, message):
766766
"""

test/test_staged_script.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def test__handle_stage_retry_error(
9898
) -> None:
9999
"""Test the :func:`_handle_stage_retry_error` method."""
100100
script.current_stage = "test"
101-
script.test_retry_attempts = retry_attempts # type: ignore[attr-defined]
101+
script.retry_attempts["test"] = retry_attempts
102102
retry = mock_Retrying()
103103
retry.statistics = {
104104
"delay_since_first_attempt": 1234,

0 commit comments

Comments
 (0)