Add HTMLThemeFactory class

This commit is contained in:
Takeshi KOMIYA 2017-04-20 20:19:55 +09:00
parent b405c0aaf5
commit faefe2b5a5
6 changed files with 81 additions and 77 deletions

View File

@ -127,6 +127,7 @@ class Sphinx(object):
self.env = None # type: BuildEnvironment
self.enumerable_nodes = {} # type: Dict[nodes.Node, Tuple[unicode, Callable]] # NOQA
self.post_transforms = [] # type: List[Transform]
self.html_themes = {} # type: Dict[unicode, unicode]
self.srcdir = srcdir
self.confdir = confdir

View File

@ -16,7 +16,7 @@ from six import iteritems
from sphinx import package_dir
from sphinx.locale import _
from sphinx.theming import Theme
from sphinx.theming import HTMLThemeFactory
from sphinx.builders import Builder
from sphinx.util import logging
from sphinx.util.osutil import ensuredir, os_path
@ -42,8 +42,8 @@ class ChangesBuilder(Builder):
def init(self):
# type: () -> None
self.create_template_bridge()
Theme.init_themes(self.confdir, self.config.html_theme_path)
self.theme = Theme.create('default')
theme_factory = HTMLThemeFactory(self.app)
self.theme = theme_factory.create('default')
self.templates.init(self, self.theme)
def get_outdated_docs(self):

View File

@ -40,7 +40,7 @@ from sphinx.util.matching import patmatch, Matcher, DOTFILES
from sphinx.config import string_classes
from sphinx.locale import _, l_
from sphinx.search import js_index
from sphinx.theming import Theme
from sphinx.theming import HTMLThemeFactory
from sphinx.builders import Builder
from sphinx.application import ENV_PICKLE_FILENAME
from sphinx.highlighting import PygmentsBridge
@ -196,9 +196,9 @@ class StandaloneHTMLBuilder(Builder):
def init_templates(self):
# type: () -> None
Theme.init_themes(self.confdir, self.config.html_theme_path)
theme_factory = HTMLThemeFactory(self.app)
themename, themeoptions = self.get_theme_config()
self.theme = Theme.create(themename)
self.theme = theme_factory.create(themename)
self.theme_options = themeoptions.copy()
self.create_template_bridge()
self.templates.init(self, self.theme)

View File

