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

ENH: Don't restrict the upper Python version or use "<4" #24810

Closed
espdev opened this issue Sep 26, 2023 · 34 comments
Closed

ENH: Don't restrict the upper Python version or use "<4" #24810

espdev opened this issue Sep 26, 2023 · 34 comments

Comments

@espdev
Copy link
Contributor

espdev commented Sep 26, 2023

Proposed new feature or change:

Hello,

NumPy 1.26.0 restricts the Python version from above to 3.13.

This makes it impossible to install the package in projects which do not restrict the Python version from above and where a modern package manager like Poetry is used.

For example:
The project Python version is specified as: >=3.10,<4
NumPy Python version is specified as: >=3.9,<3.13

When we try to add numpy dependency to the project we get a dependency resolving error:

The current project's Python requirement (>=3.10,<4.0) is not compatible with some of the required packages Python requirement:
  - numpy requires Python <3.13,>=3.9, so it will not be satisfied for Python >=3.13,<4.0

Because no versions of numpy match >1.26.0,<2.0.0
 and numpy (1.26.0) requires Python <3.13,>=3.9, numpy is forbidden.
So, because <the project> depends on numpy (^1.26.0), version solving failed.

Why restrict the Python version from above? Is backwards compatibility not working anymore?
NumPy 1.25.0 restricts the Python version as: Python >=3.9

@mattip
Copy link
Member

mattip commented Sep 26, 2023

That sounds like everything is working as designed. You cannot install numpy 1.26.0 into python>=3.13.

@espdev
Copy link
Contributor Author

espdev commented Sep 26, 2023

@mattip

This works as intended "on paper" by the specification, but it doesn't work in the real world in practice because most projects and libraries don't restrict the Python version from above within the minor version (3.x). Restriction from below is reasonable and necessary, restriction from above is almost meaningless in the vast majority of cases and it only gets in the way.

@ngoldbaum
Copy link
Member

The upper version caps in numpy releases are new but scipy has been doing it for a while, see scipy/scipy#17957 and links therein for more context.

I think poetry not allowing installation because there are possible version mismatches on different versions of python from what is installed is a strange decision. There's not much NumPy can do to change how poetry handles upper version bounds. I think you will have to make your version bounds a bit more complicated from here on out, matching what NumPy does, see the poetry docs.

@espdev
Copy link
Contributor Author

espdev commented Sep 26, 2023

@ngoldbaum

I understand the position of SciPy dev team and all their pain with the ABI, but capping the upper Python version affects a lot of projects and libraries. It just breaks what was working. Everyone's gonna have to do the same things: capping the upper Python version. Why?

However, NumPy and SciPy have worked for years in thousands of packages without capping the upper Python version. I understand what the developers are trying to convey, but it's basically shifting problems from the sick head to the healthy head. Imagine you're using a bunch of dependencies that need NumPy and suddenly it all breaks just because NumPy restricted the upper Python version. All your package management is going to hell. Because it's very difficult to get all authors of all these packages to follow the same rule (because it's very inconvenient and meaningless in their case).

We can all just write tests and specify Trove classifiers for the versions of Python that we support, from the minimal version all the way up to the latest current version instead of suffer the headache of upgrading the upper Python version every time for all our projects and libraries.

@espdev espdev changed the title ENH: Don't restrict the Python version from above or use "<4" ENH: Don't restrict the upper Python version or use "<4" Sep 27, 2023
@rgommers
Copy link
Member

It just breaks what was working. Everyone's gonna have to do the same things: capping the upper Python version.

This is either a Poetry bug, or the Poetry devs want you to do this. The Poetry docs seem to hint at the latter. I'm not sure though, since the Poetry devs did not participate in the relevant discussions even when pinged (as far as I know).

