Skip to content
Merged
30 changes: 16 additions & 14 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
# Bookend python versions
python-version: ["3.10", "3.13"]
env: [""]
numprocesses: [4]
include:
# Minimum python version:
- env: "bare-minimum"
Expand All @@ -67,6 +68,16 @@ jobs:
- env: "flaky"
python-version: "3.13"
os: ubuntu-latest
# The mypy tests must be executed using only 1 process in order to guarantee
# predictable mypy output messages for comparison to expectations.
- env: "mypy"
python-version: "3.10"
numprocesses: 1
os: ubuntu-latest
- env: "mypy"
python-version: "3.13"
numprocesses: 1
os: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -88,6 +99,10 @@ jobs:
then
echo "CONDA_ENV_FILE=ci/requirements/environment.yml" >> $GITHUB_ENV
echo "PYTEST_ADDOPTS=-m 'flaky or network' --run-flaky --run-network-tests -W default" >> $GITHUB_ENV
elif [[ "${{ matrix.env }}" == "mypy" ]] ;
then
echo "CONDA_ENV_FILE=ci/requirements/environment.yml" >> $GITHUB_ENV
echo "PYTEST_ADDOPTS=-n 1 -m 'mypy' --run-mypy -W default" >> $GITHUB_ENV
else
echo "CONDA_ENV_FILE=ci/requirements/${{ matrix.env }}.yml" >> $GITHUB_ENV
fi
Expand Down Expand Up @@ -143,25 +158,12 @@ jobs:
enableCrossOsArchive: true
save-always: true

# Run all tests in *.py files, which excludes tests in *.yml files that test type
# annotations via the pytest-mypy-plugins plugin, because the type annotations
# tests fail when using multiple processes (the -n option to pytest).
- name: Run tests
run: python -m pytest -n 4
run: python -m pytest -n ${{ matrix.numprocesses }}
--timeout 180
--cov=xarray
--cov-report=xml
--junitxml=pytest.xml
xarray/tests/test_*.py

# As noted in the comment on the previous step, we must run type annotation tests
# separately, and we will run them even if the preceding tests failed. Further,
# we must restrict these tests to run only when matrix.env is empty, as this is
# the only case when all of the necessary dependencies are included such that
# spurious mypy errors due to missing packages are eliminated.
- name: Run mypy tests
if: ${{ always() && matrix.env == '' }}
run: python -m pytest xarray/tests/test_*.yml

- name: Upload test results
if: always()
Expand Down
38 changes: 12 additions & 26 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,37 +42,23 @@ repos:
- id: prettier
args: [--cache-location=.prettier_cache/cache]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
rev: v1.14.1
hooks:
- id: mypy
files: "^xarray"
exclude: "^xarray/util/generate_.*\\.py$"
# Copied from setup.cfg
exclude: "properties|asv_bench"
# This is slow and so we take it out of the fast-path; requires passing
# `--hook-stage manual` to pre-commit
stages: [manual]
additional_dependencies:
# Type stubs plus additional dependencies from ci/requirements/environment.yml
# required in order to satisfy most (ideally all) type checks. This is rather
# brittle, so it is difficult (if not impossible) to get mypy to succeed in
# this context, even when it succeeds in CI.
- dask
- distributed
- hypothesis
- matplotlib
- numpy==2.1.3
- pandas-stubs
- pytest
- types-colorama
- types-defusedxml
- types-docutils
- types-pexpect
- types-psutil
- types-Pygments
- types-python-dateutil
- types-pytz
- types-PyYAML
- types-setuptools
- typing-extensions>=4.1.0
additional_dependencies: [
# Type stubs
types-python-dateutil,
types-setuptools,
types-PyYAML,
types-pytz,
typing-extensions>=4.1.0,
numpy,
]
- repo: https://github.com/citation-file-format/cff-converter-python
rev: ebf0b5e44d67f8beaa1cd13a0d0393ea04c6058d
hooks:
Expand Down
18 changes: 17 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import pytest


def pytest_addoption(parser):
def pytest_addoption(parser: pytest.Parser):
"""Add command-line flags for pytest."""
parser.addoption("--run-flaky", action="store_true", help="runs flaky tests")
parser.addoption(
"--run-network-tests",
action="store_true",
help="runs tests requiring a network connection",
)
parser.addoption("--run-mypy", action="store_true", help="runs mypy tests")


def pytest_runtest_setup(item):
Expand All @@ -21,6 +22,21 @@ def pytest_runtest_setup(item):
pytest.skip(
"set --run-network-tests to run test requiring an internet connection"
)
if "mypy" in item.keywords and not item.config.getoption("--run-mypy"):
pytest.skip("set --run-mypy option to run mypy tests")


# See https://docs.pytest.org/en/stable/example/markers.html#automatically-adding-markers-based-on-test-names
def pytest_collection_modifyitems(items):
for item in items:
if "mypy" in item.nodeid:
# IMPORTANT: mypy type annotation tests leverage the pytest-mypy-plugins
# plugin, and are thus written in test_*.yml files. As such, there are
# no explicit test functions on which we can apply a pytest.mark.mypy
# decorator. Therefore, we mark them via this name-based, automatic
# marking approach, meaning that each test case must contain "mypy" in the
# name.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, not ideal to use the test name, but v nice comment

