diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..97c0f19f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "template/{% if docs_template == 'sphinx-fhg-iis' %}docs{% endif %}"] + path = "template/{% if docs_template == 'sphinx-fhg-iis' %}docs{% endif %}" + url = https://git01.iis.fhg.de/mkj/sphinx_template diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 90ec3c67..88a2b1a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,7 @@ repos: hooks: - id: prettier types_or: [json, yaml, css, javascript] + pass_filenames: false # black should have the final say on python formatting, so it comes last - repo: https://github.com/psf/black rev: 23.9.1 @@ -22,7 +23,7 @@ repos: hooks: - id: pytest name: pytest - entry: pytest -n auto -m "not slow" + entry: make test language: system pass_filenames: false files: "^template/" diff --git a/CHANGELOG.md b/CHANGELOG.md index 21aa5e1b..b2d29f0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +### Added + +- github ci now runs tests, collects coverage and creates maintainability and coverage badges +- add [sphinx_template](https://git01.iis.fhg.de/sch/sphinx_template/) as an option when choosing sphinx for documentation + +### Changed + +- template now uses a static documentation badge provided by shields.io + +### Fixed + +- link to pipeline in README now correctly links to github actions +- when bumpversion is selected, add `bump2version` to dev dependencies + ## [0.0.2] - 2023-09-19 ### Added diff --git a/Makefile b/Makefile index 74b27ea2..2cc26223 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ DOC_EXAMPLES = docs/examples/mkdocs docs/examples/sphinx docs/examples/default d examples: ## build all published examples examples: $(PUBLISHED_EXAMPLES) -COPIER_ARGS?=--trust +COPIER_ARGS?=--trust --vcs-ref=HEAD COPIER_DEFAULT_VALUES=-d "project_name=Sample Project" -d "package_name=sample_project" build/examples/%: COPIER_DEFAULT_VALUES += --defaults build/examples/%: EXAMPLE_DIR:=$@ @@ -99,10 +99,12 @@ spellcheck-dump: ## save all flagged words to project terms dictionary .PHONY: test -PYTEST_ARGS=-n auto -test: ## run tests quickly +PYTEST_ARGS?= +test: ## run some tests +test: build-clean copy-template pytest ${PYTEST_ARGS} -m "not slow" test-all: ## run all tests +test-all: build-clean copy-template pytest ${PYTEST_ARGS} @@ -123,7 +125,7 @@ copy-template: @cp -r ${TEMPLATE_SRC} ${TEMPLATE_DEST} @cp copier.yaml ${PKGDIR}/. build-clean: ## remove build artifacts - rm -rf ${BUILDDIR} ${PKGDIR}/template ${PKGDIR}/copier.yaml + @rm -rf ${BUILDDIR} ${PKGDIR}/template ${PKGDIR}/copier.yaml .PHONY: release release-test release-tag release-pypi release-github release: release-test release-tag build release-pypi release-github diff --git a/README.md b/README.md index ea2bfa9a..1a1098ad 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Python Project Template