There's also the matter of propagating caps on other dependencies. E.g., SciPy has for a long time capped NumPy with the <N+3 rule (see https://numpy.org/devdocs/dev/depending_on_numpy.html#runtime-dependency-version-ranges). So if your package depends on both numpy and scipy, does Poetry force you to add your own <N+3 cap on numpy? If so, that's consistent (but I'm not sure that kind of propagation is actually useful. If not, then I'd consider it a Poetry bug to behave differently only for Python.

@seberg
Copy link
Member

seberg commented Sep 27, 2023

capping the upper Python version. Why?

Because it is the right thing for a library like NumPy (EDIT: And I like optimistic pins for smaller libs). It is very unlikely (probably already known to be true) that 3.13 will not work with NumPy 1.26.
Advertising anything else will just give a much worse opposite problem where poor end users have Python 3.13 in the future but somehow install a numpy==1.26.x due to version pins that then blow up with compilation failures.

Going to be the trigger happy person here and close this. This is a discussion for poetry devs/users to figure out.

@seberg seberg closed this as completed Sep 27, 2023
@espdev
Copy link
Contributor Author

espdev commented Sep 27, 2023

This is a discussion for poetry devs/users to figure out.

Any modern package management tool: pip with strict dependency resolving, PDM, Poetry will work problematic with restriction of upper Python version.

I'm not the first and I guess I'm not the last person to raise this issue.

Package management in Python has always been an issue. Modern package management tools give us a chance to fix things, or at least do so with less pain, but things break down again because of, I think, misuse and misunderstanding of the right concepts.

Good luck, everybody.

@seberg
Copy link
Member

seberg commented Sep 27, 2023

I'm not the first and I guess I'm not the last person to raise this issue.

Yup, and I even agree with them! But as far as I remember all of those discussions give reasons that don't really apply to this situation.
I am very confident that NumPy won't work with Python 3.13, so to me removing the pin is a lie. The discussions were never about not pins that tell the truth, they are about pins that are cautious telling the users what should work, rather than being confident about it really not working. Especially for projects that don't actually update the pins very regularly.

So, if you can argue that there is a decent chance that NumPy compiles with Python 3.13, then you can get out those arguments. I don't think that was true for any of the recent Python minor releases and Python isn't exactly slowing down when it comes to changing things these days.

From my prespective those discussions might be an argument to look at your own <4 pin. Why do you have it? The only reason I can see is if you are using the stable C-API. Since, requiring to recompile those (with maybe small fixes?) is the only reason for Python 4 that I can imagine right now: A pure Python lib probably can currently hope it will mostly work with Python 4, so the arguments actually do apply!

But honestly, this annoyance is really only about poetry. You say <4 and NumPy saying <3.13 from an end-user perspective who installs it, seems perfectly right and doesn't unreasonably limit the possibility of installing things.

@BeRT2me
Copy link

BeRT2me commented Oct 4, 2023

When poetry says

For numpy, a possible solution would be to set the `python` property to ">=3.10,<3.13"

It does NOT mean to change:

[tool.poetry.dependencies]
python = "^3.10"
numpy = "^1.26.0"

to

[tool.poetry.dependencies]
python = "^3.10,<3.13"
numpy = "^1.26.0"

It means to change it to:

[tool.poetry.dependencies]
python = "^3.10"
numpy = {version = "^1.26.0", python="^3.10,<3.13"}

Which resolves just fine!

If a future user is trying to use whatever outdated version of your package exists when 3.13 comes out, then it'll break because numpy won't get installed. But this allows them to try a newer version of numpy when using 3.13 (which may or may not work, up to them!) ... but more likely than not, you'll have a newer version by then, with a newer, compatible version of numpy pinned in a similar manner.

@mattip
Copy link
Member

mattip commented Oct 5, 2023

numpy = {version = "^1.26.0", python="^3.9,<3.13"}

Isn't this what the metadata for numpy 1.26 already states? Why the repetition?

@andyfaff
Copy link
Contributor

andyfaff commented Oct 5, 2023

@mattip, I think that was what is recommended for the downstream project.

@rgommers
Copy link
Member

rgommers commented Oct 5, 2023

Thanks for the clarification @BeRT2me, that's quite useful - and seems much more reasonable as behavior than what the initial issue description here suggested.

Isn't this what the metadata for numpy 1.26 already states? Why the repetition?

My guess is that it'll help Poetry do quicker/better solves. It is already in the numpy metadata, but Poetry cannot know that without a PyPI REST API call to query that metadata.

If you look at Spack for example, it has the numpy version ranges for numpy explicitly too: https://github.com/spack/spack/blob/bec873aec97c8c2f77353a2bc3b84d00335bf849/var/spack/repos/builtin/packages/py-numpy/package.py#L93-L100. It doesn't need to propagate those to packages declaring a dependency on numpy, but that's because it already has the metadata to query locally, there's no REST API call to avoid.

@henryiii
Copy link
Contributor

henryiii commented Oct 5, 2023

The Python ecosystem does not support upper caps in Requires-Python. I've proposed fixing this, but nothing has been done: https://discuss.python.org/t/requires-python-upper-limits/12663.

TL;DR: This field was designed to allow old Python versions to be dropped via backsolving. This is great for lower caps but wrong for upper caps. What's worse, locking solvers like Poetry and PDM solve for all Python version at once, and they don't have a way to specify a separate range for the locking solver and the package public metadata. So every single Poetry/PDM user will now have to put <3.13 or they will get NumPy 1.25 forever, and if they do, they now will also require <3.13, even if they don't need to change code (most user code doesn't change on a Python update), even if they don't lock their NumPy version!

