From cfe2b900c702c35f0b2e3281ef8b87749a890b38 Mon Sep 17 00:00:00 2001 From: Jens Bergmann <1742418+1cu@users.noreply.github.com> Date: Mon, 10 Mar 2025 06:09:49 +0100 Subject: [PATCH] feat: Introduce isort and Python-based code quality tools (#446) --- pyproject.toml | 17 +++++++++-- scripts/__init__.py | 1 + scripts/format_code.py | 33 +++++++++++++++++++++ scripts/git_utils.py | 18 ++++++++++++ scripts/lint_code.py | 65 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 scripts/__init__.py create mode 100644 scripts/format_code.py create mode 100644 scripts/git_utils.py create mode 100644 scripts/lint_code.py diff --git a/pyproject.toml b/pyproject.toml index cfa3e21..34aeb9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ dev = [ "pylint", "mypy", "pyright", + "isort>=5.13.2", # security: "bandit", # packaging: @@ -83,8 +84,8 @@ app = "python -m kleinanzeigen_bot" compile.cmd = "python -O -m PyInstaller pyinstaller.spec --clean" compile.env = {PYTHONHASHSEED = "1", SOURCE_DATE_EPOCH = "0"} # https://pyinstaller.org/en/stable/advanced-topics.html#creating-a-reproducible-build debug = "python -m pdb -m kleinanzeigen_bot" -format = "autopep8 --recursive --in-place src tests --verbose" -lint = {shell = "pylint -v src tests && autopep8 -v --exit-code --recursive --diff src tests && mypy" } +format = "python -m scripts.format_code" +lint = "python -m scripts.lint_code" audit = "bandit -c pyproject.toml -r src" test = "python -m pytest --capture=tee-sys -v" utest = "python -m pytest --capture=tee-sys -v -m 'not itest'" @@ -266,3 +267,15 @@ filterwarnings = [ "ignore:Exception ignored in:pytest.PytestUnraisableExceptionWarning", "ignore::DeprecationWarning" ] + +# Add isort configuration to format imports +[tool.isort] +profile = "black" +line_length = 160 +combine_as_imports = true +combine_star = true +multi_line_output = 3 +force_grid_wrap = 0 +ensure_newline_before_comments = true +no_lines_before = ["LOCALFOLDER"] +combine_straight_imports = true diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..34093fd --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Helper scripts for development tasks.""" diff --git a/scripts/format_code.py b/scripts/format_code.py new file mode 100644 index 0000000..a36b15c --- /dev/null +++ b/scripts/format_code.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Helper script to format Python code using isort and autopep8.""" + +import subprocess, sys + +from scripts.git_utils import get_modified_python_files + + +def format_files() -> int: + """Format Python files using isort and autopep8. + + Returns: + int: 0 on success, non-zero on failure + """ + try: + # Format imports in modified files + py_files = get_modified_python_files() + if py_files: + subprocess.run(['isort', *py_files], check=True) + + # Format all files with autopep8 + subprocess.run( + ['autopep8', '--recursive', '--in-place', 'src', 'tests', '--verbose'], + check=True + ) + return 0 + except subprocess.CalledProcessError as e: + print(f"Error during formatting: {e}", file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(format_files()) diff --git a/scripts/git_utils.py b/scripts/git_utils.py new file mode 100644 index 0000000..4b1e8f0 --- /dev/null +++ b/scripts/git_utils.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Utility functions for git operations.""" + +import subprocess +from collections.abc import Sequence + + +def get_modified_python_files() -> Sequence[str]: + """Get list of modified Python files from git. + + Returns: + Sequence[str]: List of modified Python file paths + """ + git_cmd = ['git', 'diff', '--name-only', '--diff-filter=ACMR', 'HEAD'] + result = subprocess.run(git_cmd, capture_output=True, text=True, check=True) + if not result.stdout.strip(): + return [] + return [f for f in result.stdout.splitlines() if f.endswith('.py')] diff --git a/scripts/lint_code.py b/scripts/lint_code.py new file mode 100644 index 0000000..daa517e --- /dev/null +++ b/scripts/lint_code.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Helper script to lint Python code using multiple tools.""" + +import subprocess, sys +from collections.abc import Sequence + +from scripts.git_utils import get_modified_python_files + + +def run_tool(cmd: Sequence[str], tool_name: str) -> tuple[int, str]: + """Run a lint tool and return its exit code and error message. + + Args: + cmd: Command to run as list of strings + tool_name: Name of the tool for error messages + + Returns: + tuple[int, str]: tuple of (exit_code, error_message) + """ + try: + subprocess.run(list(cmd), check=True) + return 0, "" + except subprocess.CalledProcessError as e: + return e.returncode, f"{tool_name} fehlgeschlagen\n" + + +def lint_files() -> int: + """Run all linting tools. + + Returns: + int: Number of errors encountered + """ + errors = 0 + error_messages = [] + + # Run isort on modified files + py_files = get_modified_python_files() + if py_files: + code, msg = run_tool(['isort', *py_files], 'isort') + errors += code + if msg: + error_messages.append(msg) + + # Run other linting tools + tools = [ + (['pylint', '-v', 'src', 'tests'], 'pylint'), + (['autopep8', '-v', '--in-place', '--recursive', 'src', 'tests'], 'autopep8'), + (['mypy', 'src', 'tests'], 'mypy') + ] + + for cmd, name in tools: + code, msg = run_tool(cmd, name) + errors += code + if msg: + error_messages.append(msg) + + # Print all error messages at the end + if error_messages: + print("\n".join(error_messages), file=sys.stderr) + + return errors + + +if __name__ == '__main__': + sys.exit(lint_files())