From 0f343eb5180024a36ca6685ce42eac3d83bc3228 Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Sat, 7 Dec 2024 15:51:15 +0900 Subject: [PATCH 01/29] docs: Fix small typo from LIcense to License --- .../pyproject-toml-python-package-metadata.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-structure-code/pyproject-toml-python-package-metadata.md b/package-structure-code/pyproject-toml-python-package-metadata.md index 66509f2a..99ead3d8 100644 --- a/package-structure-code/pyproject-toml-python-package-metadata.md +++ b/package-structure-code/pyproject-toml-python-package-metadata.md @@ -156,7 +156,7 @@ what dependencies your package requires. - **Description:** this is a short one-line description of your package. - **Readme:** A link to your README.md file is used for the long long-description. This information will be published on your packages PyPI landing page. - **Requires-python** (used by pip): this is a field that is used by pip. Here you tell the installer whether you are using Python 2.x or 3.x. Most projects will be using 3.x. -- **License:** the license you are using +- **License:** the License you are using - **Authors:** these are the original authors of the package. Sometimes the authors are different from the maintainers. Other times they might be the same. - **Maintainers:** you can choose to populate this or not. You can populate this using a list with a sub element for each author or maintainer name, email @@ -175,7 +175,7 @@ what dependencies your package requires. - Development Status - Intended Audience - Topic - - LIcense + - License - Programming language ### Advanced options in the pyproject.toml file From 36dd0cd753e29827c1993eced7fc15f2ba60e1e7 Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Sat, 7 Dec 2024 15:51:50 +0900 Subject: [PATCH 02/29] Update pyproject-toml-python-package-metadata.md --- .../pyproject-toml-python-package-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-structure-code/pyproject-toml-python-package-metadata.md b/package-structure-code/pyproject-toml-python-package-metadata.md index 99ead3d8..1924aa91 100644 --- a/package-structure-code/pyproject-toml-python-package-metadata.md +++ b/package-structure-code/pyproject-toml-python-package-metadata.md @@ -156,7 +156,7 @@ what dependencies your package requires. - **Description:** this is a short one-line description of your package. - **Readme:** A link to your README.md file is used for the long long-description. This information will be published on your packages PyPI landing page. - **Requires-python** (used by pip): this is a field that is used by pip. Here you tell the installer whether you are using Python 2.x or 3.x. Most projects will be using 3.x. -- **License:** the License you are using +- **License:** the license you are using - **Authors:** these are the original authors of the package. Sometimes the authors are different from the maintainers. Other times they might be the same. - **Maintainers:** you can choose to populate this or not. You can populate this using a list with a sub element for each author or maintainer name, email From 6d4f97a30d0bc4f11fbaf5d1fb24429adfb792ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:46:29 +0000 Subject: [PATCH 03/29] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/errata-ai/vale: v3.8.0 β†’ v3.9.1](https://github.com/errata-ai/vale/compare/v3.8.0...v3.9.1) - [github.com/rbubley/mirrors-prettier: v3.3.3 β†’ v3.4.1](https://github.com/rbubley/mirrors-prettier/compare/v3.3.3...v3.4.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9c9d42df..1b22284e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,12 +36,12 @@ repos: )$ - repo: https://github.com/errata-ai/vale - rev: v3.8.0 + rev: v3.9.1 hooks: - id: vale - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.3.3 + rev: v3.4.1 hooks: - id: prettier types_or: [yaml, html, css, scss, javascript, json, toml] From 3cea4b209fa25949ff158aeaf4b63b8191d8a6f2 Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Thu, 5 Dec 2024 10:51:15 -0700 Subject: [PATCH 04/29] fix: add cross links to the packaging guide --- package-structure-code/intro.md | 36 ++++-- ...-package-distribution-files-sdist-wheel.md | 4 +- .../python-package-structure.md | 118 ++++++++++-------- tutorials/intro.md | 2 + 4 files changed, 102 insertions(+), 58 deletions(-) diff --git a/package-structure-code/intro.md b/package-structure-code/intro.md index 94b80e72..99eb6d8f 100644 --- a/package-structure-code/intro.md +++ b/package-structure-code/intro.md @@ -1,13 +1,35 @@ -# Python package structure information +# Python Package Structure -This section provides guidance on your Python package's structure, code formats -and style. It also reviews the various packaging tools that you can use to -support building and publishing your package. +This section provides guidance on your Python package's structure, code format +, and style. It also reviews the various [packaging tools](python-package-build-tools) you can use to +[build](python-package-distribution-files-sdist-wheel.) and [publish](publish-python-package-pypi-conda) your Python package. + +If you want end-to-end tutorials, check out our tutorial series here. That starts by introducing [what a Python package is](what-is-a-package). If you are confused by Python packaging, you are not alone! The good news is -there are some great modern packaging tools that ensure that you're following -best practices. Here, we review tool features and suggest tools that might be -best fitted for your workflow. +that some great modern packaging tools ensure you follow +best practices. Here, we review tool features and suggest tools you can use +for your Python packaging workflow. + +:::{button-link} /tutorials/intro +:color: primary +:class: sd-rounded-pill float-left + +Checkout our beginning-to-end create a Python package tutorials + +::: + + +:::{admonition} How this content is developed +All of the content in this guide has been vetted by community members, including maintainers and developers of the core packaging tools. +::: + + + +:::{button} +:class: bdg-success +Check out our Python package +::: :::::{grid} 1 1 2 2 :class-container: text-center diff --git a/package-structure-code/python-package-distribution-files-sdist-wheel.md b/package-structure-code/python-package-distribution-files-sdist-wheel.md index 87713be5..e35805e1 100644 --- a/package-structure-code/python-package-distribution-files-sdist-wheel.md +++ b/package-structure-code/python-package-distribution-files-sdist-wheel.md @@ -152,7 +152,7 @@ Also note that we are not discussing conda build workflows in this section. ``` (python-source-distribution)= -## Source Distribution (sdist) +## What is a source distribution (sdist) **Source files** are the unbuilt files needed to build your package. These are the "raw / as-is" files that you store on GitHub or whatever @@ -226,7 +226,7 @@ the sdist may also contain a file that stores the version. ``` (python-wheel)= -## Wheel (.whl files): +## What is a Python package wheel (.whl files): A wheel file is a ZIP-format archive whose filename follows a specific format (below) and has the extension `.whl`. The `.whl` archive contains a specific diff --git a/package-structure-code/python-package-structure.md b/package-structure-code/python-package-structure.md index a9b6a0ff..eecf206b 100644 --- a/package-structure-code/python-package-structure.md +++ b/package-structure-code/python-package-structure.md @@ -2,25 +2,41 @@ There are two different layouts that you will commonly see within the Python packaging ecosystem: -[src and flat layouts.](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/) +src and flat layouts. Both layouts have advantages for different groups of maintainers. We strongly suggest, but do not require, that you use the **src/** layout (discussed below) for creating your Python package. This layout is also recommended in the -[PyPA packaging guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/). +[PyPA packaging guide tutorial](https://packaging.python.org/en/latest/tutorials/packaging-projects/). ```{admonition} pyOpenSci will never require a specific package structure for peer review :class: important -We understand that it would be tremendous effort for existing +We understand that it would take significant effort for existing maintainers to move to a new layout. The overview on this page presents recommendations that we think are best for someone getting started with Python packaging or someone who's package has a simple build and might be open to moving to a more fail-proof approach. + +Other resources you can check out: +* [PyPA's overview of src vs flat layouts](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/) ``` -An example of the **src/package** layout structure can be seen below. +You can use tools like Hatch to quickly create a modern Python package structure. Check out our quickstart tutorial: + + +:::{button-link} /tutorials/installable-code.html#step-1-set-up-the-package-directory-structure +:color: success +:class: sd-rounded-pill float-left + +Want to learn how to create the structure to build your package? Click here. + +::: + +## What is the Python package source layout? + +An example of the **src/package** layout structure is below. ``` myPackageRepoName @@ -44,8 +60,8 @@ myPackageRepoName Note the location of the following directories in the example above: -- **docs/:** discussed in our docs chapter, this directory contains your user-facing documentation website. In a **src/** layout docs/ are normally included at the same directory level of the **src/** folder. -- **tests/** this directory contains the tests for your project code. In a **src/** layout tests are normally included at the same directory level of the **src/** folder. +- **docs/:** discussed in our docs chapter, this directory contains your user-facing documentation website. In a **src/** layout docs/ are normally included at the same directory level as the **src/** folder. +- **tests/** This directory contains the tests for your project code. In a **src/** layout, tests are normally included at the same directory level as the **src/** folder. - **src/package/**: this is the directory that contains the code for your Python project. "Package" is normally your project's name. Also in the above example, notice that all of the core documentation files that @@ -61,14 +77,12 @@ include: ```{button-link} https://www.pyopensci.org/python-package-guide/documentation -:color: primary +:color: info :class: sd-rounded-pill Click here to read about our packaging documentation requirements. ``` -While we recommend the **src/** layout we also review the **flat** layout here. Both are used in the Python ecosystem. - ```{admonition} Example scientific packages that use **src/package** layout * [Sourmash](https://github.com/sourmash-bio/sourmash) @@ -81,14 +95,13 @@ While we recommend the **src/** layout we also review the **flat** layout here. ## The src/ layout and testing -The benefit of using the **src/package** layout, particularly if you -are creating a new package, is that it ensures tests are run against the +The benefit of using the **src/package** layout is that it ensures tests are run against the installed version of your package rather than the files in your package working directory. If you run your tests on your files rather than the -installed version, you may be missing issues that users encounter when +installed version of your package, you may be missing issues that users encounter when your package is installed. -If `tests/` are outside of the **src/package** directory, they aren't included in the package wheel. This makes your package size slightly smaller which then places places a smaller storage burden on PyPI which has over 400,000 packages to support. +If `tests/` are outside the **src/package** directory, they aren't included in the package [wheel](python-wheel). This makes your package size slightly smaller, which places a smaller storage burden on PyPI, which has over 400,000 packages to support. - [Read more about reasons to use the **src/package** layout](https://hynek.me/articles/testing-packaging/) @@ -98,14 +111,14 @@ By default, Python adds a module in your current working directory to the front This means that if you run your tests in your package's working directory, using a flat layout, `/package/module.py`, Python will discover `package/module.py` file before it discovers the installed package. -However, if your package lives in a src/ directory structure **src/package** then it won't be, by default, added to the Python path. This means that when you import your package, Python will be forced to search the active environment (which has your package installed). +However, if your package lives in a src/ directory structure **src/package**, then it won't be added to the Python path by default. This means that when you import your package, Python will be forced to search the active environment (which has your package installed). -Note: Python versions 3.11 and above have a path setting that can be adjusted to ensure the priority is to use installed packages first (e.g. `PYTHONSAFEPATH`). +Note: Python versions 3.11 and above have a path setting that can be adjusted to ensure the priority is to use installed packages first (e.g., `PYTHONSAFEPATH`). ``` -### Sometimes tests are needed in a distribution +### Don't include tests in your package wheel -We do not recommend including tests as part of your package wheel by default. However, not including tests in your package distribution will make it harder for people other than yourself to test whether your package is functioning correctly on their system. If you have a small test suite (Python files + data), and think your users may want to run tests locally on their systems, you can include tests by moving the `tests/` directory into the **src/package** directory (see example below). +Writing [tests](/tests/index.html) for your package is important; however, we do not recommend including tests as part of your [package wheel](python-wheel) by default. However, not including tests in your package distribution will make it harder for people other than yourself to test whether your package runs properly on their system. If you have a small test suite (Python files + data), and think your users may want to run tests locally on their systems, you can include tests by moving the `tests/` directory into the **src/package** directory (see example below). ```bash src/ @@ -114,69 +127,76 @@ src/ docs/ ``` -Including the **tests/** directory in your **src/package** directory ensures that tests will be included in your package's wheel. +Including the **tests/** directory in your **src/package** directory ensures that tests will be included in your package's [wheel](python-wheel). Be sure to read the [pytest documentation for more about including tests in your package distribution](https://docs.pytest.org/en/7.2.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules). ```{admonition} Challenges with including tests and data in a package wheel :class: tip -Tests, especially when accompanied by test data can create a few small challenges including: +Tests, especially when accompanied by test data, can create a few small challenges, including: -- Take up space in your distribution which will build up over time as storage space on PyPI -- Large file sizes can also slow down package install. +- Take up space in your distribution, which will build up over time as storage space on PyPI +- Large file sizes can also slow down package installation. -However, in some cases, particularly in the scientific Python ecosystems you may need to include tests. +However, in some cases, particularly in the scientific Python ecosystem, you may need to include tests. ``` ### **Don't include test suite datasets in your package** -If you do include your tests in your package distribution, we strongly +If you include your tests in your package distribution, we strongly discourage you from including data in your test suite directory. Rather, host your test data in a repository such as Figshare or Zenodo. Use a tool such as [Pooch](https://www.fatiando.org/pooch/latest/) to access the data when you (or a user) runs tests. -Check out the testing section of our guide for more information about tests. +For more information about Python package tests, see the [tests section of our guide](/tests/index.html). - The **src/package** layout is semantically more clear. Code is always found in the **src/package** directory, `tests/` and `docs/`are in the root directory. ```{important} -If your package tests require data, we suggest that you do NOT include that -data within your package structure. We will discuss this in more detail in a -tutorial. Include data in your package structure increases the size of your +If your package tests require data, do NOT include that +data within your package structure. Including data in your package structure increases the size of your distribution files. This places a maintenance toll on repositories like PyPI and -Anaconda.org that have to deal with thousands of package uploads. +Anaconda.org has to deal with thousands of package uploads. ``` -## About the flat Python package layout -Currently most scientific packages use the **flat-layout** given: +:::{button-link} /tutorials/installable-code.html#step-1-set-up-the-package-directory-structure +:color: success +:class: sd-rounded-pill float-left -- It's the most commonly found layout with the scientific Python ecosystem and - people tend to look to other packages / maintainers that they respect for examples - of how to build Python packages. -- Many Python tools depend upon tools in other language and / or complex builds - with compilation steps. Many developers thus appreciate / are used to features - of the flat layout. -While we present this layout here in our guide, we suggest that those just -getting started with python packaging start with the src/package layout -discussed above. Numerous packages in the ecosystem [have had to move to a -src/ layout](https://github.com/scikit-build/cmake-python-distributions/pull/145) +Click here for a quickstart tutorial on creating your Python package. + +::: + +(flat-layout)= +## What is the flat Python package layout? + +Many scientific packages use the **flat-layout** given: + +- This layout is used by many core scientific Python packages such as NumPy, SciPy, and Matplotlib. +- Many Python tools depend upon tools in other languages and/or complex builds + with compilation steps. Many maintainers prefer features + of the flat layout for more complex builds. + +While we suggest that you use the src/ layout discussed above, it's important to also +understand the flat layout, especially if you plan to contribute to a package that uses this layout + ```{admonition} Why most scientific Python packages do not use source :class: tip -In most cases the advantages of using the **src/package** layout for -larger scientific packages that already use flat approach are not worth it. -Moving from a flat layout to a **src/package** layout would come at a significant cost to -maintainers. +In most cases, moving to the **src/package** layout for +larger scientific packages that already use a flat layout would consume significant time. However, the advantages of using the **src/package** layout for a beginner are significant. -As such, we recommend that if you are getting started with creating a package, -that you consider using a **src/package** layout. +As such, we recommend that you use the **src/package** layout if you are creating a new package. + +Numerous packages in the ecosystem [have had to move to a +src/ layout](https://github.com/scikit-build/cmake-python-distributions/pull/145) ``` ## What does the flat layout structure look like? @@ -234,7 +254,7 @@ There are some benefits to the scientific community in using the flat layout. It would be a significant maintenance cost and burden to move all of these packages to a different layout. The potential benefits of the source layout -for these tools is not worth the maintenance investment. +for these tools are not worth the maintenance investment. ``` diff --git a/tutorials/intro.md b/tutorials/intro.md index cdf34e0b..76238c00 100644 --- a/tutorials/intro.md +++ b/tutorials/intro.md @@ -1,3 +1,4 @@ +(packaging-101)= # Python packaging 101 _A start to finish beginner-friendly tutorial_ @@ -75,6 +76,7 @@ After reading this lesson you will: ::: +(what-is-a-package)= ## What is a Python package? At a high level, you can think about a Python package as a toolbox From 921c200831ae4c455e26217cb1607eb430b5816e Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Thu, 5 Dec 2024 12:46:39 -0700 Subject: [PATCH 05/29] fix: edits from @hugovk Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- package-structure-code/intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-structure-code/intro.md b/package-structure-code/intro.md index 99eb6d8f..6186c42f 100644 --- a/package-structure-code/intro.md +++ b/package-structure-code/intro.md @@ -2,7 +2,7 @@ This section provides guidance on your Python package's structure, code format , and style. It also reviews the various [packaging tools](python-package-build-tools) you can use to -[build](python-package-distribution-files-sdist-wheel.) and [publish](publish-python-package-pypi-conda) your Python package. +[build](python-package-distribution-files-sdist-wheel) and [publish](publish-python-package-pypi-conda) your Python package. If you want end-to-end tutorials, check out our tutorial series here. That starts by introducing [what a Python package is](what-is-a-package). From 57d9cdb522c3fa7477013317b287d920b1510ab3 Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Thu, 5 Dec 2024 12:54:07 -0700 Subject: [PATCH 06/29] fix: broken sphinx things --- package-structure-code/intro.md | 8 +------- package-structure-code/python-package-structure.md | 4 ++-- tests/index.md | 5 +++-- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/package-structure-code/intro.md b/package-structure-code/intro.md index 6186c42f..d7141c30 100644 --- a/package-structure-code/intro.md +++ b/package-structure-code/intro.md @@ -12,7 +12,7 @@ best practices. Here, we review tool features and suggest tools you can use for your Python packaging workflow. :::{button-link} /tutorials/intro -:color: primary +:color: success :class: sd-rounded-pill float-left Checkout our beginning-to-end create a Python package tutorials @@ -25,12 +25,6 @@ All of the content in this guide has been vetted by community members, including ::: - -:::{button} -:class: bdg-success -Check out our Python package -::: - :::::{grid} 1 1 2 2 :class-container: text-center :gutter: 3 diff --git a/package-structure-code/python-package-structure.md b/package-structure-code/python-package-structure.md index eecf206b..3bf23a6a 100644 --- a/package-structure-code/python-package-structure.md +++ b/package-structure-code/python-package-structure.md @@ -118,7 +118,7 @@ Note: Python versions 3.11 and above have a path setting that can be adjusted to ### Don't include tests in your package wheel -Writing [tests](/tests/index.html) for your package is important; however, we do not recommend including tests as part of your [package wheel](python-wheel) by default. However, not including tests in your package distribution will make it harder for people other than yourself to test whether your package runs properly on their system. If you have a small test suite (Python files + data), and think your users may want to run tests locally on their systems, you can include tests by moving the `tests/` directory into the **src/package** directory (see example below). +Writing [tests](tests-intro) for your package is important; however, we do not recommend including tests as part of your [package wheel](python-wheel) by default. However, not including tests in your package distribution will make it harder for people other than yourself to test whether your package runs properly on their system. If you have a small test suite (Python files + data), and think your users may want to run tests locally on their systems, you can include tests by moving the `tests/` directory into the **src/package** directory (see example below). ```bash src/ @@ -150,7 +150,7 @@ host your test data in a repository such as Figshare or Zenodo. Use a tool such as [Pooch](https://www.fatiando.org/pooch/latest/) to access the data when you (or a user) runs tests. -For more information about Python package tests, see the [tests section of our guide](/tests/index.html). +For more information about Python package tests, see the [tests section of our guide](tests-intro). - The **src/package** layout is semantically more clear. Code is always found in the **src/package** directory, `tests/` and `docs/`are in the root directory. diff --git a/tests/index.md b/tests/index.md index 05ff0eee..73de20f4 100644 --- a/tests/index.md +++ b/tests/index.md @@ -1,11 +1,12 @@ +(tests-intro)= # Tests and data for your Python package Tests are an important part of your Python package because they provide a set of checks that ensure that your package is functioning how you expect it to. -In this section you will learn more about the importance of writing -tests for your Python package and how you can setup infrastructure +In this section, you will learn more about the importance of writing +tests for your Python package and how you can set up infrastructure to run your tests both locally and on GitHub. From a904cdf0f08f5f6ad62bceb4579e3ccaf8aae52b Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Wed, 11 Dec 2024 13:52:48 -0700 Subject: [PATCH 07/29] fix: edits from @hugovk Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- package-structure-code/python-package-structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-structure-code/python-package-structure.md b/package-structure-code/python-package-structure.md index 3bf23a6a..c69d810f 100644 --- a/package-structure-code/python-package-structure.md +++ b/package-structure-code/python-package-structure.md @@ -196,7 +196,7 @@ However, the advantages of using the **src/package** layout for a beginner are As such, we recommend that you use the **src/package** layout if you are creating a new package. Numerous packages in the ecosystem [have had to move to a -src/ layout](https://github.com/scikit-build/cmake-python-distributions/pull/145) +**src/package** layout](https://github.com/scikit-build/cmake-python-distributions/pull/145). ``` ## What does the flat layout structure look like? From 408d777617ab9d21ce4ad958636a90753a2c6ac9 Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Wed, 11 Dec 2024 13:52:58 -0700 Subject: [PATCH 08/29] fix: edits from @hugovk Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- package-structure-code/python-package-structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-structure-code/python-package-structure.md b/package-structure-code/python-package-structure.md index c69d810f..62b6ee1c 100644 --- a/package-structure-code/python-package-structure.md +++ b/package-structure-code/python-package-structure.md @@ -263,7 +263,7 @@ Not sure where to put this now ... most new users won't have multiple packages. ```{admonition} Multiple packages in a src/ folder :class: tip -In some more advanced cases, you may have more than one package in your src/ directory. See [black's GitHub repo](https://github.com/psf/black/tree/main/src) for an example of this. However, for most beginners you will likely only have one sub-directory in your **src/** folder. +In some more advanced cases, you may have more than one package in your **src/** directory. See [Black's GitHub repo](https://github.com/psf/black/tree/main/src) for an example of this. However, for most beginners you will likely only have one sub-directory in your **src/** folder. ``` --> From fb7053ff9d05ab926884f75730b36cdfc0b35bc5 Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Wed, 11 Dec 2024 13:53:18 -0700 Subject: [PATCH 10/29] fix: edits from @hugovk Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- package-structure-code/python-package-structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-structure-code/python-package-structure.md b/package-structure-code/python-package-structure.md index 680f0f07..f28d9929 100644 --- a/package-structure-code/python-package-structure.md +++ b/package-structure-code/python-package-structure.md @@ -182,7 +182,7 @@ Many scientific packages use the **flat-layout** given: with compilation steps. Many maintainers prefer features of the flat layout for more complex builds. -While we suggest that you use the src/ layout discussed above, it's important to also +While we suggest that you use the **src/package** layout discussed above, it's important to also understand the flat layout, especially if you plan to contribute to a package that uses this layout From a6d8b7ff2b057802517b0165f46a0547e8270359 Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Wed, 11 Dec 2024 13:53:26 -0700 Subject: [PATCH 11/29] fix: edits from @hugovk Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- package-structure-code/intro.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-structure-code/intro.md b/package-structure-code/intro.md index d7141c30..751e4cea 100644 --- a/package-structure-code/intro.md +++ b/package-structure-code/intro.md @@ -1,7 +1,7 @@ # Python Package Structure -This section provides guidance on your Python package's structure, code format -, and style. It also reviews the various [packaging tools](python-package-build-tools) you can use to +This section provides guidance on your Python package's structure, code format, +and style. It also reviews the various [packaging tools](python-package-build-tools) you can use to [build](python-package-distribution-files-sdist-wheel) and [publish](publish-python-package-pypi-conda) your Python package. If you want end-to-end tutorials, check out our tutorial series here. That starts by introducing [what a Python package is](what-is-a-package). From e97f35ca62de773c5251729bea93daf3abe34826 Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Wed, 11 Dec 2024 13:54:29 -0700 Subject: [PATCH 12/29] Update package-structure-code/python-package-structure.md --- package-structure-code/python-package-structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-structure-code/python-package-structure.md b/package-structure-code/python-package-structure.md index f28d9929..673ef866 100644 --- a/package-structure-code/python-package-structure.md +++ b/package-structure-code/python-package-structure.md @@ -159,7 +159,7 @@ For more information about Python package tests, see the [tests section of our g If your package tests require data, do NOT include that data within your package structure. Including data in your package structure increases the size of your distribution files. This places a maintenance toll on repositories like PyPI and -Anaconda.org has to deal with thousands of package uploads. +Anaconda.org that have to deal with thousands of package uploads. ``` From 65928a8c5a4312ae255345d5f2eb3479a26361a8 Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Wed, 11 Dec 2024 13:54:38 -0700 Subject: [PATCH 13/29] fix: edits from @hugovk Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- package-structure-code/python-package-structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-structure-code/python-package-structure.md b/package-structure-code/python-package-structure.md index 673ef866..69b0e61c 100644 --- a/package-structure-code/python-package-structure.md +++ b/package-structure-code/python-package-structure.md @@ -183,7 +183,7 @@ Many scientific packages use the **flat-layout** given: of the flat layout for more complex builds. While we suggest that you use the **src/package** layout discussed above, it's important to also -understand the flat layout, especially if you plan to contribute to a package that uses this layout +understand the flat layout, especially if you plan to contribute to a package that uses this layout. ```{admonition} Why most scientific Python packages do not use source From 4b45c5df568a1a32bf065b17cf5698a5c144b58b Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Thu, 12 Dec 2024 09:48:10 -0700 Subject: [PATCH 14/29] Fix: Edits from @ucodery Co-authored-by: Jeremiah Paige --- package-structure-code/intro.md | 2 +- .../python-package-distribution-files-sdist-wheel.md | 2 +- package-structure-code/python-package-structure.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-structure-code/intro.md b/package-structure-code/intro.md index 751e4cea..5b8048e8 100644 --- a/package-structure-code/intro.md +++ b/package-structure-code/intro.md @@ -4,7 +4,7 @@ This section provides guidance on your Python package's structure, code format, and style. It also reviews the various [packaging tools](python-package-build-tools) you can use to [build](python-package-distribution-files-sdist-wheel) and [publish](publish-python-package-pypi-conda) your Python package. -If you want end-to-end tutorials, check out our tutorial series here. That starts by introducing [what a Python package is](what-is-a-package). +If you want end-to-end tutorials, check out our tutorial series that starts by introducing [what a Python package is](what-is-a-package). If you are confused by Python packaging, you are not alone! The good news is that some great modern packaging tools ensure you follow diff --git a/package-structure-code/python-package-distribution-files-sdist-wheel.md b/package-structure-code/python-package-distribution-files-sdist-wheel.md index e35805e1..00fbe346 100644 --- a/package-structure-code/python-package-distribution-files-sdist-wheel.md +++ b/package-structure-code/python-package-distribution-files-sdist-wheel.md @@ -226,7 +226,7 @@ the sdist may also contain a file that stores the version. ``` (python-wheel)= -## What is a Python package wheel (.whl files): +## What is a Python wheel (whl): A wheel file is a ZIP-format archive whose filename follows a specific format (below) and has the extension `.whl`. The `.whl` archive contains a specific diff --git a/package-structure-code/python-package-structure.md b/package-structure-code/python-package-structure.md index 69b0e61c..2156d56c 100644 --- a/package-structure-code/python-package-structure.md +++ b/package-structure-code/python-package-structure.md @@ -60,7 +60,7 @@ myPackageRepoName Note the location of the following directories in the example above: -- **docs/:** discussed in our docs chapter, this directory contains your user-facing documentation website. In a **src/** layout docs/ are normally included at the same directory level as the **src/** folder. +- **docs/:** Discussed in our docs chapter, this directory contains your user-facing documentation website. In a **src/** layout docs/ are normally included at the same directory level as the **src/** folder. - **tests/** This directory contains the tests for your project code. In a **src/** layout, tests are normally included at the same directory level as the **src/** folder. - **src/package/**: this is the directory that contains the code for your Python project. "Package" is normally your project's name. @@ -101,7 +101,7 @@ working directory. If you run your tests on your files rather than the installed version of your package, you may be missing issues that users encounter when your package is installed. -If `tests/` are outside the **src/package** directory, they aren't included in the package [wheel](python-wheel). This makes your package size slightly smaller, which places a smaller storage burden on PyPI, which has over 400,000 packages to support. +If `tests/` are outside the **src/package** directory, they aren't included in the package's [wheel](python-wheel). This makes your package size slightly smaller, which places a smaller storage burden on PyPI, and makes them faster to fetch. - [Read more about reasons to use the **src/package** layout](https://hynek.me/articles/testing-packaging/) From 46b4f54247d9c2ce863e9a70a08552bde8633d97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 04:05:24 +0900 Subject: [PATCH 15/29] chore: bump pydata-sphinx-theme from 0.16.0 to 0.16.1 in the pip group (#434) Bumps the pip group with 1 update: [pydata-sphinx-theme](https://github.com/pydata/pydata-sphinx-theme). Updates `pydata-sphinx-theme` from 0.16.0 to 0.16.1 - [Release notes](https://github.com/pydata/pydata-sphinx-theme/releases) - [Changelog](https://github.com/pydata/pydata-sphinx-theme/blob/main/RELEASE.md) - [Commits](https://github.com/pydata/pydata-sphinx-theme/compare/v0.16.0...v0.16.1) --- updated-dependencies: - dependency-name: pydata-sphinx-theme dependency-type: direct:production update-type: version-update:semver-patch dependency-group: pip ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd18aec9..5a524d3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = [ "version" ] dependencies = [ - "pydata-sphinx-theme==0.16.0", + "pydata-sphinx-theme==0.16.1", "myst-nb", "sphinx", "sphinx-autobuild", From 64b7cf6155470184b907ac15716c0140ccfa62c2 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Sun, 14 Jul 2024 09:31:03 -0700 Subject: [PATCH 16/29] Minor Markdown improvement on packaging page Apply some minor improvements to the Markdown syntax in the pages instruction on how to create the `pyproject.toml` file. --- .../pyproject-toml-python-package-metadata.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-structure-code/pyproject-toml-python-package-metadata.md b/package-structure-code/pyproject-toml-python-package-metadata.md index 1924aa91..595ca220 100644 --- a/package-structure-code/pyproject-toml-python-package-metadata.md +++ b/package-structure-code/pyproject-toml-python-package-metadata.md @@ -139,12 +139,12 @@ When you create your `pyproject.toml` file, there are numerous metadata fields t [An overview of all of the project metadata elements can be found here.](https://packaging.python.org/en/latest/specifications/core-metadata/#project-url-multiple-use) -### Required fields for the [project] table +### Required fields for the `[project]` table -As mentioned above, your pyproject.toml file needs to have a **name** and **version** field in order to properly build your package: +As mentioned above, your `pyproject.toml` file needs to have a **`name`** and **`version`** field in order to properly build your package: -- Name: This is the name of your project provided as a string -- Version: This is the version of your project. If you are using a scm tool for versioning (using git tags to determine versions), then the version may be dynamic (more on that below). +- `name`: This is the name of your project provided as a string +- `version`: This is the version of your project. If you are using a SCM tool for versioning (using git tags to determine versions), then the version may be dynamic (more on that below). ### Optional fields to include in the `[project]` table From acab03775cbe6ca833fe6505845fa9d6045564ff Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:18:19 -0700 Subject: [PATCH 17/29] docs: add santisoler as a contributor for code, and review (#435) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 10 ++++++++++ README.md | 7 ++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index ea26a53b..e6367b0e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -808,6 +808,16 @@ "contributions": [ "question" ] + }, + { + "login": "santisoler", + "name": "Santiago Soler", + "avatar_url": "https://avatars.githubusercontent.com/u/11541317?v=4", + "profile": "https://www.santisoler.com", + "contributions": [ + "code", + "review" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index e227bcf1..e5f988ff 100644 --- a/README.md +++ b/README.md @@ -151,30 +151,31 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Revathy Venugopal
Revathy Venugopal

