From 8d02cf608279ef1882dec260d0767ed2977021d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Wed, 3 Aug 2022 17:25:02 -0300 Subject: [PATCH 01/10] WIP: Run all checks --- .dockerignore | 10 ++++++++-- .gitignore | 6 +++++- Makefile | 14 ++++++++++++++ requirements.txt | 5 +++-- tests/requirements.txt | 1 + 5 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 tests/requirements.txt diff --git a/.dockerignore b/.dockerignore index a375751..220eb70 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,14 @@ -.git -.github +.*project .vscode docs/ +/.settings/ +/tests +bin Dockerfile +include +lib +lib64 LICENSE.txt Makefile README.md +requirements-dev.txt diff --git a/.gitignore b/.gitignore index 8583b4b..e230176 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.*project .vscode /.settings/ -.*project +bin +include +lib +lib64 \ No newline at end of file diff --git a/Makefile b/Makefile index 2fa73d9..e9a4a51 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,20 @@ CURRENT_USER=$$(whoami) help: ## This help message @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' +bin/pip: + @echo "$(GREEN)==> Setup Virtual Env$(RESET)" + python3 -m venv . + bin/pip install -r requirements.txt + +.PHONY: clean +clean: ## remove virtual environment + rm -fr bin include lib lib64 + +.PHONY: setup +setup: bin/pip ## Create virtualenv and run pip install + @echo "$(GREEN)==> Setup Dev Environment$(RESET)" + bin/pip install -r tests/requirements.txt + .PHONY: build-image build-image: ## Build Docker Image @echo "Building $(IMAGE_NAME):latest" diff --git a/requirements.txt b/requirements.txt index a93a640..1261e14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ black==22.6.0 -isort==5.10.1 -flakeheaven==3.0.0 flake8-blind-except==0.2.1 flake8-debugger==4.1.2 flake8-print==5.0.0 +flakeheaven==3.0.0 +isort==5.10.1 pyroma==4.0b2 +tomli==2.0.1 zpretty==2.2.0 diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..55b033e --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1 @@ +pytest \ No newline at end of file From cf069a255b282cae90c727008b9819bfc4b902c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Thu, 4 Aug 2022 15:11:48 -0300 Subject: [PATCH 02/10] Implement settings from pyproject.toml --- .dockerignore | 7 +- .github/workflows/release.yml | 2 + .github/workflows/tests.yml | 27 +++ .gitignore | 2 + Dockerfile | 19 +- Makefile | 21 +- docker-entrypoint.py | 181 +++----------- pyproject.toml | 20 +- requirements.txt | 1 + src/plone_code_analysis/__init__.py | 1 + src/plone_code_analysis/check.py | 74 ++++++ src/plone_code_analysis/cmd.py | 46 ++++ src/plone_code_analysis/format.py | 58 +++++ src/plone_code_analysis/logger.py | 5 + src/plone_code_analysis/settings.py | 84 +++++++ src/setup.py | 35 +++ tests/conftest.py | 33 +++ tests/fixtures/configs/check_path.toml | 3 + tests/fixtures/configs/checkers.toml | 3 + tests/fixtures/configs/default.toml | 22 ++ tests/fixtures/configs/foo/__init__.py | 5 + tests/fixtures/configs/foo/configure.zcml | 8 + tests/fixtures/configs/setup.py | 37 +++ .../fixtures/packages/not_ok/bar/__init__.py | 5 + .../packages/not_ok/bar/configure.zcml | 6 + tests/fixtures/packages/not_ok/pyproject.toml | 25 ++ tests/fixtures/packages/not_ok/setup.py | 19 ++ tests/fixtures/packages/ok/README.md | 41 ++++ tests/fixtures/packages/ok/foo/__init__.py | 5 + tests/fixtures/packages/ok/foo/configure.zcml | 8 + tests/fixtures/packages/ok/pyproject.toml | 25 ++ tests/fixtures/packages/ok/setup.py | 43 ++++ tests/package/test_check.py | 119 +++++++++ tests/package/test_settings.py | 229 ++++++++++++++++++ tests/requirements.txt | 1 + 35 files changed, 1043 insertions(+), 177 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 src/plone_code_analysis/__init__.py create mode 100644 src/plone_code_analysis/check.py create mode 100644 src/plone_code_analysis/cmd.py create mode 100644 src/plone_code_analysis/format.py create mode 100644 src/plone_code_analysis/logger.py create mode 100644 src/plone_code_analysis/settings.py create mode 100644 src/setup.py create mode 100644 tests/conftest.py create mode 100644 tests/fixtures/configs/check_path.toml create mode 100644 tests/fixtures/configs/checkers.toml create mode 100644 tests/fixtures/configs/default.toml create mode 100644 tests/fixtures/configs/foo/__init__.py create mode 100644 tests/fixtures/configs/foo/configure.zcml create mode 100644 tests/fixtures/configs/setup.py create mode 100644 tests/fixtures/packages/not_ok/bar/__init__.py create mode 100644 tests/fixtures/packages/not_ok/bar/configure.zcml create mode 100644 tests/fixtures/packages/not_ok/pyproject.toml create mode 100644 tests/fixtures/packages/not_ok/setup.py create mode 100644 tests/fixtures/packages/ok/README.md create mode 100644 tests/fixtures/packages/ok/foo/__init__.py create mode 100644 tests/fixtures/packages/ok/foo/configure.zcml create mode 100644 tests/fixtures/packages/ok/pyproject.toml create mode 100644 tests/fixtures/packages/ok/setup.py create mode 100644 tests/package/test_check.py create mode 100644 tests/package/test_settings.py diff --git a/.dockerignore b/.dockerignore index 220eb70..6c3c8f9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,14 +1,15 @@ +__pycache__ .*project +.pytest_cache .vscode -docs/ /.settings/ /tests bin Dockerfile +docs/ include lib lib64 LICENSE.txt Makefile -README.md -requirements-dev.txt +README.md \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bd34d8..dc51d17 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,8 @@ name: Release new Docker image on: push: + branches: + - 'main' tags: - 'v*.*.*' pull_request: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..9f8b17e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,27 @@ +name: Test codebase + +on: push + +jobs: + + tests: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + + - name: Install requirements + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r tests/requirements.txt + + - name: Run tests + run: | + python -m pytest tests/ diff --git a/.gitignore b/.gitignore index e230176..dfd6d42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +__pycache__ .*project +.pytest_cache .vscode /.settings/ bin diff --git a/Dockerfile b/Dockerfile index 0f791ff..96d2af4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,15 @@ -FROM python:3.10-slim +FROM python:3.10-slim as base +FROM base as builder + +RUN pip install -U pip wheel \ + && mkdir /app /wheelhouse + +COPY requirements.txt /app/ +COPY src /app/src + +RUN cd /app/ && pip wheel -r requirements.txt --wheel-dir=/wheelhouse + +FROM base LABEL maintainer="Plone Community " \ org.label-schema.name="code-quality" \ @@ -6,9 +17,9 @@ LABEL maintainer="Plone Community " \ org.label-schema.vendor="Plone Foundation" \ org.label-schema.docker.cmd="docker run -rm -v "${PWD}":/github/workspace plone/code-quality check black src" -COPY requirements.txt pyproject.toml docker-entrypoint.py ./ - -RUN pip install -U pip && pip install -r requirements.txt +COPY docker-entrypoint.py / +COPY --from=builder /wheelhouse /wheelhouse +RUN pip install --force-reinstall --no-index --no-deps /wheelhouse/* WORKDIR /github/workspace diff --git a/Makefile b/Makefile index e9a4a51..fa81210 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ IMAGE_NAME=plone/code-quality DOCKERFILE=Dockerfile -CODEBASE=docker-entrypoint.py +CODEBASE=docker-entrypoint.py src/setup.py src/plone_code_analysis tests/fixtures/packages/ok tests/package tests/conftest.py LINT=docker run --rm -v "${PWD}":/github/workspace "${IMAGE_NAME}:latest" check FORMAT=docker run --rm -v "${PWD}":/github/workspace "${IMAGE_NAME}:latest" format CURRENT_USER=$$(whoami) @@ -16,14 +16,20 @@ bin/pip: python3 -m venv . bin/pip install -r requirements.txt +bin/pytest: + bin/pip install -r tests/requirements.txt + .PHONY: clean clean: ## remove virtual environment rm -fr bin include lib lib64 .PHONY: setup -setup: bin/pip ## Create virtualenv and run pip install - @echo "$(GREEN)==> Setup Dev Environment$(RESET)" - bin/pip install -r tests/requirements.txt +setup: bin/pytest ## Create virtualenv and run pip install + +.PHONY: test +test: bin/pytest ## Create virtualenv and run pip install + @echo "$(GREEN)==> Run tests $(RESET)" + bin/python -m pytest tests .PHONY: build-image build-image: ## Build Docker Image @@ -37,8 +43,13 @@ lint: build-image ## Lint code with existing image $(LINT) flake8 "${CODEBASE}" $(LINT) isort "${CODEBASE}" +.PHONY: lint-all +lint-all: build-image ## Lint code with existing image using configurations from pyproject.toml + @echo "Linting ${CODEBASE} $(IMAGE_NAME):latest" + $(LINT) + .PHONY: format format: build-image ## Format code with existing image @echo "Formatting ${CODEBASE} $(IMAGE_NAME):latest" - $(FORMAT) "${CODEBASE}" + $(FORMAT) sudo chown -R ${CURRENT_USER}: * diff --git a/docker-entrypoint.py b/docker-entrypoint.py index 4b2f33c..f26d69c 100755 --- a/docker-entrypoint.py +++ b/docker-entrypoint.py @@ -1,157 +1,22 @@ #!/usr/bin/env python3 from pathlib import Path +from plone_code_analysis.check import run_checks +from plone_code_analysis.format import run_formatters +from plone_code_analysis.logger import logger +from plone_code_analysis.settings import FORMATTERS +from plone_code_analysis.settings import parse_paths +from plone_code_analysis.settings import read_settings_from_file -import logging import os -import subprocess import sys -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger() - - -def run_command(cmd: str, args: list[str], paths: list[Path]) -> int: - """Run command using subprocess. - - :param cmd: Command to run. - :param args: Arguments to pass to the command. - :param paths: List of paths to use. - :returns: Status code. - """ - cmd = [ - cmd, - ] - all_args = cmd + args + paths - result = subprocess.run(all_args) - return result.returncode - - -def run_zpretty(cmd: str, args: list[str], paths: list[Path]) -> bool: - """Run zpretty. - - :param paths: List of paths to format. - :returns: Status code. - """ - raw_paths = [path for path in paths if path.is_dir()] - checks = { - "xml": ["-x", "-i"] + args, - "zcml": ["-z", "-i"] + args, - } - status = 0 - for ext, args in checks.items(): - for path in raw_paths: - if paths := list(path.glob(f"**/*.{ext}")): - result = run_command(cmd, args, paths) - status = status or result - return status - - -CHECKS = { - "black": [ - run_command, - [ - "black", - ["--check", "--diff"], - ], - ], - "flake8": [ - run_command, - [ - "flakeheaven", - ["lint"], - ], - ], - "isort": [ - run_command, - [ - "isort", - ["--check-only"], - ], - ], - "pyroma": [ - run_command, - [ - "pyroma", - ["-n", "10", "-d"], - ], - ], - "zpretty": [ - run_zpretty, - [ - "zpretty", - [ - "--check", - ], - ], - ], -} - - -FORMATTERS = { - "isort": [ - run_command, - [ - "isort", - [], - ], - ], - "black": [ - run_command, - [ - "black", - [], - ], - ], - "zpretty": [ - run_zpretty, - [ - "zpretty", - [], - ], - ], +ACTIONS = { + "check": run_checks, + "format": run_formatters, } -def parse_paths(value: str) -> list[Path]: - """Parse path information. - - :param value: String with path information. - :returns: List of valid paths. - """ - separator = "\n" if "\n" in value else " " - paths = [path for path in value.split(separator)] - return [Path(path) for path in paths if Path(path).exists()] - - -def main_check(tool: str, raw_paths: str) -> int: - """Run checks.""" - check = CHECKS.get(tool) - paths = parse_paths(raw_paths) - if not check: - logger.error(f"Please provide a valid check. Received {check}") - return 1 - elif not paths: - logger.error(f"Please provide a valid path. Parsed {raw_paths} to {paths}") - return 1 - func, args = check - return func(*args, paths=paths) - - -def main_format(raw_paths: str) -> int: - """Run code formatting.""" - status = 0 - paths = parse_paths(raw_paths) - if not (paths): - logger.error(f"Please provide a valid path. Parsed {raw_paths} to {paths}") - return 1 - for value in FORMATTERS.values(): - func, raw_args = value - cmd, args = raw_args - status = func(cmd, args, paths) or status - return status - - def change_working_dir(sub_dir: str): """Change the working directory. @@ -170,14 +35,32 @@ def change_working_dir(sub_dir: str): if base_dir and not (base_dir.startswith(".") or base_dir.startswith("/")): change_working_dir(base_dir) + settings = read_settings_from_file(Path("pyproject.toml")) + # Remove filename from arguments all_args = sys.argv[1:] + # Get the action to run: check or format action = all_args[0] - if action == "check": - sys.exit(main_check(all_args[1], all_args[2])) - elif action == "format": - sys.exit(main_format(all_args[1])) - else: + if action not in ACTIONS: logger.error("Please provide a valid action") sys.exit(1) + if len(all_args) == 1: + # We run all checks/formatters + tool = "" + paths = "" + elif len(all_args) == 2: + if action == "format" and all_args[1] not in FORMATTERS: + tool = "all" + paths = all_args[1] + else: + tool = all_args[1] + paths = "" + elif len(all_args) >= 3: + # Tool and many paths + tool = all_args[1] + paths = " ".join(all_args[2:]) + paths = parse_paths(paths) + status = ACTIONS[action](settings, tool, paths) + if status: + sys.exit(1) diff --git a/pyproject.toml b/pyproject.toml index 4fc94d9..f47b7fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,22 +2,6 @@ line-length = 88 target-version = ['py38'] include = '\.pyi?$' -exclude = ''' -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ -) -''' [tool.isort] profile = "black" @@ -36,3 +20,7 @@ max-complexity=25 pycodestyle = ["+*"] pyflakes = ["+*"] "flake8-*" = ["+*"] + +[tool.plone-code-analysis] +paths = "docker-entrypoint.py src/setup.py src/plone_code_analysis tests/fixtures/packages/ok tests/package tests/conftest.py" +paths_pyroma = "src/ tests/fixtures/packages/ok" diff --git a/requirements.txt b/requirements.txt index 1261e14..df1bae8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +src/ black==22.6.0 flake8-blind-except==0.2.1 flake8-debugger==4.1.2 diff --git a/src/plone_code_analysis/__init__.py b/src/plone_code_analysis/__init__.py new file mode 100644 index 0000000..e8d95f0 --- /dev/null +++ b/src/plone_code_analysis/__init__.py @@ -0,0 +1 @@ +"""Plone Code Analysis.""" \ No newline at end of file diff --git a/src/plone_code_analysis/check.py b/src/plone_code_analysis/check.py new file mode 100644 index 0000000..f117672 --- /dev/null +++ b/src/plone_code_analysis/check.py @@ -0,0 +1,74 @@ +from pathlib import Path +from plone_code_analysis.cmd import run_command +from plone_code_analysis.cmd import run_zpretty +from plone_code_analysis.logger import logger +from plone_code_analysis.settings import checks_from_settings + + +CHECKS = { + "black": [ + run_command, + [ + "black", + ["--check", "--diff"], + ], + ], + "flake8": [ + run_command, + [ + "flakeheaven", + ["lint"], + ], + ], + "isort": [ + run_command, + [ + "isort", + ["--check-only"], + ], + ], + "pyroma": [ + run_command, + [ + "pyroma", + ["-n", "10", "-d"], + ], + ], + "zpretty": [ + run_zpretty, + [ + "zpretty", + [ + "--check", + ], + ], + ], +} + + +def run_check(tool: str, paths: list[Path]) -> int: + """Run individual check on provided paths.""" + check = CHECKS.get(tool) + if not check: + logger.error(f"Please provide a valid check. Received {check}") + return 1 + elif not paths: + logger.error("Please provide a valid path.") + return 1 + func, args = check + return func(*args, paths=paths) + + +def run_checks(settings: dict, tool: str = "", paths: list[Path] = None) -> int: + """Run one or more checks.""" + status = 0 + all_checks = checks_from_settings(settings) + if tool and paths: + checks = {tool: paths} + elif tool: + checks = {tool: all_checks[tool]} + else: + checks = all_checks + for check, paths in checks.items(): + status = status or run_check(check, paths) + return status diff --git a/src/plone_code_analysis/cmd.py b/src/plone_code_analysis/cmd.py new file mode 100644 index 0000000..a4d5b28 --- /dev/null +++ b/src/plone_code_analysis/cmd.py @@ -0,0 +1,46 @@ +from pathlib import Path + +import os +import subprocess + + +def _parse_cmd(cmd: str) -> str: + if prefix := os.environ.get("PYTEST_BIN_DIR", ""): + cmd = f"{prefix}/{cmd}" + return cmd + + +def run_command(cmd: str, args: list[str], paths: list[Path]) -> int: + """Run command using subprocess. + + :param cmd: Command to run. + :param args: Arguments to pass to the command. + :param paths: List of paths to use. + :returns: Status code. + """ + cmd = [ + _parse_cmd(cmd), + ] + all_args = cmd + args + paths + result = subprocess.run(all_args) + return result.returncode + + +def run_zpretty(cmd: str, args: list[str], paths: list[Path]) -> bool: + """Run zpretty. + + :param paths: List of paths to format. + :returns: Status code. + """ + raw_paths = [path for path in paths if path.is_dir()] + checks = { + "xml": ["-x", "-i"] + args, + "zcml": ["-z", "-i"] + args, + } + status = 0 + for ext, args in checks.items(): + for path in raw_paths: + if paths := list(path.glob(f"**/*.{ext}")): + result = run_command(cmd, args, paths) + status = status or result + return status diff --git a/src/plone_code_analysis/format.py b/src/plone_code_analysis/format.py new file mode 100644 index 0000000..23b4a0a --- /dev/null +++ b/src/plone_code_analysis/format.py @@ -0,0 +1,58 @@ +from pathlib import Path +from plone_code_analysis.cmd import run_command +from plone_code_analysis.cmd import run_zpretty +from plone_code_analysis.logger import logger +from plone_code_analysis.settings import formatters_from_settings + + +FORMATTERS = { + "isort": [ + run_command, + [ + "isort", + [], + ], + ], + "black": [ + run_command, + [ + "black", + [], + ], + ], + "zpretty": [ + run_zpretty, + [ + "zpretty", + [], + ], + ], +} + + +def run_formatter(tool: str, paths: list[Path]) -> int: + """Run one formatter.""" + formatter = FORMATTERS.get(tool) + if not formatter: + logger.error(f"Please provide a valid formatter. Received {tool}") + return 1 + elif not paths: + logger.error("Please provide a valid path.") + return 1 + func, args = formatter + return func(*args, paths=paths) + + +def run_formatters(settings: dict, tool: str = "", paths: list[Path] = None) -> int: + """Run one or more formatters.""" + status = 0 + all_formatters = formatters_from_settings(settings) + if tool and paths: + formatters = {tool: paths} + elif tool: + formatters = {tool: all_formatters[tool]} + else: + formatters = all_formatters + for formatter, paths in formatters.items(): + status = status or run_formatter(formatter, paths) + return status diff --git a/src/plone_code_analysis/logger.py b/src/plone_code_analysis/logger.py new file mode 100644 index 0000000..18b0be7 --- /dev/null +++ b/src/plone_code_analysis/logger.py @@ -0,0 +1,5 @@ +import logging + + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("plone-code-analysis") diff --git a/src/plone_code_analysis/settings.py b/src/plone_code_analysis/settings.py new file mode 100644 index 0000000..2a999cb --- /dev/null +++ b/src/plone_code_analysis/settings.py @@ -0,0 +1,84 @@ +from copy import deepcopy +from pathlib import Path +from plone_code_analysis.logger import logger +from typing import Any + +import tomli + + +SECTION = "tool.plone-code-analysis" + + +CHECKERS = ["black", "flake8", "isort", "pyroma", "zpretty"] +FORMATTERS = ["black", "isort", "zpretty"] + + +DEFAULT_SETTINGS = { + "checkers": CHECKERS, + "formatters": FORMATTERS, + "paths": ".", + "paths_pyroma": ".", +} + + +def read_settings_from_file(path: Path) -> dict[str, Any]: + """Read settings from toml file. + + :param path: Path to the toml file with settings. + :returns: Dictionary with settings.. + """ + settings = deepcopy(DEFAULT_SETTINGS) + if not path.exists(): + logger.info("File not found: pyproject.toml") + return {} + config = tomli.loads(path.read_text()) + for key in SECTION.split("."): + config = config.get(key, {}) + settings.update(config) + # Parse paths into a list of paths + path_keys = [key for key in settings.keys() if key.startswith("paths")] + for key in path_keys: + value = settings.get(key, "") + settings[key] = value if isinstance(value, list) else parse_paths(value) + return settings + + +def parse_paths(value: str) -> list[Path]: + """Parse path information. + + :param value: String with path information. + :returns: List of valid paths. + """ + separator = "\n" if "\n" in value else " " + possible_paths = [Path(path) for path in value.split(separator)] + valid_paths = {path.resolve() for path in possible_paths if path.exists()} + return list(valid_paths) + + +def checks_from_settings(settings: dict) -> dict: + """Extract check information from settings. + + :param settings: Dictionary with settings information. + :returns: Dictionary with checks and paths. + """ + all_checks = settings.get("checkers", CHECKERS) + default_path = settings.get("paths", []) + checks = { + check: settings.get(f"paths_{check}", default_path) for check in all_checks + } + return checks + + +def formatters_from_settings(settings: dict) -> dict: + """Extract formatter information from settings. + + :param settings: Dictionary with settings information. + :returns: Dictionary with formatters and paths. + """ + all_formatters = settings.get("formatters", FORMATTERS) + default_path = settings.get("paths", []) + formatters = { + formatter: settings.get(f"paths_{formatter}", default_path) + for formatter in all_formatters + } + return formatters diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..4be2233 --- /dev/null +++ b/src/setup.py @@ -0,0 +1,35 @@ +"""Installer for the plone_code_analysis package.""" +from setuptools import setup + + +setup( + name="plone_code_analysis", + version="0.0.0", + description="Plone Code Analysis.", + author="Plone Community", + author_email="dev@plone.org", + url="https://github.com/plone/code-quality", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Plone :: 5.2", + "Framework :: Plone :: 6.0", + "Framework :: Plone :: Addon", + "Framework :: Plone", + "Framework :: Zope :: 5", + "Framework :: Zope", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python", + ], + keywords="plone python code-quality", + packages=["plone_code_analysis"], + package_dir={"plone_code_analysis": "plone_code_analysis"}, + include_package_data=True, + python_requires=">=3.7", + zip_safe=False, + install_requires=[], +) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1e0fc7b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,33 @@ +"""Pytest configuration.""" +from pathlib import Path + +import os +import pytest +import sys + + +@pytest.fixture(autouse=True) +def setup_python(): + current_python = sys.executable + os.environ["PYTEST_BIN_DIR"] = str(Path(current_python).parent) + + +@pytest.fixture(scope="session") +def cwd() -> Path: + """Working directory.""" + return Path(os.curdir).resolve() + + +@pytest.fixture(scope="session") +def fixtures_folder() -> Path: + """Fixtures folder.""" + return Path("tests/fixtures").resolve() + + +@pytest.fixture +def configs_folder(fixtures_folder, cwd) -> Path: + """Fixtures folder.""" + path = (fixtures_folder / "configs").resolve() + os.chdir(path) + yield path + os.chdir(cwd) diff --git a/tests/fixtures/configs/check_path.toml b/tests/fixtures/configs/check_path.toml new file mode 100644 index 0000000..d71f218 --- /dev/null +++ b/tests/fixtures/configs/check_path.toml @@ -0,0 +1,3 @@ +[tool.plone-code-analysis] +paths= "foo" +paths_isort= "setup.py" diff --git a/tests/fixtures/configs/checkers.toml b/tests/fixtures/configs/checkers.toml new file mode 100644 index 0000000..3c914c5 --- /dev/null +++ b/tests/fixtures/configs/checkers.toml @@ -0,0 +1,3 @@ +[tool.plone-code-analysis] +checkers=["black",] +formatters=["black",] diff --git a/tests/fixtures/configs/default.toml b/tests/fixtures/configs/default.toml new file mode 100644 index 0000000..83f2f0f --- /dev/null +++ b/tests/fixtures/configs/default.toml @@ -0,0 +1,22 @@ +[tool.black] +line-length = 88 +target-version = ['py38'] +include = '\.pyi?$' + +[tool.isort] +profile = "black" +force_alphabetical_sort = true +force_single_line = true +lines_after_imports = 2 +line_length = 120 + +[tool.flakeheaven] +format="grouped" +max_line_length=88 +show_source=true +max-complexity=25 + +[tool.flakeheaven.plugins] +pycodestyle = ["+*"] +pyflakes = ["+*"] +"flake8-*" = ["+*"] diff --git a/tests/fixtures/configs/foo/__init__.py b/tests/fixtures/configs/foo/__init__.py new file mode 100644 index 0000000..c0a7f9e --- /dev/null +++ b/tests/fixtures/configs/foo/__init__.py @@ -0,0 +1,5 @@ +"""Foo package""" +import logging + + +logger = logging.getLogger("logger") diff --git a/tests/fixtures/configs/foo/configure.zcml b/tests/fixtures/configs/foo/configure.zcml new file mode 100644 index 0000000..0819b4a --- /dev/null +++ b/tests/fixtures/configs/foo/configure.zcml @@ -0,0 +1,8 @@ + + + + + diff --git a/tests/fixtures/configs/setup.py b/tests/fixtures/configs/setup.py new file mode 100644 index 0000000..281a648 --- /dev/null +++ b/tests/fixtures/configs/setup.py @@ -0,0 +1,37 @@ +"""Installer for the foo package.""" +from setuptools import setup + + +setup( + name="foo", + version="1.0.0", + description="A package to be used to test code quality tool", + long_description="A package to be used to test code quality tool", + long_description_content_type="text/markdown", + author="Plone Community", + author_email="dev@plone.org", + url="https://github.com/plone/code-quality", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Plone :: 5.2", + "Framework :: Plone :: 6.0", + "Framework :: Plone :: Addon", + "Framework :: Plone", + "Framework :: Zope :: 5", + "Framework :: Zope", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python", + ], + keywords="plone python code-quality", + packages=["foo"], + package_dir={"foo": "foo"}, + include_package_data=True, + python_requires=">=3.7", + zip_safe=False, + install_requires=[], +) diff --git a/tests/fixtures/packages/not_ok/bar/__init__.py b/tests/fixtures/packages/not_ok/bar/__init__.py new file mode 100644 index 0000000..c0a7f9e --- /dev/null +++ b/tests/fixtures/packages/not_ok/bar/__init__.py @@ -0,0 +1,5 @@ +"""Foo package""" +import logging + + +logger = logging.getLogger("logger") diff --git a/tests/fixtures/packages/not_ok/bar/configure.zcml b/tests/fixtures/packages/not_ok/bar/configure.zcml new file mode 100644 index 0000000..58df73d --- /dev/null +++ b/tests/fixtures/packages/not_ok/bar/configure.zcml @@ -0,0 +1,6 @@ + + + + + diff --git a/tests/fixtures/packages/not_ok/pyproject.toml b/tests/fixtures/packages/not_ok/pyproject.toml new file mode 100644 index 0000000..1001596 --- /dev/null +++ b/tests/fixtures/packages/not_ok/pyproject.toml @@ -0,0 +1,25 @@ +[tool.black] +line-length = 88 +target-version = ['py38'] +include = '\.pyi?$' + +[tool.isort] +profile = "black" +force_alphabetical_sort = true +force_single_line = true +lines_after_imports = 2 +line_length = 120 + +[tool.flakeheaven] +format="grouped" +max_line_length=88 +show_source=true +max-complexity=25 + +[tool.flakeheaven.plugins] +pycodestyle = ["+*"] +pyflakes = ["+*"] +"flake8-*" = ["+*"] + +[tool.plone-code-analysis] +paths= "bar setup.py" diff --git a/tests/fixtures/packages/not_ok/setup.py b/tests/fixtures/packages/not_ok/setup.py new file mode 100644 index 0000000..71bcb4d --- /dev/null +++ b/tests/fixtures/packages/not_ok/setup.py @@ -0,0 +1,19 @@ +"""Installer for the foo package.""" +from setuptools import setup + + + +setup( + name="bar", + version="1.0.0", + description="A package to be used to test code quality tool", + long_description="", + long_description_content_type="text/markdown", + keywords="plone python code-quality", + packages=["bar"], + package_dir={"bar": "bar"}, + include_package_data=True, + python_requires=">=3.7", + zip_safe=False, + install_requires=[], +) diff --git a/tests/fixtures/packages/ok/README.md b/tests/fixtures/packages/ok/README.md new file mode 100644 index 0000000..9940f61 --- /dev/null +++ b/tests/fixtures/packages/ok/README.md @@ -0,0 +1,41 @@ +

