Repository: readthedocs/readthedocs.org

Issue:
Build: `--upgrade-strategy eager` upgrade packages that were already pinned
## Details

* Read the Docs project URL: https://readthedocs.org/projects/qdax/
* Build URL (if applicable): https://readthedocs.org/projects/qdax/builds/18693487/
* Read the Docs username (if applicable): https://readthedocs.org/profiles/felixchalumeau/

A recent PR from the project QDax has it's readthedocs's stage of CI [broken](https://github.com/adaptive-intelligent-robotics/QDax/pull/108) although there has been no core change in this PR. More generally speeking, all our recents PRs have a broken documentation, although they branch from develop that had a working CI.

Interestingly, I cannot reproduce the error when I try to serve the documentation on my local computer with `mkdocs serve` (everything is fine locally).

## Expected Result

I would expect the docs to build.

## Actual Result

Cf. to details section of this issue, the doc does not build on readthedocs while no breaking change happened in the code and while it seems to work locally.


Discussion:
Hi @felixchalumeau! Make sure you are using the exact same versions of Python packages locally than on Read the Docs build environment. Take a look at https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html

At first sight, I'd guess there is a version difference in `pytkdocs` since it's where the dictionary key is not found.
Hi @humitos 

Thanks for you quick answer!

First, good news, I fixed my issue. It was not due to docs requirements but to other requirements from the project.

Second, I wanted to ask you a question about the situation that I encounter. My project is indeed using the good practices presented in the link your shared. But some uncertainty in the requirements used in the final build of the docs comes from the last step from the process in readthedocs : `pip install --upgrade --upgrade-strategy eager --no-cache-dir .`, which overrides some dependencies from `requirements.txt` by using those from `setup.py`. 

This creates some uncertainties because it seems that the good practice is to not fix the dependencies in setup.py files.

This is what happened in my case. One of my dependencies is pinned in my requirements but not in the setup, and was hence updated during the `pip install .` and created the issue. 

Do you think it would be possible/better to install the package while keeping the exact requirements given in the requirement files? 


Hrm, yeah. I'm not sure why we are using `--upgrade-strategy eager`

> eager - all packages will be upgraded to the latest possible version. It should be noted here that pip’s current resolution algorithm isn’t even aware of packages other than those specified on the command line, and those identified as dependencies. This may or may not be true of the new resolver.

It's weird to me that we are telling `pip` to upgrade all the packages at this step since the packages where already installed from pinned version in the `requirements.txt` 🤔 . I suppose we could review old issues and discussions about this to have a better answer here.

As a workaround, you could install your package manually by using `build.jobs.post_install` and remove the `python.install.method`:

```yaml
build:
  jobs:
    post_install:
      - pip install .
```

_(read the docs at https://docs.readthedocs.io/en/latest/build-customization.html)_

However, I'd say that Read the Docs should not use `eager` upgrade method since that definitely bring unexpected situations like this one.
@felixchalumeau We have a loot of good experiences with pip-tools. We use it to manage two versions of our requirements: The loosely pinned requirements, and the exactly pinned requirements.

See more: https://github.com/jazzband/pip-tools
Running into this issue today specifically due to the need to pin some geospatial libraries (GDAL). Would be great to have some control over these install flags!

Resolving patch:
diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py
--- a/readthedocs/doc_builder/python_environments.py
+++ b/readthedocs/doc_builder/python_environments.py
@@ -70,15 +70,15 @@ def install_package(self, install):
                     ','.join(install.extra_requirements)
                 )
             self.build_env.run(
-                self.venv_bin(filename='python'),
-                '-m',
-                'pip',
-                'install',
-                '--upgrade',
-                '--upgrade-strategy',
-                'eager',
-                '--no-cache-dir',
-                '{path}{extra_requirements}'.format(
+                self.venv_bin(filename="python"),
+                "-m",
+                "pip",
+                "install",
+                "--upgrade",
+                "--upgrade-strategy",
+                "only-if-needed",
+                "--no-cache-dir",
+                "{path}{extra_requirements}".format(
                     path=local_path,
                     extra_requirements=extra_req_param,
                 ),


Test patch:
diff --git a/readthedocs/projects/tests/test_build_tasks.py b/readthedocs/projects/tests/test_build_tasks.py
--- a/readthedocs/projects/tests/test_build_tasks.py
+++ b/readthedocs/projects/tests/test_build_tasks.py
@@ -1509,7 +1509,7 @@ def test_python_install_pip(self, load_yaml_config):
                     "install",
                     "--upgrade",
                     "--upgrade-strategy",
-                    "eager",
+                    "only-if-needed",
                     "--no-cache-dir",
                     ".",
                     cwd=mock.ANY,
@@ -1550,7 +1550,7 @@ def test_python_install_pip_extras(self, load_yaml_config):
                     "install",
                     "--upgrade",
                     "--upgrade-strategy",
-                    "eager",
+                    "only-if-needed",
                     "--no-cache-dir",
                     ".[docs]",
                     cwd=mock.ANY,
@@ -1594,7 +1594,7 @@ def test_python_install_pip_several_options(self, load_yaml_config):
                     "install",
                     "--upgrade",
                     "--upgrade-strategy",
-                    "eager",
+                    "only-if-needed",
                     "--no-cache-dir",
                     ".[docs]",
                     cwd=mock.ANY,