@ -30,12 +30,13 @@ logger = logging.getLogger(__name__)
if False:
# For type annotation
from typing import Any, Dict, Iterator, List, Tuple # NOQA
from sphinx.application import Sphinx # NOQA
NODEFAULT = object()
THEMECONF = 'theme.conf'
class _Theme(object):
class Theme(object):
def __init__(self):
# type: () -> None
self.name = None
@ -126,69 +127,56 @@ def is_archived_theme(filename):
return False
def find_theme_entries(themedir):
# type: (unicode) -> Iterator[Tuple[unicode, unicode]]
for entry in os.listdir(themedir):
pathname = path.join(themedir, entry)
if path.isfile(pathname) and entry.lower().endswith('.zip'):
if is_archived_theme(pathname):
name = entry[:-4]
yield name, pathname
else:
logger.warning(_('file %r on theme path is not a valid '
'zipfile or contains no theme'), entry)
else:
if path.isfile(path.join(pathname, THEMECONF)):
yield entry, pathname
class HTMLThemeFactory(object):
"""A factory class for HTML Themes."""
def __init__(self, app):
# type: (Sphinx) -> None
self.confdir = app.confdir
self.themes = app.html_themes
self.load_builtin_themes()
if getattr(app.config, 'html_theme_path', None):
self.load_additional_themes(app.config.html_theme_path)
class Theme(object):
"""
Represents the theme chosen in the configuration.
"""
themes = {} # type: Dict[unicode, unicode]
def load_builtin_themes(self):
# type: () -> None
themes = self.find_themes(path.join(package_dir, 'themes'))
for name, theme in iteritems(themes):
self.themes[name] = theme
@classmethod
def init_themes(cls, confdir, theme_path):
# type: (unicode, unicode) -> None
"""Search all theme paths for available themes."""
themepath = list(theme_path)
themepath.append(path.join(package_dir, 'themes'))
def load_additional_themes(self, theme_paths):
# type: (unicode) -> None
for theme_path in theme_paths:
abs_theme_path = path.abspath(path.join(self.confdir, theme_path))
themes = self.find_themes(abs_theme_path)
for name, theme in iteritems(themes):
self.themes[name] = theme
# search themes from theme_paths
for themedir in themepath:
themedir = path.abspath(path.join(confdir, themedir))
if not path.isdir(themedir):
continue
else:
for name, theme in find_theme_entries(themedir):
cls.themes[name] = theme
@classmethod
def load_extra_theme(cls, name):
def load_extra_theme(self, name):
# type: (unicode) -> None
if name == 'alabaster':
cls.load_alabaster()
self.load_alabaster_theme()
elif name == 'sphinx_rtd_theme':
cls.load_sphinx_rtd_theme()
self.load_sphinx_rtd_theme()
else:
pass
self.load_external_theme(name)
@classmethod
def load_alabaster(cls):
def load_alabaster_theme(self):
# type: () -> None
import alabaster
cls.themes['alabaster'] = path.join(alabaster.get_path(), 'alabaster')
self.themes['alabaster'] = path.join(alabaster.get_path(), 'alabaster')
@classmethod
def load_sphinx_rtd_theme(cls):
def load_sphinx_rtd_theme(self):
# type: () -> None
try:
import sphinx_rtd_theme
cls.themes['sphinx_rtd_theme'] = path.join(sphinx_rtd_theme.get_html_theme_path(),
'sphinx_rtd_theme')
theme_path = sphinx_rtd_theme.get_html_theme_path()
self.themes['sphinx_rtd_theme'] = path.join(theme_path, 'sphinx_rtd_theme')
except ImportError:
pass
@classmethod
def load_external_themes(cls, name):
def load_external_themes(self, name):
# type: (unicode) -> None
for entry_point in pkg_resources.iter_entry_points('sphinx_themes'):
target = entry_point.load()
if callable(target):
@ -199,17 +187,38 @@ class Theme(object):
else:
themedir = target
for entry, theme in find_theme_entries(themedir):
themes = self.find_themes(themedir)
for entry, theme in iteritems(themes):
if name == entry:
cls.themes[entry] = theme
self.themes[name] = theme
@classmethod
def create(cls, name):
# type: (unicode) -> None
if name not in cls.themes:
cls.load_extra_theme(name)
def find_themes(self, theme_path):
# type: (unicode) -> Dict[unicode, unicode]
themes = {}
if not path.isdir(theme_path):
return themes
if name not in cls.themes:
for entry in os.listdir(theme_path):
pathname = path.join(theme_path, entry)
if path.isfile(pathname) and entry.lower().endswith('.zip'):
if is_archived_theme(pathname):
name = entry[:-4]
themes[name] = pathname
else:
logger.warning(_('file %r on theme path is not a valid '
'zipfile or contains no theme'), entry)
else:
if path.isfile(path.join(pathname, THEMECONF)):
themes[entry] = pathname
return themes
def create(self, name):
# type: (unicode) -> Theme
if name not in self.themes:
self.load_extra_theme(name)
if name not in self.themes:
if name == 'sphinx_rtd_theme':
raise ThemeError(_('sphinx_rtd_theme is no longer a hard dependency '
'since version 1.4.0. Please install it manually.'
@ -218,10 +227,10 @@ class Theme(object):
raise ThemeError(_('no theme named %r found '
'(missing theme.conf?)') % name)
theme = _Theme()
theme = Theme()
theme.name = name
themedir = cls.themes[name]
themedir = self.themes[name]
if path.isdir(themedir):
# already a directory, do nothing
theme.rootdir = None
@ -253,10 +262,10 @@ class Theme(object):
if inherit == 'none':
theme.base = None
elif inherit not in cls.themes:
elif inherit not in self.themes:
raise ThemeError('no theme named %r found, inherited by %r' %
(inherit, name))
else:
theme.base = cls.create(inherit)
theme.base = self.create(inherit)
return theme

View File

@ -10,14 +10,10 @@
"""
import os
import zipfile
import mock
import pytest
from sphinx.theming import Theme, ThemeError
from util import path
from sphinx.theming import ThemeError
@pytest.mark.sphinx(
@ -27,12 +23,12 @@ def test_theme_api(app, status, warning):
cfg = app.config
# test Theme class API
assert set(Theme.themes.keys()) == \
assert set(app.html_themes.keys()) == \
set(['basic', 'default', 'scrolls', 'agogo', 'sphinxdoc', 'haiku',
'traditional', 'testtheme', 'ziptheme', 'epub', 'nature',
'pyramid', 'bizstyle', 'classic', 'nonav'])
assert Theme.themes['testtheme'] == app.srcdir / 'testtheme'
assert Theme.themes['ziptheme'] == app.srcdir / 'ziptheme.zip'
assert app.html_themes['testtheme'] == app.srcdir / 'testtheme'
assert app.html_themes['ziptheme'] == app.srcdir / 'ziptheme.zip'
# test Theme instance API
theme = app.builder.theme

View File

@ -24,7 +24,6 @@ from docutils.parsers.rst import directives, roles
from sphinx import application
from sphinx.builders.latex import LaTeXBuilder
from sphinx.theming import Theme
from sphinx.ext.autodoc import AutoDirective
from sphinx.pycode import ModuleAnalyzer
from sphinx.deprecation import RemovedInSphinx17Warning
@ -168,7 +167,6 @@ class SphinxTestApp(application.Sphinx):
raise
def cleanup(self, doctrees=False):
Theme.themes.clear()
AutoDirective._registry.clear()
ModuleAnalyzer.cache.clear()
LaTeXBuilder.usepackages = []