mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Make various theme-related objects private
This commit is contained in:
parent
2ccc9d315e
commit
0ca2ddf924
@ -884,7 +884,7 @@ class StandaloneHTMLBuilder(Builder):
|
|||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
# clean up theme stuff
|
# clean up theme stuff
|
||||||
if self.theme:
|
if self.theme:
|
||||||
self.theme.cleanup()
|
self.theme._cleanup()
|
||||||
|
|
||||||
def post_process_images(self, doctree: Node) -> None:
|
def post_process_images(self, doctree: Node) -> None:
|
||||||
"""Pick the best candidate for an image and link down-scaled images to
|
"""Pick the best candidate for an image and link down-scaled images to
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
|
import contextlib
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
@ -11,13 +12,6 @@ from os import path
|
|||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
if sys.version_info >= (3, 10):
|
|
||||||
from importlib.metadata import entry_points
|
|
||||||
else:
|
|
||||||
from importlib_metadata import entry_points
|
|
||||||
|
|
||||||
import contextlib
|
|
||||||
|
|
||||||
from sphinx import package_dir
|
from sphinx import package_dir
|
||||||
from sphinx.config import check_confval_types as _config_post_init
|
from sphinx.config import check_confval_types as _config_post_init
|
||||||
from sphinx.errors import ThemeError
|
from sphinx.errors import ThemeError
|
||||||
@ -25,25 +19,31 @@ from sphinx.locale import __
|
|||||||
from sphinx.util import logging
|
from sphinx.util import logging
|
||||||
from sphinx.util.osutil import ensuredir
|
from sphinx.util.osutil import ensuredir
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
from importlib.metadata import entry_points
|
||||||
|
else:
|
||||||
|
from importlib_metadata import entry_points
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sphinx.application import Sphinx
|
from sphinx.application import Sphinx
|
||||||
|
|
||||||
|
__all__ = 'Theme', 'HTMLThemeFactory'
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
NODEFAULT = object()
|
_NO_DEFAULT = object()
|
||||||
THEMECONF = 'theme.conf'
|
_THEME_CONF = 'theme.conf'
|
||||||
|
|
||||||
|
|
||||||
def extract_zip(filename: str, targetdir: str) -> None:
|
def _extract_zip(filename: str, target_dir: str, /) -> None:
|
||||||
"""Extract zip file to target directory."""
|
"""Extract zip file to target directory."""
|
||||||
ensuredir(targetdir)
|
ensuredir(target_dir)
|
||||||
|
|
||||||
with ZipFile(filename) as archive:
|
with ZipFile(filename) as archive:
|
||||||
for name in archive.namelist():
|
for name in archive.namelist():
|
||||||
if name.endswith('/'):
|
if name.endswith('/'):
|
||||||
continue
|
continue
|
||||||
entry = path.join(targetdir, name)
|
entry = path.join(target_dir, name)
|
||||||
ensuredir(path.dirname(entry))
|
ensuredir(path.dirname(entry))
|
||||||
with open(path.join(entry), 'wb') as fp:
|
with open(path.join(entry), 'wb') as fp:
|
||||||
fp.write(archive.read(name))
|
fp.write(archive.read(name))
|
||||||
@ -55,23 +55,22 @@ class Theme:
|
|||||||
This class supports both theme directory and theme archive (zipped theme).
|
This class supports both theme directory and theme archive (zipped theme).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str, theme_path: str, factory: HTMLThemeFactory) -> None:
|
def __init__(self, name: str, theme_path: str, theme_factory: HTMLThemeFactory) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.base = None
|
self._base: Theme | None = None
|
||||||
self.rootdir = None
|
|
||||||
|
|
||||||
if path.isdir(theme_path):
|
if path.isdir(theme_path):
|
||||||
# already a directory, do nothing
|
# already a directory, do nothing
|
||||||
self.rootdir = None
|
self._root_dir = None
|
||||||
self.themedir = theme_path
|
self._theme_dir = theme_path
|
||||||
else:
|
else:
|
||||||
# extract the theme to a temp directory
|
# extract the theme to a temp directory
|
||||||
self.rootdir = tempfile.mkdtemp('sxt')
|
self._root_dir = tempfile.mkdtemp('sxt')
|
||||||
self.themedir = path.join(self.rootdir, name)
|
self._theme_dir = path.join(self._root_dir, name)
|
||||||
extract_zip(theme_path, self.themedir)
|
_extract_zip(theme_path, self._theme_dir)
|
||||||
|
|
||||||
self.config = configparser.RawConfigParser()
|
self.config = configparser.RawConfigParser()
|
||||||
config_file_path = path.join(self.themedir, THEMECONF)
|
config_file_path = path.join(self._theme_dir, _THEME_CONF)
|
||||||
if not os.path.isfile(config_file_path):
|
if not os.path.isfile(config_file_path):
|
||||||
raise ThemeError(__('theme configuration file %r not found') % config_file_path)
|
raise ThemeError(__('theme configuration file %r not found') % config_file_path)
|
||||||
self.config.read(config_file_path, encoding='utf-8')
|
self.config.read(config_file_path, encoding='utf-8')
|
||||||
@ -85,7 +84,7 @@ class Theme:
|
|||||||
|
|
||||||
if inherit != 'none':
|
if inherit != 'none':
|
||||||
try:
|
try:
|
||||||
self.base = factory.create(inherit)
|
self._base = theme_factory.create(inherit)
|
||||||
except ThemeError as exc:
|
except ThemeError as exc:
|
||||||
raise ThemeError(__('no theme named %r found, inherited by %r') %
|
raise ThemeError(__('no theme named %r found, inherited by %r') %
|
||||||
(inherit, name)) from exc
|
(inherit, name)) from exc
|
||||||
@ -94,24 +93,24 @@ class Theme:
|
|||||||
"""Return a list of theme directories, beginning with this theme's,
|
"""Return a list of theme directories, beginning with this theme's,
|
||||||
then the base theme's, then that one's base theme's, etc.
|
then the base theme's, then that one's base theme's, etc.
|
||||||
"""
|
"""
|
||||||
if self.base is None:
|
if self._base is None:
|
||||||
return [self.themedir]
|
return [self._theme_dir]
|
||||||
else:
|
else:
|
||||||
return [self.themedir] + self.base.get_theme_dirs()
|
return [self._theme_dir] + self._base.get_theme_dirs()
|
||||||
|
|
||||||
def get_config(self, section: str, name: str, default: Any = NODEFAULT) -> Any:
|
def get_config(self, section: str, name: str, default: Any = _NO_DEFAULT) -> Any:
|
||||||
"""Return the value for a theme configuration setting, searching the
|
"""Return the value for a theme configuration setting, searching the
|
||||||
base theme chain.
|
base theme chain.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return self.config.get(section, name)
|
return self.config.get(section, name)
|
||||||
except (configparser.NoOptionError, configparser.NoSectionError) as exc:
|
except (configparser.NoOptionError, configparser.NoSectionError):
|
||||||
if self.base:
|
if self._base:
|
||||||
return self.base.get_config(section, name, default)
|
return self._base.get_config(section, name, default)
|
||||||
|
|
||||||
if default is NODEFAULT:
|
if default is _NO_DEFAULT:
|
||||||
raise ThemeError(__('setting %s.%s occurs in none of the '
|
raise ThemeError(__('setting %s.%s occurs in none of the '
|
||||||
'searched theme configs') % (section, name)) from exc
|
'searched theme configs') % (section, name)) from None
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def get_options(self, overrides: dict[str, Any] | None = None) -> dict[str, Any]:
|
def get_options(self, overrides: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
@ -119,8 +118,8 @@ class Theme:
|
|||||||
if overrides is None:
|
if overrides is None:
|
||||||
overrides = {}
|
overrides = {}
|
||||||
|
|
||||||
if self.base:
|
if self._base:
|
||||||
options = self.base.get_options()
|
options = self._base.get_options()
|
||||||
else:
|
else:
|
||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
@ -135,21 +134,21 @@ class Theme:
|
|||||||
|
|
||||||
return options
|
return options
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def _cleanup(self) -> None:
|
||||||
"""Remove temporary directories."""
|
"""Remove temporary directories."""
|
||||||
if self.rootdir:
|
if self._root_dir:
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
shutil.rmtree(self.rootdir)
|
shutil.rmtree(self._root_dir)
|
||||||
|
|
||||||
if self.base:
|
if self._base is not None:
|
||||||
self.base.cleanup()
|
self._base._cleanup()
|
||||||
|
|
||||||
|
|
||||||
def is_archived_theme(filename: str) -> bool:
|
def _is_archived_theme(filename: str, /) -> bool:
|
||||||
"""Check whether the specified file is an archived theme file or not."""
|
"""Check whether the specified file is an archived theme file or not."""
|
||||||
try:
|
try:
|
||||||
with ZipFile(filename) as f:
|
with ZipFile(filename) as f:
|
||||||
return THEMECONF in f.namelist()
|
return _THEME_CONF in f.namelist()
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -158,27 +157,27 @@ class HTMLThemeFactory:
|
|||||||
"""A factory class for HTML Themes."""
|
"""A factory class for HTML Themes."""
|
||||||
|
|
||||||
def __init__(self, app: Sphinx) -> None:
|
def __init__(self, app: Sphinx) -> None:
|
||||||
self.app = app
|
self._app = app
|
||||||
self.themes = app.registry.html_themes
|
self._themes = app.registry.html_themes
|
||||||
self.load_builtin_themes()
|
self._load_builtin_themes()
|
||||||
if getattr(app.config, 'html_theme_path', None):
|
if getattr(app.config, 'html_theme_path', None):
|
||||||
self.load_additional_themes(app.config.html_theme_path)
|
self._load_additional_themes(app.config.html_theme_path)
|
||||||
|
|
||||||
def load_builtin_themes(self) -> None:
|
def _load_builtin_themes(self) -> None:
|
||||||
"""Load built-in themes."""
|
"""Load built-in themes."""
|
||||||
themes = self.find_themes(path.join(package_dir, 'themes'))
|
themes = self._find_themes(path.join(package_dir, 'themes'))
|
||||||
for name, theme in themes.items():
|
for name, theme in themes.items():
|
||||||
self.themes[name] = theme
|
self._themes[name] = theme
|
||||||
|
|
||||||
def load_additional_themes(self, theme_paths: str) -> None:
|
def _load_additional_themes(self, theme_paths: str) -> None:
|
||||||
"""Load additional themes placed at specified directories."""
|
"""Load additional themes placed at specified directories."""
|
||||||
for theme_path in theme_paths:
|
for theme_path in theme_paths:
|
||||||
abs_theme_path = path.abspath(path.join(self.app.confdir, theme_path))
|
abs_theme_path = path.abspath(path.join(self._app.confdir, theme_path))
|
||||||
themes = self.find_themes(abs_theme_path)
|
themes = self._find_themes(abs_theme_path)
|
||||||
for name, theme in themes.items():
|
for name, theme in themes.items():
|
||||||
self.themes[name] = theme
|
self._themes[name] = theme
|
||||||
|
|
||||||
def load_extra_theme(self, name: str) -> None:
|
def _load_extra_theme(self, name: str) -> None:
|
||||||
"""Try to load a theme with the specified name.
|
"""Try to load a theme with the specified name.
|
||||||
|
|
||||||
This uses the ``sphinx.html_themes`` entry point from package metadata.
|
This uses the ``sphinx.html_themes`` entry point from package metadata.
|
||||||
@ -189,10 +188,10 @@ class HTMLThemeFactory:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
self.app.registry.load_extension(self.app, entry_point.module)
|
self._app.registry.load_extension(self._app, entry_point.module)
|
||||||
_config_post_init(None, self.app.config)
|
_config_post_init(None, self._app.config)
|
||||||
|
|
||||||
def find_themes(self, theme_path: str) -> dict[str, str]:
|
def _find_themes(self, theme_path: str) -> dict[str, str]:
|
||||||
"""Search themes from specified directory."""
|
"""Search themes from specified directory."""
|
||||||
themes: dict[str, str] = {}
|
themes: dict[str, str] = {}
|
||||||
if not path.isdir(theme_path):
|
if not path.isdir(theme_path):
|
||||||
@ -201,24 +200,24 @@ class HTMLThemeFactory:
|
|||||||
for entry in os.listdir(theme_path):
|
for entry in os.listdir(theme_path):
|
||||||
pathname = path.join(theme_path, entry)
|
pathname = path.join(theme_path, entry)
|
||||||
if path.isfile(pathname) and entry.lower().endswith('.zip'):
|
if path.isfile(pathname) and entry.lower().endswith('.zip'):
|
||||||
if is_archived_theme(pathname):
|
if _is_archived_theme(pathname):
|
||||||
name = entry[:-4]
|
name = entry[:-4]
|
||||||
themes[name] = pathname
|
themes[name] = pathname
|
||||||
else:
|
else:
|
||||||
logger.warning(__('file %r on theme path is not a valid '
|
logger.warning(__('file %r on theme path is not a valid '
|
||||||
'zipfile or contains no theme'), entry)
|
'zipfile or contains no theme'), entry)
|
||||||
else:
|
else:
|
||||||
if path.isfile(path.join(pathname, THEMECONF)):
|
if path.isfile(path.join(pathname, _THEME_CONF)):
|
||||||
themes[entry] = pathname
|
themes[entry] = pathname
|
||||||
|
|
||||||
return themes
|
return themes
|
||||||
|
|
||||||
def create(self, name: str) -> Theme:
|
def create(self, name: str) -> Theme:
|
||||||
"""Create an instance of theme."""
|
"""Create an instance of theme."""
|
||||||
if name not in self.themes:
|
if name not in self._themes:
|
||||||
self.load_extra_theme(name)
|
self._load_extra_theme(name)
|
||||||
|
|
||||||
if name not in self.themes:
|
if name not in self._themes:
|
||||||
raise ThemeError(__('no theme named %r found (missing theme.conf?)') % name)
|
raise ThemeError(__('no theme named %r found (missing theme.conf?)') % name)
|
||||||
|
|
||||||
return Theme(name, self.themes[name], factory=self)
|
return Theme(name, self._themes[name], self)
|
||||||
|
@ -13,8 +13,6 @@ from sphinx.theming import Theme, ThemeError
|
|||||||
confoverrides={'html_theme': 'ziptheme',
|
confoverrides={'html_theme': 'ziptheme',
|
||||||
'html_theme_options.testopt': 'foo'})
|
'html_theme_options.testopt': 'foo'})
|
||||||
def test_theme_api(app, status, warning):
|
def test_theme_api(app, status, warning):
|
||||||
cfg = app.config
|
|
||||||
|
|
||||||
themes = ['basic', 'default', 'scrolls', 'agogo', 'sphinxdoc', 'haiku',
|
themes = ['basic', 'default', 'scrolls', 'agogo', 'sphinxdoc', 'haiku',
|
||||||
'traditional', 'epub', 'nature', 'pyramid', 'bizstyle', 'classic', 'nonav',
|
'traditional', 'epub', 'nature', 'pyramid', 'bizstyle', 'classic', 'nonav',
|
||||||
'test-theme', 'ziptheme', 'staticfiles', 'parent', 'child', 'alabaster']
|
'test-theme', 'ziptheme', 'staticfiles', 'parent', 'child', 'alabaster']
|
||||||
@ -28,8 +26,8 @@ def test_theme_api(app, status, warning):
|
|||||||
# test Theme instance API
|
# test Theme instance API
|
||||||
theme = app.builder.theme
|
theme = app.builder.theme
|
||||||
assert theme.name == 'ziptheme'
|
assert theme.name == 'ziptheme'
|
||||||
themedir = theme.themedir
|
theme_dir = theme._theme_dir
|
||||||
assert theme.base.name == 'basic'
|
assert theme._base.name == 'basic'
|
||||||
assert len(theme.get_theme_dirs()) == 2
|
assert len(theme.get_theme_dirs()) == 2
|
||||||
|
|
||||||
# direct setting
|
# direct setting
|
||||||
@ -46,13 +44,13 @@ def test_theme_api(app, status, warning):
|
|||||||
options = theme.get_options({'nonexisting': 'foo'})
|
options = theme.get_options({'nonexisting': 'foo'})
|
||||||
assert 'nonexisting' not in options
|
assert 'nonexisting' not in options
|
||||||
|
|
||||||
options = theme.get_options(cfg.html_theme_options)
|
options = theme.get_options(app.config.html_theme_options)
|
||||||
assert options['testopt'] == 'foo'
|
assert options['testopt'] == 'foo'
|
||||||
assert options['nosidebar'] == 'false'
|
assert options['nosidebar'] == 'false'
|
||||||
|
|
||||||
# cleanup temp directories
|
# cleanup temp directories
|
||||||
theme.cleanup()
|
theme._cleanup()
|
||||||
assert not os.path.exists(themedir)
|
assert not os.path.exists(theme_dir)
|
||||||
|
|
||||||
|
|
||||||
def test_nonexistent_theme_conf(tmp_path):
|
def test_nonexistent_theme_conf(tmp_path):
|
||||||
|
Loading…
Reference in New Issue
Block a user