From 8f68598590e978dc7c26f94d413dc190ed040ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20Carre=C3=B1o?= Date: Fri, 16 May 2025 18:21:50 +0200 Subject: [PATCH 1/4] chores: squash all the commits in the docs/mkdocs-migration branch before intend a pull request fix: refactor licenses configuration in cookicutter and pyproject.toml. Reduce versbosity and improve maintenance docs: initial commit MkDocs migration for the python-project template. docs: update content of LICENSE for the python-project template chores: remove bash scripts with hooks in the template tests: update placeholder test for the template chores: delete old README.rst and create a new one with different content in markdown docs: update README content, this README belongs to the project not to cookiecutter ci/cd: create workflows for testing, build documentation, security, and quality check docs: update command to run cookiecutter using saezlab organization chores: update minimal test file with __all__ method chores: improve clarity and organization in .pre-commit configuration file chores: include .python-version file in .gitignore chores: remove readthedocs.yaml file chores: improve pyproject.toml file to follow best practices and clear organization fix: exclude workflows of being rendering with jinja2 docs: fix typos in README (cookiecutter template) fix: add instructions for installing pre-commit fix: fix pre-commit versions fix: update Python version specification in workflows and remove unnecessary .python-version from .gitignore fix: correct formatting, errors and other outputs with pre-commit fix: correct formatting, errors and other outputs with pre-commit fix: correct formatting, errors and other outputs with pre-commit fix: correct formatting, errors and other outputs with pre-commit fix: correct formatting, errors and other outputs with pre-commit fix: correct formatting, errors and other outputs with pre-commit fix: correct formatting, errors and other outputs with pre-commit chores: edit badges with updated content --- .pre-commit-config.yaml | 15 +- LICENSE | 2 +- README.md | 90 ++++ README.rst | 337 -------------- cookiecutter.json | 83 ++-- hooks/post_gen_project.sh | 5 - hooks/pre_gen_project.py | 22 - hooks/pre_gen_project.sh | 2 - .../.github/workflows/ci.yaml | 105 ----- .../.github/workflows/code-quality.yml | 26 ++ .../.github/workflows/docs.yml | 45 ++ .../.github/workflows/security.yml | 21 + .../.github/workflows/sphinx_autodoc.yaml | 55 --- .../.github/workflows/test.yml | 33 ++ .../.pre-commit-config.yaml | 92 ++-- .../.readthedocs.yaml | 22 - {{cookiecutter.project_slug}}/README.md | 63 ++- {{cookiecutter.project_slug}}/docs/Makefile | 20 - {{cookiecutter.project_slug}}/docs/about.md | 3 + .../docs/assets/project-banner-readme.png | Bin 0 -> 61255 bytes .../docs/community/contribute-codebase.md | 333 +++++++++++++ .../docs/community/contribute-docs.md | 34 ++ .../docs/community/contribute.md | 51 ++ .../docs/community/index.md | 25 + {{cookiecutter.project_slug}}/docs/index.md | 3 + .../docs/installation.md | 37 ++ .../docs/learn/explanation/index.md | 0 .../docs/learn/guides/index.md | 0 .../docs/learn/tutorials/quickstart.md | 0 .../learn/tutorials/tutorial0001_basics.md | 0 {{cookiecutter.project_slug}}/docs/make.bat | 35 -- .../_metadata-docs.md | 12 + .../docs/src/conf.py | 125 ----- .../docs/src/developer_docs.md | 58 --- .../docs/src/index.rst | 30 -- .../design-philosophy.md | 0 .../project.md | 0 .../use-cases.md | 0 {{cookiecutter.project_slug}}/mkdocs.yml | 107 +++++ {{cookiecutter.project_slug}}/pyproject.toml | 436 +++++++++--------- .../tests/test_placeholder.py | 7 + .../tests/test_twentythree.py | 10 - .../{{cookiecutter.package_name}}/__init__.py | 6 +- .../_metadata.py | 16 +- 44 files changed, 1245 insertions(+), 1121 deletions(-) create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 hooks/post_gen_project.sh delete mode 100644 hooks/pre_gen_project.py delete mode 100644 hooks/pre_gen_project.sh delete mode 100644 {{cookiecutter.project_slug}}/.github/workflows/ci.yaml create mode 100644 {{cookiecutter.project_slug}}/.github/workflows/code-quality.yml create mode 100644 {{cookiecutter.project_slug}}/.github/workflows/docs.yml create mode 100644 {{cookiecutter.project_slug}}/.github/workflows/security.yml delete mode 100644 {{cookiecutter.project_slug}}/.github/workflows/sphinx_autodoc.yaml create mode 100644 {{cookiecutter.project_slug}}/.github/workflows/test.yml delete mode 100644 {{cookiecutter.project_slug}}/.readthedocs.yaml delete mode 100644 {{cookiecutter.project_slug}}/docs/Makefile create mode 100644 {{cookiecutter.project_slug}}/docs/about.md create mode 100644 {{cookiecutter.project_slug}}/docs/assets/project-banner-readme.png create mode 100644 {{cookiecutter.project_slug}}/docs/community/contribute-codebase.md create mode 100644 {{cookiecutter.project_slug}}/docs/community/contribute-docs.md create mode 100644 {{cookiecutter.project_slug}}/docs/community/contribute.md create mode 100644 {{cookiecutter.project_slug}}/docs/community/index.md create mode 100644 {{cookiecutter.project_slug}}/docs/index.md create mode 100644 {{cookiecutter.project_slug}}/docs/installation.md create mode 100644 {{cookiecutter.project_slug}}/docs/learn/explanation/index.md create mode 100644 {{cookiecutter.project_slug}}/docs/learn/guides/index.md create mode 100644 {{cookiecutter.project_slug}}/docs/learn/tutorials/quickstart.md create mode 100644 {{cookiecutter.project_slug}}/docs/learn/tutorials/tutorial0001_basics.md delete mode 100644 {{cookiecutter.project_slug}}/docs/make.bat create mode 100644 {{cookiecutter.project_slug}}/docs/reference/source/{{ cookiecutter.package_name }}/_metadata-docs.md delete mode 100644 {{cookiecutter.project_slug}}/docs/src/conf.py delete mode 100644 {{cookiecutter.project_slug}}/docs/src/developer_docs.md delete mode 100644 {{cookiecutter.project_slug}}/docs/src/index.rst create mode 100644 {{cookiecutter.project_slug}}/docs/{{ cookiecutter.project_slug}}-project/design-philosophy.md create mode 100644 {{cookiecutter.project_slug}}/docs/{{ cookiecutter.project_slug}}-project/project.md create mode 100644 {{cookiecutter.project_slug}}/docs/{{ cookiecutter.project_slug}}-project/use-cases.md create mode 100644 {{cookiecutter.project_slug}}/mkdocs.yml create mode 100644 {{cookiecutter.project_slug}}/tests/test_placeholder.py delete mode 100644 {{cookiecutter.project_slug}}/tests/test_twentythree.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 270718a..c65d0b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,11 @@ +# pre-commit-config.yaml +# See https://pre-commit.com for docs and https://pre-commit.com/hooks.html for available hooks + +# ====================================================== +# ======= repository hooks ======== +# ====================================================== repos: +# Official pre-commit-hooks for general checks - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: @@ -6,8 +13,14 @@ repos: args: [--no-sort-keys, --indent=4, --autofix] - id: end-of-file-fixer - id: trailing-whitespace +# Format TOML and YAML files with pretty-format hooks - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.14.0 hooks: - id: pretty-format-yaml - args: [--autofix, --indent=4] + args: + - --autofix + - --indent + - '4' + stages: [pre-commit] + files: \.ya?ml$ diff --git a/LICENSE b/LICENSE index c856bad..604efec 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2022-2023, Saez Lab +Copyright (c) 2022-2025, Saez Lab All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md new file mode 100644 index 0000000..1dd62b3 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Python project template (Saez-Rodriguez Group) + +## Description + +This is a Cookiecutter template to create Python projects. It has been tailored by the [Saez-Rodriguez Group](https://saezlab.org/) at Universität Heidelberg. + +This template provides tools to streamline setup and maintenance, letting you focus on your project instead of getting bogged down by technical details. It includes: + +- Documentation + - [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/): A sleek, responsive theme for MkDocs documentation sites. + +- Code Quality/Automation + - [Pre-commit hooks](https://pre-commit.com/): A framework for managing and running code quality hooks before commits. + +- Release Management + - [Bump2version](https://github.com/c4urself/bump2version): A tool to automate version number management in your project. + +- Testing + - [Pytest](https://docs.pytest.org/en/stable/): A powerful testing framework for writing and running Python tests. + + +## Pre-requisites +> **Note:** We strongly recommend you have the following pre-requisites before using this template. + +| Pre-requisite | Description | +| ------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| [uv](https://docs.astral.sh/uv/getting-started/installation/) | A high-performance tool for managing Python packages and virtual environments. | +| [Cruft](https://cruft.github.io/cruft/#installation) | A CLI tool to scaffold new projects using customizable templates. | +| [GitHub CLI](https://github.com/cli/cli#installation) | A command-lIne tool to interact with GitHub repositories, issues, and workflows. | + +## How-to use this template? + +In six easy steps you will have a ready to use Python project with batteries included. + +**1. Generate Your Project from the Template** + + Run the following command and follow the prompts in your terminal: + ```bash + cruft create https://github.com/saezlab/python-project.git --checkout master + ``` + +**2. Navigate to Your New Project Directory** +```bash +cd # replace with the name of your project +``` + +**3. Set Up and Activate a Virtual Environment using `uv`** +```bash +uv venv .venv +source .venv/bin/activate +``` +> This creates and activates a lightweight virtual environment in `.venv`. + +**4. Install Project Dependencies listed in the `pyproject.toml` file** + + Install all required and optional dependencies (development, testing, docs): + ```bash + uv pip install ".[dev,tests,docs]" + ``` + +**5. Install and update pre-commit hooks** +```bash +git init +pre-commit install +pre-commit autoupdate +``` + +**6. Initialize Git and Push to GitHub** +```bash + +git add . +git commit -m "Initial commit" +gh repo create / --public --source=. --push +``` + + +🎉 Congratulations! Wishing you every success as you begin your project journey 🚀 + +Saez-Rodriguez Group Team! + +## Contributing + +Pull requests are welcome. For major changes, please open an issue first +to discuss what you would like to change. + +Please make sure to update tests as appropriate. + +## License + +Python-project template has a [BSD3](https://opensource.org/license/bsd-3-clause) license, as found in the [LICENSE](./LICENSE) file. diff --git a/README.rst b/README.rst deleted file mode 100644 index e339486..0000000 --- a/README.rst +++ /dev/null @@ -1,337 +0,0 @@ -####################### -Python project template -####################### - -.. note:: - - This document is better readable in `its Sphinx rendered version - `_. - -Create your new Python project by forking this repo. This way you start with -a number of helpful development tools set up for you. These tools are: - -* `Poetry build system `_ -* `Tox testing `_ -* `Pre-commit hooks `_ for formatting, linting, - not only Python but also YAML and rST -* `Tests with pytest `_ -* `Bump2version `_ version - increment tool -* Documentation build with `Sphinx `_ - -This repo is part of the effort towards improving development practices in -our group. For more information on these aims and the tools themselves, check -out the `Efficient development `_ page of the group Howto, and the Tools -page in `this spreadsheet `_ (or alternatively in the `saezbook `_). - -Setup -===== - -This template uses `Cookiecutter `, a tool to create projects from templates. To create a project, first -clone this repo: - -.. code:: bash - - git clone https://github.com/saezlab/python-project - -Then edit the ``cookiecutter.json`` to enter your project's metadata: - -.. code:: bash - - $EDITOR python-project/cookiecutter.json - -Finally, use ``cookiecutter`` to create your project: - -.. code:: bash - - cookiecutter python-project - -Your project will be created in a new directory, its name will depend on the -title of the project. - -Alternatively, a single command, interactive way of creating projects is -available: - -.. code:: bash - - cookiecutter gh:saezlab/python-project - -Then you'll be asked interactively for the metadata, and the final outcome will -be the same as with the previous method. - -Manual setup -============ - -Below you can read the original instructions about how to set up a project -manually from this template. This work can be done in a second with -``cookiecutter``, as presented above. We still keep the guide below because it -shows many important details about the tools included in the template. Also, -``poetry`` and ``tox`` you have to install unless these are already available -on your machine. - -Project name ------------- - -Find a name for your project. Below we use the placeholder ``new_project``. - -Install tools -------------- - -Make sure the necessary management tools are installed on your system: - -.. code:: bash - - curl -sSL https://install.python-poetry.org | python3 - - pip install --user tox pre-commit - -Copy the template ------------------ - -Clone the repo in your new project directory, and enter the directory: - -.. code:: bash - - git clone https://github.com/saezlab/python-project new_project - cd new_project - -Project git repo ----------------- - -Create a repo for your project and make the cloned repo point to this repo: - -.. code:: bash - - git remote set-url origin git@github.com:saezlab/new_project - -Rename it ---------- - -Change the placeholder ``project_name`` used in this template in all files. -Also rename the Python module directory. - -.. note:: - - If you are on a non-BSD system (e.g. GNU), remove the first argument for - ``sed -i``. That is the backup file extension, an argument that does not - exist on GNU systems. - -.. code:: bash - - find . -depth -type f ! -path '*/.git/*' -exec sed -i '' 's/project_name/new_project/g' {} + - git mv project_name new_project - git add -u - git commit -nm 'set project name' - -Add your name -------------- - -Change the author name and email in ``pyproject.toml`` and the headers of -all files in the module directory. Commit the changes. - -.. code:: bash - - find . -depth -type f ! -path '*/.git/*' -exec sed -i '' 's/Denes Turei/Your Name/g' {} + - find . -depth -type f ! -path '*/.git/*' -exec sed -i '' 's/turei\.denes@gmail\.com/your@email/g' {} + - git add -u - git commit -nm 'set author' - -Edit metadata -------------- - -In the ``pyproject.toml`` file edit the *Description, Repository* and *Bug -Tracker* fields. - -License -------- - -Change the license if necessary (by default it's GNU GPL v3). Copy over the -``LICENSE`` file with the text of your license and edit the license field in -``pyproject.toml``. Find a `list of licenses here -`_ and the `notation used by -Poetry here `_. Commit -the changes. E.g. if you want *MIT* license, copy `this text -`_ to the ``LICENSE`` file and do the -changes below: - -.. code:: bash - - find . -depth -type f ! -path '*/.git/*' -exec sed -i '' 's/GPLv3/MIT/g' {} + - sed -i '' 's/GPL-3\.0-only/MIT/g' pyproject.toml - git add LICENSE - git add pyproject.toml - git commit -nm 'set license to MIT' - -Set up the tools ----------------- - -Initialize ``poetry`` and ``tox``: - -.. code:: bash - - poetry update - poetry install - tox - git add -u - git commit -nm 'updated poetry lock' - -Edit the readme. If you prefer markdown over rST, replace it by a markdown -file and change the ``readme`` field under the ``tool.poetry`` section of -``pyproject.toml``. Commit the changes. - -Initialize ``pre-commit``. So far we run all commits with the ``-n`` switch -to disable hooks. If you skip this switch at your next commit, pre-commit -will come into action, install all the tools listed in -``.pre-commit-config.yaml``, and run them according to the settings. - -.. code:: bash - - pre-commit install - -.. note:: - - If you addressed errors pointed out by ``pre-commit``, run ``git add`` - again. ``pre-commit`` always runs on the staged state, if you don't - ``git add`` again, you will run it on the previously staged version of - the files. - -.. warning:: - - If you staged not all modified tracked files in your commit, ``pre-commit`` - will stash the unstaged ones. This is to run the checks on the contents - as it will be committed. In such cases do not interrupt the run of - ``pre-commit`` as then the unstaged changes remain stashed. - -Choose your code formatter --------------------------- - -In the config there are three code formatter set up but all disabled. These -are YAPF, Black and fixit. To enable one of them, remove the -``stages: [manual]`` from its hook. In this case the code formatter will run -and change your files upon each commit. If you prefer to run it only manually, -you can do it by the command below (in this example YAPF): - -.. code:: bash - - pre-commit run yapf --hook-stage manual - -Do not use two code formatters at the same time: one will do changes on your -file, the other will do different changes on the same line, and they will do -it back and forth just useless. Ultimately you will always commit the outcome -of the last code formatter. - -Set up your linter ------------------- - -In the ``tool.flake8`` section of ``pyproject.toml``, -add the codes of general or directory or file specific exceptions. In -code files for individual cases use the ``# noqa:`` tags. - -Rewrite the readme ------------------- - -Since you cloned the template repo, the ``README.rst`` has exactly the -contents that you're reading right now. Delete this whole content, add a -new main title, and add some contents about your new project, at least a -one sentence rationale. - -Docs with Sphinx ----------------- - -A Github action is set up to build and publish your documentation on Github. -Edit ``docs/src/index.rst``, the main page of your documentation. You can -decide to leave the current readme included or write a completely different -document in ``docs/src/index.rst``. - -Usage -===== - -Once you finished the setup above, you can start developing your project. -You can read more about the usage of each tool on their webpages. See below -a handful of the most important tasks: - -Do a commit without running pre-commit hooks --------------------------------------------- - -Use the ``-n`` switch: - -.. code:: bash - - git commit -nm 'commit message...' - -Run the tests -------------- - -With ``tox`` you can run the tests in an automatized way, potentially in -multiple environments. Calling ``tox`` runs everything that you set up in -``tox.ini``. - -.. code:: bash - - tox - -To run the tests directly via ``pytest``, simply do: - -.. code:: bash - - poetry run pytest -v - -Add a new dependency --------------------- - -First add the new third party dependency to the ``tool.poetry.dependencies`` -section of ``pyproject.toml``, by default with the ``"*"`` version -specification. Then let Poetry update the lock file and the virtual -environment. Finally, commit these changes. - -.. code:: bash - - poetry update - poetry install - git add -u - git commit -nm 'new dependency: some-package' - -Build the package ------------------ - -Poetry builds the package for you, by default it creates and ``sdist`` and -a ``whl``: - -.. code:: bash - - poetry build - -Poetry is also happy to publish your package on PyPI. You can get a PyPI API -token, configure Poetry to use it, and push your pacakge updates to PyPI: - -.. code:: bash - - poetry config pypi-token.pypi my-token - poetry publish - -Build the docs --------------- - -The docs are build automatically by the Github action after each push. To -build them also locally and manually: - -.. code:: bash - - poetry run make html --directory docs/ - -Why should I run everything by ``poetry run``? ----------------------------------------------- - -Poetry maintains a virtual environment for your project. By running commands -with ``poetry run ...``, you run them in this virtual environment, where all -the dependencies are installed, as defined in ``poetry.lock``, along with the -latest version of your project. It means you can run Python in the virtual -environment of your project, this way all the dependencies will be imported -from this environment, so their versions meet all the criteria defined by -you. - -.. code:: bash - - poetry run python diff --git a/cookiecutter.json b/cookiecutter.json index 0751ab9..3dc6fce 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -2,22 +2,64 @@ "project_name": "Project Name", "project_slug": "{{ cookiecutter.project_name|lower|replace(' ', '-') }}", "package_name": "{{ cookiecutter.project_slug|lower|replace('-', '_') }}", - "short_description": "A great new project", + "short_description": "A great new project.", "readme": "{{ cookiecutter.short_description }}", "author_full_name": "Your Name", "author_email": "your@email", "github_organization": "saezlab", "project_repo": "https://github.com/{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}", "license": [ - "GNU Lesser General Public License Version 3", - "Apache License Version 2.0", "MIT License", "BSD 2-Clause License", "BSD 3-Clause License", + "Apache License Version 2.0", "GNU General Public License Version 3", + "GNU Lesser General Public License Version 3", "ISC License", "Unlicense" ], + "_license": { + "MIT License": { + "license_short": "MIT", + "license_classifiers": "MIT License", + "license_spdx": "MIT" + }, + "BSD 2-Clause License": { + "license_short": "BSD-2-Clause", + "license_classifiers": "BSD License", + "license_spdx": "BSD-2-Clause" + }, + "BSD 3-Clause License": { + "license_short": "BSD-3-Clause", + "license_classifiers": "BSD License", + "license_spdx": "BSD-3-Clause" + }, + "Apache License Version 2.0": { + "license_short": "Apache-2.0", + "license_classifiers": "Apache Software License", + "license_spdx": "Apache-2.0" + }, + "GNU General Public License Version 3": { + "license_short": "GPL-3.0-or-later", + "license_classifiers": "GNU General Public License v3 (GPLv3)", + "license_spdx": "GPL-3.0-or-later" + }, + "GNU Lesser General Public License Version 3": { + "license_short": "LGPL-3.0-or-later", + "license_classifiers": "GNU Lesser General Public License v3 (LGPLv3)", + "license_spdx": "LGPL-3.0-or-later" + }, + "ISC License": { + "license_short": "ISC", + "license_classifiers": "ISC License (ISCL)", + "license_spdx": "ISC" + }, + "Unlicense": { + "license_short": "Unlicense", + "license_classifiers": "The Unlicense (Unlicense)", + "license_spdx": "Unlicense" + } + }, "python_version": [ "3.13", "3.12", @@ -31,38 +73,7 @@ "cookiecutter.extensions.TimeExtension" ], "_copy_without_render": [ + ".github/workflows/*.yml", ".github/workflows/**.yaml" - ], - "_licenses_short": { - "MIT License": "MIT", - "BSD 2-Clause License": "BSD-2-Clause", - "BSD 3-Clause License": "BSD-3-Clause", - "Apache License Version 2.0": "Apache-2.0", - "GNU General Public License Version 3": "GPL-3.0-or-later", - "GNU Lesser General Public License Version 3": "LGPL-3.0-or-later", - "ISC License": "ISC", - "Unlicense": "Unlicense" - }, - "_LICENSE_CLASSIFIERS": { - "MIT License": "MIT License", - "BSD 2-Clause License": "BSD License", - "BSD 3-Clause License": "BSD License", - "Apache License Version 2.0": "Apache Software License", - "GNU General Public License Version 3": "GNU General Public License v3 or later (GPLv3+)", - "GNU Lesser General Public License Version 3": "GNU Lesser General Public License v3 or later (LGPLv3+)", - "ISC License": "ISC License (ISCL)", - "Unlicense": "The Unlicense (Unlicense)" - }, - "_LICENSE_SPDX": { - "MIT License": "MIT", - "BSD 2-Clause License": "BSD-2-Clause", - "BSD 3-Clause License": "BSD-3-Clause", - "Apache License Version 2.0": "Apache-2.0", - "GNU General Public License Version 3": "GPL-3.0-or-later", - "GNU Lesser General Public License Version 3": "LGPL-3.0-or-later", - "ISC License": "ISC", - "Unlicense": "Unlicense" - }, - "_license_classifier": "{{ cookiecutter._LICENSE_CLASSIFIERS.get(cookiecutter.license) }}", - "_license_spdx": "{{ cookiecutter._LICENSE_SPDX.get(cookiecutter.license) }}" + ] } diff --git a/hooks/post_gen_project.sh b/hooks/post_gen_project.sh deleted file mode 100644 index 845e578..0000000 --- a/hooks/post_gen_project.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -# Update pre commit hooks -pre-commit autoupdate -c .pre-commit-config.yaml -pre-commit install diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py deleted file mode 100644 index 2f18e36..0000000 --- a/hooks/pre_gen_project.py +++ /dev/null @@ -1,22 +0,0 @@ -LICENSE_CLASSIFIERS = { - 'MIT': 'MIT License', - 'BSD-2-Clause': 'BSD License', - 'BSD 3-Clause': 'BSD License', - 'Apache-2.0': 'Apache Software License', - 'GPL-3.0-or-later': 'GNU General Public License v3 or later (GPLv3+)', - 'LGPL-3.0-or-later': 'GNU Lesser General Public License v3 or later (LGPLv3+)', - 'ISC': 'ISC License (ISCL)', - 'Unlicense': 'The Unlicense (Unlicense)', -} - -LICENSE_SPDX = { - 'MIT License': 'MIT', - 'BSD 2-Clause License': 'BSD-2-Clause', - 'BSD 3-Clause License': 'BSD-3-Clause', - 'Apache License Version 2.0': 'Apache-2.0', - 'GNU General Public License Version 3': 'GPL-3.0-or-later', - 'ISC License': 'ISC', - 'Unlicense': 'Unlicense', -} - -license = '{{ cookiecutter.license }}' diff --git a/hooks/pre_gen_project.sh b/hooks/pre_gen_project.sh deleted file mode 100644 index 26e5cc7..0000000 --- a/hooks/pre_gen_project.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -git init -b main . diff --git a/{{cookiecutter.project_slug}}/.github/workflows/ci.yaml b/{{cookiecutter.project_slug}}/.github/workflows/ci.yaml deleted file mode 100644 index e18b8b3..0000000 --- a/{{cookiecutter.project_slug}}/.github/workflows/ci.yaml +++ /dev/null @@ -1,105 +0,0 @@ -name: Test - -on: - schedule: - - cron: 0 3 * * * - push: - branches: [main] - tags: [v*] - pull_request: - branches: [main] - -jobs: - - tests: - - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash -e {0} - - strategy: - fail-fast: false - max-parallel: 4 - matrix: - os: [ubuntu-latest, macos-latest] - python: ['3.9', '3.10', '3.11', '3.12', '3.13'] - exclude: - - os: macos-latest - include: - - os: macos-latest - python: '3.13' - - env: - OS: ${{ matrix.os }} - PYTHON: ${{ matrix.python }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - python-version: ${{ matrix.python }} - - - name: Install Python {{ matrix.python }} - run: | - uv python install --python-preference only-managed ${{ matrix.python }} - - - name: Install dependencies - run: | - uv sync --all-extras - uv pip install codecov - uv tool install \ - --python-preferences only-managed \ - --python ${{ matrix.python }} \ - --with tox-uv \ - --with tox-gh \ - tox - - - name: Install pip dependencies - run: | - python -m pip install --upgrade pip - - - name: Run tests - env: - MPLBACKEND: agg - PLATFORM: ${{ matrix.os }} - DISPLAY: :42 - TOX_GH_MAJOR_MINOR: ${{ matrix.python }} - run: | - tox run -vv --skip-pkg-install - - - name: Upload coverage report to Codecov - if: success() - env: - CODECOV_NAME: ${{ matrix.python }}-${{ matrix.os }} - run: | - uv run codecovcli --verbose upload-process -t ${{ secrets.CODECOV_TOKEN }} -n '${{ env.CODECOV_NAME }}' -F unittests - - deploy: - - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: tests - runs-on: ubuntu-latest - - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - - name: Build and deploy - run: | - uv build - # uv publish # see https://docs.pypi.org/trusted-publishers/adding-a-publisher/ diff --git a/{{cookiecutter.project_slug}}/.github/workflows/code-quality.yml b/{{cookiecutter.project_slug}}/.github/workflows/code-quality.yml new file mode 100644 index 0000000..b31a3fb --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/code-quality.yml @@ -0,0 +1,26 @@ +# .github/workflows/code-quality.yml +name: Code Quality + +on: [push, pull_request] + +jobs: + code-quality-checks: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - name: Install Ruff + run: pip install ruff + + - name: Run Ruff (lint + formatting + import order) + run: ruff check . + + - name: Run Ruff format check (like Black) + run: ruff format --check . diff --git a/{{cookiecutter.project_slug}}/.github/workflows/docs.yml b/{{cookiecutter.project_slug}}/.github/workflows/docs.yml new file mode 100644 index 0000000..44ee751 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/docs.yml @@ -0,0 +1,45 @@ +# .github/workflows/docs.yml +name: Build MkDocs documentation + +on: + push: + branches: + - main + - master +permissions: + contents: write + +jobs: + build-documentation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Create virtualenv and install mkdocs + run: | + uv venv .venv + source .venv/bin/activate + uv pip install ".[docs]" + + - name: configure mkdocs-material cache + uses: actions/cache@v4 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + + - name: Build documentation with mkdocs + run: uv run mkdocs gh-deploy --force diff --git a/{{cookiecutter.project_slug}}/.github/workflows/security.yml b/{{cookiecutter.project_slug}}/.github/workflows/security.yml new file mode 100644 index 0000000..e196ead --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/security.yml @@ -0,0 +1,21 @@ +# .github/workflows/security.yml +name: Security Scan + +on: [push, pull_request] + +jobs: + security-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - name: Install Bandit + run: pip install bandit + + - name: Run Bandit + run: bandit -r . --exclude venv,.venv,.tox --skip B101 diff --git a/{{cookiecutter.project_slug}}/.github/workflows/sphinx_autodoc.yaml b/{{cookiecutter.project_slug}}/.github/workflows/sphinx_autodoc.yaml deleted file mode 100644 index 770a6c6..0000000 --- a/{{cookiecutter.project_slug}}/.github/workflows/sphinx_autodoc.yaml +++ /dev/null @@ -1,55 +0,0 @@ -name: Sphinx build docs on push -on: -- push - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Check out main - uses: actions/checkout@main - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.10.5 - - name: Load cached Poetry installation - uses: actions/cache@v2 - with: - path: ~/.local - key: poetry-0 - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - - name: Poetry disable modern-installation - run: poetry config installer.modern-installation false - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v2 - with: - path: .venv - key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - - name: Install library - run: poetry install --no-interaction - - name: Build documentation - run: poetry run make html --directory docs/ - - name: Commit files - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - touch docs/_build/html/.nojekyll - git add -f docs/_build/ - git commit -m "Update autodoc" -a - # using https://github.com/marketplace/actions/push-git-subdirectory-as-branch - - name: Deploy - uses: s0/git-publish-subdir-action@develop - env: - REPO: self - BRANCH: gh-pages - FOLDER: docs/_build/html - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/{{cookiecutter.project_slug}}/.github/workflows/test.yml b/{{cookiecutter.project_slug}}/.github/workflows/test.yml new file mode 100644 index 0000000..ce60b50 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/test.yml @@ -0,0 +1,33 @@ +# .github/workflows/test.yml +name: Testing [unit testing] + +on: [push, pull_request] + +jobs: + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Create virtualenv and install dependencies + run: | + uv venv .venv + source .venv/bin/activate + uv pip install ".[dev,tests,docs]" + # uv pip install pytest pytest-cov + + - name: Run tests with coverage + run: | + source .venv/bin/activate + # pytest --cov=your_package tests/ diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index 27c42c8..35de627 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -1,61 +1,97 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks +# pre-commit-config.yaml +# See https://pre-commit.com for docs and https://pre-commit.com/hooks.html for available hooks + +# ====================================================== +# ======= pre-commit configuration ======== +# ====================================================== fail_fast: false +minimum_pre_commit_version: 3.0.0 default_language_version: python: python3 default_stages: - pre-commit - pre-push -minimum_pre_commit_version: 3.0.0 + +# ====================================================== +# ======= repository hooks ======== +# ====================================================== +# UPDATE all the hooks regularly by running in the +# terminal: +# pre-commit autoupdate + repos: + # Fast Python linter and formatter with auto-fix support - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.8 # Check for latest version + rev: v0.11.13 hooks: - id: ruff args: [--fix, --show-fixes] + stages: [pre-commit] + files: \.py$ - id: ruff-format + stages: [pre-commit] + files: \.py$ + + # Go code cleaner (removes unused exports) - repo: https://github.com/deeenes/unexport - rev: 0.4.0-patch0-8 + rev: 0.4.0-patch0-3 hooks: - id: unexport args: [--refactor, --single_quotes] exclude: __init__.py$ + + # Official pre-commit-hooks for general checks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v5.0.0 # Check for updates regularly hooks: - - id: detect-private-key - - id: check-ast - - id: end-of-file-fixer - id: check-added-large-files - - id: mixed-line-ending - args: [--fix=lf] - exclude: ^docs/make.bat$ - - id: check-merge-conflict + stages: [pre-commit, pre-push] + - id: check-ast + stages: [pre-commit] - id: check-case-conflict + stages: [pre-commit] + - id: check-merge-conflict + stages: [pre-commit, pre-push] - id: check-symlinks + stages: [pre-commit] - id: check-yaml args: [--unsafe] - - id: check-ast + stages: [pre-commit] + files: \.ya?ml$ + - id: detect-private-key + stages: [pre-commit] + - id: end-of-file-fixer + stages: [pre-commit] + - id: mixed-line-ending + args: [--fix=lf] + exclude: ^docs/make.bat$ + stages: [pre-commit] - id: requirements-txt-fixer -- repo: https://github.com/rstcheck/rstcheck - rev: v6.2.4 - hooks: - - id: rstcheck - exclude: docs + stages: [pre-commit] + + # Format code blocks in documentation files - repo: https://github.com/asottile/blacken-docs rev: 1.19.1 hooks: - id: blacken-docs + stages: [pre-commit] + files: \.(md|rst)$ + + # Format TOML and YAML files with pretty-format hooks - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.14.0 hooks: - - id: pretty-format-yaml - args: [--autofix, --indent, '4'] - id: pretty-format-toml - args: [--autofix, --indent, '4'] -- repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.10.0 - hooks: - - id: rst-backticks - - id: rst-directive-colons - - id: rst-inline-touching-normal + args: + - --autofix + - --indent + - '4' + stages: [pre-commit] + files: \.toml$ + - id: pretty-format-yaml + args: + - --autofix + - --indent + - '4' + stages: [pre-commit] + files: \.ya?ml$ diff --git a/{{cookiecutter.project_slug}}/.readthedocs.yaml b/{{cookiecutter.project_slug}}/.readthedocs.yaml deleted file mode 100644 index 56363c4..0000000 --- a/{{cookiecutter.project_slug}}/.readthedocs.yaml +++ /dev/null @@ -1,22 +0,0 @@ -version: 2 - -sphinx: - builder: html - configuration: docs/source/conf.py - fail_on_warning: true - -formats: -- htmlzip -- pdf - -build: - os: ubuntu-22.04 - tools: - python: 3.10 - -python: - install: - - method: pip - path: . - extra_requirements: - - docs diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index 8a7c282..210fd34 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -1,11 +1,60 @@ +![project-banner](./docs/assets/project-banner-readme.png) + # {{ cookiecutter.project_name }} -[![Tests][badge-tests]][link-tests] -[![Documentation][badge-docs]][link-docs] +- [ ] TODO: Add badges to your project. + +[![Tests](https://img.shields.io/github/actions/workflow/status/saezlab/{{ cookiecutter.github_username }}/test.yml?branch=master)](https://github.com/saezlab/{{ cookiecutter.github_username }}/actions/workflows/test.yml) +[![Docs](https://img.shields.io/badge/docs-MkDocs-blue)](https://saezlab.github.io/{{ cookiecutter.github_username }}/) +![Pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit) +![PyPI](https://img.shields.io/pypi/v/{{ cookiecutter.github_username }}) +![Python](https://img.shields.io/pypi/pyversions/{{ cookiecutter.github_username }}) +![License](https://img.shields.io/github/license/saezlab/{{ cookiecutter.github_username }}) +![Issues](https://img.shields.io/github/issues/saezlab/{{ cookiecutter.github_username }}) +![Last Commit](https://img.shields.io/github/last-commit/saezlab/{{ cookiecutter.github_username }}) + +## Description + +{{ cookiecutter.short_description }} + +## Installation + +- [ ] TODO: Add installation instructions for your project, if applicable. + +```bash +# Example +pip install +``` + +## Usage + +- [ ] TODO: Add usage instructions for your project. + +```python +import foobar + +# returns 'words' +foobar.pluralize('word') + +# returns 'geese' +foobar.pluralize('goose') + +# returns 'phenomenon' +foobar.singularize('phenomena') +``` + +## Contributing + +Pull requests are welcome. For major changes, please open an issue first +to discuss what you would like to change. + +Please make sure to update tests as appropriate. + +- [ ] TODO: add contribution guidelines. All of them can be modified in the mkdocs documentation (./docs/community) + +## License -[badge-tests]: https://img.shields.io/github/actions/workflow/status/{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}/test.yaml?branch=main -[link-tests]: {{ cookiecutter.project_repo }}/actions/workflows/test.yml -[badge-docs]: https://img.shields.io/readthedocs/{{ cookiecutter.project_slug }} -[link-docs]: https://{{ cookiecutter.project_slug }}.readthedocs.io +[MIT](https://choosealicense.com/licenses/mit/) -{{ cookiecutter.readme }} +- [ ] TODO: Modify this based on the license you choose. +- [ ] TODO: Modify the LICENSE file based on the license you choose. diff --git a/{{cookiecutter.project_slug}}/docs/Makefile b/{{cookiecutter.project_slug}}/docs/Makefile deleted file mode 100644 index c7c0696..0000000 --- a/{{cookiecutter.project_slug}}/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = src -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/{{cookiecutter.project_slug}}/docs/about.md b/{{cookiecutter.project_slug}}/docs/about.md new file mode 100644 index 0000000..22e6574 --- /dev/null +++ b/{{cookiecutter.project_slug}}/docs/about.md @@ -0,0 +1,3 @@ +# About + +This project was created by {{ cookiecutter.author_full_name }}. diff --git a/{{cookiecutter.project_slug}}/docs/assets/project-banner-readme.png b/{{cookiecutter.project_slug}}/docs/assets/project-banner-readme.png new file mode 100644 index 0000000000000000000000000000000000000000..9f77947aefc2bedae47459500b433c71e25ee33a GIT binary patch literal 61255 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 or open a shell with `poetry shell` and then run commands directly. + +### Updating the environment + +If you want to fix dependency issues, please do so in the Poetry +framework. If Poetry does not work for you for some reason, please let us know. + +The Poetry dependencies are organized in groups. There are groups with +dependencies needed for running {{ cookiecutter.project_slug}} (`[tool.poetry.dependencies]` with the +group name `main`) and a group with dependencies needed for development +(`[tool.poetry.group.dev.dependencies]` with the group name `dev`). + +For adding new dependencies: + +- Add new dependencies via `poetry add`: +`poetry add --group `. This will update the `pyproject.toml` +and lock file automatically. + +- Add new dependencies via `pyproject.toml`: Add the dependency to the +`pyproject.toml` file in the correct group, including version. Then update the +lock file: `poetry lock` and install the dependencies: `poetry install`. + +## Code quality and formal requirements + +For ensuring code quality, the following tools are used: + +- [isort](https://isort.readthedocs.io/en/latest/) for sorting imports + +- [black](https://black.readthedocs.io/en/stable/) for automated code formatting + +- [pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks) for +ensuring some general rules + +- [pep585-upgrade](https://github.com/snok/pep585-upgrade) for automatically +upgrading type hints to the new native types defined in PEP 585 + +- [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) for ensuring some +general naming rules + +- [Ruff](https://docs.astral.sh/ruff/) An extremely fast Python linter +and code formatter, written in Rust + +We recommend configuring your IDE to execute Ruff on save/type, which will +automatically keep your code clean and fix some linting errors as you type. This +is made possible by the fast execution of Ruff and removes the need to run a +dedicated pre-commit step. For instance, in VSCode or Cursor, you can add this +to your `.vscode/settings.json`: + +```json +{ + "editor.formatOnType": true, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.ruff": "explicit", + "source.organizeImports.ruff": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff" +} +``` + +Alternatively, pre-commit hooks can be used to automatically or manually run +these tools before each commit. They are defined in `.pre-commit-config.yaml`. +To install the hooks run `poetry run pre-commit install`. The hooks are then +executed before each commit. For running the hook for all project files (not +only the changed ones) run `poetry run pre-commit run --all-files`. Our CI runs +the pre-commit hooks, so running them locally is a good way to check if your +code conforms to the formatting rules. + +## Testing + +The project uses [pytest](https://docs.pytest.org/en/stable/) for testing. To +run the tests, please run `pytest` in the root directory of the project. We are +developing {{ cookiecutter.project_slug}} using test-driven development. Please make sure that you +add tests for your code before submitting a pull request. + +The existing tests can also help you to understand how the code works. If you +have any questions, please feel free to ask them in the issue tracker or on +Zulip. + +**Before submitting a pull request, please make sure that all tests pass and +that the documentation builds correctly.** + +## Versioning + +We use [semantic versioning](https://semver.org/) for the project. This means +that the version number is incremented according to the following scheme: + +- Increment the major version number if you make incompatible API changes. + +- Increment the minor version number if you add functionality in a backwards- + compatible manner. Since we are still in the 0.x.y version range, most of the + significant changes will increase the minor version number. + +- Increment the patch version number if you make backwards-compatible bug fixes. + +We use the `bumpversion` tool to update the version number in the +`pyproject.toml` file. This will create a new git tag automatically. Usually, +versioning is done by the maintainers, so please do not increment versions in +pull requests by default. + +## Finding an issue to contribute to + +If you are brand new to {{ cookiecutter.project_slug}} or open-source development, we recommend +searching the GitHub "Issues" tab to find issues that interest you. Unassigned +issues labeled `Docs` and `good first` are typically good for newer contributors. + +Once you've found an interesting issue, it's a good idea to assign the issue to +yourself, so nobody else duplicates the work on it. + +If for whatever reason you are not able to continue working with the issue, +please unassign it, so other people know it's available again. If you want to +work on an issue that is currently assigned but you're unsure whether work is +actually being done, feel free to kindly ask the current assignee if you can +take over (please allow at least a week of inactivity before getting in touch). + + +## Submitting a Pull Request + +### Tips for a successful pull request + +To improve the chances of your pull request being reviewed, you should: + +- **Reference an open issue** for non-trivial changes to clarify the PR's purpose. +- **Ensure you have appropriate tests**. Tests should be the focus of any PR (apart from documentation changes). +- **Keep your pull requests as simple as possible**. Larger PRs take longer to review. +- **Ensure that CI is in a green state**. Reviewers may tell you to fix the CI before looking at anything else. + +### Version control, Git, and GitHub + +{{ cookiecutter.project_slug}} is hosted on GitHub, and to contribute, you will need to sign up for a +[free GitHub account](https://github.com/signup/free). We use +[Git](https://git-scm.com/) for version control to allow many people to work +together on the project. + +If you are new to Git, you can reference some of these resources for learning +Git. Feel free to reach out to the contributor community for help if needed: + +- [Git documentation](https://git-scm.com/doc). + + +The project follows a forking workflow further described on this page whereby +contributors fork the repository, make changes and then create a Pull Request. +So please be sure to read and follow all the instructions in this guide. + +If you are new to contributing to projects through forking on GitHub, take a +look at the [GitHub documentation for contributing to +projects](https://docs.github.com/en/get-started/quickstart/contributing-to-projects). +GitHub provides a quick tutorial using a test repository that may help you +become more familiar with forking a repository, cloning a fork, creating a +feature branch, pushing changes and making Pull Requests. + +Below are some useful resources for learning more about forking and Pull +Requests on GitHub: + +- the [GitHub documentation for forking a repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo). + +- the [GitHub documentation for collaborating with Pull Requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests). + +- the [GitHub documentation for working with forks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks). + +There are also many unwritten rules and conventions that are helpful in +interacting with other open-source contributors. These +[lessons](https://www.pyopensci.org/lessons/) from PyOpenSci are a good resource +for learning more about how to interact with other open-source contributors in +scientific computing. + +### Getting started with Git + +[GitHub has +instructions](https://docs.github.com/en/get-started/quickstart/set-up-git) for +installing git, setting up your SSH key, and configuring git. All these steps +need to be completed before you can work seamlessly between your local +repository and GitHub. + +### Create a fork of {{ cookiecutter.project_slug }} + +You will need your own fork of {{ cookiecutter.project_slug}}in order to eventually open a Pull +Request. Go to the {{ cookiecutter.project_slug}} project page and hit the Fork button. Please +uncheck the box to copy only the main branch before selecting Create Fork. You +will then want to clone your fork to your machine. + +```bash +git clone https://github.com/your-user-name/{{ cookiecutter.project_slug }}.git +cd {{ cookiecutter.project_slug }} +git remote add upstream https://github.com/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}.git +git fetch upstream +``` + +This creates the directory `{{ cookiecutter.project_slug }}` and connects your repository to the +upstream (main project) *{{ cookiecutter.project_slug }}* repository. They have the same name, but +your local repository and fork are separate from the upstream repository. + +### Creating a feature branch + +Your local `main` branch should always reflect the current state of {{ cookiecutter.project_slug}} +repository. First ensure it's up-to-date with the main {{ cookiecutter.project_slug}} repository. + +```bash +git checkout main +git pull upstream main --ff-only +``` + +Then, create a feature branch for making your changes. For example, we are going +to create a branch called `my-new-feature-for-{{ cookiecutter.project_slug }}` + +```bash +git checkout -b my-new-feature-for-{{ cookiecutter.project_slug }} +``` + +This changes your working branch from `main` to the +`my-new-feature-for-{{ cookiecutter.project_slug }}` branch. Keep any changes in this branch specific +to one bug or feature so it is clear what the branch brings to *{{ cookiecutter.project_slug}}*. You +can have many feature branches and switch between them using the `git +checkout` command. + +### Making code changes + +Before modifying any code, ensure you follow the contributing environment +guidelines to set up an appropriate development environment. + +When making changes, follow these {{ cookiecutter.project_slug}}-specific guidelines: + +1. Keep changes of that branch/PR focused on a single feature or bug fix. + +2. Follow roughly the [conventional commit message conventions](https://www.conventionalcommits.org/en/v1.0.0/). + +### Pushing your changes + +When you want your [committed](https://git-scm.com/docs/git-commit) changes to +appear publicly on your GitHub page, you can +[push](https://git-scm.com/docs/git-push) your forked feature branch's commits +to your forked repository on GitHub. + +Now your code is on GitHub, but it is not yet a part of the {{ cookiecutter.project_slug}} project. +For that to happen, a Pull Request (PR) needs to be submitted. + +### Opening a Pull Request (PR) + +If everything looks good according to the general guidelines, you are ready to +make a Pull Request. A Pull Request is how code from your fork becomes available +to the project maintainers to review and merge into the project to appear in the +next release. To submit a Pull Request: + +1. Navigate to your repository on GitHub. + +1. Click on the Compare & Pull Request button. + +1. You can then click on Commits and Files Changed to make sure everything looks okay one last time. + +1. Write a descriptive title that includes prefixes. {{ cookiecutter.project_slug}} uses a convention for title prefixes, most commonly, `feat:` for features, `fix:` for bug fixes, and `refactor:` for refactoring. + +1. Write a description of your changes in the `Preview Discussion` tab. This description will inform the reviewers about the changes you made, so please include all relevant information, including the motivation, implementation details, and references to any issues that you are addressing. + +1. Make sure to `Allow edits from maintainers`; this allows the maintainers to make changes to your PR directly, which is useful if you are not sure how to fix the PR. + +1. Click `Send Pull Request`. + +1. Optionally, you can assign reviewers to your PR, if you know who should review it. + +This request then goes to the repository maintainers, and they will review the code. + +### Updating your Pull Request + +Based on the review you get on your pull request, you will probably need to make +some changes to the code. You can follow the steps above again to address any +feedback and update your pull request. + +### Parallel changes in the upstream `main` branch + +In case of simultaneous changes to the upstream code, it is important that +these changes are reflected in your pull request. To update your feature +branch with changes in the {{ cookiecutter.project_slug}} `main` branch, run: + +```shell + + git checkout my-new-feature-for-{{ cookiecutter.project_slug }} + git fetch upstream + git merge upstream/main +``` + +If there are no conflicts (or they could be fixed automatically), a file with a +default commit message will open, and you can simply save and quit this file. + +If there are merge conflicts, you need to resolve those conflicts. See +[here](https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line/) +for an explanation on how to do this. + +Once the conflicts are resolved, run: + +1. `git add -u` to stage any files you've updated; +2. `git commit` to finish the merge. + +After the feature branch has been updated locally, you can now update your pull +request by pushing to the branch on GitHub: + +```shell + git push origin my-new-feature-for-{{ cookiecutter.project_slug }} +``` + +Any `git push` will automatically update your pull request with your branch's changes +and restart the `Continuous Integration` checks. diff --git a/{{cookiecutter.project_slug}}/docs/community/contribute-docs.md b/{{cookiecutter.project_slug}}/docs/community/contribute-docs.md new file mode 100644 index 0000000..6e48410 --- /dev/null +++ b/{{cookiecutter.project_slug}}/docs/community/contribute-docs.md @@ -0,0 +1,34 @@ +# Contributing to the documentation + +Contributing to the documentation benefits everyone who uses {{ cookiecutter.project_slug }}. We +encourage you to help us improve the documentation, and you don't have to be an +expert on {{ cookiecutter.project_slug }} to do so! In fact, there are sections of the docs that are +worse off after being written by experts. If something in the docs doesn't make +sense to you, updating the relevant section after you figure it out is a great +way to ensure it will help the next person. + + +## How to contribute to the documentation + +The documentation is written in **Markdown**, which is almost like writing in +plain English, and built using [Material for +MkDocs](https://squidfunk.github.io/mkdocs-material/). The simplest way to +contribute to the docs is to click on the `Edit` button (pen and paper) at the +top right of any page. This will take you to the source file on GitHub, where +you can make your changes and create a pull request using GitHub's web +interface (the `Commit changes...` button). + +Some other important things to know about the docs: + +- The {{ cookiecutter.project_slug }} documentation consists of two parts: the docstrings in the code + itself and the docs in the `docs/` folder. The docstrings provide a clear + explanation of the usage of the individual functions, while the documentation + website you are looking at is built from the `docs/` folder. + +- The docstrings follow a convention, based on the [Google Docstring + Standard](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings). + +- Our API documentation files in `docs/reference/source` contain the + instructions for the auto-generated documentation from the docstrings. For + classes, there are a few subtleties around controlling which methods and + attributes have pages auto-generated. diff --git a/{{cookiecutter.project_slug}}/docs/community/contribute.md b/{{cookiecutter.project_slug}}/docs/community/contribute.md new file mode 100644 index 0000000..dc2b9a6 --- /dev/null +++ b/{{cookiecutter.project_slug}}/docs/community/contribute.md @@ -0,0 +1,51 @@ +# How to Start Contributing + +There are many valuable ways to contribute besides writing code. Thank you for +dedicating your time to improve our project! + +## :octicons-issue-opened-24:{ .lg .middle } Bug reports and enhancement requests + +Bug reports and enhancement requests are an important part of making any +software more stable. We curate them though Github issues. When opening an +issue or request, please select the appropriate category and fill out the issue +form fully to ensure others and the core development team can fully understand +the scope of the issue. If your category is not listed, you can create a blank +issue. + +The issue will then show up to the {{ cookiecutter.project_slug}} community and be open to +comments/ideas from others. + +### Categories + +- [Bug Report](https://www.google.com): Report incorrect behavior in the {{ cookiecutter.project_slug}} library +- [Register New Component](https://www.google.com): Register a new component in the {{ cookiecutter.project_slug}} ecosystem, either one you have created, or one that you would like to see added +- Documentation Improvement: Report wrong or missing documentation +- Feature Request: Suggest an idea for {{ cookiecutter.project_slug}} + +## :octicons-checklist-24:{ .lg .middle } Detailed Guides + +
+ +- :octicons-book-24:{ .lg .middle } __Contributing to the Documentation__ + + --- + + A simple way to get started is to contribute to the documentation. Please + follow the guide [here](./contribute-docs.md) to learn how to do so. + + [:octicons-arrow-right-24: To the contribution guide](./contribute-docs.md) + +
+ +
+ +- :octicons-code-24:{ .lg .middle } __Contributing to the Code Base__ + + --- + + The best way to contribute code is to open a pull request on Github. Please + follow the guide [here](./contribute-codebase.md) to learn how to do so. + + [:octicons-arrow-right-24: To the contribution guide](./contribute-codebase.md) + +
diff --git a/{{cookiecutter.project_slug}}/docs/community/index.md b/{{cookiecutter.project_slug}}/docs/community/index.md new file mode 100644 index 0000000..9598a6f --- /dev/null +++ b/{{cookiecutter.project_slug}}/docs/community/index.md @@ -0,0 +1,25 @@ +Welcome to the {{ cookiecutter.project_slug}} community! We follow open-source principles and +encourage any sort of contribution. We communicate on GitHub, where we also +organise our projects. + +
+ +- :octicons-book-24:{ .lg .middle } __Where to Start__ + + --- + + If you'd like to learn how to contribute to our projects, please follow + the steps outlined in the contribution guide. + + [:octicons-arrow-right-24: To the contribution guide](contribute.md) + +
+ + +## Contributing Guidelines GitHub Links + +- [Contribution guidelines](https://github.com/{{ cookiecutter.project_slug}}/{{ cookiecutter.project_slug}}/blob/main/CONTRIBUTING.md) + +- [Code of Conduct](https://github.com/{{ cookiecutter.project_slug}}/{{ cookiecutter.project_slug}}/blob/main/CODE_OF_CONDUCT.md) + +- [Developer Guide](https://github.com/{{ cookiecutter.project_slug}}/{{ cookiecutter.project_slug}}/blob/main/DEVELOPER.md) diff --git a/{{cookiecutter.project_slug}}/docs/index.md b/{{cookiecutter.project_slug}}/docs/index.md new file mode 100644 index 0000000..ee53440 --- /dev/null +++ b/{{cookiecutter.project_slug}}/docs/index.md @@ -0,0 +1,3 @@ +# Welcome to {{ cookiecutter.project_name }} + +This is the main documentation page. diff --git a/{{cookiecutter.project_slug}}/docs/installation.md b/{{cookiecutter.project_slug}}/docs/installation.md new file mode 100644 index 0000000..c328c6c --- /dev/null +++ b/{{cookiecutter.project_slug}}/docs/installation.md @@ -0,0 +1,37 @@ +# Installation guide + +We strongly recommend installing a few prerequisites to ensure a smooth experience. These prerequisites are: + +1. *Python 3* (version >= 3.10) + - [Install Python 3](https://docs.python.org/3/using/index.html) +2. *Poetry* (Python packaging and dependency manager) + - [Install Poetry](https://python-poetry.org/docs/#installation) +3. *git* (version control manager) + - [Install git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +4. *Docker* (containerization technology) [optional] + - [Install Docker](https://docs.docker.com/engine/) + +!!! tip "Tip" + If you are missing any of those pre-requisites, **please follow the installation guide in each resource before you continue**. + + +## Checking prerequisites + +You can verify access to these components in your terminal: + +1. `Python` version 3.10 or higher. + ```bash + python --version + ``` +2. `Poetry` + ```bash + poetry --version + ``` +3. `git` + ```bash + git --version + ``` +4. `Docker` + ```bash + docker --version + ``` diff --git a/{{cookiecutter.project_slug}}/docs/learn/explanation/index.md b/{{cookiecutter.project_slug}}/docs/learn/explanation/index.md new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_slug}}/docs/learn/guides/index.md b/{{cookiecutter.project_slug}}/docs/learn/guides/index.md new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_slug}}/docs/learn/tutorials/quickstart.md b/{{cookiecutter.project_slug}}/docs/learn/tutorials/quickstart.md new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_slug}}/docs/learn/tutorials/tutorial0001_basics.md b/{{cookiecutter.project_slug}}/docs/learn/tutorials/tutorial0001_basics.md new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_slug}}/docs/make.bat b/{{cookiecutter.project_slug}}/docs/make.bat deleted file mode 100644 index 5b28825..0000000 --- a/{{cookiecutter.project_slug}}/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=src -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/{{cookiecutter.project_slug}}/docs/reference/source/{{ cookiecutter.package_name }}/_metadata-docs.md b/{{cookiecutter.project_slug}}/docs/reference/source/{{ cookiecutter.package_name }}/_metadata-docs.md new file mode 100644 index 0000000..4ad1f87 --- /dev/null +++ b/{{cookiecutter.project_slug}}/docs/reference/source/{{ cookiecutter.package_name }}/_metadata-docs.md @@ -0,0 +1,12 @@ +## Description + +The `_metadata.py` file defines the structures and logic for handling metadata associated with cached items in the `{{ cookiecutter.project_slug}}` package. It provides classes and helper functions to manage, store, and retrieve metadata fields, ensuring that each cache entry can be enriched with flexible, structured, and queryable information. This enables advanced search, filtering, and organization of cached data based on user-defined or system-generated metadata. + +### Main Components + +- **Metadata Function:** + Encapsulates the metadata for a cache item, providing methods to set, get, update, and validate metadata fields. + +--- + +::: {{ cookiecutter.package_name }}._metadata diff --git a/{{cookiecutter.project_slug}}/docs/src/conf.py b/{{cookiecutter.project_slug}}/docs/src/conf.py deleted file mode 100644 index e1493db..0000000 --- a/{{cookiecutter.project_slug}}/docs/src/conf.py +++ /dev/null @@ -1,125 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -from datetime import datetime -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import sys -import pathlib - -here = pathlib.Path(__file__).parent -sys.path.insert(0, str(here.parent)) - -import {{ cookiecutter.package_name }} # noqa: E402 - -# -- Project information ----------------------------------------------------- - -project = '{{ cookiecutter.package_name }}' -version = {{ cookiecutter.package_name }}.__version__ -author = ', '.join({{ cookiecutter.package_name }}.__author__) -years = '-'.join(sorted({'2022', f'{datetime.now():%Y}'})) -copyright = f'{years}, Saez Lab' -repository_url = 'https://github.com/{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}' - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named "sphinx.ext.*") or your custom -# ones. -extensions = [ - 'myst_parser', - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.autosummary', - 'sphinx.ext.napoleon', - 'sphinx.ext.todo', # not for output but to remove warnings - 'sphinx.ext.githubpages', - 'sphinx.ext.viewcode', - 'sphinx.ext.ifconfig', - 'sphinxcontrib.bibtex', - 'sphinx_autodoc_typehints', - 'sphinx.ext.mathjax', - 'sphinx_copybutton', - 'sphinx_last_updated_by_git', - 'sphinxcontrib.fulltoc', - 'sphinx_remove_toctrees', - 'nbsphinx', - 'IPython.sphinxext.ipython_console_highlighting', -] - -autosummary_generate = True -autodoc_member_order = 'groupwise' -default_role = 'literal' -napoleon_google_docstring = False -napoleon_numpy_docstring = True -napoleon_include_init_with_doc = False -napoleon_use_rtype = True # having a separate entry generally helps readability -napoleon_use_param = True - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = 'en' - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -source_suffix = ['.rst', '.md'] - -# The master toctree document. -master_doc = 'contents' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '**.ipynb_checkpoints'] - - -# -- Autodoc configuration --------------------------------------------------- - -autodoc_mock_imports = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'manni' - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. - -html_theme = 'pydata_sphinx_theme' -html_theme_options = { - 'navigation_depth': 2, - 'collapse_navigation': True, -} -html_context = { - 'display_github': True, # Integrate GitHub - 'github_user': '{{cookiecutter.github_organization}}', # Username - 'github_repo': project, # Repo name - 'github_version': 'main', # Version - 'conf_py_path': '/docs/', # Path in the checkout to the docs root -} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -nitpick_ignore = [ - # If building the documentation fails because - # of a missing link that is outside your control, - # you can add an exception to this list. - # ("py:class", "igraph.Graph"), -] diff --git a/{{cookiecutter.project_slug}}/docs/src/developer_docs.md b/{{cookiecutter.project_slug}}/docs/src/developer_docs.md deleted file mode 100644 index 3017a37..0000000 --- a/{{cookiecutter.project_slug}}/docs/src/developer_docs.md +++ /dev/null @@ -1,58 +0,0 @@ -## Pre-commit documentation - -[Pre-commit](https://pre-commit.com/) checks are fast programs that -check code for errors, inconsistencies and code styles, before the code -is committed. This is a brief documentation of pre-commits checks -pre-sets in the scverse-template. - -The following pre-commit checks for code style and format. - -- [black](https://black.readthedocs.io/en/stable/): standard code - formatter in Python. -- [autopep8](https://github.com/hhatto/autopep8): code formatter to - conform to [PEP8](https://peps.python.org/pep-0008/) style guide. -- [isort](https://pycqa.github.io/isort/): sort module imports into - sections and types. -- [prettier](https://prettier.io/docs/en/index.html): standard code - formatter for non-Python files (e.g. YAML). -- [blacken-docs](https://github.com/asottile/blacken-docs): black on - python code in docs. - -The following pre-commit checks for errors, inconsistencies and typing. - -- [flake8](https://flake8.pycqa.org/en/latest/): standard check for errors in Python files. - - [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports): - tidy module imports. - - [flake8-docstrings](https://github.com/PyCQA/flake8-docstrings): - pydocstyle extension of flake8. - - [flake8-rst-docstrings](https://github.com/peterjc/e8-rst-docstrings): - extension of `flake8-docstrings` for `rst` docs. - - [flake8-comprehensions](https://github.com/adamchainz/e8-comprehensions): - write better list/set/dict comprehensions. - - [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear): - find possible bugs and design issues in program. - - [flake8-blind-except](https://github.com/elijahandrews/flake8-blind-except): - checks for blind, catch-all `except` statements. -- [yesqa](https://github.com/asottile/yesqa): - remove unneccesary `# noqa` comments, follows additional dependencies listed above. - Not included in this template. -- [autoflake](https://github.com/PyCQA/autoflake): - remove unused imports and variables. Not included in this template. -- [pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks): generic pre-commit hooks. - - **detect-private-key**: checks for the existence of private keys. - - **check-ast**: check whether files parse as valid python. - - **end-of-file-fixer**:check files end in a newline and only a newline. - - **mixed-line-ending**: checks mixed line ending. - - **trailing-whitespace**: trims trailing whitespace. - - **check-case-conflict**: check files that would conflict with case-insensitive file systems. -- [pyupgrade](https://github.com/asottile/pyupgrade): - upgrade syntax for newer versions of the language. - -### Notes on pre-commit checks - -- **flake8**: to ignore errors, you can add a comment `# noqa` to the offending line. - You can also specify the error id to ignore with e.g. `# noqa: E731`. - Check [flake8 guide](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html) for reference. -- You can add or remove pre-commit checks by simply deleting relevant lines in the `.pre-commit-config.yaml` file. - Some pre-commit checks have additional options that can be specified either in the `pyproject.toml` or pre-commit - specific config files, such as `.prettierrc.yml` for **prettier** and `.flake8` for **flake8**. diff --git a/{{cookiecutter.project_slug}}/docs/src/index.rst b/{{cookiecutter.project_slug}}/docs/src/index.rst deleted file mode 100644 index a3d1c9a..0000000 --- a/{{cookiecutter.project_slug}}/docs/src/index.rst +++ /dev/null @@ -1,30 +0,0 @@ -############ -Introduction -############ - -Created from a project template. Please write the docs of your project here, -and remove the parts below (or edit ``README.rst`` in the project root). - -.. include:: ../../README.rst - -######### -Reference -######### - -{{ cookiecutter.project_name }} -=============================== - -.. automodule:: {{ cookiecutter.package_name }} - :members: - -########################### -Indices, Tables, and Search -########################### - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - - -.. toctree:: - :maxdepth: 4 diff --git a/{{cookiecutter.project_slug}}/docs/{{ cookiecutter.project_slug}}-project/design-philosophy.md b/{{cookiecutter.project_slug}}/docs/{{ cookiecutter.project_slug}}-project/design-philosophy.md new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_slug}}/docs/{{ cookiecutter.project_slug}}-project/project.md b/{{cookiecutter.project_slug}}/docs/{{ cookiecutter.project_slug}}-project/project.md new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_slug}}/docs/{{ cookiecutter.project_slug}}-project/use-cases.md b/{{cookiecutter.project_slug}}/docs/{{ cookiecutter.project_slug}}-project/use-cases.md new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_slug}}/mkdocs.yml b/{{cookiecutter.project_slug}}/mkdocs.yml new file mode 100644 index 0000000..e8f77be --- /dev/null +++ b/{{cookiecutter.project_slug}}/mkdocs.yml @@ -0,0 +1,107 @@ +site_name: '{{ cookiecutter.package_name }}' +site_url: https://saezlab.github.io/{{ cookiecutter.package_name }} +theme: + name: material + font: + text: Lato + code: Roboto Mono + features: + - content.code.copy + - content.action.edit + - navigation.tabs + - navigation.instant + - navigation.footer + palette: + - scheme: default + primary: teal + accent: light blue + toggle: + icon: material/toggle-switch + name: Switch to dark mode + - scheme: slate + primary: teal + accent: light blue + toggle: + icon: material/toggle-switch-off + name: Switch to light mode + +#======= Website Navigation settings ===== +nav: +- Home: index.md + +- About: + - Project: '{{ cookiecutter.project_slug }}-project/project.md' + - Design philosophy: '{{ cookiecutter.project_slug }}-project/design-philosophy.md' + - Use Cases: '{{ cookiecutter.project_slug }}-project/use-cases.md' + - About: about.md + +- Get Started: + - Installation: installation.md + - Quickstart: learn/tutorials/quickstart.md + +- Learn: + - Tutorials: + - Basics: learn/tutorials/tutorial0001_basics.md + - HowTo / FAQ: + - learn/guides/index.md + +- Explanations: + - learn/explanation/index.md + +- Reference: + - API Documentation: + - '{{ cookiecutter.package_name }}': + - _metadata: reference/source/{{ cookiecutter.package_name}}/_metadata-docs.md +- Community: + - Join Us: community/index.md + - Where to Start: community/contribute.md + - Contribute to the Documentation: community/contribute-docs.md + - Contribute to the Code Base: community/contribute-codebase.md + + + +#======= Extension settings (sorted alphabetically) ===== +markdown_extensions: +- admonition +- attr_list +- md_in_html + #----- Python Markdown Extensions +- pymdownx.details +- pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg +- pymdownx.highlight: # Note: list of Pygments: https://pygments.org/docs/lexers/ + anchor_linenums: true + line_spans: __span + pygments_lang_class: true +- pymdownx.inlinehilite +- pymdownx.snippets +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format +- pymdownx.tabbed: + alternate_style: true + +#======= Plugins ===== +plugins: +- search +- mkdocstrings: + default_handler: python + handlers: + python: + options: + annotations_path: brief + docstring_style: google + heading_level: 3 + modernize_annotations: true + show_category_heading: true + show_object_full_path: false + show_root_toc_entry: true + show_signature: true + show_signature_annotations: false + signature_crossrefs: true + + +copyright: © Copyright 2021-2025, Saez-lab development team. diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index ed57852..3215795 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -1,50 +1,62 @@ +# =================================== +# ======= BUILD ======== +# =================================== [build-system] -requires = ["hatchling"] build-backend = "hatchling.build" +requires = ["hatchling"] -{% if cookiecutter.project_name.lower().replace("-", "_") != cookiecutter.package_name -%} -[tool.hatch.build.targets.wheel] -packages = [ "{{ cookiecutter.package_name }}" ] - -{% endif -%} +#=================================== +#======= PROJECT ======== +#=================================== [project] -name = "{{ cookiecutter.project_slug }}" -version = "0.0.1" -description = "{{ cookiecutter.short_description }}" -license = "{{ cookiecutter._license_spdx }}" -maintainers = [ - "{{ cookiecutter.author_full_name }} <{{ cookiecutter.author_email }}>" -] authors = [ - { name = "{{ cookiecutter.author_full_name }}" }, -] -packages = [ - { include = "{{ cookiecutter.package_name }}" } + {name = "{{ cookiecutter.author_full_name }}", email="{{ cookiecutter.author_email }}"}, ] classifiers = [ + # How mature is this project? Common values are + # 2 - Pre-Alpha + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "Intended Audience :: Science/Research", - "License :: OSI Approved :: {{ cookiecutter._license_classifier }}", + "License :: OSI Approved :: {{ cookiecutter._license[cookiecutter.license].license_classifiers }}", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Scientific/Engineering :: Bio-Informatics" ] -readme = "README.md" -requires-python = ">={{ cookiecutter.python_version }}" dependencies = [ "toml", ] +description = "{{ cookiecutter.short_description }}" +license = "{{ cookiecutter._license[cookiecutter.license].license_spdx }}" +maintainers = [ + {name="{{ cookiecutter.author_full_name }}", email="{{ cookiecutter.author_email }}"}, +] +name = "{{ cookiecutter.project_slug }}" +# packages = [ +# { include = "{{ cookiecutter.package_name }}" } +# ] +readme = "README.md" +requires-python = ">={{ cookiecutter.python_version }}" +version = "0.0.1" [project.optional-dependencies] - -[dependency-groups] dev = [ "distlib", - "pre-commit>=2.17.0", + "pre-commit", "bump2version", "twine", ] +docs = [ + "mkdocs-material>=9.6.14", + "pymdown-extensions>=10.15", + "mkdocstrings[python]>=0.29.1,<0.30" +] +security = [ + "bandit" +] tests = [ "pytest>=6.0", "tox>=3.20.1", @@ -54,139 +66,52 @@ tests = [ "diff_cover", "ruff", ] -docs = [ - "sphinx>=5.0.0", - "sphinx-last-updated-by-git>=0.3", - "sphinx-autodoc-typehints>=1.18.0", - "sphinxcontrib-fulltoc>=1.2.0", - "sphinxcontrib-bibtex", - "sphinx-copybutton", - "myst-parser", - "myst-nb", - "jupyterlab", - "pydata-sphinx-theme", - "sphinx-remove-toctrees", - "jupyter-contrib-nbextensions", - "nbsphinx", -] - -[tool.uv.sources] -nbsphinx = { git = "https://github.com/deeenes/nbsphinx", branch = "timings" } -jupyter-contrib-nbextensions = { git = "https://github.com/deeenes/jupyter_contrib_nbextensions.git", branch = "master" } [project.urls] +Documentation = "https://{{ cookiecutter.github_organization }}.github.io/{{ cookiecutter.project_slug }}" Homepage = "{{ cookiecutter.project_repo }}" -Repository = "{{ cookiecutter.project_repo }}" Issues = "{{ cookiecutter.project_repo }}/issues" -Documentation = "https://{{ cookiecutter.project_slug }}.readthedocs.io/" - -[tool.ruff] -line-length = 80 -target-version = "py312" -unfixable = [ "UP" ] -extend-include = [ "*.ipynb" ] -format.docstring-code-format = true -format.trailing-comma = "all" -format.quote-style = "single" -lint.select = [ - "B", # flake8-bugbear - "BLE", # flake8-blind-except - "C4", # flake8-comprehensions - "D", # pydocstyle - "E", # Error detected by Pycodestyle - "F", # Errors detected by Pyflakes - "I", # isort - "RUF100", # Report unused noqa directives - "TID", # flake8-tidy-imports - "UP", # pyupgrade - "W", # Warning detected by Pycodestyle - "Q", # Consistent quotes - "S307", # eval() detection - "ANN", # flake8-annotations -] -lint.ignore = [ - "B008", # Errors from function calls in argument defaults. These are fine when the result is immutable. - "B024", - "D100", # Missing docstring in public module - "D104", # Missing docstring in public package - "D105", # __magic__ methods are often self-explanatory, allow missing docstrings - "D107", # Missing docstring in __init__ - # Disable one in each pair of mutually incompatible rules - "D200", - "D202", - "D203", # We don’t want a blank line before a class docstring - "D213", # <> We want docstrings to start immediately after the opening triple quote - "D400", # first line should end with a period [Bug: doesn’t work with single-line docstrings] - "D401", # First line should be in imperative mood; try rephrasing - "E131", - "E251", - "E303", - "E501", # line too long -> we accept long comment lines; formatter gets rid of long code lines - "E521", - "E731", # Do not assign a lambda expression, use a def -> lambda expression assignments are convenient - "E741", # allow I, O, l as variable names -> I is the identity matrix - "W503", - "W504", -] -lint.pydocstyle.convention = "numpy" -lint.quotes.inline-quotes = "single" +Repository = "{{ cookiecutter.project_repo }}" -[tool.ruff.lint.per-file-ignores] -"docs/*" = [ "I" ] -"docs/src/conf.py" = [ "D100" ] -"tests/*" = [ "D" ] -"tests/conftest.py" = [ "D101", "D102", "D103", "E402" ] -"*/__init__.py" = [ "D104", "F401" ] -[tool.ruff.lint.isort] -known-first-party = [ "{{ cookiecutter.package_name }}" ] -known-third-party = [ "numpy", "pandas" ] -sections = [ - "FUTURE", - "STDLIB", - "THIRDPARTY", - "NUM", - "FIRSTPARTY", - "LOCALFOLDER", -] -no-lines-before = [ "LOCALFOLDER" ] -lines-after-imports = 1 -combine-as-imports = true -force-sort-within-sections = true -case-sensitive = false -order-by-type = true -length-sort = true -force-wrap-aliases = true -use-parentheses = true -indent = " " -balanced-wrapping = true -include-trailing-comma = true -multi-line-output = 3 -exclude = [ - "docs/_build", +#=================================== +#======= TOOL ======== +#=================================== +#---- Bandit: tool for security analysis +[tool.bandit] +exclude_dirs = [".venv", "venv", ".tox", "build", "dist"] +skips = [ + "B101" # Name: assert_used ] -[tool.rstcheck] -report_level = "INFO" -ignore_directives = [ - "automodule", - "toctree", -] -ignore_roles = ["ref"] -ignore_messages = '(Unknown target name:.*|No (directive|role) entry for "(auto)?(class|method|property|function|func|mod|attr)" in module "docutils\.parsers\.rst\.languages\.en"\.)' - +#---- Coverage: a tool used to measure code coverage of Python programs [tool.coverage.run] -source = ["{{ cookiecutter.package_name }}"] omit = [ "**/test_*.py", ] +source = ["{{ cookiecutter.package_name }}"] -[tool.pytest.ini_options] -python_files = "test_*.py" -testpaths = [ +#---- Cruft: a tool to manage and update projects based on templates +[tool.cruft] +skip = [ "tests", + "{{ cookiecutter.package_name }}/__init__.py", + "{{ cookiecutter.package_name }}/_metadata.py", + "docs/api.md", + "docs/changelog.md", + "docs/references.bib", + "docs/references.md", + "docs/notebooks/example.ipynb", ] -xfail_strict = true + +{% if cookiecutter.project_name.lower().replace("-", "_") != cookiecutter.package_name -%} +#---- Hatch: a modern Python project manager for builds, environments, and publishing +[tool.hatch.build.targets.wheel] +packages = [ "{{ cookiecutter.package_name }}" ] +{% endif -%} + +#--- Pytest: a lightweight and scalable testing tool using the pytest framework +[tool.pytest.ini_options] addopts = [ # "-Werror", # if 3rd party libs raise DeprecationWarnings, just use filterwarnings below "--import-mode=importlib", # allow using test files with same name @@ -194,121 +119,214 @@ addopts = [ filterwarnings = [ # "ignore:.*U.*mode is deprecated:DeprecationWarning", ] +python_files = "test_*.py" +testpaths = [ + "tests", +] +xfail_strict = true +#---- RSTcheck: a tool to lint reStructuredText (.rst) files. +[tool.rstcheck] +ignore_directives = [ + "automodule", + "toctree", +] +ignore_messages = '(Unknown target name:.*|No (directive|role) entry for "(auto)?(class|method|property|function|func|mod|attr)" in module "docutils\.parsers\.rst\.languages\.en"\.)' +ignore_roles = ["ref"] +report_level = "INFO" + +#---- Ruff: a fast Python linter and formatter. +[tool.ruff] +extend-include = [ "*.ipynb" ] +line-length = 80 +target-version = "py312" + +[tool.ruff.format] +quote-style = "single" + +[tool.ruff.lint] +exclude = [ + "docs/_build" +] +ignore = [ + # flake8-bugbear (B) + "B008", # Name: function-call-in-default-argument + "B024", # Name: abstract-base-class-without-abstract-method + # pydocstyle (D) + "D100", # Name: undocumented-public-module + "D104", # Name: undocumented-public-package + "D105", # Name: undocumented-magic-method + "D107", # Name: undocumented-public-init + "D200", # Name: unnecessary-multiline-docstring + "D202", # Name: blank-line-after-function + "D203", # Name: incorrect-blank-line-before-class + "D213", # Name: multi-line-summary-second-line + "D400", # Name: missing-trailing-period + "D401", # Name: non-imperative-mood + # Error (E) + "E251", # Name: unexpected-spaces-around-keyword-parameter-equals + "E303", # Name: too-many-blank-lines + "E501", # Name: line too long + "E731", # Name: lambda-assignment + "E741" # allow I, O, l as variable names -> I is the identity matrix +] +select = [ + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "D", # pydocstyle + "E", # Error detected by Pycodestyle + "F", # Errors detected by Pyflakes + "I", # isort + "RUF100", # Report unused noqa directives + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # Warning detected by Pycodestyle + "Q", # Consistent quotes + "S307", # eval() detection + "ANN" # flake8-annotations +] +unfixable = ["UP"] + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "single" + +[tool.ruff.lint.isort] +case-sensitive = false +combine-as-imports = true +force-sort-within-sections = true +force-wrap-aliases = true +known-first-party = [ "{{ cookiecutter.package_name }}" ] +known-third-party = ["numpy", "pandas"] +length-sort = true +lines-after-imports = 1 +no-lines-before = ["local-folder"] +order-by-type = true +section-order = [ + "future", + "standard-library", + "third-party", + "first-party", + "local-folder" +] + +[tool.ruff.lint.per-file-ignores] +"*/__init__.py" = ["D104", "F401"] +"docs/*" = ["I"] +"docs/src/conf.py" = ["D100"] +"tests/*" = ["D"] +"tests/conftest.py" = ["D101", "D102", "D103", "E402"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +#---- tox: a tool to automate testing in multiple environments. [tool.tox] -min_version = "3.20.0" -isolated_build = true -skip_missing_interpreters = true envlist = [ "covclean", "lint", "py{39,310,311,312,313}", "coverage", "readme", - "docs", -] - -[tool.tox-gh-actions] -python = [ - "3.9: py39", - "3.10: py310", - "3.11: py311", - "3.12: py312", - "3.13: py313", -] -env = [ - "py313: covclean, lint, coverage, readme, docs", + "docs" ] +isolated_build = true +min_version = "3.20.0" +skip_missing_interpreters = true [tool.tox.envs] -platform = [ - "linux: linux", - "macos: (macos|osx|darwin)", -] base_python = [ "py39: python3.9", "py310: python3.10", "py311: python3.11", "py312: python3.12", - "py313: python3.13", + "py313: python3.13" +] +commands = [ + "pytest --cov --cov-append --cov-config={toxinidir}/.coveragerc --ignore docs/ {posargs:-vv {env:_PYTEST_TOX_POSARGS:}}" ] deps = [ - ".[tests]", + ".[tests]" ] passenv = "TOXENV,CI,CODECOV_*,GITHUB_ACTIONS" +platform = [ + "linux: linux", + "macos: (macos|osx|darwin)" +] usedevelop = true + +[tool.tox.envs.clean-docs] +allowlist_externals = ["make"] +changedir = "{toxinidir}/docs" commands = [ - "pytest --cov --cov-append --cov-config={toxinidir}/.coveragerc --ignore docs/ {posargs:-vv {env:_PYTEST_TOX_POSARGS:}}", + "make clean" ] - -[tool.tox.envs.py313] -setenv = "_PYTEST_TOX_POSARGS=--log-cli-level=ERROR" +description = "Clean the documentation artifacts." +skip_install = true [tool.tox.envs.covclean] -description = "Clean coverage files." -deps = [ ".[tests]" ] -skip_install = true commands = [ - "coverage erase", + "coverage erase" ] - -[tool.tox.envs.lint] -description = "Perform linting." -deps = [ ".[dev]" ] +deps = [".[tests]"] +description = "Clean coverage files." skip_install = true -commands = [ - "pre-commit run --all-files --show-diff-on-failure {posargs:}", -] [tool.tox.envs.coverage] -description = "Report the coverage difference." -deps = [ ".[tests]" ] -skip_install = true -depends = "py{39,310,311,312,313}" -parallel_show_output = true commands = [ "coverage report --omit=\"tox/*\"", "coverage xml --omit=\"tox/*\" -o {toxinidir}/coverage.xml", - "diff-cover --compare-branch origin/main {toxinidir}/coverage.xml", + "diff-cover --compare-branch origin/main {toxinidir}/coverage.xml" ] +depends = "py{39,310,311,312,313}" +deps = [".[tests]"] +description = "Report the coverage difference." +parallel_show_output = true +skip_install = true [tool.tox.envs.docs] -description = "Build the documentation." -skip_install = true -allowlist_externals = [ "uv" ] +allowlist_externals = ["uv"] commands = [ "uv sync --extra docs", "uv run sphinx-build --color -b html {toxinidir}/docs/source {toxinidir}/docs/build/html", - "python -c 'import pathlib; print(f\"Documentation is available under:\", pathlib.Path(f\"{toxinidir}\") / \"docs\" / \"build\" / \"html\" / \"index.html\")'", + "python -c 'import pathlib; print(f\"Documentation is available under:\", pathlib.Path(f\"{toxinidir}\") / \"docs\" / \"build\" / \"html\" / \"index.html\")'" ] - -[tool.tox.envs.clean-docs] -description = "Clean the documentation artifacts." +description = "Build the documentation." skip_install = true -changedir = "{toxinidir}/docs" -allowlist_externals = [ "make" ] + +[tool.tox.envs.lint] commands = [ - "make clean", + "pre-commit run --all-files --show-diff-on-failure {posargs:}" ] +deps = [".[dev]"] +description = "Perform linting." +skip_install = true + +[tool.tox.envs.py313] +setenv = "_PYTEST_TOX_POSARGS=--log-cli-level=ERROR" [tool.tox.envs.readme] -description = "Check if README renders on PyPI." -deps = [ ".[dev]" ] -skip_install = true -allowlist_externals = [ "uv" ] +allowlist_externals = ["uv"] commands = [ "uv build --wheel --out-dir {envtmpdir}/build", - "twine check {envtmpdir}/build/*", + "twine check {envtmpdir}/build/*" ] +deps = [".[dev]"] +description = "Check if README renders on PyPI." +skip_install = true -[tool.cruft] -skip = [ - "tests", - "{{ cookiecutter.package_name }}/__init__.py", - "{{ cookiecutter.package_name }}/_metadata.py", - "docs/api.md", - "docs/changelog.md", - "docs/references.bib", - "docs/references.md", - "docs/notebooks/example.ipynb", +[tool.tox-gh-actions] +env = [ + "py313: covclean, lint, coverage, readme, docs" +] +python = [ + "3.9: py39", + "3.10: py310", + "3.11: py311", + "3.12: py312", + "3.13: py313" ] + +[tool.uv.sources] +jupyter-contrib-nbextensions = {git = "https://github.com/deeenes/jupyter_contrib_nbextensions.git", branch = "master"} +nbsphinx = {git = "https://github.com/deeenes/nbsphinx", branch = "timings"} diff --git a/{{cookiecutter.project_slug}}/tests/test_placeholder.py b/{{cookiecutter.project_slug}}/tests/test_placeholder.py new file mode 100644 index 0000000..0210723 --- /dev/null +++ b/{{cookiecutter.project_slug}}/tests/test_placeholder.py @@ -0,0 +1,7 @@ +__all__ = [ + 'test_sum_numbers', +] + + +def test_sum_numbers() -> None: + assert 1 + 1 == 2 diff --git a/{{cookiecutter.project_slug}}/tests/test_twentythree.py b/{{cookiecutter.project_slug}}/tests/test_twentythree.py deleted file mode 100644 index d9c7d19..0000000 --- a/{{cookiecutter.project_slug}}/tests/test_twentythree.py +++ /dev/null @@ -1,10 +0,0 @@ -import {{ cookiecutter.package_name }} - -__all__ = ['Test23'] - - -class Test23: - - def test_twentythree(self): - - assert {{ cookiecutter.package_name }}.twentythree() == 23 diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/__init__.py index 545997e..fb3e796 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/__init__.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/__init__.py @@ -8,7 +8,7 @@ # # File author(s): {{ cookiecutter.author_full_name }} ({{ cookiecutter.author_email }}) # -# Distributed under the {{ cookiecutter._licenses_short[cookiecutter.license] }} license +# Distributed under the {{ cookiecutter._license[cookiecutter.license].license_short }} license # See the file `LICENSE` or read a copy at {{ '' }}{%- if cookiecutter.license == 'GNU General Public License Version 3' -%} @@ -41,9 +41,7 @@ {%- endif -%}{{ '' }} # -""" -{{ cookiecutter.short_description }} -""" +"""{{ cookiecutter.short_description }}""" __all__ = [ '__version__', diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/_metadata.py b/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/_metadata.py index 676fa27..d935aef 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/_metadata.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/_metadata.py @@ -8,7 +8,7 @@ # # File author(s): {{ cookiecutter.author_full_name }} ({{ cookiecutter.author_email }}) # -# Distributed under the {{ cookiecutter._licenses_short[cookiecutter.license] }} license +# Distributed under the {{ cookiecutter._license[cookiecutter.license].license_short }} license # See the file `LICENSE` or read a copy at {{ '' }}{%- if cookiecutter.license == 'GNU General Public License Version 3' -%} @@ -41,9 +41,7 @@ {%- endif -%}{{ '' }} # -""" -Package metadata (version, authors, etc). -""" +"""Package metadata (version, authors, etc).""" __all__ = ['get_metadata'] @@ -56,9 +54,8 @@ _VERSION = '0.0.1' -def get_metadata(): - """ - Basic package metadata. +def get_metadata() -> dict: + """Basic package metadata. Retrieves package metadata from the current project directory or from the installed package. @@ -91,7 +88,8 @@ def get_metadata(): try: meta = { - k.lower(): v for k, v in + k.lower(): + v for k, v in importlib.metadata.metadata(here.name).items() } @@ -107,4 +105,4 @@ def get_metadata(): metadata = get_metadata() __version__ = metadata.get('version', None) __author__ = metadata.get('author', None) -__license__ = metadata.get('license', None) +__license__ = '{{ cookiecutter._license[cookiecutter.license].license_short }}' From 1693bc6f15f325534b08f79d2f6d714bd3281d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20Carre=C3=B1o?= Date: Wed, 11 Jun 2025 14:39:02 +0200 Subject: [PATCH 2/4] chores: update all the GitHub Actions workflows inside the template, following best practices --- .../.github/actions/setup/action.yml | 21 +++++++++++ .../workflows/{docs.yml => ci-docs.yml} | 27 ++++---------- .../.github/workflows/ci-linting.yml | 16 +++++++++ .../{security.yml => ci-security.yml} | 9 ++--- .../.github/workflows/ci-testing-unit.yml | 36 +++++++++++++++++++ .../.github/workflows/code-quality.yml | 26 -------------- .../.github/workflows/test.yml | 33 ----------------- {{cookiecutter.project_slug}}/pyproject.toml | 1 + 8 files changed, 82 insertions(+), 87 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/.github/actions/setup/action.yml rename {{cookiecutter.project_slug}}/.github/workflows/{docs.yml => ci-docs.yml} (51%) create mode 100644 {{cookiecutter.project_slug}}/.github/workflows/ci-linting.yml rename {{cookiecutter.project_slug}}/.github/workflows/{security.yml => ci-security.yml} (69%) create mode 100644 {{cookiecutter.project_slug}}/.github/workflows/ci-testing-unit.yml delete mode 100644 {{cookiecutter.project_slug}}/.github/workflows/code-quality.yml delete mode 100644 {{cookiecutter.project_slug}}/.github/workflows/test.yml diff --git a/{{cookiecutter.project_slug}}/.github/actions/setup/action.yml b/{{cookiecutter.project_slug}}/.github/actions/setup/action.yml new file mode 100644 index 0000000..d86a42f --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/actions/setup/action.yml @@ -0,0 +1,21 @@ +name: Setup Python and Install Dependencies +description: Sets up Python and installs dependencies using uv +runs: + using: composite + steps: + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + - name: Install uv + shell: bash + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Create virtualenv and install dependencies + shell: bash + run: | + uv venv .venv + source .venv/bin/activate + uv pip install ".[dev,tests,docs]" +inputs: + python-version: + required: true + description: Python version to use in the matrix. diff --git a/{{cookiecutter.project_slug}}/.github/workflows/docs.yml b/{{cookiecutter.project_slug}}/.github/workflows/ci-docs.yml similarity index 51% rename from {{cookiecutter.project_slug}}/.github/workflows/docs.yml rename to {{cookiecutter.project_slug}}/.github/workflows/ci-docs.yml index 44ee751..8901506 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/docs.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/ci-docs.yml @@ -1,11 +1,9 @@ -# .github/workflows/docs.yml name: Build MkDocs documentation on: push: - branches: - - main - - master + branches: [main, master] + permissions: contents: write @@ -18,28 +16,15 @@ jobs: run: | git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - uses: actions/setup-python@v5 + - uses: ./.github/actions/setup with: - python-version: 3.x - - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - - - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - - - name: Create virtualenv and install mkdocs - run: | - uv venv .venv - source .venv/bin/activate - uv pip install ".[docs]" - + python-version: '3.12' - name: configure mkdocs-material cache uses: actions/cache@v4 with: - key: mkdocs-material-${{ env.cache_id }} + key: mkdocs-material-${{ github.run_id }} path: .cache restore-keys: | mkdocs-material- - - name: Build documentation with mkdocs - run: uv run mkdocs gh-deploy --force + run: .venv/bin/mkdocs gh-deploy --force diff --git a/{{cookiecutter.project_slug}}/.github/workflows/ci-linting.yml b/{{cookiecutter.project_slug}}/.github/workflows/ci-linting.yml new file mode 100644 index 0000000..15ffdf8 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/ci-linting.yml @@ -0,0 +1,16 @@ +name: Linting + +on: [push, pull_request] + +jobs: + linting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + with: + python-version: '3.13' + - name: Run Ruff (lint + formatting + import order) + run: .venv/bin/ruff check . + - name: Run Ruff format check (like Black) + run: .venv/bin/ruff format --check . diff --git a/{{cookiecutter.project_slug}}/.github/workflows/security.yml b/{{cookiecutter.project_slug}}/.github/workflows/ci-security.yml similarity index 69% rename from {{cookiecutter.project_slug}}/.github/workflows/security.yml rename to {{cookiecutter.project_slug}}/.github/workflows/ci-security.yml index e196ead..21f332a 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/security.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/ci-security.yml @@ -1,4 +1,3 @@ -# .github/workflows/security.yml name: Security Scan on: [push, pull_request] @@ -8,14 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: - python-version: 3.x - + python-version: '3.12' - name: Install Bandit run: pip install bandit - - name: Run Bandit run: bandit -r . --exclude venv,.venv,.tox --skip B101 diff --git a/{{cookiecutter.project_slug}}/.github/workflows/ci-testing-unit.yml b/{{cookiecutter.project_slug}}/.github/workflows/ci-testing-unit.yml new file mode 100644 index 0000000..68a468e --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/ci-testing-unit.yml @@ -0,0 +1,36 @@ +name: CI testing [unit testing] + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + with: + python-version: '3.13' + - name: Run Ruff (lint + formatting + import order) + run: .venv/bin/ruff check . + - name: Run Ruff format check (like Black) + run: .venv/bin/ruff format --check . + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12', '3.13'] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + with: + python-version: ${{ matrix.python-version }} + - name: Run tests with coverage + run: | + source .venv/bin/activate + pytest --cov=omnigraph tests/ + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: .coverage* diff --git a/{{cookiecutter.project_slug}}/.github/workflows/code-quality.yml b/{{cookiecutter.project_slug}}/.github/workflows/code-quality.yml deleted file mode 100644 index b31a3fb..0000000 --- a/{{cookiecutter.project_slug}}/.github/workflows/code-quality.yml +++ /dev/null @@ -1,26 +0,0 @@ -# .github/workflows/code-quality.yml -name: Code Quality - -on: [push, pull_request] - -jobs: - code-quality-checks: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Install Ruff - run: pip install ruff - - - name: Run Ruff (lint + formatting + import order) - run: ruff check . - - - name: Run Ruff format check (like Black) - run: ruff format --check . diff --git a/{{cookiecutter.project_slug}}/.github/workflows/test.yml b/{{cookiecutter.project_slug}}/.github/workflows/test.yml deleted file mode 100644 index ce60b50..0000000 --- a/{{cookiecutter.project_slug}}/.github/workflows/test.yml +++ /dev/null @@ -1,33 +0,0 @@ -# .github/workflows/test.yml -name: Testing [unit testing] - -on: [push, pull_request] - -jobs: - unit-tests: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - - - name: Create virtualenv and install dependencies - run: | - uv venv .venv - source .venv/bin/activate - uv pip install ".[dev,tests,docs]" - # uv pip install pytest pytest-cov - - - name: Run tests with coverage - run: | - source .venv/bin/activate - # pytest --cov=your_package tests/ diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 3215795..917b298 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -59,6 +59,7 @@ security = [ ] tests = [ "pytest>=6.0", + "pytest-cov", "tox>=3.20.1", "tox-gh>=1.5.0", "coverage>=6.0", From 764bb01b9d808c5bffceaf69549fedc11acb6f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20Carre=C3=B1o?= Date: Wed, 11 Jun 2025 14:48:21 +0200 Subject: [PATCH 3/4] fix: correct badges links based on cookiecutter form --- {{cookiecutter.project_slug}}/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index 210fd34..fcea5b4 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -4,14 +4,14 @@ - [ ] TODO: Add badges to your project. -[![Tests](https://img.shields.io/github/actions/workflow/status/saezlab/{{ cookiecutter.github_username }}/test.yml?branch=master)](https://github.com/saezlab/{{ cookiecutter.github_username }}/actions/workflows/test.yml) -[![Docs](https://img.shields.io/badge/docs-MkDocs-blue)](https://saezlab.github.io/{{ cookiecutter.github_username }}/) +[![Tests](https://img.shields.io/github/actions/workflow/status/{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}/test.yml?branch=master)](https://github.com/{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}/actions/workflows/test.yml) +[![Docs](https://img.shields.io/badge/docs-MkDocs-blue)](https://saezlab.github.io/{{ cookiecutter.project_slug }}/) ![Pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit) -![PyPI](https://img.shields.io/pypi/v/{{ cookiecutter.github_username }}) -![Python](https://img.shields.io/pypi/pyversions/{{ cookiecutter.github_username }}) -![License](https://img.shields.io/github/license/saezlab/{{ cookiecutter.github_username }}) -![Issues](https://img.shields.io/github/issues/saezlab/{{ cookiecutter.github_username }}) -![Last Commit](https://img.shields.io/github/last-commit/saezlab/{{ cookiecutter.github_username }}) +![PyPI](https://img.shields.io/pypi/v/{{ cookiecutter.project_slug }}) +![Python](https://img.shields.io/pypi/pyversions/{{ cookiecutter.project_slug }}) +![License](https://img.shields.io/github/license/{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}) +![Issues](https://img.shields.io/github/issues/{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}) +![Last Commit](https://img.shields.io/github/last-commit/{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}) ## Description From 87874600b0d555c2fd4024f7ba0573b90a9f9395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20Carre=C3=B1o?= Date: Wed, 11 Jun 2025 14:51:30 +0200 Subject: [PATCH 4/4] fix: add .github/actions/* to the list of copy without render configuration --- cookiecutter.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cookiecutter.json b/cookiecutter.json index 3dc6fce..3cf82e6 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -74,6 +74,7 @@ ], "_copy_without_render": [ ".github/workflows/*.yml", - ".github/workflows/**.yaml" + ".github/workflows/**.yaml", + ".github/actions/*" ] }