Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(linters): Introduce ruff and fix issues #831

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ repos:
- id: shellcheck

# Python
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
hooks:
- id: ruff
args:
- --fix
- id: ruff-format
exclude: /terraform_docs_replace(_test)?\.py$

- repo: https://github.com/pre-commit/mirrors-mypy.git
rev: v1.15.0
hooks:
Expand Down
39 changes: 39 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Assume Python 3.9
target-version = "py39"

line-length = 79 # To decrease PR diff size

namespace-packages = ["src/pre_commit_terraform/", "tests/pytest/"]

[format]
quote-style = "single"

[lint.flake8-quotes]
inline-quotes = "single"

[lint.pydocstyle]
convention = "pep257"

[lint]
select = ["ALL"]
preview = true
ignore = [
"CPY001", # Missing copyright notice at top of file
]

[lint.isort]
# force-single-line = true # To decrease PR diff size
lines-after-imports = 2

[lint.flake8-pytest-style]
parametrize-values-type = "tuple"

[lint.per-file-ignores]
# Ignore in the `tests/` directory.
"tests/**.py" = [
"S101", # Use of `assert` detected
"PLC2701", # We need import marked as internal files for testing
]

"src/pre_commit_terraform/terraform_docs_replace.py" = ["ALL"] # Deprecated hook
"tests/pytest/terraform_docs_replace_test.py" = ["ALL"] # Tests for deprecated hook
3 changes: 2 additions & 1 deletion src/pre_commit_terraform/__main__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""A runpy-style CLI entry-point module."""

from sys import argv, exit as exit_with_return_code
from sys import argv
from sys import exit as exit_with_return_code

from ._cli import invoke_cli_app

Expand Down
18 changes: 12 additions & 6 deletions src/pre_commit_terraform/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,41 @@ def invoke_cli_app(cli_args: list[str]) -> ReturnCodeType:

Includes initializing parsers of all the sub-apps and
choosing what to execute.

Returns:
ReturnCodeType: The return code of the app.
Comment on lines +22 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the built-in Sphinx API doc style. Use :returns: instead. And :rtype: is probably unnecessary here as the annotation exists.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.astral.sh/ruff/rules/docstring-missing-returns/

Details
ruff.....................................................................Failed
- hook id: ruff
- exit code: 1

src/pre_commit_terraform/_cli.py:16:5: DOC201 `return` is not documented in docstring
   |
15 |   def invoke_cli_app(cli_args: list[str]) -> ReturnCodeType:
16 |       """Run the entry-point of the CLI app.
   |  _____^
17 | | 
18 | |     Includes initializing parsers of all the sub-apps and
19 | |     choosing what to execute.
20 | | 
21 | |     :returns: The return code of the app.
22 | | 
23 | |     """
   | |_______^ DOC201
24 |       root_cli_parser = initialize_argument_parser()
25 |       parsed_cli_args = root_cli_parser.parse_args(cli_args)
   |
   = help: Add a "Returns" section to the docstring

src/pre_commit_terraform/_cli_parsing.py:35:5: DOC201 `return` is not documented in docstring
   |
34 |   def initialize_argument_parser() -> ArgumentParser:
35 |       """Return the root argument parser with sub-commands.
   |  _____^
36 | | 
37 | |     :returns: The root parser with sub-commands attached.
38 | | 
39 | |     """
   | |_______^ DOC201
40 |       root_cli_parser = ArgumentParser(prog=f'python -m {__package__ !s}')
41 |       attach_subcommand_parsers_to(root_cli_parser)
   |
   = help: Add a "Returns" section to the docstring

src/pre_commit_terraform/terraform_docs_replace.py:54:5: DOC201 `return` is not documented in docstring
   |
53 |   def invoke_cli_app(parsed_cli_args: Namespace) -> ReturnCodeType:
54 |       """Run the entry-point of the CLI app.
   |  _____^
