diff --git a/_static/pyos.css b/_static/pyos.css index e8cdc9a9..d94fb75a 100644 --- a/_static/pyos.css +++ b/_static/pyos.css @@ -52,33 +52,28 @@ html, body { html, body { font-size: 1.02rem; - font-family: 'Poppins', sans-serif!important; } body p { } .admonition { - margin-top: 40px; - margin-bottom: 40px; -} - -h2, h3, h4 { - font-family: 'Poppins', sans-serif!important; + margin-top: 60px!important; + margin-bottom: 70px!important; } h1 { margin-top: 10px; margin-bottom: 40px; - font-family: 'Itim'!important; - color: #542568; + font-family: var(--pyos-font-family-h1) !important; + color: var(--pyos-h1-color); } h2 { margin-top: 1em; } h3 { - margin-top: 40px} + margin-top: 60px} figcaption .caption-text { text-align: left!important; @@ -96,7 +91,176 @@ figcaption { .admonition p { - font-size: 1em; + font-size: .9em; +} + +/* Navbar */ +/* +Don't fill all vertical space beneath TOC, which causes +readthedocs version selector to fall off the page and the +ugly scrollbar to show all the time +*/ +.bd-sidebar-primary .sidebar-primary-items__end { + margin-bottom: unset; + margin-top: unset; +} + +/* Tutorial block page */ +.left-div { + background-color: #3498db; + color: #fff; + text-align: center; + padding: 20px; + width: 35%; + border-radius: 10px; +} + +.right-div { + margin-top: 10px; +} + +.lesson-div { + cursor: pointer; + transition: background-color 0.3s; + margin-bottom: 10px; + padding: 10px; + border-radius: 5px; + text-align: center; + color: #333; +} + +.lesson-div a { + color: inherit; + text-decoration: none; + display: block; + height: 100%; + width: 100%; +} + +.lesson-div:hover { + background-color: #ccc; +} + +/* Different colors and their shades */ +.lesson-div:nth-child(1) { + background-color: #216A6B; + color: #fff; +} + +.lesson-div:nth-child(2) { + background-color: #6D597A; + color: #fff; +} + +.lesson-div:nth-child(3) { + background-color: #B56576; + color: #fff; +} + +.lesson-div:nth-child(4) { + background-color: #3A8C8E; /* Shade of #216A6B */ +} + +.lesson-div:nth-child(5) { + background-color: #8F7B8D; /* Shade of #6D597A */ +} + +.lesson-div:nth-child(6) { + background-color: #D78287; /* Shade of #B56576 */ +} + +.lesson-div:nth-child(7) { + background-color: #185355; /* Darker shade of #216A6B */ + color: #fff; +} + + + +html[data-theme=light] { + --pst-color-primary: var(--pyos-color-primary); + --pst-color-primary-text: #fff; + --pst-color-primary-highlight: #053f49; + --sd-color-primary: var(--pst-color-primary); + --sd-color-primary-text: var(--pst-color-primary-text); + --sd-color-primary-highlight: var(--pst-color-primary-highlight); + --sd-color-primary-bg: #d0ecf1; + --sd-color-primary-bg-text: #14181e; + --pst-color-secondary: var(--pyos-color-secondary); + --pst-color-secondary-text: #fff; + --pst-color-secondary-highlight: var(--pyos-color-secondary-highlight); + --sd-color-secondary: var(--pst-color-secondary); + --sd-color-secondary-text: var(--pst-color-secondary-text); + --sd-color-secondary-highlight: var(--pst-color-secondary-highlight); + --sd-color-secondary-bg: #e0c7ff; + --sd-color-secondary-bg-text: #14181e; + --pst-color-success: #00843f; + --pst-color-success-text: #fff; + --pst-color-success-highlight: #00381a; + --sd-color-success: var(--pst-color-success); + --sd-color-success-text: var(--pst-color-success-text); + --sd-color-success-highlight: var(--pst-color-success-highlight); + --sd-color-success-bg: #d6ece1; + --sd-color-success-bg-text: #14181e; + --pst-color-info: #A66C98; /* general admonition */ + --pst-color-info-bg: #eac8e2; + --pst-heading-color: var(--pyos-color-dark); + --pyos-h1-color: var(--pyos-color-dark); +} + +html[data-theme=dark] { + --pst-color-primary: var(--pyos-dm-color-primary); + --pst-color-link: var(--pyos-color-light); + --pst-color-link-hover: var(--pyos-dm-color-primary); + --pyos-h1-color: var(--pyos-color-light); +} + + +/* -------------------------------------- */ +/* Generated by https://gwfh.mranftl.com/ */ + +/* poppins-regular - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + src: url('./fonts/poppins-v20-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* poppins-italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Poppins'; + font-style: italic; + font-weight: 400; + src: url('./fonts/poppins-v20-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* poppins-700 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Poppins'; + font-style: normal; + font-weight: 700; + src: url('./fonts/poppins-v20-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* poppins-700italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Poppins'; + font-style: italic; + font-weight: 700; + src: url('./fonts/poppins-v20-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* itim-regular - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Itim'; + font-style: normal; + font-weight: 400; + src: url('./fonts/itim-v14-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ } /* poppins-500 - latin */ diff --git a/_templates/header.html b/_templates/header.html deleted file mode 100644 index d261d884..00000000 --- a/_templates/header.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/ci-tests-data/ci.md b/ci-data/ci.md similarity index 100% rename from ci-tests-data/ci.md rename to ci-data/ci.md diff --git a/ci-data/index.md b/ci-data/index.md new file mode 100644 index 00000000..be7e02ea --- /dev/null +++ b/ci-data/index.md @@ -0,0 +1,11 @@ +# Continuous Integration and Data for your package + + +:::{toctree} +:hidden: +:caption: CI & Data + + +What is CI? +Data +::: diff --git a/ci-tests-data/data.md b/ci-tests-data/data.md deleted file mode 100644 index 17f5215b..00000000 --- a/ci-tests-data/data.md +++ /dev/null @@ -1 +0,0 @@ -# Data for packaging diff --git a/ci-tests-data/index.md b/ci-tests-data/index.md deleted file mode 100644 index eecf9423..00000000 --- a/ci-tests-data/index.md +++ /dev/null @@ -1,103 +0,0 @@ -# Tests and data for your Python package - -In this section you will learn more about the importance of writing -tests for your Python package. - - -:::{figure-md} fig-target - - - -Fix-- highlight the tests part of the circle and make everything else grey / not colored?? -::: - - - -::::{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 deleted file mode 100644 index c4f74c55..00000000 --- a/ci-tests-data/run-tests.md +++ /dev/null @@ -1,204 +0,0 @@ -# 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 this 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](../package-structure-code/declare-dependencies.md). 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, 3.11, and 3.12). - -```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", "3.12"]) -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", "3.12"]) -def test_mamba(session): - """Nox function that installs dev requirements and runs - tests on Python 3.9 through 3.12 - """ - - # 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 -``` diff --git a/ci-tests-data/test-types.md b/ci-tests-data/test-types.md deleted file mode 100644 index 9a9fee79..00000000 --- a/ci-tests-data/test-types.md +++ /dev/null @@ -1,157 +0,0 @@ -# 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 deleted file mode 100644 index a34f3d13..00000000 --- a/ci-tests-data/tests-ci.md +++ /dev/null @@ -1,87 +0,0 @@ -# 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@v4 - 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 }} - cache: pip # By adding cache here, you are telling actions to reuse installed dependencies rather than re-downloading and installing them each time. This speeds up your workflow - - # 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 - python -m 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 deleted file mode 100644 index 5a96c040..00000000 --- a/ci-tests-data/write-tests.md +++ /dev/null @@ -1,88 +0,0 @@ -# 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 6822660a..0825e603 100644 --- a/conf.py +++ b/conf.py @@ -100,7 +100,7 @@ "image_light": "logo-light-mode.png", "alt_text": "pyOpenSci Python Package Guide. The pyOpenSci logo is a purple flower with pyOpenSci under it. The o in open sci is the center of the flower", }, - "header_links_before_dropdown": 4, + "header_links_before_dropdown": 5, "use_edit_page_button": True, "show_nav_level": 2, "navigation_depth": 3, diff --git a/images/test-tools.png b/images/test-tools.png deleted file mode 100644 index 7ee75206..00000000 Binary files a/images/test-tools.png and /dev/null differ diff --git a/index.md b/index.md index 76384287..1d25068b 100644 --- a/index.md +++ b/index.md @@ -294,18 +294,35 @@ Packaging ``` -```{toctree} +:::{toctree} :hidden: :caption: Documentation Documentation -``` +::: -```{toctree} +:::{toctree} +:hidden: +:caption: Build & Publish + +Packaging + +::: + +:::{toctree} :hidden: :caption: Testing Tests -``` +::: + + +:::{toctree} +:hidden: +:caption: CI & Data + +CI & Data + +::: diff --git a/package-structure-code/intro.md b/package-structure-code/intro.md index 90b35aa4..639c65be 100644 --- a/package-structure-code/intro.md +++ b/package-structure-code/intro.md @@ -162,7 +162,7 @@ if you are looking for pyOpenSci's Python package review requirements! :::{toctree} :hidden: -:caption: Package structure & code style +:caption: Create & Build Your Package Intro @@ -176,7 +176,7 @@ Complex Builds :::{toctree} :hidden: -:caption: Publishing a package +:caption: Publish your package Publish with Conda / PyPI Package versions diff --git a/ci-tests-data/code-cov.md b/tests/code-cov.md similarity index 97% rename from ci-tests-data/code-cov.md rename to tests/code-cov.md index 7a1f871c..3bf8bd05 100644 --- a/ci-tests-data/code-cov.md +++ b/tests/code-cov.md @@ -1,4 +1,4 @@ -# Code coverage +# Code coverage for your Python package test suite 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. diff --git a/tests/index.md b/tests/index.md index 8eaa2d6f..4930c5fb 100644 --- a/tests/index.md +++ b/tests/index.md @@ -79,4 +79,5 @@ Write tests Test types Run tests locally Run tests online (using CI) +Code coverage ```