126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
import logging
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
from os.path import isdir, isfile, join
|
|
from typing import TYPE_CHECKING, BinaryIO, Callable
|
|
from urllib.parse import urlsplit
|
|
|
|
from properdocs.commands.build import build
|
|
from properdocs.config import load_config
|
|
from properdocs.livereload import LiveReloadServer, _serve_url
|
|
|
|
if TYPE_CHECKING:
|
|
from properdocs.config.defaults import ProperDocsConfig
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def serve(
|
|
config_file: str | BinaryIO | None = None,
|
|
livereload: bool = True,
|
|
build_type: str | None = None,
|
|
watch_theme: bool = False,
|
|
watch: list[str] = [],
|
|
*,
|
|
open_in_browser: bool = False,
|
|
**kwargs,
|
|
) -> None:
|
|
"""
|
|
Start the ProperDocs 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
|
|
site_dir = tempfile.mkdtemp(prefix='properdocs_')
|
|
|
|
get_config_file: Callable[[], str | BinaryIO | None]
|
|
if config_file is None or isinstance(config_file, str):
|
|
get_config_file = lambda: config_file
|
|
elif sys.stdin and config_file is sys.stdin.buffer:
|
|
# Stdin must be read only once, can't be reopened later.
|
|
config_file_content = sys.stdin.buffer.read()
|
|
get_config_file = lambda: io.BytesIO(config_file_content)
|
|
else:
|
|
# If closed file descriptor, reopen it through the file path instead.
|
|
get_config_file = lambda: (
|
|
config_file.name if getattr(config_file, 'closed', False) else config_file
|
|
)
|
|
|
|
def get_config():
|
|
config = load_config(
|
|
config_file=get_config_file(),
|
|
site_dir=site_dir,
|
|
**kwargs,
|
|
)
|
|
config.watch.extend(watch)
|
|
return config
|
|
|
|
is_clean = build_type == 'clean'
|
|
is_dirty = build_type == 'dirty'
|
|
|
|
config = get_config()
|
|
config.plugins.on_startup(command=('build' if is_clean else 'serve'), dirty=is_dirty)
|
|
|
|
host, port = config.dev_addr
|
|
mount_path = urlsplit(config.site_url or '/').path
|
|
config.site_url = serve_url = _serve_url(host, port, mount_path)
|
|
|
|
def builder(config: ProperDocsConfig | None = None):
|
|
log.info("Building documentation...")
|
|
if config is None:
|
|
config = get_config()
|
|
config.site_url = serve_url
|
|
|
|
build(config, serve_url=None if is_clean else serve_url, dirty=is_dirty)
|
|
|
|
server = LiveReloadServer(
|
|
builder=builder, host=host, port=port, root=site_dir, mount_path=mount_path
|
|
)
|
|
|
|
def error_handler(code) -> bytes | None:
|
|
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
|
|
|
|
try:
|
|
# Perform the initial build
|
|
builder(config)
|
|
|
|
if livereload:
|
|
# Watch the documentation files, the config file and the theme files.
|
|
server.watch(config.docs_dir)
|
|
if config.config_file_path:
|
|
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.on_serve(server, config=config, builder=builder)
|
|
|
|
for item in config.watch:
|
|
server.watch(item)
|
|
|
|
try:
|
|
server.serve(open_in_browser=open_in_browser)
|
|
except KeyboardInterrupt:
|
|
log.info("Shutting down...")
|
|
finally:
|
|
server.shutdown()
|
|
finally:
|
|
config.plugins.on_shutdown()
|
|
if isdir(site_dir):
|
|
shutil.rmtree(site_dir)
|