diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a2935a7f..32976c48 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,9 +1,7 @@ [bumpversion] -current_version = 0.0.0 +current_version = 0.0.2 message = bump v{current_version} -> v{new_version} -commit = True -tag = True - -[bumpversion:file:pyproject.toml] [bumpversion:file:.bumpversion.cfg] + +[bumpversion:file:src/init_python_project/cli.py] diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 76ac4063..53f9b2ab 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -24,7 +24,6 @@ jobs: - uses: actions/configure-pages@v3 - run: sudo apt-get update && sudo apt-get install tree - run: | # replace symlinks with actual files - rm -r src/init_python_project/template src/init_python_project/copier.yaml cp -r template src/init_python_project/. cp copier.yaml src/init_python_project/. - run: | # creating example projects using copier requires a git identity diff --git a/.gitignore b/.gitignore index 4d493048..0c65eb72 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ __pycache__ build .obsidian docs/examples +src/init_python_project/template +src/init_python_project/copier.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7bb5a89f..90ec3c67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,17 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: check-added-large-files + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.0.3 + hooks: + - id: prettier + types_or: [json, yaml, css, javascript] + # black should have the final say on python formatting, so it comes last + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + name: format code (black) - repo: local hooks: - id: pytest @@ -17,7 +28,7 @@ repos: files: "^template/" stages: [push] - repo: https://github.com/streetsidesoftware/cspell-cli - rev: v7.0.0 + rev: v7.3.0 hooks: - id: cspell name: check spelling (cspell) diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index e801dc58..00000000 --- a/.prettierrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "overrides": [ - { - "files": ["*.md"], - "options": { - "tabWidth": 4 - } - } - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb92bed..3b044cf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] + +## [0.0.2] + +### Added + +- `init-python-project --version` outputs template version + ## [0.0.1] Started this template by forking [pypa/sampleproject] and converting it to a copier template. @@ -45,9 +53,8 @@ An example project (comparable to [pypa/sampleproject]) can be found at [jannism - trove classifiers (only relevant when publishing to PyPI) - - -[0.0.1]: https://github.com/jannismain/python-project-template/releases/tag/v0.0.1 - +[unreleased]: https://github.com/jannismain/python-project-template/compare/v0.0.2...HEAD +[0.0.2]: https://github.com/jannismain/python-project-template/compare/0.0.1...0.0.2 +[0.0.1]: https://github.com/jannismain/python-project-template/releases/tag/0.0.1 [pypa/sampleproject]: https://github.com/pypa/sampleproject [jannismain/python-project-template-example]: https://github.com/jannismain/python-project-template-example diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..02787bcc --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +prune template +prune */__pycache__ +exclude copier.yaml diff --git a/Makefile b/Makefile index 62df0074..931ad66b 100644 --- a/Makefile +++ b/Makefile @@ -111,12 +111,19 @@ PKGNAME=init_python_project PKGDIR=src/${PKGNAME} BUILDDIR?=build/dist PYTHON?=python -build: ## build package +TEMPLATE_SRC?=./template +TEMPLATE_DEST?=${PKGDIR}/template +build: build-clean copy-template ## build package @${PYTHON} -m pip install --upgrade build @${PYTHON} -m build --outdir ${BUILDDIR} . install-build: build @pip uninstall -y ${PKGNAME} pip install --force-reinstall ${BUILDDIR}/*.whl +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 .PHONY: help diff --git a/README.md b/README.md index a296a416..ea2bfa9a 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,37 @@ -# Python Project Template +
+

Python Project Template

