Skip to content

Commit 7c905c6

Browse files
authored
Merge branch 'master' into using-defaults
2 parents 77102a1 + 3304193 commit 7c905c6

27 files changed

+1099
-381
lines changed

docs/conf.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import sys
77

88
import sphinx_rtd_theme
9-
from recommonmark.parser import CommonMarkParser
109

1110
sys.path.insert(0, os.path.abspath('..'))
1211
sys.path.append(os.path.dirname(__file__))
@@ -29,13 +28,11 @@
2928
'doc_extensions',
3029
'sphinx_tabs.tabs',
3130
'sphinx-prompt',
31+
'recommonmark',
3232
]
3333
templates_path = ['_templates']
3434

3535
source_suffix = ['.rst', '.md']
36-
source_parsers = {
37-
'.md': CommonMarkParser,
38-
}
3936

4037
master_doc = 'index'
4138
project = u'Read the Docs'

docs/faq.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ We deploy readthedocs.org from the `rel` branch in our GitHub repository. You ca
233233

234234

235235
How can I avoid search results having a deprecated version of my docs?
236-
---------------------------------------------------------------------
236+
----------------------------------------------------------------------
237237

238238
If readers search something related to your docs in Google, it will probably return the most relevant version of your documentation.
239239
It may happen that this version is already deprecated and you want to stop Google indexing it as a result,

readthedocs/builds/views.py

