Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support lock dependencies with lowest solvable versions #3527

Open
2 tasks done
JorgeGarciaIrazabal opened this issue Jan 2, 2021 · 27 comments
Open
2 tasks done

Support lock dependencies with lowest solvable versions #3527

JorgeGarciaIrazabal opened this issue Jan 2, 2021 · 27 comments
Labels
kind/feature Feature requests/implementations status/needs-consensus Consensus among maintainers required status/triage This issue needs to be triaged

Comments

@JorgeGarciaIrazabal
Copy link

JorgeGarciaIrazabal commented Jan 2, 2021

  • I have searched the issues of this repo and believe that this is not a duplicate.
  • I have searched the documentation and believe that my question is not covered.

Feature Request

Context

When I am implementing a library, it would be super useful to run my tests with all the supported dependencies defined with the constrains in pyproject.toml. This will prevent errors in production when, for example, I use some new features of a dependency and forget to update the dependencies constrains (this is hard to identify when you are coding).

Because running the tests with all the possible combinations of all your supported libraries is virtually impossible, maybe running them with the lowest versions solvable solution and also the highest (I think this is the default behavior)

Proposal

  • add the flag --use-lowest-versions to the cli entrypoints (install, sync, lock, shell, and run)
  • these commands will create and/or use a different lock file poetry.lowest.lock
  • a different venv will be created by just adding lowest after the project name like this: {project_name}-lowest-{id}-{python-version}

Main simple use case

In ci do the following steps

  • Install environments
>>> poetry install 
>>> poetry install --use-lowest-versions
  • run tests for highest versions
poetry run pytest  --junit-xml pytest.xml
  • run tests for lowest versions
poetry run --use-lowest-versions pytest  --junit-xml pytest.xml

Note: I would appreciate if anyone has a better name for the flag (naming is the hardest part :P)

Also, I looked a the code and I think I know where add this logic, I can create a PR if you like the idea

@JorgeGarciaIrazabal JorgeGarciaIrazabal added kind/feature Feature requests/implementations status/triage This issue needs to be triaged labels Jan 2, 2021
@sinoroc
Copy link

sinoroc commented Jan 2, 2021

You might want to look into tox, it is very good at this kind of things.

@JorgeGarciaIrazabal
Copy link
Author

You might want to look into tox, it is very good at this kind of things.

Thank you for the rapid response @sinoroc.

tox could definitely help to run the tests, but this feature is for defining the dependencies instead. (As far as I know, you have to provide the dependencies to tox)

This feature will simplify the definition of an environment with the lowest possible dependency versions which could potentially be an environment where final users could install the package with pip install {my_library}.

So for example, if my library support flask = "^1.0.0" and I start using a feature that was only implemented in flask 1.1.0, this problem will be hidden as you will normally test your library with an environment with the latest versions solved with poetry lock.

As you can imagen, this could cause bad user experience as users with flask 1.0.0 will be able to install my library but still get errors that could look like ModuleNotFoundError: No module named flask.{new_feature}

Sorry if tox offers this feature already that I am not aware of, if so, could you please share an example @sinoroc ?

@sinoroc
Copy link

sinoroc commented Jan 2, 2021

tox allows you test against multiple versions of the same library, for example here:

So you would need to specify a test environment that tests your project against the lowest boundary of the version range for flask for example, and one for the highest, and then a couple more significant ones in the middle.

Actually, usually you would pick some significant libraries your project relies on: flask is a perfect example. And then create one test environment for each major.minor version that your project is supposed to support. So you would have a test environment for flask==1.0.* and a test environment forflask==1.1.*.

And then if your test suite, actually covers all the features of flask that your project uses, then you will immediately know if something is wrong when the test suite fails in one of the test environments.

@JorgeGarciaIrazabal
Copy link
Author

exactly, that is a way to do it, I guess I am just lazy that I don't want to do that manually and prefer to have poetry to define my lower and higher dependency boundaries in a "smart way".

Although there is manual configuration that can be done, I still think having this feature can improve the dev-cycle and be useful for multiple cases.

I found in the code that by just changing the reverse flag here:

packages.sort(
key=lambda p: (
not p.is_prerelease() and not dependency.allows_prereleases(),
p.version,
),
reverse=True,
)

it will prioritize the lower versions and then by just connecting with the commands, it can easily be implemented (hopefully)

@sinoroc
Copy link

sinoroc commented Jan 2, 2021

From my point of view, this is a case where you really want to have a human brain deciding which combinations of versions of dependencies should be tested.

prefer to have poetry to define my lower and higher dependency boundaries in a "smart way"

Not sure what you mean here.

Poetry really gives you just 1 combination, which does not necessarily end up including the highest bound of your dependency.

