Merge pull request #451 from d0ugal/path-handling

Improve Path handling across MkDocs
This commit is contained in:
Dougal Matthews
2015-04-13 23:30:52 +01:00
12 changed files with 94 additions and 151 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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 = {
@@ -78,6 +80,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:
@@ -116,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.
@@ -134,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

View File

@@ -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:

View File

@@ -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,16 +95,16 @@ 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
return utils.path_to_url(relative_path)
class FileContext(object):
@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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',),

View File

@@ -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):
@@ -162,19 +160,24 @@ 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
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
@@ -216,12 +219,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)

View File

@@ -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

View File

@@ -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 = (