+ +[![](https://img.shields.io/badge/python-3.11-blue)][sample project] +[![](https://img.shields.io/badge/Example-Sample_Project-blue)][sample project] +[![PyPI - Version](https://img.shields.io/pypi/v/init-python-project)][pypi] + +
+ +[pypi]: https://pypi.org/project/init-python-project/ + + A customizable template for new Python projects to get you up and running with current best practices faster. ## Features -- Each project has a [README][] and [CHANGELOG][] file and includes further documentation based on [Material for MkDocs][] or [Sphinx][]. -- [Testing][kb_testing] and [continuous integration][ci] tooling are included from the very beginning +- Each project has a *README* and *CHANGELOG* file and includes further documentation based on [Material for MkDocs][] or [Sphinx][]. +- *Testing* and *continuous integration* tooling are included from the very beginning - Test coverage is collected and displayed as a badge - Coverage report is integrated with [Gitlab's coverage report artifact][gitlab coverage report] - Projects use [pre-commit][] for sanity checks on each commit or push - Projects use bumpversion to increase their version according to [semantic versioning guidelines][semver] - Python projects are installable by default and provide a simple command-line interface -[readme]: https://intern.iis.fhg.de/x/I5DPFQ -[changelog]: https://intern.iis.fhg.de/display/DOCS/Changelog [material for mkdocs]: https://squidfunk.github.io/mkdocs-material [sphinx]: https://www.sphinx-doc.org -[ci]: https://intern.iis.fhg.de/x/DK6qG -[kb_testing]: https://intern.iis.fhg.de/x/DS9SFw [gitlab coverage report]: https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscoverage_report [pre-commit]: https://pre-commit.com/ [semver]: https://semver.org/ Everything comes pre-configured with sensible defaults so you can focus on your implementation and let the template handle the rest. -See this [sample project][] to see how projects generated from this template using default values look like. +See the [sample project][] to see how projects generated from this template using default values look like. [sample project]: https://github.com/jannismain/python-project-template-example @@ -32,30 +39,30 @@ See this [sample project][] to see how projects generated from this template usi ### Prerequisites -* [copier][] +* Python3.11 or newer -*Note: If you have [pipx][] installed (you should, it is good), you can simply use `pipx run copier` out of the box.* +### Installation + +```sh +pip install init-python-project +``` + +*Note: If you have [pipx][] installed (you should, it is good), you can skip this step and instead run it directly using `pipx run init-python-project`* -[copier]: https://github.com/copier-org/copier [pipx]: https://pypa.github.io/pipx/ ### Usage ```console -copier copy --trust gh:jannismain/python-project-template my_new_project +init-python-project ``` -*Note: `--trust` is required because the template uses [tasks][] to setup your git repository for you.* - -[tasks]: https://github.com/jannismain/python-project-template/blob/main/copier.yaml - ## User Guide -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. +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. -[user guide]: https://jannismain.github.io/python-project-template/user-guide/getting-started/ [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/docs/assets/custom.css b/docs/assets/custom.css index f4b3fef0..88914801 100644 --- a/docs/assets/custom.css +++ b/docs/assets/custom.css @@ -1,22 +1,22 @@ /* center mermaid graphs */ .mermaid { - text-align: center; + text-align: center; } /* convert emphasized text under images to caption */ -img+em { - display: block; +img + em { + display: block; } .md-typeset .caption, .md-typeset figcaption { - /* same as material's figcaption */ - font-style: italic; - max-width: 24rem; - margin: 1em auto 1.5em; - text-align: center; - display: block; - font-size: 0.8rem; - /* custom */ - line-height: 1.2; + /* same as material's figcaption */ + font-style: italic; + max-width: 24rem; + margin: 1em auto 1.5em; + text-align: center; + display: block; + font-size: 0.8rem; + /* custom */ + line-height: 1.2; } diff --git a/docs/assets/magiclink.css b/docs/assets/magiclink.css index 9df79892..6cd83c89 100644 --- a/docs/assets/magiclink.css +++ b/docs/assets/magiclink.css @@ -13,7 +13,6 @@ --magiclink-user-icon: url('data:image/svg+xml;charset=utf-8,'); } - .md-typeset a[href^="mailto:"]:not(.magiclink-ignore)::before { -webkit-mask-image: var(--magiclink-email-icon); mask-image: var(--magiclink-email-icon); @@ -82,7 +81,8 @@ -webkit-mask-image: var(--magiclink-discussion-icon); mask-image: var(--magiclink-discussion-icon); } -.md-typeset .magiclink-repository.magiclink-github:not(.magiclink-ignore)::before, +.md-typeset + .magiclink-repository.magiclink-github:not(.magiclink-ignore)::before, .md-typeset .magiclink-mention.magiclink-github:not(.magiclink-ignore)::before { -webkit-mask-image: var(--magiclink-github-icon); mask-image: var(--magiclink-github-icon); @@ -92,7 +92,8 @@ -webkit-mask-image: var(--magiclink-user-icon); mask-image: var(--magiclink-user-icon); } */ -.md-typeset .magiclink-repository.magiclink-bitbucket:not(.magiclink-ignore)::before { +.md-typeset + .magiclink-repository.magiclink-bitbucket:not(.magiclink-ignore)::before { -webkit-mask-image: var(--magiclink-bitbucket-icon); mask-image: var(--magiclink-bitbucket-icon); } diff --git a/docs/assets/magiclink.js b/docs/assets/magiclink.js index a1921f6b..8ce4ff97 100644 --- a/docs/assets/magiclink.js +++ b/docs/assets/magiclink.js @@ -1,5 +1,5 @@ // remove '@' from github mention magiclink -let mentions = document.querySelectorAll(".magiclink-gitlab.magiclink-mention") -mentions.forEach(el => { - el.text = el.text.replace("@", "") +let mentions = document.querySelectorAll(".magiclink-gitlab.magiclink-mention"); +mentions.forEach((el) => { + el.text = el.text.replace("@", ""); }); diff --git a/docs/developer-guide/documentation.md b/docs/developer-guide/documentation.md index c49e0eb4..4f7835e6 100644 --- a/docs/developer-guide/documentation.md +++ b/docs/developer-guide/documentation.md @@ -6,13 +6,13 @@ This project uses MkDocs with the Material for MkDocs theme. ??? quote "`mkdocs.yaml` Configuration File" - {{ includex('mkdocs.yaml', indent=4, lang='yaml') }} + {{ includex('mkdocs.yaml', indent=4, code=True) }} ## Navigation The navigation is setup using [:octicons-cpu-24:`mkdocs-literate-nav`](https://github.com/oprypin/mkdocs-literate-nav) and managed in the `_nav.md` file: -{{ includex('docs/_nav.md', lang='md') }} +{{ includex('docs/_nav.md', code=True) }} ## Macros @@ -20,6 +20,6 @@ The navigation is setup using [:octicons-cpu-24:`mkdocs-literate-nav`](https://g ??? quote "`docs/util/macros.py`" - {{ includex('docs/util/macros.py', indent=4, lang='py') }} + {{ includex('docs/util/macros.py', indent=4, code=True) }} [jinja]: https://jinja.palletsprojects.com/ diff --git a/docs/developer-guide/template/Calculated Variables.md b/docs/developer-guide/template/Calculated Variables.md index 96a78206..17f3a8c9 100644 --- a/docs/developer-guide/template/Calculated Variables.md +++ b/docs/developer-guide/template/Calculated Variables.md @@ -4,10 +4,10 @@ They do not appear in the template questionaire and therefore cannot be modified Calculated template variables are managed in the `context` file within the template: -{{ includex('template/context', lang='jinja', caption=True, raise_errors=True) }} +{{ includex('template/context', code='jinja', caption=True, raise_errors=True) }} This context can then be imported using the [jinja import](https://jinja.palletsprojects.com/templates/#import) command. As an example, this was used in the README template to reuse the `remote_url_pages` and `remote_url_https` URLs, which are calculated from the `remote_url` provided by the user: -{{ includex('template/README.md.jinja', lines=8, lang='jinja', caption=True) }} +{{ includex('template/README.md.jinja', lines=8, code='jinja', caption=True) }} diff --git a/docs/developer-guide/template/index.md b/docs/developer-guide/template/index.md index a8cf7f08..19be50ef 100644 --- a/docs/developer-guide/template/index.md +++ b/docs/developer-guide/template/index.md @@ -4,6 +4,6 @@ This template is built using [copier][]. The `_subdirectory` is set to *template*, so that the template files are separated from other project files: -{{ includex('copier.yaml', lang='yaml', start_match='_subdirectory', lines=1, caption=True) }} +{{ includex('copier.yaml', code=True, start_match='_subdirectory', lines=1, caption=True) }} [copier]: https://github.com/copier-org/copier diff --git a/docs/index.md b/docs/index.md index 8d462fe0..f3130c8c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,3 @@ # Python Project Template -{{ includex('README.md', start=2, end_match='Getting Started') }} +{{ includex('README.md', start_match='', end_match='Getting Started') }} diff --git a/docs/reference/tooling/git.md b/docs/reference/tooling/git.md index 1643a671..e7013fcc 100644 --- a/docs/reference/tooling/git.md +++ b/docs/reference/tooling/git.md @@ -9,4 +9,18 @@ tags: [Version Control] [git]: https://git-scm.com/ [git-docs]: https://git-scm.com/doc -## git hooks + +## [git hooks] + +Git provides hooks to run custom code when specific git actions occur. One such action would be a commit, for which git provides the following hooks: + +| Hook | When | Common use cases | +| -------------------- | -------------------------------------- | ------------------------------ | +| `pre-commit` | before commit process is started | linting, style checks, etc. | +| `prepare-commit-msg` | before commit message editor is opened | provide default commit message | +| `commit-msg` | after commit message is entered | validate commit message | +| `post-commit` | after commit process is completed | send notifications | + +These *hooks* are basically just scripts inside the `.git/hooks` directory that are called by git. However, there is also a framework called [pre-commit](../pre-commit), that allows for easy reuse of existing git hooks. + +[git hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks diff --git a/docs/reference/tooling/mkdocs.md b/docs/reference/tooling/mkdocs.md index c9462b64..fb9a8c92 100644 --- a/docs/reference/tooling/mkdocs.md +++ b/docs/reference/tooling/mkdocs.md @@ -19,7 +19,7 @@ MkDocs is configured in the `mkdocs.yml` file in the root of your project. ??? quote "`mkdocs.yml`" - {{ includex("docs/examples/mkdocs/mkdocs.yml", indent=4, lang="yaml") }} + {{ includex("docs/examples/mkdocs/mkdocs.yml", indent=4, code=True) }} ## Extensions diff --git a/docs/reference/tooling/sphinx.md b/docs/reference/tooling/sphinx.md index 3ad38723..c98f41da 100644 --- a/docs/reference/tooling/sphinx.md +++ b/docs/reference/tooling/sphinx.md @@ -22,7 +22,7 @@ Sphinx is configured via a `conf.py` file. ??? quote "`docs/conf.py`" - {{ includex('docs/examples/sphinx/docs/conf.py', indent=4, lang='py')}} + {{ includex('docs/examples/sphinx/docs/conf.py', indent=4, code=True)}} See [Sphinx Configuration](https://www.sphinx-doc.org/en/master/usage/configuration.html) for a list of supported options. diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index de880627..b8678a78 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -1 +1,16 @@ {{ includex('README.md', start_match='Prerequisites', end_match='')}} + +## Using [copier] directly + +```console +copier copy --trust https://git01.iis.fhg.de/mkj/project-template.git my_new_project +``` + +*Note: `--trust` is required because the template uses [tasks][] to setup your git repository for you.* + +[tasks]: https://git01.iis.fhg.de/mkj/project-template/-/blob/main/copier.yaml + +*Note: If you have [pipx][] installed (you should, it is good), you can simply use `pipx run copier` out of the box.* + +[copier]: https://github.com/copier-org/copier +[pipx]: https://pypa.github.io/pipx/ diff --git a/docs/user-guide/project-structure.md b/docs/user-guide/project-structure.md index 404f3601..e229d314 100644 --- a/docs/user-guide/project-structure.md +++ b/docs/user-guide/project-structure.md @@ -5,19 +5,19 @@ Depending on which options you selected, your project initially consists of the === "minimal" ``` - {{ run("tree", "-a", "-I", ".git", "-I", "*.egg-info", "-I", ".venv", "docs/examples/minimal/") | indent(4) }} + {{ run("tree -a -I .git -I *.egg-info -I .venv -I __pycache__ docs/examples/minimal/") | indent(4) }} ``` === "default" ``` - {{ run("tree", "-a", "-I", ".git", "-I", "*.egg-info", "-I", ".venv", "-I", "__pycache__", "docs/examples/default/") | indent(4) }} + {{ run("tree -a -I .git -I *.egg-info -I .venv -I __pycache__ docs/examples/default/") | indent(4) }} ``` === "full" ``` - {{ run("tree", "-a", "-I", ".git", "-I", "*.egg-info", "-I", ".venv", "docs/examples/full/") | indent(4) }} + {{ run("tree -a -I .git -I *.egg-info -I .venv -I __pycache__ docs/examples/full/") | indent(4) }} ``` ## Python-specific files @@ -25,7 +25,7 @@ Depending on which options you selected, your project initially consists of the The Python project structure consists of the following elements ``` -{{ run("tree", "-L", "3", "-P", "*.py", "-I", "docs", "-P", "pyproject.toml", "--gitfile", ".gitignore", "--noreport", "docs/examples/default/") }} +{{ run("tree -L 3 -P *.py -I docs -P pyproject.toml --gitfile .gitignore --noreport docs/examples/default/") }} ``` * `pyproject.toml`: The main responsibility of this file is to declare the [build system][] needed to build your Python package (`[build-system]`) as well as package metadata (in the `[project]` section). However, many Python tools support reading their configuration from this file. These sections are prefixed by `[tool.*]` diff --git a/docs/user-guide/topics/cli.md b/docs/user-guide/topics/cli.md index 3f213d43..d445dc6a 100644 --- a/docs/user-guide/topics/cli.md +++ b/docs/user-guide/topics/cli.md @@ -1 +1,18 @@ # CLI + +A CLI (or command line interface) is a type of script you can run in the terminal. + +## Frameworks + +While frameworks are not required to implement a CLI in Python, they might provide a better structure and useful utilities which make them worth exploring: + +- [click] +- [typer]: based on click, integrates with type annotations + +## References + +There is a section on [Python CLIs](https://intern.iis.fhg.de/x/IeVsBg) in the [IIS Knowledge Base][]. + +[iis knowledge base]: https://s.fhg.de/iis-kb +[click]: https://click.palletsprojects.com/ +[typer]: https://typer.tiangolo.com/ diff --git a/docs/user-guide/topics/documentation.md b/docs/user-guide/topics/documentation.md index 95324c94..819a5236 100644 --- a/docs/user-guide/topics/documentation.md +++ b/docs/user-guide/topics/documentation.md @@ -12,7 +12,7 @@ The `README` included with the template only covers the bare minimum (installati ??? quote "`README.md`" - {{ includex('docs/examples/default/README.md', indent=4, lang='md', replace=[("```", "'''")])}} + {{ includex('docs/examples/default/README.md', indent=4, code=True, replace=[("```", "'''")])}} For additional sections often found in READMEs, see [Make a README], this [README Generator] or explore [READMEs of popular GitHub projects]. @@ -26,7 +26,7 @@ The `CHANGELOG` included with the template follows the [keep a changelog][] form ??? quote "`CHANGELOG.md`" - {{ includex('docs/examples/default/CHANGELOG.md', indent=4, lang='md', replace=[("```", "'''")])}} + {{ includex('docs/examples/default/CHANGELOG.md', indent=4, code=True, replace=[("```", "'''")])}} [keep a changelog]: https://keepachangelog.com/en/1.1.0 diff --git a/docs/user-guide/topics/task-execution.md b/docs/user-guide/topics/task-execution.md index 46028573..c2e17194 100644 --- a/docs/user-guide/topics/task-execution.md +++ b/docs/user-guide/topics/task-execution.md @@ -13,18 +13,18 @@ This project template relies on [GNU make][make intro] as a task runner. While i An overview of the included Makefile targets and what they do can be obtained using `make help`: ```console -{{ run('make', 'help', cwd='docs/examples/default') | replace('[36m', '') | replace('[0m', '')}} +{{ run('make help', cwd='docs/examples/default') | replace('[36m', '') | replace('[0m', '')}} ``` ### Installation -{{ includex('docs/examples/default/Makefile', start_match='install-dev:', end_match='.PHONY', lang="Makefile") }} +{{ includex('docs/examples/default/Makefile', start_match='install-dev:', end_match='.PHONY', code=True) }} The project is being installed in place (using pip's `-e` option) including all optional requirements (given in square brackets). The project template keeps development requirements as optional requirements of the Python package in the [pyproject.toml][], so these can be installed alongside the project. -{{ includex('docs/examples/default/pyproject.toml', lang='toml', start_match='[project.optional-dependencies]', end_match='# ') }} +{{ includex('docs/examples/default/pyproject.toml', code=True, start_match='[project.optional-dependencies]', end_match='# ') }} The advantage of this is that development dependencies are handled exactly the same as other dependencies. A possible downside of this approach is that these optional dependencies are also included in the package built for users. If this is undesirable, an alternative approach would be to keep development requirements in a separate file (e.g. `dev-requirements.txt`) or use tooling that manages development requirements (e.g. [pipenv][]). @@ -43,12 +43,12 @@ A key aspect of maintainability is reducing accidental complexity[^1]. This mean [radon-docs]: https://beta.ruff.rs/docs/https://radon.readthedocs.io/ -{{ includex('docs/examples/default/Makefile', start_match='maintainability:', end_match='.PHONY', lang="Makefile") }} +{{ includex('docs/examples/default/Makefile', start_match='maintainability:', end_match='.PHONY', code=True) }} One such tool to estimate complexity is [radon][], which can be used to calculate the average cyclomatic complexity (cc) for your project: ```console -{{ run('make', 'maintainability', cwd='docs/examples/default', should_exit_with_error=True) }} +{{ run('make maintainability', cwd='docs/examples/default', should_exit_with_error=True) }} ``` [^1]: sometimes also called incidental complexity @@ -58,7 +58,7 @@ One such tool to estimate complexity is [radon][], which can be used to calculat Another type of static analysis is code linting i.e. analyzing source code for potential errors, code style violations, and programming best practice adherence. -{{ includex('docs/examples/default/Makefile', start_match='lint:', end_match='.PHONY', lang="Makefile") }} +{{ includex('docs/examples/default/Makefile', start_match='lint:', end_match='.PHONY', code=True) }} ##### ruff @@ -68,13 +68,13 @@ Another type of static analysis is code linting i.e. analyzing source code for p ### Testing -{{ includex('docs/examples/default/Makefile', start_match='coverage:', end_match='.PHONY', lang="Makefile") }} +{{ includex('docs/examples/default/Makefile', start_match='coverage:', end_match='.PHONY', code=True) }} ### Documentation ??? quote "Makefile - Documentation Targets" - {{ includex('docs/examples/default/Makefile', start_match='.PHONY: docs', end_match='.PHONY', start_offset=1, lang="Makefile", indent=4) }} + {{ includex('docs/examples/default/Makefile', start_match='.PHONY: docs', end_match='.PHONY', start_offset=1, code=True, indent=4) }} --- diff --git a/docs/user-guide/topics/testing.md b/docs/user-guide/topics/testing.md index be625c3c..9f1dd6a5 100644 --- a/docs/user-guide/topics/testing.md +++ b/docs/user-guide/topics/testing.md @@ -17,7 +17,7 @@ Both pytest and coverage.py support configuration via the `pyproject.toml` file. Python tests are implemented using [pytest][] in the `tests` subdirectory. ``` -{{ run("tree", "--noreport", "docs/examples/default/tests") }} +{{ run("tree --noreport docs/examples/default/tests") }} ``` Each test module starts with `test_` so it is automatically discovered when running `pytest`. @@ -32,7 +32,7 @@ Tests can be executed like this ```console $ pytest -q -{{ run(".venv/bin/pytest", "--config-file", "pyproject.toml", "-q", cwd="docs/examples/default") }} +{{ run(".venv/bin/pytest --config-file pyproject.toml -q", cwd="docs/examples/default") }} ``` If you have never used pytest before, check out their [pytest documentation](https://docs.pytest.org/en/7.4.x/contents.html). The more you know about pytest, the better your test suite is going to be. 😉 @@ -43,7 +43,7 @@ If a remote has been configured, your tests are also automatically run as part o ??? quote "Run tests in GitLab CI" - {{ includex('docs/examples/gitlab/.gitlab-ci.yml', lang='yaml', start_match='test:\n', end_match='pages:') | indent(4) }} + {{ includex('docs/examples/gitlab/.gitlab-ci.yml', code=True, start_match='test:\n', end_match='pages:') | indent(4) }} A coverage report is created and linked in the README file. @@ -54,10 +54,5 @@ Most Python IDE's integrate with test suites written for pytest and allow you to ![](https://cln.sh/Krfmprql+) *VSCode automatically loads your test suite in the "Testing" sidebar and makes it easy to (1) run or debug all your tests or (2) run or debug individual tests. It understands parametrized tests and breaks them out as separate test cases.*{.caption} -
- ![VSCode automatically loads your test suite in the "Testing" sidebar and makes it easy to (1) run or debug all your tests or (2) run or debug individual tests. It understands parametrized tests and breaks them out as separate test cases.](https://cln.sh/Krfmprql+) -
VSCode automatically loads your test suite in the "Testing" sidebar and makes it easy to (1) run or debug all your tests or (2) run or debug individual tests. It understands parametrized tests and breaks them out as separate test cases.
-
- [pytest]: https://pytest.org/ [coverage.py]: https://coverage.readthedocs.io/ diff --git a/docs/user-guide/topics/versioning.md b/docs/user-guide/topics/versioning.md index 6359d044..c2843d97 100644 --- a/docs/user-guide/topics/versioning.md +++ b/docs/user-guide/topics/versioning.md @@ -2,7 +2,7 @@ The version is defined in the `pyproject.toml` file: -{{ includex("docs/examples/default/pyproject.toml", start_match="Version", end_match="version =", end_offset=1, lang="toml", caption=True)}} +{{ includex("docs/examples/default/pyproject.toml", start_match="Version", end_match="version =", end_offset=1, code=True, caption=True)}} ## Semantic Versioning diff --git a/docs/util/macros.py b/docs/util/macros.py index 557323c6..ccecb3a9 100644 --- a/docs/util/macros.py +++ b/docs/util/macros.py @@ -9,11 +9,12 @@ import os import pathlib import re +import shlex import subprocess +import tomllib import unicodedata import pymdownx.magiclink -import tomllib import yaml from mkdocs_macros.plugin import MacrosPlugin @@ -116,7 +117,8 @@ def get_files(directory: str | pathlib.Path, match: str = "", ignore: str = "") def run( - *command, + command, + *args, setup: list = None, skip_lines=0, show_command=False, @@ -129,11 +131,13 @@ def run( # ensure arguments are all strings setup = [str(x) for x in setup] - command = [str(x) for x in command] - - if command[0].startswith("$ "): - command[0] = command[0].replace("$ ", "") + if command.startswith("$ "): + command = command[1:] show_command = True + if " " in command: + command = shlex.split(command) + else: + command = [command, *[str(x) for x in args]] filename = _get_filename(command, setup, cwd) fp_cached_command = fp_cli_command_output_cache / filename diff --git a/mkdocs.yaml b/mkdocs.yaml index 245fb9c6..ae9a450b 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -13,6 +13,7 @@ theme: # https://squidfunk.github.io/mkdocs-material/setup/ tag: doc: material/book-open-page-variant test: material/test-tube + vcs: octicons/git-branch-16 default: material/tag favicon: assets/material-satellite-variant.svg features: @@ -125,3 +126,4 @@ extra: Documentation: doc Testing: test Continuous Integration: ci + Version Control: vcs diff --git a/noxfile.py b/noxfile.py index d8bf7a15..efe0654d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -5,14 +5,15 @@ nox.options.reuse_existing_virtualenvs = True nox.options.stop_on_first_error -nox.options.sessions = ['build', 'test'] +nox.options.sessions = ["build", "test"] -source_directories = ('src') +source_directories = "src" -supported_python_versions = ['3.11'] +supported_python_versions = ["3.11"] root = pathlib.Path(__file__).parent -build_dir = root/"build"/"dist" +build_dir = root / "build" / "dist" + @nox.session def build(session: nox.Session): @@ -20,15 +21,17 @@ def build(session: nox.Session): shutil.rmtree(build_dir) build_dir.mkdir(parents=True, exist_ok=False) - session.run('make', 'build' , external=True, env={'BUILDDIR': str(build_dir)}) + session.run("make", "build", external=True, env={"BUILDDIR": str(build_dir)}) @nox.session(python=supported_python_versions) def test(session: nox.Session): """Run all tests against dev and target environment.""" - wheel_files = sorted(build_dir.glob('*.whl')) + wheel_files = sorted(build_dir.glob("*.whl")) print(wheel_files) assert len(wheel_files) == 1, "a single wheel should have been built" wheel_file = wheel_files[0] session.install(f"{wheel_file}[test]") - session.run('pytest', '-k', 'test_package', *session.posargs, env={'BIN_PATH': str(session.bin)}) + session.run( + "pytest", "-k", "test_package", *session.posargs, env={"BIN_PATH": str(session.bin)} + ) diff --git a/pyproject.toml b/pyproject.toml index 0aef73a5..2ff6b577 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,10 +30,6 @@ fallback_version = "0.0.0" ] "init_python_project.template" = [".*", "**/.*"] -[tool.setuptools.exclude-package-data] -"*" = ["**/__pycache__"] -"init_python_project.template" = ["**/__pycache__"] - [project.optional-dependencies] dev = ["black", "pre-commit", "ruff"] diff --git a/src/init_python_project/cli.py b/src/init_python_project/cli.py index 202f46f6..ad43efcc 100644 --- a/src/init_python_project/cli.py +++ b/src/init_python_project/cli.py @@ -1,14 +1,28 @@ import sys from pathlib import Path +from typing import Annotated, Optional from copier import run_copy -from typer import Argument, Typer, colors, confirm, style +from typer import Argument, Option, Typer, colors, confirm, style app = Typer() +__version__ = "0.0.2" + + +def version_callback(value: bool) -> None: + if value: + print(__version__) + sys.exit(0) + @app.command(name="init-python-project") -def cli(target_path: Path = Argument("new-project")) -> None: +def cli( + target_path: Path = Argument("new-project"), + version: Annotated[ + Optional[bool], Option("--version", callback=version_callback, is_eager=True) + ] = None, +) -> None: """Executes the CLI command to create a new project.""" target_path.mkdir(exist_ok=True) if ( diff --git a/src/init_python_project/copier.yaml b/src/init_python_project/copier.yaml deleted file mode 120000 index c74bb244..00000000 --- a/src/init_python_project/copier.yaml +++ /dev/null @@ -1 +0,0 @@ -../../copier.yaml \ No newline at end of file diff --git a/src/init_python_project/copier.yaml b/src/init_python_project/copier.yaml new file mode 100644 index 00000000..e0d62d66 --- /dev/null +++ b/src/init_python_project/copier.yaml @@ -0,0 +1,170 @@ +_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/src/init_python_project/template b/src/init_python_project/template deleted file mode 120000 index 8bed5e15..00000000 --- a/src/init_python_project/template +++ /dev/null @@ -1 +0,0 @@ -../../template \ No newline at end of file diff --git a/tests/test_package.py b/tests/test_package.py index 4dec9026..0e797d9b 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -7,27 +7,28 @@ @pytest.fixture def bin(): import os - yield Path(os.environ["BIN_PATH"]) + + yield Path(os.getenv("BIN_PATH", ".")) 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') - child.expect('.* package.*') - child.sendline('') # accept default - child.expect('.* pre-commit.*') - child.send('y') - child.expect('.* bumpversion.*') - child.send('y') - child.expect('.* documentation.*') - child.sendline('') # accept default - child.expect('.* platform.*') - child.sendline('') # accept default - child.expect('.* name.*') - child.sendline('cool-user') # accept default - child.expect('.* remote.*') - child.sendline('') # accept default - child.expect('.* initial git branch.*') - child.sendline('') # accept default + child = pexpect.spawn(str(bin / "init-python-project"), ["my-project"], cwd=tmp_path, timeout=3) + child.expect(".* project.*") + child.sendline("My Project") + child.expect(".* package.*") + child.sendline("") # accept default + child.expect(".* pre-commit.*") + child.send("y") + child.expect(".* bumpversion.*") + child.send("y") + child.expect(".* documentation.*") + child.sendline("") # accept default + child.expect(".* platform.*") + child.sendline("") # accept default + child.expect(".* name.*") + child.sendline("cool-user") # accept default + child.expect(".* remote.*") + child.sendline("") # accept default + child.expect(".* initial git branch.*") + child.sendline("") # accept default