Unless everyone using your library also uses your lockfile (which is most likely not the case for obvious reasons), everyone will likely end up with a different combination of dependencies, nothing poetry can do about it, different tools have different dependecy resolution algorithms that deliver different results. So it is up to you to pick up some meaningful combinations of dependencies to test against.

Dependencies and dependencies of dependencies are not as simple as one might think, testing only the lowest and the highest is not meaningful in my eyes. Again poetry just offers you 1 working combination, not the "highest" bound (whatever that could possibly mean).

@JorgeGarciaIrazabal
Copy link
Author

Thank you @sinoroc your feedback is always very clear and instructive, but I have a couple of comments.

From my point of view, this is a case where you really want to have a human brain deciding which combinations of versions of dependencies should be tested.

related to that ☝️ , I kind of think the opposite. In an ideal world, you should only have to define your dependencies constrains (we do this in pyproject.toml already) and then a computer would go through the dependency graph and decide what are the best set of dependencies to tests. I am not saying this is easy to do or if poetry should be responsible for it, but I think it is theoretically doable and this feature request is a step closer to that world.

Poetry really gives you just 1 combination, which does not necessarily end up including the highest bound of your dependency.

About this ☝️ , poetry claims to get the latest versions of the dependencies with the update command https://python-poetry.org/docs/cli/#update

# update

In order to get the latest versions of the dependencies and to update the poetry.lock file, you should use the update command.

and looking at the code and it looks like the lock command does something similar.

Also, I agree with you that testing only the lowest and the highest dependencies might not be fully meaningful in some cases, but it can complement the manual approach you mentioned before (https://stackoverflow.com/q/59377071) and definitely improve the current normal behavior of only testing you application once.

Any way, I feel like this can help the community, but if you don't think this is useful, I can build an external script to solve my issues otherwise

Thank you again for the patience and all the feedback :)

@sinoroc
Copy link

sinoroc commented Jan 3, 2021

I totally see your point, and I wish it were possible to automate this as you described (more or less), I am just not confident it is technically feasible in a meaningful way (there are just way too many moving parts, finding one working combination is already hard enough). Just my personal opinion, the maintainers will decide. Maybe there is a compromise to be found somewhere, or probably this could be feasible with a plug-in.

@Spectre5
Copy link

I really like this idea and was looking for this functionality myself. A library will often try to keep the version dependencies as open as possible so that it is as compatible as possible with other libraries that may be installed into the same virtual environment. But since you're local environment may have newer library versions (in your lock file), it is easy to use functionality from a library that was introduced after your lowest allowed library versions. Of course you cannot check all combinations of dependencies, but I think checking all of the minimum permissible versions and the latest/newest versions would give pretty strong support if yours test coverage is good.

@oprypin
Copy link

oprypin commented May 4, 2021

I think, rather than asking Poetry to solve for the lowest possible version, one could ask it to install exactly the lowest specified versions of everything.

