Repository: mkdocs/mkdocs

Issue:
Make auto-reload optional
This is an offshoot from #83. To do this, we would want something like:

```
`mkdocs serve --no-rebuild`
```

Ideally this would have a code path that doesn't even import livereload. This will help users that are having problems with livereload or just want something lighter to run.



Discussion:

Resolving patch:
diff --git a/mkdocs/cli.py b/mkdocs/cli.py
--- a/mkdocs/cli.py
+++ b/mkdocs/cli.py
@@ -15,17 +15,17 @@
 log = logging.getLogger(__name__)
 
 
-def configure_logging(is_verbose=False):
+def configure_logging(log_name='mkdocs', verbose=False):
     '''When a --verbose flag is passed, increase the verbosity of mkdocs'''
 
-    logger = logging.getLogger('mkdocs')
+    logger = logging.getLogger(log_name)
     logger.propagate = False
     stream = logging.StreamHandler()
     formatter = logging.Formatter("%(levelname)-7s -  %(message)s ")
     stream.setFormatter(formatter)
     logger.addHandler(stream)
 
-    if is_verbose:
+    if verbose:
         logger.setLevel(logging.DEBUG)
     else:
         logger.setLevel(logging.INFO)
@@ -40,6 +40,7 @@ def configure_logging(is_verbose=False):
 theme_help = "The theme to use when building your documentation."
 theme_choices = utils.get_theme_names()
 site_dir_help = "The directory to output the result of the documentation build."
+reload_help = "Enable and disable the live reloading in the development server."
 
 
 @click.group()
@@ -49,7 +50,7 @@ def cli(verbose):
     """
     MkDocs - Project documentation with Markdown.
     """
-    configure_logging(verbose)
+    configure_logging(verbose=verbose)
 
 
 @cli.command(name="serve")
@@ -57,13 +58,16 @@ def cli(verbose):
 @click.option('--dev-addr', help=dev_addr_help, metavar='<IP:PORT>')
 @click.option('--strict', is_flag=True, help=strict_help)
 @click.option('--theme', type=click.Choice(theme_choices), help=theme_help)
-def serve_command(dev_addr, config_file, strict, theme):
+@click.option('--livereload/--no-livereload', default=True, help=reload_help)
+def serve_command(dev_addr, config_file, strict, theme, livereload):
     """Run the builtin development server"""
+    configure_logging('tornado')
     serve.serve(
         config_file=config_file,
         dev_addr=dev_addr,
         strict=strict,
         theme=theme,
+        livereload=livereload,
     )
 
 
diff --git a/mkdocs/serve.py b/mkdocs/serve.py
--- a/mkdocs/serve.py
+++ b/mkdocs/serve.py
@@ -1,19 +1,68 @@
+import logging
+import shutil
 import tempfile
 
-from livereload import Server
-
 from mkdocs.build import build
 from mkdocs.config import load_config
 
+log = logging.getLogger(__name__)
+
+
+def _livereload(host, port, config, builder, site_dir):
+
+    # We are importing here for anyone that has issues with livereload. Even if
+    # this fails, the --no-livereload alternative should still work.
+    from livereload import Server
+
+    server = Server()
+
+    # Watch the documentation files, the config file and the theme files.
+    server.watch(config['docs_dir'], builder)
+    server.watch(config['config_file_path'], builder)
+
+    for d in config['theme_dir']:
+        server.watch(d, builder)
+
+    server.serve(root=site_dir, host=host, port=int(port), restart_delay=0)
+
+
+def _static_server(host, port, site_dir):
 
