Repository: sandialabs/reverse_argparse (language: python)

Issue:
chore: Switch to Ruff
**Type:  Task**

## Description
Use Ruff to replace a variety of linters and formatters.  Also turn on many of the linters Ruff adds over our prior configuration, and address any findings.

## Related Issues/PRs
Closes #56.

## Commits
1. **chore: Switch to Ruff** (6112943ca3f2a74c0419d8d2452884dd144c8309)

   Use Ruff to replace a variety of linters/formatters.
1. **docs: Add docstrings to test/example files** (f0b7eb96cff1cd6a6360b1e9eba0b66c614c6ac1)
1. **chore: Ignore security findings in tests/examples** (f18ece4a69291b96b84a80bd8eea0a5f51697026)
1. **refactor: Don't override Python builtins** (9006adbeefdbd08237325d91fbdf7ecf952b386d)
1. **test: Use UTC timezone with datetime** (25ff30ff9c19378108db1cc31f367e0a98e189d9)

   Running `flake8-datetimez` via Ruff yielded the following findings:

       example/test_examples.py:88:25: DTZ005 `datetime.datetime.now()` called without a `tz` argument
       example/test_examples.py:89:25: DTZ007 Naive datetime constructed using `datetime.datetime.strptime()` without %z
       example/test_examples.py:125:13: DTZ005 `datetime.datetime.now()` called without a `tz` argument
       example/test_examples.py:126:25: DTZ007 Naive datetime constructed using `datetime.datetime.strptime()` without %z
       Found 4 errors.

   Specifying timezone info resolves the issues.
1. **refactor: Assign exception messages to variables** (844c71ab25ce20ebb22b111524740b2c991f3f29)

   Running `flake8-errmsg` via Ruff yielded the following findings:

       reverse_argparse/reverse_argparse.py:143:17: EM102 Exception must not use an f-string literal, assign to variable first
       reverse_argparse/reverse_argparse.py:461:17: EM102 Exception must not use an f-string literal, assign to variable first
       Found 2 errors.

   This change resolves the issues.
1. **refactor: Remove unnecessary shebang lines** (a7ae1f6813797f48f3889d6b362217c55a853079)
1. **test: Use tuples for parametrize variables** (25d9be8d014f0159c4328594af84fc723fdad6f9)
1. **refactor: Ignore particular type error** (9d412ddc83a6b0ad5ad3acfb5a520eb0df20086b)
1. **refactor: Address Pylint findings** (862bc62bee7a61659cb69e668f4a1f1d1e9c0c1f)
1. **refactor: Address Ruff-specific lint findings** (076e19951c8fb2241c85d4da063d166292054f25)
1. **patch: Force `prefer_short` to be keyword-only** (f02c7f2eee4861904d3505fd7a6d2cc0fbc5cfc8)

   Turning on `flake8-boolean-trap` linting via Ruff resulted in the following findings:

       reverse_argparse/reverse_argparse.py:242:31: FBT002 Boolean default positional argument in function definition
       reverse_argparse/reverse_argparse.py:242:31: FBT001 Boolean-typed positional argument in function definition
       Found 2 errors.

   Switching `prefer_short` from a positional to a keyword-only argument addresses the problem.

   Note that this is technically a breaking change, but only for a "private" method, not in the package's public API.  Therefore the change is not registered as a breaking change via Conventional Commit syntax, and no major version update will be created.  Instead, this commit will force the creation of a patch release.  If users were relying on the prior behavior of this internal method, they can simply switch to the keyword syntax when calling it.


Discussion / hints:
## [Codecov](https://app.codecov.io/gh/sandialabs/reverse_argparse/pull/112?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sandialabs) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.04%. Comparing base [(`a8b9989`)](https://app.codecov.io/gh/sandialabs/reverse_argparse/commit/a8b9989e8293ed9bfe4b80f0181050fa0341b060?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sandialabs) to head [(`f02c7f2`)](https://app.codecov.io/gh/sandialabs/reverse_argparse/pull/112?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sandialabs).
> Report is 2 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #112      +/-   ##
==========================================
+ Coverage   94.01%   94.04%   +0.03%     
==========================================
  Files           2        2              
  Lines         167      168       +1     
  Branches       43       43              
==========================================
+ Hits          157      158       +1     
  Misses          4        4              
  Partials        6        6              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sandialabs/reverse_argparse/pull/112?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sandialabs).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sandialabs).


Fix patch:
diff --git a/pyproject.toml b/pyproject.toml
index eee3c0f..75fe78b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,10 +3,6 @@ requires = ["poetry-core"]
 build-backend = "poetry.core.masonry.api"
 
 
-[tool.bandit.assert_used]
-skips = ["**/test_*.py"]
-
-
 [tool.commitizen]
 name = "cz_customize"
 
