diff --git a/pydocstringformatter/formatting/__init__.py b/pydocstringformatter/formatting/__init__.py index a95e4a03..ad72ed13 100644 --- a/pydocstringformatter/formatting/__init__.py +++ b/pydocstringformatter/formatting/__init__.py @@ -1,4 +1,4 @@ -__all__ = ["FORMATTERS"] +__all__ = ["FORMATTERS", "Formatter"] from typing import List diff --git a/pydocstringformatter/formatting/base.py b/pydocstringformatter/formatting/base.py index 9edcbe73..d1a6a246 100644 --- a/pydocstringformatter/formatting/base.py +++ b/pydocstringformatter/formatting/base.py @@ -17,6 +17,16 @@ def name(self) -> str: user-facing and should be chosen carefully. """ + @property + def activate_option(self) -> str: + """The argparse option to activate this formatter.""" + return f"--{self.name}" + + @property + def deactivate_option(self) -> str: + """The argparse option to deactivate this formatter.""" + return f"--no-{self.name}" + @abc.abstractmethod def treat_token(self, tokeninfo: tokenize.TokenInfo) -> tokenize.TokenInfo: """Return a modified token.""" diff --git a/pydocstringformatter/testutils/__init__.py b/pydocstringformatter/testutils/__init__.py new file mode 100644 index 00000000..9a426ce4 --- /dev/null +++ b/pydocstringformatter/testutils/__init__.py @@ -0,0 +1,78 @@ +import contextlib +import logging +from pathlib import Path +from types import TracebackType +from typing import List, Optional, Type + +import pytest + +from pydocstringformatter import run_docstring_formatter +from pydocstringformatter.formatting import Formatter + +LOGGER = logging.getLogger(__name__) + + +class FormatterAsserter(contextlib.AbstractContextManager): # type: ignore[type-arg] + """ContextManager used to assert that a Formatter does something on a docstring. + + Also permit to check that nothing happens if it's deactivated. + """ + + def __init__( + self, + docstring: str, + formatters: List[Formatter], + capsys: pytest.CaptureFixture[str], + tmp_path: Path, + ) -> None: + self.formatters = formatters + file_name = "_".join([f.name for f in self.formatters]) + self.file_to_format = tmp_path / f"test_{file_name}.py" + self.file_to_format.write_text(docstring) + self.capsys = capsys + names = [f"'{f.name}'" for f in formatters] + verb = "is" if len(names) == 1 else "are" + self.assert_msg = f""" +{{}} was modified but {', '.join(names)} {verb} {{}}. +Temp file is '{self.file_to_format}' +""" + + def __enter__(self) -> "FormatterAsserter": + return self + + @staticmethod + def __launch(commands: List[str]) -> None: + """Launch pydocstringformatter while logging for easier debugging.""" + run_docstring_formatter(commands) + LOGGER.info("Launching 'pydocstringformatter' with: %s", commands) + + def assert_format_when_activated(self) -> None: + """Assert that the formatter does something when activated.""" + msg = self.assert_msg.format("Nothing", "activated") + self.__launch( + [str(self.file_to_format)] + [f.activate_option for f in self.formatters] + ) + out, err = self.capsys.readouterr() + assert not err + assert "Nothing to do!" not in out, msg + expected = ["---", "@@", "+++"] + assert all(e in out for e in expected), msg + + def assert_no_change_when_deactivated(self) -> None: + """Assert that the formatter does nothing when deactivated.""" + self.__launch( + [str(self.file_to_format)] + [f.deactivate_option for f in self.formatters] + ) + out, err = self.capsys.readouterr() + assert not err + assert "Nothing to do!" in out, self.assert_msg.format( + "Something", "deactivated" + ) + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + return None diff --git a/tests/test_run.py b/tests/test_run.py index 02d76c2e..5b4f9aad 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -2,12 +2,12 @@ import os import sys from pathlib import Path -from typing import List import pytest import pydocstringformatter from pydocstringformatter.formatting import FORMATTERS +from pydocstringformatter.testutils import FormatterAsserter def test_no_arguments(capsys: pytest.CaptureFixture[str]) -> None: @@ -116,30 +116,12 @@ def test_output_message_two_files( assert not output.err -@pytest.mark.parametrize( - "args,should_format", - [ - [[f"--no-{f.name}" for f in FORMATTERS], False], - [[f"--{f.name}" for f in FORMATTERS], True], - ], -) -def test_optional_formatters( - args: List[str], - should_format: bool, - capsys: pytest.CaptureFixture[str], - tmp_path: Path, +def test_begin_quote_formatters( + capsys: pytest.CaptureFixture[str], tmp_path: Path ) -> None: """Test that (optional) formatters are activated or not depending on options.""" - bad_docstring = tmp_path / "bad_docstring.py" - bad_docstring.write_text(f'"""{"a" * 120}\n{"b" * 120}"""') - pydocstringformatter.run_docstring_formatter([str(bad_docstring)] + args) - out, err = capsys.readouterr() - assert not err - if should_format: - msg = "Nothing was modified, but all formatters are activated." - assert "Nothing to do!" not in out - expected = ["---", "@@", "+++"] - assert all(e in out for e in expected), msg - else: - msg = "Something was modified, but all formatter are deactivated." - assert "Nothing to do!" in out, msg + with FormatterAsserter( + f'"""{"a" * 120}\n{"b" * 120}"""', FORMATTERS, capsys, tmp_path + ) as asserter: + asserter.assert_format_when_activated() + asserter.assert_no_change_when_deactivated()