Skip to content

Commit ae7d161

Browse files
committed
patch: Drop support for Python 3.9
Undo the last of 8d15f4a. * Use a match case statement in place of an if block. * Replace Union types with X | Y syntax. Also introduce support for Python 3.14. Closes #16.
1 parent 5d7c9da commit ae7d161

File tree

7 files changed

+85
-52
lines changed

7 files changed

+85
-52
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
runs-on: ubuntu-latest
2020
strategy:
2121
matrix:
22-
version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
22+
version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
2323
steps:
2424

2525
- name: Harden Runner

.readthedocs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ version: 2
99
build:
1010
os: "ubuntu-22.04"
1111
tools:
12-
python: "3.9"
12+
python: "3.10"
1313

1414
# Build documentation in the "doc/" directory with Sphinx.
1515
sphinx:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
[![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)
1818
[![PyPI - Version](https://img.shields.io/pypi/v/reverse-argparse?label=PyPI)](https://pypi.org/project/reverse-argparse/)
1919
![PyPI - Downloads](https://img.shields.io/pypi/dm/reverse-argparse?label=PyPI%20downloads)
20-
![Python Version](https://img.shields.io/badge/Python-3.9|3.10|3.11|3.12|3.13-blue.svg)
20+
![Python Version](https://img.shields.io/badge/Python-3.10|3.11|3.12|3.13|3.14-blue.svg)
2121
[![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)
2222

2323
# reverse_argparse

doc/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ reverse_argparse
6666
.. |PyPI Version| image:: https://img.shields.io/pypi/v/reverse-argparse?label=PyPI
6767
:target: https://pypi.org/project/reverse-argparse/
6868
.. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/reverse-argparse?label=PyPI%20downloads
69-
.. |Python Version| image:: https://img.shields.io/badge/Python-3.9|3.10|3.11|3.12|3.13-blue.svg
69+
.. |Python Version| image:: https://img.shields.io/badge/Python-3.10|3.11|3.12|3.13|3.14-blue.svg
7070
.. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
7171
:target: https://github.com/astral-sh/ruff
7272

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ classifiers = [
2525
"Operating System :: OS Independent",
2626
"Programming Language :: Python :: 3",
2727
"Programming Language :: Python :: 3 :: Only",
28-
"Programming Language :: Python :: 3.9",
2928
"Programming Language :: Python :: 3.10",
3029
"Programming Language :: Python :: 3.11",
3130
"Programming Language :: Python :: 3.12",
3231
"Programming Language :: Python :: 3.13",
32+
"Programming Language :: Python :: 3.14",
3333
"Topic :: Software Development",
3434
"Topic :: Software Development :: Debuggers",
3535
"Topic :: Software Development :: Documentation",
@@ -44,7 +44,7 @@ Issues = "https://github.com/sandialabs/reverse_argparse/issues"
4444

4545

4646
[tool.poetry.dependencies]
47-
python = ">=3.8"
47+
python = ">=3.10"
4848

4949

5050
[tool.poetry.dev-dependencies]

reverse_argparse/reverse_argparse.py

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -114,36 +114,37 @@ def _unparse_action(self, action: Action) -> None: # noqa: C901, PLR0912
114114
or self._arg_is_default_and_help_is_suppressed(action)
115115
):
116116
return
117-
if action_type == "_AppendAction":
118-
self._unparse_append_action(action)
119-
elif action_type == "_AppendConstAction":
120-
self._unparse_append_const_action(action)
121-
elif action_type == "_CountAction":
122-
self._unparse_count_action(action)
123-
elif action_type == "_ExtendAction":
124-
self._unparse_extend_action(action)
125-
elif action_type == "_HelpAction": # pragma: no cover
126-
return
127-
elif action_type == "_StoreAction":
128-
self._unparse_store_action(action)
129-
elif action_type == "_StoreConstAction":
130-
self._unparse_store_const_action(action)
131-
elif action_type == "_StoreFalseAction":
132-
self._unparse_store_false_action(action)
133-
elif action_type == "_StoreTrueAction":
134-
self._unparse_store_true_action(action)
135-
elif action_type == "_SubParsersAction":
136-
self._unparse_sub_parsers_action(action)
137-
elif action_type == "_VersionAction": # pragma: no cover
138-
return
139-
elif action_type == "BooleanOptionalAction":
140-
self._unparse_boolean_optional_action(action)
141-
else: # pragma: no cover
142-
message = (
143-
f"{self.__class__.__name__} does not yet support the "
144-
f"unparsing of {action_type} objects."
145-
)
146-
raise NotImplementedError(message)
117+
match action_type:
118+
case "_AppendAction":
119+
self._unparse_append_action(action)
120+
case "_AppendConstAction":
121+
self._unparse_append_const_action(action)
122+
case "_CountAction":
123+
self._unparse_count_action(action)
124+
case "_ExtendAction":
125+
self._unparse_extend_action(action)
126+
case "_HelpAction": # pragma: no cover
127+
return
128+
case "_StoreAction":
129+
self._unparse_store_action(action)
130+
case "_StoreConstAction":
131+
self._unparse_store_const_action(action)
132+
case "_StoreFalseAction":
133+
self._unparse_store_false_action(action)
134+
case "_StoreTrueAction":
135+
self._unparse_store_true_action(action)
136+
case "_SubParsersAction":
137+
self._unparse_sub_parsers_action(action)
138+
case "_VersionAction": # pragma: no cover
139+
return
140+
case "BooleanOptionalAction":
141+
self._unparse_boolean_optional_action(action)
142+
case _: # pragma: no cover
143+
message = (
144+
f"{self.__class__.__name__} does not yet support the "
145+
f"unparsing of {action_type} objects."
146+
)
147+
raise NotImplementedError(message)
147148

148149
def _arg_is_default_and_help_is_suppressed(self, action: Action) -> bool:
149150
"""

test/test_reverse_argparse.py

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66

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

9+
from __future__ import annotations
10+
911
import shlex
1012
from argparse import SUPPRESS, ArgumentParser, BooleanOptionalAction, Namespace
11-
from typing import Any, Optional
13+
from typing import Any
1214

1315
import pytest
1416

@@ -128,6 +130,30 @@ def test_strip_first_line() -> None:
128130
assert strip_first_line("foo\nbar\nbaz") == "bar\nbaz"
129131

130132

133+
def remove_pytest_prefix(input_string: str) -> str:
134+
"""
135+
Remove ``-m pytest`` from the start of a space-delimited string.
136+
137+
In Python 3.14, pytest now uses the ``__main__`` module's
138+
``__file__`` attribute to determine the entry point of the test run.
139+
This causes a command line invocation to include the pytest runner's
140+
arguments, which we then need to remove before comparing to expected
141+
output.
142+
143+
Args:
144+
input_string: The string to remove the prefix from.
145+
146+
Returns:
147+
The string without the prefix.
148+
"""
149+
return input_string.removeprefix("-m pytest").strip()
150+
151+
def test_remove_pytest_prefix() -> None:
152+
"""Ensure :func:`remove_pytest_prefix` works as expected."""
153+
assert remove_pytest_prefix("-m pytest foo bar baz") == "foo bar baz"
154+
assert remove_pytest_prefix("-m pytest") == ""
155+
156+
131157
@pytest.mark.parametrize("args", COMPLETE_ARGS)
132158
def test_get_effective_command_line_invocation(
133159
parser: ArgumentParser, args: str
@@ -143,8 +169,10 @@ def test_get_effective_command_line_invocation(
143169
"app-nargs2-val --const --app-const1 --app-const2 -vv --ext ext-val1 "
144170
"ext-val2 ext-val3 --no-bool-opt pos1-val1 pos1-val2 pos2-val"
145171
)
146-
result = strip_first_entry(
147-
unparser.get_effective_command_line_invocation()
172+
result = remove_pytest_prefix(
173+
strip_first_entry(
174+
unparser.get_effective_command_line_invocation()
175+
)
148176
)
149177
assert result == expected
150178

@@ -293,8 +321,10 @@ def test__arg_is_default_and_help_is_suppressed() -> None:
293321
parser.add_argument("--suppressed", default=10, help=SUPPRESS)
294322
namespace = parser.parse_args(shlex.split(""))
295323
unparser = ReverseArgumentParser(parser, namespace)
296-
result = strip_first_entry(
297-
unparser.get_effective_command_line_invocation()
324+
result = remove_pytest_prefix(
325+
strip_first_entry(
326+
unparser.get_effective_command_line_invocation()
327+
)
298328
)
299329
assert result == ""
300330

@@ -435,7 +465,7 @@ def test__unparse_store_const_action(
435465
("args", "expected"), [(shlex.split("--foo"), " --foo"), ([], None)]
436466
)
437467
def test__unparse_store_true_action(
438-
args: list[str], expected: Optional[str]
468+
args: list[str], expected: str | None
439469
) -> None:
440470
"""Ensure ``store_true`` actions are handled appropriately."""
441471
parser = ArgumentParser()
@@ -450,7 +480,7 @@ def test__unparse_store_true_action(
450480
("args", "expected"), [(shlex.split("--foo"), " --foo"), ([], None)]
451481
)
452482
def test__unparse_store_false_action(
453-
args: list[str], expected: Optional[str]
483+
args: list[str], expected: str | None
454484
) -> None:
455485
"""Ensure ``store_false`` actions are handled appropriately."""
456486
parser = ArgumentParser()
@@ -496,9 +526,7 @@ def test__unparse_append_action(
496526
@pytest.mark.parametrize(
497527
("args", "expected"), [("--foo", " --foo"), ("", None)]
498528
)
499-
def test__unparse_append_const_action(
500-
args: str, expected: Optional[str]
501-
) -> None:
529+
def test__unparse_append_const_action(args: str, expected: str | None) -> None:
502530
"""Ensure ``append_const`` actions are handled appropriately."""
503531
parser = ArgumentParser()
504532
action = parser.add_argument(
@@ -570,8 +598,10 @@ def test__unparse_sub_parsers_action(
570598
namespace = parser.parse_args(shlex.split(args))
571599
unparser = ReverseArgumentParser(parser, namespace)
572600
unparser._unparse_args()
573-
result = strip_first_entry(
574-
unparser.get_effective_command_line_invocation()
601+
result = remove_pytest_prefix(
602+
strip_first_entry(
603+
unparser.get_effective_command_line_invocation()
604+
)
575605
)
576606
assert result == expected
577607
result = strip_first_line(unparser.get_pretty_command_line_invocation())
@@ -612,8 +642,10 @@ def test__unparse_sub_parsers_action_nested() -> None:
612642
namespace = parser.parse_args(shlex.split(args))
613643
unparser = ReverseArgumentParser(parser, namespace)
614644
unparser._unparse_args()
615-
result = strip_first_entry(
616-
unparser.get_effective_command_line_invocation()
645+
result = remove_pytest_prefix(
646+
strip_first_entry(
647+
unparser.get_effective_command_line_invocation()
648+
)
617649
)
618650
assert result == args
619651
result = strip_first_line(unparser.get_pretty_command_line_invocation())
@@ -646,9 +678,9 @@ def test__unparse_extend_action() -> None:
646678
],
647679
)
648680
def test__unparse_boolean_optional_action(
649-
default: Optional[bool], # noqa: FBT001
681+
default: bool | None, # noqa: FBT001
650682
args: str,
651-
expected: Optional[str],
683+
expected: str | None,
652684
) -> None:
653685
"""Ensure ``BooleanOptionalAction`` actions are handled appropriately."""
654686
parser = ArgumentParser()

0 commit comments

Comments
 (0)