Use pathlib in `sphinx.project`

This commit is contained in:
Adam Turner 2024-07-23 03:53:16 +01:00
parent 932e5c56db
commit de15d61a46
15 changed files with 51 additions and 43 deletions

View File

@ -184,6 +184,7 @@ nitpick_ignore = {
('py:class', 'IndexEntry'), # sphinx.domains.IndexEntry
('py:class', 'Node'), # sphinx.domains.Domain
('py:class', 'NullTranslations'), # gettext.NullTranslations
('py:class', 'Path'), # sphinx.environment.BuildEnvironment.doc2path
('py:class', 'RoleFunction'), # sphinx.domains.Domain
('py:class', 'RSTState'), # sphinx.utils.parsing.nested_parse_to_nodes
('py:class', 'Theme'), # sphinx.application.TemplateBridge

View File

@ -519,7 +519,7 @@ class Builder:
if path.isfile(docutilsconf):
self.env.note_dependency(docutilsconf)
filename = self.env.doc2path(docname)
filename = str(self.env.doc2path(docname))
filetype = get_filetype(self.app.config.source_suffix, filename)
publisher = self.app.registry.get_publisher(self.app, filetype)
self.env.temp_data['_parser'] = publisher.parser

View File

@ -134,7 +134,7 @@ class ChangesBuilder(Builder):
with open(targetfn, 'w', encoding='utf-8') as f:
text = ''.join(hl(i + 1, line) for (i, line) in enumerate(lines))
ctx = {
'filename': self.env.doc2path(docname, False),
'filename': str(self.env.doc2path(docname, False)),
'text': text,
}
f.write(self.templates.render('changes/rstsource.html', ctx))

View File

@ -611,7 +611,7 @@ class StandaloneHTMLBuilder(Builder):
title = self.render_partial(title_node)['title'] if title_node else ''
# Suffix for the document
source_suffix = 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:
@ -976,7 +976,7 @@ class StandaloneHTMLBuilder(Builder):
def index_page(self, pagename: str, doctree: nodes.document, title: str) -> None:
# only index pages with title
if self.indexer is not None and title:
filename = self.env.doc2path(pagename, base=False)
filename = str(self.env.doc2path(pagename, base=False))
metadata = self.env.metadata.get(pagename, {})
if 'no-search' in metadata or 'nosearch' in metadata:
self.indexer.feed(pagename, filename, '', new_document(''))

View File

@ -28,6 +28,7 @@ from sphinx.util.nodes import get_node_line
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
from pathlib import Path
from typing import Any
from requests import Response
@ -82,7 +83,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
filename = self.env.doc2path(result.docname, False)
linkstat: dict[str, str | int] = {
'filename': filename, 'lineno': result.lineno,
'filename': str(filename), 'lineno': result.lineno,
'status': result.status, 'code': result.code,
'uri': result.uri, 'info': result.message,
}
@ -149,7 +150,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
self.json_outfile.write(json.dumps(data))
self.json_outfile.write('\n')
def write_entry(self, what: str, docname: str, filename: str, line: int,
def write_entry(self, what: str, docname: str, filename: Path, line: int,
uri: str) -> None:
self.txt_outfile.write(f'{filename}:{line}: [{what}] {uri}\n')
@ -225,7 +226,7 @@ class HyperlinkCollector(SphinxPostTransform):
class Hyperlink(NamedTuple):
uri: str
docname: str
docpath: str
docpath: Path
lineno: int

View File

@ -145,7 +145,7 @@ class TocTree(SphinxDirective):
continue
if docname not in frozen_all_docnames:
if excluded(self.env.doc2path(docname, False)):
if excluded(str(self.env.doc2path(docname, False))):
message = __('toctree contains reference to excluded document %r')
subtype = 'excluded'
else:

View File

@ -413,13 +413,13 @@ class BuildEnvironment:
"""
return self.project.path2doc(filename)
def doc2path(self, docname: str, base: bool = True) -> str:
def doc2path(self, docname: str, base: bool = True) -> Path:
"""Return the filename for the document name.
If *base* is True, return absolute path under self.srcdir.
If *base* is False, return relative path to self.srcdir.
"""
return self.project.doc2path(docname, base)
return self.project.doc2path(docname, absolute=base)
def relfn2path(self, filename: str, docname: str | None = None) -> tuple[str, str]:
"""Return paths to a file referenced from a document, relative to
@ -628,7 +628,7 @@ class BuildEnvironment:
doctree = pickle.loads(serialised)
doctree.settings.env = self
doctree.reporter = LoggingReporter(self.doc2path(docname))
doctree.reporter = LoggingReporter(str(self.doc2path(docname)))
return doctree
@functools.cached_property
@ -650,7 +650,7 @@ class BuildEnvironment:
try:
doctree = self._write_doc_doctree_cache.pop(docname)
doctree.settings.env = self
doctree.reporter = LoggingReporter(self.doc2path(docname))
doctree.reporter = LoggingReporter(str(self.doc2path(docname)))
except KeyError:
doctree = self.get_doctree(docname)

View File

@ -319,7 +319,7 @@ def _toctree_entry(
ref, location=toctreenode, type='toc', subtype='no_title')
except KeyError:
# this is raised if the included file does not exist
ref_path = env.doc2path(ref, False)
ref_path = str(env.doc2path(ref, False))
if excluded(ref_path):
message = __('toctree contains reference to excluded document %r')
elif not included(ref_path):

View File

@ -249,7 +249,7 @@ class Autosummary(SphinxDirective):
docname = posixpath.join(tree_prefix, real_name)
docname = posixpath.normpath(posixpath.join(dirname, docname))
if docname not in self.env.found_docs:
if excluded(self.env.doc2path(docname, False)):
if excluded(str(self.env.doc2path(docname, False))):
msg = __('autosummary references excluded document %r. Ignored.')
else:
msg = __('autosummary: stub file not found %r. '
@ -802,7 +802,7 @@ def process_generate_options(app: Sphinx) -> None:
if genfiles is True:
env = app.builder.env
genfiles = [env.doc2path(x, base=False) for x in env.found_docs
genfiles = [str(env.doc2path(x, base=False)) for x in env.found_docs
if os.path.isfile(env.doc2path(x))]
elif genfiles is False:
pass

View File

@ -373,7 +373,7 @@ Doctest summary
try:
filename = relpath(node.source, self.env.srcdir).rsplit(':docstring of ', maxsplit=1)[0] # type: ignore[arg-type] # noqa: E501
except Exception:
filename = self.env.doc2path(docname, False)
filename = str(self.env.doc2path(docname, False))
return filename
@staticmethod

View File

@ -4,13 +4,13 @@ from __future__ import annotations
import contextlib
import os
from glob import glob
from pathlib import Path
from typing import TYPE_CHECKING
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.matching import get_matching_files
from sphinx.util.osutil import path_stabilize, relpath
from sphinx.util.osutil import path_stabilize
if TYPE_CHECKING:
from collections.abc import Iterable
@ -24,7 +24,7 @@ class Project:
def __init__(self, srcdir: str | os.PathLike[str], source_suffix: Iterable[str]) -> None:
#: Source directory.
self.srcdir = srcdir
self.srcdir = Path(srcdir)
#: source_suffix. Same as :confval:`source_suffix`.
self.source_suffix = tuple(source_suffix)
@ -34,8 +34,8 @@ class Project:
self.docnames: set[str] = set()
# Bijective mapping between docnames and (srcdir relative) paths.
self._path_to_docname: dict[str, str] = {}
self._docname_to_path: dict[str, str] = {}
self._path_to_docname: dict[Path, str] = {}
self._docname_to_path: dict[str, Path] = {}
def restore(self, other: Project) -> None:
"""Take over a result of last build."""
@ -60,22 +60,25 @@ class Project:
):
if docname := self.path2doc(filename):
if docname in self.docnames:
pattern = os.path.join(self.srcdir, docname) + '.*'
files = [relpath(f, self.srcdir) for f in glob(pattern)]
files = [
str(f.relative_to(self.srcdir))
for f in self.srcdir.glob(f'{docname}.*')
]
logger.warning(
__(
'multiple files found for the document "%s": %r\n'
'multiple files found for the document "%s": %s\n'
'Use %r for the build.'
),
docname,
files,
', '.join(files),
self.doc2path(docname, absolute=True),
once=True,
)
elif os.access(os.path.join(self.srcdir, filename), os.R_OK):
elif os.access(self.srcdir / filename, os.R_OK):
self.docnames.add(docname)
self._path_to_docname[filename] = docname
self._docname_to_path[docname] = filename
path = Path(filename)
self._path_to_docname[path] = docname
self._docname_to_path[docname] = path
else:
logger.warning(
__('Ignored unreadable document %r.'), filename, location=docname
@ -91,18 +94,19 @@ class Project:
try:
return self._path_to_docname[filename] # type: ignore[index]
except KeyError:
if os.path.isabs(filename):
path = Path(filename)
if path.is_absolute():
with contextlib.suppress(ValueError):
filename = os.path.relpath(filename, self.srcdir)
path = path.relative_to(self.srcdir)
for suffix in self.source_suffix:
if os.path.basename(filename).endswith(suffix):
return path_stabilize(filename).removesuffix(suffix)
if path.name.endswith(suffix):
return path_stabilize(path).removesuffix(suffix)
# the file does not have a docname
return None
def doc2path(self, docname: str, absolute: bool) -> str:
def doc2path(self, docname: str, absolute: bool) -> Path:
"""Return the filename for the document name.
If *absolute* is True, return as an absolute path.
@ -112,8 +116,8 @@ class Project:
filename = self._docname_to_path[docname]
except KeyError:
# Backwards compatibility: the document does not exist
filename = docname + self._first_source_suffix
filename = Path(docname + self._first_source_suffix)
if absolute:
return os.path.join(self.srcdir, filename)
return self.srcdir / filename
return filename

View File

@ -50,9 +50,9 @@ def docname_join(basedocname: str, docname: str) -> str:
return posixpath.normpath(posixpath.join('/' + basedocname, '..', docname))[1:]
def get_filetype(source_suffix: dict[str, str], filename: str) -> str:
def get_filetype(source_suffix: dict[str, str], filename: str | os.PathLike) -> str:
for suffix, filetype in source_suffix.items():
if filename.endswith(suffix):
if os.fspath(filename).endswith(suffix):
# If default filetype (None), considered as restructuredtext.
return filetype or 'restructuredtext'
raise FiletypeNotFoundError

View File

@ -10,6 +10,7 @@ import time
import wsgiref.handlers
from base64 import b64encode
from http.server import BaseHTTPRequestHandler
from pathlib import Path
from queue import Queue
from typing import TYPE_CHECKING
from unittest import mock
@ -1061,7 +1062,7 @@ def test_connection_contention(get_adapter, app, capsys):
wqueue: Queue[CheckRequest] = Queue()
rqueue: Queue[CheckResult] = Queue()
for _ in range(link_count):
wqueue.put(CheckRequest(0, Hyperlink(f"http://{address}", "test", "test.rst", 1)))
wqueue.put(CheckRequest(0, Hyperlink(f"http://{address}", "test", Path("test.rst"), 1)))
begin = time.time()
checked: list[CheckResult] = []

View File

@ -14,7 +14,7 @@ def _doctree_for_test(builder, docname: str) -> nodes.document:
builder.env.prepare_settings(docname)
publisher = create_publisher(builder.app, 'restructuredtext')
with sphinx_domains(builder.env):
publisher.set_source(source_path=builder.env.doc2path(docname))
publisher.set_source(source_path=str(builder.env.doc2path(docname)))
publisher.publish()
return publisher.document

View File

@ -1,4 +1,5 @@
"""Tests project module."""
from pathlib import Path
import pytest
@ -64,15 +65,15 @@ def test_project_doc2path(app):
project.discover()
# absolute path
assert project.doc2path('index', absolute=True) == str(app.srcdir / 'index.rst')
assert project.doc2path('index', absolute=True) == app.srcdir / 'index.rst'
# relative path
assert project.doc2path('index', absolute=False) == 'index.rst'
assert project.doc2path('index', absolute=False) == Path('index.rst')
# first source_suffix is used for missing file
assert project.doc2path('foo', absolute=False) == 'foo.rst'
assert project.doc2path('foo', absolute=False) == Path('foo.rst')
# matched source_suffix is used if exists
(app.srcdir / 'bar.txt').touch()
project.discover()
assert project.doc2path('bar', absolute=False) == 'bar.txt'
assert project.doc2path('bar', absolute=False) == Path('bar.txt')