+47
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""Views for builds app."""
44

55
import logging
6+
import textwrap
67

78
from django.contrib import messages
89
from django.contrib.auth.decorators import login_required
@@ -15,7 +16,10 @@
1516
from django.urls import reverse
1617
from django.utils.decorators import method_decorator
1718
from django.views.generic import DetailView, ListView
19+
from requests.utils import quote
20+
from urllib.parse import urlparse
1821

22+
from readthedocs.doc_builder.exceptions import BuildEnvironmentError
1923
from readthedocs.builds.models import Build, Version
2024
from readthedocs.core.permissions import AdminPermission
2125
from readthedocs.core.utils import trigger_build
@@ -104,6 +108,49 @@ class BuildDetail(BuildBase, DetailView):
104108
def get_context_data(self, **kwargs):
105109
context = super().get_context_data(**kwargs)
106110
context['project'] = self.project
111+
112+
build = self.get_object()
113+
if build.error != BuildEnvironmentError.GENERIC_WITH_BUILD_ID.format(build_id=build.pk):
114+
# Do not suggest to open an issue if the error is not generic
115+
return context
116+
117+
scheme = (
118+
'https://github.com/rtfd/readthedocs.org/issues/new'
119+
'?title={title}{build_id}'
120+
'&body={body}'
121+
)
122+
123+
# TODO: we could use ``.github/ISSUE_TEMPLATE.md`` here, but we would
124+
# need to add some variables to it which could impact in the UX when
125+
# filling an issue from the web
126+
body = """
127+
## Details:
128+
129+
* Project URL: https://readthedocs.org/projects/{project_slug}/
130+
* Build URL(if applicable): https://readthedocs.org{build_path}
131+
* Read the Docs username(if applicable): {username}
132+
133+
## Expected Result
134+
135+
*A description of what you wanted to happen*
136+
137+
## Actual Result
138+
139+
*A description of what actually happened*""".format(
140+
project_slug=self.project,
141+
build_path=self.request.path,
142+
username=self.request.user,
143+
)
144+
145+
scheme_dict = {
146+
'title': quote('Build error with build id #'),
147+
'build_id': context['build'].id,
148+
'body': quote(textwrap.dedent(body)),
149+
}
150+
151+
issue_url = scheme.format(**scheme_dict)
152+
issue_url = urlparse(issue_url).geturl()
153+
context['issue_url'] = issue_url
107154
return context
108155

109156

readthedocs/config/config.py

+139-44
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
1-
# -*- coding: utf-8 -*-
2-
31
# pylint: disable=too-many-lines
42

53
"""Build configuration for rtd."""
64

5+
import copy
76
import os
87
from contextlib import contextmanager
98

109
from django.conf import settings
1110

11+
from readthedocs.config.utils import list_to_dict, to_dict
1212
from readthedocs.projects.constants import DOCUMENTATION_CHOICES
1313

1414
from .find import find_one
15-
from .models import Build, Conda, Mkdocs, Python, Sphinx, Submodules
15+
from .models import (
16+
Build,
17+
Conda,
18+
Mkdocs,
19+
Python,
20+
PythonInstall,
21+
PythonInstallRequirements,
22+
Sphinx,
23+
Submodules,
24+
)
1625
from .parser import ParseError, parse
1726
from .validation import (
1827
VALUE_NOT_FOUND,
1928
ValidationError,
2029
validate_bool,
2130
validate_choice,
2231
validate_dict,
32+
validate_directory,
2333
validate_file,
2434
validate_list,
2535
validate_string,
@@ -34,9 +44,13 @@
3444
'ConfigError',
3545
'ConfigOptionNotSupportedError',
3646
'InvalidConfig',
47+
'PIP',
48+
'SETUPTOOLS',
3749
)
3850

3951
ALL = 'all'
52+
PIP = 'pip'
53+
SETUPTOOLS = 'setuptools'
4054
CONFIG_FILENAME_REGEX = r'^\.?readthedocs.ya?ml$'
4155

4256
CONFIG_NOT_SUPPORTED = 'config-not-supported'
@@ -136,7 +150,7 @@ class BuildConfigBase:
136150

137151
def __init__(self, env_config, raw_config, source_file):
138152
self.env_config = env_config
139-
self.raw_config = raw_config
153+
self.raw_config = copy.deepcopy(raw_config)
140154
self.source_file = source_file
141155
if os.path.isdir(self.source_file):
142156
self.base_path = self.source_file
@@ -236,10 +250,7 @@ def as_dict(self):
236250
config = {}
237251
for name in self.PUBLIC_ATTRIBUTES:
238252
attr = getattr(self, name)
239-
if hasattr(attr, '_asdict'):
240-
config[name] = attr._asdict()
241-
else:
242-
config[name] = attr
253+
config[name] = to_dict(attr)
243254
return config
244255

245256
def __getattr__(self, name):
@@ -456,7 +467,9 @@ def validate_requirements_file(self):
456467
if not requirements_file:
457468
return None
458469
with self.catch_validation_error('requirements_file'):
459-
validate_file(requirements_file, self.base_path)
470+
requirements_file = validate_file(
471+
requirements_file, self.base_path
472+
)
460473
return requirements_file
461474

462475
def validate_formats(self):
@@ -482,9 +495,39 @@ def formats(self):
482495
@property
483496
def python(self):
484497
"""Python related configuration."""
498+
python = self._config['python']
485499
requirements = self._config['requirements_file']
486-
self._config['python']['requirements'] = requirements
487-
return Python(**self._config['python'])
500+
python_install = []
501+
502+
# Always append a `PythonInstallRequirements` option.
503+
# If requirements is None, rtd will try to find a requirements file.
504+
python_install.append(
505+
PythonInstallRequirements(
506+
requirements=requirements,
507+
)
508+
)
509+
if python['install_with_pip']:
510+
python_install.append(
511+
PythonInstall(
512+
path=self.base_path,
513+
method=PIP,
514+
extra_requirements=python['extra_requirements'],
515+
)
516+
)
517+
elif python['install_with_setup']:
518+
python_install.append(
519+
PythonInstall(
520+
path=self.base_path,
521+
method=SETUPTOOLS,
522+
extra_requirements=[],
523+
)
524+
)
525+
526+
return Python(
527+
version=python['version'],
528+
install=python_install,
529+
use_system_site_packages=python['use_system_site_packages'],
530+
)
488531

489532
@property
490533
def conda(self):
@@ -536,7 +579,7 @@ class BuildConfigV2(BuildConfigBase):
536579
valid_formats = ['htmlzip', 'pdf', 'epub']
537580
valid_build_images = ['1.0', '2.0', '3.0', 'stable', 'latest']
538581
default_build_image = 'latest'
539-
valid_install_options = ['pip', 'setup.py']
582+
valid_install_method = [PIP, SETUPTOOLS]
540583
valid_sphinx_builders = {
541584
'html': 'sphinx',
542585
'htmldir': 'sphinx_htmldir',
@@ -659,39 +702,22 @@ def validate_python(self):
659702
self.get_valid_python_versions(),
660703
)
661704

662-
with self.catch_validation_error('python.requirements'):
663-
requirements = self.defaults.get('requirements_file')
664-
requirements = self.pop_config('python.requirements', requirements)
665-
if requirements != '' and requirements is not None:
666-
requirements = validate_file(requirements, self.base_path)
667-
python['requirements'] = requirements
668-
669705
with self.catch_validation_error('python.install'):
670-
install = (
671-
'setup.py' if self.defaults.get('install_project') else None
672-
)
673-
install = self.pop_config('python.install', install)
674-
if install is not None:
675-
validate_choice(install, self.valid_install_options)
676-
python['install_with_setup'] = install == 'setup.py'
677-
python['install_with_pip'] = install == 'pip'
678-
679-
with self.catch_validation_error('python.extra_requirements'):
680-
extra_requirements = self.pop_config(
681-
'python.extra_requirements',
682-
[],
683-
)
684-
extra_requirements = validate_list(extra_requirements)
685-
if extra_requirements and not python['install_with_pip']:
686-
self.error(
687-
'python.extra_requirements',
688-
'You need to install your project with pip '
689-
'to use extra_requirements',
690-
code=PYTHON_INVALID,
706+
raw_install = self.raw_config.get('python', {}).get('install', [])
707+
validate_list(raw_install)
708+
if raw_install:
709+
# Transform to a dict, so it's easy to validate extra keys.
710+
self.raw_config.setdefault('python', {})['install'] = (
711+
list_to_dict(raw_install)
691712
)
692-
python['extra_requirements'] = [
693-
validate_string(extra) for extra in extra_requirements
694-
]
713+
else:
714+
self.pop_config('python.install')
715+
716+
raw_install = self.raw_config.get('python', {}).get('install', [])
717+
python['install'] = [
718+
self.validate_python_install(index)
719+
for index in range(len(raw_install))
720+
]
695721

696722
with self.catch_validation_error('python.system_packages'):
697723
system_packages = self.defaults.get(
@@ -706,6 +732,60 @@ def validate_python(self):
706732

707733
return python
708734

735+
def validate_python_install(self, index):
736+
"""Validates the python.install.{index} key."""
737+
python_install = {}
738+
key = 'python.install.{}'.format(index)
739+
raw_install = self.raw_config['python']['install'][str(index)]
740+
with self.catch_validation_error(key):
741+
validate_dict(raw_install)
742+
743+
if 'requirements' in raw_install:
744+
requirements_key = key + '.requirements'
745+
with self.catch_validation_error(requirements_key):
746+
requirements = validate_file(
747+
self.pop_config(requirements_key),
748+
self.base_path
749+
)
750+
python_install['requirements'] = requirements
751+
elif 'path' in raw_install:
752+
path_key = key + '.path'
753+
with self.catch_validation_error(path_key):
754+
path = validate_directory(
755+
self.pop_config(path_key),
756+
self.base_path
757+
)
758+
python_install['path'] = path
759+
760+
method_key = key + '.method'
761+
with self.catch_validation_error(method_key):
762+
method = validate_choice(
763+
self.pop_config(method_key, PIP),
764+
self.valid_install_method
765+
)
766+
python_install['method'] = method
767+
768+
extra_req_key = key + '.extra_requirements'
769+
with self.catch_validation_error(extra_req_key):
770+
extra_requirements = validate_list(
771+
self.pop_config(extra_req_key, [])
772+
)
773+
if extra_requirements and python_install['method'] != PIP:
774+
self.error(
775+
extra_req_key,
776+
'You need to install your project with pip '
777+
'to use extra_requirements',
778+
code=PYTHON_INVALID,
779+
)
780+
python_install['extra_requirements'] = extra_requirements
781+
else:
782+
self.error(
783+
key,
784+
'"path" or "requirements" key is required',
785+
code=CONFIG_REQUIRED,
786+
)
787+
return python_install
788+
709789
def get_valid_python_versions(self):
710790
"""
711791
Get the valid python versions for the current docker image.
@@ -942,7 +1022,22 @@ def build(self):
9421022

9431023
@property
9441024
def python(self):
945-
return Python(**self._config['python'])
1025+
python_install = []
1026+
python = self._config['python']
1027+
for install in python['install']:
1028+
if 'requirements' in install:
1029+
python_install.append(
1030+
PythonInstallRequirements(**install)
1031+
)
1032+
elif 'path' in install:
1033+
python_install.append(
1034+
PythonInstall(**install)
1035+
)
1036+
return Python(
1037+
version=python['version'],
1038+
install=python_install,
1039+
use_system_site_packages=python['use_system_site_packages'],
1040+
)
9461041

9471042
@property
9481043
def sphinx(self):

0 commit comments

Comments
 (0)