Skip to content

Commit 0e36556

Browse files
authored
Merge pull request #29 from openscm/resolution-strategies
Add tests of different resolution strategies
2 parents 7d69798 + c02c1ef commit 0e36556

26 files changed

+1050
-403
lines changed

.github/workflows/ci.yaml

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,53 @@ jobs:
104104
env:
105105
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
106106

107+
tests-resolution-strategies:
108+
strategy:
109+
fail-fast: false
110+
matrix:
111+
# Only test on ubuntu here for now.
112+
# We could consider doing this on different platforms too,
113+
# although that probably belongs better with the PyPI tests.
114+
os: [ "ubuntu-latest" ]
115+
# Tests with lowest direct resolution.
116+
# We don't do lowest because that is essentially testing
117+
# whether downstream dependencies
118+
# have set their minimum support dependencies correctly,
119+
# which isn't our problem to solve.
120+
resolution-strategy: [ "lowest-direct" ]
121+
# Only test against the oldest supported python version
122+
# because python is itself a direct dependency
123+
# (so we're testing against the lowest direct python too).
124+
python-version: [ "3.9" ]
125+
runs-on: "${{ matrix.os }}"
126+
defaults:
127+
run:
128+
# This might be needed for Windows
129+
# and doesn't seem to affect unix-based systems so we include it.
130+
# If you have better proof of whether this is needed or not,
131+
# feel free to update.
132+
shell: bash
133+
steps:
134+
- name: Check out repository
135+
uses: actions/checkout@v4
136+
- name: Setup uv
137+
id: setup-uv
138+
uses: astral-sh/setup-uv@v4
139+
with:
140+
version: "0.5.11"
141+
python-version: ${{ matrix.python-version }}
142+
- name: Create venv
143+
run: |
144+
uv venv --seed
145+
- name: Install dependencies
146+
run: |
147+
uv pip install --requirements requirements-only-tests-locked.txt
148+
uv pip compile --python ${{ matrix.python-version }} --resolution ${{ matrix.resolution-strategy }} --all-extras pyproject.toml -o requirements-tmp.txt
149+
uv pip install -r requirements-tmp.txt .
150+
- name: Run tests
151+
run: |
152+
uv run --no-sync pytest tests -r a -v
153+
107154
tests-without-extras:
108155
# Run the tests without installing extras.
109156
# This is just a test to make sure to avoid
@@ -135,7 +182,7 @@ jobs:
135182
run: |
136183
pip install --upgrade pip wheel
137184
pip install .
138-
pip install -r requirements-only-tests-locked.txt
185+
pip install -r requirements-only-tests-min-locked.txt
139186
- name: Run tests
140187
run: |
141188
pytest tests -r a -vv tests

.github/workflows/install-pypi.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ jobs:
2222
fail-fast: false
2323
matrix:
2424
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
25-
python-version: [ "3.9", "3.10", "3.11" ]
25+
# Test against all security and bugfix versions: https://devguide.python.org/versions/
26+
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
2627
# Check both 'library' install and the 'application' (i.e. locked) install
2728
install-target: ["continuous-timeseries", "continuous-timeseries[locked]"]
2829
runs-on: "${{ matrix.os }}"
@@ -67,7 +68,7 @@ jobs:
6768
python scripts/test-install.py
6869
- name: Install min test dependencies
6970
run: |
70-
pip install pytest pytest-regressions
71+
pip install -r requirements-only-tests-min-locked.txt
7172
- name: Run tests
7273
run: |
7374
# Can't run doctests here because the paths are different.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Test against upstream latest
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
# * is a special character in YAML so you have to quote this string
7+
# This means At 03:00 on Wednesday.
8+
# see https://crontab.guru/#0_0_*_*_3
9+
- cron: '0 3 * * 3'
10+
11+
jobs:
12+
tests-upstream-latest:
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
# Only test on ubuntu here for now.
17+
# We could consider doing this on different platforms too,
18+
# but this is mainly a warning for us of what is coming,
19+
# rather than a super robust dive.
20+
os: [ "ubuntu-latest" ]
21+
# Test against all bugfix versions: https://devguide.python.org/versions/
22+
# as they are latest and ones most likely to support new features
23+
python-version: [ "3.12", "3.13" ]
24+
runs-on: "${{ matrix.os }}"
25+
defaults:
26+
run:
27+
# This might be needed for Windows
28+
# and doesn't seem to affect unix-based systems so we include it.
29+
# If you have better proof of whether this is needed or not,
30+
# feel free to update.
31+
shell: bash
32+
steps:
33+
- name: Check out repository
34+
uses: actions/checkout@v4
35+
- name: Setup uv
36+
id: setup-uv
37+
uses: astral-sh/setup-uv@v4
38+
with:
39+
version: "0.5.11"
40+
python-version: ${{ matrix.python-version }}
41+
- name: Setup compilation dependencies
42+
run: |
43+
echo "python${{ matrix.python-version }}-dev"
44+
sudo add-apt-repository ppa:deadsnakes/ppa -y
45+
sudo apt update
46+
sudo apt install -y libopenblas-dev "python${{ matrix.python-version }}-dev"
47+
- name: Create venv
48+
run: |
49+
uv venv --seed
50+
- name: Install dependencies
51+
run: |
52+
uv pip install --requirements requirements-only-tests-locked.txt --requirements requirements-only-tests-min-locked.txt
53+
uv pip install --all-extras pyproject.toml
54+
uv pip install -r requirements-upstream-dev.txt
55+
- name: Run tests
56+
run: |
57+
uv run --no-sync pytest tests -r a -v

