Skip to content
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

Implement pipfile support #4783

Closed
wants to merge 14 commits into from
50 changes: 48 additions & 2 deletions readthedocs/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Mkdocs,
Python,
PythonInstall,
PythonInstallPipfile,
PythonInstallRequirements,
Sphinx,
Submodules,
Expand Down Expand Up @@ -833,6 +834,8 @@ def validate_python_install(self, index):
code=PYTHON_INVALID,
)
python_install['extra_requirements'] = extra_requirements
elif 'pipfile' in raw_install:
python_install.update(self.validate_pipfile(key))
else:
self.error(
key,
Expand All @@ -841,6 +844,41 @@ def validate_python_install(self, index):
)
return python_install

def validate_pipfile(self, key):
"""
Validates the pipfile key.

:param key: The key in a dotted form
:return: The dictionary with valid data
"""
python_install = {}
pipfile_key = key + '.pipfile'
with self.catch_validation_error(pipfile_key):
python_install['pipfile'] = validate_directory(
self.pop_config(pipfile_key),
self.base_path
)

dev_key = key + '.dev'
with self.catch_validation_error(dev_key):
python_install['dev'] = validate_bool(
self.pop_config(dev_key, False),
)

ignore_pipfile_key = key + '.ignore_pipfile'
with self.catch_validation_error(ignore_pipfile_key):
python_install['ignore_pipfile'] = validate_bool(
self.pop_config(ignore_pipfile_key, False),
)

skip_lock_key = key + '.skip_lock'
with self.catch_validation_error(skip_lock_key):
python_install['skip_lock'] = validate_bool(
self.pop_config(skip_lock_key, True),
)

return python_install

def get_valid_python_versions(self):
"""
Get the valid python versions for the current docker image.
Expand Down Expand Up @@ -1075,9 +1113,17 @@ def python(self):
python = self._config['python']
for install in python['install']:
if 'requirements' in install:
python_install.append(PythonInstallRequirements(**install),)
python_install.append(
PythonInstallRequirements(**install)
)
elif 'path' in install:
python_install.append(PythonInstall(**install),)
python_install.append(
PythonInstall(**install)
)
elif 'pipfile' in install:
python_install.append(
PythonInstallPipfile(**install)
)
return Python(
version=python['version'],
install=python_install,
Expand Down
4 changes: 4 additions & 0 deletions readthedocs/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class PythonInstall(Base):
__slots__ = ('path', 'method', 'extra_requirements',)


class PythonInstallPipfile(Base):
__slots__ = ('pipfile', 'dev', 'ignore_pipfile', 'skip_lock')


class Conda(Base):

__slots__ = ('environment',)
Expand Down
146 changes: 146 additions & 0 deletions readthedocs/config/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,152 @@ def test_python_install_extra_requirements_allow_empty(self, tmpdir):
assert len(install) == 1
assert install[0].extra_requirements == []

def test_python_install_pipfile_check_valid(self, tmpdir):
build = self.get_build_config(
{
'python': {
'install': [{
'pipfile': '.',
}],
},
},
source_file=str(tmpdir.join('readthedocs.yml')),
)
build.validate()
install = build.python.install
assert len(install) == 1
pipfile = install[0]
assert pipfile.pipfile == str(tmpdir)
assert pipfile.dev is False
assert pipfile.ignore_pipfile is False
assert pipfile.skip_lock is True

def test_python_install_pipfile_check_invalid_path(self, tmpdir):
build = self.get_build_config(
{
'python': {
'install': [{
'pipfile': 'nonexisting',
}],
},
},
source_file=str(tmpdir.join('readthedocs.yml')),
)
with raises(InvalidConfig) as excinfo:
build.validate()
assert excinfo.value.key == 'python.install.0.pipfile'

@pytest.mark.parametrize('value', [2, [], {}, None])
def test_python_install_pipfile_check_type(self, value):
build = self.get_build_config(
{
'python': {
'install': [{
'pipfile': value,
}],
},
},
)
with raises(InvalidConfig) as excinfo:
build.validate()
assert excinfo.value.key == 'python.install.0.pipfile'

@pytest.mark.parametrize('value', [True, False])
def test_python_install_pipfile_dev_check_valid(self, value):
build = self.get_build_config(
{
'python': {
'install': [{
'pipfile': '.',
'dev': value,
}],
},
},
)
build.validate()
pipfile = build.python.install[0]
assert pipfile.dev is value

@pytest.mark.parametrize('value', [2, [], {}, None, ''])
def test_python_install_pipfile_dev_check_type(self, value):
build = self.get_build_config(
{
'python': {
'install': [{
'pipfile': '.',
'dev': value,
}],
},
},
)
with raises(InvalidConfig) as excinfo:
build.validate()
assert excinfo.value.key == 'python.install.0.dev'

@pytest.mark.parametrize('value', [True, False])
def test_python_install_pipfile_ignore_pipfile_check_valid(self, value):
build = self.get_build_config(
{
'python': {
'install': [{
'pipfile': '.',
'ignore_pipfile': value,
}],
},
},
)
build.validate()
pipfile = build.python.install[0]
assert pipfile.ignore_pipfile is value

@pytest.mark.parametrize('value', [2, [], {}, None, ''])
def test_python_install_pipfile_ignore_pipfile_check_type(self, value):
build = self.get_build_config(
{
'python': {
'install': [{
'pipfile': '.',
'ignore_pipfile': value,
}],
},
},
)
with raises(InvalidConfig) as excinfo:
build.validate()
assert excinfo.value.key == 'python.install.0.ignore_pipfile'

@pytest.mark.parametrize('value', [True, False])
def test_python_install_pipfile_skip_lock_check_valid(self, value):
build = self.get_build_config(
{
'python': {
'install': [{
'pipfile': '.',
'skip_lock': value,
}],
},
},
)
build.validate()
pipfile = build.python.install[0]
assert pipfile.skip_lock is value

@pytest.mark.parametrize('value', [2, [], {}, None, ''])
def test_python_install_pipfile_skip_lock_check_type(self, value):
build = self.get_build_config(
{
'python': {
'install': [{
'pipfile': '.',
'skip_lock': value,
}],
},
},
)
with raises(InvalidConfig) as excinfo:
build.validate()
assert excinfo.value.key == 'python.install.0.skip_lock'

def test_python_install_several_respects_order(self, tmpdir):
apply_fs(tmpdir, {
'one': {},
Expand Down
29 changes: 28 additions & 1 deletion readthedocs/doc_builder/python_environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from django.conf import settings

from readthedocs.config import PIP, SETUPTOOLS
from readthedocs.config.models import PythonInstall, PythonInstallRequirements
from readthedocs.config.models import PythonInstall, PythonInstallRequirements, PythonInstallPipfile
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 @@ -75,6 +75,8 @@ def install_requirements(self):
self.install_requirements_file(install)
if isinstance(install, PythonInstall):
self.install_package(install)
if isinstance(install, PythonInstallPipfile):
self.install_pipfile(install)

def install_package(self, install):
"""
Expand Down Expand Up @@ -119,6 +121,30 @@ def install_package(self, install):
bin_path=self.venv_bin(),
)

