Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 33 additions & 59 deletions readthedocs/doc_builder/backends/mkdocs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""
MkDocs_ backend for building docs.

.. _MkDocs: http://www.mkdocs.org/
"""

import os
Expand All @@ -14,34 +12,25 @@
from readthedocs.doc_builder.base import BaseBuilder
from readthedocs.projects.constants import MKDOCS
from readthedocs.projects.constants import MKDOCS_HTML
from readthedocs.projects.exceptions import UserFileNotFound


log = structlog.get_logger(__name__)


def get_absolute_static_url():
class BaseMkdocs(BaseBuilder):
"""
Get the fully qualified static URL from settings.
MkDocs builder.

Mkdocs needs a full domain because it tries to link to local files.
This class and the following class use a different build method
than the Sphinx builders. We don't use `make` and instead call
the Python module directly.
"""
static_url = settings.STATIC_URL

if not static_url.startswith("http"):
domain = settings.PRODUCTION_DOMAIN
static_url = "http://{}{}".format(domain, static_url)

return static_url


class BaseMkdocs(BaseBuilder):
"""Mkdocs builder."""

# The default theme for mkdocs is the 'mkdocs' theme
DEFAULT_THEME_NAME = "mkdocs"
type = "mkdocs"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.old_artifact_path = os.path.join(
self.project_path, self.config.mkdocs.build_dir
)

# This is the *MkDocs* yaml file
self.yaml_file = self.get_yaml_config()
Expand All @@ -50,63 +39,48 @@ def get_final_doctype(self):
"""
Select a doctype based on the ``use_directory_urls`` setting.

https://www.mkdocs.org/user-guide/configuration/#use_directory_urls
MkDocs produces a different style of html depending on the
``use_directory_urls`` setting. We use this to set the doctype.
"""
with safe_open(self.yaml_file, "r") as f:
config = yaml.safe_load(f)

# TODO: we should eventually remove this method completely and stop
# relying on "loading the `mkdocs.yml` file in a safe way just to know
# if it's a MKDOCS or MKDOCS_HTML documentation type".

# Allow symlinks, but only the ones that resolve inside the base directory.
with safe_open(
self.yaml_file,
"r",
allow_symlinks=True,
base_path=self.project_path,
) as fh:
config = yaml_load_safely(fh)
use_directory_urls = config.get("use_directory_urls", True)
return MKDOCS if use_directory_urls else MKDOCS_HTML
if config.get("use_directory_urls"):
return MKDOCS_HTML
return MKDOCS

def get_yaml_config(self):
"""Find the ``mkdocs.yml`` file in the project root."""
mkdocs_path = self.config.mkdocs.configuration
if not mkdocs_path:
mkdocs_path = "mkdocs.yml"
return os.path.join(
self.project_path,
mkdocs_path,
)

def show_conf(self):
"""Show the current ``mkdocs.yaml`` being used."""
# Write the mkdocs.yml to the build logs
self.run(
"cat",
os.path.relpath(self.yaml_file, self.project_path),
cwd=self.project_path,
)
"""Find the MkDocs yaml configuration file."""
# We support both `.yml` and `.yaml` extensions
for extension in ["yml", "yaml"]:
config_file = os.path.join(
self.project_path, f"mkdocs.{extension}"
)
if os.path.exists(config_file):
return config_file

# If we didn't find any, return the default one
return os.path.join(self.project_path, "mkdocs.yml")

def build(self):
build_command = [
self.python_env.venv_bin(filename="python"),
"-m",
"mkdocs",
self.builder,
"build",
"--clean",
"--site-dir",
os.path.join("$READTHEDOCS_OUTPUT", "html"),
self.old_artifact_path,
"--config-file",
os.path.relpath(self.yaml_file, self.project_path),
]

if self.config.mkdocs.fail_on_warning:
build_command.append("--strict")
cmd_ret = self.run(
*build_command,
cwd=self.project_path,
bin_path=self.python_env.venv_bin(),
*build_command, cwd=self.project_path, bin_path=self.python_env.venv_bin()
)
return cmd_ret.successful
return cmd_ret.success


class MkdocsHTML(BaseMkdocs):
Expand Down
12 changes: 6 additions & 6 deletions readthedocs/projects/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@


class ProjectConfigurationError(BuildUserError):
"""Error raised trying to configure a project for build."""

NOT_FOUND = "project:sphinx:conf-py-not-found"
MULTIPLE_CONF_FILES = "project:sphinx:multiple-conf-py-files-found"
NOT_FOUND = "project:configuration:not-found"
MULTIPLE_CONF_FILES = "project:configuration:multiple-conf-files"
INVALID_VERSION = "project:configuration:invalid-version"
INVALID = "project:configuration:invalid"
MKDOCS_YAML_NOT_FOUND = "project:configuration:mkdocs-yaml-not-found"


class UserFileNotFound(BuildUserError):
FILE_NOT_FOUND = "project:file:not-found"



class RepositoryError(BuildUserError):
"""Failure during repository operation."""

Expand Down
20 changes: 13 additions & 7 deletions readthedocs/projects/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,19 @@
id=ProjectConfigurationError.MULTIPLE_CONF_FILES,
header=_("Multiple Sphinx configuration files found"),
body=_(
textwrap.dedent(
"""
We found more than one <code>conf.py</code> and are not sure which one to use.
Please, specify the correct file under the Advanced settings tab
in the project's Admin.
"""
).strip(),
"We found multiple Sphinx configuration files in your repository. "
"Please, remove one of them and specify the correct one in your "
"configuration file."
),
type=ERROR,
),
Message(
id=ProjectConfigurationError.MKDOCS_YAML_NOT_FOUND,
header=_("MkDocs configuration file not found"),
body=_(
"We could not find a MkDocs configuration file in your repository. "
"Please, make sure you have a 'mkdocs.yml' file in your repository "
"and try again."
),
type=ERROR,
),
Expand Down
16 changes: 16 additions & 0 deletions readthedocs/projects/tests/test_build_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,13 @@ def test_build_updates_documentation_type(self, load_yaml_config):
validate=True,
)

mkdocs_path = os.path.join(
self.project.checkout_path(self.version.slug),
"mkdocs.yml"
)
with open(mkdocs_path, "w") as f:
f.write("site_name: Test Project\n")

# Create the artifact paths, so that `store_build_artifacts`
# properly runs: https://github.com/readthedocs/readthedocs.org/blob/faa611fad689675f81101ea643770a6b669bf529/readthedocs/projects/tasks/builds.py#L798-L804
os.makedirs(self.project.artifact_path(version=self.version.slug, type_="html"))
Expand Down Expand Up @@ -2578,6 +2585,15 @@ def test_mkdocs_fail_on_warning(self, load_yaml_config):
validate=True,
)

mkdocs_path = os.path.join(
self.project.checkout_path(self.version.slug),
"docs",
"mkdocs.yaml"
)
os.makedirs(os.path.dirname(mkdocs_path), exist_ok=True)
with open(mkdocs_path, "w") as f:
f.write("site_name: Test Project\n")

self._trigger_update_docs_task()

self.mocker.mocks["environment.run"].assert_has_calls(
Expand Down