diff --git a/docs/usage.rst b/docs/usage.rst index a143f9b0..64b252eb 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -6,6 +6,7 @@ Current usage of ``pydocstringformatter``: .. code-block:: shell usage: pydocstringformatter [-h] [-w] [--quiet] [-v] [--exclude EXCLUDE] + [--exit-code] [--max-summary-lines MAX_SUMMARY_LINES] [--summary-quotes-same-line] [--split-summary-body --no-split-summary-body] @@ -30,6 +31,9 @@ Current usage of ``pydocstringformatter``: configuration: --exclude EXCLUDE A comma separated list of glob patterns of file path names not to be formatted. + --exit-code Turn on if the program should exit with bitwise exit + codes. 0 = No changes, 32 = Changed files or printed + diff. --max-summary-lines MAX_SUMMARY_LINES The maximum numbers of lines a summary can span. The default value is 1. diff --git a/pydocstringformatter/configuration/arguments_manager.py b/pydocstringformatter/configuration/arguments_manager.py index f81b22fc..12ae1e97 100644 --- a/pydocstringformatter/configuration/arguments_manager.py +++ b/pydocstringformatter/configuration/arguments_manager.py @@ -76,6 +76,16 @@ def _register_arguments(self, version: str) -> None: ), ) + self.configuration_group.add_argument( + "--exit-code", + action="store_true", + default=False, + help=( + "Turn on if the program should exit with bitwise exit codes. " + "0 = No changes, 32 = Changed files or printed diff." + ), + ) + self.configuration_group.add_argument( "--max-summary-lines", action="store", diff --git a/pydocstringformatter/run.py b/pydocstringformatter/run.py index 20c9a55a..d48537d4 100644 --- a/pydocstringformatter/run.py +++ b/pydocstringformatter/run.py @@ -21,18 +21,36 @@ def __init__(self, argv: Union[List[str], None]) -> None: ) self.config = self._arguments_manager.namespace - if argv := argv or sys.argv[1:]: - self._arguments_manager.parse_options(argv) - for formatter in formatting.FORMATTERS: - formatter.set_config_namespace(self.config) - self._check_files(self.config.files) - else: + # Display help message if nothing is passed + if not (argv := argv or sys.argv[1:]): self._arguments_manager.print_help() + return - def _check_files(self, arguments: List[str]) -> None: + # Parse options and register on formatters + self._arguments_manager.parse_options(argv) + for formatter in formatting.FORMATTERS: + formatter.set_config_namespace(self.config) + + self._check_files(self.config.files) + + # pylint: disable-next=inconsistent-return-statements + def _check_files(self, files: List[str]) -> None: """Find all files and perform the formatting.""" - filepaths = utils._find_python_files(arguments, self.config.exclude) - self._format_files(filepaths) + filepaths = utils._find_python_files(files, self.config.exclude) + + is_changed = self._format_files(filepaths) + + if is_changed: # pylint: disable=consider-using-assignment-expr + return utils._sys_exit(32, self.config.exit_code) + + files_string = f"{len(filepaths)} " + files_string += "files" if len(filepaths) != 1 else "file" + utils._print_to_console( + f"Nothing to do! All docstrings in {files_string} are correct 🎉\n", + self.config.quiet, + ) + + utils._sys_exit(0, self.config.exit_code) def _format_file(self, filename: Path) -> bool: """Format a file.""" @@ -83,14 +101,11 @@ def _format_file(self, filename: Path) -> bool: return is_changed - def _format_files(self, filepaths: List[Path]) -> None: + def _format_files(self, filepaths: List[Path]) -> bool: """Format a list of files.""" is_changed = False for file in filepaths: is_changed = self._format_file(file) or is_changed - if not is_changed: - utils._print_to_console( - "Nothing to do! All docstrings are correct 🎉\n", self.config.quiet - ) + return is_changed diff --git a/pydocstringformatter/utils/__init__.py b/pydocstringformatter/utils/__init__.py index 443b9c57..80b50eff 100644 --- a/pydocstringformatter/utils/__init__.py +++ b/pydocstringformatter/utils/__init__.py @@ -6,7 +6,7 @@ from pydocstringformatter.utils.file_diference import _generate_diff from pydocstringformatter.utils.find_docstrings import _is_docstring from pydocstringformatter.utils.find_python_file import _find_python_files -from pydocstringformatter.utils.output import _print_to_console +from pydocstringformatter.utils.output import _print_to_console, _sys_exit __all__ = [ "_find_python_files", @@ -16,4 +16,5 @@ "PydocstringFormatterError", "TomlParsingError", "_print_to_console", + "_sys_exit", ] diff --git a/pydocstringformatter/utils/output.py b/pydocstringformatter/utils/output.py index 59473b14..8b9b0981 100644 --- a/pydocstringformatter/utils/output.py +++ b/pydocstringformatter/utils/output.py @@ -20,3 +20,9 @@ def _print_to_console(string: str, quiet: bool) -> None: """ if not quiet: sys.stdout.buffer.write(_encode_string(string)) + + +def _sys_exit(value: int, option: bool) -> None: + """Sys.exit if the boolean passed says to do so.""" + if option: + sys.exit(value) diff --git a/tests/test_config.py b/tests/test_config.py index 10af2e50..f1faaeac 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -38,7 +38,7 @@ def test_valid_toml( monkeypatch.chdir(CONFIG_DATA / "valid_toml") pydocstringformatter.run_docstring_formatter(["test_package"]) output = capsys.readouterr() - assert output.out == "Nothing to do! All docstrings are correct 🎉\n" + assert output.out == "Nothing to do! All docstrings in 1 file are correct 🎉\n" assert not output.err @@ -175,7 +175,7 @@ def test_exclude_match( monkeypatch.chdir(CONFIG_DATA / "exclude_match") pydocstringformatter.run_docstring_formatter(["test_package"]) output = capsys.readouterr() - assert output.out == "Nothing to do! All docstrings are correct 🎉\n" + assert output.out == "Nothing to do! All docstrings in 0 files are correct 🎉\n" assert not output.err @staticmethod @@ -186,7 +186,7 @@ def test_exclude_match_inner_directory( monkeypatch.chdir(CONFIG_DATA / "exclude_match_inner") pydocstringformatter.run_docstring_formatter(["test_package"]) output = capsys.readouterr() - assert output.out == "Nothing to do! All docstrings are correct 🎉\n" + assert output.out == "Nothing to do! All docstrings in 0 files are correct 🎉\n" assert not output.err @staticmethod @@ -197,7 +197,7 @@ def test_exclude_csv_string( monkeypatch.chdir(CONFIG_DATA / "exclude_match_csv") pydocstringformatter.run_docstring_formatter(["test_package"]) output = capsys.readouterr() - assert output.out == "Nothing to do! All docstrings are correct 🎉\n" + assert output.out == "Nothing to do! All docstrings in 0 files are correct 🎉\n" assert not output.err @staticmethod @@ -208,7 +208,7 @@ def test_exclude_csv_list( monkeypatch.chdir(CONFIG_DATA / "exclude_match_csv_list") pydocstringformatter.run_docstring_formatter(["test_package"]) output = capsys.readouterr() - assert output.out == "Nothing to do! All docstrings are correct 🎉\n" + assert output.out == "Nothing to do! All docstrings in 0 files are correct 🎉\n" assert not output.err @staticmethod diff --git a/tests/test_run.py b/tests/test_run.py index f5e59955..c6443159 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -39,7 +39,9 @@ class OptionalFormatter(StringFormatter): name = "optional-formatter" optional = True - def _treat_string(self, tokeninfo: tokenize.TokenInfo, _: int) -> str: + @staticmethod + def _treat_string(tokeninfo: tokenize.TokenInfo, _: int) -> str: + """Treat a string.""" return tokeninfo.string class NonOptionalFormatter(StringFormatter): @@ -47,7 +49,9 @@ class NonOptionalFormatter(StringFormatter): name = "non-optional-formatter" - def _treat_string(self, tokeninfo: tokenize.TokenInfo, _: int) -> str: + @staticmethod + def _treat_string(tokeninfo: tokenize.TokenInfo, _: int) -> str: + """Treat a string.""" return tokeninfo.string FORMATTERS.append(OptionalFormatter()) @@ -96,7 +100,7 @@ def test_output_message_nothing_done( """Test that we emit the correct message when nothing was done.""" with open(test_file, "w", encoding="utf-8") as file: file.write('"""A multi-line\ndocstring.\n"""') - with open(test_file + "2", "w", encoding="utf-8") as file: + with open(test_file.replace(".py", "2.py"), "w", encoding="utf-8") as file: file.write('"""A multi-line\ndocstring.\n"""') pydocstringformatter.run_docstring_formatter( @@ -104,7 +108,7 @@ def test_output_message_nothing_done( ) output = capsys.readouterr() - assert output.out == "Nothing to do! All docstrings are correct 🎉\n" + assert output.out == "Nothing to do! All docstrings in 2 files are correct 🎉\n" assert not output.err @@ -117,8 +121,8 @@ def test_output_message_one_file( except ValueError: expected_path = test_file - with open(test_file + "2", "w", encoding="utf-8") as file: - file.write('"""A multi-line\ndocstring\n"""') + with open(test_file.replace(".py", "2.py"), "w", encoding="utf-8") as file: + file.write('"""A multi-line\ndocstring.\n"""') pydocstringformatter.run_docstring_formatter( [str(Path(test_file).parent), "--write"] @@ -179,3 +183,43 @@ def test_optional_formatters_argument( ) as asserter: asserter.assert_format_when_activated() asserter.assert_no_change_when_deactivated() + + +class TestExitCodes: + """Tests for the --exit-code option.""" + + @staticmethod + def test_exit_code_with_write(test_file: str) -> None: + """Test that we emit the correct exit code in write mode.""" + with pytest.raises(SystemExit) as exit_exec: + pydocstringformatter.run_docstring_formatter( + [str(Path(test_file)), "--write", "--exit-code"] + ) + + assert exit_exec.value.code == 32 + + # After first writing changes, now we expect no changes + with pytest.raises(SystemExit) as exit_exec: + pydocstringformatter.run_docstring_formatter( + [str(Path(test_file)), "--write", "--exit-code"] + ) + + assert not exit_exec.value.code + + @staticmethod + def test_exit_code_without_write(test_file: str) -> None: + """Test that we emit the correct exit code in write mode.""" + with pytest.raises(SystemExit) as exit_exec: + pydocstringformatter.run_docstring_formatter( + [str(Path(test_file)), "--exit-code"] + ) + + assert exit_exec.value.code == 32 + + # We expect an exit code on both occassions + with pytest.raises(SystemExit) as exit_exec: + pydocstringformatter.run_docstring_formatter( + [str(Path(test_file)), "--exit-code"] + ) + + assert exit_exec.value.code == 32 diff --git a/tests/test_utils.py b/tests/test_utils.py index 7490ffeb..8096f7d5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -154,5 +154,5 @@ def test_encoding_of_console_messages( pydocstringformatter.run_docstring_formatter([test_file, "--write"]) output = capsys.readouterr() - assert output.out == "Nothing to do! All docstrings are correct 🎉\n" + assert output.out == "Nothing to do! All docstrings in 1 file are correct 🎉\n" assert not output.err