Skip to content
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.9", "3.10", "3.11", "3.12", "3.13"]
version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:

- name: Harden Runner
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ version: 2
build:
os: "ubuntu-22.04"
tools:
python: "3.9"
python: "3.10"

# Build documentation in the "doc/" directory with Sphinx.
sphinx:
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.9|3.10|3.11|3.12|3.13-blue.svg)
![Python Version](https://img.shields.io/badge/Python-3.10|3.11|3.12|3.13|3.14-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.9|3.10|3.11|3.12|3.13-blue.svg
.. |Python Version| image:: https://img.shields.io/badge/Python-3.10|3.11|3.12|3.13|3.14-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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Software Development",
"Topic :: Software Development :: Debuggers",
"Topic :: Software Development :: Documentation",
Expand All @@ -44,7 +44,7 @@ Issues = "https://github.com/sandialabs/reverse_argparse/issues"


[tool.poetry.dependencies]
python = ">=3.8"
python = ">=3.10"


[tool.poetry.dev-dependencies]
Expand Down
61 changes: 31 additions & 30 deletions reverse_argparse/reverse_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,36 +114,37 @@ def _unparse_action(self, action: Action) -> None: # noqa: C901, PLR0912
or self._arg_is_default_and_help_is_suppressed(action)
):
return
if action_type == "_AppendAction":
self._unparse_append_action(action)
elif action_type == "_AppendConstAction":
self._unparse_append_const_action(action)
elif action_type == "_CountAction":
self._unparse_count_action(action)
elif action_type == "_ExtendAction":
self._unparse_extend_action(action)
elif action_type == "_HelpAction": # pragma: no cover
return
elif action_type == "_StoreAction":
self._unparse_store_action(action)
elif action_type == "_StoreConstAction":
self._unparse_store_const_action(action)
elif action_type == "_StoreFalseAction":
self._unparse_store_false_action(action)
elif action_type == "_StoreTrueAction":
self._unparse_store_true_action(action)
elif action_type == "_SubParsersAction":
self._unparse_sub_parsers_action(action)
elif action_type == "_VersionAction": # pragma: no cover
return
elif action_type == "BooleanOptionalAction":
self._unparse_boolean_optional_action(action)
else: # pragma: no cover
message = (
f"{self.__class__.__name__} does not yet support the "
f"unparsing of {action_type} objects."
)
raise NotImplementedError(message)
match action_type:
case "_AppendAction":
self._unparse_append_action(action)
case "_AppendConstAction":
self._unparse_append_const_action(action)
case "_CountAction":
self._unparse_count_action(action)
case "_ExtendAction":
self._unparse_extend_action(action)
case "_HelpAction": # pragma: no cover
return
case "_StoreAction":
self._unparse_store_action(action)
case "_StoreConstAction":
self._unparse_store_const_action(action)
case "_StoreFalseAction":
self._unparse_store_false_action(action)
case "_StoreTrueAction":
self._unparse_store_true_action(action)
case "_SubParsersAction":
self._unparse_sub_parsers_action(action)
case "_VersionAction": # pragma: no cover
return
case "BooleanOptionalAction":
self._unparse_boolean_optional_action(action)
case _: # pragma: no cover
message = (
f"{self.__class__.__name__} does not yet support the "
f"unparsing of {action_type} objects."
)
raise NotImplementedError(message)

def _arg_is_default_and_help_is_suppressed(self, action: Action) -> bool:
"""
Expand Down
57 changes: 41 additions & 16 deletions test/test_reverse_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

import shlex
from argparse import SUPPRESS, ArgumentParser, BooleanOptionalAction, Namespace
from typing import Any, Optional
from typing import Any

import pytest

Expand Down Expand Up @@ -128,6 +130,31 @@ def test_strip_first_line() -> None:
assert strip_first_line("foo\nbar\nbaz") == "bar\nbaz"


def remove_pytest_prefix(input_string: str) -> str:
"""
Remove ``-m pytest`` from the start of a space-delimited string.

In Python 3.14, pytest now uses the ``__main__`` module's
``__file__`` attribute to determine the entry point of the test run.
This causes a command line invocation to include the pytest runner's
arguments, which we then need to remove before comparing to expected
output.