55 | | 
56 | |     :returns: The return code of the app.
57 | | 
58 | |     """
   | |_______^ DOC201
59 |       warnings.warn(  # noqa: B028. that's user warning, no need to show stacktrace etc.
60 |           '`terraform_docs_replace` hook is DEPRECATED.'
   |
   = help: Add a "Returns" section to the docstring

Found 3 errors.


Raises:
PreCommitTerraformExit: If the app is exiting with error.
"""
root_cli_parser = initialize_argument_parser()
parsed_cli_args = root_cli_parser.parse_args(cli_args)
invoke_cli_app = cast_to(
# FIXME: attempt typing per https://stackoverflow.com/a/75666611/595220
CLIAppEntryPointCallableType,
# FIXME: attempt typing per https://stackoverflow.com/a/75666611/595220 # noqa: TD001, TD002, TD003, FIX001, E501
'CLIAppEntryPointCallableType',
parsed_cli_args.invoke_cli_app,
)

try:
return invoke_cli_app(parsed_cli_args)
except PreCommitTerraformExit as exit_err:
print(f'App exiting: {exit_err !s}', file=sys.stderr)
print(f'App exiting: {exit_err !s}', file=sys.stderr) # noqa: T201 FIXME
raise
except PreCommitTerraformRuntimeError as unhandled_exc:
print(
print( # noqa: T201 FIXME
f'App execution took an unexpected turn: {unhandled_exc !s}. '
'Exiting...',
file=sys.stderr,
)
return ReturnCode.ERROR
except PreCommitTerraformBaseError as unhandled_exc:
print(
print( # noqa: T201 FIXME
f'A surprising exception happened: {unhandled_exc !s}. Exiting...',
file=sys.stderr,
)
return ReturnCode.ERROR
except KeyboardInterrupt as ctrl_c_exc:
print(
print( # noqa: T201 FIXME
f'User-initiated interrupt: {ctrl_c_exc !s}. Exiting...',
file=sys.stderr,
)
Expand Down
11 changes: 9 additions & 2 deletions src/pre_commit_terraform/_cli_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,22 @@ def attach_subcommand_parsers_to(root_cli_parser: ArgumentParser, /) -> None:
required=True,
)
for subcommand_module in SUBCOMMAND_MODULES:
subcommand_parser = subcommand_parsers.add_parser(subcommand_module.CLI_SUBCOMMAND_NAME)
subcommand_parser = subcommand_parsers.add_parser(
subcommand_module.CLI_SUBCOMMAND_NAME,
)
subcommand_parser.set_defaults(
invoke_cli_app=subcommand_module.invoke_cli_app,
)
subcommand_module.populate_argument_parser(subcommand_parser)


def initialize_argument_parser() -> ArgumentParser:
"""Return the root argument parser with sub-commands."""
"""Return the root argument parser with sub-commands.

Returns:
ArgumentParser: The root parser with sub-commands attached.

"""
root_cli_parser = ArgumentParser(prog=f'python -m {__package__ !s}')
attach_subcommand_parsers_to(root_cli_parser)
return root_cli_parser
Expand Down
6 changes: 3 additions & 3 deletions src/pre_commit_terraform/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ class PreCommitTerraformBaseError(Exception):


class PreCommitTerraformRuntimeError(
PreCommitTerraformBaseError,
RuntimeError,
PreCommitTerraformBaseError,
RuntimeError,
):
"""An exception representing a runtime error condition."""


class PreCommitTerraformExit(PreCommitTerraformBaseError, SystemExit):
class PreCommitTerraformExit(PreCommitTerraformBaseError, SystemExit): # noqa: N818 FIXME
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh?

Copy link
Collaborator Author

@MaxymVlasov MaxymVlasov Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming. It wants Error suffix in name as it based on PreCommitTerraformBaseError

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it say FIXME then? Add justification to the suppression. The name mimics the built-in SystemExit and is meant to have exactly the same semantics. For this reason, it shouldn't have Error in the name to maintain resemblance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ruff.....................................................................Failed
- hook id: ruff
- exit code: 1

src/pre_commit_terraform/_errors.py:15:7: N818 Exception name `PreCommitTerraformExit` should be named with an Error suffix
   |
15 | class PreCommitTerraformExit(PreCommitTerraformBaseError, SystemExit):
   |       ^^^^^^^^^^^^^^^^^^^^^^ N818
16 |     """An exception for terminating execution from deep app layers."""
   |

Found 1 error.

https://docs.astral.sh/ruff/rules/error-suffix-on-exception-name/

Why does it say FIXME then? Add justification to the suppression.

Because rule reference to PEP8, and it was logical to leave FIXME for some future investigation.
So you want just suppress this rule with comment
The name mimics the built-in SystemExit and is meant to have exactly the same semantics. For this reason, it shouldn't have Error in the name to maintain resemblance. ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, suppress it and leave a justification comment. It shouldn't be changed because it's an intentional special case.

"""An exception for terminating execution from deep app layers."""
3 changes: 2 additions & 1 deletion src/pre_commit_terraform/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class CLISubcommandModuleProtocol(Protocol):
"""This constant contains a CLI."""

def populate_argument_parser(
self, subcommand_parser: ArgumentParser,
self,
subcommand_parser: ArgumentParser,
) -> None:
"""Run a module hook for populating the subcommand parser."""

Expand Down
43 changes: 25 additions & 18 deletions tests/pytest/_cli_test.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Tests for the high-level CLI entry point."""

from argparse import ArgumentParser, Namespace
import pytest

import pytest
from pre_commit_terraform import _cli_parsing as _cli_parsing_mod
from pre_commit_terraform._cli import invoke_cli_app
from pre_commit_terraform._errors import (
PreCommitTerraformExit,
PreCommitTerraformBaseError,
PreCommitTerraformExit,
PreCommitTerraformRuntimeError,
)
from pre_commit_terraform._structs import ReturnCode
Expand All @@ -23,7 +23,6 @@
@pytest.mark.parametrize(
('raised_error', 'expected_stderr'),
(
# pytest.param(PreCommitTerraformExit('sentinel'), 'App exiting: sentinel', id='app-exit'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this commented? Have you tried uncommenting?

Copy link
Collaborator Author

@MaxymVlasov MaxymVlasov Mar 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea.
Will try

UPD. Mar 25th: I have broken pytest in tox locally, no idea why. Can't test now, will return to it on next iteration

Copy link
Collaborator Author

@MaxymVlasov MaxymVlasov Mar 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It breaks tests

Details
➜ tox -qq
===================================== test session starts =====================================
platform linux -- Python 3.10.12, pytest-8.3.5, pluggy-1.5.0
cachedir: .tox/py/.pytest_cache
rootdir: /home/vm/code/0-other/open-source/pre-commit-terraform
configfile: pytest.ini
testpaths: tests/pytest/
plugins: cov-6.0.0, xdist-3.6.1, mock-3.14.0
collected 12 items                                                                            

tests/pytest/_cli_test.py F....                                                         [ 41%]
tests/pytest/terraform_docs_replace_test.py .......                                     [100%]

========================================== FAILURES ===========================================
_______________________________ test_known_interrupts[app-exit] _______________________________

capsys = <_pytest.capture.CaptureFixture object at 0x7fc7fc012470>
expected_stderr = 'App exiting: sentinel'
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7fc7fc0138b0>
raised_error = PreCommitTerraformExit('sentinel')

    @pytest.mark.parametrize(
        ('raised_error', 'expected_stderr'),
        (
            pytest.param(PreCommitTerraformExit('sentinel'), 'App exiting: sentinel', id='app-exit'),
            pytest.param(
                PreCommitTerraformRuntimeError('sentinel'),
                'App execution took an unexpected turn: sentinel. Exiting...',
                id='app-runtime-exc',
            ),
            pytest.param(
                PreCommitTerraformBaseError('sentinel'),
                'A surprising exception happened: sentinel. Exiting...',
                id='app-base-exc',
            ),
            pytest.param(
                KeyboardInterrupt('sentinel'),
                'User-initiated interrupt: sentinel. Exiting...',
                id='ctrl-c',
            ),
        ),
    )
    def test_known_interrupts(
        capsys: pytest.CaptureFixture[str],
        expected_stderr: str,
        monkeypatch: pytest.MonkeyPatch,
        raised_error: BaseException,
    ) -> None:
        """Check that known interrupts are turned into return code 1."""
    
        class CustomCmdStub:
            CLI_SUBCOMMAND_NAME = 'sentinel'
    
            def populate_argument_parser(  # noqa: PLR6301
                self,
                subcommand_parser: ArgumentParser,  # noqa: ARG002
            ) -> None:
                return None
    
            def invoke_cli_app(self, parsed_cli_args: Namespace) -> ReturnCodeType:  # noqa: PLR6301, ARG002
                raise raised_error
    
        monkeypatch.setattr(
            _cli_parsing_mod,
            'SUBCOMMAND_MODULES',
            [CustomCmdStub()],
        )
    
>       assert invoke_cli_app(['sentinel']) == ReturnCode.ERROR

CustomCmdStub = <class '_cli_test.test_known_interrupts.<locals>.CustomCmdStub'>
capsys     = <_pytest.capture.CaptureFixture object at 0x7fc7fc012470>
expected_stderr = 'App exiting: sentinel'
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7fc7fc0138b0>
raised_error = PreCommitTerraformExit('sentinel')

tests/pytest/_cli_test.py:70: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/pre_commit_terraform/_cli.py:37: in invoke_cli_app
    return invoke_cli_app(parsed_cli_args)
        cli_args   = ['sentinel']
        invoke_cli_app = <bound method test_known_interrupts.<locals>.CustomCmdStub.invoke_cli_app of <_cli_test.test_known_interrupts.<locals>.CustomCmdStub object at 0x7fc7fc0127a0>>
        parsed_cli_args = Namespace(check_name='sentinel', invoke_cli_app=<bound method test_known_interrupts.<locals>.CustomCmdStub.invoke_cli_app of <_cli_test.test_known_interrupts.<locals>.CustomCmdStub object at 0x7fc7fc0127a0>>)
        root_cli_parser = ArgumentParser(prog='python -m pre_commit_terraform', usage=None, description=None, formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <_cli_test.test_known_interrupts.<locals>.CustomCmdStub object at 0x7fc7fc0127a0>
parsed_cli_args = Namespace(check_name='sentinel', invoke_cli_app=<bound method test_known_interrupts.<locals>.CustomCmdStub.invoke_cli_app of <_cli_test.test_known_interrupts.<locals>.CustomCmdStub object at 0x7fc7fc0127a0>>)

    def invoke_cli_app(self, parsed_cli_args: Namespace) -> ReturnCodeType:  # noqa: PLR6301, ARG002
>       raise raised_error
E       pre_commit_terraform._errors.PreCommitTerraformExit: sentinel

parsed_cli_args = Namespace(check_name='sentinel', invoke_cli_app=<bound method test_known_interrupts.<locals>.CustomCmdStub.invoke_cli_app of <_cli_test.test_known_interrupts.<locals>.CustomCmdStub object at 0x7fc7fc0127a0>>)
raised_error = PreCommitTerraformExit('sentinel')
self       = <_cli_test.test_known_interrupts.<locals>.CustomCmdStub object at 0x7fc7fc0127a0>

tests/pytest/_cli_test.py:62: PreCommitTerraformExit
------------------------------------ Captured stderr call -------------------------------------
App exiting: sentinel
==================================== slowest 10 durations =====================================
0.01s setup    tests/pytest/_cli_test.py::test_known_interrupts[app-exit]
0.01s teardown tests/pytest/terraform_docs_replace_test.py::test_control_flow_positive[no-files]

(8 durations < 0.005s hidden.  Use -vv to show these durations.)
=================================== short test summary info ===================================
FAILED tests/pytest/_cli_test.py::test_known_interrupts[app-exit] - pre_commit_terraform._errors.PreCommitTerraformExit: sentinel
================================ 1 failed, 11 passed in 0.18s =================================
py: exit 1 (0.50 seconds) /home/vm/code/0-other/open-source/pre-commit-terraform> .tox/py/bin/python -bb -E -s -I -Werror -W 'ignore:Coverage failure::pytest_cov.plugin' -m pytest --color=yes --cov-report=html:/home/vm/code/0-other/open-source/pre-commit-terraform/.tox/py/tmp/htmlcov/ pid=174329

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, test_app_exit actually covers this case because it's supposed to work a bit differently.

pytest.param(
PreCommitTerraformRuntimeError('sentinel'),
'App execution took an unexpected turn: sentinel. Exiting...',
Expand All @@ -42,21 +41,23 @@
),
)
def test_known_interrupts(
capsys: pytest.CaptureFixture[str],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything against args shifted with a different indent than the function body?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing from my side. Need to find ruff config for that then

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's autofixed by ruff even if there no ruff.toml

ruff.....................................................................Passed
ruff-format..............................................................Failed
- hook id: ruff-format
- files were modified by this hook

1 file reformatted, 9 files left unchanged

Maybe that's because it has ) without indents?

def test_known_interrupts(
    params,
) -> None:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, most tools use this formatting. But some have rules for extra indent. I like the extra indent style because then, it's not aligned with the following function body and is visually distinct.

You can change the style. Just make sure to do so in a separate PR.

expected_stderr: str,
monkeypatch: pytest.MonkeyPatch,
raised_error: BaseException,
capsys: pytest.CaptureFixture[str],
expected_stderr: str,
monkeypatch: pytest.MonkeyPatch,
raised_error: BaseException,
) -> None:
"""Check that known interrupts are turned into return code 1."""

class CustomCmdStub:
CLI_SUBCOMMAND_NAME = 'sentinel'

def populate_argument_parser(
self, subcommand_parser: ArgumentParser,
def populate_argument_parser( # noqa: PLR6301
self,
subcommand_parser: ArgumentParser, # noqa: ARG002
) -> None:
return None

def invoke_cli_app(self, parsed_cli_args: Namespace) -> ReturnCodeType:
def invoke_cli_app(self, parsed_cli_args: Namespace) -> ReturnCodeType: # noqa: PLR6301, ARG002
raise raised_error

monkeypatch.setattr(
Expand All @@ -65,35 +66,41 @@ def invoke_cli_app(self, parsed_cli_args: Namespace) -> ReturnCodeType:
[CustomCmdStub()],
)

assert ReturnCode.ERROR == invoke_cli_app(['sentinel'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Urgh..

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol. Didn't know that ruff able to replace code in this way

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, some of the Ruff's linting rules also support autofixing feature. This is what happens when you pass --fix. It's fine, just keep it separate.

assert invoke_cli_app(['sentinel']) == ReturnCode.ERROR

captured_outputs = capsys.readouterr()
assert captured_outputs.err == f'{expected_stderr !s}\n'


def test_app_exit(
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Check that an exit exception is re-raised."""

class CustomCmdStub:
CLI_SUBCOMMAND_NAME = 'sentinel'

def populate_argument_parser(
self, subcommand_parser: ArgumentParser,
def populate_argument_parser( # noqa: PLR6301
self,
subcommand_parser: ArgumentParser, # noqa: ARG002
) -> None:
return None

def invoke_cli_app(self, parsed_cli_args: Namespace) -> ReturnCodeType:
raise PreCommitTerraformExit('sentinel')
def invoke_cli_app( # noqa: PLR6301
self,
parsed_cli_args: Namespace, # noqa: ARG002
) -> ReturnCodeType:
err = 'sentinel'
raise PreCommitTerraformExit(err)

monkeypatch.setattr(
_cli_parsing_mod,
'SUBCOMMAND_MODULES',
[CustomCmdStub()],
)

with pytest.raises(PreCommitTerraformExit, match='^sentinel$'):
with pytest.raises(PreCommitTerraformExit, match=r'^sentinel$'):
invoke_cli_app(['sentinel'])

captured_outputs = capsys.readouterr()
Expand Down
Loading