If you run pip in CPython 3.13, it backsolves, since that's what this field was designed for:

$ pyenv install 3.13-dev
$ ~/.pyenv/versions/3.13-dev/bin/pip install numpy
Collecting numpy
  Downloading numpy-1.25.2.tar.gz (10.8 MB)
...

PS: The current holdup in moving forward fixing this at the ecosystem level is there's no way to tell if a specifier set failed because the version was too new, too old, or somewhere in the middle of a complicated expression (like >3.9,!=3.9,<3.12). It either matches or fails the specifier you place here. Unless you can detect that, you can't specify a different behavior for upper bounds. A way to get this from packaging is needed before further action is taken. I'd like to add the ability to compute overlaps, so you could ask python_requires.intersect(">={sys.version_info}"). If None, then this was restricted by an upper bound.

You can see a long discussion on Numba and they finally dropped their added upper cap, and only have a check with a nicer error message in setup.py, and have been very happy with the results. I assume you could either make the meson file error out if Python was unsupported, or an option could be added to meson-python to add a nice error. (The whole point is to get a nice error, otherwise you could just let the build go and see the real error).

@henryiii
Copy link
Contributor

henryiii commented Oct 5, 2023

Which resolves just fine!

I'd never thought of that, but it's not really the correct solution, even if it's possibly better than the Python limit since it lets your dependencies avoid the mess; it's still producing broken metadata all to get Poetry's locking solver happy. If you depend on NumPy, you do want it to install on 3.13 if a user installs your package. By the time 3.13 comes out, there will be a version of NumPy supporting that, so you just want that, you don't want no NumPy at all. It would only be correct if you were capping or pinning NumPy.