.pre-commit-config.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ ci:
44
autoupdate_schedule: quarterly
55
autoupdate_branch: pre-commit-autoupdate
66
# Currently network access isn't supported in the pre-commit CI product.
7-
skip: [uv-lock, uv-export, pdm-lock-check]
7+
skip: [uv-sync, uv-lock, uv-export, pdm-lock-check]
88

99
# See https://pre-commit.com/hooks.html for more hooks
1010
repos:
@@ -39,9 +39,9 @@ repos:
3939
args: [ --fix, --exit-non-zero-on-fix ]
4040
- id: ruff-format
4141
- repo: https://github.com/astral-sh/uv-pre-commit
42-
# uv version.
43-
rev: 0.5.11
42+
rev: 0.5.21
4443
hooks:
44+
- id: uv-sync
4545
- id: uv-lock
4646
name: uv-lock-check
4747
args: ["--check"]
@@ -55,11 +55,12 @@ repos:
5555
- id: uv-export
5656
name: export-requirements-docs
5757
args: ["-o", "requirements-docs-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--all-extras", "--group", "docs"]
58+
- id: uv-export
59+
name: export-requirements-only-tests-min
60+
args: ["-o", "requirements-only-tests-min-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--only-group", "tests-min"]
5861
- id: uv-export
5962
name: export-requirements-only-tests
6063
args: ["-o", "requirements-only-tests-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--only-group", "tests"]
61-
# # Not released yet
62-
# - id: uv-sync
6364
- repo: https://github.com/pdm-project/pdm
6465
rev: 2.22.1
6566
hooks:

changelog/29.docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added documentation on our dependency pinning and testing strategy.

changelog/29.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed the minimum versions of our requirements (also tested that installation with minimum versions works).

changelog/29.trivial.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added testing of our lowest direct dependencies.

