Skip to content

[WIP] Add support for "requires" key in Python install config and allow extra keys beginning with "x-" #7259

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/config-file/v2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,27 @@ Example:
manage the build, this setting will not have any effect. Instead
add the extra requirements to the ``environment`` file of Conda.

Requirements List
'''''''''''''''''

Install packages from a list of requirements as a string (similar to how they would be
listed on the command-line when invoking ``pip install``).

:Key: `requires`
:Type: ``string``
:Required: ``true``

Example:

.. code-block:: yaml

version: 2

python:
version: 3.7
install:
- requires: mkdocs-material Pygments

Packages
''''''''

Expand Down
14 changes: 11 additions & 3 deletions readthedocs/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import copy
import os
import re
import shlex
from contextlib import contextmanager

from django.conf import settings
Expand All @@ -19,6 +20,7 @@
Python,
PythonInstall,
PythonInstallRequirements,
PythonInstallRequirementsList,
Search,
Sphinx,
Submodules,
Expand Down Expand Up @@ -833,6 +835,9 @@ def validate_python_install(self, index):
self.base_path,
)
python_install['requirements'] = requirements
elif 'requires' in raw_install:
requires_key = key + 'requires'
python_install['requires'] = shlex.split(self.pop_config(requires_key))
elif 'path' in raw_install:
path_key = key + '.path'
with self.catch_validation_error(path_key):
Expand Down Expand Up @@ -1086,9 +1091,10 @@ def _get_extra_key(self, value):

Will return `['key', 'name']`.
"""
if isinstance(value, dict) and value:
key_name = next(iter(value))
return [key_name] + self._get_extra_key(value[key_name])
if isinstance(value, dict):
key_name = next((k for k in value if not k.startswith('x-')), None)
if key_name is not None:
return [key_name] + self._get_extra_key(value[key_name])
return []

@property
Expand All @@ -1112,6 +1118,8 @@ def python(self):
for install in python['install']:
if 'requirements' in install:
python_install.append(PythonInstallRequirements(**install),)
elif 'requires' in install:
python_install.append(PythonInstallRequirementsList(install['requires']),)
elif 'path' in install:
python_install.append(PythonInstall(**install),)
return Python(
Expand Down
5 changes: 5 additions & 0 deletions readthedocs/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ class PythonInstallRequirements(Base):
__slots__ = ('requirements',)


class PythonInstallRequirementsList(Base):

__slots__ = ('requirements',)


class PythonInstall(Base):

__slots__ = (
Expand Down
18 changes: 18 additions & 0 deletions readthedocs/config/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
Conda,
PythonInstall,
PythonInstallRequirements,
PythonInstallRequirementsList,
)
from readthedocs.config.validation import (
INVALID_BOOL,
Expand Down Expand Up @@ -1104,6 +1105,23 @@ def test_python_install_requirements_check_valid(self, tmpdir):
assert isinstance(install[0], PythonInstallRequirements)
assert install[0].requirements == 'requirements.txt'

def test_python_install_requires_check_valid(self, tmpdir):
build = self.get_build_config(
{
'python': {
'install': [{
'requires': 'package_a "package_b >=3.0.0,<4.0.0"',
}],
},
},
source_file=str(tmpdir.join('readthedocs.yml')),
)
build.validate()
install = build.python.install
assert len(install) == 1
assert isinstance(install[0], PythonInstallRequirementsList)
assert install[0].requirements == ['package_a', 'package_b >=3.0.0,<4.0.0']

def test_python_install_requirements_does_not_allow_null(self, tmpdir):
build = self.get_build_config(
{
Expand Down
35 changes: 34 additions & 1 deletion readthedocs/doc_builder/python_environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

from readthedocs.builds.constants import EXTERNAL
from readthedocs.config import PIP, SETUPTOOLS, ParseError, parse as parse_yaml
from readthedocs.config.models import PythonInstall, PythonInstallRequirements
from readthedocs.config.models import (
PythonInstall,
PythonInstallRequirements,
PythonInstallRequirementsList,
)
from readthedocs.doc_builder.config import load_yaml_config
from readthedocs.doc_builder.constants import DOCKER_IMAGE
from readthedocs.doc_builder.environments import DockerBuildEnvironment
Expand Down Expand Up @@ -77,6 +81,8 @@ def install_requirements(self):
for install in self.config.python.install:
if isinstance(install, PythonInstallRequirements):
self.install_requirements_file(install)
if isinstance(install, PythonInstallRequirementsList):
self.install_requirements_list(install)
if isinstance(install, PythonInstall):
self.install_package(install)

Expand Down Expand Up @@ -430,6 +436,33 @@ def install_requirements_file(self, install):
bin_path=self.venv_bin(),
)

def install_requirements_list(self, install):
"""
Install requirements from a string specification using pip.

:param install: A instal object from the config module.
:type install: readthedocs.config.modules.PythonInstallRequirementsList
"""

args = [
self.venv_bin(filename='python'),
'-m',
'pip',
'install',
]
if self.project.has_feature(Feature.PIP_ALWAYS_UPGRADE):
args += ['--upgrade']
args += [
'--exists-action=w',
*self._pip_cache_cmd_argument(),
]
args += install.requirements
self.build_env.run(
*args,
cwd=self.checkout_path,
bin_path=self.venv_bin(),
)

def list_packages_installed(self):
"""List packages installed in pip."""
args = [
Expand Down