+ + Plone Code Quality tool + +

+ +

+ Plone Code Quality tool +

+ +
+ +[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/plone/code-quality)](https://hub.docker.com/r/plone/code-quality) +![GitHub Repo stars](https://img.shields.io/github/stars/plone/code-quality?style=flat-square) +[![license badge](https://img.shields.io/github/license/plone/code-quality)](./LICENSE) + +
+ +## Usage + +First, go to the repository you want to check. + +## Contribute + +- [Issue Tracker](https://github.com/plone/code-quality/issues) +- [Source Code](https://github.com/plone/code-quality/) +- [Documentation](https://github.com/plone/code-quality/) + +Please **DO NOT** commit to version branches directly. Even for the smallest and most trivial fix. + +**ALWAYS** open a pull request and ask somebody else to merge your code. **NEVER** merge it yourself. + +## Credits + +Based on a solution originally developed by [kitconcept GmbH](https://kitconcept.com). + +[![Plone Foundation](https://raw.githubusercontent.com/plone/.github/main/plone-foundation.png)](https://plone.org/) + +## License + +The project is licensed under the GPLv2. diff --git a/tests/fixtures/packages/ok/foo/__init__.py b/tests/fixtures/packages/ok/foo/__init__.py new file mode 100644 index 0000000..c0a7f9e --- /dev/null +++ b/tests/fixtures/packages/ok/foo/__init__.py @@ -0,0 +1,5 @@ +"""Foo package""" +import logging + + +logger = logging.getLogger("logger") diff --git a/tests/fixtures/packages/ok/foo/configure.zcml b/tests/fixtures/packages/ok/foo/configure.zcml new file mode 100644 index 0000000..0819b4a --- /dev/null +++ b/tests/fixtures/packages/ok/foo/configure.zcml @@ -0,0 +1,8 @@ + + + + + diff --git a/tests/fixtures/packages/ok/pyproject.toml b/tests/fixtures/packages/ok/pyproject.toml new file mode 100644 index 0000000..1001596 --- /dev/null +++ b/tests/fixtures/packages/ok/pyproject.toml @@ -0,0 +1,25 @@ +[tool.black] +line-length = 88 +target-version = ['py38'] +include = '\.pyi?$' + +[tool.isort] +profile = "black" +force_alphabetical_sort = true +force_single_line = true +lines_after_imports = 2 +line_length = 120 + +[tool.flakeheaven] +format="grouped" +max_line_length=88 +show_source=true +max-complexity=25 + +[tool.flakeheaven.plugins] +pycodestyle = ["+*"] +pyflakes = ["+*"] +"flake8-*" = ["+*"] + +[tool.plone-code-analysis] +paths= "bar setup.py" diff --git a/tests/fixtures/packages/ok/setup.py b/tests/fixtures/packages/ok/setup.py new file mode 100644 index 0000000..08ea47d --- /dev/null +++ b/tests/fixtures/packages/ok/setup.py @@ -0,0 +1,43 @@ +"""Installer for the foo package.""" +from pathlib import Path +from setuptools import setup + + +long_description = f""" +{Path("README.md").read_text()}\n +""" + + +setup( + name="foo", + version="1.0.0", + description="A package to be used to test code quality tool", + long_description=long_description, + long_description_content_type="text/markdown", + author="Plone Community", + author_email="dev@plone.org", + url="https://github.com/plone/code-quality", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Plone :: 5.2", + "Framework :: Plone :: 6.0", + "Framework :: Plone :: Addon", + "Framework :: Plone", + "Framework :: Zope :: 5", + "Framework :: Zope", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python", + ], + keywords="plone python code-quality", + packages=["foo"], + package_dir={"foo": "foo"}, + include_package_data=True, + python_requires=">=3.7", + zip_safe=False, + install_requires=[], +) diff --git a/tests/package/test_check.py b/tests/package/test_check.py new file mode 100644 index 0000000..df05165 --- /dev/null +++ b/tests/package/test_check.py @@ -0,0 +1,119 @@ +"""Test plone_code_analysis.settings.""" +from pathlib import Path +from plone_code_analysis import check +from plone_code_analysis import settings + +import os +import pytest + + +PCK_OK = Path("tests/fixtures/packages/ok").resolve() +PCK_NOK = Path("tests/fixtures/packages/not_ok").resolve() + + +@pytest.fixture +def package_ok(cwd) -> Path: + """package folder.""" + os.chdir(PCK_OK) + yield PCK_OK + os.chdir(cwd) + + +@pytest.fixture +def package_nok(cwd) -> Path: + """package folder.""" + os.chdir(PCK_NOK) + yield PCK_NOK + os.chdir(cwd) + + +@pytest.mark.parametrize( + "tool,paths,expected", + [ + [ + "black", + [ + PCK_OK / "setup.py", + ], + 0, + ], + [ + "flake8", + [ + PCK_OK / "setup.py", + ], + 0, + ], + [ + "isort", + [ + PCK_OK / "setup.py", + ], + 0, + ], + [ + "pyroma", + [ + PCK_OK, + ], + 0, + ], + [ + "zpretty", + [ + PCK_OK / "foo", + ], + 0, + ], + [ + "black", + [ + PCK_NOK / "setup.py", + ], + 1, + ], + [ + "flake8", + [ + PCK_NOK / "setup.py", + ], + 1, + ], + [ + "isort", + [ + PCK_NOK / "setup.py", + ], + 1, + ], + [ + "pyroma", + [ + PCK_NOK, + ], + 2, + ], + [ + "zpretty", + [ + PCK_NOK / "bar", + ], + 1, + ], + ], +) +def test_run_check(tool, paths, expected): + """Check check.run_check.""" + assert check.run_check(tool, paths) == expected + + +def test_run_checks_pass(package_ok): + """Check check.run_checks.""" + config = settings.read_settings_from_file(package_ok / "pyproject.toml") + assert check.run_checks(config) == 0 + + +def test_run_checks_fail(package_nok): + """Check check.run_checks.""" + config = settings.read_settings_from_file(package_nok / "pyproject.toml") + assert check.run_checks(config) == 1 diff --git a/tests/package/test_settings.py b/tests/package/test_settings.py new file mode 100644 index 0000000..1f3687c --- /dev/null +++ b/tests/package/test_settings.py @@ -0,0 +1,229 @@ +"""Test plone_code_analysis.settings.""" +from pathlib import Path +from plone_code_analysis import settings + +import pytest + + +CONFIGS_PATH = Path("tests/fixtures/configs").resolve() + + +@pytest.mark.parametrize( + "value,elements", + [ + ["", 1], + [" ", 1], + [".", 1], + ["setup.py foo", 2], + ["setup.py 404", 1], + ["404", 0], + ["setup.py foo setup.py foo", 2], + ], +) +def test_parse_paths(configs_folder, value, elements): + result = settings.parse_paths(value) + assert len(result) == elements + if elements: + assert isinstance(result, list) + assert isinstance(result[0], Path) + + +@pytest.mark.parametrize( + "filename,key,expected", + [ + [ + "check_path", + "paths_isort", + [ + CONFIGS_PATH / "setup.py", + ], + ], + [ + "check_path", + "paths_pyroma", + [ + CONFIGS_PATH, + ], + ], + [ + "check_path", + "paths", + [ + CONFIGS_PATH / "foo", + ], + ], + ["check_path", "checkers", settings.CHECKERS], + ["check_path", "formatters", settings.FORMATTERS], + [ + "checkers", + "checkers", + [ + "black", + ], + ], + [ + "checkers", + "formatters", + [ + "black", + ], + ], + ["default", "paths_isort", None], + [ + "default", + "paths_pyroma", + [ + CONFIGS_PATH, + ], + ], + [ + "default", + "paths", + [ + CONFIGS_PATH, + ], + ], + ["default", "checkers", settings.CHECKERS], + ["default", "formatters", settings.FORMATTERS], + ], +) +def test_default_settings(configs_folder, filename, key, expected): + """Check with default values""" + config = settings.read_settings_from_file(Path(f"{filename}.toml")) + assert config.get(key) == expected + + +@pytest.mark.parametrize( + "filename,check,paths", + [ + [ + "check_path", + "black", + [ + CONFIGS_PATH / "foo", + ], + ], + [ + "check_path", + "isort", + [ + CONFIGS_PATH / "setup.py", + ], + ], + [ + "check_path", + "pyroma", + [ + CONFIGS_PATH, + ], + ], + [ + "check_path", + "zpretty", + [ + CONFIGS_PATH / "foo", + ], + ], + [ + "checkers", + "black", + [ + CONFIGS_PATH, + ], + ], + [ + "default", + "black", + [ + CONFIGS_PATH, + ], + ], + [ + "default", + "isort", + [ + CONFIGS_PATH, + ], + ], + [ + "default", + "pyroma", + [ + CONFIGS_PATH, + ], + ], + [ + "default", + "zpretty", + [ + CONFIGS_PATH, + ], + ], + ], +) +def test_checks_from_settings(configs_folder, filename, check, paths): + """Check with default values""" + config = settings.read_settings_from_file(Path(f"{filename}.toml")) + checks = settings.checks_from_settings(config) + assert checks[check] == paths + + +@pytest.mark.parametrize( + "filename,formatter,paths", + [ + [ + "check_path", + "black", + [ + CONFIGS_PATH / "foo", + ], + ], + [ + "check_path", + "isort", + [ + CONFIGS_PATH / "setup.py", + ], + ], + [ + "check_path", + "zpretty", + [ + CONFIGS_PATH / "foo", + ], + ], + [ + "checkers", + "black", + [ + CONFIGS_PATH, + ], + ], + [ + "default", + "black", + [ + CONFIGS_PATH, + ], + ], + [ + "default", + "isort", + [ + CONFIGS_PATH, + ], + ], + [ + "default", + "zpretty", + [ + CONFIGS_PATH, + ], + ], + ], +) +def test_formatters_from_settings(configs_folder, filename, formatter, paths): + """Check with default values""" + config = settings.read_settings_from_file(Path(f"{filename}.toml")) + formatters = settings.formatters_from_settings(config) + assert formatters[formatter] == paths diff --git a/tests/requirements.txt b/tests/requirements.txt index 55b033e..795916c 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1 +1,2 @@ +-e "src/" pytest \ No newline at end of file From 22ef7abdb37fbfbb9bea4d4dcc4cac253a3d72c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Thu, 4 Aug 2022 15:34:53 -0300 Subject: [PATCH 03/10] Update README. --- README.md | 176 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 127 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 96192fe..7428a77 100644 --- a/README.md +++ b/README.md @@ -16,53 +16,51 @@ -## Usage - -First, go to the repository you want to check. +## Configuration +This tool looks for configuration in a `pyproject.toml`file in the root of the codebase being analysed. -### [black](https://black.readthedocs.io/en/stable/) +The default configuration values are: -Check **src** directory and **setup.py** file with **black** -```bash -docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check black src setup.py +```toml +[tool.plone-code-analysis] +checkers = ["black", "flake8", "isort", "pyroma", "zpretty"] +formatters = ["black", "isort", "zpretty"] +paths = "." +paths_pyroma = "." +paths_black = "." +paths_flake8 = "." +paths_isort = "." +paths_pyroma = "." +paths_zpretty = "." ``` -### [flake8](https://flake8.pycqa.org/en/stable/) +If you want to change only the `paths`, you should add to your `pyproject.toml` the following settings: -Flake8 checks, using [flakeheaven](https://pypi.org/project/flakeheaven/) configuration format. - -Current plugins in use: - -* [flake8-blind-except](https://pypi.org/project/flake8-blind-except/) -* [flake8-debugger](https://pypi.org/project/flake8-debugger/) -* [flake8-print](https://pypi.org/project/flake8-print/) - -Check **src** directory and **setup.py** file with **flake8** - -```bash -docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check flake8 src setup.py +```toml +[tool.plone-code-analysis] +paths = "src/ setup.py" ``` +Also, it is possible to change the paths used for individual tools: -### [isort](https://pycqa.github.io/isort/) - -Check **src** directory and **setup.py** file with **isort** - -```bash -docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check isort src setup.py +```toml +[tool.plone-code-analysis] +paths_black = "src/ tests/ setup.py" +paths_flake8 = "src/ setup.py" ``` -### [zpretty](https://pypi.org/project/zpretty/) +Or explicitly set `checkers` or `formatters` to be used: -Check **src** directory with **zpretty** - -```bash -docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check zpretty src +```toml +[tool.plone-code-analysis] +checkers = ["black", "flake8", "isort", "pyroma", ] +formatters = ["black", "isort",] ``` -## Configuration -To configure black, flake8 (via flakeheaven) and isort, make sure you have a pyproject.toml in the root of the directory you are mounting. +### Tools configuration + +To configure black, flake8 (via flakeheaven) and isort, also use the `pyproject.toml` file in the root of the directory you are mounting. An example configuration, used by this image, follows: @@ -71,22 +69,6 @@ An example configuration, used by this image, follows: line-length = 88 target-version = ['py38'] include = '\.pyi?$' -exclude = ''' -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ -) -''' [tool.isort] profile = "black" @@ -105,6 +87,102 @@ max-complexity=25 pycodestyle = ["+*"] pyflakes = ["+*"] "flake8-*" = ["+*"] + +[tool.plone-code-analysis] +paths = "docker-entrypoint.py src/setup.py src/plone_code_analysis tests/fixtures/packages/ok tests/package tests/conftest.py" +paths_pyroma = "src/ tests/fixtures/packages/ok" +``` + +## Usage + +First, go to the repository you want to check or format. + +### Run all Checks + +Using the configuration available in `pyproject.toml`, run: + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check +``` + +### Check with [black](https://black.readthedocs.io/en/stable/) + +Check with `pyproject.toml` settings: + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check black +``` + +Explicitly check **src** directory and **setup.py** file. + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check black src setup.py +``` + +### Check with [flake8](https://flake8.pycqa.org/en/stable/) + +Flake8 checks, using [flakeheaven](https://pypi.org/project/flakeheaven/) configuration format. + +Current plugins in use: + +* [flake8-blind-except](https://pypi.org/project/flake8-blind-except/) +* [flake8-debugger](https://pypi.org/project/flake8-debugger/) +* [flake8-print](https://pypi.org/project/flake8-print/) + +Check with `pyproject.toml` settings: + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check flake8 +``` + +Explicitly check **src** directory and **setup.py** file. + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check flake8 src setup.py +``` + +### Check with [isort](https://pycqa.github.io/isort/) + +Check with `pyproject.toml` settings: + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check isort +``` + +Explicitly check **src** directory and **setup.py** file. + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check isort src setup.py +``` + + +### Check with [pyroma](https://pycqa.github.io/pyroma/) + +Check with `pyproject.toml` settings: + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check pyroma +``` + +Explicitly check **src/mypackage** directory . + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check pyroma src/mypackage +``` + +### Check with [zpretty](https://pypi.org/project/zpretty/) + +Check with `pyproject.toml` settings: + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check zpretty +``` + +Explicitly check **src** directory . + +```bash +docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check zpretty src + ``` ## Contribute From e3c5da4627452b7fa566376f86542ca1531b87e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 5 Aug 2022 12:55:38 -0300 Subject: [PATCH 04/10] Implement debug options to the image --- .github/workflows/release.yml | 2 - Makefile | 7 ++- README.md | 37 ++++++++++++ docker-entrypoint.py | 9 +++ src/README.md | 41 ++++++++++++++ src/plone_code_analysis/__init__.py | 2 +- src/plone_code_analysis/check.py | 3 +- src/plone_code_analysis/cmd.py | 12 +++- src/plone_code_analysis/format.py | 3 +- src/plone_code_analysis/logger.py | 88 ++++++++++++++++++++++++++++- src/plone_code_analysis/settings.py | 6 +- src/setup.py | 6 ++ 12 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 src/README.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc51d17..b2c67e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,8 +56,6 @@ jobs: - name: Test image run: | docker run --rm -v "${PWD}":/github/workspace ${{ env.IMAGE_NAME }}:${{ env.TEST_TAG }} check black ${{ env.TEST_PATHS }} - docker run --rm -v "${PWD}":/github/workspace ${{ env.IMAGE_NAME }}:${{ env.TEST_TAG }} check flake8 ${{ env.TEST_PATHS }} - docker run --rm -v "${PWD}":/github/workspace ${{ env.IMAGE_NAME }}:${{ env.TEST_TAG }} check isort ${{ env.TEST_PATHS }} - name: Login to DockerHub uses: docker/login-action@v1 diff --git a/Makefile b/Makefile index fa81210..bfc52ff 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ IMAGE_NAME=plone/code-quality DOCKERFILE=Dockerfile +ifndef LOG_LEVEL + LOG_LEVEL=INFO +endif CODEBASE=docker-entrypoint.py src/setup.py src/plone_code_analysis tests/fixtures/packages/ok tests/package tests/conftest.py -LINT=docker run --rm -v "${PWD}":/github/workspace "${IMAGE_NAME}:latest" check -FORMAT=docker run --rm -v "${PWD}":/github/workspace "${IMAGE_NAME}:latest" format +LINT=docker run -e LOG_LEVEL="${LOG_LEVEL}" --rm -v "${PWD}":/github/workspace "${IMAGE_NAME}:latest" check +FORMAT=docker run -e LOG_LEVEL="${LOG_LEVEL}" --rm -v "${PWD}":/github/workspace "${IMAGE_NAME}:latest" format CURRENT_USER=$$(whoami) # Add the following 'help' target to your Makefile diff --git a/README.md b/README.md index 7428a77..07b38dd 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,43 @@ Please **DO NOT** commit to version branches directly. Even for the smallest and **ALWAYS** open a pull request and ask somebody else to merge your code. **NEVER** merge it yourself. + +### Running linters on this codebase + +Use this tool to lint its own codebase: + +```shell +make lint-all +``` + +You can also increase the verbosity with: + +```shell +LOG_LEVEL=DEBUG make lint-all +``` +### Running formatters on this codebase + +Use this tool to format its own codebase: + +```shell +make format +``` + +You can also increase the verbosity with: + +```shell +LOG_LEVEL=DEBUG make format +``` + + +### Running local tests + +Tests are implemented with `pytest` and can be run with: + +```shell +make test +``` + ## Credits Based on a solution originally developed by [kitconcept GmbH](https://kitconcept.com). diff --git a/docker-entrypoint.py b/docker-entrypoint.py index f26d69c..274b0e3 100755 --- a/docker-entrypoint.py +++ b/docker-entrypoint.py @@ -26,11 +26,15 @@ def change_working_dir(sub_dir: str): new_dir = current_path / sub_dir if new_dir.exists() and new_dir.is_dir(): os.chdir(new_dir) + logger.debug(f"Changed base directory to {new_dir}") + else: + logger.debug(f"Failed to changed base directory to {sub_dir}") if __name__ == "__main__": """Run code-quality.""" # Change working directory, if needed + logger.info("Starting plone-code-analysis", header=1) base_dir = os.environ.get("BASE_DIR", "") if base_dir and not (base_dir.startswith(".") or base_dir.startswith("/")): change_working_dir(base_dir) @@ -61,6 +65,11 @@ def change_working_dir(sub_dir: str): tool = all_args[1] paths = " ".join(all_args[2:]) paths = parse_paths(paths) + logger.debug(f"Provided paths {paths}") status = ACTIONS[action](settings, tool, paths) + + logger.info("Results", header=1) if status: + logger.error("There were errors") sys.exit(1) + logger.success("Done") diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..9940f61 --- /dev/null +++ b/src/README.md @@ -0,0 +1,41 @@ +

+ + Plone Code Quality tool + +

+ +

+ Plone Code Quality tool +

+ +
+ +[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/plone/code-quality)](https://hub.docker.com/r/plone/code-quality) +![GitHub Repo stars](https://img.shields.io/github/stars/plone/code-quality?style=flat-square) +[![license badge](https://img.shields.io/github/license/plone/code-quality)](./LICENSE) + +
+ +## Usage + +First, go to the repository you want to check. + +## Contribute + +- [Issue Tracker](https://github.com/plone/code-quality/issues) +- [Source Code](https://github.com/plone/code-quality/) +- [Documentation](https://github.com/plone/code-quality/) + +Please **DO NOT** commit to version branches directly. Even for the smallest and most trivial fix. + +**ALWAYS** open a pull request and ask somebody else to merge your code. **NEVER** merge it yourself. + +## Credits + +Based on a solution originally developed by [kitconcept GmbH](https://kitconcept.com). + +[![Plone Foundation](https://raw.githubusercontent.com/plone/.github/main/plone-foundation.png)](https://plone.org/) + +## License + +The project is licensed under the GPLv2. diff --git a/src/plone_code_analysis/__init__.py b/src/plone_code_analysis/__init__.py index e8d95f0..357601b 100644 --- a/src/plone_code_analysis/__init__.py +++ b/src/plone_code_analysis/__init__.py @@ -1 +1 @@ -"""Plone Code Analysis.""" \ No newline at end of file +"""Plone Code Analysis.""" diff --git a/src/plone_code_analysis/check.py b/src/plone_code_analysis/check.py index f117672..2bab217 100644 --- a/src/plone_code_analysis/check.py +++ b/src/plone_code_analysis/check.py @@ -70,5 +70,6 @@ def run_checks(settings: dict, tool: str = "", paths: list[Path] = None) -> int: else: checks = all_checks for check, paths in checks.items(): - status = status or run_check(check, paths) + logger.info(f"Running check: {check}", header=2) + status = run_check(check, paths) or status return status diff --git a/src/plone_code_analysis/cmd.py b/src/plone_code_analysis/cmd.py index a4d5b28..59fa37f 100644 --- a/src/plone_code_analysis/cmd.py +++ b/src/plone_code_analysis/cmd.py @@ -1,4 +1,5 @@ from pathlib import Path +from plone_code_analysis.logger import logger import os import subprocess @@ -22,8 +23,15 @@ def run_command(cmd: str, args: list[str], paths: list[Path]) -> int: _parse_cmd(cmd), ] all_args = cmd + args + paths - result = subprocess.run(all_args) - return result.returncode + logger.debug(f"Running command {all_args}") + proc = subprocess.run(all_args, capture_output=True) + code = proc.returncode + if code: + msg = proc.stdout.decode() + logger.error(msg, fancy=False) + else: + logger.success("All good!") + return code def run_zpretty(cmd: str, args: list[str], paths: list[Path]) -> bool: diff --git a/src/plone_code_analysis/format.py b/src/plone_code_analysis/format.py index 23b4a0a..87f76e1 100644 --- a/src/plone_code_analysis/format.py +++ b/src/plone_code_analysis/format.py @@ -54,5 +54,6 @@ def run_formatters(settings: dict, tool: str = "", paths: list[Path] = None) -> else: formatters = all_formatters for formatter, paths in formatters.items(): - status = status or run_formatter(formatter, paths) + logger.info(f"Running formatter: {formatter}", header=2) + status = run_formatter(formatter, paths) or status return status diff --git a/src/plone_code_analysis/logger.py b/src/plone_code_analysis/logger.py index 18b0be7..8e3902d 100644 --- a/src/plone_code_analysis/logger.py +++ b/src/plone_code_analysis/logger.py @@ -1,5 +1,89 @@ import logging +import os +import re -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("plone-code-analysis") +LOG_LEVEL = { + "INFO": logging.INFO, + "DEBUG": logging.DEBUG, + "WARNING": logging.WARNING, +} + + +level = LOG_LEVEL.get(os.environ.get("LOG_LEVEL"), LOG_LEVEL["INFO"]) + +logging.basicConfig(level=level, format="%(message)s") + + +PREFIXES = { + "debug": "\x1b[1;33m", + "warning": "\x1b[1;33m", + "info": "\x1b[1;34m", + "hint": "\x1b[3;35m", + "success": "\x1b[1;32m", + "error": "\x1b[1;31m", +} + + +class FancyLogger: + + TERMINATOR = "\x1b[0m" + HEADER_DELIMITER = "=" + REPLACEMENTS = ( + (r"\/github\/workspace\/", ""), + (r"PosixPath\('([^']*)'\)", r"\1"), + ) + + def __init__(self) -> None: + self.logger = logging.getLogger("plone-code-analysis") + + def __getattr__(self, attr): + """Catch all method.""" + return getattr(self.logger, attr) + + def _redact_msg(self, msg: str) -> str: + """Clean up msg to be logged.""" + for pattern, replace in self.REPLACEMENTS: + msg = re.sub(pattern, replace, msg) + return msg + + def _log(self, level, msg, *args, **kwargs): + fancy = kwargs.get("fancy", True) + header_level = kwargs.get("header", None) + kwargs = {k: v for k, v in kwargs.items() if k not in ("fancy", "header")} + msg = self._redact_msg(msg) + if fancy: + if header_level: + msg_len = len(msg) + header = self.HEADER_DELIMITER * msg_len + msg = f"{msg}\n{header}" + msg = f"{header}\n{msg}" if header_level == 1 else msg + msg = f"\n{msg}" + prefix = PREFIXES.get(level, PREFIXES["info"]) + msg = f"{prefix}{msg}{self.TERMINATOR}" + + method = getattr(self.logger, level, getattr(self.logger, "info")) + method(msg, *args, **kwargs) + + def success(self, msg, *args, **kwargs): + """Info""" + self._log("success", msg, *args, **kwargs) + + def info(self, msg, *args, **kwargs): + """Info""" + self._log("info", msg, *args, **kwargs) + + def debug(self, msg, *args, **kwargs): + """Debug""" + self._log("debug", msg, *args, **kwargs) + + def error(self, msg, *args, **kwargs): + """Error""" + self._log("error", msg, *args, **kwargs) + + def warning(self, msg, *args, **kwargs): + """Warning""" + self._log("warning", msg, *args, **kwargs) + + +logger = FancyLogger() diff --git a/src/plone_code_analysis/settings.py b/src/plone_code_analysis/settings.py index 2a999cb..f6d6e31 100644 --- a/src/plone_code_analysis/settings.py +++ b/src/plone_code_analysis/settings.py @@ -51,8 +51,8 @@ def parse_paths(value: str) -> list[Path]: """ separator = "\n" if "\n" in value else " " possible_paths = [Path(path) for path in value.split(separator)] - valid_paths = {path.resolve() for path in possible_paths if path.exists()} - return list(valid_paths) + valid_paths = list({path.resolve() for path in possible_paths if path.exists()}) + return valid_paths def checks_from_settings(settings: dict) -> dict: @@ -66,6 +66,7 @@ def checks_from_settings(settings: dict) -> dict: checks = { check: settings.get(f"paths_{check}", default_path) for check in all_checks } + logger.debug(f"Settings: {checks}") return checks @@ -81,4 +82,5 @@ def formatters_from_settings(settings: dict) -> dict: formatter: settings.get(f"paths_{formatter}", default_path) for formatter in all_formatters } + logger.debug(f"Settings: {formatters}") return formatters diff --git a/src/setup.py b/src/setup.py index 4be2233..49acec7 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,11 +1,17 @@ """Installer for the plone_code_analysis package.""" +from pathlib import Path from setuptools import setup +long_description = Path("README.md").read_text() + + setup( name="plone_code_analysis", version="0.0.0", description="Plone Code Analysis.", + long_description=long_description, + long_description_content_type="text/markdown", author="Plone Community", author_email="dev@plone.org", url="https://github.com/plone/code-quality", From 5e86855feeeb0bab40a1a579ce9322836b90742b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 5 Aug 2022 14:38:20 -0300 Subject: [PATCH 05/10] Change Dockerfile layer order --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 96d2af4..b7f03bf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,10 +17,10 @@ LABEL maintainer="Plone Community " \ org.label-schema.vendor="Plone Foundation" \ org.label-schema.docker.cmd="docker run -rm -v "${PWD}":/github/workspace plone/code-quality check black src" -COPY docker-entrypoint.py / -COPY --from=builder /wheelhouse /wheelhouse -RUN pip install --force-reinstall --no-index --no-deps /wheelhouse/* - WORKDIR /github/workspace ENTRYPOINT [ "/docker-entrypoint.py" ] + +COPY docker-entrypoint.py / +COPY --from=builder /wheelhouse /wheelhouse +RUN pip install --force-reinstall --no-index --no-deps /wheelhouse/* From 5955ca1d89632bf4cf5d3fcefd749bc371c60c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 5 Aug 2022 14:42:52 -0300 Subject: [PATCH 06/10] Improve debugging for settings file detection. --- docker-entrypoint.py | 3 ++- src/plone_code_analysis/settings.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docker-entrypoint.py b/docker-entrypoint.py index 274b0e3..c8e99da 100755 --- a/docker-entrypoint.py +++ b/docker-entrypoint.py @@ -39,7 +39,8 @@ def change_working_dir(sub_dir: str): if base_dir and not (base_dir.startswith(".") or base_dir.startswith("/")): change_working_dir(base_dir) - settings = read_settings_from_file(Path("pyproject.toml")) + settings_file = Path("pyproject.toml").resolve() + settings = read_settings_from_file(settings_file) # Remove filename from arguments all_args = sys.argv[1:] diff --git a/src/plone_code_analysis/settings.py b/src/plone_code_analysis/settings.py index f6d6e31..84629e9 100644 --- a/src/plone_code_analysis/settings.py +++ b/src/plone_code_analysis/settings.py @@ -29,8 +29,8 @@ def read_settings_from_file(path: Path) -> dict[str, Any]: """ settings = deepcopy(DEFAULT_SETTINGS) if not path.exists(): - logger.info("File not found: pyproject.toml") - return {} + logger.debug(f"Settings: {path} not found, using default settings.") + return settings config = tomli.loads(path.read_text()) for key in SECTION.split("."): config = config.get(key, {}) From 6e949c790241527344998f6327725d8e88ba26e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 5 Aug 2022 15:41:31 -0300 Subject: [PATCH 07/10] Improve paths detections in docker-entrypoint.py --- docker-entrypoint.py | 6 +++--- src/plone_code_analysis/logger.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-entrypoint.py b/docker-entrypoint.py index c8e99da..f1a75ea 100755 --- a/docker-entrypoint.py +++ b/docker-entrypoint.py @@ -57,15 +57,15 @@ def change_working_dir(sub_dir: str): elif len(all_args) == 2: if action == "format" and all_args[1] not in FORMATTERS: tool = "all" - paths = all_args[1] + paths = all_args[1].strip() else: tool = all_args[1] paths = "" elif len(all_args) >= 3: # Tool and many paths tool = all_args[1] - paths = " ".join(all_args[2:]) - paths = parse_paths(paths) + paths = " ".join(all_args[2:]).strip() + paths = parse_paths(paths) if paths else [] logger.debug(f"Provided paths {paths}") status = ACTIONS[action](settings, tool, paths) diff --git a/src/plone_code_analysis/logger.py b/src/plone_code_analysis/logger.py index 8e3902d..1be7e2f 100644 --- a/src/plone_code_analysis/logger.py +++ b/src/plone_code_analysis/logger.py @@ -30,7 +30,7 @@ class FancyLogger: TERMINATOR = "\x1b[0m" HEADER_DELIMITER = "=" REPLACEMENTS = ( - (r"\/github\/workspace\/", ""), + (r"\/github\/workspace\/?", "./"), (r"PosixPath\('([^']*)'\)", r"\1"), ) From 65f4d691d22b3265c42ad9905cc31a35573aef37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 5 Aug 2022 16:40:12 -0300 Subject: [PATCH 08/10] Small refactoring --- docker-entrypoint.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-entrypoint.py b/docker-entrypoint.py index f1a75ea..358da6c 100755 --- a/docker-entrypoint.py +++ b/docker-entrypoint.py @@ -57,15 +57,15 @@ def change_working_dir(sub_dir: str): elif len(all_args) == 2: if action == "format" and all_args[1] not in FORMATTERS: tool = "all" - paths = all_args[1].strip() + paths = all_args[1] else: tool = all_args[1] paths = "" elif len(all_args) >= 3: # Tool and many paths tool = all_args[1] - paths = " ".join(all_args[2:]).strip() - paths = parse_paths(paths) if paths else [] + paths = " ".join(all_args[2:]) + paths = parse_paths(paths.strip()) if paths else [] logger.debug(f"Provided paths {paths}") status = ACTIONS[action](settings, tool, paths) From 554be2cb0e217308b1402459fe58b1b4e1284c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 5 Aug 2022 16:44:41 -0300 Subject: [PATCH 09/10] Report stderr and stdout when returncode is not 0 --- src/plone_code_analysis/cmd.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plone_code_analysis/cmd.py b/src/plone_code_analysis/cmd.py index 59fa37f..6e34821 100644 --- a/src/plone_code_analysis/cmd.py +++ b/src/plone_code_analysis/cmd.py @@ -27,7 +27,11 @@ def run_command(cmd: str, args: list[str], paths: list[Path]) -> int: proc = subprocess.run(all_args, capture_output=True) code = proc.returncode if code: - msg = proc.stdout.decode() + # Each tool behaves in a distinct way, so we + # concatenate the stdout and stderr + stdout = proc.stdout.decode() + stderr = proc.stderr.decode() + msg = f"{stdout}\n{stderr}".strip() logger.error(msg, fancy=False) else: logger.success("All good!") From 0ffc12abb09dad1def6455f1b0a10bb3cad013ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 5 Aug 2022 18:21:14 -0300 Subject: [PATCH 10/10] Add formatter examples --- Makefile | 7 +++--- README.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index bfc52ff..8938406 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,12 @@ DOCKERFILE=Dockerfile ifndef LOG_LEVEL LOG_LEVEL=INFO endif +CURRENT_USER=$$(whoami) +USER_INFO=$$(id -u ${CURRENT_USER}):$$(getent group ${CURRENT_USER}|cut -d: -f3) CODEBASE=docker-entrypoint.py src/setup.py src/plone_code_analysis tests/fixtures/packages/ok tests/package tests/conftest.py LINT=docker run -e LOG_LEVEL="${LOG_LEVEL}" --rm -v "${PWD}":/github/workspace "${IMAGE_NAME}:latest" check -FORMAT=docker run -e LOG_LEVEL="${LOG_LEVEL}" --rm -v "${PWD}":/github/workspace "${IMAGE_NAME}:latest" format -CURRENT_USER=$$(whoami) +FORMAT=docker run --user="${USER_INFO}" -e LOG_LEVEL="${LOG_LEVEL}" --rm -v "${PWD}":/github/workspace "${IMAGE_NAME}:latest" format + # Add the following 'help' target to your Makefile # And add help text after each target name starting with '\#\#' @@ -55,4 +57,3 @@ lint-all: build-image ## Lint code with existing image using configurations fro format: build-image ## Format code with existing image @echo "Formatting ${CODEBASE} $(IMAGE_NAME):latest" $(FORMAT) - sudo chown -R ${CURRENT_USER}: * diff --git a/README.md b/README.md index 07b38dd..ffb6d92 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,9 @@ paths_pyroma = "src/ tests/fixtures/packages/ok" First, go to the repository you want to check or format. -### Run all Checks +### Checks / Linter + +#### Run all Checks Using the configuration available in `pyproject.toml`, run: @@ -105,7 +107,7 @@ Using the configuration available in `pyproject.toml`, run: docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check ``` -### Check with [black](https://black.readthedocs.io/en/stable/) +#### Check with [black](https://black.readthedocs.io/en/stable/) Check with `pyproject.toml` settings: @@ -119,7 +121,7 @@ Explicitly check **src** directory and **setup.py** file. docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check black src setup.py ``` -### Check with [flake8](https://flake8.pycqa.org/en/stable/) +#### Check with [flake8](https://flake8.pycqa.org/en/stable/) Flake8 checks, using [flakeheaven](https://pypi.org/project/flakeheaven/) configuration format. @@ -141,7 +143,7 @@ Explicitly check **src** directory and **setup.py** file. docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check flake8 src setup.py ``` -### Check with [isort](https://pycqa.github.io/isort/) +#### Check with [isort](https://pycqa.github.io/isort/) Check with `pyproject.toml` settings: @@ -156,7 +158,7 @@ docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check is ``` -### Check with [pyroma](https://pycqa.github.io/pyroma/) +#### Check with [pyroma](https://pycqa.github.io/pyroma/) Check with `pyproject.toml` settings: @@ -170,7 +172,7 @@ Explicitly check **src/mypackage** directory . docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check pyroma src/mypackage ``` -### Check with [zpretty](https://pypi.org/project/zpretty/) +#### Check with [zpretty](https://pypi.org/project/zpretty/) Check with `pyproject.toml` settings: @@ -185,6 +187,63 @@ docker run --rm -v "${PWD}":/github/workspace plone/code-quality:latest check zp ``` +### Formatter + +To avoid rewriting the owner and group information of the formatted files, we need to pass the correct `--user` option to the `docker run` command. + +In all examples bellow we use the `"$(id -u $(whoami)):$(getent group $(whoami)|cut -d: -f3)"` snippet to set the `--user` option to the current user running the formatter. + +#### Run all formatters + +Using the configuration available in `pyproject.toml`, run: + +```bash +docker run --user="$(id -u $(whoami)):$(getent group $(whoami)|cut -d: -f3)" --rm -v "${PWD}":/github/workspace plone/code-quality:latest format +``` + +#### Format with [black](https://black.readthedocs.io/en/stable/) + +Format with `pyproject.toml` settings: + +```bash +docker run --user="$(id -u $(whoami)):$(getent group $(whoami)|cut -d: -f3)" --rm -v "${PWD}":/github/workspace plone/code-quality:latest format black +``` + +Explicitly format **src** directory and **setup.py** file. + +```bash +docker run --user="$(id -u $(whoami)):$(getent group $(whoami)|cut -d: -f3)" --rm -v "${PWD}":/github/workspace plone/code-quality:latest format src setup.py +``` + +#### Format with [isort](https://pycqa.github.io/isort/) + +Format with `pyproject.toml` settings: + +```bash +docker run --user="$(id -u $(whoami)):$(getent group $(whoami)|cut -d: -f3)" --rm -v "${PWD}":/github/workspace plone/code-quality:latest format isort +``` + +Explicitly format **src** directory and **setup.py** file. + +```bash +docker run --user="$(id -u $(whoami)):$(getent group $(whoami)|cut -d: -f3)" --rm -v "${PWD}":/github/workspace plone/code-quality:latest format src setup.py +``` + +#### Format with [zpretty](https://pypi.org/project/zpretty/) + +Format with `pyproject.toml` settings: + +```bash +docker run --user="$(id -u $(whoami)):$(getent group $(whoami)|cut -d: -f3)" --rm -v "${PWD}":/github/workspace plone/code-quality:latest format zpretty +``` + +Explicitly format the **src** directory . + +```bash +docker run --user="$(id -u $(whoami)):$(getent group $(whoami)|cut -d: -f3)" --rm -v "${PWD}":/github/workspace plone/code-quality:latest format src + +``` + ## Contribute - [Issue Tracker](https://github.com/plone/code-quality/issues)