Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor sphinx.environment.adapters.TocTree #11565

Merged
merged 24 commits into from
Aug 9, 2023
Merged
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
8 changes: 4 additions & 4 deletions sphinx/builders/html/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from sphinx.environment import BuildEnvironment
from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.environment.adapters.indexentries import IndexEntries
from sphinx.environment.adapters.toctree import TocTree
from sphinx.environment.adapters.toctree import document_toc, global_toctree_for_doc
from sphinx.errors import ConfigError, ThemeError
from sphinx.highlighting import PygmentsBridge
from sphinx.locale import _, __
Expand Down Expand Up @@ -638,7 +638,7 @@ def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, A
meta = self.env.metadata.get(docname)

# local TOC and global TOC tree
self_toc = TocTree(self.env).get_toc_for(docname, self)
self_toc = document_toc(self.env, docname, self.tags)
toc = self.render_partial(self_toc)['fragment']

return {
Expand Down Expand Up @@ -969,8 +969,8 @@ def _get_local_toctree(self, docname: str, collapse: bool = True, **kwargs: Any)
kwargs['includehidden'] = False
if kwargs.get('maxdepth') == '':
kwargs.pop('maxdepth')
return self.render_partial(TocTree(self.env).get_toctree_for(
docname, self, collapse, **kwargs))['fragment']
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:
return path.join(self.outdir, os_path(pagename) + self.out_suffix)
Expand Down
12 changes: 8 additions & 4 deletions sphinx/builders/singlehtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from sphinx.application import Sphinx
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.environment.adapters.toctree import TocTree
from sphinx.environment.adapters.toctree import global_toctree_for_doc
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.console import darkgreen # type: ignore
Expand Down Expand Up @@ -61,9 +61,13 @@ def fix_refuris(self, tree: Node) -> None:
refnode['refuri'] = fname + refuri[hashindex:]

def _get_local_toctree(self, docname: str, collapse: bool = True, **kwargs: Any) -> str:
if 'includehidden' not in kwargs:
if kwargs.get('includehidden', 'false').lower() == 'false':
kwargs['includehidden'] = False
toctree = TocTree(self.env).get_toctree_for(docname, self, collapse, **kwargs)
elif kwargs['includehidden'].lower() == 'true':
kwargs['includehidden'] = True
if kwargs.get('maxdepth') == '':
kwargs.pop('maxdepth')
toctree = global_toctree_for_doc(self.env, docname, self, collapse=collapse, **kwargs)
if toctree is not None:
self.fix_refuris(toctree)
return self.render_partial(toctree)['fragment']
Expand Down Expand Up @@ -118,7 +122,7 @@ def assemble_toc_fignumbers(self) -> dict[str, dict[str, dict[str, tuple[int, ..

def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, Any]:
# no relation links...
toctree = TocTree(self.env).get_toctree_for(self.config.root_doc, self, False)
toctree = global_toctree_for_doc(self.env, self.config.root_doc, self, collapse=False)
# if there is no toctree, toc is None
if toctree:
self.fix_refuris(toctree)
Expand Down
95 changes: 53 additions & 42 deletions sphinx/directives/other.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from sphinx import addnodes
from sphinx.domains.changeset import VersionChange # noqa: F401 # for compatibility
from sphinx.domains.std import StandardDomain
from sphinx.locale import _, __
from sphinx.util import docname_join, logging, url_re
from sphinx.util.docutils import SphinxDirective
Expand Down Expand Up @@ -79,71 +80,81 @@ def run(self) -> list[Node]:
return ret

def parse_content(self, toctree: addnodes.toctree) -> list[Node]:
generated_docnames = frozenset(self.env.domains['std']._virtual_doc_names)
generated_docnames = frozenset(StandardDomain._virtual_doc_names)
suffixes = self.config.source_suffix
current_docname = self.env.docname
glob = toctree['glob']

# glob target documents
all_docnames = self.env.found_docs.copy() | generated_docnames
all_docnames.remove(self.env.docname) # remove current document
all_docnames.remove(current_docname) # remove current document
frozen_all_docnames = frozenset(all_docnames)

ret: list[Node] = []
excluded = Matcher(self.config.exclude_patterns)
for entry in self.content:
if not entry:
continue

# look for explicit titles ("Some Title <document>")
explicit = explicit_title_re.match(entry)
if (toctree['glob'] and glob_re.match(entry) and
not explicit and not url_re.match(entry)):
patname = docname_join(self.env.docname, entry)
docnames = sorted(patfilter(all_docnames, patname))
for docname in docnames:
url_match = url_re.match(entry) is not None
if glob and glob_re.match(entry) and not explicit and not url_match:
pat_name = docname_join(current_docname, entry)
doc_names = sorted(patfilter(all_docnames, pat_name))
for docname in doc_names:
if docname in generated_docnames:
# don't include generated documents in globs
continue
all_docnames.remove(docname) # don't include it again
toctree['entries'].append((None, docname))
toctree['includefiles'].append(docname)
if not docnames:
if not doc_names:
logger.warning(__("toctree glob pattern %r didn't match any documents"),
entry, location=toctree)
continue

if explicit:
ref = explicit.group(2)
title = explicit.group(1)
docname = ref
else:
if explicit:
ref = explicit.group(2)
title = explicit.group(1)
docname = ref
else:
ref = docname = entry
title = None
# remove suffixes (backwards compatibility)
for suffix in suffixes:
if docname.endswith(suffix):
docname = docname[:-len(suffix)]
break
# absolutize filenames
docname = docname_join(self.env.docname, docname)
if url_re.match(ref) or ref == 'self':
toctree['entries'].append((title, ref))
elif docname not in self.env.found_docs | generated_docnames:
if excluded(self.env.doc2path(docname, False)):
message = __('toctree contains reference to excluded document %r')
subtype = 'excluded'
else:
message = __('toctree contains reference to nonexisting document %r')
subtype = 'not_readable'

logger.warning(message, docname, type='toc', subtype=subtype,
location=toctree)
self.env.note_reread()
ref = docname = entry
title = None

# remove suffixes (backwards compatibility)
for suffix in suffixes:
if docname.endswith(suffix):
docname = docname.removesuffix(suffix)
break

# absolutise filenames
docname = docname_join(current_docname, docname)
if url_match or ref == 'self':
toctree['entries'].append((title, ref))
continue

if docname not in frozen_all_docnames:
if excluded(self.env.doc2path(docname, False)):
message = __('toctree contains reference to excluded document %r')
subtype = 'excluded'
else:
if docname in all_docnames:
all_docnames.remove(docname)
else:
logger.warning(__('duplicated entry found in toctree: %s'), docname,
location=toctree)
message = __('toctree contains reference to nonexisting document %r')
subtype = 'not_readable'

toctree['entries'].append((title, docname))
toctree['includefiles'].append(docname)
logger.warning(message, docname, type='toc', subtype=subtype,
location=toctree)
self.env.note_reread()
continue

if docname in all_docnames:
all_docnames.remove(docname)
else:
logger.warning(__('duplicated entry found in toctree: %s'), docname,
location=toctree)

toctree['entries'].append((title, docname))
toctree['includefiles'].append(docname)

# entries contains all entries (self references, external links etc.)
if 'reversed' in self.options:
Expand Down
79 changes: 43 additions & 36 deletions sphinx/environment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from sphinx import addnodes
from sphinx.config import Config
from sphinx.domains import Domain
from sphinx.environment.adapters.toctree import TocTree
from sphinx.environment.adapters.toctree import _resolve_toctree
from sphinx.errors import BuildEnvironmentError, DocumentError, ExtensionError, SphinxError
from sphinx.events import EventManager
from sphinx.locale import __
Expand Down Expand Up @@ -58,7 +58,7 @@

# This is increased every time an environment attribute is added
# or changed to properly invalidate pickle files.
ENV_VERSION = 58
ENV_VERSION = 59

# config status
CONFIG_UNSET = -1
Expand Down Expand Up @@ -630,9 +630,9 @@ def get_and_resolve_doctree(

# now, resolve all toctree nodes
for toctreenode in doctree.findall(addnodes.toctree):
result = TocTree(self).resolve(docname, builder, toctreenode,
prune=prune_toctrees,
includehidden=includehidden)
result = _resolve_toctree(self, docname, builder, toctreenode,
prune=prune_toctrees,
includehidden=includehidden)
if result is None:
toctreenode.parent.replace(toctreenode, [])
else:
Expand All @@ -654,9 +654,8 @@ def resolve_toctree(self, docname: str, builder: Builder, toctree: addnodes.toct
If *collapse* is True, all branches not containing docname will
be collapsed.
"""
return TocTree(self).resolve(docname, builder, toctree, prune,
maxdepth, titles_only, collapse,
includehidden)
return _resolve_toctree(self, docname, builder, toctree, prune,
maxdepth, titles_only, collapse, includehidden)

def resolve_references(self, doctree: nodes.document, fromdocname: str,
builder: Builder) -> None:
Expand All @@ -680,38 +679,21 @@ def apply_post_transforms(self, doctree: nodes.document, docname: str) -> None:
self.events.emit('doctree-resolved', doctree, docname)

def collect_relations(self) -> dict[str, list[str | None]]:
traversed = set()

def traverse_toctree(
parent: str | None, docname: str,
) -> Iterator[tuple[str | None, str]]:
if parent == docname:
logger.warning(__('self referenced toctree found. Ignored.'),
location=docname, type='toc',
subtype='circular')
return

# traverse toctree by pre-order
yield parent, docname
traversed.add(docname)

for child in (self.toctree_includes.get(docname) or []):
for subparent, subdocname in traverse_toctree(docname, child):
if subdocname not in traversed:
yield subparent, subdocname
traversed.add(subdocname)
traversed: set[str] = set()

relations = {}
docnames = traverse_toctree(None, self.config.root_doc)
prevdoc = None
docnames = _traverse_toctree(
traversed, None, self.config.root_doc, self.toctree_includes,
)
prev_doc = None
parent, docname = next(docnames)
for nextparent, nextdoc in docnames:
relations[docname] = [parent, prevdoc, nextdoc]
prevdoc = docname
docname = nextdoc
parent = nextparent
for next_parent, next_doc in docnames:
relations[docname] = [parent, prev_doc, next_doc]
prev_doc = docname
docname = next_doc
parent = next_parent

relations[docname] = [parent, prevdoc, None]
relations[docname] = [parent, prev_doc, None]

return relations

Expand Down Expand Up @@ -750,3 +732,28 @@ def _last_modified_time(filename: str | os.PathLike[str]) -> int:

# upside-down floor division to get the ceiling
return -(os.stat(filename).st_mtime_ns // -1_000)


def _traverse_toctree(
traversed: set[str],
parent: str | None,
docname: str,
toctree_includes: dict[str, list[str]],
) -> Iterator[tuple[str | None, str]]:
if parent == docname:
logger.warning(__('self referenced toctree found. Ignored.'),
location=docname, type='toc',
subtype='circular')
return

# traverse toctree by pre-order
yield parent, docname
traversed.add(docname)

for child in toctree_includes.get(docname, ()):
for sub_parent, sub_docname in _traverse_toctree(
traversed, docname, child, toctree_includes,
):
if sub_docname not in traversed:
yield sub_parent, sub_docname
traversed.add(sub_docname)
Loading
Loading