diff --git a/.dockerignore b/.dockerignore index a375751..6c3c8f9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,15 @@ -.git -.github +__pycache__ +.*project +.pytest_cache .vscode -docs/ +/.settings/ +/tests +bin Dockerfile +docs/ +include +lib +lib64 LICENSE.txt Makefile -README.md +README.md \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bd34d8..b2c67e4 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: @@ -54,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/.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 8583b4b..dfd6d42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +__pycache__ +.*project +.pytest_cache .vscode /.settings/ -.*project +bin +include +lib +lib64 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 0f791ff..b7f03bf 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,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 requirements.txt pyproject.toml docker-entrypoint.py ./ - -RUN pip install -U pip && pip install -r requirements.txt - 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/* diff --git a/Makefile b/Makefile index 2fa73d9..8938406 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,14 @@ IMAGE_NAME=plone/code-quality DOCKERFILE=Dockerfile -CODEBASE=docker-entrypoint.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 +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 --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 '\#\#' @@ -11,6 +16,26 @@ 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 + +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/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 @echo "Building $(IMAGE_NAME):latest" @@ -23,8 +48,12 @@ 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}" - sudo chown -R ${CURRENT_USER}: * + $(FORMAT) diff --git a/README.md b/README.md index 96192fe..ffb6d92 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,112 @@ +## Configuration + +This tool looks for configuration in a `pyproject.toml`file in the root of the codebase being analysed. + +The default configuration values are: + +```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 = "." +``` + +If you want to change only the `paths`, you should add to your `pyproject.toml` the following settings: + +```toml +[tool.plone-code-analysis] +paths = "src/ setup.py" +``` +Also, it is possible to change the paths used for individual tools: + +```toml +[tool.plone-code-analysis] +paths_black = "src/ tests/ setup.py" +paths_flake8 = "src/ setup.py" +``` + +Or explicitly set `checkers` or `formatters` to be used: + + +```toml +[tool.plone-code-analysis] +checkers = ["black", "flake8", "isort", "pyroma", ] +formatters = ["black", "isort",] +``` + +### 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: + +```toml +[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 = "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. +First, go to the repository you want to check or format. + +### Checks / Linter + +#### 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 +``` -### [black](https://black.readthedocs.io/en/stable/) +Explicitly check **src** directory and **setup.py** file. -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 ``` -### [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. @@ -38,73 +131,117 @@ Current plugins in use: * [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** +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 ``` -### [isort](https://pycqa.github.io/isort/) +#### 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 +``` -Check **src** directory and **setup.py** file with **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 ``` -### [zpretty](https://pypi.org/project/zpretty/) -Check **src** directory with **zpretty** +#### 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 ``` -## 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. +### Formatter -An example configuration, used by this image, follows: +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. -```toml -[tool.black] -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 - )/ -) -''' +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. -[tool.isort] -profile = "black" -force_alphabetical_sort = true -force_single_line = true -lines_after_imports = 2 -line_length = 120 +#### Run all formatters -[tool.flakeheaven] -format="grouped" -max_line_length=88 -show_source=true -max-complexity=25 +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 -[tool.flakeheaven.plugins] -pycodestyle = ["+*"] -pyflakes = ["+*"] -"flake8-*" = ["+*"] ``` ## Contribute @@ -117,6 +254,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 4b2f33c..358da6c 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. @@ -161,23 +26,51 @@ 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) + settings_file = Path("pyproject.toml").resolve() + settings = read_settings_from_file(settings_file) + # 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.strip()) if paths else [] + 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/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 a93a640..df1bae8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,10 @@ +src/ 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/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 new file mode 100644 index 0000000..357601b --- /dev/null +++ b/src/plone_code_analysis/__init__.py @@ -0,0 +1 @@ +"""Plone Code Analysis.""" diff --git a/src/plone_code_analysis/check.py b/src/plone_code_analysis/check.py new file mode 100644 index 0000000..2bab217 --- /dev/null +++ b/src/plone_code_analysis/check.py @@ -0,0 +1,75 @@ +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(): + 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 new file mode 100644 index 0000000..6e34821 --- /dev/null +++ b/src/plone_code_analysis/cmd.py @@ -0,0 +1,58 @@ +from pathlib import Path +from plone_code_analysis.logger import logger + +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 + logger.debug(f"Running command {all_args}") + proc = subprocess.run(all_args, capture_output=True) + code = proc.returncode + if code: + # 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!") + return code + + +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..87f76e1 --- /dev/null +++ b/src/plone_code_analysis/format.py @@ -0,0 +1,59 @@ +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(): + 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 new file mode 100644 index 0000000..1be7e2f --- /dev/null +++ b/src/plone_code_analysis/logger.py @@ -0,0 +1,89 @@ +import logging +import os +import re + + +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 new file mode 100644 index 0000000..84629e9 --- /dev/null +++ b/src/plone_code_analysis/settings.py @@ -0,0 +1,86 @@ +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.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, {}) + 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 = list({path.resolve() for path in possible_paths if path.exists()}) + return 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 + } + logger.debug(f"Settings: {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 + } + logger.debug(f"Settings: {formatters}") + return formatters diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..49acec7 --- /dev/null +++ b/src/setup.py @@ -0,0 +1,41 @@ +"""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", + 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 new file mode 100644 index 0000000..795916c --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +-e "src/" +pytest \ No newline at end of file