docs/NAVIGATION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ See https://oprypin.github.io/mkdocs-literate-nav/
1717
- [Why this API?](further-background/why-this-api.py)
1818
- [Discrete to continuous conversions](further-background/discrete_to_continuous_conversions.py)
1919
- [Representations](further-background/representations.py)
20+
- [Dependency pinning and testing strategy](further-background/dependency-pinning-and-testing.md)
2021
- [Development](development.md)
2122
- [Pandas accessors](pandas-accessors.md)
2223
- [API reference](api/continuous_timeseries/)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Dependency pinning and associated testing strategy
2+
3+
<!--
4+
This text comes from the copier template.
5+
If you find you need to update your testing strategy,
6+
you probably want to update this too.
7+
-->
8+
Here we explain our dependency pinning and associated testing strategy.
9+
This will help you, as a user, to know what to expect
10+
and what your options are.
11+
As a developer, these docs can also be helpful to understand
12+
the overall philosophy and thinking.
13+
14+
## Dependency pinning
15+
16+
We use lower-bound pinning.
17+
In other words, we pin the lowest supported version of the packages on which we depend.
18+
As a user, this helps you get a working install
19+
while giving you freedom to use newer versions, should you wish.
20+
21+
We don't use upper-bound pins.
22+
The reason is that we have had bad experiences with upper-bound pinning.
23+
In the majority of cases, new releases do not cause issues
24+
so pinning simply forces users to workaround overly strict pins[^1]
25+
(which can be done, see
26+
[working around incorrectly set pins][working-around-incorrectly-set-pins]).
27+
The tradeoff with this approach is
28+
This does run the risk that,
29+
if a dependency releases a breaking change,
30+
the function provided by our package may break too.
31+
32+
[^1]:
33+
Yes, if the entire world followed semantic versioning perfectly,
34+
we could use upper-bound pins for the next major version with more confidence
35+
but that isn't the current state of the ecosystem.
36+
Even if it were, we still think this would result in unnecessary pins
37+
in many cases because many major releases are still compatible
38+
because most packages don't use the entire API of their dependencies.
39+
40+
### Working around incorrectly set pins
41+
42+
Despite our best efforts, it is possible that we will set our pins incorrectly.
43+
Part of this is because we simply cannot test all possible combinations of package installs
44+
(see [testing strategy][testing-strategy]),
45+
so we might miss valid/invalid combinations.
46+
47+
If we set our pins incorrectly and you need to effectively overwrite them,
48+
unfortunately there is currently no universal solution.
49+
There has been quite some discussion,
50+
see e.g. [this issue](https://github.com/pypa/pip/issues/8076),
51+
but no universal resolution.
52+
53+
However, for some environment managers, there is a solution.
54+
This comes in the form of dependency overrides,
55+
which allow you to override a package's stated dependencies
56+
(essentially fixing them on the fly,
57+
rather than having to fix them upstream).
58+
Here are the docs for the package managers that we know support this:
59+
60+
- [uv dependency overrides](https://docs.astral.sh/uv/concepts/resolution/#dependency-overrides).
61+
- [pdm dependency overrides](https://pdm-project.org/latest/usage/dependency/#dependency-overrides).
62+
63+
We do not know if this strategy can be used for packaging.
64+
For example, you are building package A.
65+
This depends on version 2 of package B and version 1 of package C.
66+
However, version 1 of package C (incorrectly) says
67+
that it is only compatible with version 1 of package B.
68+
We are not sure if the dependency overrides
69+
can be used to release a version of package A
70+
that can be relased to and installed from PyPI.
71+
If this is the situation you are in and you would like a resolution,
72+
please comment on [this issue](https://gitlab.com/openscm/copier-core-python-repository/-/issues/4).
73+
74+
## Testing strategy
75+
76+
We test against multiple python versions in our CI.
77+
These tests run with the latest compatible versions of our dependencies
78+
and a 'full' installation, i.e. with all optional dependencies too.
79+
This gives us the best possible coverage of our code base
80+
against the latest compatible version of all our possible dependencies.
81+
82+
In an attempt to anticipate changes to the API's of our key dependencies,
83+
we also test against the latest unreleased version of our key dependencies once a week.
84+
As a user, this probably won't matter too much,
85+
except that it should reduce the chance
86+
that a new release of one of our dependencies breaks our package
87+
without us knowing in advance and being able to set a pin in anticipation.
88+
As a developer, this is important to be aware of,
89+
so we can anticipate changes as early as possible.
90+
91+
We additionally test with the lowest/oldest compatible versions of our direct dependencies.
92+
This includes Python, i.e. these tests are only run
93+
with the lowest/oldest version of Python compatible with our project.
94+
This is because Python is itself a dependency of our project
95+
and newer versions of Python tend to not work
96+
with the lowest/oldest versions of our direct dependencies.
97+
These tests ensure that our minimum supported versions are actually supported
98+
(if they are all installed simultaneously,
99+
see the next paragraph for why this caveat matters).
100+
As a note for developers,
101+
the key trick to making this work is to use `uv pip compile`
102+
rather than `uv run` (or similar) in the CI.
103+
The reason is that `uv pip compile`
104+
allows you to install dependencies for a very specific combination of things,
105+
which is different to `uv`'s normal 'all-at-once' environment handling
106+
(for more details, see [here](https://github.com/astral-sh/uv/issues/10774#issuecomment-2601925564)).
107+
108+
We do not test the combinations in between lowest-supported and latest,
109+
e.g. the oldest compatible version of package A
110+
with the newest compatiable version of package B.
111+
The reason for this is simply combinatorics,
112+
it is generally not feasible
113+
for us to test all possible combinations of our dependencies' versions.
114+
115+
We also don't test with the oldest versions of our dependencies' dependencies.
116+
We don't do this because, in practice,
117+
all that such tests actually test is
118+
whether our dependencies have set their minimum support dependencies correctly,
119+
which isn't our problem to solve.
120+
121+
Once a week, we also test what happens when a user installs from PyPI on the 'happy path'.
122+
In other words, they do `pip install continuous-timeseries`.
123+
We check that such an install passes all the tests that don't require extras
124+
(for developers, this is why we have `tests-min` and `tests-full` dev dependency groups,
125+
they allow us to test a truly minimal testing environment,
126+
separate from any extras we install to get full coverage).
127+
Finally, we also check the installation of the locked versions of the package,
128+
i.e. installation with `pip install 'continuous-timeseries[locked]'`.
129+
These tests give us the greatest coverage of Python versions and operating systems
130+
and help alert us to places where users may face issues.
131+
Having said that, these tests do require 30 separate CI runs,
132+
which is why we don't run them in CI.
133+
134+
Through this combination of CI testing and installation testing,
135+
we get a pretty good coverage of the different ways in which our package can be used.
136+
It is not perfect, largely because the combinatorics become unfriendly.
137+
If we find a particular, key, use case failing often,
138+
then we would happily discuss whether this should be included in the CI too,
139+
to catch issues earlier than at user time.

docs/further-background/representations.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
# (see https://github.com/hgrecco/pint/blob/74b708661577623c0c288933d8ed6271f32a4b8b/pint/util.py#L1004)
3131
#
3232
# In short, we try and have as nice an experience for developers as possible.
33+
#
34+
# (As one other note/trick for representation of objects,
35+
# you can control how numpy represents its objects using
36+
# [numpy.set_printoptions](https://numpy.org/doc/stable/reference/generated/numpy.set_printoptions.html)).
3337

3438
# %% [markdown]
3539
# ## Imports

0 commit comments

Comments
 (0)