From c67118f34772f03ae17d009216ee32c82ea389c5 Mon Sep 17 00:00:00 2001 From: Gavin Crooks Date: Fri, 11 Dec 2020 16:11:00 -0800 Subject: [PATCH] Initial commit --- .github/workflows/python-build.yml | 50 ++ .github/workflows/python-publish.yml | 31 ++ .gitignore | 129 +++++ .readthedocs.yml | 6 + LICENSE | 201 +++++++ Makefile | 100 ++++ README.md | 694 ++++++++++++++++++++++++ docs/Makefile | 20 + docs/_static/.gitignore | 0 docs/_templates/.gitignore | 0 docs/conf.py | 54 ++ docs/index.rst | 17 + gecrooks_python_template/__init__.py | 7 + gecrooks_python_template/about.py | 15 + gecrooks_python_template/config.py | 60 ++ gecrooks_python_template/config_test.py | 41 ++ pyproject.toml | 9 + setup.cfg | 105 ++++ setup.py | 6 + 19 files changed, 1545 insertions(+) create mode 100644 .github/workflows/python-build.yml create mode 100644 .github/workflows/python-publish.yml create mode 100644 .gitignore create mode 100644 .readthedocs.yml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 docs/Makefile create mode 100644 docs/_static/.gitignore create mode 100644 docs/_templates/.gitignore create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 gecrooks_python_template/__init__.py create mode 100644 gecrooks_python_template/about.py create mode 100644 gecrooks_python_template/config.py create mode 100644 gecrooks_python_template/config_test.py create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100755 setup.py diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml new file mode 100644 index 0000000..13235d6 --- /dev/null +++ b/.github/workflows/python-build.yml @@ -0,0 +1,50 @@ +# This workflow will install the python package and dependencies, and run tests against a variety of Python versions + +name: Build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: "0 13 * * *" # Every day at 1pm UTC (6am PST) + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.7', '3.8'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + python -m pip install .[dev] # install package + test dependencies + - name: About + run: | + python -m $(python -Wi setup.py --name).about + - name: Test with pytest + run: | + python -m pytest --cov-fail-under 100 + - name: Lint with black and isort (run `make delint` to fix issues) + run: | + black --check . + isort --check -m 3 --tc . + - name: Lint with flake8 + run: | + flake8 . + - name: Typecheck with mypy + run: | + mypy + - name: Build documentation with sphinx + run: | + sphinx-build -M html docs docs/_build diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..4e1ef42 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,31 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..7f13c92 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,6 @@ +version: 2 +formats: [] +sphinx: + configuration: docs/conf.py +python: + version: 3.8 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5b56d5a --- /dev/null +++ b/Makefile @@ -0,0 +1,100 @@ + +# Kudos: Adapted from Auto-documenting default target +# https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html + +.DEFAULT_GOAL := help + +PROJECT = gecrooks_python_template +FILES = $(PROJECT) docs/conf.py setup.py + +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}' + +all: about coverage lint typecheck docs build ## Run all tests + +test: ## Run unittests + pytest --disable-pytest-warnings + +coverage: ## Report test coverage + @echo + pytest --disable-pytest-warnings --cov + @echo + +lint: ## Lint check python source + @echo + isort --check -m 3 --tc $(PROJECT) || echo "FAILED isort!" + @echo + black --diff --color $(PROJECT) || echo "FAILED black" + @echo + flake8 $(FILES) || echo "FAILED flake8" + @echo + +delint: ## Run isort and black to delint project + @echo + isort -m 3 --tc $(PROJECT) + @echo + black $(PROJECT) + @echo + +typecheck: ## Static typechecking + mypy $(PROJECT) + +docs: ## Build documentation + (cd docs; make html) + +docs-open: ## Build documentation and open in webbrowser + (cd docs; make html) + open docs/_build/html/index.html + +docs-clean: ## Clean documentation build + (cd docs; make clean) + +pragmas: ## Report all pragmas in code + @echo + @echo "** Code that needs something done **" + @grep 'TODO' --color -r -n $(FILES) || echo "No TODO pragmas" + @echo + @echo "** Code that needs fixing **" + @grep 'FIXME' --color -r -n $(FILES) || echo "No FIXME pragmas" + @echo + @echo "** Code that needs documenting **" + @grep 'DOCME' --color -r -n $(FILES) || echo "No DOCME pragmas" + @echo + @echo "** Code that needs more tests **" + @grep 'TESTME' --color -r -n $(FILES) || echo "No TESTME pragmas" + @echo + @echo "** Implementation notes **" + @grep 'NB:' --color -r -n $(FILES) || echo "No NB implementation notes Pragmas" + @echo + @echo "** Acknowledgments **" + @grep 'kudos:' --color -r -n -i $(FILES) || echo "No kudos" + @echo + @echo "** Pragma for test coverage **" + @grep 'pragma: no cover' --color -r -n $(FILES) || echo "No Typecheck Pragmas" + @echo + @echo "** flake8 linting pragmas **" + @echo "(http://flake8.pycqa.org/en/latest/user/error-codes.html)" + @grep '# noqa:' --color -r -n $(FILES) || echo "No flake8 pragmas" + @echo + @echo "** Typecheck pragmas **" + @grep '# type:' --color -r -n $(FILES) || echo "No Typecheck Pragmas" + +about: ## Report versions of dependent packages + @python -m $(PROJECT).about + +status: ## git status -uno + @echo + @git status -uno + +build: ## Setuptools build + ./setup.py clean --all + ./setup.py sdist bdist_wheel + + +clean: ## Clean up after setuptools + ./setup.py clean --all + + +.PHONY: help +.PHONY: docs +.PHONY: build diff --git a/README.md b/README.md new file mode 100644 index 0000000..d674321 --- /dev/null +++ b/README.md @@ -0,0 +1,694 @@ +# gecrooks-python-template: Minimal viable setup for an open source, github hosted, python package + + +![Build Status](https://github.com/gecrooks/gecrooks-python-template/workflows/Build/badge.svg) [![Documentation Status](https://readthedocs.org/projects/gecrooks-python-template/badge/?version=latest)](https://gecrooks-python-template.readthedocs.io/en/latest/?badge=latest) + +[Source](https://github.com/gecrooks/gecrooks-python-template) + + +## Installation for development + +``` +$ git clone https://github.com/gecrooks/gecrooks-python-template.git +$ cd gecrooks-python-template +$ pip install -e .[dev] +``` + + +## About: On the creation and crafting of a python project + +This is a discussion of the steps needed to setup an open source, github hosted, python package ready for further development. + +## Naming + +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. + +Note that github repo and pypi packages are named using dashes (`-`), but that the corresponsing python module 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.) + +## License + +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. + + +## Create repo + +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. + + +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. + +``` + # specify a global exclusion list + git config --global core.excludesfile ~/.gitignore + # adding .DS_Store to that list + echo .DS_Store >> ~/.gitignore +``` + +## Clone repo + +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. +``` + $ conda create --name GPT + $ source activate GPT + (GPT) $ python --version + Python 3.8.3 +``` + +Now we clone the repo locally. + +``` + (GPT) $ git clone https://github.com/gecrooks/gecrooks-python-template.git + Cloning into 'gecrooks-python-template'... + remote: Enumerating objects: 4, done. + remote: Counting objects: 100% (4/4), done. + remote: Compressing objects: 100% (3/3), done. + remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 + Unpacking objects: 100% (4/4), done. + (GPT) $ cd gecrooks-python-template +``` + +Lets tag this initial commit for posterities sake (And so I can [link](https://github.com/gecrooks/gecrooks-python-template/releases/tag/v0.0.0) to the code at this instance). +``` + (GPT) $ git tag v0.0.0 + (GPT) $ git push origin v0.0.0 +``` +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. + + +## Branch +It's always best to craft code in a branch, and then merge that code into the master branch. +``` +$ git branch gec001-init +$ git checkout gec001-init +Switched to branch 'gec001-init' +``` +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). + + +## Packaging + +Let's complete the minimum viable python project. We need the actual python module, signaled by a (currently) blank `__init__.py` file. +``` + (GPT) $ mkdir gecrooks_python_template + (GPT) $ touch gecrooks_python_template/__init__.py +``` + +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`. + +The modern `setup.py` is just a husk: + +``` +#!/usr/bin/env python + +import setuptools + +if __name__ == "__main__": + setuptools.setup(use_scm_version=True) +``` +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. +``` + $ chmod a+x setup.py +``` +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. +``` +# pyproject.toml +[build-system] +requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] +build-backend = "setuptools.build_meta" + + +# pyproject.toml +[tool.setuptools_scm] + +``` +Again, the parts with `setuptools_scm` are additions. + + +All of the rest of the metadata goes in `setup.cfg` (in INI format). +``` +# Setup Configuration File +# https://docs.python.org/3/distutils/configfile.html +# [INI](https://docs.python.org/3/install/index.html#inst-config-syntax) file format. + +[metadata] +name = gecrooks-python-template +url = https://github.com/gecrooks/gecrooks_python_template/ +author = Gavin Crooks +author_email = gavincrooks@gmail.com +description = "Minimal Viable Product for an open source, github hosted, python package" +long_description = file:README.md +long_description_content_type = text/markdown +license_file = LICENSE +license = Apache-2.0 + +classifiers= + Development Status :: 4 - Beta + Intended Audience :: Science/Research + License :: OSI Approved :: Apache Software License + Topic :: Scientific/Engineering + Programming Language :: Python + Natural Language :: English + Topic :: Software Development :: Libraries + Topic :: Software Development :: Libraries :: Python Modules + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Operating System :: OS Independent + + +[options] +zip_safe = True +python_requires = >= 3.7 + +# importlib_metadata required for python 3.7 +install_requires = + importlib_metadata + numpy + +setup_requires = + setuptools_scm + +[options.extras_require] +dev = + pytest + pytest-cov + flake8 + mypy + sphinx + sphinxcontrib-bibtex + setuptools_scm +``` + +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 February each year. Upgrading involves changing the python version numbers in the tests and `config.cfg`, and then cleaning up any `__future__` or conditional imports, or other hacks added to maintain compatibility with older python releases. + + +We can now install our package (as editable -e, so that the code in our repo is live). +``` + $ pip install -e .[dev] +``` +The optional `[dev]` will install all of the extra packages we need for test and development, listed under `[options.extras_require]` above. + + + +## Versioning +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. + +There should be a +[single source of truth](https://packaging.python.org/guides/single-sourcing-package-version/) for this number. +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. + +The convention is that the version number of a python packages should be available as `packagename.__version__`. +So we add the following code to `gecrooks_python_template/config.py` to extract the version number metadata. +``` +try: + # python >= 3.8 + from importlib import metadata as importlib_metadata # type: ignore +except ImportError: # pragma: no cover + # python == 3.7 + import importlib_metadata # type: ignore # noqa: F401 + + +__all__ = ["__version__", "about"] + + +package_name = "gecrooks_python_template" + +try: + __version__ = importlib_metadata.version(package_name) # type: ignore +except Exception: # pragma: no cover + # package is not installed + __version__ = "?.?.?" + + +``` +and then in `gecrooks_python_template/__init__.py`, we import this version number. +``` +from .config import __version__ as __version__ # noqa: F401 +``` +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. + +The various pragmas in the code above ("pragma: no cover" and "type: ignore") are there because the conditional import needed for python 3.7 compatibility confuses both our type checker and code coverage tools. + +## about + +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. + +``` +# Configuration (> python -m gecrooks_python_template.about) +platform macOS-10.13.6-x86_64-i386-64bit +gecrooks-python-template 0.0.1 +python 3.8.3 +numpy 1.18.5 +pytest 5.4.3 +pytest-cov 2.10.0 +flake8 3.8.3 +mypy 0.780 +sphinx 3.1.1 +sphinxcontrib-bibtex 1.0.0 +setuptools_scm 4.1.2 +``` +The `about()` function to print this information is placed in `config.py`. The file `about.py` contains the standard python command line interface (CLI), +``` +if __name__ == '__main__': + import gecrooks_python_template + gecrooks_python_template.about() +``` +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. + + +## Unit tests + +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. + +(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.) + +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. + +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. + +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 `gecrooks_python_template/config_test.py` we add + +``` +import gecrooks_python_template + +def test_version(): + assert gecrooks_python_template.__version__ +``` +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.) +``` + +(GTP) $ python -m pytest +========================================================================================== test session starts =========================================================================================== +platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 +rootdir: /Users/work/Work/Projects/gecrooks_python_template +collected 1 item + +gecrooks_python_template/config_test.py . [100%] + +=========================================================================================== 1 passed in 0.02s ============================================================================================ +``` + +Note that in the main code we'll access the package with relative imports, e.g. +``` +from . import __version__ +``` +But in the test code we use absolute imports. +``` +from gecrooks_python_template import __version__ +``` +In tests we want to access our code in the same way we would access it from the outside as an end user. + + +## Test coverage + +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? + +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 seperate `.coveragerc`, but I think its better to avoid a proliferation of configuration files.) +``` +# pytest configuration +[tool:pytest] +testpaths = + gecrooks_python_template + + +# Configuration for test coverage +# +# https://coverage.readthedocs.io/en/latest/config.html +# +# python -m pytest --cov + +[coverage:paths] +source = + gecrooks_python_template + +[coverage:run] +omit = + *_test.py + +[coverage:report] +# Use ``# pragma: no cover`` to exclude specific lines +exclude_lines = + pragma: no cover +``` + +We have to explicitly omit the unit tests since we have placed the test files in the same directories as the code to test. + +The pragam `pragma: no cover` is used to mark untestable lines. This often happens with conditional imports used for backwards compatibility between python versions. + + +## Linting + +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: + + The depth of the indentation shall be 4 spaces. + And 4 spaces shall be the depth of the indentation. + Two spaces thou shall not use. + And tabs are right out. + +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. + +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 was [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. + + +The configuration also lives in `setup.cfg`. +``` +# flake8 linter configuration +[flake8] +max-line-length = 88 +ignore = E203, W503 +``` +We need to override the linter on occasion. We add pragma such as `# noqa: F401` to assert that no, really, in this case we do know what we're doing. + + +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. + +The command `make delint` will run these `isort` and `black` on your code, with the right magic incantations so that they are compatible. + + +## Copyright +It's common practice to add a copyright and license notice to the top of every source file -- something like this: +``` + +# Copyright 2019-, Gavin E. Crooks and contributors +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +``` + +I tend to forget to add these lines. So let's add a unit test `gecrooks_python_template/config_test.py::test_copyright` to make sure we don't. +``` +def test_copyright(): + """Check that source code files contain a copyright line""" + exclude = set(['gecrooks_python_template/version.py']) + for fname in glob.glob('gecrooks_python_template/**/*.py', recursive=True): + if fname in exclude: + continue + print("Checking " + fname + " for copyright header") + + with open(fname) as f: + for line in f.readlines(): + if not line.strip(): + continue + assert line.startswith('# Copyright') + break +``` + + +## API Documentation +[Sphinx](https://www.sphinx-doc.org/en/master/usage/quickstart.html) is the standard +tool used to generate API documentation from the python source. Use the handy quick start tools. +``` +$ mkdir docs +$ cd docs +$ sphinx-quickstart +``` +The defaults are reasonable. Enter the project name and author when prompted. + +Edit the conf.py, and add the following collection of extensions. +``` +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', +] +``` +[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. + +We also add a newline at the end of `conf.py`, since the lack of a blank line at the end upsets our linter. + +Go ahead and give it a whirl. This won't do anything interesting yet, but it's a start. +``` +$ make html +``` + +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. (Sphinx will create the `_build` directory when it needs it.) + +``` +$ touch _templates/.gitignore _build/.gitignore _static/.gitignore +$ git add -f _templates/.gitignore _build/.gitignore _static/.gitignore +$ git add Makefile *.* +# cd .. +``` + + + +## Makefile +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. + +``` +(GTP) $ make +all Run all tests +test Run unittests +coverage Report test coverage +lint Lint check python source +delint Run isort and black to delint project +typecheck Static typechecking +docs Build documentation +docs-open Build documentation and open in webbrowser +docs-clean Clean documentation build +pragmas Report all pragmas in code +about Report versions of dependent packages +status git status -uno +build Setuptools build +clean Clean up after setuptools +``` + +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. I also tend to scatter other keywords throughout my code: TODO (For things that need doing), FIXME (For code that's broken, but I can't fix right this moment), DOCME (code that needs more documentation), and TESTME (for code that needs more tests). In principle, production code shouldn't have these pragmas. Either the problem should be fixed, or if it can't be immediately fixed, it should become a github issue. + + +## Readthedocs +We'll host our API documentation on [Read the Docs](readthedocs.org). We'll need a basic configuration file, `.readthedocs.yml`. +``` +version: 2 +formats: [] +sphinx: + configuration: docs/conf.py +python: + version: 3.8 +``` +I've already got a readthedocs account, so setting up a new project takes but a few minutes. + + +## README.md + +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.) + +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) + + +## Continuous Integration + +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. + +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. +``` +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: "0 13 * * *" # Every day at 1pm UTC (6am PST) + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.7', '3.8'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + python -m pip install -e .[dev] # install package + test dependencies + - name: About + run: | + python -m $(python -Wi setup.py --name).about + - name: Lint with flake8 + run: | + flake8 . + - name: Test with pytest + run: | + python -m pytest --cov-fail-under 100 + - name: Typecheck with mypy + run: | + mypy + - name: Build documentation with sphinx + run: | + sphinx-build -M html docs docs/_build + +``` +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. + +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 it's rules.) + +Let's add, commit, and push our changes. +``` +$ git status +On branch gec001-init +Changes to be committed: + (use "git reset HEAD ..." to unstage) + + new file: .readthedocs.yml + new file: .github/workflows/python-package.yml + new file: Makefile + modified: README.md + new file: docs/Makefile + new file: docs/_build/.gitignore + new file: docs/_static/.gitignore + new file: docs/_templates/.gitignore + new file: docs/conf.py + new file: docs/index.rst + new file: pyproject.toml + new file: gecrooks_python_template/__init__.py + new file: gecrooks_python_template/about.py + new file: gecrooks_python_template/config.py + new file: gecrooks_python_template/config_test.py + new file: setup.cfg + new file: setup.py + +$ git commit -m "Minimum viable package" +... +$ git push --set-upstream origin gec001-init +... +``` +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. + + + +## PyPi + +We should now be ready to do a test submission to PyPI, The Python Package Index (PyPI). +Follow the directions laid out in the [python packaging](https://packaging.python.org/tutorials/packaging-projects/) documentation. + +``` +$ pip install -q wheel setuptools twine +... +$ git tag v0.1.0rc1 +$ python setup.py sdist bdist_wheel +... +``` +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). + +First we push to the pypi's test repository. +``` +(GTP) $ python -m twine upload --repository testpypi dist/* +``` +You'll need to create a pypi account if you don't already have one. + +Let's make sure it worked by installing from pypi into a fresh conda environment. +``` +(GTP) $ conda deactivate +$ conda create --name tmp +$ conda activate tmp +(tmp) $ pip install --index-url https://test.pypi.org/simple/ --no-deps gecrooks-python-template +(tmp) $ python -m gecrooks_python_template.about +(tmp) $ conda activate GTP +``` + + +## Merge and Tag + +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. + +The full developer sequence goes something like this + +1.) Sync the master branch. +``` +$ git checkout master +$ git pull origin master +``` +(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 +``` +$ git checkout master +$ git fetch upstream +$ git merge upstream/master +``` +This should go smoothly as long as you never commit directly to your local master branch.) + + +2.) Create a working branch. +``` +$ git branch BRANCH +$ git checkout BRANCH +``` + +3.) Do a bunch of development on the branch, committing incremental changes as we go along. + +4.) Sync the master branch with github (since other development may be ongoing.) (i.e. repeat step 1) + +5.) Rebase our branch to master. +``` +$ git checkout BRANCH +$ git rebase master +``` +If there are conflicts, resolve them, and then go back to step 4. + +6.) Sync our branch to github + +``` +$ git push +``` + +7.) Over on github, create a pull request to merge into the master branch + +8.) Wait for the integration tests to pass. If they don't, fix them, and then go back to step 4. + +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.) + +10.) Goto step 1. Back on our local machine, we resync master, create a new branch, and continue developing. + + +## Tag and release + +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 you repo settings). + + + +## Conclusion + +By my count we have 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). We're now ready to create a new git branch and start coding in earnest. + + +## License + +This software template is public domain. The included open-source software license `LICENSE.txt` and copyright lines are for illustrative purposes only. If you wish to use this template as the basis of your own project, you should feel free to assert your own copyrights (at the top of the python source code files) and subsitute your own choice of software license. + +Gavin E. Crooks (2020) + + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/docs/_templates/.gitignore b/docs/_templates/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..f7542d4 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,54 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = "python_mvp" +copyright = "2020, Gavin Crooks" +author = "Gavin Crooks" + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "alabaster" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..7587743 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,17 @@ + + +Welcome to the documentation! +============================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/gecrooks_python_template/__init__.py b/gecrooks_python_template/__init__.py new file mode 100644 index 0000000..de2adb4 --- /dev/null +++ b/gecrooks_python_template/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2019-2021, Gavin E. Crooks and contributors +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +from .config import __version__ # noqa: F401 +from .config import about # noqa: F401 diff --git a/gecrooks_python_template/about.py b/gecrooks_python_template/about.py new file mode 100644 index 0000000..9ac4a9f --- /dev/null +++ b/gecrooks_python_template/about.py @@ -0,0 +1,15 @@ +# Copyright 2019-2021, Gavin E. Crooks and contributors +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +# Command line interface for the about() function +# > python -m gecrooks_python_template.about +# +# NB: This module should not be imported by any other code in the package +# (else we will get multiple import warnings) + +if __name__ == "__main__": + import gecrooks_python_template + + gecrooks_python_template.about() diff --git a/gecrooks_python_template/config.py b/gecrooks_python_template/config.py new file mode 100644 index 0000000..f0624e9 --- /dev/null +++ b/gecrooks_python_template/config.py @@ -0,0 +1,60 @@ +# Copyright 2019-2021, Gavin E. Crooks and contributors +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +""" +Package wide configuration +""" + +import platform +import re +import sys +import typing + +try: + # python >= 3.8 + from importlib import metadata as importlib_metadata # type: ignore +except ImportError: # pragma: no cover + # python == 3.7 + import importlib_metadata # type: ignore # noqa: F401 + + +__all__ = ["__version__", "about"] + + +package_name = "gecrooks_python_template" + +try: + __version__ = importlib_metadata.version(package_name) # type: ignore +except Exception: # pragma: no cover + # package is not installed + __version__ = "?.?.?" + + +def about(file: typing.TextIO = None) -> None: + f"""Print information about the configuration + + ``> python -m {package_name}.about`` + + Args: + file: Output stream (Defaults to stdout) + """ + name_width = 24 + versions = {} + versions["platform"] = platform.platform(aliased=True) + versions[package_name] = __version__ + versions["python"] = sys.version[0:5] + + for req in importlib_metadata.requires(package_name): # type: ignore + name = re.split("[; =><]", req)[0] + try: + versions[name] = importlib_metadata.version(name) # type: ignore + except Exception: # pragma: no cover + pass + + print(file=file) + print(f"# Configuration (> python -m {package_name}.about)", file=file) + for name, vers in versions.items(): + print(name.ljust(name_width), vers, file=file) + print(file=file) diff --git a/gecrooks_python_template/config_test.py b/gecrooks_python_template/config_test.py new file mode 100644 index 0000000..b460e59 --- /dev/null +++ b/gecrooks_python_template/config_test.py @@ -0,0 +1,41 @@ +# Copyright 2019-2021, Gavin E. Crooks and contributors +# +# This source code is licensed under the Apache License, Version 2.0 found in +# the LICENSE.txt file in the root directory of this source tree. + +import glob +import io +import subprocess + +import gecrooks_python_template + + +def test_version() -> None: + assert gecrooks_python_template.__version__ + + +def test_about() -> None: + out = io.StringIO() + gecrooks_python_template.about(out) + print(out) + + +def test_about_main() -> None: + rval = subprocess.call(["python", "-m", "gecrooks_python_template.about"]) + assert rval == 0 + + +def test_copyright() -> None: + """Check that source code files contain a copyright line""" + exclude = set(["gecrooks_python_template/version.py"]) + for fname in glob.glob("gecrooks_python_template/**/*.py", recursive=True): + if fname in exclude: + continue + print("Checking " + fname + " for copyright header") + + with open(fname) as f: + for line in f.readlines(): + if not line.strip(): + continue + assert line.startswith("# Copyright") + break diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6831c78 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +# pyproject.toml +[build-system] +requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] +build-backend = "setuptools.build_meta" + + +# pyproject.toml +[tool.setuptools_scm] + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..e809114 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,105 @@ +# Setup Configuration File +# https://docs.python.org/3/distutils/configfile.html +# [INI](https://docs.python.org/3/install/index.html#inst-config-syntax) file format. + +[metadata] +name = gecrooks_python_template +url = https://github.com/gecrooks/gecrooks-python-template/ +author = Gavin Crooks +author_email = gavincrooks@gmail.com +description = "Minimal Viable Product for an open source, github hosted, python package" +long_description = file:README.md +long_description_content_type = text/markdown +license_file = LICENSE +license = Apache-2.0 + +classifiers= + Development Status :: 4 - Beta + Intended Audience :: Science/Research + License :: OSI Approved :: Apache Software License + Topic :: Scientific/Engineering + Programming Language :: Python + Natural Language :: English + Topic :: Software Development :: Libraries + Topic :: Software Development :: Libraries :: Python Modules + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Operating System :: OS Independent + + +[options] +zip_safe = True +python_requires = >= 3.7 + +# importlib_metadata required for python 3.7 +install_requires = + importlib_metadata + numpy + +setup_requires = + setuptools_scm + +[options.extras_require] +dev = + pytest >= 4.6 + pytest-cov + flake8 + mypy + black + isort + sphinx + sphinxcontrib-bibtex + setuptools_scm + + +# pytest configuration +[tool:pytest] +testpaths = + gecrooks_python_template + + +# Configuration for test coverage +# +# https://coverage.readthedocs.io/en/latest/config.html +# > python -m pytest --cov +# Use ``# pragma: no cover`` to exclude specific lines + +[coverage:paths] +source = + gecrooks_python_template + +[coverage:run] +omit = + *_test.py + +[coverage:report] +show_missing = true +exclude_lines = + pragma: no cover + except ImportError + assert False + raise NotImplementedError() + + +# flake8 linter configuration +[flake8] +max-line-length = 88 +ignore = E203, W503 + + +# mypy typecheck configuration +# +# https://mypy.readthedocs.io/en/stable/config_file.html + +[mypy] +files = gecrooks_python_template + +# Suppresses error about unresolved imports (i.e. from numpy) +ignore_missing_imports = True + +# Disallows functions without type annotations +disallow_untyped_defs = True + +# Disable strict optional checks (Was default prior to mypy 0.600) +strict_optional = False diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..827e955 --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +import setuptools + +if __name__ == "__main__": + setuptools.setup(use_scm_version=True)