serve.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import logging
  2. import shutil
  3. import tempfile
  4. import sys
  5. from os.path import isfile, join
  6. from mkdocs.commands.build import build
  7. from mkdocs.config import load_config
  8. log = logging.getLogger(__name__)
  9. def _init_asyncio_patch():
  10. """
  11. Select compatible event loop for Tornado 5+.
  12. As of Python 3.8, the default event loop on Windows is `proactor`,
  13. however Tornado requires the old default "selector" event loop.
  14. As Tornado has decided to leave this to users to set, MkDocs needs
  15. to set it. See https://github.com/tornadoweb/tornado/issues/2608.
  16. """
  17. if sys.platform.startswith("win") and sys.version_info >= (3, 8):
  18. import asyncio
  19. try:
  20. from asyncio import WindowsSelectorEventLoopPolicy
  21. except ImportError:
  22. pass # Can't assign a policy which doesn't exist.
  23. else:
  24. if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy):
  25. asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
  26. def _get_handler(site_dir, StaticFileHandler):
  27. from tornado.template import Loader
  28. class WebHandler(StaticFileHandler):
  29. def write_error(self, status_code, **kwargs):
  30. if status_code in (404, 500):
  31. error_page = '{}.html'.format(status_code)
  32. if isfile(join(site_dir, error_page)):
  33. self.write(Loader(site_dir).load(error_page).generate())
  34. else:
  35. super().write_error(status_code, **kwargs)
  36. return WebHandler
  37. def _livereload(host, port, config, builder, site_dir):
  38. # We are importing here for anyone that has issues with livereload. Even if
  39. # this fails, the --no-livereload alternative should still work.
  40. _init_asyncio_patch()
  41. from livereload import Server
  42. import livereload.handlers
  43. class LiveReloadServer(Server):
  44. def get_web_handlers(self, script):
  45. handlers = super().get_web_handlers(script)
  46. # replace livereload handler
  47. return [(handlers[0][0], _get_handler(site_dir, livereload.handlers.StaticFileHandler), handlers[0][2],)]
  48. server = LiveReloadServer()
  49. # Watch the documentation files, the config file and the theme files.
  50. server.watch(config['docs_dir'], builder)
  51. server.watch(config['config_file_path'], builder)
  52. for d in config['theme'].dirs:
  53. server.watch(d, builder)
  54. # Run `serve` plugin events.
  55. server = config['plugins'].run_event('serve', server, config=config, builder=builder)
  56. server.serve(root=site_dir, host=host, port=port, restart_delay=0)
  57. def _static_server(host, port, site_dir):
  58. # Importing here to separate the code paths from the --livereload
  59. # alternative.
  60. _init_asyncio_patch()
  61. from tornado import ioloop
  62. from tornado import web
  63. application = web.Application([
  64. (r"/(.*)", _get_handler(site_dir, web.StaticFileHandler), {
  65. "path": site_dir,
  66. "default_filename": "index.html"
  67. }),
  68. ])
  69. application.listen(port=port, address=host)
  70. log.info('Running at: http://%s:%s/', host, port)
  71. log.info('Hold ctrl+c to quit.')
  72. try:
  73. ioloop.IOLoop.instance().start()
  74. except KeyboardInterrupt:
  75. log.info('Stopping server...')
  76. def serve(config_file=None, dev_addr=None, strict=None, theme=None,
  77. theme_dir=None, livereload='livereload', **kwargs):
  78. """
  79. Start the MkDocs development server
  80. By default it will serve the documentation on http://localhost:8000/ and
  81. it will rebuild the documentation and refresh the page automatically
  82. whenever a file is edited.
  83. """
  84. # Create a temporary build directory, and set some options to serve it
  85. # PY2 returns a byte string by default. The Unicode prefix ensures a Unicode
  86. # string is returned. And it makes MkDocs temp dirs easier to identify.
  87. site_dir = tempfile.mkdtemp(prefix='mkdocs_')
  88. def builder():
  89. log.info("Building documentation...")
  90. config = load_config(
  91. config_file=config_file,
  92. dev_addr=dev_addr,
  93. strict=strict,
  94. theme=theme,
  95. theme_dir=theme_dir,
  96. site_dir=site_dir,
  97. **kwargs
  98. )
  99. # Override a few config settings after validation
  100. config['site_url'] = 'http://{}/'.format(config['dev_addr'])
  101. live_server = livereload in ['dirty', 'livereload']
  102. dirty = livereload == 'dirty'
  103. build(config, live_server=live_server, dirty=dirty)
  104. return config
  105. try:
  106. # Perform the initial build
  107. config = builder()
  108. host, port = config['dev_addr']
  109. if livereload in ['livereload', 'dirty']:
  110. _livereload(host, port, config, builder, site_dir)
  111. else:
  112. _static_server(host, port, site_dir)
  113. finally:
  114. shutil.rmtree(site_dir)