From 5fa3ff4d1bef13ec22a70c94ad671e73a7f24ed0 Mon Sep 17 00:00:00 2001 From: Alexander Goscinski Date: Tue, 10 Sep 2024 14:36:27 +0200 Subject: [PATCH] Modernize project infrastructure and switching to hatch (#108) * Switch to src project structure * Prevent CI being run twice on PR Having CI jobs run on PR and push without any further restrictions runs the CI jobs twice on a new PR, with this setting the CI is only run on pushes to main. * Ignore QbFields warning in sphinx Bug that appeears with aiida 2.6 (independent of this PR), it also goes away when building the docs twice. For now I ignore it. * Remove outdated install_aiida_github.sh It is nowhere used and I am not sure in which context it should be used. Installing aiida-core seems trivial. What would be useful is something for creating new profiles in the tests. * Split pre-commit hook fmt into lint and format * Change ruff settings single quotes -> double quotes double quotes are default so nothing has to be changed * Apply the formatting to double quotes on template repo * Move docs to an optional dependency for readthedocs Because readthedocs can only use extra dependencies from the pyproject.toml, we move the deps of docs into one, so we can reference it in the readthedocs.yaml * Updates in the update-aiida-diff.sh script - Remove `git init` in update aiida-diff script to base on current version - Remove `hatch fmt` at the end since it is done by hook properly * Fix formatting in the cookiecutter hook Now `hatch fmt` is executed twice, running it one time did not fix all isues. Needs to be investigated, if a reviewer has some ideas. * Add script update-template-formatting.sh that can be used to fix formatting To apply the formatting on the cookiecutter template the script initiates a cookiecutter repo instance and applies the formatting on this instance the changes are diffed and copied back to the template. * Update update-aiida-diff.sh Since the cookiecutter hooks is now also performing the formatting, the pre-commit is not necessary. The git init has been removed since the repository already exists. --------- Co-authored-by: Rico Haeuselmann --- .github/workflows/ci.yml | 40 +++-- hooks/post_gen_project.sh | 10 +- requirements.txt | 2 +- tox.ini | 2 +- update-aiida-diff.sh | 5 - update-template-formatting.sh | 29 ++++ .../.github/install_aiida_github.sh | 6 - .../.github/workflows/ci.yml | 60 +++---- .../.github/workflows/publish-on-pypi.yml | 16 +- .../.pre-commit-config.yaml | 44 ++--- {{cookiecutter.plugin_name}}/CONTRIBUTING.md | 52 ++++++ {{cookiecutter.plugin_name}}/conftest.py | 1 + .../docs/source/conf.py | 16 +- .../examples/example_01.py | 17 +- {{cookiecutter.plugin_name}}/pyproject.toml | 150 ++++++++++-------- .../{{cookiecutter.module_name}}/__init__.py | 0 .../calculations.py | 13 +- .../{{cookiecutter.module_name}}/cli.py | 8 +- .../data/__init__.py | 7 +- .../{{cookiecutter.module_name}}/helpers.py | 3 +- .../{{cookiecutter.module_name}}/parsers.py | 5 +- .../tests/__init__.py | 3 +- .../tests/test_calculations.py | 7 +- .../tests/test_cli.py | 13 +- 24 files changed, 283 insertions(+), 226 deletions(-) create mode 100755 update-template-formatting.sh delete mode 100755 {{cookiecutter.plugin_name}}/.github/install_aiida_github.sh create mode 100644 {{cookiecutter.plugin_name}}/CONTRIBUTING.md rename {{cookiecutter.plugin_name}}/{ => src}/{{cookiecutter.module_name}}/__init__.py (100%) rename {{cookiecutter.plugin_name}}/{ => src}/{{cookiecutter.module_name}}/calculations.py (88%) rename {{cookiecutter.plugin_name}}/{ => src}/{{cookiecutter.module_name}}/cli.py (91%) rename {{cookiecutter.plugin_name}}/{ => src}/{{cookiecutter.module_name}}/data/__init__.py (98%) rename {{cookiecutter.plugin_name}}/{ => src}/{{cookiecutter.module_name}}/helpers.py (97%) rename {{cookiecutter.plugin_name}}/{ => src}/{{cookiecutter.module_name}}/parsers.py (93%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 550f410..d41b82f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: timeout-minutes: 30 strategy: matrix: - python-version: ['3.10'] + python-version: ['3.12'] aiida-version: ['stable'] services: postgres: @@ -31,10 +31,10 @@ 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 }} @@ -44,7 +44,6 @@ jobs: pip install -r requirements.txt # Change plugin_name to test we're not specific to "aiida-diff" cookiecutter --no-input . plugin_name=${PLUGIN_NAME} - pip install -e ${PLUGIN_NAME}[testing] env: PLUGIN_NAME: aiida-ck @@ -55,17 +54,17 @@ jobs: PLUGIN_NAME: aiida-ck run: | cd ${PLUGIN_NAME} - pytest -v + hatch test -v 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 3.12 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - name: Install python dependencies env: PLUGIN_NAME: aiida-ck @@ -74,21 +73,22 @@ jobs: pip install -r requirements.txt # Change plugin_name to test we're not specific to "aiida-diff" cookiecutter --no-input . plugin_name=${PLUGIN_NAME} - pip install -e ${PLUGIN_NAME}[docs] - name: Build docs env: PLUGIN_NAME: aiida-ck - run: cd ${PLUGIN_NAME}/docs && make + run: | + cd ${PLUGIN_NAME} + 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 3.12 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - name: Install python dependencies env: PLUGIN_NAME: aiida-ck @@ -97,13 +97,9 @@ jobs: pip install -r requirements.txt # Change plugin_name to test we're not specific to "aiida-diff" cookiecutter --no-input . plugin_name=${PLUGIN_NAME} - pip install -e ${PLUGIN_NAME}[pre-commit,docs,testing] - - name: Run pre-commit + - name: Run formatter and linter env: PLUGIN_NAME: aiida-ck run: | cd ${PLUGIN_NAME} - git init - git add -A - pre-commit install - pre-commit run --all-files || ( git status --short ; git diff ; exit 1 ) + hatch fmt diff --git a/hooks/post_gen_project.sh b/hooks/post_gen_project.sh index c12ffdc..71bd95f 100644 --- a/hooks/post_gen_project.sh +++ b/hooks/post_gen_project.sh @@ -1,9 +1,11 @@ #!/bin/bash -if [ -x "$(command -v black)" ]; then - echo "Running black on {{ cookiecutter.plugin_name }}" - black '../{{ cookiecutter.plugin_name }}/' +if [ -x "$(command -v hatch)" ]; then + echo "Running hatch on {{ cookiecutter.plugin_name }}" + # For some reason we need to invoke the formatting twice to be effective + hatch fmt || true + hatch fmt || true else - echo "black not found. 'pip install black' to automatically \ + echo "hatch not found. 'pip install hatch' to automatically \ run formatter on {{ cookiecutter.plugin_name }}" fi diff --git a/requirements.txt b/requirements.txt index 92e49d5..18a2a75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ cookiecutter -black==22.12.0 +hatch~=1.12.0 diff --git a/tox.ini b/tox.ini index 8435db0..68886d1 100644 --- a/tox.ini +++ b/tox.ini @@ -19,4 +19,4 @@ commands = cookiecutter --no-input . plugin_name={env:PLUGIN_NAME} git init -b main {env:PLUGIN_NAME} cd {env:PLUGIN_NAME} && git add -A - pip install -e {env:PLUGIN_NAME}[testing,docs,pre-commit] + pip install -e {env:PLUGIN_NAME} diff --git a/update-aiida-diff.sh b/update-aiida-diff.sh index d561494..7f57ac1 100755 --- a/update-aiida-diff.sh +++ b/update-aiida-diff.sh @@ -11,8 +11,3 @@ else fi cookiecutter --no-input -f . version=1.2.0 -pip install -e aiida-diff[docs,pre-commit,testing] -cd aiida-diff -git init && git add -A -pre-commit install -pre-commit run diff --git a/update-template-formatting.sh b/update-template-formatting.sh new file mode 100755 index 0000000..3ea2149 --- /dev/null +++ b/update-template-formatting.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Creates plugin cutter with unique labels so we can format and then reverses +# the ids to the cookiecutter identifiers and git apply the diff +set -e + +# clean directory contents if exist (but don't delete .git directory) +if [ -d aiida-diff ]; then + rm -rf cookiecutter_plugin_name +fi +cookiecutter --accept-hooks no --no-input -f . plugin_name=cookiecutter_plugin_name module_name=cookiecutter_module_name short_description=cookiecutter_short_description entry_point_prefix=cookiecutter_entry_point_prefix version=0.0.0-dev author=cookiecutter_author year=cookiecutter_year + +cd cookiecutter_plugin_name +git init && git add -A && git commit -am 'init' +hatch fmt || true +# takes the diff and applies it on the cookie cutter template +git diff > patch +cp patch .. +cd .. +# git diff replacements of specified labels with cookiecutter identifiers +sed -i 's/ a\// a\/\{\{cookiecutter.plugin_name\}\}\//g' patch +sed -i 's/ b\// b\/\{\{cookiecutter.plugin_name\}\}\//g' patch +sed -i 's/cookiecutter_plugin_name/\{\{cookiecutter.plugin_name\}\}/g' patch +sed -i 's/cookiecutter_module_name/\{\{cookiecutter.module_name\}\}/g' patch +sed -i 's/cookiecutter_entry_point_prefix/\{\{cookiecutter.entry_point_prefix\}\}/g' patch +sed -i 's/cookiecutter_short_description/\{\{cookiecutter.short_description\}\}/g' patch +sed -i 's/0\.0\.0-dev/\{\{cookiecutter.version\}\}/g' patch +sed -i 's/cookiecutter_author/\{\{cookiecutter.author\}\}/g' patch +sed -i 's/cookiecutter_year/\{\{cookiecutter.year\}\}/g' patch +git apply --reject --whitespace=fix patch diff --git a/{{cookiecutter.plugin_name}}/.github/install_aiida_github.sh b/{{cookiecutter.plugin_name}}/.github/install_aiida_github.sh deleted file mode 100755 index dc56b09..0000000 --- a/{{cookiecutter.plugin_name}}/.github/install_aiida_github.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -git clone https://github.com/aiidateam/aiida_core ../aiida_core -cd ../aiida_core -git checkout $AIIDA_DEVELOP_GIT_HASH -pip install -e .[docs,pre-commit,testing] -cd ${TRAVIS_BUILD_DIR} diff --git a/{{cookiecutter.plugin_name}}/.github/workflows/ci.yml b/{{cookiecutter.plugin_name}}/.github/workflows/ci.yml index 7e7278c..eb85c6f 100644 --- a/{{cookiecutter.plugin_name}}/.github/workflows/ci.yml +++ b/{{cookiecutter.plugin_name}}/.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 {{cookiecutter.module_name}} --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/{{cookiecutter.plugin_name}}/.github/workflows/publish-on-pypi.yml b/{{cookiecutter.plugin_name}}/.github/workflows/publish-on-pypi.yml index 75088d5..97268e5 100644 --- a/{{cookiecutter.plugin_name}}/.github/workflows/publish-on-pypi.yml +++ b/{{cookiecutter.plugin_name}}/.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/{{cookiecutter.plugin_name}}/.pre-commit-config.yaml b/{{cookiecutter.plugin_name}}/.pre-commit-config.yaml index ad3236b..4d839fd 100644 --- a/{{cookiecutter.plugin_name}}/.pre-commit-config.yaml +++ b/{{cookiecutter.plugin_name}}/.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/{{cookiecutter.plugin_name}}/CONTRIBUTING.md b/{{cookiecutter.plugin_name}}/CONTRIBUTING.md new file mode 100644 index 0000000..9292117 --- /dev/null +++ b/{{cookiecutter.plugin_name}}/CONTRIBUTING.md @@ -0,0 +1,52 @@ +### Tests + +Tests can be run in different ways +``` +hatch test # Run tests whith your current python version +hatch test --python 3.9 # Run tests for python version 3.9 +hatch test --show # See all defined test environment +hatch test --all # Run tests for all test environments +hatch test --coverage # Run tests with coverage +``` +You can add arbitrary flags to pytest at the end of the command. For example to run the tests in debug mode use +``` +hatch test --pdb +``` +We use ipdb as debugger backend for autocompletion. + +### Static code analysis + +To check the formatting and linting run +``` +hatch fmt --check +``` +If you want to automatically fix errors that are fixable run +``` +hatch fmt +``` +If you want to run this command before each commit, please install the pre-commit hook +``` +pip install .[pre-commit] +pre-commit install +``` +You can also run the linter and formatter separately +``` +hatch fmt --formatter +hatch fmt --linter +``` + +### Building the docs + +Please run +``` +hatch run docs:build +``` + +### Build and publishing package + +To build and publish a package please use +``` +hatch build +hatch publish -r test # test pypi +hatch publish # pypi +``` diff --git a/{{cookiecutter.plugin_name}}/conftest.py b/{{cookiecutter.plugin_name}}/conftest.py index 74752a2..e696b9d 100644 --- a/{{cookiecutter.plugin_name}}/conftest.py +++ b/{{cookiecutter.plugin_name}}/conftest.py @@ -1,4 +1,5 @@ """pytest fixtures for simplified testing.""" + import pytest pytest_plugins = ["aiida.manage.tests.pytest_fixtures"] diff --git a/{{cookiecutter.plugin_name}}/docs/source/conf.py b/{{cookiecutter.plugin_name}}/docs/source/conf.py index 6a6a582..522cf1c 100755 --- a/{{cookiecutter.plugin_name}}/docs/source/conf.py +++ b/{{cookiecutter.plugin_name}}/docs/source/conf.py @@ -14,15 +14,14 @@ import sys import time +import {{cookiecutter.module_name}} from aiida import load_profile from aiida.storage.sqlite_temp import SqliteTempBackend -import {{cookiecutter.module_name}} - # -- AiiDA-related setup -------------------------------------------------- # Load AiiDA profile -temp_profile = SqliteTempBackend.create_profile('temp-profile') +temp_profile = SqliteTempBackend.create_profile("temp-profile") load_profile(temp_profile, allow_switch=True) # -- General configuration ------------------------------------------------ @@ -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, "{{cookiecutter.module_name}}") + package_dir = os.path.join(source_dir, os.pardir, os.pardir, "src", "{{cookiecutter.module_name}}") # 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/{{cookiecutter.plugin_name}}/examples/example_01.py b/{{cookiecutter.plugin_name}}/examples/example_01.py index 85e2d4b..a9ec4df 100644 --- a/{{cookiecutter.plugin_name}}/examples/example_01.py +++ b/{{cookiecutter.plugin_name}}/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 {{cookiecutter.module_name}} import helpers INPUT_DIR = path.join(path.dirname(path.realpath(__file__)), "input_files") @@ -23,15 +22,17 @@ def test_run({{cookiecutter.entry_point_prefix}}_code): if not {{cookiecutter.entry_point_prefix}}_code: # get code computer = helpers.get_computer() - {{cookiecutter.entry_point_prefix}}_code = helpers.get_code(entry_point="{{cookiecutter.entry_point_prefix}}", computer=computer) + {{cookiecutter.entry_point_prefix}}_code = helpers.get_code( + entry_point="{{cookiecutter.entry_point_prefix}}", computer=computer + ) # Prepare input parameters - DiffParameters = DataFactory("{{cookiecutter.entry_point_prefix}}") - parameters = DiffParameters({"ignore-case": True}) + diff_parameters = DataFactory("{{cookiecutter.entry_point_prefix}}") + 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/{{cookiecutter.plugin_name}}/pyproject.toml b/{{cookiecutter.plugin_name}}/pyproject.toml index 45d78c0..d55889f 100644 --- a/{{cookiecutter.plugin_name}}/pyproject.toml +++ b/{{cookiecutter.plugin_name}}/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 = "{{cookiecutter.plugin_name}}" -dynamic = ["version"] # read from {{cookiecutter.module_name}}/__init__.py +dynamic = ["version"] # read from {{cookiecutter.module_name}}/src/__init__.py description = "{{cookiecutter.short_description}}" {%- if cookiecutter.contact_email %} authors = [{name = "{{cookiecutter.author}}", email = "{{cookiecutter.contact_email}}"}] @@ -24,26 +23,15 @@ classifiers = [ "Framework :: AiiDA" ] keywords = ["aiida", "plugin"] -requires-python = ">=3.7" +requires-python = ">=3.9" dependencies = [ "aiida-core>={{cookiecutter.aiida_min_version}},<3", "voluptuous" ] -[project.urls] -Source = "https://github.com/{{cookiecutter.github_user}}/{{cookiecutter.repo_name}}" - [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", @@ -53,6 +41,9 @@ docs = [ "markupsafe<2.1" ] +[project.urls] +Source = "https://github.com/{{cookiecutter.github_user}}/{{cookiecutter.repo_name}}" + [project.entry-points."aiida.data"] "{{cookiecutter.entry_point_prefix}}" = "{{cookiecutter.module_name}}.data:DiffParameters" @@ -65,22 +56,10 @@ docs = [ [project.entry-points."aiida.cmdline.data"] "{{cookiecutter.entry_point_prefix}}" = "{{cookiecutter.module_name}}.cli:data_cli" -[tool.flit.module] -name = "{{cookiecutter.module_name}}" - -[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:", @@ -88,39 +67,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=["{{cookiecutter.module_name}}"] - -[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/{{cookiecutter.module_name}}"] + +[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/{{cookiecutter.module_name}}/__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/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/__init__.py b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/__init__.py similarity index 100% rename from {{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/__init__.py rename to {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/__init__.py diff --git a/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/calculations.py b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/calculations.py similarity index 88% rename from {{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/calculations.py rename to {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/calculations.py index 8a4495c..7574183 100644 --- a/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/calculations.py +++ b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/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 = "{{cookiecutter.entry_point_prefix}}" # 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( "{{cookiecutter.entry_point_prefix}}", valid_type=SinglefileData, diff --git a/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/cli.py b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/cli.py similarity index 91% rename from {{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/cli.py rename to {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/cli.py index d4ce642..fe56b5e 100644 --- a/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/cli.py +++ b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/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("{{cookiecutter.entry_point_prefix}}") + diff_parameters = DataFactory("{{cookiecutter.entry_point_prefix}}") 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/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/data/__init__.py b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/data/__init__.py similarity index 98% rename from {{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/data/__init__.py rename to {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/data/__init__.py index f2aa313..2b39992 100644 --- a/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/data/__init__.py +++ b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/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/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/helpers.py b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/helpers.py similarity index 97% rename from {{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/helpers.py rename to {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/helpers.py index 981a066..6c693cf 100644 --- a/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/helpers.py +++ b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/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/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/parsers.py b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/parsers.py similarity index 93% rename from {{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/parsers.py rename to {{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/parsers.py index f78ef20..3e040d3 100644 --- a/{{cookiecutter.plugin_name}}/{{cookiecutter.module_name}}/parsers.py +++ b/{{cookiecutter.plugin_name}}/src/{{cookiecutter.module_name}}/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/{{cookiecutter.plugin_name}}/tests/__init__.py b/{{cookiecutter.plugin_name}}/tests/__init__.py index 94cbb42..484b467 100644 --- a/{{cookiecutter.plugin_name}}/tests/__init__.py +++ b/{{cookiecutter.plugin_name}}/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/{{cookiecutter.plugin_name}}/tests/test_calculations.py b/{{cookiecutter.plugin_name}}/tests/test_calculations.py index 1d5f3cc..0bfdd90 100644 --- a/{{cookiecutter.plugin_name}}/tests/test_calculations.py +++ b/{{cookiecutter.plugin_name}}/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({{cookiecutter.entry_point_prefix}}_code): note this does not test that the expected outputs are created of output parsing""" # Prepare input parameters - DiffParameters = DataFactory("{{cookiecutter.entry_point_prefix}}") - parameters = DiffParameters({"ignore-case": True}) + diff_parameters = DataFactory("{{cookiecutter.entry_point_prefix}}") + 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/{{cookiecutter.plugin_name}}/tests/test_cli.py b/{{cookiecutter.plugin_name}}/tests/test_cli.py index a5099fa..afbd24b 100644 --- a/{{cookiecutter.plugin_name}}/tests/test_cli.py +++ b/{{cookiecutter.plugin_name}}/tests/test_cli.py @@ -1,8 +1,7 @@ -""" Tests for command line interface.""" -from click.testing import CliRunner +"""Tests for command line interface.""" from aiida.plugins import DataFactory - +from click.testing import CliRunner from {{cookiecutter.module_name}}.cli import export, list_ @@ -12,8 +11,8 @@ class TestDataCli: def setup_method(self): """Prepare nodes for cli tests.""" - DiffParameters = DataFactory("{{cookiecutter.entry_point_prefix}}") - self.parameters = DiffParameters({"ignore-case": True}) + diff_parameters = DataFactory("{{cookiecutter.entry_point_prefix}}") + 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