From 32f8dd11f4f2c77d31611459ef1139da5f3e4028 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Wed, 7 Jan 2026 12:39:32 -0500 Subject: [PATCH 01/17] Drop docopt in favor of click docopt hasn't been updated in years and is still built using setup.py. click is a more modern CLI library. For more information see: - https://click.palletsprojects.com/en/stable/ - https://github.com/pallets/click --- .pre-commit-config.yaml | 3 +- pyproject.toml | 5 +- src/example/example.py | 101 +++++++++++++++++++--------------------- 3 files changed, 52 insertions(+), 57 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 816fa90..4ffee24 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -176,8 +176,7 @@ repos: # mypy pre-commit hook additional_dependencies in sync with # the dev section of pyproject.toml to avoid discrepancies in # type checking between environments. - additional_dependencies: - - types-docopt + additional_dependencies: [] - repo: https://github.com/pypa/pip-audit rev: v2.10.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index 705b249..650844a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,8 +43,8 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", ] dependencies = [ - "docopt", - "schema", + "click>=8.3.1", + "rich>=14.2.0", ] description = "Example Python library" dynamic = ["readme", "version"] @@ -63,7 +63,6 @@ requires-python = ">=3.10" dev = [ "build", "twine", - "types-docopt", ] test = [ "coverage", diff --git a/src/example/example.py b/src/example/example.py index 78dd72e..c602612 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -7,16 +7,6 @@ This utility exits with one of the following values: 0 Calculation completed successfully. >0 An error occurred. - -Usage: - example [--log-level=LEVEL] - example (-h | --help) - -Options: - -h --help Show this message. - --log-level=LEVEL If specified, then the log level will be set to - the specified value. Valid values are "debug", "info", - "warning", "error", and "critical". [default: info] """ # Standard Python Libraries @@ -24,18 +14,24 @@ import logging import os import sys -from typing import Any # Third-Party Libraries -import docopt - -# There are no type stubs for the schema library, so mypy requires the type: -# ignore hint. -from schema import And, Schema, SchemaError, Use # type: ignore +import click +from rich.logging import RichHandler from ._version import __version__ DEFAULT_ECHO_MESSAGE: str = "Hello World from the example default!" +LOG_LEVELS: list[str] = list() +if sys.version_info.minor > 10: + LOG_LEVELS = [*logging.getLevelNamesMapping()] +else: + LOG_LEVELS = [ + logging.getLevelName(x) + for x in range(0, 101) + if not logging.getLevelName(x).startswith("Level") + ] +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) def example_div(dividend: int, divisor: int) -> float: @@ -48,44 +44,40 @@ def example_div(dividend: int, divisor: int) -> float: return dividend / divisor -def main() -> None: - """Set up logging and call the example function.""" - args: dict[str, str] = docopt.docopt(__doc__, version=__version__) - # Validate and convert arguments as needed - schema: Schema = Schema( - { - "--log-level": And( - str, - Use(str.lower), - lambda n: n in ("debug", "info", "warning", "error", "critical"), - error="Possible values for --log-level are " - + "debug, info, warning, error, and critical.", - ), - "": Use(int, error=" must be an integer."), - "": And( - Use(int), - lambda n: n != 0, - error=" must be an integer that is not 0.", - ), - str: object, # Don't care about other keys, if any - } - ) - - try: - validated_args: dict[str, Any] = schema.validate(args) - except SchemaError as err: - # Exit because one or more of the arguments were invalid - print(err, file=sys.stderr) - sys.exit(1) - - # Assign validated arguments to variables - dividend: int = validated_args[""] - divisor: int = validated_args[""] - log_level: str = validated_args["--log-level"] - +def divisor_callback(ctx: click.Context, param: click.Parameter, value: int): + """Verify that the value is nonzero.""" + if value == 0: + raise click.BadParameter("divisor must be nonzero") + + return value + + +@click.command(context_settings=CONTEXT_SETTINGS) +@click.argument("dividend", type=click.INT) +@click.argument("divisor", callback=divisor_callback, type=click.INT) +@click.option( + "-l", + "--log-level", + default="info", + help="The logging level.", + type=click.Choice(LOG_LEVELS, case_sensitive=False), +) +@click.version_option(version=__version__) +def example( + dividend: int, + divisor: int, + log_level: str = "info", +) -> None: + """Set up logging and call the division function. + + DIVIDEND is the integer dividend. + DIVISOR is the nonzero integer divisor. + """ # Set up logging logging.basicConfig( - format="%(asctime)-15s %(levelname)s %(message)s", level=log_level.upper() + format="%(asctime)-15s %(levelname)s %(message)s", + level=log_level.upper(), + handlers=[RichHandler(rich_tracebacks=True)], ) logging.info("%d / %d == %f", dividend, divisor, example_div(dividend, divisor)) @@ -102,3 +94,8 @@ def main() -> None: # Stop logging and clean up logging.shutdown() + + +def main() -> None: + """Run the CLI.""" + example() From ac89f10d3815f7c307ca0ec67845b44bc787fdcd Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Wed, 7 Jan 2026 12:40:27 -0500 Subject: [PATCH 02/17] Update return values in tests to match what click uses Also update the version tests to deal with the version output that click generates by default. --- tests/test_example.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_example.py b/tests/test_example.py index 96f4560..ffa931b 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -38,8 +38,8 @@ def test_stdout_version(capsys): with patch.object(sys, "argv", ["bogus", "--version"]): example.example.main() captured = capsys.readouterr() - assert ( - captured.out == f"{PROJECT_VERSION}\n" + assert captured.out.endswith( + f"{PROJECT_VERSION}\n" ), "standard output by '--version' should agree with module.__version__" @@ -55,8 +55,8 @@ def test_running_as_module(capsys): # cisagov Libraries import example.__main__ # noqa: F401 captured = capsys.readouterr() - assert ( - captured.out == f"{PROJECT_VERSION}\n" + assert captured.out.endswith( + f"{PROJECT_VERSION}\n" ), "standard output by '--version' should agree with module.__version__" @@ -83,14 +83,14 @@ def test_log_levels(level): example.example.main() except SystemExit as sys_exit: return_code = sys_exit.code - assert return_code is None, "main() should return success" + assert return_code == 0, "main() should return success" assert ( logging.root.hasHandlers() is True ), "root logger should now have a handler" assert ( logging.getLevelName(logging.root.getEffectiveLevel()) == level.upper() ), f"root logger level should be set to {level.upper()}" - assert return_code is None, "main() should return success" + assert return_code == 0, "main() should return success" def test_bad_log_level(): @@ -101,7 +101,7 @@ def test_bad_log_level(): example.example.main() except SystemExit as sys_exit: return_code = sys_exit.code - assert return_code == 1, "main() should exit with error" + assert return_code == 2, "main() should exit with error" @pytest.mark.parametrize("dividend, divisor, quotient", div_params) @@ -140,4 +140,4 @@ def test_zero_divisor_argument(): example.example.main() except SystemExit as sys_exit: return_code = sys_exit.code - assert return_code == 1, "main() should exit with error" + assert return_code == 2, "main() should exit with error" From a609f6a419420008610be2dc9daf1995096451bc Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Wed, 7 Jan 2026 13:02:00 -0500 Subject: [PATCH 03/17] Rework test code to use click.testing.CliRunner This is a little cleaner and results in clightly more readable code than patching argv. --- tests/test_example.py | 63 ++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/tests/test_example.py b/tests/test_example.py index ffa931b..d0405c2 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -7,6 +7,7 @@ from unittest.mock import patch # Third-Party Libraries +from click.testing import CliRunner import pytest # cisagov Libraries @@ -34,11 +35,10 @@ def test_stdout_version(capsys): """Verify that version string sent to stdout agrees with the module version.""" - with pytest.raises(SystemExit): - with patch.object(sys, "argv", ["bogus", "--version"]): - example.example.main() - captured = capsys.readouterr() - assert captured.out.endswith( + runner = CliRunner() + result = runner.invoke(example.example.example, ["--version"]) + assert result.exit_code == 0, "should exit cleanly" + assert result.output.endswith( f"{PROJECT_VERSION}\n" ), "standard output by '--version' should agree with module.__version__" @@ -73,35 +73,28 @@ def test_release_version(): @pytest.mark.parametrize("level", log_levels) def test_log_levels(level): """Validate commandline log-level arguments.""" - with patch.object(sys, "argv", ["bogus", f"--log-level={level}", "1", "1"]): - with patch.object(logging.root, "handlers", []): - assert ( - logging.root.hasHandlers() is False - ), "root logger should not have handlers yet" - return_code = None - try: - example.example.main() - except SystemExit as sys_exit: - return_code = sys_exit.code - assert return_code == 0, "main() should return success" - assert ( - logging.root.hasHandlers() is True - ), "root logger should now have a handler" - assert ( - logging.getLevelName(logging.root.getEffectiveLevel()) == level.upper() - ), f"root logger level should be set to {level.upper()}" - assert return_code == 0, "main() should return success" + runner = CliRunner() + with patch.object(logging.root, "handlers", []): + assert ( + logging.root.hasHandlers() is False + ), "root logger should not have handlers yet" + result = runner.invoke( + example.example.example, [f"--log-level={level}", "1", "1"] + ) + assert result.exit_code == 0, "should exit cleanly" + assert ( + logging.root.hasHandlers() is True + ), "root logger should now have a handler" + assert ( + logging.getLevelName(logging.root.getEffectiveLevel()) == level.upper() + ), f"root logger level should be set to {level.upper()}" def test_bad_log_level(): """Validate bad log-level argument returns error.""" - with patch.object(sys, "argv", ["bogus", "--log-level=emergency", "1", "1"]): - return_code = None - try: - example.example.main() - except SystemExit as sys_exit: - return_code = sys_exit.code - assert return_code == 2, "main() should exit with error" + runner = CliRunner() + result = runner.invoke(example.example.example, ["--log-level=emergency", "1", "1"]) + assert result.exit_code == 2, "should exit with return code 2" @pytest.mark.parametrize("dividend, divisor, quotient", div_params) @@ -134,10 +127,6 @@ def test_zero_division(): def test_zero_divisor_argument(): """Verify that a divisor of zero is handled as expected.""" - with patch.object(sys, "argv", ["bogus", "1", "0"]): - return_code = None - try: - example.example.main() - except SystemExit as sys_exit: - return_code = sys_exit.code - assert return_code == 2, "main() should exit with error" + runner = CliRunner() + result = runner.invoke(example.example.example, ["1", "0"]) + assert result.exit_code == 2, "should exit with return code 2" From bebcdea5da8dfac34be2bf6d13cccbdb8b6ec0cb Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Wed, 7 Jan 2026 13:20:18 -0500 Subject: [PATCH 04/17] Add a type hint for CONTEXT_SETTINGS --- src/example/example.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/example/example.py b/src/example/example.py index c602612..8aef0be 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -10,10 +10,12 @@ """ # Standard Python Libraries +from collections.abc import MutableMapping from importlib.resources import files import logging import os import sys +from typing import Any # Third-Party Libraries import click @@ -31,7 +33,8 @@ for x in range(0, 101) if not logging.getLevelName(x).startswith("Level") ] -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) +# Context settings for click +CONTEXT_SETTINGS: MutableMapping[str, Any] = dict(help_option_names=["-h", "--help"]) def example_div(dividend: int, divisor: int) -> float: From a25f21364e7d1c5233fe818d7a9f96709a06ef85 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Wed, 7 Jan 2026 13:39:42 -0500 Subject: [PATCH 05/17] Rename functions to increase clarity Also go ahead and export the setup_logging_and_divide function as part of the module. --- pyproject.toml | 2 +- src/example/__init__.py | 4 ++-- src/example/__main__.py | 4 ++-- src/example/example.py | 8 ++++---- tests/test_example.py | 16 +++++++++------- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 650844a..7de996a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ test = [ ] [project.scripts] -example = "example.example:main" +example = "example.example:setup_logging_and_divide" [project.urls] homepage = "https://github.com/cisagov/skeleton-python-library" diff --git a/src/example/__init__.py b/src/example/__init__.py index 556a7d2..571e1d8 100644 --- a/src/example/__init__.py +++ b/src/example/__init__.py @@ -5,6 +5,6 @@ # package_name.__version__, which is used to get version information about this # Python package. from ._version import __version__ # noqa: F401 -from .example import example_div +from .example import divide, setup_logging_and_divide -__all__ = ["example_div"] +__all__ = ["divide", "setup_logging_and_divide"] diff --git a/src/example/__main__.py b/src/example/__main__.py index 11a3238..c7c3338 100644 --- a/src/example/__main__.py +++ b/src/example/__main__.py @@ -1,5 +1,5 @@ """Code to run if this package is used as a Python module.""" -from .example import main +from .example import setup_logging_and_divide -main() +setup_logging_and_divide() diff --git a/src/example/example.py b/src/example/example.py index 8aef0be..75202ec 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -37,7 +37,7 @@ CONTEXT_SETTINGS: MutableMapping[str, Any] = dict(help_option_names=["-h", "--help"]) -def example_div(dividend: int, divisor: int) -> float: +def divide(dividend: int, divisor: int) -> float: """Print some logging messages.""" logging.debug("This is a debug message") logging.info("This is an info message") @@ -66,7 +66,7 @@ def divisor_callback(ctx: click.Context, param: click.Parameter, value: int): type=click.Choice(LOG_LEVELS, case_sensitive=False), ) @click.version_option(version=__version__) -def example( +def setup_logging_and_divide( dividend: int, divisor: int, log_level: str = "info", @@ -83,7 +83,7 @@ def example( handlers=[RichHandler(rich_tracebacks=True)], ) - logging.info("%d / %d == %f", dividend, divisor, example_div(dividend, divisor)) + logging.info("%d / %d == %f", dividend, divisor, divide(dividend, divisor)) # Access some data from an environment variable message: str = os.getenv("ECHO_MESSAGE", DEFAULT_ECHO_MESSAGE) @@ -101,4 +101,4 @@ def example( def main() -> None: """Run the CLI.""" - example() + setup_logging_and_divide() diff --git a/tests/test_example.py b/tests/test_example.py index d0405c2..0a5dc90 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -36,7 +36,7 @@ def test_stdout_version(capsys): """Verify that version string sent to stdout agrees with the module version.""" runner = CliRunner() - result = runner.invoke(example.example.example, ["--version"]) + result = runner.invoke(example.setup_logging_and_divide, ["--version"]) assert result.exit_code == 0, "should exit cleanly" assert result.output.endswith( f"{PROJECT_VERSION}\n" @@ -79,7 +79,7 @@ def test_log_levels(level): logging.root.hasHandlers() is False ), "root logger should not have handlers yet" result = runner.invoke( - example.example.example, [f"--log-level={level}", "1", "1"] + example.setup_logging_and_divide, [f"--log-level={level}", "1", "1"] ) assert result.exit_code == 0, "should exit cleanly" assert ( @@ -93,14 +93,16 @@ def test_log_levels(level): def test_bad_log_level(): """Validate bad log-level argument returns error.""" runner = CliRunner() - result = runner.invoke(example.example.example, ["--log-level=emergency", "1", "1"]) + result = runner.invoke( + example.setup_logging_and_divide, ["--log-level=emergency", "1", "1"] + ) assert result.exit_code == 2, "should exit with return code 2" @pytest.mark.parametrize("dividend, divisor, quotient", div_params) def test_division(dividend, divisor, quotient): """Verify division results.""" - result = example.example_div(dividend, divisor) + result = example.divide(dividend, divisor) assert result == quotient, "result should equal quotient" @@ -114,7 +116,7 @@ def test_slow_division(): # Standard Python Libraries import time - result = example.example_div(256, 16) + result = example.divide(256, 16) time.sleep(4) assert result == 16, "result should equal be 16" @@ -122,11 +124,11 @@ def test_slow_division(): def test_zero_division(): """Verify that division by zero throws the correct exception.""" with pytest.raises(ZeroDivisionError): - example.example_div(1, 0) + example.divide(1, 0) def test_zero_divisor_argument(): """Verify that a divisor of zero is handled as expected.""" runner = CliRunner() - result = runner.invoke(example.example.example, ["1", "0"]) + result = runner.invoke(example.setup_logging_and_divide, ["1", "0"]) assert result.exit_code == 2, "should exit with return code 2" From c4507f5d0611d3ee030dd25ec813bfdbba7151a9 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Wed, 7 Jan 2026 13:43:11 -0500 Subject: [PATCH 06/17] Explain why log levels must be extracted differently on Python 10 Co-authored-by: dav3r --- src/example/example.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/example/example.py b/src/example/example.py index 75202ec..93458fc 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -28,6 +28,8 @@ if sys.version_info.minor > 10: LOG_LEVELS = [*logging.getLevelNamesMapping()] else: + # The logging.getLevelNamesMapping method was only introduced in + # Python 3.11. LOG_LEVELS = [ logging.getLevelName(x) for x in range(0, 101) From f00f06960a559058986be736816da6509042684b Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Wed, 7 Jan 2026 14:16:15 -0500 Subject: [PATCH 07/17] Bump version from 1.0.0 to 1.1.0-rc.1 --- src/example/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/example/_version.py b/src/example/_version.py index f8d70f6..92bd2ad 100644 --- a/src/example/_version.py +++ b/src/example/_version.py @@ -1,3 +1,3 @@ """This file defines the version of this module.""" -__version__ = "1.0.0" +__version__ = "1.1.0-rc.1" From 48639fadd8df2dcaec1a81178ad9a0918be5769c Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Mon, 2 Mar 2026 14:12:38 -0500 Subject: [PATCH 08/17] Use version tuple vice only comparing the minor version This will allow the feature gate to remain valid across major versions. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/example/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/example/example.py b/src/example/example.py index 93458fc..dc939ed 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -25,7 +25,7 @@ DEFAULT_ECHO_MESSAGE: str = "Hello World from the example default!" LOG_LEVELS: list[str] = list() -if sys.version_info.minor > 10: +if sys.version_info >= (3, 11): LOG_LEVELS = [*logging.getLevelNamesMapping()] else: # The logging.getLevelNamesMapping method was only introduced in From 9abc187a870017d5a016223767583ca1f2f15e65 Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Mon, 2 Mar 2026 14:13:27 -0500 Subject: [PATCH 09/17] Improve docstring The new docstring describes more fully what the function does. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/example/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/example/example.py b/src/example/example.py index dc939ed..c8166b6 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -40,7 +40,7 @@ def divide(dividend: int, divisor: int) -> float: - """Print some logging messages.""" + """Divide dividend by divisor, log messages at various levels, and return the quotient.""" logging.debug("This is a debug message") logging.info("This is an info message") logging.warning("This is a warning message") From 3e61721ffddda07ef1878a138db5ec649c5b753d Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Mon, 2 Mar 2026 14:15:36 -0500 Subject: [PATCH 10/17] Ensure that only the version is printed with the version option This is what docopt used to do, so to avoid an unexpected change we should do the same. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/example/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/example/example.py b/src/example/example.py index c8166b6..244b2d8 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -67,7 +67,7 @@ def divisor_callback(ctx: click.Context, param: click.Parameter, value: int): help="The logging level.", type=click.Choice(LOG_LEVELS, case_sensitive=False), ) -@click.version_option(version=__version__) +@click.version_option(version=__version__, message="%(version)s") def setup_logging_and_divide( dividend: int, divisor: int, From bdb3193dc7f70c9b5511fba9e7afff78fb30654b Mon Sep 17 00:00:00 2001 From: Shane Frasier Date: Mon, 2 Mar 2026 14:18:09 -0500 Subject: [PATCH 11/17] Remove unused capsys fixture capsys is no longer used in test_stdout_version after switching to CliRunner. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_example.py b/tests/test_example.py index 0a5dc90..72df47f 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -33,7 +33,7 @@ PROJECT_VERSION = example.__version__ -def test_stdout_version(capsys): +def test_stdout_version(): """Verify that version string sent to stdout agrees with the module version.""" runner = CliRunner() result = runner.invoke(example.setup_logging_and_divide, ["--version"]) From d614cffe4573a70ce06d6c518d46119eac241cc6 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Tue, 10 Mar 2026 15:22:56 -0400 Subject: [PATCH 12/17] Add return type annotation --- src/example/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/example/example.py b/src/example/example.py index 244b2d8..651cffe 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -49,7 +49,7 @@ def divide(dividend: int, divisor: int) -> float: return dividend / divisor -def divisor_callback(ctx: click.Context, param: click.Parameter, value: int): +def divisor_callback(ctx: click.Context, param: click.Parameter, value: int) -> int: """Verify that the value is nonzero.""" if value == 0: raise click.BadParameter("divisor must be nonzero") From df49fb985d7890ac43eed71ae48f29f7f33603c8 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Tue, 10 Mar 2026 15:26:26 -0400 Subject: [PATCH 13/17] Check string equality vs using endswith() This is possible now that `--version` returns only the version string. --- tests/test_example.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_example.py b/tests/test_example.py index 72df47f..c017916 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -38,8 +38,8 @@ def test_stdout_version(): runner = CliRunner() result = runner.invoke(example.setup_logging_and_divide, ["--version"]) assert result.exit_code == 0, "should exit cleanly" - assert result.output.endswith( - f"{PROJECT_VERSION}\n" + assert ( + result.output == f"{PROJECT_VERSION}\n" ), "standard output by '--version' should agree with module.__version__" @@ -55,8 +55,8 @@ def test_running_as_module(capsys): # cisagov Libraries import example.__main__ # noqa: F401 captured = capsys.readouterr() - assert captured.out.endswith( - f"{PROJECT_VERSION}\n" + assert ( + captured.out == f"{PROJECT_VERSION}\n" ), "standard output by '--version' should agree with module.__version__" From f0ffcc3f7f97a337a712aa62d1954c063456e47e Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Mon, 13 Apr 2026 14:17:28 -0400 Subject: [PATCH 14/17] Remove an unnecessary list() call This gets rid of the following warning from our flake8 pre-commit hook: C408 Unnecessary list call - rewrite as a literal --- src/example/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/example/example.py b/src/example/example.py index 651cffe..0791d9b 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -24,7 +24,7 @@ from ._version import __version__ DEFAULT_ECHO_MESSAGE: str = "Hello World from the example default!" -LOG_LEVELS: list[str] = list() +LOG_LEVELS: list[str] = [] if sys.version_info >= (3, 11): LOG_LEVELS = [*logging.getLevelNamesMapping()] else: From 6361f92cf34940418339bf418a10275a99126c81 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Mon, 13 Apr 2026 14:21:31 -0400 Subject: [PATCH 15/17] Remove an unnecessary dict() call This gets rid of the following warning from our flake8 pre-commit hook: C408 Unnecessary dict call - rewrite as a literal. --- src/example/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/example/example.py b/src/example/example.py index 0791d9b..e7d6a49 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -36,7 +36,7 @@ if not logging.getLevelName(x).startswith("Level") ] # Context settings for click -CONTEXT_SETTINGS: MutableMapping[str, Any] = dict(help_option_names=["-h", "--help"]) +CONTEXT_SETTINGS: MutableMapping[str, Any] = {"help_option_names": ["-h", "--help"]} def divide(dividend: int, divisor: int) -> float: From bf8d49ddc44bed02c2288f62557be7f20d8ce9e0 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Mon, 13 Apr 2026 14:22:43 -0400 Subject: [PATCH 16/17] Shorten an overly long comment line This gets rid of the following error from our flake8 pre-commit hook: B950 line too long (94 > 80 characters) --- src/example/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/example/example.py b/src/example/example.py index e7d6a49..7e473ad 100644 --- a/src/example/example.py +++ b/src/example/example.py @@ -40,7 +40,7 @@ def divide(dividend: int, divisor: int) -> float: - """Divide dividend by divisor, log messages at various levels, and return the quotient.""" + """Perform division, log messages at various levels, and return quotient.""" logging.debug("This is a debug message") logging.info("This is an info message") logging.warning("This is a warning message") From 04dc2108b4c63df7ec9ea98f7951c45d27d26e02 Mon Sep 17 00:00:00 2001 From: Jeremy Frasier Date: Mon, 13 Apr 2026 15:17:19 -0400 Subject: [PATCH 17/17] Add click and rich to list of Python Dependabot ignore directives These dependencies will be commonly used with this type of repo, so we may as well handle them at the skeleton level. --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0bedb40..af15e6f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,11 +44,13 @@ updates: # ignore: # # Managed by cisagov/skeleton-python-library # - dependency-name: build + # - dependency-name: click # - dependency-name: coverage # - dependency-name: coveralls # - dependency-name: pre-commit # - dependency-name: pytest-cov # - dependency-name: pytest + # - dependency-name: rich # - dependency-name: setuptools # - dependency-name: twine package-ecosystem: pip