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 1 commit
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
69 changes: 41 additions & 28 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ repos:
hooks:
- id: gitleaks

# Dockerfile linter
- repo: https://github.com/hadolint/hadolint
rev: v2.12.1-beta
hooks:
- id: hadolint
args:
- --ignore=DL3007 # Using latest
- --ignore=DL3013 # Pin versions in pip
- --ignore=DL3027 # Do not use apt
- --ignore=DL3059 # Docker `RUN`s shouldn't be consolidated here
- --ignore=DL4006 # Not related to alpine
- --ignore=SC1091 # Useless check
- --ignore=SC2015 # Useless check
- --ignore=SC3037 # Not related to alpine

#
# YAML Linters
#
Expand Down Expand Up @@ -79,6 +94,32 @@ repos:
# https://prettier.io/docs/en/options.html#parser
files: .json5$


# Bash Linter
- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 3.0.0
hooks:
- id: shfmt
args:
- -l
- -i
- '2'
- -ci
- -sr
- -w
- id: shellcheck

#
# Python Linters
#
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
hooks:
- id: ruff
args:
- --fix
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy.git
rev: v1.15.0
hooks:
Expand Down Expand Up @@ -133,31 +174,3 @@ repos:
- --lineprecision-report=.tox/.tmp/.test-results/mypy--py-3.9
- --txt-report=.tox/.tmp/.test-results/mypy--py-3.9
pass_filenames: false

- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 3.0.0
hooks:
- id: shfmt
args:
- -l
- -i
- '2'
- -ci
- -sr
- -w
- id: shellcheck

# Dockerfile linter
- repo: https://github.com/hadolint/hadolint
rev: v2.12.1-beta
hooks:
- id: hadolint
args:
- --ignore=DL3007 # Using latest
- --ignore=DL3013 # Pin versions in pip
- --ignore=DL3027 # Do not use apt
- --ignore=DL3059 # Docker `RUN`s shouldn't be consolidated here
- --ignore=DL4006 # Not related to alpine
- --ignore=SC1091 # Useless check
- --ignore=SC2015 # Useless check
- --ignore=SC3037 # Not related to alpine
28 changes: 28 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Assume Python 3.9
target-version = "py39"

[format]
quote-style = "single"

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

[lint]
select = ["ALL"]
preview = true
ignore = [
"CPY001", # Missing copyright notice at top of file
"D213", # multi-line-summary-second-line. Incompatible with multi-line-summary-first-line (D212)
"D203", # one-blank-line-before-class. Incompatible with no-blank-line-before-class (D211)
"INP001", # We use namespace packages in this project
]

[lint.isort]
force-single-line = true

[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
]
4 changes: 2 additions & 2 deletions src/pre_commit_terraform/__main__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""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


return_code = invoke_cli_app(argv[1:])
exit_with_return_code(return_code)
30 changes: 16 additions & 14 deletions src/pre_commit_terraform/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,51 @@
from typing import cast as cast_to

from ._cli_parsing import initialize_argument_parser
from ._errors import (
PreCommitTerraformBaseError,
PreCommitTerraformExit,
PreCommitTerraformRuntimeError,
)
from ._errors import PreCommitTerraformBaseError
from ._errors import PreCommitTerraformExit
from ._errors import PreCommitTerraformRuntimeError
from ._structs import ReturnCode
from ._types import CLIAppEntryPointCallableType, ReturnCodeType
from ._types import CLIAppEntryPointCallableType
from ._types import ReturnCodeType


