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', 'IndexEntry'), # sphinx.domains.IndexEntry
('py:class', 'Node'), # sphinx.domains.Domain ('py:class', 'Node'), # sphinx.domains.Domain
('py:class', 'NullTranslations'), # gettext.NullTranslations ('py:class', 'NullTranslations'), # gettext.NullTranslations
('py:class', 'Path'), # sphinx.environment.BuildEnvironment.doc2path
('py:class', 'RoleFunction'), # sphinx.domains.Domain ('py:class', 'RoleFunction'), # sphinx.domains.Domain
('py:class', 'RSTState'), # sphinx.utils.parsing.nested_parse_to_nodes ('py:class', 'RSTState'), # sphinx.utils.parsing.nested_parse_to_nodes
('py:class', 'Theme'), # sphinx.application.TemplateBridge ('py:class', 'Theme'), # sphinx.application.TemplateBridge

View File

@ -519,7 +519,7 @@ class Builder:
if path.isfile(docutilsconf): if path.isfile(docutilsconf):
self.env.note_dependency(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) filetype = get_filetype(self.app.config.source_suffix, filename)
publisher = self.app.registry.get_publisher(self.app, filetype) publisher = self.app.registry.get_publisher(self.app, filetype)
self.env.temp_data['_parser'] = publisher.parser 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: with open(targetfn, 'w', encoding='utf-8') as f:
text = ''.join(hl(i + 1, line) for (i, line) in enumerate(lines)) text = ''.join(hl(i + 1, line) for (i, line) in enumerate(lines))
ctx = { ctx = {
'filename': self.env.doc2path(docname, False), 'filename': str(self.env.doc2path(docname, False)),
'text': text, 'text': text,
} }
f.write(self.templates.render('changes/rstsource.html', ctx)) 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 '' title = self.render_partial(title_node)['title'] if title_node else ''
# Suffix for the document # 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 # the name for the copied source
if self.config.html_copy_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: def index_page(self, pagename: str, doctree: nodes.document, title: str) -> None:
# only index pages with title # only index pages with title
if self.indexer is not None and 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, {}) metadata = self.env.metadata.get(pagename, {})
if 'no-search' in metadata or 'nosearch' in metadata: if 'no-search' in metadata or 'nosearch' in metadata:
self.indexer.feed(pagename, filename, '', new_document('')) self.indexer.feed(pagename, filename, '', new_document(''))

View File

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

View File

@ -145,7 +145,7 @@ class TocTree(SphinxDirective):
continue continue
if docname not in frozen_all_docnames: 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') message = __('toctree contains reference to excluded document %r')
subtype = 'excluded' subtype = 'excluded'
else: else:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import time
import wsgiref.handlers import wsgiref.handlers
from base64 import b64encode from base64 import b64encode
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler
from pathlib import Path
from queue import Queue from queue import Queue
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest import mock from unittest import mock
@ -1061,7 +1062,7 @@ def test_connection_contention(get_adapter, app, capsys):
wqueue: Queue[CheckRequest] = Queue() wqueue: Queue[CheckRequest] = Queue()
rqueue: Queue[CheckResult] = Queue() rqueue: Queue[CheckResult] = Queue()
for _ in range(link_count): 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() begin = time.time()
checked: list[CheckResult] = [] checked: list[CheckResult] = []

View File

@ -14,7 +14,7 @@ def _doctree_for_test(builder, docname: str) -> nodes.document:
builder.env.prepare_settings(docname) builder.env.prepare_settings(docname)
publisher = create_publisher(builder.app, 'restructuredtext') publisher = create_publisher(builder.app, 'restructuredtext')
with sphinx_domains(builder.env): 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() publisher.publish()
return publisher.document return publisher.document

View File

@ -1,4 +1,5 @@
"""Tests project module.""" """Tests project module."""
from pathlib import Path
import pytest import pytest
@ -64,15 +65,15 @@ def test_project_doc2path(app):
project.discover() project.discover()
# absolute path # 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 # 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 # 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 # matched source_suffix is used if exists
(app.srcdir / 'bar.txt').touch() (app.srcdir / 'bar.txt').touch()
project.discover() project.discover()
assert project.doc2path('bar', absolute=False) == 'bar.txt' assert project.doc2path('bar', absolute=False) == Path('bar.txt')