|
| 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. |
0 commit comments