Our current dependency versioning can put users in a tight spot due to being overly narrow. This task is to loosen our dependency specifications but to do so in a safe and testable way. And at the same time we want to maintain supply chain attack mitigation measures for library developers.
Problem
For example: if you start a project and install geopandas 1.1.3 then install epymorph 1.1.0, this will result in a dependency resolution failure because we declare geopandas~=0.14.4 (which allows >=0.14.4 but <0.15.0). It's entirely likely that geopandas 1.1.3 would work just fine however, so this could lead to user frustration and the potential for incompatibilities when combined with other libraries.
Dependencies may have overly strict lower bounds (epymorph works with earlier versions) as well as overly strict upper bounds (epymorph works with newer versions). We should strive to ensure that all bound assertions are backed by evidence of suitability, primarily in the form of automated tests.
The drawback is the potential for exponential growth in the testing surface. The maximalist test matrix consists of all supported Python versions and all operating systems (as it currently does) as well as all combinations of all dependency versions in the supported range. (Just considering direct dependencies, we currently declare 16; if each had only 4 released versions that would be 16^4 or 65,536 unique combinations, times 4 Python versions, times 3 operating systems and you're already at 786,432.) Clearly we have to be strategic about which versions we would test.
Proposed solution
As a starting point, we should consider adopting the following project policies and tools to help address this issue. (This is a sketch of a solution with a few things still to consider.)
- uv.lock is for developers and represents a known-good dependency resolution shared with the development team for the sake of keeping a consistent dev environment. It can be advanced as-needed/as-desired, and to keep
uv audit clean of CVE warnings. It is generated with uv's "exclude-newer" setting as a supply chain attack mitigation.
- Runtime dependencies: use minimal ">=" lower bounds and, only if necessary, "<" upper bounds. Upper bounds should only be used for known-incompatible versions.
- Dev dependencies: use "~=x.y.z" specifiers. These are primarily governed by uv.lock, but the compatible specifier makes it trivial to safely upgrade dev deps when regenerating uv.lock. Major/minor dev dep upgrades should be intentional.
- New GitHub Actions workflows will help us spot dependency issues in real time.
- Min-resolution test: run a test matrix using uv's
--resolution lowest strategy (ignore uv.lock) to ensure that the library works with the oldest claimed versions. This runs weekly, as it's possible (however unlikely) that low versions can shift over time (e.g., a release is yanked).
- Max-resolution test: run a test matrix using uv's
--resolution highest strategy (ignore uv.lock) to ensure that the library works with the newest claimed versions. This runs weekly, as it's expected that new versions will be published regularly.
- (EXISTING) Push test: tests/fast will be run on every push, just ubuntu, just Python min ver, using uv.lock to quickly catch issues in the dev cycle.
- (EXISTING) Canary test: monthly canary test is also single platform and uses uv.lock, as its goal is to detect issues in external systems.
- The min- and max-resolution tests both exclude deps newer than one week for safety, and they both report issues with unclean
uv audit results (maybe?).
- When CVEs affect the project deps, I think the appropriate strategy is to bump the minimum version to resolve the CVE unless we have a high certainty that our usage is not affected by the vulnerability, as long as a version is in the supported range that does resolve the CVE. This gives the end user a bit of flexibility to choose to update or not depending on their project needs. I need to think about this a bit more though.
We should also have smoke tests which run an example project without any of the dev deps installed. I'm worried about catching situations where, say, pytest provides a transitive dep that we secretly rely upon thereby invalidating the test.
Exemplars
Other libraries use this loosely-bounded strategy.
Our current dependency versioning can put users in a tight spot due to being overly narrow. This task is to loosen our dependency specifications but to do so in a safe and testable way. And at the same time we want to maintain supply chain attack mitigation measures for library developers.
Problem
For example: if you start a project and install geopandas 1.1.3 then install epymorph 1.1.0, this will result in a dependency resolution failure because we declare geopandas~=0.14.4 (which allows >=0.14.4 but <0.15.0). It's entirely likely that geopandas 1.1.3 would work just fine however, so this could lead to user frustration and the potential for incompatibilities when combined with other libraries.
Dependencies may have overly strict lower bounds (epymorph works with earlier versions) as well as overly strict upper bounds (epymorph works with newer versions). We should strive to ensure that all bound assertions are backed by evidence of suitability, primarily in the form of automated tests.
The drawback is the potential for exponential growth in the testing surface. The maximalist test matrix consists of all supported Python versions and all operating systems (as it currently does) as well as all combinations of all dependency versions in the supported range. (Just considering direct dependencies, we currently declare 16; if each had only 4 released versions that would be 16^4 or 65,536 unique combinations, times 4 Python versions, times 3 operating systems and you're already at 786,432.) Clearly we have to be strategic about which versions we would test.
Proposed solution
As a starting point, we should consider adopting the following project policies and tools to help address this issue. (This is a sketch of a solution with a few things still to consider.)
uv auditclean of CVE warnings. It is generated with uv's "exclude-newer" setting as a supply chain attack mitigation.--resolution loweststrategy (ignore uv.lock) to ensure that the library works with the oldest claimed versions. This runs weekly, as it's possible (however unlikely) that low versions can shift over time (e.g., a release is yanked).--resolution higheststrategy (ignore uv.lock) to ensure that the library works with the newest claimed versions. This runs weekly, as it's expected that new versions will be published regularly.uv auditresults (maybe?).We should also have smoke tests which run an example project without any of the dev deps installed. I'm worried about catching situations where, say, pytest provides a transitive dep that we secretly rely upon thereby invalidating the test.
Exemplars
Other libraries use this loosely-bounded strategy.