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