Skip to content

Commit

Permalink
Improve error handling with theme discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
d0ugal committed Jun 7, 2015
1 parent f9b90d9 commit a4a1e3f
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 18 deletions.
42 changes: 41 additions & 1 deletion mkdocs/tests/utils/utils_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
# coding: utf-8

from __future__ import unicode_literals

import mock
import os
import unittest

from mkdocs import nav, utils
from mkdocs import nav, utils, exceptions
from mkdocs.tests.base import dedent


Expand Down Expand Up @@ -114,6 +116,44 @@ def test_get_themes(self):
'spacelab', 'united', 'readable', 'simplex', 'mkdocs',
'cosmo', 'journal', 'cyborg', 'readthedocs', 'amelia']))

@mock.patch('pkg_resources.iter_entry_points', autospec=True)
def test_get_themes_warning(self, mock_iter):

theme1 = mock.Mock()
theme1.name = 'mkdocs2'
theme1.dist.key = 'mkdocs2'
theme1.load().__file__ = "some/path1"

theme2 = mock.Mock()
theme2.name = 'mkdocs2'
theme2.dist.key = 'mkdocs3'
theme2.load().__file__ = "some/path2"

mock_iter.return_value = iter([theme1, theme2])

self.assertEqual(
sorted(utils.get_theme_names()),
sorted(['mkdocs2', ]))

@mock.patch('pkg_resources.iter_entry_points', autospec=True)
@mock.patch('pkg_resources.get_entry_map', autospec=True)
def test_get_themes_error(self, mock_get, mock_iter):

theme1 = mock.Mock()
theme1.name = 'mkdocs'
theme1.dist.key = 'mkdocs'
theme1.load().__file__ = "some/path1"

theme2 = mock.Mock()
theme2.name = 'mkdocs'
theme2.dist.key = 'mkdocs2'
theme2.load().__file__ = "some/path2"

mock_iter.return_value = iter([theme1, theme2])
mock_get.return_value = {'mkdocs': theme1, }

self.assertRaises(exceptions.ConfigurationError, utils.get_theme_names)

def test_nest_paths(self):

j = os.path.join
Expand Down
59 changes: 42 additions & 17 deletions mkdocs/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@

from __future__ import unicode_literals

from pkg_resources import iter_entry_points
import logging
import markdown
import os
import pkg_resources
import shutil
import sys
import yaml

from mkdocs import toc
from mkdocs import toc, exceptions

try: # pragma: no cover
from urllib.parse import urlparse, urlunparse, urljoin # noqa
Expand All @@ -37,6 +38,8 @@
string_types = basestring, # noqa
text_type = unicode # noqa

log = logging.getLogger(__name__)


def yaml_load(source, loader=yaml.Loader):
"""
Expand All @@ -47,32 +50,41 @@ def yaml_load(source, loader=yaml.Loader):
"""

def construct_yaml_str(self, node):
"""Override the default string handling function to always return unicode objects."""
"""
Override the default string handling function to always return
unicode objects.
"""
return self.construct_scalar(node)

class Loader(loader):
"""Define a custom loader derived from the global loader to leave the global loader unaltered."""
"""
Define a custom loader derived from the global loader to leave the
global loader unaltered.
"""

# Attach our unicode constructor to our custom loader ensuring all strings will be unicode on translation.
# Attach our unicode constructor to our custom loader ensuring all strings
# will be unicode on translation.
Loader.add_constructor('tag:yaml.org,2002:str', construct_yaml_str)

try:
return yaml.load(source, Loader)
finally:
# TODO: Remove this when external calls are properly cleaning up file objects.
# Some mkdocs internal calls, sometimes in test lib, will load configs
# with a file object but never close it. On some systems, if a delete
# action is performed on that file without Python closing that object,
# there will be an access error. This will process the file and close it
# as there should be no more use for the file once we process the yaml content.
# TODO: Remove this when external calls are properly cleaning up file
# objects. Some mkdocs internal calls, sometimes in test lib, will
# load configs with a file object but never close it. On some
# systems, if a delete action is performed on that file without Python
# closing that object, there will be an access error. This will
# process the file and close it as there should be no more use for the
# file once we process the yaml content.
if hasattr(source, 'close'):
source.close()


def reduce_list(data_set):
""" Reduce duplicate items in a list and preserve order """
seen = set()
return [item for item in data_set if item not in seen and not seen.add(item)]
return [item for item in data_set if
item not in seen and not seen.add(item)]


def copy_file(source_path, output_path):
Expand Down Expand Up @@ -341,12 +353,25 @@ def get_themes():
"""Return a dict of theme names and their locations"""

themes = {}
builtins = pkg_resources.get_entry_map(dist='mkdocs', group='mkdocs.themes')

for theme in pkg_resources.iter_entry_points(group='mkdocs.themes'):

if theme.name in builtins and theme.dist.key != 'mkdocs':
raise exceptions.ConfigurationError(
"The theme {0} is a builtin theme but {1} provides a theme "
"with the same name".format(theme.name, theme.dist.key))

elif theme.name in themes:
multiple_packages = [themes[theme.name].dist.key, theme.dist.key]
log.warning("The theme %s is provided by the Python package "
"'%s'. The one in %s will be used.",
theme.name, ','.join(multiple_packages), theme.dist.key)

themes[theme.name] = theme

for theme in iter_entry_points(group='mkdocs.themes', name=None):
name = theme.name
theme = theme.load()
location = os.path.dirname(theme.__file__)
themes[name] = location
themes = dict((name, os.path.dirname(theme.load().__file__))
for name, theme in themes.items())

return themes

Expand Down

0 comments on commit a4a1e3f

Please sign in to comment.