blob: 41cbcae96281285ebb5d5bc59c5766f43402ab9e [file] [log] [blame]
# -*- coding: utf-8 -*-
"""
sphinx.theming
~~~~~~~~~~~~~~
Theming support for HTML builders.
:copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import os
import shutil
import zipfile
import tempfile
from os import path
from six import string_types, iteritems
from six.moves import configparser
try:
import pkg_resources
except ImportError:
pkg_resources = False
from sphinx import package_dir
from sphinx.errors import ThemeError
NODEFAULT = object()
THEMECONF = 'theme.conf'
class Theme(object):
"""
Represents the theme chosen in the configuration.
"""
themes = {}
@classmethod
def init_themes(cls, confdir, theme_path, warn=None):
"""Search all theme paths for available themes."""
cls.themepath = list(theme_path)
cls.themepath.append(path.join(package_dir, 'themes'))
for themedir in cls.themepath[::-1]:
themedir = path.join(confdir, themedir)
if not path.isdir(themedir):
continue
for theme in os.listdir(themedir):
if theme.lower().endswith('.zip'):
try:
zfile = zipfile.ZipFile(path.join(themedir, theme))
if THEMECONF not in zfile.namelist():
continue
tname = theme[:-4]
tinfo = zfile
except Exception:
if warn:
warn('file %r on theme path is not a valid '
'zipfile or contains no theme' % theme)
continue
else:
if not path.isfile(path.join(themedir, theme, THEMECONF)):
continue
tname = theme
tinfo = None
cls.themes[tname] = (path.join(themedir, theme), tinfo)
@classmethod
def load_extra_themes(cls):
for themedir in load_theme_plugins():
if not path.isdir(themedir):
continue
for theme in os.listdir(themedir):
if not path.isfile(path.join(themedir, theme, THEMECONF)):
continue
cls.themes[theme] = (path.join(themedir, theme), None)
def __init__(self, name):
if name not in self.themes:
self.load_extra_themes()
if name not in self.themes:
raise ThemeError('no theme named %r found '
'(missing theme.conf?)' % name)
self.name = name
tdir, tinfo = self.themes[name]
if tinfo is None:
# already a directory, do nothing
self.themedir = tdir
self.themedir_created = False
else:
# extract the theme to a temp directory
self.themedir = tempfile.mkdtemp('sxt')
self.themedir_created = True
for name in tinfo.namelist():
if name.endswith('/'): continue
dirname = path.dirname(name)
if not path.isdir(path.join(self.themedir, dirname)):
os.makedirs(path.join(self.themedir, dirname))
fp = open(path.join(self.themedir, name), 'wb')
fp.write(tinfo.read(name))
fp.close()
self.themeconf = configparser.RawConfigParser()
self.themeconf.read(path.join(self.themedir, THEMECONF))
try:
inherit = self.themeconf.get('theme', 'inherit')
except configparser.NoOptionError:
raise ThemeError('theme %r doesn\'t have "inherit" setting' % name)
if inherit == 'none':
self.base = None
elif inherit not in self.themes:
raise ThemeError('no theme named %r found, inherited by %r' %
(inherit, name))
else:
self.base = Theme(inherit)
def get_confstr(self, section, name, default=NODEFAULT):
"""Return the value for a theme configuration setting, searching the
base theme chain.
"""
try:
return self.themeconf.get(section, name)
except (configparser.NoOptionError, configparser.NoSectionError):
if self.base is not None:
return self.base.get_confstr(section, name, default)
if default is NODEFAULT:
raise ThemeError('setting %s.%s occurs in none of the '
'searched theme configs' % (section, name))
else:
return default
def get_options(self, overrides):
"""Return a dictionary of theme options and their values."""
chain = [self.themeconf]
base = self.base
while base is not None:
chain.append(base.themeconf)
base = base.base
options = {}
for conf in reversed(chain):
try:
options.update(conf.items('options'))
except configparser.NoSectionError:
pass
for option, value in iteritems(overrides):
if option not in options:
raise ThemeError('unsupported theme option %r given' % option)
options[option] = value
return options
def get_dirchain(self):
"""Return a list of theme directories, beginning with this theme's,
then the base theme's, then that one's base theme's, etc.
"""
chain = [self.themedir]
base = self.base
while base is not None:
chain.append(base.themedir)
base = base.base
return chain
def cleanup(self):
"""Remove temporary directories."""
if self.themedir_created:
try:
shutil.rmtree(self.themedir)
except Exception:
pass
if self.base:
self.base.cleanup()
def load_theme_plugins():
"""load plugins by using``sphinx_themes`` section in setuptools entry_points.
This API will return list of directory that contain some theme directory.
"""
if not pkg_resources:
return []
theme_paths = []
for plugin in pkg_resources.iter_entry_points('sphinx_themes'):
func_or_path = plugin.load()
try:
path = func_or_path()
except:
path = func_or_path
if isinstance(path, string_types):
theme_paths.append(path)
else:
raise ThemeError('Plugin %r does not response correctly.' %
plugin.module_name)
return theme_paths