Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion sphinx/builders/epub3.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,20 @@

if TYPE_CHECKING:
from collections.abc import Set
from typing import Any
from typing import Any, Literal

from sphinx.application import Sphinx
from sphinx.builders.html._ctx import _GlobalContextHTML
from sphinx.config import Config
from sphinx.util.typing import ExtensionMetadata

class _GlobalContextEpub3(_GlobalContextHTML):
theme_writing_mode: str | None
html_tag: str
use_meta_charset: bool
skip_ua_compatible: Literal[True]


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -89,6 +97,8 @@ class Epub3Builder(_epub_base.EpubBuilder):
html_tag = HTML_TAG
use_meta_charset = True

globalcontext: _GlobalContextEpub3

# Finish by building the epub file
def handle_finish(self) -> None:
"""Create the metainfo files and finally the epub."""
Expand Down
17 changes: 10 additions & 7 deletions sphinx/builders/html/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
from docutils.nodes import Node

from sphinx.application import Sphinx
from sphinx.builders.html._ctx import _GlobalContextHTML, _PageContextHTML
from sphinx.config import Config
from sphinx.environment import BuildEnvironment
from sphinx.util.typing import ExtensionMetadata
Expand Down Expand Up @@ -137,6 +138,8 @@ class StandaloneHTMLBuilder(Builder):
imgpath: str = ''
domain_indices: list[DOMAIN_INDEX_TYPE] = []

globalcontext: _GlobalContextHTML

def __init__(self, app: Sphinx, env: BuildEnvironment) -> None:
super().__init__(app, env)

