diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 6858c8e..70ab0b2 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"] steps: - name: Harden Runner diff --git a/README.md b/README.md index 46f0942..aa481a4 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ [![pre-commit.ci Status](https://results.pre-commit.ci/badge/github/sandialabs/reverse_argparse/master.svg)](https://results.pre-commit.ci/latest/github/sandialabs/reverse_argparse/master) [![PyPI - Version](https://img.shields.io/pypi/v/reverse-argparse?label=PyPI)](https://pypi.org/project/reverse-argparse/) ![PyPI - Downloads](https://img.shields.io/pypi/dm/reverse-argparse?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) # reverse_argparse diff --git a/doc/source/index.rst b/doc/source/index.rst index f8860ae..9f8fca6 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -66,7 +66,7 @@ reverse_argparse .. |PyPI Version| image:: https://img.shields.io/pypi/v/reverse-argparse?label=PyPI :target: https://pypi.org/project/reverse-argparse/ .. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/reverse-argparse?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/pyproject.toml b/pyproject.toml index 6e28d51..fecd86e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,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/reverse_argparse/reverse_argparse.py b/reverse_argparse/reverse_argparse.py index 44f4a9b..f4530b8 100644 --- a/reverse_argparse/reverse_argparse.py +++ b/reverse_argparse/reverse_argparse.py @@ -14,12 +14,10 @@ # SPDX-License-Identifier: BSD-3-Clause import re -import sys from argparse import SUPPRESS, Action, ArgumentParser, Namespace -from typing import List, Sequence +from typing import Sequence -BOOLEAN_OPTIONAL_ACTION_MINOR_VERSION = 9 SHORT_OPTION_LENGTH = 2 @@ -38,20 +36,20 @@ class ReverseArgumentParser: such that they're able to reproduce a prior run of a script exactly. Attributes: - _args (List[str]): The list of arguments corresponding to each + _args (list[str]): The list of arguments corresponding to each :class:`argparse.Action` in the given parser, which is built up as the arguments are unparsed. _indent (int): The number of spaces with which to indent subsequent lines when pretty-printing the effective command line invocation. _namespace (Namespace): The parsed arguments. - _parsers (List[argparse.ArgumentParser]): The parser that was + _parsers (list[argparse.ArgumentParser]): The parser that was used to generate the parsed arguments. This is a ``list`` (conceptually a stack) to allow for sub-parsers, so the outer-most parser is the first item in the list, and sub-parsers are pushed onto and popped off of the stack as they are processed. - _unparsed (List[bool]): A list in which the elements indicate + _unparsed (list[bool]): A list in which the elements indicate whether the corresponding parser in :attr:`parsers` has been unparsed. """ @@ -136,10 +134,7 @@ def _unparse_action(self, action: Action) -> None: # noqa: C901, PLR0912 self._unparse_sub_parsers_action(action) elif action_type == "_VersionAction": # pragma: no cover return - elif ( - action_type == "BooleanOptionalAction" - and sys.version_info.minor >= BOOLEAN_OPTIONAL_ACTION_MINOR_VERSION - ): + elif action_type == "BooleanOptionalAction": self._unparse_boolean_optional_action(action) else: # pragma: no cover message = ( @@ -202,7 +197,7 @@ def get_pretty_command_line_invocation(self) -> str: def _get_long_option_strings( self, option_strings: Sequence[str] - ) -> List[str]: + ) -> list[str]: """ Get the long options from a list of options strings. @@ -224,7 +219,7 @@ def _get_long_option_strings( def _get_short_option_strings( self, option_strings: Sequence[str] - ) -> List[str]: + ) -> list[str]: """ Get the short options from a list of options strings. @@ -278,7 +273,7 @@ def _get_option_string( return short_options[0] return "" - def _append_list_of_list_of_args(self, args: List[List[str]]) -> None: + def _append_list_of_list_of_args(self, args: list[list[str]]) -> None: """ Append to the list of unparsed arguments. @@ -293,7 +288,7 @@ def _append_list_of_list_of_args(self, args: List[List[str]]) -> None: for line in args: self._args.append(self._indent_str + " ".join(line)) - def _append_list_of_args(self, args: List[str]) -> None: + def _append_list_of_args(self, args: list[str]) -> None: """ Append to the list of unparsed arguments. diff --git a/test/test_reverse_argparse.py b/test/test_reverse_argparse.py index bd8f462..f6ec472 100644 --- a/test/test_reverse_argparse.py +++ b/test/test_reverse_argparse.py @@ -7,21 +7,13 @@ # SPDX-License-Identifier: BSD-3-Clause import shlex -import sys -from argparse import SUPPRESS, ArgumentParser, Namespace +from argparse import SUPPRESS, ArgumentParser, BooleanOptionalAction, Namespace import pytest from reverse_argparse import ReverseArgumentParser -BOOLEAN_OPTIONAL_ACTION_MINOR_VERSION = 9 - - -if sys.version_info.minor >= BOOLEAN_OPTIONAL_ACTION_MINOR_VERSION: - from argparse import BooleanOptionalAction - - @pytest.fixture def parser() -> ArgumentParser: """ @@ -51,10 +43,7 @@ def parser() -> ArgumentParser: ) p.add_argument("--verbose", "-v", action="count", default=2) p.add_argument("--ext", action="extend", nargs="*") - if sys.version_info.minor >= BOOLEAN_OPTIONAL_ACTION_MINOR_VERSION: - p.add_argument( - "--bool-opt", action=BooleanOptionalAction, default=False - ) + p.add_argument("--bool-opt", action=BooleanOptionalAction, default=False) return p @@ -144,20 +133,12 @@ def test_get_effective_command_line_invocation(parser, args) -> None: namespace = parser.parse_args(shlex.split(args)) unparser = ReverseArgumentParser(parser, namespace) expected = ( - ( - "--opt1 opt1-val --opt2 opt2-val1 opt2-val2 --store-true " - "--store-false --needs-quotes 'hello world' --default 42 --app1 " - "app1-val1 --app1 app1-val2 --app2 app2-val1 --app2 app2-val2 " - "--app-nargs app-nargs1-val1 app-nargs1-val2 --app-nargs " - "app-nargs2-val --const --app-const1 --app-const2 -vv --ext " - "ext-val1 ext-val2 ext-val3 " - ) - + ( - "--no-bool-opt " - if sys.version_info.minor >= BOOLEAN_OPTIONAL_ACTION_MINOR_VERSION - else "" - ) - + "pos1-val1 pos1-val2 pos2-val" + "--opt1 opt1-val --opt2 opt2-val1 opt2-val2 --store-true " + "--store-false --needs-quotes 'hello world' --default 42 --app1 " + "app1-val1 --app1 app1-val2 --app2 app2-val1 --app2 app2-val2 " + "--app-nargs app-nargs1-val1 app-nargs1-val2 --app-nargs " + "app-nargs2-val --const --app-const1 --app-const2 -vv --ext ext-val1 " + "ext-val2 ext-val3 --no-bool-opt pos1-val1 pos1-val2 pos2-val" ) result = strip_first_entry( unparser.get_effective_command_line_invocation() @@ -186,10 +167,9 @@ def test_get_pretty_command_line_invocation(parser, args) -> None: --app-const1 \\ --app-const2 \\ -vv \\ - --ext ext-val1 ext-val2 ext-val3 \\""" - if sys.version_info.minor >= BOOLEAN_OPTIONAL_ACTION_MINOR_VERSION: - expected += "\n --no-bool-opt \\" - expected += """\n pos1-val1 pos1-val2 \\ + --ext ext-val1 ext-val2 ext-val3 \\ + --no-bool-opt \\ + pos1-val1 pos1-val2 \\ pos2-val""" result = strip_first_line(unparser.get_pretty_command_line_invocation()) assert result == expected @@ -274,16 +254,15 @@ def test__unparse_args_boolean_optional_action() -> None: With a ``BooleanOptionalAction``, which became available in Python 3.9. """ - if sys.version_info.minor >= BOOLEAN_OPTIONAL_ACTION_MINOR_VERSION: - parser = ArgumentParser() - parser.add_argument("--foo", action=BooleanOptionalAction) - try: - namespace = parser.parse_args(shlex.split("--foo")) - except SystemExit: - namespace = Namespace() - unparser = ReverseArgumentParser(parser, namespace) - unparser._unparse_args() - assert unparser._args[1:] == [" --foo"] + parser = ArgumentParser() + parser.add_argument("--foo", action=BooleanOptionalAction) + try: + namespace = parser.parse_args(shlex.split("--foo")) + except SystemExit: + namespace = Namespace() + unparser = ReverseArgumentParser(parser, namespace) + unparser._unparse_args() + assert unparser._args[1:] == [" --foo"] def test__unparse_args_already_unparsed() -> None: @@ -635,14 +614,11 @@ def test__unparse_extend_action() -> None: ) def test__unparse_boolean_optional_action(default, args, expected) -> None: """Ensure ``BooleanOptionalAction`` actions are handled appropriately.""" - if sys.version_info.minor >= BOOLEAN_OPTIONAL_ACTION_MINOR_VERSION: - parser = ArgumentParser() - action = parser.add_argument( - "--bool-opt", action=BooleanOptionalAction, default=default - ) - namespace = parser.parse_args(shlex.split(args)) - unparser = ReverseArgumentParser(parser, namespace) - unparser._unparse_boolean_optional_action(action) - assert unparser._args[1:] == ( - [expected] if expected is not None else [] - ) + parser = ArgumentParser() + action = parser.add_argument( + "--bool-opt", action=BooleanOptionalAction, default=default + ) + namespace = parser.parse_args(shlex.split(args)) + unparser = ReverseArgumentParser(parser, namespace) + unparser._unparse_boolean_optional_action(action) + assert unparser._args[1:] == ([expected] if expected is not None else [])