From a9c80052734c44f8d25bbce7599b315ac627c639 Mon Sep 17 00:00:00 2001 From: "Jason M. Gates" Date: Mon, 2 Dec 2024 10:45:43 -0700 Subject: [PATCH] chore!: Drop support for Python 3.8 * Use type-hinting provided out of the box in 3.9. * Use new dictionary update syntax. * Update the docs and CI accordingly. --- .github/workflows/continuous-integration.yml | 2 +- README.md | 5 ++- doc/source/examples.rst | 16 ++++----- doc/source/index.rst | 2 +- example/ex_0_the_basics.py | 3 +- example/ex_1_removing_the_retry_arguments.py | 3 +- .../ex_2_running_certain_stages_by_default.py | 3 +- example/ex_3_adding_arguments.py | 5 ++- example/ex_4_customizing_stage_behavior.py | 5 ++- example/ex_5_customizing_individual_stages.py | 5 ++- example/ex_6_creating_retryable_stages.py | 8 ++--- example/ex_7_customizing_the_summary.py | 12 +++---- example/test_examples.py | 3 +- pyproject.toml | 1 - staged_script/staged_script.py | 34 +++++++++---------- test/test_staged_script.py | 4 +-- test/test_staged_script_advanced_subclass.py | 3 +- test/test_staged_script_basic_subclass.py | 4 +-- 18 files changed, 53 insertions(+), 65 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 461bb6a..9973bd0 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + version: ["3.9", "3.10", "3.11", "3.12"] fail-fast: false steps: diff --git a/README.md b/README.md index 29b9902..4e2c36f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ [![pre-commit.ci Status](https://results.pre-commit.ci/badge/github/sandialabs/staged-script/master.svg)](https://results.pre-commit.ci/latest/github/sandialabs/staged-script/master) [![PyPI - Version](https://img.shields.io/pypi/v/staged-script?label=PyPI)](https://pypi.org/project/staged-script/) ![PyPI - Downloads](https://img.shields.io/pypi/dm/staged-script?label=PyPI%20downloads) -![Python Version](https://img.shields.io/badge/Python-3.8|3.9|3.10|3.11|3.12-blue.svg) +![Python Version](https://img.shields.io/badge/Python-3.9|3.10|3.11|3.12-blue.svg) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) # staged-script @@ -44,7 +44,6 @@ python3 -m pip install staged-script Once installed, you can simply ```python import sys -from typing import List from staged_script import StagedScript @@ -59,7 +58,7 @@ class MyScript(StagedScript): def say_goodbye(self) -> None: self.run("echo 'Goodbye World'", shell=True) - def main(self, argv: List[str]) -> None: + def main(self, argv: list[str]) -> None: self.parse_args(argv) try: self.say_hello() diff --git a/doc/source/examples.rst b/doc/source/examples.rst index 1fab056..a113b44 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -20,7 +20,7 @@ simple stages to say "hello" and "goodbye". :language: python :linenos: :lines: 10- - :emphasize-lines: 9-10,13-14,19-20 + :emphasize-lines: 8-9,12-13,18-19 :caption: ``example/ex_0_the_basics.py`` The two methods ``say_hello()`` and ``say_goodbye()`` are stand-ins for @@ -73,7 +73,7 @@ by adding the following to the ``MyScript`` class: .. literalinclude:: ../../example/ex_1_removing_the_retry_arguments.py :language: python :linenos: - :lines: 28-40 + :lines: 27-39 :caption: ``example/ex_1_removing_the_retry_arguments.py`` .. note:: @@ -109,7 +109,7 @@ that case, you can add the highlighted line: .. literalinclude:: ../../example/ex_2_running_certain_stages_by_default.py :language: python :linenos: - :lines: 28-30,42-43 + :lines: 27-29,41-42 :emphasize-lines: 4 :caption: ``example/ex_2_running_certain_stages_by_default.py`` @@ -138,7 +138,7 @@ Now let's see about adding some arguments to the parser beyond what .. literalinclude:: ../../example/ex_3_adding_arguments.py :language: python :linenos: - :lines: 33-35,46-58 + :lines: 32-34,45-57 :emphasize-lines: 4-15 :caption: ``example/ex_3_adding_arguments.py`` @@ -149,7 +149,7 @@ arguments to handle these new options. You can do so by extending the .. literalinclude:: ../../example/ex_3_adding_arguments.py :language: python :linenos: - :lines: 60-71 + :lines: 59-70 :caption: ``example/ex_3_adding_arguments.py`` .. note:: @@ -166,7 +166,7 @@ the two stages to take them into account. .. literalinclude:: ../../example/ex_3_adding_arguments.py :language: python :linenos: - :lines: 21-31 + :lines: 20-30 :emphasize-lines: 4,10 :caption: ``example/ex_3_adding_arguments.py`` @@ -193,7 +193,7 @@ overridden in your subclasses. .. literalinclude:: ../../example/ex_4_customizing_stage_behavior.py :language: python :linenos: - :lines: 73-94 + :lines: 72-93 :caption: ``example/ex_4_customizing_stage_behavior.py`` .. note:: @@ -243,7 +243,7 @@ is the name of the stage as provided to the :ref:`StagedScript.stage() .. literalinclude:: ../../example/ex_5_customizing_individual_stages.py :language: python :linenos: - :lines: 96-113 + :lines: 95-112 :caption: ``example/ex_5_customizing_individual_stages.py`` Now when we run both stages we see: diff --git a/doc/source/index.rst b/doc/source/index.rst index 449e29e..cc86865 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -54,7 +54,7 @@ .. |PyPI Version| image:: https://img.shields.io/pypi/v/staged-script?label=PyPI :target: https://pypi.org/project/staged-script/ .. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/staged-script?label=PyPI%20downloads -.. |Python Version| image:: https://img.shields.io/badge/Python-3.8|3.9|3.10|3.11|3.12-blue.svg +.. |Python Version| image:: https://img.shields.io/badge/Python-3.9|3.10|3.11|3.12-blue.svg .. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff diff --git a/example/ex_0_the_basics.py b/example/ex_0_the_basics.py index 7faafa4..1f1c64e 100755 --- a/example/ex_0_the_basics.py +++ b/example/ex_0_the_basics.py @@ -8,7 +8,6 @@ # SPDX-License-Identifier: BSD-3-Clause import sys -from typing import List from staged_script import StagedScript @@ -22,7 +21,7 @@ def say_hello(self) -> None: def say_goodbye(self) -> None: self.run("echo 'Goodbye World'", shell=True) - def main(self, argv: List[str]) -> None: + def main(self, argv: list[str]) -> None: self.parse_args(argv) try: self.say_hello() diff --git a/example/ex_1_removing_the_retry_arguments.py b/example/ex_1_removing_the_retry_arguments.py index d63124a..dc8a167 100755 --- a/example/ex_1_removing_the_retry_arguments.py +++ b/example/ex_1_removing_the_retry_arguments.py @@ -11,7 +11,6 @@ import functools import sys from argparse import ArgumentParser -from typing import List from staged_script import StagedScript @@ -39,7 +38,7 @@ def parser(self) -> ArgumentParser: self.goodbye_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined] return my_parser - def main(self, argv: List[str]) -> None: + def main(self, argv: list[str]) -> None: self.parse_args(argv) try: self.say_hello() diff --git a/example/ex_2_running_certain_stages_by_default.py b/example/ex_2_running_certain_stages_by_default.py index 4f70ed5..683674e 100755 --- a/example/ex_2_running_certain_stages_by_default.py +++ b/example/ex_2_running_certain_stages_by_default.py @@ -11,7 +11,6 @@ import functools import sys from argparse import ArgumentParser -from typing import List from staged_script import StagedScript @@ -42,7 +41,7 @@ def parser(self) -> ArgumentParser: my_parser.set_defaults(stage=list(self.stages)) return my_parser - def main(self, argv: List[str]) -> None: + def main(self, argv: list[str]) -> None: self.parse_args(argv) try: self.say_hello() diff --git a/example/ex_3_adding_arguments.py b/example/ex_3_adding_arguments.py index 222dd15..282b931 100755 --- a/example/ex_3_adding_arguments.py +++ b/example/ex_3_adding_arguments.py @@ -12,7 +12,6 @@ import sys from argparse import ArgumentParser from pathlib import Path -from typing import List from staged_script import StagedScript @@ -57,7 +56,7 @@ def parser(self) -> ArgumentParser: ) return my_parser - def parse_args(self, argv: List[str]) -> None: + def parse_args(self, argv: list[str]) -> None: # The base class saves the parsed arguments as `self.args`. super().parse_args(argv) @@ -70,7 +69,7 @@ def parse_args(self, argv: List[str]) -> None: # not. self.args.some_file = self.args.some_file.resolve() - def main(self, argv: List[str]) -> None: + def main(self, argv: list[str]) -> None: self.parse_args(argv) try: self.say_hello() diff --git a/example/ex_4_customizing_stage_behavior.py b/example/ex_4_customizing_stage_behavior.py index 2ac7422..f6e507c 100755 --- a/example/ex_4_customizing_stage_behavior.py +++ b/example/ex_4_customizing_stage_behavior.py @@ -12,7 +12,6 @@ import sys from argparse import ArgumentParser from pathlib import Path -from typing import List from staged_script import StagedScript @@ -57,7 +56,7 @@ def parser(self) -> ArgumentParser: ) return my_parser - def parse_args(self, argv: List[str]) -> None: + def parse_args(self, argv: list[str]) -> None: # The base class saves the parsed arguments as `self.args`. super().parse_args(argv) @@ -93,7 +92,7 @@ def _run_post_stage_actions(self) -> None: "Checking to make sure all is well after running the stage..." ) - def main(self, argv: List[str]) -> None: + def main(self, argv: list[str]) -> None: self.parse_args(argv) try: self.say_hello() diff --git a/example/ex_5_customizing_individual_stages.py b/example/ex_5_customizing_individual_stages.py index b10f61a..7cf63b8 100755 --- a/example/ex_5_customizing_individual_stages.py +++ b/example/ex_5_customizing_individual_stages.py @@ -12,7 +12,6 @@ import sys from argparse import ArgumentParser from pathlib import Path -from typing import List from staged_script import StagedScript @@ -57,7 +56,7 @@ def parser(self) -> ArgumentParser: ) return my_parser - def parse_args(self, argv: List[str]) -> None: + def parse_args(self, argv: list[str]) -> None: # The base class saves the parsed arguments as `self.args`. super().parse_args(argv) @@ -112,7 +111,7 @@ def _end_stage_goodbye(self) -> None: # or `super()` calls to the default method for the corresponding # phase, if you like. - def main(self, argv: List[str]) -> None: + def main(self, argv: list[str]) -> None: self.parse_args(argv) try: self.say_hello() diff --git a/example/ex_6_creating_retryable_stages.py b/example/ex_6_creating_retryable_stages.py index bf0eff8..bfa9079 100755 --- a/example/ex_6_creating_retryable_stages.py +++ b/example/ex_6_creating_retryable_stages.py @@ -12,7 +12,7 @@ import sys from argparse import ArgumentParser from pathlib import Path -from typing import List, Optional, Set +from typing import Optional from staged_script import RetryStage, StagedScript @@ -20,7 +20,7 @@ class MyScript(StagedScript): def __init__( self, - stages: Set[str], + stages: set[str], *, console_force_terminal: Optional[bool] = None, console_log_path: bool = True, @@ -86,7 +86,7 @@ def parser(self) -> ArgumentParser: ) return my_parser - def parse_args(self, argv: List[str]) -> None: + def parse_args(self, argv: list[str]) -> None: # The base class saves the parsed arguments as `self.args`. super().parse_args(argv) @@ -141,7 +141,7 @@ def _end_stage_goodbye(self) -> None: # or `super()` calls to the default method for the corresponding # phase, if you like. - def main(self, argv: List[str]) -> None: + def main(self, argv: list[str]) -> None: self.parse_args(argv) try: self.say_hello() diff --git a/example/ex_7_customizing_the_summary.py b/example/ex_7_customizing_the_summary.py index 4a0b173..4d8c1fe 100755 --- a/example/ex_7_customizing_the_summary.py +++ b/example/ex_7_customizing_the_summary.py @@ -14,7 +14,7 @@ import sys from argparse import ArgumentParser from pathlib import Path -from typing import Dict, List, Optional, Set +from typing import Optional from staged_script import RetryStage, StagedScript @@ -22,7 +22,7 @@ class MyScript(StagedScript): def __init__( self, - stages: Set[str], + stages: set[str], *, console_force_terminal: Optional[bool] = None, console_log_path: bool = True, @@ -88,7 +88,7 @@ def parser(self) -> ArgumentParser: ) return my_parser - def parse_args(self, argv: List[str]) -> None: + def parse_args(self, argv: list[str]) -> None: # The base class saves the parsed arguments as `self.args`. super().parse_args(argv) @@ -145,7 +145,7 @@ def _end_stage_goodbye(self) -> None: def print_script_execution_summary( self, - extra_sections: Optional[Dict[str, str]] = None, + extra_sections: Optional[dict[str, str]] = None, ) -> None: extras = { "Machine details": ( @@ -154,10 +154,10 @@ def print_script_execution_summary( ), } if extra_sections is not None: - extras.update(extra_sections) + extras |= extra_sections super().print_script_execution_summary(extra_sections=extras) - def main(self, argv: List[str]) -> None: + def main(self, argv: list[str]) -> None: self.parse_args(argv) try: self.say_hello() diff --git a/example/test_examples.py b/example/test_examples.py index 10a105f..05a6261 100755 --- a/example/test_examples.py +++ b/example/test_examples.py @@ -9,10 +9,9 @@ import subprocess from pathlib import Path -from typing import List -def assert_output_in_order(stdout: str, output: List[str]) -> None: +def assert_output_in_order(stdout: str, output: list[str]) -> None: """ Ensure the output appears in the correct order. diff --git a/pyproject.toml b/pyproject.toml index 0d09c40..a2e8fa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", diff --git a/staged_script/staged_script.py b/staged_script/staged_script.py index 7f4b9f1..2780c20 100644 --- a/staged_script/staged_script.py +++ b/staged_script/staged_script.py @@ -24,7 +24,7 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from subprocess import CompletedProcess -from typing import Callable, Dict, List, NamedTuple, Optional, Set +from typing import Callable, NamedTuple, Optional import __main__ import rich.traceback @@ -58,14 +58,14 @@ class StagedScript: Attributes: args (Namespace): The parsed command line arguments for the script. - commands_executed (List[str]): The commands that were executed + commands_executed (list[str]): The commands that were executed in the shell. console (Console): Used to print rich text to the console. current_stage (str): The name of the stage being run. dry_run (bool): If ``True``, don't actually run the command that would be executed in the shell; instead just print it out. - durations (List[StageDuration]): A mapping from stage names to + durations (list[StageDuration]): A mapping from stage names to how long it took for each to run. This is implemented as a ``list`` of named tuples instead of as a ``dict`` to allow the flexibility for stages to be run multiple times. @@ -81,12 +81,12 @@ class StagedScript: script_success (bool): Subclass developers can toggle this attribute to indicate whether the script has succeeded. stage_start_time (datetime): The time at which a stage began. - stages (Set[str]): The stages registered for an instantiation + stages (set[str]): The stages registered for an instantiation of a :class:`StagedScript` subclass, which are used to automatically populate pieces of the :class:`ArgumentParser`. This may be a subset of all the stages defined in the subclass. - stages_to_run (Set[str]): Which stages to run, as specified by + stages_to_run (set[str]): Which stages to run, as specified by the user via the command line arguments. start_time (datetime): The time at which this object was initialized. @@ -116,7 +116,7 @@ class StagedScript: def __init__( self, - stages: Set[str], + stages: set[str], *, console_force_terminal: Optional[bool] = None, console_log_path: bool = True, @@ -147,20 +147,20 @@ def __init__( for stage in stages: self._validate_stage_name(stage) self.args = Namespace() - self.commands_executed: List[str] = [] + self.commands_executed: list[str] = [] self.console = Console( force_terminal=console_force_terminal, log_path=console_log_path ) self.current_stage = "CURRENT STAGE NOT SET" self.dry_run = False - self.durations: List[StageDuration] = [] + self.durations: list[StageDuration] = [] self.print_commands = print_commands self.script_name = Path(__main__.__file__).name self.script_stem = Path(__main__.__file__).stem self.script_success = True self.stage_start_time = datetime.now(tz=timezone.utc) self.stages = stages - self.stages_to_run: Set[str] = set() + self.stages_to_run: set[str] = set() self.start_time = datetime.now(tz=timezone.utc) @staticmethod @@ -725,7 +725,7 @@ def parser(self) -> ArgumentParser: setattr(self, f"{stage}_retry_timeout_arg", retry_timeout) return my_parser - def parse_args(self, argv: List[str]) -> None: + def parse_args(self, argv: list[str]) -> None: """ Parse the command line arguments. @@ -737,7 +737,7 @@ def parse_args(self, argv: List[str]) -> None: .. code-block:: python - def parse_args(self, argv: List[str]) -> None: + def parse_args(self, argv: list[str]) -> None: super().parse_args(argv) # Parse additional arguments and store as attributes. self.foo = self.args.foo @@ -868,7 +868,7 @@ def pretty_print_command(self, command: str, indent: int = 4) -> str: # def print_script_execution_summary( - self, extra_sections: Optional[Dict[str, str]] = None + self, extra_sections: Optional[dict[str, str]] = None ) -> None: """ Print a summary of everything that was done by the script. @@ -890,11 +890,11 @@ def print_script_execution_summary( def print_script_execution_summary( self, - extra_sections: Optional[Dict[str, str]] = None + extra_sections: Optional[dict[str, str]] = None ) -> None: extras = {"Additional section": "With some details."} if extra_sections is not None: - extras.update(extra_sections) + extras |= extra_sections super().print_script_execution_summary( extra_sections=extras ) @@ -911,7 +911,7 @@ def print_script_execution_summary( ), } if extra_sections is not None: - sections.update(extra_sections) + sections |= extra_sections items = [""] for section, details in sections.items(): items.extend([f"➤ {section}:", Padding(details, (1, 0, 1, 4))]) @@ -977,7 +977,7 @@ def _get_timing_report(self) -> Table: return table @staticmethod - def _current_arg_is_long_flag(args: List[str]) -> bool: + def _current_arg_is_long_flag(args: list[str]) -> bool: """ Determine if the first argument in the list is a long flag. @@ -990,7 +990,7 @@ def _current_arg_is_long_flag(args: List[str]) -> bool: return len(args) > 0 and args[0].startswith("--") @staticmethod - def _next_arg_is_flag(args: List[str]) -> bool: + def _next_arg_is_flag(args: list[str]) -> bool: """ Determine if the second argument in the list is a flag. diff --git a/test/test_staged_script.py b/test/test_staged_script.py index b72b3ae..ae73784 100644 --- a/test/test_staged_script.py +++ b/test/test_staged_script.py @@ -9,7 +9,7 @@ import shlex from datetime import datetime, timedelta, timezone from subprocess import CompletedProcess -from typing import Dict, Optional +from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -252,7 +252,7 @@ def test_run_dry_run( ) def test_print_script_execution_summary( mock_get_pretty_command_line_invocation: MagicMock, - extras: Optional[Dict[str, str]], + extras: Optional[dict[str, str]], script_success: bool, # noqa: FBT001 script: StagedScript, capsys: pytest.CaptureFixture, diff --git a/test/test_staged_script_advanced_subclass.py b/test/test_staged_script_advanced_subclass.py index fa003b4..82227b3 100644 --- a/test/test_staged_script_advanced_subclass.py +++ b/test/test_staged_script_advanced_subclass.py @@ -11,7 +11,6 @@ import pytest from rich.console import Console from tenacity import RetryCallState, Retrying, TryAgain -from typing import Set from staged_script import StagedScript @@ -104,7 +103,7 @@ def ensure_phase_comes_next( @pytest.mark.parametrize("custom_pre_stage", [True, False]) @pytest.mark.parametrize("stages_to_run", [{"test"}, set()]) def test_stage( # noqa: PLR0913 - stages_to_run: Set[str], + stages_to_run: set[str], custom_pre_stage: bool, # noqa: FBT001 custom_begin_stage: bool, # noqa: FBT001 custom_skip_stage: bool, # noqa: FBT001 diff --git a/test/test_staged_script_basic_subclass.py b/test/test_staged_script_basic_subclass.py index 7891b40..d30f553 100644 --- a/test/test_staged_script_basic_subclass.py +++ b/test/test_staged_script_basic_subclass.py @@ -6,8 +6,6 @@ # SPDX-License-Identifier: BSD-3-Clause -from typing import Set - import pytest from rich.console import Console @@ -51,7 +49,7 @@ def script() -> MyBasicScript: @pytest.mark.parametrize("stages_to_run", [{"good"}, set()]) def test_good_stage( - stages_to_run: Set[str], + stages_to_run: set[str], script: MyBasicScript, capsys: pytest.CaptureFixture, ) -> None: