├── .github └── workflows │ └── python-build.yml ├── LICENSE ├── Makefile ├── README.md ├── cookiecutter.json ├── example_python_project ├── .editorconfig ├── .github │ └── workflows │ │ ├── python-build.yml │ │ └── python-publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── LICENSE ├── Makefile ├── README.md ├── docsrc │ ├── Makefile │ ├── _static │ │ └── .gitignore │ ├── _templates │ │ └── .gitignore │ ├── conf.py │ └── index.rst ├── example_python_project │ ├── __init__.py │ ├── about.py │ ├── config.py │ └── config_test.py └── pyproject.toml ├── hooks ├── post_gen_project.py └── pre_gen_project.py └── {{cookiecutter.module_name}} ├── .editorconfig ├── .github └── workflows │ ├── python-build.yml │ └── python-publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── LICENSE ├── Makefile ├── README.md ├── docsrc ├── Makefile ├── _static │ └── .gitignore ├── _templates │ └── .gitignore ├── conf.py └── index.rst ├── pyproject.toml └── {{cookiecutter.module_name}} ├── __init__.py ├── about.py ├── config.py └── config_test.py /.github/workflows/python-build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install the python package and dependencies, and run tests against a variety of Python versions 2 | 3 | name: Build 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | schedule: 11 | - cron: "0 13 * * *" # Every day at 1pm UTC (6am PST) 12 | workflow_dispatch: 13 | 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | python-version: ['3.11', '3.12', '3.13'] 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Cut cookies 31 | run: | 32 | python -m pip install cookiecutter 33 | git config --global user.email "you@example.com" 34 | git config --global user.name "Your Name" 35 | cookiecutter --no-input --overwrite-if-exists . 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi 40 | cd example_python_project 41 | python -m pip install ".[dev]" # install package + test dependencies 42 | - name: About 43 | run: | 44 | cd example_python_project 45 | python -m example_python_project.about 46 | - name: Test with pytest 47 | run: | 48 | cd example_python_project 49 | python -m pytest --cov --cov-fail-under 100 50 | - name: Lint ruff 51 | run: | 52 | python -m ruff check example_python_project hooks 53 | - name: Typecheck with mypy 54 | run: | 55 | cd example_python_project 56 | python -m mypy example_python_project 57 | - name: Build documentation with sphinx 58 | run: | 59 | cd example_python_project 60 | sphinx-build -M html docsrc docsrc/_build 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Kudos: Adapted from Auto-documenting default target 3 | # https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 4 | 5 | .DEFAULT_GOAL := help 6 | 7 | FILES = hooks 8 | 9 | help: 10 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}' 11 | 12 | 13 | test: ## Build example cookicutter project and test 14 | rm -rf example_python_project/.git # Remove old git test repo if previously initilized 15 | python -m cookiecutter --no-input --overwrite-if-exists . 16 | cd example_python_project; make all 17 | rm -rf example_python_project/.git # Not needed 18 | 19 | status: ## git status --short --branch 20 | @git status --short --branch 21 | 22 | .PHONY: help 23 | .PHONY: test 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modern-python-template: How to setup an open source, github hosted, python package 2 | 3 | 4 | ![Build Status](https://github.com/gecrooks/modern-python-template/workflows/Build/badge.svg) 5 | 6 | [Source](https://github.com/gecrooks/modern-python-template) 7 | 8 | ## History 9 | 10 | ### v3 2024-02-28 11 | * Move all project configuration into pyproject.toml (and remove the legacy setup.cfg and setup.py files) 12 | * Replace isort, black, and flake8 with ruff 13 | 14 | 15 | ## Quickstart 16 | 17 | This is a [cookiecutter](https://github.com/cookiecutter/cookiecutter) python template for a minimal python package. 18 | Install and run cookiecutter, answer the configuration questions, and you should be good to go. 19 | 20 | pip install -U cookiecutter 21 | cookiecutter https://github.com/gecrooks/modern-python-template.git 22 | 23 | To complete github setup, create a new empty repo on github with the same name, add origin to our 24 | project, and push to github. 25 | 26 | cd example_python_project 27 | git remote add origin https://github.com/somebody/example_python_project.git 28 | git push -u origin master 29 | git push origin v0.0.0 30 | 31 | On github, you'll want to complete the About section (project description, website, and topics), add your PyPi user name and password as Secrets (if you're planning to upload to PyPi), and protect the [master branch](https://amachreeowanate.medium.com/how-to-protect-the-master-branch-on-github-ab85e9b6b03). 32 | 33 | ## About: On the creation and crafting of a python project 34 | 35 | This is a discussion of the steps needed to setup an open source, github hosted, python package ready for further development. 36 | The minimal project we're building is located in the [example_python_project](example_python_project) subdirectory. The rest of the files in the repo are for a [cookiecutter](https://github.com/cookiecutter/cookiecutter) template to create the example python project. 37 | 38 | ## Naming 39 | 40 | The first decision to make is the name of the project. And for python packages the most important criteria is that the name isn't already taken on [pypi](https://pypi.org/), the repository from which we install python packages with `pip`. So we should do a quick Internet search: This name is available on pypi, there are no other repos of that name on github, and a google search doesn't pull up anything relevant. So we're good to go. 41 | 42 | Note that github repo and pypi packages are generally named using dashes (`-`), but that the corresponding python modules are named with underscores (`_`). (The reason for this dichotomy appears to be that underscores don't work well in URLs, but dashes are frowned upon in filenames.) 43 | 44 | ## License 45 | 46 | The next decision is which of the plethora of [Open Source](https://opensource.org/licenses) licenses to use. We'll use the [Apache License](https://opensource.org/licenses/Apache-2.0), a perfectly reasonable, and increasingly popular choice. 47 | 48 | 49 | ## Create repo 50 | 51 | Next we need to initialize a git repo. It's easiest to create the repo on github and clone to our local machine (This way we don't have to mess around setting the origin and such like). Github will helpfully add a `README.md`, the license, and a python `.gitignore` for us. On Github, add a description, website url (typically pointing at readthedocs), project tags, and review the rest of github's settings. 52 | 53 | 54 | Note that MacOS likes to scatter `.DS_Store` folders around (they store the finder icon display options). We don't want to accidentally add these to our repo. But this is a machine/developer issue, not a project issue. So if you're on a mac you should configure git to ignore `.DS_Store` globally. 55 | 56 | ``` 57 | # specify a global exclusion list 58 | git config --global core.excludesfile ~/.gitignore 59 | # adding .DS_Store to that list 60 | echo .DS_Store >> ~/.gitignore 61 | ``` 62 | 63 | ## Clone repo 64 | 65 | On our local machine the first thing we do is create a new conda environment. (You have conda installed, right?) This way if we balls up the installation of some dependency (which happens distressingly often) we can nuke the environment and start again. 66 | ``` 67 | $ conda create --name GPT 68 | $ source activate GPT 69 | (GPT) $ python --version 70 | Python 3.11.0 71 | ``` 72 | 73 | Now we clone the repo locally. 74 | 75 | ``` 76 | (GPT) $ git clone https://github.com/gecrooks/modern-python-template.git 77 | Cloning into 'modern-python-template'... 78 | remote: Enumerating objects: 4, done. 79 | remote: Counting objects: 100% (4/4), done. 80 | remote: Compressing objects: 100% (3/3), done. 81 | remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 82 | Unpacking objects: 100% (4/4), done. 83 | (GPT) $ cd modern-python-template 84 | ``` 85 | 86 | Lets tag this initial commit for posterities sake (And so I can [link](https://github.com/gecrooks/modern-python-template/releases/tag/v0.0.0) to the code at this instance). 87 | ``` 88 | (GPT) $ git tag v0.0.0 89 | (GPT) $ git push origin v0.0.0 90 | ``` 91 | For reasons that are unclear to me the regular `git push` doesn't push tags. We have push the tags explicitly by name. Note we need to specify a full MAJOR.MINOR.PATCH version number, and not just e.g. '0.1', for technical reasons that have to do with how we're going to manage package versions. 92 | 93 | 94 | ## Branch 95 | It's always best to craft code in a branch, and then merge that code into the master branch. 96 | ``` 97 | $ git branch gec001-init 98 | $ git checkout gec001-init 99 | Switched to branch 'gec001-init' 100 | ``` 101 | I tend to name branches with my initials (so I know it's my branch on multi-developer projects), a serial number (so I can keep track of the chronological order of branches), and a keyword (if I know ahead of time what the branch is for). 102 | 103 | 104 | ## Packaging 105 | 106 | Let's complete the minimum viable python project. We need the actual python module, signaled by a (currently) blank `__init__.py` file. 107 | ``` 108 | (GPT) $ mkdir example_python_project 109 | (GPT) $ touch example_python_project/__init__.py 110 | ``` 111 | 112 | Python standards for packaging and distribution seems to be in flux (again...). So, following what I think the current standard is, we need 3 files, `setup.py`, `pyproject.toml`, and `setup.cfg`. 113 | 114 | The modern `setup.py` is just a husk: 115 | 116 | ``` 117 | #!/usr/bin/env python 118 | 119 | import setuptools 120 | 121 | if __name__ == "__main__": 122 | setuptools.setup(use_scm_version=True) 123 | ``` 124 | Our only addition is `use_scm_version=True`, which activates versioning with git tags. More on that anon. Don't forget to set executable permissions on the setup.py script. 125 | ``` 126 | $ chmod a+x setup.py 127 | ``` 128 | The [pyproject.toml](https://snarky.ca/what-the-heck-is-pyproject-toml/) file (written in [toml](https://github.com/toml-lang/toml) format) is a recent addition to the canon. It specifies the tools used to build the project. 129 | ``` 130 | # pyproject.toml 131 | [build-system] 132 | requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] 133 | build-backend = "setuptools.build_meta" 134 | 135 | 136 | # pyproject.toml 137 | [tool.setuptools_scm] 138 | 139 | ``` 140 | Again, the parts with `setuptools_scm` are additions. 141 | 142 | 143 | All of the rest of the metadata goes in `setup.cfg` (in INI format). 144 | ``` 145 | # Setup Configuration File 146 | # setup.cfg is the configuration file for setuptools. It tells setuptools about your package 147 | # (such as the name and version) as well as which code files to include. Eventually much of 148 | # this configuration may be able to move to pyproject.toml. 149 | # 150 | # https://packaging.python.org/tutorials/packaging-projects/ 151 | # https://docs.python.org/3/distutils/configfile.html 152 | # [INI](https://docs.python.org/3/install/index.html#inst-config-syntax) file format. 153 | # 154 | # Project cut from gecrooks_python_template cookiecutter template 155 | # https://github.com/gecrooks/modern-python-template 156 | 157 | 158 | [metadata] 159 | # https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html 160 | # SPDX license short-form identifier, https://spdx.org/licenses/ 161 | # https://pypi.org/classifiers/ 162 | # setuptools v53.1.0+ expects lower cased keys, e.g. "Name" must be "name". 163 | 164 | name = {{cookiecutter.module_name}} 165 | summary = {{cookiecutter.short_description}} 166 | long_description = file:README.md 167 | long_description_content_type = text/markdown 168 | keywords = python 169 | url = https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.module_name}}/ 170 | author = {{cookiecutter.author_name}} 171 | author_email = {{cookiecutter.author_email}} 172 | license = {{cookiecutter.license}} 173 | license_file = LICENSE 174 | classifiers= 175 | Development Status :: 4 - Beta 176 | Intended Audience :: Developers 177 | Intended Audience :: Science/Research 178 | Programming Language :: Python 179 | Natural Language :: English 180 | Operating System :: OS Independent 181 | Programming Language :: Python :: 3 182 | Programming Language :: Python :: 3.9 183 | Programming Language :: Python :: 3.10 184 | Programming Language :: Python :: 3.11 185 | Topic :: Scientific/Engineering 186 | Topic :: Software Development 187 | Topic :: Software Development :: Libraries 188 | Topic :: Software Development :: Libraries :: Python Modules 189 | Typing :: Typed 190 | 191 | 192 | [options] 193 | zip_safe = True 194 | python_requires = >= 3.9 195 | packages = find: 196 | 197 | install_requires = 198 | numpy 199 | 200 | setup_requires = 201 | setuptools_scm 202 | 203 | 204 | [options.extras_require] 205 | dev = 206 | numpy >= 1.20 # v1.20 introduces typechecking for numpy 207 | setuptools_scm 208 | pytest >= 4.6 209 | pytest-cov 210 | flake8 211 | mypy 212 | black 213 | isort 214 | sphinx 215 | ``` 216 | Confusingly there are two different standards for metadata. At present the metadata 217 | lives in `setup.cfg` and should follow the setuptools 218 | [specification](https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html). 219 | But the intention seems to be 220 | that in the long run the metadata moves to `pyproject.toml` and follows a different 221 | [specification](https://packaging.python.org/specifications/core-metadata/). 222 | 223 | 224 | It's good practice to support at least two consecutive versions of python. Starting with 3.9, python is moving to an annual [release schedule](https://www.python.org/dev/peps/pep-0602/). The initial 3.x.0 release will be in early October and the first bug patch 3.x.1 in early December, second in February, and so on. Since it takes many important packages some time to upgrade (e.g. numpy and tensorflow are often bottlenecks), one should probably plan to upgrade python support around the beginning of each year. Upgrading involves changing the python version numbers in the workflow tests and `config.cfg`, and then cleaning up any `__future__` or conditional imports, or other hacks added to maintain compatibility with older python releases. If you protected the master branch on github, and added required status checks, you'll need to update those too. Supporting older python versions is often a good idea, if you don't need the newest wizz-bang python features. 225 | 226 | 227 | We can now install our package (as editable -e, so that the code in our repo is live). 228 | ``` 229 | $ pip install -e .[dev] 230 | ``` 231 | The optional `[dev]` will install all of the extra packages we need for test and development, listed under `[options.extras_require]` above. 232 | 233 | 234 | 235 | ## Versioning 236 | Our project needs a version number (e.g. '3.1.4'). We'll try and follow the [semantic versioning](https://semver.org/) conventions. But as long as the major version number is '0' we're allowed to break things. 237 | 238 | There should be a 239 | [single source of truth](https://packaging.python.org/guides/single-sourcing-package-version/) for this number. 240 | My favored approach is use git tags as the source of truth (Option 7 in the above linked list). We're going to tag releases anyways, so if we also hard code the version number into the python code we'd violate the single source of truth principle. We use the [setuptools_scm](https://github.com/pypa/setuptools_scm) package to automatically construct a version number from the latest git tag during installation. 241 | 242 | The convention is that the version number of a python packages should be available as `packagename.__version__`. 243 | So we add the following code to `example_python_project/config.py` to extract the version number metadata. 244 | ``` 245 | 246 | __all__ = ["__version__", "importlib_metadata", "about"] 247 | 248 | 249 | # Backwards compatibility imports 250 | try: 251 | # python >= 3.9 252 | from importlib import metadata as importlib_metadata # type: ignore 253 | except ImportError: # pragma: no cover 254 | import importlib_metadata # type: ignore # noqa: F401 255 | 256 | 257 | try: 258 | __version__ = importlib_metadata.version(__package__) # type: ignore 259 | except Exception: # pragma: no cover 260 | # package is not installed 261 | __version__ = "0.0.0" 262 | 263 | ``` 264 | and then in `example_python_project/__init__.py`, we import this version number. 265 | ``` 266 | from .config import __version__ as __version__ # noqa: F401 267 | ``` 268 | We put the code to extract the version number in `config.py` and not `__init__.py`, because we don't want to pollute our top level package namespace. 269 | 270 | The various pragmas in the code above ("pragma: no cover" and "type: ignore") are there because the conditional imports confuse both our type checker and code coverage tools. 271 | 272 | 273 | 274 | ## about 275 | 276 | One of my tricks is to add a function to print the versions of the core upstream dependencies. This can be extremely helpful when debugging configuration or system dependent bugs, particularly when running continuous integration tests. 277 | 278 | ``` 279 | # Configuration (> python -m example_python_project.about) 280 | platform macOS-10.16-x86_64-i386-64bit 281 | example_python_project 0.0.0 282 | python 3.10.3 283 | numpy 1.20.1 284 | setuptools_scm 5.0.2 285 | pytest 6.2.2 286 | pytest-cov 2.11.1 287 | flake8 6.0.0 288 | mypy 0.812 289 | black 20.8b1 290 | isort 5.7.0 291 | sphinx 3.5.1 292 | pre-commit 2.20.0 293 | ``` 294 | The `about()` function to print this information is placed in `about_.py`. The file `about.py` contains the standard python command line interface (CLI), 295 | ``` 296 | if __name__ == '__main__': 297 | import example_python_project 298 | example_python_project.about() 299 | ``` 300 | It's important that `about.py` isn't imported by any other code in the package, else we'll get multiple import warnings when we try to run the CLI. 301 | 302 | If you don't want the `about` functionality remove the file `about.py`, `about()` function in config.py, and relevant tests in `config_test.py`, and edit the Makefile. 303 | 304 | ## Unit tests 305 | 306 | Way back when I worked as a commercial programmer, the two most important things that I learned were source control and unit tests. Both were largely unknown in the academic world at the time. 307 | 308 | (I was once talking to a chap who was developing a new experimental platform. The plan was to build several dozens of these gadgets, and sell them to other research groups so they didn't have to build their own. A couple of grad students wandered in. They were working with one of the prototypes, and they'd found some minor bug. Oh yes, says the chap, who goes over to his computer, pulls up the relevant file, edits the code, and gives the students a new version of that file. He didn't run any tests, because there were no tests. And there was no source control, so there was no record of the change he'd just made. That was it. The horror.) 309 | 310 | Currently, the two main options for python unit tests appear to be `unittest` from the standard library and `pytest`. To me `unittest` feels very javonic. There's a lot of boiler plate code and I believe it's a direct descendant of an early java unit testing framework. Pytest, on the other hand, feels pythonic. In the basic case all we have to do is to write functions (whose names are prefixed with 'test_'), within which we test code with `asserts`. Easy. 311 | 312 | There's two common ways to organize tests. Either we place tests in a separate directory, or they live in the main package along with the rest of the code. In the past I've used the former approach. It keeps the test organized and separate from the production code. But I'm going to try the second approach for this project. The advantage is that the unit tests for a piece of code live right next to the code being tested. 313 | 314 | Let's test that we can access the version number (There is no piece of code too trivial that it shouldn't have a unit test.) In `example_python_project/config_test.py` we add 315 | 316 | ``` 317 | import example_python_project 318 | 319 | def test_version(): 320 | assert example_python_project.__version__ 321 | ``` 322 | and run our test. (The 'python -m' prefix isn't strictly necessary, but it helps ensure that pytest is running under the correct copy of python.) 323 | ``` 324 | 325 | (GTP) $ python -m pytest 326 | ========================================================================================== test session starts =========================================================================================== 327 | platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 328 | rootdir: /Users/work/Work/Projects/example_python_project 329 | collected 1 item 330 | 331 | example_python_project/config_test.py . [100%] 332 | 333 | =========================================================================================== 1 passed in 0.02s ============================================================================================ 334 | ``` 335 | 336 | Note that in the main code we'll access the package with relative imports, e.g. 337 | ``` 338 | from . import __version__ 339 | ``` 340 | But in the test code we use absolute imports. 341 | ``` 342 | from example_python_project import __version__ 343 | ``` 344 | In tests we want to access our code in the same way we would access it from the outside as an end user. 345 | 346 | 347 | ## Test coverage 348 | 349 | At a bare minimum the unit tests should run (almost) every line of code. If a line of code never runs, then how do you know it works at all? (High code coverage does not mean you have a [good test suite](https://preslav.me/2020/12/03/the-myth-of-code-coverage/). But a good set of unit tests will have high code coverage.) 350 | 351 | So we want to monitor the test coverage. The [pytest-cov](https://pypi.org/project/pytest-cov/) plugin to pytest will do this for us. Configuration is placed in the setup.cfg file (Config can also be placed in a separate `.coveragerc`, but I think it's better to avoid a proliferation of configuration files.) 352 | ``` 353 | # pytest configuration 354 | [tool:pytest] 355 | testpaths = 356 | example_python_project 357 | 358 | 359 | # Configuration for test coverage 360 | # 361 | # https://coverage.readthedocs.io/en/latest/config.html 362 | # 363 | # python -m pytest --cov 364 | 365 | [coverage:paths] 366 | source = 367 | example_python_project 368 | 369 | [coverage:run] 370 | omit = 371 | *_test.py 372 | 373 | [coverage:report] 374 | # Use ``# pragma: no cover`` to exclude specific lines 375 | exclude_lines = 376 | pragma: no cover 377 | except ImportError 378 | assert False 379 | raise NotImplementedError() 380 | pass 381 | ``` 382 | 383 | We have to explicitly omit the unit tests since we have placed the test files in the same directories as the code to test. 384 | 385 | The [pragma](https://en.wikipedia.org/wiki/Directive_(programming)) `pragma: no cover` is used to mark untestable lines. This often happens with conditional imports used for backwards compatibility between python versions. The other excluded lines are common patterns of code that don't need test coverage. 386 | 387 | 388 | ## Linting 389 | 390 | We need to lint our code before pushing any commits. I like [flake8](https://flake8.pycqa.org/en/latest/). It's faster than pylint, and (I think) better error messages. I will hereby declare: 391 | 392 | The depth of the indentation shall be 4 spaces. 393 | And 4 spaces shall be the depth of the indentation. 394 | Two spaces thou shall not use. 395 | And tabs are right out. 396 | 397 | Four spaces is standard. [Tabs are evil](https://www.emacswiki.org/emacs/TabsAreEvil). I've worked on a project with 2-space indents, and I see the appeal, but I found it really weird. 398 | 399 | Most of flake8's defaults are perfectly reasonable and in line with [PEP8](https://www.python.org/dev/peps/pep-0008/) guidance. But even [Linus](https://lkml.org/lkml/2020/5/29/1038) agrees that the old standard of 80 columns of text is too restrictive. (Allegedly, 2-space indents were [Google's](https://www.youtube.com/watch?v=wf-BqAjZb8M&feature=youtu.be&t=260) solution to the problem that 80 character lines are too short. Just make the indents smaller!) Raymond Hettinger suggests 90ish (without a hard cutoff), and [black](https://black.readthedocs.io/en/stable/the_black_code_style.html) uses 88. So let's try 88. 400 | 401 | 402 | The configuration also lives in `setup.cfg`. 403 | ``` 404 | # flake8 linter configuration 405 | [flake8] 406 | max-line-length = 88 407 | ignore = E203, W503 408 | ``` 409 | We need to override the linter on occasion. We add pragmas such as `# noqa: F401` to assert that no, really, in this case we do know what we're doing. 410 | 411 | 412 | Two other python code format tools to consider using are [isort](https://pypi.org/project/isort/) and [black, The uncompromising code formatter](https://black.readthedocs.io/en/stable/). Isort sorts your import statements into a canonical order. And Black is the Model-T Ford of code formatting -- any format you want, so long as it's Black. I could quibble about some of Black's code style, but in the end it's just easier to blacken your code and accept black's choices, and thereby gain a consistent coding style across developers. 413 | 414 | The command `make delint` will run `isort` and `black` on your code, with the right magic incantations so that they are compatible. (`isort --profile black` which appears to be equivalent to `isort -m 3 --tc --line-length 88`. We set this configuration project wide in `setup.cfg`) 415 | 416 | 417 | ## Copyright 418 | It's common practice to add a copyright and license notice to the top of every source file -- something like this: 419 | ``` 420 | 421 | # Copyright 2019-, Gavin E. Crooks and contributors 422 | # 423 | # This source code is licensed under the Apache License, Version 2.0 424 | # found in the LICENSE file in the root directory of this source tree. 425 | 426 | ``` 427 | 428 | I tend to forget to add these lines. So let's add a unit test `example_python_project/config_test.py::test_copyright` to make sure we don't. 429 | ``` 430 | def test_copyright(): 431 | """Check that source code files contain a copyright line""" 432 | exclude = set(['example_python_project/version.py']) 433 | for fname in glob.glob('example_python_project/**/*.py', recursive=True): 434 | if fname in exclude: 435 | continue 436 | print("Checking " + fname + " for copyright header") 437 | 438 | with open(fname) as f: 439 | for line in f.readlines(): 440 | if not line.strip(): 441 | continue 442 | assert line.startswith('# Copyright') 443 | break 444 | ``` 445 | 446 | 447 | ## API Documentation 448 | [Sphinx](https://www.sphinx-doc.org/en/master/usage/quickstart.html) is the standard 449 | tool used to generate API documentation from the python source. Use the handy quick start tools. 450 | ``` 451 | $ mkdir docsrc 452 | $ cd docsrc 453 | $ sphinx-quickstart 454 | ``` 455 | The defaults are reasonable. Enter the project name and author when prompted. 456 | 457 | Edit the conf.py, and add the following collection of extensions. 458 | ``` 459 | extensions = [ 460 | 'sphinx.ext.autodoc', 461 | 'sphinx.ext.napoleon', 462 | ] 463 | ``` 464 | [Autodoc](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html) automatically extracts documentation from docstrings, and [napolean](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) enables [Google style](http://google.github.io/styleguide/pyguide.html) python docstrings. 465 | 466 | We also add a newline at the end of `conf.py`, since the lack of a blank line at the end upsets our linter. 467 | 468 | Go ahead and give it a whirl. This won't do anything interesting yet, but it's a start. 469 | ``` 470 | $ make html 471 | ``` 472 | 473 | One problem is that sphinx creates three (initially) empty directories, `_build`, `_static`, and `_templates`. But we can't add empty directories to git, since git only tracks files. The workaround is to add an empty `.gitignore` file to each of the `_static` and `_templates` directories. (An alternative convention is to add a `.gitkeep` file.) If we never want the files in these directories to be under source control, we can add a `*` to the `.gitignore` file. Sphinx will create the `_build` directory when it's needed. 474 | 475 | ``` 476 | $ touch _templates/.gitignore _build/.gitignore _static/.gitignore 477 | $ git add -f _templates/.gitignore _build/.gitignore _static/.gitignore 478 | $ git add Makefile *.* 479 | # cd .. 480 | ``` 481 | 482 | 483 | Note that we have placed the sphinx documentation tools in `docsrc` rather than the more traditional `docs`. This is to keep the `docs` directory available to serve documentation using `githubs-pages`. (We also have to update the root `.gitignore` file.) 484 | 485 | 486 | ## Makefile 487 | I like to add a Makefile with targets for all of the common development tools I need to run. This is partially for convenience, and partially as documentation, i.e. here are all the commands you need to run to test, lint, typecheck, and build the code (and so on.) I use a [clever hack](https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html) so that the makefile self documents. 488 | 489 | ``` 490 | (GTP) $ make 491 | about Report versions of dependent packages 492 | status git status --short --branch 493 | init Install package ready for development 494 | all Run all tests 495 | test Run unittests 496 | coverage Report test coverage 497 | lint Lint check python source 498 | delint Run isort and black to delint project 499 | typecheck Static typechecking 500 | docs Build documentation 501 | docs-open Build documentation and open in webbrowser 502 | docs-clean Clean documentation build 503 | docs-github-pages Install html in docs directory ready for github pages 504 | pragmas Report all pragmas in code 505 | build Setuptools build 506 | requirements Make requirements.txt 507 | ``` 508 | 509 | The pragmas target searches the code and lists all of the pragmas that occur. Common uses of [pragmas](https://en.wikipedia.org/wiki/Directive_(programming)) are to override the linter, tester, or typechecker. 510 | 511 | 512 | ## Readthedocs 513 | We'll host our API documentation on [Read the Docs](readthedocs.org). We'll need a basic configuration file, `.readthedocs.yml`. 514 | ``` 515 | version: 2 516 | formats: [] 517 | sphinx: 518 | configuration: docs/conf.py 519 | python: 520 | version: 3.9 521 | ``` 522 | I've already got a readthedocs account, so setting up a new project takes but a few minutes. 523 | 524 | 525 | ## README.md 526 | 527 | We add some basic information and installation instructions to `README.mb`. Github displays this file on your project home page (but under the file list, so if you have a lot of files at the top level of your project, people might not notice your README.) 528 | 529 | A handy trick is to add Build Status and Documentation Status badges for Github actions tests and readthedocs. These will proudly declare that your tests are passing (hopefully). (See top of this file) 530 | 531 | 532 | ## Continuous Integration 533 | 534 | Another brilliant advance to software engineering practice is continuous integration (CI). The basic idea is that all code gets thoroughly tested before it's added to the master branch. 535 | 536 | Github now makes this very easy to setup with Github actions. They even provide basic templates. This testing workflow lives in `.github/workflows/python-build.yml`, and is a modification of Github's `python-package.yml` workflow. 537 | ``` 538 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 539 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 540 | 541 | name: Python package 542 | 543 | on: 544 | push: 545 | branches: [ master ] 546 | pull_request: 547 | branches: [ master ] 548 | schedule: 549 | - cron: "0 13 * * *" # Every day at 1pm UTC (6am PST) 550 | 551 | jobs: 552 | build: 553 | 554 | runs-on: ubuntu-latest 555 | strategy: 556 | matrix: 557 | python-version: ['3.9', '3.10', '3.11'] 558 | 559 | steps: 560 | - uses: actions/checkout@v2 561 | - name: Set up Python ${{ matrix.python-version }} 562 | uses: actions/setup-python@v2 563 | with: 564 | python-version: ${{ matrix.python-version }} 565 | - name: Install dependencies 566 | run: | 567 | python -m pip install --upgrade pip 568 | python -m pip install flake8 pytest 569 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 570 | python -m pip install -e .[dev] # install package + test dependencies 571 | - name: About 572 | run: | 573 | python -m $(python -Wi setup.py --name).about 574 | - name: Lint with flake8 575 | run: | 576 | flake8 . 577 | - name: Test with pytest 578 | run: | 579 | python -m pytest --cov-fail-under 100 580 | - name: Typecheck with mypy 581 | run: | 582 | mypy 583 | - name: Build documentation with sphinx 584 | run: | 585 | sphinx-build -M html docsrc docsrc/_build 586 | 587 | ``` 588 | Note that these tests are picky. Not only must the unit tests pass, but test coverage must be 100%, the code must be delinted, blackened, isorted, and properly typed, and the docs have to build without error. 589 | 590 | It's a good idea to set a cron job to run the test suite against the main branch on a regular basis (the `schedule` block above). This will alert you of problems caused by your dependencies updating. (For instance, one of my other projects just broke, apparently because flake8 updated its rules.) 591 | 592 | Let's add, commit, and push our changes. 593 | ``` 594 | $ git status 595 | On branch gec001-init 596 | Changes to be committed: 597 | (use "git reset HEAD ..." to unstage) 598 | 599 | new file: .readthedocs.yml 600 | new file: .github/workflows/python-package.yml 601 | new file: Makefile 602 | modified: README.md 603 | new file: docs/Makefile 604 | new file: docs/_build/.gitignore 605 | new file: docs/_static/.gitignore 606 | new file: docs/_templates/.gitignore 607 | new file: docs/conf.py 608 | new file: docs/index.rst 609 | new file: pyproject.toml 610 | new file: example_python_project/__init__.py 611 | new file: example_python_project/about.py 612 | new file: example_python_project/config.py 613 | new file: example_python_project/config_test.py 614 | new file: setup.cfg 615 | new file: setup.py 616 | 617 | $ git commit -m "Minimum viable package" 618 | ... 619 | $ git push --set-upstream origin gec001-init 620 | ... 621 | ``` 622 | If all goes well Github will see our push, and build and test the code in the branch. Probably all the tests won't pass on the first try. It's easy to forget something (which is why we have automatic tests). So tweak the code, and push another commit until the tests pass. 623 | 624 | ## Git pre-commit 625 | 626 | Another handy trick is to add a (pre-commit](https://ljvmiranda921.github.io/notebook/2018/06/21/precommits-using-black-and-flake8/) hook to git, so that some tests are run before code can be committed. 627 | A basic example hook to run black before commit is located in `.pre-commit-config.yaml`. The make command `init` 628 | will install the pre-commit hook. 629 | 630 | ## Editorconfig 631 | 632 | [EditorConfig](https://editorconfig.org/) is a handy way of specifying code formatting conventions, such as indent levels and line endings. The .editorconfig lives in the root of the repository, and is understood by many popular IDEs and ext editors. 633 | 634 | 635 | ## PyPi 636 | 637 | We should now be ready to do a test submission to PyPI, The Python Package Index (PyPI). 638 | Follow the directions laid out in the [python packaging](https://packaging.python.org/tutorials/packaging-projects/) documentation. 639 | 640 | ``` 641 | $ pip install -q build twine 642 | ... 643 | $ git tag v0.1.0rc1 644 | $ python -m build 645 | ... 646 | ``` 647 | We tag our release candidate so that we get a clean version number (pypi will object to the development version numbers setuptools_scm generates if the tag or git repo isn't up to date). 648 | 649 | First we push to the pypi's test repository. 650 | ``` 651 | (GTP) $ python -m twine upload --repository testpypi dist/* 652 | ``` 653 | You'll need to create a pypi account if you don't already have one. 654 | 655 | Let's make sure it worked by installing from pypi into a fresh conda environment. 656 | ``` 657 | (GTP) $ conda deactivate 658 | $ conda create --name tmp 659 | $ conda activate tmp 660 | (tmp) $ pip install --index-url https://test.pypi.org/simple/ --no-deps modern-python-template 661 | (tmp) $ python -m example_python_project.about 662 | (tmp) $ conda activate GTP 663 | ``` 664 | 665 | 666 | ## Merge and Tag 667 | 668 | Over on github we create a pull request, wait for the github action checks to give us the green light once all the tests have passed, and then squash and merge. 669 | 670 | The full developer sequence goes something like this 671 | 672 | 1.) Sync the master branch. 673 | ``` 674 | $ git checkout master 675 | $ git pull origin master 676 | ``` 677 | (If we're working on somebody else's project, this step is a little more complicated. We fork the project on github, clone our fork to the local machine, and then set git's 'upstream' to be the original repo. We then sync our local master branch with the upstream master branch 678 | ``` 679 | $ git checkout master 680 | $ git fetch upstream 681 | $ git merge upstream/master 682 | ``` 683 | This should go smoothly as long as you never commit directly to your local master branch.) 684 | 685 | 686 | 2.) Create a working branch. 687 | ``` 688 | $ git branch BRANCH 689 | $ git checkout BRANCH 690 | ``` 691 | 692 | 3.) Do a bunch of development on the branch, committing incremental changes as we go along. 693 | 694 | 4.) Sync the master branch with github (since other development may be ongoing.) (i.e. repeat step 1) 695 | 696 | 5.) Rebase our branch to master. 697 | ``` 698 | $ git checkout BRANCH 699 | $ git rebase master 700 | ``` 701 | If there are conflicts, resolve them, and then go back to step 4. 702 | 703 | 6.) Sync our branch to github 704 | 705 | ``` 706 | $ git push 707 | ``` 708 | 709 | 7.) Over on github, create a pull request to merge into the master branch 710 | 711 | 8.) Wait for the integration tests to pass. If they don't, fix them, and then go back to step 4. 712 | 713 | 9.) Squash and merge into the master branch on github. Squashing merges all of our commits on the branch into a single commit to merge into the master branch. We generally don't want to pollute the master repo history with lots of micro commits. (On multi-developer projects, code should be reviewed. Somebody other than the branch author approves the changes before the final merge into master.) 714 | 715 | 10.) Goto step 1. Back on our local machine, we resync master, create a new branch, and continue developing. 716 | 717 | 718 | ## Tag and release 719 | 720 | Assuming everything went well, you can now upload a release to pypi proper. We can add a [github workflow](.github/workflows/python-publish.yml) to automatically upload new releases tagged on github. The only additional configuration is to upload `PYPI_USERNAME` and `PYPI_PASSWORD` to github as secrets (under your repo settings). 721 | 722 | ## Extras: requirements.txt 723 | The `setup.cfg` file specifies the minimum versions of dependencies. But for testing and deployment it can be useful to pin exact versions. 724 | 725 | > pip freeze > requirements.txt 726 | 727 | And to install these exact versions: 728 | 729 | > pip install -r requirements.txt 730 | 731 | If a `requirements.txt` exists then those versions are installed by the github workflows and the `make init` command. 732 | 733 | 734 | ## Extras: MANIFEST.in 735 | You don't need a [`MANIFEST.in` file](https://www.remarkablyrestrained.com/python-setuptools-manifest-in/). 736 | 737 | Historically, this file was used to specify which additional files, (typically data files) should be included in a packaged distribution. 738 | But `setuptools_scm` takes care of that for us (in most cases), by default including all files under source control. 739 | 740 | 741 | ## Cookiecutter 742 | 743 | Having shorted out our basic module configuration and layout, the next trick is to turn the package into a 744 | [cookiecutter](https://cookiecutter.readthedocs.io/) project template. That way we can create a new project in 745 | just a few moments. 746 | 747 | pip install -U cookiecutter 748 | cookiecutter https://github.com/gecrooks/modern-python-template.git 749 | 750 | Answer the questions, create a new empty repo on github with the same name, push, and you should be good to go. 751 | 752 | cd example_python_project 753 | git remote add origin https://github.com/somebody/example_python_project.git 754 | git push -u origin master 755 | 756 | 757 | The basic idea is to replace customizable text with template strings, e.g. `{{cookiecutter.author_email}}`. 758 | Defaults for these templates are stored in `cookiecutter.json`. In particular example_python_package is moved to a directory called 759 | `{{cookiecutter.module_name}}`, and the module code is moved to 760 | `{{cookiecutter.module_name}}/{{cookiecutter.module_name}}`. 761 | I'm more or less following [cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage) 762 | 763 | One tricky bit is that some of the github configuration files already contain similar template strings. So we have to 764 | wrap those strings in special raw tags. 765 | 766 | {% raw %} some stuff with {templates} {% endraw %} 767 | 768 | I also added some pre- and post- templating hooks (in the `hooks` subdirectory). These initialize and tag a git repo in the created module, and pip install the package. 769 | 770 | 771 | ## Conclusion 772 | 773 | By my count our minimal project has 13 configuration files (In python, toml, yaml, INI, gitignore, Makefile, and plain text formats), 2 documentation files, one file of unit tests, and 3 files of code (containing 31 lines of code). 774 | 775 | We're now ready to create a new git branch and start coding in earnest. 776 | 777 | 778 | ## Further reading 779 | 780 | * [Boring Python: dependency management, by James Bennett](https://www.b-list.org/weblog/2022/may/13/boring-python-dependencies/) 781 | * [Boring Python: code quality, by James Bennett](https://www.b-list.org/weblog/2022/dec/19/boring-python-code-quality/) 782 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "Example Python Project", 3 | "module_name": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_') }}", 4 | "short_description": "A short description of this example project", 5 | "author_name": "Some Body", 6 | "author_email": "somebody@example.com", 7 | "copywrite": "{% now 'local', '%Y' %}, {{ cookiecutter.author_name }} and contributors", 8 | "license": ["Apache-2.0", "MIT", "None"], 9 | "github_username": "somebody", 10 | "initilize_git_repo": "y" 11 | } 12 | -------------------------------------------------------------------------------- /example_python_project/.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | [*.py] 14 | max_line_length = 88 15 | 16 | [*.html] 17 | indent_size = 2 18 | 19 | [*.json] 20 | indent_size = 2 21 | insert_final_newline = ignore 22 | 23 | [Makefile] 24 | indent_style = tab 25 | 26 | 27 | [*.yml] 28 | indent_size = 2 29 | -------------------------------------------------------------------------------- /example_python_project/.github/workflows/python-build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install the python package and dependencies, and run tests against a variety of Python versions 2 | 3 | name: Build 4 | 5 | on: 6 | push: 7 | branches: [ main, master ] 8 | pull_request: 9 | branches: [ main, master ] 10 | schedule: 11 | - cron: "0 13 * * *" # Every day at 1pm UTC (6am PST) 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python-version: ['3.11', '3.12', '3.13'] 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi 32 | python -m pip install .[dev] # install package + test dependencies 33 | - name: About 34 | run: | 35 | python -m example_python_project.about 36 | - name: Test with pytest 37 | run: | 38 | python -m pytest --cov-fail-under 100 39 | - name: Lint with ruff 40 | run: | 41 | python -m ruff check . 42 | - name: Typecheck with mypy 43 | run: | 44 | python -m mypy 45 | - name: Build documentation with sphinx 46 | run: | 47 | (cd docsrc; make html) 48 | -------------------------------------------------------------------------------- /example_python_project/.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | python -m pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | 28 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 29 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 30 | 31 | run: | 32 | python -m python setup.py sdist bdist_wheel 33 | python -m twine upload dist/* 34 | -------------------------------------------------------------------------------- /example_python_project/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Additions 132 | docsrc/_build/ 133 | -------------------------------------------------------------------------------- /example_python_project/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/astral-sh/ruff-pre-commit 5 | rev: v0.1.4 6 | hooks: 7 | - id: ruff 8 | args: ["--fix"] 9 | - id: ruff-format 10 | -------------------------------------------------------------------------------- /example_python_project/.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docsrc/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | # formats: 27 | # - pdf 28 | # - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | # python: 34 | # install: 35 | # - requirements: docs/requirements.txt 36 | -------------------------------------------------------------------------------- /example_python_project/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright [yyyy] [name of copyright owner] 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | 205 | 206 | -------------------------------------------------------------------------------- /example_python_project/Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Kudos: Adapted from Auto-documenting default target 3 | # https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 4 | 5 | .DEFAULT_GOAL := help 6 | 7 | NAME = example_python_project 8 | FILES = $(NAME) docsrc/conf.py setup.py 9 | 10 | .PHONY: help 11 | help: 12 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}' 13 | 14 | 15 | .PHONY: about 16 | about: ## Report versions of dependent packages 17 | @python -m $(NAME).about 18 | 19 | 20 | .PHONY: status 21 | status: ## git status --short --branch 22 | @git status --short --branch 23 | 24 | .PHONY: init 25 | init: ## Install package ready for development 26 | python -m pip install --upgrade pip 27 | if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi 28 | python -m pip install -e .[dev] 29 | python -m pre-commit install # Install git pre-commit hook 30 | 31 | 32 | .PHONY: all 33 | all: about coverage lint typecheck docs build ## Run all tests 34 | 35 | .PHONY: test 36 | test: ## Run unittests 37 | python -m pytest --disable-pytest-warnings 38 | 39 | .PHONY: coverage 40 | coverage: ## Report test coverage 41 | @echo 42 | python -m pytest --disable-pytest-warnings --cov-report term-missing --cov $(NAME) 43 | @echo 44 | 45 | .PHONY: lint 46 | lint: ## Lint check python source 47 | @python -m ruff check 48 | 49 | .PHONY: delint 50 | delint: 51 | @echo 52 | python -m ruff format 53 | 54 | .PHONY: typecheck 55 | typecheck: ## Static typechecking 56 | python -m mypy $(NAME) 57 | 58 | .PHONY: precommit 59 | precommit: ## Run all pre-commit hooks 60 | pre-commit run --all 61 | 62 | .PHONY: docs 63 | docs: ## Build documentation 64 | (cd docsrc; make html) 65 | 66 | .PHONY: docs-open 67 | docs-open: ## Build documentation and open in webbrowser 68 | (cd docsrc; make html) 69 | open docsrc/_build/html/index.html 70 | 71 | .PHONY: docs-clean 72 | docs-clean: ## Clean documentation build 73 | (cd docsrc; make clean) 74 | 75 | .PHONY: docs-github-pages 76 | docs-github-pages: docs ## Install html in docs directory ready for github pages 77 | # https://www.docslikecode.com/articles/github-pages-python-sphinx/ 78 | @mkdir -p docs 79 | @touch docs/.nojekyll # Tell github raw html, not jekyll 80 | @cp -a _build/html/. ../docs 81 | 82 | .PHONY: pragmas 83 | pragmas: ## Report all pragmas in code 84 | @echo "** Test coverage pragmas**" 85 | @grep 'pragma: no cover' --color -r -n $(FILES) || echo "No test coverage pragmas" 86 | @echo 87 | @echo "** Linting pragmas **" 88 | @echo "(http://flake8.pycqa.org/en/latest/user/error-codes.html)" 89 | @grep '# noqa:' --color -r -n $(FILES) || echo "No linting pragmas" 90 | @echo 91 | @echo "** Typecheck pragmas **" 92 | @grep '# type:' --color -r -n $(FILES) || echo "No typecheck pragmas" 93 | 94 | .PHONY: build 95 | build: ## Build a phython soruce distribution and wheel 96 | python -m build 97 | 98 | 99 | .PHONY: requirements 100 | requirements: ## Make requirements.txt 101 | python -m pip freeze > requirements.txt 102 | -------------------------------------------------------------------------------- /example_python_project/README.md: -------------------------------------------------------------------------------- 1 | # Example Python Project: A short description of this example project 2 | 3 | 4 | ![Build Status](https://github.com/somebody/example_python_project/workflows/Build/badge.svg) 5 | [![Documentation Status](https://readthedocs.org/projects/example_python_project/badge/?version=latest)](https://example_python_project.readthedocs.io/en/latest/?badge=latest) 6 | 7 | [Source](https://github.com/gecrooks/example_python_project) 8 | 9 | -------------------------------------------------------------------------------- /example_python_project/docsrc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /example_python_project/docsrc/_static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gecrooks/modern-python-template/ea47c630a9b27edf4847d02a4a93fadb80e8f58d/example_python_project/docsrc/_static/.gitignore -------------------------------------------------------------------------------- /example_python_project/docsrc/_templates/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gecrooks/modern-python-template/ea47c630a9b27edf4847d02a4a93fadb80e8f58d/example_python_project/docsrc/_templates/.gitignore -------------------------------------------------------------------------------- /example_python_project/docsrc/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "example_python_project" 21 | copyright = "2024, Some Body and contributors" 22 | author = "Some Body" 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | "sphinx.ext.autodoc", 32 | "sphinx.ext.napoleon", 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # List of patterns, relative to source directory, that match files and 39 | # directories to ignore when looking for source files. 40 | # This pattern also affects html_static_path and html_extra_path. 41 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 42 | 43 | 44 | # -- Options for HTML output ------------------------------------------------- 45 | 46 | # The theme to use for HTML and HTML Help pages. See the documentation for 47 | # a list of builtin themes. 48 | # 49 | html_theme = "alabaster" 50 | 51 | # Add any paths that contain custom static files (such as style sheets) here, 52 | # relative to this directory. They are copied after the builtin static files, 53 | # so a file named "default.css" will overwrite the builtin "default.css". 54 | html_static_path = ["_static"] 55 | -------------------------------------------------------------------------------- /example_python_project/docsrc/index.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | Welcome to the documentation! 4 | ============================= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Contents: 9 | 10 | 11 | 12 | Indices and tables 13 | ================== 14 | 15 | * :ref:`genindex` 16 | * :ref:`modindex` 17 | * :ref:`search` 18 | -------------------------------------------------------------------------------- /example_python_project/example_python_project/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024, Some Body and contributors 2 | # 3 | # This source code is licensed under the Apache-2.0 License 4 | # found in the LICENSE file in the root directory of this source tree. 5 | 6 | from .config import * # noqa: F401, F403 7 | -------------------------------------------------------------------------------- /example_python_project/example_python_project/about.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024, Some Body and contributors 2 | # 3 | # This source code is licensed under the Apache-2.0 License 4 | # found in the LICENSE file in the root directory of this source tree. 5 | 6 | # Command line interface for the about() function 7 | # > python -m example_python_project.about 8 | # 9 | # NB: This module should not be imported by any other code in the package 10 | # (else we will get multiple import warnings) 11 | # Implementation is located in about_.py 12 | 13 | if __name__ == "__main__": 14 | import example_python_project 15 | 16 | example_python_project.about() 17 | -------------------------------------------------------------------------------- /example_python_project/example_python_project/config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024, Some Body and contributors 2 | # 3 | # This source code is licensed under the Apache-2.0 License 4 | # found in the LICENSE file in the root directory of this source tree. 5 | 6 | """ 7 | Package wide configuration 8 | """ 9 | 10 | import platform 11 | import re 12 | import sys 13 | import typing 14 | from importlib import metadata as importlib_metadata 15 | 16 | __all__ = ["__version__", "about"] 17 | 18 | 19 | try: 20 | __version__ = importlib_metadata.version(__package__) # type: ignore 21 | except Exception: # pragma: no cover 22 | # package is not installed 23 | __version__ = "0.0.0" 24 | 25 | 26 | # NB: command line access is located in about.py 27 | def about(file: typing.Optional[typing.TextIO] = None) -> None: 28 | f"""Print information about this package. 29 | 30 | ``> python -m {__package__}.about`` 31 | 32 | Args: 33 | file: Output stream (Defaults to stdout) 34 | """ 35 | metadata = importlib_metadata.metadata(__package__) # type: ignore 36 | print(f"# {metadata['Name']}", file=file) 37 | print(f"{metadata['Summary']}", file=file) 38 | print(f"{metadata['Home-page']}", file=file) 39 | 40 | name_width = 24 41 | versions = {} 42 | versions["platform"] = platform.platform(aliased=True) 43 | versions[__package__] = __version__ 44 | versions["python"] = sys.version[0:5] 45 | 46 | for req in importlib_metadata.requires(__package__): # type: ignore 47 | name = re.split("[; =><]", req)[0] 48 | try: 49 | versions[name] = importlib_metadata.version(name) # type: ignore 50 | except Exception: # pragma: no cover 51 | pass 52 | 53 | print(file=file) 54 | print("# Configuration", file=file) 55 | for name, vers in versions.items(): 56 | print(name.ljust(name_width), vers, file=file) 57 | print(file=file) 58 | 59 | 60 | # end about 61 | 62 | -------------------------------------------------------------------------------- /example_python_project/example_python_project/config_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024, Some Body and contributors 2 | # 3 | # This source code is licensed under the Apache-2.0 License 4 | # found in the LICENSE file in the root directory of this source tree. 5 | 6 | import glob 7 | import io 8 | import subprocess 9 | 10 | import example_python_project 11 | 12 | 13 | def test_version() -> None: 14 | assert example_python_project.__version__ 15 | 16 | 17 | def test_copyright() -> None: 18 | """Check that source code files contain a copyright line""" 19 | for fname in glob.glob("example_python_project/**/*.py", recursive=True): 20 | print("Checking " + fname + " for copyright header") 21 | 22 | with open(fname) as f: 23 | for line in f.readlines(): 24 | if not line.strip(): 25 | continue 26 | assert line.startswith("# Copyright") 27 | break 28 | 29 | 30 | def test_about() -> None: 31 | out = io.StringIO() 32 | example_python_project.about(out) 33 | print(out) 34 | 35 | 36 | def test_about_main() -> None: 37 | rval = subprocess.call(["python", "-m", "example_python_project.about"]) 38 | assert rval == 0 39 | -------------------------------------------------------------------------------- /example_python_project/pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | # https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-pyproject-toml 4 | 5 | 6 | [build-system] 7 | build-backend = "setuptools.build_meta" 8 | requires = ["setuptools>=67.0", "setuptools_scm[toml]>=7.1"] 9 | 10 | 11 | [project] 12 | name = "example_python_project" 13 | dynamic = ["version"] 14 | dependencies = [ 15 | "numpy", 16 | ] 17 | requires-python =">=3.11" 18 | 19 | authors = [ 20 | {name = "Some Body", email = "somebody@example.com"}, 21 | # {name = "Another person"}, {email = "different.person@example.com"}, 22 | ] 23 | maintainers = [ 24 | # {name = "Another person"}, {email = "different.person@example.com"}, 25 | ] 26 | description = "A short description of this example project" 27 | readme = "README.md" 28 | license = {file = "LICENSE"} 29 | keywords = [] 30 | classifiers = [ 31 | # Full List: https://pypi.org/classifiers/ 32 | 33 | # How mature is this project? Common values are 34 | # 3 - Alpha 35 | # 4 - Beta 36 | # 5 - Production/Stable 37 | "Development Status :: 4 - Beta", 38 | 39 | # Indicate who your project is intended for 40 | "Intended Audience :: Developers", 41 | "Topic :: Software Development :: Build Tools", 42 | 43 | # Pick your license as you wish (see also "license" above) 44 | # "License :: OSI Approved :: MIT License", 45 | # "License :: OSI Approved :: Apache Software License", 46 | ] 47 | 48 | [project.urls] 49 | Homepage = "https://github.com/somebody/example_python_project/" 50 | # Documentation = "https://readthedocs.org" 51 | # Repository = "https://github.com/me/spam.git" 52 | # Issues = "https://github.com/me/spam/issues" 53 | # Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md" 54 | 55 | 56 | [project.optional-dependencies] 57 | dev = [ 58 | "setuptools_scm", 59 | "pytest", 60 | "pytest-cov", 61 | "mypy", 62 | "sphinx", 63 | "pre-commit", 64 | "ruff", 65 | ] 66 | 67 | 68 | [tool.setuptools] 69 | packages = ["example_python_project"] 70 | 71 | 72 | [tool.setuptools_scm] 73 | fallback_version = "0.0.0-dev" 74 | # write_to = "example_python_project/_version.py" 75 | 76 | 77 | [tool.ruff] 78 | # https://docs.astral.sh/ruff/configuration/ 79 | line-length = 88 80 | indent-width = 4 81 | 82 | [tool.ruff.lint] 83 | ignore = [] 84 | 85 | [tool.ruff.lint.per-file-ignores] 86 | # Don't complaine about unused imports in __init__.py 87 | "__init__.py" = ["F401", "F403"] 88 | 89 | 90 | # pytest configuration 91 | # https://docs.pytest.org/en/7.2.x/reference/customize.html 92 | 93 | [tool.pytest.ini_options] 94 | testpaths = "example_python_project" 95 | 96 | 97 | 98 | [tool.coverage.run] 99 | branch = true 100 | source = ["example_python_project"] 101 | parallel = true 102 | 103 | [tool.coverage.report] 104 | omit = ['*_test.py'] 105 | exclude_lines = [ 106 | '\#\s*pragma: no cover', 107 | '^\s*raise AssertionError\b', 108 | '^\s*raise NotImplementedError\b', 109 | '^\s*return NotImplemented\b', 110 | '^\s*raise$', 111 | '^assert False\b', 112 | '''^if __name__ == ['"]__main__['"]:$''', 113 | ] 114 | 115 | 116 | # mypy typecheck configuration 117 | # https://mypy.readthedocs.io/en/stable/config_file.html 118 | 119 | [tool.mypy] 120 | files = "example_python_project" 121 | 122 | # Suppresses error about unresolved imports (i.e. from numpy) 123 | ignore_missing_imports = true 124 | 125 | # Disallows functions without type annotations 126 | disallow_untyped_defs = true 127 | -------------------------------------------------------------------------------- /hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | 4 | def git_init_and_tag(): 5 | """ 6 | Invoke the initial git and tag with 0.0.0 to make an initial version. 7 | """ 8 | 9 | subprocess.check_output("git init", shell=True, stderr=subprocess.STDOUT) 10 | subprocess.check_output("git add -A .", shell=True, stderr=subprocess.STDOUT) 11 | subprocess.check_output( 12 | "git commit -m 'Initialize from modern-python-template'", 13 | shell=True, 14 | stderr=subprocess.STDOUT, 15 | ) 16 | subprocess.check_output("git tag v0.0.0", shell=True, stderr=subprocess.STDOUT) 17 | subprocess.check_output( 18 | "pip install -e .[dev]", shell=True, stderr=subprocess.STDOUT 19 | ) 20 | 21 | 22 | if __name__ == "__main__": 23 | if "{{ cookiecutter.initilize_git_repo }}" in ["y", "Y"]: 24 | git_init_and_tag() 25 | -------------------------------------------------------------------------------- /hooks/pre_gen_project.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$" 5 | EMAIL_REGEX = r"[^@]+@[^@]+\.[^@]+" 6 | 7 | 8 | module_name = "{{cookiecutter.module_name}}" 9 | author_email = "{{cookiecutter.author_email}}" 10 | 11 | 12 | if not re.match(MODULE_REGEX, module_name): 13 | print(f"ERROR: The python module name is not valid: {module_name}") 14 | sys.exit(1) 15 | 16 | if not re.match(EMAIL_REGEX, author_email): 17 | print(f"ERROR: Author email is invalid: {author_email}") 18 | sys.exit(1) 19 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | [*.py] 14 | max_line_length = 88 15 | 16 | [*.html] 17 | indent_size = 2 18 | 19 | [*.json] 20 | indent_size = 2 21 | insert_final_newline = ignore 22 | 23 | [Makefile] 24 | indent_style = tab 25 | 26 | 27 | [*.yml] 28 | indent_size = 2 29 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/.github/workflows/python-build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install the python package and dependencies, and run tests against a variety of Python versions 2 | 3 | name: Build 4 | 5 | on: 6 | push: 7 | branches: [ main, master ] 8 | pull_request: 9 | branches: [ main, master ] 10 | schedule: 11 | - cron: "0 13 * * *" # Every day at 1pm UTC (6am PST) 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python-version: ['3.11', '3.12', '3.13'] 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Python {% raw %} ${{ matrix.python-version }} {% endraw %} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: {% raw %} ${{ matrix.python-version }} {% endraw %} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi 32 | python -m pip install .[dev] # install package + test dependencies 33 | - name: About 34 | run: | 35 | python -m {{cookiecutter.module_name}}.about 36 | - name: Test with pytest 37 | run: | 38 | python -m pytest --cov-fail-under 100 39 | - name: Lint with ruff 40 | run: | 41 | python -m ruff check . 42 | - name: Typecheck with mypy 43 | run: | 44 | python -m mypy 45 | - name: Build documentation with sphinx 46 | run: | 47 | (cd docsrc; make html) 48 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | python -m pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | {% raw %} 28 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 29 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 30 | {% endraw %} 31 | run: | 32 | python -m python setup.py sdist bdist_wheel 33 | python -m twine upload dist/* 34 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Additions 132 | docsrc/_build/ 133 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/astral-sh/ruff-pre-commit 5 | rev: v0.1.4 6 | hooks: 7 | - id: ruff 8 | args: ["--fix"] 9 | - id: ruff-format 10 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docsrc/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | # formats: 27 | # - pdf 28 | # - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | # python: 34 | # install: 35 | # - requirements: docs/requirements.txt 36 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/LICENSE: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.license == 'Apache-2.0' %} 2 | Apache License 3 | 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright [yyyy] [name of copyright owner] 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | 205 | {% elif cookiecutter.license == 'MIT' %} 206 | 207 | MIT License 208 | 209 | Copyright (c) {{cookiecutter.copywrite}} 210 | 211 | Permission is hereby granted, free of charge, to any person obtaining a copy 212 | of this software and associated documentation files (the "Software"), to deal 213 | in the Software without restriction, including without limitation the rights 214 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 215 | copies of the Software, and to permit persons to whom the Software is 216 | furnished to do so, subject to the following conditions: 217 | 218 | The above copyright notice and this permission notice shall be included in all 219 | copies or substantial portions of the Software. 220 | 221 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 222 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 223 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 224 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 225 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 226 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 227 | SOFTWARE. 228 | 229 | 230 | {% endif %} 231 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Kudos: Adapted from Auto-documenting default target 3 | # https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 4 | 5 | .DEFAULT_GOAL := help 6 | 7 | NAME = {{cookiecutter.module_name}} 8 | FILES = $(NAME) docsrc/conf.py setup.py 9 | 10 | .PHONY: help 11 | help: 12 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}' 13 | 14 | 15 | .PHONY: about 16 | about: ## Report versions of dependent packages 17 | @python -m $(NAME).about 18 | 19 | 20 | .PHONY: status 21 | status: ## git status --short --branch 22 | @git status --short --branch 23 | 24 | .PHONY: init 25 | init: ## Install package ready for development 26 | python -m pip install --upgrade pip 27 | if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi 28 | python -m pip install -e .[dev] 29 | python -m pre-commit install # Install git pre-commit hook 30 | 31 | 32 | .PHONY: all 33 | all: about coverage lint typecheck docs build ## Run all tests 34 | 35 | .PHONY: test 36 | test: ## Run unittests 37 | python -m pytest --disable-pytest-warnings 38 | 39 | .PHONY: coverage 40 | coverage: ## Report test coverage 41 | @echo 42 | python -m pytest --disable-pytest-warnings --cov-report term-missing --cov $(NAME) 43 | @echo 44 | 45 | .PHONY: lint 46 | lint: ## Lint check python source 47 | @python -m ruff check 48 | 49 | .PHONY: delint 50 | delint: 51 | @echo 52 | python -m ruff format 53 | 54 | .PHONY: typecheck 55 | typecheck: ## Static typechecking 56 | python -m mypy $(NAME) 57 | 58 | .PHONY: precommit 59 | precommit: ## Run all pre-commit hooks 60 | pre-commit run --all 61 | 62 | .PHONY: docs 63 | docs: ## Build documentation 64 | (cd docsrc; make html) 65 | 66 | .PHONY: docs-open 67 | docs-open: ## Build documentation and open in webbrowser 68 | (cd docsrc; make html) 69 | open docsrc/_build/html/index.html 70 | 71 | .PHONY: docs-clean 72 | docs-clean: ## Clean documentation build 73 | (cd docsrc; make clean) 74 | 75 | .PHONY: docs-github-pages 76 | docs-github-pages: docs ## Install html in docs directory ready for github pages 77 | # https://www.docslikecode.com/articles/github-pages-python-sphinx/ 78 | @mkdir -p docs 79 | @touch docs/.nojekyll # Tell github raw html, not jekyll 80 | @cp -a _build/html/. ../docs 81 | 82 | .PHONY: pragmas 83 | pragmas: ## Report all pragmas in code 84 | @echo "** Test coverage pragmas**" 85 | @grep 'pragma: no cover' --color -r -n $(FILES) || echo "No test coverage pragmas" 86 | @echo 87 | @echo "** Linting pragmas **" 88 | @echo "(http://flake8.pycqa.org/en/latest/user/error-codes.html)" 89 | @grep '# noqa:' --color -r -n $(FILES) || echo "No linting pragmas" 90 | @echo 91 | @echo "** Typecheck pragmas **" 92 | @grep '# type:' --color -r -n $(FILES) || echo "No typecheck pragmas" 93 | 94 | .PHONY: build 95 | build: ## Build a phython soruce distribution and wheel 96 | python -m build 97 | 98 | 99 | .PHONY: requirements 100 | requirements: ## Make requirements.txt 101 | python -m pip freeze > requirements.txt 102 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_name}}: {{cookiecutter.short_description}} 2 | 3 | 4 | ![Build Status](https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.module_name}}/workflows/Build/badge.svg) 5 | [![Documentation Status](https://readthedocs.org/projects/{{cookiecutter.module_name}}/badge/?version=latest)](https://{{cookiecutter.module_name}}.readthedocs.io/en/latest/?badge=latest) 6 | 7 | [Source](https://github.com/gecrooks/{{cookiecutter.module_name}}) 8 | 9 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/docsrc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/docsrc/_static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gecrooks/modern-python-template/ea47c630a9b27edf4847d02a4a93fadb80e8f58d/{{cookiecutter.module_name}}/docsrc/_static/.gitignore -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/docsrc/_templates/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gecrooks/modern-python-template/ea47c630a9b27edf4847d02a4a93fadb80e8f58d/{{cookiecutter.module_name}}/docsrc/_templates/.gitignore -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/docsrc/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "{{cookiecutter.module_name}}" 21 | copyright = "{{cookiecutter.copywrite}}" 22 | author = "{{cookiecutter.author_name}}" 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | "sphinx.ext.autodoc", 32 | "sphinx.ext.napoleon", 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # List of patterns, relative to source directory, that match files and 39 | # directories to ignore when looking for source files. 40 | # This pattern also affects html_static_path and html_extra_path. 41 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 42 | 43 | 44 | # -- Options for HTML output ------------------------------------------------- 45 | 46 | # The theme to use for HTML and HTML Help pages. See the documentation for 47 | # a list of builtin themes. 48 | # 49 | html_theme = "alabaster" 50 | 51 | # Add any paths that contain custom static files (such as style sheets) here, 52 | # relative to this directory. They are copied after the builtin static files, 53 | # so a file named "default.css" will overwrite the builtin "default.css". 54 | html_static_path = ["_static"] 55 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/docsrc/index.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | Welcome to the documentation! 4 | ============================= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Contents: 9 | 10 | 11 | 12 | Indices and tables 13 | ================== 14 | 15 | * :ref:`genindex` 16 | * :ref:`modindex` 17 | * :ref:`search` 18 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | # https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-pyproject-toml 4 | 5 | 6 | [build-system] 7 | build-backend = "setuptools.build_meta" 8 | requires = ["setuptools>=67.0", "setuptools_scm[toml]>=7.1"] 9 | 10 | 11 | [project] 12 | name = "{{cookiecutter.module_name}}" 13 | dynamic = ["version"] 14 | dependencies = [ 15 | "numpy", 16 | ] 17 | requires-python =">=3.11" 18 | 19 | authors = [ 20 | {name = "{{cookiecutter.author_name}}", email = "{{cookiecutter.author_email}}"}, 21 | # {name = "Another person"}, {email = "different.person@example.com"}, 22 | ] 23 | maintainers = [ 24 | # {name = "Another person"}, {email = "different.person@example.com"}, 25 | ] 26 | description = "{{cookiecutter.short_description}}" 27 | readme = "README.md" 28 | license = {file = "LICENSE"} 29 | keywords = [] 30 | classifiers = [ 31 | # Full List: https://pypi.org/classifiers/ 32 | 33 | # How mature is this project? Common values are 34 | # 3 - Alpha 35 | # 4 - Beta 36 | # 5 - Production/Stable 37 | "Development Status :: 4 - Beta", 38 | 39 | # Indicate who your project is intended for 40 | "Intended Audience :: Developers", 41 | "Topic :: Software Development :: Build Tools", 42 | 43 | # Pick your license as you wish (see also "license" above) 44 | # "License :: OSI Approved :: MIT License", 45 | # "License :: OSI Approved :: Apache Software License", 46 | ] 47 | 48 | [project.urls] 49 | Homepage = "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.module_name}}/" 50 | # Documentation = "https://readthedocs.org" 51 | # Repository = "https://github.com/me/spam.git" 52 | # Issues = "https://github.com/me/spam/issues" 53 | # Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md" 54 | 55 | 56 | [project.optional-dependencies] 57 | dev = [ 58 | "setuptools_scm", 59 | "pytest", 60 | "pytest-cov", 61 | "mypy", 62 | "sphinx", 63 | "pre-commit", 64 | "ruff", 65 | ] 66 | 67 | 68 | [tool.setuptools] 69 | packages = ["{{cookiecutter.module_name}}"] 70 | 71 | 72 | [tool.setuptools_scm] 73 | fallback_version = "0.0.0-dev" 74 | # write_to = "{{cookiecutter.module_name}}/_version.py" 75 | 76 | 77 | [tool.ruff] 78 | # https://docs.astral.sh/ruff/configuration/ 79 | line-length = 88 80 | indent-width = 4 81 | 82 | [tool.ruff.lint] 83 | ignore = [] 84 | 85 | [tool.ruff.lint.per-file-ignores] 86 | # Don't complaine about unused imports in __init__.py 87 | "__init__.py" = ["F401", "F403"] 88 | 89 | 90 | # pytest configuration 91 | # https://docs.pytest.org/en/7.2.x/reference/customize.html 92 | 93 | [tool.pytest.ini_options] 94 | testpaths = "{{cookiecutter.module_name}}" 95 | 96 | 97 | 98 | [tool.coverage.run] 99 | branch = true 100 | source = ["{{cookiecutter.module_name}}"] 101 | parallel = true 102 | 103 | [tool.coverage.report] 104 | omit = ['*_test.py'] 105 | exclude_lines = [ 106 | '\#\s*pragma: no cover', 107 | '^\s*raise AssertionError\b', 108 | '^\s*raise NotImplementedError\b', 109 | '^\s*return NotImplemented\b', 110 | '^\s*raise$', 111 | '^assert False\b', 112 | '''^if __name__ == ['"]__main__['"]:$''', 113 | ] 114 | 115 | 116 | # mypy typecheck configuration 117 | # https://mypy.readthedocs.io/en/stable/config_file.html 118 | 119 | [tool.mypy] 120 | files = "{{cookiecutter.module_name}}" 121 | 122 | # Suppresses error about unresolved imports (i.e. from numpy) 123 | ignore_missing_imports = true 124 | 125 | # Disallows functions without type annotations 126 | disallow_untyped_defs = true 127 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/{{cookiecutter.module_name}}/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright {{cookiecutter.copywrite}} 2 | # 3 | # This source code is licensed under the {{cookiecutter.license}} License 4 | # found in the LICENSE file in the root directory of this source tree. 5 | 6 | from .config import * # noqa: F401, F403 7 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/{{cookiecutter.module_name}}/about.py: -------------------------------------------------------------------------------- 1 | # Copyright {{cookiecutter.copywrite}} 2 | # 3 | # This source code is licensed under the {{cookiecutter.license}} License 4 | # found in the LICENSE file in the root directory of this source tree. 5 | 6 | # Command line interface for the about() function 7 | # > python -m {{cookiecutter.module_name}}.about 8 | # 9 | # NB: This module should not be imported by any other code in the package 10 | # (else we will get multiple import warnings) 11 | # Implementation is located in about_.py 12 | 13 | if __name__ == "__main__": 14 | import {{cookiecutter.module_name}} 15 | 16 | {{cookiecutter.module_name}}.about() 17 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/{{cookiecutter.module_name}}/config.py: -------------------------------------------------------------------------------- 1 | # Copyright {{cookiecutter.copywrite}} 2 | # 3 | # This source code is licensed under the {{cookiecutter.license}} License 4 | # found in the LICENSE file in the root directory of this source tree. 5 | 6 | """ 7 | Package wide configuration 8 | """ 9 | 10 | import platform 11 | import re 12 | import sys 13 | import typing 14 | from importlib import metadata as importlib_metadata 15 | 16 | __all__ = ["__version__", "about"] 17 | 18 | 19 | try: 20 | __version__ = importlib_metadata.version(__package__) # type: ignore 21 | except Exception: # pragma: no cover 22 | # package is not installed 23 | __version__ = "0.0.0" 24 | 25 | 26 | # NB: command line access is located in about.py 27 | def about(file: typing.Optional[typing.TextIO] = None) -> None: 28 | f"""Print information about this package. 29 | 30 | ``> python -m {__package__}.about`` 31 | 32 | Args: 33 | file: Output stream (Defaults to stdout) 34 | """ 35 | metadata = importlib_metadata.metadata(__package__) # type: ignore 36 | print(f"# {metadata['Name']}", file=file) 37 | print(f"{metadata['Summary']}", file=file) 38 | print(f"{metadata['Home-page']}", file=file) 39 | 40 | name_width = 24 41 | versions = {} 42 | versions["platform"] = platform.platform(aliased=True) 43 | versions[__package__] = __version__ 44 | versions["python"] = sys.version[0:5] 45 | 46 | for req in importlib_metadata.requires(__package__): # type: ignore 47 | name = re.split("[; =><]", req)[0] 48 | try: 49 | versions[name] = importlib_metadata.version(name) # type: ignore 50 | except Exception: # pragma: no cover 51 | pass 52 | 53 | print(file=file) 54 | print("# Configuration", file=file) 55 | for name, vers in versions.items(): 56 | print(name.ljust(name_width), vers, file=file) 57 | print(file=file) 58 | 59 | 60 | # end about 61 | 62 | -------------------------------------------------------------------------------- /{{cookiecutter.module_name}}/{{cookiecutter.module_name}}/config_test.py: -------------------------------------------------------------------------------- 1 | # Copyright {{cookiecutter.copywrite}} 2 | # 3 | # This source code is licensed under the {{cookiecutter.license}} License 4 | # found in the LICENSE file in the root directory of this source tree. 5 | 6 | import glob 7 | import io 8 | import subprocess 9 | 10 | import {{cookiecutter.module_name}} 11 | 12 | 13 | def test_version() -> None: 14 | assert {{cookiecutter.module_name}}.__version__ 15 | 16 | 17 | def test_copyright() -> None: 18 | """Check that source code files contain a copyright line""" 19 | for fname in glob.glob("{{cookiecutter.module_name}}/**/*.py", recursive=True): 20 | print("Checking " + fname + " for copyright header") 21 | 22 | with open(fname) as f: 23 | for line in f.readlines(): 24 | if not line.strip(): 25 | continue 26 | assert line.startswith("# Copyright") 27 | break 28 | 29 | 30 | def test_about() -> None: 31 | out = io.StringIO() 32 | {{cookiecutter.module_name}}.about(out) 33 | print(out) 34 | 35 | 36 | def test_about_main() -> None: 37 | rval = subprocess.call(["python", "-m", "{{cookiecutter.module_name}}.about"]) 38 | assert rval == 0 39 | --------------------------------------------------------------------------------