The correct solution (which isn't possible, sadly) would be to have separate bounds for making the lock file and the package metadata. You probably only want to solve for existing Python versions, but your package metadata can (and usually should) allow for future Python and NumPy versions.

@BeRT2me
Copy link

BeRT2me commented Oct 6, 2023

@mattip @rgommers The repetition is because otherwise you're technically breaking the provided rules by having both python=^3.10 and numpy=^1.26.0 specified... Since you allow python>=3.13, while NumPy does not.

I'm not saying I agree with what NumPy has done here, but I believe my suggestion is a far better approach to the Poetry issue than capping your own project's version just because you use a library that made a controversial choice.

Providing a nice error message via an __init__.py is a good idea though, something like:

try:
    import numpy
except ImportError as e:
    raise ImportError(
        "See <link-to-fix> for help installing numpy",
    ) from e

@henryiii

would be to have separate bounds for making the lock file and the package metadata

That is possible!

[tool.poetry.dependencies]
python = "^3.10"
numpy = [
    {version = "*", python=">=3.13"},
    {version = "^1.26.0", python=">=3.10,<3.13"},
]

Which puts both 1.25.2 in your lockfile for the hypothetical py3.13, and 1.26.0 for existing versions compatible with your project.

The published METADATA then includes:

Requires-Python: >=3.10,<4.0
Requires-Dist: numpy (>=1.26.0,<2.0.0) ; python_version >= "3.10" and python_version < "3.13"
Requires-Dist: numpy ; python_version >= "3.13"

Which is reasonable - given the circumstances.

@BeRT2me
Copy link

BeRT2me commented Oct 6, 2023

@henryiii

If you run pip in CPython 3.13, it backsolves, since that's what this field was designed for:

Poetry does this as well, this difference is poetry add numpy is equivalent to pip install numpy>=1.26.0. As if it were running in any of your allowed Python versions. Both of which I'm sure would fail on CPython3.13.

If I wanted Poetry to act like pip, I'd do poetry add numpy="*" which would give 1.25.2 ... I use Poetry because it doesn't act like pip.

Personally, I'd rather be warned: "Hey, add the proper python marker to numpy if you're going to specify that as a dependency without thinking about the version" - rather than having it arbitrarily break the rules I've specified and been given, or have it output distro Metadata that doesn't actually match my pyproject.toml.

@seberg
Copy link
Member

seberg commented Oct 6, 2023

TBH, while I don't care what we do (the one argument that is somewhat convincing to me is that there is in practice almost no gain in adding the upper bound). But, I find most of this rather impenetrable collection of arguments...
Which probably just shows my ignorance and this is all clear to specialists. Maybe you should start with adding a clear recommendation that no upper bound be specified at this point due to ecosystem limitations(?) on that field in the docs, even when the upper bound is clearly correct?!
That would certainly at least help to convince individual projects to change their pattern.

My understanding:

  • The upper bound is really OK for libraries like NumPy or numba (there isn't a real argument that NumPy/numba "might actually work with Python 3.13"). Yes, an upper bound is not really good for the vast majority of packages, though!
  • The field was meant to be used for backsolving, fine. The one issue I understand around that that seems to be that e.g. pip will just pick up an older version of NumPy missing the upper bound:
    • Things will still fail. To me what this gives is the argument that there is very little gain in providing the upper bound. But I don't understand that it makes it particularly wrong to add it.
  • There is something about locking solvers/poetry which makes things very annoying for "reasons"...
    • Are Python versions handled differently from library versions!? Having upper bounds on NumPy is common in some form (<2 or current+.2, etc.). If not, isn't this a much bigger issue/annoyance!?
    • Is this a decision in poetry or upstream tooling that doesn't even allow poetry to do something more reasonable?
    • Can this be solved while ignoring all the rest of the discussion about picking up old NumPy versions which are missing the tag?!

I don't care about removing the upper bound, but I am still unhappy about what comes across as tooling forcing libraries to not provide correct information. The original issue still sounds to me like "Poetry is making my life hard because NumPy is providing correct metadata". You cannot resolve a fixed NumPy version for future Python at this time. That is clearly correct! I suspect this is what you mean with:

The correct solution (which isn't possible, sadly) would be to have separate bounds for making the lock file and the package metadata.

So what does it take for this being the correct solution? Isn't this a poetry issue? Why can't poetry use no bounds in the metadata, but lock things to <3.13 based on the NumPy constraint?

@rgommers
Copy link
Member

rgommers commented Oct 6, 2023

@seberg I think you're mostly asking the right questions, and yes it's a Poetry / "design of Python packaging" problem. For context: there are still significant gaps in the conceptual model for metadata of Python packages. On the one hand, PyPA folks will tell you that sdists/PyPI is more flexible/generic than conda/debian/etc, and the metadata applies in any environment - and that that's why PyPI is special and other distros should rely on / pull from PyPI. On the other hand, when you then add metadata that is (a) correct, and (b) also in fact needed in other distros, there's a "oh but this is special because pip/poetry does it like X".

Example of where this exact metadata for Python version support is necessary and now actually manually guessed by peeking at what CPython versions we release wheels for:

https://github.com/spack/spack/blob/e20c05fcdfc7a9ad995956bfee269e123860288f/var/spack/repos/builtin/packages/py-numpy/package.py#L93-L100

See https://pypackaging-native.github.io/key-issues/pypi_metadata_handling/ for more connected issues.

The correct solution (which isn't possible, sadly)

I think at some point we're going to have to bite that bullet (or multiple bullets). The "wheel metadata can't differ from sdist metadata" that you (@henryiii) are preparing a PEP for, as well as this "sdist metadata isn't actually generic" will need to be resolved in a consistent conceptual manner sooner or later (probably later).

@seberg
Copy link
Member

seberg commented Oct 6, 2023

Clearly, it is true that for the majority of libraries it is impossible to give a correct upper version. And I can only agree that upper version limits on Python must be useless at least in almost all cases until some large changes happen.
(Which is a heroic effort and it's good to know that at least something is moving slowly!)

For the right now, it sounds a like removing the upper bound is the pragmatic thing because for "reasons" it's sometimes problematic and it doesn't really help anyone in practice.

What would help me be more convinced would probably be (one or both):

  • A guidance from PyPA. (A better description for "Requires-Python" metadata field pypa/packaging.python.org#1274 seems short of that. It only removes the suggestion to add an upper bound. It doesn't quite make it clear that there is agreement on a correct upper bound being discouraged.)
  • Clarity that the behavior of poetry isn't actually exactly what poetry wants under the assumption that the upper bounds are correct. Maybe poetry even has docs that discourage upper version bounds for Python?

@BeRT2me
Copy link

BeRT2me commented Oct 7, 2023

@seberg I believe it is exactly the behavior that poetry wants, because it can't do more than follow the rules you've given it. Their error message says python property, not python version. Just because people expect it to read their minds, doesn't mean it knows what you want without specification. Could they do a better job of hand-holding the user through some of these tough configurations? Absolutely, but that's for them to figure out.

A hypothetical to back this up. Say NumPy stopped all development today and never pushed a single update.

  • The claim that my project works on >=3.13 with numpy>=1.26.0 would be blatantly false.
    Even when NumPy does push updates as expected, the underlying claim my project is making is still that it'll work with numpy==1.26.0 when using >=3.13, which again is false.

It seems silly to expect Poetry to implicitly choose that I say: "Trust me bro, I can see the future and I know NumPy will come out with a 3.13 compatible version by the time you use my library in 3.13." ... This is essentially what people are asking for.

  • It's reasonable to say, "This version and any currently hypothetical higher versions will probably still work, so I'll let the future try things out for themselves."
    It's not reasonable to have something like Poetry assume you want to say, "Although I know this version won't work in the future, just pray a hypothetical higher version will."
    • However, If I do want to say this, Poetry is perfectly fine letting me do so. ENH: Don't restrict the upper Python version or use "<4" #24810 (comment) It won't ever give me a "correct" Lockfile by telling me exactly what to install for 3.13 (How could it know??), but that's okay because the Lockfile should only be used for development, It's not the distro's Metadata...

If I have a pyproject.toml like:

[tool.poetry.dependencies]
python = "^3.10"
numpy = "^1.26.0"

And I run pip install . It'll install NumPy just fine.

  • pip's promise is that it'll work in this environment, and maybe warn you about possible conflicts within that environment.
  • Poetry's (Lockfile especially) is a promise that it'll work in all environments, given what it knows. It's not like it can be blamed when it says no to the user asking it to break that promise.

@rgommers
Copy link
Member

rgommers commented Oct 7, 2023

Nice explanation, thanks @BeRT2me. I think it's aligned then: it's what Poetry wants and what NumPy wants too. If you use CPython from its main branch or an alpha/beta, we want you to install NumPy's main too - the latest release on PyPI is unlikely to work (and we regularly get bug reports, then have to explain that no you can't use a released version).

@andyfaff
Copy link
Contributor

andyfaff commented Oct 7, 2023

Could we have a definitive comment on what entries downstream users should put in their pyproject.toml files? There's a lot of comments and it's not clear on which of those it should be.

@seberg
Copy link
Member

seberg commented Oct 7, 2023

The claim that my project works on >=3.13 with numpy>=1.26.0 would be blatantly false.

From a lockfile perspective, yes, since there is no way to specify versions that work. But it is not correct from a metadata one! Your project will work with 3.13 (or is assumed to work)! It may fail to resolve valid depdencies, but failing earlier is just harmful.

If poetry wants to encode this type of thing into metadata, it needs a better way to do that. That needs a poetry issue to point to and if that issue isn't actionable at this time, that is fine.

What rubs me (and probably others) wrong about this is that a big chunk of the discussion (when it isn't about how to fix things) to me comes across as "NumPy is doing a terrible thing! It does exactly what poetry wants it to do and it makes my life as a poetry user hard. So NumPy should not do this.".

@seberg
Copy link
Member

seberg commented Oct 7, 2023

And to be clear: I agree that peotry forcing things that belong into lockfiles to end up into metadata is absolutely terrible because it forces completely nonsense version bounds on libraries just because they have dependencies. So, yes, I think that is probably enough of a reason to change what NumPy does, but can we also shout at poetry very clearly?

@BeRT2me
Copy link

BeRT2me commented Oct 8, 2023

@andyfaff

$ poetry add numpy@latest
Using version ^1.26.0 for numpy

Updating dependencies
Resolving dependencies... (0.0s)

The current project's Python requirement (>=3.10,<4.0) is not compatible with some of the required packages Python requirement:
  - numpy requires Python <3.13,>=3.9, so it will not be satisfied for Python >=3.13,<4.0

Because no versions of numpy match >1.26.0,<2.0.0
 and numpy (1.26.0) requires Python <3.13,>=3.9, numpy is forbidden.
So, because ai-testing depends on numpy (^1.26.0), version solving failed.

  • Check your dependencies Python requirement: The Python requirement can be specified via the `python` or `markers` properties
    
    For numpy, a possible solution would be to set the `python` property to ">=3.10,<3.13"

    https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies,
    https://python-poetry.org/docs/dependency-specification/#using-environment-markers

Poetry states to specify the python requirement via the python or markers properties. Suggesting, for NumPy, to set its python property to >=3.10,<3.13.

It then provides links to how to do this using either "Python restricted dependencies" or "environment markers".

The very next entry in their documentation is for Multiple constraints dependencies, with which you can add NumPy as a * dependency for >=3.13 if you so please. There really isn't any other alternative since the version that will support 3.13 is unknown.

@rgommers
Copy link
Member

rgommers commented Oct 8, 2023

The very next entry in their documentation is for Multiple constraints dependencies,

Interesting, I hadn't seen that section before. It specifier exactly what Poetry wants its users to do here, which is indeed declare different version ranges of Python for different NumPy versions.

And that seems to equally apply to lower bounds? If we dropped Python 3.8 support in NumPy 1.25.0 and your package supports numpy 1.24.0 and up, it seems to want:

[tool.poetry.dependencies]
numpy = [
    {version = "~1.24.0", python = ">=3.8"},
    {version = "^1.25.0", python = ">=3.9"}
]

?

@BeRT2me
Copy link

BeRT2me commented Oct 9, 2023

@seberg

Advertising anything else will just give a much worse opposite problem where poor end users have Python 3.13 in the future but somehow install a numpy==1.26.x due to version pins that then blow up with compilation failures.

Any library that has a hard pin like this deserves to have its users blow up with compilation failures. If I set numpy>=1.26, and my user is using Python 3.13... the only ways they get an incompatible version are from:

  1. Another library setting an upper version.
  • That's the other library's fault, setting an upper Python version didn't fix this.
  1. Because a Python 3.13 compatible version of NumPy simply doesn't exist yet.
  • That's a NumPy issue, setting an upper Python version didn't fix this. A simple search on the internet will tell any user why it didn't work... but once one is released it'll just work.

Neither of these issues are solved by adding an upper version to NumPy...

The hypothetical end user being affected in any way by NumPy not having an upper version set... is a logical fallacy, in every scenario, something else causes their issue.

@BeRT2me
Copy link

BeRT2me commented Oct 9, 2023

@rgommers For development, Poetry will always choose the highest compatible version for all dependencies.

  • If I set python=^3.8 and numpy=^1.24, it'll install 1.24.4 locally for development.
    • If I tried to add numpy=^1.25, it'd fail acceptably with:

      numpy requires Python >=3.9, so it will not be satisfied for Python >=3.8,<3.9

    • This holds me to not accidently using 1.25 specific additions that'd be impossible in 3.8, while still allowing my users to work with 1.25+.
  • If I set python=^3.9 and numpy=^1.24; python>=3.9,<3.13, it'll install 1.26.0 locally for development.
    • But this has the side-effect of dropping NumPy from my lib's dependencies when using it in python 3.13. Probably not a problem, but it just feels bad.
  • If I set python=^3.9 and numpy=^1.24, it'll install 1.25.2 locally for development.
    • Although I'd like it to install 1.26.0.
    • It is my responsibility as the developer to bump the minimum version of NumPy if I use something only available in a newer release. Adding a maximum python-version in no way helps this to happen.
    • It is NumPy's responsibility to not make new releases with incompatible APIs, at least not without a Major Version bump. I trust them to do so.

However, If NumPy had not implemented a Maximum python version.

  • If I set python=^3.9 and numpy=^1.24, it'd install 1.26.0 locally for development, which is what the expected behavior would be when using Poetry.

If I do like you suggest, and have set python=^3.8, then... first off it fails. But after a minor adjustment:

numpy = [
    {version = "~1.24.0", python = ">=3.8,<3.9"},
    {version = "^1.25.0", python = ">=3.9"}
]

The version used for local development now appears to arbitrarily change depending on the version of python being used. Installing 1.24.4 when developing in python 3.8, and installing 1.25.2 in python>=3.9.

If I make a further mistake and do:

numpy = [
    {version = "~1.24.0", python = ">=3.8,<3.9"},
    {version = "^1.25.0", python = ">=3.9,<3.13"}
]

Then I'm now developing with 1.26.0 in python>=3.9, rather than the 1.24.4 of simply saying numpy = "^1.24".


The change appears, to the casual user, to have caused Poetry to arbitrarily refuse to update to 1.26.0 for development, when according to how the rules have always worked... it should only ever refuse to update based on my lowest compatible python version.

The question becomes, "If I'm only ever going to be developing with the highest compatible version of NumPy - Currently 3.26.0 for my lowest compatible version of Python 3.9, why should I have to care that this version may be incompatible with a higher version of python?" (Especially one that hasn't even come out yet).

If the API changes enough, I trust NumPy to do a Major version bump, but otherwise I expect a compatible version to exist for my users by that point in time... making me jump through hoops to express that trust seems like an odd decision by NumPy.

  • A side point, just because Poetry provides the tools to manually fight our way past this dependency hell, doesn't mean that they necessarily want their users to have to do so. Poetry's decision has been to follow the rules and expect those rules to be loose enough to allow us to act as consenting adults with reasonable expectations... this change, made for the sake of adding more "correct" rules, only pushes the community as a whole towards breaking the rules. I don't think it matters whether those rules are "correct" or not if those rules only serve to make things worse without solving any discernable issue.

@rgommers
Copy link
Member

rgommers commented Oct 9, 2023

I have quite a hard time following these arguments, but regarding "Poetry's decision has been to follow the rules": that means "its own rules" I assume (which, given the question sentence below, rely or try to force strict SemVer versioning). Because, as @seberg said right at the start, these bounds are correct.

It is Poetry's design that is at fault here, and making life harder than it should for its users - bold caps don't change that. I just woke up to a question about a build failure in a really old SciPy version that didn't have any upper bounds, and had to spend 15 minutes reverse engineering upper bounds based on release dates of various versions to add the right bounds into metadata in Spack. So I'm not feeling particularly charitable on this point right now - these upper bounds are (a) correct, and (b) very useful information that I don't want to have to reverse engineer or get right with trial and error years later.

It is NumPy's responsibility to not make new releases with incompatible APIs, at least not without a Major Version bump. I trust them to do so.

You cannot trust NumPy, or CPython, or pretty much any other major Python package, to stricly follow SemVer. That is just not how it works in the Python ecosystem. There are deprecations and removals that happen between minor versions, and that is not going to change.

Is that indeed the actual problem in the Poetry design here? It's designed with the assumption Python follows SemVer, while that is clearly not the case?

@seberg
Copy link
Member

seberg commented Oct 9, 2023

Is that indeed the actual problem in the Poetry design here? It's designed with the assumption Python follows SemVer, while that is clearly not the case?

I think there are two problems? The first one is that poetry has no way to limit the Python version, but rather downgrades NumPy, because it thinks that NumPy 1.25.x supports Python 3.13. (A problem that will go away in a few years, but until then might mean libs lock in old versions of NumPy which just makes them less compatible, but poetry thinks it is more compatible.)

I also thought that one problem with Poetry is that:

  • NumPy has a (correct) upper bound
  • Library B using NumPy is nudged towards adding the same bound, although there are other solutions and poetry maybe even prefers those
    • This is OK from a lockfile perspective
    • It is incorrect from a metadata/pip perspective, but Poetry will use it for that also: unlike NumPy the library will clearly work with Python 3.13. (As far as I understood, I may be wrong.)
  • This means: Library B (and even its downstream) may end up advertising completely baseless upper bounds.

Which gives us the danger of proliferation of incorrect upper bounds in downstream libraries and that could end up being a nightmare for end-users/packagers when Python 3.13 comes out?

@henryiii
Copy link
Contributor

henryiii commented Oct 9, 2023

A few quick comments:

  • This is not Poetry's problem, all locking solvers that solve for a range of Python versions will have this issue. The root of the problem is the solver range and the public metadata range are the same; if they could be specified differently, it wouldn't be an issue.
  • Conda isn't affected, because in Conda, you can solve for the Python version itself. That fixes the whole mess. Otherwise, the only usefulness of an upper cap is to change the error message, or for most packages that actually do support the next Python version, add an error messages instead of just working. (And this is why Python version is special in Poetry, though they write it like any other package, which is weird).
  • An end user should not be forced to handle every dependency's limits for you! They should be able to just write "numpy>=X.Y" and let NumPy handle Python versions automatically. That used to work, but now every Poetry or PDM user has to manually inject multiple lines to get the the right metadata, and they have to update them every year. This is not scalable to lots of dependencies! Lots of packages follow NumPy's example, even if they don't need this cap!
  • Pip also doesn't handle a cap correctly. It's going to be about a year before you see the effects of this, but it will cause weird failures once 3.13 is out, much slower solves, and lots of unnecessary downloads of incompatible wheels to check the metadata (maybe less with the new JSON API). That's because this field is not a minimum and a maximum, it's just a SpecifierSet, and if that set doesn't match, a solver is supposed to backsolve. That only makes sense if the specifier set specifies a minimum. The ecosystem isn't designed (yet?) to support maximums. And since many, many packages would add maximums that don't need them, which could cause Python updates to be harder, not easier, I'm not sure there's much motivation to work quickly to a solution.
  • Yes, there is a real limit and yes it's (very mildly1) useful information, but this metadata field currently is not the place to put it. You can just check to see what binaries are supplied, that is much more reliable and works for the entire history of NumPy. Other ecosystems (like Spack) might benefit from a limit, that's great for them, but they don't use our metadata anyway.

Footnotes

  1. What use does knowing the upper limit actually provide in practice? As far as I know, it's only useful for error messages, since you can't downgrade a user's Python. Limits are there to help solvers produce working solves, but this one can't do that.

@seberg
Copy link
Member

seberg commented Oct 12, 2023

I will reopen this since I think we decided to remove it (PRs welcome).

(I honestly still can't really condense all of these arguments into what and where the big problems are and what are more arguments along something being infeasible but actually working as designed. I also still think that if this is so clear cut, there should be a link to documentation that says so, rather than meandering discussions with 100+ posts.)

@seberg seberg reopened this Oct 12, 2023
@seberg seberg added this to the 1.26.2 release milestone Oct 12, 2023
@Vuizur
Copy link

Vuizur commented Oct 18, 2023

I found a simple example where this leads to problems: If you create a new poetry project using Python 3.12 and then install spacy with the command poetry add spacy, you get the error Backend 'setuptools.build_meta:__legacy__' is not available. (which doesn't return anything useful when you google it.) As discussed earlier, the reason is that you get numpy 1.25.2 because of the upper bound, which only exists for the newer and not the older numpy version. (If I understand it correctly, this problem will apply to each project depending on numpy.)

A new user would probably be stuck at that point, because the solution of adding Python version constraints in the pyproject.toml is not obvious at all, and there is nothing hinting at it in the log. I think it's great that numpy will solve this problem soon 👍. (There was a discussion in the poetry issues, and IIRC they said it would be too difficult to implement a fix for this specific scenario.)

The only other alternative would be convincing every single project using numpy to put

[tool.poetry.dependencies]
python = "^3.10"
numpy = [
    {version = "*", python=">=3.13"},
    {version = "^1.26.0", python=">=3.10,<3.13"},
]

into their pyproject.toml, which doesn't seem sustainable and also would have to be updated each year.

Edit: And installing pandas is similarly confusing, with poetry add pandas you get:

Using version ^2.1.1 for pandas

Updating dependencies
Resolving dependencies...

The current project's Python requirement (>=3.10,<4.0) is not compatible with some of the required packages Python requirement:
  - numpy requires Python <3.13,>=3.9, so it will not be satisfied for Python >=3.13,<4.0
  - numpy requires Python <3.13,>=3.9, so it will not be satisfied for Python >=3.13,<4.0

Because no versions of numpy match >1.26.0,<1.26.1 || >1.26.1
 and numpy (1.26.0) requires Python <3.13,>=3.9, numpy is forbidden.
And because numpy (1.26.1) requires Python <3.13,>=3.9, numpy is forbidden.
Because no versions of pandas match >2.1.1,<3.0.0
 and pandas (2.1.1) depends on numpy (>=1.26.0), pandas (>=2.1.1,<3.0.0) requires numpy (>=1.26.0).
Thus, pandas is forbidden.
So, because newpythontest depends on pandas (^2.1.1), version solving failed.

  • Check your dependencies Python requirement: The Python requirement can be specified via the `python` or `markers` properties

    For numpy, a possible solution would be to set the `python` property to ">=3.10,<3.13"
    For numpy, a possible solution would be to set the `python` property to ">=3.10,<3.13"

    https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies,
    https://python-poetry.org/docs/dependency-specification/#using-environment-markers

seberg added a commit to seberg/numpy that referenced this issue Oct 19, 2023
While the upper version is technically correct for the released
version of NumPy (we are sure it will not work on Python 3.13)
advertising it creates some problems, mostly for locking resolvers.
They try to guess correct versions for non-released Python versions...

This is probably an ecosystem or just "reasons", but it seems less
useful than trouble to do the correct advertising here.

See numpygh-24810 for *way* too much discussion about the why (and you will
still be confused afterwards probably, so...).
This needs to be fixed or at least documented clearer upstream by
PyPA or similar, but...

Closes numpygh-24810
charris pushed a commit to charris/numpy that referenced this issue Nov 11, 2023
While the upper version is technically correct for the released
version of NumPy (we are sure it will not work on Python 3.13)
advertising it creates some problems, mostly for locking resolvers.
They try to guess correct versions for non-released Python versions...

This is probably an ecosystem or just "reasons", but it seems less
useful than trouble to do the correct advertising here.

See numpygh-24810 for *way* too much discussion about the why (and you will
still be confused afterwards probably, so...).
This needs to be fixed or at least documented clearer upstream by
PyPA or similar, but...

Closes numpygh-24810
@ilayn
Copy link
Contributor

ilayn commented Nov 13, 2023

I've hit with this poetry lock issue myself again with different packages and definitely not to ignite the discussion again. I'm with @seberg in not understanding the counter arguments and I am also not knowledgeable on the subject. So please let's not restart.

However I'd like to invite to the issues we will have when 3.13 or further versions break SciPy/NumPy and let the folks see exactly what we are talking about. Please leave any emoji to this one so I can ping you later and discuss exactly what we mean and how we should solve it when it happens. If possible I'd like to invite poetry devs too. Because it is much easier to look at a postmortem instead and it seems to me that counter argument owners never saw our codebase breaking with every major release. Maybe stable ABI work will save us then but anyways. Fingers crossed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants