From c4cb25e584ad496bb6cb5a189dcd5085a696adc8 Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Sat, 4 Apr 2015 20:08:51 +0100 Subject: [PATCH 1/6] Automatically refresh the browser on edits Use python-livereload to automatically refresh your browser. Closes #163 --- mkdocs/config.py | 2 + mkdocs/serve.py | 119 ++++++----------------------------------------- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 18 insertions(+), 107 deletions(-) diff --git a/mkdocs/config.py b/mkdocs/config.py index 787fdd36..06559274 100644 --- a/mkdocs/config.py +++ b/mkdocs/config.py @@ -78,6 +78,8 @@ def load_config(filename='mkdocs.yml', options=None): options = options or {} if 'config' in options: filename = options['config'] + else: + options['config'] = filename if not os.path.exists(filename): raise ConfigurationError("Config file '%s' does not exist." % filename) with open(filename, 'r', encoding='utf-8') as fp: diff --git a/mkdocs/serve.py b/mkdocs/serve.py index 911e9b53..3b436b8e 100644 --- a/mkdocs/serve.py +++ b/mkdocs/serve.py @@ -1,72 +1,9 @@ -# coding: utf-8 -from __future__ import print_function - -from watchdog import events -from watchdog.observers.polling import PollingObserver -from mkdocs.build import build -from mkdocs.compat import httpserver, socketserver, urlunquote -from mkdocs.config import load_config -import os -import posixpath -import shutil -import sys import tempfile +from livereload import Server -class BuildEventHandler(events.FileSystemEventHandler): - """ - Perform a rebuild when anything in the theme or docs directory changes. - """ - def __init__(self, options): - super(BuildEventHandler, self).__init__() - self.options = options - - def on_any_event(self, event): - if not isinstance(event, events.DirModifiedEvent): - print('Rebuilding documentation...', end='') - config = load_config(options=self.options) - build(config, live_server=True) - print(' done') - - -class ConfigEventHandler(BuildEventHandler): - """ - Perform a rebuild when the config file changes. - """ - def on_any_event(self, event): - try: - if os.path.basename(event.src_path) == 'mkdocs.yml': - super(ConfigEventHandler, self).on_any_event(event) - except Exception as e: - print(e) - - -class FixedDirectoryHandler(httpserver.SimpleHTTPRequestHandler): - """ - Override the default implementation to allow us to specify the served - directory, instead of being hardwired to the current working directory. - """ - base_dir = os.getcwd() - - def translate_path(self, path): - # abandon query parameters - path = path.split('?', 1)[0] - path = path.split('#', 1)[0] - path = posixpath.normpath(urlunquote(path)) - words = path.split('/') - words = filter(None, words) - path = self.base_dir - for word in words: - drive, word = os.path.splitdrive(word) - head, word = os.path.split(word) - if word in (os.curdir, os.pardir): - continue - path = os.path.join(path, word) - return path - - def log_message(self, format, *args): - date_str = self.log_date_time_string() - sys.stderr.write('[%s] %s\n' % (date_str, format % args)) +from mkdocs.build import build +from mkdocs.config import load_config def serve(config, options=None): @@ -77,48 +14,20 @@ def serve(config, options=None): tempdir = tempfile.mkdtemp() options['site_dir'] = tempdir - # Only use user-friendly URLs when running the live server - options['use_directory_urls'] = True + def builder(): + config = load_config(options=options) + build(config, live_server=True) # Perform the initial build - config = load_config(options=options) - build(config, live_server=True) + builder() - # Note: We pass any command-line options through so that we - # can re-apply them if the config file is reloaded. - event_handler = BuildEventHandler(options) - config_event_handler = ConfigEventHandler(options) + server = Server() - # We could have used `Observer()`, which can be faster, but - # `PollingObserver()` works more universally. - observer = PollingObserver() - observer.schedule(event_handler, config['docs_dir'], recursive=True) - for theme_dir in config['theme_dir']: - if not os.path.exists(theme_dir): - continue - observer.schedule(event_handler, theme_dir, recursive=True) - observer.schedule(config_event_handler, '.') - observer.start() - - class TCPServer(socketserver.TCPServer): - allow_reuse_address = True - - class DocsDirectoryHandler(FixedDirectoryHandler): - base_dir = config['site_dir'] + # Watch the documentation files, the config file and the theme files. + server.watch(config['docs_dir'], builder) + server.watch(config['config'], builder) + for d in config['theme_dir']: + server.watch(d, builder) host, port = config['dev_addr'].split(':', 1) - server = TCPServer((host, int(port)), DocsDirectoryHandler) - - print('Running at: http://%s:%s/' % (host, port)) - print('Live reload enabled.') - print('Hold ctrl+c to quit.') - try: - server.serve_forever() - except KeyboardInterrupt: - print('Stopping server...') - - # Clean up - observer.stop() - observer.join() - shutil.rmtree(tempdir) - print('Quit complete') + server.serve(root=tempdir, host=host, port=port, restart_delay=0) diff --git a/requirements.txt b/requirements.txt index ac87b96d..5f8cf682 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ ghp-import>=0.4.1 Jinja2>=2.7.1 +livereload>=2.3.2 Markdown>=2.5 PyYAML>=3.10 -watchdog>=0.7.0 diff --git a/setup.py b/setup.py index 64698632..d6e55b8c 100755 --- a/setup.py +++ b/setup.py @@ -20,9 +20,9 @@ license = 'BSD' install_requires = [ 'ghp-import>=0.4.1', 'Jinja2>=2.7.1', + 'livereload>=2.3.2', 'Markdown>=2.3.1,<2.5' if PY26 else 'Markdown>=2.3.1', 'PyYAML>=3.10', - 'watchdog>=0.7.0', ] long_description = ( From d0198092f9cfcd54a6a644651f9df558cc5fc716 Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Fri, 10 Apr 2015 19:14:28 +0100 Subject: [PATCH 2/6] Improve Path handling across MkDocs This change aims to standardise path handling in MkDocs. Paths should always be in the form expected by the current platform and then they only need to be converted to URL's when used. Fixes #439 --- mkdocs/compat.py | 2 ++ mkdocs/config.py | 33 ++++++++++++++++++++++++++------- mkdocs/main.py | 5 ++++- mkdocs/nav.py | 25 ++++++++++++++----------- mkdocs/relative_path_ext.py | 8 +++++--- mkdocs/tests/nav_tests.py | 1 + mkdocs/utils.py | 16 ++++++---------- 7 files changed, 58 insertions(+), 32 deletions(-) diff --git a/mkdocs/compat.py b/mkdocs/compat.py index 518a4937..05b2ed03 100644 --- a/mkdocs/compat.py +++ b/mkdocs/compat.py @@ -8,6 +8,7 @@ if PY2: from urlparse import urljoin, urlparse, urlunparse import urllib urlunquote = urllib.unquote + from urllib import pathname2url import SimpleHTTPServer as httpserver httpserver = httpserver @@ -25,6 +26,7 @@ if PY2: basestring = basestring else: # PY3 from urllib.parse import urljoin, urlparse, urlunparse, unquote + from urllib.request import pathname2url urlunquote = unquote import http.server as httpserver diff --git a/mkdocs/config.py b/mkdocs/config.py index 06559274..99171ced 100644 --- a/mkdocs/config.py +++ b/mkdocs/config.py @@ -1,14 +1,16 @@ # coding: utf-8 +from io import open +import logging +import ntpath +import os + +import yaml + from mkdocs import utils from mkdocs.compat import urlparse from mkdocs.exceptions import ConfigurationError -import logging -import os -import yaml -from io import open - log = logging.getLogger(__name__) DEFAULT_CONFIG = { @@ -118,10 +120,10 @@ def validate_config(user_config): pages = [] extra_css = [] extra_javascript = [] - for (dirpath, dirnames, filenames) in os.walk(config['docs_dir']): + for (dirpath, _, filenames) in os.walk(config['docs_dir']): for filename in sorted(filenames): fullpath = os.path.join(dirpath, filename) - relpath = os.path.relpath(fullpath, config['docs_dir']) + relpath = os.path.normpath(os.path.relpath(fullpath, config['docs_dir'])) if utils.is_markdown_file(filename): # index pages should always be the first listed page. @@ -136,6 +138,23 @@ def validate_config(user_config): if config['pages'] is None: config['pages'] = pages + else: + """ + If the user has provided the pages config, then iterate through and + check for Windows style paths. If they are found, output a warning + and continue. + """ + for page_config in config['pages']: + if isinstance(page_config, str): + path = page_config + elif len(page_config) in (1, 2, 3): + path = page_config[0] + + if ntpath.sep in path: + log.warning("The config path contains Windows style paths (\\ " + " backward slash) and will have comparability " + "issues if it is used on another platform.") + break if config['extra_css'] is None: config['extra_css'] = extra_css diff --git a/mkdocs/main.py b/mkdocs/main.py index 8b9a9412..a196c039 100755 --- a/mkdocs/main.py +++ b/mkdocs/main.py @@ -17,7 +17,10 @@ from mkdocs.serve import serve def configure_logging(options): '''When a --verbose flag is passed, increase the verbosity of mkdocs''' logger = logging.getLogger('mkdocs') - logger.addHandler(logging.StreamHandler()) + sh = logging.StreamHandler() + formatter = logging.Formatter('%(levelname)s - %(message)s') + sh.setFormatter(formatter) + logger.addHandler(sh) if 'verbose' in options: logger.setLevel(logging.DEBUG) else: diff --git a/mkdocs/nav.py b/mkdocs/nav.py index 3e95a074..a148aef5 100644 --- a/mkdocs/nav.py +++ b/mkdocs/nav.py @@ -6,10 +6,13 @@ Deals with generating the site-wide navigation. This consists of building a set of interlinked page and header objects. """ -from mkdocs import utils, exceptions -import posixpath +import logging import os +from mkdocs import utils, exceptions + +log = logging.getLogger(__name__) + def filename_to_title(filename): """ @@ -84,7 +87,7 @@ class URLContext(object): self.base_path = '/' def set_current_url(self, current_url): - self.base_path = posixpath.dirname(current_url) + self.base_path = os.path.dirname(current_url) def make_relative(self, url): """ @@ -92,14 +95,14 @@ class URLContext(object): given the context of the current page. """ suffix = '/' if (url.endswith('/') and len(url) > 1) else '' - # Workaround for bug on `posixpath.relpath()` in Python 2.6 + # Workaround for bug on `os.path.relpath()` in Python 2.6 if self.base_path == '/': if url == '/': # Workaround for static assets return '.' return url.lstrip('/') # Under Python 2.6, relative_path adds an extra '/' at the end. - relative_path = posixpath.relpath(url, start=self.base_path).rstrip('/') + suffix + relative_path = os.path.relpath(url, start=self.base_path).rstrip('/') + suffix return relative_path @@ -126,7 +129,7 @@ class FileContext(object): Given a relative file path return it as a POSIX-style absolute filepath, given the context of the current page. """ - return posixpath.normpath(posixpath.join(self.base_path, path)) + return os.path.normpath(os.path.join(self.base_path, path)) class Page(object): @@ -196,13 +199,13 @@ def _generate_site_navigation(pages_config, url_context, use_directory_urls=True for config_line in pages_config: if isinstance(config_line, str): - path = utils.normalise_path(config_line) + path = os.path.normpath(config_line) title, child_title = None, None elif len(config_line) in (1, 2, 3): # Pad any items that don't exist with 'None' padded_config = (list(config_line) + [None, None])[:3] path, title, child_title = padded_config - path = utils.normalise_path(path) + path = os.path.normpath(path) else: msg = ( "Line in 'page' config contained %d items. " @@ -213,12 +216,12 @@ def _generate_site_navigation(pages_config, url_context, use_directory_urls=True # If both the title and child_title are None, then we # have just been given a path. If that path contains a / # then lets automatically nest it. - if title is None and child_title is None and posixpath.sep in path: - filename = path.split(posixpath.sep)[-1] + if title is None and child_title is None and os.path.sep in path: + filename = path.split(os.path.sep)[-1] child_title = filename_to_title(filename) if title is None: - filename = path.split(posixpath.sep)[0] + filename = path.split(os.path.sep)[0] title = filename_to_title(filename) url = utils.get_url_path(path, use_directory_urls) diff --git a/mkdocs/relative_path_ext.py b/mkdocs/relative_path_ext.py index 248ea7bf..cfd1fcbf 100644 --- a/mkdocs/relative_path_ext.py +++ b/mkdocs/relative_path_ext.py @@ -35,7 +35,7 @@ tutorial/install.md | tutorial/install/ | ../img/initial-layout.png | tutorial/intro.md | tutorial/intro/ | ../../img/initial-layout.png | """ -from __future__ import print_function +import logging from markdown.extensions import Extension from markdown.treeprocessors import Treeprocessor @@ -44,6 +44,8 @@ from mkdocs import utils from mkdocs.compat import urlparse, urlunparse from mkdocs.exceptions import MarkdownNotFound +log = logging.getLogger(__name__) + def _iter(node): # TODO: Remove when dropping Python 2.6. Replace this @@ -74,9 +76,9 @@ def path_to_url(url, nav, strict): # In strict mode raise an error at this point. if strict: raise MarkdownNotFound(msg) - # Otherwise, when strict mode isn't enabled, print out a warning + # Otherwise, when strict mode isn't enabled, log a warning # to the user and leave the URL as it is. - print(msg) + log.warning(msg) return url path = utils.get_url_path(target_file, nav.use_directory_urls) path = nav.url_context.make_relative(path) diff --git a/mkdocs/tests/nav_tests.py b/mkdocs/tests/nav_tests.py index 26ecdc94..4e8531cd 100644 --- a/mkdocs/tests/nav_tests.py +++ b/mkdocs/tests/nav_tests.py @@ -97,6 +97,7 @@ class SiteNavigationTests(unittest.TestCase): self.assertEqual(len(site_navigation.nav_items), 2) self.assertEqual(len(site_navigation.pages), 3) + @mock.patch.object(os.path, 'sep', '\\') def test_nested_ungrouped_no_titles_windows(self): pages = [ ('index.md',), diff --git a/mkdocs/utils.py b/mkdocs/utils.py index 8fa5af38..56db1bb5 100644 --- a/mkdocs/utils.py +++ b/mkdocs/utils.py @@ -7,12 +7,10 @@ Nothing in this module should have an knowledge of config or the layout and structure of the site and pages in the site. """ -import ntpath import os -import posixpath import shutil -from mkdocs.compat import urlparse +from mkdocs.compat import urlparse, pathname2url def copy_file(source_path, output_path): @@ -216,12 +214,10 @@ def create_relative_media_url(nav, url): return relative_url -def normalise_path(path): - """ - Normalise POSIX and NT paths to be consistently POSIX style. - """ +def path_to_url(path): + """Convert a system path to a URL.""" - if ntpath.sep in path: - path = path.replace(ntpath.sep, posixpath.sep) + if os.path.sep == '/': + return path - return path + return pathname2url(path) From 273bf1776f49f100a3ba9b62bdcb23551e4883f0 Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Sat, 11 Apr 2015 18:06:00 +0100 Subject: [PATCH 3/6] Start the process of converting outgoing urls --- mkdocs/build.py | 4 ++-- mkdocs/toc.py | 3 ++- mkdocs/utils.py | 8 +++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mkdocs/build.py b/mkdocs/build.py index 412aff32..87e88e83 100644 --- a/mkdocs/build.py +++ b/mkdocs/build.py @@ -63,9 +63,9 @@ def get_global_context(nav, config): page_description = config['site_description'] - extra_javascript = utils.create_media_urls(nav=nav, url_list=config['extra_javascript']) + extra_javascript = utils.create_media_urls(nav, config['extra_javascript']) - extra_css = utils.create_media_urls(nav=nav, url_list=config['extra_css']) + extra_css = utils.create_media_urls(nav, config['extra_css']) return { 'site_name': site_name, diff --git a/mkdocs/toc.py b/mkdocs/toc.py index 3c6ada50..8a0e84a7 100644 --- a/mkdocs/toc.py +++ b/mkdocs/toc.py @@ -15,6 +15,7 @@ The steps we take to generate a table of contents are: """ from mkdocs.compat import HTMLParser +from mkdocs.utils import path_to_url class TableOfContents(object): @@ -36,7 +37,7 @@ class AnchorLink(object): A single entry in the table of contents. """ def __init__(self, title, url): - self.title, self.url = title, url + self.title, self.url = title, path_to_url(url) self.children = [] def __str__(self): diff --git a/mkdocs/utils.py b/mkdocs/utils.py index 56db1bb5..e3d6aea0 100644 --- a/mkdocs/utils.py +++ b/mkdocs/utils.py @@ -160,13 +160,15 @@ def is_html_file(path): ] -def create_media_urls(nav, url_list): +def create_media_urls(nav, path_list): """ - Return a list of URLs that have been processed correctly for inclusion in a page. + Return a list of URLs that have been processed correctly for inclusion in + a page. """ final_urls = [] - for url in url_list: + for path in path_list: # Allow links to fully qualified URL's + url = path_to_url(path) parsed = urlparse(url) if parsed.netloc: final_urls.append(url) From 85a3805dd02d0ca8fde0256fe2c61afeca961737 Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Sat, 11 Apr 2015 18:14:20 +0100 Subject: [PATCH 4/6] Convert relative path to a URL --- mkdocs/nav.py | 2 +- mkdocs/utils.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/mkdocs/nav.py b/mkdocs/nav.py index a148aef5..5a215b28 100644 --- a/mkdocs/nav.py +++ b/mkdocs/nav.py @@ -104,7 +104,7 @@ class URLContext(object): # Under Python 2.6, relative_path adds an extra '/' at the end. relative_path = os.path.relpath(url, start=self.base_path).rstrip('/') + suffix - return relative_path + return utils.path_to_url(relative_path) class FileContext(object): diff --git a/mkdocs/utils.py b/mkdocs/utils.py index e3d6aea0..55c8d154 100644 --- a/mkdocs/utils.py +++ b/mkdocs/utils.py @@ -166,15 +166,18 @@ def create_media_urls(nav, path_list): a page. """ final_urls = [] + for path in path_list: # Allow links to fully qualified URL's - url = path_to_url(path) - parsed = urlparse(url) + parsed = urlparse(path) if parsed.netloc: - final_urls.append(url) - else: - relative_url = '%s/%s' % (nav.url_context.make_relative('/'), url) - final_urls.append(relative_url) + final_urls.append(path) + continue + # We must be looking at a local path. + url = path_to_url(path) + relative_url = '%s/%s' % (nav.url_context.make_relative('/'), url) + final_urls.append(relative_url) + return final_urls From 0424d9f50760a6c04dd47dabb9d93827d803e2cb Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Sat, 11 Apr 2015 21:18:21 +0100 Subject: [PATCH 5/6] Remove path to URL conversion in one place It was totally silly :) --- mkdocs/toc.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mkdocs/toc.py b/mkdocs/toc.py index 8a0e84a7..3c6ada50 100644 --- a/mkdocs/toc.py +++ b/mkdocs/toc.py @@ -15,7 +15,6 @@ The steps we take to generate a table of contents are: """ from mkdocs.compat import HTMLParser -from mkdocs.utils import path_to_url class TableOfContents(object): @@ -37,7 +36,7 @@ class AnchorLink(object): A single entry in the table of contents. """ def __init__(self, title, url): - self.title, self.url = title, path_to_url(url) + self.title, self.url = title, url self.children = [] def __str__(self): From 690e833f0b18c59f18831ac858a3cba422d83ddd Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Mon, 13 Apr 2015 17:08:27 +0100 Subject: [PATCH 6/6] Add to the release notes --- docs/about/release-notes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 65e96358..a0cda862 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -12,7 +12,7 @@ You can determine your currently installed version using `pip freeze`: pip freeze | grep mkdocs -## Version 0.12.0 (2015-04-08) +## Version 0.12.0 (2015-04-14) * Display the current MkDocs version in the CLI output. (#258) * Check for CNAME file when using gh-deploy. (#285) @@ -26,6 +26,7 @@ You can determine your currently installed version using `pip freeze`: * Add the option to specify a remote branch when deploying to GitHub. This enables deploying to GitHub pages on personal and repo sites. (#354) * Add favicon support to the ReadTheDocs theme HTML. (#422) +* Automatically refresh the browser when files are edited. (#163) * Bugfix: Never re-write URL's in code blocks. (#240) * Bugfix: Don't copy ditfiles when copying media from the `docs_dir`. (#254) * Bugfix: Fix the rendering of tables in the ReadTheDocs theme. (#106)