Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore!: Drop support for Python 3.8 #244

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 9 additions & 14 deletions reverse_argparse/reverse_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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.
"""
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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.

Expand All @@ -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.

Expand Down
80 changes: 28 additions & 52 deletions test/test_reverse_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 [])
Loading