-[![](https://img.shields.io/badge/python-3.11-blue)][sample project] +[![](https://img.shields.io/badge/Documentation-main-blue)][docs] [![](https://img.shields.io/badge/Example-Sample_Project-blue)][sample project] [![PyPI - Version](https://img.shields.io/pypi/v/init-python-project)][pypi] @@ -63,6 +63,7 @@ init-python-project The first part of the user guide consists of tutorials on how to answer the template questions for [Your First Project][], what [Next Steps][] there are after your project is created and why the [Project Structure][] looks like it does. +[docs]: https://jannismain.github.io/python-project-template/ [your first project]: https://jannismain.github.io/python-project-template/user-guide/first-project [next steps]: https://jannismain.github.io/python-project-template/user-guide/first-project [project structure]: https://jannismain.github.io/python-project-template/user-guide/project-structure diff --git a/copier.yaml b/copier.yaml index e0d62d66..bb0ba671 100644 --- a/copier.yaml +++ b/copier.yaml @@ -98,6 +98,17 @@ docs: If you are not sure which one to use, simply go with the default 😉. +docs_template: + type: str + choices: + Fraunhofer IIS Sphinx Template: sphinx-fhg-iis + None: none + default: "none" + when: "{{ docs == 'sphinx' }}" + help: Which documentation template do you want to use? + explanation: | + See [Fraunhofer IIS Sphinx Template](https://git01.iis.fhg.de/sch/sphinx_template/). + remote: choices: GitHub: github diff --git a/pyproject.toml b/pyproject.toml index 2ff6b577..eea793b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "init-python-project" -description = "A python project generator based on copier." +description = "A python project generator." readme = "README.md" requires-python = ">=3.11" authors = [{ name = "Jannis Mainczyk", email = "jmainczyk@gmail.com" }] @@ -50,9 +50,10 @@ init-python-project = "init_python_project.cli:app" [tool.pytest.ini_options] minversion = "6.0" -addopts = "-ra -q" +addopts = "-ra -q -n auto --exitfirst" testpaths = ["tests"] markers = ["slow"] +filterwarnings = ["ignore::copier.vcs.DirtyLocalWarning"] [tool.black] line-length = 100 diff --git a/src/init_python_project/copier.yaml b/src/init_python_project/copier.yaml deleted file mode 100644 index e0d62d66..00000000 --- a/src/init_python_project/copier.yaml +++ /dev/null @@ -1,170 +0,0 @@ -_subdirectory: template - -project_name: - type: str - help: What is the name of your project? - placeholder: Sample Project - expected: Title case string, can contain spaces - explanation: | - The project name will be used to propose a suitable package name and [Remote Url][remote-url]. - It is also repeated in multiple places (Python package configuration, documentation, etc.). - -package_name: - type: str - help: What is the name of your Python package? - default: "{{ project_name|lower|replace(' ', '_')|replace('-', '_') }}" - expected: Lowercase string, can contain underscores - explanation: | - This is the name of your Python package. As such, it will be used as the name of the directory containing your code. - All imports of your package start with this name. - - Example: If you choose `sample_project` as your package name, your code files would be created in - - ``` - ./ - ├── src - │ └── sample_project - │ ├── __init__.py - │ ├── __main__.py - │ └── some_module.py - ├── pyproject.toml - └── ... - ``` - - and your imports would look like this: - - ```python - from sample_project import some_module - ``` - - You might want to consult [PEP 423 – Naming conventions and recipes related to packaging](https://peps.python.org/pep-0423/) - for guidance on Python package name conventions. - - The cli command included with this template will be named after your package, only with dashes instead of underscores. - Following the example above, your cli would then be available as - - ```console - $ sample-project --help - ``` - - If you ever want to publish your Python package to [PyPI](https://pypi.org), the package name has to be unique to be accepted there. - - Finally, the package name is repeated across multiple configuration and documentation files. - -use_precommit: - type: bool - default: true - help: Use pre-commit to run checks on each commit? - explanation: | - [pre-commit](../reference/tooling/pre-commit.md) is a tool that makes configuring [git hooks][git-hooks] a lot easier. - - This template uses pre-commit hooks to ensure, all changes match the expected formatting and style. - Additionally, running linters at this stage can prevent committing something that contains obvious issues. - - Most formatters and some linters are able to fix issues automatically. - So you can simply review the changes those tools made, stage them and commit again. - -use_bumpversion: - type: bool - default: false - help: Use bumpversion to manage semantic version across multiple files? - explanation: | - If you want to version your project, [bumpversion][] provides an easy way to increase version numbers across multiple files. - - It integrates with git and helps with your release workflow by automating version bump, commit and tag creation. - Used correctly, releasing a new version of your project can be done with a single command. - - It also helps to follow [semantic versioning][semantic-versioning] guidelines when increasing your version numbers. - -docs: - type: str - choices: - Material for MkDocs: mkdocs - Sphinx: sphinx - None: none - default: "mkdocs" - help: Which documentation tool do you want to use? - explanation: | - [Documentation][documentation] is an important part of any project. - - So far, this template supports two documentation tools: [MkDocs][mkdocs] and [Sphinx][sphinx]. - - [MkDocs][mkdocs] is a great documentation tool built around [Markdown][markdown]. - It is easy to use and produces a great looking documentation website with minimal overhead and configuration. - - [Sphinx][sphinx] is a powerful tool for documentation written in [Markdown][markdown] or [reStructuredText][restructuredtext]. - With the [`myst_parser`][myst_parser] it is now (almost) possible to rely on Markdown only. - Choose this if you want to publish your documentation in multiple formats (e.g. PDF, HTML, ePub, ...). - - If you are not sure which one to use, simply go with the default 😉. - -remote: - choices: - GitHub: github - FHG Gitlab: gitlab-fhg - IIS Gitlab: gitlab-iis - help: Which platform will your project be hosted on? - default: "github" - explanation: | - This template provides a CI configuration for either GitHub (Github Actions) or GitLab (Gitlab CI). - - Also, the link to your documentation depends on which remote you choose. - - If you want to push your project to multiple remotes, you can add them later. - -user_name: - type: str - help: User name (the one you used with your hosted git provider) - explanation: | - This is the user name you are using with the remote provider you provided in the previous question. - - Together with [remote][], it will be used to suggest a [remote url][remote-url] for your project. - -remote_url: - type: str - help: URL of the remote repository - default: git@{% if remote=='github' %}github.com{% elif remote=='gitlab-iis' %}git01.iis.fhg.de{% elif remote=='gitlab-fhg' %}gitlab.cc-asp.fraunhofer.de{% endif %}:{{user_name}}/{{project_name | lower | replace(' ', '-')}}.git - expected: SSH URL to your remote repository - explanation: | - Apart from configuring the git remote for you, the remote URL is required to determine other values, such as - - - Links in your README and CHANGELOG files - - Links to your documentation - - Link to your repository from your documentation - - ... - - If you did not create your remote repository yet (i.e. new project at GitHub or Gitlab), this might be a good time to do so. - - !!! tip "Create empty repository" - - Do not initialize your remote project with a LICENSE or README file. - This will let you push your initial commit without merge or rebase. - - === "Gitlab" - - ![](https://cln.sh/gwzwgtHH+) - *The SSH URL to your Gitlab repository can be found under the `Clone` dropdown (screenshot taken 23.08.23)*{.caption} - - - === "GitHub" - - ![](https://cln.sh/SscJVB6N+) - *The SSH URL to your GitHub repository can be found under the `<> Code` dropdown menu (screenshot taken 23.08.23)*{.caption} - -default_branch: - type: str - default: main - help: Name of the initial git branch that will be created - expected: Lowercase string, can contain dashes - examples: [main, master, dev] - explanation: | - Name of the [initial branch][] of your new project. - - This branch traditionally was called `master`, but is more often called `main` now. - - [initial branch]: https://git-scm.com/docs/git-init#Documentation/git-init.txt---initial-branchltbranch-namegt -_tasks: - - "rm -rf context" - - "git init --initial-branch={{default_branch}}" - - "git remote add origin {{remote_url}} || true" - - "{% if use_precommit %}pre-commit install || echo 'Error during installation of pre-commit hooks. Is pre-commit installed?'{% endif %}" diff --git a/template/Makefile.jinja b/template/Makefile.jinja index a12bc283..64cdbf1f 100644 --- a/template/Makefile.jinja +++ b/template/Makefile.jinja @@ -1,6 +1,7 @@ .PHONY: install-dev install-dev: ## install project including all development dependencies - pip install -e .[test,doc,dev] + pip install -e .[test,dev] + pip install -r docs/requirements.txt .PHONY: maintainability maintainability: ## run maintainability checks @@ -35,7 +36,7 @@ docs-live: ## serve documentation {%- if docs == 'mkdocs' %} mkdocs serve {%- elif docs == 'sphinx' %} - sphinx-autobuild docs ${DOCS_TARGET}/livehtml + cd docs && $(MAKE) serve {% endif %} {% endif %} diff --git a/template/README.md.jinja b/template/README.md.jinja index 8e9aafe4..c4682de5 100644 --- a/template/README.md.jinja +++ b/template/README.md.jinja @@ -2,11 +2,14 @@ {%- import 'template/context' as ctx with context %} -[![documentation][badge_documentation]]({{ctx.remote_url_pages}}) [![badge_pipeline][]]({{ctx.remote_url_https}}/-/pipelines) [![badge_coverage][]]({{ctx.remote_url_pages}}/coverage) [![badge_maintainability][]]() - -[badge_documentation]: {{ctx.remote_url_pages}}/badges/documentation.svg -[badge_coverage]: {{ctx.remote_url_https}}/badges/{{default_branch}}/coverage.svg -[badge_pipeline]: {{ctx.remote_url_https}}/badges/{{default_branch}}/pipeline.svg +[![badge_documentation][]][documentation] [![badge_pipeline][]][pipeline] [![badge_coverage][]][coverage] [![badge_maintainability][]]() + +[documentation]: {{ctx.remote_url_pages}} +[badge_documentation]: https://img.shields.io/badge/Documentation-{{default_branch}}-blue +[coverage]: {{ctx.remote_url_pages}}/coverage +[badge_coverage]: {{ctx.remote_url_coverage_badge}} +[badge_pipeline]: {{ctx.remote_url_pipeline_badge}} +[pipeline]: {{ctx.remote_url_pipeline}} [badge_maintainability]: {{ctx.remote_url_pages}}/badges/maintainability.svg diff --git a/template/context b/template/context index 91cf72d1..57d05f78 100644 --- a/template/context +++ b/template/context @@ -15,11 +15,30 @@ {% if remote == 'github' %} {% set domain_pages = 'github.io' %} +{% set path_pipeline = '/actions?query=branch%3A' + default_branch %} {% elif remote == 'gitlab-iis' %} {% set domain_pages = domain %} {% elif remote == 'gitlab-fhg' %} {% set domain_pages = 'pages.fraunhofer.de' %} {% endif %} + +{% if remote.startswith('gitlab') %} +{% set path_pipeline = '/-/pipelines' %} +{% endif %} + {% set remote_url_pages = "https://" + group + "." + domain_pages + "/" + pages_path %} +{% set remote_url_pipeline = remote_url_https + path_pipeline %} + +{% if remote == 'github' %} +{# coverage badge is provided by gitlab #} +## coverage badge is generated by github action and published on github pages +{% set remote_url_coverage_badge = remote_url_pages + '/badges/coverage.svg' %} +## pipeline badge is provided by github +{% set remote_url_pipeline_badge = remote_url_https + '/actions/workflows/ci.yaml/badge.svg' %} +{% else %} +## coverage and pipeline badges are provided by gitlab +{% set remote_url_coverage_badge = remote_url_https + '/badges/' + default_branch + 'coverage.svg' %} +{% set remote_url_pipeline_badge = remote_url_https + '/badges/' + default_branch + 'pipeline.svg' %} +{% endif %} {% set cli_command = package_name | replace("_", "-") %} diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 82492780..bfa8c0c8 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -70,24 +70,8 @@ dependencies = ["click"] # Similar to `dependencies` above, these must be valid existing # projects. [project.optional-dependencies] -dev = ["black", "radon", "ruff"] +dev = ["black", "radon", "ruff"{% if use_bumpversion %}, "bump2version"{% endif %}] test = ["pytest", "pytest-cov", "coverage[toml]"] -{% if docs!="none" -%} -doc = [ -{%- if docs=="mkdocs" %} - "mkdocs-material", - "mkdocstrings[python]", - "mkdocs-git-revision-date-localized-plugin", - "mkdocs-macros-plugin", -{%- elif docs=="sphinx" %} - "sphinx", - "furo", - "myst_parser", - "sphinx-autodoc2", - "sphinx-autobuild", -{%- endif %} -] -{%- endif %} # The following would provide a command line executable which executes # the function `main` from this package's cli module when invoked. diff --git a/template/{% if docs == 'mkdocs' %}docs{% endif %}/requirements.txt b/template/{% if docs == 'mkdocs' %}docs{% endif %}/requirements.txt new file mode 100644 index 00000000..5090e64c --- /dev/null +++ b/template/{% if docs == 'mkdocs' %}docs{% endif %}/requirements.txt @@ -0,0 +1,4 @@ +mkdocs-material +mkdocstrings[python] +mkdocs-git-revision-date-localized-plugin +mkdocs-macros-plugin diff --git a/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/.gitlab/docs.yml b/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/.gitlab/docs.yml new file mode 100644 index 00000000..6e536da8 --- /dev/null +++ b/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/.gitlab/docs.yml @@ -0,0 +1,10 @@ +docs: + image: python:latest + script: + - pip install -r docs/requirements.txt + - pushd docs && BUILDDIR=_build make html && popd + - mkdir -p build/docs + - mv docs/_build/html/* build/docs + artifacts: + paths: + - build/docs diff --git a/template/{% if docs == 'sphinx' %}docs{% endif %}/Makefile b/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/Makefile similarity index 68% rename from template/{% if docs == 'sphinx' %}docs{% endif %}/Makefile rename to template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/Makefile index 76bb8dbc..cf7e036c 100644 --- a/template/{% if docs == 'sphinx' %}docs{% endif %}/Makefile +++ b/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/Makefile @@ -3,16 +3,20 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR ?= ../build/docs +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SPHINXAUTOBUILD ?= sphinx-autobuild +SOURCEDIR = . +BUILDDIR ?= ../build/docs # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -.PHONY: help Makefile +serve: + @$(SPHINXAUTOBUILD) "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help serve Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). diff --git a/template/{% if docs == 'sphinx' %}docs{% endif %}/_static/.gitkeep b/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/_static/.gitkeep similarity index 100% rename from template/{% if docs == 'sphinx' %}docs{% endif %}/_static/.gitkeep rename to template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/_static/.gitkeep diff --git a/template/{% if docs == 'sphinx' %}docs{% endif %}/changelog.md b/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/changelog.md similarity index 100% rename from template/{% if docs == 'sphinx' %}docs{% endif %}/changelog.md rename to template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/changelog.md diff --git a/template/{% if docs == 'sphinx' %}docs{% endif %}/conf.py.jinja b/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/conf.py.jinja similarity index 100% rename from template/{% if docs == 'sphinx' %}docs{% endif %}/conf.py.jinja rename to template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/conf.py.jinja diff --git a/template/{% if docs == 'sphinx' %}docs{% endif %}/index.md b/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/index.md similarity index 100% rename from template/{% if docs == 'sphinx' %}docs{% endif %}/index.md rename to template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/index.md diff --git a/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/requirements.txt b/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/requirements.txt new file mode 100644 index 00000000..4df05720 --- /dev/null +++ b/template/{% if docs == 'sphinx' and docs_template=='none' %}docs{% endif %}/requirements.txt @@ -0,0 +1,5 @@ +sphinx +furo +myst_parser +sphinx-autodoc2 +sphinx-autobuild diff --git a/template/{% if remote == 'github' %}.github{% endif %}/workflows/ci.yaml.jinja b/template/{% if remote == 'github' %}.github{% endif %}/workflows/ci.yaml.jinja new file mode 100644 index 00000000..6c1d0b19 --- /dev/null +++ b/template/{% if remote == 'github' %}.github{% endif %}/workflows/ci.yaml.jinja @@ -0,0 +1,98 @@ +name: CI + +on: + push: + branches: ["{{default_branch}}"] + +permissions: + contents: read + pages: write + id-token: write + +jobs: + coverage: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install project (including test requirements) + run: pip install .[test] + - name: Run tests and collect coverage + run: | + make coverage + - run: coverage xml + - run: pip install genbadge[coverage] + - run: genbadge coverage -i coverage.xml -o build/coverage/coverage.svg + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: coverage + path: build/coverage + maintainability: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Build maintainability badge + run: | + pip install anybadge radon + score=$(python -m radon cc --total-average src | tail -n 1 | cut -d' ' -f 3-4) + [[ "$score" = A* ]] && color="green"; [[ "$score" = B* ]] && color="green" + [[ "$score" = C* ]] && color="yellow"; [[ "$score" = D* ]] && color="orange_2" + [[ "$score" = E* ]] && color="orange"; [[ "$score" = F* ]] && color="orangered" + python -m anybadge --label=Maintainability --value="$score" --color="$color" -f maintainability -o + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: maintainability + path: maintainability.svg + {%- if docs != 'none' %} + docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install doc requirements + run: pip install -r docs/requirements.txt + - name: Build docs + run: make docs + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: docs + path: build/docs/html + {%- endif %} + deploy: + environment: + name: github-pages + url: {% raw %}${{steps.deployment.outputs.page_url}}{% endraw %} + runs-on: ubuntu-latest + needs: [coverage, maintainability, docs] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Pages + uses: actions/configure-pages@v3 + - uses: actions/download-artifact@master + with: + path: public + - run: ls -lR public + - run: mv public/maintainability public/badges + - run: mv public/coverage/coverage.svg public/badges/. + - run: mv public/docs/* public/. && rm -r public/docs || echo "no documentation found!" + - name: Upload Pages + uses: actions/upload-pages-artifact@v2 + with: + path: public + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/template/{% if remote == 'github' %}.github{% endif %}/workflows/{% if docs != 'none' %}docs.yaml{% endif %}.jinja b/template/{% if remote == 'github' %}.github{% endif %}/workflows/{% if docs != 'none' %}docs.yaml{% endif %}.jinja deleted file mode 100644 index 02399baf..00000000 --- a/template/{% if remote == 'github' %}.github{% endif %}/workflows/{% if docs != 'none' %}docs.yaml{% endif %}.jinja +++ /dev/null @@ -1,34 +0,0 @@ -name: deploy documentation to github pages - -on: - push: - branches: ["{{default_branch}}"] - -permissions: - contents: read - pages: write - id-token: write - -jobs: - # Single deploy job no building - deploy: - environment: - name: github-pages - url: {% raw %}${{steps.deployment.outputs.page_url}}{% endraw %} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Pages - uses: actions/configure-pages@v3 - - name: Setup environment - run: python -m venv env; env/bin/pip install .[doc] - - name: Build docs - run: {% if docs=='mkdocs' %}MKDOCS_BIN=env/bin/mkdocs {% elif docs=='sphinx' %}SPHINXBUILD=env/bin/sphinx-build {% endif %}make docs - - name: Upload Artifact - uses: actions/upload-pages-artifact@v2 - with: - path: "build/docs/html" - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v2 diff --git a/template/{% if remote.startswith('gitlab') %}.gitlab-ci.yml{% endif %}.jinja b/template/{% if remote.startswith('gitlab') %}.gitlab-ci.yml{% endif %}.jinja index db6c3e68..f33e6eeb 100644 --- a/template/{% if remote.startswith('gitlab') %}.gitlab-ci.yml{% endif %}.jinja +++ b/template/{% if remote.startswith('gitlab') %}.gitlab-ci.yml{% endif %}.jinja @@ -1,8 +1,6 @@ image: python:latest -stages: [test, publish] test: - stage: test cache: # reuse venv in subsequent jobs key: $CI_JOB_NAME paths: @@ -13,18 +11,18 @@ test: - source env/bin/activate script: - pip install .[test] - - pytest --doctest-modules --cov --cov-config=pyproject.toml --cov-branch --cov-report term --cov-report html:build/coverage --junitxml=report.xml - - coverage report - - coverage xml + - pytest --doctest-modules --cov --cov-config=pyproject.toml --cov-branch --cov-report term --cov-report html:build/coverage --junitxml=report.xml --cov-report xml - pip install anybadge==1.9.0 - mkdir -p build/badges - pip install radon==5.1.0 - make maintainability # generate a badge for the maintainability index with the total average of cyclomatic complexity as value - - python -m anybadge --label=Maintainability --value="$(python -m radon cc --total-average -nc src | tail -n 1 | cut -d' ' -f 3) ($(python -m radon cc --total-average -nc src | tail -n 1 | cut -d' ' -f 4 | cut -c 2-4))" -f build/badges/maintainability -o - # generate a badge for the documentation with the current version as label - # - color: #1082C2 corresponds to the default "informational" color set by https://shields.io - - python -m anybadge --label=Documentation --value="v$(python -m {{package_name}} --version)" --color "#1082C2" -f build/badges/documentation -o + - | + score=$(python -m radon cc --total-average src | tail -n 1 | cut -d' ' -f 3-4) + [[ "$score" = A* ]] && color="green"; [[ "$score" = B* ]] && color="green" + [[ "$score" = C* ]] && color="yellow"; [[ "$score" = D* ]] && color="orange_2" + [[ "$score" = E* ]] && color="orange"; [[ "$score" = F* ]] && color="orangered" + python -m anybadge --label=Maintainability --value="$score" --color="$color" -f build/badges/maintainability -o coverage: '/TOTAL.+?(\d+\%)/' artifacts: when: always @@ -37,23 +35,33 @@ test: - build/coverage - build/badges - env/ -{% if docs != 'none' %} + +{% if docs == 'sphinx' -%} +include: docs/.gitlab/docs.yml + +{% elif docs == 'mkdocs' %} +docs: + script: + mkdocs build --clean --site-dir build/docs + artifacts: + paths: + - build/docs + +{% endif -%} + pages: - stage: publish - needs: - - test + needs: [test{% if docs != 'none' %}, docs{% endif %}] only: - {{default_branch}} - staging script: - - mkdir -p public - - source env/bin/activate - - pip install .[doc] - - DOCS_TARGET=public make docs - - mv public/html/* public/. && rm -r public/html + {% if docs != 'none' -%} + - mv build/docs public + {% else -%} + - mkdir public + {% endif -%} - mv build/badges public/. - mv build/coverage public/. artifacts: paths: - public -{% endif -%} diff --git a/tests/test_cli.py b/tests/test_cli.py index f449e735..07bccc95 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,5 @@ import logging as log +import re import pytest from init_python_project.cli import app @@ -15,3 +16,9 @@ def test_default_values(cli): result = cli("--help") log.debug(result.output) assert result.output.strip().startswith("Usage: init-python-project") + + +def test_version(cli): + result = cli("--version") + log.debug(result.output) + assert re.match(r"^\d\.\d\.\d$", result.output.strip()), "should return semantic version number" diff --git a/tests/test_package.py b/tests/test_package.py index 0e797d9b..af4a3caa 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -11,8 +11,8 @@ def bin(): yield Path(os.getenv("BIN_PATH", ".")) +@pytest.mark.slow def test_template_generation_via_cli(bin: Path, tmp_path: Path): - # generate project child = pexpect.spawn(str(bin / "init-python-project"), ["my-project"], cwd=tmp_path, timeout=3) child.expect(".* project.*") child.sendline("My Project") @@ -27,7 +27,7 @@ def test_template_generation_via_cli(bin: Path, tmp_path: Path): child.expect(".* platform.*") child.sendline("") # accept default child.expect(".* name.*") - child.sendline("cool-user") # accept default + child.sendline("cool-user") child.expect(".* remote.*") child.sendline("") # accept default child.expect(".* initial git branch.*") diff --git a/tests/test_template.py b/tests/test_template.py index f11c18de..0bbe7134 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -1,6 +1,8 @@ +import itertools import os +import tomllib from pathlib import Path -from subprocess import check_output +from subprocess import check_call, check_output, run import pytest import yaml @@ -10,6 +12,13 @@ template = yaml.safe_load(Path(__file__).parent.with_name("copier.yaml").read_text()) SUPPORTED_REMOTES = template["remote"]["choices"].values() SUPPORTED_DOCS = template["docs"]["choices"].values() +SUPPORTED_DOCS_TEMPLATES = template["docs_template"]["choices"].values() +SUPPORTED_DOCS_TEMPLATES_COMBINATIONS = [ + t + for t in itertools.product(SUPPORTED_DOCS, SUPPORTED_DOCS_TEMPLATES) + if t[1] == "none" or t[1].startswith(t[0]) +] +"""All combinations of docs and docs_template options.""" fp_template = Path(__file__).parent.parent @@ -26,19 +35,22 @@ def venv(tmp_path): venv.create() (venv.path / ".gitignore").unlink() yield venv + print(tmp_path) # useful for debugging the built project @pytest.mark.slow @pytest.mark.parametrize("use_precommit", [True, False], ids=["pre-commit", "no pre-commit"]) -@pytest.mark.parametrize("use_bumpversion", [True, False], ids=["bumpversion", "no bumpversion"]) -@pytest.mark.parametrize("docs", SUPPORTED_DOCS) +@pytest.mark.parametrize( + "docs,docs_template", + SUPPORTED_DOCS_TEMPLATES_COMBINATIONS, +) @pytest.mark.parametrize("remote", SUPPORTED_REMOTES) def test_template_generation( venv: VirtualEnvironment, tmp_path: Path, use_precommit: bool, - use_bumpversion: bool, docs: str, + docs_template: str, remote: str, project_name: str = "Sample Project", ): @@ -48,12 +60,13 @@ def test_template_generation( data=dict( **required_static_data, use_precommit=use_precommit, - use_bumpversion=use_bumpversion, docs=docs, + docs_template=docs_template, remote=remote, ), defaults=True, unsafe=True, + vcs_ref="HEAD", ) fp_readme = tmp_path / "README.md" @@ -70,9 +83,6 @@ def test_template_generation( fp_precommit_config = tmp_path / ".pre-commit-config.yaml" assert fp_precommit_config.is_file() == use_precommit - fp_bumpversion_config = tmp_path / ".bumpversion.cfg" - assert fp_bumpversion_config.is_file() == use_bumpversion - fp_git = tmp_path / ".git" assert fp_git.is_dir(), "new projects should be git repositories" @@ -81,8 +91,12 @@ def test_template_generation( fp_mkdocs_cfg = tmp_path / "mkdocs.yml" assert fp_mkdocs_cfg.is_file(), "mkdocs configuration file should exist" elif docs == "sphinx": - fp_sphinx_cfg = fp_docs / "conf.py" - assert fp_sphinx_cfg.is_file(), "sphinx configuration file should exist" + fp_sphinx_makefile = fp_docs / "Makefile" + assert fp_sphinx_makefile.is_file(), "sphinx Makefile file should exist" + fp_sphinx_requirements = fp_docs / "requirements.txt" + assert fp_sphinx_requirements.is_file(), "sphinx requirements file should exist" + fp_sphinx_ci_job = fp_docs / ".gitlab" / "docs.yml" + assert fp_sphinx_ci_job.is_file(), "sphinx ci job should exist" use_docs = docs != "none" assert fp_docs.is_dir() == use_docs, "docs directory should exist if configured" @@ -94,32 +108,21 @@ def test_template_generation( ), "new projects should have a remote repository configured" os.chdir(tmp_path) + if docs_template != "none": + # docs template needs to be formatted before we can assume + # that all pre-commit hooks pass + check_output(["git", "add", "."]) + run(["pre-commit", "run", "--all-files"]) check_output(["git", "add", "."]) check_output(["git", "commit", "-m", "initial commit"]) # verify that example can be installed - venv.install(".[doc,dev,test]", editable=True) + venv.install(".[dev,test]", editable=True) venv_bin = Path(venv.bin) # verify that pytest works and all tests pass check_output([venv_bin / "pytest", "-q"]) - # verify docs can be built - if use_docs: - fp_docs_built = tmp_path / "build" / "docs" / "html" - assert not fp_docs_built.is_dir() - check_output( - ["make", "docs"], - env={ - "SPHINXBUILD": str(venv_bin / "sphinx-build"), - "MKDOCS_BIN": str(venv_bin / "mkdocs"), - }, - ) - assert fp_docs_built.is_dir(), "docs should have been built into build directory" - assert (fp_docs_built / "index.html").is_file(), "index should exist" - - # TODO: Test template update - def test_default_branch_option(tmp_path: Path): default_branch = "custom" @@ -133,6 +136,7 @@ def test_default_branch_option(tmp_path: Path): unsafe=True, defaults=True, user_defaults={"default_branch": default_branch}, + vcs_ref="HEAD", ) assert ( check_output(["git", "status", "--branch", "--porcelain"], cwd=str(tmp_path)) @@ -158,6 +162,7 @@ def test_remote_option(tmp_path: Path, remote: str): ), unsafe=True, defaults=True, + vcs_ref="HEAD", ) git_remote_output = check_output(["git", "remote", "-v"], cwd=str(tmp_path)).decode() @@ -192,20 +197,30 @@ def test_docs_option(venv: VirtualEnvironment, tmp_path: Path, docs: str): ), defaults=True, unsafe=True, + vcs_ref="HEAD", ) if docs == "mkdocs": fp_mkdocs_cfg = root / "mkdocs.yml" assert fp_mkdocs_cfg.is_file(), "mkdocs configuration file should exist" elif docs == "sphinx": - fp_sphinx_cfg = root / "docs" / "conf.py" - assert fp_sphinx_cfg.is_file(), "sphinx configuration file should exist" + fp_sphinx_makefile = root / "docs" / "Makefile" + assert fp_sphinx_makefile.is_file(), "sphinx Makefile should exist" + fp_sphinx_requirements = root / "docs" / "requirements.txt" + assert fp_sphinx_requirements.is_file(), "sphinx requirements file should exist" + fp_sphinx_ci_job = root / "docs" / ".gitlab" / "docs.yml" + assert fp_sphinx_ci_job.is_file(), "sphinx ci job should exist" if docs != "none": assert (root / "docs").is_dir(), "docs directory should exist" + fp_requirements = root / "docs" / "requirements.txt" + assert fp_requirements.is_file(), "doc requirements file should exist" # install example including its doc requirements - venv.install(f"{root}[doc]", editable=True) + venv.install(f"{root}", editable=True) + for req in fp_requirements.open().readlines(): + if not req.strip().startswith("#"): + venv.install(req) venv_bin = Path(venv.bin) # verify docs can be built @@ -225,7 +240,7 @@ def test_docs_option(venv: VirtualEnvironment, tmp_path: Path, docs: str): @pytest.mark.parametrize("docs", SUPPORTED_DOCS) @pytest.mark.parametrize("remote", SUPPORTED_REMOTES) -def test_publish_docs_ci(venv: VirtualEnvironment, tmp_path: Path, docs: str, remote: str): +def test_publish_docs_ci(tmp_path: Path, docs: str, remote: str): root = tmp_path run_copy( @@ -238,15 +253,112 @@ def test_publish_docs_ci(venv: VirtualEnvironment, tmp_path: Path, docs: str, re ), defaults=True, unsafe=True, + vcs_ref="HEAD", ) ci_platform = "gitlab" if remote.startswith("gitlab") else remote + docs_job = "docs" if ci_platform == "gitlab": - gitlab_ci_config = yaml.safe_load((root / ".gitlab-ci.yml").read_text()) + ci_file = root / ".gitlab-ci.yml" + if docs == "sphinx": + # job for sphinx is included via separate file due to external template support + ci_file = root / "docs" / ".gitlab" / "docs.yml" + elif ci_platform == "github": + ci_file = root / ".github" / "workflows" / "ci.yaml" + + assert ci_file.is_file() + ci_config = yaml.safe_load(ci_file.read_text()) + + if ci_platform == "github": + ci_config = ci_config["jobs"] match (docs, ci_platform): - case ("none", "github"): - assert not (root / ".github" / "docs.yaml").is_file() - case ("none", "gitlab"): - assert "pages" not in gitlab_ci_config + case ("none", _): + assert docs_job not in ci_config, "docs job should not be present if docs are disabled" + case _: + assert docs_job in ci_config, "docs job should be present if docs are enabled" + + +DOCS_WITH_TEMPLATE = [c for c in SUPPORTED_DOCS_TEMPLATES_COMBINATIONS if c[1] != "none"] +"""Only those combinations that actually use a template.""" + + +@pytest.mark.parametrize("docs,docs_template", DOCS_WITH_TEMPLATE) +def test_docs_with_template(tmp_path: Path, docs: str, docs_template: str): + root = tmp_path + + run_copy( + str(fp_template), + str(root), + data=dict( + **required_static_data, + docs=docs, + docs_template=docs_template, + ), + defaults=True, + unsafe=True, + vcs_ref="HEAD", + ) + + docs = root / "docs" + + docs_requirements = docs / "requirements.txt" + assert docs_requirements.is_file(), "all doc templates must come with a requirements file" + + ci_job = docs / ".gitlab" / "docs.yml" + assert ci_job.is_file(), "doc templates must provide their ci job in separate file" + + +def read_pyproject_version(path: Path): + return tomllib.load(path.open("rb"))["project"]["version"] + + +def read_last_commit_msg(cwd: Path | str = None): + return check_output(["git", "log", "-1", "--pretty=%B"], cwd=str(cwd or ".")).decode().strip() + + +@pytest.mark.parametrize("use_bumpversion", [True, False], ids=["bumpversion", "no bumpversion"]) +def test_bumpversion_option(venv: VirtualEnvironment, tmp_path: Path, use_bumpversion: bool): + run_copy( + str(fp_template), + str(tmp_path), + data=dict( + **required_static_data, + use_bumpversion=use_bumpversion, + use_precommit=False, # makes testing easier + ), + unsafe=True, + defaults=True, + vcs_ref="HEAD", + ) + if not use_bumpversion: + assert not (tmp_path / ".bumpversion.cfg").is_file() + return + + assert (tmp_path / ".bumpversion.cfg").is_file() + fp_pyproject = tmp_path / "pyproject.toml" + + os.chdir(tmp_path) + check_output(["git", "add", "."]) + check_output(["git", "commit", "-m", "initial commit"]) + + venv.install(".[dev]", editable=True) + venv_bin = Path(venv.bin) + + # verify that pytest works and all tests pass + check_output([venv_bin / "bumpversion", "-h"]) + + # bumpversion git interaction requires initial commit + run(["git", "add", "."]) + run(["git", "commit", "-m", "initial commit", "--no-verify"]) + assert read_pyproject_version(fp_pyproject) == "0.0.1" + check_call([venv_bin / "bumpversion", "patch"]) + assert read_pyproject_version(fp_pyproject) == "0.0.2" + assert read_last_commit_msg() == "bump v0.0.1 -> v0.0.2" + check_call([venv_bin / "bumpversion", "minor"]) + assert read_pyproject_version(fp_pyproject) == "0.1.0" + assert read_last_commit_msg() == "bump v0.0.2 -> v0.1.0" + check_output([venv_bin / "bumpversion", "major"]) + assert read_pyproject_version(fp_pyproject) == "1.0.0" + assert read_last_commit_msg() == "bump v0.1.0 -> v1.0.0"