-def serve(config_file=None, dev_addr=None, strict=None, theme=None):
+    # Importing here to seperate the code paths from the --livereload
+    # alternative.
+    from tornado import ioloop
+    from tornado import web
+
+    application = web.Application([
+        (r"/(.*)", web.StaticFileHandler, {
+            "path": site_dir,
+            "default_filename": "index.html"
+        }),
+    ])
+    application.listen(port=port, address=host)
+
+    log.info('Running at: http://%s:%s/', host, port)
+    log.info('Hold ctrl+c to quit.')
+    try:
+        ioloop.IOLoop.instance().start()
+    except KeyboardInterrupt:
+        log.info('Stopping server...')
+
+
+def serve(config_file=None, dev_addr=None, strict=None, theme=None,
+          livereload=True):
     """
-    Start the devserver, and rebuild the docs whenever any changes take effect.
+    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
     tempdir = tempfile.mkdtemp()
 
     def builder():
+        log.info("Rebuilding documentation...")
         config = load_config(
             config_file=config_file,
             dev_addr=dev_addr,
@@ -27,14 +76,12 @@ def builder():
     # Perform the initial build
     config = builder()
 
-    server = Server()
-
-    # Watch the documentation files, the config file and the theme files.
-    server.watch(config['docs_dir'], builder)
-    server.watch(config['config_file_path'], builder)
-
-    for d in config['theme_dir']:
-        server.watch(d, builder)
-
     host, port = config['dev_addr'].split(':', 1)
-    server.serve(root=tempdir, host=host, port=int(port), restart_delay=0)
+
+    try:
+        if livereload:
+            _livereload(host, port, config, builder, tempdir)
+        else:
+            _static_server(host, port, tempdir)
+    finally:
+        shutil.rmtree(tempdir)
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -81,6 +81,7 @@ def get_package_data(package):
         'Markdown>=2.3.1,<2.5' if PY26 else 'Markdown>=2.3.1',
         'PyYAML>=3.10',
         'six>=1.9.0',
+        'tornado>=4.1',
     ],
     entry_points={
         'console_scripts': [


Test patch:
diff --git a/mkdocs/tests/integration.py b/mkdocs/tests/integration.py
--- a/mkdocs/tests/integration.py
+++ b/mkdocs/tests/integration.py
@@ -16,33 +16,26 @@
 
 from __future__ import print_function
 
-import os
 import click
-import contextlib
+import logging
+import os
 import sys
 
-from six.moves import cStringIO
-
-from mkdocs import cli, config, serve, build, utils
+from mkdocs import cli, config, build, utils
 
-MKDOCS_CONFIG = os.path.join(os.path.dirname(__file__), '../../mkdocs.yml')
+DIR = os.path.dirname(__file__)
+MKDOCS_CONFIG = os.path.abspath(os.path.join(DIR, '../../mkdocs.yml'))
 MKDOCS_THEMES = utils.get_theme_names()
 
 
-@contextlib.contextmanager
-def capture_stdout():
-    oldout, olderr = sys.stdout, sys.stderr
-    try:
-        out = [cStringIO(), cStringIO()]
-        sys.stdout, sys.stderr = out
-        yield out
-    finally:
-        sys.stdout, sys.stderr = oldout, olderr
-        out[0] = out[0].getvalue()
-        out[1] = out[1].getvalue()
+def silence_logging(is_verbose=False):
+    '''When a --verbose flag is passed, increase the verbosity of mkdocs'''
+
+    logger = logging.getLogger('mkdocs')
+    logger.setLevel(logging.WARNING)
 
 
-def run_build(theme_name, output=None, config_file=None, quiet=False):
+def run_build(theme_name, output, config_file, quiet):
     """
     Given a theme name and output directory use the configuration
     for the MkDocs documentation and overwrite the site_dir and
@@ -50,59 +43,50 @@ def run_build(theme_name, output=None, config_file=None, quiet=False):
     each theme, one at a time.
     """
 
-    should_serve = output is None
-    options = {}
-
-    if not serve:
-        if not os.path.exists(output):
-            os.makedirs(output)
-        options['site_dir'] = os.path.join(output, theme_name)
+    options = {
+        'theme': theme_name,
+    }
 
     if config_file is None:
         config_file = open(MKDOCS_CONFIG, 'rb')
-
-    if not quiet:
-        print("Using config: {0}".format(config_file))
-
-    cli.configure_logging()
-    conf = config.load_config(config_file=config_file, theme=theme_name)
-
-    if should_serve:
-        if not quiet:
-            print("Serving {0}".format(theme_name))
-        try:
-            serve.serve(conf)
-        except KeyboardInterrupt:
-            return
-    else:
         if not quiet:
-            print("Building {0}".format(theme_name))
+            print("Using config: {0}".format(config_file.name))
 
-        try:
-            with capture_stdout() as out:
-                build.build(conf)
-                build.build(conf, dump_json=True)
-        except Exception:
-            print("Failed building {0}".format(theme_name), file=sys.stderr)
-            raise
+    if not os.path.exists(output):
+        os.makedirs(output)
+    options['site_dir'] = os.path.join(output, theme_name)
 
-        if not quiet:
-            print(''.join(out))
+    if not quiet:
+        print("Building {0}".format(theme_name))
+
+    try:
+        conf = config.load_config(config_file=config_file, **options)
+        config_file.close()
+        build.build(conf)
+        build.build(conf, dump_json=True)
+    except Exception:
+        print("Error building: {0}".format(theme_name), file=sys.stderr)
+        raise
 
 
 @click.command()
 @click.option('--output',
               help="The output directory to use when building themes",
               type=click.Path(file_okay=False, writable=True))
-@click.option('--config',
+@click.option('--config-file',
               help="The MkDocs project config to use.",
               type=click.File('rb'))
 @click.option('--quiet', is_flag=True)
-def main(output=None, config=None, quiet=False):
+def main(output=None, config_file=None, quiet=False):
+
+    if quiet:
+        silence_logging()
+    else:
+        cli.configure_logging()
 
     for theme in sorted(MKDOCS_THEMES):
 
-        run_build(theme, output, config, quiet)
+        run_build(theme, output, config_file, quiet)
 
     print("The theme builds are available in {0}".format(output))