diff --git a/.gitignore b/.gitignore index b486fe1..0a19790 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,174 @@ -# Created by .ignore support plugin (hsz.mobi) +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class -# User-specific stuff -.idea +# C extensions +*.so -# IntelliJ -out/ +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST -# JIRA plugin -atlassian-ide-plugin.xml +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec -venv \ No newline at end of file +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..49ae3db --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: debug-statements + - id: name-tests-test + - id: requirements-txt-fixer + - repo: https://github.com/asottile/setup-cfg-fmt + rev: v2.8.0 + hooks: + - id: setup-cfg-fmt + - repo: https://github.com/asottile/add-trailing-comma + rev: v3.1.0 + hooks: + - id: add-trailing-comma + - repo: https://github.com/asottile/pyupgrade + rev: v3.19.1 + hooks: + - id: pyupgrade + args: [--py39-plus] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.8 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 3f6b553..aaf2c12 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,40 +1,37 @@ - id: sbt-fatal-warnings name: Scala fatal warnings - stages: [commit,push] + stages: [pre-commit, pre-push] language: python entry: sbt-fatal-warnings pass_filenames: false - always_run: true - minimum_pre_commit_version: '0.19.0' + minimum_pre_commit_version: "0.19.0" - id: sbt-unused-imports name: Scala unused imports (+ fatal warnings) - stages: [commit,push] + stages: [pre-commit, pre-push] language: python entry: sbt-fatal-warnings --add_arg='-Ywarn-unused-import' pass_filenames: false - always_run: true - minimum_pre_commit_version: '0.19.0' + minimum_pre_commit_version: "0.19.0" - id: sbt-scalafmt + alias: scalafmt name: scalafmt formatting check - stages: [commit,push] + stages: [pre-commit, pre-push] language: python entry: scalafmt pass_filenames: false - always_run: true - minimum_pre_commit_version: '0.19.0' + minimum_pre_commit_version: "0.19.0" - id: sbt-wartremover name: Scala WartRemover plugin check + stages: [pre-commit, pre-push] language: python - stages: [commit,push] entry: sbt-wartremover pass_filenames: false - always_run: true - minimum_pre_commit_version: '0.19.0' + minimum_pre_commit_version: "0.19.0" - id: sbt-scalafmt-apply + alias: scalafmt-apply name: scalafmt formatting fix - stages: [commit,push] + stages: [pre-commit, pre-push] language: python entry: scalafmt-apply pass_filenames: false - always_run: true - minimum_pre_commit_version: '0.19.0' + minimum_pre_commit_version: "0.19.0" diff --git a/README.adoc b/README.adoc index 1ba97e3..d35a7ab 100644 --- a/README.adoc +++ b/README.adoc @@ -2,7 +2,7 @@ :repoRoot: https://github.com/softwaremill/scala-pre-commit-hooks :repoMaster: {repoRoot}/blob/master :defaultScope: test:compile -:currentVersion: v0.3.0 +:currentVersion: v0.5.0 == What for? @@ -37,7 +37,7 @@ To add one or more of the hooks into your repo: ..pre-commit-config.yaml ---- repos: -- repo: https://github.com/pre-commit/pre-commit-hooks +- repo: https://github.com/softwaremill/scala-pre-commit-hooks rev: {currentVersion} default_phase: push #change to commit if desired hooks: #mix and match any of the following: @@ -47,6 +47,7 @@ repos: args: [--scope={defaultScope}] - id: sbt-scalafmt - id: sbt-scalafmt-apply + args: [--no-clean] - id: sbt-wartremover #arguments are optional args: [--warts=Warts.unsafe, --scope={defaultScope}] ---- @@ -59,6 +60,12 @@ All hooks except for `sbt-scalafmt` and `sbt-scalafmt-apply` have the optional ` are relevant for the hook's check. The default is `{defaultScope}`. -- +[NOTE] +-- +All hooks have an optional `project-dir` arugument, which is useful for monorepos that do not have a `build.sbt` in their root. +All hooks have an optional `no-clean` arugument, which won't automatically run an `sbt clean` before executing the command. +-- + [IMPORTANT] -- Steps 1-2 are only really required for the person setting up the _pre-commit library_ integration. _pre-commit library_ plugins, once installed, are normal Git hooks and are thus "visible" to everyone using the repo. diff --git a/pre_commit_hooks/__init__.py b/pre_commit_hooks/__init__.py index 8b13789..e69de29 100644 --- a/pre_commit_hooks/__init__.py +++ b/pre_commit_hooks/__init__.py @@ -1 +0,0 @@ - diff --git a/pre_commit_hooks/runner.py b/pre_commit_hooks/runner.py index 850593b..ea9d472 100644 --- a/pre_commit_hooks/runner.py +++ b/pre_commit_hooks/runner.py @@ -1,23 +1,83 @@ +import argparse import subprocess +from dataclasses import dataclass, field +from typing import Callable -def run_sbt_command(task_def, missing_plugin_check_string=None, missing_plugin_error_msg=None): - sbt_process = subprocess.run([f"sbt '{task_def}'"], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) +@dataclass +class Opts: + project_dir: str | None = None + clean: bool = True + varargs: dict = field(default_factory=dict) + + +def default_argparse( + description: str, + argv=None, + additional_args: list[Callable[[argparse.ArgumentParser], None]] = [], +) -> Opts: + arg_p = argparse.ArgumentParser(description=description) + arg_p.add_argument( + "--project-dir", + default=None, + help="Path to build.sbt. Default: Project root", + ) + arg_p.add_argument( + "--no-clean", + action="store_true", + default=False, + help="Turn off sbt clean. Default: False", + ) + for fn in additional_args: + fn(arg_p) + varags = vars(arg_p.parse_args(argv)) + return Opts( + project_dir=varags.pop("project_dir", None), + clean=not varags.pop("no_clean", False), + varargs=varags, + ) + + +def run_sbt_command( + task_def: str, + missing_plugin_check_string: str | None = None, + missing_plugin_error_msg: str | None = None, + opts: Opts = Opts(), +): + print(f"Running SBT command: {task_def} with options: {opts}") + if opts.clean: + task_def = f"; clean ; {task_def}" + else: + task_def = f"; {task_def}" + sbt_process = subprocess.run( + [f"sbt '{task_def}'"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True, + cwd=opts.project_dir, + ) raw_output = sbt_process.stdout.decode("utf-8") - if missing_plugin_check_string is not None and missing_plugin_check_string in raw_output: + if ( + missing_plugin_check_string is not None + and missing_plugin_check_string in raw_output + ): print(missing_plugin_error_msg) else: print(raw_output) return sbt_process.returncode + def run_git_add_modified(): """Adds stages files if changes are detected.""" try: # Check if there are modified files before running git add -u - status = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True) + status = subprocess.run( + ["git", "status", "--porcelain"], + capture_output=True, + text=True, + ) if status.stdout.strip(): # If there are modified files print("Staged formatted files.") diff --git a/pre_commit_hooks/sbt_fatal_warnings.py b/pre_commit_hooks/sbt_fatal_warnings.py index 08514e5..e457953 100644 --- a/pre_commit_hooks/sbt_fatal_warnings.py +++ b/pre_commit_hooks/sbt_fatal_warnings.py @@ -1,26 +1,47 @@ -import argparse -from pre_commit_hooks.runner import * - -ARG_ADDITIONAL_ARGS = 'add_arg' -ARG_COMPILE_SCOPE = 'scope' -DEFAULT_COMPILE_SCOPE = 'test:compile' - - -def main(argv=None): - arg_p = argparse.ArgumentParser(description='Run SBT wartremover') - arg_p.add_argument(f'--{ARG_ADDITIONAL_ARGS}', action='append', - help='Additional arguments for scalac, such as warning flags, can be multi-valued.') - arg_p.add_argument(f'--{ARG_COMPILE_SCOPE}', default=DEFAULT_COMPILE_SCOPE, - help=f'Compile scope for the check. Default: {DEFAULT_COMPILE_SCOPE}') - - args = arg_p.parse_args(argv).__dict__ +from __future__ import annotations - args[ARG_ADDITIONAL_ARGS].append("-Xfatal-warnings") +import argparse - add_args = ", ".join(f'"{a}"' for a in args[ARG_ADDITIONAL_ARGS]) +from pre_commit_hooks.runner import default_argparse, run_sbt_command - return run_sbt_command(f'; clean ; set scalacOptions ++= Seq({add_args}) ; {args[ARG_COMPILE_SCOPE]}') +ARG_ADDITIONAL_ARGS = "add_arg" +ARG_COMPILE_SCOPE = "scope" +DEFAULT_COMPILE_SCOPE = "test:compile" -if __name__ == '__main__': +def main(argv=None): + def arg_append(arg_p: argparse.ArgumentParser) -> None: + arg_p.add_argument( + f"--{ARG_ADDITIONAL_ARGS}", + action="append", + help="Additional arguments for scalac, such as warning flags, can be multi-valued.", + ) + + def arg_compile_scope(arg_p: argparse.ArgumentParser) -> None: + arg_p.add_argument( + f"--{ARG_COMPILE_SCOPE}", + default=DEFAULT_COMPILE_SCOPE, + help=f"Compile scope for the check. Default: {DEFAULT_COMPILE_SCOPE}", + ) + + args = default_argparse( + description="Run SBT wartremover", + argv=argv, + additional_args=[arg_append, arg_compile_scope], + ) + addtl_args = args.varargs.get(ARG_ADDITIONAL_ARGS, []) + if not addtl_args: + addtl_args = [] + if "-Xfatal-warnings" not in addtl_args: + addtl_args.append("-Xfatal-warnings") + + add_args = ", ".join(f'"{a}"' for a in addtl_args) + + return run_sbt_command( + task_def=f"set scalacOptions ++= Seq({add_args})", + opts=args, + ) + + +if __name__ == "__main__": exit(main()) diff --git a/pre_commit_hooks/sbt_wartremover.py b/pre_commit_hooks/sbt_wartremover.py index ede80f6..2441cc9 100644 --- a/pre_commit_hooks/sbt_wartremover.py +++ b/pre_commit_hooks/sbt_wartremover.py @@ -1,29 +1,50 @@ -import argparse -from pre_commit_hooks.runner import * -from colorama import init as colorama_init, Fore +from __future__ import annotations -ARG_WARTREMOVER_ARGS = 'warts' -ARG_COMPILE_SCOPE = 'scope' -DEFAULT_WARTREMOVER_ARGS = 'Warts.unsafe' -DEFAULT_COMPILE_SCOPE = 'test:compile' -MISSING_PLUGIN_CHECK_STRING = 'error: not found: value wartremoverErrors' -MISSING_PLUGIN_ERROR_MSG = f'{Fore.RED}ERROR: wartremover SBT plugin not present! See {Fore.BLUE}https://www.wartremover.org/doc/install-setup.html{Fore.RED} for installation instructions.' +import argparse +from colorama import Fore +from colorama import init as colorama_init -def main(argv=None): - colorama_init() +from pre_commit_hooks.runner import default_argparse, run_sbt_command - arg_p = argparse.ArgumentParser(description='Run SBT wartremover') - arg_p.add_argument(f'--{ARG_WARTREMOVER_ARGS}', default=DEFAULT_WARTREMOVER_ARGS, - help=f'Value for wartremoverErrors, as per https://www.wartremover.org/doc/install-setup.html . Default: {DEFAULT_WARTREMOVER_ARGS}') - arg_p.add_argument(f'--{ARG_COMPILE_SCOPE}', default=DEFAULT_COMPILE_SCOPE, - help=f'Compile scope for the wartremover check. Default: {DEFAULT_COMPILE_SCOPE}') - arg_p.print_help() +ARG_WARTREMOVER_ARGS = "warts" +ARG_COMPILE_SCOPE = "scope" +DEFAULT_WARTREMOVER_ARGS = "Warts.unsafe" +DEFAULT_COMPILE_SCOPE = "test:compile" +MISSING_PLUGIN_CHECK_STRING = "error: not found: value wartremoverErrors" +MISSING_PLUGIN_ERROR_MSG = f"{Fore.RED}ERROR: wartremover SBT plugin not present! See {Fore.BLUE}https://www.wartremover.org/doc/install-setup.html{Fore.RED} for installation instructions." - args = arg_p.parse_args(argv).__dict__ - - return run_sbt_command(f'; clean ; set wartremoverErrors ++= {args[ARG_WARTREMOVER_ARGS]}; {args[ARG_COMPILE_SCOPE]}', MISSING_PLUGIN_CHECK_STRING, MISSING_PLUGIN_ERROR_MSG) +def main(argv=None): + colorama_init() -if __name__ == '__main__': + def arg_wartremover(arg_p: argparse.ArgumentParser) -> None: + arg_p.add_argument( + f"--{ARG_WARTREMOVER_ARGS}", + default=DEFAULT_WARTREMOVER_ARGS, + help=f"Value for wartremoverErrors, as per https://www.wartremover.org/doc/install-setup.html . Default: {DEFAULT_WARTREMOVER_ARGS}", + ) + + def arg_compile_scope(arg_p: argparse.ArgumentParser) -> None: + arg_p.add_argument( + f"--{ARG_COMPILE_SCOPE}", + default=DEFAULT_COMPILE_SCOPE, + help=f"Compile scope for the check. Default: {DEFAULT_COMPILE_SCOPE}", + ) + + args = default_argparse( + description="Run SBT wartremover", + argv=argv, + additional_args=[arg_wartremover, arg_compile_scope], + ) + + return run_sbt_command( + task_def=f"set wartremoverErrors ++= {args.varargs.get(ARG_WARTREMOVER_ARGS)}; {args.varargs.get(ARG_COMPILE_SCOPE)}", + missing_plugin_check_string=MISSING_PLUGIN_CHECK_STRING, + missing_plugin_error_msg=MISSING_PLUGIN_ERROR_MSG, + opts=args, + ) + + +if __name__ == "__main__": exit(main()) diff --git a/pre_commit_hooks/scalafmt.py b/pre_commit_hooks/scalafmt.py index c0e9e29..86cc81d 100644 --- a/pre_commit_hooks/scalafmt.py +++ b/pre_commit_hooks/scalafmt.py @@ -1,16 +1,30 @@ -from pre_commit_hooks.runner import run_sbt_command -from colorama import init as colorama_init, Fore +from __future__ import annotations -TASK_SCALAFMT = 'scalafmtCheckAll' -MISSING_PLUGIN_CHECK_STRING = 'Not a valid key: scalafmtCheck' -MISSING_PLUGIN_ERROR_MSG = f'{Fore.RED}ERROR: scalafmt SBT plugin not present! See {Fore.BLUE}https://scalameta.org/scalafmt/docs/installation.html#sbt{Fore.RED} for installation instructions.' +from colorama import Fore +from colorama import init as colorama_init + +from pre_commit_hooks.runner import ( + default_argparse, + run_sbt_command, +) + +TASK_SCALAFMT = "scalafmtCheckAll" +MISSING_PLUGIN_CHECK_STRING = "Not a valid key: scalafmtCheck" +MISSING_PLUGIN_ERROR_MSG = f"{Fore.RED}ERROR: scalafmt SBT plugin not present! See {Fore.BLUE}https://scalameta.org/scalafmt/docs/installation.html#sbt{Fore.RED} for installation instructions." def main(argv=None): colorama_init() - return run_sbt_command(f'; clean ; {TASK_SCALAFMT}', MISSING_PLUGIN_CHECK_STRING, MISSING_PLUGIN_ERROR_MSG) + args = default_argparse("Run SBT scalafmt", argv=argv) + print(f"args: {args}") + return run_sbt_command( + task_def=f"{TASK_SCALAFMT}", + missing_plugin_check_string=MISSING_PLUGIN_CHECK_STRING, + missing_plugin_error_msg=MISSING_PLUGIN_ERROR_MSG, + opts=args, + ) -if __name__ == '__main__': +if __name__ == "__main__": exit(main()) diff --git a/pre_commit_hooks/scalafmt_apply.py b/pre_commit_hooks/scalafmt_apply.py index f7591d2..d055202 100644 --- a/pre_commit_hooks/scalafmt_apply.py +++ b/pre_commit_hooks/scalafmt_apply.py @@ -1,19 +1,34 @@ -from pre_commit_hooks.runner import run_sbt_command, run_git_add_modified -from colorama import init as colorama_init, Fore +from __future__ import annotations -TASK_SCALAFMT = 'scalafmtAll' -MISSING_PLUGIN_CHECK_STRING = 'Not a valid key: scalafmtAll' -MISSING_PLUGIN_ERROR_MSG = f'{Fore.RED}ERROR: scalafmt SBT plugin not present! See {Fore.BLUE}https://scalameta.org/scalafmt/docs/installation.html#sbt{Fore.RED} for installation instructions.' +from colorama import Fore +from colorama import init as colorama_init + +from pre_commit_hooks.runner import ( + default_argparse, + run_git_add_modified, + run_sbt_command, +) + +TASK_SCALAFMT = "scalafmtAll" +MISSING_PLUGIN_CHECK_STRING = "Not a valid key: scalafmtAll" +MISSING_PLUGIN_ERROR_MSG = f"{Fore.RED}ERROR: scalafmt SBT plugin not present! See {Fore.BLUE}https://scalameta.org/scalafmt/docs/installation.html#sbt{Fore.RED} for installation instructions." def main(argv=None): colorama_init() - sbt = run_sbt_command(f'; clean ; {TASK_SCALAFMT}', MISSING_PLUGIN_CHECK_STRING, MISSING_PLUGIN_ERROR_MSG) + args = default_argparse("Run SBT scalafmt", argv=argv) + + sbt = run_sbt_command( + task_def=f"{TASK_SCALAFMT}", + missing_plugin_check_string=MISSING_PLUGIN_CHECK_STRING, + missing_plugin_error_msg=MISSING_PLUGIN_ERROR_MSG, + opts=args, + ) run_git_add_modified() return sbt -if __name__ == '__main__': +if __name__ == "__main__": exit(main()) diff --git a/setup.cfg b/setup.cfg index 7e99faf..49cb88c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,19 @@ [metadata] name = pre_commit_hooks -version = 0.8.0 +version = 0.5.0 +long_description = file: README.adoc +long_description_content_type = text/plain +license = Apache-2.0 +license_files = LICENSE +classifiers = + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only [options] packages = find: install_requires = colorama -python_requires = >=3.6 +python_requires = >=3.9 [options.entry_points] console_scripts = diff --git a/setup.py b/setup.py index 8bf1ba9..a03590f 100644 --- a/setup.py +++ b/setup.py @@ -1,2 +1,5 @@ +from __future__ import annotations + from setuptools import setup + setup()