From ededf00035e6ccfac78946213009c1ecd7c110a9 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 7 Jan 2022 11:46:51 +0000 Subject: [PATCH 01/79] Initial structure --- .gitattributes | 1 + .github/pages/index.html | 9 + .github/workflows/code.yml | 112 ++++++++++++ .github/workflows/docs.yml | 46 +++++ .gitignore | 65 +++++++ .gitlab-ci.yml | 4 + .gitremotes | 1 + .pre-commit-config.yaml | 31 ++++ .vscode/extensions.json | 7 + .vscode/launch.json | 23 +++ .vscode/settings.json | 16 ++ .vscode/tasks.json | 16 ++ CONTRIBUTING.rst | 102 +++++++++++ LICENSE | 201 ++++++++++++++++++++++ Pipfile | 17 ++ README.rst | 55 ++++++ docs/_static/theme_overrides.css | 34 ++++ docs/conf.py | 117 +++++++++++++ docs/explanations.rst | 11 ++ docs/explanations/why-is-something-so.rst | 7 + docs/how-to.rst | 11 ++ docs/how-to/accomplish-a-task.rst | 7 + docs/images/dls-favicon.ico | Bin 0 -> 99678 bytes docs/images/dls-logo.svg | 11 ++ docs/index.rst | 48 ++++++ docs/reference.rst | 18 ++ docs/reference/api.rst | 24 +++ docs/reference/contributing.rst | 1 + docs/tutorials.rst | 11 ++ docs/tutorials/installation.rst | 48 ++++++ pyproject.toml | 6 + setup.cfg | 84 +++++++++ setup.py | 13 ++ src/dls_python3_skeleton/__init__.py | 6 + src/dls_python3_skeleton/__main__.py | 20 +++ src/dls_python3_skeleton/_version_git.py | 100 +++++++++++ src/dls_python3_skeleton/hello.py | 41 +++++ tests/test_boilerplate_removed.py | 79 +++++++++ tests/test_dls_python3_skeleton.py | 28 +++ 39 files changed, 1431 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/pages/index.html create mode 100644 .github/workflows/code.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .gitremotes create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CONTRIBUTING.rst create mode 100644 LICENSE create mode 100644 Pipfile create mode 100644 README.rst create mode 100644 docs/_static/theme_overrides.css create mode 100644 docs/conf.py create mode 100644 docs/explanations.rst create mode 100644 docs/explanations/why-is-something-so.rst create mode 100644 docs/how-to.rst create mode 100644 docs/how-to/accomplish-a-task.rst create mode 100644 docs/images/dls-favicon.ico create mode 100644 docs/images/dls-logo.svg create mode 100644 docs/index.rst create mode 100644 docs/reference.rst create mode 100644 docs/reference/api.rst create mode 100644 docs/reference/contributing.rst create mode 100644 docs/tutorials.rst create mode 100644 docs/tutorials/installation.rst create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 src/dls_python3_skeleton/__init__.py create mode 100644 src/dls_python3_skeleton/__main__.py create mode 100644 src/dls_python3_skeleton/_version_git.py create mode 100644 src/dls_python3_skeleton/hello.py create mode 100644 tests/test_boilerplate_removed.py create mode 100644 tests/test_dls_python3_skeleton.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..075748c4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +src/*/_version_git.py export-subst diff --git a/.github/pages/index.html b/.github/pages/index.html new file mode 100644 index 00000000..cc33127d --- /dev/null +++ b/.github/pages/index.html @@ -0,0 +1,9 @@ + + + + Redirecting to master branch + + + + + diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml new file mode 100644 index 00000000..62b51616 --- /dev/null +++ b/.github/workflows/code.yml @@ -0,0 +1,112 @@ +name: Code CI + +on: + push: + branches: + # Restricting to these branches and tags stops duplicate jobs on internal + # PRs but stops CI running on internal branches without a PR. Delete the + # next 5 lines to restore the original behaviour + - master + - main + tags: + - "*" + pull_request: + schedule: + # Run every Monday at 8am to check latest versions of dependencies + - cron: '0 8 * * MON' + +jobs: + lint: + runs-on: "ubuntu-latest" + steps: + - name: Run black, flake8, mypy + uses: dls-controls/pipenv-run-action@v1 + with: + pipenv-run: lint + + wheel: + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Create Sdist and Wheel + # Set SOURCE_DATE_EPOCH from git commit for reproducible build + # https://reproducible-builds.org/ + # Set group writable and umask to do the same to match inside DLS + run: | + chmod -R g+w . + umask 0002 + SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) pipx run build --sdist --wheel + + - name: Test cli works from the installed wheel + # Can remove the repository reference after https://github.com/pypa/pipx/pull/733 + run: pipx run --spec dist/*.whl ${GITHUB_REPOSITORY##*/} --version + + - name: Upload Wheel and Sdist as artifacts + uses: actions/upload-artifact@v2 + with: + name: dist + path: dist/* + + test: + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest"] # can add windows-latest, macos-latest + python: ["3.7", "3.8", "3.9"] + pipenv: ["skip-lock"] + + include: + # Add an extra Python3.7 runner to use the lockfile + - os: "ubuntu-latest" + python: "3.7" + pipenv: "deploy" + + runs-on: ${{ matrix.os }} + env: + # https://github.com/pytest-dev/pytest/issues/2042 + PY_IGNORE_IMPORTMISMATCH: "1" + + steps: + - name: Setup repo and test + uses: dls-controls/pipenv-run-action@v1 + with: + python-version: ${{ matrix.python }} + pipenv-install: --dev --${{ matrix.pipenv }} + allow-editable-installs: ${{ matrix.pipenv == 'deploy' }} + pipenv-run: tests + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 + with: + name: ${{ matrix.python }}/${{ matrix.os }}/${{ matrix.pipenv }} + files: cov.xml + + release: + needs: [lint, wheel, test] + runs-on: ubuntu-latest + # upload to PyPI and make a release on every tag + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + steps: + - uses: actions/download-artifact@v2 + with: + name: dist + path: dist + + - name: Github Release + # We pin to the SHA, not the tag, for security reasons. + # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions + uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14 + with: + files: dist/* + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.pypi_token }} + run: pipx run twine upload dist/* diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..e3b8fc90 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,46 @@ +name: Docs CI + +on: + push: + branches: + # Add more branches here to publish docs from other branches + - master + - main + tags: + - "*" + pull_request: + +jobs: + docs: + runs-on: ubuntu-latest + + steps: + - name: Avoid git conflicts when tag and branch pushed at same time + if: startsWith(github.ref, 'refs/tags') + run: sleep 60 + + - name: Install Packages + # Can delete this if you don't use graphviz in your docs + run: sudo apt-get install graphviz + + - name: Build docs + uses: dls-controls/pipenv-run-action@v1 + with: + pipenv-run: docs + + - name: Move to versioned directory + # e.g. master or 0.1.2 + run: mv build/html ".github/pages/${GITHUB_REF##*/}" + + - name: Write versions.txt + run: pipenv run sphinx_rtd_theme_github_versions .github/pages + + - name: Publish Docs to gh-pages + if: github.event_name == 'push' + # We pin to the SHA, not the tag, for security reasons. + # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions + uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 # v3.8.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: .github/pages + keep_files: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0ce69d99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.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 +cov.xml +.pytest_cache/ +.mypy_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# DLS build dir and virtual environment +/prefix/ +/venv/ +/lightweight-venv/ +/installed.files diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..1efd5024 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,4 @@ +include: + - project: 'controls/reports/ci_templates' + ref: master + file: 'python3/dls_py3_template.yml' diff --git a/.gitremotes b/.gitremotes new file mode 100644 index 00000000..3d3f0349 --- /dev/null +++ b/.gitremotes @@ -0,0 +1 @@ +github git@github.com:dls-controls/dls-python3-skeleton.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..23a81d4e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-added-large-files + - id: check-yaml + - id: check-merge-conflict + + - repo: local + hooks: + - id: black + name: Run black + stages: [commit] + language: system + entry: pipenv run black --check --diff + types: [python] + + - id: flake8 + name: Run flake8 + stages: [commit] + language: system + entry: pipenv run flake8 + types: [python] + exclude: setup.py + + - id: mypy + name: Run mypy + stages: [commit] + language: system + entry: pipenv run mypy src tests + pass_filenames: false diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..734f215e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-python.vscode-pylance", + "ms-python.python", + "ryanluker.vscode-coverage-gutters" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..1d960dc9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Unit Test", + "type": "python", + "request": "launch", + "justMyCode": false, + "program": "${file}", + "purpose": ["debug-test"], + "console": "integratedTerminal", + "env": { + // The default config in setup.cfg's "[tool:pytest]" adds coverage. + // Cannot have coverage and debugging at the same time. + // https://github.com/microsoft/vscode-python/issues/693 + "PYTEST_ADDOPTS": "--no-cov" + }, + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..192c474e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "editor.defaultFormatter": "ms-python.python", + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "python.linting.mypyEnabled": true, + "python.linting.enabled": true, + "python.testing.pytestArgs": [], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.formatting.provider": "black", + "python.languageServer": "Pylance", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..ff78a11b --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,16 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "label": "Tests with coverage", + "command": "pipenv run tests", + "options": { + "cwd": "${workspaceRoot}" + }, + "problemMatcher": [], + } + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..eba66b59 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,102 @@ +Contributing +============ + +Contributions and issues are most welcome! All issues and pull requests are +handled through GitHub_. Also, please check for any existing issues before +filing a new one. If you have a great idea but it involves big changes, please +file a ticket before making a pull request! We want to make sure you don't spend +your time coding something that might not fit the scope of the project. + +.. _GitHub: https://github.com/dls-controls/dls-python3-skeleton/issues + +Running the tests +----------------- + +To get the source source code and run the unit tests, run:: + + $ git clone git://github.com/dls-controls/dls-python3-skeleton.git + $ cd dls-python3-skeleton + $ pipenv install --dev + $ pipenv run tests + +While 100% code coverage does not make a library bug-free, it significantly +reduces the number of easily caught bugs! Please make sure coverage remains the +same or is improved by a pull request! + +Code Styling +------------ + +The code in this repository conforms to standards set by the following tools: + +- black_ for code formatting +- flake8_ for style checks +- isort_ for import ordering +- mypy_ for static type checking + +These checks will be run by pre-commit_. You can either choose to run these +tests on all files tracked by git:: + + $ pipenv run lint + +Or you can install a pre-commit hook that will run each time you do a ``git +commit`` on just the files that have changed:: + + $ pipenv run pre-commit install + +.. _black: https://github.com/psf/black +.. _flake8: http://flake8.pycqa.org/en/latest/ +.. _isort: https://github.com/timothycrosley/isort +.. _mypy: https://github.com/python/mypy +.. _pre-commit: https://pre-commit.com/ + +Documentation +------------- + +Documentation is contained in the ``docs`` directory and extracted from +docstrings of the API. + +Docs follow the underlining convention:: + + Headling 1 (page title) + ======================= + + Heading 2 + --------- + + Heading 3 + ~~~~~~~~~ + +You can build the docs from the project directory by running:: + + $ pipenv run docs + $ firefox build/html/index.html + +Release Process +--------------- + +To make a new release, please follow this checklist: + +- Choose a new PEP440 compliant release number +- Git tag the version +- Push to GitHub and the actions will make a release on pypi +- Push to internal gitlab and do a dls-release.py of the tag +- Check and edit for clarity the autogenerated GitHub release_ + +.. _release: https://dls-controls.github.io/dls-python3-skeleton/releases + +Updating the tools +------------------ + +This module is merged with the dls-python3-skeleton_. This is a generic +Python project structure which provides a means to keep tools and +techniques in sync between multiple Python projects. To update to the +latest version of the skeleton, run:: + + $ git pull https://github.com/dls-controls/dls-python3-skeleton skeleton + +Any merge conflicts will indicate an area where something has changed that +conflicts with the setup of the current module. Check the `closed pull requests +`_ +of the skeleton module for more details. + +.. _dls-python3-skeleton: https://dls-controls.github.io/dls-python3-skeleton diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..d1ebb0d9 --- /dev/null +++ b/Pipfile @@ -0,0 +1,17 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +dls_python3_skeleton = {editable = true, extras = ["dev"], path = "."} + +[packages] +dls_python3_skeleton = {editable = true, path = "."} + +[scripts] +lint = "pre-commit run --all-files --show-diff-on-failure --color=always -v" +tests = "pytest" +docs = "sphinx-build -EWT --keep-going docs build/html" +# Delete any files that git ignore hides from us +gitclean = "git clean -fdX" diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..9b99e2cb --- /dev/null +++ b/README.rst @@ -0,0 +1,55 @@ +dls-python3-skeleton +=========================== + +|code_ci| |docs_ci| |coverage| |pypi_version| |license| + +This is where you should write a short paragraph that describes what your module does, +how it does it, and why people should use it. + +============== ============================================================== +PyPI ``pip install dls_python3_skeleton`` +Source code https://github.com/dls-controls/dls-python3-skeleton +Documentation https://dls-controls.github.io/dls-python3-skeleton +Releases https://github.com/dls-controls/dls-python3-skeleton/releases +============== ============================================================== + +This is where you should put some images or code snippets that illustrate +some relevant examples. If it is a library then you might put some +introductory code here: + +.. code:: python + + from dls_python3_skeleton.hello import HelloClass + + hello = HelloClass("me") + print(hello.format_greeting()) + +Or if it is a commandline tool then you might put some example commands here:: + + dls-python3-skeleton person --times=2 + +.. |code_ci| image:: https://github.com/dls-controls/dls-python3-skeleton/workflows/Code%20CI/badge.svg?branch=master + :target: https://github.com/dls-controls/dls-python3-skeleton/actions?query=workflow%3A%22Code+CI%22 + :alt: Code CI + +.. |docs_ci| image:: https://github.com/dls-controls/dls-python3-skeleton/workflows/Docs%20CI/badge.svg?branch=master + :target: https://github.com/dls-controls/dls-python3-skeleton/actions?query=workflow%3A%22Docs+CI%22 + :alt: Docs CI + +.. |coverage| image:: https://codecov.io/gh/dls-controls/dls-python3-skeleton/branch/master/graph/badge.svg + :target: https://codecov.io/gh/dls-controls/dls-python3-skeleton + :alt: Test Coverage + +.. |pypi_version| image:: https://img.shields.io/pypi/v/dls_python3_skeleton.svg + :target: https://pypi.org/project/dls_python3_skeleton + :alt: Latest PyPI version + +.. |license| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg + :target: https://opensource.org/licenses/Apache-2.0 + :alt: Apache License + +.. + Anything below this line is used when viewing README.rst and will be replaced + when included in index.rst + +See https://dls-controls.github.io/dls-python3-skeleton for more detailed documentation. diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css new file mode 100644 index 00000000..5fd9b721 --- /dev/null +++ b/docs/_static/theme_overrides.css @@ -0,0 +1,34 @@ +/* override table width restrictions */ +@media screen and (min-width: 639px) { + .wy-table-responsive table td { + /* !important prevents the common CSS stylesheets from + overriding this as on RTD they are loaded after this stylesheet */ + white-space: normal !important; + } +} + +/* override table padding */ +.rst-content table.docutils th, .rst-content table.docutils td { + padding: 4px 6px; +} + +/* Add two-column option */ +@media only screen and (min-width: 1000px) { + .columns { + padding-left: 10px; + padding-right: 10px; + float: left; + width: 50%; + min-height: 145px; + } +} + +.endcolumns { + clear: both +} + +/* Hide toctrees within columns and captions from all toctrees. + This is what makes the include trick in index.rst work */ +.columns .toctree-wrapper, .toctree-wrapper .caption-text { + display: none; +} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..f2ca6a2d --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,117 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import dls_python3_skeleton + +# -- General configuration ------------------------------------------------ + +# General information about the project. +project = "dls-python3-skeleton" + +# The full version, including alpha/beta/rc tags. +release = dls_python3_skeleton.__version__ + +# The short X.Y version. +if "+" in release: + # Not on a tag + version = "master" +else: + version = release + +extensions = [ + # Use this for generating API docs + "sphinx.ext.autodoc", + # This can parse google style docstrings + "sphinx.ext.napoleon", + # For linking to external sphinx documentation + "sphinx.ext.intersphinx", + # Add links to source code in API docs + "sphinx.ext.viewcode", + # Adds the inheritance-diagram generation directive + "sphinx.ext.inheritance_diagram", +] + +# If true, Sphinx will warn about all references where the target cannot +# be found. +nitpicky = True + +# A list of (type, target) tuples (by default empty) that should be ignored when +# generating warnings in "nitpicky mode". Note that type should include the +# domain name if present. Example entries would be ('py:func', 'int') or +# ('envvar', 'LD_LIBRARY_PATH'). +nitpick_ignore = [("py:func", "int")] + +# Both the class’ and the __init__ method’s docstring are concatenated and +# inserted into the main body of the autoclass directive +autoclass_content = "both" + +# Order the members by the order they appear in the source code +autodoc_member_order = "bysource" + +# Don't inherit docstrings from baseclasses +autodoc_inherit_docstrings = False + +# Output graphviz directive produced images in a scalable format +graphviz_output_format = "svg" + +# The name of a reST role (builtin or Sphinx extension) to use as the default +# role, that is, for text marked up `like this` +default_role = "any" + +# The suffix of source filenames. +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# These patterns also affect html_static_path and html_extra_path +exclude_patterns = ["_build"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# This means you can link things like `str` and `asyncio` to the relevant +# docs in the python documentation. +intersphinx_mapping = dict(python=("https://docs.python.org/3/", None)) + +# A dictionary of graphviz graph attributes for inheritance diagrams. +inheritance_graph_attrs = dict(rankdir="TB") + +# Common links that should be available on every page +rst_epilog = """ +.. _Diamond Light Source: + http://www.diamond.ac.uk +""" + +# -- 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_github_versions" + +# Options for the sphinx rtd theme, use DLS blue +html_theme_options = dict(style_nav_header_background="rgb(7, 43, 93)") + +# 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"] + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = False + +# Add some CSS classes for columns and other tweaks in a custom css file +html_css_files = ["theme_overrides.css"] + +# Logo +html_logo = "images/dls-logo.svg" +html_favicon = "images/dls-favicon.ico" diff --git a/docs/explanations.rst b/docs/explanations.rst new file mode 100644 index 00000000..39de4e6b --- /dev/null +++ b/docs/explanations.rst @@ -0,0 +1,11 @@ +:orphan: + +Explanations +============ + +Explanation of how the library works and why it works that way. + +.. toctree:: + :caption: Explanations + + explanations/why-is-something-so diff --git a/docs/explanations/why-is-something-so.rst b/docs/explanations/why-is-something-so.rst new file mode 100644 index 00000000..21708377 --- /dev/null +++ b/docs/explanations/why-is-something-so.rst @@ -0,0 +1,7 @@ +Why is something the way it is +============================== + +Often, reading the code will not explain *why* it is written that way. These +explanations should be grouped together in articles here. They might include +history of dls-python3-skeleton, architectural decisions, or the +real world tests that influenced the design of dls-python3-skeleton. diff --git a/docs/how-to.rst b/docs/how-to.rst new file mode 100644 index 00000000..86b2ddbd --- /dev/null +++ b/docs/how-to.rst @@ -0,0 +1,11 @@ +:orphan: + +How-to Guides +============= + +Practical step-by-step guides for the more experienced user. + +.. toctree:: + :caption: How-to Guides + + how-to/accomplish-a-task diff --git a/docs/how-to/accomplish-a-task.rst b/docs/how-to/accomplish-a-task.rst new file mode 100644 index 00000000..8ee49390 --- /dev/null +++ b/docs/how-to/accomplish-a-task.rst @@ -0,0 +1,7 @@ +How to accomplish a task +======================== + +Here you would explain how to use dls-python3-skeleton to accomplish +a particular task. It doesn't have to be an exhaustive guide like the tutorials, +just enough information to show someone who knows what they want to do, how to +accomplish that task. diff --git a/docs/images/dls-favicon.ico b/docs/images/dls-favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9a11f508ef8aed28f14c5ce0d8408e1ec8b614a1 GIT binary patch literal 99678 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 ZYRRh@^bCDh5vs@!z69bV>$COq{{Z);QUw42 literal 0 HcmV?d00001 diff --git a/docs/images/dls-logo.svg b/docs/images/dls-logo.svg new file mode 100644 index 00000000..79ba266f --- /dev/null +++ b/docs/images/dls-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..9bde8adf --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,48 @@ +.. include:: ../README.rst + :end-before: when included in index.rst + + +How the documentation is structured +----------------------------------- + +Documentation is split into four categories, also accessible from links in the +side-bar. + +.. rst-class:: columns + +`tutorials` +~~~~~~~~~~~ + +.. include:: tutorials.rst + :start-after: ========= + +.. rst-class:: columns + +`how-to` +~~~~~~~~ + +.. include:: how-to.rst + :start-after: ============= + +.. rst-class:: columns + +`explanations` +~~~~~~~~~~~~~~ + +.. include:: explanations.rst + :start-after: ============ + +.. rst-class:: columns + +`reference` +~~~~~~~~~~~ + +.. include:: reference.rst + :start-after: ========= + +.. rst-class:: endcolumns + +About the documentation +~~~~~~~~~~~~~~~~~~~~~~~ + +`Why is the documentation structured this way? `_ diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 00000000..3f01ee35 --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,18 @@ +:orphan: + +Reference +========= + +Practical step-by-step guides for the more experienced user. + +.. toctree:: + :caption: Reference + + reference/api + reference/contributing + Releases + Index + +.. + Index link above is a hack to make genindex.html a relative link + https://stackoverflow.com/a/31820846 diff --git a/docs/reference/api.rst b/docs/reference/api.rst new file mode 100644 index 00000000..06bf3c66 --- /dev/null +++ b/docs/reference/api.rst @@ -0,0 +1,24 @@ +API +=== + +.. automodule:: dls_python3_skeleton + + ``dls_python3_skeleton`` + ----------------------------------- + +This is the internal API reference for dls_python3_skeleton + +You can mix verbose text with docstring and signature extraction by +using ``autoclass`` and ``autofunction`` directives instead of +``automodule`` below. + +.. data:: dls_python3_skeleton.__version__ + :type: str + + Version number as calculated by https://github.com/dls-controls/versiongit + +.. automodule:: dls_python3_skeleton.hello + :members: + + ``dls_python3_skeleton.hello`` + ----------------------------------------- diff --git a/docs/reference/contributing.rst b/docs/reference/contributing.rst new file mode 100644 index 00000000..ac7b6bcf --- /dev/null +++ b/docs/reference/contributing.rst @@ -0,0 +1 @@ +.. include:: ../../CONTRIBUTING.rst diff --git a/docs/tutorials.rst b/docs/tutorials.rst new file mode 100644 index 00000000..dfdef509 --- /dev/null +++ b/docs/tutorials.rst @@ -0,0 +1,11 @@ +:orphan: + +Tutorials +========= + +Tutorials for installation, library and commandline usage. New users start here. + +.. toctree:: + :caption: Tutorials + + tutorials/installation diff --git a/docs/tutorials/installation.rst b/docs/tutorials/installation.rst new file mode 100644 index 00000000..d23580a3 --- /dev/null +++ b/docs/tutorials/installation.rst @@ -0,0 +1,48 @@ +Installation +============ + +.. note:: + + For installation inside DLS, please see the internal documentation on + ``dls-python3`` and ``pipenv``. Although these instructions will work + inside DLS, they are intended for external use. + + If you want to contribute to the library itself, please follow + the `../reference/contributing` instructions. + + +Check your version of python +---------------------------- + +You will need python 3.7 or later. You can check your version of python by +typing into a terminal:: + + python3 --version + + +Create a virtual environment +---------------------------- + +It is recommended that you install into a “virtual environment” so this +installation will not interfere with any existing Python software:: + + python3 -m venv /path/to/venv + source /path/to/venv/bin/activate + + +Installing the library +---------------------- + +You can now use ``pip`` to install the library:: + + python3 -m pip install dls_python3_skeleton + +If you require a feature that is not currently released you can also install +from github:: + + python3 -m pip install git+git://github.com/dls-controls/dls-python3-skeleton.git + +The library should now be installed and the commandline interface on your path. +You can check the version that has been installed by typing:: + + dls-python3-skeleton --version diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..ccd70c76 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +# To get a reproducible wheel, wheel must be pinned to the same version as in +# dls-python3, and setuptools must produce the same dist-info. Cap setuptools +# to the last version that didn't add License-File to METADATA +requires = ["setuptools<57", "wheel==0.33.1"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..167860a5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,84 @@ +[metadata] +name = dls_python3_skeleton +description = One line description of your module +url = https://github.com/dls-controls/dls-python3-skeleton +author = Firstname Lastname +author_email = email@address.com +license = Apache License 2.0 +long_description = file: README.rst +long_description_content_type = text/x-rst +classifiers = + Development Status :: 4 - Beta + License :: OSI Approved :: Apache Software License + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.7 +packages = find: +package_dir = + =src +# Specify any package dependencies below. +# install_requires = +# numpy +# scipy + +[options.extras_require] +# For development tests/docs +dev = + black==21.9b0 + isort>5.0 + pytest-cov + mypy + flake8-isort + sphinx-rtd-theme-github-versions + pre-commit + +[options.packages.find] +where = src + +# Specify any package data to be included in the wheel below. +# [options.package_data] +# dls_python3_skeleton = +# subpackage/*.yaml + +[options.entry_points] +# Include a command line script +console_scripts = + dls-python3-skeleton = dls_python3_skeleton.__main__:main + +[mypy] +# Ignore missing stubs for modules we use +ignore_missing_imports = True + +[isort] +profile=black +float_to_top=true +skip=setup.py,conf.py,build + +[flake8] +# Make flake8 respect black's line length (default 88), +max-line-length = 88 +extend-ignore = + E203, # See https://github.com/PyCQA/pycodestyle/issues/373 + F811, # support typing.overload decorator + +[tool:pytest] +# Run pytest with all our checkers, and don't spam us with massive tracebacks on error +addopts = + --tb=native -vv --doctest-modules --doctest-glob="*.rst" + --cov=dls_python3_skeleton --cov-report term --cov-report xml:cov.xml +# https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings +filterwarnings = error + +[coverage:run] +# This is covered in the versiongit test suite so exclude it here +omit = */_version_git.py +data_file = /tmp/dls_python3_skeleton.coverage + +[coverage:paths] +# Tests are run from installed location, map back to the src directory +source = + src + **/site-packages/ diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..39de827e --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +# type: ignore +import glob +import importlib.util + +from setuptools import setup + +# Import ._version_git.py without importing +path = glob.glob(__file__.replace("setup.py", "src/*/_version_git.py"))[0] +spec = importlib.util.spec_from_file_location("_version_git", path) +vg = importlib.util.module_from_spec(spec) +spec.loader.exec_module(vg) + +setup(cmdclass=vg.get_cmdclass(), version=vg.__version__) diff --git a/src/dls_python3_skeleton/__init__.py b/src/dls_python3_skeleton/__init__.py new file mode 100644 index 00000000..25eb98fa --- /dev/null +++ b/src/dls_python3_skeleton/__init__.py @@ -0,0 +1,6 @@ +from . import hello +from ._version_git import __version__ + +# __all__ defines the public API for the package. +# Each module also defines its own __all__. +__all__ = ["__version__", "hello"] diff --git a/src/dls_python3_skeleton/__main__.py b/src/dls_python3_skeleton/__main__.py new file mode 100644 index 00000000..960eff10 --- /dev/null +++ b/src/dls_python3_skeleton/__main__.py @@ -0,0 +1,20 @@ +from argparse import ArgumentParser + +from . import __version__ +from .hello import HelloClass, say_hello_lots + +__all__ = ["main"] + + +def main(args=None): + parser = ArgumentParser() + parser.add_argument("--version", action="version", version=__version__) + parser.add_argument("name", help="Name of the person to greet") + parser.add_argument("--times", type=int, default=5, help="Number of times to greet") + args = parser.parse_args(args) + say_hello_lots(HelloClass(args.name), args.times) + + +# test with: pipenv run python -m dls_python3_skeleton +if __name__ == "__main__": + main() diff --git a/src/dls_python3_skeleton/_version_git.py b/src/dls_python3_skeleton/_version_git.py new file mode 100644 index 00000000..bb7f0c2a --- /dev/null +++ b/src/dls_python3_skeleton/_version_git.py @@ -0,0 +1,100 @@ +# Compute a version number from a git repo or archive + +# This file is released into the public domain. Generated by: +# versiongit-2.1 (https://github.com/dls-controls/versiongit) +import re +import sys +from pathlib import Path +from subprocess import STDOUT, CalledProcessError, check_output + +# These will be filled in if git archive is run or by setup.py cmdclasses +GIT_REFS = "$Format:%D$" +GIT_SHA1 = "$Format:%h$" + +# Git describe gives us sha1, last version-like tag, and commits since then +CMD = "git describe --tags --dirty --always --long --match=[0-9]*[-.][0-9]*" + + +def get_version_from_git(path=None): + """Try to parse version from git describe, fallback to git archive tags.""" + tag, plus, suffix = "0.0", "untagged", "" + if not GIT_SHA1.startswith("$"): + # git archive or the cmdclasses below have filled in these strings + sha1 = GIT_SHA1 + for ref_name in GIT_REFS.split(", "): + if ref_name.startswith("tag: "): + # git from 1.8.3 onwards labels archive tags "tag: TAGNAME" + tag, plus = ref_name[5:], "0" + else: + if path is None: + # If no path to git repo, choose the directory this file is in + path = Path(__file__).absolute().parent + # output is TAG-NUM-gHEX[-dirty] or HEX[-dirty] + try: + cmd_out = check_output(CMD.split(), stderr=STDOUT, cwd=path) + except Exception as e: + sys.stderr.write("%s: %s\n" % (type(e).__name__, str(e))) + if isinstance(e, CalledProcessError): + sys.stderr.write("-> %s" % e.output.decode()) + return "0.0+unknown", None, e + else: + out = cmd_out.decode().strip() + if out.endswith("-dirty"): + out = out[:-6] + suffix = ".dirty" + if "-" in out: + # There is a tag, extract it and the other pieces + match = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", out) + tag, plus, sha1 = match.groups() + else: + # No tag, just sha1 + sha1 = out + # Replace dashes in tag for dots + tag = tag.replace("-", ".") + if plus != "0" or suffix: + # Not on a tag, add additional info + tag = f"{tag}+{plus}.g{sha1}{suffix}" + return tag, sha1, None + + +__version__, git_sha1, git_error = get_version_from_git() + + +def get_cmdclass(build_py=None, sdist=None): + """Create cmdclass dict to pass to setuptools.setup. + + Create cmdclass dict to pass to setuptools.setup which will write a + _version_static.py file in our resultant sdist, wheel or egg. + """ + if build_py is None: + from setuptools.command.build_py import build_py + if sdist is None: + from setuptools.command.sdist import sdist + + def make_version_static(base_dir: str, pkg: str): + vg = Path(base_dir) / pkg.split(".")[0] / "_version_git.py" + if vg.is_file(): + lines = open(vg).readlines() + with open(vg, "w") as f: + for line in lines: + # Replace GIT_* with static versions + if line.startswith("GIT_SHA1 = "): + f.write("GIT_SHA1 = '%s'\n" % git_sha1) + elif line.startswith("GIT_REFS = "): + f.write("GIT_REFS = 'tag: %s'\n" % __version__) + else: + f.write(line) + + class BuildPy(build_py): + def run(self): + build_py.run(self) + for pkg in self.packages: + make_version_static(self.build_lib, pkg) + + class Sdist(sdist): + def make_release_tree(self, base_dir, files): + sdist.make_release_tree(self, base_dir, files) + for pkg in self.distribution.packages: + make_version_static(base_dir, pkg) + + return dict(build_py=BuildPy, sdist=Sdist) diff --git a/src/dls_python3_skeleton/hello.py b/src/dls_python3_skeleton/hello.py new file mode 100644 index 00000000..c0b09939 --- /dev/null +++ b/src/dls_python3_skeleton/hello.py @@ -0,0 +1,41 @@ +# The purpose of __all__ is to define the public API of this module, and which +# objects are imported if we call "from dls_python3_skeleton.hello import *" +__all__ = [ + "HelloClass", + "say_hello_lots", +] + + +class HelloClass: + """A class whose only purpose in life is to say hello""" + + def __init__(self, name: str): + """ + Args: + name: The initial value of the name of the person who gets greeted + """ + #: The name of the person who gets greeted + self.name = name + + def format_greeting(self) -> str: + """Return a greeting for `name` + + >>> HelloClass("me").format_greeting() + 'Hello me' + """ + greeting = f"Hello {self.name}" + return greeting + + +def say_hello_lots(hello: HelloClass = None, times=5): + """Print lots of greetings using the given `HelloClass` + + Args: + hello: A `HelloClass` that `format_greeting` will be called on. + If not given, use a HelloClass with name="me" + times: The number of times to call it + """ + if hello is None: + hello = HelloClass("me") + for _ in range(times): + print(hello.format_greeting()) diff --git a/tests/test_boilerplate_removed.py b/tests/test_boilerplate_removed.py new file mode 100644 index 00000000..ca8086cb --- /dev/null +++ b/tests/test_boilerplate_removed.py @@ -0,0 +1,79 @@ +""" +This file checks that all the example boilerplate text has been removed. +It can be deleted when all the contained tests pass +""" +import configparser +from pathlib import Path + +ROOT = Path(__file__).parent.parent + + +def skeleton_check(check: bool, text: str): + if ROOT.name == "dls-python3-skeleton": + # In the skeleton module the check should fail + check = not check + text = f"Skeleton didn't raise: {text}" + if check: + raise AssertionError(text) + + +def assert_not_contains_text(path: str, text: str, explanation: str): + full_path = ROOT / path + if full_path.exists(): + contents = full_path.read_text().replace("\n", " ") + skeleton_check(text in contents, f"Please change ./{path} {explanation}") + + +def assert_not_exists(path: str, explanation: str): + exists = (ROOT / path).exists() + skeleton_check(exists, f"Please delete ./{path} {explanation}") + + +# setup.cfg +def test_module_description(): + conf = configparser.ConfigParser() + conf.read("setup.cfg") + description = conf["metadata"]["description"] + skeleton_check( + "One line description of your module" in description, + "Please change description in ./setup.cfg " + "to be a one line description of your module", + ) + + +# README +def test_changed_README_intro(): + assert_not_contains_text( + "README.rst", + "This is where you should write a short paragraph", + "to include an intro on what your module does", + ) + + +def test_changed_README_body(): + assert_not_contains_text( + "README.rst", + "This is where you should put some images or code snippets", + "to include some features and why people should use it", + ) + + +# Docs +def test_docs_ref_api_changed(): + assert_not_contains_text( + "docs/reference/api.rst", + "You can mix verbose text with docstring and signature", + "to introduce the API for your module", + ) + + +def test_how_tos_written(): + assert_not_exists( + "docs/how-to/accomplish-a-task.rst", "and write some docs/how-tos" + ) + + +def test_explanations_written(): + assert_not_exists( + "docs/explanations/why-is-something-so.rst", "and write some docs/explanations" + ) diff --git a/tests/test_dls_python3_skeleton.py b/tests/test_dls_python3_skeleton.py new file mode 100644 index 00000000..fffef4b4 --- /dev/null +++ b/tests/test_dls_python3_skeleton.py @@ -0,0 +1,28 @@ +import subprocess +import sys + +from dls_python3_skeleton import __main__, __version__, hello + + +def test_hello_class_formats_greeting() -> None: + inst = hello.HelloClass("person") + assert inst.format_greeting() == "Hello person" + + +def test_hello_lots_defaults(capsys) -> None: + hello.say_hello_lots() + captured = capsys.readouterr() + assert captured.out == "Hello me\n" * 5 + assert captured.err == "" + + +def test_cli_greets(capsys) -> None: + __main__.main(["person", "--times=2"]) + captured = capsys.readouterr() + assert captured.out == "Hello person\n" * 2 + assert captured.err == "" + + +def test_cli_version(): + cmd = [sys.executable, "-m", "dls_python3_skeleton", "--version"] + assert subprocess.check_output(cmd).decode().strip() == __version__ From de6d5f161b940ebd052ee3123a7250a0776dcf50 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Mon, 7 Feb 2022 15:47:21 +0000 Subject: [PATCH 02/79] Squash all into new skeleton base commit --- .containerignore | 7 + .devcontainer.json | 39 ++++ .gitattributes | 1 - .github/dependabot.yml | 16 ++ .github/pages/index.html | 14 +- .github/workflows/code.yml | 189 ++++++++++++------ .github/workflows/container_tests.sh | 13 ++ .github/workflows/docs.yml | 37 ++-- .github/workflows/docs_clean.yml | 41 ++++ .github/workflows/linkcheck.yml | 34 ++++ .gitignore | 12 +- .gitlab-ci.yml | 4 - .gitremotes | 1 - .pre-commit-config.yaml | 12 +- .vscode/extensions.json | 3 +- .vscode/launch.json | 4 +- .vscode/settings.json | 1 - .vscode/tasks.json | 4 +- CONTRIBUTING.rst | 109 +++++++--- Dockerfile | 47 +++++ Pipfile | 17 -- README.rst | 47 ++--- docs/conf.py | 23 ++- docs/explanations.rst | 2 +- docs/explanations/decisions.rst | 17 ++ .../0001-record-architecture-decisions.rst | 26 +++ docs/explanations/why-is-something-so.rst | 7 - docs/how-to.rst | 2 +- docs/how-to/accomplish-a-task.rst | 7 - docs/{reference => how-to}/contributing.rst | 0 docs/images/dls-logo.svg | 2 +- docs/reference.rst | 5 +- docs/reference/api.rst | 20 +- docs/tutorials/installation.rst | 25 +-- pyproject.toml | 8 +- setup.cfg | 72 +++++-- setup.py | 13 -- src/dls_python3_skeleton/__init__.py | 6 - src/dls_python3_skeleton/__main__.py | 20 -- src/dls_python3_skeleton/_version_git.py | 100 --------- src/dls_python3_skeleton/hello.py | 41 ---- src/python3_pip_skeleton/__init__.py | 12 ++ src/python3_pip_skeleton/__main__.py | 16 ++ tests/test_boilerplate_removed.py | 34 +--- tests/test_cli.py | 9 + tests/test_dls_python3_skeleton.py | 28 --- 46 files changed, 667 insertions(+), 480 deletions(-) create mode 100644 .containerignore create mode 100644 .devcontainer.json delete mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/container_tests.sh create mode 100644 .github/workflows/docs_clean.yml create mode 100644 .github/workflows/linkcheck.yml delete mode 100644 .gitlab-ci.yml delete mode 100644 .gitremotes create mode 100644 Dockerfile delete mode 100644 Pipfile create mode 100644 docs/explanations/decisions.rst create mode 100644 docs/explanations/decisions/0001-record-architecture-decisions.rst delete mode 100644 docs/explanations/why-is-something-so.rst delete mode 100644 docs/how-to/accomplish-a-task.rst rename docs/{reference => how-to}/contributing.rst (100%) delete mode 100644 setup.py delete mode 100644 src/dls_python3_skeleton/__init__.py delete mode 100644 src/dls_python3_skeleton/__main__.py delete mode 100644 src/dls_python3_skeleton/_version_git.py delete mode 100644 src/dls_python3_skeleton/hello.py create mode 100644 src/python3_pip_skeleton/__init__.py create mode 100644 src/python3_pip_skeleton/__main__.py create mode 100644 tests/test_cli.py delete mode 100644 tests/test_dls_python3_skeleton.py diff --git a/.containerignore b/.containerignore new file mode 100644 index 00000000..eb7d5ae1 --- /dev/null +++ b/.containerignore @@ -0,0 +1,7 @@ +Dockerfile +build/ +dist/ +.mypy_cache +.tox +.venv* +venv* diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 00000000..d0921df6 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,39 @@ +// For format details, see https://aka.ms/devcontainer.json +{ + "name": "Python 3 Developer Container", + "build": { + "dockerfile": "Dockerfile", + "target": "build", + "context": ".", + "args": {} + }, + "remoteEnv": { + "DISPLAY": "${localEnv:DISPLAY}" + }, + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/venv/bin/python", + "python.linting.enabled": true + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance" + ], + // Make sure the files we are mapping into the container exist on the host + "initializeCommand": "bash -c 'for i in $HOME/.inputrc; do [ -f $i ] || touch $i; done'", + "runArgs": [ + "--net=host", + "-v=${localEnv:HOME}/.ssh:/root/.ssh", + "-v=${localEnv:HOME}/.inputrc:/root/.inputrc" + ], + "mounts": [ + // map in home directory - not strictly necessary but useful + "source=${localEnv:HOME},target=${localEnv:HOME},type=bind,consistency=cached" + ], + // make the workspace folder the same inside and outside of the container + "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind", + "workspaceFolder": "${localWorkspaceFolder}", + // After the container is created, install the python project in editable form + "postCreateCommand": "pip install $([ -f requirements_dev.txt ] && echo -r requirements_dev.txt ) -e .[dev]" +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 075748c4..00000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -src/*/_version_git.py export-subst diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..fb7c6ee6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/pages/index.html b/.github/pages/index.html index cc33127d..80f0a009 100644 --- a/.github/pages/index.html +++ b/.github/pages/index.html @@ -1,9 +1,11 @@ - - Redirecting to master branch + + + Redirecting to main branch - - - - + + + + + \ No newline at end of file diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 62b51616..5ea6e1f2 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -2,67 +2,39 @@ name: Code CI on: push: - branches: - # Restricting to these branches and tags stops duplicate jobs on internal - # PRs but stops CI running on internal branches without a PR. Delete the - # next 5 lines to restore the original behaviour - - master - - main - tags: - - "*" pull_request: schedule: # Run every Monday at 8am to check latest versions of dependencies - - cron: '0 8 * * MON' + - cron: "0 8 * * WED" jobs: lint: - runs-on: "ubuntu-latest" - steps: - - name: Run black, flake8, mypy - uses: dls-controls/pipenv-run-action@v1 - with: - pipenv-run: lint + # 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 - wheel: - runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup python + uses: actions/setup-python@v4 with: - fetch-depth: 0 + python-version: "3.10" - - name: Create Sdist and Wheel - # Set SOURCE_DATE_EPOCH from git commit for reproducible build - # https://reproducible-builds.org/ - # Set group writable and umask to do the same to match inside DLS + - name: Lint run: | - chmod -R g+w . - umask 0002 - SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) pipx run build --sdist --wheel - - - name: Test cli works from the installed wheel - # Can remove the repository reference after https://github.com/pypa/pipx/pull/733 - run: pipx run --spec dist/*.whl ${GITHUB_REPOSITORY##*/} --version - - - name: Upload Wheel and Sdist as artifacts - uses: actions/upload-artifact@v2 - with: - name: dist - path: dist/* + touch requirements_dev.txt requirements.txt + pip install -r requirements.txt -r requirements_dev.txt -e .[dev] + tox -e pre-commit,mypy test: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: - os: ["ubuntu-latest"] # can add windows-latest, macos-latest - python: ["3.7", "3.8", "3.9"] - pipenv: ["skip-lock"] - - include: - # Add an extra Python3.7 runner to use the lockfile - - os: "ubuntu-latest" - python: "3.7" - pipenv: "deploy" + os: ["ubuntu-latest"] # can add windows-latest, macos-latest + python: ["3.8", "3.9", "3.10"] runs-on: ${{ matrix.os }} env: @@ -70,37 +42,130 @@ jobs: PY_IGNORE_IMPORTMISMATCH: "1" steps: - - name: Setup repo and test - uses: dls-controls/pipenv-run-action@v1 + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup python ${{ matrix.python }} + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - pipenv-install: --dev --${{ matrix.pipenv }} - allow-editable-installs: ${{ matrix.pipenv == 'deploy' }} - pipenv-run: tests + + - name: Install with latest dependencies + run: pip install .[dev] + + - name: Run tests + run: pytest tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: - name: ${{ matrix.python }}/${{ matrix.os }}/${{ matrix.pipenv }} + name: ${{ matrix.python }}/${{ matrix.os }} files: cov.xml - release: - needs: [lint, wheel, test] + container: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository runs-on: ubuntu-latest - # upload to PyPI and make a release on every tag - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + permissions: + contents: read + packages: write + steps: - - uses: actions/download-artifact@v2 + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Log in to GitHub Docker Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=ref,event=tag + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Build developer image for testing + uses: docker/build-push-action@v3 + with: + tags: build:latest + context: . + target: build + load: true + + - name: Run tests in the container locked with requirements_dev.txt + run: | + docker run --name test build bash /project/.github/workflows/container_tests.sh + docker cp test:/project/dist . + docker cp test:/project/cov.xml . + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + name: 3.10-locked/ubuntu-latest + files: cov.xml + + - name: Build runtime image + uses: docker/build-push-action@v3 + with: + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + context: . + labels: ${{ steps.meta.outputs.labels }} + + - name: Check runtime + run: for i in ${{ steps.meta.outputs.tags }}; do docker run ${i} --version; done + + - name: Upload build files + uses: actions/upload-artifact@v3 with: name: dist - path: dist + path: dist/* + + sdist: + needs: container + runs-on: ubuntu-latest + + steps: + - uses: actions/download-artifact@v3 + + - name: Install sdist in a venv and check cli works + # ${GITHUB_REPOSITORY##*/} is the repo name without org + # Replace this with the cli command if different to the repo name + run: | + pip install dist/*.gz + ${GITHUB_REPOSITORY##*/} --version + + release: + # upload to PyPI and make a release on every tag + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + needs: container + runs-on: ubuntu-latest + + steps: + - uses: actions/download-artifact@v3 - name: Github Release # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions - uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14 + uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14 with: - files: dist/* + prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} + files: | + dist/* generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -108,5 +173,5 @@ jobs: - name: Publish to PyPI env: TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.pypi_token }} - run: pipx run twine upload dist/* + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: pipx run twine upload dist/*/whl dist/*.tar.gz diff --git a/.github/workflows/container_tests.sh b/.github/workflows/container_tests.sh new file mode 100644 index 00000000..5f921597 --- /dev/null +++ b/.github/workflows/container_tests.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -x + +cd /project +source /venv/bin/activate + +touch requirements_dev.txt +pip install -r requirements_dev.txt -e .[dev] +pip freeze --exclude-editable > dist/requirements_dev.txt + +pipdeptree + +pytest tests diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e3b8fc90..c3c5eda8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,16 +2,16 @@ name: Docs CI on: push: - branches: - # Add more branches here to publish docs from other branches - - master - - main - tags: - - "*" pull_request: jobs: docs: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + strategy: + fail-fast: false + matrix: + python: ["3.10"] + runs-on: ubuntu-latest steps: @@ -19,27 +19,40 @@ jobs: if: startsWith(github.ref, 'refs/tags') run: sleep 60 + - name: Install python version + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Install Packages # Can delete this if you don't use graphviz in your docs run: sudo apt-get install graphviz - - name: Build docs - uses: dls-controls/pipenv-run-action@v1 + - name: checkout + uses: actions/checkout@v2 with: - pipenv-run: docs + fetch-depth: 0 + + - name: Install dependencies + run: | + touch requirements_dev.txt + pip install -r requirements_dev.txt -e .[dev] + + - name: Build docs + run: tox -e docs - name: Move to versioned directory - # e.g. master or 0.1.2 + # e.g. main or 0.1.2 run: mv build/html ".github/pages/${GITHUB_REF##*/}" - name: Write versions.txt - run: pipenv run sphinx_rtd_theme_github_versions .github/pages + run: sphinx_rtd_theme_github_versions .github/pages - name: Publish Docs to gh-pages if: github.event_name == 'push' # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions - uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 # v3.8.0 + uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 # v3.8.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .github/pages diff --git a/.github/workflows/docs_clean.yml b/.github/workflows/docs_clean.yml new file mode 100644 index 00000000..2059c7f3 --- /dev/null +++ b/.github/workflows/docs_clean.yml @@ -0,0 +1,41 @@ +name: Docs Cleanup CI + +# delete branch documentation when a branch is deleted +# also allow manually deleting a documentation version +on: + delete: + workflow_dispatch: + inputs: + version: + description: "documentation version to DELETE" + required: true + type: string + +jobs: + remove: + if: github.event.ref_type == 'branch' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v2 + with: + ref: gh-pages + + - name: removing documentation for branch ${{ github.event.ref }} + if: ${{ github.event_name != 'workflow_dispatch' }} + run: echo "remove_me=${{ github.event.ref }}" >> $GITHUB_ENV + + - name: manually removing documentation version ${{ github.event.inputs.version }} + if: ${{ github.event_name == 'workflow_dispatch' }} + run: echo "remove_me=${{ github.event.inputs.version }}" >> $GITHUB_ENV + + - name: update index and push changes + run: | + echo removing redundant documentation version ${{ env.remove_me }} + rm -r ${{ env.remove_me }} + sed -i /${{ env.remove_me }}/d versions.txt + git config --global user.name 'GitHub Actions Docs Cleanup CI' + git config --global user.email 'gha@users.noreply.github.com' + git commit -am"removing redundant docs version ${{ env.remove_me }}" + git push diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml new file mode 100644 index 00000000..6b37f194 --- /dev/null +++ b/.github/workflows/linkcheck.yml @@ -0,0 +1,34 @@ +name: Link Check + +on: + schedule: + # Run every Monday at 8am to check URL links still resolve + - cron: "0 8 * * WED" + +jobs: + docs: + strategy: + fail-fast: false + matrix: + python: ["3.10"] + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install python version + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Install dependencies + run: | + touch requirements_dev.txt + pip install -r requirements_dev.txt -e .[dev] + + - name: Check links + run: tox -e docs -- -b linkcheck diff --git a/.gitignore b/.gitignore index 0ce69d99..e0fba46a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ # Distribution / packaging .Python env/ +.venv build/ develop-eggs/ dist/ @@ -22,6 +23,7 @@ var/ *.egg-info/ .installed.cfg *.egg +**/_version.py # PyInstaller # Usually these files are written by a python script from a template @@ -58,8 +60,8 @@ docs/_build/ # PyBuilder target/ -# DLS build dir and virtual environment -/prefix/ -/venv/ -/lightweight-venv/ -/installed.files +# likely venv names +.venv* +venv* + + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 1efd5024..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,4 +0,0 @@ -include: - - project: 'controls/reports/ci_templates' - ref: master - file: 'python3/dls_py3_template.yml' diff --git a/.gitremotes b/.gitremotes deleted file mode 100644 index 3d3f0349..00000000 --- a/.gitremotes +++ /dev/null @@ -1 +0,0 @@ -github git@github.com:dls-controls/dls-python3-skeleton.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23a81d4e..5e270b08 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,20 +12,12 @@ repos: name: Run black stages: [commit] language: system - entry: pipenv run black --check --diff + entry: black --check --diff types: [python] - id: flake8 name: Run flake8 stages: [commit] language: system - entry: pipenv run flake8 + entry: flake8 types: [python] - exclude: setup.py - - - id: mypy - name: Run mypy - stages: [commit] - language: system - entry: pipenv run mypy src tests - pass_filenames: false diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 734f215e..d173f8d6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,8 @@ { "recommendations": [ + "ms-vscode-remote.remote-containers" "ms-python.vscode-pylance", "ms-python.python", "ryanluker.vscode-coverage-gutters" ] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 1d960dc9..f8fcdb4f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,9 @@ "request": "launch", "justMyCode": false, "program": "${file}", - "purpose": ["debug-test"], + "purpose": [ + "debug-test" + ], "console": "integratedTerminal", "env": { // The default config in setup.cfg's "[tool:pytest]" adds coverage. diff --git a/.vscode/settings.json b/.vscode/settings.json index 192c474e..2472acfd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "editor.defaultFormatter": "ms-python.python", "python.linting.pylintEnabled": false, "python.linting.flake8Enabled": true, "python.linting.mypyEnabled": true, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ff78a11b..946e69d4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,8 +5,8 @@ "tasks": [ { "type": "shell", - "label": "Tests with coverage", - "command": "pipenv run tests", + "label": "Tests, lint and docs", + "command": "tox -p", "options": { "cwd": "${workspaceRoot}" }, diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index eba66b59..327e3920 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -7,17 +7,45 @@ filing a new one. If you have a great idea but it involves big changes, please file a ticket before making a pull request! We want to make sure you don't spend your time coding something that might not fit the scope of the project. -.. _GitHub: https://github.com/dls-controls/dls-python3-skeleton/issues +.. _GitHub: https://github.com/epics-containers/python3-pip-skeleton/issues Running the tests ----------------- -To get the source source code and run the unit tests, run:: +To run in a container +~~~~~~~~~~~~~~~~~~~~~ - $ git clone git://github.com/dls-controls/dls-python3-skeleton.git - $ cd dls-python3-skeleton - $ pipenv install --dev - $ pipenv run tests +Use vscode devcontainer as follows:: + + $ git clone git://github.com/epics-containers/python3-pip-skeleton.git + $ vscode python3-pip-skeleton + Click on 'Reopen in Container' when prompted + In a vscode Terminal: + $ tox -p + + +To run locally +~~~~~~~~~~~~~~ + +Get the source source code and run the unit tests directly +on your workstation as follows:: + + $ git clone git://github.com/epics-containers/python3-pip-skeleton.git + $ cd python3-pip-skeleton + $ virtualenv .venv + $ source .venv/bin/activate + $ pip install -e .[dev] + $ tox -p + +In both cases tox -p runs in parallel the following checks: + + - Build Sphinx Documentation + - run pytest on all tests in ./tests + - run mypy linting on all files in ./src ./tests + - run pre-commit checks: + + - run flake8 style checks against all source + - run black formatting checks against all source While 100% code coverage does not make a library bug-free, it significantly reduces the number of easily caught bugs! Please make sure coverage remains the @@ -33,22 +61,43 @@ The code in this repository conforms to standards set by the following tools: - isort_ for import ordering - mypy_ for static type checking -These checks will be run by pre-commit_. You can either choose to run these -tests on all files tracked by git:: +flake8 and black and isort are run by pre-commit_. You can run the above checks on +all files with this command:: - $ pipenv run lint + $ tox -e pre-commit,mypy Or you can install a pre-commit hook that will run each time you do a ``git -commit`` on just the files that have changed:: +commit`` on just the files that have changed. Note that mypy is not in +the pre-commit because it is a little slow :: - $ pipenv run pre-commit install + $ pre-commit install .. _black: https://github.com/psf/black -.. _flake8: http://flake8.pycqa.org/en/latest/ -.. _isort: https://github.com/timothycrosley/isort +.. _flake8: https://flake8.pycqa.org/en/latest/ +.. _isort: https://github.com/PyCQA/isort .. _mypy: https://github.com/python/mypy .. _pre-commit: https://pre-commit.com/ +Docstrings are pre-processed using the Sphinx Napoleon extension. As such, +google-style_ is considered as standard for this repository. Please use type +hints in the function signature for types. For example:: + + def func(arg1: str, arg2: int) -> bool: + """Summary line. + + Extended description of function. + + Args: + arg1: Description of arg1 + arg2: Description of arg2 + + Returns: + Description of return value + """ + return True + +.. _google-style: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/index.html#google-vs-numpy + Documentation ------------- @@ -68,7 +117,7 @@ Docs follow the underlining convention:: You can build the docs from the project directory by running:: - $ pipenv run docs + $ tox -e docs $ firefox build/html/index.html Release Process @@ -76,27 +125,39 @@ Release Process To make a new release, please follow this checklist: -- Choose a new PEP440 compliant release number -- Git tag the version -- Push to GitHub and the actions will make a release on pypi -- Push to internal gitlab and do a dls-release.py of the tag -- Check and edit for clarity the autogenerated GitHub release_ +- Choose a new PEP440 compliant release number (see https://peps.python.org/pep-0440/) +- Go to the GitHub release_ page +- Choose ``Draft New Release`` +- Click ``Choose Tag`` and supply the new tag you chose (click create new tag) +- Click ``Generate release notes``, review and edit these notes +- Choose a title and click ``Publish Release`` + +Note that tagging and pushing to the main branch has the same effect except that +you will not get the option to edit the release notes. + +.. _release: https://github.com/epics-containers/python3-pip-skeleton/releases + + +Checking Dependencies +--------------------- + +To see a graph of the python package dependency tree type:: -.. _release: https://dls-controls.github.io/dls-python3-skeleton/releases + pipdeptree Updating the tools ------------------ -This module is merged with the dls-python3-skeleton_. This is a generic +This module is merged with the python3-pip-skeleton_. This is a generic Python project structure which provides a means to keep tools and techniques in sync between multiple Python projects. To update to the latest version of the skeleton, run:: - $ git pull https://github.com/dls-controls/dls-python3-skeleton skeleton + $ git pull https://github.com/dls-controls/python3-pip-skeleton main Any merge conflicts will indicate an area where something has changed that conflicts with the setup of the current module. Check the `closed pull requests -`_ +`_ of the skeleton module for more details. -.. _dls-python3-skeleton: https://dls-controls.github.io/dls-python3-skeleton +.. _python3-pip-skeleton: https://epics-containers.github.io/python3-pip-skeleton diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..76b5dad3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# This file is for use as a devcontainer and a runtime container +# +# The devcontainer should use the build target and run as root with podman +# or docker with user namespaces. +# +FROM python:3.10 as build + +# Add any system dependencies for the developer/build environment here +RUN apt-get update && apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + build-essential \ + busybox \ + git \ + net-tools \ + vim \ + && rm -rf /var/lib/apt/lists/* \ + && busybox --install + +COPY . /project + +RUN cd /project && \ + pip install --upgrade pip build && \ + export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) && \ + python -m build --sdist --wheel && \ + touch requirements.txt + +RUN python -m venv /venv +ENV PATH=/venv/bin:$PATH + +RUN cd /project && \ + pip install --upgrade pip && \ + pip install -r requirements.txt dist/*.whl && \ + pip freeze > dist/requirements.txt && \ + # we don't want to include our own wheel in requirements - remove with sed + # and replace with a comment to avoid a zero length asset upload later + sed -i '/file:/s/^/# Requirements for /' dist/requirements.txt + +FROM python:3.10-slim as runtime + +# Add apt-get system dependecies for runtime here if needed + +COPY --from=build /venv/ /venv/ +ENV PATH=/venv/bin:$PATH + +# change this entrypoint if it is not the same as the repo +ENTRYPOINT ["python3-pip-skeleton"] +CMD ["--version"] diff --git a/Pipfile b/Pipfile deleted file mode 100644 index d1ebb0d9..00000000 --- a/Pipfile +++ /dev/null @@ -1,17 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -dls_python3_skeleton = {editable = true, extras = ["dev"], path = "."} - -[packages] -dls_python3_skeleton = {editable = true, path = "."} - -[scripts] -lint = "pre-commit run --all-files --show-diff-on-failure --color=always -v" -tests = "pytest" -docs = "sphinx-build -EWT --keep-going docs build/html" -# Delete any files that git ignore hides from us -gitclean = "git clean -fdX" diff --git a/README.rst b/README.rst index 9b99e2cb..360d0ef6 100644 --- a/README.rst +++ b/README.rst @@ -1,47 +1,44 @@ -dls-python3-skeleton +python3-pip-skeleton =========================== |code_ci| |docs_ci| |coverage| |pypi_version| |license| +.. note:: + + This project contains template code only. For documentation on how to + adopt this skeleton project see + https://epics-containers.github.io/python3-pip-skeleton-cli + This is where you should write a short paragraph that describes what your module does, how it does it, and why people should use it. ============== ============================================================== -PyPI ``pip install dls_python3_skeleton`` -Source code https://github.com/dls-controls/dls-python3-skeleton -Documentation https://dls-controls.github.io/dls-python3-skeleton -Releases https://github.com/dls-controls/dls-python3-skeleton/releases +PyPI ``pip install python3-pip-skeleton`` +Source code https://github.com/epics-containers/python3-pip-skeleton +Documentation https://epics-containers.github.io/python3-pip-skeleton +Releases https://github.com/epics-containers/python3-pip-skeleton/releases ============== ============================================================== This is where you should put some images or code snippets that illustrate some relevant examples. If it is a library then you might put some -introductory code here: - -.. code:: python - - from dls_python3_skeleton.hello import HelloClass - - hello = HelloClass("me") - print(hello.format_greeting()) - -Or if it is a commandline tool then you might put some example commands here:: +introductory code here. - dls-python3-skeleton person --times=2 +Or if it is a commandline tool then you might put some example commands here. -.. |code_ci| image:: https://github.com/dls-controls/dls-python3-skeleton/workflows/Code%20CI/badge.svg?branch=master - :target: https://github.com/dls-controls/dls-python3-skeleton/actions?query=workflow%3A%22Code+CI%22 +.. |code_ci| image:: https://github.com/epics-containers/python3-pip-skeleton/workflows/Code%20CI/badge.svg?branch=main + :target: https://github.com/epics-containers/python3-pip-skeleton/actions?query=workflow%3A%22Code+CI%22 :alt: Code CI -.. |docs_ci| image:: https://github.com/dls-controls/dls-python3-skeleton/workflows/Docs%20CI/badge.svg?branch=master - :target: https://github.com/dls-controls/dls-python3-skeleton/actions?query=workflow%3A%22Docs+CI%22 +.. |docs_ci| image:: https://github.com/epics-containers/python3-pip-skeleton/workflows/Docs%20CI/badge.svg?branch=main + :target: https://github.com/epics-containers/python3-pip-skeleton/actions?query=workflow%3A%22Docs+CI%22 :alt: Docs CI -.. |coverage| image:: https://codecov.io/gh/dls-controls/dls-python3-skeleton/branch/master/graph/badge.svg - :target: https://codecov.io/gh/dls-controls/dls-python3-skeleton +.. |coverage| image:: https://codecov.io/gh/epics-containers/python3-pip-skeleton/branch/main/graph/badge.svg + :target: https://codecov.io/gh/epics-containers/python3-pip-skeleton :alt: Test Coverage -.. |pypi_version| image:: https://img.shields.io/pypi/v/dls_python3_skeleton.svg - :target: https://pypi.org/project/dls_python3_skeleton +.. |pypi_version| image:: https://img.shields.io/pypi/v/python3-pip-skeleton.svg + :target: https://pypi.org/project/python3-pip-skeleton :alt: Latest PyPI version .. |license| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg @@ -52,4 +49,4 @@ Or if it is a commandline tool then you might put some example commands here:: Anything below this line is used when viewing README.rst and will be replaced when included in index.rst -See https://dls-controls.github.io/dls-python3-skeleton for more detailed documentation. +See https://epics-containers.github.io/python3-pip-skeleton for more detailed documentation. diff --git a/docs/conf.py b/docs/conf.py index f2ca6a2d..bee41d4b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,20 +4,20 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html -import dls_python3_skeleton +import python3_pip_skeleton # -- General configuration ------------------------------------------------ # General information about the project. -project = "dls-python3-skeleton" +project = "python3-pip-skeleton" # The full version, including alpha/beta/rc tags. -release = dls_python3_skeleton.__version__ +release = python3_pip_skeleton.__version__ # The short X.Y version. if "+" in release: # Not on a tag - version = "master" + version = "main" else: version = release @@ -42,7 +42,17 @@ # generating warnings in "nitpicky mode". Note that type should include the # domain name if present. Example entries would be ('py:func', 'int') or # ('envvar', 'LD_LIBRARY_PATH'). -nitpick_ignore = [("py:func", "int")] +nitpick_ignore = [ + ("py:class", "NoneType"), + ("py:class", "'str'"), + ("py:class", "'float'"), + ("py:class", "'int'"), + ("py:class", "'bool'"), + ("py:class", "'object'"), + ("py:class", "'id'"), + ("py:class", "apischema.utils.UndefinedType"), + ("py:class", "typing_extensions.Literal"), +] # Both the class’ and the __init__ method’s docstring are concatenated and # inserted into the main body of the autoclass directive @@ -88,6 +98,9 @@ http://www.diamond.ac.uk """ +# Ignore localhost links for period check that links in docs are valid +linkcheck_ignore = [r"http://localhost:\d+/"] + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for diff --git a/docs/explanations.rst b/docs/explanations.rst index 39de4e6b..1e329673 100644 --- a/docs/explanations.rst +++ b/docs/explanations.rst @@ -8,4 +8,4 @@ Explanation of how the library works and why it works that way. .. toctree:: :caption: Explanations - explanations/why-is-something-so + explanations/decisions diff --git a/docs/explanations/decisions.rst b/docs/explanations/decisions.rst new file mode 100644 index 00000000..5841e6ea --- /dev/null +++ b/docs/explanations/decisions.rst @@ -0,0 +1,17 @@ +.. This Source Code Form is subject to the terms of the Mozilla Public +.. License, v. 2.0. If a copy of the MPL was not distributed with this +.. file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Architectural Decision Records +============================== + +We record major architectural decisions in Architecture Decision Records (ADRs), +as `described by Michael Nygard +`_. +Below is the list of our current ADRs. + +.. toctree:: + :maxdepth: 1 + :glob: + + decisions/* \ No newline at end of file diff --git a/docs/explanations/decisions/0001-record-architecture-decisions.rst b/docs/explanations/decisions/0001-record-architecture-decisions.rst new file mode 100644 index 00000000..b2d3d0fe --- /dev/null +++ b/docs/explanations/decisions/0001-record-architecture-decisions.rst @@ -0,0 +1,26 @@ +1. Record architecture decisions +================================ + +Date: 2022-02-18 + +Status +------ + +Accepted + +Context +------- + +We need to record the architectural decisions made on this project. + +Decision +-------- + +We will use Architecture Decision Records, as `described by Michael Nygard +`_. + +Consequences +------------ + +See Michael Nygard's article, linked above. To create new ADRs we will copy and +paste from existing ones. diff --git a/docs/explanations/why-is-something-so.rst b/docs/explanations/why-is-something-so.rst deleted file mode 100644 index 21708377..00000000 --- a/docs/explanations/why-is-something-so.rst +++ /dev/null @@ -1,7 +0,0 @@ -Why is something the way it is -============================== - -Often, reading the code will not explain *why* it is written that way. These -explanations should be grouped together in articles here. They might include -history of dls-python3-skeleton, architectural decisions, or the -real world tests that influenced the design of dls-python3-skeleton. diff --git a/docs/how-to.rst b/docs/how-to.rst index 86b2ddbd..700797cc 100644 --- a/docs/how-to.rst +++ b/docs/how-to.rst @@ -8,4 +8,4 @@ Practical step-by-step guides for the more experienced user. .. toctree:: :caption: How-to Guides - how-to/accomplish-a-task + how-to/contributing diff --git a/docs/how-to/accomplish-a-task.rst b/docs/how-to/accomplish-a-task.rst deleted file mode 100644 index 8ee49390..00000000 --- a/docs/how-to/accomplish-a-task.rst +++ /dev/null @@ -1,7 +0,0 @@ -How to accomplish a task -======================== - -Here you would explain how to use dls-python3-skeleton to accomplish -a particular task. It doesn't have to be an exhaustive guide like the tutorials, -just enough information to show someone who knows what they want to do, how to -accomplish that task. diff --git a/docs/reference/contributing.rst b/docs/how-to/contributing.rst similarity index 100% rename from docs/reference/contributing.rst rename to docs/how-to/contributing.rst diff --git a/docs/images/dls-logo.svg b/docs/images/dls-logo.svg index 79ba266f..0af1a177 100644 --- a/docs/images/dls-logo.svg +++ b/docs/images/dls-logo.svg @@ -8,4 +8,4 @@ - + \ No newline at end of file diff --git a/docs/reference.rst b/docs/reference.rst index 3f01ee35..bfa7a4f4 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -3,14 +3,13 @@ Reference ========= -Practical step-by-step guides for the more experienced user. +Technical reference material including APIs and release notes. .. toctree:: :caption: Reference reference/api - reference/contributing - Releases + Releases Index .. diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 06bf3c66..8544e172 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -1,24 +1,14 @@ API === -.. automodule:: dls_python3_skeleton +.. automodule:: python3_pip_skeleton - ``dls_python3_skeleton`` + ``python3_pip_skeleton`` ----------------------------------- -This is the internal API reference for dls_python3_skeleton +This is the internal API reference for python3_pip_skeleton -You can mix verbose text with docstring and signature extraction by -using ``autoclass`` and ``autofunction`` directives instead of -``automodule`` below. - -.. data:: dls_python3_skeleton.__version__ +.. data:: python3_pip_skeleton.__version__ :type: str - Version number as calculated by https://github.com/dls-controls/versiongit - -.. automodule:: dls_python3_skeleton.hello - :members: - - ``dls_python3_skeleton.hello`` - ----------------------------------------- + Version number as calculated by https://github.com/pypa/setuptools_scm diff --git a/docs/tutorials/installation.rst b/docs/tutorials/installation.rst index d23580a3..bbffd015 100644 --- a/docs/tutorials/installation.rst +++ b/docs/tutorials/installation.rst @@ -1,20 +1,10 @@ Installation ============ -.. note:: - - For installation inside DLS, please see the internal documentation on - ``dls-python3`` and ``pipenv``. Although these instructions will work - inside DLS, they are intended for external use. - - If you want to contribute to the library itself, please follow - the `../reference/contributing` instructions. - - Check your version of python ---------------------------- -You will need python 3.7 or later. You can check your version of python by +You will need python 3.8 or later. You can check your version of python by typing into a terminal:: python3 --version @@ -35,14 +25,21 @@ Installing the library You can now use ``pip`` to install the library:: - python3 -m pip install dls_python3_skeleton + python3 -m pip install python3-pip-skeleton If you require a feature that is not currently released you can also install from github:: - python3 -m pip install git+git://github.com/dls-controls/dls-python3-skeleton.git + python3 -m pip install git+https://github.com/epics-containers/python3-pip-skeleton.git The library should now be installed and the commandline interface on your path. You can check the version that has been installed by typing:: - dls-python3-skeleton --version + python3-pip-skeleton --version + +Running in a container +---------------------- + +To pull the container from github container registry and run:: + + docker run ghcr.io/epics-containers/python3-pip-skeleton:main --version diff --git a/pyproject.toml b/pyproject.toml index ccd70c76..1b8c998a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -# To get a reproducible wheel, wheel must be pinned to the same version as in -# dls-python3, and setuptools must produce the same dist-info. Cap setuptools -# to the last version that didn't add License-File to METADATA -requires = ["setuptools<57", "wheel==0.33.1"] +requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2", "wheel"] build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "src/python3_pip_skeleton/_version.py" diff --git a/setup.cfg b/setup.cfg index 167860a5..008fd8a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,24 +1,29 @@ [metadata] -name = dls_python3_skeleton +name = python3-pip-skeleton description = One line description of your module -url = https://github.com/dls-controls/dls-python3-skeleton +url = https://github.com/epics-containers/python3-pip-skeleton author = Firstname Lastname author_email = email@address.com license = Apache License 2.0 long_description = file: README.rst long_description_content_type = text/x-rst classifiers = - Development Status :: 4 - Beta License :: OSI Approved :: Apache Software License - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 [options] -python_requires = >=3.7 +python_requires = >=3.8 packages = find: +# =src is interpreted as {"": "src"} +# as per recommendation here https://hynek.me/articles/testing-packaging/ package_dir = =src + +setup_requires = + setuptools_scm[toml]>=6.2 + # Specify any package dependencies below. # install_requires = # numpy @@ -27,26 +32,32 @@ package_dir = [options.extras_require] # For development tests/docs dev = - black==21.9b0 + black==22.6.0 + flake8-isort isort>5.0 - pytest-cov mypy - flake8-isort - sphinx-rtd-theme-github-versions + pipdeptree pre-commit + pytest-cov + setuptools_scm[toml]>=6.2 + sphinx-rtd-theme-github-versions + tox + types-mock [options.packages.find] where = src +# Don't include our tests directory in the distribution +exclude = tests # Specify any package data to be included in the wheel below. # [options.package_data] -# dls_python3_skeleton = +# python3_pip_skeleton = # subpackage/*.yaml [options.entry_points] # Include a command line script console_scripts = - dls-python3-skeleton = dls_python3_skeleton.__main__:main + python3-pip-skeleton = python3_pip_skeleton.__main__:main [mypy] # Ignore missing stubs for modules we use @@ -55,7 +66,6 @@ ignore_missing_imports = True [isort] profile=black float_to_top=true -skip=setup.py,conf.py,build [flake8] # Make flake8 respect black's line length (default 88), @@ -63,22 +73,52 @@ max-line-length = 88 extend-ignore = E203, # See https://github.com/PyCQA/pycodestyle/issues/373 F811, # support typing.overload decorator + F722, # allow Annotated[typ, some_func("some string")] +exclude = + ui_* + .tox + .venv [tool:pytest] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error addopts = --tb=native -vv --doctest-modules --doctest-glob="*.rst" - --cov=dls_python3_skeleton --cov-report term --cov-report xml:cov.xml + --cov=python3_pip_skeleton --cov-report term --cov-report xml:cov.xml # https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings filterwarnings = error +# Doctest python code in docs, python code in src docstrings, test functions in tests +testpaths = + docs src tests [coverage:run] -# This is covered in the versiongit test suite so exclude it here -omit = */_version_git.py -data_file = /tmp/dls_python3_skeleton.coverage +data_file = /tmp/python3_pip_skeleton.coverage [coverage:paths] # Tests are run from installed location, map back to the src directory source = src **/site-packages/ + +# Use tox to provide parallel linting and testing +# NOTE that we pre-install all tools in the dev dependencies (including tox). +# Hence the use of allowlist_externals instead of using the tox virtualenvs. +# This ensures a match between developer time tools in the IDE and tox tools. +[tox:tox] +minversion = 3.7 +skipsdist=true +skipinstall=true + +[testenv:{pre-commit,mypy,pytest,docs}] +passenv = + PYTHONPATH + HOME +allowlist_externals = + pytest + pre-commit + mypy + sphinx-build +commands = + pytest: pytest tests {posargs} + mypy: mypy src tests {posargs} + pre-commit: pre-commit run --all-files {posargs} + docs: sphinx-build -EWT --keep-going docs build/html {posargs} diff --git a/setup.py b/setup.py deleted file mode 100644 index 39de827e..00000000 --- a/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -# type: ignore -import glob -import importlib.util - -from setuptools import setup - -# Import ._version_git.py without importing -path = glob.glob(__file__.replace("setup.py", "src/*/_version_git.py"))[0] -spec = importlib.util.spec_from_file_location("_version_git", path) -vg = importlib.util.module_from_spec(spec) -spec.loader.exec_module(vg) - -setup(cmdclass=vg.get_cmdclass(), version=vg.__version__) diff --git a/src/dls_python3_skeleton/__init__.py b/src/dls_python3_skeleton/__init__.py deleted file mode 100644 index 25eb98fa..00000000 --- a/src/dls_python3_skeleton/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from . import hello -from ._version_git import __version__ - -# __all__ defines the public API for the package. -# Each module also defines its own __all__. -__all__ = ["__version__", "hello"] diff --git a/src/dls_python3_skeleton/__main__.py b/src/dls_python3_skeleton/__main__.py deleted file mode 100644 index 960eff10..00000000 --- a/src/dls_python3_skeleton/__main__.py +++ /dev/null @@ -1,20 +0,0 @@ -from argparse import ArgumentParser - -from . import __version__ -from .hello import HelloClass, say_hello_lots - -__all__ = ["main"] - - -def main(args=None): - parser = ArgumentParser() - parser.add_argument("--version", action="version", version=__version__) - parser.add_argument("name", help="Name of the person to greet") - parser.add_argument("--times", type=int, default=5, help="Number of times to greet") - args = parser.parse_args(args) - say_hello_lots(HelloClass(args.name), args.times) - - -# test with: pipenv run python -m dls_python3_skeleton -if __name__ == "__main__": - main() diff --git a/src/dls_python3_skeleton/_version_git.py b/src/dls_python3_skeleton/_version_git.py deleted file mode 100644 index bb7f0c2a..00000000 --- a/src/dls_python3_skeleton/_version_git.py +++ /dev/null @@ -1,100 +0,0 @@ -# Compute a version number from a git repo or archive - -# This file is released into the public domain. Generated by: -# versiongit-2.1 (https://github.com/dls-controls/versiongit) -import re -import sys -from pathlib import Path -from subprocess import STDOUT, CalledProcessError, check_output - -# These will be filled in if git archive is run or by setup.py cmdclasses -GIT_REFS = "$Format:%D$" -GIT_SHA1 = "$Format:%h$" - -# Git describe gives us sha1, last version-like tag, and commits since then -CMD = "git describe --tags --dirty --always --long --match=[0-9]*[-.][0-9]*" - - -def get_version_from_git(path=None): - """Try to parse version from git describe, fallback to git archive tags.""" - tag, plus, suffix = "0.0", "untagged", "" - if not GIT_SHA1.startswith("$"): - # git archive or the cmdclasses below have filled in these strings - sha1 = GIT_SHA1 - for ref_name in GIT_REFS.split(", "): - if ref_name.startswith("tag: "): - # git from 1.8.3 onwards labels archive tags "tag: TAGNAME" - tag, plus = ref_name[5:], "0" - else: - if path is None: - # If no path to git repo, choose the directory this file is in - path = Path(__file__).absolute().parent - # output is TAG-NUM-gHEX[-dirty] or HEX[-dirty] - try: - cmd_out = check_output(CMD.split(), stderr=STDOUT, cwd=path) - except Exception as e: - sys.stderr.write("%s: %s\n" % (type(e).__name__, str(e))) - if isinstance(e, CalledProcessError): - sys.stderr.write("-> %s" % e.output.decode()) - return "0.0+unknown", None, e - else: - out = cmd_out.decode().strip() - if out.endswith("-dirty"): - out = out[:-6] - suffix = ".dirty" - if "-" in out: - # There is a tag, extract it and the other pieces - match = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", out) - tag, plus, sha1 = match.groups() - else: - # No tag, just sha1 - sha1 = out - # Replace dashes in tag for dots - tag = tag.replace("-", ".") - if plus != "0" or suffix: - # Not on a tag, add additional info - tag = f"{tag}+{plus}.g{sha1}{suffix}" - return tag, sha1, None - - -__version__, git_sha1, git_error = get_version_from_git() - - -def get_cmdclass(build_py=None, sdist=None): - """Create cmdclass dict to pass to setuptools.setup. - - Create cmdclass dict to pass to setuptools.setup which will write a - _version_static.py file in our resultant sdist, wheel or egg. - """ - if build_py is None: - from setuptools.command.build_py import build_py - if sdist is None: - from setuptools.command.sdist import sdist - - def make_version_static(base_dir: str, pkg: str): - vg = Path(base_dir) / pkg.split(".")[0] / "_version_git.py" - if vg.is_file(): - lines = open(vg).readlines() - with open(vg, "w") as f: - for line in lines: - # Replace GIT_* with static versions - if line.startswith("GIT_SHA1 = "): - f.write("GIT_SHA1 = '%s'\n" % git_sha1) - elif line.startswith("GIT_REFS = "): - f.write("GIT_REFS = 'tag: %s'\n" % __version__) - else: - f.write(line) - - class BuildPy(build_py): - def run(self): - build_py.run(self) - for pkg in self.packages: - make_version_static(self.build_lib, pkg) - - class Sdist(sdist): - def make_release_tree(self, base_dir, files): - sdist.make_release_tree(self, base_dir, files) - for pkg in self.distribution.packages: - make_version_static(base_dir, pkg) - - return dict(build_py=BuildPy, sdist=Sdist) diff --git a/src/dls_python3_skeleton/hello.py b/src/dls_python3_skeleton/hello.py deleted file mode 100644 index c0b09939..00000000 --- a/src/dls_python3_skeleton/hello.py +++ /dev/null @@ -1,41 +0,0 @@ -# The purpose of __all__ is to define the public API of this module, and which -# objects are imported if we call "from dls_python3_skeleton.hello import *" -__all__ = [ - "HelloClass", - "say_hello_lots", -] - - -class HelloClass: - """A class whose only purpose in life is to say hello""" - - def __init__(self, name: str): - """ - Args: - name: The initial value of the name of the person who gets greeted - """ - #: The name of the person who gets greeted - self.name = name - - def format_greeting(self) -> str: - """Return a greeting for `name` - - >>> HelloClass("me").format_greeting() - 'Hello me' - """ - greeting = f"Hello {self.name}" - return greeting - - -def say_hello_lots(hello: HelloClass = None, times=5): - """Print lots of greetings using the given `HelloClass` - - Args: - hello: A `HelloClass` that `format_greeting` will be called on. - If not given, use a HelloClass with name="me" - times: The number of times to call it - """ - if hello is None: - hello = HelloClass("me") - for _ in range(times): - print(hello.format_greeting()) diff --git a/src/python3_pip_skeleton/__init__.py b/src/python3_pip_skeleton/__init__.py new file mode 100644 index 00000000..0fe6655f --- /dev/null +++ b/src/python3_pip_skeleton/__init__.py @@ -0,0 +1,12 @@ +try: + # Use live version from git + from setuptools_scm import get_version + + # Warning: If the install is nested to the same depth, this will always succeed + __version__ = get_version(root="../../", relative_to=__file__) + del get_version +except (ImportError, LookupError): + # Use installed version + from ._version import __version__ + +__all__ = ["__version__"] diff --git a/src/python3_pip_skeleton/__main__.py b/src/python3_pip_skeleton/__main__.py new file mode 100644 index 00000000..1a97fb44 --- /dev/null +++ b/src/python3_pip_skeleton/__main__.py @@ -0,0 +1,16 @@ +from argparse import ArgumentParser + +from . import __version__ + +__all__ = ["main"] + + +def main(args=None): + parser = ArgumentParser() + parser.add_argument("--version", action="version", version=__version__) + args = parser.parse_args(args) + + +# test with: pipenv run python -m python3_pip_skeleton +if __name__ == "__main__": + main() diff --git a/tests/test_boilerplate_removed.py b/tests/test_boilerplate_removed.py index ca8086cb..f5204fa9 100644 --- a/tests/test_boilerplate_removed.py +++ b/tests/test_boilerplate_removed.py @@ -9,7 +9,7 @@ def skeleton_check(check: bool, text: str): - if ROOT.name == "dls-python3-skeleton": + if ROOT.name == "python3-pip-skeleton" or str(ROOT) == "/project": # In the skeleton module the check should fail check = not check text = f"Skeleton didn't raise: {text}" @@ -24,11 +24,6 @@ def assert_not_contains_text(path: str, text: str, explanation: str): skeleton_check(text in contents, f"Please change ./{path} {explanation}") -def assert_not_exists(path: str, explanation: str): - exists = (ROOT / path).exists() - skeleton_check(exists, f"Please delete ./{path} {explanation}") - - # setup.cfg def test_module_description(): conf = configparser.ConfigParser() @@ -50,30 +45,17 @@ def test_changed_README_intro(): ) -def test_changed_README_body(): +def test_removed_adopt_skeleton(): assert_not_contains_text( "README.rst", - "This is where you should put some images or code snippets", - "to include some features and why people should use it", + "This project contains template code only", + "remove the note at the start", ) -# Docs -def test_docs_ref_api_changed(): +def test_changed_README_body(): assert_not_contains_text( - "docs/reference/api.rst", - "You can mix verbose text with docstring and signature", - "to introduce the API for your module", - ) - - -def test_how_tos_written(): - assert_not_exists( - "docs/how-to/accomplish-a-task.rst", "and write some docs/how-tos" - ) - - -def test_explanations_written(): - assert_not_exists( - "docs/explanations/why-is-something-so.rst", "and write some docs/explanations" + "README.rst", + "This is where you should put some images or code snippets", + "to include some features and why people should use it", ) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..2ff648c0 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,9 @@ +import subprocess +import sys + +from python3_pip_skeleton import __version__ + + +def test_cli_version(): + cmd = [sys.executable, "-m", "python3_pip_skeleton", "--version"] + assert subprocess.check_output(cmd).decode().strip() == __version__ diff --git a/tests/test_dls_python3_skeleton.py b/tests/test_dls_python3_skeleton.py deleted file mode 100644 index fffef4b4..00000000 --- a/tests/test_dls_python3_skeleton.py +++ /dev/null @@ -1,28 +0,0 @@ -import subprocess -import sys - -from dls_python3_skeleton import __main__, __version__, hello - - -def test_hello_class_formats_greeting() -> None: - inst = hello.HelloClass("person") - assert inst.format_greeting() == "Hello person" - - -def test_hello_lots_defaults(capsys) -> None: - hello.say_hello_lots() - captured = capsys.readouterr() - assert captured.out == "Hello me\n" * 5 - assert captured.err == "" - - -def test_cli_greets(capsys) -> None: - __main__.main(["person", "--times=2"]) - captured = capsys.readouterr() - assert captured.out == "Hello person\n" * 2 - assert captured.err == "" - - -def test_cli_version(): - cmd = [sys.executable, "-m", "dls_python3_skeleton", "--version"] - assert subprocess.check_output(cmd).decode().strip() == __version__ From 52a0239c36920828c63207a77487f16709565a04 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Fri, 2 Sep 2022 10:39:24 +0000 Subject: [PATCH 03/79] switch org to DiamondLightSource --- CONTRIBUTING.rst | 14 +++++++------- README.rst | 22 +++++++++++----------- docs/reference.rst | 2 +- docs/tutorials/installation.rst | 4 ++-- setup.cfg | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 327e3920..8f7950d1 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -7,7 +7,7 @@ filing a new one. If you have a great idea but it involves big changes, please file a ticket before making a pull request! We want to make sure you don't spend your time coding something that might not fit the scope of the project. -.. _GitHub: https://github.com/epics-containers/python3-pip-skeleton/issues +.. _GitHub: https://github.com/DiamondLightSource/python3-pip-skeleton/issues Running the tests ----------------- @@ -17,7 +17,7 @@ To run in a container Use vscode devcontainer as follows:: - $ git clone git://github.com/epics-containers/python3-pip-skeleton.git + $ git clone git://github.com/DiamondLightSource/python3-pip-skeleton.git $ vscode python3-pip-skeleton Click on 'Reopen in Container' when prompted In a vscode Terminal: @@ -30,7 +30,7 @@ To run locally Get the source source code and run the unit tests directly on your workstation as follows:: - $ git clone git://github.com/epics-containers/python3-pip-skeleton.git + $ git clone git://github.com/DiamondLightSource/python3-pip-skeleton.git $ cd python3-pip-skeleton $ virtualenv .venv $ source .venv/bin/activate @@ -135,7 +135,7 @@ To make a new release, please follow this checklist: Note that tagging and pushing to the main branch has the same effect except that you will not get the option to edit the release notes. -.. _release: https://github.com/epics-containers/python3-pip-skeleton/releases +.. _release: https://github.com/DiamondLightSource/python3-pip-skeleton/releases Checking Dependencies @@ -153,11 +153,11 @@ Python project structure which provides a means to keep tools and techniques in sync between multiple Python projects. To update to the latest version of the skeleton, run:: - $ git pull https://github.com/dls-controls/python3-pip-skeleton main + $ git pull https://github.com/DiamondLightSource/python3-pip-skeleton main Any merge conflicts will indicate an area where something has changed that conflicts with the setup of the current module. Check the `closed pull requests -`_ +`_ of the skeleton module for more details. -.. _python3-pip-skeleton: https://epics-containers.github.io/python3-pip-skeleton +.. _python3-pip-skeleton: https://DiamondLightSource.github.io/python3-pip-skeleton diff --git a/README.rst b/README.rst index 360d0ef6..95d590c3 100644 --- a/README.rst +++ b/README.rst @@ -7,16 +7,16 @@ python3-pip-skeleton This project contains template code only. For documentation on how to adopt this skeleton project see - https://epics-containers.github.io/python3-pip-skeleton-cli + https://DiamondLightSource.github.io/python3-pip-skeleton-cli This is where you should write a short paragraph that describes what your module does, how it does it, and why people should use it. ============== ============================================================== PyPI ``pip install python3-pip-skeleton`` -Source code https://github.com/epics-containers/python3-pip-skeleton -Documentation https://epics-containers.github.io/python3-pip-skeleton -Releases https://github.com/epics-containers/python3-pip-skeleton/releases +Source code https://github.com/DiamondLightSource/python3-pip-skeleton +Documentation https://DiamondLightSource.github.io/python3-pip-skeleton +Releases https://github.com/DiamondLightSource/python3-pip-skeleton/releases ============== ============================================================== This is where you should put some images or code snippets that illustrate @@ -25,16 +25,16 @@ introductory code here. Or if it is a commandline tool then you might put some example commands here. -.. |code_ci| image:: https://github.com/epics-containers/python3-pip-skeleton/workflows/Code%20CI/badge.svg?branch=main - :target: https://github.com/epics-containers/python3-pip-skeleton/actions?query=workflow%3A%22Code+CI%22 +.. |code_ci| image:: https://github.com/DiamondLightSource/python3-pip-skeleton/workflows/Code%20CI/badge.svg?branch=main + :target: https://github.com/DiamondLightSource/python3-pip-skeleton/actions?query=workflow%3A%22Code+CI%22 :alt: Code CI -.. |docs_ci| image:: https://github.com/epics-containers/python3-pip-skeleton/workflows/Docs%20CI/badge.svg?branch=main - :target: https://github.com/epics-containers/python3-pip-skeleton/actions?query=workflow%3A%22Docs+CI%22 +.. |docs_ci| image:: https://github.com/DiamondLightSource/python3-pip-skeleton/workflows/Docs%20CI/badge.svg?branch=main + :target: https://github.com/DiamondLightSource/python3-pip-skeleton/actions?query=workflow%3A%22Docs+CI%22 :alt: Docs CI -.. |coverage| image:: https://codecov.io/gh/epics-containers/python3-pip-skeleton/branch/main/graph/badge.svg - :target: https://codecov.io/gh/epics-containers/python3-pip-skeleton +.. |coverage| image:: https://codecov.io/gh/DiamondLightSource/python3-pip-skeleton/branch/main/graph/badge.svg + :target: https://codecov.io/gh/DiamondLightSource/python3-pip-skeleton :alt: Test Coverage .. |pypi_version| image:: https://img.shields.io/pypi/v/python3-pip-skeleton.svg @@ -49,4 +49,4 @@ Or if it is a commandline tool then you might put some example commands here. Anything below this line is used when viewing README.rst and will be replaced when included in index.rst -See https://epics-containers.github.io/python3-pip-skeleton for more detailed documentation. +See https://DiamondLightSource.github.io/python3-pip-skeleton for more detailed documentation. diff --git a/docs/reference.rst b/docs/reference.rst index bfa7a4f4..84c8cf13 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -9,7 +9,7 @@ Technical reference material including APIs and release notes. :caption: Reference reference/api - Releases + Releases Index .. diff --git a/docs/tutorials/installation.rst b/docs/tutorials/installation.rst index bbffd015..399dc2c5 100644 --- a/docs/tutorials/installation.rst +++ b/docs/tutorials/installation.rst @@ -30,7 +30,7 @@ You can now use ``pip`` to install the library:: If you require a feature that is not currently released you can also install from github:: - python3 -m pip install git+https://github.com/epics-containers/python3-pip-skeleton.git + python3 -m pip install git+https://github.com/DiamondLightSource/python3-pip-skeleton.git The library should now be installed and the commandline interface on your path. You can check the version that has been installed by typing:: @@ -42,4 +42,4 @@ Running in a container To pull the container from github container registry and run:: - docker run ghcr.io/epics-containers/python3-pip-skeleton:main --version + docker run ghcr.io/DiamondLightSource/python3-pip-skeleton:main --version diff --git a/setup.cfg b/setup.cfg index 008fd8a4..f32a3441 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = python3-pip-skeleton description = One line description of your module -url = https://github.com/epics-containers/python3-pip-skeleton +url = https://github.com/DiamondLightSource/python3-pip-skeleton author = Firstname Lastname author_email = email@address.com license = Apache License 2.0 From 13da3015fba3f486b0f784449a87982c491e9bf7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Sep 2022 10:39:50 +0000 Subject: [PATCH 04/79] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/code.yml | 6 +++--- .github/workflows/docs.yml | 2 +- .github/workflows/docs_clean.yml | 2 +- .github/workflows/linkcheck.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 5ea6e1f2..b2f561a7 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup python uses: actions/setup-python@v4 @@ -43,7 +43,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -73,7 +73,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c3c5eda8..09e6f3ef 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,7 +29,7 @@ jobs: run: sudo apt-get install graphviz - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/docs_clean.yml b/.github/workflows/docs_clean.yml index 2059c7f3..f39519f7 100644 --- a/.github/workflows/docs_clean.yml +++ b/.github/workflows/docs_clean.yml @@ -18,7 +18,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: gh-pages diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 6b37f194..e6838560 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 From 0f37bbc885ae54486d890deee65547ae3cc10290 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Sep 2022 09:28:58 +0000 Subject: [PATCH 05/79] Bump black from 22.6.0 to 22.8.0 Bumps [black](https://github.com/psf/black) from 22.6.0 to 22.8.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/22.6.0...22.8.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f32a3441..568ecc48 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,7 @@ setup_requires = [options.extras_require] # For development tests/docs dev = - black==22.6.0 + black==22.8.0 flake8-isort isort>5.0 mypy From 9e16fc2ce9bd9a318b19c31107aeb4d3c64b3811 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Fri, 2 Sep 2022 12:50:57 +0000 Subject: [PATCH 06/79] fix twine command --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index b2f561a7..ebffb2fc 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -174,4 +174,4 @@ jobs: env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: pipx run twine upload dist/*/whl dist/*.tar.gz + run: pipx run twine upload dist/*.whl dist/*.tar.gz From c975f6dfc55dffc395099ba7ae2b9ad28e136814 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Fri, 2 Sep 2022 15:26:48 +0000 Subject: [PATCH 07/79] obscure the docs cleanup email --- .github/workflows/docs_clean.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs_clean.yml b/.github/workflows/docs_clean.yml index f39519f7..e0f7e485 100644 --- a/.github/workflows/docs_clean.yml +++ b/.github/workflows/docs_clean.yml @@ -36,6 +36,6 @@ jobs: rm -r ${{ env.remove_me }} sed -i /${{ env.remove_me }}/d versions.txt git config --global user.name 'GitHub Actions Docs Cleanup CI' - git config --global user.email 'gha@users.noreply.github.com' + git config --global user.email 'GithubActionsCleanup@users.noreply.github.com' git commit -am"removing redundant docs version ${{ env.remove_me }}" git push From 10e23967dbdd36a2e207a1b28f1d79f201dff262 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 9 Sep 2022 14:21:46 +0000 Subject: [PATCH 08/79] Made tox faster with tox-direct --- Dockerfile | 1 + setup.cfg | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index 76b5dad3..056d144e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ RUN cd /project && \ RUN python -m venv /venv ENV PATH=/venv/bin:$PATH +ENV TOX_DIRECT=1 RUN cd /project && \ pip install --upgrade pip && \ diff --git a/setup.cfg b/setup.cfg index 568ecc48..5d9ac269 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,7 @@ dev = setuptools_scm[toml]>=6.2 sphinx-rtd-theme-github-versions tox + tox-direct types-mock [options.packages.find] @@ -75,9 +76,8 @@ extend-ignore = F811, # support typing.overload decorator F722, # allow Annotated[typ, some_func("some string")] exclude = - ui_* .tox - .venv + venv [tool:pytest] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error @@ -103,22 +103,22 @@ source = # NOTE that we pre-install all tools in the dev dependencies (including tox). # Hence the use of allowlist_externals instead of using the tox virtualenvs. # This ensures a match between developer time tools in the IDE and tox tools. +# Setting TOX_DIRECT=1 in the environment will make this even faster [tox:tox] -minversion = 3.7 -skipsdist=true -skipinstall=true - -[testenv:{pre-commit,mypy,pytest,docs}] -passenv = - PYTHONPATH - HOME -allowlist_externals = - pytest - pre-commit - mypy - sphinx-build -commands = - pytest: pytest tests {posargs} - mypy: mypy src tests {posargs} - pre-commit: pre-commit run --all-files {posargs} - docs: sphinx-build -EWT --keep-going docs build/html {posargs} +skipsdist = True + +[testenv:pytest] +allowlist_externals = pytest +commands = pytest {posargs} + +[testenv:mypy] +allowlist_externals = mypy +commands = mypy src tests {posargs} + +[testenv:pre-commit] +allowlist_externals = pre-commit +commands = pre-commit run --all-files {posargs} + +[testenv:docs] +allowlist_externals = sphinx-build +commands = sphinx-build -EWT --keep-going docs build/html {posargs} From d7a937a22bdf6c95c2ae9a49dfbce9e9509e1c9f Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 9 Sep 2022 15:45:31 +0000 Subject: [PATCH 09/79] Switch to pydata-theme and split docs There are now developer and user guides. The version switcher is native to the theme so added a script to generate it. --- .github/CONTRIBUTING.rst | 35 ++++ .github/pages/make_switcher.py | 99 +++++++++++ .github/workflows/code.yml | 5 +- .github/workflows/docs.yml | 6 +- .github/workflows/docs_clean.yml | 7 +- .vscode/extensions.json | 4 +- CONTRIBUTING.rst | 163 ------------------ Dockerfile | 4 +- README.rst | 16 +- docs/_static/theme_overrides.css | 34 ---- docs/conf.py | 80 +++++++-- .../explanations/decisions.rst | 0 .../0001-record-architecture-decisions.rst | 0 docs/developer/how-to/build-docs.rst | 20 +++ docs/developer/how-to/contribute.rst | 1 + docs/developer/how-to/lint.rst | 38 ++++ docs/developer/how-to/make-release.rst | 16 ++ docs/developer/how-to/run-tests.rst | 12 ++ docs/developer/how-to/static-analysis.rst | 8 + docs/developer/how-to/update-tools.rst | 16 ++ docs/developer/index.rst | 62 +++++++ docs/developer/reference/standards.rst | 64 +++++++ docs/developer/tutorials/dev-install.rst | 60 +++++++ docs/explanations.rst | 11 -- docs/genindex.rst | 5 + docs/how-to.rst | 11 -- docs/how-to/contributing.rst | 1 - docs/index.rst | 51 ++---- docs/reference.rst | 17 -- docs/tutorials.rst | 11 -- docs/user/explanations/docs-structure.rst | 18 ++ docs/user/how-to/run-container.rst | 15 ++ docs/user/index.rst | 57 ++++++ docs/{ => user}/reference/api.rst | 0 docs/{ => user}/tutorials/installation.rst | 21 +-- setup.cfg | 6 +- src/python3_pip_skeleton/__main__.py | 2 +- 37 files changed, 640 insertions(+), 336 deletions(-) create mode 100644 .github/CONTRIBUTING.rst create mode 100755 .github/pages/make_switcher.py delete mode 100644 CONTRIBUTING.rst delete mode 100644 docs/_static/theme_overrides.css rename docs/{ => developer}/explanations/decisions.rst (100%) rename docs/{ => developer}/explanations/decisions/0001-record-architecture-decisions.rst (100%) create mode 100644 docs/developer/how-to/build-docs.rst create mode 100644 docs/developer/how-to/contribute.rst create mode 100644 docs/developer/how-to/lint.rst create mode 100644 docs/developer/how-to/make-release.rst create mode 100644 docs/developer/how-to/run-tests.rst create mode 100644 docs/developer/how-to/static-analysis.rst create mode 100644 docs/developer/how-to/update-tools.rst create mode 100644 docs/developer/index.rst create mode 100644 docs/developer/reference/standards.rst create mode 100644 docs/developer/tutorials/dev-install.rst delete mode 100644 docs/explanations.rst create mode 100644 docs/genindex.rst delete mode 100644 docs/how-to.rst delete mode 100644 docs/how-to/contributing.rst delete mode 100644 docs/reference.rst delete mode 100644 docs/tutorials.rst create mode 100644 docs/user/explanations/docs-structure.rst create mode 100644 docs/user/how-to/run-container.rst create mode 100644 docs/user/index.rst rename docs/{ => user}/reference/api.rst (100%) rename docs/{ => user}/tutorials/installation.rst (56%) diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst new file mode 100644 index 00000000..19ab494f --- /dev/null +++ b/.github/CONTRIBUTING.rst @@ -0,0 +1,35 @@ +Contributing to the project +=========================== + +Contributions and issues are most welcome! All issues and pull requests are +handled through GitHub_. Also, please check for any existing issues before +filing a new one. If you have a great idea but it involves big changes, please +file a ticket before making a pull request! We want to make sure you don't spend +your time coding something that might not fit the scope of the project. + +.. _GitHub: https://github.com/DiamondLightSource/python3-pip-skeleton/issues + +Issue or Discussion? +-------------------- + +Github also offers discussions_ as a place to ask questions and share ideas. If +your issue is open ended and it is not obvious when it can be "closed", please +raise it as a discussion instead. + +.. _discussions: https://github.com/DiamondLightSource/python3-pip-skeleton/discussions + +Code coverage +------------- + +While 100% code coverage does not make a library bug-free, it significantly +reduces the number of easily caught bugs! Please make sure coverage remains the +same or is improved by a pull request! + +Developer guide +--------------- + +The `Developer Guide`_ contains information on setting up a development +environment, running the tests and what standards the code and documentation +should follow. + +.. _Developer Guide: https://diamondlightsource.github.io/python3-pip-skeleton/main/developer/how-to/contribute.html diff --git a/.github/pages/make_switcher.py b/.github/pages/make_switcher.py new file mode 100755 index 00000000..5c65d788 --- /dev/null +++ b/.github/pages/make_switcher.py @@ -0,0 +1,99 @@ +import json +import logging +from argparse import ArgumentParser +from pathlib import Path +from subprocess import CalledProcessError, check_output +from typing import List, Optional + + +def report_output(stdout: bytes, label: str) -> List[str]: + ret = stdout.decode().strip().split("\n") + print(f"{label}: {ret}") + return ret + + +def get_branch_contents(ref: str) -> List[str]: + """Get the list of directories in a branch.""" + stdout = check_output(["git", "ls-tree", "-d", "--name-only", ref]) + return report_output(stdout, "Branch contents") + + +def get_sorted_tags_list() -> List[str]: + """Get a list of sorted tags in descending order from the repository.""" + stdout = check_output(["git", "tag", "-l", "--sort=-v:refname"]) + return report_output(stdout, "Tags list") + + +def get_versions(ref: str, add: Optional[str], remove: Optional[str]) -> List[str]: + """Generate the file containing the list of all GitHub Pages builds.""" + # Get the directories (i.e. builds) from the GitHub Pages branch + try: + builds = set(get_branch_contents(ref)) + except CalledProcessError: + builds = set() + logging.warning(f"Cannot get {ref} contents") + + # Add and remove from the list of builds + if add: + builds.add(add) + if remove: + assert remove in builds, f"Build '{remove}' not in {sorted(builds)}" + builds.remove(remove) + + # Get a sorted list of tags + tags = get_sorted_tags_list() + + # Make the sorted versions list from main branches and tags + versions: List[str] = [] + for version in ["master", "main"] + tags: + if version in builds: + versions.append(version) + builds.remove(version) + + # Add in anything that is left to the bottom + versions += sorted(builds) + print(f"Sorted versions: {versions}") + return versions + + +def write_json(path: Path, repository: str, versions: str): + org, repo_name = repository.split("/") + struct = [ + dict(name=version, url=f"https://{org}.github.io/{repo_name}/{version}/") + for version in versions + ] + text = json.dumps(struct, indent=2) + print(f"JSON switcher:\n{text}") + path.write_text(text) + + +def main(args=None): + parser = ArgumentParser( + description="Make a versions.txt file from gh-pages directories" + ) + parser.add_argument( + "--add", + help="Add this directory to the list of existing directories", + ) + parser.add_argument( + "--remove", + help="Remove this directory from the list of existing directories", + ) + parser.add_argument( + "repository", + help="The GitHub org and repository name: ORG/REPO", + ) + parser.add_argument( + "output", + type=Path, + help="Path of write switcher.json to", + ) + args = parser.parse_args(args) + + # Write the versions file + versions = get_versions("origin/gh-pages", args.add, args.remove) + write_json(args.output, args.repository, versions) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index ebffb2fc..0aae7fb5 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -133,7 +133,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: dist - path: dist/* + path: dist sdist: needs: container @@ -164,8 +164,7 @@ jobs: uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14 with: prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} - files: | - dist/* + files: dist/* generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 09e6f3ef..a684d031 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -43,10 +43,10 @@ jobs: - name: Move to versioned directory # e.g. main or 0.1.2 - run: mv build/html ".github/pages/${GITHUB_REF##*/}" + run: mv build/html ".github/pages/${{ github.ref_name }}" - - name: Write versions.txt - run: sphinx_rtd_theme_github_versions .github/pages + - name: Write switcher.json + run: python .github/pages/make_switcher.py --add "${{ github.ref_name }}" ${{ github.repository }} .github/pages/switcher.json - name: Publish Docs to gh-pages if: github.event_name == 'push' diff --git a/.github/workflows/docs_clean.yml b/.github/workflows/docs_clean.yml index e0f7e485..b80e4c22 100644 --- a/.github/workflows/docs_clean.yml +++ b/.github/workflows/docs_clean.yml @@ -32,10 +32,9 @@ jobs: - name: update index and push changes run: | - echo removing redundant documentation version ${{ env.remove_me }} rm -r ${{ env.remove_me }} - sed -i /${{ env.remove_me }}/d versions.txt + python make_switcher.py --remove ${{ env.remove_me }} ${{ github.repository }} switcher.json git config --global user.name 'GitHub Actions Docs Cleanup CI' - git config --global user.email 'GithubActionsCleanup@users.noreply.github.com' - git commit -am"removing redundant docs version ${{ env.remove_me }}" + git config --global user.email 'GithubActionsCleanup@noreply.github.com' + git commit -am"removing redundant docs version ${{ env.remove_me }}" git push diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d173f8d6..041f8944 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,8 +1,8 @@ { "recommendations": [ - "ms-vscode-remote.remote-containers" + "ms-vscode-remote.remote-containers", "ms-python.vscode-pylance", "ms-python.python", "ryanluker.vscode-coverage-gutters" ] -} +} \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 8f7950d1..00000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,163 +0,0 @@ -Contributing -============ - -Contributions and issues are most welcome! All issues and pull requests are -handled through GitHub_. Also, please check for any existing issues before -filing a new one. If you have a great idea but it involves big changes, please -file a ticket before making a pull request! We want to make sure you don't spend -your time coding something that might not fit the scope of the project. - -.. _GitHub: https://github.com/DiamondLightSource/python3-pip-skeleton/issues - -Running the tests ------------------ - -To run in a container -~~~~~~~~~~~~~~~~~~~~~ - -Use vscode devcontainer as follows:: - - $ git clone git://github.com/DiamondLightSource/python3-pip-skeleton.git - $ vscode python3-pip-skeleton - Click on 'Reopen in Container' when prompted - In a vscode Terminal: - $ tox -p - - -To run locally -~~~~~~~~~~~~~~ - -Get the source source code and run the unit tests directly -on your workstation as follows:: - - $ git clone git://github.com/DiamondLightSource/python3-pip-skeleton.git - $ cd python3-pip-skeleton - $ virtualenv .venv - $ source .venv/bin/activate - $ pip install -e .[dev] - $ tox -p - -In both cases tox -p runs in parallel the following checks: - - - Build Sphinx Documentation - - run pytest on all tests in ./tests - - run mypy linting on all files in ./src ./tests - - run pre-commit checks: - - - run flake8 style checks against all source - - run black formatting checks against all source - -While 100% code coverage does not make a library bug-free, it significantly -reduces the number of easily caught bugs! Please make sure coverage remains the -same or is improved by a pull request! - -Code Styling ------------- - -The code in this repository conforms to standards set by the following tools: - -- black_ for code formatting -- flake8_ for style checks -- isort_ for import ordering -- mypy_ for static type checking - -flake8 and black and isort are run by pre-commit_. You can run the above checks on -all files with this command:: - - $ tox -e pre-commit,mypy - -Or you can install a pre-commit hook that will run each time you do a ``git -commit`` on just the files that have changed. Note that mypy is not in -the pre-commit because it is a little slow :: - - $ pre-commit install - -.. _black: https://github.com/psf/black -.. _flake8: https://flake8.pycqa.org/en/latest/ -.. _isort: https://github.com/PyCQA/isort -.. _mypy: https://github.com/python/mypy -.. _pre-commit: https://pre-commit.com/ - -Docstrings are pre-processed using the Sphinx Napoleon extension. As such, -google-style_ is considered as standard for this repository. Please use type -hints in the function signature for types. For example:: - - def func(arg1: str, arg2: int) -> bool: - """Summary line. - - Extended description of function. - - Args: - arg1: Description of arg1 - arg2: Description of arg2 - - Returns: - Description of return value - """ - return True - -.. _google-style: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/index.html#google-vs-numpy - -Documentation -------------- - -Documentation is contained in the ``docs`` directory and extracted from -docstrings of the API. - -Docs follow the underlining convention:: - - Headling 1 (page title) - ======================= - - Heading 2 - --------- - - Heading 3 - ~~~~~~~~~ - -You can build the docs from the project directory by running:: - - $ tox -e docs - $ firefox build/html/index.html - -Release Process ---------------- - -To make a new release, please follow this checklist: - -- Choose a new PEP440 compliant release number (see https://peps.python.org/pep-0440/) -- Go to the GitHub release_ page -- Choose ``Draft New Release`` -- Click ``Choose Tag`` and supply the new tag you chose (click create new tag) -- Click ``Generate release notes``, review and edit these notes -- Choose a title and click ``Publish Release`` - -Note that tagging and pushing to the main branch has the same effect except that -you will not get the option to edit the release notes. - -.. _release: https://github.com/DiamondLightSource/python3-pip-skeleton/releases - - -Checking Dependencies ---------------------- - -To see a graph of the python package dependency tree type:: - - pipdeptree - -Updating the tools ------------------- - -This module is merged with the python3-pip-skeleton_. This is a generic -Python project structure which provides a means to keep tools and -techniques in sync between multiple Python projects. To update to the -latest version of the skeleton, run:: - - $ git pull https://github.com/DiamondLightSource/python3-pip-skeleton main - -Any merge conflicts will indicate an area where something has changed that -conflicts with the setup of the current module. Check the `closed pull requests -`_ -of the skeleton module for more details. - -.. _python3-pip-skeleton: https://DiamondLightSource.github.io/python3-pip-skeleton diff --git a/Dockerfile b/Dockerfile index 056d144e..b8bfe727 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # This file is for use as a devcontainer and a runtime container -# -# The devcontainer should use the build target and run as root with podman +# +# The devcontainer should use the build target and run as root with podman # or docker with user namespaces. # FROM python:3.10 as build diff --git a/README.rst b/README.rst index 95d590c3..0450bd2b 100644 --- a/README.rst +++ b/README.rst @@ -4,9 +4,9 @@ python3-pip-skeleton |code_ci| |docs_ci| |coverage| |pypi_version| |license| .. note:: - + This project contains template code only. For documentation on how to - adopt this skeleton project see + adopt this skeleton project see https://DiamondLightSource.github.io/python3-pip-skeleton-cli This is where you should write a short paragraph that describes what your module does, @@ -21,9 +21,17 @@ Releases https://github.com/DiamondLightSource/python3-pip-skeleton/releas This is where you should put some images or code snippets that illustrate some relevant examples. If it is a library then you might put some -introductory code here. +introductory code here: + +.. code-block:: python + + from python3_pip_skeleton import __version__ + + print(f"Hello python3_pip_skeleton {__version__}") + +Or if it is a commandline tool then you might put some example commands here:: -Or if it is a commandline tool then you might put some example commands here. + $ python -m python3_pip_skeleton --version .. |code_ci| image:: https://github.com/DiamondLightSource/python3-pip-skeleton/workflows/Code%20CI/badge.svg?branch=main :target: https://github.com/DiamondLightSource/python3-pip-skeleton/actions?query=workflow%3A%22Code+CI%22 diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css deleted file mode 100644 index 5fd9b721..00000000 --- a/docs/_static/theme_overrides.css +++ /dev/null @@ -1,34 +0,0 @@ -/* override table width restrictions */ -@media screen and (min-width: 639px) { - .wy-table-responsive table td { - /* !important prevents the common CSS stylesheets from - overriding this as on RTD they are loaded after this stylesheet */ - white-space: normal !important; - } -} - -/* override table padding */ -.rst-content table.docutils th, .rst-content table.docutils td { - padding: 4px 6px; -} - -/* Add two-column option */ -@media only screen and (min-width: 1000px) { - .columns { - padding-left: 10px; - padding-right: 10px; - float: left; - width: 50%; - min-height: 145px; - } -} - -.endcolumns { - clear: both -} - -/* Hide toctrees within columns and captions from all toctrees. - This is what makes the include trick in index.rst work */ -.columns .toctree-wrapper, .toctree-wrapper .caption-text { - display: none; -} diff --git a/docs/conf.py b/docs/conf.py index bee41d4b..0b6dd5bb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,6 +4,9 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +from pathlib import Path +from subprocess import check_output + import python3_pip_skeleton # -- General configuration ------------------------------------------------ @@ -16,9 +19,12 @@ # The short X.Y version. if "+" in release: - # Not on a tag - version = "main" + # Not on a tag, use branch name + root = Path(__file__).absolute().parent.parent + git_branch = check_output("git branch --show-current".split(), cwd=root) + version = git_branch.decode().strip() else: + branch = "main" version = release extensions = [ @@ -32,6 +38,10 @@ "sphinx.ext.viewcode", # Adds the inheritance-diagram generation directive "sphinx.ext.inheritance_diagram", + # Add a copy button to each code block + "sphinx_copybutton", + # For the card element + "sphinx_design", ] # If true, Sphinx will warn about all references where the target cannot @@ -50,7 +60,6 @@ ("py:class", "'bool'"), ("py:class", "'object'"), ("py:class", "'id'"), - ("py:class", "apischema.utils.UndefinedType"), ("py:class", "typing_extensions.Literal"), ] @@ -94,27 +103,65 @@ # Common links that should be available on every page rst_epilog = """ -.. _Diamond Light Source: - http://www.diamond.ac.uk +.. _Diamond Light Source: http://www.diamond.ac.uk +.. _black: https://github.com/psf/black +.. _flake8: https://flake8.pycqa.org/en/latest/ +.. _isort: https://github.com/PyCQA/isort +.. _mypy: http://mypy-lang.org/ +.. _pre-commit: https://pre-commit.com/ """ -# Ignore localhost links for period check that links in docs are valid +# Ignore localhost links for periodic check that links in docs are valid linkcheck_ignore = [r"http://localhost:\d+/"] +# Set copy-button to ignore python and bash prompts +# https://sphinx-copybutton.readthedocs.io/en/latest/use.html#using-regexp-prompt-identifiers +copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " +copybutton_prompt_is_regexp = True + # -- 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_github_versions" - -# Options for the sphinx rtd theme, use DLS blue -html_theme_options = dict(style_nav_header_background="rgb(7, 43, 93)") - -# 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"] +html_theme = "pydata_sphinx_theme" +github_repo = project +github_user = "DiamondLightSource" + +# Theme options for pydata_sphinx_theme +html_theme_options = dict( + logo=dict( + text=project, + ), + use_edit_page_button=True, + github_url=f"https://github.com/{github_user}/{github_repo}", + icon_links=[ + dict( + name="PyPI", + url=f"https://pypi.org/project/{project}", + icon="fas fa-cube", + ) + ], + switcher=dict( + json_url=f"https://{github_user}.github.io/{github_repo}/switcher.json", + version_match=version, + ), + navbar_end=["theme-switcher", "icon-links", "version-switcher"], + external_links=[ + dict( + name="Release Notes", + url=f"https://github.com/{github_user}/{github_repo}/releases", + ) + ], +) + +# A dictionary of values to pass into the template engine’s context for all pages +html_context = dict( + github_user=github_user, + github_repo=project, + github_version=version, + doc_path="docs", +) # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False @@ -122,9 +169,6 @@ # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = False -# Add some CSS classes for columns and other tweaks in a custom css file -html_css_files = ["theme_overrides.css"] - # Logo html_logo = "images/dls-logo.svg" html_favicon = "images/dls-favicon.ico" diff --git a/docs/explanations/decisions.rst b/docs/developer/explanations/decisions.rst similarity index 100% rename from docs/explanations/decisions.rst rename to docs/developer/explanations/decisions.rst diff --git a/docs/explanations/decisions/0001-record-architecture-decisions.rst b/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst similarity index 100% rename from docs/explanations/decisions/0001-record-architecture-decisions.rst rename to docs/developer/explanations/decisions/0001-record-architecture-decisions.rst diff --git a/docs/developer/how-to/build-docs.rst b/docs/developer/how-to/build-docs.rst new file mode 100644 index 00000000..9540de1c --- /dev/null +++ b/docs/developer/how-to/build-docs.rst @@ -0,0 +1,20 @@ +Build the docs using sphinx +=========================== + +You can build the `sphinx`_ based docs from the project directory by running:: + + $ tox -e docs + +This will build the static docs on the ``docs`` directory, which includes API +docs that pull in docstrings from the code. + +.. seealso:: + + `documentation_standards` + +The docs will be built into the ``build/html`` directory, and can be opened +locally with a web browse:: + + $ firefox build/html/index.html + +.. _sphinx: https://www.sphinx-doc.org/ \ No newline at end of file diff --git a/docs/developer/how-to/contribute.rst b/docs/developer/how-to/contribute.rst new file mode 100644 index 00000000..65b992f0 --- /dev/null +++ b/docs/developer/how-to/contribute.rst @@ -0,0 +1 @@ +.. include:: ../../../.github/CONTRIBUTING.rst diff --git a/docs/developer/how-to/lint.rst b/docs/developer/how-to/lint.rst new file mode 100644 index 00000000..1086c3c4 --- /dev/null +++ b/docs/developer/how-to/lint.rst @@ -0,0 +1,38 @@ +Run linting using pre-commit +============================ + +Code linting is handled by black_, flake8_ and isort_ run under pre-commit_. + +Running pre-commit +------------------ + +You can run the above checks on all files with this command:: + + $ tox -e pre-commit + +Or you can install a pre-commit hook that will run each time you do a ``git +commit`` on just the files that have changed:: + + $ pre-commit install + +Fixing issues +------------- + +If black reports an issue you can tell it to reformat all the files in the +repository:: + + $ black . + +Likewise with isort:: + + $ isort . + +If you get any flake8 issues you will have to fix those manually. + +VSCode support +-------------- + +The ``.vscode/settings.json`` will run black and isort formatters as well as +flake8 checking on save. Issues will be highlighted in the editor window. + + diff --git a/docs/developer/how-to/make-release.rst b/docs/developer/how-to/make-release.rst new file mode 100644 index 00000000..747e44a2 --- /dev/null +++ b/docs/developer/how-to/make-release.rst @@ -0,0 +1,16 @@ +Make a release +============== + +To make a new release, please follow this checklist: + +- Choose a new PEP440 compliant release number (see https://peps.python.org/pep-0440/) +- Go to the GitHub release_ page +- Choose ``Draft New Release`` +- Click ``Choose Tag`` and supply the new tag you chose (click create new tag) +- Click ``Generate release notes``, review and edit these notes +- Choose a title and click ``Publish Release`` + +Note that tagging and pushing to the main branch has the same effect except that +you will not get the option to edit the release notes. + +.. _release: https://github.com/DiamondLightSource/python3-pip-skeleton/releases \ No newline at end of file diff --git a/docs/developer/how-to/run-tests.rst b/docs/developer/how-to/run-tests.rst new file mode 100644 index 00000000..d2e03644 --- /dev/null +++ b/docs/developer/how-to/run-tests.rst @@ -0,0 +1,12 @@ +Run the tests using pytest +========================== + +Testing is done with pytest_. It will find functions in the project that `look +like tests`_, and run them to check for errors. You can run it with:: + + $ tox -e pytest + +It will also report coverage to the commandline and to ``cov.xml``. + +.. _pytest: https://pytest.org/ +.. _look like tests: https://docs.pytest.org/explanation/goodpractices.html#test-discovery diff --git a/docs/developer/how-to/static-analysis.rst b/docs/developer/how-to/static-analysis.rst new file mode 100644 index 00000000..065920e1 --- /dev/null +++ b/docs/developer/how-to/static-analysis.rst @@ -0,0 +1,8 @@ +Run static analysis using mypy +============================== + +Static type analysis is done with mypy_. It checks type definition in source +files without running them, and highlights potential issues where types do not +match. You can run it with:: + + $ tox -e mypy diff --git a/docs/developer/how-to/update-tools.rst b/docs/developer/how-to/update-tools.rst new file mode 100644 index 00000000..c1075ee8 --- /dev/null +++ b/docs/developer/how-to/update-tools.rst @@ -0,0 +1,16 @@ +Update the tools +================ + +This module is merged with the python3-pip-skeleton_. This is a generic +Python project structure which provides a means to keep tools and +techniques in sync between multiple Python projects. To update to the +latest version of the skeleton, run:: + + $ git pull --rebase=false https://github.com/DiamondLightSource/python3-pip-skeleton + +Any merge conflicts will indicate an area where something has changed that +conflicts with the setup of the current module. Check the `closed pull requests +`_ +of the skeleton module for more details. + +.. _python3-pip-skeleton: https://DiamondLightSource.github.io/python3-pip-skeleton diff --git a/docs/developer/index.rst b/docs/developer/index.rst new file mode 100644 index 00000000..bf291875 --- /dev/null +++ b/docs/developer/index.rst @@ -0,0 +1,62 @@ +Developer Guide +=============== + +Documentation is split into four categories, also accessible from links in the +side-bar. + +.. grid:: 2 + :gutter: 4 + + .. grid-item-card:: :material-regular:`directions_run;3em` + + .. toctree:: + :caption: Tutorials + :maxdepth: 1 + + tutorials/dev-install + + +++ + + Tutorials for getting up and running as a developer. + + .. grid-item-card:: :material-regular:`task;3em` + + .. toctree:: + :caption: How-to Guides + :maxdepth: 1 + + how-to/contribute + how-to/build-docs + how-to/run-tests + how-to/static-analysis + how-to/lint + how-to/update-tools + how-to/make-release + + +++ + + Practical step-by-step guides for day-to-day dev tasks. + + .. grid-item-card:: :material-regular:`apartment;3em` + + .. toctree:: + :caption: Explanations + :maxdepth: 1 + + explanations/decisions + + +++ + + Explanations of how and why the architecture is why it is. + + .. grid-item-card:: :material-regular:`description;3em` + + .. toctree:: + :caption: Reference + :maxdepth: 1 + + reference/standards + + +++ + + Technical reference material on standards in use. diff --git a/docs/developer/reference/standards.rst b/docs/developer/reference/standards.rst new file mode 100644 index 00000000..b78a719e --- /dev/null +++ b/docs/developer/reference/standards.rst @@ -0,0 +1,64 @@ +Standards +========= + +This document defines the code and documentation standards used in this +repository. + +Code Standards +-------------- + +The code in this repository conforms to standards set by the following tools: + +- black_ for code formatting +- flake8_ for style checks +- isort_ for import ordering +- mypy_ for static type checking + +.. seealso:: + + How-to guides `../how-to/lint` and `../how-to/static-analysis` + +.. _documentation_standards: + +Documentation Standards +----------------------- + +Docstrings are pre-processed using the Sphinx Napoleon extension. As such, +google-style_ is considered as standard for this repository. Please use type +hints in the function signature for types. For example: + +.. code:: python + + def func(arg1: str, arg2: int) -> bool: + """Summary line. + + Extended description of function. + + Args: + arg1: Description of arg1 + arg2: Description of arg2 + + Returns: + Description of return value + """ + return True + +.. _google-style: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/index.html#google-vs-numpy + +Documentation is contained in the ``docs`` directory and extracted from +docstrings of the API. + +Docs follow the underlining convention:: + + Headling 1 (page title) + ======================= + + Heading 2 + --------- + + Heading 3 + ~~~~~~~~~ + +.. seealso:: + + How-to guide `../how-to/build-docs` \ No newline at end of file diff --git a/docs/developer/tutorials/dev-install.rst b/docs/developer/tutorials/dev-install.rst new file mode 100644 index 00000000..c2632683 --- /dev/null +++ b/docs/developer/tutorials/dev-install.rst @@ -0,0 +1,60 @@ +Developer install +================= + +These instructions will take you through the minimal steps required to get a dev +environment setup, so you can run the tests locally. + +Clone the repository +-------------------- + +First clone the repository locally using `Git +`_:: + + $ git clone git://github.com/DiamondLightSource/python3-pip-skeleton.git + +Install dependencies +-------------------- + +You can choose to either develop on the host machine using a `venv` (which +requires python 3.8 or later) or to run in a container under `VSCode +`_ + +.. tab-set:: + + .. tab-item:: Local virtualenv + + .. code:: + + $ cd python3-pip-skeleton + $ python3 -m venv venv + $ source venv/bin/activate + $ pip install -e .[dev] + + .. tab-item:: VSCode devcontainer + + .. code:: + + $ vscode python3-pip-skeleton + # Click on 'Reopen in Container' when prompted + # Open a new terminal + +See what was installed +---------------------- + +To see a graph of the python package dependency tree type:: + + $ pipdeptree + +Build and test +-------------- + +Now you have a development environment you can run the tests in a terminal:: + + $ tox -p + +This will run in parallel the following checks: + +- `../how-to/build-docs` +- `../how-to/run-tests` +- `../how-to/static-analysis` +- `../how-to/lint` diff --git a/docs/explanations.rst b/docs/explanations.rst deleted file mode 100644 index 1e329673..00000000 --- a/docs/explanations.rst +++ /dev/null @@ -1,11 +0,0 @@ -:orphan: - -Explanations -============ - -Explanation of how the library works and why it works that way. - -.. toctree:: - :caption: Explanations - - explanations/decisions diff --git a/docs/genindex.rst b/docs/genindex.rst new file mode 100644 index 00000000..93eb8b29 --- /dev/null +++ b/docs/genindex.rst @@ -0,0 +1,5 @@ +API Index +========= + +.. + https://stackoverflow.com/a/42310803 diff --git a/docs/how-to.rst b/docs/how-to.rst deleted file mode 100644 index 700797cc..00000000 --- a/docs/how-to.rst +++ /dev/null @@ -1,11 +0,0 @@ -:orphan: - -How-to Guides -============= - -Practical step-by-step guides for the more experienced user. - -.. toctree:: - :caption: How-to Guides - - how-to/contributing diff --git a/docs/how-to/contributing.rst b/docs/how-to/contributing.rst deleted file mode 100644 index ac7b6bcf..00000000 --- a/docs/how-to/contributing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../CONTRIBUTING.rst diff --git a/docs/index.rst b/docs/index.rst index 9bde8adf..df33c8e6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,48 +1,29 @@ +:html_theme.sidebar_secondary.remove: + .. include:: ../README.rst :end-before: when included in index.rst - How the documentation is structured ----------------------------------- -Documentation is split into four categories, also accessible from links in the -side-bar. - -.. rst-class:: columns - -`tutorials` -~~~~~~~~~~~ - -.. include:: tutorials.rst - :start-after: ========= - -.. rst-class:: columns - -`how-to` -~~~~~~~~ - -.. include:: how-to.rst - :start-after: ============= - -.. rst-class:: columns - -`explanations` -~~~~~~~~~~~~~~ +The documentation is split into 2 sections: -.. include:: explanations.rst - :start-after: ============ +.. grid:: 2 -.. rst-class:: columns + .. grid-item-card:: :material-regular:`person;4em` + :link: user/index + :link-type: doc -`reference` -~~~~~~~~~~~ + The User Guide contains documentation on how to install and use python3-pip-skeleton. -.. include:: reference.rst - :start-after: ========= + .. grid-item-card:: :material-regular:`code;4em` + :link: developer/index + :link-type: doc -.. rst-class:: endcolumns + The Developer Guide contains documentation on how to develop and contribute changes back to python3-pip-skeleton. -About the documentation -~~~~~~~~~~~~~~~~~~~~~~~ +.. toctree:: + :hidden: -`Why is the documentation structured this way? `_ + user/index + developer/index diff --git a/docs/reference.rst b/docs/reference.rst deleted file mode 100644 index 84c8cf13..00000000 --- a/docs/reference.rst +++ /dev/null @@ -1,17 +0,0 @@ -:orphan: - -Reference -========= - -Technical reference material including APIs and release notes. - -.. toctree:: - :caption: Reference - - reference/api - Releases - Index - -.. - Index link above is a hack to make genindex.html a relative link - https://stackoverflow.com/a/31820846 diff --git a/docs/tutorials.rst b/docs/tutorials.rst deleted file mode 100644 index dfdef509..00000000 --- a/docs/tutorials.rst +++ /dev/null @@ -1,11 +0,0 @@ -:orphan: - -Tutorials -========= - -Tutorials for installation, library and commandline usage. New users start here. - -.. toctree:: - :caption: Tutorials - - tutorials/installation diff --git a/docs/user/explanations/docs-structure.rst b/docs/user/explanations/docs-structure.rst new file mode 100644 index 00000000..f25a09ba --- /dev/null +++ b/docs/user/explanations/docs-structure.rst @@ -0,0 +1,18 @@ +About the documentation +----------------------- + + :material-regular:`format_quote;2em` + + The Grand Unified Theory of Documentation + + -- David Laing + +There is a secret that needs to be understood in order to write good software +documentation: there isn't one thing called *documentation*, there are four. + +They are: *tutorials*, *how-to guides*, *technical reference* and *explanation*. +They represent four different purposes or functions, and require four different +approaches to their creation. Understanding the implications of this will help +improve most documentation - often immensely. + +`More information on this topic. `_ diff --git a/docs/user/how-to/run-container.rst b/docs/user/how-to/run-container.rst new file mode 100644 index 00000000..84f857af --- /dev/null +++ b/docs/user/how-to/run-container.rst @@ -0,0 +1,15 @@ +Run in a container +================== + +Pre-built containers with python3-pip-skeleton and its dependencies already +installed are available on `Github Container Registry +`_. + +Starting the container +---------------------- + +To pull the container from github container registry and run:: + + $ docker run ghcr.io/DiamondLightSource/python3-pip-skeleton:main --version + +To get a released version, use a numbered release instead of ``main``. diff --git a/docs/user/index.rst b/docs/user/index.rst new file mode 100644 index 00000000..2c94a0c0 --- /dev/null +++ b/docs/user/index.rst @@ -0,0 +1,57 @@ +User Guide +========== + +Documentation is split into four categories, also accessible from links in the +side-bar. + +.. grid:: 2 + :gutter: 4 + + .. grid-item-card:: :material-regular:`directions_walk;3em` + + .. toctree:: + :caption: Tutorials + :maxdepth: 1 + + tutorials/installation + + +++ + + Tutorials for installation and typical usage. New users start here. + + .. grid-item-card:: :material-regular:`directions;3em` + + .. toctree:: + :caption: How-to Guides + :maxdepth: 1 + + how-to/run-container + + +++ + + Practical step-by-step guides for the more experienced user. + + .. grid-item-card:: :material-regular:`info;3em` + + .. toctree:: + :caption: Explanations + :maxdepth: 1 + + explanations/docs-structure + + +++ + + Explanations of how the library works and why it works that way. + + .. grid-item-card:: :material-regular:`menu_book;3em` + + .. toctree:: + :caption: Reference + :maxdepth: 1 + + reference/api + ../genindex + + +++ + + Technical reference material including APIs and release notes. diff --git a/docs/reference/api.rst b/docs/user/reference/api.rst similarity index 100% rename from docs/reference/api.rst rename to docs/user/reference/api.rst diff --git a/docs/tutorials/installation.rst b/docs/user/tutorials/installation.rst similarity index 56% rename from docs/tutorials/installation.rst rename to docs/user/tutorials/installation.rst index 399dc2c5..e90d3efb 100644 --- a/docs/tutorials/installation.rst +++ b/docs/user/tutorials/installation.rst @@ -7,7 +7,7 @@ Check your version of python You will need python 3.8 or later. You can check your version of python by typing into a terminal:: - python3 --version + $ python3 --version Create a virtual environment @@ -16,30 +16,23 @@ Create a virtual environment It is recommended that you install into a “virtual environment” so this installation will not interfere with any existing Python software:: - python3 -m venv /path/to/venv - source /path/to/venv/bin/activate + $ python3 -m venv /path/to/venv + $ source /path/to/venv/bin/activate Installing the library ---------------------- -You can now use ``pip`` to install the library:: +You can now use ``pip`` to install the library and its dependencies:: - python3 -m pip install python3-pip-skeleton + $ python3 -m pip install python3-pip-skeleton If you require a feature that is not currently released you can also install from github:: - python3 -m pip install git+https://github.com/DiamondLightSource/python3-pip-skeleton.git + $ python3 -m pip install git+https://github.com/DiamondLightSource/python3-pip-skeleton.git The library should now be installed and the commandline interface on your path. You can check the version that has been installed by typing:: - python3-pip-skeleton --version - -Running in a container ----------------------- - -To pull the container from github container registry and run:: - - docker run ghcr.io/DiamondLightSource/python3-pip-skeleton:main --version + $ python3-pip-skeleton --version diff --git a/setup.cfg b/setup.cfg index 5d9ac269..3daa1c9b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ packages = find: # as per recommendation here https://hynek.me/articles/testing-packaging/ package_dir = =src - + setup_requires = setuptools_scm[toml]>=6.2 @@ -38,9 +38,11 @@ dev = mypy pipdeptree pre-commit + pydata-sphinx-theme pytest-cov setuptools_scm[toml]>=6.2 - sphinx-rtd-theme-github-versions + sphinx-copybutton + sphinx-design tox tox-direct types-mock diff --git a/src/python3_pip_skeleton/__main__.py b/src/python3_pip_skeleton/__main__.py index 1a97fb44..c680183b 100644 --- a/src/python3_pip_skeleton/__main__.py +++ b/src/python3_pip_skeleton/__main__.py @@ -11,6 +11,6 @@ def main(args=None): args = parser.parse_args(args) -# test with: pipenv run python -m python3_pip_skeleton +# test with: python -m python3_pip_skeleton if __name__ == "__main__": main() From 4d21bc31ee9a9b8120cb21dfb519c40a48aa3cd8 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Tue, 13 Sep 2022 08:51:26 +0000 Subject: [PATCH 10/79] Run sdist install in container workflow This saves another runner starting up just for this --- .github/workflows/code.yml | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 0aae7fb5..13711d46 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -126,29 +126,20 @@ jobs: context: . labels: ${{ steps.meta.outputs.labels }} - - name: Check runtime + - name: Test cli works in runtime image run: for i in ${{ steps.meta.outputs.tags }}; do docker run ${i} --version; done + - name: Test cli works in sdist installed in local python + # ${GITHUB_REPOSITORY##*/} is the repo name without org + # Replace this with the cli command if different to the repo name + run: pip install dist/*.gz && ${GITHUB_REPOSITORY##*/} --version + - name: Upload build files uses: actions/upload-artifact@v3 with: name: dist path: dist - sdist: - needs: container - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v3 - - - name: Install sdist in a venv and check cli works - # ${GITHUB_REPOSITORY##*/} is the repo name without org - # Replace this with the cli command if different to the repo name - run: | - pip install dist/*.gz - ${GITHUB_REPOSITORY##*/} --version - release: # upload to PyPI and make a release on every tag if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') From 52038569d52a5471e90f21b0d5ab5d685a18d19c Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Tue, 13 Sep 2022 09:44:58 +0100 Subject: [PATCH 11/79] Update CI badges --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 0450bd2b..a014631e 100644 --- a/README.rst +++ b/README.rst @@ -33,12 +33,12 @@ Or if it is a commandline tool then you might put some example commands here:: $ python -m python3_pip_skeleton --version -.. |code_ci| image:: https://github.com/DiamondLightSource/python3-pip-skeleton/workflows/Code%20CI/badge.svg?branch=main - :target: https://github.com/DiamondLightSource/python3-pip-skeleton/actions?query=workflow%3A%22Code+CI%22 +.. |code_ci| image:: https://github.com/DiamondLightSource/python3-pip-skeleton/actions/workflows/code.yml/badge.svg?branch=main + :target: https://github.com/DiamondLightSource/python3-pip-skeleton/actions/workflows/code.yml :alt: Code CI -.. |docs_ci| image:: https://github.com/DiamondLightSource/python3-pip-skeleton/workflows/Docs%20CI/badge.svg?branch=main - :target: https://github.com/DiamondLightSource/python3-pip-skeleton/actions?query=workflow%3A%22Docs+CI%22 +.. |docs_ci| image:: https://github.com/DiamondLightSource/python3-pip-skeleton/actions/workflows/docs.yml/badge.svg?branch=main + :target: https://github.com/DiamondLightSource/python3-pip-skeleton/actions/workflows/docs.yml :alt: Docs CI .. |coverage| image:: https://codecov.io/gh/DiamondLightSource/python3-pip-skeleton/branch/main/graph/badge.svg From d96f15a97efae989aabac10b5f815bd1e3ea143c Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Mon, 3 Oct 2022 21:12:20 +0100 Subject: [PATCH 12/79] Update code.yml Fixing a bug that occurs when releasing. (This is already fixed in the skeleton-cli project but failed to get copied to skeleton). --- .github/workflows/code.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 13711d46..89960f22 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -127,7 +127,8 @@ jobs: labels: ${{ steps.meta.outputs.labels }} - name: Test cli works in runtime image - run: for i in ${{ steps.meta.outputs.tags }}; do docker run ${i} --version; done + # check that the first tag can run with --version parameter + run: docker run $(echo ${{ steps.meta.outputs.tags }} | sed -e 's/\s.*$//') --version - name: Test cli works in sdist installed in local python # ${GITHUB_REPOSITORY##*/} is the repo name without org From 68976f406d9c74bc2674897be2cefbd49dbaeba4 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Tue, 4 Oct 2022 11:03:29 +0100 Subject: [PATCH 13/79] Update .github/workflows/code.yml Co-authored-by: Tom C (DLS) <101418278+coretl@users.noreply.github.com> --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 89960f22..19773839 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -128,7 +128,7 @@ jobs: - name: Test cli works in runtime image # check that the first tag can run with --version parameter - run: docker run $(echo ${{ steps.meta.outputs.tags }} | sed -e 's/\s.*$//') --version + run: docker run $(echo ${{ steps.meta.outputs.tags }} | head -1) --version - name: Test cli works in sdist installed in local python # ${GITHUB_REPOSITORY##*/} is the repo name without org From 92f10b4dfd56e314255147a989c0103b910f7291 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 14 Oct 2022 13:52:05 +0100 Subject: [PATCH 14/79] Fix make version switcher to use the right key Pin pydata-sphinx-theme to allow the build to complete https://github.com/pydata/pydata-sphinx-theme/issues/987 --- .github/pages/make_switcher.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pages/make_switcher.py b/.github/pages/make_switcher.py index 5c65d788..39c12772 100755 --- a/.github/pages/make_switcher.py +++ b/.github/pages/make_switcher.py @@ -59,7 +59,7 @@ def get_versions(ref: str, add: Optional[str], remove: Optional[str]) -> List[st def write_json(path: Path, repository: str, versions: str): org, repo_name = repository.split("/") struct = [ - dict(name=version, url=f"https://{org}.github.io/{repo_name}/{version}/") + dict(version=version, url=f"https://{org}.github.io/{repo_name}/{version}/") for version in versions ] text = json.dumps(struct, indent=2) diff --git a/setup.cfg b/setup.cfg index 3daa1c9b..857492eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ dev = mypy pipdeptree pre-commit - pydata-sphinx-theme + pydata-sphinx-theme < 0.10.1 pytest-cov setuptools_scm[toml]>=6.2 sphinx-copybutton From d35ffdbd28a1cba3fda62503151bcd1da82efbd4 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 14 Oct 2022 13:54:50 +0100 Subject: [PATCH 15/79] Add sphinx autobuild --- docs/conf.py | 1 - docs/developer/how-to/build-docs.rst | 18 ++++++++++++++++++ setup.cfg | 5 +++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0b6dd5bb..c4c2126a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,7 +24,6 @@ git_branch = check_output("git branch --show-current".split(), cwd=root) version = git_branch.decode().strip() else: - branch = "main" version = release extensions = [ diff --git a/docs/developer/how-to/build-docs.rst b/docs/developer/how-to/build-docs.rst index 9540de1c..79e3f780 100644 --- a/docs/developer/how-to/build-docs.rst +++ b/docs/developer/how-to/build-docs.rst @@ -17,4 +17,22 @@ locally with a web browse:: $ firefox build/html/index.html +Autobuild +--------- + +You can also run an autobuild process, which will watch your ``docs`` +directory for changes and rebuild whenever it sees changes, reloading any +browsers watching the pages:: + + $ tox -e docs autobuild + +You can view the pages at localhost:: + + $ firefox http://localhost:8000 + +If you are making changes to source code too, you can tell it to watch +changes in this directory too:: + + $ tox -e docs autobuild -- --watch src + .. _sphinx: https://www.sphinx-doc.org/ \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 857492eb..2e9dacb8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,7 @@ dev = pydata-sphinx-theme < 0.10.1 pytest-cov setuptools_scm[toml]>=6.2 + sphinx-autobuild sphinx-copybutton sphinx-design tox @@ -122,5 +123,5 @@ allowlist_externals = pre-commit commands = pre-commit run --all-files {posargs} [testenv:docs] -allowlist_externals = sphinx-build -commands = sphinx-build -EWT --keep-going docs build/html {posargs} +allowlist_externals = sphinx-build sphinx-autobuild +commands = sphinx-{posargs:build -EW --keep-going} -T docs build/html From 7c11165980fe4ccc9260538c9490dfe0cdfe40b1 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Wed, 5 Oct 2022 15:21:53 +0100 Subject: [PATCH 16/79] Use PyPA action for PyPI Publish Use the official Python Packaging Authority (PyPA) Action to publish to PyPI --- .github/workflows/code.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 19773839..93dd6e37 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -162,7 +162,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish to PyPI - env: - TWINE_USERNAME: __token__ + uses: pypa/gh-action-pypi-publish@release/v1 + with: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: pipx run twine upload dist/*.whl dist/*.tar.gz From 1acb1d47463edeec386cdd82a549e652c57f9b46 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Wed, 5 Oct 2022 15:27:44 +0100 Subject: [PATCH 17/79] Fix password parameter --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 93dd6e37..690bb7cd 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -164,4 +164,4 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + password: ${{ secrets.PYPI_TOKEN }} From 5ff0a302f73174dec5eed0942ef1abd369264b5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 17:00:48 +0000 Subject: [PATCH 18/79] Bump black from 22.8.0 to 22.10.0 Bumps [black](https://github.com/psf/black) from 22.8.0 to 22.10.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/22.8.0...22.10.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2e9dacb8..bd277c9a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,7 @@ setup_requires = [options.extras_require] # For development tests/docs dev = - black==22.8.0 + black==22.10.0 flake8-isort isort>5.0 mypy From 323424fb1d9d037e029ae45920770f656b2e991a Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 14 Oct 2022 15:31:24 +0100 Subject: [PATCH 19/79] Sanitize ref name for docs version Translate punctuation and unicode in branch names to _ --- .github/workflows/docs.yml | 8 +++++--- .github/workflows/docs_clean.yml | 13 ++++++++----- setup.cfg | 4 +++- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a684d031..f0e7ebb6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -41,12 +41,14 @@ jobs: - name: Build docs run: tox -e docs + - name: Sanitize ref name for docs version + run: echo "DOCS_VERSION=${GITHUB_REF_NAME//[^A-Za-z0-9._-]/_}" >> $GITHUB_ENV + - name: Move to versioned directory - # e.g. main or 0.1.2 - run: mv build/html ".github/pages/${{ github.ref_name }}" + run: mv build/html .github/pages/$DOCS_VERSION - name: Write switcher.json - run: python .github/pages/make_switcher.py --add "${{ github.ref_name }}" ${{ github.repository }} .github/pages/switcher.json + run: python .github/pages/make_switcher.py --add $DOCS_VERSION ${{ github.repository }} .github/pages/switcher.json - name: Publish Docs to gh-pages if: github.event_name == 'push' diff --git a/.github/workflows/docs_clean.yml b/.github/workflows/docs_clean.yml index b80e4c22..d5425e42 100644 --- a/.github/workflows/docs_clean.yml +++ b/.github/workflows/docs_clean.yml @@ -24,17 +24,20 @@ jobs: - name: removing documentation for branch ${{ github.event.ref }} if: ${{ github.event_name != 'workflow_dispatch' }} - run: echo "remove_me=${{ github.event.ref }}" >> $GITHUB_ENV + run: echo "REF_NAME=${{ github.event.ref }}" >> $GITHUB_ENV - name: manually removing documentation version ${{ github.event.inputs.version }} if: ${{ github.event_name == 'workflow_dispatch' }} - run: echo "remove_me=${{ github.event.inputs.version }}" >> $GITHUB_ENV + run: echo "REF_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV + + - name: Sanitize ref name for docs version + run: echo "DOCS_VERSION=${REF_NAME//[^A-Za-z0-9._-]/_}" >> $GITHUB_ENV - name: update index and push changes run: | - rm -r ${{ env.remove_me }} - python make_switcher.py --remove ${{ env.remove_me }} ${{ github.repository }} switcher.json + rm -r ${{ env.DOCS_VERSION }} + python make_switcher.py --remove $DOCS_VERSION ${{ github.repository }} switcher.json git config --global user.name 'GitHub Actions Docs Cleanup CI' git config --global user.email 'GithubActionsCleanup@noreply.github.com' - git commit -am"removing redundant docs version ${{ env.remove_me }}" + git commit -am "Removing redundant docs version $DOCS_VERSION" git push diff --git a/setup.cfg b/setup.cfg index bd277c9a..87c5b596 100644 --- a/setup.cfg +++ b/setup.cfg @@ -123,5 +123,7 @@ allowlist_externals = pre-commit commands = pre-commit run --all-files {posargs} [testenv:docs] -allowlist_externals = sphinx-build sphinx-autobuild +allowlist_externals = + sphinx-build + sphinx-autobuild commands = sphinx-{posargs:build -EW --keep-going} -T docs build/html From 964446a43cda17b235a5577dc586d89c0054f9af Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Tue, 18 Oct 2022 08:43:27 +0100 Subject: [PATCH 20/79] move requirements assests to lockfiles zip --- .containerignore | 7 ------- .dockerignore | 10 ++++++++++ .github/workflows/code.yml | 11 ++++++++++- .github/workflows/container_tests.sh | 3 ++- Dockerfile | 16 ++++++++++------ 5 files changed, 32 insertions(+), 15 deletions(-) delete mode 100644 .containerignore create mode 100644 .dockerignore diff --git a/.containerignore b/.containerignore deleted file mode 100644 index eb7d5ae1..00000000 --- a/.containerignore +++ /dev/null @@ -1,7 +0,0 @@ -Dockerfile -build/ -dist/ -.mypy_cache -.tox -.venv* -venv* diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..4fb4c9ef --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +build/ +dist/ +.mypy_cache +.tox +.venv* +venv* +.devcontainer.json +.pre-commit-config.yaml +.vscode +README.rst diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 690bb7cd..d6322890 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -110,6 +110,7 @@ jobs: run: | docker run --name test build bash /project/.github/workflows/container_tests.sh docker cp test:/project/dist . + docker cp test:/project/lockfiles . docker cp test:/project/cov.xml . - name: Upload coverage to Codecov @@ -141,6 +142,12 @@ jobs: name: dist path: dist + - name: Upload lock files + uses: actions/upload-artifact@v3 + with: + name: lockfiles + path: lockfiles + release: # upload to PyPI and make a release on every tag if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') @@ -156,7 +163,9 @@ jobs: uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14 with: prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} - files: dist/* + files: | + dist/ + lockfiles/ generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/container_tests.sh b/.github/workflows/container_tests.sh index 5f921597..c36bbd9a 100644 --- a/.github/workflows/container_tests.sh +++ b/.github/workflows/container_tests.sh @@ -6,7 +6,8 @@ source /venv/bin/activate touch requirements_dev.txt pip install -r requirements_dev.txt -e .[dev] -pip freeze --exclude-editable > dist/requirements_dev.txt +mkdir -p lockfiles +pip freeze --exclude-editable > lockfiles/requirements_dev.txt pipdeptree diff --git a/Dockerfile b/Dockerfile index b8bfe727..5e04e438 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,29 +17,33 @@ RUN apt-get update && apt-get upgrade -y && \ && busybox --install COPY . /project +WORKDIR /project -RUN cd /project && \ - pip install --upgrade pip build && \ +# make the wheel outside of the venv so 'build' does not dirty requirements.txt +RUN pip install --upgrade pip build && \ export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) && \ python -m build --sdist --wheel && \ touch requirements.txt +# set up a virtual environment and put it in PATH RUN python -m venv /venv ENV PATH=/venv/bin:$PATH ENV TOX_DIRECT=1 -RUN cd /project && \ - pip install --upgrade pip && \ +# install the wheel and generate the requirements file +RUN pip install --upgrade pip && \ pip install -r requirements.txt dist/*.whl && \ - pip freeze > dist/requirements.txt && \ + mkdir -p lockfiles && \ + pip freeze > lockfiles/requirements.txt && \ # we don't want to include our own wheel in requirements - remove with sed # and replace with a comment to avoid a zero length asset upload later - sed -i '/file:/s/^/# Requirements for /' dist/requirements.txt + sed -i '/file:/s/^/# Requirements for /' lockfiles/requirements.txt FROM python:3.10-slim as runtime # Add apt-get system dependecies for runtime here if needed +# copy the virtual environment from the build stage and put it in PATH COPY --from=build /venv/ /venv/ ENV PATH=/venv/bin:$PATH From b0426cef26f36c5cc379af879a8a40e1c558721f Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Tue, 18 Oct 2022 12:22:16 +0100 Subject: [PATCH 21/79] fix .dockerignore, build options --- .dockerignore | 2 -- .gitignore | 2 ++ Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index 4fb4c9ef..e2ed7105 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,7 +4,5 @@ dist/ .tox .venv* venv* -.devcontainer.json .pre-commit-config.yaml .vscode -README.rst diff --git a/.gitignore b/.gitignore index e0fba46a..9fbb6bfe 100644 --- a/.gitignore +++ b/.gitignore @@ -64,4 +64,6 @@ target/ .venv* venv* +# further build artifacts +lockfiles/ diff --git a/Dockerfile b/Dockerfile index 5e04e438..55020416 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ WORKDIR /project # make the wheel outside of the venv so 'build' does not dirty requirements.txt RUN pip install --upgrade pip build && \ export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) && \ - python -m build --sdist --wheel && \ + python -m build && \ touch requirements.txt # set up a virtual environment and put it in PATH From a8d55dd788dc3b7ed7eba0278683475604d1d2d3 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Tue, 1 Nov 2022 08:25:50 +0000 Subject: [PATCH 22/79] add check for dirty repo when building wheel --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 55020416..c96bee05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ WORKDIR /project # make the wheel outside of the venv so 'build' does not dirty requirements.txt RUN pip install --upgrade pip build && \ export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) && \ + git diff && \ python -m build && \ touch requirements.txt From 081e205c8530f819115e9d8a99be7c3b4d112c45 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Tue, 1 Nov 2022 08:34:20 +0000 Subject: [PATCH 23/79] fix dockerignore to not dirty repo --- .dockerignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index e2ed7105..a6fab0ea 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,5 +4,3 @@ dist/ .tox .venv* venv* -.pre-commit-config.yaml -.vscode From 9da307f5c84f03f563ae16645d6f29cb6b9d00da Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Wed, 2 Nov 2022 07:59:43 +0000 Subject: [PATCH 24/79] fix Github Release assets spec --- .github/workflows/code.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index d6322890..3aa05f1b 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -164,8 +164,8 @@ jobs: with: prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} files: | - dist/ - lockfiles/ + dist/* + lockfiles/* generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From dd95a77887f0ae9cf7a8ee04749e85a2af848d3e Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 4 Nov 2022 12:29:53 +0000 Subject: [PATCH 25/79] Improve tox-direct handling - Environment variable no longer needs to be set - All commands run with tox-direct by default - All environment variables passed through --- Dockerfile | 1 - setup.cfg | 29 +++++++++++++---------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index c96bee05..df6249c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,6 @@ RUN pip install --upgrade pip build && \ # set up a virtual environment and put it in PATH RUN python -m venv /venv ENV PATH=/venv/bin:$PATH -ENV TOX_DIRECT=1 # install the wheel and generate the requirements file RUN pip install --upgrade pip && \ diff --git a/setup.cfg b/setup.cfg index 87c5b596..abe2919d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -106,24 +106,21 @@ source = # NOTE that we pre-install all tools in the dev dependencies (including tox). # Hence the use of allowlist_externals instead of using the tox virtualenvs. # This ensures a match between developer time tools in the IDE and tox tools. -# Setting TOX_DIRECT=1 in the environment will make this even faster [tox:tox] skipsdist = True -[testenv:pytest] -allowlist_externals = pytest -commands = pytest {posargs} - -[testenv:mypy] -allowlist_externals = mypy -commands = mypy src tests {posargs} - -[testenv:pre-commit] -allowlist_externals = pre-commit -commands = pre-commit run --all-files {posargs} - -[testenv:docs] -allowlist_externals = +[testenv:{pre-commit,mypy,pytest,docs}] +# Don't create a virtualenv for the command, requires tox-direct plugin +direct = True +passenv = * +allowlist_externals = + pytest + pre-commit + mypy sphinx-build sphinx-autobuild -commands = sphinx-{posargs:build -EW --keep-going} -T docs build/html +commands = + pytest: pytest {posargs} + mypy: mypy src tests {posargs} + pre-commit: pre-commit run --all-files {posargs} + docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html From 4ccb60169da316ebd2b80f99d0131ad7b0043790 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Tue, 8 Nov 2022 09:37:20 +0000 Subject: [PATCH 26/79] Rely on the container less - Moved wheel and sdist creation to the dist job - Rely on the test matrix to run tests - Simplified container build to make minimal for build and runtime and use wheel from 'dist': only publish to GHCR for tagged builds - Create separate requirements-*.txt for each of the test matrix - Fix actions-gh-pages version and don't run it for dependabot - Move Dockerfile to .devcontainer and use as context to improve build times - Other minor improvements and simplifications --- .devcontainer/Dockerfile | 37 +++++ .../devcontainer.json | 12 +- .dockerignore | 6 - .../actions/install_requirements/action.yml | 58 +++++++ .github/workflows/code.yml | 153 ++++++++++-------- .github/workflows/container_tests.sh | 14 -- .github/workflows/docs.yml | 30 ++-- .github/workflows/docs_clean.yml | 4 +- .github/workflows/linkcheck.yml | 19 +-- Dockerfile | 52 ------ setup.cfg | 3 +- 11 files changed, 212 insertions(+), 176 deletions(-) create mode 100644 .devcontainer/Dockerfile rename .devcontainer.json => .devcontainer/devcontainer.json (81%) delete mode 100644 .dockerignore create mode 100644 .github/actions/install_requirements/action.yml delete mode 100644 .github/workflows/container_tests.sh delete mode 100644 Dockerfile diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..b6b4bef9 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,37 @@ +# This file is for use as a devcontainer and a runtime container +# +# The devcontainer should use the build target and run as root with podman +# or docker with user namespaces. +# +FROM python:3.11 as build + +ARG PIP_OPTIONS + +# Add any system dependencies for the developer/build environment here e.g. +# RUN apt-get update && apt-get upgrade -y && \ +# apt-get install -y --no-install-recommends \ +# desired-packages \ +# && rm -rf /var/lib/apt/lists/* + +# set up a virtual environment and put it in PATH +RUN python -m venv /venv +ENV PATH=/venv/bin:$PATH + +# Copy any required context for the pip install over +COPY . /context +WORKDIR /context + +# install python package into /venv +RUN pip install ${PIP_OPTIONS} + +FROM python:3.11-slim as runtime + +# Add apt-get system dependecies for runtime here if needed + +# copy the virtual environment from the build stage and put it in PATH +COPY --from=build /venv/ /venv/ +ENV PATH=/venv/bin:$PATH + +# change this entrypoint if it is not the same as the repo +ENTRYPOINT ["python3-pip-skeleton"] +CMD ["--version"] diff --git a/.devcontainer.json b/.devcontainer/devcontainer.json similarity index 81% rename from .devcontainer.json rename to .devcontainer/devcontainer.json index d0921df6..1046ef9e 100644 --- a/.devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,16 +4,17 @@ "build": { "dockerfile": "Dockerfile", "target": "build", - "context": ".", - "args": {} + // Only upgrade pip, we will install the project below + "args": { + "PIP_OPTIONS": "--upgrade pip" + } }, "remoteEnv": { "DISPLAY": "${localEnv:DISPLAY}" }, // Set *default* container specific settings.json values on container create. "settings": { - "python.defaultInterpreterPath": "/venv/bin/python", - "python.linting.enabled": true + "python.defaultInterpreterPath": "/venv/bin/python" }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ @@ -24,6 +25,7 @@ "initializeCommand": "bash -c 'for i in $HOME/.inputrc; do [ -f $i ] || touch $i; done'", "runArgs": [ "--net=host", + "--security-opt=label=type:container_runtime_t", "-v=${localEnv:HOME}/.ssh:/root/.ssh", "-v=${localEnv:HOME}/.inputrc:/root/.inputrc" ], @@ -35,5 +37,5 @@ "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind", "workspaceFolder": "${localWorkspaceFolder}", // After the container is created, install the python project in editable form - "postCreateCommand": "pip install $([ -f requirements_dev.txt ] && echo -r requirements_dev.txt ) -e .[dev]" + "postCreateCommand": "pip install -e .[dev]" } \ No newline at end of file diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index a6fab0ea..00000000 --- a/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -build/ -dist/ -.mypy_cache -.tox -.venv* -venv* diff --git a/.github/actions/install_requirements/action.yml b/.github/actions/install_requirements/action.yml new file mode 100644 index 00000000..25a146d1 --- /dev/null +++ b/.github/actions/install_requirements/action.yml @@ -0,0 +1,58 @@ +name: Install requirements +description: Run pip install with requirements and upload resulting requirements +inputs: + requirements_file: + description: Name of requirements file to use and upload + required: true + install_options: + description: Parameters to pass to pip install + required: true + python_version: + description: Python version to install + default: "3.x" + +runs: + using: composite + + steps: + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python_version }} + + - name: Pip install + run: | + touch ${{ inputs.requirements_file }} + # -c uses requirements.txt as constraints, see 'Validate requirements file' + pip install -c ${{ inputs.requirements_file }} ${{ inputs.install_options }} + shell: bash + + - name: Create lockfile + run: | + mkdir -p lockfiles + pip freeze --exclude-editable > lockfiles/${{ inputs.requirements_file }} + # delete the self referencing line and make sure it isn't blank + sed -i '/file:/d' lockfiles/${{ inputs.requirements_file }} + shell: bash + + - name: Upload lockfiles + uses: actions/upload-artifact@v3 + with: + name: lockfiles + path: lockfiles + + # This eliminates the class of problems where the requirements being given no + # longer match what the packages themselves dictate. E.g. In the rare instance + # where I install some-package which used to depend on vulnerable-dependency + # but now uses good-dependency (despite being nominally the same version) + # pip will install both if given a requirements file with -r + - name: If requirements file exists, check it matches pip installed packages + run: | + if [ -s ${{ inputs.requirements_file }} ]; then + if ! diff -u ${{ inputs.requirements_file }} lockfiles/${{ inputs.requirements_file }}; then + echo "Error: ${{ inputs.requirements_file }} need the above changes to be exhaustive" + exit 1 + fi + fi + shell: bash + diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 3aa05f1b..200b07b6 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -4,8 +4,11 @@ on: push: pull_request: schedule: - # Run every Monday at 8am to check latest versions of dependencies + # Run weekly to check latest versions of dependencies - cron: "0 8 * * WED" +env: + # The target python version, which must match the Dockerfile version + CONTAINER_PYTHON: "3.11" jobs: lint: @@ -17,16 +20,14 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Setup python - uses: actions/setup-python@v4 + - name: Install python packages + uses: ./.github/actions/install_requirements with: - python-version: "3.10" + requirements_file: requirements-dev-3.x.txt + install_options: -e .[dev] - name: Lint - run: | - touch requirements_dev.txt requirements.txt - pip install -r requirements.txt -r requirements_dev.txt -e .[dev] - tox -e pre-commit,mypy + run: tox -e pre-commit,mypy test: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository @@ -34,7 +35,13 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest"] # can add windows-latest, macos-latest - python: ["3.8", "3.9", "3.10"] + python: ["3.9", "3.10", "3.11"] + install: ["-e .[dev]"] + # Make one version be non-editable to test both paths of version code + include: + - os: "ubuntu-latest" + python: "3.8" + install: ".[dev]" runs-on: ${{ matrix.os }} env: @@ -45,18 +52,21 @@ jobs: - name: Checkout uses: actions/checkout@v3 with: + # Need this to get version number from last tag fetch-depth: 0 - - name: Setup python ${{ matrix.python }} - uses: actions/setup-python@v4 + - name: Install python packages + uses: ./.github/actions/install_requirements with: - python-version: ${{ matrix.python }} + python_version: ${{ matrix.python }} + requirements_file: requirements-test-${{ matrix.os }}-${{ matrix.python }}.txt + install_options: ${{ matrix.install }} - - name: Install with latest dependencies - run: pip install .[dev] + - name: List dependency tree + run: pipdeptree - name: Run tests - run: pytest tests + run: pytest - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 @@ -64,9 +74,46 @@ jobs: name: ${{ matrix.python }}/${{ matrix.os }} files: cov.xml - container: + dist: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + runs-on: "ubuntu-latest" + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + # Need this to get version number from last tag + fetch-depth: 0 + + - name: Build sdist and wheel + run: | + export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) && \ + pipx run build + + - name: Upload sdist and wheel as artifacts + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist + + - name: Check for packaging errors + run: pipx run twine check dist/* + + - name: Install python packages + uses: ./.github/actions/install_requirements + with: + python_version: ${{env.CONTAINER_PYTHON}} + requirements_file: requirements.txt + install_options: dist/*.whl + + - name: Test module --version works using the installed wheel + # If more than one module in src/ replace with module name to test + run: python -m $(ls src | head -1) --version + + container: + needs: [lint, dist, test] runs-on: ubuntu-latest + permissions: contents: read packages: write @@ -74,8 +121,15 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + + # image names must be all lower case + - name: Generate image repo name + run: echo IMAGE_REPOSITORY=ghcr.io/$(tr '[:upper:]' '[:lower:]' <<< "${{ github.repository }}") >> $GITHUB_ENV + + - name: Download wheel and lockfiles + uses: actions/download-artifact@v3 with: - fetch-depth: 0 + path: .devcontainer - name: Log in to GitHub Docker Registry if: github.event_name != 'pull_request' @@ -89,74 +143,45 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/${{ github.repository }} + images: ${{ env.IMAGE_REPOSITORY }} tags: | - type=ref,event=branch type=ref,event=tag + type=raw,value=latest - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v2 - - name: Build developer image for testing - uses: docker/build-push-action@v3 - with: - tags: build:latest - context: . - target: build - load: true - - - name: Run tests in the container locked with requirements_dev.txt - run: | - docker run --name test build bash /project/.github/workflows/container_tests.sh - docker cp test:/project/dist . - docker cp test:/project/lockfiles . - docker cp test:/project/cov.xml . - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - name: 3.10-locked/ubuntu-latest - files: cov.xml - - name: Build runtime image uses: docker/build-push-action@v3 with: - push: ${{ github.event_name != 'pull_request' }} + build-args: | + PIP_OPTIONS=-r lockfiles/requirements.txt dist/*.whl + push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} + load: ${{ ! (github.event_name == 'push' && startsWith(github.ref, 'refs/tags')) }} tags: ${{ steps.meta.outputs.tags }} - context: . - labels: ${{ steps.meta.outputs.labels }} + context: .devcontainer + # If you have a long docker build, uncomment the following to turn on caching + # For short build times this makes it a little slower + #cache-from: type=gha + #cache-to: type=gha,mode=max - name: Test cli works in runtime image - # check that the first tag can run with --version parameter - run: docker run $(echo ${{ steps.meta.outputs.tags }} | head -1) --version - - - name: Test cli works in sdist installed in local python - # ${GITHUB_REPOSITORY##*/} is the repo name without org - # Replace this with the cli command if different to the repo name - run: pip install dist/*.gz && ${GITHUB_REPOSITORY##*/} --version - - - name: Upload build files - uses: actions/upload-artifact@v3 - with: - name: dist - path: dist - - - name: Upload lock files - uses: actions/upload-artifact@v3 - with: - name: lockfiles - path: lockfiles + run: docker run ${{ env.IMAGE_REPOSITORY }} --version release: # upload to PyPI and make a release on every tag - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: container + needs: [lint, dist, test] + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v3 + - name: Fixup blank lockfiles + # Github release artifacts can't be blank + run: for f in lockfiles/*; do [ -s $f ] || echo '# No requirements' >> $f; done + - name: Github Release # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions diff --git a/.github/workflows/container_tests.sh b/.github/workflows/container_tests.sh deleted file mode 100644 index c36bbd9a..00000000 --- a/.github/workflows/container_tests.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -x - -cd /project -source /venv/bin/activate - -touch requirements_dev.txt -pip install -r requirements_dev.txt -e .[dev] -mkdir -p lockfiles -pip freeze --exclude-editable > lockfiles/requirements_dev.txt - -pipdeptree - -pytest tests diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f0e7ebb6..94fa2151 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,11 +7,6 @@ on: jobs: docs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository - strategy: - fail-fast: false - matrix: - python: ["3.10"] - runs-on: ubuntu-latest steps: @@ -19,24 +14,21 @@ jobs: if: startsWith(github.ref, 'refs/tags') run: sleep 60 - - name: Install python version - uses: actions/setup-python@v4 + - name: Checkout + uses: actions/checkout@v3 with: - python-version: ${{ matrix.python }} + # Need this to get version number from last tag + fetch-depth: 0 - - name: Install Packages + - name: Install system packages # Can delete this if you don't use graphviz in your docs run: sudo apt-get install graphviz - - name: checkout - uses: actions/checkout@v3 + - name: Install python packages + uses: ./.github/actions/install_requirements with: - fetch-depth: 0 - - - name: Install dependencies - run: | - touch requirements_dev.txt - pip install -r requirements_dev.txt -e .[dev] + requirements_file: requirements-dev-3.x.txt + install_options: -e .[dev] - name: Build docs run: tox -e docs @@ -51,10 +43,10 @@ jobs: run: python .github/pages/make_switcher.py --add $DOCS_VERSION ${{ github.repository }} .github/pages/switcher.json - name: Publish Docs to gh-pages - if: github.event_name == 'push' + if: github.event_name == 'push' && github.actor != 'dependabot[bot]' # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions - uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 # v3.8.0 + uses: peaceiris/actions-gh-pages@de7ea6f8efb354206b205ef54722213d99067935 # v3.9.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .github/pages diff --git a/.github/workflows/docs_clean.yml b/.github/workflows/docs_clean.yml index d5425e42..a67e1881 100644 --- a/.github/workflows/docs_clean.yml +++ b/.github/workflows/docs_clean.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: checkout + - name: Checkout uses: actions/checkout@v3 with: ref: gh-pages @@ -35,7 +35,7 @@ jobs: - name: update index and push changes run: | - rm -r ${{ env.DOCS_VERSION }} + rm -r $DOCS_VERSION python make_switcher.py --remove $DOCS_VERSION ${{ github.repository }} switcher.json git config --global user.name 'GitHub Actions Docs Cleanup CI' git config --global user.email 'GithubActionsCleanup@noreply.github.com' diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index e6838560..42d199c4 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -1,8 +1,9 @@ name: Link Check on: + workflow_dispatch: schedule: - # Run every Monday at 8am to check URL links still resolve + # Run weekly to check URL links still resolve - cron: "0 8 * * WED" jobs: @@ -17,18 +18,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Install python version - uses: actions/setup-python@v4 + - name: Install python packages + uses: ./.github/actions/install_requirements with: - python-version: ${{ matrix.python }} - - - name: Install dependencies - run: | - touch requirements_dev.txt - pip install -r requirements_dev.txt -e .[dev] + requirements_file: requirements-dev-3.x.txt + install_options: -e .[dev] - name: Check links - run: tox -e docs -- -b linkcheck + run: tox -e docs build -- -b linkcheck diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index df6249c9..00000000 --- a/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -# This file is for use as a devcontainer and a runtime container -# -# The devcontainer should use the build target and run as root with podman -# or docker with user namespaces. -# -FROM python:3.10 as build - -# Add any system dependencies for the developer/build environment here -RUN apt-get update && apt-get upgrade -y && \ - apt-get install -y --no-install-recommends \ - build-essential \ - busybox \ - git \ - net-tools \ - vim \ - && rm -rf /var/lib/apt/lists/* \ - && busybox --install - -COPY . /project -WORKDIR /project - -# make the wheel outside of the venv so 'build' does not dirty requirements.txt -RUN pip install --upgrade pip build && \ - export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) && \ - git diff && \ - python -m build && \ - touch requirements.txt - -# set up a virtual environment and put it in PATH -RUN python -m venv /venv -ENV PATH=/venv/bin:$PATH - -# install the wheel and generate the requirements file -RUN pip install --upgrade pip && \ - pip install -r requirements.txt dist/*.whl && \ - mkdir -p lockfiles && \ - pip freeze > lockfiles/requirements.txt && \ - # we don't want to include our own wheel in requirements - remove with sed - # and replace with a comment to avoid a zero length asset upload later - sed -i '/file:/s/^/# Requirements for /' lockfiles/requirements.txt - -FROM python:3.10-slim as runtime - -# Add apt-get system dependecies for runtime here if needed - -# copy the virtual environment from the build stage and put it in PATH -COPY --from=build /venv/ /venv/ -ENV PATH=/venv/bin:$PATH - -# change this entrypoint if it is not the same as the repo -ENTRYPOINT ["python3-pip-skeleton"] -CMD ["--version"] diff --git a/setup.cfg b/setup.cfg index abe2919d..ba377343 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,6 +12,7 @@ classifiers = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 [options] python_requires = >=3.8 @@ -50,8 +51,6 @@ dev = [options.packages.find] where = src -# Don't include our tests directory in the distribution -exclude = tests # Specify any package data to be included in the wheel below. # [options.package_data] From fa99c26a72861597d419620622d859df9d65a722 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Mon, 14 Nov 2022 13:08:36 +0000 Subject: [PATCH 27/79] Mount ssh & inputrc in mounts list --- .devcontainer/devcontainer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1046ef9e..7c7d6da1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -25,11 +25,11 @@ "initializeCommand": "bash -c 'for i in $HOME/.inputrc; do [ -f $i ] || touch $i; done'", "runArgs": [ "--net=host", - "--security-opt=label=type:container_runtime_t", - "-v=${localEnv:HOME}/.ssh:/root/.ssh", - "-v=${localEnv:HOME}/.inputrc:/root/.inputrc" + "--security-opt=label=type:container_runtime_t" ], "mounts": [ + "source=${localEnv:HOME}/.ssh,target=/root/.ssh,type=bind", + "source=${localEnv:HOME}/.inputrc,target=/root/.inputrc,type=bind", // map in home directory - not strictly necessary but useful "source=${localEnv:HOME},target=${localEnv:HOME},type=bind,consistency=cached" ], From c5f583f6cce94d157d45a3288e62332ace63097a Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Fri, 11 Nov 2022 15:44:26 +0000 Subject: [PATCH 28/79] Moved config to pyproject.toml --- .vscode/extensions.json | 3 +- pyproject.toml | 108 ++++++++++++++++++++++++++ setup.cfg | 125 ------------------------------ tests/test_boilerplate_removed.py | 14 ++-- 4 files changed, 116 insertions(+), 134 deletions(-) delete mode 100644 setup.cfg diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 041f8944..fe4a5809 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "ms-vscode-remote.remote-containers", "ms-python.vscode-pylance", "ms-python.python", - "ryanluker.vscode-coverage-gutters" + "ryanluker.vscode-coverage-gutters", + "tamasfe.even-better-toml" ] } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1b8c998a..1c710455 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,5 +2,113 @@ requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2", "wheel"] build-backend = "setuptools.build_meta" +[project] +name = "python3-pip-skeleton" +classifiers = [ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +description = "One line description of your module" +dependencies = [] # Add project dependencies here, e.g. ["click", "numpy"] +dynamic = ["version"] +license.file = "LICENSE" +readme = "README.rst" +requires-python = ">=3.8" + +[project.optional-dependencies] +dev = [ + "black", + "isort", + "mypy", + "flake8", + "flake8-isort", + "Flake8-pyproject", + "pipdeptree", + "pre-commit", + "pydata-sphinx-theme", + "pytest-cov", + "setuptools_scm[toml]>=6.2", + "sphinx-autobuild", + "sphinx-copybutton", + "sphinx-design", + "tox", + "tox-direct", + "types-mock", +] + +[project.scripts] +python3-pip-skeleton = "python3_pip_skeleton.__main__:main" + +[project.urls] +GitHub = "https://github.com/DiamondLightSource/python3-pip-skeleton" + +[[project.authors]] # Further authors may be added by duplicating this section +email = "email@address.com" +name = "Firstname Lastname" + + [tool.setuptools_scm] write_to = "src/python3_pip_skeleton/_version.py" + +[tool.mypy] +ignore_missing_imports = true # Ignore missing stubs in imported modules + +[tool.isort] +float_to_top = true +profile = "black" + +[tool.flake8] +extend-ignore = [ + "E203", # See https://github.com/PyCQA/pycodestyle/issues/373 + "F811", # support typing.overload decorator + "F722", # allow Annotated[typ, some_func("some string")] +] +max-line-length = 88 # Respect black's line length (default 88), +exclude = [".tox", "venv"] + + +[tool.pytest.ini_options] +# Run pytest with all our checkers, and don't spam us with massive tracebacks on error +addopts = """ + --tb=native -vv --doctest-modules --doctest-glob="*.rst" + --cov=python3_pip_skeleton --cov-report term --cov-report xml:cov.xml + """ +# https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings +filterwarnings = "error" +# Doctest python code in docs, python code in src docstrings, test functions in tests +testpaths = "docs src tests" + +[tool.coverage.run] +data_file = "/tmp/python3_pip_skeleton.coverage" + +[tool.coverage.paths] +# Tests are run from installed location, map back to the src directory +source = ["src", "**/site-packages/"] + +# tox must currently be configured via an embedded ini string +# See: https://github.com/tox-dev/tox/issues/999 +[tool.tox] +legacy_tox_ini = """ +[tox] +skipsdist=True + +[testenv:{pre-commit,mypy,pytest,docs}] +# Don't create a virtualenv for the command, requires tox-direct plugin +direct = True +passenv = * +allowlist_externals = + pytest + pre-commit + mypy + sphinx-build + sphinx-autobuild +commands = + pytest: pytest {posargs} + mypy: mypy src tests {posargs} + pre-commit: pre-commit run --all-files {posargs} + docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html +""" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ba377343..00000000 --- a/setup.cfg +++ /dev/null @@ -1,125 +0,0 @@ -[metadata] -name = python3-pip-skeleton -description = One line description of your module -url = https://github.com/DiamondLightSource/python3-pip-skeleton -author = Firstname Lastname -author_email = email@address.com -license = Apache License 2.0 -long_description = file: README.rst -long_description_content_type = text/x-rst -classifiers = - License :: OSI Approved :: Apache Software License - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - -[options] -python_requires = >=3.8 -packages = find: -# =src is interpreted as {"": "src"} -# as per recommendation here https://hynek.me/articles/testing-packaging/ -package_dir = - =src - -setup_requires = - setuptools_scm[toml]>=6.2 - -# Specify any package dependencies below. -# install_requires = -# numpy -# scipy - -[options.extras_require] -# For development tests/docs -dev = - black==22.10.0 - flake8-isort - isort>5.0 - mypy - pipdeptree - pre-commit - pydata-sphinx-theme < 0.10.1 - pytest-cov - setuptools_scm[toml]>=6.2 - sphinx-autobuild - sphinx-copybutton - sphinx-design - tox - tox-direct - types-mock - -[options.packages.find] -where = src - -# Specify any package data to be included in the wheel below. -# [options.package_data] -# python3_pip_skeleton = -# subpackage/*.yaml - -[options.entry_points] -# Include a command line script -console_scripts = - python3-pip-skeleton = python3_pip_skeleton.__main__:main - -[mypy] -# Ignore missing stubs for modules we use -ignore_missing_imports = True - -[isort] -profile=black -float_to_top=true - -[flake8] -# Make flake8 respect black's line length (default 88), -max-line-length = 88 -extend-ignore = - E203, # See https://github.com/PyCQA/pycodestyle/issues/373 - F811, # support typing.overload decorator - F722, # allow Annotated[typ, some_func("some string")] -exclude = - .tox - venv - -[tool:pytest] -# Run pytest with all our checkers, and don't spam us with massive tracebacks on error -addopts = - --tb=native -vv --doctest-modules --doctest-glob="*.rst" - --cov=python3_pip_skeleton --cov-report term --cov-report xml:cov.xml -# https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings -filterwarnings = error -# Doctest python code in docs, python code in src docstrings, test functions in tests -testpaths = - docs src tests - -[coverage:run] -data_file = /tmp/python3_pip_skeleton.coverage - -[coverage:paths] -# Tests are run from installed location, map back to the src directory -source = - src - **/site-packages/ - -# Use tox to provide parallel linting and testing -# NOTE that we pre-install all tools in the dev dependencies (including tox). -# Hence the use of allowlist_externals instead of using the tox virtualenvs. -# This ensures a match between developer time tools in the IDE and tox tools. -[tox:tox] -skipsdist = True - -[testenv:{pre-commit,mypy,pytest,docs}] -# Don't create a virtualenv for the command, requires tox-direct plugin -direct = True -passenv = * -allowlist_externals = - pytest - pre-commit - mypy - sphinx-build - sphinx-autobuild -commands = - pytest: pytest {posargs} - mypy: mypy src tests {posargs} - pre-commit: pre-commit run --all-files {posargs} - docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html diff --git a/tests/test_boilerplate_removed.py b/tests/test_boilerplate_removed.py index f5204fa9..b823c53b 100644 --- a/tests/test_boilerplate_removed.py +++ b/tests/test_boilerplate_removed.py @@ -2,7 +2,7 @@ This file checks that all the example boilerplate text has been removed. It can be deleted when all the contained tests pass """ -import configparser +from importlib.metadata import metadata from pathlib import Path ROOT = Path(__file__).parent.parent @@ -24,14 +24,12 @@ def assert_not_contains_text(path: str, text: str, explanation: str): skeleton_check(text in contents, f"Please change ./{path} {explanation}") -# setup.cfg -def test_module_description(): - conf = configparser.ConfigParser() - conf.read("setup.cfg") - description = conf["metadata"]["description"] +# pyproject.toml +def test_module_summary(): + summary = metadata("python3-pip-skeleton")["summary"] skeleton_check( - "One line description of your module" in description, - "Please change description in ./setup.cfg " + "One line description of your module" in summary, + "Please change project.description in ./pyproject.toml " "to be a one line description of your module", ) From a08124fd6d4950732098ab3af7e3c781690ec7c2 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Thu, 17 Nov 2022 05:20:59 +0000 Subject: [PATCH 29/79] add version label to container registry push --- .github/workflows/code.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 200b07b6..a3a1c487 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -160,6 +160,7 @@ jobs: push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} load: ${{ ! (github.event_name == 'push' && startsWith(github.ref, 'refs/tags')) }} tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} context: .devcontainer # If you have a long docker build, uncomment the following to turn on caching # For short build times this makes it a little slower From d97fd677d6e7e666aaf6c8d1e079d338e75e00db Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Fri, 11 Nov 2022 16:06:36 +0000 Subject: [PATCH 30/79] Use importlib.metadata to get package version --- pyproject.toml | 1 - src/python3_pip_skeleton/__init__.py | 12 +++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1c710455..0bcc80db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ dev = [ "pre-commit", "pydata-sphinx-theme", "pytest-cov", - "setuptools_scm[toml]>=6.2", "sphinx-autobuild", "sphinx-copybutton", "sphinx-design", diff --git a/src/python3_pip_skeleton/__init__.py b/src/python3_pip_skeleton/__init__.py index 0fe6655f..ef94dff1 100644 --- a/src/python3_pip_skeleton/__init__.py +++ b/src/python3_pip_skeleton/__init__.py @@ -1,12 +1,6 @@ -try: - # Use live version from git - from setuptools_scm import get_version +from importlib.metadata import version - # Warning: If the install is nested to the same depth, this will always succeed - __version__ = get_version(root="../../", relative_to=__file__) - del get_version -except (ImportError, LookupError): - # Use installed version - from ._version import __version__ +__version__ = version("python3-pip-skeleton") +del version __all__ = ["__version__"] From 7383739e7f5ae5cc4309eded64774c7c4bf58c1d Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Thu, 17 Nov 2022 12:02:43 +0000 Subject: [PATCH 31/79] Make twine check strict --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index a3a1c487..5ff24420 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -97,7 +97,7 @@ jobs: path: dist - name: Check for packaging errors - run: pipx run twine check dist/* + run: pipx run twine check --strict dist/* - name: Install python packages uses: ./.github/actions/install_requirements From 15a1d441d72fc2132bae767d0a947dccfe22ba33 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Thu, 24 Nov 2022 14:44:08 +0000 Subject: [PATCH 32/79] Don't check switcher if not published - pydata-sphinx-theme 0.11 started checking switcher - this meant you couldn't bootstrap a gh-pages build - pydata-sphinx-theme 0.12 put in an option not to check - but we want checking if the file exists - so only check if we can get the json file - and suggest user turns pages on if we can't --- docs/conf.py | 17 ++++++++++++++++- pyproject.toml | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index c4c2126a..6bd4d230 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,9 +4,12 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import sys from pathlib import Path from subprocess import check_output +import requests + import python3_pip_skeleton # -- General configuration ------------------------------------------------ @@ -126,6 +129,17 @@ html_theme = "pydata_sphinx_theme" github_repo = project github_user = "DiamondLightSource" +switcher_json = f"https://{github_user}.github.io/{github_repo}/switcher.json" +# Don't check switcher if it doesn't exist, but warn in a non-failing way +check_switcher = requests.get(switcher_json).ok +if not check_switcher: + print( + "*** Can't read version switcher, is GitHub pages enabled? \n" + " Once Docs CI job has successfully run once, set the " + "Github pages source branch to be 'gh-pages' at:\n" + f" https://github.com/{github_user}/{github_repo}/settings/pages", + file=sys.stderr, + ) # Theme options for pydata_sphinx_theme html_theme_options = dict( @@ -142,9 +156,10 @@ ) ], switcher=dict( - json_url=f"https://{github_user}.github.io/{github_repo}/switcher.json", + json_url=switcher_json, version_match=version, ), + check_switcher=check_switcher, navbar_end=["theme-switcher", "icon-links", "version-switcher"], external_links=[ dict( diff --git a/pyproject.toml b/pyproject.toml index 0bcc80db..5e06a4f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dev = [ "Flake8-pyproject", "pipdeptree", "pre-commit", - "pydata-sphinx-theme", + "pydata-sphinx-theme>=0.12", "pytest-cov", "sphinx-autobuild", "sphinx-copybutton", From 6f0604bf457831bc4824de810461ccbd19fd854c Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 25 Nov 2022 13:43:30 +0000 Subject: [PATCH 33/79] Don't use flake8==6 until plugins catch up https://github.com/john-hen/Flake8-pyproject/issues/12 --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5e06a4f0..bf1da9b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,8 @@ dev = [ "black", "isort", "mypy", - "flake8", + # https://github.com/john-hen/Flake8-pyproject/issues/12 + "flake8<6", "flake8-isort", "Flake8-pyproject", "pipdeptree", From 7a688851d9c4491d420b92c624da16408cabddf1 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Mon, 28 Nov 2022 11:45:00 +0000 Subject: [PATCH 34/79] Remove flake8 constraint Also shrink dep list where intermediate modules are covered by others --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bf1da9b4..0397a22d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,10 +22,7 @@ requires-python = ">=3.8" [project.optional-dependencies] dev = [ "black", - "isort", "mypy", - # https://github.com/john-hen/Flake8-pyproject/issues/12 - "flake8<6", "flake8-isort", "Flake8-pyproject", "pipdeptree", @@ -35,7 +32,6 @@ dev = [ "sphinx-autobuild", "sphinx-copybutton", "sphinx-design", - "tox", "tox-direct", "types-mock", ] From d4633ab1f059eebbd76f08ddfec979b36f81396c Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Fri, 18 Nov 2022 17:26:38 +0000 Subject: [PATCH 35/79] Add agreed upon extensions to customizations --- .devcontainer/devcontainer.json | 16 +++++++++++----- .vscode/extensions.json | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7c7d6da1..7f5c7e3b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,11 +16,17 @@ "settings": { "python.defaultInterpreterPath": "/venv/bin/python" }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance" - ], + "customizations": { + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "tamasfe.even-better-toml", + "redhat.vscode-yaml", + "ryanluker.vscode-coverage-gutters" + ] + } + }, // Make sure the files we are mapping into the container exist on the host "initializeCommand": "bash -c 'for i in $HOME/.inputrc; do [ -f $i ] || touch $i; done'", "runArgs": [ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index fe4a5809..81922991 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,9 +1,9 @@ { "recommendations": [ "ms-vscode-remote.remote-containers", - "ms-python.vscode-pylance", "ms-python.python", - "ryanluker.vscode-coverage-gutters", - "tamasfe.even-better-toml" + "tamasfe.even-better-toml", + "redhat.vscode-yaml", + "ryanluker.vscode-coverage-gutters" ] } \ No newline at end of file From f916925ab167a0aa79d7c34e63ebe68d06631aab Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Fri, 25 Nov 2022 16:29:06 +0000 Subject: [PATCH 36/79] Add common-utils to dev container features --- .devcontainer/devcontainer.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7f5c7e3b..6c707ab0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,6 +12,13 @@ "remoteEnv": { "DISPLAY": "${localEnv:DISPLAY}" }, + // Add the URLs of features you want added when the container is built. + "features": { + "ghcr.io/devcontainers/features/common-utils:1": { + "username": "none", + "upgradePackages": false + } + }, // Set *default* container specific settings.json values on container create. "settings": { "python.defaultInterpreterPath": "/venv/bin/python" From 4981421ca3aab9b9b39ae8dcace211ab519d3187 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Fri, 2 Dec 2022 15:48:42 +0000 Subject: [PATCH 37/79] Link to condatiners.dev for devcontainer spec --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6c707ab0..f5e71fe9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,4 @@ -// For format details, see https://aka.ms/devcontainer.json +// For format details, see https://containers.dev/implementors/json_reference/ { "name": "Python 3 Developer Container", "build": { From 7300883bd8a3cb5ba2e338c728d320cbc5681922 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Wed, 7 Dec 2022 13:36:36 +0000 Subject: [PATCH 38/79] Remove unused matrix from linkcheck CI --- .github/workflows/linkcheck.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 42d199c4..02d8df4c 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -8,11 +8,6 @@ on: jobs: docs: - strategy: - fail-fast: false - matrix: - python: ["3.10"] - runs-on: ubuntu-latest steps: From f67ddd4e6c86db2521c6855c8b6d4bf0aab20089 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Wed, 30 Nov 2022 16:44:32 +0000 Subject: [PATCH 39/79] Add publish to anaconda step --- .github/workflows/code.yml | 38 +++++++++++++++++++++++++++++++++++++- README.rst | 7 ++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 5ff24420..7c671aae 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -170,11 +170,42 @@ jobs: - name: Test cli works in runtime image run: docker run ${{ env.IMAGE_REPOSITORY }} --version + conda: + needs: [dist] + runs-on: ubuntu-latest + + steps: + - uses: actions/download-artifact@v3 + + - name: Create Conda recipe + run: | + mkdir -p conda/recipe + pipx run grayskull pypi -o conda/recipe dist/*.tar.gz + + - name: Install conda-build + run: conda install -y conda-build + + - name: Build Conda distribution + run: | + mkdir -p conda/build + conda build --output-folder conda/build conda/recipe/*/meta.yaml + + - name: Upload conda build + uses: actions/upload-artifact@v3 + with: + name: conda_build + path: | + conda/build/ + !conda/build/*/.cache + release: # upload to PyPI and make a release on every tag - needs: [lint, dist, test] + needs: [lint, dist, test, conda] if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} runs-on: ubuntu-latest + env: + HAS_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN != '' }} + HAS_ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN != '' }} steps: - uses: actions/download-artifact@v3 @@ -197,6 +228,11 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish to PyPI + if: ${{ env.HAS_PYPI_TOKEN }} uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_TOKEN }} + + - name: Publish to Anaconda + if: ${{ env.HAS_ANACONDA_TOKEN }} + run: pipx run --spec anaconda-client anaconda --token ${{ secrets.ANACONDA_TOKEN }} upload conda_build/noarch/*.tar.bz2 diff --git a/README.rst b/README.rst index a014631e..78f7f140 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ python3-pip-skeleton =========================== -|code_ci| |docs_ci| |coverage| |pypi_version| |license| +|code_ci| |docs_ci| |coverage| |pypi_version| |anaconda_version| |license| .. note:: @@ -14,6 +14,7 @@ how it does it, and why people should use it. ============== ============================================================== PyPI ``pip install python3-pip-skeleton`` +Conda ``conda install -c DiamondLightSource python3-pip-skeleton`` Source code https://github.com/DiamondLightSource/python3-pip-skeleton Documentation https://DiamondLightSource.github.io/python3-pip-skeleton Releases https://github.com/DiamondLightSource/python3-pip-skeleton/releases @@ -49,6 +50,10 @@ Or if it is a commandline tool then you might put some example commands here:: :target: https://pypi.org/project/python3-pip-skeleton :alt: Latest PyPI version +.. |anaconda_version| image:: https://anaconda.org/DiamondLightSource/python3-pip-skeleton/badges/version.svg + :target: https://anaconda.org/DiamondLightSource/python3-pip-skeleton + :alt: Latest Anaconda version + .. |license| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg :target: https://opensource.org/licenses/Apache-2.0 :alt: Apache License From e2e7d86d53048ab88c30cd311ac51dcce2c2a8d2 Mon Sep 17 00:00:00 2001 From: Garry O'Donnell Date: Tue, 20 Dec 2022 10:41:01 +0000 Subject: [PATCH 40/79] Remove conda build & publish from CI --- .github/workflows/code.yml | 35 +---------------------------------- README.rst | 7 +------ 2 files changed, 2 insertions(+), 40 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 7c671aae..cf957a4f 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -170,42 +170,13 @@ jobs: - name: Test cli works in runtime image run: docker run ${{ env.IMAGE_REPOSITORY }} --version - conda: - needs: [dist] - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v3 - - - name: Create Conda recipe - run: | - mkdir -p conda/recipe - pipx run grayskull pypi -o conda/recipe dist/*.tar.gz - - - name: Install conda-build - run: conda install -y conda-build - - - name: Build Conda distribution - run: | - mkdir -p conda/build - conda build --output-folder conda/build conda/recipe/*/meta.yaml - - - name: Upload conda build - uses: actions/upload-artifact@v3 - with: - name: conda_build - path: | - conda/build/ - !conda/build/*/.cache - release: # upload to PyPI and make a release on every tag - needs: [lint, dist, test, conda] + needs: [lint, dist, test] if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} runs-on: ubuntu-latest env: HAS_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN != '' }} - HAS_ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN != '' }} steps: - uses: actions/download-artifact@v3 @@ -232,7 +203,3 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_TOKEN }} - - - name: Publish to Anaconda - if: ${{ env.HAS_ANACONDA_TOKEN }} - run: pipx run --spec anaconda-client anaconda --token ${{ secrets.ANACONDA_TOKEN }} upload conda_build/noarch/*.tar.bz2 diff --git a/README.rst b/README.rst index 78f7f140..a014631e 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ python3-pip-skeleton =========================== -|code_ci| |docs_ci| |coverage| |pypi_version| |anaconda_version| |license| +|code_ci| |docs_ci| |coverage| |pypi_version| |license| .. note:: @@ -14,7 +14,6 @@ how it does it, and why people should use it. ============== ============================================================== PyPI ``pip install python3-pip-skeleton`` -Conda ``conda install -c DiamondLightSource python3-pip-skeleton`` Source code https://github.com/DiamondLightSource/python3-pip-skeleton Documentation https://DiamondLightSource.github.io/python3-pip-skeleton Releases https://github.com/DiamondLightSource/python3-pip-skeleton/releases @@ -50,10 +49,6 @@ Or if it is a commandline tool then you might put some example commands here:: :target: https://pypi.org/project/python3-pip-skeleton :alt: Latest PyPI version -.. |anaconda_version| image:: https://anaconda.org/DiamondLightSource/python3-pip-skeleton/badges/version.svg - :target: https://anaconda.org/DiamondLightSource/python3-pip-skeleton - :alt: Latest Anaconda version - .. |license| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg :target: https://opensource.org/licenses/Apache-2.0 :alt: Apache License From 8eb1194091b92adbf07f19090f9f59a1e8086b0b Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Thu, 5 Jan 2023 09:05:51 +0000 Subject: [PATCH 41/79] Stop checking switcher in docs build --- docs/conf.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 6bd4d230..7022f617 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -130,9 +130,8 @@ github_repo = project github_user = "DiamondLightSource" switcher_json = f"https://{github_user}.github.io/{github_repo}/switcher.json" -# Don't check switcher if it doesn't exist, but warn in a non-failing way -check_switcher = requests.get(switcher_json).ok -if not check_switcher: +switcher_exists = requests.get(switcher_json).ok +if not switcher_exists: print( "*** Can't read version switcher, is GitHub pages enabled? \n" " Once Docs CI job has successfully run once, set the " @@ -142,6 +141,14 @@ ) # Theme options for pydata_sphinx_theme +# We don't check switcher because there are 3 possible states for a repo: +# 1. New project, docs are not published so there is no switcher +# 2. Existing project with latest skeleton, switcher exists and works +# 3. Existing project with old skeleton that makes broken switcher, +# switcher exists but is broken +# Point 3 makes checking switcher difficult, because the updated skeleton +# will fix the switcher at the end of the docs workflow, but never gets a chance +# to complete as the docs build warns and fails. html_theme_options = dict( logo=dict( text=project, @@ -159,7 +166,7 @@ json_url=switcher_json, version_match=version, ), - check_switcher=check_switcher, + check_switcher=False, navbar_end=["theme-switcher", "icon-links", "version-switcher"], external_links=[ dict( From 0f6456ec5ea49753f95e8b4a456456fc4912b458 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 16:10:46 +0000 Subject: [PATCH 42/79] Bump softprops/action-gh-release from 0.1.14 to 0.1.15 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 0.1.14 to 0.1.15. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/1e07f4398721186383de40550babbdf2b84acfc5...de2c0eb89ae2a093876385947365aca7b0e5f844) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index cf957a4f..14802fef 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -188,7 +188,7 @@ jobs: - name: Github Release # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions - uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14 + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 with: prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} files: | From b9bf30b9c5f8e9884af62259297495326e6c43c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 16:06:56 +0000 Subject: [PATCH 43/79] Bump peaceiris/actions-gh-pages from 3.9.0 to 3.9.1 Bumps [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages) from 3.9.0 to 3.9.1. - [Release notes](https://github.com/peaceiris/actions-gh-pages/releases) - [Changelog](https://github.com/peaceiris/actions-gh-pages/blob/main/CHANGELOG.md) - [Commits](https://github.com/peaceiris/actions-gh-pages/compare/de7ea6f8efb354206b205ef54722213d99067935...64b46b4226a4a12da2239ba3ea5aa73e3163c75b) --- updated-dependencies: - dependency-name: peaceiris/actions-gh-pages dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 94fa2151..c510d577 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -46,7 +46,7 @@ jobs: if: github.event_name == 'push' && github.actor != 'dependabot[bot]' # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions - uses: peaceiris/actions-gh-pages@de7ea6f8efb354206b205ef54722213d99067935 # v3.9.0 + uses: peaceiris/actions-gh-pages@64b46b4226a4a12da2239ba3ea5aa73e3163c75b # v3.9.1 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .github/pages From db76a312c5195d4f46feaa3ea5b1b3d4617dd9dc Mon Sep 17 00:00:00 2001 From: tizayi Date: Mon, 9 Jan 2023 14:04:56 +0000 Subject: [PATCH 44/79] simplify local container workflow --- .devcontainer/devcontainer.json | 2 +- .github/workflows/code.yml | 5 +++-- .devcontainer/Dockerfile => Dockerfile | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) rename .devcontainer/Dockerfile => Dockerfile (98%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f5e71fe9..7a30e9be 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ { "name": "Python 3 Developer Container", "build": { - "dockerfile": "Dockerfile", + "dockerfile": "../Dockerfile", "target": "build", // Only upgrade pip, we will install the project below "args": { diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 14802fef..9686f0d4 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -129,7 +129,7 @@ jobs: - name: Download wheel and lockfiles uses: actions/download-artifact@v3 with: - path: .devcontainer + path: artifacts/ - name: Log in to GitHub Docker Registry if: github.event_name != 'pull_request' @@ -161,7 +161,8 @@ jobs: load: ${{ ! (github.event_name == 'push' && startsWith(github.ref, 'refs/tags')) }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - context: .devcontainer + context: artifacts/ + file: ./Dockerfile # If you have a long docker build, uncomment the following to turn on caching # For short build times this makes it a little slower #cache-from: type=gha diff --git a/.devcontainer/Dockerfile b/Dockerfile similarity index 98% rename from .devcontainer/Dockerfile rename to Dockerfile index b6b4bef9..31d05606 100644 --- a/.devcontainer/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ # FROM python:3.11 as build -ARG PIP_OPTIONS +ARG PIP_OPTIONS=. # Add any system dependencies for the developer/build environment here e.g. # RUN apt-get update && apt-get upgrade -y && \ From db2f8e3410c9190dbd10a5b48b0948da9b35478c Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Wed, 18 Jan 2023 14:16:09 +0000 Subject: [PATCH 45/79] add how to do pin requirements doc --- docs/developer/how-to/pin-requirements.rst | 74 ++++++++++++++++++++++ docs/developer/index.rst | 1 + 2 files changed, 75 insertions(+) create mode 100644 docs/developer/how-to/pin-requirements.rst diff --git a/docs/developer/how-to/pin-requirements.rst b/docs/developer/how-to/pin-requirements.rst new file mode 100644 index 00000000..89639623 --- /dev/null +++ b/docs/developer/how-to/pin-requirements.rst @@ -0,0 +1,74 @@ +Pinning Requirements +==================== + +Introduction +------------ + +By design this project only defines dependencies in one place, i.e. in +the ``requires`` table in ``pyproject.toml``. + +In the ``requires`` table it is possible to pin versions of some dependencies +as needed. For library projects it is best to leave pinning to a minimum so +that your library can be used by the widest range of applications. + +When CI builds the project it will use the latest compatible set of +dependencies available (after applying your pins and any dependencies' pins). + +This approach means that there is a possibility that a future build may +break because an updated release of a dependency has made a breaking change. + +The correct way to fix such an issue is to work out the minimum pinning in +``requires`` that will resolve the problem. However this can be quite hard to +do and may be time consuming when simply trying to release a minor update. + +For this reason we provide a mechanism for locking all dependencies to +the same version as a previous successful release. This is a quick fix that +should guarantee a successful CI build. + +Finding the lock files +---------------------- + +Every release of the project will have a set of requirements files published +as release assets. + +For example take a look at the release page for python3-pip-skeleton-cli here: +https://github.com/DiamondLightSource/python3-pip-skeleton-cli/releases/tag/3.3.0 + +There is a list of requirements*.txt files showing as assets on the release. + +There is one file for each time the CI installed the project into a virtual +environment. There are multiple of these as the CI creates a number of +different environments. + +The files are created using ``pip freeze`` and will contain a full list +of the dependencies and sub-dependencies with pinned versions. + +You can download any of these files by clicking on them. It is best to use +the one that ran with the lowest Python version as this is more likely to +be compatible with all the versions of Python in the test matrix. +i.e. ``requirements-test-ubuntu-latest-3.8.txt`` in this example. + +Applying the lock file +---------------------- + +To apply a lockfile: + +- copy the requirements file you have downloaded to the root of your + repository +- rename it to requirements.txt +- commit it into the repo +- push the changes + +The CI looks for a requirements.txt in the root and will pass it to pip +when installing each of the test environments. pip will then install exactly +the same set of packages as the previous release. + +Removing dependency locking from CI +----------------------------------- + +Once the reasons for locking the build have been resolved it is a good idea +to go back to an unlocked build. This is because you get an early indication +of any incoming problems. + +To restore unlocked builds in CI simply remove requirements.txt from the root +of the repo and push. diff --git a/docs/developer/index.rst b/docs/developer/index.rst index bf291875..08d01270 100644 --- a/docs/developer/index.rst +++ b/docs/developer/index.rst @@ -32,6 +32,7 @@ side-bar. how-to/lint how-to/update-tools how-to/make-release + how-to/pin-requirements +++ From dace85fd1d8ed0885e52be6ce2588cc6937134ff Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Thu, 19 Jan 2023 14:23:01 +0000 Subject: [PATCH 46/79] add local container testing docs --- docs/developer/how-to/test-container.rst | 25 ++++++++++++++++++++++++ docs/developer/index.rst | 1 + 2 files changed, 26 insertions(+) create mode 100644 docs/developer/how-to/test-container.rst diff --git a/docs/developer/how-to/test-container.rst b/docs/developer/how-to/test-container.rst new file mode 100644 index 00000000..a4a43a6f --- /dev/null +++ b/docs/developer/how-to/test-container.rst @@ -0,0 +1,25 @@ +Container Local Build and Test +============================== + +CI builds a runtime container for the project. The local tests +checks available via ``tox -p`` do not verify this because not +all developers will have docker installed locally. + +If CI is failing to build the container, then it is best to fix and +test the problem locally. This would require that you have docker +or podman installed on your local workstation. + +In the following examples the command ``docker`` is interchangeable with +``podman`` depending on which container cli you have installed. + +To build the container and call it ``test``:: + + cd + docker build -t test . + +To verify that the container runs:: + + docker run -it test --help + +You can pass any other command line parameters to your application +instead of --help. diff --git a/docs/developer/index.rst b/docs/developer/index.rst index 08d01270..8a6369b9 100644 --- a/docs/developer/index.rst +++ b/docs/developer/index.rst @@ -33,6 +33,7 @@ side-bar. how-to/update-tools how-to/make-release how-to/pin-requirements + how-to/test-container +++ From c4a3e7c18acaad5dc2e016b93ca739c7f7486b6a Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 25 Jan 2023 11:24:13 +0000 Subject: [PATCH 47/79] Fix reference to non-existent setup.cfg file --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index f8fcdb4f..f65cb376 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,7 @@ ], "console": "integratedTerminal", "env": { - // The default config in setup.cfg's "[tool:pytest]" adds coverage. + // The default config in pyproject.toml's "[tool.pytest.ini_options]" adds coverage. // Cannot have coverage and debugging at the same time. // https://github.com/microsoft/vscode-python/issues/693 "PYTEST_ADDOPTS": "--no-cov" From ea63670bd91e27958085c31d36ee4bc72f71d306 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Fri, 27 Jan 2023 15:40:17 +0000 Subject: [PATCH 48/79] changed to use '[dev]' instead of [dev] in the docs --- .devcontainer/devcontainer.json | 4 ++-- docs/developer/tutorials/dev-install.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7a30e9be..44de8d36 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -50,5 +50,5 @@ "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind", "workspaceFolder": "${localWorkspaceFolder}", // After the container is created, install the python project in editable form - "postCreateCommand": "pip install -e .[dev]" -} \ No newline at end of file + "postCreateCommand": "pip install -e '.[dev]'" +} diff --git a/docs/developer/tutorials/dev-install.rst b/docs/developer/tutorials/dev-install.rst index c2632683..7b50cc45 100644 --- a/docs/developer/tutorials/dev-install.rst +++ b/docs/developer/tutorials/dev-install.rst @@ -28,7 +28,7 @@ requires python 3.8 or later) or to run in a container under `VSCode $ cd python3-pip-skeleton $ python3 -m venv venv $ source venv/bin/activate - $ pip install -e .[dev] + $ pip install -e '.[dev]' .. tab-item:: VSCode devcontainer From bfe204ab020181d1e162adc6dafc36a97cdb57c0 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Fri, 27 Jan 2023 11:07:39 +0000 Subject: [PATCH 49/79] Removed scheduled job in code.yaml and added keepalive-workflow --- .github/workflows/code.yml | 3 --- .github/workflows/linkcheck.yml | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 9686f0d4..ab9cd60d 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -3,9 +3,6 @@ name: Code CI on: push: pull_request: - schedule: - # Run weekly to check latest versions of dependencies - - cron: "0 8 * * WED" env: # The target python version, which must match the Dockerfile version CONTAINER_PYTHON: "3.11" diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 02d8df4c..6b64fdea 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -22,3 +22,6 @@ jobs: - name: Check links run: tox -e docs build -- -b linkcheck + + - name: Keepalive Workflow + uses: gautamkrishnar/keepalive-workflow@v1 \ No newline at end of file From 2fd923e1409d32751126b6a3b6159b20302a5df2 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Mon, 6 Feb 2023 11:54:01 +0000 Subject: [PATCH 50/79] Added the link to autogenerate precommit --- docs/developer/how-to/lint.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/developer/how-to/lint.rst b/docs/developer/how-to/lint.rst index 1086c3c4..8f4e92db 100644 --- a/docs/developer/how-to/lint.rst +++ b/docs/developer/how-to/lint.rst @@ -15,6 +15,9 @@ commit`` on just the files that have changed:: $ pre-commit install +It is also possible to `automatically enable pre-commit on cloned repositories `_. +This will result in pre-commits being enabled on every repo your user clones from now on. + Fixing issues ------------- From 913fd74c8a15628f64a5823fc7255fcee1c695a2 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Fri, 20 Jan 2023 13:57:36 +0000 Subject: [PATCH 51/79] Made suggested changes --- .../0002-switched-to-pip-skeleton.rst | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst diff --git a/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst b/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst new file mode 100644 index 00000000..41d90fd4 --- /dev/null +++ b/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst @@ -0,0 +1,35 @@ +2. Adopt python3-pip-skeleton for project structure +=================================================== + +Date: 2022-02-18 + +Status +------ + +Accepted + +Context +------- + +We should use the following `pip-skeleton `_. +The skeleton will ensure consistency in developer +environments and package management. + +Decision +-------- + +We have switched to using the skeleton. + +Consequences +------------ + +This module will use a fixed set of tools as developed in python3-pip-skeleton +and can pull from this skeleton to update the packaging to the latest techniques. + +As such, the developer environment may have changed, the following could be +different: + +- linting +- formatting +- pip venv setup +- CI/CD From 9e4055eaa450b844a9dde1b207885d4cf3107194 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 2 Mar 2023 13:16:56 +0000 Subject: [PATCH 52/79] Add explicit dependency on pytest --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0397a22d..977a6498 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dev = [ "pipdeptree", "pre-commit", "pydata-sphinx-theme>=0.12", + "pytest", "pytest-cov", "sphinx-autobuild", "sphinx-copybutton", From d5ab1fb0a00b5dafc318eca86bdf27894bcfd87a Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Fri, 24 Mar 2023 14:41:47 +0000 Subject: [PATCH 53/79] Made changes --- .pre-commit-config.yaml | 2 +- docs/developer/how-to/build-docs.rst | 2 +- src/python3_pip_skeleton/__main__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e270b08..aa2a4cb2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + rev: v4.4.0 hooks: - id: check-added-large-files - id: check-yaml diff --git a/docs/developer/how-to/build-docs.rst b/docs/developer/how-to/build-docs.rst index 79e3f780..0174fc82 100644 --- a/docs/developer/how-to/build-docs.rst +++ b/docs/developer/how-to/build-docs.rst @@ -13,7 +13,7 @@ docs that pull in docstrings from the code. `documentation_standards` The docs will be built into the ``build/html`` directory, and can be opened -locally with a web browse:: +locally with a web browser:: $ firefox build/html/index.html diff --git a/src/python3_pip_skeleton/__main__.py b/src/python3_pip_skeleton/__main__.py index c680183b..e348a31e 100644 --- a/src/python3_pip_skeleton/__main__.py +++ b/src/python3_pip_skeleton/__main__.py @@ -7,7 +7,7 @@ def main(args=None): parser = ArgumentParser() - parser.add_argument("--version", action="version", version=__version__) + parser.add_argument("-v", "--version", action="version", version=__version__) args = parser.parse_args(args) From 9cd95324f544e9bd7cd43479fdabe3b76ef6a504 Mon Sep 17 00:00:00 2001 From: Joshua Appleby Date: Tue, 4 Apr 2023 07:46:53 +0000 Subject: [PATCH 54/79] Add python 3.7 support --- .github/workflows/code.yml | 4 ++-- pyproject.toml | 7 +++++-- src/python3_pip_skeleton/__init__.py | 7 ++++++- tests/test_boilerplate_removed.py | 7 ++++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index ab9cd60d..fb468b23 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -32,12 +32,12 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest"] # can add windows-latest, macos-latest - python: ["3.9", "3.10", "3.11"] + python: ["3.8", "3.9", "3.10", "3.11"] install: ["-e .[dev]"] # Make one version be non-editable to test both paths of version code include: - os: "ubuntu-latest" - python: "3.8" + python: "3.7" install: ".[dev]" runs-on: ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index 977a6498..2aa5efc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,17 +7,20 @@ name = "python3-pip-skeleton" classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] description = "One line description of your module" -dependencies = [] # Add project dependencies here, e.g. ["click", "numpy"] +dependencies = [ + "typing-extensions;python_version<'3.8'", +] # Add project dependencies here, e.g. ["click", "numpy"] dynamic = ["version"] license.file = "LICENSE" readme = "README.rst" -requires-python = ">=3.8" +requires-python = ">=3.7" [project.optional-dependencies] dev = [ diff --git a/src/python3_pip_skeleton/__init__.py b/src/python3_pip_skeleton/__init__.py index ef94dff1..82cce3c2 100644 --- a/src/python3_pip_skeleton/__init__.py +++ b/src/python3_pip_skeleton/__init__.py @@ -1,4 +1,9 @@ -from importlib.metadata import version +import sys + +if sys.version_info < (3, 8): + from importlib_metadata import version # noqa +else: + from importlib.metadata import version # noqa __version__ = version("python3-pip-skeleton") del version diff --git a/tests/test_boilerplate_removed.py b/tests/test_boilerplate_removed.py index b823c53b..a0675d9c 100644 --- a/tests/test_boilerplate_removed.py +++ b/tests/test_boilerplate_removed.py @@ -2,9 +2,14 @@ This file checks that all the example boilerplate text has been removed. It can be deleted when all the contained tests pass """ -from importlib.metadata import metadata +import sys from pathlib import Path +if sys.version_info < (3, 8): + from importlib_metadata import metadata # noqa +else: + from importlib.metadata import metadata # noqa + ROOT = Path(__file__).parent.parent From 64d2a03ba80d52b6d22ca541bb32eb6b455d83f9 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Tue, 18 Apr 2023 16:16:32 +0100 Subject: [PATCH 55/79] Include link in dev install tutorial to epics-containers devcontainer page --- docs/developer/tutorials/dev-install.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/developer/tutorials/dev-install.rst b/docs/developer/tutorials/dev-install.rst index 7b50cc45..3a6627a1 100644 --- a/docs/developer/tutorials/dev-install.rst +++ b/docs/developer/tutorials/dev-install.rst @@ -34,9 +34,14 @@ requires python 3.8 or later) or to run in a container under `VSCode .. code:: - $ vscode python3-pip-skeleton + $ code python3-pip-skeleton # Click on 'Reopen in Container' when prompted # Open a new terminal + + .. note:: + + See the epics-containers_ documentation for more complex + use cases, such as integration with podman. See what was installed ---------------------- @@ -58,3 +63,6 @@ This will run in parallel the following checks: - `../how-to/run-tests` - `../how-to/static-analysis` - `../how-to/lint` + + +.. _epics-containers: https://epics-containers.github.io/main/user/tutorials/devcontainer.html \ No newline at end of file From 8d283aa235fd2dc95e3bda5d75bbb3e9133e118a Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 30 May 2023 12:23:29 +0100 Subject: [PATCH 56/79] Improve container build workflow This will test the container before it is pushed to GHCR --- .github/workflows/code.yml | 65 +++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index fb468b23..4e5e7e23 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -115,6 +115,9 @@ jobs: contents: read packages: write + env: + TEST_TAG: "testing" + steps: - name: Checkout uses: actions/checkout@v3 @@ -136,42 +139,66 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Docker meta + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and export to Docker local cache + uses: docker/build-push-action@v4 + with: + # Note build-args, context, file, and target must all match between this + # step and the later build-push-action, otherwise the second build-push-action + # will attempt to build the image again + build-args: | + PIP_OPTIONS=-r lockfiles/requirements.txt dist/*.whl + context: artifacts/ + file: ./Dockerfile + target: runtime + load: true + tags: ${{ env.TEST_TAG }} + # If you have a long docker build (2+ minutes), uncomment the + # following to turn on caching. For short build times this + # makes it a little slower + #cache-from: type=gha + #cache-to: type=gha,mode=max + + - name: Test cli works in cached runtime image + run: docker run docker.io/library/${{ env.TEST_TAG }} --version + + - name: Create tags for publishing image id: meta uses: docker/metadata-action@v4 with: images: ${{ env.IMAGE_REPOSITORY }} tags: | type=ref,event=tag - type=raw,value=latest + type=raw,value=latest, enable=${{ github.ref_type == 'tag' }} + # type=edge,branch=main + # Add line above to generate image for every commit to given branch, + # and uncomment the end of if clause in next step - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - - - name: Build runtime image + - name: Push cached image to container registry + if: github.ref_type == 'tag' # || github.ref_name == 'main' uses: docker/build-push-action@v3 + # This does not build the image again, it will find the image in the + # Docker cache and publish it with: + # Note build-args, context, file, and target must all match between this + # step and the previous build-push-action, otherwise this step will + # attempt to build the image again build-args: | PIP_OPTIONS=-r lockfiles/requirements.txt dist/*.whl - push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} - load: ${{ ! (github.event_name == 'push' && startsWith(github.ref, 'refs/tags')) }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} context: artifacts/ file: ./Dockerfile - # If you have a long docker build, uncomment the following to turn on caching - # For short build times this makes it a little slower - #cache-from: type=gha - #cache-to: type=gha,mode=max - - - name: Test cli works in runtime image - run: docker run ${{ env.IMAGE_REPOSITORY }} --version + target: runtime + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} release: # upload to PyPI and make a release on every tag needs: [lint, dist, test] - if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} + if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} runs-on: ubuntu-latest env: HAS_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN != '' }} From c68c75e2fa2578d2f2609abaf20a09b19b1be03e Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Fri, 2 Jun 2023 15:46:56 +0100 Subject: [PATCH 57/79] Changed coverage parameters --- .github/workflows/code.yml | 2 +- .vscode/settings.json | 7 +++++-- pyproject.toml | 3 +-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 4e5e7e23..cbc3e280 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -63,7 +63,7 @@ jobs: run: pipdeptree - name: Run tests - run: pytest + run: tox -e pytest - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 diff --git a/.vscode/settings.json b/.vscode/settings.json index 2472acfd..35402dc5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,10 @@ "python.linting.flake8Enabled": true, "python.linting.mypyEnabled": true, "python.linting.enabled": true, - "python.testing.pytestArgs": [], + "python.testing.pytestArgs": [ + "--cov=python3_pip_skeleton", + "--cov-report xml:cov.xml" + ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.formatting.provider": "black", @@ -12,4 +15,4 @@ "editor.codeActionsOnSave": { "source.organizeImports": true } -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index 2aa5efc0..ec8e6931 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,6 @@ exclude = [".tox", "venv"] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error addopts = """ --tb=native -vv --doctest-modules --doctest-glob="*.rst" - --cov=python3_pip_skeleton --cov-report term --cov-report xml:cov.xml """ # https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings filterwarnings = "error" @@ -107,7 +106,7 @@ allowlist_externals = sphinx-build sphinx-autobuild commands = - pytest: pytest {posargs} + pytest: pytest --cov=python3_pip_skeleton --cov-report term --cov-report xml:cov.xml {posargs} mypy: mypy src tests {posargs} pre-commit: pre-commit run --all-files {posargs} docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html From f02b12ec81d82a39cb4022ffd6e8c3f3f80ac711 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Mon, 5 Jun 2023 08:58:00 +0100 Subject: [PATCH 58/79] Added vscode settings --- .vscode/settings.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 35402dc5..b8cc9ad6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,10 +3,7 @@ "python.linting.flake8Enabled": true, "python.linting.mypyEnabled": true, "python.linting.enabled": true, - "python.testing.pytestArgs": [ - "--cov=python3_pip_skeleton", - "--cov-report xml:cov.xml" - ], + "python.testing.pytestArgs": ["--cov=python3_pip_skeleton", "--cov-report", "xml:cov.xml"], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.formatting.provider": "black", From 3415f1c8b7c9305600d06a6be460cab081556950 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Tue, 20 Jun 2023 11:29:18 +0100 Subject: [PATCH 59/79] Slight changes to the sed command, and ensuring output is utf-8 on windows --- .github/actions/install_requirements/action.yml | 2 +- .github/pages/make_switcher.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/install_requirements/action.yml b/.github/actions/install_requirements/action.yml index 25a146d1..c5d4db43 100644 --- a/.github/actions/install_requirements/action.yml +++ b/.github/actions/install_requirements/action.yml @@ -32,7 +32,7 @@ runs: mkdir -p lockfiles pip freeze --exclude-editable > lockfiles/${{ inputs.requirements_file }} # delete the self referencing line and make sure it isn't blank - sed -i '/file:/d' lockfiles/${{ inputs.requirements_file }} + sed -i'' -e '/file:/d' lockfiles/${{ inputs.requirements_file }} shell: bash - name: Upload lockfiles diff --git a/.github/pages/make_switcher.py b/.github/pages/make_switcher.py index 39c12772..d70367ae 100755 --- a/.github/pages/make_switcher.py +++ b/.github/pages/make_switcher.py @@ -64,7 +64,7 @@ def write_json(path: Path, repository: str, versions: str): ] text = json.dumps(struct, indent=2) print(f"JSON switcher:\n{text}") - path.write_text(text) + path.write_text(text, encoding="utf-8") def main(args=None): From 553608123944a5e8c6e7843754f350fb6b52bc7f Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 1 Sep 2023 13:47:50 +0100 Subject: [PATCH 60/79] Remove trailing spaces and rationalise newlines Some tools remove trailing whitespace by default on save, so may as well correct the originals. Ensures every file ends with an empty blank line. Again some tools do this automatically. --- .github/actions/install_requirements/action.yml | 1 - .github/pages/index.html | 2 +- .github/workflows/linkcheck.yml | 2 +- .gitignore | 1 - .vscode/extensions.json | 2 +- .vscode/launch.json | 2 +- .vscode/tasks.json | 2 +- docs/developer/how-to/build-docs.rst | 2 +- docs/developer/how-to/lint.rst | 2 -- docs/developer/how-to/make-release.rst | 2 +- docs/developer/reference/standards.rst | 2 +- docs/developer/tutorials/dev-install.rst | 10 +++++----- pyproject.toml | 4 ++-- 13 files changed, 15 insertions(+), 19 deletions(-) diff --git a/.github/actions/install_requirements/action.yml b/.github/actions/install_requirements/action.yml index c5d4db43..20d7a3ad 100644 --- a/.github/actions/install_requirements/action.yml +++ b/.github/actions/install_requirements/action.yml @@ -55,4 +55,3 @@ runs: fi fi shell: bash - diff --git a/.github/pages/index.html b/.github/pages/index.html index 80f0a009..c495f39f 100644 --- a/.github/pages/index.html +++ b/.github/pages/index.html @@ -8,4 +8,4 @@ - \ No newline at end of file + diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 6b64fdea..70cba115 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -24,4 +24,4 @@ jobs: run: tox -e docs build -- -b linkcheck - name: Keepalive Workflow - uses: gautamkrishnar/keepalive-workflow@v1 \ No newline at end of file + uses: gautamkrishnar/keepalive-workflow@v1 diff --git a/.gitignore b/.gitignore index 9fbb6bfe..62dcd9a8 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,3 @@ venv* # further build artifacts lockfiles/ - diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 81922991..cda78d45 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,4 +6,4 @@ "redhat.vscode-yaml", "ryanluker.vscode-coverage-gutters" ] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index f65cb376..3cda7432 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,4 +22,4 @@ }, } ] -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 946e69d4..c999e864 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,4 +13,4 @@ "problemMatcher": [], } ] -} \ No newline at end of file +} diff --git a/docs/developer/how-to/build-docs.rst b/docs/developer/how-to/build-docs.rst index 0174fc82..11a5e638 100644 --- a/docs/developer/how-to/build-docs.rst +++ b/docs/developer/how-to/build-docs.rst @@ -35,4 +35,4 @@ changes in this directory too:: $ tox -e docs autobuild -- --watch src -.. _sphinx: https://www.sphinx-doc.org/ \ No newline at end of file +.. _sphinx: https://www.sphinx-doc.org/ diff --git a/docs/developer/how-to/lint.rst b/docs/developer/how-to/lint.rst index 8f4e92db..ffb6ee5a 100644 --- a/docs/developer/how-to/lint.rst +++ b/docs/developer/how-to/lint.rst @@ -37,5 +37,3 @@ VSCode support The ``.vscode/settings.json`` will run black and isort formatters as well as flake8 checking on save. Issues will be highlighted in the editor window. - - diff --git a/docs/developer/how-to/make-release.rst b/docs/developer/how-to/make-release.rst index 747e44a2..d8f92f85 100644 --- a/docs/developer/how-to/make-release.rst +++ b/docs/developer/how-to/make-release.rst @@ -13,4 +13,4 @@ To make a new release, please follow this checklist: Note that tagging and pushing to the main branch has the same effect except that you will not get the option to edit the release notes. -.. _release: https://github.com/DiamondLightSource/python3-pip-skeleton/releases \ No newline at end of file +.. _release: https://github.com/DiamondLightSource/python3-pip-skeleton/releases diff --git a/docs/developer/reference/standards.rst b/docs/developer/reference/standards.rst index b78a719e..06c4af53 100644 --- a/docs/developer/reference/standards.rst +++ b/docs/developer/reference/standards.rst @@ -61,4 +61,4 @@ Docs follow the underlining convention:: .. seealso:: - How-to guide `../how-to/build-docs` \ No newline at end of file + How-to guide `../how-to/build-docs` diff --git a/docs/developer/tutorials/dev-install.rst b/docs/developer/tutorials/dev-install.rst index 3a6627a1..183b1893 100644 --- a/docs/developer/tutorials/dev-install.rst +++ b/docs/developer/tutorials/dev-install.rst @@ -37,10 +37,10 @@ requires python 3.8 or later) or to run in a container under `VSCode $ code python3-pip-skeleton # Click on 'Reopen in Container' when prompted # Open a new terminal - - .. note:: - - See the epics-containers_ documentation for more complex + + .. note:: + + See the epics-containers_ documentation for more complex use cases, such as integration with podman. See what was installed @@ -65,4 +65,4 @@ This will run in parallel the following checks: - `../how-to/lint` -.. _epics-containers: https://epics-containers.github.io/main/user/tutorials/devcontainer.html \ No newline at end of file +.. _epics-containers: https://epics-containers.github.io/main/user/tutorials/devcontainer.html diff --git a/pyproject.toml b/pyproject.toml index ec8e6931..c84d3936 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,8 +99,8 @@ skipsdist=True # Don't create a virtualenv for the command, requires tox-direct plugin direct = True passenv = * -allowlist_externals = - pytest +allowlist_externals = + pytest pre-commit mypy sphinx-build From e1504912eaf42d555468ba08f36234192ae59820 Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Tue, 12 Sep 2023 14:49:29 +0100 Subject: [PATCH 61/79] Use ruff as a linter as a replacement for flake8/isort --- .github/pages/make_switcher.py | 2 +- .gitignore | 3 ++ .pre-commit-config.yaml | 6 +-- .vscode/extensions.json | 3 +- .vscode/settings.json | 15 ++++-- docs/conf.py | 67 +++++++++++++------------- docs/developer/how-to/lint.rst | 12 ++--- docs/developer/reference/standards.rst | 3 +- pyproject.toml | 29 +++++------ 9 files changed, 73 insertions(+), 67 deletions(-) diff --git a/.github/pages/make_switcher.py b/.github/pages/make_switcher.py index d70367ae..ae227ab7 100755 --- a/.github/pages/make_switcher.py +++ b/.github/pages/make_switcher.py @@ -59,7 +59,7 @@ def get_versions(ref: str, add: Optional[str], remove: Optional[str]) -> List[st def write_json(path: Path, repository: str, versions: str): org, repo_name = repository.split("/") struct = [ - dict(version=version, url=f"https://{org}.github.io/{repo_name}/{version}/") + {"version": version, "url": f"https://{org}.github.io/{repo_name}/{version}/"} for version in versions ] text = json.dumps(struct, indent=2) diff --git a/.gitignore b/.gitignore index 62dcd9a8..a37be99b 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,6 @@ venv* # further build artifacts lockfiles/ + +# ruff cache +.ruff_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa2a4cb2..5bc9f001 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,9 +15,9 @@ repos: entry: black --check --diff types: [python] - - id: flake8 - name: Run flake8 + - id: ruff + name: Run ruff stages: [commit] language: system - entry: flake8 + entry: ruff types: [python] diff --git a/.vscode/extensions.json b/.vscode/extensions.json index cda78d45..a1227b34 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,7 @@ "ms-python.python", "tamasfe.even-better-toml", "redhat.vscode-yaml", - "ryanluker.vscode-coverage-gutters" + "ryanluker.vscode-coverage-gutters", + "charliermarsh.Ruff" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index b8cc9ad6..f5b8508f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,22 @@ { "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, + "python.linting.flake8Enabled": false, "python.linting.mypyEnabled": true, "python.linting.enabled": true, - "python.testing.pytestArgs": ["--cov=python3_pip_skeleton", "--cov-report", "xml:cov.xml"], + "python.testing.pytestArgs": [ + "--cov=python3_pip_skeleton", + "--cov-report", + "xml:cov.xml" + ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.formatting.provider": "black", "python.languageServer": "Pylance", "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": true + "[python]": { + "editor.codeActionsOnSave": { + "source.fixAll.ruff": false, + "source.organizeImports.ruff": true + } } } diff --git a/docs/conf.py b/docs/conf.py index 7022f617..82146804 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -98,17 +98,16 @@ # This means you can link things like `str` and `asyncio` to the relevant # docs in the python documentation. -intersphinx_mapping = dict(python=("https://docs.python.org/3/", None)) +intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} # A dictionary of graphviz graph attributes for inheritance diagrams. -inheritance_graph_attrs = dict(rankdir="TB") +inheritance_graph_attrs = {"rankdir": "TB"} # Common links that should be available on every page rst_epilog = """ .. _Diamond Light Source: http://www.diamond.ac.uk .. _black: https://github.com/psf/black -.. _flake8: https://flake8.pycqa.org/en/latest/ -.. _isort: https://github.com/PyCQA/isort +.. _ruff: https://beta.ruff.rs/docs/ .. _mypy: http://mypy-lang.org/ .. _pre-commit: https://pre-commit.com/ """ @@ -149,40 +148,40 @@ # Point 3 makes checking switcher difficult, because the updated skeleton # will fix the switcher at the end of the docs workflow, but never gets a chance # to complete as the docs build warns and fails. -html_theme_options = dict( - logo=dict( - text=project, - ), - use_edit_page_button=True, - github_url=f"https://github.com/{github_user}/{github_repo}", - icon_links=[ - dict( - name="PyPI", - url=f"https://pypi.org/project/{project}", - icon="fas fa-cube", - ) +html_theme_options = { + "logo": { + "text": project, + }, + "use_edit_page_button": True, + "github_url": f"https://github.com/{github_user}/{github_repo}", + "icon_links": [ + { + "name": "PyPI", + "url": f"https://pypi.org/project/{project}", + "icon": "fas fa-cube", + } ], - switcher=dict( - json_url=switcher_json, - version_match=version, - ), - check_switcher=False, - navbar_end=["theme-switcher", "icon-links", "version-switcher"], - external_links=[ - dict( - name="Release Notes", - url=f"https://github.com/{github_user}/{github_repo}/releases", - ) + "switcher": { + "json_url": switcher_json, + "version_match": version, + }, + "check_switcher": False, + "navbar_end": ["theme-switcher", "icon-links", "version-switcher"], + "external_links": [ + { + "name": "Release Notes", + "url": f"https://github.com/{github_user}/{github_repo}/releases", + } ], -) +} # A dictionary of values to pass into the template engine’s context for all pages -html_context = dict( - github_user=github_user, - github_repo=project, - github_version=version, - doc_path="docs", -) +html_context = { + "github_user": github_user, + "github_repo": project, + "github_version": version, + "doc_path": "docs", +} # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False diff --git a/docs/developer/how-to/lint.rst b/docs/developer/how-to/lint.rst index ffb6ee5a..2df258d8 100644 --- a/docs/developer/how-to/lint.rst +++ b/docs/developer/how-to/lint.rst @@ -1,7 +1,7 @@ Run linting using pre-commit ============================ -Code linting is handled by black_, flake8_ and isort_ run under pre-commit_. +Code linting is handled by black_ and ruff_ run under pre-commit_. Running pre-commit ------------------ @@ -26,14 +26,14 @@ repository:: $ black . -Likewise with isort:: +Likewise with ruff:: - $ isort . + $ ruff --fix . -If you get any flake8 issues you will have to fix those manually. +Ruff may not be able to automatically fix all issues; in this case, you will have to fix those manually. VSCode support -------------- -The ``.vscode/settings.json`` will run black and isort formatters as well as -flake8 checking on save. Issues will be highlighted in the editor window. +The ``.vscode/settings.json`` will run black formatting as well as +ruff checking on save. Issues will be highlighted in the editor window. diff --git a/docs/developer/reference/standards.rst b/docs/developer/reference/standards.rst index 06c4af53..5a1fd478 100644 --- a/docs/developer/reference/standards.rst +++ b/docs/developer/reference/standards.rst @@ -10,8 +10,7 @@ Code Standards The code in this repository conforms to standards set by the following tools: - black_ for code formatting -- flake8_ for style checks -- isort_ for import ordering +- ruff_ for style checks - mypy_ for static type checking .. seealso:: diff --git a/pyproject.toml b/pyproject.toml index c84d3936..b390e7da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,13 +26,12 @@ requires-python = ">=3.7" dev = [ "black", "mypy", - "flake8-isort", - "Flake8-pyproject", "pipdeptree", "pre-commit", "pydata-sphinx-theme>=0.12", "pytest", "pytest-cov", + "ruff", "sphinx-autobuild", "sphinx-copybutton", "sphinx-design", @@ -57,20 +56,6 @@ write_to = "src/python3_pip_skeleton/_version.py" [tool.mypy] ignore_missing_imports = true # Ignore missing stubs in imported modules -[tool.isort] -float_to_top = true -profile = "black" - -[tool.flake8] -extend-ignore = [ - "E203", # See https://github.com/PyCQA/pycodestyle/issues/373 - "F811", # support typing.overload decorator - "F722", # allow Annotated[typ, some_func("some string")] -] -max-line-length = 88 # Respect black's line length (default 88), -exclude = [".tox", "venv"] - - [tool.pytest.ini_options] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error addopts = """ @@ -111,3 +96,15 @@ commands = pre-commit: pre-commit run --all-files {posargs} docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html """ + + +[tool.ruff] +src = ["src", "tests"] +line-length = 88 +select = [ + "C4", # flake8-comprehensions - https://beta.ruff.rs/docs/rules/#flake8-comprehensions-c4 + "E", # pycodestyle errors - https://beta.ruff.rs/docs/rules/#error-e + "F", # pyflakes rules - https://beta.ruff.rs/docs/rules/#pyflakes-f + "W", # pycodestyle warnings - https://beta.ruff.rs/docs/rules/#warning-w + "I001", # isort +] From 2e53832e6c1b78e7f7927274bf05b080f280d3b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:54:19 +0000 Subject: [PATCH 62/79] Bump docker/build-push-action from 3 to 5 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 5. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v3...v5) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/code.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index cbc3e280..34e527a5 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -144,7 +144,7 @@ jobs: uses: docker/setup-buildx-action@v2 - name: Build and export to Docker local cache - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: # Note build-args, context, file, and target must all match between this # step and the later build-push-action, otherwise the second build-push-action @@ -179,7 +179,7 @@ jobs: - name: Push cached image to container registry if: github.ref_type == 'tag' # || github.ref_name == 'main' - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 # This does not build the image again, it will find the image in the # Docker cache and publish it with: From 239f39fdc49aa4a5b8024c003427b7051fc1d273 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:31:46 +0000 Subject: [PATCH 63/79] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/code.yml | 8 ++++---- .github/workflows/docs.yml | 2 +- .github/workflows/docs_clean.yml | 2 +- .github/workflows/linkcheck.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 34e527a5..0f83a490 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install python packages uses: ./.github/actions/install_requirements @@ -47,7 +47,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Need this to get version number from last tag fetch-depth: 0 @@ -77,7 +77,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Need this to get version number from last tag fetch-depth: 0 @@ -120,7 +120,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # image names must be all lower case - name: Generate image repo name diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c510d577..d89a0862 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,7 +15,7 @@ jobs: run: sleep 60 - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Need this to get version number from last tag fetch-depth: 0 diff --git a/.github/workflows/docs_clean.yml b/.github/workflows/docs_clean.yml index a67e1881..e324640e 100644 --- a/.github/workflows/docs_clean.yml +++ b/.github/workflows/docs_clean.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: gh-pages diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 70cba115..d2a80410 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install python packages uses: ./.github/actions/install_requirements From dfe03c27da2651538ff9a0dcb59ef1f3b149db86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:31:35 +0000 Subject: [PATCH 64/79] Bump docker/login-action from 2 to 3 Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 0f83a490..f7eb413d 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -133,7 +133,7 @@ jobs: - name: Log in to GitHub Docker Registry if: github.event_name != 'pull_request' - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} From 4de663b88e2918c138677959f0b783affcba6891 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:31:51 +0000 Subject: [PATCH 65/79] Bump docker/metadata-action from 4 to 5 Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4 to 5. - [Release notes](https://github.com/docker/metadata-action/releases) - [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md) - [Commits](https://github.com/docker/metadata-action/compare/v4...v5) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index f7eb413d..859b3806 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -167,7 +167,7 @@ jobs: - name: Create tags for publishing image id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE_REPOSITORY }} tags: | From 357e9274a92ec3d755580cf5dbb3650f2874354d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:31:41 +0000 Subject: [PATCH 66/79] Bump docker/setup-buildx-action from 2 to 3 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 859b3806..ae990294 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -141,7 +141,7 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build and export to Docker local cache uses: docker/build-push-action@v5 From 6f588b906eef4e134885fa9d29b031584bda3a06 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 27 Oct 2023 11:02:47 +0100 Subject: [PATCH 67/79] (#153) Fix docs build by providing default arg --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 82146804..4f313c7e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -173,6 +173,7 @@ "url": f"https://github.com/{github_user}/{github_repo}/releases", } ], + "navigation_with_keys": False, } # A dictionary of values to pass into the template engine’s context for all pages From 02e98d9261ab045dd4070b84a41f614949ffc3e1 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Sun, 17 Dec 2023 11:43:46 +0000 Subject: [PATCH 68/79] remove redundant vscode settings --- .vscode/settings.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 474dd61d..83d09f2e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,16 +1,10 @@ { - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, - "python.linting.mypyEnabled": true, - "python.linting.enabled": true, "python.testing.pytestArgs": [], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "python.formatting.provider": "black", - "python.languageServer": "Pylance", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" }, "cSpell.words": [ "gphotos" From c036b2ec7252d574ea88da8a228a5f4a92e7fa39 Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Sun, 17 Dec 2023 11:45:23 +0000 Subject: [PATCH 69/79] Rename python3-pip-skeleton -> gphotos-sync --- .github/CONTRIBUTING.rst | 6 ++-- Dockerfile | 2 +- README.rst | 34 +++++++++---------- docs/conf.py | 8 ++--- .../0002-switched-to-pip-skeleton.rst | 6 ++-- docs/developer/how-to/make-release.rst | 2 +- docs/developer/tutorials/dev-install.rst | 6 ++-- docs/index.rst | 4 +-- docs/user/how-to/run-container.rst | 6 ++-- docs/user/reference/api.rst | 8 ++--- docs/user/tutorials/installation.rst | 6 ++-- pyproject.toml | 16 ++++----- .../__init__.py | 2 +- .../__main__.py | 2 +- tests/test_boilerplate_removed.py | 2 +- tests/test_cli.py | 4 +-- 16 files changed, 57 insertions(+), 57 deletions(-) rename src/{python3_pip_skeleton => photos_sync}/__init__.py (80%) rename src/{python3_pip_skeleton => photos_sync}/__main__.py (86%) diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index 19ab494f..a3f9dd1d 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -7,7 +7,7 @@ filing a new one. If you have a great idea but it involves big changes, please file a ticket before making a pull request! We want to make sure you don't spend your time coding something that might not fit the scope of the project. -.. _GitHub: https://github.com/DiamondLightSource/python3-pip-skeleton/issues +.. _GitHub: https://github.com/gilesknap/gphotos-sync/issues Issue or Discussion? -------------------- @@ -16,7 +16,7 @@ Github also offers discussions_ as a place to ask questions and share ideas. If your issue is open ended and it is not obvious when it can be "closed", please raise it as a discussion instead. -.. _discussions: https://github.com/DiamondLightSource/python3-pip-skeleton/discussions +.. _discussions: https://github.com/gilesknap/gphotos-sync/discussions Code coverage ------------- @@ -32,4 +32,4 @@ The `Developer Guide`_ contains information on setting up a development environment, running the tests and what standards the code and documentation should follow. -.. _Developer Guide: https://diamondlightsource.github.io/python3-pip-skeleton/main/developer/how-to/contribute.html +.. _Developer Guide: https://diamondlightsource.github.io/gphotos-sync/main/developer/how-to/contribute.html diff --git a/Dockerfile b/Dockerfile index 31d05606..192d44a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,5 +33,5 @@ COPY --from=build /venv/ /venv/ ENV PATH=/venv/bin:$PATH # change this entrypoint if it is not the same as the repo -ENTRYPOINT ["python3-pip-skeleton"] +ENTRYPOINT ["gphotos-sync"] CMD ["--version"] diff --git a/README.rst b/README.rst index a014631e..c21fe6bb 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -python3-pip-skeleton +gphotos-sync =========================== |code_ci| |docs_ci| |coverage| |pypi_version| |license| @@ -13,10 +13,10 @@ This is where you should write a short paragraph that describes what your module how it does it, and why people should use it. ============== ============================================================== -PyPI ``pip install python3-pip-skeleton`` -Source code https://github.com/DiamondLightSource/python3-pip-skeleton -Documentation https://DiamondLightSource.github.io/python3-pip-skeleton -Releases https://github.com/DiamondLightSource/python3-pip-skeleton/releases +PyPI ``pip install gphotos-sync`` +Source code https://github.com/gilesknap/gphotos-sync +Documentation https://gilesknap.github.io/gphotos-sync +Releases https://github.com/gilesknap/gphotos-sync/releases ============== ============================================================== This is where you should put some images or code snippets that illustrate @@ -25,28 +25,28 @@ introductory code here: .. code-block:: python - from python3_pip_skeleton import __version__ + from photos_sync import __version__ - print(f"Hello python3_pip_skeleton {__version__}") + print(f"Hello photos_sync {__version__}") Or if it is a commandline tool then you might put some example commands here:: - $ python -m python3_pip_skeleton --version + $ python -m photos_sync --version -.. |code_ci| image:: https://github.com/DiamondLightSource/python3-pip-skeleton/actions/workflows/code.yml/badge.svg?branch=main - :target: https://github.com/DiamondLightSource/python3-pip-skeleton/actions/workflows/code.yml +.. |code_ci| image:: https://github.com/gilesknap/gphotos-sync/actions/workflows/code.yml/badge.svg?branch=main + :target: https://github.com/gilesknap/gphotos-sync/actions/workflows/code.yml :alt: Code CI -.. |docs_ci| image:: https://github.com/DiamondLightSource/python3-pip-skeleton/actions/workflows/docs.yml/badge.svg?branch=main - :target: https://github.com/DiamondLightSource/python3-pip-skeleton/actions/workflows/docs.yml +.. |docs_ci| image:: https://github.com/gilesknap/gphotos-sync/actions/workflows/docs.yml/badge.svg?branch=main + :target: https://github.com/gilesknap/gphotos-sync/actions/workflows/docs.yml :alt: Docs CI -.. |coverage| image:: https://codecov.io/gh/DiamondLightSource/python3-pip-skeleton/branch/main/graph/badge.svg - :target: https://codecov.io/gh/DiamondLightSource/python3-pip-skeleton +.. |coverage| image:: https://codecov.io/gh/gilesknap/gphotos-sync/branch/main/graph/badge.svg + :target: https://codecov.io/gh/gilesknap/gphotos-sync :alt: Test Coverage -.. |pypi_version| image:: https://img.shields.io/pypi/v/python3-pip-skeleton.svg - :target: https://pypi.org/project/python3-pip-skeleton +.. |pypi_version| image:: https://img.shields.io/pypi/v/gphotos-sync.svg + :target: https://pypi.org/project/gphotos-sync :alt: Latest PyPI version .. |license| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg @@ -57,4 +57,4 @@ Or if it is a commandline tool then you might put some example commands here:: Anything below this line is used when viewing README.rst and will be replaced when included in index.rst -See https://DiamondLightSource.github.io/python3-pip-skeleton for more detailed documentation. +See https://gilesknap.github.io/gphotos-sync for more detailed documentation. diff --git a/docs/conf.py b/docs/conf.py index 4f313c7e..bbe520b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,15 +10,15 @@ import requests -import python3_pip_skeleton +import photos_sync # -- General configuration ------------------------------------------------ # General information about the project. -project = "python3-pip-skeleton" +project = "gphotos-sync" # The full version, including alpha/beta/rc tags. -release = python3_pip_skeleton.__version__ +release = photos_sync.__version__ # The short X.Y version. if "+" in release: @@ -127,7 +127,7 @@ # html_theme = "pydata_sphinx_theme" github_repo = project -github_user = "DiamondLightSource" +github_user = "gilesknap" switcher_json = f"https://{github_user}.github.io/{github_repo}/switcher.json" switcher_exists = requests.get(switcher_json).ok if not switcher_exists: diff --git a/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst b/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst index 41d90fd4..994e08e5 100644 --- a/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst +++ b/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst @@ -1,4 +1,4 @@ -2. Adopt python3-pip-skeleton for project structure +2. Adopt gphotos-sync for project structure =================================================== Date: 2022-02-18 @@ -11,7 +11,7 @@ Accepted Context ------- -We should use the following `pip-skeleton `_. +We should use the following `pip-skeleton `_. The skeleton will ensure consistency in developer environments and package management. @@ -23,7 +23,7 @@ We have switched to using the skeleton. Consequences ------------ -This module will use a fixed set of tools as developed in python3-pip-skeleton +This module will use a fixed set of tools as developed in gphotos-sync and can pull from this skeleton to update the packaging to the latest techniques. As such, the developer environment may have changed, the following could be diff --git a/docs/developer/how-to/make-release.rst b/docs/developer/how-to/make-release.rst index d8f92f85..bb682317 100644 --- a/docs/developer/how-to/make-release.rst +++ b/docs/developer/how-to/make-release.rst @@ -13,4 +13,4 @@ To make a new release, please follow this checklist: Note that tagging and pushing to the main branch has the same effect except that you will not get the option to edit the release notes. -.. _release: https://github.com/DiamondLightSource/python3-pip-skeleton/releases +.. _release: https://github.com/gilesknap/gphotos-sync/releases diff --git a/docs/developer/tutorials/dev-install.rst b/docs/developer/tutorials/dev-install.rst index 183b1893..74d698e6 100644 --- a/docs/developer/tutorials/dev-install.rst +++ b/docs/developer/tutorials/dev-install.rst @@ -10,7 +10,7 @@ Clone the repository First clone the repository locally using `Git `_:: - $ git clone git://github.com/DiamondLightSource/python3-pip-skeleton.git + $ git clone git://github.com/gilesknap/gphotos-sync.git Install dependencies -------------------- @@ -25,7 +25,7 @@ requires python 3.8 or later) or to run in a container under `VSCode .. code:: - $ cd python3-pip-skeleton + $ cd gphotos-sync $ python3 -m venv venv $ source venv/bin/activate $ pip install -e '.[dev]' @@ -34,7 +34,7 @@ requires python 3.8 or later) or to run in a container under `VSCode .. code:: - $ code python3-pip-skeleton + $ code gphotos-sync # Click on 'Reopen in Container' when prompted # Open a new terminal diff --git a/docs/index.rst b/docs/index.rst index df33c8e6..539aade8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,13 +14,13 @@ The documentation is split into 2 sections: :link: user/index :link-type: doc - The User Guide contains documentation on how to install and use python3-pip-skeleton. + The User Guide contains documentation on how to install and use gphotos-sync. .. grid-item-card:: :material-regular:`code;4em` :link: developer/index :link-type: doc - The Developer Guide contains documentation on how to develop and contribute changes back to python3-pip-skeleton. + The Developer Guide contains documentation on how to develop and contribute changes back to gphotos-sync. .. toctree:: :hidden: diff --git a/docs/user/how-to/run-container.rst b/docs/user/how-to/run-container.rst index 84f857af..d478966c 100644 --- a/docs/user/how-to/run-container.rst +++ b/docs/user/how-to/run-container.rst @@ -1,15 +1,15 @@ Run in a container ================== -Pre-built containers with python3-pip-skeleton and its dependencies already +Pre-built containers with gphotos-sync and its dependencies already installed are available on `Github Container Registry -`_. +`_. Starting the container ---------------------- To pull the container from github container registry and run:: - $ docker run ghcr.io/DiamondLightSource/python3-pip-skeleton:main --version + $ docker run ghcr.io/gilesknap/gphotos-sync:main --version To get a released version, use a numbered release instead of ``main``. diff --git a/docs/user/reference/api.rst b/docs/user/reference/api.rst index 8544e172..cd0ba148 100644 --- a/docs/user/reference/api.rst +++ b/docs/user/reference/api.rst @@ -1,14 +1,14 @@ API === -.. automodule:: python3_pip_skeleton +.. automodule:: photos_sync - ``python3_pip_skeleton`` + ``photos_sync`` ----------------------------------- -This is the internal API reference for python3_pip_skeleton +This is the internal API reference for photos_sync -.. data:: python3_pip_skeleton.__version__ +.. data:: photos_sync.__version__ :type: str Version number as calculated by https://github.com/pypa/setuptools_scm diff --git a/docs/user/tutorials/installation.rst b/docs/user/tutorials/installation.rst index e90d3efb..b3e5f18f 100644 --- a/docs/user/tutorials/installation.rst +++ b/docs/user/tutorials/installation.rst @@ -25,14 +25,14 @@ Installing the library You can now use ``pip`` to install the library and its dependencies:: - $ python3 -m pip install python3-pip-skeleton + $ python3 -m pip install gphotos-sync If you require a feature that is not currently released you can also install from github:: - $ python3 -m pip install git+https://github.com/DiamondLightSource/python3-pip-skeleton.git + $ python3 -m pip install git+https://github.com/gilesknap/gphotos-sync.git The library should now be installed and the commandline interface on your path. You can check the version that has been installed by typing:: - $ python3-pip-skeleton --version + $ gphotos-sync --version diff --git a/pyproject.toml b/pyproject.toml index b390e7da..7bbb776b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "python3-pip-skeleton" +name = "gphotos-sync" classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: Apache Software License", @@ -40,18 +40,18 @@ dev = [ ] [project.scripts] -python3-pip-skeleton = "python3_pip_skeleton.__main__:main" +gphotos-sync = "photos_sync.__main__:main" [project.urls] -GitHub = "https://github.com/DiamondLightSource/python3-pip-skeleton" +GitHub = "https://github.com/gilesknap/gphotos-sync" [[project.authors]] # Further authors may be added by duplicating this section -email = "email@address.com" -name = "Firstname Lastname" +email = "gilesknap@gmail.com" +name = "Giles Knap" [tool.setuptools_scm] -write_to = "src/python3_pip_skeleton/_version.py" +write_to = "src/photos_sync/_version.py" [tool.mypy] ignore_missing_imports = true # Ignore missing stubs in imported modules @@ -67,7 +67,7 @@ filterwarnings = "error" testpaths = "docs src tests" [tool.coverage.run] -data_file = "/tmp/python3_pip_skeleton.coverage" +data_file = "/tmp/photos_sync.coverage" [tool.coverage.paths] # Tests are run from installed location, map back to the src directory @@ -91,7 +91,7 @@ allowlist_externals = sphinx-build sphinx-autobuild commands = - pytest: pytest --cov=python3_pip_skeleton --cov-report term --cov-report xml:cov.xml {posargs} + pytest: pytest --cov=photos_sync --cov-report term --cov-report xml:cov.xml {posargs} mypy: mypy src tests {posargs} pre-commit: pre-commit run --all-files {posargs} docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html diff --git a/src/python3_pip_skeleton/__init__.py b/src/photos_sync/__init__.py similarity index 80% rename from src/python3_pip_skeleton/__init__.py rename to src/photos_sync/__init__.py index 82cce3c2..49865e13 100644 --- a/src/python3_pip_skeleton/__init__.py +++ b/src/photos_sync/__init__.py @@ -5,7 +5,7 @@ else: from importlib.metadata import version # noqa -__version__ = version("python3-pip-skeleton") +__version__ = version("gphotos-sync") del version __all__ = ["__version__"] diff --git a/src/python3_pip_skeleton/__main__.py b/src/photos_sync/__main__.py similarity index 86% rename from src/python3_pip_skeleton/__main__.py rename to src/photos_sync/__main__.py index e348a31e..8c8b8792 100644 --- a/src/python3_pip_skeleton/__main__.py +++ b/src/photos_sync/__main__.py @@ -11,6 +11,6 @@ def main(args=None): args = parser.parse_args(args) -# test with: python -m python3_pip_skeleton +# test with: python -m photos_sync if __name__ == "__main__": main() diff --git a/tests/test_boilerplate_removed.py b/tests/test_boilerplate_removed.py index a0675d9c..e9e85f96 100644 --- a/tests/test_boilerplate_removed.py +++ b/tests/test_boilerplate_removed.py @@ -31,7 +31,7 @@ def assert_not_contains_text(path: str, text: str, explanation: str): # pyproject.toml def test_module_summary(): - summary = metadata("python3-pip-skeleton")["summary"] + summary = metadata("gphotos-sync")["summary"] skeleton_check( "One line description of your module" in summary, "Please change project.description in ./pyproject.toml " diff --git a/tests/test_cli.py b/tests/test_cli.py index 2ff648c0..a74f90e7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,9 +1,9 @@ import subprocess import sys -from python3_pip_skeleton import __version__ +from photos_sync import __version__ def test_cli_version(): - cmd = [sys.executable, "-m", "python3_pip_skeleton", "--version"] + cmd = [sys.executable, "-m", "photos_sync", "--version"] assert subprocess.check_output(cmd).decode().strip() == __version__ From 15c52ea8a15b12b7528284aa05121718d75cedca Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Sun, 17 Dec 2023 12:49:43 +0000 Subject: [PATCH 70/79] fixes for skeleton merge - remove new docs --- docs/conf.py | 195 ------------------ docs/developer/explanations/decisions.rst | 17 -- .../0001-record-architecture-decisions.rst | 26 --- .../0002-switched-to-pip-skeleton.rst | 35 ---- docs/developer/how-to/build-docs.rst | 38 ---- docs/developer/how-to/contribute.rst | 1 - docs/developer/how-to/lint.rst | 39 ---- docs/developer/how-to/make-release.rst | 16 -- docs/developer/how-to/pin-requirements.rst | 74 ------- docs/developer/how-to/run-tests.rst | 12 -- docs/developer/how-to/static-analysis.rst | 8 - docs/developer/how-to/test-container.rst | 25 --- docs/developer/how-to/update-tools.rst | 16 -- docs/developer/index.rst | 64 ------ docs/developer/reference/standards.rst | 63 ------ docs/developer/tutorials/dev-install.rst | 68 ------ docs/genindex.rst | 5 - docs/images/dls-favicon.ico | Bin 99678 -> 0 bytes docs/images/dls-logo.svg | 11 - docs/index.rst | 29 --- docs/user/explanations/docs-structure.rst | 18 -- docs/user/how-to/run-container.rst | 15 -- docs/user/index.rst | 57 ----- docs/user/reference/api.rst | 14 -- docs/user/tutorials/installation.rst | 38 ---- pyproject.toml | 7 +- setup.cfg | 141 ------------- tests/test_credentials/.gphotos.token | 2 +- 28 files changed, 6 insertions(+), 1028 deletions(-) delete mode 100644 docs/conf.py delete mode 100644 docs/developer/explanations/decisions.rst delete mode 100644 docs/developer/explanations/decisions/0001-record-architecture-decisions.rst delete mode 100644 docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst delete mode 100644 docs/developer/how-to/build-docs.rst delete mode 100644 docs/developer/how-to/contribute.rst delete mode 100644 docs/developer/how-to/lint.rst delete mode 100644 docs/developer/how-to/make-release.rst delete mode 100644 docs/developer/how-to/pin-requirements.rst delete mode 100644 docs/developer/how-to/run-tests.rst delete mode 100644 docs/developer/how-to/static-analysis.rst delete mode 100644 docs/developer/how-to/test-container.rst delete mode 100644 docs/developer/how-to/update-tools.rst delete mode 100644 docs/developer/index.rst delete mode 100644 docs/developer/reference/standards.rst delete mode 100644 docs/developer/tutorials/dev-install.rst delete mode 100644 docs/genindex.rst delete mode 100644 docs/images/dls-favicon.ico delete mode 100644 docs/images/dls-logo.svg delete mode 100644 docs/index.rst delete mode 100644 docs/user/explanations/docs-structure.rst delete mode 100644 docs/user/how-to/run-container.rst delete mode 100644 docs/user/index.rst delete mode 100644 docs/user/reference/api.rst delete mode 100644 docs/user/tutorials/installation.rst delete mode 100644 setup.cfg diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index bbe520b8..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,195 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -import sys -from pathlib import Path -from subprocess import check_output - -import requests - -import photos_sync - -# -- General configuration ------------------------------------------------ - -# General information about the project. -project = "gphotos-sync" - -# The full version, including alpha/beta/rc tags. -release = photos_sync.__version__ - -# The short X.Y version. -if "+" in release: - # Not on a tag, use branch name - root = Path(__file__).absolute().parent.parent - git_branch = check_output("git branch --show-current".split(), cwd=root) - version = git_branch.decode().strip() -else: - version = release - -extensions = [ - # Use this for generating API docs - "sphinx.ext.autodoc", - # This can parse google style docstrings - "sphinx.ext.napoleon", - # For linking to external sphinx documentation - "sphinx.ext.intersphinx", - # Add links to source code in API docs - "sphinx.ext.viewcode", - # Adds the inheritance-diagram generation directive - "sphinx.ext.inheritance_diagram", - # Add a copy button to each code block - "sphinx_copybutton", - # For the card element - "sphinx_design", -] - -# If true, Sphinx will warn about all references where the target cannot -# be found. -nitpicky = True - -# A list of (type, target) tuples (by default empty) that should be ignored when -# generating warnings in "nitpicky mode". Note that type should include the -# domain name if present. Example entries would be ('py:func', 'int') or -# ('envvar', 'LD_LIBRARY_PATH'). -nitpick_ignore = [ - ("py:class", "NoneType"), - ("py:class", "'str'"), - ("py:class", "'float'"), - ("py:class", "'int'"), - ("py:class", "'bool'"), - ("py:class", "'object'"), - ("py:class", "'id'"), - ("py:class", "typing_extensions.Literal"), -] - -# Both the class’ and the __init__ method’s docstring are concatenated and -# inserted into the main body of the autoclass directive -autoclass_content = "both" - -# Order the members by the order they appear in the source code -autodoc_member_order = "bysource" - -# Don't inherit docstrings from baseclasses -autodoc_inherit_docstrings = False - -# Output graphviz directive produced images in a scalable format -graphviz_output_format = "svg" - -# The name of a reST role (builtin or Sphinx extension) to use as the default -# role, that is, for text marked up `like this` -default_role = "any" - -# The suffix of source filenames. -source_suffix = ".rst" - -# The master toctree document. -master_doc = "index" - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# These patterns also affect html_static_path and html_extra_path -exclude_patterns = ["_build"] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# This means you can link things like `str` and `asyncio` to the relevant -# docs in the python documentation. -intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} - -# A dictionary of graphviz graph attributes for inheritance diagrams. -inheritance_graph_attrs = {"rankdir": "TB"} - -# Common links that should be available on every page -rst_epilog = """ -.. _Diamond Light Source: http://www.diamond.ac.uk -.. _black: https://github.com/psf/black -.. _ruff: https://beta.ruff.rs/docs/ -.. _mypy: http://mypy-lang.org/ -.. _pre-commit: https://pre-commit.com/ -""" - -# Ignore localhost links for periodic check that links in docs are valid -linkcheck_ignore = [r"http://localhost:\d+/"] - -# Set copy-button to ignore python and bash prompts -# https://sphinx-copybutton.readthedocs.io/en/latest/use.html#using-regexp-prompt-identifiers -copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " -copybutton_prompt_is_regexp = True - -# -- 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 = "pydata_sphinx_theme" -github_repo = project -github_user = "gilesknap" -switcher_json = f"https://{github_user}.github.io/{github_repo}/switcher.json" -switcher_exists = requests.get(switcher_json).ok -if not switcher_exists: - print( - "*** Can't read version switcher, is GitHub pages enabled? \n" - " Once Docs CI job has successfully run once, set the " - "Github pages source branch to be 'gh-pages' at:\n" - f" https://github.com/{github_user}/{github_repo}/settings/pages", - file=sys.stderr, - ) - -# Theme options for pydata_sphinx_theme -# We don't check switcher because there are 3 possible states for a repo: -# 1. New project, docs are not published so there is no switcher -# 2. Existing project with latest skeleton, switcher exists and works -# 3. Existing project with old skeleton that makes broken switcher, -# switcher exists but is broken -# Point 3 makes checking switcher difficult, because the updated skeleton -# will fix the switcher at the end of the docs workflow, but never gets a chance -# to complete as the docs build warns and fails. -html_theme_options = { - "logo": { - "text": project, - }, - "use_edit_page_button": True, - "github_url": f"https://github.com/{github_user}/{github_repo}", - "icon_links": [ - { - "name": "PyPI", - "url": f"https://pypi.org/project/{project}", - "icon": "fas fa-cube", - } - ], - "switcher": { - "json_url": switcher_json, - "version_match": version, - }, - "check_switcher": False, - "navbar_end": ["theme-switcher", "icon-links", "version-switcher"], - "external_links": [ - { - "name": "Release Notes", - "url": f"https://github.com/{github_user}/{github_repo}/releases", - } - ], - "navigation_with_keys": False, -} - -# A dictionary of values to pass into the template engine’s context for all pages -html_context = { - "github_user": github_user, - "github_repo": project, - "github_version": version, - "doc_path": "docs", -} - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -html_show_sphinx = False - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -html_show_copyright = False - -# Logo -html_logo = "images/dls-logo.svg" -html_favicon = "images/dls-favicon.ico" diff --git a/docs/developer/explanations/decisions.rst b/docs/developer/explanations/decisions.rst deleted file mode 100644 index 5841e6ea..00000000 --- a/docs/developer/explanations/decisions.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. This Source Code Form is subject to the terms of the Mozilla Public -.. License, v. 2.0. If a copy of the MPL was not distributed with this -.. file, You can obtain one at http://mozilla.org/MPL/2.0/. - -Architectural Decision Records -============================== - -We record major architectural decisions in Architecture Decision Records (ADRs), -as `described by Michael Nygard -`_. -Below is the list of our current ADRs. - -.. toctree:: - :maxdepth: 1 - :glob: - - decisions/* \ No newline at end of file diff --git a/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst b/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst deleted file mode 100644 index b2d3d0fe..00000000 --- a/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst +++ /dev/null @@ -1,26 +0,0 @@ -1. Record architecture decisions -================================ - -Date: 2022-02-18 - -Status ------- - -Accepted - -Context -------- - -We need to record the architectural decisions made on this project. - -Decision --------- - -We will use Architecture Decision Records, as `described by Michael Nygard -`_. - -Consequences ------------- - -See Michael Nygard's article, linked above. To create new ADRs we will copy and -paste from existing ones. diff --git a/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst b/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst deleted file mode 100644 index 994e08e5..00000000 --- a/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst +++ /dev/null @@ -1,35 +0,0 @@ -2. Adopt gphotos-sync for project structure -=================================================== - -Date: 2022-02-18 - -Status ------- - -Accepted - -Context -------- - -We should use the following `pip-skeleton `_. -The skeleton will ensure consistency in developer -environments and package management. - -Decision --------- - -We have switched to using the skeleton. - -Consequences ------------- - -This module will use a fixed set of tools as developed in gphotos-sync -and can pull from this skeleton to update the packaging to the latest techniques. - -As such, the developer environment may have changed, the following could be -different: - -- linting -- formatting -- pip venv setup -- CI/CD diff --git a/docs/developer/how-to/build-docs.rst b/docs/developer/how-to/build-docs.rst deleted file mode 100644 index 11a5e638..00000000 --- a/docs/developer/how-to/build-docs.rst +++ /dev/null @@ -1,38 +0,0 @@ -Build the docs using sphinx -=========================== - -You can build the `sphinx`_ based docs from the project directory by running:: - - $ tox -e docs - -This will build the static docs on the ``docs`` directory, which includes API -docs that pull in docstrings from the code. - -.. seealso:: - - `documentation_standards` - -The docs will be built into the ``build/html`` directory, and can be opened -locally with a web browser:: - - $ firefox build/html/index.html - -Autobuild ---------- - -You can also run an autobuild process, which will watch your ``docs`` -directory for changes and rebuild whenever it sees changes, reloading any -browsers watching the pages:: - - $ tox -e docs autobuild - -You can view the pages at localhost:: - - $ firefox http://localhost:8000 - -If you are making changes to source code too, you can tell it to watch -changes in this directory too:: - - $ tox -e docs autobuild -- --watch src - -.. _sphinx: https://www.sphinx-doc.org/ diff --git a/docs/developer/how-to/contribute.rst b/docs/developer/how-to/contribute.rst deleted file mode 100644 index 65b992f0..00000000 --- a/docs/developer/how-to/contribute.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../../.github/CONTRIBUTING.rst diff --git a/docs/developer/how-to/lint.rst b/docs/developer/how-to/lint.rst deleted file mode 100644 index 2df258d8..00000000 --- a/docs/developer/how-to/lint.rst +++ /dev/null @@ -1,39 +0,0 @@ -Run linting using pre-commit -============================ - -Code linting is handled by black_ and ruff_ run under pre-commit_. - -Running pre-commit ------------------- - -You can run the above checks on all files with this command:: - - $ tox -e pre-commit - -Or you can install a pre-commit hook that will run each time you do a ``git -commit`` on just the files that have changed:: - - $ pre-commit install - -It is also possible to `automatically enable pre-commit on cloned repositories `_. -This will result in pre-commits being enabled on every repo your user clones from now on. - -Fixing issues -------------- - -If black reports an issue you can tell it to reformat all the files in the -repository:: - - $ black . - -Likewise with ruff:: - - $ ruff --fix . - -Ruff may not be able to automatically fix all issues; in this case, you will have to fix those manually. - -VSCode support --------------- - -The ``.vscode/settings.json`` will run black formatting as well as -ruff checking on save. Issues will be highlighted in the editor window. diff --git a/docs/developer/how-to/make-release.rst b/docs/developer/how-to/make-release.rst deleted file mode 100644 index bb682317..00000000 --- a/docs/developer/how-to/make-release.rst +++ /dev/null @@ -1,16 +0,0 @@ -Make a release -============== - -To make a new release, please follow this checklist: - -- Choose a new PEP440 compliant release number (see https://peps.python.org/pep-0440/) -- Go to the GitHub release_ page -- Choose ``Draft New Release`` -- Click ``Choose Tag`` and supply the new tag you chose (click create new tag) -- Click ``Generate release notes``, review and edit these notes -- Choose a title and click ``Publish Release`` - -Note that tagging and pushing to the main branch has the same effect except that -you will not get the option to edit the release notes. - -.. _release: https://github.com/gilesknap/gphotos-sync/releases diff --git a/docs/developer/how-to/pin-requirements.rst b/docs/developer/how-to/pin-requirements.rst deleted file mode 100644 index 89639623..00000000 --- a/docs/developer/how-to/pin-requirements.rst +++ /dev/null @@ -1,74 +0,0 @@ -Pinning Requirements -==================== - -Introduction ------------- - -By design this project only defines dependencies in one place, i.e. in -the ``requires`` table in ``pyproject.toml``. - -In the ``requires`` table it is possible to pin versions of some dependencies -as needed. For library projects it is best to leave pinning to a minimum so -that your library can be used by the widest range of applications. - -When CI builds the project it will use the latest compatible set of -dependencies available (after applying your pins and any dependencies' pins). - -This approach means that there is a possibility that a future build may -break because an updated release of a dependency has made a breaking change. - -The correct way to fix such an issue is to work out the minimum pinning in -``requires`` that will resolve the problem. However this can be quite hard to -do and may be time consuming when simply trying to release a minor update. - -For this reason we provide a mechanism for locking all dependencies to -the same version as a previous successful release. This is a quick fix that -should guarantee a successful CI build. - -Finding the lock files ----------------------- - -Every release of the project will have a set of requirements files published -as release assets. - -For example take a look at the release page for python3-pip-skeleton-cli here: -https://github.com/DiamondLightSource/python3-pip-skeleton-cli/releases/tag/3.3.0 - -There is a list of requirements*.txt files showing as assets on the release. - -There is one file for each time the CI installed the project into a virtual -environment. There are multiple of these as the CI creates a number of -different environments. - -The files are created using ``pip freeze`` and will contain a full list -of the dependencies and sub-dependencies with pinned versions. - -You can download any of these files by clicking on them. It is best to use -the one that ran with the lowest Python version as this is more likely to -be compatible with all the versions of Python in the test matrix. -i.e. ``requirements-test-ubuntu-latest-3.8.txt`` in this example. - -Applying the lock file ----------------------- - -To apply a lockfile: - -- copy the requirements file you have downloaded to the root of your - repository -- rename it to requirements.txt -- commit it into the repo -- push the changes - -The CI looks for a requirements.txt in the root and will pass it to pip -when installing each of the test environments. pip will then install exactly -the same set of packages as the previous release. - -Removing dependency locking from CI ------------------------------------ - -Once the reasons for locking the build have been resolved it is a good idea -to go back to an unlocked build. This is because you get an early indication -of any incoming problems. - -To restore unlocked builds in CI simply remove requirements.txt from the root -of the repo and push. diff --git a/docs/developer/how-to/run-tests.rst b/docs/developer/how-to/run-tests.rst deleted file mode 100644 index d2e03644..00000000 --- a/docs/developer/how-to/run-tests.rst +++ /dev/null @@ -1,12 +0,0 @@ -Run the tests using pytest -========================== - -Testing is done with pytest_. It will find functions in the project that `look -like tests`_, and run them to check for errors. You can run it with:: - - $ tox -e pytest - -It will also report coverage to the commandline and to ``cov.xml``. - -.. _pytest: https://pytest.org/ -.. _look like tests: https://docs.pytest.org/explanation/goodpractices.html#test-discovery diff --git a/docs/developer/how-to/static-analysis.rst b/docs/developer/how-to/static-analysis.rst deleted file mode 100644 index 065920e1..00000000 --- a/docs/developer/how-to/static-analysis.rst +++ /dev/null @@ -1,8 +0,0 @@ -Run static analysis using mypy -============================== - -Static type analysis is done with mypy_. It checks type definition in source -files without running them, and highlights potential issues where types do not -match. You can run it with:: - - $ tox -e mypy diff --git a/docs/developer/how-to/test-container.rst b/docs/developer/how-to/test-container.rst deleted file mode 100644 index a4a43a6f..00000000 --- a/docs/developer/how-to/test-container.rst +++ /dev/null @@ -1,25 +0,0 @@ -Container Local Build and Test -============================== - -CI builds a runtime container for the project. The local tests -checks available via ``tox -p`` do not verify this because not -all developers will have docker installed locally. - -If CI is failing to build the container, then it is best to fix and -test the problem locally. This would require that you have docker -or podman installed on your local workstation. - -In the following examples the command ``docker`` is interchangeable with -``podman`` depending on which container cli you have installed. - -To build the container and call it ``test``:: - - cd - docker build -t test . - -To verify that the container runs:: - - docker run -it test --help - -You can pass any other command line parameters to your application -instead of --help. diff --git a/docs/developer/how-to/update-tools.rst b/docs/developer/how-to/update-tools.rst deleted file mode 100644 index c1075ee8..00000000 --- a/docs/developer/how-to/update-tools.rst +++ /dev/null @@ -1,16 +0,0 @@ -Update the tools -================ - -This module is merged with the python3-pip-skeleton_. This is a generic -Python project structure which provides a means to keep tools and -techniques in sync between multiple Python projects. To update to the -latest version of the skeleton, run:: - - $ git pull --rebase=false https://github.com/DiamondLightSource/python3-pip-skeleton - -Any merge conflicts will indicate an area where something has changed that -conflicts with the setup of the current module. Check the `closed pull requests -`_ -of the skeleton module for more details. - -.. _python3-pip-skeleton: https://DiamondLightSource.github.io/python3-pip-skeleton diff --git a/docs/developer/index.rst b/docs/developer/index.rst deleted file mode 100644 index 8a6369b9..00000000 --- a/docs/developer/index.rst +++ /dev/null @@ -1,64 +0,0 @@ -Developer Guide -=============== - -Documentation is split into four categories, also accessible from links in the -side-bar. - -.. grid:: 2 - :gutter: 4 - - .. grid-item-card:: :material-regular:`directions_run;3em` - - .. toctree:: - :caption: Tutorials - :maxdepth: 1 - - tutorials/dev-install - - +++ - - Tutorials for getting up and running as a developer. - - .. grid-item-card:: :material-regular:`task;3em` - - .. toctree:: - :caption: How-to Guides - :maxdepth: 1 - - how-to/contribute - how-to/build-docs - how-to/run-tests - how-to/static-analysis - how-to/lint - how-to/update-tools - how-to/make-release - how-to/pin-requirements - how-to/test-container - - +++ - - Practical step-by-step guides for day-to-day dev tasks. - - .. grid-item-card:: :material-regular:`apartment;3em` - - .. toctree:: - :caption: Explanations - :maxdepth: 1 - - explanations/decisions - - +++ - - Explanations of how and why the architecture is why it is. - - .. grid-item-card:: :material-regular:`description;3em` - - .. toctree:: - :caption: Reference - :maxdepth: 1 - - reference/standards - - +++ - - Technical reference material on standards in use. diff --git a/docs/developer/reference/standards.rst b/docs/developer/reference/standards.rst deleted file mode 100644 index 5a1fd478..00000000 --- a/docs/developer/reference/standards.rst +++ /dev/null @@ -1,63 +0,0 @@ -Standards -========= - -This document defines the code and documentation standards used in this -repository. - -Code Standards --------------- - -The code in this repository conforms to standards set by the following tools: - -- black_ for code formatting -- ruff_ for style checks -- mypy_ for static type checking - -.. seealso:: - - How-to guides `../how-to/lint` and `../how-to/static-analysis` - -.. _documentation_standards: - -Documentation Standards ------------------------ - -Docstrings are pre-processed using the Sphinx Napoleon extension. As such, -google-style_ is considered as standard for this repository. Please use type -hints in the function signature for types. For example: - -.. code:: python - - def func(arg1: str, arg2: int) -> bool: - """Summary line. - - Extended description of function. - - Args: - arg1: Description of arg1 - arg2: Description of arg2 - - Returns: - Description of return value - """ - return True - -.. _google-style: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/index.html#google-vs-numpy - -Documentation is contained in the ``docs`` directory and extracted from -docstrings of the API. - -Docs follow the underlining convention:: - - Headling 1 (page title) - ======================= - - Heading 2 - --------- - - Heading 3 - ~~~~~~~~~ - -.. seealso:: - - How-to guide `../how-to/build-docs` diff --git a/docs/developer/tutorials/dev-install.rst b/docs/developer/tutorials/dev-install.rst deleted file mode 100644 index 74d698e6..00000000 --- a/docs/developer/tutorials/dev-install.rst +++ /dev/null @@ -1,68 +0,0 @@ -Developer install -================= - -These instructions will take you through the minimal steps required to get a dev -environment setup, so you can run the tests locally. - -Clone the repository --------------------- - -First clone the repository locally using `Git -`_:: - - $ git clone git://github.com/gilesknap/gphotos-sync.git - -Install dependencies --------------------- - -You can choose to either develop on the host machine using a `venv` (which -requires python 3.8 or later) or to run in a container under `VSCode -`_ - -.. tab-set:: - - .. tab-item:: Local virtualenv - - .. code:: - - $ cd gphotos-sync - $ python3 -m venv venv - $ source venv/bin/activate - $ pip install -e '.[dev]' - - .. tab-item:: VSCode devcontainer - - .. code:: - - $ code gphotos-sync - # Click on 'Reopen in Container' when prompted - # Open a new terminal - - .. note:: - - See the epics-containers_ documentation for more complex - use cases, such as integration with podman. - -See what was installed ----------------------- - -To see a graph of the python package dependency tree type:: - - $ pipdeptree - -Build and test --------------- - -Now you have a development environment you can run the tests in a terminal:: - - $ tox -p - -This will run in parallel the following checks: - -- `../how-to/build-docs` -- `../how-to/run-tests` -- `../how-to/static-analysis` -- `../how-to/lint` - - -.. _epics-containers: https://epics-containers.github.io/main/user/tutorials/devcontainer.html diff --git a/docs/genindex.rst b/docs/genindex.rst deleted file mode 100644 index 93eb8b29..00000000 --- a/docs/genindex.rst +++ /dev/null @@ -1,5 +0,0 @@ -API Index -========= - -.. - https://stackoverflow.com/a/42310803 diff --git a/docs/images/dls-favicon.ico b/docs/images/dls-favicon.ico deleted file mode 100644 index 9a11f508ef8aed28f14c5ce0d8408e1ec8b614a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99678 zcmeI537lO;m4{y-5^&fxAd7U62#m-odom^>E-11%hzcU;%qW8>qNAV!X%rBR1O<_G zlo>@u83#o{W)z#S%OZm8BQq&!G^+HP(&n3&@W+)-9$hLOTPl^tjT;RAocFi!Zm+$n;Ww8`pBh^#O`bd$ z-t~}DY10X%Qg3fHyy2+Q{%4m;y8?rxKpcFJm&pY|u-6wCRW5(cjPg@`tAm&CdMS9B z-%o#TQRNE0?HvbX^O@z1HkeVq^0H->N|}gPE~^Af__3Vl@}-qP@2*;2sSxMtEoPQq zYs1-$wB*q@dPX_;yP4(Sk(Y_=xW{?7G2ax2xYNn62IKezl`B5Buo8T2aYcCq3)VS_ z2|mxetNC`;i~d2h<| z1L0&p|I2sR_3;k8>*A623f?_wr#*T>B~WUWL3O6z&+%LSv3#@RlJ;qyHRj!$W|xB( zN%WHym4NyQ9$Hfg9(}nIY|8IzDf?2s?L21)2hy%J={F+IpH>IKr=B0mmvt^~WxsY|c^bETWshNJpW zo$@@vv!?nyiT?vrUORpeluB!QN~QiWrBdJegHP`$_({ZLzALWMD6RO+IG)Ko;$Mxr zZTricy>@2#IB>ms%#88_@SR08{a5sSWpQPZ-fcLue2wC4*IyQkE5reRJkK>V)&{E% z92jcH7t#KVy8@nOXuCIU{mHcfy&?D^&(3*~*uKBK5q)ne?R>4thi)5uo^}hZ1Mv;x z{>%rxJDI*_y$&v2R#^*-Y1_{p;)z-Cfk*5Fyhl_f>NJ@C(okN?Q~cX?FFL&S{xv}W zEy8*M*5Bamnd$?A*(yZ;*}=7!GXGstcPv-!+svtxk;n?+nIj;uKAVVKj4>H-SrGs?lGN^-$l0Z(cPHo;nGh{BdY^4mkch_3#He)3d}>zw>nrufYt`-Uf^x z0&5B|PXf01zW6tJ{!nG#y1%>$ZElsJPn55|eJW#CR`+Fi1pKhZlcHdf=jyHClkkUQ zqrSWEz7GCb-8AGnH+@u?ypIFV$T8NAe+YH9E_?Q&d~`VN--Z$Oo4l`~ZtsoyX5P_P zf_YX)5G(v8{mX6>bd}&2yt8G*7f2(%W#B~l|GM@^IHb8--!6QO3C11uTy*|QW9Sjp7Rc)X`oQHj?0=(Pqw3p^ zqu;wTwitIH@~r#a4T~OU)1K`2+ihDPm^AQF*-*m)ZOP**fh8%qAo4#;w8A1NQUC9Xpx)qI~4V-LvBGFZ5~6 zN8Eg(!oXaJejuDzN9Ak3Q$0{mskHb2d@pVuZsVXjPb;^bzkY8;d#JX_*nY9s+)ALi zyq%ZxdoBI!+wiIlUHDnU>YL&Z)ZZ{3#k){OaPrh#XC-N_BJKFB`J}}g3!fCP2JYq5 z=e;}&c-B-O{nooHh;uA)H%WtMzK1-#e@qbcjtVNJ(v)?j(xf$|QqR&-X|sM8#lYW9pmxw^n**Nr$3;l zcor0v@`QQ}{AF*QQ=Y-MKN9Cs;-1hmyS)8uDOB3zz-dcl%G0)-Rlc8gRntMK%}F2P zy7xM=meNp;2k%`Ie1W*HYgIAGYa5>L@vP)Q=NT{`t{k5!LhU6{s`YXJ3w<5~0 z`Kz;>I6s;&zf&peU<4Z8;5#mNRE)L1bNr^ ziwi#~Ou7djVE({*;?^1;lH$gF(|UQMPP*hc_$luzto?4!`1j$Ic#-h;g*Quw+^F*z z!(2SU{RHN87rF1#!WvVggD%R6w@A00maqFA+%Kga{oZ|_7QP-H5#@e|F!5E|gXS}? z({hLO#P<4z9p_fk!UMg^fX%>djLD%rN*d1QdsLej5BjV%Kb&gW02myvw&q_aF~5}T z<~rZL0PZt*78%^q{HQknEbVAN%YH#HPLAl;XFB~9S*vbMNoDcv3*f$j=cP2f^*yT1 zt1TcC4x_o&JzS?cck@B64}Qd$Xgi<20Pba;)h^tqu-)cOdlCPSikn4$VyAQ4Q`Wvv z#Xq(E*lk|zMRLELzxx~AlwGCa?>%WRZah2ewx=w80sNQqB=%ps&BwJD8xQ@4uMM+t zU_Cw&f2FhAQYAAGP@? z{t_48e*af%y;}0B{VmIH^razx(mGIF*`f0#jKOv5j0U#WI@Mn6`J4Hc#aF(@-N)Q1 zOBy$hX;0E~xZe~;2K^W^&{k3M+hLBrH7b45JKL`4*VIE&+_Y~oxKz-ih4V1l(OqdU ze7|e`%Q)K(&lgTyd~m+s$p6emPKk?`_q}uo#`Q+nG3147(t-2o27lR5(uV4EoF-mg zU;1d{Bv0gp6O|5JSJ8Ir)(q&&v3w{BM%uec=%tL4{wOWJ&v(hqrtXc8zFPfwnGc+# zxLVUN?tql>Ith;Z4IEdZBiz>DZTqyTFS_ybhS8yhHtZ^cw9MSBOgT-tse-T`YW31rt*8EKy_tFp4YY`A z>N%|V!Tn^D0ny9TY&$Koh;?t7Q{En35Jwcz#9P3rKS;a_0`QfIIX9I*C#A`-3U#GtG{o?b2|G@o|K(!L|MYJQI^=fDLW+S619$izU~?F_!3WB`KnEW zYPr9TFT2E=(>@gR2QEDW>EGg<_Ha1#5A|#jYdgz;aRE=;>VdWM_0R!+8vB6fz5=FGTAv(v!xyZ!W1U0*6zNTUdefw$8COJRUxEhoRLC=mF!L_F<_% zFusO;LUt^1PJ2{MJlW+)KON^3cT9EujI41ldsZ{eAsekF`0_`Q7wTj@tu-alNoCNU z#w=^IGoiPhB&WHz%PZhF(!ZS4X!+vObDqF@*osXxGwBhP#GD{TPZzTVC!>bKf#vz=^sqw==jf$NRz``a*2S=}@T&#P=eU;m8_ zKkhfOe~`or8m$|{AL8=2--Gk-_Z?`g4zPz_kGlN14L9w#c!AcXigt@5`g|HL)WMDK zou9uiAcuZCEsv=0K6`(&)>GcK8p?2q+orRG8CO3NRkpNulKW)uS+tAVkC}#x`A%6* z%2H+%hdJ<@C`Y0`Mtz-l?CBWXQ*{RVB-!BG>$64If%MMWJIwhV!Ewk@+DnBj5-V$) z@@s4a*G%%kM;DgYL!P)@bd>yhP_=xrbK$I>KzpYjW1oH#NSwR6w4}@#BH`Y~tH4pV zQpcm?n+WdMfQI$MJnCNdzop8F$QGYWJHIG5qHRj3`cd2AETmIR8;|lqZxfycZ9=mZ z+3F0O*f|r`bWSUfXlEXj@q#GY?>xJ_DSYz9x3W)N`=Dgo^lfYfbT-Pp2(~&$i?ki@ zgrjVH?gwYdV&7q{MY;p&%lBmeDe}p3)(W?D>wt0cG{Z0BeAY~Y-Kd}UF{jnpTRKxq zYf-8noh`;+)1Bmh&3|-;k@N!Er=vZ1+S|JaxFP?i%Ey%B47>cC%}|0rJ|0)@tnaDA zaBgCs|4~$hXs?BI2Re@}D?V}Ym}AfQ#KIw68a8bE#>LI^Ui1B;-DR}nJh;TAc|H0> z(*}@}FN}+q=Y2EeKkf05%#{b9s5F%MyQciKhex8~wwNO!R-+s}Ys%E!H_$ZrZ3bUIjyIW{+BS?{>OIcmd^K&69YRU{o3OF0RkJ z?cGh!94l5naL?PY(+D|d@<;V~o!=PM-t97&-%)$K)RM5Rifl6`oqY8N zSW2DCD;KEzzU@D%&x^muwRanL^E+y-OmdU?p5{mOhdjK1@~i#NNXyUu?)Le#_HL&& zzjd~$>!hDD-?R8p{Xw{7No(Rz_Ib#`nfF-OeLjxA8`w#H#Cm>kJ4(8wG;!awC&?Zk ze0Tw6e~2;gx;WVOd%Ms3ws#wjeqYL5)^*aOxbd=v?f&4y3nc#_|DBbVkKO0qfB*ZKFZbNA6^ffE(S^as(&mA%~f z)Y&oP=ak11FV@ae@_3Rw#=)e_Ppa&4@PPB<;x*&F)(u@J-E=eZii1g+rwx{# zvm5)%`^3d-#(QM0VYV{h(9-hLrVlpd=Y9(Hfx>ivS?WxCh>eptr1jP;>57O$S)XP- z1Z(Ia#~AmSB4B5Qq4gQ#^6X>Inom?b%KC3ZB_I67-iVE%LGHP5R6a@XZektXIISNg z#Vzt1Wn9X>!Oh+BD~vplDhm~bi`MCl)A?CN!A*lh8PAIAAWjQ+N@kwQN zzp>BygQPEHUCiKN`#RUuxc#XM`&-e!ndcnmmM=?~aq=5Q<6__;e+H%oTw87vrwAW@ zKkV%KEM-@mchXo;oPoKYjzkzIhKCVvC*N+Cy4N>?v`c8N1 z$*!nTI8o`r`8Vu6E9AUpY<{#yxA1nLJwgxXyAL9<&M5oOlg?9(qjl2zcgzcI!Nm^> z^e)Raav;X`}MU^iLoFkDF8COrF-gD0vbpDg>Me?P!iBH}Ok!k<=o%6~~ zYwu}wfgH23=8fRuJ$KgrHOT>{JXwA6dXYSP(O+(whF`0`b63*F(xEg{j|A+;$m2Bb zSm>B?yY-8WONskT_J*$KgYUyhy7dh7uBbkNbs;eKMMvyr*YRQ6#aOMeP=>SMnb%RC zJK90HRoXfo*vvo(EUDrOpWtX74 zL$W$?3V2NJ{B({V_ruHw%!NEV6ETOheH!Rh0DJV)@fO|R!kmZnFiF4W&A^4!joSb=;GoowoT z#sl5WuWEl^9=6RL754Yv%vpH5k+$jmtdla}jKK{#gXUcHqTyXgI`<~8(|Evoa3ZaAwvDe# zvt88vI4S%-G0UG-_eG#5UW?uERL(lwxRYqqEL^Z*pTL~C?hYgMqdYV+6`V94Xk5-g z{t$HB-me_|-k=)#(l6+)R-3=T$7Zs&|1J*CZC2H{748YoSH{rJFJvwjsjrdkyzU{* z>(s-qVa?s%ldL&>Bj@(%-dq=+?k38~@57?0Epmoo9qmm!kUoj_`hE7M{9Rj#RdD9q z<^E>$ruUn2#`(IV+nGCgHwWEKtb1+0k8GfR)~J&`zxGLK?%Cf!`!smyo~^j@oA>B7 zALUN^-3ul|%fZcnmtlW@G;5!kZB84J1xy`xs;@Dhb%a#mLx8JF)ASYjZ#-jXZowmHwlOeVu8?h#m zdakftR{OVPHr=m1Qk>Qk;>LWt+;P8IQ}`WcKXE_TmxS{dE%QTypTg{E2Y@EP;n^1ET?h)=^u z#&sIqh0nx+^0wetH?Mc`_YG^^t`WUJRvI-cp5`Aq7s$8VN%65k>ECy5rKL8}Y3+@( z^xptph0@;CfnUv>IYft0q()z*1Xoc z3zh`yp&R{KBl!EI)qMyur051$^qDtF^@L90X7*dP)GqlM^m^zerX=CjjBh$0z0;l6 zA#^uIGs+(a6B+5^sY_d@CqyrKgs)yN4)?6@wLbKUDz^)q?2WRPtB80SYp{Vop%tS5 z>k>Pmn|`qfytBg4I^HkPop+1Vx*_{VTG|G%rC7=O@gB`=14lhq*#JG%Jz43NH=gJ% z9;$VA?x74Gi#a2>liR~Q^w+(de}jEf0KW{FA2+={FeiBQu;t65+DUlirKOL9fG2znk3P-_6XAGmLI5c~&r3g^-`bYA8=xf_-> z(p>uu>^e2SS$FyVpH~(y3t$g_Ao{phOg>3Iyh!6wFp2)Feeh>PU)g4ezRy74h+~31 zYI0;omED8v3%El&(D@lUN9>pc>Vl;DBisk(tE!lGTz%s_o|pT0$K?B6^=;i>+ZK}usKHGew+5dYkr}5 z#(B&)evDi>9r;r85bc3@wQ+Pt~a8i zMjybMLZaQa^qJC2NIxMxh4dBDTink4RCb_2`_}cCqrMI zQJ}q#oLyR*`x_mN>!YuEkK51V!mKGbysj@Dp!AxYwH)d>rSFv9vkx7D^q{Zm0EcL< z{wsTT-G%&9=&Q3+b$0xFsXNwm0_odadisX3)A@ZIz3unhy}2!V-nG8)ed24qQf1P* zh}KF!L0Pp{axLxSPqYu+%OoAsNO985h`x71-|L|71y%aWPDJ;wp<8Xby^z-HS-aiE zrghYB)(_6|p=Gn;YJ5@q&^=w!J9eAXy{7*{yAJtt3+S7L4&04&Q54P1JJxE}qb)w0 z1y(ELXiwM=SRd>br*)84toQoT0G_+x6ARDrjqJN_W!pI@=8oi6 z)m2hHth;}}^mo^%jxS3}+wO1AcZvO9Gw(fVlm^Iw*SU08`1pmD(eQ_XM&UOrz2*|# zG6G;H)v&zYta`+DZ|RZP?YnJ2_8ra2vk17d59$`DdAjzl6;bYHz~DT@!(93!8#e7u zs7A}6{?u)YP_k)jwA{@~kACM;m;T88_cbfOM&N1=xTp)peU~>$|JiCg@T~RB+~ld7 ztaHn`XJ!ld)yrAaw<@0OfW=F@)#>b@?OMDSBnxe1BY5zhW_m7xTV$kC*_Cz za-ehEMvCi1SpXT}Zqc7QF7Z3}U3W=z%=1lSzSglvnv*QRp4pD!1Jv`1ud;6#`;M`! z$CdNYsu^jfjes#fuI+Y`ETA>meK?mBUOS-~bj$;@h%sOLPh|h1b0oEwrcw6@>v)>W z>n{60%c8nL*GaMfXDS?y9C%_LS{0q9(RsecSlO6p{4ls_-SCPA^oD<69nZGCP@j>l zg3nztZgY{X2lMS3jt19u_?+Ky8hXFpcI0j6+2}l9wr|>JYr{0ZS?>sz=9DFOk2$AV zHg!JtNx5yHQ)B_;{)>68GIiB1zmYLtcde)CSZ?&V`_0fw`;e3BLpD1)6FRSk;#Tk$ ze@e=u+27Cu-#|Hj)9ieb;7hlkrw+yMH6~}#t>n=oWxc;%``2~l(VR~zC2k=fLZyyi=%jjuRATn zJq>O?3Tr&@ogcJIFF>1J$r!LOsvOOH=R42$<@YY`;?KVBSnQ5nI9bDa#)Edq0`UG< zcv^avm+zRLMZQm?i_YTmH6hU1Q)zIMzWa^`?N}o~w^42-{e8xKBj4++sHA$%@=fzB z-!r8ex&PP3$!C7hYFR+^ZzccFI_4+obL_hH`K@znvO2Xr*_->oPm1fKFKVSMApXz% zxh4BOvX1$Z@6+@-Np&6fO?&IIx+RDU()Gr{%JZJO&OAS8l`J6n54@T_|I0Gw8-AZf zpOdHleeMC&y$yNt$dV?@co6CZ*jJqeUL$cBj|ZUtU5&s_&l+}Hi^#V72Gs8*af%-|a_7QMy$VHs#d_vJ>OB(Zw(C6gA zSNAVwbvm;+Pach=Ntz!tOBSH-e-8VvgBrm*Ds9x5-)esE;w4!sD+hRYj4g=^vl-#I z`GMA>i=G=%C-1}l^L5O1*A-QksCmBlz0MIUDvvyHkaaSjDHCV+lPBLiY2wC%B4q(+ zUcvq|yhji{;M_cTx@n^3`K^-gU0kBVYKLh~qXc7OTidE|H{*eg@1QJDOh00bUdH{Z z>uV1HbAX$o>dWUH`^xL=_6@&pw~dos2Hne&=CpRJve@a``P-cz%wr*|hWeJ?`Y6o zB2V7FX+DEZSDMo~bG~p}9badHicj5#Jd-DH#{S2CcNw)BuRQrluGaladDf}X`_{&O!vi>9>uq`P=%zFW(^k{mr&uTGrZVNhl`(tR zoe&>dP+1>6Kz|;1-I7M<3Zyyi%^K14XKuUbkolD{rr+B>bAs=73oY~D$zHcWa#NDq zFQ-hE2cLGNVIFa_G<{bT=j;MA%-HD;!rA=>J@yIWOulMiP-#ohyed^8GKuIct* z2A6jDe@ob>r8^0_MV8G|ce3|7;oa~PC$kW|YcExLNQ2z>>nK`By+aqU7kseX4dwF1@rwz2!F9*6FT8GuueE;UzR6Lvj(XQSE6|$o z&D~HoUmTB1*b9EX=bmrhyxSEY-TvKYEGc{41IwD=1ht!X;oPiz51ALw|38~^&v&zM zEefveyrTMf(z~;lj!Yh)`y4mf;{+jN}BgYoCo=(7Vr5kx-O9Sm!PlNxlm%q07IX6DC5j4MVFyf@b-?_3og6* zR^?xGKGO5BC+veUJ>*mW?T&kswHJJ52k!Y!svl_oBHidPn6ce$*{v!Pl+5;Q!U(d%jKk6VGSwZ%6few;k+1x4a2* zNypm`o?`6Qp`9LDpVyoekTGbeCR_J36NvpVNM?Xqx7MCtWf2LmjtXzbk2T5F@zL^8DmH1`vX#m4AM z%on=uOe*C0XTbeyd$ND7C6zUTGdY@b$&eBD)48RrfpzQ|mEbl2j+fEbC%$KXdu*~s za5D&t_Cd}mWqf!W>r3ar7w&|=wrx)m`kI%es{@yB&^`}@=80#kjda?yqkIQ*c0F?A zyXbdkGtS-w-<^xBRrq{TFzMg(C7+U4FY6kI9XL?lq8(*^HP7T4VEuVZc<_O&Kc2uC zd=~Sq%Xw}TzrcSCp79LrWC7vDgcs{K@1EuN<2-ls{@3_dl6DGusuO$q%M&KfE00ai zwL8C>HSl_WU8yw5e$!hjjk3aPRMwuM7kvt^KNME5RH}u6CO65vSUMOUW5Rud;TnL! zU=2Vuc@03AyW;c=0_ZpKs{s2gaRpa#C0K@EI0gDSQHvYF!d*T9v+ z4Eu({VTQd!;jqqzf?`SF7EI`=bC)J@7B4nWxB4nWxBIJhqZFnHqXNN)14fopL zLD&u3pH+bR@RU0ADUcJMWYw-x4hz>6j{>^ky5dpbv~Yhteq(&Yef8Ob9ufIP3F}~rn_UC?g+p`-^>mN>ka{Jd5w?8`J;r+SSt^oRbpB;|i5B>Ic z_(@#>VTf+Hu7Ewm`B`0oT>eM6t^fpWh7|Hs3*nI8S_p>x*g`1e*A_xOf@jtEB!w-6 zrYJm=VVIp&Lt%E-01##u1hou$!sJ64Od1T=N>mM+DzAd8Rbhy&;#4u5Wa3u=)PjQm ZYRRh@^bCDh5vs@!z69bV>$COq{{Z);QUw42 diff --git a/docs/images/dls-logo.svg b/docs/images/dls-logo.svg deleted file mode 100644 index 0af1a177..00000000 --- a/docs/images/dls-logo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 539aade8..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,29 +0,0 @@ -:html_theme.sidebar_secondary.remove: - -.. include:: ../README.rst - :end-before: when included in index.rst - -How the documentation is structured ------------------------------------ - -The documentation is split into 2 sections: - -.. grid:: 2 - - .. grid-item-card:: :material-regular:`person;4em` - :link: user/index - :link-type: doc - - The User Guide contains documentation on how to install and use gphotos-sync. - - .. grid-item-card:: :material-regular:`code;4em` - :link: developer/index - :link-type: doc - - The Developer Guide contains documentation on how to develop and contribute changes back to gphotos-sync. - -.. toctree:: - :hidden: - - user/index - developer/index diff --git a/docs/user/explanations/docs-structure.rst b/docs/user/explanations/docs-structure.rst deleted file mode 100644 index f25a09ba..00000000 --- a/docs/user/explanations/docs-structure.rst +++ /dev/null @@ -1,18 +0,0 @@ -About the documentation ------------------------ - - :material-regular:`format_quote;2em` - - The Grand Unified Theory of Documentation - - -- David Laing - -There is a secret that needs to be understood in order to write good software -documentation: there isn't one thing called *documentation*, there are four. - -They are: *tutorials*, *how-to guides*, *technical reference* and *explanation*. -They represent four different purposes or functions, and require four different -approaches to their creation. Understanding the implications of this will help -improve most documentation - often immensely. - -`More information on this topic. `_ diff --git a/docs/user/how-to/run-container.rst b/docs/user/how-to/run-container.rst deleted file mode 100644 index d478966c..00000000 --- a/docs/user/how-to/run-container.rst +++ /dev/null @@ -1,15 +0,0 @@ -Run in a container -================== - -Pre-built containers with gphotos-sync and its dependencies already -installed are available on `Github Container Registry -`_. - -Starting the container ----------------------- - -To pull the container from github container registry and run:: - - $ docker run ghcr.io/gilesknap/gphotos-sync:main --version - -To get a released version, use a numbered release instead of ``main``. diff --git a/docs/user/index.rst b/docs/user/index.rst deleted file mode 100644 index 2c94a0c0..00000000 --- a/docs/user/index.rst +++ /dev/null @@ -1,57 +0,0 @@ -User Guide -========== - -Documentation is split into four categories, also accessible from links in the -side-bar. - -.. grid:: 2 - :gutter: 4 - - .. grid-item-card:: :material-regular:`directions_walk;3em` - - .. toctree:: - :caption: Tutorials - :maxdepth: 1 - - tutorials/installation - - +++ - - Tutorials for installation and typical usage. New users start here. - - .. grid-item-card:: :material-regular:`directions;3em` - - .. toctree:: - :caption: How-to Guides - :maxdepth: 1 - - how-to/run-container - - +++ - - Practical step-by-step guides for the more experienced user. - - .. grid-item-card:: :material-regular:`info;3em` - - .. toctree:: - :caption: Explanations - :maxdepth: 1 - - explanations/docs-structure - - +++ - - Explanations of how the library works and why it works that way. - - .. grid-item-card:: :material-regular:`menu_book;3em` - - .. toctree:: - :caption: Reference - :maxdepth: 1 - - reference/api - ../genindex - - +++ - - Technical reference material including APIs and release notes. diff --git a/docs/user/reference/api.rst b/docs/user/reference/api.rst deleted file mode 100644 index cd0ba148..00000000 --- a/docs/user/reference/api.rst +++ /dev/null @@ -1,14 +0,0 @@ -API -=== - -.. automodule:: photos_sync - - ``photos_sync`` - ----------------------------------- - -This is the internal API reference for photos_sync - -.. data:: photos_sync.__version__ - :type: str - - Version number as calculated by https://github.com/pypa/setuptools_scm diff --git a/docs/user/tutorials/installation.rst b/docs/user/tutorials/installation.rst deleted file mode 100644 index b3e5f18f..00000000 --- a/docs/user/tutorials/installation.rst +++ /dev/null @@ -1,38 +0,0 @@ -Installation -============ - -Check your version of python ----------------------------- - -You will need python 3.8 or later. You can check your version of python by -typing into a terminal:: - - $ python3 --version - - -Create a virtual environment ----------------------------- - -It is recommended that you install into a “virtual environment” so this -installation will not interfere with any existing Python software:: - - $ python3 -m venv /path/to/venv - $ source /path/to/venv/bin/activate - - -Installing the library ----------------------- - -You can now use ``pip`` to install the library and its dependencies:: - - $ python3 -m pip install gphotos-sync - -If you require a feature that is not currently released you can also install -from github:: - - $ python3 -m pip install git+https://github.com/gilesknap/gphotos-sync.git - -The library should now be installed and the commandline interface on your path. -You can check the version that has been installed by typing:: - - $ gphotos-sync --version diff --git a/pyproject.toml b/pyproject.toml index 8886fa22..20cf70c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] -description = "One line description of your module" +description = "Google Photos and Albums backup tool" dependencies = [ "attrs", "exif", @@ -68,7 +68,10 @@ addopts = """ --tb=native -vv --doctest-modules --doctest-glob="*.rst" """ # https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings -filterwarnings = "error" +filterwarnings = [ + "error", + "ignore:.*socket.*:ResourceWarning" +] # Doctest python code in docs, python code in src docstrings, test functions in tests testpaths = "docs src tests" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1b89c1e4..00000000 --- a/setup.cfg +++ /dev/null @@ -1,141 +0,0 @@ -[metadata] -name = gphotos-sync -description = Google Photos and Albums backup tool -url = https://github.com/gilesknap/gphotos-sync -author = Giles Knap -author_email = gilesknap@gmail.com -license = Apache License 2.0 -long_description = file: README.rst -long_description_content_type = text/x-rst -classifiers = - License :: OSI Approved :: Apache Software License - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 -plaforrms = - Linux - Windows - Mac - -[options] -python_requires = >=3.7 -packages = find: -# =src is interpreted as {"": "src"} -# as per recommendation here https://hynek.me/articles/testing-packaging/ -package_dir = - =src - -setup_requires = - setuptools_scm[toml]>=6.2 - -install_requires = - attrs==22.2.0 - exif==1.6.0 - appdirs==1.4.4 - pyyaml==6.0.1 - psutil==5.9.4 - google-auth-oauthlib==1.0.0 - -[options.extras_require] -# For development tests/docs -dev = - black==22.3.0 - flake8-isort - isort>5.0 - mypy - pre-commit - pytest-cov - sphinx-rtd-theme-github-versions - tox - tox-direct - setuptools_scm[toml]>=6.2 - mock - types-setuptools - types-requests - types-PyYAML - -[options.packages.find] -where = src -# Don't include our tests directory in the distribution -exclude = tests - -# Specify any package data to be included in the wheel below. -[options.package_data] -gphotos_sync = - sql/gphotos_create.sql - -[options.entry_points] -# Include a command line script -console_scripts = - gphotos-sync = gphotos_sync.Main:main - -[mypy] -# Ignore missing stubs for modules we use -ignore_missing_imports = True - -[isort] -profile=black -float_to_top=true - -[flake8] -# Make flake8 respect black's line length (default 88), -max-line-length = 88 -extend-ignore = - # See https://github.com/PyCQA/pycodestyle/issues/373 - E203, - # support typing.overload decorator - F811, - # allow Annotated[typ, some_func("some string")] - F722, -exclude = - .tox - .venv - -[tool:pytest] -# Run pytest with all our checkers, and don't spam us with massive tracebacks on error -addopts = - --tb=native -vv --doctest-modules --doctest-glob="*.rst" - --cov=gphotos_sync --cov-report term --cov-report xml:cov.xml -# https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings -filterwarnings = - error - # its difficult to ensure all sockets are closed in tests so ignore - ignore:.*socket.*:ResourceWarning -# Doctest python code in docs, python code in src docstrings, test functions in tests -testpaths = - tests - -[coverage:run] -# This is covered in the versiongit test suite so exclude it here -omit = */_version_git.py -data_file = /tmp/gphotos_sync.coverage - -[coverage:paths] -# Tests are run from installed location, map back to the src directory -source = - src - **/site-packages/ - - -# Use tox to provide parallel linting and testing -# NOTE that we pre-install all tools in the dev dependencies (including tox). -# Hence the use of allowlist_externals instead of using the tox virtualenvs. -# This ensures a match between developer time tools in the IDE and tox tools. -[tox:tox] -skipsdist = True - -[testenv:{pre-commit,mypy,pytest,docs}] -# Don't create a virtualenv for the command, requires tox-direct plugin -direct = True -passenv = * -allowlist_externals = - pytest - pre-commit - mypy - sphinx-build - sphinx-autobuild -commands = - pytest: pytest {posargs} - mypy: mypy src tests {posargs} - pre-commit: pre-commit run --all-files {posargs} - docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html diff --git a/tests/test_credentials/.gphotos.token b/tests/test_credentials/.gphotos.token index 0993c29a..b3113e65 100644 --- a/tests/test_credentials/.gphotos.token +++ b/tests/test_credentials/.gphotos.token @@ -1 +1 @@ -{"access_token": "ya29.a0ARrdaM-gnPDJz9yQsqsqSk9JuTjWOEmcHOhAaOK4kqaq-TvXKdyiq3poSywztKprh1LOVR_jzMUZzFN5WsHpT9tGS0NxNbf-JWaix3iqKEF5Y0vZhs6u9iEee-drUkoBpLVeIdNR70w-KDiWzDx73MSnU7RHCx4", "expires_in": 3599, "scope": ["https://www.googleapis.com/auth/photoslibrary.readonly", "https://www.googleapis.com/auth/photoslibrary.sharing"], "token_type": "Bearer", "expires_at": 1653085052.2558572, "refresh_token": "1//03CEqAzsnP-8PCgYIARAAGAMSNwF-L9Irz4_ilhRw0HIwVImT4gTCUPlV8YaCTYQiIjD4juWOI5eQh_-Rzh9nTmBND0jliOnabq4"} \ No newline at end of file +{"access_token": "ya29.a0AfB_byDXJCKlwvJkqVdZ_TI33xHRdO5eYU2qwTeBCFc8p_Pvah0hdPsKxnvm7hNIO8a0GZag78SkLbZktC-MEeRN0ahVCCVwxad5Cf29i3qNBZpNjbUF4T6DzNlucvYtm5Aqh06jtqPlGu3PxeOBCRIUqN7O6MbKvTzZ8M0aCgYKAQ8SARMSFQHGX2MivgKN6cJHMpjPkhO_a_9Onw0174", "expires_in": 3599, "scope": ["https://www.googleapis.com/auth/photoslibrary.readonly", "https://www.googleapis.com/auth/photoslibrary.sharing"], "token_type": "Bearer", "expires_at": 1702819489.791437, "refresh_token": "1//03CEqAzsnP-8PCgYIARAAGAMSNwF-L9Irz4_ilhRw0HIwVImT4gTCUPlV8YaCTYQiIjD4juWOI5eQh_-Rzh9nTmBND0jliOnabq4"} \ No newline at end of file From 0752c59009181391110781cc96546a5da25a799a Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Sun, 17 Dec 2023 14:41:47 +0000 Subject: [PATCH 71/79] restore classic docs --- docs/.notes | 11 + docs/_static/theme_overrides.css | 34 +++ docs/conf.py | 120 +++++++++++ docs/explanations.rst | 14 ++ docs/explanations/folders.rst | 43 ++++ docs/explanations/googleapiissues.rst | 71 +++++++ docs/explanations/notes.rst | 17 ++ docs/explanations/tokens.rst | 55 +++++ docs/how-to.rst | 13 ++ docs/how-to/comparison.rst | 42 ++++ docs/how-to/cron.rst | 14 ++ docs/how-to/windows.rst | 88 ++++++++ docs/images/logo.png | Bin 0 -> 42908 bytes docs/index.rst | 48 +++++ docs/reference.rst | 13 ++ docs/reference/api.rst | 54 +++++ docs/reference/contributing.rst | 1 + docs/tutorials.rst | 13 ++ docs/tutorials/installation.rst | 129 ++++++++++++ docs/tutorials/login-images/01-sign-in.png | Bin 0 -> 41247 bytes docs/tutorials/login-images/02-verify.png | Bin 0 -> 44621 bytes docs/tutorials/login-images/03-verify2.png | Bin 0 -> 56822 bytes docs/tutorials/login-images/04-access.png | Bin 0 -> 100580 bytes docs/tutorials/login.rst | 62 ++++++ docs/tutorials/oauth-images/0.png | Bin 0 -> 6203 bytes docs/tutorials/oauth-images/1.png | Bin 0 -> 14059 bytes docs/tutorials/oauth-images/10-test_users.png | Bin 0 -> 95488 bytes docs/tutorials/oauth-images/11-summary.png | Bin 0 -> 149189 bytes .../oauth-images/12-create_creds.png | Bin 0 -> 112022 bytes docs/tutorials/oauth-images/14-create_id.png | Bin 0 -> 60497 bytes docs/tutorials/oauth-images/15-created.png | Bin 0 -> 44369 bytes docs/tutorials/oauth-images/2.png | Bin 0 -> 46864 bytes docs/tutorials/oauth-images/3.png | Bin 0 -> 52427 bytes docs/tutorials/oauth-images/4.png | Bin 0 -> 83230 bytes docs/tutorials/oauth-images/5.png | Bin 0 -> 35506 bytes docs/tutorials/oauth-images/6.png | Bin 0 -> 39626 bytes .../oauth-images/7-oauth_concent.png | Bin 0 -> 121958 bytes .../oauth-images/8-app_registration.png | Bin 0 -> 222527 bytes docs/tutorials/oauth-images/9-scopes.png | Bin 0 -> 140850 bytes docs/tutorials/oauth2.rst | 193 ++++++++++++++++++ src/gphotos_sync/BadIds.py | 2 +- 41 files changed, 1036 insertions(+), 1 deletion(-) create mode 100644 docs/.notes create mode 100644 docs/_static/theme_overrides.css create mode 100644 docs/conf.py create mode 100644 docs/explanations.rst create mode 100644 docs/explanations/folders.rst create mode 100644 docs/explanations/googleapiissues.rst create mode 100644 docs/explanations/notes.rst create mode 100644 docs/explanations/tokens.rst create mode 100644 docs/how-to.rst create mode 100644 docs/how-to/comparison.rst create mode 100644 docs/how-to/cron.rst create mode 100644 docs/how-to/windows.rst create mode 100644 docs/images/logo.png create mode 100644 docs/index.rst create mode 100644 docs/reference.rst create mode 100644 docs/reference/api.rst create mode 100644 docs/reference/contributing.rst create mode 100644 docs/tutorials.rst create mode 100644 docs/tutorials/installation.rst create mode 100644 docs/tutorials/login-images/01-sign-in.png create mode 100644 docs/tutorials/login-images/02-verify.png create mode 100644 docs/tutorials/login-images/03-verify2.png create mode 100644 docs/tutorials/login-images/04-access.png create mode 100644 docs/tutorials/login.rst create mode 100644 docs/tutorials/oauth-images/0.png create mode 100644 docs/tutorials/oauth-images/1.png create mode 100644 docs/tutorials/oauth-images/10-test_users.png create mode 100644 docs/tutorials/oauth-images/11-summary.png create mode 100644 docs/tutorials/oauth-images/12-create_creds.png create mode 100644 docs/tutorials/oauth-images/14-create_id.png create mode 100644 docs/tutorials/oauth-images/15-created.png create mode 100644 docs/tutorials/oauth-images/2.png create mode 100644 docs/tutorials/oauth-images/3.png create mode 100644 docs/tutorials/oauth-images/4.png create mode 100644 docs/tutorials/oauth-images/5.png create mode 100644 docs/tutorials/oauth-images/6.png create mode 100644 docs/tutorials/oauth-images/7-oauth_concent.png create mode 100644 docs/tutorials/oauth-images/8-app_registration.png create mode 100644 docs/tutorials/oauth-images/9-scopes.png create mode 100644 docs/tutorials/oauth2.rst diff --git a/docs/.notes b/docs/.notes new file mode 100644 index 00000000..51465611 --- /dev/null +++ b/docs/.notes @@ -0,0 +1,11 @@ +System Tests +============ + +The system tests use a real Google Account with its own Photos Library. + +This account is called gphotos.sync.test@gmail.com + +It's password uses giles' password scheme C + +(I'd like this to be in public domain for use by other contributors but I +don't believe there is a secure way to do this.) \ No newline at end of file diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css new file mode 100644 index 00000000..5fd9b721 --- /dev/null +++ b/docs/_static/theme_overrides.css @@ -0,0 +1,34 @@ +/* override table width restrictions */ +@media screen and (min-width: 639px) { + .wy-table-responsive table td { + /* !important prevents the common CSS stylesheets from + overriding this as on RTD they are loaded after this stylesheet */ + white-space: normal !important; + } +} + +/* override table padding */ +.rst-content table.docutils th, .rst-content table.docutils td { + padding: 4px 6px; +} + +/* Add two-column option */ +@media only screen and (min-width: 1000px) { + .columns { + padding-left: 10px; + padding-right: 10px; + float: left; + width: 50%; + min-height: 145px; + } +} + +.endcolumns { + clear: both +} + +/* Hide toctrees within columns and captions from all toctrees. + This is what makes the include trick in index.rst work */ +.columns .toctree-wrapper, .toctree-wrapper .caption-text { + display: none; +} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..744109c3 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,120 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/main/usage/configuration.html + +import gphotos_sync + +# -- General configuration ------------------------------------------------ + +# General information about the project. +project = "gphotos-sync" + +# The full version, including alpha/beta/rc tags. +release = gphotos_sync.__version__ + +# The short X.Y version. +if "+" in release: + # Not on a tag + version = "main" +else: + version = release + +extensions = [ + # Use this for generating API docs + "sphinx.ext.autodoc", + # This can parse google style docstrings + "sphinx.ext.napoleon", + # For linking to external sphinx documentation + "sphinx.ext.intersphinx", + # Add links to source code in API docs + "sphinx.ext.viewcode", + # Adds the inheritance-diagram generation directive + "sphinx.ext.inheritance_diagram", +] + +# If true, Sphinx will warn about all references where the target cannot +# be found. +nitpicky = False + +# A list of (type, target) tuples (by default empty) that should be ignored when +# generating warnings in "nitpicky mode". Note that type should include the +# domain name if present. Example entries would be ('py:func', 'int') or +# ('envvar', 'LD_LIBRARY_PATH'). +nitpick_ignore = [("py:func", "int", "py:class")] + +# Both the class’ and the __init__ method’s docstring are concatenated and +# inserted into the main body of the autoclass directive +autoclass_content = "both" + +# Order the members by the order they appear in the source code +autodoc_member_order = "bysource" + +# Don't inherit docstrings from baseclasses +autodoc_inherit_docstrings = False + +# Output graphviz directive produced images in a scalable format +graphviz_output_format = "svg" + +# The name of a reST role (builtin or Sphinx extension) to use as the default +# role, that is, for text marked up `like this` +default_role = "any" + +# The suffix of source filenames. +source_suffix = ".rst" + +# The main toctree document. +main_doc = "index" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# These patterns also affect html_static_path and html_extra_path +exclude_patterns = ["_build"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# This means you can link things like `str` and `asyncio` to the relevant +# docs in the python documentation. +intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} + +# A dictionary of graphviz graph attributes for inheritance diagrams. +inheritance_graph_attrs = {"rankdir": "TB"} + +# Common links that should be available on every page +rst_epilog = """ +.. _Diamond Light Source: + http://www.diamond.ac.uk +""" + +# Ignore localhost links for period check that links in docs are valid +linkcheck_ignore = [r"http://localhost:\d+/"] + +# -- 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_github_versions" + +# Options for the sphinx rtd theme, use DLS blue +html_theme_options = {"style_nav_header_background": "rgb(7, 43, 93)"} + +# 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"] + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = False + +# Add some CSS classes for columns and other tweaks in a custom css file +html_css_files = ["theme_overrides.css"] + +# Logo +html_logo = "images/logo.png" +html_favicon = "images/logo.png" diff --git a/docs/explanations.rst b/docs/explanations.rst new file mode 100644 index 00000000..0c1f3c18 --- /dev/null +++ b/docs/explanations.rst @@ -0,0 +1,14 @@ +:orphan: + +Explanations +============ + +Explanation of how the library works and why it works that way. + +.. toctree:: + :caption: Explanations + + explanations/folders + explanations/notes + explanations/tokens + explanations/googleapiissues diff --git a/docs/explanations/folders.rst b/docs/explanations/folders.rst new file mode 100644 index 00000000..49e943fa --- /dev/null +++ b/docs/explanations/folders.rst @@ -0,0 +1,43 @@ +.. _Folders: + +Folder Layout +============= + +After doing a full sync you will have 2 directories off of the specified root: + +Media Files +----------- + +photos: + contains all photos and videos from your Google Photos Library organized + into folders with the structure 'photos/YYYY/MM' where 'YYYY/MM' is + the date the photo/video was taken. The filenames within a folder will + be as per the original upload except that duplicate names will have a + suffix ' (n)' where n is the duplicate number of the file (this matches + the approach used in the official Google tool for Windows). + +albums: + contains a folder hierarchy representing the set of albums and shared + albums in your library. All the files are symlinks to content in the photos + folder. The folder names will be 'albums/YYYY/MM Original Album Name'. + +Note that these are the default layouts and you can change what is downloaded +and how it is layed out with command line options. See the help for details:: + + gphotos-sync --help + +Other Files +----------- + +The following files will also appear in the root folder:- + + - gphotos.sqlite: the database that tracks what files have been indexed, + you can open this with sqlite3 to examine what media and metadata you have. + - gphotos.log: a log of the most recent run, including debug info + - gphotos.lock: the lock file used to make sure only one gphotos-sync runs + at a time + - gphotos.trace: a trace file if logging is set to trace level. This logs + the calls to the Google Photos API. + - gphotos.bad_ids.yaml: A list of bad entries that cause the API to get + an error when downloading. Delete this file to retry downloading these + bad items. diff --git a/docs/explanations/googleapiissues.rst b/docs/explanations/googleapiissues.rst new file mode 100644 index 00000000..8cc6ba3a --- /dev/null +++ b/docs/explanations/googleapiissues.rst @@ -0,0 +1,71 @@ +Known Issues +============ + + +Known Issues with Google API +---------------------------- +A few outstanding limitations of the Google API restrict what can be achieved. +All these issues have been reported to Google and this project will be updated +once they are resolved. + +Unfortunately, a number of quite significant issues have remained unfixed for +several years. I'm starting a discussion group in this repo for people to +discuss workarounds, and collect reports of how these things are affecting +users. I intend to link this discussion group in the Google issue trackers +to see if it will encourage some response from Google. + +To join the discussion go here +https://github.com/gilesknap/gphotos-sync/discussions. + +Pending API Issues +~~~~~~~~~~~~~~~~~~ + +- There is no way to discover modified date of library media items. Currently + ``gphotos-sync`` will refresh your local copy with any new photos added since + the last scan but will not update any photos that have been modified in + Google Photos. + + - https://issuetracker.google.com/issues/122737849. + + +- GOOGLE WON'T FIX. The API strips GPS data from images. + + - https://issuetracker.google.com/issues/80379228. + +- Video download transcodes the videos even if you ask for the original file + (=vd parameter). My experience is that the result is looks similar to the original + but the compression is more clearly visible. It is a smaller file with + approximately 60% bitrate (same resolution). + + - https://issuetracker.google.com/issues/80149160 + +- Photo download compresses the photos even if you ask for the original file + (=d parameter). This is similar to the above issue, except in my experience + is is nearly impossible to notice a loss in quality. It + is a file compressed to approximately 60% of the original size (same resolution). + + - https://issuetracker.google.com/issues/112096115 + +- Burst shots are not supported. You will only see the first file of a burst shot. + + - https://issuetracker.google.com/issues/124656564 + +Fixed API Issues +~~~~~~~~~~~~~~~~ +- FIXED BY GOOGLE. Some types of video will not download using the new API. + + - https://issuetracker.google.com/issues/116842164. + - https://issuetracker.google.com/issues/141255600 + +Other Issues +------------ +- Some mounted filesystems including NFS, CIFS and AFP do not support file locks + and database access will fail on them. + + - To fix, use the parameter --db-path to specify a location for your DB on + the local disk. This will perform better anyway. + +GPS workaround +-------------- +For a workaround to the GPS issue described below see this project +https://github.com/DennyWeinberg/manager-for-google-photos \ No newline at end of file diff --git a/docs/explanations/notes.rst b/docs/explanations/notes.rst new file mode 100644 index 00000000..31d97876 --- /dev/null +++ b/docs/explanations/notes.rst @@ -0,0 +1,17 @@ +Why is Google Photos Sync Read Only +=================================== + +Google Photos Sync is a backup tool only. It never makes any changes to your +Google Photos Library in the cloud. There are two primary reasons for this: + +- The Photos API provided by Google is far too restricted to make changes + in any meaningful way. For example + + - there is no delete function + - you cannot add photos to an album unless it was created by the same + application that is trying to add photos + +- Even if the API allowed it, this would be a very hard problem, because + it is often hard to identify if a local photo or video matches one in the + cloud. Besides this, I would not want the resposibility of potentially + trashing someone's photo collection. diff --git a/docs/explanations/tokens.rst b/docs/explanations/tokens.rst new file mode 100644 index 00000000..51c7e258 --- /dev/null +++ b/docs/explanations/tokens.rst @@ -0,0 +1,55 @@ +.. _Tokens: + +Google OAuth Tokens for gphotos-sync +==================================== + +Introduction +------------ + +There are two kinds of authentication required to run gphotos-sync. + + - First the application must authenticate with Google to authorize the use + of the Google Photos API. This gives it permission to perform the + second authentication step. + - Second, an individual User Account must me authenticated to allow access + to that user's Google Photos Library. + +The secret information that enables these authentication steps is held in +two files: + + - client_secret.json holds the OAuth application ID that allows the + first step. This is stored in an application configuration folder. + There is only one of these files per installation of gphotos-sync. + See `Client ID` for details of creating this file. + - .gphotos.token holds the user token for each user you are backing up + photos for. This resides in the root folder of the library backup. + See `Login` for details of creating this file. + +Why Do We Need Client ID ? +-------------------------- + +The expected use of the client ID is that a vendor provides a single ID +for their application, Google verifies the application and then anyone +can use it. + +In this scenario ALL Google API calls would count against the vendor's +account. They would be charged for use of those APIs and they would +need to charge their users to make this worthwhile. + +If I was to provide my own client ID with gphotos-sync then I would need +to charge a subscription to cover API costs. + +Since this is FOSS I ask every user to create their own client ID +so they can take advantage of the free tier of Google API use that is +available to every user. + +Most normal use of gphotos-sync does not exceed the free tier. If it does +you will not be charged. The code is supposed to throttle back and go slower +to drop back into the free usage rate. However there is an issue with this +feature at present and you will likely see an error: + + ``429 Client Error: Too Many Requests for url``. + +See https://github.com/gilesknap/gphotos-sync/issues/320, +https://github.com/gilesknap/gphotos-sync/issues/202 for details and +workarounds. diff --git a/docs/how-to.rst b/docs/how-to.rst new file mode 100644 index 00000000..d60e4300 --- /dev/null +++ b/docs/how-to.rst @@ -0,0 +1,13 @@ +:orphan: + +How-to Guides +============= + +Practical step-by-step guides for the more experienced user. + +.. toctree:: + :caption: How-to Guides + + how-to/windows + how-to/cron + how-to/comparison diff --git a/docs/how-to/comparison.rst b/docs/how-to/comparison.rst new file mode 100644 index 00000000..1c5dab1b --- /dev/null +++ b/docs/how-to/comparison.rst @@ -0,0 +1,42 @@ +Comparing The Google Photos Library With Local files +==================================================== + +.. warning:: + This feature is deprecated. Working out if files in the filesystem match + those in the Google Library more of an art than a science. I used this + feature to prove that gphotos-sync had worked on my library when I fully + committed to Google Photos in 2015 and it has not been touched since. It uses + complicated SQL functions to do the comparison and is probably not working + anymore. + + I'm leaving it enabled in case anyone wants to have a go or if any + contributors want to resurrect it. But I'm not supporting this feature + anymore. + +There will be additional folders created when using the --compare-folder option. + +The option is used to make a +comparison of the contents of your library with a local folder such as a previous backup. The comparison does not require +that the files are arranged in the same folders, it uses meta-data in the files such as create date and +exif UID to match pairs of items. The additional folders after a comparison will be: + +* **comparison** a new folder off of the specified root containing the following: + +* **missing_files** - contains symlinks to the files in the comparison folder that were not found in the Google + Photos Library. The folder structure is the same as that in the comparison folder. These are the + files that you would upload to Google Photos via the Web interface to restore from backup. + +* **extra_files** - contains symlinks into to the files in photos folder which appear in the Library but not in the + comparison folder. The folder structure is the same as the photos folder. + +* **duplicates** - contains symlinks to any duplicate files found in the comparison folder. This is a flat structure + and the symlink filenames have a numeric prefix to make them unique and group the duplicates together. + +NOTES: + +* the comparison code uses an external tool 'ffprobe'. It will run without it but will not be able to + extract metadata from video files and revert to relying on Google Photos meta data and file modified date (this is + a much less reliable way to match video files, but the results should be OK if the backup folder + was originally created using gphotos-sync). +* If you have shared albums and have clicked 'add to library' on items from others' libraries then you will have two + copies of those items and they will show as duplicates too. diff --git a/docs/how-to/cron.rst b/docs/how-to/cron.rst new file mode 100644 index 00000000..9e6f3c15 --- /dev/null +++ b/docs/how-to/cron.rst @@ -0,0 +1,14 @@ +Scheduling a Regular Backup +--------------------------- +On linux you can add gphotos-sync to your cron schedule easily. See https://crontab.guru/ +for tips on how to configure regular execution of a command. You will need a script that +looks something like this:: + + #!/bin/bash + /bin/python gphotos-sync $@ >> /gphotos_full.log --logfile /tmp 2>&1 + +gphotos-sync uses a lockfile so that if a cron job starts while a previous one +is still running then the 2nd instance will abort. + +Note that cron does not have access to your profile so none of the usual +environment variables are available. \ No newline at end of file diff --git a/docs/how-to/windows.rst b/docs/how-to/windows.rst new file mode 100644 index 00000000..fc0a9163 --- /dev/null +++ b/docs/how-to/windows.rst @@ -0,0 +1,88 @@ +.. _Windows: + +Additional Setup for Windows Machines +===================================== + +Python +------ + +To install python on Windows. + +- Open a command prompt with "Windows-Key CMD " +- Type 'python' +- This will take you to the Microsoft Store and prompt you to install python +- When complete return to the command prompt +- Type 'pip install gphotos-sync' + +You can now run using the following but replacing with your username +and with the python version installed (look in the Packages folder +to find the full VERSION): + + ``C:\Users\\AppData\Local\Packages\PythonSoftwareFoundation.Python.\LocalCache\local-packages\Python310\Scripts\gphotos-sync.exe`` + +As an alternative to typing the full path you can add the Scripts folder +to your path. See +https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/. + +Using the installer downloadable from https://www.python.org/downloads/ will have +the same effect and includes a checkbox to add python to your Windows Path. + +Virtual Environment +------------------- +It is recommended you create a virtual environment to run you python code in to +avoid messing up your root python install. In the below example we create a virtual +environment on the desktop. In the below example we assume that python has been +added to your window path variable as above. + +- Create a new folder on your desktop called 'GPhotosSync' +- Hold shift and right click on your desktop and click 'Open PowerShell window here' +- type ``python -m venv GPhotosSync`` this will create a virtual environment +- next activate the environment using the command ``.\GPhotosSync\Scripts\activate.ps1`` +- you can then install gphotos-sync using the command ``pip install gphotos-sync`` +- You run it the same way as listed above. But now you need to activate the virtual environment every time you run it. + +Symlinks +-------- + +Album information is created as a set of folders with symbolic links into +the photos folder. Windows supports symbolic links but it is turned off by default. +You can either turn it on for your account or you can use the operation +``--skip-albums``. + +To enable symbolic links permission for the account that gphoto-sync +will run under, see `Enabling SymLinks on Windows`_. + +.. _`Enabling SymLinks on Windows`: https://community.perforce.com/s/article/3472 + +Alternative approach +-------------------- +To avoid fiddling with symlinks and python paths you could try WSL2. + +This project was developed in Linux, so if you would like to get the +native experience I recommend installing WSL2 and Ubuntu. +This gives you a linux environment inside of your Windows OS and +handles command line installation of python and python applications +in a far cleaner way. + +The integration +if particularly good on Windows 11. +See https://docs.microsoft.com/en-us/windows/wsl/install. + +.. _WindowsDocker: + +Initial Setup on Windows for Docker desktop +=========================================== + +If you want to run the app in a container then there are some additional +steps required on Windows. + +First you need to have installed Docker Desktop from +https://www.docker.com/products/docker-desktop/ + +- make sure leave ticked 'use WSL2 instead of Hyper V' +- if you already have docker installed with Hyper V consider re-installing with + WSL2 + + + + diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..172f835fa42098abb624920b8b4a14247fb508a0 GIT binary patch literal 42908 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�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 diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 00000000..221bceab --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,13 @@ +:orphan: + +Reference +========= + +Detailed information including autogenerated code documentation. + +.. toctree:: + :caption: Reference + + reference/contributing + reference/api + Releases diff --git a/docs/reference/api.rst b/docs/reference/api.rst new file mode 100644 index 00000000..c1e95363 --- /dev/null +++ b/docs/reference/api.rst @@ -0,0 +1,54 @@ +API +=== + +.. automodule:: gphotos_sync + + ``gphotos_sync`` + ----------------------------------- + +This is the internal code reference for gphotos_sync + +.. data:: gphotos_sync.__version__ + :type: str + + Version number as calculated by setuptools_scm + +.. automodule:: gphotos_sync.Main + :members: + + ``gphotos_sync.Main`` + ----------------------------------------- +.. automodule:: gphotos_sync.BaseMedia + :members: + + ``gphotos_sync.BaseMedia`` + ----------------------------------------- +.. automodule:: gphotos_sync.DatabaseMedia + :members: + + ``gphotos_sync.DatabaseMedia`` + ----------------------------------------- +.. automodule:: gphotos_sync.GooglePhotosMedia + :members: + + ``gphotos_sync.GooglePhotosMedia`` + ----------------------------------------- +.. automodule:: gphotos_sync.GoogleAlbumMedia + :members: + + ``gphotos_sync.GoogleAlbumMedia`` +.. automodule:: gphotos_sync.LocalFilesMedia + :members: + + ``gphotos_sync.LocalFilesMedia`` + ----------------------------------------- +.. automodule:: gphotos_sync.GooglePhotosDownload + :members: + + ``gphotos_sync.GooglePhotosDownload`` + ----------------------------------------- +.. automodule:: gphotos_sync.GooglePhotosIndex + :members: + + ``gphotos_sync.GooglePhotosIndex`` + ----------------------------------------- \ No newline at end of file diff --git a/docs/reference/contributing.rst b/docs/reference/contributing.rst new file mode 100644 index 00000000..ac7b6bcf --- /dev/null +++ b/docs/reference/contributing.rst @@ -0,0 +1 @@ +.. include:: ../../CONTRIBUTING.rst diff --git a/docs/tutorials.rst b/docs/tutorials.rst new file mode 100644 index 00000000..fdc88afb --- /dev/null +++ b/docs/tutorials.rst @@ -0,0 +1,13 @@ +:orphan: + +Tutorials +========= + +Tutorials for installation, library and commandline usage. New users start here. + +.. toctree:: + :caption: Tutorials + + tutorials/installation + tutorials/oauth2 + tutorials/login diff --git a/docs/tutorials/installation.rst b/docs/tutorials/installation.rst new file mode 100644 index 00000000..aeb3a811 --- /dev/null +++ b/docs/tutorials/installation.rst @@ -0,0 +1,129 @@ +.. _Tutorial: + +Initial Setup +============= + +Before you run gphotos_sync for the first time you will need to create +your own OAuth client ID. This is a once only operation and the instructions +are here: `Client ID`. + +- Once the client ID is created, download it as ``client_secret.json`` and save + it under the application configuration directory: + + - ``~/Library/Application Support/gphotos-sync/`` under Mac OS X, + - ``~/.config/gphotos-sync/`` under Linux, + - ``C:\Users\\AppData\Local\gphotos-sync\gphotos-sync\`` under Windows. + +If you are running Windows, also see `Windows`. + +You are ready to run gphotos-sync for the first time, either locally or +inside of a container. The first run will require a user login see +`Login` + +.. _Container: + +Execute in a container +====================== + +This project now automatically releases a container image with each release. +The latest image will be here ``ghcr.io/gilesknap/gphotos-sync``. + +Your container has two volumes ``/config`` for the client_secret.json file and +``/storage`` for the backup data. You should map these to host folders if you +want to see them outside of the container. + +Hence the typical way to launch the container with docker runtime would be:: + + $ CONFIG=$HOME/.config/gphotos-sync + $ STORAGE=$HOME/My_photos_backup + $ docker run --rm -v $CONFIG:/config -v $STORAGE:/storage -p 8080:8080 -it ghcr.io/gilesknap/gphotos-sync /storage + +The options ``-p 8080:8080 -it`` are required for the first invocation only, +so that the browser can find authentication service. + +Note that the authentication flow uses a redirect url that sends authentication +token back to the process. The default redirect is localhost:8080 and you can +adjust the port with ``--port``. + +Headless gphotos-sync Servers +----------------------------- + +The authentication +flow only allows localhost for security reasons so the first run must always +be done on a machine with a browser. + +If you are running on a NAS or other headless server you will first +need to run locally so that you can do initial login flow with a browser. +Then copy /.gphotos.token to the server. For this +first run you could use the following options so that no backup is performed: + + ``--skip-files --skip-albums --skip-index`` + + +Local Installation +================== + +To install on your workstation (linux Mac or Windows) follow this section. + +Check your version of python +---------------------------- + +You will need python 3.7 or later. You can check your version of python by +typing into a terminal:: + + python3 --version + + +Create a virtual environment +---------------------------- + +It is recommended that you install into a “virtual environment” so this +installation will not interfere with any existing Python software:: + + python3 -m venv /path/to/venv + source /path/to/venv/bin/activate + + +Install gphotos-sync +-------------------- + +You can now use ``pip`` to install the application:: + + python3 -m pip install gphotos-sync + +If you require a feature that is not currently released you can also install +directly from github:: + + python3 -m pip install git+git://github.com/gilesknap/gphotos-sync.git + +The application should now be installed and the commandline interface on your path. +You can check the version that has been installed by typing:: + + gphotos-sync --version + +Running gphotos-sync +==================== + +To begin a backup with default settings create a new empty TARGET DIRECTORY +in which your backups will go and type:: + + gphotos-sync + +For the first invocation you will need login the user whose files you +are backing up, see `Login`. + +Once this process has started it will first index all of your library and then +start a download of the files. The download is multithreaded and will use +most of your internet bandwidth, you can throttle it if needed using the +``--threads`` option. + +For a description of additional command line parameters type:: + + gphotos-sync --help + +As the download progresses it will create folders and files in your target +directory. The layout of these is described in `Folders`. + +Next time you run gphotos-sync it will incrementally download all new files +since the previous. It is OK to abort gphotos-sync and restart it, this will +just cause it to continue from where the abort happened. diff --git a/docs/tutorials/login-images/01-sign-in.png b/docs/tutorials/login-images/01-sign-in.png new file mode 100644 index 0000000000000000000000000000000000000000..02b879305767292d1ef67aad1c5d80dc4aa590c8 GIT binary patch literal 41247 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 UK}E#g9gslfz2>`01=FDa0Vz?;&Hw-a literal 0 HcmV?d00001 diff --git a/docs/tutorials/login-images/02-verify.png b/docs/tutorials/login-images/02-verify.png new file mode 100644 index 0000000000000000000000000000000000000000..5be62215607c7b9e7c163dd007420e29cc5b8f1f GIT binary patch literal 44621 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

{#hhDU*`?{Q51MMMN~vz zhtab9!?`IestpDT3hFKKw*|A$Cjf#!iZ%8q(W;W8+H6pAzH0b} zYJB?`=b`E6xz+HteIQHFp#P));hOuU85O$iAv&QxC7%`!A`#x7wk-)228`~Rr0B<2 zbfNkK=>Bu?U!elti`D+`e@C~H&sO`_tNwRrqJBNi|1SOa|G8Mx_E2a}zCSSn(qvlm zIQHjP>zEgp#V%Lhvg39+34NXaC%n)#<-V?y>sPt_NE^3D%0Hn}2qe7x_W0D%tB{r6 z!3h49I#Twbq^0)3QgG;WMNOfYZ(3kj3cUMSym$g1=1coOOj^r0>Oa7=Jx&l80nL#^ zSza;TUJY4T3sqW7*-gR0RO3u5Sl)M~Ekv8== zT0eJ_YCk{xm+Em27J~4$M(A#2wYSujp6R%6yE(O2lndV9xgxI?v+xea(k~>+iode> z^Ejc+ktwM2Io87zC2E}k#(pG$>R4R1gDxWUdzjtnKW!Ytk1 zptm$vJi|DT*Ej!QX@4Q#c%jV74_3XtLLPTII-PrY1LS8dl392$Ke#0+42^Ll#0*#N z;YcsmPgi~eLxxFj?@g&=&-`Y-F{YaM?_Z;6%!tAJr}g2J5Amqb#dyeV&yh++QliPp z%Kb&!+4-+S85ymhIKh6U6?mVqUevnXRr{vu=jQN~N4lZsS{{rKu5_N{k-!PkWgTpC zb~@x73!>9kW~b5K6!$zT2$kZpdCP{gXX1!PbKF5&?fCeQdOv3!3ZaYRp10H8=D|K3 zu#(Wo7yEeQ%&nrKPF;Hk>ZuMSpABnGQXzR<*oiy(C){NYH>)sJ%*>G@jYfG48}yIC zE;~?I0R~30ZZ;LR;=k0!ACWlLBA`nMJnugbDxOfK=61^os(k(P&;HA&GB1yWCEs6O zAch^h))+rPf`vs*xE#bq0jPfeb_UWBEu`-_jz-0FF zMGFVl)6&e3M8Fk&nASb5g6oeT>e+ICVu$IcFO%#Yu>(fh#1hG&)6V;p^mc*`hVS6$ zr!oHQQ(6Ue8*Y!X)1|t>Urg|CH~-_{@r^}mo+U4NxLDTXAg0`wU?1GP4vU z{l5NS(H%U+jB8nM-aSA1P#U#)>`Hf-l9cqhy6_~$VA<36X4#WKS480n02M9i<2VpS zfl#>!cXib?e^__cWn^GKB0VG-s=f!_M|EE!z`Y#lEcDRZ?_9e3Jn6Jy{pflz4YGNM5p}zduGJFPJ0)?0-l)l-zE`DlT+Q?XE?X zzn~TJy5rM*X)>zfcT?IQ7u5(qoTwgYad`*C^|>OrJ~^_Omd=5F1Tq(qxS?iE2Zpx` zYa+wL)`cn{6p)af=B3KOEuCV`)7)zdm3ScG={ILkV&u^760%a#*c9|Ttg` zsd&H7*CL^QHDn{1}=f5F>BHxE#qv8=Wm~H4IJC^H5~V&9@0Zu=3&fpA5Quv zW2eWlRxK>r^_$MCcbN&{XSWlQLX`YQF`to@pL4%_r4|yq3B58&`v9*KGn2U^Yz5{B z%7O!u$6?v}LF$bD7^^Wa(q}t6he)xr5KOPp!`7mw0DzLxmL`7Exo5V7#BYFHj=i z5#aH%6h-l<)?;=``A~o^w^YbybkTjsQP$#B3Ar24R_fF>z{|>3w|9;HrGw=iPIY7L zxN&~CpWI0?Uecw*TuExZ#XGE`Dw2D8xA%xI9CMudNazM$-kuG{ezY~-OfVtbRG1P+ zy}v7KLQ1sR@J1D;^aVu{E?w(Rq$bOi%(hP6HhZbTP#8O!oX+SLICpXz+T2nh<(lIw8${<#ah<_-pC-Pc^YBxX-XZw9t37p60EVDr0o;<0C@N z-*{KJQ@rl5v+07!#y5dON(0lqkt8}%NBZlpH8K@~5p`=|f!>2UCY;o9)^+a^2YZ!A z>JSotln3F*^xY2jq~^KhH&$PSN(Om)X^O{ z?)MLqjqJ*eAztlGf49XxZs(4}pXW1Pv}UU^)vsw8#OUdTEh4@Jz&|k7?LJ2(eOXDX z5o!aADy~3hk*x7nSZKkuoG90mAY{S!YeOq&EN@4pIc4GAsWf(%I}}_qz!`U6@~QWx z(7S!<@+le~OqubVDr4EK2A_sA&c4`IaE=2A3%;X3+ldv8G)f|zv|uZG*wuHZ$vLmY4M=QJm|v@ z*CPG|Ew#T(!7=jcByMhngvmit;2X!Pl>G%B_LeRv%%GOA*8nok);VuR9Yb2EQco9M zwh1c^x1wBesAmYAN{Ao`WtWs|feNU`QAsgc6*5o1bpHMgHyNYYovxh27zE#8DX4;{ zp?GzDi4wy(uQr&B=;;^Im{rgmt+bA`n>E0y@ev*_r@l_v4m3O!3q6rkyLk*)uU|!F zGX@%vQ#?Jne_UP;9apCf<~GbhUoYlRw*0fi9*q%x+A&{avpn%$=|%P)$(x&r+i!Y8 z-pT7*@f#9dR*kecn0CxNCOHqFbN#|}sExp@?>Md7sX0eBw1+1F6+bI(`d3$z7sD;= zYTa#gcB2%$D-%oGqpLn0k!Ie=FVys<%%yqS5ZG)POZ+LpAdRZ-`q#Aik9`>73muT# z2Qw(-<#bZ$JSV8|vxN69hib7su5D%IngLOkZP?eUI|1*-9`p=_(oR1IR|%%dvIl!J zUUfo%J?SOvs>eUXLwPs4?dfm|JX6tOtnbK{#e`-Oah%TLkXw6EVQ>-e=5JOkd2i+r z5xtfIsvT;ONpX$EzN`h~6-fJO>;?zcGXK$2Bv9N38;RTwD=;Nv>ID?k{5;ZMx=rI? zbVQrtJ`>NftjyTT<57~FC2PJwEo z&g>xsHPHvp$eB3pZ=Nm=Ay6nOw5fWbQz-eh6h;;&loSmU<#K=mu+3Jg{u)KCn?dt|W+rn79zQ}Kk z=g>vkmY$(t?zjG=xhG&;M;aJWFd&VOO_Cdr#654OzA6n#DI2u6=sY@nf?oW1;+}!wCh*A#eO-_tEv;A;ndi{9E z0_D%^9`lARV}4f+F&h!v6j6?YL*#!r`etcpibGkl?Jz=d2OtcFh3Oya1H(2&ErI+M zy%vCke-<3jfLsZWL>Z|<0HZp3UUqCOE9< zFlpW;x`d*EF4G>KHe6-V1N_j^Z2I$4hKveU2yX9eY{x)avoNj}W*$m<2%M@JQwLYG?q1Z;m#b=nS55Jx;H*xX&OMj$XBM?B~6-UKYdEyJlk zxg`nLJGtcnzo2XKx9my@UhRU5o`t&(V0sBQrv(I%pdLf;f`xU~7zj;vKwC{c;4ne%wQV@)vcF z0C6jFJ(4U(d#Xl^B^-w~&@ec#((|}4Y}VdM{AD>Xay3j>jd8Z5Kr)K`^I~S!uT?{z zG9i|L@{2q27E$H*op~*=Pg|f~&XkYvh!Xcn739h04np%W@(jqyNiGn9E-!xO&|QWiKw8 zn$L|<)idW}%q=1x-dVG(NFLYf#>NlQU#Xg|)W{qMHWkXdZ+bEZI<3DyW1(^0x9rRr zw=fCaV^)2{_dL||S%qOC$Q{&!2JD>w>xofHZEI;G9D)1GIO zrzHlTm8`@uw%o5T;U7u~qPdz~+dWBPVv;FAkA%qj=rTZ0brQ(O21I*LkePs#9W?4M zfkZkdTrAZBq_Tps=b^gI_q1ao==o2CDKAIg@ZH<%UopAlqK9d45y@4WJsMR{aN12a zlX#iURAHCiuw-}Nhi=V73#zCRGa>fciGHMz_*`o*KSY(BDAHzmdtmh?P`Owy0}g*$ z?Z$}QKI@sYe>&H3bhLoy$KUOASb@v%>MlL*YWUiE9qD+~vL&6pd^;Gw>Yu5J(;*HZ zKR-|5FfP7SR3bT>ie`NE?CL=+ez9D0d1+yC0kuVU37yD4J=1o&v!$if9qtk%Z{r>M zPA0}Fk{9lhofz5ijQ3KATVgRS@TcrbBfY10n-7#Mb0{ zGpGpZC^F8+UjP=ol!PbNY`00~MR$+J#50wkt38nt3tpsQOGGpHjxrnmDl&o^TqYy>(iqkO6BgOYm*=%cFJ&;(`f2Ev$G55%U*t^jk-7q~2q z!W`{dpL)-N`HvYZS-fv&xMzHhzH=;QMyXMV=S#N6M%i#FBP%G()PELRFH>*UH97ky zsbLy^WK-N4JK-q&_PAA8M+Vq@>$Yfz)7kD5SQAS@A14g*xypc@+V?)_35XPU)le5k zGe6i7$)3b)3>G?4cl(VpkKeQ45Luer%9?T?LPj6l9{1TsQ|_Ihi^nk6x%WJ!ASxMU ztd$NvdIMW)w%1Ji&h$ugoW*6IXZv&{oW;?2ING&{9Xo|NUKV z<>OnP@Y!%fR3*`9WmgyQ{{pI+9!dk+llxX8L;+RXK3u!6D)0vkeFp&T^>~4o>(gs^ zY^oQkubm+n%*tdhyxyeWUcfMF2v{9DuTWb|Im$!A@f(qHM26uIY}dj+1b)1|UDIFg zU+NrGTY7VUP!+*xQ#58x`JF23f$-lSAMFKhU&@HjK|u=#iRrbmP)*P}e17qS)l zs*K^5KywyDK+7}AJgr`321c}ks%)hERL0fu!N2q~9ahy*XYCH!}Lf6ch=P2LGF*aE3T)p2rx+3{@sdO%1d1K$(cE`^>vastU8IDi)o|S zQYDv45B=niP%@)Ok@n6OgZX=``ZzTvV_x6FRJm56pGepqzP{CMuoU5|54U@Fk58H< zPEeY-nm&24Yo=xwfAh_KuU(%LiWH=0vu8*ij6nQD;CvtuW>O6`E3SZrFgqWof5r8X z`n;I-Y71gg>)>m+C<&8o(||gJl%pqvsI?U$MS_`TRSL&Z__tt(3RTx4vcL^v_Vjs9 zy)jB>{G!~Uf6ZJX!jvSDKI_%JD2~U_@?-P=Pz3BT9av(r7MWe{a;X0#M%`p?&3$4I z*?y14&^7saQp&WK_z3I8eZRnpFVt+kfKoNH=2?vwQ6^i}P7n1?NDS{i0-FV72*K{as@7nMuRW$~bbO zpKvu*PPkxGkx&34H*nwM*$5h~seCKs)#Do8sc%5tKQaK(=zT`IB#uN`%W|H@B)cea zy8o*6ciNZYd-1g@dFVKtb&e4xnmFIsPVMzm&0>IkG6~;@V#3@+9COI zwZo&u^FDj`#~;?3Zrjk{%|hwc`jd@o6C%$~HI&4!6(ymTeCQ#Z?fR|mpUFT<=8S}V zoU^}3IQ=IneR;*4KS79rRbF(MA^Sv;XYrDCgO;ipB#p|=s=mt$2{w;8>?y-Gzvj*c zOt9LHA^rO!SitA9?j6DpIK`4d^8Cw}fODCkB zc58h@!~Lft@bWk`rZ6*ZzwfZ-Kk#-Ejkv=9!S->f+u$PVAZc60r4uvG;>?MFu>FK5 zRFQHU4ATgM{hN=ZmmTkW%adtnqFR5UYp~_Zzj#^;$$?^x*N3z4kYy+^PsEeKUy&*Z z$0%v82)CQ=)^@MSkud-8m@Zv1RWjSmr)`g^<{%`13@4GSts?QJ|MpSl5S^=`LVJb; zC8bQEC;BQ;)6^d-+Ts7$|6yGRZ*HuTC&D+;`b4{N=o;$or_GL5y2Thic&YiX#JfpPvTRUD_8{UT%CZvWt|> zVJaT9C}U#3hgh-oY~zZt2N`+mM>fSNadYoy&i2Ib0%px#|xB7bsQ$6hg;&8g5I z!pg$BeN6;E^cz#EZ{gvGa8A2_xv$ZkrtcNg1!k(w#-NMqYK|$*zMvoLC1ARbm-!n& ziG`zug%4DQd^7r`41nreQ~i0P&j_;?Eg7cgQ#BI|dig&rJvZmmUr8%k_20kI=*Kw{ z^e*YaO^O;S=zO!^leK=Gqu_BtyIu58*kQ14GM=rcW1No1g~hIn1v|ZStG(0wp=VXm z-4_b^Tf$EqXWKcV;oS%qG4EeO3#9vH<~QF1OO+Cwx~i;uXV&fFpx88>8;`WZ;by8f zHGyWXM$nKD(Y;Iolbv7Mj+w9A+!|5Yl~;sXyZP&+)V)U}izA9=dpYIY!gULDR$tf8 z)*CX-u46>!fVC<^Zv=*`_BtzBnkBw_Uhui6;A~zvWkc#Q<6sn^0SlkJ+?%1su~lmN0R!8 zOH#Lz=x$|)weCDOyY9EVZgWM<9Fv0=csIRL!cSvX&;lcwP9wrTAD4FtP0HEz`hssk z7|1*P^6>rrP75(Wh^F3wJSZFw6h`B{wP-oNWcGVaEqrKmJj+8R!ITGigvssnR#(j<}@%1AIrmwj|3 z)algh7S@2|27p09K@*M7o&0dV96JXk#0!{CaICfk4N6<^@~8ME)OF7)hPQTvI5ik) zp8wJ{GYpBp@j7%WH)(^V=kTm@X=Q_j~Cr)=6S zO(5O!lH5}9UkkmkzUOBXLUSBTI5`zI`K5xM%9a>-IG1H@n}^818`>fnnttsLFmyZ( zhT&A<$BQ=+buT?x2gkr1!r1Nb)P{|oPLh!Qn8`j^*|9CwIi zx)}bcxCn7gGART{lp$9!WO1@5^9x6C;|s;diV3mQY26pH<4@@bH(8kKD*Zrv0@#A5 zqf^M&Kt-rCth&UAzlL&ymQ#LW6@+d|1QQOEn%G_j7&ROCx)-yMe-ruy5Jzfrzr|sj z&ql?0HOKniA$!~k(tRdf_=!L{c42qQGZOFqOJH>X_TIyUs zHd5G+L%!kuO={L_?(*OBvve^VB~QSv)(_duhjjeIPSF}ZJ6-lYq7t0ylP8ETY(~zg zro^yoyIi0fuyi|6>Nfj;reL#&0?vB|eC&eX_6GgR{D^z~c^N4;yG|{lZ`j|>a~!+Q zM2_2jjj@inDP&*w1E|uMQ%_J3t4&O_`Xx2$2s+yHX1z*cE3MJN7{u4M?U~WWFRAs( z`fT^Bm*{LmsrWH5+ZlKo&bDdxZ|pOdHK0|7=t#2u=#SGM!ufy>uDTJ+WIv!b!33kT zGGr99PJbf7dbJV60kD2)`eC#~LbyQAcR6*XNi!^K`>0n)IPzUi1Hg?|zH7}?NZcXAfYF^ z>bO`B?h9I2goMB=vMkM(>s`tI`p@hu{q>`Yqt&b5S`ci5XEFUKi%y*7A1pE9@2G}f zl^7@bEPoIQ48wIpS?^@r|4ZZWqDGjg2N0&*Og?HxJh0|BQs)6@v8SJ;6E@xAf9j?j z>$V)UKqjAuedsB$bv?{|06Lk93jK){r?bNGMyJle8pX^)1R2 zc|##IFN}jcjhs#e8OR}@r-+&bRstQVV3>WDQ__lCP($&+RA@gDe*86v=AUgJzt%pz zWeEv(obwtTnsaiMSRD-xk0OBrwx4KWA>AmRwnVf&coVD}Uv4Bn@zCp3y?w`{;^LzF zyjC#d;d%Oh%CMGF4Xn?=Dpn{emGn5cw7GG(-%+0jF1oZzk%kfvtWLrzo|}}XLq%YD zm%C%Bn;y;TZd*v*Ni7P3ex_!Udbu58cfM1Hc!m|#tOGr!{^tDXkNxO8D|psK(5K4# z@Pqnhh)x5CMIBfmQc|j3jE=2uNg_AZCu9En5%eMb6g4e-C%%t@&|ypdf^J-`dzZQ!g9F{x^5u&fFdz0+gg7wV&G zB59zStl`^=8508e(YZV3Xsv-PrG+$ZfeRgCPfQH|1?-&0H7&P3qRaQShd4i7s*inOsCQFC$uR!g4EK|(>7-NVAE$UHTJ<8=NV zdIamZ&)S8e!|<#!o&cd|1CM^Y57v}X&P3GMKdTx3;y`5i3(l!)hbyh#($DGr&HTjL2ZXG`*EUw&Q1ep> z`4CG)c>OGQ-6q<}?y3z~5BFM1bxE9H2^2hR)u5oe#C_mLA32Sh%Vi*TS&SE35GPv> z3NFzP^_C`Uvt6cEWUaY9MRqt@MPzZm18a9qZgj{z9br}@d+i)PNY^j!JXr-=tgeK* ztTOikNK9HsT`yawT#jOhU@w2n9mA_0haMfD=(&^y<_$|7&`I#@7fh7}E#Wo&p@J49tnyrufTLgu)^!p~96I7D1&ium#2}N3a@3p;xy za6;LqqN2?_rRzVOgl3ji|CICnRJHY2x-GPX&3z;}r{RNFt(G z`4`=y5W}KeZ9g>JzFm6H<#LlKMU3+JhtYw6boJYF47U>w=AuzN&MR%&?gpnItxcNl z!0AqL1uvw&y+sA7G$e{fz;9M5aiLj=S!@TW;46GIOcpZsX}FU1WIza+aK3=a|D^x@ zUL@2~JHAc&cqL1$a>Pb6*Wx&Gtw6UueLh@pahGy>zdpIUQHuTX|NSS2nBX@QG2dSp zd75Bc6qM_xL}RU`$})4cr3Ee2wMs2#Efu-na&r8s%oI6~(!mZW-+k&w3|3t~L*dItftskzjCVpD308VK#T@N@&EsE_tEE$N|;K1_yV`q`i^B^x% znvY=PKj;I{vs?q&ima;a%zsa)vkrzr8jsXu4u{pDz{-d

mF{u~n0FHHsHTXE~|h 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 literal 0 HcmV?d00001 diff --git a/docs/tutorials/login-images/03-verify2.png b/docs/tutorials/login-images/03-verify2.png new file mode 100644 index 0000000000000000000000000000000000000000..a0ac8ddd9e587e1f95dbcb9e0cfc49cb36e1be2e GIT binary patch literal 56822 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 b9G1$Ozia#$1QukZIWL`O2AU-rcJKZN6T6CM literal 0 HcmV?d00001 diff --git a/docs/tutorials/login-images/04-access.png b/docs/tutorials/login-images/04-access.png new file mode 100644 index 0000000000000000000000000000000000000000..77fcb5d5aabf059698100f45cfd9261cde58a73f GIT binary patch literal 100580 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

>6{j3jqOXpjRE4`uS zTDPx>x=+yS4vvx!-me^2?d>5&)*5q-5&&nC(JYYRxL(f_)%m^k#2v$if?Z{OC0B4t zr&|BNd+p{bbA4os3WAs$t0HYvin`|Lqy5#ZyF0}}vi{Aj(N$=-7d`~3qqh{*q%k2m z8L>N?EknEr6Tfwy+9CJ$kN4xEaJqt87wA*{Y&@wkZh^<)dz7u}k3q0?!Hk&>^Y1Hb zi*>*?C~}`~UcC0CSNdnUibI#u5gwa+ZE?t`woGv;SvqPt<|jsLqm2Z~+^i*6vJcVs zq(&{iyr}Hw{qpp|gYqK5QWJ6Yp=EMvRh{}ua84vNbci&ubzbmm<=*`b7D`5D;{g`d z9xTnKycn_`fBO2SU~ChzfSRBL?^>}jx^@^_&s&tL2Obw}N3QZr@!uqLY+3d5q|x}x zIO5}4dPe0N{zitq4j1}icWoz^0mbp5{gf+tvYWOs$?^b#i_2Eh4V*9 z!gjX$;dSwAIC!#d#9EpC9GJ@auT*PUY>@zh;vfT)%!)?1oC|vSc_hDtgrb&#AOIm)^s?OlQ@yVB@VT_SDMtUouCUq?z$J%|DreK{QjBU%*;}a+3}d{ z(AmNas+GH^RDlpeCcu!?fK)nPcM!LwiWBU870|zG>T8 z@x~qQm1{Z{=y_Oz4&>M_Y4vBEQx{;vnpJHDUrU3m2lrKTyaPODesX_rqY1jVd=yx~ z#(i0X8%?~#l&KgsQ2<>mE?sH`%?p9DrMyv`+D`xV+yW_X!TyQgV14Ar9?k>wcc8F{`K|`FPz^`y^H8oR9NzzCROqz)6_P&?-}- zfCOY2UljTJVU8|YnKQf9xlyHxE8mfLVux-FB~rgx$_-RK@kxK|D8bnodMTiSh$VT` zGDjpxd9;*D9Nd@MVUlP<{DEl&N)%5obM|t&>|PIex}DsSTfLTY97O~ZtMz&E&4jT- zvsXHYYYjoVw`ygN5$#T1NeYW&TdIA{(5?)+6Q$BP{X_UBeTxW!xUAfdB!@-2Vu|UA zfg6BdLE!@dK~#U~fmKx=gQu6y9uAgm9iIG0Xau?d!HU0TxNJ5eYbFifDGSkt-2T~a zx_w;j`Z(3e)otMakLJ!YDy}F<&>?sT5(p522G_=2Lm-4;NpN@f#yvO$*TyBdyEg7k zBZ0=<8`t2rot@nu`*Y5jGqdmKyHD!ASLdFpTVGYZCdYGG!R8n05bO2>7^zC))Hs%u zuOxnvI08uBW(hrs;Pa1}_mrF3EGI~%RX8M1mJAYPl*rP8scI#MEVXe9HBUNFpIdWU z0WeMw!iYLbFw=~oYyM7S_0SrAN$TpPZG+8cKNn0>PvWRL-hr-u^?^t}YvjR2STH_b zVWmW8Q}b)CqVTE?!ZUFDc>9$O*ZPs$!?ab=aYZ>!qzijVNxXAeajfoTPJN@^OPMD! zJvnDpNk!qPhaP5pqp@}OUrF4Rsite{4V-}#HX{<+T*5Qu8&#b|)oY+P<@1ih>?^DB zK*YmqzyhrXZ)4a6oF2O(>z(XY-@f~FVSy!Hr7vgH$?R^gVBlrvamNb~oZ9>XcP}Ma zO^Jo=NO|JT|K^W-aaBPk3p7y5?u9Ci3mOyHZ7YSW`fVm0t8=2(VwqkQeho~1ZS^Of zg3Wh(I)?c4pxm{Mulkk2;Fdb*<0qjkvPPDk(ezUwJrHRrz|v{cAT3G!VneS|OE%`l zD}z@mN%d)TT?EeFlFE_PtS>B})0}Y?ug9t}i_FoI5i&v@%)HpaUSA1y{*U#lu9iH4sTyfCkL{t}gUt&@UcO>l@B`Y+y?DtK{Yn0A z)q$Q8EAPOCALt}S=gsQcsQ4J5&zD?bQ3RUUjE^!-^vx9Vxw$uDl1pccTNJd@BIaYO z`w3wQ+D%>#x0?^2*%VuApC?FJDSg9_zNIcXisSaDsyo$xg1gy@N&BkkyEmIJNnPsb zVC#CFFV7X4C_WWGsZ=h9vcpYzo6 z*pBGiA@6fah}0c`cKwfUxzvwJofiOW3MAfxH`0T_h#hA+;C%e0^<;c?0$ypYR)U~C0wsfIC zHVZL?da%sa)JrmlvQ8|0dsf*UH`xB&V4`Nx%YA*x(kux6;YJMp14a$TGYZJhH~G~G zQj0zkl#KQxEDiS5bI=9(j+lxQ%cW21>=<`ve|BtmcYi@dG!8We^M+L_jENDC)tC$V zojCp3ibiiKP+fP0d5?*!roIfobmfmrOhZG-zP^L+d@nqhqQf*XwXKs*4aCF-(!htl z<12x$OeuuE)b>gq#aOOHS{WW_jRFXl7S@R zY+Xw+zV@^JxeNWQ*yslq8Yd0uhYCn}3b^`4nfIsdvn!64?hRV-2DTJ$xzbN31}8Hc zCRb7D`~^QOnW6_r!-}7FVRj%*&?&-pJgFstgj7StmHlY+Enq0*62tM(_&CEyI+`J-!Vh2;!_qwD{oZY9 z`Gxtq0|?RyKv>aanw&AJ46p1HDyMiyv_JuF6(M~$DGsKUYjx^+o9&pG!S|;axn|Q; z4qI_P$?ykgM;1EvCaD2ru4|-U<{}#)GOn*yJyj}adm=x>x}b3=Tb&m%a5{5Jsga~2 zf@YI>g~{G_=TcMj_ObNyj|rHRtLv4=(Y;ZaomTc*|!&e)Tx+^e16NCLR527O>k4%efSY^FQ7iw_?{MaC-*2jj(U zNGugiyAK=Y0uCmd?*v0woDNNdb+&ehEN>fqY~~U?W?aU!K0j;qNofewx}thT=+I;^ z;LDCZ!U=+gftjsu0mT*R%y^Wf(VKs`h-gjTJ}o;&24mq@-w)#XWl>fSt7G>kCw8@= zYLB_sUi^s`%@WEXTvp4lg?Q>z*&XVh<=L(5vCjS#SkjT-cc*~xe+Y+}-&4zYD_!qc!XPoA|ay8=P z8R?DL3UOQVLNs3k%bNUGgZ1CoTm4IlV50S#`+m-5CD9cU;mqq5rF`WJts6LPE$|HA z^-s#a?Rx^{9uV{Np)kcqaB&H%!RBO-p4QV9lylY$#Q-`!F%sr_9b@} zZvRMRV3v^xkHyRhDeTTPQ+@e?ETt6e4HP*$h5}WI^FR9+_)~Nvxz0Eb^3TwgA@!#PL%{FE0GK-1a0(d zVlVl`TY1`J8E~FSoEjbsp)G?W^^xp%*jwE^HX50bO=51KI}Y1EN}Lm;|2Af5&C8aV z!+`BpoZaD#IPQ11SxI9RR)&x{=wzo_EdD}>mlmO~{jzw)K^@b5i$+>4CA7p4dX!$C zSmkzHb<8naEY}Xi(tKR}@|nUyf^hsbUzOINKM#6JEu^+3XN2H9aBmXKd-2Q< z{1n=XqH$egaHn%6utxcmqB38p{OmwUCz#Ert#U~bxLbQiVCKu0u@zlWLcV1({ww&Q z9ShO2i0{c=dvn9KT1VCeD*K_;mxz>M6wd|0Mt=W-gDb=6-2s;<)}Q>k_J;$% z-Xz@aFXPV5SUgXT3Hxnq?w>+Kc;}cK1mO;J5}C)37Ohrq zJEI>cWfu4gOU=>JliAF4wdl}rqk2h;=Y6OuX1#lUSA{0jv=d&kxX0C66Z%ztr7iUn zU64`f_ORik><~xNPM2}tySwly)dujR)?R!oIr3>Ey5i^FVKp9|Onpeo3=LJncvG|7 zQlhW)p3vFr9_G5Me50B+G*tk-@zDij`5R6hIO7K1_YpgPR6hVxvZE*(FHM=sKil%s zXX8;DwVo~IWJzUjlL8d{YhL>RF?_{G4Q|_*%CD=wyB>_@Wd zH3U{Ib9R0Cwy9LteP0|=HsFjQqUhnPR#tG%>4MI47xv$$P;q4Ek4t3)tBjE?e!-LZ zzGsi>2d?6{+Yf!s!K5iQv+}`V0%+p%p?L}Pb!-gJCF1s7KfvtcVmssP1Uua3JywSl zAEfx&q79Kf2|Xl!Mtqw!SH5zp;V~6UPUubUv{{f#Xm&!kOi)4&5fej!vcojnEhy4` zn157_;-Ll6bJ+QEF~sX+C=J(4k$1sxo4L(;^dD~esYL?I{UtB;&9qxN9w;l}@%V~dJZBAmO;0^vZ)kbSCEeBiD_0I3$=-1j{}i0!F{05NLg&O6 zn+5M-5Qw~6J~ogz>0z&JnA(NZ^*)e@pDE3ZJR}8cV8ldjju}2$>et!8-6U*Szw22; zqJ;+?wq*;uG$_1x3t3{55ss5bI5%<{$&i_?HtWpH343oY^3z5^;{UP$_<-Py(BwJ;hQ2=iXv)`5Zs5Xw zA$!V*(AWvWAJD}Lf~o7uR8 z`zI&_mD^*VdPiR4Yb#9BU4``cXcaV*G_|gYZ+=d+GpZY%2V}io@eqBpzujz7S<>oC zTkCo($*08(+dLL#H#jZ``!a8d^N#`;vP5tZ2nt|J|MUMZtib;aq41x5>wnM&adFaG zo@j!NIywN*ME{$Xom6>xR=q3~w42S76Yc$Tt2}DZ_-&(-pfF-tjZMt>HJ$e9+c&BX zu@yuLNnQjkaMYjK#Z;h7#gbS5_jN$POP4re&M=4x zH6vqe{ohD@eRWboPDRTj<*!8&YM&AEdi%6R0YZj4)2d?l1sj*}^_3A9%(N^NB`W%t zMav5MB&EvhfkqdMad6-Wa4GeU zx2?J6lVGa~7pZO!7m(BI(2zp!U~2{3HP$bz?uJF+p5PXg#P|})RocEX zF%me4W6h$-946v4mXR+{%jWB7Gwq~7ZsBF!Xx2G}HIt7USUkONWadBkr#BlI?FXDPfxFac4 zIU~PiyXGt?p-@a68p;)il_x?axjH+xsG8D*w)sF z_6GMa%0Ip*3b|sINsoQNYT9&1dz8tKry%H(xzzElrHa@d%xu1Ifwf4#V0XJ>X{;hZ zue$;2p*dacI0~Q#47b2)O2lKDu4$%pyj#9Iai?@J#N_J~zdKYcGJ6vFQ+NZUR&VWP zQ9PZXK;Q21xjFB^v(o+*0wCBfy*ZyT7BdgxRCfg!!wW7J%~-&&uEBBngjTVoMY^AJ zKOZb1rq+J09k+)VC&y0P_U_t3mstvf_*F z;)Z^^-O4S|g(gWP-1Tz+tZ@?X$(ps>+;c)tEDg)Ue)FT4BT7E`MXDz+IG}n=G}Yi{ z_R5zpCiWxi)1H~9)yEgA5Gb7JL3trme~orT6(W-EOJYs-MBF2W;Y$7RN^L2 zDZmF|?wPIFC{koFN*2+;F9EhlL0HA>dx5y;r_9kx5pHa15e#nOlp2xDbQuccNluxX z?eXQOrLgF;T)%o@v7hQc(UCXXNHkU>@Gj=PwsaKHjR)v2_SDdmn7#0}vD016F8<<{ z*o-)3x+xSRzJa8uiP1ar+RSB-HOgZ}GM z${w^GwPP^`&s6*Xh-R7sry+udn@voX@^i)vP)s{Bf{Bl}{>NWx+vjSNsj%D{>)$?r ze?;=v?69g82!C;F#h5cUwM&cm>hkDb>bMTH^VMrRcX#jZJQA)LQE-g=h=Us~@WZI3 zk)l5EWvWh|4j(CZqtNMT-Ko6zGxaAAJDvlgK819GTUUD=zbuEz=Pj0khEAu;pv=s0 zK3imqeNbm51R9t%LE-Ji6O%u3aqyw;gw6W@ifi<)3D3>Sy{lu>wb2w@e5ifbKc8f2 zI;Rd98Rdl4*;0v?rPUN}*8qKpz%SI#-Vk`d98m#yPOwkD0b$M6lA9nkNRF6Oa-jmb z$n#$`?B8A3aF64Q^`~Q$mYYj#Gq0P`GxwpSy3QQcz(foGR+&4R3V#_!?tFDDmG#tD zkdFR6mtJ00F}g^&o8fsJAvqSI-f=yJ3;$29pc+^uuGK7%?mHDhZ%P}R4yCc#m=>x2 z$Z)>pGmPO9?qs&(*teYN5D{LUIX^NUuuG5BwhHZBd$};PVY2a5egOys#gOQ)In)Nq z3s^G>H|PKhBX(OmP!s%>lGvex$j`MaZ>3(t6~_mN#1_m=Czp6CV_xOD%IaGiHaT%X zqnQ2eQoKikJi7{2guTvng4~I+7h7l`!C2he$V*+mJB5I<1dmZmw>YfeggfOUG62_T z{o#?;)+DTi9u1f$cTFX9)04K%(0l+=QKKb(#5gcIDdahR-o-!`Q!+9w#9ESff6=(q zv$k0~e*=t5YQtjm_j0jsMM)E=WlO-F-Fj=s4IKbyll^|?hm^~^5wU_isUp_>Oo_c) z-OLu;eN?D*#YD37#p24Ow#~NPStsQub8U`Ellto0TckZJbTS0u`@#C;1+&liB<-`& z5NJe}%gC1Lg8OsK>-bk`r3u@0KS;brr28?jG8IqSnS&vTlqoirIPriV1i%X?hAC`C zvf=fUsRxvu+U>LHHhKIA)*@Fnb^OcN@Z6;U?;8sX4y9-J0L=rgW*1$0#8<&+*z)5% zTqk=$FnZwrX3w^#sSr2wkvp~aLsYsg-$tOpA|EMf1pu%me5RnMC$c;qLnv;}u`_Et zk_lH#tP0PRITwh>YS>RJSw9JZs5CDn?#Y(xtbYsyR{Qu-+yfPO?Cf$YvoSAYA*#Kj zf#4GiIyZSP%9Hx@KDgRT?3bo{^BT$>~x*6Z>fenn42 zpdOzfG;#9^Q6{PxJeQp)^9v>wsI~H%^zHCesN^ep#2&3X_rt`mwv}N?1zZNw>mA1~ z%R*%WzYf1Pk=fA9=TB_(>{#rp_+N^C*_q_e$&NUkjdRBeH@(K$F40gL7A7xWHXkhe z#ZI}b9Zg?E`29$$^6fwB8X`7%GS?7OoHorqy~AE+y{92nX=ZL6fiPbOz{v~i+hRlr zJwdn&|D{AR3rzx~kM3b!=?xPy2pSAgIcHZYi~9BMKW9|#%}#xmj&!N~Keley3%?)Au)ml(eN+HY#MBheOXLSRZ(UoJK7oP;C8kV7FyQw7 ze&fh?eJwZ7#LV>IH#;!3SqH#XS1ahg%eb;a^zi5-r=w`AvUSd4eV;hlLaO_-OxEHH z9tnwNhCtldL?5m0sWbpN0TR>?O#u2M73dvKf0hPRU07xV?OvI{A*-wE44FfO^0q2c zm7m|&zWpBptpBs_%YU6I|KAUj|Nk;qR|s+{bh~v5jEDH?Ef$eO01!`X3mClVAOP3~ z{A23@%%Q!AavArg%Wyhu6Ft2w6Kz`%koX^rPpsNF9RdQu|6|CvW5vYGOm|rKADFMB zlA5Y{bL*);Xfn~tZ~r)dby^U#KQ3?ZtkzxkYK8Mf z!U0!7zOgLR9MC2x1ZDlmwuKD*Pl=h55U%PEAT#1#<{&iEA0bepp%;_fT(XL?y-*mk zxVXM|TRTVkXo`FxR22nr!z~E$W&tup;Z8m-?z(>h;*(i?8J}YU@y2GxXT`~yB@?Ez z5rda~Tin^B$H z#Y`KfO`X42dW>z>xgQ+uk&P+qMuZJD3_ebtP^VVe@AYXzYscOD34zF+tiCbHS%rx0 zbj|T5^ErQEO@TYW%j@R>He_)cyFQgH>jUlKG?GY)g;b@Nt46n{m8Pq?j~vEX9+4%g zdRw36=<`Awjf1vLll_(`$EhR8-#@;nSaCYrOk=mu-*?=pU7IF#w}ML)Mt4MfeQwh_ zk7%`{J+jY)iu%cK9Bb`x6IYX)?GK)u-EQ?C3K{i-e<)~Ox26|S?8)S>N;CI~ZxKxe z?dC5wU#sE{v`-U?=V8$Tr)%qgf6E>k=Rp8m@lQ>q(v_I=);AAP`t#}~+)CmLQL#VL zI*m_wR_?t&*s&eMTS%?+7L4?X7!J&g?bfrGmd-HCB`R(z{~qha0O@ojk%ks~PoA%_ z(~-evH$F#-q1O!t4G`!yDJls z8pplz88yNhP3=wNy6NyL{;_!f+x|XLx3g_5tWQt=p_$FMa3rE$RcZh4yPi*jq{KQH z1Y+&MNC}%a#`_$K=3SEiLA#Q@PH6^Jc1 z)q7aE&NB&G$6bYSha1)Gyg05iVy<6N2y?Q3;~1x{rxB6f4F7F+wxnYzmnyRPf;{^^ zZm_Z}<9RTR*Q095v32=ew9}5PhoA;@#3RU{{0*I=qeKW!(mVP*c7-d4eCdmRm{OGw$HWa%**9 z*Z0)Vy5R5k`dKDbVZydO#o8nSC1}s&iO?ARaRcj8;R)O0t*C2?TuX1)5^(=bhOVL&zpox0(&Ed0`Tta=G;Ms`iFD&EJPne1W0|r?{Tz9Vk3B*qa%f7 z=W$3vaiR>)85~QjOS0F0$T}&sU#n&b;tSNV#vjcEzUMmKTW|CIg!0W~_i}NeZTgCWo~#L(Wl!(k%kEuE#v7l9 zF5eWv?6&desLa7*9$aSLqfr>Um`v4z*sV*04?=i{+RC2y6II$Ad|nV~u2j+P*Wu_m z@p9V%cc&$O#Cg8^0rqoTSyp28eZc(6`=q8&zy9*+`=N_hMIYh99*>=F-v-T@lp2^? z4#zfGNG`81eD$isY1J{XTbCgR>?atox_XT&b!$k8o1vmJCL_7{S~MZn@o*O zz;1Aj0jNBCv%&}{c5*${`}SCX3Qh$_20MwVv?F~l<(R*d5vvNFzP~X7aZ*}#1H0m7JU#G%fo5*fBM$>_Z1ikkK(l=`S$kE#u zFSdGBQ=bN|ZE%i9V7M}z@W=1vyu90pa;gM|eEu#ps~y}SN4X54fle{i&I3G8E6t&s zngDM}>pyLamwzL%MO$FTjP)LdVxcqL`-!p|%7{+y>=N+f?(GeSkQxvf z{)Qg=IgE~kxB2nbtK`J#5ot+p@4%S*cJ^ySxL#wqicT9rI*p{g&MCy6i4ea7qNif}2Pg~BJQ z^kwcw!fi&`7Ew;NEUUPkOBTdOX|=AIK}Z66ufS z5mEy527?eC)(Jh(6Zmb0ag9x?*G6+iIAQ&^H1r(i`%SMgHM^B@+k36bSUM|@{rpjj z_V+jk!z!Mf21SJFA zZ!?^sldfv9Og@D>VS>HE2=1nI-nZM1+Rv*VQSjY>T27rVOT8h6LNu*X7o2s`Iguxm zJx=huZh*BxN;BZ!a;Kkb zYsXTh4M)%}F*XdyQqSLs&g!e6i$fNjGvLrr^~)8FUn4<+)s9#ms_!Rw8eb9p3Zl9$ zfv&-GEL>=Jmh&->aVb-l)<*2r&x<*t#v-B92rH=yYpTek z%eK0s#St83l(W%&f}JG4rOGMna-p?PHqd0A^4*Q0Rwi$*b9Gmbncu35J3%dUHNFEU ztU?;Ov(JATYDtRwt=W%^X4w3aELX$5 zaN|fcK*yQ;%G2Xg(}Sy2)sxwC-iiuKf2pdWu-AU|=1(SPL*QLePqB0Y97(uPku6&w zIhf$=b(uqV!m&0YPe*AL4@V*j-QHv*QR&OvPL6#U>}3|1Snrr#{#egFGh=mEL?~jM z!OZb~*4{}D_JHCy3&rK^e6*I=ebO&2QZO2>5m>n^sjk%Xab>7G)^JXqbjQEG!Ih7G zihq;myIla5IkS|2dcQ3sIU$#xKM=^JJoLFIXY9%r^|c=kW3C_Dk%xslgoYK}WZR>V zF8!K{@Dflh^{6(P1yHq(xQLky;l1#EI2***?|i&k#7cg1>-NR;Z1)W-U_D8h4d8P0 zQjHY-mL2Qv&_m2HK_wb>38H|Hr=7* zpg`v;im@>TZ3w42UFAo{SUHUZ(q_>PvIe741El@5>x5IUm*6Y23BW0yADehUnWUUu zXC0-XnL~dPJR+v3&iJ0<&ec+CD^cO_>s(*>SQ2EpHi|YhxcM{o*Q{07B&MUb12ur3 zpi=`t` z**HDZkw?O|T+StYLqTt)j$t-ULE3>P^2bk;;qv*(W|7+OjJEje`mt#H%Tpk`_b3#K z70ULkkM4Rjpi_W~9TB~lE*in=XXk?Uy5PC~nIr##Xulba_ zj}am*@L&)qoGkrlB&vl=cH9~N8>G`lSUivcsY5y~5=4wdPtXFtsn<;OxmRm6p)lG$ z8ojL?TD9dPP0q6<6KErKy4-Lvt#wcOp`JB$e2bCiDHd?K=4mE>7hcurk&ILp&1E?9 zNH>3y?)QnT2963ZREZapJySbbqH^G;hdn$8Rh!sGwrQU5^hWji=r& zOozH;iYE@YeyKy$L-Q7?6mO;I1b56gPHC&$Xtk=;VL?hSZ9*)N#+wUd70gjfhb$7v zm?o-0s-pxQQqnRTrQg44-02xt+@cSvu)REMSp4+EQ>aBWGT%f;W_vP;Pl(PSxXjv^ ztI@{rK19B9g|V>jn~;$)hr!7B4loxJs%E$Q)qIOFYqu|@B=c*N-WQo{dNL(h2DTz( zMBbzS42YqfGKPVG(tRZfjVrhjvy&MmXDSCETu|{o_p|@NKuB$}<<#z&xf#ccQX=P9 zS6in$CSqwW`0XHyDm7o{-DBOmpC6A(r)3{KtKN$dRs8tbA`?~iaqsOwTNF54VP$m8 zC)`F`AWajiR|EeVu$)N=G=7D}%KPfqF{U-52)?}P2#h)Zok~pgm);|4`TZC4Raz4E z_rs!AQO;Kx+GwvDP>J^UT~^HATnpq)Rk`2WS3oUnE$E5nUs%?x!L zHzd=Bgw1W&XVr&|LIeJ2vV2J+hlEL8+%t3exn(%Mc!7Z?EA>%@FmDpq>^8LKI!FY| zMpq@r)qM9i+h6(R(P#Ylxh4;yM>rdm>vyuM;JGAryBAmu{Z)I_~e|<+py?EvZQK?hM0HTMLIK^hEqIh#Yjddwz(A=DE*7w%iqOKUv!1$l|?l zYv5ub9!fyKsn@JPfQZv*ilX%w_HXW;rV|Kvg~eC50| z?&s4n6Y6&`N>+PsS#rJRYW7~HD)IV5+N+2WMU|@ZpO4w>MtjjZZGkjlnKyin*2I(7 z1N3TkLdt=8mH~N+qZMVSI#s87So|{QF$M8@8a3HdA4@zXH5#X=C5 zfGd84T7~gfxXs2!Q({vC69}q~Eg&@gpy)MMZP?e>_vIbI1`=5&x&MKQeqCSxGB&>( z|B{ZaL`84+!UJzbe*S}Jyw^Uf2Yt0$JvZ39U71L2v7x~BC$1hY>A+Wydh3Yj2wI!R zI1$r%2fCD3ijL=!oK{U#cVgQLPBOx2D!UZl7X&s83UU|^dX_zP3rQ_)BJYD#!DBBC zw#mP+RK!R6fCW{J6eJAF?#>cdXuiQNjtx^Q$;}t!21%8!=S z!09ajCp(YQ5JnaIH^;_uGyb+@Bi8cf>fDb<*QWmZKXIkcN^}@Kj_rIZqLI-j&Y*&k z>1$n6aeh7hcS3us?B)Y^M!$F3H8%0F6^_!3MxIjhX&9zNR?ayZW8+DOSK*7gWY$FL+XSKr&+TZ70rHgbKjoS&KF_6l#ToE?KKv>sFiujew z+f5@3TU$B9tM4f-aOf6 zhLzjB5Prpkw35E@>n6h7L@N5eKtX4W4^->4q1WYWc>#bMA!f)|*^O^m#5D9$u;CF; z^XqaW1zCm+0h#j^*q^!d=qDj=Iq{CfQRGvnIY?!>wuUF_hP77_u9RUxZ};c!5@pQt z+d>RtX(DmDmDE5rBb4&L&PTiQ`(r2@o7m2$)f*eRQaHNb@W@o&+wf92+gL;6@>v*d zvT}d&UU3<#yZD47 z+S7wkaq8JREMrMJ>+mP2-88*gU&?WZ?TwN1t^?E?aYh$xI2z`k=2k(>O43Hd_~_q z5yi1>a`fgnV;ZY`C$shQ4XfB%Pcf&DqdH-%b@AOC;R4mllMM;y7<#H$KKk-qq?t!6 zF3fCjt@4HKDXrSbUT*{YDlIi@q%S)=UFU%cQKz2R_32$MiZ379o0%5Js6`cw@1m~` z_@rV;6jQR1)s zd-<75M4A5-cf(?`5ZNUqmo?y~*~45-hy^0A0-h}VSp-2Zc=gt4NEQ}xM{i9Uy_*s? z`vF1N(R4jkhgT|5p?;+GT?D@V&0?Q<+pIiSM0s4W&?0}WleGUzJhHwWCth`2+ESeR z^Tye&L0RbRW(hRq zhHB$oAs*2HyK((oTo1;(l-DHZ^65h{0`n$fK47a~b!@ZFrF87bC2Prh1t zE@888Kd%qs{Ln|4%*l{+L@GvOcYrCcNWMGuqi1e2#}I|9jA}s9GfR6_ezPU3TPjNP zt1q1Y=H>`qTAr^@s-pKvQ$Y0R_+TeAX8N5gVQ19Yx!Ouf2dMkPd}6uAFr!ft)CT;^kNLclT@Mi zm&f=Jg!eldG8cm-JVAmUF@W)XkTw<*F(Ns|iD`lu2*Pubf0_DUaLM8GhAMq) U$5Z~<3&bHStteF~X%O(g09YJ$8vp click on the link to launch your +browser, if this does not work then copy the URL and paste into your +browser. + +You should now see: + + .. image:: login-images/01-sign-in.png + :align: center + :scale: 75 % + +Choose the user you want to backup photos for. Or pick 'Use Another Account' +if they are not shown. + +You will be presented with a warning because you have made your own client ID +which is not verified by Google. This is expected for this application (see +`Tokens`). + + .. image:: login-images/02-verify.png + :align: center + :scale: 75 % + +Click 'Advanced' and then 'go to gphotos-sync (unsafe)'. + + .. image:: login-images/03-verify2.png + :align: center + :scale: 75 % + +The next screen shows you the permissions you may grant to this application. +Tick all the boxes and click 'Continue'. The command line gphotos-sync will +then continue to run and start to backup the user's library. + + .. image:: login-images/04-access.png + :align: center + :scale: 75 % + diff --git a/docs/tutorials/oauth-images/0.png b/docs/tutorials/oauth-images/0.png new file mode 100644 index 0000000000000000000000000000000000000000..31150c8d67860db1b5fd25e206d26c15f050ccc2 GIT binary patch literal 6203 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 qz5&QO`?y-VTROOUgWX;2K-O+HZ>*2x9 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