Skip to content

Commit c9edb04

Browse files
committed
Append core requirements to Conda environment file
We run 3 steps when a project depends on conda. 1. create the whole environment based on user's YAML file 2. run `conda install` with our own dependencies 3. run `pip install` with some of our dependencies that are not published on conda repositories. This commit changes this to make it in just one step (at environment creation). To do this, it appends our own requirements to the `environment.yml` file under `dependencies` and `dependencies.pip` config (see https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#create-env-file-manually) It also shows the resulting `environment.yml` in the build output. This behavior is added behind a feature flag so we can test it first. This allow users to be able to pin their dependencies as they want without us upgrading them in the second step. Also, this should improve the build time, since dependencies resolution is done just once. Related to * #3829 * #5631
1 parent 7be01d8 commit c9edb04

File tree

2 files changed

+94
-6
lines changed

2 files changed

+94
-6
lines changed

readthedocs/doc_builder/python_environments.py

+89-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
"""An abstraction over virtualenv and Conda environments."""
22

33
import copy
4+
import codecs
45
import hashlib
56
import itertools
67
import json
78
import logging
89
import os
910
import shutil
11+
import yaml
1012

11-
from readthedocs.config import PIP, SETUPTOOLS
13+
from readthedocs.config import PIP, SETUPTOOLS, ParseError, parse as parse_yaml
1214
from readthedocs.config.models import PythonInstall, PythonInstallRequirements
1315
from readthedocs.doc_builder.config import load_yaml_config
1416
from readthedocs.doc_builder.constants import DOCKER_IMAGE
@@ -442,6 +444,10 @@ def setup_base(self):
442444
if self.project.has_feature(Feature.UPDATE_CONDA_STARTUP):
443445
self._update_conda_startup()
444446

447+
if self.project.has_feature(Feature.CONDA_APPEND_CORE_REQUIREMENTS):
448+
self._append_core_requirements()
449+
self._show_environment_yaml()
450+
445451
self.build_env.run(
446452
'conda',
447453
'env',
@@ -455,10 +461,75 @@ def setup_base(self):
455461
cwd=self.checkout_path,
456462
)
457463

458-
def install_core_requirements(self):
459-
"""Install basic Read the Docs requirements into the Conda env."""
464+
def _show_environment_yaml(self):
465+
"""Show ``environment.yml`` file in the Build output."""
466+
self.build_env.run(
467+
'cat',
468+
self.config.conda.environment,
469+
cwd=self.checkout_path,
470+
)
471+
472+
def _append_core_requirements(self):
473+
"""
474+
Append Read the Docs dependencies to Conda environment file.
475+
476+
This help users to pin their dependencies properly without us upgrading
477+
them in the second ``conda install`` run.
478+
479+
See https://github.com/readthedocs/readthedocs.org/pull/5631
480+
"""
481+
try:
482+
inputfile = codecs.open(
483+
os.path.join(
484+
self.checkout_path,
485+
self.config.conda.environment,
486+
),
487+
encoding='utf-8',
488+
mode='r',
489+
)
490+
environment = parse_yaml(inputfile)
491+
except IOError:
492+
log.warning(
493+
'There was an error while reading Conda environment file.',
494+
)
495+
except ParseError:
496+
log.warning(
497+
'There was an error while parsing Conda environment file.',
498+
)
499+
else:
500+
# Append conda dependencies directly to ``dependencies`` and pip
501+
# dependencies to ``dependencies.pip``
502+
pip_requirements, conda_requirements = self._get_core_requirements()
503+
dependencies = environment.get('dependencies', [])
504+
pip_dependencies = {'pip': pip_requirements}
505+
506+
for item in dependencies:
507+
if isinstance(item, dict) and 'pip' in item:
508+
pip_requirements.extend(item.get('pip', []))
509+
dependencies.remove(item)
510+
break
511+
512+
dependencies.append(pip_dependencies)
513+
environment.update({'dependencies': dependencies})
514+
try:
515+
outputfile = codecs.open(
516+
os.path.join(
517+
self.checkout_path,
518+
self.config.conda.environment,
519+
),
520+
encoding='utf-8',
521+
mode='w',
522+
)
523+
yaml.safe_dump(environment, outputfile)
524+
except IOError:
525+
log.warning(
526+
'There was an error while writing the new Conda ',
527+
'environment file.',
528+
)
529+
530+
def _get_core_requirements(self):
460531
# Use conda for requirements it packages
461-
requirements = [
532+
conda_requirements = [
462533
'mock',
463534
'pillow',
464535
]
@@ -472,8 +543,19 @@ def install_core_requirements(self):
472543
pip_requirements.append('mkdocs')
473544
else:
474545
pip_requirements.append('readthedocs-sphinx-ext')
475-
requirements.extend(['sphinx', 'sphinx_rtd_theme'])
546+
conda_requirements.extend(['sphinx', 'sphinx_rtd_theme'])
547+
548+
return pip_requirements, conda_requirements
549+
550+
def install_core_requirements(self):
551+
"""Install basic Read the Docs requirements into the Conda env."""
476552

553+
if self.project.has_feature(Feature.CONDA_APPEND_CORE_REQUIREMENTS):
554+
return
555+
556+
pip_requirements, conda_requirements = self._get_core_requirements()
557+
# Install requirements via ``conda install`` command if they were
558+
# not appended to the ``environment.yml`` file.
477559
cmd = [
478560
'conda',
479561
'install',
@@ -482,12 +564,13 @@ def install_core_requirements(self):
482564
'--name',
483565
self.version.slug,
484566
]
485-
cmd.extend(requirements)
567+
cmd.extend(conda_requirements)
486568
self.build_env.run(
487569
*cmd,
488570
cwd=self.checkout_path,
489571
)
490572

573+
# Install requirements via ``pip install``
491574
pip_cmd = [
492575
self.venv_bin(filename='python'),
493576
'-m',

readthedocs/projects/models.py

+5
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,7 @@ def add_features(sender, **kwargs):
13741374
DEFAULT_TO_MKDOCS_0_17_3 = 'default_to_mkdocs_0_17_3'
13751375
CLEAN_AFTER_BUILD = 'clean_after_build'
13761376
UPDATE_CONDA_STARTUP = 'update_conda_startup'
1377+
CONDA_APPEND_CORE_REQUIREMENTS = 'conda_append_core_requirements'
13771378

13781379
FEATURES = (
13791380
(USE_SPHINX_LATEST, _('Use latest version of Sphinx')),
@@ -1418,6 +1419,10 @@ def add_features(sender, **kwargs):
14181419
UPDATE_CONDA_STARTUP,
14191420
_('Upgrade conda before creating the environment'),
14201421
),
1422+
(
1423+
CONDA_APPEND_CORE_REQUIREMENTS,
1424+
_('Append Read the Docs core requirements to environment.yml file'),
1425+
),
14211426
)
14221427

14231428
projects = models.ManyToManyField(

0 commit comments

Comments
 (0)