diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 969f876..50ee5cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,11 @@ name: ci -on: [push, pull_request] +on: + push: + # only pushes to main trigger + branches: [main] + pull_request: + # always triggered jobs: @@ -9,7 +14,7 @@ jobs: timeout-minutes: 30 strategy: matrix: - python-version: ['3.10'] + python-version: ['3.12'] aiida-version: ['stable'] services: @@ -32,54 +37,49 @@ jobs: - 5672:5672 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Install python dependencies + - name: Install project manager run: | - pip install --upgrade pip - pip install -e .[testing] - + pip install hatch - name: Run test suite env: - # show timings of tests PYTEST_ADDOPTS: "--durations=0" - run: pytest --cov aiida_diff --cov-append . + run: | + hatch test --cover docs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.10 - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: - python-version: "3.10" - - name: Install python dependencies + python-version: ${{ matrix.python-version }} + - name: Install project manager run: | - pip install --upgrade pip - pip install -e .[docs] + pip install hatch - name: Build docs - run: cd docs && make + run: | + hatch run docs:build - pre-commit: + static-analysis: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.10 - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: - python-version: "3.10" - - name: Install python dependencies + python-version: ${{ matrix.python-version }} + - name: Install project manager run: | - pip install --upgrade pip - pip install -e .[pre-commit,docs,testing] - - name: Run pre-commit + pip install hatch + - name: Run formatter and linter run: | - pre-commit install - pre-commit run --all-files || ( git status --short ; git diff ; exit 1 ) + hatch fmt --check diff --git a/.github/workflows/publish-on-pypi.yml b/.github/workflows/publish-on-pypi.yml index 77a8d39..cf29648 100644 --- a/.github/workflows/publish-on-pypi.yml +++ b/.github/workflows/publish-on-pypi.yml @@ -15,19 +15,19 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Set up Python 3.10 - uses: actions/setup-python@v1 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.12' - - name: Install flit + - name: Install hatch run: | python -m pip install --upgrade pip - python -m pip install flit~=3.4 + python -m pip install hatch~=1.12.0 - name: Build and publish run: | - flit publish + hatch publish env: - FLIT_USERNAME: __token__ - FLIT_PASSWORD: ${{ secrets.pypi_token }} + HATCH_INDEX_USER: __token__ + HATCH_INDEX_AUTH: ${{ secrets.pypi_token }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad3236b..4d839fd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,39 +1,13 @@ -# Install pre-commit hooks via: -# pre-commit install repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: end-of-file-fixer - - id: mixed-line-ending - - id: trailing-whitespace - - id: check-json - -- repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: ["--py37-plus"] - -- repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - -- repo: https://github.com/psf/black - rev: 22.12.0 - hooks: - - id: black - - repo: local hooks: - - id: pylint + - id: format + name: format + entry: hatch fmt -f + language: system + types: [python] + - id: lint + name: lint + entry: hatch fmt -l language: system - types: [file, python] - name: pylint - description: "This hook runs the pylint static code analyzer" - exclude: &exclude_files > - (?x)^( - docs/.*| - )$ - entry: pylint + types: [python] diff --git a/conftest.py b/conftest.py index 19937db..dfc42bf 100644 --- a/conftest.py +++ b/conftest.py @@ -1,4 +1,5 @@ """pytest fixtures for simplified testing.""" + import pytest pytest_plugins = ["aiida.manage.tests.pytest_fixtures"] diff --git a/docs/source/conf.py b/docs/source/conf.py index 28e15a2..8d9db62 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,11 +14,10 @@ import sys import time +import aiida_diff from aiida import load_profile from aiida.storage.sqlite_temp import SqliteTempBackend -import aiida_diff - # -- AiiDA-related setup -------------------------------------------------- # Load AiiDA profile @@ -68,9 +67,7 @@ current_year = str(time.localtime().tm_year) copyright_year_string = ( - current_year - if current_year == copyright_first_year - else f"{copyright_first_year}-{current_year}" + current_year if current_year == copyright_first_year else f"{copyright_first_year}-{current_year}" ) # pylint: disable=redefined-builtin copyright = f"{copyright_year_string}, {copyright_owners}. All rights reserved" @@ -185,6 +182,7 @@ # We should ignore any python built-in exception, for instance nitpick_ignore = [ ("py:class", "Logger"), + ("py:class", "QbFields"), # Warning started to appear with aiida 2.6 ] @@ -198,7 +196,7 @@ def run_apidoc(_): """ source_dir = os.path.abspath(os.path.dirname(__file__)) apidoc_dir = os.path.join(source_dir, "apidoc") - package_dir = os.path.join(source_dir, os.pardir, os.pardir, "aiida_diff") + package_dir = os.path.join(source_dir, os.pardir, os.pardir, "src", "aiida_diff") # In #1139, they suggest the route below, but this ended up # calling sphinx-build, not sphinx-apidoc @@ -223,9 +221,7 @@ def run_apidoc(_): # See https://stackoverflow.com/a/30144019 env = os.environ.copy() - env[ - "SPHINX_APIDOC_OPTIONS" - ] = "members,special-members,private-members,undoc-members,show-inheritance" + env["SPHINX_APIDOC_OPTIONS"] = "members,special-members,private-members,undoc-members,show-inheritance" subprocess.check_call([cmd_path] + options, env=env) diff --git a/examples/example_01.py b/examples/example_01.py index b71881b..48202d4 100644 --- a/examples/example_01.py +++ b/examples/example_01.py @@ -3,13 +3,12 @@ Usage: ./example_01.py """ + from os import path import click - from aiida import cmdline, engine from aiida.plugins import CalculationFactory, DataFactory - from aiida_diff import helpers INPUT_DIR = path.join(path.dirname(path.realpath(__file__)), "input_files") @@ -26,12 +25,12 @@ def test_run(diff_code): diff_code = helpers.get_code(entry_point="diff", computer=computer) # Prepare input parameters - DiffParameters = DataFactory("diff") - parameters = DiffParameters({"ignore-case": True}) + diff_parameters = DataFactory("diff") + parameters = diff_parameters({"ignore-case": True}) - SinglefileData = DataFactory("core.singlefile") - file1 = SinglefileData(file=path.join(INPUT_DIR, "file1.txt")) - file2 = SinglefileData(file=path.join(INPUT_DIR, "file2.txt")) + singlefile_data = DataFactory("core.singlefile") + file1 = singlefile_data(file=path.join(INPUT_DIR, "file1.txt")) + file2 = singlefile_data(file=path.join(INPUT_DIR, "file2.txt")) # set up calculation inputs = { diff --git a/pyproject.toml b/pyproject.toml index 39a9cdb..8d70c8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,11 @@ [build-system] -# build the package with [flit](https://flit.readthedocs.io) -requires = ["flit_core >=3.4,<4"] -build-backend = "flit_core.buildapi" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] # See https://www.python.org/dev/peps/pep-0621/ name = "aiida-diff" -dynamic = ["version"] # read from aiida_diff/__init__.py +dynamic = ["version"] # read from aiida_diff/src/__init__.py description = "AiiDA demo plugin that wraps the `diff` executable for computing the difference between two files." authors = [{name = "The AiiDA Team"}] readme = "README.md" @@ -20,26 +19,15 @@ classifiers = [ "Framework :: AiiDA" ] keywords = ["aiida", "plugin"] -requires-python = ">=3.7" +requires-python = ">=3.9" dependencies = [ "aiida-core>=2.5,<3", "voluptuous" ] -[project.urls] -Source = "https://github.com/aiidateam/aiida-diff" - [project.optional-dependencies] -testing = [ - "pgtest~=1.3.1", - "wheel~=0.31", - "coverage[toml]", - "pytest~=6.0", - "pytest-cov" -] pre-commit = [ - "pre-commit~=2.2", - "pylint~=2.15.10" + 'pre-commit~=3.5', ] docs = [ "sphinx", @@ -49,6 +37,9 @@ docs = [ "markupsafe<2.1" ] +[project.urls] +Source = "https://github.com/aiidateam/aiida-diff" + [project.entry-points."aiida.data"] "diff" = "aiida_diff.data:DiffParameters" @@ -61,22 +52,10 @@ docs = [ [project.entry-points."aiida.cmdline.data"] "diff" = "aiida_diff.cli:data_cli" -[tool.flit.module] -name = "aiida_diff" - -[tool.pylint.format] -max-line-length = 125 - -[tool.pylint.messages_control] -disable = [ - "too-many-ancestors", - "invalid-name", - "duplicate-code", -] - [tool.pytest.ini_options] # Configuration for [pytest](https://docs.pytest.org) python_files = "test_*.py example_*.py" +addopts = "--pdbcls=IPython.terminal.debugger:TerminalPdb" filterwarnings = [ "ignore::DeprecationWarning:aiida:", "ignore:Creating AiiDA configuration folder:", @@ -84,39 +63,84 @@ filterwarnings = [ "ignore::DeprecationWarning:yaml:", ] + [tool.coverage.run] # Configuration of [coverage.py](https://coverage.readthedocs.io) # reporting which lines of your plugin are covered by tests -source=["aiida_diff"] - -[tool.isort] -# Configuration of [isort](https://isort.readthedocs.io) -line_length = 120 -force_sort_within_sections = true -sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIIDA', 'FIRSTPARTY', 'LOCALFOLDER'] -known_aiida = ['aiida'] - -[tool.tox] -legacy_tox_ini = """ -[tox] -envlist = py38 - -[testenv] -usedevelop=True - -[testenv:py{37,38,39,310}] -description = Run the test suite against a python version -extras = testing -commands = pytest {posargs} - -[testenv:pre-commit] -description = Run the pre-commit checks -extras = pre-commit -commands = pre-commit run {posargs} - -[testenv:docs] -description = Build the documentation -extras = docs -commands = sphinx-build -nW --keep-going -b html {posargs} docs/source docs/build/html -commands_post = echo "open file://{toxinidir}/docs/build/html/index.html" -""" +source = ["src/aiida_diff"] + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +ignore = [ + 'F403', # Star imports unable to detect undefined names + 'F405', # Import may be undefined or defined from star imports + 'PLR0911', # Too many return statements + 'PLR0912', # Too many branches + 'PLR0913', # Too many arguments in function definition + 'PLR0915', # Too many statements + 'PLR2004', # Magic value used in comparison + 'RUF005', # Consider iterable unpacking instead of concatenation + 'RUF012' # Mutable class attributes should be annotated with `typing.ClassVar` +] +select = [ + 'E', # pydocstyle + 'W', # pydocstyle + 'F', # pyflakes + 'I', # isort + 'N', # pep8-naming + 'PLC', # pylint-convention + 'PLE', # pylint-error + 'PLR', # pylint-refactor + 'PLW', # pylint-warning + 'RUF' # ruff +] + +## Hatch configurations + +[tool.hatch.version] +path = "src/aiida_diff/__init__.py" + +[tool.hatch.envs.hatch-test] +dependencies = [ + 'pgtest~=1.3,>=1.3.1', + 'coverage~=7.0', + 'pytest~=7.0', + "pytest-cov~=4.1", + "ipdb" +] + +[tool.hatch.envs.hatch-test.scripts] +# These are the efault scripts provided by hatch. +# The have been copied to make the execution more transparent + +# This command is run with the command `hatch test` +run = "pytest{env:HATCH_TEST_ARGS:} {args}" +# The three commands below are run with the command `hatch test --coverage` +run-cov = "coverage run -m pytest{env:HATCH_TEST_ARGS:} {args}" +cov-combine = "coverage combine" +cov-report = "coverage report" + +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.9", "3.10", "3.11", "3.12"] + +[tool.hatch.envs.hatch-static-analysis] +dependencies = ["ruff==0.4.3"] + +[tool.hatch.envs.hatch-static-analysis.scripts] +# Fixes are executed with `hatch fmt`. +# Checks are executed with `hatch fmt --check`. + +format-check = "ruff format --check --config pyproject.toml {args:.}" +format-fix = "ruff format --config pyproject.toml {args:.}" +lint-check = "ruff check --config pyproject.toml {args:.}" +lint-fix = "ruff check --config pyproject.toml --fix --exit-non-zero-on-fix --show-fixes {args:.}" + +[tool.hatch.envs.docs] +features = ["docs"] + +[tool.hatch.envs.docs.scripts] +build = [ + "make -C docs" +] diff --git a/aiida_diff/__init__.py b/src/aiida_diff/__init__.py similarity index 100% rename from aiida_diff/__init__.py rename to src/aiida_diff/__init__.py diff --git a/aiida_diff/calculations.py b/src/aiida_diff/calculations.py similarity index 88% rename from aiida_diff/calculations.py rename to src/aiida_diff/calculations.py index b0a4aa7..ed6a785 100644 --- a/aiida_diff/calculations.py +++ b/src/aiida_diff/calculations.py @@ -3,6 +3,7 @@ Register calculations via the "aiida.calculations" entry point in setup.json. """ + from aiida.common import datastructures from aiida.engine import CalcJob from aiida.orm import SinglefileData @@ -31,20 +32,14 @@ def define(cls, spec): spec.inputs["metadata"]["options"]["parser_name"].default = "diff" # new ports - spec.input( - "metadata.options.output_filename", valid_type=str, default="patch.diff" - ) + spec.input("metadata.options.output_filename", valid_type=str, default="patch.diff") spec.input( "parameters", valid_type=DiffParameters, help="Command line parameters for diff", ) - spec.input( - "file1", valid_type=SinglefileData, help="First file to be compared." - ) - spec.input( - "file2", valid_type=SinglefileData, help="Second file to be compared." - ) + spec.input("file1", valid_type=SinglefileData, help="First file to be compared.") + spec.input("file2", valid_type=SinglefileData, help="Second file to be compared.") spec.output( "diff", valid_type=SinglefileData, diff --git a/aiida_diff/cli.py b/src/aiida_diff/cli.py similarity index 92% rename from aiida_diff/cli.py rename to src/aiida_diff/cli.py index 462e259..8aecc1e 100644 --- a/aiida_diff/cli.py +++ b/src/aiida_diff/cli.py @@ -5,10 +5,10 @@ directly into the 'verdi' command by using AiiDA-specific entry points like "aiida.cmdline.data" (both in the setup.json file). """ + import sys import click - from aiida.cmdline.commands.cmd_data import verdi_data from aiida.cmdline.params.types import DataParamType from aiida.cmdline.utils import decorators @@ -28,16 +28,16 @@ def list_(): # pylint: disable=redefined-builtin """ Display all DiffParameters nodes """ - DiffParameters = DataFactory("diff") + diff_parameters = DataFactory("diff") qb = QueryBuilder() - qb.append(DiffParameters) + qb.append(diff_parameters) results = qb.all() s = "" for result in results: obj = result[0] - s += f"{str(obj)}, pk: {obj.pk}\n" + s += f"{obj!s}, pk: {obj.pk}\n" sys.stdout.write(s) diff --git a/aiida_diff/data/__init__.py b/src/aiida_diff/data/__init__.py similarity index 98% rename from aiida_diff/data/__init__.py rename to src/aiida_diff/data/__init__.py index f2aa313..2b39992 100644 --- a/aiida_diff/data/__init__.py +++ b/src/aiida_diff/data/__init__.py @@ -1,13 +1,12 @@ -""" -Data types provided by plugin +"""Data types provided by plugin Register data types via the "aiida.data" entry point in setup.json. """ + # You can directly use or subclass aiida.orm.data.Data # or any other data type listed under 'verdi data' -from voluptuous import Optional, Schema - from aiida.orm import Dict +from voluptuous import Optional, Schema # A subset of diff's command line options cmdline_options = { diff --git a/aiida_diff/helpers.py b/src/aiida_diff/helpers.py similarity index 97% rename from aiida_diff/helpers.py rename to src/aiida_diff/helpers.py index b080d2c..c63a968 100644 --- a/aiida_diff/helpers.py +++ b/src/aiida_diff/helpers.py @@ -1,4 +1,4 @@ -""" Helper functions for automatically setting up computer & code. +"""Helper functions for automatically setting up computer & code. Helper functions for setting up 1. An AiiDA localhost computer @@ -7,6 +7,7 @@ Note: Point 2 is made possible by the fact that the ``diff`` executable is available in the PATH on almost any UNIX system. """ + import shutil import tempfile diff --git a/aiida_diff/parsers.py b/src/aiida_diff/parsers.py similarity index 93% rename from aiida_diff/parsers.py rename to src/aiida_diff/parsers.py index e832035..fd03408 100644 --- a/aiida_diff/parsers.py +++ b/src/aiida_diff/parsers.py @@ -3,6 +3,7 @@ Register parsers via the "aiida.parsers" entry point in setup.json. """ + from aiida.common import exceptions from aiida.engine import ExitCode from aiida.orm import SinglefileData @@ -43,9 +44,7 @@ def parse(self, **kwargs): files_expected = [output_filename] # Note: set(A) <= set(B) checks whether A is a subset of B if not set(files_expected) <= set(files_retrieved): - self.logger.error( - f"Found files '{files_retrieved}', expected to find '{files_expected}'" - ) + self.logger.error(f"Found files '{files_retrieved}', expected to find '{files_expected}'") return self.exit_codes.ERROR_MISSING_OUTPUT_FILES # add output file diff --git a/tests/__init__.py b/tests/__init__.py index 94cbb42..484b467 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,8 +1,9 @@ -""" Tests for the plugin. +"""Tests for the plugin. Includes both tests written in unittest style (test_cli.py) and tests written in pytest style (test_calculations.py). """ + import os TEST_DIR = os.path.dirname(os.path.realpath(__file__)) diff --git a/tests/test_calculations.py b/tests/test_calculations.py index 9d27370..d22c38c 100644 --- a/tests/test_calculations.py +++ b/tests/test_calculations.py @@ -1,4 +1,5 @@ -""" Tests for calculations.""" +"""Tests for calculations.""" + import os from aiida.engine import run @@ -13,8 +14,8 @@ def test_process(diff_code): note this does not test that the expected outputs are created of output parsing""" # Prepare input parameters - DiffParameters = DataFactory("diff") - parameters = DiffParameters({"ignore-case": True}) + diff_parameters = DataFactory("diff") + parameters = diff_parameters({"ignore-case": True}) file1 = SinglefileData(file=os.path.join(TEST_DIR, "input_files", "file1.txt")) file2 = SinglefileData(file=os.path.join(TEST_DIR, "input_files", "file2.txt")) diff --git a/tests/test_cli.py b/tests/test_cli.py index 704c9e7..dea39d2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,9 +1,8 @@ -""" Tests for command line interface.""" -from click.testing import CliRunner +"""Tests for command line interface.""" from aiida.plugins import DataFactory - from aiida_diff.cli import export, list_ +from click.testing import CliRunner # pylint: disable=attribute-defined-outside-init @@ -12,8 +11,8 @@ class TestDataCli: def setup_method(self): """Prepare nodes for cli tests.""" - DiffParameters = DataFactory("diff") - self.parameters = DiffParameters({"ignore-case": True}) + diff_parameters = DataFactory("diff") + self.parameters = diff_parameters({"ignore-case": True}) self.parameters.store() self.runner = CliRunner() @@ -31,7 +30,5 @@ def test_data_diff_export(self): Tests that it can be reached and that it shows the contents of the node we have set up. """ - result = self.runner.invoke( - export, [str(self.parameters.pk)], catch_exceptions=False - ) + result = self.runner.invoke(export, [str(self.parameters.pk)], catch_exceptions=False) assert "ignore-case" in result.output