diff --git a/CHANGELOG.md b/CHANGELOG.md index dacde367..c686fe0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Changed +* Moved from prospector to ruff [#336](https://github.com/NLeSC/python-template/issues/336) * Renamed `project_name` to `directory_name` in cookiecutter questionnaire * Initial linting is error free [#227](https://github.com/NLeSC/python-template/issues/227) * Consolidated test/lint/build/docs into single matrix workflow [#270](https://github.com/NLeSC/python-template/issues/276) diff --git a/README.md b/README.md index c2cb63b0..df725406 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ an empty Python package. Features include: - [Python static setup configuration]({{cookiecutter.directory_name}}/setup.cfg), - Open source software license, - Continuous integration with [GitHub action workflows]({{cookiecutter.directory_name}}/.github/workflows) for building, testing, link checking and linting, -- Code style checking with [prospector](https://pypi.org/project/prospector/), +- Code style checking with [ruff](https://beta.ruff.rs/), - [Editorconfig]({{cookiecutter.directory_name}}/.editorconfig), - Usage and contribution documents: - [README.md]({{cookiecutter.directory_name}}/README.md) for package users, @@ -131,7 +131,6 @@ my-python-project/ ├── next_steps.md ├── NOTICE ├── project_setup.md -├── .prospector.yml ├── .pylintrc ├── pyproject.toml ├── README.dev.md diff --git a/tests/test_project.py b/tests/test_project.py index c1b56c73..a47d3f9b 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,5 +1,6 @@ import os import subprocess +import sys from sys import platform from typing import Sequence @@ -107,6 +108,8 @@ def test_generate_api_docs(baked_with_development_dependencies, project_env_bin_ assert (project_dir / 'docs' / '_build' / 'html' / 'index.html').exists() +@pytest.mark.skipif(sys.version_info < (3, 9), reason= +"requires python 3.9 or higher, see https://github.com/NLeSC/python-template/pull/347#issuecomment-1710684574") def test_coverage_api_docs(baked_with_development_dependencies, project_env_bin_dir): project_dir = baked_with_development_dependencies bin_dir = project_env_bin_dir @@ -116,9 +119,28 @@ def test_coverage_api_docs(baked_with_development_dependencies, project_env_bin_ assert 'build succeeded' in result.stdout coverage_file = project_dir / 'docs' / '_build' / 'coverage' / 'python.txt' coverage_file_lines = coverage_file.read_text('utf8').splitlines() - expected = ['Undocumented Python objects', - '==========================='] - assert coverage_file_lines == expected + # Coverage file lines should look globally like: + # ['Undocumented Python objects', + # '===========================', + # '', + # 'Statistics', + # '----------', + # '', + # '+-----------------------------+----------+--------------+', + # '| Module | Coverage | Undocumented |', + # '+=============================+==========+==============+', + # '| my_python_package | 100.00% | 0 |', + # '+-----------------------------+----------+--------------+', + # '| my_python_package.my_module | 100.00% | 0 |', + # '+-----------------------------+----------+--------------+', + # '| TOTAL | 100.00% | 0 |', + # '+-----------------------------+----------+--------------+', + # '' + # ] + # The package coverage lines change order between runs, so we test for each data row individually: + assert '| my_python_package | 100.00% | 0 |' in coverage_file_lines + assert '| my_python_package.my_module | 100.00% | 0 |' in coverage_file_lines + assert '| TOTAL | 100.00% | 0 |' in coverage_file_lines def test_doctest_api_docs(baked_with_development_dependencies, project_env_bin_dir): @@ -131,20 +153,11 @@ def test_doctest_api_docs(baked_with_development_dependencies, project_env_bin_d assert (project_dir / 'docs' / '_build' / 'doctest' / 'output.txt').exists() -def test_prospector(baked_with_development_dependencies, project_env_bin_dir): +def test_ruff_check(baked_with_development_dependencies, project_env_bin_dir): project_dir = baked_with_development_dependencies bin_dir = project_env_bin_dir - result = run([f'{bin_dir}prospector'], project_dir) - assert result.returncode == 0 - assert 'Messages Found: 0' in result.stdout - - -def test_isort_check(baked_with_development_dependencies, project_env_bin_dir): - project_dir = baked_with_development_dependencies - bin_dir = project_env_bin_dir - - result = run([f'{bin_dir}isort', '--check-only', 'my_python_package'], project_dir) + result = run([f'{bin_dir}ruff', '.'], project_dir) assert result.returncode == 0 assert '' in result.stdout diff --git a/{{cookiecutter.directory_name}}/.githooks/pre-commit b/{{cookiecutter.directory_name}}/.githooks/pre-commit index 2e38f129..a0f9c2e0 100755 --- a/{{cookiecutter.directory_name}}/.githooks/pre-commit +++ b/{{cookiecutter.directory_name}}/.githooks/pre-commit @@ -3,29 +3,17 @@ echo "Script $0 triggered ..." # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -echo "Starting prospector analysis using configuration from .prospector.yml..." +echo "Starting ruff analysis..." -# quietly run prospector -prospector 1>/dev/null +# quietly run ruff +ruff . --fix # use return code to abort commit if necessary if [ $? != "0" ]; then - echo "Commit aborted. Run 'prospector' to see the errors." + echo "Commit aborted. Fix linter issues found by ruff before committing." exit 1 fi # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -echo "Starting isort analysis using configuration from setup.cfg..." - -# recursively run isort on {{ cookiecutter.package_name }}/ directory, don't try to automatically fix anything -isort --recursive --check-only {{ cookiecutter.package_name }} - -if [ $? != "0" ]; then - echo "Commit aborted." - echo " Run 'isort --check-only --diff {{ cookiecutter.package_name }}' to see what's wrong." - echo " Run 'isort {{ cookiecutter.package_name }}' to let isort fix problems automatically." - exit 1 -fi - echo "Pre-commit checks completed successfully." exit 0 diff --git a/{{cookiecutter.directory_name}}/.github/next_steps/05_linting.md b/{{cookiecutter.directory_name}}/.github/next_steps/05_linting.md index e2b32c24..471a09b4 100644 --- a/{{cookiecutter.directory_name}}/.github/next_steps/05_linting.md +++ b/{{cookiecutter.directory_name}}/.github/next_steps/05_linting.md @@ -7,7 +7,7 @@ Your repository has a [workflow]({{ cookiecutter.repository }}/blob/main/.github Linter workflow may fail if `description` or `keywords` field in [setup.cfg]({{ cookiecutter.repository }}/blob/main/setup.cfg) is empty. Please update these fields. To validate your changes run: ```shell -prospector +ruff . ``` Enabling [githook](https://git-scm.com/docs/githooks) will automatically lint your code in every commit. You can enable it by running the command below. diff --git a/{{cookiecutter.directory_name}}/.github/workflows/build.yml b/{{cookiecutter.directory_name}}/.github/workflows/build.yml index 666b6930..80ec495f 100644 --- a/{{cookiecutter.directory_name}}/.github/workflows/build.yml +++ b/{{cookiecutter.directory_name}}/.github/workflows/build.yml @@ -58,7 +58,5 @@ jobs: run: | python -m pip install --upgrade pip setuptools python -m pip install .[dev,publishing] - - name: Check style against standards using prospector - run: prospector - - name: Check import order - run: isort --check-only {{ cookiecutter.package_name }} --diff + - name: Check style against standards using ruff + run: ruff . diff --git a/{{cookiecutter.directory_name}}/.github/workflows/sonarcloud.yml b/{{cookiecutter.directory_name}}/.github/workflows/sonarcloud.yml index 1067aa92..dc4f8672 100644 --- a/{{cookiecutter.directory_name}}/.github/workflows/sonarcloud.yml +++ b/{{cookiecutter.directory_name}}/.github/workflows/sonarcloud.yml @@ -29,8 +29,6 @@ jobs: python --version - name: Install dependencies run: python -m pip install .[dev] - - name: Check style against standards using prospector - run: prospector --zero-exit --output-format grouped --output-format pylint:pylint-report.txt - name: Run unit tests with coverage run: python -m pytest --cov --cov-report term --cov-report xml --junitxml=xunit-result.xml tests/ - name: Correct coverage paths diff --git a/{{cookiecutter.directory_name}}/.prospector.yml b/{{cookiecutter.directory_name}}/.prospector.yml deleted file mode 100644 index a5000487..00000000 --- a/{{cookiecutter.directory_name}}/.prospector.yml +++ /dev/null @@ -1,29 +0,0 @@ -# prospector configuration file - ---- - -output-format: grouped - -strictness: medium -doc-warnings: false -test-warnings: true -member-warnings: false - -ignore-paths: - - docs - -pyroma: - run: true - -pycodestyle: - full: true - -pydocstyle: - disable: [ - # Disable because not part of PEP257 official convention: - # see http://pep257.readthedocs.io/en/latest/error_codes.html - D203, # 1 blank line required before class docstring - D212, # Multi-line docstring summary should start at the first line - D213, # Multi-line docstring summary should start at the second line - D404, # First word of the docstring should not be This - ] diff --git a/{{cookiecutter.directory_name}}/README.dev.md b/{{cookiecutter.directory_name}}/README.dev.md index 9b48b4e4..839a4a0f 100644 --- a/{{cookiecutter.directory_name}}/README.dev.md +++ b/{{cookiecutter.directory_name}}/README.dev.md @@ -62,28 +62,20 @@ coverage report ## Running linters locally -For linting we will use [prospector](https://pypi.org/project/prospector/) and to sort imports we will use -[isort](https://pycqa.github.io/isort/). Running the linters requires an activated virtual environment with the -development tools installed. +For linting and sorting imports we will use [ruff](https://beta.ruff.rs/docs/). Running the linters requires an +activated virtual environment with the development tools installed. ```shell # linter -prospector +ruff . -# recursively check import style for the {{ cookiecutter.package_name }} module only -isort --check-only {{ cookiecutter.package_name }} - -# recursively check import style for the {{ cookiecutter.package_name }} module only and show -# any proposed changes as a diff -isort --check-only --diff {{ cookiecutter.package_name }} - -# recursively fix import style for the {{ cookiecutter.package_name }} module only -isort {{ cookiecutter.package_name }} +# linter with automatic fixing +ruff . --fix ``` To fix readability of your code style you can use [yapf](https://github.com/google/yapf). -You can enable automatic linting with `prospector` and `isort` on commit by enabling the git hook from `.githooks/pre-commit`, like so: +You can enable automatic linting with `ruff` on commit by enabling the git hook from `.githooks/pre-commit`, like so: ```shell git config --local core.hooksPath .githooks diff --git a/{{cookiecutter.directory_name}}/pyproject.toml b/{{cookiecutter.directory_name}}/pyproject.toml index e929771d..4e50c29d 100644 --- a/{{cookiecutter.directory_name}}/pyproject.toml +++ b/{{cookiecutter.directory_name}}/pyproject.toml @@ -19,3 +19,78 @@ skip_missing_interpreters = true commands = pytest extras = dev """ + +[tool.ruff] +# Enable Pyflakes `E` and `F` codes by default. +select = [ + "F", # Pyflakes + "E", # pycodestyle (error) + "W", # pycodestyle (warning) + # "C90", # mccabe + "I", # isort + "D", # pydocstyle + # "PL", # Pylint + # "PLC", # Convention + # "PLE", # Error + # "PLR", # Refactor + # "PLW", # Warning + +] +ignore = [ + 'D100', # Missing module docstring + 'D104', # Missing public package docstring + # The following list excludes rules irrelevant to the Google style + 'D203', + 'D204', + 'D213', + 'D215', + 'D400', + 'D401', + 'D404', + 'D406', + 'D407', + 'D408', + 'D409', + 'D413', +] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +fixable = ["A", "B", "C", "D", "E", "F", "I"] +unfixable = [] + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + ".venv", + "scripts", +] +per-file-ignores = {} + + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +target-version = "py39" +line-length = 120 + +[tool.ruff.isort] +known-first-party = ["{{ cookiecutter.package_name }}"] +force-single-line = true +no-lines-before = ["future","standard-library","third-party","first-party","local-folder"] diff --git a/{{cookiecutter.directory_name}}/setup.cfg b/{{cookiecutter.directory_name}}/setup.cfg index 33c72c7f..d623faa8 100644 --- a/{{cookiecutter.directory_name}}/setup.cfg +++ b/{{cookiecutter.directory_name}}/setup.cfg @@ -51,8 +51,7 @@ install_requires = dev = bump2version coverage [toml] - prospector[with_pyroma] - isort + ruff pytest pytest-cov sphinx @@ -66,11 +65,3 @@ publishing = [options.packages.find] include = {{ cookiecutter.package_name }}, {{ cookiecutter.package_name }}.* - -[isort] -lines_after_imports = 2 -force_single_line = 1 -no_lines_before = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -known_first_party = {{ cookiecutter.package_name }} -src_paths = {{ cookiecutter.package_name }},tests -line_length = 120 diff --git a/{{cookiecutter.directory_name}}/setup.py b/{{cookiecutter.directory_name}}/setup.py index 64c3869b..3bac968b 100644 --- a/{{cookiecutter.directory_name}}/setup.py +++ b/{{cookiecutter.directory_name}}/setup.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -import os from setuptools import setup - # see setup.cfg setup() diff --git a/{{cookiecutter.directory_name}}/tests/test_my_module.py b/{{cookiecutter.directory_name}}/tests/test_my_module.py index c78f5249..ad6481d6 100644 --- a/{{cookiecutter.directory_name}}/tests/test_my_module.py +++ b/{{cookiecutter.directory_name}}/tests/test_my_module.py @@ -1,15 +1,15 @@ -"""Tests for the {{ cookiecutter.package_name }}.my_module module. -""" +"""Tests for the {{ cookiecutter.package_name }}.my_module module.""" import pytest - from {{ cookiecutter.package_name }}.my_module import hello def test_hello(): + """Example using assert.""" assert hello('nlesc') == 'Hello nlesc!' def test_hello_with_error(): + """Example of testing for raised errors.""" with pytest.raises(ValueError) as excinfo: hello('nobody') assert 'Can not say hello to nobody' in str(excinfo.value) @@ -17,8 +17,10 @@ def test_hello_with_error(): @pytest.fixture def some_name(): + """Example fixture.""" return 'Jane Smith' def test_hello_with_fixture(some_name): + """Example using a fixture.""" assert hello(some_name) == 'Hello Jane Smith!' diff --git a/{{cookiecutter.directory_name}}/{{cookiecutter.package_name}}/__init__.py b/{{cookiecutter.directory_name}}/{{cookiecutter.package_name}}/__init__.py index 939ed008..4a72a364 100644 --- a/{{cookiecutter.directory_name}}/{{cookiecutter.package_name}}/__init__.py +++ b/{{cookiecutter.directory_name}}/{{cookiecutter.package_name}}/__init__.py @@ -1,7 +1,6 @@ -"""Documentation about {{ cookiecutter.package_name }}""" +"""Documentation about {{ cookiecutter.package_name }}.""" import logging - logging.getLogger(__name__).addHandler(logging.NullHandler()) __author__ = "{{ cookiecutter.full_name }}" diff --git a/{{cookiecutter.directory_name}}/{{cookiecutter.package_name}}/my_module.py b/{{cookiecutter.directory_name}}/{{cookiecutter.package_name}}/my_module.py index be0172d9..1ebf6f27 100644 --- a/{{cookiecutter.directory_name}}/{{cookiecutter.package_name}}/my_module.py +++ b/{{cookiecutter.directory_name}}/{{cookiecutter.package_name}}/my_module.py @@ -3,7 +3,7 @@ # FIXME: put actual code here def hello(name): - """Say hello + """Say hello. Function docstring using Google docstring style.