Args:
input_string: The string to remove the prefix from.

Returns:
The string without the prefix.
"""
return input_string.removeprefix("-m pytest").strip()


def test_remove_pytest_prefix() -> None:
"""Ensure :func:`remove_pytest_prefix` works as expected."""
assert remove_pytest_prefix("-m pytest foo bar baz") == "foo bar baz"
assert remove_pytest_prefix("-m pytest") == ""


@pytest.mark.parametrize("args", COMPLETE_ARGS)
def test_get_effective_command_line_invocation(
parser: ArgumentParser, args: str
Expand All @@ -143,8 +170,8 @@ def test_get_effective_command_line_invocation(
"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()
result = remove_pytest_prefix(
strip_first_entry(unparser.get_effective_command_line_invocation())
)
assert result == expected

Expand Down Expand Up @@ -293,8 +320,8 @@ def test__arg_is_default_and_help_is_suppressed() -> None:
parser.add_argument("--suppressed", default=10, help=SUPPRESS)
namespace = parser.parse_args(shlex.split(""))
unparser = ReverseArgumentParser(parser, namespace)
result = strip_first_entry(
unparser.get_effective_command_line_invocation()
result = remove_pytest_prefix(
strip_first_entry(unparser.get_effective_command_line_invocation())
)
assert result == ""

Expand Down Expand Up @@ -435,7 +462,7 @@ def test__unparse_store_const_action(
("args", "expected"), [(shlex.split("--foo"), " --foo"), ([], None)]
)
def test__unparse_store_true_action(
args: list[str], expected: Optional[str]
args: list[str], expected: str | None
) -> None:
"""Ensure ``store_true`` actions are handled appropriately."""
parser = ArgumentParser()
Expand All @@ -450,7 +477,7 @@ def test__unparse_store_true_action(
("args", "expected"), [(shlex.split("--foo"), " --foo"), ([], None)]
)
def test__unparse_store_false_action(
args: list[str], expected: Optional[str]
args: list[str], expected: str | None
) -> None:
"""Ensure ``store_false`` actions are handled appropriately."""
parser = ArgumentParser()
Expand Down Expand Up @@ -496,9 +523,7 @@ def test__unparse_append_action(
@pytest.mark.parametrize(
("args", "expected"), [("--foo", " --foo"), ("", None)]
)
def test__unparse_append_const_action(
args: str, expected: Optional[str]
) -> None:
def test__unparse_append_const_action(args: str, expected: str | None) -> None:
"""Ensure ``append_const`` actions are handled appropriately."""
parser = ArgumentParser()
action = parser.add_argument(
Expand Down Expand Up @@ -570,8 +595,8 @@ def test__unparse_sub_parsers_action(
namespace = parser.parse_args(shlex.split(args))
unparser = ReverseArgumentParser(parser, namespace)
unparser._unparse_args()
result = strip_first_entry(
unparser.get_effective_command_line_invocation()
result = remove_pytest_prefix(
strip_first_entry(unparser.get_effective_command_line_invocation())
)
assert result == expected
result = strip_first_line(unparser.get_pretty_command_line_invocation())
Expand Down Expand Up @@ -612,8 +637,8 @@ def test__unparse_sub_parsers_action_nested() -> None:
namespace = parser.parse_args(shlex.split(args))
unparser = ReverseArgumentParser(parser, namespace)
unparser._unparse_args()
result = strip_first_entry(
unparser.get_effective_command_line_invocation()
result = remove_pytest_prefix(
strip_first_entry(unparser.get_effective_command_line_invocation())
)
assert result == args
result = strip_first_line(unparser.get_pretty_command_line_invocation())
Expand Down Expand Up @@ -646,9 +671,9 @@ def test__unparse_extend_action() -> None:
],
)
def test__unparse_boolean_optional_action(
default: Optional[bool], # noqa: FBT001
default: bool | None, # noqa: FBT001
args: str,
expected: Optional[str],
expected: str | None,
) -> None:
"""Ensure ``BooleanOptionalAction`` actions are handled appropriately."""
parser = ArgumentParser()
Expand Down