Environment
(This is an issue in the context of upgrading pip from one version to another)
Description
Upgrading a package after upgrading pip did not result in the same outcome as installing that package initially with the upgraded version of pip.
Specifically, the jupyter
package was installed with pip 8.1.1 using python 3.5.2 which resulted in its dependency package ipython
being installed to version 7.10.1. No error was produced for this. Later after pip install --upgrade pip
, the jupyter package was upgraded via pip install --upgrade jupyter
and this resulted in an error message that ipython requires a different version of python (>= 3.6).
When instead pip 19.3.1 is used from the beginning in a clean venv, no error is produced, and jupyter
's dependency ipython
is installed to version 7.9.0.
Thus, installing the jupyter package using the current pip version via pip install
results in a different configuration than installing the jupyter package via upgrading with pip install --upgrade
after updating the pip version.
Expected behavior
My intuitive expectation as a user is that the pip install --upgrade
command on a package results in the singular, deterministic setup of that package in its most updated form at that point in time. The expectation is the same from pip install
. The expected behavior is then that those two commands result in the same package configuration, but this was observed to not be the case.
How to Reproduce
Method 1:
python 3.5 -m venv .venv
. .venv/bin/activate
python3.5 -m pip install jupyter
python3.5 -m pip install --upgrade pip
python3.5 -m pip install --upgrade jupyter
Results in the error message below, that ipython
requires a different python version >=3.6
Method 2:
python3.5 -m venv .venv
. .venv/bin/activate
python3.5 -m pip install --upgrade pip
python3.5 -m pip install jupyter
No errors result.
Output
After Method 1 Step 1 above, it seems that jupyter installs successfully, and the following warning is shown:
You are using pip version 8.1.1, however version 19.3.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
At this point pip freeze
shows jupyter==1.0.0
and ipython==7.10.1
, and ipywidgets==7.5.1
.
After/during Step 2 the following error is shown:
Requirement already satisfied, skipping upgrade: ipython>=4.0.0; python_version >= "3.3" in /home/nafty/.local/lib/python3.5/site-packages (from ipywidgets->jupyter) (7.10.1)
ERROR: Package 'ipython' requires a different Python: 3.5.2 not in '>=3.6'
At this point pip freeze
shows the same versions still (jupyter 1.0.0, ipython 7.10.1).
After Method 2 no errors are shown and pip freeze
shows jupyter==1.0.0
and ipython==7.9.0
Summary
Seems to me that the outdated pip v8.1.1 installed jupyter with ipython version 7.10.1 which is a version that is not supposed to become installed on python 3.5.2 (but, I guess we cannot expect any guarantees of correctness from an outdated version of pip...). Then/however, the updated/current version of pip does not rectify this situation; perhaps this would be a reasonable expectation on pip...? Furthermore, this leads to a scenario where installing the 'current' version of the jupyter package via install --upgrade
results in a different configuration than via installing directly via pip install
using an updated pip, which is probably an undesired mismatched situation to be in...
Another issue is that while it is true that ipython 7.10.1 requires python >= 3.6 (as the error says), the situation is that I am trying to install jupyter which does not require ipython 7.10.1 (in fact, it naturally collects 7.9.0---but it pathologically already has 7.10.1 which shouldn't actually be there), so really python 3.6+ isn't actually required (unlike the error says...). This is a situation that is 'fixable' by pip, and a user would benefit from the situation being fixed.
(The actual error that produces this is a raising of UnsupportedPythonVersion exception in a call to _check_dist_requires_python, which is defined here).
My intuitive expectation as a user is that the
pip install --upgrade
command on a package results in the singular, deterministic setup of that package in its most updated form at that point in time.
This is not the case because pip only upgrades packages if needed. The behaviour that matches your expectation would be pip install --upgrade --upgrade-strategy=eager
. This was the default before pip 10.0, but was changed because the “always upgrade to latest” behaviour would introduce unexpected API breakage when a user does not intend to upgrade a dependency (e.g. IPython in your case).
My personal interpretation of the current behaviour is that Requires-Python
is checked when the dependency is first installed. pip does not enforce it again when checking whether it needs to be upgraded, because since it is installed it is presumed to be compatible, and there must be a reason if it it has an incompatible Requires-Python
(e.g. maybe the user explicitly disabled that check). But the “reason” you got that incompatible python_version
is because pip 8.x predates that configuration (Requires-Python
was implemented in pip 9.x in 2016), so the incompatible IPython was installed by mistake.
Technically this can be prevented if we also checks Requires-Python
when deciding whether a dependency needs to be updated. But this would break some currently working workflows (they will need to set --ignore-requires-python
), with only limited merit. So I’m tempted to say the current behaviour is good enough, and the user needs to fix the environment manually in edge cases like this.
I think pip
should still provide some functionality around this, so that users don't have to investigate substantially to understand the issue or be misled by the error message to think that they need to change the python version—which in the above case was not actually needed. It seems much more efficient that pip would identify this issue quickly by aggregating all the dependencies requirements and realizing that 7.10.1 is not needed than for the user to manually figure that out and fix it.
How about a non-breaking optional flag somewhere... Instead of making existing users need to use an --ignore-requires-python
flag, provide an optional --with-requires-python
or --check-requires-python
flag for a user in this situation to use. That way, after the error message, which warns about ipython being incompatible due to python version, the user will have something simple to do about it: pip install --upgrade --with-requires-python ipython
, and then pip would go ahead and figure out the actual requirement on ipython due to all the dependency relations to installed packages and realize that 7.10.1 is not actually needed, and essentially downgrade/fix to 7.9.0. (Maybe this is problematic in that a 'downgrade' results from --upgrade
, which is then counter intuitive..? Can be solved with an informative error message instead and a second --repair
step advised)
Another point is about pip check
, which returned No broken requirements found.
and/but is documented to "Verify installed packages have compatible dependencies." In this case the installed packages (jupyter) had a non-compatible dependency (ipython 7.10.1). Seems that pip check should have caught it. So maybe pip check should check Requires-Python
on every package that it checks..? Or even without that additional overhead an optional flag would still be useful so that the user can target it to a specific problematic package, like ipython above.
Again in the above case the error message was presented but didn't really advise the user of what to do about it... If anything, the error message misleads the user to thinking that changing to python 3.6+ is required, which was not actually the case. An improved pip check
feature would be useful here to understand the situation, such as by calling pip check --with-requires-python jupyter
(or ipython
directly, even..) which would result in pip discovering that ipython is incompatible due to Requires-Python
and also discovering that the actual requirement on ipython is just >=5.0.0
due to jupyter. This could then be reported to the user, and the user would be in a good position to do something to fix it, like confidently uninstalling and reinstalling ipython while knowing what to expect (that a version >=5.0.0 and <7.10.1 will be installed, so the result 7.9.0 will be acceptable), or even using an optional flag pip install --upgrade --with-requires-python --repair-broken ipython
-style command.
Most helpful comment
This is not the case because pip only upgrades packages if needed. The behaviour that matches your expectation would be
pip install --upgrade --upgrade-strategy=eager
. This was the default before pip 10.0, but was changed because the “always upgrade to latest” behaviour would introduce unexpected API breakage when a user does not intend to upgrade a dependency (e.g. IPython in your case).My personal interpretation of the current behaviour is that
Requires-Python
is checked when the dependency is first installed. pip does not enforce it again when checking whether it needs to be upgraded, because since it is installed it is presumed to be compatible, and there must be a reason if it it has an incompatibleRequires-Python
(e.g. maybe the user explicitly disabled that check). But the “reason” you got that incompatiblepython_version
is because pip 8.x predates that configuration (Requires-Python
was implemented in pip 9.x in 2016), so the incompatible IPython was installed by mistake.Technically this can be prevented if we also checks
Requires-Python
when deciding whether a dependency needs to be updated. But this would break some currently working workflows (they will need to set--ignore-requires-python
), with only limited merit. So I’m tempted to say the current behaviour is good enough, and the user needs to fix the environment manually in edge cases like this.