def invoke_cli_app(cli_args: list[str]) -> ReturnCodeType:
"""Run the entry-point of the CLI app.

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.


"""
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(
f'App execution took an unexpected turn: {unhandled_exc !s}. '
'Exiting...',
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
1 change: 0 additions & 1 deletion src/pre_commit_terraform/_cli_subcommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from . import terraform_docs_replace
from ._types import CLISubcommandModuleProtocol


SUBCOMMAND_MODULES: list[CLISubcommandModuleProtocol] = [
terraform_docs_replace,
]
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."""
11 changes: 5 additions & 6 deletions src/pre_commit_terraform/_types.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Composite types for annotating in-project code."""

from argparse import ArgumentParser, Namespace
from argparse import ArgumentParser
from argparse import Namespace
from collections.abc import Callable
from typing import Protocol, Union
from typing import Protocol
from typing import Union

from ._structs import ReturnCode


ReturnCodeType = Union[ReturnCode, int] # Union instead of pipe for Python 3.9
CLIAppEntryPointCallableType = Callable[[Namespace], ReturnCodeType]

Expand All @@ -17,9 +18,7 @@ class CLISubcommandModuleProtocol(Protocol):
CLI_SUBCOMMAND_NAME: str
"""This constant contains a CLI."""

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

def invoke_cli_app(self, parsed_cli_args: Namespace) -> ReturnCodeType:
Expand Down
69 changes: 44 additions & 25 deletions src/pre_commit_terraform/terraform_docs_replace.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
"""`terraform_docs_replace` hook. Deprecated."""

import os
import subprocess
import subprocess # noqa: S404. We invoke cli tools
Copy link
Contributor

Choose a reason for hiding this comment

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

Since more places will use subprocesses, it might be a good idea to have a helper module that imports subprocess and wraps it into another function. Then use that helper everywhere. This noqa would be in that one centralized location. This is a security-related linter (coming from the bandit plugin) so it's a good idea to bundle such violations in a single location with the justification documented and easily inspectable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have a slightly related question: Can we somehow utilize pip-installable terraform binary?
https://github.com/AleksaC/terraform-py

To be able to utilize https://pre-commit.com/#config-additional_dependencies or something like that, that will make users able to choose terraform version right into hooks configuration

Copy link
Contributor

Choose a reason for hiding this comment

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

sys.executable holds the absolute path to the current Python runtime binary. Things installed into the same virtualenv will be in the same directory if they ship scripts.

So doing something like str(pathlib.Path(sys.executable).parent.resolve() / 'terraform-py') will allow you to run that other binary.

Since that project also has a runpy interface (via the __main__.py), instead of invoking terraform-py, you can invoke python -Im terraform_py instead.

When running it from here, you can construct the command for passing into subprocess' functions smth like (sys.executable, '-I', '-m', 'terraform_py').

import warnings
from argparse import ArgumentParser, Namespace
from argparse import ArgumentParser
from argparse import Namespace
from typing import cast as cast_to

from ._structs import ReturnCode
from ._types import ReturnCodeType


CLI_SUBCOMMAND_NAME: str = 'replace-docs'


def populate_argument_parser(subcommand_parser: ArgumentParser) -> None:
"""Populate the parser for the subcommand."""
subcommand_parser.description = (
'Run terraform-docs on a set of files. Follows the standard '
'convention of pulling the documentation from main.tf in order to '
'replace the entire README.md file each time.'
)
subcommand_parser.add_argument(
'--dest', dest='dest', default='README.md',
'--dest',
dest='dest',
default='README.md',
)
subcommand_parser.add_argument(
'--sort-inputs-by-required', dest='sort', action='store_true',
'--sort-inputs-by-required',
dest='sort',
action='store_true',
help='[deprecated] use --sort-by-required instead',
)
subcommand_parser.add_argument(
'--sort-by-required', dest='sort', action='store_true',
'--sort-by-required',
dest='sort',
action='store_true',
)
subcommand_parser.add_argument(
'--with-aggregate-type-defaults',
Expand All @@ -41,7 +50,13 @@ def populate_argument_parser(subcommand_parser: ArgumentParser) -> None:


def invoke_cli_app(parsed_cli_args: Namespace) -> ReturnCodeType:
warnings.warn(
"""Run the entry-point of the CLI app.

Returns:
ReturnCodeType: The return code of the app.

"""
warnings.warn( # noqa: B028. that's user warning, no need to show stacktrace etc.
'`terraform_docs_replace` hook is DEPRECATED.'
'For migration instructions see '
'https://github.com/antonbabenko/pre-commit-terraform/issues/248'
Expand All @@ -50,28 +65,32 @@ def invoke_cli_app(parsed_cli_args: Namespace) -> ReturnCodeType:
)

dirs: list[str] = []
for filename in cast_to(list[str], parsed_cli_args.filenames):
if (os.path.realpath(filename) not in dirs and
(filename.endswith(".tf") or filename.endswith(".tfvars"))):
dirs.append(os.path.dirname(filename))
for filename in cast_to('list[str]', parsed_cli_args.filenames):
if os.path.realpath(filename) not in dirs and (
filename.endswith(('.tf', '.tfvars'))
):
dirs.append(os.path.dirname(filename)) # noqa: PTH120. Legacy hook, no need to refactor

retval = ReturnCode.OK

for dir in dirs:
for directory in dirs:
try:
procArgs = []
procArgs.append('terraform-docs')
if cast_to(bool, parsed_cli_args.sort):
procArgs.append('--sort-by-required')
procArgs.append('md')
procArgs.append("./{dir}".format(dir=dir))
procArgs.append('>')
procArgs.append(
'./{dir}/{dest}'.
format(dir=dir, dest=cast_to(bool, parsed_cli_args.dest)),
proc_args = []
proc_args.append('terraform-docs')
if cast_to('bool', parsed_cli_args.sort):
proc_args.append('--sort-by-required')
proc_args.extend(
(
'md',
f'./{directory}',
'>',
f"./{directory}/{cast_to('bool', parsed_cli_args.dest)}",
),
)
subprocess.check_call(" ".join(procArgs), shell=True)
except subprocess.CalledProcessError as e:
print(e)
# We call cli tools, of course we use shell=True
subprocess.check_call(' '.join(proc_args), shell=True) # noqa: S602
# Legacy hook, no need to refactor
except subprocess.CalledProcessError as e: # noqa: PERF203
print(e) # noqa: T201
retval = ReturnCode.ERROR
return retval
Loading
Loading