1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
|
from __future__ import annotations
import functools
import logging
import shutil
import tempfile
from os.path import isdir, isfile, join
from typing import Optional
from urllib.parse import urlsplit
import jinja2.exceptions
from mkdocs.commands.build import build
from mkdocs.config import load_config
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.exceptions import Abort
from mkdocs.livereload import LiveReloadServer
log = logging.getLogger(__name__)
def serve(
config_file=None,
dev_addr=None,
strict=None,
theme=None,
theme_dir=None,
livereload='livereload',
watch_theme=False,
watch=[],
**kwargs,
):
"""
Start the MkDocs development server
By default it will serve the documentation on http://localhost:8000/ and
it will rebuild the documentation and refresh the page automatically
whenever a file is edited.
"""
# Create a temporary build directory, and set some options to serve it
# PY2 returns a byte string by default. The Unicode prefix ensures a Unicode
# string is returned. And it makes MkDocs temp dirs easier to identify.
site_dir = tempfile.mkdtemp(prefix='mkdocs_')
def mount_path(config: MkDocsConfig):
return urlsplit(config.site_url or '/').path
get_config = functools.partial(
load_config,
config_file=config_file,
dev_addr=dev_addr,
strict=strict,
theme=theme,
theme_dir=theme_dir,
site_dir=site_dir,
**kwargs,
)
live_server = livereload in ('dirty', 'livereload')
dirty = livereload == 'dirty'
def builder(config: Optional[MkDocsConfig] = None):
log.info("Building documentation...")
if config is None:
config = get_config()
# combine CLI watch arguments with config file values
if config.watch is None:
config.watch = watch
else:
config.watch.extend(watch)
# Override a few config settings after validation
config.site_url = f'http://{config.dev_addr}{mount_path(config)}'
build(config, live_server=live_server, dirty=dirty)
config = get_config()
config['plugins'].run_event('startup', command='serve', dirty=dirty)
try:
# Perform the initial build
builder(config)
host, port = config.dev_addr
server = LiveReloadServer(
builder=builder, host=host, port=port, root=site_dir, mount_path=mount_path(config)
)
def error_handler(code) -> Optional[bytes]:
if code in (404, 500):
error_page = join(site_dir, f'{code}.html')
if isfile(error_page):
with open(error_page, 'rb') as f:
return f.read()
return None
server.error_handler = error_handler
if live_server:
# Watch the documentation files, the config file and the theme files.
server.watch(config.docs_dir)
server.watch(config.config_file_path)
if watch_theme:
for d in config.theme.dirs:
server.watch(d)
# Run `serve` plugin events.
server = config.plugins.run_event('serve', server, config=config, builder=builder)
for item in config.watch:
server.watch(item)
try:
server.serve()
except KeyboardInterrupt:
log.info("Shutting down...")
finally:
server.shutdown()
except jinja2.exceptions.TemplateError:
# This is a subclass of OSError, but shouldn't be suppressed.
raise
except OSError as e: # pragma: no cover
# Avoid ugly, unhelpful traceback
raise Abort(f'{type(e).__name__}: {e}')
finally:
config['plugins'].run_event('shutdown')
if isdir(site_dir):
shutil.rmtree(site_dir)
|