def install_pipfile(self, install):
extra_args = []
if install.dev:
extra_args.append('--dev')
if install.ignore_pipfile:
extra_args.append('--ignore-pipfile')
if install.skip_lock:
extra_args.append('--skip-lock')
pipfile = os.path.relpath(
os.path.join(install.pipfile, 'Pipfile'),
self.checkout_path
)
pipfile = os.path.join('.', pipfile)
self.build_env.run(
'PIPENV_PIPFILE={}'.format(pipfile),
'python',
self.venv_bin(filename='pipenv'),
'install',
'--system',
*extra_args,
cwd=self.checkout_path,
bin_path=self.venv_bin() # no comma here for py2.7
)

def venv_bin(self, filename=None):
"""
Return path to the virtualenv bin path, or a specific binary.
Expand Down Expand Up @@ -290,6 +316,7 @@ def install_core_requirements(self):
'mock==1.0.1',
'pillow==5.4.1',
'alabaster>=0.7,<0.8,!=0.7.5',
'pipenv==2018.11.26',
'commonmark==0.8.1',
'recommonmark==0.5.0',
]
Expand Down
53 changes: 50 additions & 3 deletions readthedocs/rtd_tests/tests/test_config_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
BuildConfigV1,
InvalidConfig,
)
from readthedocs.config.models import PythonInstallRequirements
from readthedocs.config.models import (
PythonInstallPipfile,
PythonInstallRequirements,
)
from readthedocs.config.tests.utils import apply_fs
from readthedocs.doc_builder.config import load_yaml_config
from readthedocs.doc_builder.environments import LocalBuildEnvironment
Expand Down Expand Up @@ -651,13 +654,49 @@ def test_python_install_extra_requirements(self, run, checkout_path, tmpdir):
assert len(install) == 1
assert install[0].method == PIP

@patch('readthedocs.doc_builder.environments.BuildEnvironment.run')
def test_python_install_pipfile(self, run, checkout_path, tmpdir):
checkout_path.return_value = str(tmpdir)
self.create_config_file(
tmpdir,
{
'python': {
'install': [{
'pipfile': '.',
'dev': True,
}],
},
}
)

update_docs = self.get_update_docs_task()
config = update_docs.config

python_env = Virtualenv(
version=self.version,
build_env=update_docs.build_env,
config=config
)
update_docs.python_env = python_env
update_docs.python_env.install_requirements()

args, kwargs = run.call_args

assert 'PIPENV_PIPFILE=./Pipfile' in args
assert 'install' in args
assert '--dev' in args
install = config.python.install
assert len(install) == 1
assert isinstance(install[0], PythonInstallPipfile)

@patch('readthedocs.doc_builder.environments.BuildEnvironment.run')
def test_python_install_several_options(self, run, checkout_path, tmpdir):
checkout_path.return_value = str(tmpdir)
apply_fs(tmpdir, {
'one': {},
'two': {},
'three.txt': '',
'pipfile': {'Pipfile': ''},
})
self.create_config_file(
tmpdir,
Expand All @@ -672,9 +711,12 @@ def test_python_install_several_options(self, run, checkout_path, tmpdir):
'method': 'setuptools',
}, {
'requirements': 'three.txt',
}, {
'pipfile': 'pipfile',
'dev': True,
}],
},
}
},
)

update_docs = self.get_update_docs_task()
Expand All @@ -689,7 +731,7 @@ def test_python_install_several_options(self, run, checkout_path, tmpdir):
update_docs.python_env.install_requirements()

install = config.python.install
assert len(install) == 3
assert len(install) == 4

args, kwargs = run.call_args_list[0]
assert 'install' in args
Expand All @@ -706,6 +748,11 @@ def test_python_install_several_options(self, run, checkout_path, tmpdir):
assert '-r' in args
assert 'three.txt' in args

args, kwargs = run.call_args_list[3]
assert 'install' in args
assert '--dev' in args
assert 'PIPENV_PIPFILE=./pipfile/Pipfile' in args

@patch('readthedocs.doc_builder.environments.BuildEnvironment.run')
def test_system_packages(self, run, checkout_path, tmpdir):
checkout_path.return_value = str(tmpdir)
Expand Down
Loading