Format `sphinx/builders/html/`

This commit is contained in:
Adam Turner 2024-10-05 00:09:04 +01:00
parent e50e1db203
commit ecbdca0b65
4 changed files with 305 additions and 163 deletions

View File

@ -120,8 +120,7 @@ class StandaloneHTMLBuilder(Builder):
indexer_dumps_unicode = True
# create links to original images from images [True/False]
html_scaled_image_link = True
supported_image_types = ['image/svg+xml', 'image/png',
'image/gif', 'image/jpeg']
supported_image_types = ['image/svg+xml', 'image/png', 'image/gif', 'image/jpeg']
supported_remote_images = True
supported_data_uri_images = True
searchindex_filename = 'searchindex.js'
@ -188,13 +187,17 @@ class StandaloneHTMLBuilder(Builder):
return BuildInfo(self.config, self.tags, frozenset({'html'}))
def _get_translations_js(self) -> str:
candidates = [path.join(dir, self.config.language,
'LC_MESSAGES', 'sphinx.js')
for dir in self.config.locale_dirs] + \
[path.join(package_dir, 'locale', self.config.language,
'LC_MESSAGES', 'sphinx.js'),
path.join(sys.prefix, 'share/sphinx/locale',
self.config.language, 'sphinx.js')]
candidates = [
path.join(dir, self.config.language, 'LC_MESSAGES', 'sphinx.js')
for dir in self.config.locale_dirs
] + [
path.join(
package_dir, 'locale', self.config.language, 'LC_MESSAGES', 'sphinx.js'
),
path.join(
sys.prefix, 'share/sphinx/locale', self.config.language, 'sphinx.js'
),
]
for jsfile in candidates:
if path.isfile(jsfile):
@ -242,15 +245,19 @@ class StandaloneHTMLBuilder(Builder):
self.dark_highlighter: PygmentsBridge | None
if dark_style is not None:
self.dark_highlighter = PygmentsBridge('html', dark_style)
self.app.add_css_file('pygments_dark.css',
media='(prefers-color-scheme: dark)',
id='pygments_dark_css')
self.app.add_css_file(
'pygments_dark.css',
media='(prefers-color-scheme: dark)',
id='pygments_dark_css',
)
else:
self.dark_highlighter = None
@property
def css_files(self) -> list[_CascadingStyleSheet]:
_deprecation_warning(__name__, f'{self.__class__.__name__}.css_files', remove=(9, 0))
_deprecation_warning(
__name__, f'{self.__class__.__name__}.css_files', remove=(9, 0)
)
return self._css_files
def init_css_files(self) -> None:
@ -264,7 +271,8 @@ class StandaloneHTMLBuilder(Builder):
self.add_css_file(filename, **attrs)
for filename, attrs in self.get_builder_config('css_files', 'html'):
attrs.setdefault('priority', 800) # User's CSSs are loaded after extensions'
# User's CSSs are loaded after extensions'
attrs.setdefault('priority', 800)
self.add_css_file(filename, **attrs)
def add_css_file(self, filename: str, **kwargs: Any) -> None:
@ -343,7 +351,9 @@ class StandaloneHTMLBuilder(Builder):
pass # ignore errors
else:
# only log on success
msg = __('build_info mismatch, copying .buildinfo to .buildinfo.bak')
msg = __(
'build_info mismatch, copying .buildinfo to .buildinfo.bak'
)
logger.info(bold(__('building [html]: ')) + msg)
yield from self.env.found_docs
@ -359,10 +369,10 @@ class StandaloneHTMLBuilder(Builder):
# Let users know they have a newer template
if template_mtime > old_mtime:
logger.info(
bold("building [html]: ") +
__(
"template %s has been changed since the previous build, "
"all docs will be rebuilt"
bold('building [html]: ')
+ __(
'template %s has been changed since the previous build, '
'all docs will be rebuilt'
),
self.templates.newest_template_name(),
)
@ -379,7 +389,8 @@ class StandaloneHTMLBuilder(Builder):
except Exception:
targetmtime = 0
try:
srcmtime = max(_last_modified_time(self.env.doc2path(docname)), template_mtime)
doc_mtime = _last_modified_time(self.env.doc2path(docname))
srcmtime = max(doc_mtime, template_mtime)
if srcmtime > targetmtime:
logger.debug(
'[build target] targetname %r(%s), template(%s), docname %r(%s)',
@ -387,7 +398,7 @@ class StandaloneHTMLBuilder(Builder):
_format_rfc3339_microseconds(targetmtime),
_format_rfc3339_microseconds(template_mtime),
docname,
_format_rfc3339_microseconds(_last_modified_time(self.env.doc2path(docname))),
_format_rfc3339_microseconds(doc_mtime),
)
yield docname
except OSError:
@ -413,10 +424,14 @@ class StandaloneHTMLBuilder(Builder):
self.indexer = None
if self.search:
from sphinx.search import IndexBuilder
lang = self.config.html_search_language or self.config.language
self.indexer = IndexBuilder(self.env, lang,
self.config.html_search_options,
self.config.html_search_scorer)
self.indexer = IndexBuilder(
self.env,
lang,
self.config.html_search_options,
self.config.html_search_scorer,
)
self.load_indexer(docnames)
self.docwriter = HTMLWriter(self)
@ -427,7 +442,8 @@ class StandaloneHTMLBuilder(Builder):
self.docsettings: Any = OptionParser(
defaults=self.env.settings,
components=(self.docwriter,),
read_config_files=True).get_default_values()
read_config_files=True,
).get_default_values()
self.docsettings.compact_lists = bool(self.config.html_compact_lists)
# determine the additional indices to include
@ -446,8 +462,12 @@ class StandaloneHTMLBuilder(Builder):
continue
content, collapse = index_cls(domain).generate()
if content:
self.domain_indices.append(
(index_name, index_cls, content, collapse))
self.domain_indices.append((
index_name,
index_cls,
content,
collapse,
))
# format the "last updated on" string, only once is enough since it
# typically doesn't include the time of day
@ -479,8 +499,7 @@ class StandaloneHTMLBuilder(Builder):
for indexname, indexcls, _content, _collapse in self.domain_indices:
# if it has a short name
if indexcls.shortname:
rellinks.append((indexname, indexcls.localname,
'', indexcls.shortname))
rellinks.append((indexname, indexcls.localname, '', indexcls.shortname))
# add assets registered after ``Builder.init()``.
for css_filename, attrs in self.app.registry.css_files:
@ -530,8 +549,8 @@ class StandaloneHTMLBuilder(Builder):
}
if self.theme:
self.globalcontext |= {
f'theme_{key}': val for key, val in
self.theme.get_options(self.theme_options).items()
f'theme_{key}': val
for key, val in self.theme.get_options(self.theme_options).items()
}
self.globalcontext |= self.config.html_context
@ -565,9 +584,10 @@ class StandaloneHTMLBuilder(Builder):
prev = None
while related and related[0]:
with contextlib.suppress(KeyError):
parents.append(
{'link': self.get_relative_uri(docname, related[0]),
'title': self.render_partial(titles[related[0]])['title']})
parents.append({
'link': self.get_relative_uri(docname, related[0]),
'title': self.render_partial(titles[related[0]])['title'],
})
related = self.relations.get(related[0])
if parents:
@ -581,7 +601,7 @@ class StandaloneHTMLBuilder(Builder):
title = self.render_partial(title_node)['title'] if title_node else ''
# Suffix for the document
source_suffix = str(self.env.doc2path(docname, False))[len(docname):]
source_suffix = str(self.env.doc2path(docname, False))[len(docname) :]
# the name for the copied source
if self.config.html_copy_source:
@ -691,8 +711,10 @@ class StandaloneHTMLBuilder(Builder):
# the total count of lines for each index letter, used to distribute
# the entries into two columns
genindex = IndexEntries(self.env).create_index(self)
indexcounts = [sum(1 + len(subitems) for _, (_, subitems, _) in entries)
for _k, entries in genindex]
indexcounts = [
sum(1 + len(subitems) for _, (_, subitems, _) in entries)
for _k, entries in genindex
]
genindexcontext = {
'genindexentries': genindex,
@ -702,15 +724,16 @@ class StandaloneHTMLBuilder(Builder):
logger.info('genindex ', nonl=True)
if self.config.html_split_index:
self.handle_page('genindex', genindexcontext,
'genindex-split.html')
self.handle_page('genindex-all', genindexcontext,
'genindex.html')
self.handle_page('genindex', genindexcontext, 'genindex-split.html')
self.handle_page('genindex-all', genindexcontext, 'genindex.html')
for (key, entries), count in zip(genindex, indexcounts, strict=True):
ctx = {'key': key, 'entries': entries, 'count': count,
'genindexentries': genindex}
self.handle_page('genindex-' + key, ctx,
'genindex-single.html')
ctx = {
'key': key,
'entries': entries,
'count': count,
'genindexentries': genindex,
}
self.handle_page('genindex-' + key, ctx, 'genindex-single.html')
else:
self.handle_page('genindex', genindexcontext, 'genindex.html')
@ -728,9 +751,14 @@ class StandaloneHTMLBuilder(Builder):
if self.images:
stringify_func = ImageAdapter(self.app.env).get_original_image_uri
ensuredir(self.outdir / self.imagedir)
for src in status_iterator(self.images, __('copying images... '), "brown",
len(self.images), self.app.verbosity,
stringify_func=stringify_func):
for src in status_iterator(
self.images,
__('copying images... '),
'brown',
len(self.images),
self.app.verbosity,
stringify_func=stringify_func,
):
dest = self.images[src]
try:
copyfile(
@ -739,8 +767,9 @@ class StandaloneHTMLBuilder(Builder):
force=True,
)
except Exception as err:
logger.warning(__("cannot copy image file '%s': %s"),
self.srcdir / src, err)
logger.warning(
__("cannot copy image file '%s': %s"), self.srcdir / src, err
)
def copy_download_files(self) -> None:
def to_relpath(f: str) -> str:
@ -749,26 +778,34 @@ class StandaloneHTMLBuilder(Builder):
# copy downloadable files
if self.env.dlfiles:
ensuredir(self.outdir / '_downloads')
for src in status_iterator(self.env.dlfiles, __('copying downloadable files... '),
"brown", len(self.env.dlfiles), self.app.verbosity,
stringify_func=to_relpath):
for src in status_iterator(
self.env.dlfiles,
__('copying downloadable files... '),
'brown',
len(self.env.dlfiles),
self.app.verbosity,
stringify_func=to_relpath,
):
try:
dest = self.outdir / '_downloads' / self.env.dlfiles[src][1]
ensuredir(dest.parent)
copyfile(self.srcdir / src, dest, force=True)
except OSError as err:
logger.warning(__('cannot copy downloadable file %r: %s'),
self.srcdir / src, err)
logger.warning(
__('cannot copy downloadable file %r: %s'),
self.srcdir / src,
err,
)
def create_pygments_style_file(self) -> None:
"""Create a style file for pygments."""
with open(path.join(self.outdir, '_static', 'pygments.css'), 'w',
encoding="utf-8") as f:
pyg_path = path.join(self.outdir, '_static', 'pygments.css')
with open(pyg_path, 'w', encoding='utf-8') as f:
f.write(self.highlighter.get_stylesheet())
if self.dark_highlighter:
with open(path.join(self.outdir, '_static', 'pygments_dark.css'), 'w',
encoding="utf-8") as f:
dark_path = path.join(self.outdir, '_static', 'pygments_dark.css')
with open(dark_path, 'w', encoding='utf-8') as f:
f.write(self.dark_highlighter.get_stylesheet())
def copy_translation_js(self) -> None:
@ -810,23 +847,28 @@ class StandaloneHTMLBuilder(Builder):
copy_asset(
Path(entry) / 'static',
self.outdir / '_static',
excluded=DOTFILES, context=context,
renderer=self.templates, onerror=onerror,
excluded=DOTFILES,
context=context,
renderer=self.templates,
onerror=onerror,
force=True,
)
def copy_html_static_files(self, context: dict[str, Any]) -> None:
def onerror(filename: str, error: Exception) -> None:
logger.warning(__('Failed to copy a file in html_static_file: %s: %r'),
filename, error)
logger.warning(
__('Failed to copy a file in html_static_file: %s: %r'), filename, error
)
excluded = Matcher([*self.config.exclude_patterns, '**/.*'])
for entry in self.config.html_static_path:
copy_asset(
self.confdir / entry,
self.outdir / '_static',
excluded=excluded, context=context,
renderer=self.templates, onerror=onerror,
excluded=excluded,
context=context,
renderer=self.templates,
onerror=onerror,
force=True,
)
@ -916,8 +958,7 @@ class StandaloneHTMLBuilder(Builder):
uri = node['uri']
reference = nodes.reference('', '', internal=True)
if uri in self.images:
reference['refuri'] = posixpath.join(self.imgpath,
self.images[uri])
reference['refuri'] = posixpath.join(self.imgpath, self.images[uri])
else:
reference['refuri'] = uri
node.replace_self(reference)
@ -936,9 +977,13 @@ class StandaloneHTMLBuilder(Builder):
self.indexer.load(fb, self.indexer_format)
except (OSError, ValueError):
if keep:
logger.warning(__("search index couldn't be loaded, but not all "
'documents will be built: the index will be '
'incomplete.'))
logger.warning(
__(
"search index couldn't be loaded, but not all "
'documents will be built: the index will be '
'incomplete.'
)
)
# delete all entries for files that will be rebuilt
self.indexer.prune(keep)
@ -952,12 +997,16 @@ class StandaloneHTMLBuilder(Builder):
else:
self.indexer.feed(pagename, filename, title, doctree)
def _get_local_toctree(self, docname: str, collapse: bool = True, **kwargs: Any) -> str:
def _get_local_toctree(
self, docname: str, collapse: bool = True, **kwargs: Any
) -> str:
if 'includehidden' not in kwargs:
kwargs['includehidden'] = False
if kwargs.get('maxdepth') == '':
kwargs.pop('maxdepth')
toctree = global_toctree_for_doc(self.env, docname, self, collapse=collapse, **kwargs)
toctree = global_toctree_for_doc(
self.env, docname, self, collapse=collapse, **kwargs
)
return self.render_partial(toctree)['fragment']
def get_outfilename(self, pagename: str) -> str:
@ -995,7 +1044,8 @@ class StandaloneHTMLBuilder(Builder):
return quote(docname) + self.link_suffix
def handle_page(
self, pagename: str,
self,
pagename: str,
addctx: dict[str, Any],
templatename: str = 'page.html',
outfilename: str | None = None,
@ -1011,13 +1061,16 @@ class StandaloneHTMLBuilder(Builder):
default_baseuri = default_baseuri.rsplit('#', 1)[0]
if self.config.html_baseurl:
ctx['pageurl'] = posixpath.join(self.config.html_baseurl,
pagename + self.out_suffix)
ctx['pageurl'] = posixpath.join(
self.config.html_baseurl, pagename + self.out_suffix
)
else:
ctx['pageurl'] = None
def pathto(
otheruri: str, resource: bool = False, baseuri: str = default_baseuri,
otheruri: str,
resource: bool = False,
baseuri: str = default_baseuri,
) -> str:
if resource and '://' in otheruri:
# allow non-local resources given by scheme
@ -1028,6 +1081,7 @@ class StandaloneHTMLBuilder(Builder):
if uri == '#' and not self.allow_sharp_as_current_path:
uri = baseuri
return uri
ctx['pathto'] = pathto
def hasdoc(name: str) -> bool:
@ -1036,6 +1090,7 @@ class StandaloneHTMLBuilder(Builder):
if name == 'search' and self.search:
return True
return name == 'genindex' and self.get_builder_config('use_index', 'html')
ctx['hasdoc'] = hasdoc
ctx['toctree'] = lambda **kwargs: self._get_local_toctree(pagename, **kwargs)
@ -1048,9 +1103,11 @@ class StandaloneHTMLBuilder(Builder):
outdir = self.app.outdir
def css_tag(css: _CascadingStyleSheet) -> str:
attrs = [f'{key}="{html.escape(value, quote=True)}"'
for key, value in css.attributes.items()
if value is not None]
attrs = [
f'{key}="{html.escape(value, quote=True)}"'
for key, value in css.attributes.items()
if value is not None
]
uri = pathto(os.fspath(css.filename), resource=True)
# the EPUB format does not allow the use of query components
# the Windows help compiler requires that css links
@ -1068,9 +1125,11 @@ class StandaloneHTMLBuilder(Builder):
return f'<script src="{pathto(js, resource=True)}"></script>'
body = js.attributes.get('body', '')
attrs = [f'{key}="{html.escape(value, quote=True)}"'
for key, value in js.attributes.items()
if key != 'body' and value is not None]
attrs = [
f'{key}="{html.escape(value, quote=True)}"'
for key, value in js.attributes.items()
if key != 'body' and value is not None
]
if not js.filename:
if attrs:
@ -1099,15 +1158,18 @@ class StandaloneHTMLBuilder(Builder):
self._js_files[:] = self._orig_js_files
self.update_page_context(pagename, templatename, ctx, event_arg)
newtmpl = self.app.emit_firstresult('html-page-context', pagename,
templatename, ctx, event_arg)
newtmpl = self.app.emit_firstresult(
'html-page-context', pagename, templatename, ctx, event_arg
)
if newtmpl:
templatename = newtmpl
# sort JS/CSS before rendering HTML
try: # NoQA: SIM105
# Convert script_files to list to support non-list script_files (refs: #8889)
ctx['script_files'] = sorted(ctx['script_files'], key=lambda js: js.priority)
ctx['script_files'] = sorted(
ctx['script_files'], key=lambda js: js.priority
)
except AttributeError:
# Skip sorting if users modifies script_files directly (maybe via `html_context`).
# refs: #8885
@ -1121,33 +1183,42 @@ class StandaloneHTMLBuilder(Builder):
try:
output = self.templates.render(templatename, ctx)
except UnicodeError:
logger.warning(__("a Unicode error occurred when rendering the page %s. "
"Please make sure all config values that contain "
"non-ASCII content are Unicode strings."), pagename)
logger.warning(
__(
'a Unicode error occurred when rendering the page %s. '
'Please make sure all config values that contain '
'non-ASCII content are Unicode strings.'
),
pagename,
)
return
except Exception as exc:
raise ThemeError(__("An error happened in rendering the page %s.\nReason: %r") %
(pagename, exc)) from exc
msg = __('An error happened in rendering the page %s.\nReason: %r') % (
pagename,
exc,
)
raise ThemeError(msg) from exc
if not outfilename:
outfilename = self.get_outfilename(pagename)
# outfilename's path is in general different from self.outdir
ensuredir(path.dirname(outfilename))
try:
with open(outfilename, 'w', encoding=ctx['encoding'],
errors='xmlcharrefreplace') as f:
with open(
outfilename, 'w', encoding=ctx['encoding'], errors='xmlcharrefreplace'
) as f:
f.write(output)
except OSError as err:
logger.warning(__("error writing file %s: %s"), outfilename, err)
logger.warning(__('error writing file %s: %s'), outfilename, err)
if self.copysource and ctx.get('sourcename'):
# copy the source file for the "show source" link
source_name = path.join(self.outdir, '_sources',
os_path(ctx['sourcename']))
source_name = path.join(self.outdir, '_sources', os_path(ctx['sourcename']))
ensuredir(path.dirname(source_name))
copyfile(self.env.doc2path(pagename), source_name, force=True)
def update_page_context(self, pagename: str, templatename: str,
ctx: dict[str, Any], event_arg: Any) -> None:
def update_page_context(
self, pagename: str, templatename: str, ctx: dict[str, Any], event_arg: Any
) -> None:
pass
def handle_finish(self) -> None:
@ -1210,8 +1281,13 @@ def convert_html_js_files(app: Sphinx, config: Config) -> None:
config.html_js_files = html_js_files
def setup_resource_paths(app: Sphinx, pagename: str, templatename: str,
context: dict[str, Any], doctree: Node) -> None:
def setup_resource_paths(
app: Sphinx,
pagename: str,
templatename: str,
context: dict[str, Any],
doctree: Node,
) -> None:
"""Set up relative resource paths."""
pathto = context['pathto']
@ -1232,8 +1308,10 @@ def validate_math_renderer(app: Sphinx) -> None:
name = app.builder.math_renderer_name # type: ignore[attr-defined]
if name is None:
raise ConfigError(__('Many math_renderers are registered. '
'But no math_renderer is selected.'))
msg = __(
'Many math_renderers are registered. But no math_renderer is selected.'
)
raise ConfigError(msg)
if name not in app.registry.html_inline_math_renderers:
raise ConfigError(__('Unknown math_renderer %r is given.') % name)
@ -1245,9 +1323,13 @@ def validate_html_extra_path(app: Sphinx, config: Config) -> None:
if not path.exists(extra_path):
logger.warning(__('html_extra_path entry %r does not exist'), entry)
config.html_extra_path.remove(entry)
elif (path.splitdrive(app.outdir)[0] == path.splitdrive(extra_path)[0] and
path.commonpath((app.outdir, extra_path)) == path.normpath(app.outdir)):
logger.warning(__('html_extra_path entry %r is placed inside outdir'), entry)
elif (
path.splitdrive(app.outdir)[0] == path.splitdrive(extra_path)[0]
and path.commonpath((app.outdir, extra_path)) == path.normpath(app.outdir)
): # fmt: skip
logger.warning(
__('html_extra_path entry %r is placed inside outdir'), entry
)
config.html_extra_path.remove(entry)
@ -1258,26 +1340,34 @@ def validate_html_static_path(app: Sphinx, config: Config) -> None:
if not path.exists(static_path):
logger.warning(__('html_static_path entry %r does not exist'), entry)
config.html_static_path.remove(entry)
elif (path.splitdrive(app.outdir)[0] == path.splitdrive(static_path)[0] and
path.commonpath((app.outdir, static_path)) == path.normpath(app.outdir)):
logger.warning(__('html_static_path entry %r is placed inside outdir'), entry)
elif (
path.splitdrive(app.outdir)[0] == path.splitdrive(static_path)[0]
and path.commonpath((app.outdir, static_path)) == path.normpath(app.outdir)
): # fmt: skip
logger.warning(
__('html_static_path entry %r is placed inside outdir'), entry
)
config.html_static_path.remove(entry)
def validate_html_logo(app: Sphinx, config: Config) -> None:
"""Check html_logo setting."""
if (config.html_logo and
not path.isfile(path.join(app.confdir, config.html_logo)) and
not isurl(config.html_logo)):
if (
config.html_logo
and not path.isfile(path.join(app.confdir, config.html_logo))
and not isurl(config.html_logo)
):
logger.warning(__('logo file %r does not exist'), config.html_logo)
config.html_logo = None
def validate_html_favicon(app: Sphinx, config: Config) -> None:
"""Check html_favicon setting."""
if (config.html_favicon and
not path.isfile(path.join(app.confdir, config.html_favicon)) and
not isurl(config.html_favicon)):
if (
config.html_favicon
and not path.isfile(path.join(app.confdir, config.html_favicon))
and not isurl(config.html_favicon)
):
logger.warning(__('favicon file %r does not exist'), config.html_favicon)
config.html_favicon = None
@ -1290,9 +1380,11 @@ def error_on_html_sidebars_string_values(app: Sphinx, config: Config) -> None:
errors[pattern] = [pat_sidebars]
if not errors:
return
msg = __("Values in 'html_sidebars' must be a list of strings. "
"At least one pattern has a string value: %s. "
"Change to `html_sidebars = %r`.")
msg = __(
"Values in 'html_sidebars' must be a list of strings. "
'At least one pattern has a string value: %s. '
'Change to `html_sidebars = %r`.'
)
bad_patterns = ', '.join(map(repr, errors))
fixed = config.html_sidebars | errors
raise ConfigError(msg % (bad_patterns, fixed))
@ -1301,10 +1393,11 @@ def error_on_html_sidebars_string_values(app: Sphinx, config: Config) -> None:
def error_on_html_4(_app: Sphinx, config: Config) -> None:
"""Error on HTML 4."""
if config.html4_writer:
raise ConfigError(_(
msg = __(
'HTML 4 is no longer supported by Sphinx. '
'("html4_writer=True" detected in configuration options)',
))
)
raise ConfigError(msg)
def setup(app: Sphinx) -> ExtensionMetadata:
@ -1316,7 +1409,11 @@ def setup(app: Sphinx) -> ExtensionMetadata:
app.add_config_value('html_theme_path', [], 'html')
app.add_config_value('html_theme_options', {}, 'html')
app.add_config_value(
'html_title', lambda c: _('%s %s documentation') % (c.project, c.release), 'html', str)
'html_title',
lambda c: _('%s %s documentation') % (c.project, c.release),
'html',
str,
)
app.add_config_value('html_short_title', lambda self: self.html_title, 'html')
app.add_config_value('html_style', None, 'html', {list, str})
app.add_config_value('html_logo', None, 'html', str)
@ -1326,8 +1423,9 @@ def setup(app: Sphinx) -> ExtensionMetadata:
app.add_config_value('html_static_path', [], 'html')
app.add_config_value('html_extra_path', [], 'html')
app.add_config_value('html_last_updated_fmt', None, 'html', str)
app.add_config_value('html_last_updated_time_zone', 'local', 'html',
ENUM('GMT', 'local'))
app.add_config_value(
'html_last_updated_time_zone', 'local', 'html', ENUM('GMT', 'local')
)
app.add_config_value('html_sidebars', {}, 'html')
app.add_config_value('html_additional_pages', {}, 'html')
app.add_config_value('html_domain_indices', True, 'html', types={set, list})
@ -1354,8 +1452,9 @@ def setup(app: Sphinx) -> ExtensionMetadata:
app.add_config_value('html_scaled_image_link', True, 'html')
app.add_config_value('html_baseurl', '', 'html')
# removal is indefinitely on hold (ref: https://github.com/sphinx-doc/sphinx/issues/10265)
app.add_config_value('html_codeblock_linenos_style', 'inline', 'html',
ENUM('table', 'inline'))
app.add_config_value(
'html_codeblock_linenos_style', 'inline', 'html', ENUM('table', 'inline')
)
app.add_config_value('html_math_renderer', None, 'env')
app.add_config_value('html4_writer', False, 'html')
@ -1390,7 +1489,11 @@ def setup(app: Sphinx) -> ExtensionMetadata:
# deprecated name -> (object to return, canonical path or empty string, removal version)
_DEPRECATED_OBJECTS: dict[str, tuple[Any, str, tuple[int, int]]] = {
'Stylesheet': (_CascadingStyleSheet, 'sphinx.builders.html._assets._CascadingStyleSheet', (9, 0)), # NoQA: E501
'Stylesheet': (
_CascadingStyleSheet,
'sphinx.builders.html._assets._CascadingStyleSheet',
(9, 0),
),
'JavaScript': (_JavaScript, 'sphinx.builders.html._assets._JavaScript', (9, 0)),
}

View File

@ -19,7 +19,9 @@ class _CascadingStyleSheet:
def __init__(
self,
filename: str | os.PathLike[str], /, *,
filename: str | os.PathLike[str],
/,
*,
priority: int = 500,
rel: str = 'stylesheet',
type: str = 'text/css',
@ -31,20 +33,28 @@ class _CascadingStyleSheet:
def __str__(self) -> str:
attr = ', '.join(f'{k}={v!r}' for k, v in self.attributes.items())
return (f'{self.__class__.__name__}({self.filename!r}, '
f'priority={self.priority}, '
f'{attr})')
return (
f'{self.__class__.__name__}({self.filename!r}, '
f'priority={self.priority}, '
f'{attr})'
)
def __eq__(self, other: object) -> bool:
if isinstance(other, str):
warnings.warn('The str interface for _CascadingStyleSheet objects is deprecated. '
'Use css.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
warnings.warn(
'The str interface for _CascadingStyleSheet objects is deprecated. '
'Use css.filename instead.',
RemovedInSphinx90Warning,
stacklevel=2,
)
return self.filename == other
if not isinstance(other, _CascadingStyleSheet):
return NotImplemented
return (self.filename == other.filename
and self.priority == other.priority
and self.attributes == other.attributes)
return (
self.filename == other.filename
and self.priority == other.priority
and self.attributes == other.attributes
)
def __hash__(self) -> int:
return hash((self.filename, self.priority, *sorted(self.attributes.items())))
@ -58,13 +68,21 @@ class _CascadingStyleSheet:
raise AttributeError(msg)
def __getattr__(self, key: str) -> str:
warnings.warn('The str interface for _CascadingStyleSheet objects is deprecated. '
'Use css.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
warnings.warn(
'The str interface for _CascadingStyleSheet objects is deprecated. '
'Use css.filename instead.',
RemovedInSphinx90Warning,
stacklevel=2,
)
return getattr(os.fspath(self.filename), key)
def __getitem__(self, key: int | slice) -> str:
warnings.warn('The str interface for _CascadingStyleSheet objects is deprecated. '
'Use css.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
warnings.warn(
'The str interface for _CascadingStyleSheet objects is deprecated. '
'Use css.filename instead.',
RemovedInSphinx90Warning,
stacklevel=2,
)
return os.fspath(self.filename)[key]
@ -75,7 +93,9 @@ class _JavaScript:
def __init__(
self,
filename: str | os.PathLike[str], /, *,
filename: str | os.PathLike[str],
/,
*,
priority: int = 500,
**attributes: str,
) -> None:
@ -87,20 +107,28 @@ class _JavaScript:
attr = ''
if self.attributes:
attr = ', ' + ', '.join(f'{k}={v!r}' for k, v in self.attributes.items())
return (f'{self.__class__.__name__}({self.filename!r}, '
f'priority={self.priority}'
f'{attr})')
return (
f'{self.__class__.__name__}({self.filename!r}, '
f'priority={self.priority}'
f'{attr})'
)
def __eq__(self, other: object) -> bool:
if isinstance(other, str):
warnings.warn('The str interface for _JavaScript objects is deprecated. '
'Use js.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
warnings.warn(
'The str interface for _JavaScript objects is deprecated. '
'Use js.filename instead.',
RemovedInSphinx90Warning,
stacklevel=2,
)
return self.filename == other
if not isinstance(other, _JavaScript):
return NotImplemented
return (self.filename == other.filename
and self.priority == other.priority
and self.attributes == other.attributes)
return (
self.filename == other.filename
and self.priority == other.priority
and self.attributes == other.attributes
)
def __hash__(self) -> int:
return hash((self.filename, self.priority, *sorted(self.attributes.items())))
@ -114,13 +142,21 @@ class _JavaScript:
raise AttributeError(msg)
def __getattr__(self, key: str) -> str:
warnings.warn('The str interface for _JavaScript objects is deprecated. '
'Use js.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
warnings.warn(
'The str interface for _JavaScript objects is deprecated. '
'Use js.filename instead.',
RemovedInSphinx90Warning,
stacklevel=2,
)
return getattr(os.fspath(self.filename), key)
def __getitem__(self, key: int | slice) -> str:
warnings.warn('The str interface for _JavaScript objects is deprecated. '
'Use js.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
warnings.warn(
'The str interface for _JavaScript objects is deprecated. '
'Use js.filename instead.',
RemovedInSphinx90Warning,
stacklevel=2,
)
return os.fspath(self.filename)[key]

View File

@ -24,7 +24,7 @@ class BuildInfo:
@classmethod
def load(cls: type[BuildInfo], filename: Path, /) -> BuildInfo:
content = filename.read_text(encoding="utf-8")
content = filename.read_text(encoding='utf-8')
lines = content.splitlines()
version = lines[0].rstrip()
@ -61,8 +61,9 @@ class BuildInfo:
self.tags_hash = stable_hash(sorted(tags))
def __eq__(self, other: BuildInfo) -> bool: # type: ignore[override]
return (self.config_hash == other.config_hash and
self.tags_hash == other.tags_hash)
return (
self.config_hash == other.config_hash and self.tags_hash == other.tags_hash
)
def dump(self, filename: Path, /) -> None:
build_info = (
@ -72,4 +73,4 @@ class BuildInfo:
f'config: {self.config_hash}\n'
f'tags: {self.tags_hash}\n'
)
filename.write_text(build_info, encoding="utf-8")
filename.write_text(build_info, encoding='utf-8')

View File

@ -36,16 +36,18 @@ class KeyboardTransform(SphinxPostTransform):
default_priority = 400
formats = ('html',)
pattern = re.compile(r'(?<=.)(-|\+|\^|\s+)(?=.)')
multiwords_keys = (('caps', 'lock'),
('page', 'down'),
('page', 'up'),
('scroll', 'lock'),
('num', 'lock'),
('sys', 'rq'),
('back', 'space'))
multiwords_keys = (
('caps', 'lock'),
('page', 'down'),
('page', 'up'),
('scroll', 'lock'),
('num', 'lock'),
('sys', 'rq'),
('back', 'space'),
)
def run(self, **kwargs: Any) -> None:
matcher = NodeMatcher(nodes.literal, classes=["kbd"])
matcher = NodeMatcher(nodes.literal, classes=['kbd'])
# this list must be pre-created as during iteration new nodes
# are added which match the condition in the NodeMatcher.
for node in list(matcher.findall(self.document)):
@ -61,7 +63,7 @@ class KeyboardTransform(SphinxPostTransform):
parts[:3] = []
else:
key = parts.pop(0)
node += nodes.literal('', key, classes=["kbd"])
node += nodes.literal('', key, classes=['kbd'])
try:
# key separator (ex. -, +, ^)