Repository: readthedocs/readthedocs.org

Issue:
Do not install unsupported Pillow for Python 3.8+
## Details

* Read the Docs project URL: https://readthedocs.org/projects/pep-previews/
* Build URL (if applicable): https://readthedocs.org/projects/pep-previews/builds/16667606/


## Expected Result

Unnecessary dependencies are not installed (related https://github.com/readthedocs/readthedocs.org/issues/8208), and do not slow down the build.


## Actual Result

https://docs.readthedocs.io/en/stable/build-default-versions.html#external-dependencies says that Pillow 5.4.1 is an external dependency and that it "could be removed in the future".

We require Python 3.9 to build the PEPs docs and do not need Pillow.

Pillow 5.4.1 ([released Jan 2019](https://pypi.org/project/Pillow/5.4.1/#files)) supported Python 2.7 and 3.4-3.7, which means binary wheels are only available for those versions. Pillow 8.0 is the first version support to Python 3.9 with binary wheels:

* https://pillow.readthedocs.io/en/stable/installation.html#python-support 

This means Python 3.9 downloads the big sdist and builds from source, which is much slower than installing from wheel:

```
Collecting pillow==5.4.1
  Downloading Pillow-5.4.1.tar.gz (16.0 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.0/16.0 MB 136.4 MB/s eta 0:00:00
...
Building wheels for collected packages: mock, pillow, future
  Building wheel for mock (setup.py): started
  Building wheel for mock (setup.py): finished with status 'done'
  Created wheel for mock: filename=mock-1.0.1-py3-none-any.whl size=23772 sha256=3178833b900aad79f904be4ec39fd9218034a3c8eb93ff168e7655db36f4a7ef
  Stored in directory: /tmp/pip-ephem-wheel-cache-bnbn_mt2/wheels/44/dc/c7/e89296d3612588db0aa6545ee3305c23b7a48b3a9922916e1f
  Building wheel for pillow (setup.py): started
  Building wheel for pillow (setup.py): finished with status 'done'
  Created wheel for pillow: filename=Pillow-5.4.1-cp39-cp39-linux_x86_64.whl size=1361211 sha256=065d9d668909332f20be3ebc31f77207067f846cdf142b3395dceac8aa236828
  Stored in directory: /tmp/pip-ephem-wheel-cache-bnbn_mt2/wheels/d3/de/21/f0ad1b0453288616d3f4d2ce28431aa7a2adde8f27c9b7a0af
  Building wheel for future (setup.py): started
  Building wheel for future (setup.py): finished with status 'done'
  Created wheel for future: filename=future-0.18.2-py3-none-any.whl size=491070 sha256=a59cfed6bc119fcf842c411312471446a70d5791d55d8bf4f27a58d52cf62dbf
  Stored in directory: /tmp/pip-ephem-wheel-cache-bnbn_mt2/wheels/2f/a0/d3/4030d9f80e6b3be787f19fc911b8e7aa462986a40ab1e4bb94
Successfully built mock pillow future
...
Command time: 39s Return: 0
```

* https://readthedocs.org/projects/pep-previews/builds/16667606/

Some suggestions, in my order of preference:

* Do not install Pillow (as suggested by the docs, "could be removed in the future")
* Install a newer version of Pillow for Python 3.8+, that has binary wheels
* Cache the built wheels so they do not need to be rebuilt. But it's quite possible that Pillow 5.4.1 won't actually work on Python 3.8+, and at some point may even break the build.

I'm focusing on Pillow here as the main bottleneck, but we probably don't need most of the other [external dependencies](https://docs.readthedocs.io/en/stable/builds.html#external-dependencies), and would prefer to only install things we've explicitly specified.

But if we can begin by addressing Pillow, that would be a good first step. Thank you!


Discussion:
Thanks for reporting this. We are working on the "Future builders" (`build.jobs` and `build.commands` config keys) and moving in a direction where all these things can be configured by the user. There are other issues where this conversation is taking place and we will consider this use case as well. However, I don't have a specific and good answer for a workaround currently.
I did a quick query to our database and I found 1.5k projects in this situation (`Pillow==5.4.1` and Python 3.8) in the last month and a half.

```
In [3]: BuildData.objects.filter(
   ...:     data__packages__pip__all__contains=[{"name": "Pillow", "version": "5.4.1"}],
   ...:     data__python__startswith="3.8",
   ...: ).values_list("data__project__slug", flat=True).distinct().count()
Out[3]: 1564
```

It may be worth prioritizing a potential solution here to avoid re-compile Pillow over and over for these projects.

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
@@ -169,13 +169,23 @@ def install_core_requirements(self):
             *cmd, bin_path=self.venv_bin(), cwd=self.checkout_path
         )
 
-        requirements = [
-            'mock==1.0.1',
-            'pillow==5.4.1',
-            'alabaster>=0.7,<0.8,!=0.7.5',
-            'commonmark==0.8.1',
-            'recommonmark==0.5.0',
-        ]
+        requirements = []
+
+        # Unpin Pillow on newer Python versions to avoid re-compiling
+        # https://pillow.readthedocs.io/en/stable/installation.html#python-support
+        if self.config.python.version in ("2.7", "3.4", "3.5", "3.6", "3.7"):
+            requirements.append("pillow==5.4.1")
+        else:
+            requirements.append("pillow")
+
+        requirements.extend(
+            [
+                "mock==1.0.1",
+                "alabaster>=0.7,<0.8,!=0.7.5",
+                "commonmark==0.8.1",
+                "recommonmark==0.5.0",
+            ]
+        )
 
         if self.config.doctype == 'mkdocs':
             requirements.append(


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
@@ -579,8 +579,8 @@ def test_build_commands_executed(self, load_yaml_config):
                     "install",
                     "--upgrade",
                     "--no-cache-dir",
+                    "pillow",
                     "mock==1.0.1",
-                    "pillow==5.4.1",
                     "alabaster>=0.7,<0.8,!=0.7.5",
                     "commonmark==0.8.1",
                     "recommonmark==0.5.0",
diff --git a/readthedocs/rtd_tests/tests/test_doc_building.py b/readthedocs/rtd_tests/tests/test_doc_building.py
--- a/readthedocs/rtd_tests/tests/test_doc_building.py
+++ b/readthedocs/rtd_tests/tests/test_doc_building.py
@@ -368,9 +368,9 @@ def setUp(self):
         self.build_env_mock = Mock()
 
         self.base_requirements = [
-            'mock',
-            'pillow',
-            'alabaster',
+            "pillow",
+            "mock",
+            "alabaster",
         ]
         self.base_conda_requirements = [
             'mock',
@@ -395,7 +395,7 @@ def assertArgsStartsWith(self, args, call):
         for arg, arg_mock in zip_longest(args, args_mock):
             if arg is not mock.ANY:
                 self.assertIsNotNone(arg_mock)
-                self.assertTrue(arg_mock.startswith(arg))
+                self.assertTrue(arg_mock.startswith(arg), arg)
 
     @patch('readthedocs.projects.models.Project.checkout_path')
     def test_install_core_requirements_sphinx(self, checkout_path):