On one hand, this can seem restrictive, as one would need to manually fix versions until this becomes solvable. But on the other hand, I think it's better, because

  1. isn't it very good to always be sure that the exact combination of all lowest versions is installable?
  2. if Poetry did have such a feature in the solver, what use would it be, seeing as it would resolve to some unknown versions, rather than the ones you wrote? (i.e. you're still not sure that your min versions are truthful)

And, well, then it also becomes definitely technically feasible.

For now I'll be doing literal replacement of "^1.2.3 to "==1.2.3...

sed -i -E 's/"(\^|>=)([0-9])/"==\2/' pyproject.toml

And I'm actually happy with that, because I don't pull in Poetry for my CI anyway.

My CI: oprypin/mkdocs-literate-nav@ffab008

@pohlt
Copy link

pohlt commented Oct 5, 2021

IMHO, this feature makes a lot of sense. It is under discussion for pip as well.

@Spectre5
Copy link

Spectre5 commented Oct 10, 2021

Ya, I don't understand how library authors are not clamoring for this. If you put a minimum version in your dependency, it makes 100% sense to test with that version in case you accidentally use a feature from a newer version of it. Then you can decide to bump the dependency version or re-work your code to maintain that older compatibility. I've seen this happen on multiple projects and each would have been saved by testing against the minimum version dependencies.

@nugend
Copy link

nugend commented Feb 7, 2022

I had a script that tweaked the lock file to do just this (use lowest version of all specified deps), but it’s now busted because of the sha256 check fix in 1.1.10

Not that I’m suggesting that shouldn’t work as is, it’s just annoying that there’s not a way to ask poetry to do this at the moment.

@davegaeddert
Copy link
Contributor

davegaeddert commented Feb 11, 2022

Ya, I don't understand how library authors are not clamoring for this. If you put a minimum version in your dependency, it makes 100% sense to test with that version in case you accidentally use a feature from a newer version of it. Then you can decide to bump the dependency version or re-work your code to maintain that older compatibility. I've seen this happen on multiple projects and each would have been saved by testing against the minimum version dependencies.

I just opened a PR to fix this kind of problem in Poetry itself... I was trying to figure out how you could test (and prevent) this kind of thing and having an option to install minimum versions feels like a really good answer. Searched to see if it existed and found this issue...

@Spectre5
Copy link

This exact issue you created a PR for regarding urllib3 is one of the places I've seen this crop up a number of times.

@davegaeddert
Copy link
Contributor

I went off of @JorgeGarciaIrazabal's comment and played with here if anyone is curious: https://github.com/davegaeddert/poetry/pull/1

Very rough stab and I'm sure there's all kinds of quirks that could come out of it. But, it was enough for me to poke around some of my own packages and do poetry upgrade --min-versions and then run my tests, and realize there's usually a problem with the min requirements.

@steve-mavens
Copy link

steve-mavens commented May 11, 2022

I've just cobbled together something for an internal library where, in CI tests, I first poetry install and test, then pip install a whole bunch of old stuff over that env and test again. This means my test setup code duplicates information from pyproject.toml. Before doing that work I looked for this exact feature and found it doesn't exist yet: so if poetry would just magically sort it for me I would be very happy!

I appreciate that I could be more thorough about the testing, and that checking "top and bottom" is only one step better than what I was doing before, which is to only automate testing the top versions of everything. But my library has 10 dependencies, so it's not really feasible to run a test matrix over every combination of versions that I claim to support, and "top and bottom" feel like the places most likely to catch bugs.

What I discovered, in case anyone is interested in the benefits of this feature, is that my declared minimum dependency versions passed my tests on Python 3.8, but core-dumped on Python 3.10. So that was good to know, and now I've bumped my dependency constraints for Python 3.10 to versions that actually claim to work on 3.10 (in their changelogs, I mean, as opposed to just according to some wildly optimistic python=^3.8 that was probably put into their dependencies before 3.10 existed, never mind them testing on it).

@RoelantStegmann
Copy link

RoelantStegmann commented Apr 20, 2023

I am not sure just 'minimum versions' covers all the use-cases. A current lock file might also not have the max version.

If I can formulate it differently:

  • In the pyproject.toml file, I want to have the packages that I need. I prefer to keep them open ended. Specifying a max version here makes people lazy and stops incremental progress.
  • As long as it gives me a single lock pyproject.toml for a working development environment, it is great to work with. When something new is needed, people can run poetry update to move onward.
  • However, when I'm distributing, I want to generate a range of lock files.
    • Python[5]: I want to test at most 3 python versions (low, some majors in between, latest)
    • Pandas[3]: I want to test at most 3 pandas versions (low, middle, max)
    • numpy[2]: I want to test at most 2 numpy versions (low, max)
  • Yes, this will give me at most 30 (!!) environments, which I can then use with tox to test for.
  • Now when I use poetry build, I would want to specify that in case of open ended requirements, it's not allowed to go above the highest tested version. Thén I feel pretty safe that my distribution is good, and will stay good :)

@steve-mavens
Copy link

steve-mavens commented Apr 20, 2023

For my use case, I would want it to be the "minimum" in the same sense that poetry update gives you the "maximum": if everything at max is a valid solve, then it locks to the max, otherwise something has to give way and poetry will decide what. Open-ended in effect means that the "maximum" is the latest release at the time of the solve: otherwise the same definitions apply.

If everyone in the world set all their constraints (including Python) to "*", then poetry update would give you the current latest release of everything, and this new proposed minimum solve would give you the first ever non-yanked release of everything (which, let's face it, won't actually work with each other or on your current Python: which is why lower bounds are essential).

I accept that this doesn't provide a full testing strategy, and I certainly wouldn't object to a means of defining a testing matrix within the poetry config and allowing poetry to spit out a lock file for each point in the matrix. I wouldn't use that right away, though. I would use a minimum solve right away, because if my tests don't all pass on the lower-bound versions I declare for all my dependencies, then as the publisher of a library that makes me a lying liar. My goal is to avoid falsely claiming compatibility with old versions that my code doesn't really work with.

@TiansuYu
Copy link

Is there any update on this PR? I am developing a lib, and I would like to have the same behavior as well. To have all my libs pinned to the lower boundary in lock file, so I am always testing my lib in a most backward compatible way to my users.

@sinoroc
Copy link

sinoroc commented Feb 20, 2024

uv seems to have support for something like this: https://github.com/astral-sh/uv?tab=readme-ov-file#resolution-strategy

uv is not comparable to Poetry, though. It is only an installer, not a development workflow tool. But now I guess we can say that there is "prior art" in this domain.

@TiansuYu
Copy link

The point is that I don't want to have another "better pip" in python. There is already WAY too many tools, yet none can have all the major use cases covered. If poetry declares that it supports both App and Lib development, then it should live up to its promises; not pointing to another lib A for case A and lib B for case B. I don't see that this is a healthy way of thinking how things should be done.

And in terms of your argument, I think you are overthinking what the majority of lib devs want for this tool. Ofc it would be great that we can test against all the combos that the Lib wants to have. But in reality, many will be satiesfied that we have tested on the oldest version, knowing that most dependencies will keep to the SemVer convention.

If I really want to have this combo testing strategy, yes, I will setup tox in addition to add onto this. But for a minimal approach, I would love to have a switch on Poetry to stick the lock to that.

@radoering
Copy link
Member

I assume we are open to such a feature in general.

I'm not convinced about adding the flag to each of the commands (lock, install, run) as proposed in the intial request. Handling several lockfiles and several environments might be something that should be thought of more general. However, that's a bigger topic and maybe we can keep it a little simpler to start with.

Maybe, it's sufficient to start with a flag for poetry lock. If you provide this flag, the result will just be written into the default lock file and you can just use the result with each command. (I know that this is not that comfortable if you often want to switch between lowest and latest but we have to start somewhere.)

Regarding lowest versions, I think there are two strategies that make sense:

  1. Just find a solution with lowest possible versions. That means it might not be the lower bound for each dependency if some of them are not compatible with each other. That's simple to implement, basically Support lock dependencies with lowest solvable versions #3527 (comment)
  2. Force the lowest version of each direct dependency and fail if there is no solution. Basically, Support lock dependencies with lowest solvable versions #3527 (comment) That's a bit more difficult to implement because you have to tinker with the constraints.

My proposal would be poetry lock --strategy=lowest for the first one and poetry lock --strategy=force-lowest for the second one.

So if someone wants to implement this feature I believe this description should lead to an implementation that will probably be accepted.

@TiansuYu
Copy link

@radoering Thanks. I agree with the "start small" approach. We can always expand the scope and converge on UX later. But I am happy to see there is something happening to cover this area.

Thanks a lot for the great work Poetry team! Looking forward to see this getting release.

@steve-mavens
Copy link

steve-mavens commented Feb 26, 2024

Just a warning (of course ignore me if you like): if you define it now to overwrite the standard poetry.lock file, then changing it in future to write a separate lock file would be a breaking change. Doesn't stop you doing it, but might restrict when you can do it.

@radoering
Copy link
Member

Yeah, I know. It makes sense to think about the target image. Unfortunately, I'm not sure about that. I just know, I don't really like the initial proposal to add --use-lowest (or --strategy if there are several) to each command. Maybe, something like poetry env use to activate a specfic lockfile is more comfortable? Or maybe I'm wrong and it's fine to have a flag for lock, run, shell, show and export? (It probably doesn't make sense for add, remove and update though.)

Maybe, you are right and we should define the target image before and decide afterwards if it is a good idea to start with overwriting the default lockfile. Several questions come to my mind:

  • Will most users use lowest in addition to default locking? Then, it probably does not make sense to overwrite the default lockfile in the long term.
  • Will most users want to have two venvs to switch between lowest and default or prefer one venv (less space) so you have to install when you switch (takes longer)?

@radoering radoering added the status/needs-consensus Consensus among maintainers required label Feb 26, 2024
@steve-mavens
Copy link

Adding my one datapoint - I mostly would not actively use both environments on my development machine, just like I mostly don't have separate python3.8/3.9/3.10/3.11/3.12 environments for every component I work on. It's in CI where I run all the permutations. I don't mind if CI commands are verbose because I only have to type them once, ever.

Personally for library development I don't check in any lock files, and I believe that's common. But possibly for test reproducibility some people would want to be able to check in both lock files. They could still do that even if poetry writes its output always to the same file poetry.lock, but they'd have to rename that file before checking it in, which is maybe a bit un-ergonomic. Then again, if they're checking in lock files for test reproducibility, they might already have multiple different lock files to test multiple scenarios, and this "oldest solve" is just one more they want to add to their list. So they must already be able to manage this.

@TiansuYu
Copy link

TiansuYu commented Mar 4, 2024

FYI, take a look at https://github.com/astral-sh/uv?tab=readme-ov-file#resolution-strategy for their resolution strategy --resolution=lowest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/feature Feature requests/implementations status/needs-consensus Consensus among maintainers required status/triage This issue needs to be triaged
Projects
None yet
Development

No branches or pull requests