Introduction
Today, I’m going to look at using tox, linters, and a GitLab CI/CD pipeline to check code quality before committing to a repository.
First – what is tox? Tox can be used to automate and standardize testing in Python. It is a virtualenv management and test command line tool you can use to run different tests for Python projects and also check the syntax of YAML and Ansible syntax as well, for example.
What are linters? Linters are programs that are used to check code quality. They can help to prevent bugs in a project, make code more readable, and make code cleaner and less complex.
There are many linters you can use. In this example, I will use a couple of different ones such as pylint, flake8, yamllint, and ansible-lint.
Once we have tox and the linters setup, we will use pre-commit Git hooks with GitLab CI/CD pipelines to automate the linting process on commit.
Procedure
-
Set up a simple tox.ini file. This file will go in the same directory as your setup.py file.
[tox]
minversion = 3.8.0
envlist = linters
skipsdist = True
ignore_basepython_conflict = True
skip_missing_interpreters = False
requires =
tox-extra
[gh-actions]python = 3.9: py39
[testenv]
usedevelop = True
install_command = pip install -U {opts} {packages}
[testenv:linters]
deps = pre-commit>=1.21.0
pylint>=2.12.0
-r {toxinidir}/requirements.txt
-r {toxinidir}/test-requirements.txt
commands = python -m pre_commit run -a
-
Now you can run tox locally and see what errors are detected if any:
3. To set up the linter to run every time you commit code locally, install and configure pre-commit:
pip install pre-commit
4. Add a pre-commit configuration file and set stages to commit:
.pre-commit-config.yaml
--- default_language_version: python: python3 minimum_pre_commit_version: "1.14.0" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.1.0 hooks: - id: end-of-file-fixer stages: [commit] - id: trailing-whitespace stages: [commit] - id: mixed-line-ending stages: [commit] - id: check-byte-order-marker stages: [commit] - id: check-executables-have-shebangs stages: [commit] - id: check-merge-conflict stages: [commit] - id: debug-statements stages: [commit] - id: check-json stages: [commit] - id: check-yaml stages: [commit] files: .*\.(yaml|yml)$ args: ["--unsafe"] - repo: https://github.com/pre-commit/mirrors-isort rev: v5.1.4 hooks: - id: isort stages: [commit] - repo: https://github.com/PyCQA/flake8.git rev: 3.8.3 hooks: - id: flake8 stages: [commit] - repo: local hooks: - id: pylint stages: [commit] name: pylint entry: pylint language: system types: [python] require_serial: true args: [ "-j 4", "--reports=n", # Simple report "--fail-under=9", "--output=pylint.report.txt", "--output-format=text" ] - repo: https://github.com/adrienverge/yamllint.git rev: v1.24.2 hooks: - id: yamllint stages: [commit] files: \.(yaml|yml)$ types: [file, yaml] entry: yamllint --strict -f parsable - repo: https://github.com/ansible/ansible-lint.git rev: v5.3.2 hooks: - id: ansible-lint stages: [commit] always_run: true pass_filenames: false verbose: true entry: bash -c "ANSIBLE_LIBRARY=plugins/modules ansible-lint --force-color -p -v" - repo: https://github.com/openstack-dev/bashate.git rev: 2.0.0 hooks: - id: bashate stages: [commit] entry: bashate --error . --ignore=E006,E040
5. Run pre-commit install to setup the git hook scripts:
pre-commit install
Now pre-commit will automatically run locally on git commit.
To run tox from a GitLab CI/CD, create a .gitlab-ci.yml file:
stages: - test lint: images: python: 3.8 stage: test tags: - shared # use the shared tag when using shared runners before_script: - pip install -r requirements.txt - pip install tox script: - tox -v -e linters
Test your pipeline now by committing code to your repo.
Conclusion
Now you can go ahead and add more tests or test multiple Python versions, etc.
Note: I came across this error when configuring tox to run in the CI/CD pipeline:
× python setup.py develop did not run successfully. │ exit code: 1 ╰─> [4 lines of output] running develop /builds/my_project/.tox/linters/lib/python3.8/site-packages/setuptools/command/easy_install.py:144: EasyInstallDeprecationWarning: easy_install command is deprecated. Use build and pip and other standards-based tools. warnings.warn( error: error in 'egg_base' option: 'my_project' does not exist or is not a directory [end of output] note: This error originates from a subprocess, and is likely not a problem with pip. error: subprocess-exited-with-error × python setup.py develop did not run successfully. │ exit code: 1 ╰─> [4 lines of output] running develop /builds/my_project/.tox/linters/lib/python3.8/site-packages/setuptools/command/easy_install.py:144: EasyInstallDeprecationWarning: easy_install command is deprecated. Use build and pip and other standards-based tools. warnings.warn( error: error in 'egg_base' option: 'my_project' does not exist or is not a directory [end of output] note: This error originates from a subprocess, and is likely not a problem with pip.
The fix was to add add the following to my setup.py file:
from setuptools import find_packages, setup setup( packages=find_packages(exclude=["tests.*", "tests"]), )