├── docs ├── source │ ├── _static │ │ └── .placeholder │ ├── example_pre-commit.yml │ ├── test_examples.py │ ├── example_travis.yml │ ├── refraction.py │ ├── further-reading.rst │ ├── example_travis_with_doctr.yml │ ├── python-publish.yml │ ├── index.rst │ ├── publish-docs.yml │ ├── environments.rst │ ├── ci.rst │ ├── vendor │ │ └── youtube.py │ ├── philosophy.rst │ ├── pre-commit.rst │ ├── publishing-docs.rst │ ├── including-data-files.rst │ ├── advanced-testing.rst │ ├── publishing-releases.rst │ ├── conf.py │ ├── writing-docs.rst │ ├── guiding-design-principles.rst │ ├── preliminaries.rst │ └── the-code-itself.rst ├── Makefile └── make.bat ├── {{ cookiecutter.repo_name }} ├── docs │ ├── source │ │ ├── _static │ │ │ └── .placeholder │ │ ├── release-history.rst │ │ ├── installation.rst │ │ ├── usage.rst │ │ ├── index.rst │ │ ├── min_versions.rst │ │ └── conf.py │ ├── Makefile │ └── make.bat ├── {{ cookiecutter.package_dir_name }} │ ├── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_examples.py │ ├── __init__.py │ └── _version.py ├── requirements.txt ├── .gitattributes ├── .isort.cfg ├── setup.cfg ├── .codecov.yml ├── AUTHORS.rst ├── .coveragerc ├── .flake8 ├── pyproject.toml ├── MANIFEST.in ├── requirements-dev.txt ├── .github │ └── workflows │ │ ├── pre-commit.yml │ │ ├── publish-pypi.yml │ │ ├── docs.yml │ │ └── testing.yml ├── .pre-commit-config.yaml ├── README.rst ├── .gitignore ├── LICENSE ├── setup.py └── CONTRIBUTING.rst ├── .gitignore ├── copy_user_content.sh ├── RELEASES.rst ├── requirements.txt ├── README.md ├── cookiecutter.json ├── run_cookiecutter_example.py ├── LICENSE └── .github └── workflows └── docs.yml /docs/source/_static/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/docs/source/_static/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/{{ cookiecutter.package_dir_name }}/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/{{ cookiecutter.package_dir_name }}/tests/conftest.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/requirements.txt: -------------------------------------------------------------------------------- 1 | # List required packages in this file, one per line. 2 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/.gitattributes: -------------------------------------------------------------------------------- 1 | {{cookiecutter.package_dir_name}}/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/build/ 2 | docs/source/generated/ 3 | *.swp 4 | example/ 5 | __pycache__/ 6 | venv/ 7 | *.idea* 8 | .vscode/ 9 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length = 115 3 | multi_line_output = 3 4 | include_trailing_comma = True 5 | -------------------------------------------------------------------------------- /copy_user_content.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cp docs/source/refraction.py example/example/refraction.py 3 | cp docs/source/test_examples.py example/example/tests/test_examples.py 4 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/{{ cookiecutter.package_dir_name }}/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import get_versions 2 | 3 | __version__ = get_versions()["version"] 4 | del get_versions 5 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/docs/source/release-history.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Release History 3 | =============== 4 | 5 | Initial Release (YYYY-MM-DD) 6 | ---------------------------- 7 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/{{ cookiecutter.package_dir_name }}/tests/test_examples.py: -------------------------------------------------------------------------------- 1 | def test_one_plus_one_is_two(): 2 | "Check that one and one are indeed two." 3 | assert 1 + 1 == 2 4 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | $ pip install {{ cookiecutter.package_dist_name }} 8 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | Start by importing {{ cookiecutter.project_name }}. 6 | 7 | .. code-block:: python 8 | 9 | import {{ cookiecutter.package_dir_name }} 10 | -------------------------------------------------------------------------------- /RELEASES.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Release History 3 | =============== 4 | 5 | v0.1.1 (2021-10-26) 6 | ................... 7 | + relax python version for pre-commit 8 | 9 | 10 | v0.1.0 (2021-10-08) 11 | ................... 12 | Initial Release 13 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/setup.cfg: -------------------------------------------------------------------------------- 1 | [versioneer] 2 | VCS = git 3 | style = pep440-post 4 | versionfile_source = {{ cookiecutter.package_dir_name }}/_version.py 5 | versionfile_build = {{ cookiecutter.package_dir_name }}/_version.py 6 | tag_prefix = v 7 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/.codecov.yml: -------------------------------------------------------------------------------- 1 | # show coverage in CI status, not as a comment. 2 | comment: off 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | patch: 9 | default: 10 | target: auto 11 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Maintainer 6 | ---------- 7 | 8 | * {{ cookiecutter.full_name }} <{{ cookiecutter.email }}> 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? See: CONTRIBUTING.rst 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # These are the requirements for building the tutorial documentation. 2 | # See a separate requirements.txt file inside the cookiecutter. 3 | cookiecutter 4 | ipython 5 | matplotlib 6 | numpy 7 | numpydoc 8 | pexpect 9 | pre-commit 10 | pytest 11 | sphinx 12 | sphinx-copybutton 13 | sphinx_rtd_theme 14 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | {{ cookiecutter.package_dir_name }} 4 | [report] 5 | omit = 6 | */python?.?/* 7 | */site-packages/nose/* 8 | # ignore _version.py and versioneer.py 9 | .*version.* 10 | *_version.py 11 | 12 | exclude_lines = 13 | if __name__ == '__main__': 14 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git, 4 | __pycache__, 5 | build, 6 | dist, 7 | versioneer.py, 8 | {{ cookiecutter.package_dir_name }}/_version.py, 9 | docs/source/conf.py 10 | max-line-length = 115 11 | # Ignore some style 'errors' produced while formatting by 'black' 12 | ignore = E203, W503 13 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 115 3 | include = '\.pyi?$' 4 | exclude = ''' 5 | /( 6 | \.git 7 | | \.hg 8 | | \.mypy_cache 9 | | \.tox 10 | | \.venv 11 | | _build 12 | | buck-out 13 | | build 14 | | dist 15 | 16 | # The following are specific to Black, you probably don't want those. 17 | | blib2to3 18 | | tests/data 19 | )/ 20 | ''' 21 | -------------------------------------------------------------------------------- /docs/source/example_pre-commit.yml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3.7 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v2.0.0 9 | hooks: 10 | - id: flake8 11 | - repo: https://github.com/kynan/nbstripout 12 | rev: 0.3.9 13 | hooks: 14 | - id: nbstripout 15 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include LICENSE 4 | include README.rst 5 | include requirements.txt 6 | 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | 10 | recursive-include docs *.rst conf.py Makefile make.bat 11 | 12 | include versioneer.py 13 | include {{ cookiecutter.package_dir_name }}/_version.py 14 | 15 | # If including data files in the package, add them like: 16 | # include path/to/data_file 17 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # These are required for developing the package (running the tests, building 2 | # the documentation) but not necessarily required for _using_ it. 3 | black 4 | codecov 5 | coverage 6 | flake8 7 | isort 8 | nbstripout 9 | pre-commit 10 | pre-commit-hooks 11 | pytest 12 | sphinx 13 | twine 14 | # These are dependencies of various sphinx extensions for documentation. 15 | ipython 16 | matplotlib 17 | numpydoc 18 | sphinx-copybutton 19 | sphinx_rtd_theme 20 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Packaging Scientific Python documentation master file, created by 2 | sphinx-quickstart on Thu Jun 28 12:35:56 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | {{ cookiecutter.project_name }} Documentation 7 | {{ '=' * (cookiecutter.project_name|length + ' Documentation'|length) }} 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | installation 13 | usage 14 | release-history 15 | min_versions 16 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | pre-commit: 10 | # pull requests are a duplicate of a branch push if within the same repo. 11 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository 12 | 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-python@v4 17 | - uses: pre-commit/action@v3.0.0 18 | with: 19 | extra_args: --all-files 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scientific Python Cookiecutter 2 | 3 | **[Documentation](https://nsls-ii.github.io/scientific-python-cookiecutter/)** 4 | 5 | This repository contains a cookiecutter template for generating a scientific 6 | Python project complete with CI testing and documentation. 7 | 8 | ``` 9 | cookiecutter https://github.com/NSLS-II/scientific-python-cookiecutter 10 | ``` 11 | 12 | Separately, beside the cookiecutter template, it contains 13 | [tutorial documentation](https://nsls-ii.github.io/scientific-python-cookiecutter/) 14 | on how to use this template to organize scientific Python code into a package 15 | and use popular tools for testing, documentation, and maintenance. 16 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "full_name": "Name or Organization", 3 | "email": "", 4 | "github_username": "", 5 | "project_name": "Your Project Name", 6 | "package_dist_name": "{{ cookiecutter.project_name|replace(' ', '-')|lower }}", 7 | "package_dir_name": "{{ cookiecutter.project_name|replace(' ', '_')|replace('-', '_')|lower }}", 8 | "repo_name": "{{ cookiecutter.project_name|replace(' ', '-')|lower }}", 9 | "project_short_description": "Python package for doing science.", 10 | "minimum_supported_python_version": ["3.8", "3.9", "3.10"], 11 | "_copy_without_render": [ 12 | "*.html", 13 | "*.js", 14 | ".github/*" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = "-W" # This flag turns warnings into errors. 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = PackagingScientificPython 8 | SOURCEDIR = source 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.repo_name }}/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.4.0 6 | hooks: 7 | - id: check-yaml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - repo: https://github.com/ambv/black 11 | rev: 23.3.0 12 | hooks: 13 | - id: black 14 | - repo: https://github.com/pycqa/flake8 15 | rev: 6.0.0 16 | hooks: 17 | - id: flake8 18 | - repo: https://github.com/pycqa/isort 19 | rev: 5.12.0 20 | hooks: 21 | - id: isort 22 | args: ["--profile", "black"] 23 | - repo: https://github.com/kynan/nbstripout 24 | rev: 0.6.1 25 | hooks: 26 | - id: nbstripout 27 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = "-W" # This flag turns warnings into errors. 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = PackagingScientificPython 8 | SOURCEDIR = source 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 | -------------------------------------------------------------------------------- /run_cookiecutter_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import pexpect 3 | 4 | p = pexpect.spawn('cookiecutter .') 5 | 6 | p.expect('full_name .*') 7 | p.sendline('Brookhaven National Lab') 8 | 9 | p.expect('email .*') 10 | p.sendline('dallan@bnl.gov') 11 | 12 | p.expect('github_username .*') 13 | p.sendline('danielballan') 14 | 15 | p.expect('project_name .*') 16 | p.sendline('Example') 17 | 18 | p.expect('package_dist_name .*') 19 | p.sendline('') 20 | 21 | p.expect('package_dir_name .*') 22 | p.sendline('') 23 | 24 | p.expect('repo_name .*') 25 | p.sendline('') 26 | 27 | p.expect('project_short_description .*') 28 | p.sendline('') 29 | 30 | p.expect('Select minimum_supported_python_version.*') 31 | p.sendline('') 32 | 33 | # Runs until the cookiecutter is done; then exits. 34 | p.interact() 35 | -------------------------------------------------------------------------------- /docs/source/test_examples.py: -------------------------------------------------------------------------------- 1 | # example/tests/test_examples.py 2 | 3 | import numpy as np 4 | from ..refraction import snell 5 | # (The above is equivalent to `from example.refraction import snell`. 6 | # Read on for why.) 7 | 8 | 9 | def test_perpendicular(): 10 | # For any indexes, a ray normal to the surface should not bend. 11 | # We'll try a couple different combinations of indexes.... 12 | 13 | actual = snell(0, 2.00, 3.00) 14 | expected = 0 15 | assert actual == expected 16 | 17 | actual = snell(0, 3.00, 2.00) 18 | expected = 0 19 | assert actual == expected 20 | 21 | 22 | def test_air_water(): 23 | n_air, n_water = 1.00, 1.33 24 | actual = snell(np.pi/4, n_air, n_water) 25 | expected = 0.5605584137424605 26 | assert np.allclose(actual, expected) 27 | -------------------------------------------------------------------------------- /docs/source/example_travis.yml: -------------------------------------------------------------------------------- 1 | # .travis.yml 2 | 3 | language: python 4 | python: 5 | - 3.9 6 | cache: 7 | directories: 8 | - $HOME/.cache/pip 9 | - $HOME/.ccache # https://github.com/travis-ci/travis-ci/issues/5853 10 | 11 | install: 12 | # Install this package and the packages listed in requirements.txt. 13 | - pip install . 14 | # Install extra requirements for running tests and building docs. 15 | - pip install -r requirements-dev.txt 16 | 17 | script: 18 | - coverage run -m pytest # Run the tests and check for test coverage. 19 | - coverage report -m # Generate test coverage report. 20 | - codecov # Upload the report to codecov. 21 | - flake8 --max-line-length=115 # Enforce code style (but relax line length limit a bit). 22 | - make -C docs html # Build the documentation. 23 | -------------------------------------------------------------------------------- /docs/source/refraction.py: -------------------------------------------------------------------------------- 1 | # example/refraction.py 2 | 3 | import numpy as np 4 | 5 | 6 | def snell(theta_inc, n1, n2): 7 | """ 8 | Compute the refraction angle using Snell's Law. 9 | 10 | See https://en.wikipedia.org/wiki/Snell%27s_law 11 | 12 | Parameters 13 | ---------- 14 | theta_inc : float 15 | Incident angle in radians. 16 | n1, n2 : float 17 | The refractive index of medium of origin and destination medium. 18 | 19 | Returns 20 | ------- 21 | theta : float 22 | refraction angle 23 | 24 | Examples 25 | -------- 26 | A ray enters an air--water boundary at pi/4 radians (45 degrees). 27 | Compute exit angle. 28 | 29 | >>> snell(np.pi/4, 1.00, 1.33) 30 | 0.5605584137424605 31 | """ 32 | return np.arcsin(n1 / n2 * np.sin(theta_inc)) 33 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/README.rst: -------------------------------------------------------------------------------- 1 | {% set section_separator = "=" * cookiecutter.project_name | length -%} 2 | {{ section_separator }} 3 | {{ cookiecutter.project_name }} 4 | {{ section_separator }} 5 | 6 | .. image:: https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.repo_name }}/actions/workflows/testing.yml/badge.svg 7 | :target: https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.repo_name }}/actions/workflows/testing.yml 8 | 9 | 10 | .. image:: https://img.shields.io/pypi/v/{{ cookiecutter.repo_name }}.svg 11 | :target: https://pypi.python.org/pypi/{{ cookiecutter.repo_name }} 12 | 13 | 14 | {{ cookiecutter.project_short_description}} 15 | 16 | * Free software: 3-clause BSD license 17 | * Documentation: (COMING SOON!) https://{{ cookiecutter.github_username}}.github.io/{{ cookiecutter.repo_name }}. 18 | 19 | Features 20 | -------- 21 | 22 | * TODO 23 | -------------------------------------------------------------------------------- /docs/source/further-reading.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Futher Reading 3 | ============== 4 | 5 | Other Tutorials and Articles 6 | ---------------------------- 7 | 8 | * `Official Python Packaging Guide `_ 9 | * `"SettingUpOpenSource" lesson plans by John Leeman `_ 10 | * `"Structuring your Project" (written from the perspective of Python for web applications, as opposed to scientific libraries) `_ 11 | 12 | Other Python Cookiecutters 13 | -------------------------- 14 | 15 | * https://github.com/tylerdave/cookiecutter-python-package 16 | * https://github.com/audreyr/cookiecutter-pypackage 17 | 18 | Basics 19 | ------ 20 | 21 | * `Python Modules `_ 22 | * `Software Carpentry Git Tutorial (For Novices) `_ 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=PackagingScientificPython 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=PackagingScientificPython 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/example_travis_with_doctr.yml: -------------------------------------------------------------------------------- 1 | # .travis.yml 2 | 3 | language: python 4 | python: 5 | - 3.6 6 | cache: 7 | directories: 8 | - $HOME/.cache/pip 9 | - $HOME/.ccache # https://github.com/travis-ci/travis-ci/issues/5853 10 | 11 | env: 12 | global: 13 | # Doctr deploy key for YOUR_GITHUB_USERNAME/YOUR_REPO_NAME 14 | - secure: "" 15 | 16 | install: 17 | # Install this package and the packages listed in requirements.txt. 18 | - pip install . 19 | # Install extra requirements for running tests and building docs. 20 | - pip install -r requirements-dev.txt 21 | 22 | script: 23 | - coverage run -m pytest # Run the tests and check for test coverage. 24 | - coverage report -m # Generate test coverage report. 25 | - codecov # Upload the report to codecov. 26 | - flake8 --max-line-length=115 # Enforce code style (but relax line length limit a bit). 27 | - set -e # If any of the following steps fail, just stop at that point. 28 | - make -C docs html # Build the documentation. 29 | - pip install doctr 30 | - doctr deploy --built-docs docs/build/html . # Publish the documentation. 31 | -------------------------------------------------------------------------------- /docs/source/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using flit 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@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install wheel twine setuptools 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: __token__ 28 | # The PYPI_PASSWORD must be a pypi token with the "pypi-" prefix with sufficient permissions to upload this package 29 | # https://pypi.org/help/#apitoken 30 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 31 | run: | 32 | python setup.py sdist bdist_wheel 33 | twine upload dist/* 34 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/docs/source/min_versions.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Minimum Version of Python and NumPy 3 | =================================== 4 | 5 | 6 | - This project supports at least the minor versions of Python 7 | initially released 42 months prior to a planned project release 8 | date. 9 | - The project will always support at least the 2 latest minor 10 | versions of Python. 11 | - The project will support minor versions of ``numpy`` initially 12 | released in the 24 months prior to a planned project release date or 13 | the oldest version that supports the minimum Python version 14 | (whichever is higher). 15 | - The project will always support at least the 3 latest minor 16 | versions of NumPy. 17 | 18 | The minimum supported version of Python will be set to 19 | ``python_requires`` in ``setup``. All supported minor versions of 20 | Python will be in the test matrix and have binary artifacts built 21 | for releases. 22 | 23 | The project should adjust upward the minimum Python and NumPy 24 | version support on every minor and major release, but never on a 25 | patch release. 26 | 27 | This is consistent with NumPy `NEP 29 28 | `__. 29 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/.github/workflows/publish-pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using flit when a release is 2 | # created. For more information see: 3 | # https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 4 | 5 | name: PyPI upload 6 | 7 | on: 8 | release: 9 | types: [created] 10 | 11 | jobs: 12 | publish_pypi: 13 | name: Publish package to PyPI 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: '3.x' 25 | 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install wheel twine setuptools 30 | 31 | - name: Build and publish 32 | env: 33 | TWINE_USERNAME: __token__ 34 | # The PYPI_PASSWORD must be a pypi token with the "pypi-" prefix with sufficient permissions to upload this package 35 | # https://pypi.org/help/#apitoken 36 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 37 | run: | 38 | python setup.py sdist bdist_wheel 39 | twine upload dist/* 40 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | venv/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/build/ 58 | docs/source/generated/ 59 | 60 | # pytest 61 | .pytest_cache/ 62 | 63 | # PyBuilder 64 | target/ 65 | 66 | # Editor files 67 | # mac 68 | .DS_Store 69 | *~ 70 | 71 | # vim 72 | *.swp 73 | *.swo 74 | 75 | # pycharm 76 | .idea/ 77 | 78 | # VSCode 79 | .vscode/ 80 | 81 | # Ipython Notebook 82 | .ipynb_checkpoints 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Brookhaven National Laboratory 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) {% now 'utc', '%Y' %}, {{ cookiecutter.full_name }} 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software 18 | without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Packaging Scientific Python documentation master file, created by 2 | sphinx-quickstart on Thu Jun 28 12:35:56 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Bootstrap a Scientific Python Library 7 | ===================================== 8 | 9 | This is a tutorial with a template for packaging, testing, documenting, and 10 | publishing scientific Python code. 11 | 12 | Do you have a folder of disorganized scientific Python scripts? Are you always 13 | hunting for code snippets scattered across dozens of Jupyter notebooks? Has it 14 | become unwieldy to manage, update, and share with collaborators? This tutorial 15 | is for you. 16 | 17 | See this lightning talk from Scipy 2018 for a short overview of the motivation 18 | for and scope of this tutorial. 19 | 20 | .. youtube:: 1HDq7QoOlI4?start=1961 21 | 22 | Starting from a working, full-featured template, you will: 23 | 24 | * Move your code into a version-controlled, installable Python package. 25 | * Share your code on GitHub. 26 | * Generate documentation including interactive usage examples and plots. 27 | * Add automated tests to help ensure that new changes don't break existing 28 | functionality. 29 | * Use a free CI (continuous integration) service to automatically run your 30 | tests against any proposed changes and automatically publish the latest 31 | documentation when a change is made. 32 | * Publish a release on PyPI so that users and collaborators can install your 33 | code with pip. 34 | 35 | .. toctree:: 36 | :maxdepth: 2 37 | 38 | philosophy 39 | preliminaries 40 | the-code-itself 41 | guiding-design-principles 42 | ci 43 | pre-commit 44 | writing-docs 45 | including-data-files 46 | publishing-docs 47 | publishing-releases 48 | advanced-testing 49 | environments 50 | further-reading 51 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Documentation 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build_docs: 10 | # pull requests are a duplicate of a branch push if within the same repo. 11 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: ["3.10"] 17 | fail-fast: false 18 | 19 | defaults: 20 | run: 21 | shell: bash -l {0} 22 | 23 | steps: 24 | - name: Set env vars 25 | run: | 26 | export REPOSITORY_NAME=${GITHUB_REPOSITORY#*/} # just the repo, as opposed to org/repo 27 | echo "REPOSITORY_NAME=${REPOSITORY_NAME}" >> $GITHUB_ENV 28 | 29 | - name: Checkout the code 30 | uses: actions/checkout@v3 31 | with: 32 | fetch-depth: 1000 # should be enough to reach the most recent tag 33 | 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v4 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | 39 | - name: Install documentation-building requirements 40 | run: | 41 | # For reference: https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html. 42 | set -vxeuo pipefail 43 | 44 | # These packages are installed in the base environment but may be older 45 | # versions. Explicitly upgrade them because they often create 46 | # installation problems if out of date. 47 | python -m pip install --upgrade pip setuptools numpy 48 | 49 | pip install . 50 | pip install -r requirements-dev.txt 51 | pip list 52 | 53 | - name: Build Docs 54 | run: make -C docs/ html 55 | 56 | - uses: actions/upload-artifact@v3 57 | with: 58 | name: ${{ env.REPOSITORY_NAME }}-docs 59 | path: docs/build/html/ 60 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | # schedule: 8 | # - cron: '00 4 * * *' # daily at 4AM 9 | 10 | jobs: 11 | run_tests: 12 | # pull requests are a duplicate of a branch push if within the same repo. 13 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository 14 | 15 | runs-on: ${{ matrix.host-os }} 16 | strategy: 17 | matrix: 18 | host-os: ["ubuntu-latest"] 19 | # host-os: ["ubuntu-latest", "macos-latest", "windows-latest"] 20 | python-version: ["3.8", "3.9", "3.10"] 21 | fail-fast: false 22 | 23 | defaults: 24 | run: 25 | shell: bash -l {0} 26 | 27 | steps: 28 | - name: Set env vars 29 | run: | 30 | export REPOSITORY_NAME=${GITHUB_REPOSITORY#*/} # just the repo, as opposed to org/repo 31 | echo "REPOSITORY_NAME=${REPOSITORY_NAME}" >> $GITHUB_ENV 32 | 33 | - name: Checkout the code 34 | uses: actions/checkout@v3 35 | 36 | - name: Set up Python ${{ matrix.python-version }} 37 | uses: actions/setup-python@v4 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | 41 | - name: Install dependencies 42 | run: | 43 | # For reference: https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html. 44 | set -vxeuo pipefail 45 | 46 | # These packages are installed in the base environment but may be older 47 | # versions. Explicitly upgrade them because they often create 48 | # installation problems if out of date. 49 | python -m pip install --upgrade pip setuptools numpy 50 | 51 | pip install . 52 | pip install -r requirements-dev.txt 53 | pip list 54 | 55 | - name: Test with pytest 56 | run: | 57 | set -vxeuo pipefail 58 | coverage run -m pytest -vv -s 59 | coverage report -m 60 | -------------------------------------------------------------------------------- /docs/source/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | # This conditional ensures that forks on GitHub do not attempt to publish 11 | # documentation, only the "upstream" one. (If forks _did_ try to publish 12 | # they would fail later below because they would not have access to the 13 | # secret credentials. But it's better to opt out here.) 14 | if: github.repository_owner == 'YOUR_GITHUB_USERNAME_OR_ORGANIZATION' 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.8] 19 | fail-fast: false 20 | 21 | steps: 22 | 23 | - name: Set env.REPOSITORY_NAME # just the repo, as opposed to org/repo 24 | shell: bash -l {0} 25 | run: | 26 | export REPOSITORY_NAME=${GITHUB_REPOSITORY#*/} 27 | echo "REPOSITORY_NAME=${REPOSITORY_NAME}" >> $GITHUB_ENV 28 | 29 | - uses: actions/checkout@v2 30 | with: 31 | fetch-depth: 1000 # should be enough to reach the most recent tag 32 | 33 | - name: Set up Python ${{ matrix.python-version }} 34 | uses: actions/setup-python@v2 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | 38 | - name: Install 39 | shell: bash -l {0} 40 | run: source continuous_integration/scripts/install.sh 41 | 42 | - name: Install documentation-building requirements 43 | shell: bash -l {0} 44 | run: | 45 | set -vxeuo pipefail 46 | python -m pip install -r requirements-dev.txt 47 | python -m pip list 48 | 49 | - name: Build Docs 50 | shell: bash -l {0} 51 | run: make -C docs/ html 52 | 53 | - name: Deploy documentation to blueskyproject.io. 54 | # We pin to the SHA, not the tag, for security reasons. 55 | # https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions 56 | uses: peaceiris/actions-gh-pages@bbdfb200618d235585ad98e965f4aafc39b4c501 # v3.7.3 57 | with: 58 | deploy_key: ${{ secrets.ACTIONS_DOCUMENTATION_DEPLOY_KEY }} 59 | publish_branch: gh-pages 60 | publish_dir: ./docs/build/html 61 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | import versioneer 7 | 8 | # NOTE: This file must remain Python 2 compatible for the foreseeable future, 9 | # to ensure that we error out properly for people with outdated setuptools 10 | # and/or pip. 11 | min_version = ( 12 | {{ cookiecutter.minimum_supported_python_version[0] }}, 13 | {{ cookiecutter.minimum_supported_python_version[2] }}, 14 | ) 15 | if sys.version_info < min_version: 16 | error = """ 17 | {{ cookiecutter.package_dist_name }} does not support Python {0}.{1}. 18 | Python {2}.{3} and above is required. Check your Python version like so: 19 | 20 | python3 --version 21 | 22 | This may be due to an out-of-date pip. Make sure you have pip >= 9.0.1. 23 | Upgrade pip like so: 24 | 25 | pip install --upgrade pip 26 | """.format( 27 | *(sys.version_info[:2] + min_version) 28 | ) 29 | sys.exit(error) 30 | 31 | here = path.abspath(path.dirname(__file__)) 32 | 33 | with open(path.join(here, "README.rst"), encoding="utf-8") as readme_file: 34 | readme = readme_file.read() 35 | 36 | with open(path.join(here, "requirements.txt")) as requirements_file: 37 | # Parse requirements.txt, ignoring any commented-out lines. 38 | requirements = [line for line in requirements_file.read().splitlines() if not line.startswith("#")] 39 | 40 | 41 | setup( 42 | name="{{ cookiecutter.package_dist_name }}", 43 | version=versioneer.get_version(), 44 | cmdclass=versioneer.get_cmdclass(), 45 | description="{{ cookiecutter.project_short_description }}", 46 | long_description=readme, 47 | author="{{ cookiecutter.full_name }}", 48 | author_email="{{ cookiecutter.email }}", 49 | url="https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.repo_name }}", 50 | python_requires=">={}".format(".".join(str(n) for n in min_version)), 51 | packages=find_packages(exclude=["docs", "tests"]), 52 | entry_points={ 53 | "console_scripts": [ 54 | # 'command = some.module:some_function', 55 | ], 56 | }, 57 | include_package_data=True, 58 | package_data={ 59 | "{{ cookiecutter.package_dir_name }}": [ 60 | # When adding files here, remember to update MANIFEST.in as well, 61 | # or else they will not be included in the distribution on PyPI! 62 | # 'path/to/data_file', 63 | ] 64 | }, 65 | install_requires=requirements, 66 | license="BSD (3-clause)", 67 | classifiers=[ 68 | "Development Status :: 2 - Pre-Alpha", 69 | "Natural Language :: English", 70 | "Programming Language :: Python :: 3", 71 | ], 72 | ) 73 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | 8 | jobs: 9 | publish-docs: 10 | name: Build and publish documentation 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.8", "3.9", "3.10"] 15 | fail-fast: false 16 | 17 | defaults: 18 | run: 19 | shell: bash -l {0} 20 | 21 | steps: 22 | - name: Set environment variable 23 | run: | 24 | export REPOSITORY_NAME=${GITHUB_REPOSITORY#*/} # just the repo, as opposed to org/repo 25 | echo "REPOSITORY_NAME=${REPOSITORY_NAME}" >> $GITHUB_ENV 26 | 27 | - uses: actions/checkout@v3 28 | 29 | - name: Set up Python ${{ matrix.python-version }} 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | 34 | - name: Build example code 35 | shell: 'script --return --quiet --command "bash -l {0}"' 36 | run: | 37 | set -vxeo pipefail 38 | 39 | pip install -r requirements.txt 40 | ./run_cookiecutter_example.py 41 | 42 | # Test the style with pre-commit 43 | cd example/ 44 | git config --global user.name "CI Tester" 45 | git config --global user.email "noreply@github.com" 46 | git init 47 | git add . 48 | git commit -m "Initial commit" 49 | pre-commit run --all-files 50 | cd .. 51 | 52 | pip install -e example/ 53 | ./copy_user_content.sh 54 | pytest example/ 55 | 56 | - name: Build docs 57 | run: make -C docs html 58 | 59 | - uses: actions/upload-artifact@v3 60 | with: 61 | name: ${{ env.REPOSITORY_NAME }}-py${{ matrix.python-version }}-docs 62 | path: docs/build/html/ 63 | 64 | - name: Publish docs 65 | # We pin to the SHA, not the tag, for security reasons. 66 | # https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions 67 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && matrix.python-version == '3.9' }} 68 | uses: peaceiris/actions-gh-pages@bbdfb200618d235585ad98e965f4aafc39b4c501 # v3.7.3 69 | with: 70 | deploy_key: ${{ secrets.ACTIONS_DOCUMENTATION_DEPLOY_KEY }} 71 | publish_branch: master 72 | publish_dir: ./docs/build/html 73 | external_repository: NSLS-II/NSLS-II.github.io 74 | destination_dir: ${{ env.REPOSITORY_NAME }} 75 | keep_files: true # Keep old files. 76 | force_orphan: false # Keep git history. 77 | -------------------------------------------------------------------------------- /docs/source/environments.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | Environments and Package Managers 3 | ================================= 4 | 5 | Throughout this tutorial, we used ``pip``, a Python package manager, to install 6 | our software and other Python software we needed. In :doc:`preliminaries`, we 7 | used ``venv`` to create a "virtual environment", a sandbox separate from the 8 | general system Python where we could install and use software without 9 | interfering with any system Python tools or other projects. 10 | 11 | There are many alternatives to ``venv``. 12 | `This post of Stack Overflow `_ 13 | is a good survey of the field. 14 | 15 | Conda 16 | ----- 17 | 18 | Conda, which is both a package manager (like ``pip``) and an virtual 19 | environment manager (like ``venv``) was developed specifically for the 20 | scientific Python community. Conda is not limited to Python packages: it has 21 | grown into a general-purpose user-space package manager. Many important 22 | scientific Python packages have important dependencies that aren't Python 23 | packages and can't be installed using ``pip``. Before conda, the user had to 24 | sort out how to install these extra dependencies using the system package 25 | manager, and it was a common pain point. Conda can install all of the 26 | dependencies. 27 | 28 | Conda is conceptually a heavier lift that ``pip`` and ``venv``, which is why we 29 | still with the basics in :doc:`preliminaries`, but it is extremely popular 30 | among both beginners and experts, and we recommend becoming familiar with it. 31 | 32 | Check whether ``conda`` is already installed: 33 | 34 | .. code-block:: bash 35 | 36 | conda 37 | 38 | If that is not found, follow the steps in the 39 | `conda installation guide `_, 40 | which has instructions for Windows, OSX, and Linux. It offers both miniconda (a 41 | minimal installation) and Anaconda. For our purposes, either is suitable. 42 | Miniconda is a faster installation. 43 | 44 | Now, create an environment. 45 | 46 | .. code-block:: bash 47 | 48 | conda create -n my-env python=3.6 49 | 50 | The term ``my-env`` can be anything. It names the new environment. 51 | 52 | Every time you open up a new Terminal / Command Prompt to work on your 53 | project, activate that environment. This means that when you type ``python3`` 54 | or, equivalently, ``python`` you will be getting a specific installation of 55 | Python and Python packages, separate from any default installation on your 56 | machine. 57 | 58 | .. code-block:: bash 59 | 60 | conda activate my-env 61 | 62 | The use of virtual environments leads to multiple instances of ``python``, 63 | ``pip``, ``ipython``, ``pytest``, ``flake8`` and other executables on your 64 | machine. If you encounter unexpected behavior, use ``which ____`` to see which 65 | environment a given command is coming from. (Linux and OSX only.) 66 | -------------------------------------------------------------------------------- /docs/source/ci.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Continous Integration Testing 3 | ============================= 4 | 5 | In this section you will: 6 | 7 | * Understand the benefits Continuous Integration. 8 | * Configure Travis-CI, a "continuous integration" service, to operate on your 9 | GitHub repository. 10 | 11 | What is CI for? 12 | --------------- 13 | 14 | If "Continuous Integration" (CI) is new to you, we refer you to 15 | `this excellent Software Carpentry tutorial `_ 16 | on the subject. To summarize, CI speeds development by checking out your code on 17 | a fresh, clean server, installing your software, running the tests, and 18 | reporting the results. This helps you ensure that your code will work on your 19 | colleague's computer---that it doesn't accidentally depend on some local detail 20 | of your machine. It also creates a clear, public record of whether the tests 21 | passed or failed, so if things are accidentally broken (say, while you are on 22 | vacation) you can trace when the breaking change occurred. 23 | 24 | Travis-CI Configuration 25 | ----------------------- 26 | 27 | The cookiecutter template has already generated a configuration file for 28 | Travis-CI, which is one of several CI services that are free for public 29 | open-source projects. 30 | 31 | .. literalinclude:: example_travis.yml 32 | 33 | You can customize this to your liking. For example, if you are migrating a 34 | large amount of existing code that is not compliant with PEP8, you may want to 35 | remove the line that does ``flake8`` style-checking. 36 | 37 | Activate Travis-CI for Your GitHub Repository 38 | --------------------------------------------- 39 | 40 | #. Go to https://travis-ci.org and sign in with your GitHub account. 41 | #. You will be prompted to authorize Travis-CI to access your GitHub account. 42 | Authorize it. 43 | #. You will be redirected to https://travis-ci.org/profile, which shows a list 44 | of your GitHub repositories. If necessary, click the "Sync Account" button 45 | to refresh that list. 46 | #. Find your new repository in the list. Click the on/off switch next to its 47 | name activate Travis-CI on that repository. 48 | #. Click the repository name, which will direct you to the list of *builds* at 49 | ``https://travis-ci.org/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME/builds``. The 50 | list will currently be empty. You'll see construction cones. 51 | #. The next time you open a pull request or push a new commit to the master 52 | branch, Travis-CI will kick off a new build, and that list will update. 53 | 54 | .. note:: 55 | 56 | If this repository belongs to a GitHub *organization* (e.g. 57 | http://github.com/NSLS-II) as opposed to a personal user account 58 | (e.g. http://github.com/danielballan) you should follow Steps 3-5 59 | above for the organization's profile at 60 | ``https://travis-ci.org/profile/YOUR_GITHUB_ORGANIZATION``. It does no 61 | harm to *also* activate Travis-CI for your personal fork at 62 | ``https://travis-ci.org/profile``, but it's more important to activate it for 63 | the upstream fork associated with the organization. 64 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.repo_name }}/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Any details about your local setup that might be helpful in troubleshooting. 21 | * Detailed steps to reproduce the bug. 22 | 23 | Fix Bugs 24 | ~~~~~~~~ 25 | 26 | Look through the GitHub issues for bugs. Anything tagged with "bug" 27 | is open to whoever wants to implement it. 28 | 29 | Implement Features 30 | ~~~~~~~~~~~~~~~~~~ 31 | 32 | Look through the GitHub issues for features. Anything tagged with "feature" 33 | is open to whoever wants to implement it. 34 | 35 | Write Documentation 36 | ~~~~~~~~~~~~~~~~~~~ 37 | 38 | {{ cookiecutter.project_name }} could always use more documentation, whether 39 | as part of the official {{ cookiecutter.project_name }} docs, in docstrings, 40 | or even on the web in blog posts, articles, and such. 41 | 42 | Submit Feedback 43 | ~~~~~~~~~~~~~~~ 44 | 45 | The best way to send feedback is to file an issue at https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.repo_name }}/issues. 46 | 47 | If you are proposing a feature: 48 | 49 | * Explain in detail how it would work. 50 | * Keep the scope as narrow as possible, to make it easier to implement. 51 | * Remember that this is a volunteer-driven project, and that contributions 52 | are welcome :) 53 | 54 | Get Started! 55 | ------------ 56 | 57 | Ready to contribute? Here's how to set up `{{ cookiecutter.package_dist_name }}` for local development. 58 | 59 | 1. Fork the `{{ cookiecutter.repo_name }}` repo on GitHub. 60 | 2. Clone your fork locally:: 61 | 62 | $ git clone git@github.com:your_name_here/{{ cookiecutter.repo_name }}.git 63 | 64 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 65 | 66 | $ mkvirtualenv {{ cookiecutter.repo_name }} 67 | $ cd {{ cookiecutter.repo_name }}/ 68 | $ python setup.py develop 69 | 70 | 4. Create a branch for local development:: 71 | 72 | $ git checkout -b name-of-your-bugfix-or-feature 73 | 74 | Now you can make your changes locally. 75 | 76 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 77 | 78 | $ flake8 {{ cookiecutter.package_dir_name }} tests 79 | $ python setup.py test 80 | $ tox 81 | 82 | To get flake8 and tox, just pip install them into your virtualenv. 83 | 84 | 6. Commit your changes and push your branch to GitHub:: 85 | 86 | $ git add . 87 | $ git commit -m "Your detailed description of your changes." 88 | $ git push origin name-of-your-bugfix-or-feature 89 | 90 | 7. Submit a pull request through the GitHub website. 91 | 92 | Pull Request Guidelines 93 | ----------------------- 94 | 95 | Before you submit a pull request, check that it meets these guidelines: 96 | 97 | 1. The pull request should include tests. 98 | 2. If the pull request adds functionality, the docs should be updated. Put 99 | your new functionality into a function with a docstring, and add the 100 | feature to the list in README.rst. 101 | 3. The pull request should work for Python 2.7, 3.3, 3.4, 3.5 and for PyPy. Check 102 | https://travis-ci.org/{{ cookiecutter.github_username }}/{{ cookiecutter.repo_name }}/pull_requests 103 | and make sure that the tests pass for all supported Python versions. 104 | -------------------------------------------------------------------------------- /docs/source/vendor/youtube.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # VENDORED FROM 5 | # https://github.com/sphinx-contrib/youtube/blob/master/sphinxcontrib/youtube.py 6 | 7 | from __future__ import division 8 | 9 | import re 10 | from docutils import nodes 11 | from docutils.parsers.rst import directives, Directive 12 | 13 | CONTROL_HEIGHT = 30 14 | 15 | def get_size(d, key): 16 | if key not in d: 17 | return None 18 | m = re.match("(\d+)(|%|px)$", d[key]) 19 | if not m: 20 | raise ValueError("invalid size %r" % d[key]) 21 | return int(m.group(1)), m.group(2) or "px" 22 | 23 | def css(d): 24 | return "; ".join(sorted("%s: %s" % kv for kv in d.items())) 25 | 26 | class youtube(nodes.General, nodes.Element): pass 27 | 28 | def visit_youtube_node(self, node): 29 | aspect = node["aspect"] 30 | width = node["width"] 31 | height = node["height"] 32 | 33 | if aspect is None: 34 | aspect = 16, 9 35 | 36 | if (height is None) and (width is not None) and (width[1] == "%"): 37 | style = { 38 | "padding-top": "%dpx" % CONTROL_HEIGHT, 39 | "padding-bottom": "%f%%" % (width[0] * aspect[1] / aspect[0]), 40 | "width": "%d%s" % width, 41 | "position": "relative", 42 | } 43 | self.body.append(self.starttag(node, "div", style=css(style))) 44 | style = { 45 | "position": "absolute", 46 | "top": "0", 47 | "left": "0", 48 | "width": "100%", 49 | "height": "100%", 50 | "border": "0", 51 | } 52 | attrs = { 53 | "src": "https://www.youtube.com/embed/%s" % node["id"], 54 | "style": css(style), 55 | } 56 | self.body.append(self.starttag(node, "iframe", **attrs)) 57 | self.body.append("") 58 | else: 59 | if width is None: 60 | if height is None: 61 | width = 560, "px" 62 | else: 63 | width = height[0] * aspect[0] / aspect[1], "px" 64 | if height is None: 65 | height = width[0] * aspect[1] / aspect[0], "px" 66 | style = { 67 | "width": "%d%s" % width, 68 | "height": "%d%s" % (height[0] + CONTROL_HEIGHT, height[1]), 69 | "border": "0", 70 | } 71 | attrs = { 72 | "src": "https://www.youtube.com/embed/%s" % node["id"], 73 | "style": css(style), 74 | } 75 | self.body.append(self.starttag(node, "iframe", **attrs)) 76 | self.body.append("") 77 | 78 | def depart_youtube_node(self, node): 79 | pass 80 | 81 | def visit_youtube_node_latex(self,node): 82 | self.body.append(r'\begin{quote}\begin{center}\fbox{\url{https://www.youtu.be/%s}}\end{center}\end{quote}'%node['id']) 83 | 84 | 85 | class YouTube(Directive): 86 | has_content = True 87 | required_arguments = 1 88 | optional_arguments = 0 89 | final_argument_whitespace = False 90 | option_spec = { 91 | "width": directives.unchanged, 92 | "height": directives.unchanged, 93 | "aspect": directives.unchanged, 94 | } 95 | 96 | def run(self): 97 | if "aspect" in self.options: 98 | aspect = self.options.get("aspect") 99 | m = re.match("(\d+):(\d+)", aspect) 100 | if m is None: 101 | raise ValueError("invalid aspect ratio %r" % aspect) 102 | aspect = tuple(int(x) for x in m.groups()) 103 | else: 104 | aspect = None 105 | width = get_size(self.options, "width") 106 | height = get_size(self.options, "height") 107 | return [youtube(id=self.arguments[0], aspect=aspect, width=width, height=height)] 108 | 109 | 110 | def unsupported_visit_youtube(self, node): 111 | self.builder.warn('youtube: unsupported output format (node skipped)') 112 | raise nodes.SkipNode 113 | 114 | 115 | _NODE_VISITORS = { 116 | 'html': (visit_youtube_node, depart_youtube_node), 117 | 'latex': (visit_youtube_node_latex, depart_youtube_node), 118 | 'man': (unsupported_visit_youtube, None), 119 | 'texinfo': (unsupported_visit_youtube, None), 120 | 'text': (unsupported_visit_youtube, None) 121 | } 122 | 123 | 124 | def setup(app): 125 | app.add_node(youtube, **_NODE_VISITORS) 126 | app.add_directive("youtube", YouTube) 127 | -------------------------------------------------------------------------------- /docs/source/philosophy.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Philosophy of this Tutorial 3 | =========================== 4 | 5 | We aim to nudge scientist--developers toward good practices and standard tools, 6 | to help small- and medium-sized projects start off on the right foot. Keeping 7 | in mind that too much development infrastructure can be overwhelming to those 8 | who haven't yet encountered the need for it, we stop short of recommending the 9 | *full* stack of tools used by large scientific Python projects. 10 | 11 | This is an opinionated tutorial. We guide readers toward certain tools and 12 | conventions. We do not always mention alternatives and their respective 13 | trade-offs because we want to avoid overwhelming beginners with too many 14 | decisions. Our choices are based on the current consensus in the scientific 15 | Python community and the personal experience of the authors. 16 | 17 | The Tools, and the Problems They Solve 18 | -------------------------------------- 19 | 20 | .. 21 | Note: The bolded items here are intentionally not links. These things are 22 | easy enough to Google if people want to know more, and I'd like to avoid 23 | scaring anyone off by making them feel like they need to review the project 24 | home pages for each of these projects before proceeding. 25 | 26 | * **Python 3** has been around since 2008, and it gained real traction in the 27 | scientific Python ecosystem around 2014. It offers 28 | `many benefits for scientific applications `_. 29 | Python 2 will reach its end-of-life (no new security updates) in 2020. Some 30 | projects still support both 2 and 3, but many have already dropped support 31 | for Python 2 in new releases or 32 | `publicly stated their plans to do so `_. 33 | This cookiecutter focuses on Python 3 only. 34 | * **Pip** is the official Python package manager, and it can install packages 35 | either from code on your local machine or by downloading them from the Python 36 | Package Index (PyPI). 37 | * **Git** is a version control system that has become the consensus choice. It 38 | is used by virtually all of the major scientific Python projects, including 39 | Python itself. 40 | * **GitHub** is a website, owned by Microsoft, for hosting git repositories and 41 | discussion between contributors and users. 42 | * **Cookiecutter** is a tool for generating a directory of files from a 43 | template. We will use it to generate the boilerplate scaffolding of a Python 44 | project and various configuration files without necessarily understanding all 45 | the details up front. 46 | * **PyTest** is a framework for writing code that tests other Python code. 47 | There are several such frameworks, but pytest has emerged as the favorite, 48 | and major projects like numpy have switched from older systems to pytest. 49 | * **Flake8** is a tool for inspecting code for likely mistakes (such as a 50 | variable that is defined but never used) and/or inconsitent style. This tool 51 | is right "on the line" as far as what we would recommend for beginners. Your 52 | mileage may vary, and so the tutorial includes clear instructions for 53 | ommitting this piece. 54 | * **Travis-CI** is an online service, free for open-source projects, that 55 | speeds software development by checking out your code on a fresh, clean 56 | server, installing your software, running the tests, and reporting the 57 | results. This helps you ensure that your code will work on your colleague’s 58 | computer---that it doesn’t accidentally depend on some local detail of your 59 | machine. It also creates a clear, public record of whether the tests passed 60 | or failed, so if things are accidentally broken (say, while you are on 61 | vacation) you can trace when the breaking change occurred. 62 | * **RestructuredText** (``.rst``) is a markup language, like HTML. It is 63 | designed for writing software documentation. It is used by almost all 64 | scientific Python projects, large and small. 65 | * **Sphinx** is a documentation-publishing tool that renders 66 | RestructuredText into HTML, PDF, LaTeX, and other useful formats. It also 67 | inspects Python code to extract information (e.g. the list of functions in a 68 | module and the arguments they expect) from which it can automatically 69 | generate documentation. Extensions for sphinx provide additional 70 | functionality, some of which we cover in this tutorial. 71 | * **GitHub Pages** is a free service for publishing static websites. It is 72 | suitable for publishing the documentation generated by sphinx. 73 | -------------------------------------------------------------------------------- /docs/source/pre-commit.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Git hooks and pre-commit 3 | ======================== 4 | 5 | In this section you will: 6 | 7 | * Learn about git hooks 8 | * Configure pre-commit, a handy program to set up your git hooks. 9 | 10 | What are git hooks for? 11 | ----------------------- 12 | `Git hooks `_ are way of running custom 13 | scripts on your repository either client-side or server-side. In fact, server-side hooks can 14 | be used to trigger things like continuous integration. Here we will focus on some client-side 15 | (i.e. on your local machine) hooks that will ensure certain formatting standards 16 | are met prior to commits. This way, all of your commits are meaningful changes to code, and 17 | your git history doesn't get littered with "apply black" and "fix PEP-8" messages. 18 | Notably these kinds of tests can also be run during 19 | continuous integration (as seen in the configuration of :doc:`Travis-CI`). 20 | 21 | We also include a hook that scrubs any output from jupyter notebooks. 22 | This way, the only time git thinks a notebook has changed is when the contents of the cells have changed. 23 | 24 | It's just formatting, who cares? 25 | -------------------------------- 26 | Ideally, someone else is going to read your code, and better yet, make changes to it. 27 | Having a consistent code style and format makes that easier. Going a step further, 28 | using an even more specific formatter such as `Black `_, 29 | ensures that changes to programs produce the smallest changes to text possible. 30 | 31 | Configuring pre-commit 32 | ---------------------- 33 | The cookiecutter template has already generated a configuration file for pre-commit, 34 | which will construct your git hooks. 35 | 36 | .. literalinclude:: example_pre-commit.yml 37 | 38 | You can customize this to your liking, or not use it entirely. 39 | For example, if you are migrating a large amount of existing code that is not compliant with 40 | PEP8, yet managing notebooks collaboratively, 41 | you may want to remove the section that does ``flake8`` style-checking and ``black``. 42 | 43 | Running ``pre-commit install`` inside the top-level directory of your repository 44 | will use this configuration file to set up git hooks to run prior to completing a commit. 45 | 46 | Hooks included in this cookiecutter 47 | ----------------------------------- 48 | - ``flake8``: Python style enforcement 49 | - ``black``: An uncompromising formatter complient with flake8 50 | - ``isort``: An import sorter for added consistency 51 | - Select hooks from ``pre-commit-hooks``: Whitespace enfrocement and yaml style-checking 52 | - ``nbstripout``: Jupyter notebook stripping of output 53 | 54 | Committing Changes 55 | ------------------ 56 | Assuming you've decided to keep ``nbstripout``, ``black``, and ``flake8`` as hooks, each 57 | time you commit changes to the repository files will be checked for these standards. 58 | If your files don't fit the standard, the commit will fail. 59 | 60 | For example with notebooks and ``nbstripout``: 61 | **Before you commit changes to git** the *output* area of the notebooks must 62 | be cleared. This ensures that (1) the potentially-large output artifacts 63 | (such as figures) do not bloat the repository and (2) users visiting the 64 | tutorial will see a clean notebook, uncluttered by any previous code 65 | execution. This can be accomplished by running ``nbstripout`` before the commit. 66 | 67 | If you forget to do this, an error message will protect you from accidentally 68 | committing. It looks like this:: 69 | 70 | $ git add . 71 | $ git commit -m "oops" 72 | nbstripout...............................................................Failed 73 | - hook id: nbstripout 74 | - files were modified by this hook 75 | 76 | 77 | What happened here? Your attempt to commit has been blocked. The files have 78 | been fixed for you---clearing the outputs from your notebooks, but 79 | git-hooks won't assume you want these fixes committed. 80 | Before trying again to commit, you must add those fixes to the "staged" changes:: 81 | 82 | # Stage again to include the fixes that we just applied (the cleared output areas). 83 | $ git add . 84 | 85 | # Now try committing again. 86 | $ git commit -m "this will work" 87 | nbstripout...............................................................Passed 88 | [main 315536e] changed things 89 | 2 files changed, 44 insertions(+), 18 deletions(-) 90 | 91 | 92 | The same procedure holds for applying black to files. 93 | **However, Flake8 is a checker and not a formatter. 94 | It will only report issues, not change them.** 95 | -------------------------------------------------------------------------------- /docs/source/publishing-docs.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Publishing the Documentation 3 | ============================ 4 | 5 | In this section you will publish your documentation on a public website hosted 6 | by GitHub Pages. 7 | 8 | This section assume you have already set up Travis-CI, as we covered in a 9 | previous section, :doc:`ci`. 10 | 11 | Configure Doctr 12 | --------------- 13 | 14 | We recommend `doctr `_ a tool that configures 15 | Travis-CI to automatically publish your documentation every time the master 16 | branch is updated. 17 | 18 | .. warning:: 19 | The repo you want to build the docs for has to be a root repo. 20 | You cannot build docs for a forked repo by doctr yet. 21 | The doctr team is working on enabling a forked repo build under 22 | `PR #343 `_. 23 | 24 | Install doctr. 25 | 26 | .. code-block:: bash 27 | 28 | python3 -m pip install --upgrade doctr 29 | 30 | Just type ``doctr configure`` and follow the prompts. Example: 31 | 32 | .. code-block:: bash 33 | 34 | $ doctr configure 35 | Welcome to Doctr. 36 | 37 | We need to ask you a few questions to get you on your way to automatically 38 | deploying from Travis CI to GitHub pages. 39 | 40 | What is your GitHub username? YOUR_GITHUB_USERNAME 41 | Enter the GitHub password for YOUR_GITHUB_USERNAME: 42 | A two-factor authentication code is required: app 43 | Authentication code: XXXXXX 44 | What repo do you want to build the docs for (org/reponame, like 'drdoctr/doctr')? YOUR_GITHUB_USERNAME/YOUR_REPO_NAME 45 | What repo do you want to deploy the docs to? [YOUR_GITHUB_USERNAME/YOUR_REPO_NAME] 46 | 47 | The deploy key has been added for YOUR_GITHUB_USERNAME/YOUR_REPO_NAME. 48 | 49 | You can go to https://github.com/NSLS-II/NSLS-II.github.io/settings/keys to revoke the deploy key. 50 | 51 | Then doctr shows some instructions, which we will take one at a time, 52 | elaborating here. 53 | 54 | .. code-block:: bash 55 | 56 | ================== You should now do the following ================== 57 | 58 | 1. Add the file github_deploy_key_your_github_username_your_repo_name.enc to be staged for commit: 59 | 60 | git add github_deploy_key_your_github_username_your_repo_name.enc 61 | 62 | Remember that you can always use ``git status`` to list uncommitted files like 63 | this one. 64 | 65 | .. code-block:: bash 66 | 67 | 2. Add these lines to your `.travis.yml` file: 68 | 69 | env: 70 | global: 71 | # Doctr deploy key for YOUR_GITHUB_USERNAME/YOUR_REPO_NAME 72 | - secure: "" 73 | 74 | script: 75 | - set -e 76 | - 77 | - pip install doctr 78 | - doctr deploy --built-docs 79 | 80 | Replace the text in with the relevant 81 | things for your repository. 82 | 83 | Note: the `set -e` prevents doctr from running when the docs build fails. 84 | We put this code under `script:` so that if doctr fails it causes the 85 | build to fail. 86 | 87 | This output includes an encrypted token --- the string next to ``secure:`` 88 | which I have redacted in the example above --- that gives Travis-CI permission 89 | to upload to GitHub Pages. 90 | 91 | Putting this together with the default ``.travis.yml`` file generated by the 92 | cookiecutter template, you'll have something like: 93 | 94 | .. literalinclude:: example_travis_with_doctr.yml 95 | :emphasize-lines: 11-14,27-30 96 | 97 | where ```` is replaced by the output you got from ``doctr configure`` 98 | above. 99 | 100 | .. code-block:: bash 101 | 102 | 3. Commit and push these changes to your GitHub repository. 103 | The docs should now build automatically on Travis. 104 | 105 | See the documentation at https://drdoctr.github.io/ for more information. 106 | 107 | Once the changes are pushed to the ``master`` branch on GitHub, Travis-CI will 108 | publish the documentation to 109 | ``https://.github.io/``. It may take a 110 | minute for the updates to process. 111 | 112 | Alternatives 113 | ------------ 114 | 115 | Another popular option for publishing documentation is 116 | `https://readthedocs.org/ `_ . We slightly prefer 117 | using Travis-CI + GitHub Pages because it is easier to debug any installation 118 | issues. It is also more efficient: we have to build the documentation on 119 | Travis-CI anyway to verify that any changes haven't broken them, so we might as 120 | well upload the result and be done, rather than having readthedocs build them 121 | again. 122 | -------------------------------------------------------------------------------- /docs/source/including-data-files.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Including Data Files 3 | ==================== 4 | 5 | In this section you will: 6 | 7 | * Understand the importance of keeping large files out of your package. 8 | * Learn some alternative approaches. 9 | * Learn how to include small data files in your package. 10 | 11 | Consider Alternatives 12 | --------------------- 13 | 14 | **Never include large binary files in your Python package or git repository.** 15 | Once committed, the file lives in git history forever. Git will become 16 | sluggish, because it is not designed to operate on large binary files, and your 17 | package will become an annoyingly large download. 18 | 19 | Removing accidentally-committed files after the fact is *possible* but 20 | destructive, so it's important to avoid committing large files in the first 21 | place. 22 | 23 | Alternatives: 24 | 25 | * Can you generate the file using code instead? This is a good approach for 26 | test data: generate the test data files as part of the test. Of course it's 27 | important to test against *real* data from time to time, but for automated 28 | tests, simulated data is just fine. If you don't understand your data well 29 | enough to simulate it accurately, you don't know enough to write useful tests 30 | against it. 31 | * Can you write a Python function that fetches the data on demand from some 32 | public URL? This is the approach used by projects such as scikit-learn that 33 | need to download large datasets for their examples and tests. 34 | 35 | If you use one these alternatives, add the names of the generated or downloaded 36 | files to the project's ``.gitignore`` file, which was provided by the 37 | cookiecutter template. This helps protect you against accidentally committing 38 | the file to git. 39 | 40 | If the file in question is a text file and not very large (< 100 kB) than it's 41 | reasonable to just bundle it with the package. 42 | 43 | How to Package Data Files 44 | ------------------------- 45 | 46 | What's the problem we are solving here? If your Python program needs to access 47 | a data file, the naïve solution is just to hard-code the path to that file. 48 | 49 | .. code-block:: python 50 | 51 | data_file = open('peak_spacings/LaB6.txt') 52 | 53 | But this is not a good solution because: 54 | 55 | * The data file won't be included in the distribution: users who ``pip 56 | install`` your package will find it's missing! 57 | * The path to the data file depends on the platform and on how the package is 58 | installed. We need Python to handle those details for us. 59 | 60 | As an example, suppose we have text files with Bragg peak spacings of various 61 | crystalline structures, and we want to use these files in our Python package. 62 | Let's put them in a new directory named ``peak_spacings/``. 63 | 64 | .. code-block:: text 65 | 66 | # peak_spacings/LaB6.txt 67 | 68 | 4.15772 69 | 2.94676 70 | 2.40116 71 | 72 | .. code-block:: text 73 | 74 | # peak_spacings/Si.txt 75 | 76 | 3.13556044 77 | 1.92013079 78 | 1.63749304 79 | 1.04518681 80 | 81 | To access these files from the Python package, you need to edit the code in 82 | three places: 83 | 84 | #. Include the data files' paths to ``setup.py`` to make them accessible from 85 | the package. 86 | 87 | .. code-block:: python 88 | 89 | # setup.py (excerpt) 90 | 91 | package_data={ 92 | 'YOUR_PACKAGE_NAME': [ 93 | # When adding files here, remember to update MANIFEST.in as well, 94 | # or else they will not be included in the distribution on PyPI! 95 | 'peak_spacings/*.txt', 96 | ] 97 | }, 98 | 99 | We have used the wildcard ``*`` to capture *all* filenames that end in 100 | ``.txt``. We could alternatively have listed the specific filenames. 101 | 102 | #. Add the data files' paths to ``MANIFEST.in`` to include them in the source 103 | distribution. By default the distribution omits extraneous files that are 104 | not ``.py`` files, so we need to specifically include them. 105 | 106 | .. code-block:: text 107 | 108 | # MANIFEST.in (excerpt) 109 | 110 | include peak_spacings/*.txt 111 | 112 | #. Finally, wherever we actually use the files in our scientific code, we can 113 | access them like this. 114 | 115 | .. code-block:: python 116 | 117 | from pkg_resources import resource_filename 118 | 119 | 120 | filename = resource_filename('peak_spacings/LaB6.txt') 121 | 122 | # `filename` is the specific path to this file in this installation. 123 | # We can now, for example, read the file. 124 | with open(filename) as f: 125 | # Read in each line and convert the string to a number. 126 | spacings = [float(line) for line in f.read().splitlines()] 127 | -------------------------------------------------------------------------------- /docs/source/advanced-testing.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Common Patterns for Tests 3 | ========================= 4 | 5 | In this section you will learn some useful features of pytest that can make 6 | your tests succinct and easy to maintain. 7 | 8 | Parametrized Tests 9 | ------------------ 10 | 11 | Tests that apply the same general test logic to a collection of different 12 | parameters can use parametrized tests. For example, this: 13 | 14 | .. code-block:: python 15 | 16 | import numpy as np 17 | from ..refraction import snell 18 | 19 | 20 | def test_perpendicular(): 21 | # For any indexes, a ray normal to the surface should not bend. 22 | # We'll try a couple different combinations of indexes.... 23 | 24 | actual = snell(0, 2.00, 3.00) 25 | expected = 0 26 | assert actual == expected 27 | 28 | actual = snell(0, 3.00, 2.00) 29 | expected = 0 30 | assert actual == expected 31 | 32 | can be rewritten as: 33 | 34 | .. code-block:: python 35 | 36 | import numpy as np 37 | import pytest 38 | from ..refraction import snell 39 | 40 | 41 | @pytest.mark.parametrize('n1, n2', 42 | [(2.00, 3.00), 43 | (3.00, 2.00), 44 | ]) 45 | def test_perpendicular(n1, n2): 46 | # For any indexes, a ray normal to the surface should not bend. 47 | # We'll try a couple different combinations of indexes.... 48 | 49 | actual = snell(0, n1, n2) 50 | expected = 0 51 | assert actual == expected 52 | 53 | The string ``'n1, n2'`` specifies which parameters this decorator will fill in. 54 | Pytest will run ``test_perpendicular`` twice, one for each entry in the 55 | list ``[(2.00, 3.00), (3.00, 2.00)]``, passing in the respective values ``n1`` 56 | and ``n2`` as arguments. 57 | 58 | From here we refer you to the 59 | `pytest parametrize documentation `_. 60 | 61 | Fixtures 62 | -------- 63 | 64 | Tests that have different logic but share the same setup code can use pytest 65 | fixtures. For example, this: 66 | 67 | .. code-block:: python 68 | 69 | import numpy as np 70 | 71 | 72 | def test_height(): 73 | # Construct a 1-dimensional Gaussian peak. 74 | x = np.linspace(-10, 10, num=21) 75 | sigma = 3.0 76 | peak = np.exp(-(x / sigma)**2 / 2) / (sigma * np.sqrt(2 * np.pi)) 77 | expected = 1 / (sigma * np.sqrt(2 * np.pi)) 78 | # Test that the peak height is correct. 79 | actual = np.max(peak) 80 | assert np.allclose(actual, expected) 81 | 82 | 83 | def test_nonnegative(): 84 | # Construct a 1-dimensional Gaussian peak. 85 | x = np.linspace(-10, 10, num=20) 86 | sigma = 3.0 87 | peak = np.exp(-(x / sigma)**2 / 2) / (sigma * np.sqrt(2 * np.pi)) 88 | # Test that there are no negative values. 89 | assert np.all(peak >= 0) 90 | 91 | can be written as: 92 | 93 | .. code-block:: python 94 | 95 | import pytest 96 | import numpy as np 97 | 98 | 99 | @pytest.fixture 100 | def peak(): 101 | # Construct a 1-dimensional Gaussian peak. 102 | x = np.linspace(-10, 10, num=21) 103 | sigma = 3.0 104 | peak = np.exp(-(x / sigma)**2 / 2) / (sigma * np.sqrt(2 * np.pi)) 105 | return peak 106 | 107 | 108 | def test_height(peak): 109 | expected = 1 / (sigma * np.sqrt(2 * np.pi)) 110 | # Test that the peak height is correct. 111 | actual = np.max(peak) 112 | assert np.allclose(actual, expected) 113 | 114 | 115 | def test_nonnegative(peak): 116 | # Test that there are no negative values. 117 | assert np.all(peak >= 0) 118 | 119 | To reuse a fixture in multiple files, add it to ``conftest.py`` located in the 120 | ``tests/`` directory. It will automatically be imported by pytest into each 121 | test module. 122 | 123 | From here we refer you to the 124 | `pytest fixtures documentation `_. 125 | 126 | Skipping Tests 127 | -------------- 128 | 129 | Sometimes it is useful to skip specific tests under certain conditions. 130 | Examples: 131 | 132 | .. code-block:: python 133 | 134 | import pytest 135 | import sys 136 | 137 | 138 | @pytest.mark.skipif(sys.version_info < (3, 7), 139 | reason="requires python3.7 or higher") 140 | def test_something(): 141 | ... 142 | 143 | 144 | @pytest.mark.skipif(sys.platform == 'win32', 145 | reason="does not run on windows") 146 | def test_something_that_does_not_work_on_windows(): 147 | ... 148 | 149 | 150 | def test_something_that_needs_a_special_dependency(): 151 | some_library = pytest.importorskip("some_library") 152 | ... 153 | 154 | From here we refer you to the 155 | `pytest skipping documentation `_. 156 | -------------------------------------------------------------------------------- /docs/source/publishing-releases.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Publishing Releases 3 | =================== 4 | 5 | In this section you will: 6 | 7 | * Tag a release of your Python package. 8 | * Upload it to `PyPI `_ (Python Package Index) so that 9 | users can download and install it using pip. 10 | 11 | We strongly encourage you to share your code GitHub from the start, which is 12 | why we covered it in :doc:`preliminaries`. People often overestimate the risks 13 | and underestimate the benefits of making their research code public, and the 14 | idea of waiting to make it public "until it's cleaned up" is a punchline, an 15 | exercise in infinite regress. But *releases* are little different: you should 16 | wait to publish a release until your package is usable and tested. 17 | 18 | #. Choose a version number. The convention followed by most scientific Python 19 | packages is ``vMAJOR.MINOR.MICRO``, as in ``v1.3.0``. A good number to start 20 | with is ``v0.1.0``. 21 | 22 | These numbers have meanings. 23 | The goal is to communicate to the user whether upgrading will break anything 24 | in *their* code that will need to be updated in turn. This is 25 | `semantic versioning `_. 26 | 27 | * Incrementing the ``MICRO`` number (``v1.3.0`` -> ``v1.3.1``) means, "I 28 | have fixed some bugs, but I have not added any major new features or 29 | changed any names. All of your code should still work without changes." 30 | * Incrementing the ``MINOR`` number (``v1.3.0`` -> ``v1.4.0``) means, "I 31 | have added some new features and changed some minor things. Your code 32 | should work with perhaps some small updates." 33 | * Incrementing the ``MAJOR`` number (``v1.3.0`` -> ``v2.0.0``) means, "I 34 | have made major changes that will probably require you to update your 35 | code." 36 | 37 | Additionally, if the ``MAJOR`` version is ``0``, the project is considered 38 | to be in early development and may make major breaking changes in minor 39 | releases. 40 | 41 | Obviously this is an imprecise system. Think of it a highly-compressed, 42 | lossy representation of how painful it will be for the user to upgrade. 43 | 44 | #. Update ``docs/source/release-history.rst`` in the documentation if you have 45 | not done so already. (See :doc:`writing-docs`.) For the first tagged 46 | release, you don't need to write much --- some projects just write "Initial 47 | release" under the heading with the version and release date. But for every 48 | subsequent release, you should list any alterations that could require users 49 | of your Python package to change their code. You may also highlight any 50 | additions, improvements, and bug fixes. As examples, see 51 | `the release notes for this small project `_ 52 | and 53 | `this large project `_. 54 | 55 | #. Type ``git status`` and check that you are on the ``master`` branch with no 56 | uncommitted code. 57 | 58 | #. Mark the release with an empty commit, just to leave a marker. This is 59 | optional, but it makes it easier to find the release when skimming through 60 | the git history. 61 | 62 | .. code-block:: bash 63 | 64 | git commit --allow-empty -m "REL: vX.Y.Z" 65 | 66 | #. Tag the commit. 67 | 68 | .. code-block:: bash 69 | 70 | git tag -a vX.Y.Z # Don't forget the leading v 71 | 72 | This will create a tag named ``vX.Y.Z``. The ``-a`` flag (strongly 73 | recommended) opens up a text editor where you should enter a brief 74 | description of the release, such as "This releases fixes some bugs but does 75 | not introduce any breaking changes. All users are encouraged to upgrade." 76 | 77 | #. Verify that the ``__version__`` attribute is correctly updated. 78 | 79 | The version is reported in three places: 80 | 81 | 1. The git tag 82 | 2. The ``setup(version=...)`` parameter in the ``setup.py`` file 83 | 3. Your package's ``__version__`` attribute, in Python 84 | 85 | `Versioneer `_, which was 86 | included and configured for you by the cookiecutter template, automatically 87 | keeps these three in sync. Just to be sure that it worked properly, start up 88 | Python, import the module, and check the ``__version__``. It should have 89 | automatically updated to match the tag. The leading ``v`` is not included. 90 | 91 | .. code-block:: python 92 | 93 | import your_package 94 | your_package.__version__ # should be 'X.Y.Z' 95 | 96 | Incidentally, once you resume development and add the first commit after 97 | this tag, ``__version__`` will take on a value like ``X.Y.Z+1.g58ad5f7``, 98 | where ``+1`` means "1 commit past version X.Y.Z" and ``58ad5f7`` is the 99 | first 7 characters of the hash of the current commit. The letter ``g`` 100 | stands for "git". This is all managed automatically by versioneer and in 101 | accordance with the specification in 102 | `PEP 440 `_. 103 | 104 | #. Push the new commit and the tag to ``master``. 105 | 106 | .. code-block:: bash 107 | 108 | git push origin master 109 | git push origin vX.Y.Z 110 | 111 | .. note:: 112 | 113 | Check your remotes using ``git remote -v``. If your respoitory is 114 | stored in an organization account, you may need to push to ``upstream`` 115 | as well as ``origin``. 116 | 117 | #. `Register for a PyPI account `_. 118 | 119 | #. Install wheel, a tool for producing `built distributions `_ for PyPI. 120 | 121 | .. code-block:: bash 122 | 123 | python3 -m pip install --upgrade wheel 124 | 125 | #. Remove any extraneous files. If you happen to have any important files in 126 | your project directory that are not committed to git, move them first; this 127 | will delete them! 128 | 129 | .. code-block:: bash 130 | 131 | git clean -dfx 132 | 133 | #. Publish a release on PyPI. Note that you might need to configure 134 | your ``~/.pypirc`` with a login token. See `the packaging documentation `_ for more details. 135 | 136 | .. code-block:: bash 137 | 138 | python3 setup.py sdist 139 | python3 setup.py bdist_wheel 140 | twine upload dist/* 141 | 142 | The package is now installable with pip. It may take a couple minutes to become 143 | available. 144 | 145 | If you would also like to make your package available via conda, we recommend 146 | conda-forge, a community-led collection of recipes and build infrastructure. 147 | See in particular 148 | `the section of the conda-forge documentation on adding a recipe `_. 149 | 150 | #. Finally, if you generally work with an "editable" installation of the 151 | package on your machine, as we suggested in :doc:`preliminaries`, you'll 152 | need to reinstall because running ``git clean -dfx`` above will have wiped 153 | out your installation. 154 | 155 | .. code-block:: bash 156 | 157 | pip install -e . 158 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Scientific Python Cookiecutter documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Jun 28 12:35:56 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('vendor')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.autosummary', 37 | 'sphinx.ext.githubpages', 38 | 'sphinx.ext.intersphinx', 39 | 'sphinx.ext.mathjax', 40 | 'sphinx.ext.viewcode', 41 | 'youtube', 42 | 'IPython.sphinxext.ipython_directive', 43 | 'IPython.sphinxext.ipython_console_highlighting', 44 | 'matplotlib.sphinxext.plot_directive', 45 | 'numpydoc', 46 | 'sphinx_copybutton', 47 | ] 48 | 49 | # Configuration options for plot_directive. See: 50 | # https://github.com/matplotlib/matplotlib/blob/f3ed922d935751e08494e5fb5311d3050a3b637b/lib/matplotlib/sphinxext/plot_directive.py#L81 51 | plot_html_show_source_link = False 52 | plot_html_show_formats = False 53 | 54 | # Generate the API documentation when building 55 | autosummary_generate = True 56 | numpydoc_show_class_members = False 57 | 58 | # Add any paths that contain templates here, relative to this directory. 59 | templates_path = ['_templates'] 60 | 61 | # The suffix(es) of source filenames. 62 | # You can specify multiple suffix as a list of string: 63 | # 64 | # source_suffix = ['.rst', '.md'] 65 | source_suffix = '.rst' 66 | 67 | # The master toctree document. 68 | master_doc = 'index' 69 | 70 | # General information about the project. 71 | project = 'Scientific Python Cookiecutter' 72 | copyright = '2018, Contributors' 73 | author = 'Contributors' 74 | 75 | # The version info for the project you're documenting, acts as replacement for 76 | # |version| and |release|, also used in various other places throughout the 77 | # built documents. 78 | # 79 | # The short X.Y version. 80 | version = '0.1' 81 | # The full version, including alpha/beta/rc tags. 82 | release = '0.1' 83 | 84 | # The language for content autogenerated by Sphinx. Refer to documentation 85 | # for a list of supported languages. 86 | # 87 | # This is also used if you do content translation via gettext catalogs. 88 | # Usually you set "language" from the command line for these cases. 89 | language = 'en' 90 | 91 | # List of patterns, relative to source directory, that match files and 92 | # directories to ignore when looking for source files. 93 | # This patterns also effect to html_static_path and html_extra_path 94 | exclude_patterns = [] 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # If true, `todo` and `todoList` produce output, else they produce nothing. 100 | todo_include_todos = False 101 | 102 | 103 | # -- Options for HTML output ---------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. See the documentation for 106 | # a list of builtin themes. 107 | # 108 | html_theme = 'sphinx_rtd_theme' 109 | import sphinx_rtd_theme 110 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 111 | 112 | # Theme options are theme-specific and customize the look and feel of a theme 113 | # further. For a list of options available for each theme, see the 114 | # documentation. 115 | # 116 | # html_theme_options = {} 117 | 118 | # Add any paths that contain custom static files (such as style sheets) here, 119 | # relative to this directory. They are copied after the builtin static files, 120 | # so a file named "default.css" will overwrite the builtin "default.css". 121 | html_static_path = ['_static'] 122 | 123 | # Custom sidebar templates, must be a dictionary that maps document names 124 | # to template names. 125 | # 126 | # This is required for the alabaster theme 127 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 128 | html_sidebars = { 129 | '**': [ 130 | 'relations.html', # needs 'show_related': True theme option to display 131 | 'searchbox.html', 132 | ] 133 | } 134 | 135 | 136 | # -- Options for HTMLHelp output ------------------------------------------ 137 | 138 | # Output file base name for HTML help builder. 139 | htmlhelp_basename = 'ScientificPythonCookiecutterdoc' 140 | 141 | 142 | # -- Options for LaTeX output --------------------------------------------- 143 | 144 | latex_elements = { 145 | # The paper size ('letterpaper' or 'a4paper'). 146 | # 147 | # 'papersize': 'letterpaper', 148 | 149 | # The font size ('10pt', '11pt' or '12pt'). 150 | # 151 | # 'pointsize': '10pt', 152 | 153 | # Additional stuff for the LaTeX preamble. 154 | # 155 | # 'preamble': '', 156 | 157 | # Latex figure (float) alignment 158 | # 159 | # 'figure_align': 'htbp', 160 | } 161 | 162 | # Grouping the document tree into LaTeX files. List of tuples 163 | # (source start file, target name, title, 164 | # author, documentclass [howto, manual, or own class]). 165 | latex_documents = [ 166 | (master_doc, 'ScientificPythonCookiecutter.tex', 'Scientific Python Cookiecutter Documentation', 167 | 'Contributors', 'manual'), 168 | ] 169 | 170 | 171 | # -- Options for manual page output --------------------------------------- 172 | 173 | # One entry per manual page. List of tuples 174 | # (source start file, name, description, authors, manual section). 175 | man_pages = [ 176 | (master_doc, 'scientificpythoncookiecutter', 'Scientific Python Cookiecutter Documentation', 177 | [author], 1) 178 | ] 179 | 180 | 181 | # -- Options for Texinfo output ------------------------------------------- 182 | 183 | # Grouping the document tree into Texinfo files. List of tuples 184 | # (source start file, target name, title, author, 185 | # dir menu entry, description, category) 186 | texinfo_documents = [ 187 | (master_doc, 'ScientificPythonCookiecutter', 'Scientific Python Cookiecutter Documentation', 188 | author, 'ScientificPythonCookiecutter', 'One line description of project.', 189 | 'Miscellaneous'), 190 | ] 191 | 192 | 193 | 194 | 195 | # Example configuration for intersphinx: refer to the Python standard library. 196 | intersphinx_mapping = { 197 | 'python': ('https://docs.python.org/3/', None), 198 | 'numpy': ('https://docs.scipy.org/doc/numpy/', None), 199 | 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), 200 | 'pandas': ('https://pandas.pydata.org/pandas-docs/stable', None), 201 | 'matplotlib': ('https://matplotlib.org', None), 202 | } 203 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # {{ cookiecutter.project_name }} documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Jun 28 12:35:56 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.autosummary", 37 | "sphinx.ext.githubpages", 38 | "sphinx.ext.intersphinx", 39 | "sphinx.ext.mathjax", 40 | "sphinx.ext.viewcode", 41 | "IPython.sphinxext.ipython_directive", 42 | "IPython.sphinxext.ipython_console_highlighting", 43 | "matplotlib.sphinxext.plot_directive", 44 | "numpydoc", 45 | "sphinx_copybutton", 46 | ] 47 | 48 | # Configuration options for plot_directive. See: 49 | # https://github.com/matplotlib/matplotlib/blob/f3ed922d935751e08494e5fb5311d3050a3b637b/lib/matplotlib/sphinxext/plot_directive.py#L81 50 | plot_html_show_source_link = False 51 | plot_html_show_formats = False 52 | 53 | # Generate the API documentation when building 54 | autosummary_generate = True 55 | numpydoc_show_class_members = False 56 | 57 | # Add any paths that contain templates here, relative to this directory. 58 | templates_path = ["_templates"] 59 | 60 | # The suffix(es) of source filenames. 61 | # You can specify multiple suffix as a list of string: 62 | # 63 | # source_suffix = ['.rst', '.md'] 64 | source_suffix = ".rst" 65 | 66 | # The master toctree document. 67 | master_doc = "index" 68 | 69 | # General information about the project. 70 | project = "{{ cookiecutter.project_name }}" 71 | copyright = "{% now 'utc', '%Y' %}, {{ cookiecutter.full_name }}" 72 | author = "{{ cookiecutter.full_name }}" 73 | 74 | # The version info for the project you're documenting, acts as replacement for 75 | # |version| and |release|, also used in various other places throughout the 76 | # built documents. 77 | # 78 | import {{ cookiecutter.package_dir_name }} 79 | 80 | # The short X.Y version. 81 | version = {{ cookiecutter.package_dir_name }}.__version__ 82 | # The full version, including alpha/beta/rc tags. 83 | release = {{ cookiecutter.package_dir_name }}.__version__ 84 | 85 | # The language for content autogenerated by Sphinx. Refer to documentation 86 | # for a list of supported languages. 87 | # 88 | # This is also used if you do content translation via gettext catalogs. 89 | # Usually you set "language" from the command line for these cases. 90 | language = "en" 91 | 92 | # List of patterns, relative to source directory, that match files and 93 | # directories to ignore when looking for source files. 94 | # This patterns also effect to html_static_path and html_extra_path 95 | exclude_patterns = [] 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = "sphinx" 99 | 100 | # If true, `todo` and `todoList` produce output, else they produce nothing. 101 | todo_include_todos = False 102 | 103 | 104 | # -- Options for HTML output ---------------------------------------------- 105 | 106 | # The theme to use for HTML and HTML Help pages. See the documentation for 107 | # a list of builtin themes. 108 | # 109 | html_theme = "sphinx_rtd_theme" 110 | import sphinx_rtd_theme 111 | 112 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 113 | 114 | # Theme options are theme-specific and customize the look and feel of a theme 115 | # further. For a list of options available for each theme, see the 116 | # documentation. 117 | # 118 | # html_theme_options = {} 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ["_static"] 124 | 125 | # Custom sidebar templates, must be a dictionary that maps document names 126 | # to template names. 127 | # 128 | # This is required for the alabaster theme 129 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 130 | html_sidebars = { 131 | "**": [ 132 | "relations.html", # needs 'show_related': True theme option to display 133 | "searchbox.html", 134 | ] 135 | } 136 | 137 | 138 | # -- Options for HTMLHelp output ------------------------------------------ 139 | 140 | # Output file base name for HTML help builder. 141 | htmlhelp_basename = "{{ cookiecutter.package_dist_name }}" 142 | 143 | 144 | # -- Options for LaTeX output --------------------------------------------- 145 | 146 | latex_elements = { 147 | # The paper size ('letterpaper' or 'a4paper'). 148 | # 'papersize': 'letterpaper', 149 | # 150 | # The font size ('10pt', '11pt' or '12pt'). 151 | # 'pointsize': '10pt', 152 | # 153 | # Additional stuff for the LaTeX preamble. 154 | # 'preamble': '', 155 | # 156 | # Latex figure (float) alignment 157 | # 'figure_align': 'htbp', 158 | } 159 | 160 | # Grouping the document tree into LaTeX files. List of tuples 161 | # (source start file, target name, title, 162 | # author, documentclass [howto, manual, or own class]). 163 | latex_documents = [ 164 | ( 165 | master_doc, 166 | "{{ cookiecutter.package_dist_name }}.tex", 167 | "{{ cookiecutter.project_name }} Documentation", 168 | "Contributors", 169 | "manual", 170 | ), 171 | ] 172 | 173 | 174 | # -- Options for manual page output --------------------------------------- 175 | 176 | # One entry per manual page. List of tuples 177 | # (source start file, name, description, authors, manual section). 178 | man_pages = [ 179 | ( 180 | master_doc, 181 | "{{ cookiecutter.package_dist_name }}", 182 | "{{ cookiecutter.project_name }} Documentation", 183 | [author], 184 | 1, 185 | ) 186 | ] 187 | 188 | 189 | # -- Options for Texinfo output ------------------------------------------- 190 | 191 | # Grouping the document tree into Texinfo files. List of tuples 192 | # (source start file, target name, title, author, 193 | # dir menu entry, description, category) 194 | texinfo_documents = [ 195 | ( 196 | master_doc, 197 | "{{ cookiecutter.package_dist_name }}", 198 | "{{ cookiecutter.project_name }} Documentation", 199 | author, 200 | "{{ cookiecutter.package_dist_name }}", 201 | "{{ cookiecutter.project_short_description }}", 202 | "Miscellaneous", 203 | ), 204 | ] 205 | 206 | 207 | # Example configuration for intersphinx: refer to the Python standard library. 208 | intersphinx_mapping = { 209 | "python": ("https://docs.python.org/3/", None), 210 | "numpy": ("https://numpy.org/doc/stable/", None), 211 | "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), 212 | "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), 213 | "matplotlib": ("https://matplotlib.org/stable", None), 214 | } 215 | -------------------------------------------------------------------------------- /docs/source/writing-docs.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Writing Documentation 3 | ===================== 4 | 5 | In this section you will: 6 | 7 | * Generate HTML documentation using Sphinx, starting from a working example 8 | provided by the cookiecutter template. 9 | * Edit ``usage.rst`` to add API documentation and narrative documentation. 10 | * Learn how to incorporate code examples, IPython examples, matplotlib plots, 11 | and typeset math. 12 | 13 | Build the docs 14 | -------------- 15 | 16 | Almost all scientific Python projects use the 17 | `Sphinx documentation generator `_. 18 | The cookiecutter template provided a working example with some popular 19 | extensions installed and some sample pages. 20 | 21 | .. code-block:: none 22 | 23 | example/ 24 | (...) 25 | ├── docs 26 | │   ├── Makefile 27 | │   ├── build 28 | │   ├── make.bat 29 | │   └── source 30 | │   ├── _static 31 | │   │   └── .placeholder 32 | │   ├── _templates 33 | │   ├── conf.py 34 | │   ├── index.rst 35 | │   ├── installation.rst 36 | │   ├── release-history.rst 37 | │   └── usage.rst 38 | (...) 39 | 40 | The ``.rst`` files are source code for our documentation. To build HTML pages 41 | from this source, run: 42 | 43 | .. code-block:: bash 44 | 45 | make -C docs html 46 | 47 | You should see some log message ending in ``build succeeded.`` 48 | 49 | This output HTML will be located in ``docs/build/html``. In your Internet 50 | browser, open ``file://.../docs/build/html/index.html``, where ``...`` is the 51 | path to your project directory. If you aren't sure sure where that is, type 52 | ``pwd``. 53 | 54 | Update the docs 55 | --------------- 56 | 57 | The source code for the documentation is located in ``docs/source/``. 58 | Sphinx uses a markup language called ReStructured Text (.rst). We refer you to 59 | `this primer `_ 60 | to learn how to denote headings, links, lists, cross-references, etc. 61 | 62 | Sphinx formatting is sensitive to whitespace and generally quite picky. We 63 | recommend running ``make -C docs html`` often to check that the documentation 64 | builds successfully. Remember to commit your changes to git periodically. 65 | 66 | Good documentation includes both: 67 | 68 | * API (Application Programming Interface) documentation, listing every public 69 | object in the library and its usage 70 | * Narrative documentation interleaving prose and code examples to explain how 71 | and why a library is meant to be used 72 | 73 | API Documentation 74 | ----------------- 75 | 76 | Most the work of writing good API documentation goes into writing good, 77 | accurate docstrings. Sphinx can scrape that content and generate HTML from it. 78 | Again, most scientific Python libraries use the 79 | `numpydoc standard `_, 80 | which looks like this: 81 | 82 | .. literalinclude:: refraction.py 83 | 84 | Autodoc 85 | ^^^^^^^ 86 | 87 | In an rst file, such as ``docs/source/usage.rst``, we can write: 88 | 89 | .. code-block:: rst 90 | 91 | .. autofunction:: example.refraction.snell 92 | 93 | which renders in HTML like so: 94 | 95 | .. autofunction:: example.refraction.snell 96 | :noindex: 97 | 98 | From here we refer you to the 99 | `sphinx autodoc documentation `_. 100 | 101 | Autosummary 102 | ^^^^^^^^^^^ 103 | 104 | If you have many related objects to document, it may be better to display them 105 | in a table. Each row will include the name, the signature (optional), and the 106 | one-line description from the docstring. 107 | 108 | In rst we can write: 109 | 110 | .. code-block:: rst 111 | 112 | .. autosummary:: 113 | :toctree: generated/ 114 | 115 | example.refraction.snell 116 | 117 | which renders in HTML like so: 118 | 119 | .. autosummary:: 120 | :toctree: generated/ 121 | 122 | example.refraction.snell 123 | 124 | It links to the full rendered docstring on a separate page that is 125 | automatically generated. 126 | 127 | From here we refer you to the 128 | `sphinx autosummary documentation `_. 129 | 130 | Narrative Documentation 131 | ----------------------- 132 | 133 | Code Blocks 134 | ^^^^^^^^^^^ 135 | 136 | Code blocks can be interspersed with narrative text like this: 137 | 138 | .. code-block:: rst 139 | 140 | Scientific libraries conventionally use radians. Numpy provides convenience 141 | functions for converting between radians and degrees. 142 | 143 | .. code-block:: python 144 | 145 | import numpy as np 146 | 147 | 148 | np.deg2rad(90) # pi / 2 149 | np.rad2deg(np.pi / 2) # 90.0 150 | 151 | which renders in HTML as: 152 | 153 | Scientific libraries conventionally use radians. Numpy provides convenience 154 | functions for converting between radians and degrees. 155 | 156 | .. code-block:: python 157 | 158 | import numpy as np 159 | 160 | 161 | np.deg2rad(90) # pi / 2 162 | np.rad2deg(np.pi / 2) # 90.0 163 | 164 | To render short code expressions inline, surround them with back-ticks. This: 165 | 166 | .. code-block:: rst 167 | 168 | Try ``snell(0, 1, 1.33)``. 169 | 170 | renders in HTML as: 171 | 172 | 173 | Try ``snell(0, 1, 1.33)``. 174 | 175 | Embedded Scripts 176 | ^^^^^^^^^^^^^^^^ 177 | 178 | For lengthy examples with tens of lines or more, it can be convenient to embed 179 | the content of a .py file rather than writing it directly into the 180 | documentation. 181 | 182 | This can be done using the directive 183 | 184 | .. code-block:: rest 185 | 186 | .. literalinclude:: examples/some_example.py 187 | 188 | where the path is given relative to the current file's path. Thus, relative to 189 | the repository's root directory, the path to this example script would be 190 | ``docs/source/examples/some_example.py``. 191 | 192 | From here we refer you to the 193 | `sphinx code example documentation `_. 194 | 195 | To go beyond embedded scripts to a more richly-featured example gallery that 196 | shows scripts and their outputs, we encourage you to look at 197 | `sphinx-gallery `_. 198 | 199 | IPython Examples 200 | ^^^^^^^^^^^^^^^^ 201 | 202 | IPython's sphinx extension, which is included by the cookiecutter template, 203 | makes it possible to execute example code and capture its output when the 204 | documentation is built. This rst code: 205 | 206 | .. code-block:: rst 207 | 208 | .. ipython:: python 209 | 210 | 1 + 1 211 | 212 | renders in HTML as: 213 | 214 | .. ipython:: python 215 | 216 | 1 + 1 217 | 218 | From here we refer you to the 219 | `IPython sphinx directive documentation `_. 220 | 221 | Plots 222 | ^^^^^ 223 | 224 | Matplotlib's sphinx extension, which is included by the cookiecutter template, 225 | makes it possible to display matplotlib figures in line. This rst code: 226 | 227 | .. code-block:: rst 228 | 229 | .. plot:: 230 | 231 | import matplotlib.pyplot as plt 232 | fig, ax = plt.subplots() 233 | ax.plot([1, 1, 2, 3, 5, 8]) 234 | 235 | renders in HTML as: 236 | 237 | .. plot:: 238 | 239 | import matplotlib.pyplot as plt 240 | fig, ax = plt.subplots() 241 | ax.plot([1, 1, 2, 3, 5, 8]) 242 | 243 | From here we refer you to the 244 | `matplotlib plot directive documentation `_. 245 | 246 | Math (LaTeX) 247 | ^^^^^^^^^^^^ 248 | 249 | Sphinx can render LaTeX typeset math in the browser (using 250 | `MathJax `_). This rst code: 251 | 252 | .. code-block:: rst 253 | 254 | .. math:: 255 | 256 | \int_0^a x\,dx = \frac{1}{2}a^2 257 | 258 | renders in HTML as: 259 | 260 | .. math:: 261 | 262 | \int_0^a x\,dx = \frac{1}{2}a^2 263 | 264 | This notation can also be used in docstrings. For example, we could add 265 | the equation of Snell's Law to the docstring of 266 | :func:`~example.refraction.snell`. 267 | 268 | Math can also be written inline. This rst code: 269 | 270 | .. code-block:: rst 271 | 272 | The value of :math:`\pi` is 3.141592653.... 273 | 274 | renders in HTML as: 275 | 276 | The value of :math:`\pi` is 3.141592653.... 277 | 278 | Referencing Documented Objects 279 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 280 | 281 | You can create links to documented functions like so: 282 | 283 | .. code-block:: rst 284 | 285 | The :func:`example.refraction.snell` function encodes Snell's Law. 286 | 287 | The :func:`example.refraction.snell` function encodes Snell's Law. 288 | 289 | Adding a ``~`` omits the module path from the link text. 290 | 291 | .. code-block:: rst 292 | 293 | The :func:`~example.refraction.snell` function encodes Snell's Law. 294 | 295 | The :func:`~example.refraction.snell` function encodes Snell's Law. 296 | 297 | See `the Sphinx documentation `_ for more. 298 | -------------------------------------------------------------------------------- /docs/source/guiding-design-principles.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Guiding Design Principles 3 | ========================= 4 | 5 | In this section we summarize some guiding principles for designing and 6 | organizing scientific Python code. 7 | 8 | Collaborate 9 | ----------- 10 | 11 | Software developed by several people is preferable to software developed by 12 | one. By adopting the conventions and tooling used by many other scientific 13 | software projects, you are well on your way to making it easy for others to 14 | contribute. Familiarity works in both directions: it will be easier for others 15 | to understand and contribute to your project, and it will be easier for you to 16 | use other popular open-source scientific software projects and modify them to 17 | your purposes. 18 | 19 | Talking through a design and the assumptions in it helps to clarify your 20 | thinking. 21 | 22 | Collaboration takes trust. It is OK to be "wrong"; it is part of the process 23 | of making things better. 24 | 25 | Having more than one person understanding every part of the code prevents 26 | systematic risks for the project and keeps you from being tied to that code. 27 | 28 | If you can bring together contributors with diverse scientific backgrounds, it 29 | becomes easier to identify functionality that should be generalized for reuse 30 | by different fields. 31 | 32 | .. _refactor: 33 | 34 | Don't Be Afraid to Refactor 35 | --------------------------- 36 | 37 | No code is ever right the first (or second) time. 38 | 39 | Refactoring the code once you understand the problem and the design trade-offs 40 | more fully helps keep the code maintainable. Version control, tests, and 41 | linting are your safety net, empowering you to make changes with confidence. 42 | 43 | Prefer "Wide" over "Deep" 44 | ------------------------- 45 | 46 | It should be possible to reuse pieces of software in a way not anticipated by 47 | the original author. That is, branching out from the initial use case should 48 | enable unplanned functionality without a massive increase in complexity. 49 | 50 | When building new things, work your way down to the lowest level, understand 51 | that level, and then build back up. Try to imagine what else you would want to 52 | do with the capability you are implementing for other research groups, for 53 | related scientific applications, and next year. 54 | 55 | Take the time to understand how things need to work at the bottom. It is better 56 | to slowly deploy a robust extensible solution than to quickly deploy a brittle 57 | narrow solution. 58 | 59 | Keep I/O Separate 60 | ----------------- 61 | 62 | One of the biggest impediments to reuse of scientific code is when I/O 63 | code---assuming certain file locations, names, formats, or layouts---is 64 | interspersed with scientific logic. 65 | 66 | I/O-related functions should *only* perform I/O. For example, they should take 67 | in a filepath and return a numpy array, or a dictionary of arrays and metadata. 68 | The valuable scientific logic should be encoded in functions that take in 69 | standard data types and return standard data types. This makes them easier to 70 | test, maintain when data formats change, or reuse for unforeseen applications. 71 | 72 | Duck Typing is a Good Idea 73 | -------------------------- 74 | 75 | `Duck typing `_ treats objects based 76 | on what they can *do*, not based on what type they *are*. "If it walks like a 77 | duck and it quacks like a duck, then it must be a duck." 78 | 79 | Python in general and scientific Python in particular leverage *interfaces* to 80 | support interoperability and reuse. For example, it is possible to pass a 81 | pandas DataFrame to the :func:`numpy.sum` function even though pandas was 82 | created long after :func:`numpy.sum`. This is because :func:`numpy.sum` avoids 83 | assuming it will be passed specific data types; it accepts any object that 84 | provides the right methods (interfaces). Where possible, avoid ``isinstance`` 85 | checks in your code, and try to make your functions work on the broadest 86 | possible range of input types. 87 | 88 | "Stop Writing Classes" 89 | ---------------------- 90 | 91 | Not everything needs to be object-oriented. Object-oriented design frequently 92 | does not add value in scientific computing. 93 | 94 | .. epigraph:: 95 | 96 | It is better to have 100 functions operate on one data structure than 10 97 | functions on 10 data structures. 98 | 99 | -- From ACM's SIGPLAN publication, (September, 1982), Article "Epigrams in 100 | Programming", by Alan J. Perlis of Yale University. 101 | 102 | It is often tempting to invent special objects for a use case or workflow --- 103 | an ``Image`` object or a ``DiffractionAnalysis`` object. This approach has 104 | proven again and again to be difficult to extend and maintain. It is better to 105 | prefer standard, simple data structures like Python dictionaries and numpy 106 | arrays and use simple functions to operate on them. 107 | 108 | A popular talk, "Stop Writing Classes," which you can 109 | `watch on YouTube `_, 110 | illustrates how some situations that *seem* to lend themselves to 111 | object-oriented programming are much more simply handled using plain, built-in 112 | data structures and functions. 113 | 114 | As another example, the widely-used scikit-image library initially experimented 115 | with using an ``Image`` class, but ultimately decided that it was better to use 116 | plain old numpy arrays. All scientific Python libraries understand numpy 117 | arrays, but they don't understand custom classes, so it is better to pass 118 | application-specific metadata *alongside* a standard array than to try to 119 | encapsulate all of that information in a new, bespoke object. 120 | 121 | Permissiveness Isn't Always Convenient 122 | -------------------------------------- 123 | 124 | Overly permissive code can lead to very confusing bugs. If you need a flexible 125 | user-facing interface that tries to "do the right thing" by guessing what the 126 | users wants, separate it into two layers: a thin "friendly" layer on top of a 127 | "cranky" layer that takes in only exactly what it needs and does the actual 128 | work. The cranky layer should be easy to test; it should be constrained about 129 | what it accepts and what it returns. This layered design makes it possible to 130 | write *many* friendly layers with different opinions and different defaults. 131 | 132 | When it doubt, make function arguments required. Optional arguments are harder 133 | to discover and can hide important choices that the user should know that they 134 | are making. 135 | 136 | Exceptions should just be raised: don't catch them and print. Exceptions are a 137 | tool for being clear about what the code needs and letting the caller decide 138 | what to do about it. *Application* code (e.g. GUIs) should catch and handle 139 | errors to avoid crashing, but *library* code should generally raise errors 140 | unless it is sure how the user or the caller wants to handle them. 141 | 142 | Write Useful Error Messages 143 | --------------------------- 144 | 145 | Be specific. Include what the wrong value was, what was wrong with it, and 146 | perhaps how it might be fixed. For example, if the code fails to locate a file 147 | it needs, it should say what it was looking for and where it looked. 148 | 149 | Write for Readability 150 | --------------------- 151 | 152 | Unless you are writing a script that you plan to delete tomorrow or next week, 153 | your code will probably be read many more times than it is written. And today's 154 | "temporary solution" often becomes tomorrow's critical code. Therefore, 155 | optimize for clarity over brevity, using descriptive and consistent names. 156 | 157 | Complexity is Always Conserved 158 | ------------------------------ 159 | 160 | Complexity is always conserved and is strictly greater than the system the code 161 | is modeling. Attempts to hide complexity from the user frequently backfire. 162 | 163 | For example, it is often tempting to hide certain reused keywords in a 164 | function, shortening this: 165 | 166 | .. code-block:: python 167 | 168 | def get_image(filename, normalize=True, beginning=0, end=None): 169 | ... 170 | 171 | into this: 172 | 173 | .. code-block:: python 174 | 175 | def get_image(filename, options={}): 176 | ... 177 | 178 | Although the interface appears to have been simplified through hidden keyword 179 | arguments, now the user needs to remember what the ``options`` are or dig 180 | through documentation to better understand how to use them. 181 | 182 | Because new science occurs when old ideas are reapplied or extended in 183 | unforeseen ways, scientific code should not bury its complexity or overly 184 | optimize for a specific use case. It should expose what complexity there is 185 | straightforwardly. 186 | 187 | .. note:: 188 | 189 | Even better, you should consider using "keyword-only" arguments, introduced 190 | in Python 3, which require the user to pass an argument by keyword rather 191 | than position. 192 | 193 | .. code-block:: python 194 | 195 | get_image(filename, *, normalize=True, beginning=0, end=None): 196 | ... 197 | 198 | Every argument after the ``*`` is keyword-only. Therefore, the usage 199 | ``get_image('thing.png', False)`` will not be allowed; the caller must 200 | explicitly type ``get_image('thing.png', normalize=False)``. The latter is 201 | easier to read, and it enables the author to insert additional parameters 202 | without breaking backward compatibility. 203 | 204 | Similarly, it can be tempting to write one function that performs multiple 205 | steps and has many options instead of multiple functions that do a single step 206 | and have few options. The advantages of "many small functions" reveal 207 | themselves in time: 208 | 209 | * Small functions are easier to explain and document because their behavior is 210 | well-scoped. 211 | * Small functions can be tested individually, and it is easy to see which paths 212 | have and have not yet been tested. 213 | * It is easier to compose a function with other functions and reuse it in an 214 | unanticipated way if its behavior is well-defined and tightly scoped. This is 215 | `the UNIX philosophy `_: 216 | "Do one thing and do it well." 217 | * The number of possible interactions between arguments goes up with the number 218 | of arguments, which makes the function difficult to reason about and test. In 219 | particular, arguments whose meaning depends on other arguments should be 220 | avoided. 221 | 222 | Functions should return the same kind of thing no matter what their arguments, 223 | particularly their optional arguments. Violating "return type stability" puts 224 | a burden on the function's caller, which now must understand the internal 225 | details of the function to know what type to expect for any given input. That 226 | makes the function harder to document, test, and use. Python does not enforce 227 | return type stability, but we should try for it anyway. If you have a function 228 | that returns different types of things depending on its inputs, that is a sign 229 | that it should be :ref:`refactored ` into multiple functions. 230 | 231 | Python is incredibly flexible. It accommodates many possible design choices. 232 | By exercising some restraint and consistency with the scientific Python 233 | ecosystem, Python can be used to build scientific tools that last and grow well 234 | over time. 235 | -------------------------------------------------------------------------------- /docs/source/preliminaries.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Getting Started 3 | =============== 4 | 5 | In this section you will: 6 | 7 | * Sign up for Github. 8 | * Generate a scaffold for your new Python project. 9 | * Upload it to GitHub. 10 | * Install your new Python project for development. 11 | 12 | Then, in the next section, we will begin to move your scientific code into that 13 | template. 14 | 15 | You will need a Terminal (Windows calls it a "Command Prompt") and a plain text 16 | editor. Any will do; we won't assume anything about the editor you are using. 17 | If you are looking for a recommendation for beginners, the `Atom Editor 18 | `_ by GitHub is a good one. For minimalists, ``nano`` on 19 | Linux or OSX and Notebook on Windows will get you up and running. 20 | 21 | Reading the steps that follow, you might reasonably wonder, "Why isn't there 22 | just an automated script for this?" We prefer to do this process manually so 23 | that we are forced to think carefully about each step, notice when something 24 | goes wrong, and debug if necessary. We recommend you do the same. 25 | 26 | #. `Sign up for GitHub `_. 27 | 28 | #. Verify that you have Python 3. 29 | 30 | .. code-block:: bash 31 | 32 | python3 --version 33 | 34 | If necessary, install it by your method of choice: apt, Homebrew, conda, 35 | etc. 36 | 37 | #. Create an *environment*, a sandboxed area for installing software that is 38 | separate from the system defaults. This is not essential, but it is 39 | strongly encouraged. It ensures that your project and its software 40 | dependencies will not interfere with other Python software on your system. 41 | There are several tools for this. But the simplest is Python's built-in 42 | ``venv`` (short for "virtual environments"), illustrated here. 43 | 44 | Do this once: 45 | 46 | .. code-block:: bash 47 | 48 | python3 -m venv my-env 49 | 50 | The term ``my-env`` can be anything. It names the new environment. 51 | 52 | Do this every time you open up a new Terminal / Command Prompt to work on 53 | your project: 54 | 55 | .. code-block:: bash 56 | 57 | . my-env/bin/activate 58 | 59 | .. note:: 60 | 61 | If you are a conda user, you may prefer a conda environment: 62 | 63 | .. code-block:: bash 64 | 65 | conda create -n my-env python=3.7 66 | conda activate my-env 67 | 68 | #. Verify that you have git installed. 69 | 70 | .. code-block:: bash 71 | 72 | git 73 | 74 | If necessary, install it by your method of choice (apt, Homebrew, conda, etc.). 75 | 76 | #. Choose a name for your project. 77 | 78 | Ideal names are descriptive, succinct, and easy to Google. Think about who 79 | else might be interested in using or contributing to your project in the 80 | future, and choose a name that will help that person discover your project. 81 | There is no need to put "py" in the name; usually your user will already 82 | know that this is a Python project. 83 | 84 | Check that the name is not already taken by 85 | `searching for it on the Python Package Index (PyPI) `_. 86 | 87 | #. Install cookiecutter. 88 | 89 | .. code-block:: bash 90 | 91 | python3 -m pip install --upgrade cookiecutter 92 | 93 | #. Generate a new Python project using our cookiecutter template. 94 | 95 | .. code-block:: bash 96 | 97 | cookiecutter https://github.com/NSLS-II/scientific-python-cookiecutter 98 | 99 | 100 | You will see the following the prompts. The default suggestion is given in square brackets. 101 | 102 | For the last question, ``minimum_supported_python_version``, we recommend 103 | supporting back to Python 3.6 unless you have a need for newer Python 104 | features. 105 | 106 | .. code-block:: bash 107 | 108 | full_name [Name or Organization]: Brookhaven National Lab 109 | email []: dallan@bnl.gov 110 | github_username []: danielballan 111 | project_name [Your Project Name]: Example 112 | package_dist_name [example]: 113 | package_dir_name [example]: 114 | repo_name [example]: 115 | project_short_description [Python package for doing science.]: Example package for docs. 116 | year [2018]: 117 | Select minimum_supported_python_version: 118 | 1 - Python 3.6 119 | 2 - Python 3.7 120 | 3 - Python 3.8 121 | Choose from 1, 2, 3 [1]: 122 | 123 | This generates a new directory, ``example`` in this case, with all the 124 | "scaffolding" of a working Python project. 125 | 126 | .. code-block:: bash 127 | 128 | $ ls example/ 129 | AUTHORS.rst MANIFEST.in example setup.cfg 130 | CONTRIBUTING.rst README.rst requirements-dev.txt setup.py 131 | LICENSE docs requirements.txt versioneer.py 132 | 133 | .. note:: 134 | 135 | Cookiecutter prompted us for several variations of *name*. 136 | If are you wondering what differentiates all these names, here's a primer: 137 | 138 | * ``project_name`` -- Human-friendly title. Case sensitive. Spaces allowed. 139 | * ``package_dist_name`` -- The name to use when you ``pip install ___``. 140 | Dashes and underscores are allowed. Dashes are conventional. Case 141 | insensitive. 142 | * ``package_dir_name`` --- The name to use when you ``import ___`` in Python. 143 | Underscores are the only punctuation allowed. Conventionally lowercase. 144 | * ``repo_name`` --- The name of the GitHub repository. This will be the 145 | name of the new directory on your filesystem. 146 | 147 | #. Take a moment to see what we have. (Some systems treat files whose name 148 | begins with ``.`` as "hidden files", not shown by default. Use the ``ls -a`` 149 | command in the Terminal to show them.) 150 | 151 | .. The following code-block output was generated using `tree -a example/`. 152 | 153 | .. code-block:: none 154 | 155 | example/ 156 | ├── .flake8 157 | ├── .gitattributes 158 | ├── .gitignore 159 | ├── .travis.yml 160 | ├── AUTHORS.rst 161 | ├── CONTRIBUTING.rst 162 | ├── LICENSE 163 | ├── MANIFEST.in 164 | ├── README.rst 165 | ├── docs 166 | │   ├── Makefile 167 | │   ├── build 168 | │   ├── make.bat 169 | │   └── source 170 | │   ├── _static 171 | │   │   └── .placeholder 172 | │   ├── _templates 173 | │   ├── conf.py 174 | │   ├── index.rst 175 | │   ├── installation.rst 176 | │   ├── release-history.rst 177 | │   └── usage.rst 178 | ├── example 179 | │   ├── __init__.py 180 | │   ├── _version.py 181 | │   └── tests 182 | │   └── test_examples.py 183 | ├── requirements-dev.txt 184 | ├── requirements.txt 185 | ├── setup.cfg 186 | ├── setup.py 187 | └── versioneer.py 188 | 189 | In this top ``example/`` directory, we have files specifying metadata about 190 | the Python package (e.g. ``LICENSE``) and configuration files related to 191 | tools we will cover in later sections. We are mostly concerned with the 192 | ``example/example/`` subdirectory, which is the Python package itself. This 193 | is where we'll put the scientific code. But first, we should version-control 194 | our project using git. 195 | 196 | #. Change directories into your new project. 197 | 198 | .. code-block:: bash 199 | 200 | cd example 201 | 202 | We are now in the top-level ``example/`` directory---not ``example/example``! 203 | 204 | #. Make the directory a git repository. 205 | 206 | .. code-block:: bash 207 | 208 | $ git init 209 | Initialized empty Git repository in (...) 210 | 211 | #. Make the first "commit". If we break anything in later steps, we can always 212 | roll back to this clean initial state. 213 | 214 | .. code-block:: bash 215 | 216 | $ git add . 217 | $ git commit -m "Initial commit." 218 | 219 | #. `Create a new repository on GitHub `_, 220 | naming it with the ``repo_name`` from your cookiecutter input above. 221 | 222 | .. important:: 223 | 224 | Do **not** check "Initialize this repository with a README". 225 | 226 | #. Configure your local repository to know about the remote repository on 227 | GitHub... 228 | 229 | .. code-block:: bash 230 | 231 | $ git remote add origin https://github.com/YOUR_GITHUB_USER_NAME/YOUR_REPOSITORY_NAME. 232 | 233 | ... and upload the code. 234 | 235 | .. code-block:: bash 236 | 237 | $ git push -u origin master 238 | Counting objects: 42, done. 239 | Delta compression using up to 4 threads. 240 | Compressing objects: 100% (40/40), done. 241 | Writing objects: 100% (42/42), 29.63 KiB | 0 bytes/s, done. 242 | Total 42 (delta 4), reused 0 (delta 0) 243 | remote: Resolving deltas: 100% (4/4), done. 244 | To github.com:YOUR_GITHUB_USER_NAME/YOUR_REPO_NAME.git 245 | * [new branch] master -> master 246 | Branch master set up to track remote branch master from origin. 247 | 248 | 249 | .. note:: 250 | 251 | If this repository is to belong to a GitHub *organization* (e.g. 252 | http://github.com/NSLS-II) as opposed to a personal user account 253 | (e.g. http://github.com/danielballan) it is conventional to name the 254 | organization remote ``upstream`` instead of ``origin``. 255 | 256 | .. code-block:: bash 257 | 258 | $ git remote add upstream https://github.com/ORGANIZATION_NAME/YOUR_REPOSITORY_NAME. 259 | $ git push -u upstream master 260 | Counting objects: 42, done. 261 | Delta compression using up to 4 threads. 262 | Compressing objects: 100% (40/40), done. 263 | Writing objects: 100% (42/42), 29.63 KiB | 0 bytes/s, done. 264 | Total 42 (delta 4), reused 0 (delta 0) 265 | remote: Resolving deltas: 100% (4/4), done. 266 | To github.com:ORGANIZATION_NAME/YOUR_REPO_NAME.git 267 | * [new branch] master -> master 268 | Branch master set up to track remote branch master from upstream. 269 | 270 | and, separately, add your personal fork as ``origin``. 271 | 272 | .. code-block:: bash 273 | 274 | $ git remote add origin https://github.com/YOUR_GITHUB_USER_NAME/YOUR_REPOSITORY_NAME. 275 | 276 | #. Now let's install your project for development. 277 | 278 | .. code-block:: python 279 | 280 | python3 -m pip install -e . 281 | 282 | .. note:: 283 | 284 | The ``-e`` stands for "editable". It uses simlinks to link to the actual 285 | files in your repository (rather than copying them, which is what plain 286 | ``pip install .`` would do) so that you do not need to re-install the 287 | package for an edit to take effect. 288 | 289 | This is similar to the behavior of ``python setup.py develop``. If you 290 | have seen that before, we recommend always using ``pip install -e .`` 291 | instead because it avoids certain pitfalls. 292 | 293 | #. Finally, verify that we can import it. 294 | 295 | .. code-block:: bash 296 | 297 | python3 298 | 299 | .. code-block:: python 300 | 301 | >>> import your_package_name 302 | 303 | #. Looking ahead, we'll also need the "development requirements" for our 304 | package. These are third-party Python packages that aren't necessary to 305 | *use* our package, but are necessary to *develop* it (run tests, build the 306 | documentation). The cookiecutter template has listed some defaults in 307 | ``requirements-dev.txt``. Install them now. 308 | 309 | .. code-block:: bash 310 | 311 | python3 -m pip install --upgrade -r requirements-dev.txt 312 | 313 | Now we have a working but empty Python project. In the next section, we'll 314 | start moving your scientific code into the project. 315 | -------------------------------------------------------------------------------- /docs/source/the-code-itself.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | The Code Itself 3 | =============== 4 | 5 | In this section you will: 6 | 7 | * Put some scientific code in your new Python package. 8 | * Update your package's list of dependencies in ``requirements.txt``. 9 | * Write a test and run the test suite. 10 | * Use a "linter" and style-checker. 11 | * Commit your changes to git and sync your changes with GitHub. 12 | 13 | A simple function with inline documentation 14 | ------------------------------------------- 15 | 16 | Let's write a simple function that encodes 17 | `Snell's Law `_ and include it in 18 | our Python package. 19 | 20 | Look again at the directory structure. 21 | 22 | .. code-block:: none 23 | 24 | example/ 25 | ├── .flake8 26 | ├── .gitattributes 27 | ├── .gitignore 28 | ├── .travis.yml 29 | ├── AUTHORS.rst 30 | ├── CONTRIBUTING.rst 31 | ├── LICENSE 32 | ├── MANIFEST.in 33 | ├── README.rst 34 | ├── docs 35 | │   ├── Makefile 36 | │   ├── build 37 | │   ├── make.bat 38 | │   └── source 39 | │   ├── _static 40 | │   │   └── .placeholder 41 | │   ├── _templates 42 | │   ├── conf.py 43 | │   ├── index.rst 44 | │   ├── installation.rst 45 | │   ├── release-history.rst 46 | │   └── usage.rst 47 | ├── example 48 | │   ├── __init__.py 49 | │   ├── _version.py 50 | │   └── tests 51 | │   └── test_examples.py 52 | ├── requirements-dev.txt 53 | ├── requirements.txt 54 | ├── setup.cfg 55 | ├── setup.py 56 | └── versioneer.py 57 | 58 | Our scientific code should go in the ``example/`` subdirectory, next to 59 | ``__init__.py``. Let's make a new file in that directory named 60 | ``refraction.py``, meaning our new layout will be: 61 | 62 | .. code-block:: none 63 | 64 | ├── example 65 | │   ├── __init__.py 66 | │   ├── _version.py 67 | │   ├── refraction.py 68 | │   └── tests 69 | │   └── test_examples.py 70 | 71 | This is our new file. You may follow along exactly or, instead, make a file 72 | with a different name and your own scientific function. 73 | 74 | .. literalinclude:: refraction.py 75 | 76 | Notice that this example includes inline documentation --- a "docstring". This 77 | is extremely useful for collaborators, and the most common collaborator is 78 | Future You! 79 | 80 | Further, by following the 81 | `numpydoc standard `_, 82 | we will be able to automatically generate nice-looking HTML documentation 83 | later. Notable features: 84 | 85 | * There is a succinct, one-line summary of the function's purpose. It must one 86 | line. 87 | * (Optional) There is an paragraph elaborating on that summary. 88 | * There is a section listing input parameters, with the structure 89 | 90 | .. code-block :: none 91 | 92 | parameter_name : parameter_type 93 | optional description 94 | 95 | Note that space before the ``:``. That is part of the standard. 96 | * Similar parameters may be combined into one entry for brevity's sake, as we 97 | have done for ``n1, n2`` here. 98 | * There is a section describing what the function returns. 99 | * (Optional) There is a section of one or more examples. 100 | 101 | We will revisit docstrings in the section on :doc:`writing-docs`. 102 | 103 | Update Requirements 104 | ------------------- 105 | 106 | Notice that our package has a third-party dependency, numpy. We should 107 | update our package's ``requirements.txt``. 108 | 109 | .. code-block:: text 110 | 111 | # requirements.txt 112 | 113 | # List required packages in this file, one per line. 114 | numpy 115 | 116 | Our cookiecutter configured ``setup.py`` to read this file. It will ensure that 117 | numpy is installed when our package is installed. 118 | 119 | We can test it by reinstalling the package. 120 | 121 | .. code-block:: bash 122 | 123 | python3 -m pip install -e . 124 | 125 | Try it 126 | ------ 127 | 128 | Try importing and using the function. 129 | 130 | 131 | .. code-block:: python 132 | 133 | >>> from example.refraction import snell 134 | >>> import numpy as np 135 | >>> snell(np.pi/4, 1.00, 1.33) 136 | 1.2239576240104186 137 | 138 | The docstring can be viewed with :func:`help`. 139 | 140 | .. code-block:: python 141 | 142 | >>> help(snell) 143 | 144 | Or, as a shortcut, use ``?`` in IPython/Jupyter. 145 | 146 | .. ipython:: python 147 | :verbatim: 148 | 149 | snell? 150 | 151 | Run the Tests 152 | ------------- 153 | 154 | You should add a test right away while the details are still fresh in mind. 155 | Writing tests encourages you to write modular, reusable code, which is easier 156 | to test. 157 | 158 | The cookiecutter template included an example test suite with one test: 159 | 160 | .. code-block:: python 161 | 162 | # example/tests/test_examples.py 163 | 164 | def test_one_plus_one_is_two(): 165 | assert 1 + 1 == 2 166 | 167 | Before writing our own test, let's practice running that test to check that 168 | everything is working. 169 | 170 | .. important:: 171 | 172 | We assume you have installed the "development requirements," as covered 173 | in :doc:`preliminaries`. If you are not sure whether you have, there is no 174 | harm in running this a second time: 175 | 176 | .. code-block:: bash 177 | 178 | python3 -m pip install --upgrade -r requirements-dev.txt 179 | 180 | .. code-block:: bash 181 | 182 | python3 -m pytest 183 | 184 | This walks through all the directories and files in our package that start with 185 | the word 'test' and collects all the functions whose name also starts with 186 | ``test``. Currently, there is just one, ``test_one_plus_one_is_two``. 187 | ``pytest`` runs that function. If no exceptions are raised, the test passes. 188 | 189 | The output should look something like this: 190 | 191 | .. code-block:: bash 192 | 193 | ======================================== test session starts ======================================== 194 | platform darwin -- Python 3.6.4, pytest-3.6.2, py-1.5.4, pluggy-0.6.0 195 | benchmark: 3.1.1 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000) 196 | rootdir: /private/tmp/test11/example, inifile: 197 | plugins: xdist-1.22.2, timeout-1.2.1, rerunfailures-4.0, pep8-1.0.6, lazy-fixture-0.3.0, forked-0.2, benchmark-3.1.1 198 | collected 1 item 199 | 200 | example/tests/test_examples.py . [100%] 201 | 202 | ===================================== 1 passed in 0.02 seconds ====================================== 203 | 204 | .. note:: 205 | 206 | The output of ``pytest`` is customizable. Commonly useful command-line 207 | arguments include: 208 | 209 | * ``-v`` verbose 210 | * ``-s`` Do not capture stdout/err per test. 211 | * ``-k EXPRESSION`` Filter tests by pattern-matching test name. 212 | 213 | Consult the `pytest documentation `_ 214 | for more. 215 | 216 | Write a Test 217 | ------------ 218 | 219 | Let's add a test to ``test_examples.py`` that exercises our ``snell`` function. 220 | We can delete ``test_one_plus_one_is_two`` now. 221 | 222 | .. literalinclude:: test_examples.py 223 | 224 | Things to notice: 225 | 226 | * It is sometime useful to put multiple ``assert`` statements in one test. You 227 | should make a separate test for each *behavior* that you are checking. When a 228 | monolithic, multi-step tests fails, it's difficult to figure out why. 229 | * When comparing floating-point numbers (as opposed to integers) you should not 230 | test for exact equality. Use :func:`numpy.allclose`, which checks for 231 | equality within a (configurable) tolerance. Numpy provides several 232 | `testing utilities `_, 233 | which should always be used when testing numpy arrays. 234 | * Remember that the names of all test modules and functions must begin with 235 | ``test`` or they will not be picked up by pytest! 236 | 237 | See :doc:`advanced-testing` for more. 238 | 239 | "Lint": Check for suspicious-looking code 240 | ----------------------------------------- 241 | 242 | A `linter `_ is a tool that 243 | analyzes code to flag potential errors. For example, it can catch variables you 244 | defined by never used, which is likely a spelling error. 245 | 246 | The cookiecutter configured ``flake8`` for this purpose. Flake8 checks for 247 | "lint" and also enforces the standard Python coding style, 248 | `PEP8 `_. Enforcing 249 | consistent style helps projects stay easy to read and maintain as they grow. 250 | While not all projects strictly enfore PEP8, we generally recommend it. 251 | 252 | .. important:: 253 | 254 | We assume you have installed the "development requirements," as covered 255 | in :doc:`preliminaries`. If you are not sure whether you have, there is no 256 | harm in running this a second time: 257 | 258 | .. code-block:: bash 259 | 260 | python3 -m pip install --upgrade -r requirements-dev.txt 261 | 262 | .. code-block:: bash 263 | 264 | python3 -m flake8 265 | 266 | This will list linting or stylistic errors. If there is no output, all is well. 267 | See the `flake8 documentation `_ for more. 268 | 269 | Commit and Push Changes 270 | ----------------------- 271 | 272 | Remember to commit your changes to version control and push them up to GitHub. 273 | 274 | .. important:: 275 | 276 | The following is a quick reference that makes some assumptions about your 277 | local configuration and workflow. 278 | 279 | This usage is part of a workflow named *GitHub flow*. See 280 | `this guide `_ for more. 281 | 282 | Remember that at any time you may use ``git status`` to check which branch 283 | you are currently on and which files have uncommitted changes. Use ``git diff`` 284 | to review the content of those changes. 285 | 286 | 1. If you have not already done so, create a new "feature branch" for this work 287 | with some descriptive name. 288 | 289 | .. code-block:: bash 290 | 291 | git checkout master # Starting from the master branch... 292 | git checkout -b add-snell-function # ...make a new branch. 293 | 294 | 2. Stage changes to be committed. In our example, we have created one new file 295 | and changed an existing one. We ``git add`` both. 296 | 297 | .. code-block:: bash 298 | 299 | git add example/refraction.py 300 | git add example/tests/test_examples.py 301 | 302 | 3. Commit changes. 303 | 304 | .. code-block:: bash 305 | 306 | git commit -m "Add snell function and tests." 307 | 308 | 4. Push changes to remote repository on GitHub. 309 | 310 | .. code-block:: bash 311 | 312 | git push origin add-snell-function 313 | 314 | 5. Repeat steps 2-4 until you are happy with this feature. 315 | 316 | 6. Create a Pull Request --- or merge to master. 317 | 318 | When you are ready for collaborators to review your work and consider merging 319 | the ``add-snell-function`` branch into the ``master`` branch, 320 | `create a pull request `_. 321 | Even if you presently have no collaborators, going through this process is a 322 | useful way to document the history of changes to the project for any *future* 323 | collaborators (and Future You). 324 | 325 | However, if you are in the early stages of just getting a project up and you 326 | are the only developer, you might skip the pull request step and merge the 327 | changes yourself. 328 | 329 | .. code-block:: bash 330 | 331 | git checkout master 332 | # Ensure local master branch is up to date with remote master branch. 333 | git pull --ff-only origin master 334 | # Merge the commits from add-snell-function into master. 335 | git merge add-snell-function 336 | # Update the remote master branch. 337 | git push origin master 338 | 339 | Multiple modules 340 | ---------------- 341 | 342 | We created just one module, ``example.refraction``. We might eventually grow a 343 | second module --- say, ``example.utils``. Some brief advice: 344 | 345 | * When in doubt, resist the temptation to grow deep taxonomies of modules and 346 | sub-packages, lest it become difficult for users and collaborators to 347 | remember where everything is. The Python built-in libraries are generally 348 | flat. 349 | 350 | * When making intra-package imports, we recommend relative imports. 351 | 352 | This works: 353 | 354 | .. code-block:: bash 355 | 356 | # example/refraction.py 357 | 358 | from example import utils 359 | from example.utils import some_function 360 | 361 | but this is equivalent, and preferred: 362 | 363 | .. code-block:: bash 364 | 365 | # example/refraction.py 366 | 367 | from . import utils 368 | from .utils import some_function 369 | 370 | For one thing, if you change the name of the package in the future, you won't 371 | need to update this file. 372 | 373 | * Take care to avoid circular imports, wherein two modules each import the 374 | other. 375 | -------------------------------------------------------------------------------- /{{ cookiecutter.repo_name }}/{{ cookiecutter.package_dir_name }}/_version.py: -------------------------------------------------------------------------------- 1 | # This file helps to compute a version number in source trees obtained from 2 | # git-archive tarball (such as those provided by githubs download-from-tag 3 | # feature). Distribution tarballs (built by setup.py sdist) and build 4 | # directories (produced by setup.py build) will contain a much shorter file 5 | # that just contains the computed version number. 6 | 7 | # This file is released into the public domain. Generated by 8 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 9 | 10 | """Git implementation of _version.py.""" 11 | 12 | import errno 13 | import os 14 | import re 15 | import subprocess 16 | import sys 17 | 18 | 19 | def get_keywords(): 20 | """Get the keywords needed to look up the version information.""" 21 | # these strings will be replaced by git during git-archive. 22 | # setup.py/versioneer.py will grep for the variable names, so they must 23 | # each be defined on a line of their own. _version.py will just call 24 | # get_keywords(). 25 | git_refnames = "$Format:%d$" 26 | git_full = "$Format:%H$" 27 | git_date = "$Format:%ci$" 28 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 29 | return keywords 30 | 31 | 32 | class VersioneerConfig: 33 | """Container for Versioneer configuration parameters.""" 34 | 35 | 36 | def get_config(): 37 | """Create, populate and return the VersioneerConfig() object.""" 38 | # these strings are filled in when 'setup.py versioneer' creates 39 | # _version.py 40 | cfg = VersioneerConfig() 41 | cfg.VCS = "git" 42 | cfg.style = "pep440-post" 43 | cfg.tag_prefix = "v" 44 | cfg.parentdir_prefix = "None" 45 | cfg.versionfile_source = "{{ cookiecutter.package_dir_name }}/_version.py" 46 | cfg.verbose = False 47 | return cfg 48 | 49 | 50 | class NotThisMethod(Exception): 51 | """Exception raised if a method is not valid for the current scenario.""" 52 | 53 | 54 | LONG_VERSION_PY = {} 55 | HANDLERS = {} 56 | 57 | 58 | def register_vcs_handler(vcs, method): # decorator 59 | """Decorator to mark a method as the handler for a particular VCS.""" 60 | 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | 68 | return decorate 69 | 70 | 71 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): 72 | """Call the given command(s).""" 73 | assert isinstance(commands, list) 74 | p = None 75 | for c in commands: 76 | try: 77 | dispcmd = str([c] + args) 78 | # remember shell=False, so use git.cmd on windows, not just git 79 | p = subprocess.Popen( 80 | [c] + args, 81 | cwd=cwd, 82 | env=env, 83 | stdout=subprocess.PIPE, 84 | stderr=(subprocess.PIPE if hide_stderr else None), 85 | ) 86 | break 87 | except EnvironmentError: 88 | e = sys.exc_info()[1] 89 | if e.errno == errno.ENOENT: 90 | continue 91 | if verbose: 92 | print("unable to run %s" % dispcmd) 93 | print(e) 94 | return None, None 95 | else: 96 | if verbose: 97 | print("unable to find command, tried %s" % (commands,)) 98 | return None, None 99 | stdout = p.communicate()[0].strip() 100 | if sys.version_info[0] >= 3: 101 | stdout = stdout.decode() 102 | if p.returncode != 0: 103 | if verbose: 104 | print("unable to run %s (error)" % dispcmd) 105 | print("stdout was %s" % stdout) 106 | return None, p.returncode 107 | return stdout, p.returncode 108 | 109 | 110 | def versions_from_parentdir(parentdir_prefix, root, verbose): 111 | """Try to determine the version from the parent directory name. 112 | 113 | Source tarballs conventionally unpack into a directory that includes both 114 | the project name and a version string. We will also support searching up 115 | two directory levels for an appropriately named parent directory 116 | """ 117 | rootdirs = [] 118 | 119 | for i in range(3): 120 | dirname = os.path.basename(root) 121 | if dirname.startswith(parentdir_prefix): 122 | return { 123 | "version": dirname[len(parentdir_prefix) :], 124 | "full-revisionid": None, 125 | "dirty": False, 126 | "error": None, 127 | "date": None, 128 | } 129 | else: 130 | rootdirs.append(root) 131 | root = os.path.dirname(root) # up a level 132 | 133 | if verbose: 134 | print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) 135 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 136 | 137 | 138 | @register_vcs_handler("git", "get_keywords") 139 | def git_get_keywords(versionfile_abs): 140 | """Extract version information from the given file.""" 141 | # the code embedded in _version.py can just fetch the value of these 142 | # keywords. When used from setup.py, we don't want to import _version.py, 143 | # so we do it with a regexp instead. This function is not used from 144 | # _version.py. 145 | keywords = {} 146 | try: 147 | f = open(versionfile_abs, "r") 148 | for line in f.readlines(): 149 | if line.strip().startswith("git_refnames ="): 150 | mo = re.search(r'=\s*"(.*)"', line) 151 | if mo: 152 | keywords["refnames"] = mo.group(1) 153 | if line.strip().startswith("git_full ="): 154 | mo = re.search(r'=\s*"(.*)"', line) 155 | if mo: 156 | keywords["full"] = mo.group(1) 157 | if line.strip().startswith("git_date ="): 158 | mo = re.search(r'=\s*"(.*)"', line) 159 | if mo: 160 | keywords["date"] = mo.group(1) 161 | f.close() 162 | except EnvironmentError: 163 | pass 164 | return keywords 165 | 166 | 167 | @register_vcs_handler("git", "keywords") 168 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 169 | """Get version information from git keywords.""" 170 | if not keywords: 171 | raise NotThisMethod("no keywords at all, weird") 172 | date = keywords.get("date") 173 | if date is not None: 174 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 175 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 176 | # -like" string, which we must then edit to make compliant), because 177 | # it's been around since git-1.5.3, and it's too difficult to 178 | # discover which version we're using, or to work around using an 179 | # older one. 180 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 181 | refnames = keywords["refnames"].strip() 182 | if refnames.startswith("$Format"): 183 | if verbose: 184 | print("keywords are unexpanded, not using") 185 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 186 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 187 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 188 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 189 | TAG = "tag: " 190 | tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) 191 | if not tags: 192 | # Either we're using git < 1.8.3, or there really are no tags. We use 193 | # a heuristic: assume all version tags have a digit. The old git %d 194 | # expansion behaves like git log --decorate=short and strips out the 195 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 196 | # between branches and tags. By ignoring refnames without digits, we 197 | # filter out many common branch names like "release" and 198 | # "stabilization", as well as "HEAD" and "master". 199 | tags = set([r for r in refs if re.search(r"\d", r)]) 200 | if verbose: 201 | print("discarding '%s', no digits" % ",".join(refs - tags)) 202 | if verbose: 203 | print("likely tags: %s" % ",".join(sorted(tags))) 204 | for ref in sorted(tags): 205 | # sorting will prefer e.g. "2.0" over "2.0rc1" 206 | if ref.startswith(tag_prefix): 207 | r = ref[len(tag_prefix) :] 208 | if verbose: 209 | print("picking %s" % r) 210 | return { 211 | "version": r, 212 | "full-revisionid": keywords["full"].strip(), 213 | "dirty": False, 214 | "error": None, 215 | "date": date, 216 | } 217 | # no suitable tags, so version is "0+unknown", but full hex is still there 218 | if verbose: 219 | print("no suitable tags, using unknown + full revision id") 220 | return { 221 | "version": "0+unknown", 222 | "full-revisionid": keywords["full"].strip(), 223 | "dirty": False, 224 | "error": "no suitable tags", 225 | "date": None, 226 | } 227 | 228 | 229 | @register_vcs_handler("git", "pieces_from_vcs") 230 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 231 | """Get version from 'git describe' in the root of the source tree. 232 | 233 | This only gets called if the git-archive 'subst' keywords were *not* 234 | expanded, and _version.py hasn't already been rewritten with a short 235 | version string, meaning we're inside a checked out source tree. 236 | """ 237 | GITS = ["git"] 238 | if sys.platform == "win32": 239 | GITS = ["git.cmd", "git.exe"] 240 | 241 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) 242 | if rc != 0: 243 | if verbose: 244 | print("Directory %s not under git control" % root) 245 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 246 | 247 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 248 | # if there isn't one, this yields HEX[-dirty] (no NUM) 249 | describe_out, rc = run_command( 250 | GITS, 251 | [ 252 | "describe", 253 | "--tags", 254 | "--dirty", 255 | "--always", 256 | "--long", 257 | "--match", 258 | "%s*" % tag_prefix, 259 | ], 260 | cwd=root, 261 | ) 262 | # --long was added in git-1.5.5 263 | if describe_out is None: 264 | raise NotThisMethod("'git describe' failed") 265 | describe_out = describe_out.strip() 266 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 267 | if full_out is None: 268 | raise NotThisMethod("'git rev-parse' failed") 269 | full_out = full_out.strip() 270 | 271 | pieces = {} 272 | pieces["long"] = full_out 273 | pieces["short"] = full_out[:7] # maybe improved later 274 | pieces["error"] = None 275 | 276 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 277 | # TAG might have hyphens. 278 | git_describe = describe_out 279 | 280 | # look for -dirty suffix 281 | dirty = git_describe.endswith("-dirty") 282 | pieces["dirty"] = dirty 283 | if dirty: 284 | git_describe = git_describe[: git_describe.rindex("-dirty")] 285 | 286 | # now we have TAG-NUM-gHEX or HEX 287 | 288 | if "-" in git_describe: 289 | # TAG-NUM-gHEX 290 | mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) 291 | if not mo: 292 | # unparseable. Maybe git-describe is misbehaving? 293 | pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out 294 | return pieces 295 | 296 | # tag 297 | full_tag = mo.group(1) 298 | if not full_tag.startswith(tag_prefix): 299 | if verbose: 300 | fmt = "tag '%s' doesn't start with prefix '%s'" 301 | print(fmt % (full_tag, tag_prefix)) 302 | pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( 303 | full_tag, 304 | tag_prefix, 305 | ) 306 | return pieces 307 | pieces["closest-tag"] = full_tag[len(tag_prefix) :] 308 | 309 | # distance: number of commits since tag 310 | pieces["distance"] = int(mo.group(2)) 311 | 312 | # commit: short hex revision ID 313 | pieces["short"] = mo.group(3) 314 | 315 | else: 316 | # HEX: no tags 317 | pieces["closest-tag"] = None 318 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) 319 | pieces["distance"] = int(count_out) # total number of commits 320 | 321 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 322 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() 323 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 324 | 325 | return pieces 326 | 327 | 328 | def plus_or_dot(pieces): 329 | """Return a + if we don't already have one, else return a .""" 330 | if "+" in pieces.get("closest-tag", ""): 331 | return "." 332 | return "+" 333 | 334 | 335 | def render_pep440(pieces): 336 | """Build up version string, with post-release "local version identifier". 337 | 338 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 339 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 340 | 341 | Exceptions: 342 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 343 | """ 344 | if pieces["closest-tag"]: 345 | rendered = pieces["closest-tag"] 346 | if pieces["distance"] or pieces["dirty"]: 347 | rendered += plus_or_dot(pieces) 348 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 349 | if pieces["dirty"]: 350 | rendered += ".dirty" 351 | else: 352 | # exception #1 353 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 354 | if pieces["dirty"]: 355 | rendered += ".dirty" 356 | return rendered 357 | 358 | 359 | def render_pep440_pre(pieces): 360 | """TAG[.post.devDISTANCE] -- No -dirty. 361 | 362 | Exceptions: 363 | 1: no tags. 0.post.devDISTANCE 364 | """ 365 | if pieces["closest-tag"]: 366 | rendered = pieces["closest-tag"] 367 | if pieces["distance"]: 368 | rendered += ".post.dev%d" % pieces["distance"] 369 | else: 370 | # exception #1 371 | rendered = "0.post.dev%d" % pieces["distance"] 372 | return rendered 373 | 374 | 375 | def render_pep440_post(pieces): 376 | """TAG[.postDISTANCE[.dev0]+gHEX] . 377 | 378 | The ".dev0" means dirty. Note that .dev0 sorts backwards 379 | (a dirty tree will appear "older" than the corresponding clean one), 380 | but you shouldn't be releasing software with -dirty anyways. 381 | 382 | Exceptions: 383 | 1: no tags. 0.postDISTANCE[.dev0] 384 | """ 385 | if pieces["closest-tag"]: 386 | rendered = pieces["closest-tag"] 387 | if pieces["distance"] or pieces["dirty"]: 388 | rendered += ".post%d" % pieces["distance"] 389 | if pieces["dirty"]: 390 | rendered += ".dev0" 391 | rendered += plus_or_dot(pieces) 392 | rendered += "g%s" % pieces["short"] 393 | else: 394 | # exception #1 395 | rendered = "0.post%d" % pieces["distance"] 396 | if pieces["dirty"]: 397 | rendered += ".dev0" 398 | rendered += "+g%s" % pieces["short"] 399 | return rendered 400 | 401 | 402 | def render_pep440_old(pieces): 403 | """TAG[.postDISTANCE[.dev0]] . 404 | 405 | The ".dev0" means dirty. 406 | 407 | Eexceptions: 408 | 1: no tags. 0.postDISTANCE[.dev0] 409 | """ 410 | if pieces["closest-tag"]: 411 | rendered = pieces["closest-tag"] 412 | if pieces["distance"] or pieces["dirty"]: 413 | rendered += ".post%d" % pieces["distance"] 414 | if pieces["dirty"]: 415 | rendered += ".dev0" 416 | else: 417 | # exception #1 418 | rendered = "0.post%d" % pieces["distance"] 419 | if pieces["dirty"]: 420 | rendered += ".dev0" 421 | return rendered 422 | 423 | 424 | def render_git_describe(pieces): 425 | """TAG[-DISTANCE-gHEX][-dirty]. 426 | 427 | Like 'git describe --tags --dirty --always'. 428 | 429 | Exceptions: 430 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 431 | """ 432 | if pieces["closest-tag"]: 433 | rendered = pieces["closest-tag"] 434 | if pieces["distance"]: 435 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 436 | else: 437 | # exception #1 438 | rendered = pieces["short"] 439 | if pieces["dirty"]: 440 | rendered += "-dirty" 441 | return rendered 442 | 443 | 444 | def render_git_describe_long(pieces): 445 | """TAG-DISTANCE-gHEX[-dirty]. 446 | 447 | Like 'git describe --tags --dirty --always -long'. 448 | The distance/hash is unconditional. 449 | 450 | Exceptions: 451 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 452 | """ 453 | if pieces["closest-tag"]: 454 | rendered = pieces["closest-tag"] 455 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 456 | else: 457 | # exception #1 458 | rendered = pieces["short"] 459 | if pieces["dirty"]: 460 | rendered += "-dirty" 461 | return rendered 462 | 463 | 464 | def render(pieces, style): 465 | """Render the given version pieces into the requested style.""" 466 | if pieces["error"]: 467 | return { 468 | "version": "unknown", 469 | "full-revisionid": pieces.get("long"), 470 | "dirty": None, 471 | "error": pieces["error"], 472 | "date": None, 473 | } 474 | 475 | if not style or style == "default": 476 | style = "pep440" # the default 477 | 478 | if style == "pep440": 479 | rendered = render_pep440(pieces) 480 | elif style == "pep440-pre": 481 | rendered = render_pep440_pre(pieces) 482 | elif style == "pep440-post": 483 | rendered = render_pep440_post(pieces) 484 | elif style == "pep440-old": 485 | rendered = render_pep440_old(pieces) 486 | elif style == "git-describe": 487 | rendered = render_git_describe(pieces) 488 | elif style == "git-describe-long": 489 | rendered = render_git_describe_long(pieces) 490 | else: 491 | raise ValueError("unknown style '%s'" % style) 492 | 493 | return { 494 | "version": rendered, 495 | "full-revisionid": pieces["long"], 496 | "dirty": pieces["dirty"], 497 | "error": None, 498 | "date": pieces.get("date"), 499 | } 500 | 501 | 502 | def get_versions(): 503 | """Get version information or return default if unable to do so.""" 504 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 505 | # __file__, we can work backwards from there to the root. Some 506 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 507 | # case we can only use expanded keywords. 508 | 509 | cfg = get_config() 510 | verbose = cfg.verbose 511 | 512 | try: 513 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) 514 | except NotThisMethod: 515 | pass 516 | 517 | try: 518 | root = os.path.realpath(__file__) 519 | # versionfile_source is the relative path from the top of the source 520 | # tree (where the .git directory might live) to this file. Invert 521 | # this to find the root from __file__. 522 | for i in cfg.versionfile_source.split("/"): 523 | root = os.path.dirname(root) 524 | except NameError: 525 | return { 526 | "version": "0+unknown", 527 | "full-revisionid": None, 528 | "dirty": None, 529 | "error": "unable to find root of source tree", 530 | "date": None, 531 | } 532 | 533 | try: 534 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 535 | return render(pieces, cfg.style) 536 | except NotThisMethod: 537 | pass 538 | 539 | try: 540 | if cfg.parentdir_prefix: 541 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 542 | except NotThisMethod: 543 | pass 544 | 545 | return { 546 | "version": "0+unknown", 547 | "full-revisionid": None, 548 | "dirty": None, 549 | "error": "unable to compute version", 550 | "date": None, 551 | } 552 | --------------------------------------------------------------------------------