πŸ’» πŸ‘€ πŸ“– Roberto Pastor Muela
Roberto Pastor Muela

πŸ’» πŸ‘€ 🌍 πŸ€” Ryan
Ryan

πŸ’» πŸ‘€ + Santiago Soler
Santiago Soler

πŸ’» πŸ‘€ Simon
Simon

πŸ“– 🎨 Sneha Yadav
Sneha Yadav

πŸ’» πŸ‘€ Stefan van der Walt
Stefan van der Walt

πŸ’» 🎨 πŸ‘€ - Stefanie Molin
Stefanie Molin

πŸ’» πŸ‘€ + Stefanie Molin
Stefanie Molin

πŸ’» πŸ‘€ Stefano Rivera
Stefano Rivera

πŸ‘€ Tetsuo Koyama
Tetsuo Koyama

πŸ’» πŸ‘€ πŸ“– 🌍 πŸ€” Tom Russell
Tom Russell

πŸ’» πŸ‘€ βœ… Trevor James Smith
Trevor James Smith

πŸ’» πŸ‘€ Tyler Bonnell
Tyler Bonnell

πŸ’» πŸ‘€ Vaunty
Vaunty

πŸ’» πŸ‘€ - William F. Broderick
William F. Broderick

βœ… + William F. Broderick
William F. Broderick

