Repository: readthedocs/readthedocs.org

Issue:
Refactor BuildConfig to not use hardcoded constants
While reviewing https://github.com/rtfd/readthedocs.org/pull/4379 it came to my attention that we are using some hardcoded constants for logic like the string `empty` for a path to a nonexistant file. This causes a lot of weird behavior in tests. Instead, we should simply check if these variables exist. They should use `None` instead of a nonexistent file.


Discussion:

Resolving patch:
diff --git a/readthedocs/config/config.py b/readthedocs/config/config.py
--- a/readthedocs/config/config.py
+++ b/readthedocs/config/config.py
@@ -128,8 +128,17 @@ class BuildConfigBase(object):
     """
     Config that handles the build of one particular documentation.
 
-    You need to call ``validate`` before the config is ready to use. Also
-    setting the ``output_base`` is required before using it for a build.
+    .. note::
+
+       You need to call ``validate`` before the config is ready to use.
+
+    :param env_config: A dict that cointains additional information
+                       about the environment.
+    :param raw_config: A dict with all configuration without validation.
+    :param source_file: The file that contains the configuration.
+                        All paths are relative to this file.
+                        If a dir is given, the configuration was loaded
+                        from another source (like the web admin).
     """
 
     version = None
@@ -139,21 +148,27 @@ def __init__(self, env_config, raw_config, source_file, source_position):
         self.raw_config = raw_config
         self.source_file = source_file
         self.source_position = source_position
-        self.base_path = os.path.dirname(self.source_file)
+        if os.path.isdir(self.source_file):
+            self.base_path = self.source_file
+        else:
+            self.base_path = os.path.dirname(self.source_file)
         self.defaults = self.env_config.get('defaults', {})
 
         self._config = {}
 
     def error(self, key, message, code):
         """Raise an error related to ``key``."""
-        source = '{file} [{pos}]'.format(
-            file=os.path.relpath(self.source_file, self.base_path),
-            pos=self.source_position,
-        )
-        error_message = '{source}: {message}'.format(
-            source=source,
-            message=message,
-        )
+        if not os.path.isdir(self.source_file):
+            source = '{file} [{pos}]'.format(
+                file=os.path.relpath(self.source_file, self.base_path),
+                pos=self.source_position,
+            )
+            error_message = '{source}: {message}'.format(
+                source=source,
+                message=message,
+            )
+        else:
+            error_message = message
         raise InvalidConfig(
             key=key,
             code=code,
@@ -271,10 +286,9 @@ def validate_output_base(self):
         """Validates that ``output_base`` exists and set its absolute path."""
         assert 'output_base' in self.env_config, (
                '"output_base" required in "env_config"')
-        base_path = os.path.dirname(self.source_file)
         output_base = os.path.abspath(
             os.path.join(
-                self.env_config.get('output_base', base_path),
+                self.env_config.get('output_base', self.base_path),
             )
         )
         return output_base
@@ -302,10 +316,9 @@ def validate_base(self):
         if 'base' in self.raw_config:
             base = self.raw_config['base']
         else:
-            base = os.path.dirname(self.source_file)
+            base = self.base_path
         with self.catch_validation_error('base'):
-            base_path = os.path.dirname(self.source_file)
-            base = validate_directory(base, base_path)
+            base = validate_directory(base, self.base_path)
         return base
 
     def validate_build(self):
@@ -452,9 +465,8 @@ def validate_conda(self):
             conda_environment = None
             if 'file' in raw_conda:
                 with self.catch_validation_error('conda.file'):
-                    base_path = os.path.dirname(self.source_file)
                     conda_environment = validate_file(
-                        raw_conda['file'], base_path
+                        raw_conda['file'], self.base_path
                     )
             conda['environment'] = conda_environment
 
@@ -469,9 +481,8 @@ def validate_requirements_file(self):
             requirements_file = self.raw_config['requirements_file']
         if not requirements_file:
             return None
-        base_path = os.path.dirname(self.source_file)
         with self.catch_validation_error('requirements_file'):
-            validate_file(requirements_file, base_path)
+            validate_file(requirements_file, self.base_path)
         return requirements_file
 
     def validate_formats(self):
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
@@ -73,7 +73,7 @@ def load_yaml_config(version):
         config = BuildConfigV1(
             env_config=env_config,
             raw_config={},
-            source_file=path.join(checkout_path, 'empty'),
+            source_file=checkout_path,
             source_position=0,
         )
         config.validate()


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
@@ -834,6 +834,14 @@ def test_version(self):
         build = self.get_build_config({})
         assert build.version == '2'
 
+    def test_correct_error_when_source_is_dir(self, tmpdir):
+        build = self.get_build_config({}, source_file=str(tmpdir))
+        with raises(InvalidConfig) as excinfo:
+            build.error(key='key', message='Message', code='code')
+        # We don't have any extra information about
+        # the source_file.
+        assert str(excinfo.value) == 'Invalid "key": Message'
+
     def test_formats_check_valid(self):
         build = self.get_build_config({'formats': ['htmlzip', 'pdf', 'epub']})
         build.validate()