Skip to content

Commit 67b01e2

Browse files
stsewdhumitos
andauthored
Config file: make sphinx or mkdocs configuration required for projects using Sphinx or MkDocs (#11852)
- This introduces a breaking change for users overriding the new (undocumented) jobs (create_environment, install, build.html and friends). This is, if they have overridden any of those jobs without explicitly declaring a Sphinx or MkDocs key, we will no longer run the sphinx/mkdocs commands, not their setup. - This hardcodes the dates from readthedocs/website#342 to through an error to users using the configuration file without an explicit sphinx/mkdocs configuration. This allows for users to keep using the new overrides without worrying about sphinx/mkdocs, while giving enough time to old users to migrate their projects to give an explicit path. ### Some notes - We are allowing to use sphinx/mkdocs and probably other keys with build.commands, even if those keys don't affect the build in anything. Not really something that "interrupt" users in any way, but it can be missleading. - A next step should be to not make python required at all, right now we still create a virtual environment. We probably want to create a virtual env only if python was provided in the list of build.tools. ---- - Ref: readthedocs/website#342 - Closes #10637 - Closes #11819 - Closes #11810 - Closes #11216 --------- Co-authored-by: Manuel Kaufmann <[email protected]>
1 parent ac87f67 commit 67b01e2

File tree

9 files changed

+412
-17
lines changed

9 files changed

+412
-17
lines changed

docs/user/config-file/v2.rst

+3-10
Original file line numberDiff line numberDiff line change
@@ -493,8 +493,7 @@ The ``$READTHEDOCS_OUTPUT/html`` directory will be uploaded and hosted by Read t
493493
sphinx
494494
~~~~~~
495495

496-
Configuration for Sphinx documentation
497-
(this is the default documentation type).
496+
Configuration for Sphinx documentation.
498497

499498
.. code-block:: yaml
500499
@@ -535,10 +534,7 @@ sphinx.configuration
535534
The path to the ``conf.py`` file, relative to the root of the project.
536535

537536
:Type: ``path``
538-
:Default: ``null``
539-
540-
If the value is ``null``,
541-
Read the Docs will try to find a ``conf.py`` file in your project.
537+
:Required: ``true``
542538

543539
sphinx.fail_on_warning
544540
``````````````````````
@@ -580,10 +576,7 @@ mkdocs.configuration
580576
The path to the ``mkdocs.yml`` file, relative to the root of the project.
581577

582578
:Type: ``path``
583-
:Default: ``null``
584-
585-
If the value is ``null``,
586-
Read the Docs will try to find a ``mkdocs.yml`` file in your project.
579+
:Required: ``true``
587580

588581
mkdocs.fail_on_warning
589582
``````````````````````

readthedocs/config/config.py

+79-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Build configuration for rtd."""
2-
32
import copy
3+
import datetime
44
import os
55
import re
66
from contextlib import contextmanager
77
from functools import lru_cache
88

9+
import pytz
910
from django.conf import settings
1011
from pydantic import BaseModel
1112

@@ -87,7 +88,13 @@ class BuildConfigBase:
8788

8889
version = None
8990

90-
def __init__(self, raw_config, source_file, base_path=None):
91+
def __init__(
92+
self,
93+
raw_config,
94+
source_file,
95+
base_path=None,
96+
deprecate_implicit_keys=None,
97+
):
9198
self._raw_config = copy.deepcopy(raw_config)
9299
self.source_config = copy.deepcopy(raw_config)
93100
self.source_file = source_file
@@ -102,6 +109,25 @@ def __init__(self, raw_config, source_file, base_path=None):
102109

103110
self._config = {}
104111

112+
if deprecate_implicit_keys is not None:
113+
self.deprecate_implicit_keys = deprecate_implicit_keys
114+
elif settings.RTD_ENFORCE_BROWNOUTS_FOR_DEPRECATIONS:
115+
tzinfo = pytz.timezone("America/Los_Angeles")
116+
now = datetime.datetime.now(tz=tzinfo)
117+
# Dates as per https://about.readthedocs.com/blog/2024/12/deprecate-config-files-without-sphinx-or-mkdocs-config/
118+
# fmt: off
119+
self.deprecate_implicit_keys = (
120+
# 12 hours brownout.
121+
datetime.datetime(2025, 1, 6, 0, 0, 0, tzinfo=tzinfo) < now < datetime.datetime(2025, 1, 6, 12, 0, 0, tzinfo=tzinfo)
122+
# 24 hours brownout.
123+
or datetime.datetime(2025, 1, 13, 0, 0, 0, tzinfo=tzinfo) < now < datetime.datetime(2025, 1, 14, 0, 0, 0, tzinfo=tzinfo)
124+
# Permanent removal.
125+
or datetime.datetime(2025, 1, 20, 0, 0, 0, tzinfo=tzinfo) < now
126+
)
127+
# fmt: on
128+
else:
129+
self.deprecate_implicit_keys = False
130+
105131
@contextmanager
106132
def catch_validation_error(self, key):
107133
"""Catch a ``ConfigValidationError`` and raises a ``ConfigError`` error."""
@@ -219,7 +245,6 @@ def __getattr__(self, name):
219245

220246

221247
class BuildConfigV2(BuildConfigBase):
222-
223248
"""Version 2 of the configuration file."""
224249

225250
version = "2"
@@ -251,6 +276,8 @@ def validate(self):
251276
self._config["sphinx"] = self.validate_sphinx()
252277
self._config["submodules"] = self.validate_submodules()
253278
self._config["search"] = self.validate_search()
279+
if self.deprecate_implicit_keys:
280+
self.validate_deprecated_implicit_keys()
254281
self.validate_keys()
255282

256283
def validate_formats(self):
@@ -722,6 +749,50 @@ def validate_search(self):
722749

723750
return search
724751

752+
def validate_deprecated_implicit_keys(self):
753+
"""
754+
Check for deprecated usages and raise an exception if found.
755+
756+
- If the sphinx key is used, a path to the configuration file is required.
757+
- If the mkdocs key is used, a path to the configuration file is required.
758+
- If none of the sphinx or mkdocs keys are used,
759+
and the user isn't overriding the new build jobs,
760+
the sphinx key is explicitly required.
761+
"""
762+
has_sphinx_key = "sphinx" in self.source_config
763+
has_mkdocs_key = "mkdocs" in self.source_config
764+
if has_sphinx_key and not self.sphinx.configuration:
765+
raise ConfigError(
766+
message_id=ConfigError.SPHINX_CONFIG_MISSING,
767+
)
768+
769+
if has_mkdocs_key and not self.mkdocs.configuration:
770+
raise ConfigError(
771+
message_id=ConfigError.MKDOCS_CONFIG_MISSING,
772+
)
773+
774+
if not self.new_jobs_overriden and not has_sphinx_key and not has_mkdocs_key:
775+
raise ConfigError(
776+
message_id=ConfigError.SPHINX_CONFIG_MISSING,
777+
)
778+
779+
@property
780+
def new_jobs_overriden(self):
781+
"""Check if any of the new (undocumented) build jobs are overridden."""
782+
build_jobs = self.build.jobs
783+
new_jobs = (
784+
build_jobs.create_environment,
785+
build_jobs.install,
786+
build_jobs.build.html,
787+
build_jobs.build.pdf,
788+
build_jobs.build.epub,
789+
build_jobs.build.htmlzip,
790+
)
791+
for job in new_jobs:
792+
if job is not None:
793+
return True
794+
return False
795+
725796
def validate_keys(self):
726797
"""
727798
Checks that we don't have extra keys (invalid ones).
@@ -812,6 +883,11 @@ def doctype(self):
812883
if "commands" in self._config["build"] and self._config["build"]["commands"]:
813884
return GENERIC
814885

886+
has_sphinx_key = "sphinx" in self.source_config
887+
has_mkdocs_key = "mkdocs" in self.source_config
888+
if self.new_jobs_overriden and not has_sphinx_key and not has_mkdocs_key:
889+
return GENERIC
890+
815891
if self.mkdocs:
816892
return "mkdocs"
817893
return self.sphinx.builder

readthedocs/config/exceptions.py

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class ConfigError(BuildUserError):
2626
SYNTAX_INVALID = "config:base:invalid-syntax"
2727
CONDA_KEY_REQUIRED = "config:conda:required"
2828

29+
SPHINX_CONFIG_MISSING = "config:sphinx:missing-config"
30+
MKDOCS_CONFIG_MISSING = "config:mkdocs:missing-config"
31+
2932

3033
# TODO: improve these error messages shown to the user
3134
# See https://github.com/readthedocs/readthedocs.org/issues/10502

readthedocs/config/notifications.py

+26
Original file line numberDiff line numberDiff line change
@@ -361,5 +361,31 @@
361361
),
362362
type=ERROR,
363363
),
364+
Message(
365+
id=ConfigError.SPHINX_CONFIG_MISSING,
366+
header=_("Missing Sphinx configuration key"),
367+
body=_(
368+
textwrap.dedent(
369+
"""
370+
The <code>sphinx.configuration</code> key is missing.
371+
This key is now required, see our <a href="https://about.readthedocs.com/blog/2024/12/deprecate-config-files-without-sphinx-or-mkdocs-config/">blog post</a> for more information.
372+
"""
373+
).strip(),
374+
),
375+
type=ERROR,
376+
),
377+
Message(
378+
id=ConfigError.MKDOCS_CONFIG_MISSING,
379+
header=_("Missing MkDocs configuration key"),
380+
body=_(
381+
textwrap.dedent(
382+
"""
383+
The <code>mkdocs.configuration</code> key is missing.
384+
This key is now required, see our <a href="https://about.readthedocs.com/blog/2024/12/deprecate-config-files-without-sphinx-or-mkdocs-config/">blog post</a> for more information.
385+
"""
386+
).strip(),
387+
),
388+
type=ERROR,
389+
),
364390
]
365391
registry.add(messages)