βœ… Zack Weinberg
Zack Weinberg

πŸ‘€ h-vetinari
h-vetinari

πŸ’» πŸ‘€ βœ… hpodzorski-USGS
hpodzorski-USGS

πŸ’» πŸ‘€ jaimergp
jaimergp

πŸ’» πŸ‘€ βœ… miguelalizo
miguelalizo

πŸ’» πŸ‘€ πŸ“– nyeshlur
nyeshlur

πŸ’» πŸ‘€ - ruoxi
ruoxi

πŸ’» πŸ‘€ + ruoxi
ruoxi

πŸ’» πŸ‘€ Γ‰ric
Γ‰ric

πŸ’» πŸ‘€ From 69d00aa5407fbe21dec8e78caddcbc4ab5fb59ca Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:00:10 -0700 Subject: [PATCH 18/29] docs: add webknjaz as a contributor for review (#436) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index e6367b0e..8330b86e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -818,6 +818,15 @@ "code", "review" ] + }, + { + "login": "webknjaz", + "name": "πŸ‡ΊπŸ‡¦ Sviatoslav Sydorenko (Бвятослав Π‘ΠΈΠ΄ΠΎΡ€Π΅Π½ΠΊΠΎ)", + "avatar_url": "https://avatars.githubusercontent.com/u/578543?v=4", + "profile": "https://webknjaz.me", + "contributions": [ + "review" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index e5f988ff..52728d2a 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d ruoxi
ruoxi

πŸ’» πŸ‘€ Γ‰ric
Γ‰ric

πŸ’» πŸ‘€ + πŸ‡ΊπŸ‡¦ Sviatoslav Sydorenko (Бвятослав Π‘ΠΈΠ΄ΠΎΡ€Π΅Π½ΠΊΠΎ)
πŸ‡ΊπŸ‡¦ Sviatoslav Sydorenko (Бвятослав Π‘ΠΈΠ΄ΠΎΡ€Π΅Π½ΠΊΠΎ)

πŸ‘€ From b2a8dc9d2fff61f4b7a0670cae6b14957fb84418 Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Mon, 25 Sep 2023 19:21:33 -0600 Subject: [PATCH 19/29] Fix: add initial tests content to guide ENH: fixes from Jonny's review Fix: review edits from Jonny p2 Fix: typos and cleanup Fix: add example to tests ci page --- _static/pyos.css | 2 +- _templates/header.html | 6 + ci-tests-data/ci.md | 66 ++++++++++ ci-tests-data/code-cov.md | 13 ++ ci-tests-data/data.md | 1 + ci-tests-data/index.md | 93 ++++++++++++++ ci-tests-data/run-tests.md | 220 ++++++++++++++++++++++++++++++++++ ci-tests-data/test-types.md | 157 ++++++++++++++++++++++++ ci-tests-data/tests-ci.md | 86 +++++++++++++ ci-tests-data/write-tests.md | 88 ++++++++++++++ conf.py | 4 +- images/code-cov-stravalib.png | Bin 0 -> 568356 bytes images/test-tools.png | Bin 0 -> 50808 bytes 13 files changed, 733 insertions(+), 3 deletions(-) create mode 100644 _templates/header.html create mode 100644 ci-tests-data/ci.md create mode 100644 ci-tests-data/code-cov.md create mode 100644 ci-tests-data/data.md create mode 100644 ci-tests-data/index.md create mode 100644 ci-tests-data/run-tests.md create mode 100644 ci-tests-data/test-types.md create mode 100644 ci-tests-data/tests-ci.md create mode 100644 ci-tests-data/write-tests.md create mode 100644 images/code-cov-stravalib.png create mode 100644 images/test-tools.png diff --git a/_static/pyos.css b/_static/pyos.css index ff33950e..ad7ccaa6 100644 --- a/_static/pyos.css +++ b/_static/pyos.css @@ -413,4 +413,4 @@ td, th { border: 1px solid #ccc; /* Light gray border */ padding: 8px; /* Add some padding for better readability */ -} +} \ No newline at end of file diff --git a/_templates/header.html b/_templates/header.html new file mode 100644 index 00000000..d261d884 --- /dev/null +++ b/_templates/header.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/ci-tests-data/ci.md b/ci-tests-data/ci.md new file mode 100644 index 00000000..1cff2f87 --- /dev/null +++ b/ci-tests-data/ci.md @@ -0,0 +1,66 @@ +# What is continuous integration? + +When you’re ready to publish your code online, you can setup Continuous Integration (CI). CI is a platform that allows you to specify and run jobs or workflows that you define. +These workflows include: + +- Running your test suite +- Running code checkers / linters / spellcheck +- Building your documentation +- Deploying your documentation to GitHub pages + +CI also allows you to automate running workflows across a suite of environments including: + +- environments containing different Python versions and +- different operating systems (Mac, Linux, Unix). + +### What is Continuous Deployment (CD)? + +Continuous deployment (CD) extends the CI process by automating the deployment of code changes to production or staging environments. In the case of your open source tool, CD can be used to: + +- Automate publishing to PyPI +- Automate publishing your documentation to github pages or Read the Docs. + +It is also used once your conda-forge recipe is set up to keep your package up to date on conda-forge. + +### Why Use CI + +CI can be configured to run a workflow on every commit pushed to GitHub and every pull request opened. This ensures that any changes made to your package are tested across environments before they are merged into the main branch of your code. + +These checks are particularly useful if someone new is contributing to your code. Every change a contributor makes will be tested when it’s pushed to your code repository. + +Together, CI and CD streamline the process of building, testing, and deploying code. They aim to improve the efficiency, quality, and reliability of software development and publication. + +```{note} +All pyOpenSci packages must use some form of continuous integration. Even if you are not planning to go through peer review, we strongly recommend that you use continuous integration too! +``` + +In the case of GitHub actions (which we will focus on here), CI workflows are running on online servers that support GitHub. + +## CI / CD Platforms + +There are numerous platforms available for CI/CD. Here, we will focus on GitHub Actions (GHA) which is built into GitHub. GitHub is the most commonly used platform to store scientific open source software. + +:::{note} +If you are using GitLab CI/CD many of the principles described here will apply, however the workflow files may look different. +::: + +### If you aren't sure, use GitHub Actions + +While you are welcome to use the continuous integration platform of your choice, +we recommend Github actions because it is free-to-use and integrated tightly +into the GitHub user interface. There is also an entire store of GitHub action +templates that you can easily use and adapt to your own needs. + +:::{admonition} Other platforms that you may run into +:class: info + +- [Appveyor:](https://www.appveyor.com/) used to be a goto for running tests on Windows operating systems until GitHub actions evolved to support Windows. AppVeyor has evolved to support other operating systems since Microsoft acquired GitHub. +- [Travis CI:](https://www.travis-ci.com/) Used to be the most common CI platform used in our ecosystem until they dropped free support for open source. +- [CircleCI:](https://circleci.com/) You will still see some people using CircleCI for specific tasks. CircleCi can be useful for automated builds of websites and documentation allowing you to preview the changes to that website in your browser. + ::: + +## Embrace automation + +By embracing CI/CD, you can ensure that your code runs as you expect it to across the diverse landscapes of user environments. Further you can +automate certain checks (and in some cases code fixes) including linting and code style. You can even automate spell checking your documentation +and docstrings! diff --git a/ci-tests-data/code-cov.md b/ci-tests-data/code-cov.md new file mode 100644 index 00000000..a8624a98 --- /dev/null +++ b/ci-tests-data/code-cov.md @@ -0,0 +1,13 @@ +# Code coverage + +Code coverage is the amount of your package's codebase that is run as a part of running your project's tests. A good rule of thumb is to ensure that every line of your code is run at least once during testing. However, note that good code coverage does not guarantee that your package is well-tested. For example, you may run all of your lines of code, but not account for many edge-cases that users may have. Ultimately, you should think carefully about the way your package will be used, and decide whether your tests adequately cover all of that usage. + +A common service for analyzing code coverage is [codecov.io](https://codecov.io/). This project is free for open source tools, and will provide dashboards that tell you how much of your codebase is covered during your tests. We recommend setting up an account, and using codecov to keep track of your code coverage. + +```{figure} ../images/code-cov-stravalib.png +:height: 450px +:alt: Screenshot of the code cov service - showing test coverage for the stravalib package. in this image you can see a list of package modules and the associated number of lines and % lines covered by tests. at the top of the image you can see what branch is being evaluated and the path to the repository being shown. + +the Code cov platform is a useful tool if you wish to visually track code coverage. Using it you can not only get the same summary information that you can get with **pytest-cov** extension. You can also get a visual representation of what lines are covered by your tests and what lines are not covered. Code cove is mostly useful for evaluating unit tests and/or how much of your package code is "covered. It however will not evaluate things like integration tests and end-to-end workflows. b + +``` diff --git a/ci-tests-data/data.md b/ci-tests-data/data.md new file mode 100644 index 00000000..17f5215b --- /dev/null +++ b/ci-tests-data/data.md @@ -0,0 +1 @@ +# Data for packaging diff --git a/ci-tests-data/index.md b/ci-tests-data/index.md new file mode 100644 index 00000000..73f3a324 --- /dev/null +++ b/ci-tests-data/index.md @@ -0,0 +1,93 @@ +# Tests and data for your Python package + +In this section you will learn more about the importance of writing +tests for you Python package. + +::::{grid} 1 1 2 2 +:class-container: text-center +:gutter: 3 + +:::{grid-item-card} +:link: write-tests +:link-type: doc + +✨ Why write tests ✨ +^^^ + +Learn more about the art of writing tests for your Python package. +Learn about why you should write tests and how they can help you and +potential contributors to your project. +::: + +:::{grid-item-card} +:link: test-types +:link-type: doc + +✨ Types of tests ✨ +^^^ +There are three general types of tests that you can write for your Python +package: unit tests, integration tests and end-to-end (or functional) tests. Learn about all three. +::: + +:::{grid-item-card} +:link: run-tests +:link-type: doc + +✨ How to Run Your Tests ✨ +^^^ + +Learn more about what tools you can use to run tests. And how to run your +tests on different Python versions and different operating systems both on +your computer and using continuous integration on GitHub (or in any other CI). +::: + +:::{grid-item-card} +:link: data +:link-type: doc + +✨ Package data ✨ +^^^ +This section is current in progress... link coming soon +::: + +:::{grid-item-card} +:link: ci +:link-type: doc + +✨ Continuous Integration ✨ +^^^ +Learn what Continuous Integration is and how you can set it up to run tests, build documentation and publish your package to PyPI. +::: +:::: + +```{toctree} +:hidden: +:maxdepth: 2 +:caption: Create & Run Tests + +Intro +Write tests +Test types +Run tests +Code coverage + +``` + +```{toctree} +:hidden: +:maxdepth: 2 +:caption: Continuous Integration + +Intro to CI +Run tests in CI + +``` + +```{toctree} +:hidden: +:maxdepth: 2 +:caption: Package data + +Package data + +``` diff --git a/ci-tests-data/run-tests.md b/ci-tests-data/run-tests.md new file mode 100644 index 00000000..3de94b7f --- /dev/null +++ b/ci-tests-data/run-tests.md @@ -0,0 +1,220 @@ +# Run your tests + +Running your tests is important to ensure that your package +is working as expected. However, it's also important to think about your code running, not only on your computer, but also on the computers of your users who may be running various Python versions and using various operating systems. Thus, you will want to consider the following when running your tests: + +1. Run your test suite in a series of environments that represent the Python versions and operating systems your users are likely to have. +2. Running your tests in an isolated environment ensures that they do not pass randomly due to your computer's specific setup. For instance, you might have locally installed dependencies that are not declared in your package's dependency list. This oversight could lead to issues when others try to install or run your package on their computers. + +On this page, you will learn about the tools that you can use to both run tests in isolated environments and across +Python versions. + +### Tools to run your tests + +There are three types of tools that will make is easier to setup and run your tests in various environments: + +1. A **test framework**, is a package that provides a particular syntax and set of tools for _both writing and running your tests_. Some test frameworks also have plugins that add additional features such as evaluating how much of your code the tests cover. Below you will learn about the **pytest** framework which is one of the most commonly used Python testing frameworks in the scientific ecosystem. Testing frameworks are essential but they only serve to run your tests. They won't allow you to run tests across Python versions without additional automation tools (see automation tools below). +2. **Automation tools** allow you to automate running workflows such as tests in specific ways using user-defined commands. For instance it's useful to be able to run tests across different Python versions with a single command. Tools such as [**nox**](https://nox.thea.codes/en/stable/index.html) and [**tox**](https://tox.wiki/en/latest/index.html) also allow you to run tests across Python versions. However, it will be difficult to test your build on different operating systems using only nox and tox - this is where continuous integration (CI) comes into play. +3. **Continuous Integration (CI):** is the last tool that you'll need to run your tests. CI will not only allow you to replicate any automated builds you create using nox or tox to run your package in different Python environments. It will also allow you to run your tests on different operating systems (Windows, Mac and Linux). [We discuss using CI to run tests here](tests-ci). + +:::{figure-md} +![Figure showing three boxes - the first hasTest Frameworks in it, the second Test Runner and the third Continuous Integration....](../images/test-tools.png) + +There are three types of tools that will help you develop and run your tests. Test frameworks like pytest +provide syntax and a **framework** for you to write and +run tests. Test runners automate processes such as creating isolated environments to run your tests in, and running tests across Python versions with a single command. Finally Continuous integrate (CI) is a generic platform where you can run your tests across operating systems. CI allows you to run your tests on every PR and commit to ensure iterative checks as contributors suggest changes to your code. + +::: + +## What testing framework / package should I use to run tests? + +We recommend using `Pytest` to build and run your package tests. Pytest is the most common testing tool used in the Python ecosystem. + +[The Pytest package](https://docs.pytest.org/en/latest/) also has a number of +extensions that can be used to add functionality such as: + +- [pytest-cov](https://pytest-cov.readthedocs.io/en/latest/) allows you to analyze the code coverage of your package during your tests, and generates a report that you can [upload to codecov](https://codecov.io/). + +[Learn more about code coverage here.](code-cov) + +```{note} +TODO: add note about running tests in vscode, breakpoints and –no-cov flag. Then link to tutorial that explains how to deal with this. +``` + +## Run tests using pytest + +If you are using **pytest**, you can run your tests locally by +calling: + +`pytest` + +Or if you want to run a specific test file - let's call such file - filename.py - you can run: + +`pytest filename.py` + +Learn more from the [get started docs here](https://docs.pytest.org/en/7.1.x/getting-started.html). + +Running pytest on your computer is going to run your tests in whatever +Python environment you currently have activated. This means that tests will be +run on a single version of Python and only on the operating system that you +are running locally. + +This is a great start to making your Python package more robust! However, your users may be using your package on different +versions of Python. Or they also may use other operating systems. + +An automation tool can simplify the process of running tests +in various Python environments. + +### Tools to automate running your tests + +To run tests on various Python versions or in various specific environments with a single command, you can use an automation tool such as nox or tox. +Both nox and tox can create an isolated virtual environment that you define. This allows you to easily run your tests in multiple environments and across Python versions. + +We will focus on [Nox](https://nox.thea.codes/) in this guide. nox is a Python-based automation tool that builds upon the features of both Make and Tox. Nox is designed to simplify and streamline testing and development workflows. Everything that you do with nox can be implemented using a Python-based interface. + +```{admonition} Other automation tools you'll see in the wild +:class: note + +- **[Tox](https://tox.wiki/en/latest/index.html#useful-links)** is an automation tool that supports common steps such as building documentation, running tests across various versions of Python, and more. You can find [a nice overview of tox in the plasmaPy documentation](https://docs.plasmapy.org/en/stable/contributing/testing_guide.html#using-tox). + +- **[Hatch](https://github.com/ofek/hatch)** is a modern end-to-end packaging tool that works with the popular build backend called hatchling. Hatch offers a tox-like setup where you can run tests locally using different Python versions. If you are using hatch to support your packaging workflow, you may want to also use its testing capabilities rather than using nox. + +* [**make:**](https://www.gnu.org/software/make/manual/make.html) Some developers use Make, which is a build automation tool, for running tests +due to its versatility; it's not tied to a specific language and can be used +to run various build processes. However, Make's unique syntax and approach can +make it more challenging to learn, particularly if you're not already familiar +with it. Make also won't manage environments for you like **nox** will do. +``` + +## Run tests across Python versions with nox + +**Nox** is a great automation tool to learn give it is: + +- Python-based making it accessible if you already know Python and +- It will create isolated environments to run workflows. + +Nox simplifies creating and managing testing environments. With Nox, you can +set up virtual environments, and run tests across Python versions using the environment manager of your choice with a +single command. + +Nox can also be used for other development tasks such as building +documentation, creating your package distribution, and testing across various +environment managers such as conda and pip. + +## Test Environments + +By default, Nox uses the Python built in `venv` environment manager. A virtual environment (`venv`) is a self-contained Python environment that allows you to isolate and manage dependencies for different Python projects. It helps ensure that project-specific libraries and packages do not interfere with each other, promoting a clean and organized development environment. + +An example of using nox to run tests in `venv` environments for Python versions 3.9, 3.10 and 3.11 is below. + +```{warning} +Note that for the code below to work, you need to have all 3 versions of Python installed on your computer for `venv` to find. +``` + +### Nox with venv environments + +```{admonition} TODO: +TODO: add some tests above and show what the output would look like in the examples below... +``` + +Below is an example of setting up nox to run tests using `venv` which is the built in environment manager that comes with base Python. + +Note that the example below assumes that you have setup your pyproject.toml to declare test dependencies in a way that pip +can understand. And example of that setup is below. + +```toml +[project] +dependencies = [ + "geopandas", + "xarray", +] + +[project.optional-dependencies] +tests = ["pytest", "pytest-cov"] +``` + +If you have the above setup, then you can use `session.install(".[tests]")` to install your test dependencies. +Notice that below one single nox session allows you to run +your tests on 3 different Python environments (Python 3.9, 3.10 and 3.11). + +```python +import nox + +# For this to run you will need to have python3.9, python3.10 and python3.11 installed on your computer. Otherwise nox will skip running tests for whatever versions are missing + +@nox.session(python=["3.9", "3.10", "3.11"]) +def test(session): + +# install +session.install(".[tests]") + +# Run tests +session.run("pytest") + +``` + +Above you create a nox session in the form of a function +with a `@nox.session` decorator. Notice that within the decorator you declare the versions of python that you +wish to run. + +To run the above you'd use the command where `-s` stands for +session. Your function above is called test there for +the session name is test. + +``` +nox -s test +``` + +### Nox with conda / mamba + +Below is an example for setting up nox to use mamba (or conda) for your +environment manager. +Note that unlike venv, conda can automatically install +the various versions of Python that you need. You won't need to install all three Python versions if you use conda/mamba, like you do with `venv`. + +```{note} +For conda to work with nox, you will need to +install a conda-friendly version of Python. We suggest +the mamba-forge installation. + +More on that here... +``` + +```python +import nox + +# The syntax below allows you to use mamba / conda as your environment manager, if you use this approach you don’t have to worry about installing different versions of Python + +@nox.session(venv_backend='mamba', python=["3.9", "3.10", "3.11"]) +def test_mamba(session): + """Nox function that installs dev requirements and runs + tests on Python 3.9 through 3.11 + """ + + # Install dev requirements + session.install(".[tests]") + # Run tests using any parameters that you need + session.run("pytest") +``` + +To run the above session you'd use: + +```bash +nox -s test_mamba +``` + +## IGNORE ME - testing doctest (not working now) + +```{testcode} +a = 1+2 +print(a) +``` + +```{testoutput} + +``` + +TODO: might be able to use doctest to run examples However so far it's not working as intended and may be extra work. +https://www.sphinx-doc.org/en/master/usage/extensions/doctest.html + +--- diff --git a/ci-tests-data/test-types.md b/ci-tests-data/test-types.md new file mode 100644 index 00000000..9a9fee79 --- /dev/null +++ b/ci-tests-data/test-types.md @@ -0,0 +1,157 @@ +# Test Types for Python packages + +_Unit, Integration & Functional Tests_ + +There are different types of tests that you want to consider when creating your +test suite: + +1. Unit tests +2. Integration +3. End-to-end (also known as Functional) tests + +Each type of test has a different purpose. Here, you will learn about all three types of tests. + +```{admonition} +I think this page would be stronger if we did have some +examples from our package here: https://github.com/pyOpenSci/pyosPackage + + +``` + +## Unit Tests + +A unit test involves testing individual components or units of code in isolation to ensure that they work correctly. The goal of unit testing is to verify that each part of the software, typically at the function or method level, performs its intended task correctly. + +Unit tests can be compared to examining each piece of your puzzle to ensure parts of it are not broken. If all of the pieces of your puzzle don’t fit together, you will never complete it. Similarly, when working with code, tests ensure that each function, attribute, class, method works properly when isolated. + +**Unit test example:** Pretend that you have a function that converts a temperature value from Celsius to Fahrenheit. A test for that function might ensure that when provided with a value in Celsius, the function returns the correct value in degrees Fahrenheit. That function is a unit test. It checks a single unit (function) in your code. + +```python +# Example package function +def celsius_to_fahrenheit(celsius): + """ + Convert temperature from Celsius to Fahrenheit. + + Parameters: + celsius (float): Temperature in Celsius. + + Returns: + float: Temperature in Fahrenheit. + """ + fahrenheit = (celsius * 9/5) + 32 + return fahrenheit +``` + +Example unit test for the above function. You'd run this test using the `pytest` command in your **tests/** directory. + +```python +import pytest +from temperature_converter import celsius_to_fahrenheit + +def test_celsius_to_fahrenheit(): + """ + Test the celsius_to_fahrenheit function. + """ + # Test with freezing point of water + assert pytest.approx(celsius_to_fahrenheit(0), abs=0.01) == 32.0 + + # Test with boiling point of water + assert pytest.approx(celsius_to_fahrenheit(100), abs=0.01) == 212.0 + + # Test with a negative temperature + assert pytest.approx(celsius_to_fahrenheit(-40), abs=0.01) == -40.0 + +``` + +```{figure} ../images/pyopensci-puzzle-pieces-tests.png +:height: 300px +:alt: image of puzzle pieces that all fit together nicely. The puzzle pieces are colorful - purple, green and teal. + +Your unit tests should ensure each part of your code works as expected on its own. +``` + +### Integration tests + +Integration tests involve testing how parts of your package work together or integrate. Integration tests can be compared to connecting a bunch of puzzle pieces together to form a whole picture. Integration tests focus on how different pieces of your code fit and work together. + +For example, if you had a series of steps that collected temperature data in a spreadsheet, converted it from degrees celsius to Fahrenheit and then provided an average temperature for a particular time period. An integration test would ensure that all parts of that workflow behaved as expected. + +```python + +def fahr_to_celsius(fahrenheit): + """ + Convert temperature from Fahrenheit to Celsius. + + Parameters: + fahrenheit (float): Temperature in Fahrenheit. + + Returns: + float: Temperature in Celsius. + """ + celsius = (fahrenheit - 32) * 5/9 + return celsius + +# Function to calculate the mean temperature for each year and the final mean +def calc_annual_mean(df): + # TODO: make this a bit more robust so we can write integration test examples?? + # Calculate the mean temperature for each year + yearly_means = df.groupby('Year').mean() + + # Calculate the final mean temperature across all years + final_mean = yearly_means.mean() + + # Return a converted value + return fahr_to_celsius(yearly_means), fahr_to_celsius(final_mean) + +``` + +```{figure} ../images/python-tests-puzzle.png +:height: 350px +:alt: image of two puzzle pieces with some missing parts. The puzzle pieces are purple teal yellow and blue. The shapes of each piece don’t fit together. + +If puzzle pieces have missing ends, they can’t work together with other elements in the puzzle. The same is true with individual functions, methods and classes in your software. The code needs to work both individually and together to perform certain sets of tasks. + +``` + +```{figure} ../images/python-tests-puzzle-fit.png +:height: 450px +:alt: image of puzzle pieces that all fit together nicely. The puzzle pieces are colorful - purple, green and teal. + +Your integration tests should ensure that parts of your code that are expected to work +together, do so as expected. + +``` + +### End-to-end (functional) tests + +End-to-end tests (also referred to as functional tests) in Python are like comprehensive checklists for your software. They simulate real user end-to-end workflows to make sure the code base supports real life applications and use-cases from start to finish. These tests help catch issues that might not show up in smaller tests and ensure your entire application or program behaves correctly. Think of them as a way to give your software a final check before it's put into action, making sure it's ready to deliver a smooth experience to its users. + +```{figure} ../images/flower-puzzle-pyopensci.jpg +:height: 450px +:alt: Image of a completed puzzle showing a daisy + +End-to-end or functional tests represent an entire workflow that you +expect your package to support. + +``` + +End-to-end test also test how a program runs from start to finish. A tutorial that you add to your documentation that runs in CI in an isolated environment is another example of an end-to-end test. + +```{note} +For scientific packages, creating short tutorials that highlight core workflows that your package supports, that are run when your documentation is built could also serve as end-to-end tests. +``` + +### Comparing unit, integration and end-to-end tests + +Unit tests, integration tests, and end-to-end tests have complementary advantages and disadvantages. The fine-grained nature of unit tests make them well-suited for isolating where errors are occurring. However, unit tests are not useful for verifying that different sections of code work together. + +Integration and end-to-end tests verify that the different portions of the program work together, but are less well-suited for isolating where errors are occurring. For example, when you refactor your code, it is possible that that your end-to-end tests will +break. But if the refactor didn't introduce new behavior to your existing +code, then you can rely on your unit tests to continue to pass, testing the +original functionality of your code. + +It is important to note that you don't need to spend energy worrying about +the specifics surrounding the different types of tests. When you begin to +work on your test suite, consider what your package does and how you +may need to test parts of your package. Bring familiar with the different types of tests can provides a framework to +help you think about writing tests and how different types of tests can complement each other. diff --git a/ci-tests-data/tests-ci.md b/ci-tests-data/tests-ci.md new file mode 100644 index 00000000..26d58b00 --- /dev/null +++ b/ci-tests-data/tests-ci.md @@ -0,0 +1,86 @@ +# Run tests with Continuous Integration + +Running your [test suite locally](run-tests) is useful as you develop code and also test new features or changes to the code base. However, you also will want to setup Continuous Integration (CI) to run your tests online. CI allows you to run all of your tests in the cloud. While you may only be able to run tests locally on a specific operating system that you run, in CI you can specify tests to run both on various versions of Python and across different operating systems. + +CI can also be triggered for pull requests and pushes to your repository. This means that every pull request that you, your maintainer team or a contributor submit, can be tested. In the end CI testing ensures your code continues to run as expected even as changes are made to the code base. + +```{note} +[Learn more about Continuous Integration and how it can be used, here.](ci) +``` + +## CI & pull requests + +CI is invaluable if you have outside people contributing to your software. +You can setup CI to run on all pull requests submitted to your repository. +CI can make your repository more friendly to new potential contributors. +It allows users to contribute code, documentation fixes and more without +having to create development environments, run tests and build documentation +locally. + +## Example GitHub action that runs tests + +Below is an example github action that runs tests using nox +across both Windows, Mac and Linux and on Python versions +3.9-3.11. It also includes two steps that make your build more +efficient so your dependencies aren't downloaded multiple times. + +```yaml +name: Pytest unit/integration + +on: + pull_request: + push: + branches: + - main + +# Use bash by default in all jobs +defaults: + run: + shell: bash + +jobs: + build-test: + name: Test Run (${{ matrix.python-version }}, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "macos-latest", "windows-latest"] + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + with: + # fetch more than the last single commit to help scm generate proper version + fetch-depth: 20 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + # This step and the step below are an optional steps to cache variables to make your build faster / more efficient + - name: Set Variables + id: set_variables + shell: bash + run: | + echo "PY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_OUTPUT + echo "PIP_CACHE=$(pip cache dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.set_variables.outputs.PIP_CACHE }} + key: ${{ runner.os }}-pip-${{ steps.set_variables.outputs.PY }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install nox + - name: List installed packages + run: pip list + - name: Run tests with pytest & nox + run: | + nox -s tests-${{ matrix.python-version }} + # You only need to upload code coverage once to codecov + - name: Upload coverage to Codecov + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10'}} + uses: codecov/codecov-action@v3 +``` diff --git a/ci-tests-data/write-tests.md b/ci-tests-data/write-tests.md new file mode 100644 index 00000000..5a96c040 --- /dev/null +++ b/ci-tests-data/write-tests.md @@ -0,0 +1,88 @@ +# Write tests for your Python package + +Writing code that tests your package code, also known as test suites, is important for you as a maintainer, your users, and package contributors. Test suites consist of sets of functions, methods, and classes +that are written with the intention of making sure a specific part of your code +works as you expected it to. + +## Why write tests for your package? + +Tests act as a safety net for code changes. They help you spot and rectify bugs +before they affect users. Tests also instill confidence that code alterations from +contributors won't breaking existing functionality. + +Writing tests for your Python package is important because: + +- **Catch Mistakes:** Tests are a safety net. When you make changes or add new features to your package, tests can quickly tell you if you accidentally broke something that was working fine before. +- **Save Time:** Imagine you have a magic button that can automatically check if your package is still working properly. Tests are like that magic button! They can run all those checks for you saving you time. +- **Easier Collaboration:** If you're working with others, or have outside contributors, tests help everyone stay on the same page. Your tests explain how your package is supposed to work, making it easier for others to understand and contribute to your project. +- **Fearless Refactoring:** Refactoring means making improvements to your code structure without changing its behavior. Tests empower you to make these changes as if you break something, test failures will let you know. +- **Documentation:** Tests serve as technical examples of how to use your package. This can be helpful for a new technical contributor that wants to contribute code to your package. They can look at your tests to understand how parts of your code functionality fits together. +- **Long-Term ease of maintenance:** As your package evolves, tests ensure that your code continues to behave as expected, even as you make changes over time. Thus you are helping your future self when writing tests. +- **Easier pull request reviews:** By running your tests in a CI framework such as GitHub actions, each time you or a contributor makes a change to your code-base, you can catch issues and things that may have changed in your code base. This ensures that your software behaves the way you expect it to. + +### Tests for user edge cases + +Edge cases refer to unexpected or "outlier" ways that some users may use your package. Tests enable you to address various edge cases that could impair +your package's functionality. For example, what occurs if a function expects a +pandas `dataframe` but a user supplies a numpy `array`? Does your code gracefully +handle this situation, providing clear feedback, or does it leave users +frustrated by an unexplained failure? + +:::{note} + +For a good introduction to testing, see [this Software Carpentry lesson](https://swcarpentry.github.io/python-novice-inflammation/10-defensive.html) + +::: + +```{figure} ../images/python-tests-puzzle.png +:height: 350px + +Imagine you're working on a puzzle where each puzzle piece represents a function, method, class or attribute in your Python package that you want other people to be able to use. Would you want to give someone a puzzle that has missing pieces or pieces that don't fit together? Providing people with the right puzzle pieces that work together can be compared to writing tests for your Python package. + +``` + +````{admonition} Test examples +:class: note + +Let’s say you have a Python function that adds two numbers a and b together. + +```python +def add_numbers(a, b): + return a + b +``` + +A test to ensure that function runs as you might expect when provided with different numbers might look like this: + +```python +def test_add_numbers(): + result = add_numbers(2, 3) + assert result == 5, f"Expected 5, but got {result}" + + result2 = add_numbers(-1, 4) + assert result2 == 3, f"Expected 3, but got {result2}" + + result3 = add_numbers(0, 0) + assert result3 == 0, f"Expected 0, but got {result3}" + +test_add_numbers() + +``` +```` + +🧩🐍 + +### How do I know what type of tests to write? + +:::{note} +This section has been adapted from [a presentation by Nick Murphy](https://zenodo.org/record/8185113). +::: + +At this point, you may be wondering - what should you be testing in your package? Below are a few examples: + +- **Test some typical cases:** Test that the package functions as you expect it to when users use it. For instance, if your package is supposed to add two numbers, test that the outcome value of adding those two numbers is correct. + +- **Test special cases:** Sometimes there are special or outlier cases. For instance, if a function performs a specific calculation that may become problematic closer to the value = 0, test it with the input of both 0 and + +* **Test at and near the expected boundaries:** If a function requires a value that is greater than or equal to 1, make sure that the function still works with both the values 1 and less than one and 1.001 as well (something close to the constraint value).. + +* **Test that code fails correctly:** If a function requires a value greater than or equal to 1, then test at 0.999. Make sure that the function fails gracefully when given unexpected values and help and that the user can easily understand why if failed (provides a useful error message). diff --git a/conf.py b/conf.py index 455d6840..68d7800d 100644 --- a/conf.py +++ b/conf.py @@ -54,7 +54,7 @@ "sphinx_sitemap", "sphinxext.opengraph", "sphinx_favicon", - "sphinxcontrib.bibtex" + "sphinxcontrib.bibtex", ] # colon fence for card support in md @@ -164,6 +164,6 @@ } # Bibliographies -bibtex_bibfiles = ['bibliography.bib'] +bibtex_bibfiles = ["bibliography.bib"] # myst complains about bibtex footnotes because of render order suppress_warnings = ["myst.footnote"] diff --git a/images/code-cov-stravalib.png b/images/code-cov-stravalib.png new file mode 100644 index 0000000000000000000000000000000000000000..832ee3fced7b84921de1f3b64f4380ccd5dbf035 GIT binary patch literal 568356 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