Repository: readthedocs/readthedocs.org

Issue:
.readthedocs.yaml not being used
## Details

* Read the Docs project URL: https://readthedocs.org/projects/testfixtures/
* Build URL (if applicable): https://readthedocs.org/projects/testfixtures/builds/8783893/
* Read the Docs username (if applicable): chris@python.org

## Expected Result

I'm trying to move this project from the web config interface to using `.readthedocs.yaml`, I've added the config file and configured it to do a `pip install .[build]` before running Sphinx.

## Actual Result

I've had this work great with new projects of mine and really like it, but `testfixtures` is one of my oldest projects and seems to ignore the `.readthedocs.yaml` that I added in https://github.com/Simplistix/testfixtures/commit/5b0857f125d15a5f4b5b120bede6e0e1934f7ebc. Looking at the build log, I don't see the `pip install .[build]` I've configured.



Discussion:
I'm going to investigate a little more here, at first sight seems like you have a valid config file.
I think there is a syntax error here

https://github.com/Simplistix/testfixtures/blob/5b0857f125d15a5f4b5b120bede6e0e1934f7ebc/.readthedocs.yml#L7-L8

The indentation is wrong.

But instead of showing that error to the user, we are defaulting to create our own config file, I think showing the error should be the way to go, instead of trying to fix it magically.
Ah, nice spot! Yes, showing the error would definitely save lots of work all round :-)
I'm going to fix the bug around not showing the correct message to the user, but fixing the indentation should make your project pass.
yaml fix worked, thanks: https://readthedocs.org/projects/testfixtures/builds/8786855/

Resolving patch:
diff --git a/readthedocs/config/config.py b/readthedocs/config/config.py
--- a/readthedocs/config/config.py
+++ b/readthedocs/config/config.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # pylint: disable=too-many-lines
 
 """Build configuration for rtd."""
@@ -37,6 +36,7 @@
     validate_string,
 )
 
+
 __all__ = (
     'ALL',
     'load',
@@ -44,6 +44,7 @@
     'BuildConfigV2',
     'ConfigError',
     'ConfigOptionNotSupportedError',
+    'ConfigFileNotFound',
     'InvalidConfig',
     'PIP',
     'SETUPTOOLS',
@@ -58,7 +59,7 @@
 VERSION_INVALID = 'version-invalid'
 CONFIG_SYNTAX_INVALID = 'config-syntax-invalid'
 CONFIG_REQUIRED = 'config-required'
-CONF_FILE_REQUIRED = 'conf-file-required'
+CONFIG_FILE_REQUIRED = 'config-file-required'
 PYTHON_INVALID = 'python-invalid'
 SUBMODULES_INVALID = 'submodules-invalid'
 INVALID_KEYS_COMBINATION = 'invalid-keys-combination'
@@ -87,6 +88,17 @@ def __init__(self, message, code):
         super().__init__(message)
 
 
+class ConfigFileNotFound(ConfigError):
+
+    """Error when we can't find a configuration file."""
+
+    def __init__(self, directory):
+        super().__init__(
+            f"Configuration file not found in: {directory}",
+            CONFIG_FILE_REQUIRED,
+        )
+
+
 class ConfigOptionNotSupportedError(ConfigError):
 
     """Error for unsupported configuration options in a version."""
@@ -298,7 +310,6 @@ class BuildConfigV1(BuildConfigBase):
 
     """Version 1 of the configuration file."""
 
-    CONF_FILE_REQUIRED_MESSAGE = 'Missing key "conf_file"'
     PYTHON_INVALID_MESSAGE = '"python" section must be a mapping.'
     PYTHON_EXTRA_REQUIREMENTS_INVALID_MESSAGE = (
         '"python.extra_requirements" section must be a list.'
@@ -1104,14 +1115,14 @@ def load(path, env_config):
     filename = find_one(path, CONFIG_FILENAME_REGEX)
 
     if not filename:
-        raise ConfigError('No configuration file found', code=CONFIG_REQUIRED)
+        raise ConfigFileNotFound(path)
     with open(filename, 'r') as configuration_file:
         try:
             config = parse(configuration_file.read())
         except ParseError as error:
             raise ConfigError(
                 'Parse error in {filename}: {message}'.format(
-                    filename=filename,
+                    filename=os.path.relpath(filename, path),
                     message=str(error),
                 ),
                 code=CONFIG_SYNTAX_INVALID,
diff --git a/readthedocs/doc_builder/config.py b/readthedocs/doc_builder/config.py
--- a/readthedocs/doc_builder/config.py
+++ b/readthedocs/doc_builder/config.py
@@ -1,10 +1,8 @@
-# -*- coding: utf-8 -*-
-
 """An API to load config from a readthedocs.yml file."""
 
 from os import path
 
-from readthedocs.config import BuildConfigV1, ConfigError, InvalidConfig
+from readthedocs.config import BuildConfigV1, ConfigFileNotFound
 from readthedocs.config import load as load_config
 from readthedocs.projects.models import ProjectConfigurationError
 
@@ -59,10 +57,9 @@ def load_yaml_config(version):
             path=checkout_path,
             env_config=env_config,
         )
-    except InvalidConfig:
-        # This is a subclass of ConfigError, so has to come first
-        raise
-    except ConfigError:
+    except ConfigFileNotFound:
+        # Dafault to use v1 with some defaults from the web interface
+        # if we don't find a configuration file.
         config = BuildConfigV1(
             env_config=env_config,
             raw_config={},


Test patch:
diff --git a/readthedocs/config/tests/test_config.py b/readthedocs/config/tests/test_config.py
--- a/readthedocs/config/tests/test_config.py
+++ b/readthedocs/config/tests/test_config.py
@@ -15,14 +15,17 @@
     BuildConfigV1,
     BuildConfigV2,
     ConfigError,
+    ConfigFileNotFound,
     ConfigOptionNotSupportedError,
     InvalidConfig,
     load,
 )
 from readthedocs.config.config import (
+    CONFIG_FILE_REQUIRED,
     CONFIG_FILENAME_REGEX,
     CONFIG_NOT_SUPPORTED,
     CONFIG_REQUIRED,
+    CONFIG_SYNTAX_INVALID,
     INVALID_KEY,
     PYTHON_INVALID,
     VERSION_INVALID,
@@ -73,9 +76,9 @@ def get_build_config(config, env_config=None, source_file='readthedocs.yml'):
 def test_load_no_config_file(tmpdir, files):
     apply_fs(tmpdir, files)
     base = str(tmpdir)
-    with raises(ConfigError) as e:
+    with raises(ConfigFileNotFound) as e:
         load(base, {})
-    assert e.value.code == CONFIG_REQUIRED
+    assert e.value.code == CONFIG_FILE_REQUIRED
 
 
 def test_load_empty_config_file(tmpdir):
@@ -136,6 +139,27 @@ def test_load_unknow_version(tmpdir):
     assert excinfo.value.code == VERSION_INVALID
 
 
+def test_load_raise_exception_invalid_syntax(tmpdir):
+    apply_fs(
+        tmpdir, {
+            'readthedocs.yml': textwrap.dedent('''
+                version: 2
+                python:
+                  install:
+                    - method: pip
+                      path: .
+                        # bad indentation here
+                        extra_requirements:
+                          - build
+            '''),
+        },
+    )
+    base = str(tmpdir)
+    with raises(ConfigError) as excinfo:
+        load(base, {})
+    assert excinfo.value.code == CONFIG_SYNTAX_INVALID
+
+
 def test_yaml_extension(tmpdir):
     """Make sure loading the 'readthedocs' file with a 'yaml' extension."""
     apply_fs(