@@ -103,6 +99,10 @@ ignore = [
 ]
 
 
+[tool.ruff.lint.per-file-ignores]
+"**/test_*.py" = ["S101"]
+
+
 [tool.ruff.lint.pydocstyle]
 convention = "google"
 


Test patch:
diff --git a/example/test_examples.py b/example/test_examples.py
index 48b4644..2b671d8 100755
--- a/example/test_examples.py
+++ b/example/test_examples.py
@@ -23,7 +23,7 @@ def test_basic() -> None:
         check=True,
         text=True,
     )
-    assert (  # noqa: S101
+    assert (
         result.stdout
         == """
 The effective command line invocation was:
@@ -41,7 +41,7 @@ def test_default_values() -> None:
         check=True,
         text=True,
     )
-    assert (  # noqa: S101
+    assert (
         result.stdout
         == """
 The effective command line invocation was:
@@ -59,14 +59,14 @@ def test_relative_references() -> None:
         check=True,
         text=True,
     )
-    assert (  # noqa: S101
+    assert (
         """
 The effective command line invocation was:
 relative_references.py --bar spam --baz 42 --src
 """.strip()
         in result.stdout
     )
-    assert re.search(r"--src /\S+/bar\.txt", result.stdout)  # noqa: S101
+    assert re.search(r"--src /\S+/bar\.txt", result.stdout)
 
 
 def test_post_processing() -> None:
@@ -78,7 +78,7 @@ def test_post_processing() -> None:
         check=True,
         text=True,
     )
-    assert (  # noqa: S101
+    assert (
         """
 The effective command line invocation was:
 post_processing.py --bar spam --baz 42 --before
@@ -89,9 +89,7 @@ def test_post_processing() -> None:
     time_from_example = datetime.strptime(
         shlex.split(result.stdout)[-1], "%Y-%m-%d %H:%M:%S.%f"
     ).astimezone(timezone.utc)
-    assert (  # noqa: S101
-        thirty_miutes_ago - time_from_example < timedelta(seconds=1)
-    )
+    assert thirty_miutes_ago - time_from_example < timedelta(seconds=1)
 
 
 def test_pretty_printing() -> None:
@@ -111,7 +109,7 @@ def test_pretty_printing() -> None:
         check=True,
         text=True,
     )
-    assert (  # noqa: S101
+    assert (
         """
 The effective command line invocation was:
 pretty_printing.py \\
@@ -121,13 +119,13 @@ def test_pretty_printing() -> None:
 """.strip()
         in result.stdout
     )
-    assert re.search(r"--src /\S+/file\.txt", result.stdout)  # noqa: S101
+    assert re.search(r"--src /\S+/file\.txt", result.stdout)
     today = datetime.now(tz=timezone.utc)
     time_from_example = datetime.strptime(
         shlex.split(result.stdout.splitlines()[-1])[-1],
         "%Y-%m-%d %H:%M:%S.%f",
     ).astimezone(timezone.utc)
-    assert today - time_from_example < timedelta(seconds=1)  # noqa: S101
+    assert today - time_from_example < timedelta(seconds=1)
 
 
 def test_subparsers() -> None:
@@ -139,7 +137,7 @@ def test_subparsers() -> None:
         check=True,
         text=True,
     )
-    assert (  # noqa: S101
+    assert (
         result.stdout
         == """
 The effective command line invocation was:
diff --git a/test/test_reverse_argparse.py b/test/test_reverse_argparse.py
index 223c328..87ab117 100644
--- a/test/test_reverse_argparse.py
+++ b/test/test_reverse_argparse.py
@@ -117,7 +117,7 @@ def strip_first_entry(input_string: str) -> str:
 
 def test_strip_first_entry() -> None:
     """Ensure :func:`strip_first_entry` works as expected."""
-    assert strip_first_entry("foo bar baz") == "bar baz"  # noqa: S101
+    assert strip_first_entry("foo bar baz") == "bar baz"
 
 
 def strip_first_line(input_string: str) -> str:
@@ -135,7 +135,7 @@ def strip_first_line(input_string: str) -> str:
 
 def test_strip_first_line() -> None:
     """Ensure :func:`strip_first_line` works as expected."""
-    assert strip_first_line("foo\nbar\nbaz") == "bar\nbaz"  # noqa: S101
+    assert strip_first_line("foo\nbar\nbaz") == "bar\nbaz"
 
 
 @pytest.mark.parametrize("args", COMPLETE_ARGS)
@@ -162,7 +162,7 @@ def test_get_effective_command_line_invocation(parser, args) -> None:
     result = strip_first_entry(
         unparser.get_effective_command_line_invocation()
     )
-    assert result == expected  # noqa: S101
+    assert result == expected
 
 
 @pytest.mark.parametrize("args", COMPLETE_ARGS)
@@ -192,7 +192,7 @@ def test_get_pretty_command_line_invocation(parser, args) -> None:
     expected += """\n    pos1-val1 pos1-val2 \\
     pos2-val"""
     result = strip_first_line(unparser.get_pretty_command_line_invocation())
-    assert result == expected  # noqa: S101
+    assert result == expected
 
 
 def test_get_command_line_invocation_strip_spaces() -> None:
@@ -203,13 +203,11 @@ def test_get_command_line_invocation_strip_spaces() -> None:
     unparser._args = ["program_name", "    --foo", "    ", "    --bar"]
     unparser._unparsed = [True]
     expected = "program_name --foo --bar"
-    assert (  # noqa: S101
-        unparser.get_effective_command_line_invocation() == expected
-    )
+    assert unparser.get_effective_command_line_invocation() == expected
     expected = """    --foo \\
     --bar"""
     result = strip_first_line(unparser.get_pretty_command_line_invocation())
-    assert result == expected  # noqa: S101
+    assert result == expected
 
 
 @pytest.mark.parametrize(
@@ -266,7 +264,7 @@ def test__unparse_args(add_args, add_kwargs, args, expected) -> None:
             unparser._unparse_args()
     else:
         unparser._unparse_args()
-        assert unparser._args[1:] == expected  # noqa: S101
+        assert unparser._args[1:] == expected
 
 
 def test__unparse_args_boolean_optional_action() -> None:
@@ -285,7 +283,7 @@ def test__unparse_args_boolean_optional_action() -> None:
             namespace = Namespace()
         unparser = ReverseArgumentParser(parser, namespace)
         unparser._unparse_args()
-        assert unparser._args[1:] == ["    --foo"]  # noqa: S101
+        assert unparser._args[1:] == ["    --foo"]
 
 
 def test__unparse_args_already_unparsed() -> None:
@@ -297,7 +295,7 @@ def test__unparse_args_already_unparsed() -> None:
     args_before = unparser._args.copy()
     unparser._unparsed = [True]
     unparser._unparse_args()
-    assert unparser._args == args_before  # noqa: S101
+    assert unparser._args == args_before
 
 
 def test__arg_is_default_and_help_is_suppressed() -> None:
@@ -309,7 +307,7 @@ def test__arg_is_default_and_help_is_suppressed() -> None:
     result = strip_first_entry(
         unparser.get_effective_command_line_invocation()
     )
-    assert result == ""  # noqa: S101
+    assert result == ""
 
 
 @pytest.mark.parametrize(
@@ -323,7 +321,7 @@ def test__arg_is_default_and_help_is_suppressed() -> None:
 def test__get_long_option_strings(strings, expected) -> None:
     """Ensure the long-form option is selected from a list."""
     unparser = ReverseArgumentParser(ArgumentParser(), Namespace())
-    assert unparser._get_long_option_strings(strings) == expected  # noqa: S101
+    assert unparser._get_long_option_strings(strings) == expected
 
 
 @pytest.mark.parametrize(
@@ -337,9 +335,7 @@ def test__get_long_option_strings(strings, expected) -> None:
 def test__get_short_option_strings(strings, expected) -> None:
     """Ensure the short-form option is selected from a list."""
     unparser = ReverseArgumentParser(ArgumentParser(), Namespace())
-    assert (  # noqa: S101
-        unparser._get_short_option_strings(strings) == expected
-    )
+    assert unparser._get_short_option_strings(strings) == expected
 
 
 @pytest.mark.parametrize(
@@ -355,7 +351,7 @@ def test__get_option_string(strings, expected) -> None:
     parser = ArgumentParser()
     action = parser.add_argument(*strings)
     unparser = ReverseArgumentParser(parser, Namespace())
-    assert unparser._get_option_string(action) == expected  # noqa: S101
+    assert unparser._get_option_string(action) == expected
 
 
 @pytest.mark.parametrize(
@@ -371,9 +367,7 @@ def test__get_option_string_prefer_short(strings, expected) -> None:
     parser = ArgumentParser()
     action = parser.add_argument(*strings)
     unparser = ReverseArgumentParser(parser, Namespace())
-    assert (  # noqa: S101
-        unparser._get_option_string(action, prefer_short=True) == expected
-    )
+    assert unparser._get_option_string(action, prefer_short=True) == expected
 
 
 @pytest.mark.parametrize(
@@ -401,7 +395,7 @@ def test__unparse_store_action(add_args, add_kwargs, args, expected) -> None:
     namespace = parser.parse_args(shlex.split(args))
     unparser = ReverseArgumentParser(parser, namespace)
     unparser._unparse_store_action(action)
-    assert unparser._args[1:] == [expected]  # noqa: S101
+    assert unparser._args[1:] == [expected]
 
 
 @pytest.mark.parametrize(
@@ -437,9 +431,7 @@ def test__unparse_store_const_action(
     namespace = parser.parse_args(shlex.split(args))
     unparser = ReverseArgumentParser(parser, namespace)
     unparser._unparse_store_const_action(action)
-    assert (  # noqa: S101
-        unparser._args[1:] == ([expected] if expected is not None else [])
-    )
+    assert unparser._args[1:] == ([expected] if expected is not None else [])
 
 
 @pytest.mark.parametrize(
@@ -452,9 +444,7 @@ def test__unparse_store_true_action(args, expected) -> None:
     namespace = parser.parse_args(args)
     unparser = ReverseArgumentParser(parser, namespace)
     unparser._unparse_store_true_action(action)
-    assert (  # noqa: S101
-        unparser._args[1:] == ([expected] if expected is not None else [])
-    )
+    assert unparser._args[1:] == ([expected] if expected is not None else [])
 
 
 @pytest.mark.parametrize(
@@ -467,9 +457,7 @@ def test__unparse_store_false_action(args, expected) -> None:
     namespace = parser.parse_args(args)
     unparser = ReverseArgumentParser(parser, namespace)
     unparser._unparse_store_false_action(action)
-    assert (  # noqa: S101
-        unparser._args[1:] == ([expected] if expected is not None else [])
-    )
+    assert unparser._args[1:] == ([expected] if expected is not None else [])
 
 
 @pytest.mark.parametrize(
@@ -496,7 +484,7 @@ def test__unparse_append_action(add_args, add_kwargs, args, expected) -> None:
     namespace = parser.parse_args(shlex.split(args))
     unparser = ReverseArgumentParser(parser, namespace)
     unparser._unparse_append_action(action)
-    assert unparser._args[1:] == expected  # noqa: S101
+    assert unparser._args[1:] == expected
 
 
 @pytest.mark.parametrize(
@@ -511,9 +499,7 @@ def test__unparse_append_const_action(args, expected) -> None:
     namespace = parser.parse_args(shlex.split(args))
     unparser = ReverseArgumentParser(parser, namespace)
     unparser._unparse_append_const_action(action)
-    assert (  # noqa: S101
-        unparser._args[1:] == ([expected] if expected is not None else [])
-    )
+    assert unparser._args[1:] == ([expected] if expected is not None else [])
 
 
 @pytest.mark.parametrize(
@@ -546,7 +532,7 @@ def test__unparse_count_action(add_args, add_kwargs, args, expected) -> None:
     namespace = parser.parse_args(shlex.split(args))
     unparser = ReverseArgumentParser(parser, namespace)
     unparser._unparse_count_action(action)
-    assert unparser._args[1:] == [expected]  # noqa: S101
+    assert unparser._args[1:] == [expected]
 
 
 @pytest.mark.parametrize(
@@ -575,9 +561,9 @@ def test__unparse_sub_parsers_action(args, expected, pretty) -> None:
     result = strip_first_entry(
         unparser.get_effective_command_line_invocation()
     )
-    assert result == expected  # noqa: S101
+    assert result == expected
     result = strip_first_line(unparser.get_pretty_command_line_invocation())
-    assert result == pretty  # noqa: S101
+    assert result == pretty
 
 
 def test__unparse_sub_parsers_action_nested() -> None:
@@ -617,9 +603,9 @@ def test__unparse_sub_parsers_action_nested() -> None:
     result = strip_first_entry(
         unparser.get_effective_command_line_invocation()
     )
-    assert result == args  # noqa: S101
+    assert result == args
     result = strip_first_line(unparser.get_pretty_command_line_invocation())
-    assert result == pretty  # noqa: S101
+    assert result == pretty
 
 
 def test__unparse_extend_action() -> None:
@@ -630,7 +616,7 @@ def test__unparse_extend_action() -> None:
     unparser = ReverseArgumentParser(parser, namespace)
     expected = "    --foo bar baz bif"
     unparser._unparse_extend_action(action)
-    assert unparser._args[1:] == [expected]  # noqa: S101
+    assert unparser._args[1:] == [expected]
 
 
 @pytest.mark.parametrize(
@@ -657,6 +643,6 @@ def test__unparse_boolean_optional_action(default, args, expected) -> None:
         namespace = parser.parse_args(shlex.split(args))
         unparser = ReverseArgumentParser(parser, namespace)
         unparser._unparse_boolean_optional_action(action)
-        assert unparser._args[1:] == (  # noqa: S101
+        assert unparser._args[1:] == (
             [expected] if expected is not None else []
         )