readthedocs/config/tests/test_config.py

+60-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from .utils import apply_fs
2424

2525

26-
def get_build_config(config, source_file="readthedocs.yml", validate=False):
26+
def get_build_config(config, source_file="readthedocs.yml", validate=False, **kwargs):
2727
# I'm adding these defaults here to avoid modifying all the config file from all the tests
2828
final_config = {
2929
"version": "2",
@@ -39,6 +39,7 @@ def get_build_config(config, source_file="readthedocs.yml", validate=False):
3939
build_config = BuildConfigV2(
4040
final_config,
4141
source_file=source_file,
42+
**kwargs,
4243
)
4344
if validate:
4445
build_config.validate()
@@ -1805,6 +1806,64 @@ def test_pop_config_raise_exception(self):
18051806
assert excinfo.value.format_values.get("value") == "invalid"
18061807
assert excinfo.value.message_id == ConfigValidationError.VALUE_NOT_FOUND
18071808

1809+
def test_sphinx_without_explicit_configuration(self):
1810+
data = {
1811+
"sphinx": {},
1812+
}
1813+
get_build_config(data, validate=True)
1814+
1815+
with raises(ConfigError) as excinfo:
1816+
get_build_config(data, validate=True, deprecate_implicit_keys=True)
1817+
1818+
assert excinfo.value.message_id == ConfigError.SPHINX_CONFIG_MISSING
1819+
1820+
data["sphinx"]["configuration"] = "conf.py"
1821+
get_build_config(data, validate=True, deprecate_implicit_keys=True)
1822+
1823+
def test_mkdocs_without_explicit_configuration(self):
1824+
data = {
1825+
"mkdocs": {},
1826+
}
1827+
get_build_config(data, validate=True)
1828+
1829+
with raises(ConfigError) as excinfo:
1830+
get_build_config(data, validate=True, deprecate_implicit_keys=True)
1831+
1832+
assert excinfo.value.message_id == ConfigError.MKDOCS_CONFIG_MISSING
1833+
1834+
data["mkdocs"]["configuration"] = "mkdocs.yml"
1835+
get_build_config(data, validate=True, deprecate_implicit_keys=True)
1836+
1837+
def test_config_without_sphinx_key(self):
1838+
data = {
1839+
"build": {
1840+
"os": "ubuntu-22.04",
1841+
"tools": {
1842+
"python": "3",
1843+
},
1844+
"jobs": {},
1845+
},
1846+
}
1847+
get_build_config(data, validate=True)
1848+
1849+
with raises(ConfigError) as excinfo:
1850+
get_build_config(data, validate=True, deprecate_implicit_keys=True)
1851+
1852+
assert excinfo.value.message_id == ConfigError.SPHINX_CONFIG_MISSING
1853+
1854+
# No exception should be raised when overriding any of the the new jobs.
1855+
data_copy = data.copy()
1856+
data_copy["build"]["jobs"]["create_environment"] = ["echo 'Hello World'"]
1857+
get_build_config(data_copy, validate=True, deprecate_implicit_keys=True)
1858+
1859+
data_copy = data.copy()
1860+
data_copy["build"]["jobs"]["install"] = ["echo 'Hello World'"]
1861+
get_build_config(data_copy, validate=True, deprecate_implicit_keys=True)
1862+
1863+
data_copy = data.copy()
1864+
data_copy["build"]["jobs"]["build"] = {"html": ["echo 'Hello World'"]}
1865+
get_build_config(data_copy, validate=True, deprecate_implicit_keys=True)
1866+
18081867
def test_as_dict_new_build_config(self, tmpdir):
18091868
build = get_build_config(
18101869
{

readthedocs/doc_builder/director.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from readthedocs.doc_builder.exceptions import BuildUserError
2424
from readthedocs.doc_builder.loader import get_builder_class
2525
from readthedocs.doc_builder.python_environments import Conda, Virtualenv
26-
from readthedocs.projects.constants import BUILD_COMMANDS_OUTPUT_PATH_HTML
26+
from readthedocs.projects.constants import BUILD_COMMANDS_OUTPUT_PATH_HTML, GENERIC
2727
from readthedocs.projects.exceptions import RepositoryError
2828
from readthedocs.projects.signals import after_build, before_build, before_vcs
2929
from readthedocs.storage import build_tools_storage
@@ -301,6 +301,12 @@ def create_environment(self):
301301
if self.data.config.build.jobs.create_environment is not None:
302302
self.run_build_job("create_environment")
303303
return
304+
305+
# If the builder is generic, we have nothing to do here,
306+
# as the commnads are provided by the user.
307+
if self.data.config.doctype == GENERIC:
308+
return
309+
304310
self.language_environment.setup_base()
305311

306312
# Install
@@ -309,6 +315,11 @@ def install(self):
309315
self.run_build_job("install")
310316
return
311317

318+
# If the builder is generic, we have nothing to do here,
319+
# as the commnads are provided by the user.
320+
if self.data.config.doctype == GENERIC:
321+
return
322+
312323
self.language_environment.install_core_requirements()
313324
self.language_environment.install_requirements()
314325

@@ -642,6 +653,11 @@ def build_docs_class(self, builder_class):
642653
only raise a warning exception here. A hard error will halt the build
643654
process.
644655
"""
656+
# If the builder is generic, we have nothing to do here,
657+
# as the commnads are provided by the user.
658+
if builder_class == GENERIC:
659+
return
660+
645661
builder = get_builder_class(builder_class)(
646662
build_env=self.build_environment,
647663
python_env=self.language_environment,

readthedocs/doc_builder/python_environments.py

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from readthedocs.config.models import PythonInstall, PythonInstallRequirements
1212
from readthedocs.core.utils.filesystem import safe_open
1313
from readthedocs.doc_builder.config import load_yaml_config
14+
from readthedocs.projects.constants import GENERIC
1415
from readthedocs.projects.exceptions import UserFileNotFound
1516
from readthedocs.projects.models import Feature
1617

@@ -168,6 +169,10 @@ def _install_latest_requirements(self, pip_install_cmd):
168169
cwd=self.checkout_path,
169170
)
170171

172+
# Nothing else to install for generic projects.
173+
if self.config.doctype == GENERIC:
174+
return
175+
171176
# Second, install all the latest core requirements
172177
requirements = []
173178

0 commit comments

Comments
 (0)