item.add_marker(pytest.mark.mypy)


@pytest.fixture(autouse=True)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ filterwarnings = [
log_cli_level = "INFO"
markers = [
"flaky: flaky tests",
"mypy: type annotation tests",
"network: tests requiring a network connection",
"slow: slow tests",
"slow_hypothesis: slow hypothesis tests",
Expand Down
28 changes: 14 additions & 14 deletions xarray/tests/test_dataarray_typing.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
- case: test_pipe_lambda_noarg_return_type
- case: test_mypy_pipe_lambda_noarg_return_type
main: |
from xarray import DataArray

da = DataArray().pipe(lambda data: data)

reveal_type(da) # N: Revealed type is "xarray.core.dataarray.DataArray"

- case: test_pipe_lambda_posarg_return_type
- case: test_mypy_pipe_lambda_posarg_return_type
main: |
from xarray import DataArray

da = DataArray().pipe(lambda data, arg: arg, "foo")

reveal_type(da) # N: Revealed type is "builtins.str"

- case: test_pipe_lambda_chaining_return_type
- case: test_mypy_pipe_lambda_chaining_return_type
main: |
from xarray import DataArray

answer = DataArray().pipe(lambda data, arg: arg, "foo").count("o")

reveal_type(answer) # N: Revealed type is "builtins.int"

- case: test_pipe_lambda_missing_arg
- case: test_mypy_pipe_lambda_missing_arg
main: |
from xarray import DataArray

Expand All @@ -34,7 +34,7 @@
main:4: note: def [P`2, T] pipe(self, func: Callable[[DataArray, **P], T], *args: P.args, **kwargs: P.kwargs) -> T
main:4: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T

- case: test_pipe_lambda_extra_arg
- case: test_mypy_pipe_lambda_extra_arg
main: |
from xarray import DataArray

Expand All @@ -46,7 +46,7 @@
main:4: note: def [P`2, T] pipe(self, func: Callable[[DataArray, **P], T], *args: P.args, **kwargs: P.kwargs) -> T
main:4: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T

- case: test_pipe_function_missing_posarg
- case: test_mypy_pipe_function_missing_posarg
main: |
from xarray import DataArray

Expand All @@ -61,7 +61,7 @@
main:7: note: def [P`2, T] pipe(self, func: Callable[[DataArray, **P], T], *args: P.args, **kwargs: P.kwargs) -> T
main:7: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T

- case: test_pipe_function_extra_posarg
- case: test_mypy_pipe_function_extra_posarg
main: |
from xarray import DataArray

Expand All @@ -76,7 +76,7 @@
main:7: note: def [P`2, T] pipe(self, func: Callable[[DataArray, **P], T], *args: P.args, **kwargs: P.kwargs) -> T
main:7: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T

- case: test_pipe_function_missing_kwarg
- case: test_mypy_pipe_function_missing_kwarg
main: |
from xarray import DataArray

Expand All @@ -91,7 +91,7 @@
main:7: note: def [P`2, T] pipe(self, func: Callable[[DataArray, **P], T], *args: P.args, **kwargs: P.kwargs) -> T
main:7: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T

- case: test_pipe_function_missing_keyword
- case: test_mypy_pipe_function_missing_keyword
main: |
from xarray import DataArray

Expand All @@ -106,7 +106,7 @@
main:7: note: def [P`2, T] pipe(self, func: Callable[[DataArray, **P], T], *args: P.args, **kwargs: P.kwargs) -> T
main:7: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T

- case: test_pipe_function_unexpected_keyword
- case: test_mypy_pipe_function_unexpected_keyword
main: |
from xarray import DataArray

Expand All @@ -118,7 +118,7 @@
out: |
main:7: error: Unexpected keyword argument "kw" for "pipe" of "DataWithCoords" [call-arg]

- case: test_pipe_tuple_return_type_dataarray
- case: test_mypy_pipe_tuple_return_type_dataarray
main: |
from xarray import DataArray

Expand All @@ -128,7 +128,7 @@
da = DataArray().pipe((f, "da"), 42)
reveal_type(da) # N: Revealed type is "xarray.core.dataarray.DataArray"

- case: test_pipe_tuple_return_type_other
- case: test_mypy_pipe_tuple_return_type_other
main: |
from xarray import DataArray

Expand All @@ -139,7 +139,7 @@

reveal_type(answer) # N: Revealed type is "builtins.int"

- case: test_pipe_tuple_missing_arg
- case: test_mypy_pipe_tuple_missing_arg
main: |
from xarray import DataArray

Expand All @@ -164,7 +164,7 @@
main:17: note: def [P`9, T] pipe(self, func: Callable[[DataArray, **P], T], *args: P.args, **kwargs: P.kwargs) -> T
main:17: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T

- case: test_pipe_tuple_extra_arg
- case: test_mypy_pipe_tuple_extra_arg
main: |
from xarray import DataArray

Expand Down
Loading
Loading