├── 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 |
--------------------------------------------------------------------------------