Expand Down Expand Up @@ -565,7 +568,7 @@ def prepare_writing(self, docnames: Set[str]) -> None:
'html5_doctype': True,
}
if self.theme:
self.globalcontext |= {
self.globalcontext |= { # type: ignore[typeddict-item]
f'theme_{key}': val
for key, val in self.theme.get_options(self.theme_options).items()
}
Comment on lines +571 to 574
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to indicate globalcontext as "this fixed set of keys, plus zero or more arbitrary others of str -> Any" -- e.g. we add these theme_{...} keys, the self.config.html_context mapping on the next line.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the juice isn't worth the squeeze - something like the following code might do it but that is a mess. I wonder - could you separate something like base_context and extra_context variables - meaning there is always type checking for the base_context. I haven't looked into the details of where this is used and what trade-offs are there.

    class _GlobalContextHTML(Protocol):

        def __delitem__(self, key: str) -> None: ...
        def __iter__(self) -> Iterator[str]: ...
        def __len__(self) -> int: ...
        def items(self) -> Iterable[tuple[str, Any]]: ...
        def copy(self) -> Self: ...
        def __or__(self, other: dict[str, Any]) -> dict[str, Any]: ...
        def __ror__(self, other: dict[str, Any]) -> dict[str, Any]: ...
        def __ior__(self, other: dict[str, Any]) -> Self: ...

        def get(self, key: str, default: Any = ...) -> Any: ...
        def update(self, other: dict[str, Any]) -> None: ...

        @overload
        def __getitem__(self, key: Literal['embedded']) -> bool: ...

        @overload
        def __getitem__(self, key: Literal['project']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['release']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['version']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['last_updated']) -> str | None: ...

        @overload
        def __getitem__(self, key: Literal['copyright']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['master_doc']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['root_doc']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['use_opensearch']) -> bool: ...

        @overload
        def __getitem__(self, key: Literal['docstitle']) -> str | None: ...

        @overload
        def __getitem__(self, key: Literal['shorttitle']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['show_copyright']) -> bool: ...

        @overload
        def __getitem__(self, key: Literal['show_search_summary']) -> bool: ...

        @overload
        def __getitem__(self, key: Literal['show_sphinx']) -> bool: ...

        @overload
        def __getitem__(self, key: Literal['has_source']) -> bool: ...

        @overload
        def __getitem__(self, key: Literal['show_source']) -> bool: ...

        @overload
        def __getitem__(self, key: Literal['sourcelink_suffix']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['file_suffix']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['link_suffix']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['script_files']) -> Sequence[_JavaScript]: ...

        @overload
        def __getitem__(self, key: Literal['language']) -> str | None: ...

        @overload
        def __getitem__(self, key: Literal['css_files']) -> Sequence[_CascadingStyleSheet]: ...

        @overload
        def __getitem__(self, key: Literal['sphinx_version']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['sphinx_version_tuple']) -> tuple[int, int, int, str, int]: ...

        @overload
        def __getitem__(self, key: Literal['docutils_version_info']) -> tuple[int, int, int, str, int]: ...

        @overload
        def __getitem__(self, key: Literal['styles']) -> Sequence[str]: ...

        @overload
        def __getitem__(self, key: Literal['rellinks']) -> Sequence[tuple[str, str, str, str]]: ...

        @overload
        def __getitem__(self, key: Literal['builder']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['parents']) -> Sequence[_NavigationRelation]: ...

        @overload
        def __getitem__(self, key: Literal['logo_url']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['logo_alt']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['favicon_url']) -> str: ...

        @overload
        def __getitem__(self, key: Literal['html5_doctype']) -> Literal[True]: ...

        @overload
        def __getitem__(self, key: str) -> Any: ...

        def __setitem__(self, key: str, value: Any) -> None: ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://peps.python.org/pep-0728/ - this is approved and will solve for the case here, I believe.

Expand All @@ -580,7 +583,7 @@ def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, A
# find out relations
prev = next = None
parents = []
rellinks = self.globalcontext['rellinks'][:]
rellinks = list(self.globalcontext['rellinks'])
related = self.relations.get(docname)
titles = self.env.titles
if related and related[2]:
Expand Down Expand Up @@ -921,7 +924,7 @@ def copy_static_files(self) -> None:
self._static_dir.mkdir(parents=True, exist_ok=True)

# prepare context for templates
context = self.globalcontext.copy()
context: dict[str, Any] = self.globalcontext.copy() # type: ignore[assignment]
if self.indexer is not None:
context.update(self.indexer.context_for_searchtool())

Expand Down Expand Up @@ -1040,7 +1043,7 @@ def get_output_path(self, page_name: str, /) -> Path:
def get_outfilename(self, pagename: str) -> _StrPath:
return _StrPath(self.get_output_path(pagename))

def add_sidebars(self, pagename: str, ctx: dict[str, Any]) -> None:
def add_sidebars(self, pagename: str, ctx: _PageContextHTML) -> None:
def has_wildcard(pattern: str) -> bool:
return any(char in pattern for char in '*?[')

Expand Down Expand Up @@ -1080,7 +1083,7 @@ def handle_page(
outfilename: Path | None = None,
event_arg: Any = None,
) -> None:
ctx = self.globalcontext.copy()
ctx: _PageContextHTML = self.globalcontext.copy() # type: ignore[assignment]
# current_page_name is backwards compatibility
ctx['pagename'] = ctx['current_page_name'] = pagename
ctx['encoding'] = self.config.html_output_encoding
Expand Down Expand Up @@ -1124,7 +1127,7 @@ def hasdoc(name: str) -> bool:

ctx['toctree'] = lambda **kwargs: self._get_local_toctree(pagename, **kwargs)
self.add_sidebars(pagename, ctx)
ctx.update(addctx)
ctx.update(addctx) # type: ignore[typeddict-item]

# 'blah.html' should have content_root = './' not ''.
ctx['content_root'] = (f'..{SEP}' * default_baseuri.count(SEP)) or f'.{SEP}'
Expand Down Expand Up @@ -1261,7 +1264,7 @@ def js_tag(js: _JavaScript | str) -> str:
copyfile(self.env.doc2path(pagename), source_file_path, force=True)

def update_page_context(
self, pagename: str, templatename: str, ctx: dict[str, Any], event_arg: Any
self, pagename: str, templatename: str, ctx: _PageContextHTML, event_arg: Any
) -> None:
pass

Expand Down
89 changes: 89 additions & 0 deletions sphinx/builders/html/_ctx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from typing import Any, Literal, Protocol, TypedDict

from sphinx.builders.html._assets import _CascadingStyleSheet, _JavaScript

class _NavigationRelation(TypedDict):
link: str
title: str

class _GlobalContextHTML(TypedDict):
embedded: bool
project: str
release: str
version: str
last_updated: str | None
copyright: str
master_doc: str
root_doc: str
use_opensearch: bool
docstitle: str | None
shorttitle: str
show_copyright: bool
show_search_summary: bool
show_sphinx: bool
has_source: bool
show_source: bool
sourcelink_suffix: str
file_suffix: str
link_suffix: str
script_files: Sequence[_JavaScript]
language: str | None
css_files: Sequence[_CascadingStyleSheet]
sphinx_version: str
sphinx_version_tuple: tuple[int, int, int, str, int]
docutils_version_info: tuple[int, int, int, str, int]
styles: Sequence[str]
rellinks: Sequence[tuple[str, str, str, str]]
builder: str
parents: Sequence[_NavigationRelation]
logo_url: str
logo_alt: str
favicon_url: str
html5_doctype: Literal[True]

class _PathtoCallable(Protocol):
def __call__(
self, otheruri: str, resource: bool = False, baseuri: str = ...
) -> str: ...

class _ToctreeCallable(Protocol):
def __call__(self, **kwargs: Any) -> str: ...

class _PageContextHTML(_GlobalContextHTML):
# get_doc_context()
prev: Sequence[_NavigationRelation]
next: Sequence[_NavigationRelation]
title: str
meta: dict[str, Any] | None
body: str
metatags: str
sourcename: str
toc: str
display_toc: bool
page_source_suffix: str

# handle_page()
pagename: str
current_page_name: str
encoding: str
pageurl: str | None
pathto: _PathtoCallable
hasdoc: Callable[[str], bool]
toctree: _ToctreeCallable
content_root: str
css_tag: Callable[[_CascadingStyleSheet], str]
js_tag: Callable[[_JavaScript], str]

# add_sidebars()
sidebars: Sequence[str] | None

else:
_NavigationRelation = dict
_GlobalContextHTML = dict
_PageContextHTML = dict
5 changes: 2 additions & 3 deletions tests/test_builders/test_build_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
from tests.test_builders.xpath_util import check_xpath

if TYPE_CHECKING:
from typing import Any

from sphinx.builders.html._ctx import _PageContextHTML
from sphinx.testing.util import SphinxTestApp


Expand Down Expand Up @@ -416,7 +415,7 @@ def test_html_style(app: SphinxTestApp) -> None:
},
)
def test_html_sidebar(app: SphinxTestApp) -> None:
ctx: dict[str, Any] = {}
ctx: _PageContextHTML = {} # type: ignore[typeddict-item]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test app.builder.add_sidebars takes as its ctx parameter an empty dictionary - is that a valid use of app.builder.add_sidebars?

If so, it seems as though the type hint of app.builder.add_sidebars's ctx parameter should change.

If not, it seems as though this test should change to be realistic by actually having ctx be a _PageContextHTML rather than an empty dictionary.


# default for alabaster
app.build(force_all=True)
Expand Down
Loading