diff --git a/.github/workflows/builddoc.yml b/.github/workflows/builddoc.yml index f2055e2798f..e3347a7b535 100644 --- a/.github/workflows/builddoc.yml +++ b/.github/workflows/builddoc.yml @@ -27,7 +27,7 @@ jobs: with: python-version: "3" - name: Install graphviz - run: sudo apt-get install graphviz + run: sudo apt-get install --no-install-recommends --yes graphviz - name: Install uv run: > curl --no-progress-meter --location --fail diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 27f67597d46..aa3a27753b2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,7 +54,7 @@ jobs: - name: Check Python version run: python --version --version - name: Install graphviz - run: sudo apt-get install graphviz + run: sudo apt-get install --no-install-recommends --yes graphviz - name: Install uv run: > curl --no-progress-meter --location --fail @@ -92,7 +92,7 @@ jobs: - name: Check Python version run: python --version --version - name: Install graphviz - run: sudo apt-get install graphviz + run: sudo apt-get install --no-install-recommends --yes graphviz - name: Install dependencies run: | python -m pip install --upgrade pip @@ -125,7 +125,7 @@ jobs: - name: Check Python version run: python --version --version - name: Install graphviz - run: sudo apt-get install graphviz + run: sudo apt-get install --no-install-recommends --yes graphviz - name: Install dependencies run: | python -m pip install --upgrade pip @@ -158,7 +158,7 @@ jobs: - name: Check Python version run: python --version --version - name: Install graphviz - run: sudo apt-get install graphviz + run: sudo apt-get install --no-install-recommends --yes graphviz - name: Install dependencies run: | python -m pip install --upgrade pip @@ -218,7 +218,7 @@ jobs: - name: Check Python version run: python --version --version - name: Install graphviz - run: sudo apt-get install graphviz + run: sudo apt-get install --no-install-recommends --yes graphviz - name: Install uv run: > curl --no-progress-meter --location --fail @@ -250,7 +250,7 @@ jobs: - name: Check Python version run: python --version --version - name: Install graphviz - run: sudo apt-get install graphviz + run: sudo apt-get install --no-install-recommends --yes graphviz - name: Install uv run: > curl --no-progress-meter --location --fail @@ -310,7 +310,7 @@ jobs: - name: Check Python version run: python --version --version - name: Install graphviz - run: sudo apt-get install graphviz + run: sudo apt-get install --no-install-recommends --yes graphviz - name: Install uv run: > curl --no-progress-meter --location --fail diff --git a/AUTHORS.rst b/AUTHORS.rst index 5c463beed8e..8068ae4ae22 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -8,7 +8,7 @@ Maintainers * Chris Sewell <@chrisjsewell> * François Freitag <@francoisfreitag> * Jakob Lykke Andersen <@jakobandersen> -* Jean-François Burnol <@jfbu> +* Jean-François B. <@jfbu> * Stephen Finucane <@stephenfin> * Takayuki Shimizukawa <@shimizukawa> * Takeshi Komiya <@tk0miya> @@ -35,7 +35,6 @@ Contributors * Christopher Perkins -- autosummary integration * Dan MacKinlay -- metadata fixes * Daniel Bültmann -- todo extension -* Daniel Neuhäuser -- JavaScript domain, Python 3 support (GSOC) * Daniel Pizetta -- inheritance diagram improvements * Dave Kuhlman -- original LaTeX writer * Doug Hellmann -- graphviz improvements @@ -52,6 +51,7 @@ Contributors * Hugo van Kemenade -- support FORCE_COLOR and NO_COLOR * Ian Lee -- quickstart improvements * Jacob Mason -- websupport library (GSOC project) +* James Addison -- linkcheck and HTML search improvements * Jeppe Pihl -- literalinclude improvements * Joel Wurtz -- cellspanning support in LaTeX * John Waltman -- Texinfo builder @@ -72,14 +72,12 @@ Contributors * Michael Wilson -- Intersphinx HTTP basic auth support * Nathan Damon -- bugfix in validation of static paths in html builders * Pauli Virtanen -- autodoc improvements, autosummary extension -* A. Rafey Khan -- improved intersphinx typing -* Rob Ruana -- napoleon extension -* Robert Lehmann -- gettext builder (GSOC project) +* \A. Rafey Khan -- improved intersphinx typing * Roland Meister -- epub builder * Sebastian Wiesner -- image handling, distutils support * Stefan Seefeld -- toctree improvements * Stefan van der Walt -- autosummary extension -* T. Powers -- HTML output improvements +* \T. Powers -- HTML output improvements * Taku Shimizu -- epub3 builder * Thomas Lamb -- linkcheck builder * Thomas Waldmann -- apidoc module fixes diff --git a/doc/conf.py b/doc/conf.py index e7976f59a87..9c8aa877075 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -173,7 +173,6 @@ } # Sphinx document translation with sphinx gettext feature uses these settings: -locale_dirs = ['locale/'] gettext_compact = False nitpick_ignore = { diff --git a/doc/development/html_themes/index.rst b/doc/development/html_themes/index.rst index bc75beb7ba7..ad2820495ac 100644 --- a/doc/development/html_themes/index.rst +++ b/doc/development/html_themes/index.rst @@ -203,10 +203,10 @@ using the :meth:`~sphinx.application.Sphinx.add_html_theme` API: .. code-block:: python # your_theme_package.py - from os import path + from pathlib import Path def setup(app): - app.add_html_theme('name_of_theme', path.abspath(path.dirname(__file__))) + app.add_html_theme('name_of_theme', Path(__file__).resolve().parent) If your theme package contains two or more themes, please call ``add_html_theme()`` twice or more. diff --git a/doc/development/tutorials/extending_build.rst b/doc/development/tutorials/extending_build.rst index a81c84b0075..4d3606a0a33 100644 --- a/doc/development/tutorials/extending_build.rst +++ b/doc/development/tutorials/extending_build.rst @@ -313,10 +313,10 @@ For example: .. code-block:: python - import os import sys + from pathlib import Path - sys.path.append(os.path.abspath("./_ext")) + sys.path.append(str(Path('_ext').resolve())) extensions = ['todo'] diff --git a/doc/development/tutorials/extending_syntax.rst b/doc/development/tutorials/extending_syntax.rst index bab80371703..a8a5bfe62ec 100644 --- a/doc/development/tutorials/extending_syntax.rst +++ b/doc/development/tutorials/extending_syntax.rst @@ -169,10 +169,10 @@ For example: .. code-block:: python - import os import sys + from pathlib import Path - sys.path.append(os.path.abspath("./_ext")) + sys.path.append(str(Path('_ext').resolve())) extensions = ['helloworld'] diff --git a/doc/extdev/i18n.rst b/doc/extdev/i18n.rst index 3c476820fbd..8542ae684b6 100644 --- a/doc/extdev/i18n.rst +++ b/doc/extdev/i18n.rst @@ -51,7 +51,7 @@ In practice, you have to: :caption: src/__init__.py def setup(app): - package_dir = Path(__file__).parent.resolve() + package_dir = Path(__file__).resolve().parent locale_dir = package_dir / 'locales' app.add_message_catalog(MESSAGE_CATALOG_NAME, locale_dir) diff --git a/doc/tutorial/describing-code.rst b/doc/tutorial/describing-code.rst index e8c6a804fd2..32108df4de8 100644 --- a/doc/tutorial/describing-code.rst +++ b/doc/tutorial/describing-code.rst @@ -145,9 +145,9 @@ at the beginning of ``conf.py``: # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. - import pathlib import sys - sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix()) + from pathlib import Path + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) .. note:: diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 9768d248cce..40fe5931250 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -223,11 +223,14 @@ General configuration Ensure that absolute paths are used when modifying :data:`sys.path`. If your custom extensions live in a directory that is relative to the - :term:`configuration directory`, use :func:`os.path.abspath` like so: + :term:`configuration directory`, use :meth:`pathlib.Path.resolve` like so: .. code-block:: python - import os, sys; sys.path.append(os.path.abspath('sphinxext')) + import sys + from pathlib import Path + + sys.path.append(str(Path('sphinxext').resolve())) extensions = [ ... diff --git a/doc/usage/extensions/autosummary.rst b/doc/usage/extensions/autosummary.rst index 0a25d8dbd21..0b2b0c69cf8 100644 --- a/doc/usage/extensions/autosummary.rst +++ b/doc/usage/extensions/autosummary.rst @@ -61,10 +61,12 @@ The :mod:`sphinx.ext.autosummary` extension does this in two parts: :event:`autodoc-process-docstring` and :event:`autodoc-process-signature` hooks as :mod:`~sphinx.ext.autodoc`. - **Options** + .. rubric:: Options - * If you want the :rst:dir:`autosummary` table to also serve as a - :rst:dir:`toctree` entry, use the ``toctree`` option, for example:: + .. rst:directive:option:: toctree: optional directory name + + If you want the :rst:dir:`autosummary` table to also serve as a + :rst:dir:`toctree` entry, use the ``toctree`` option, for example:: .. autosummary:: :toctree: DIRNAME @@ -72,52 +74,54 @@ The :mod:`sphinx.ext.autosummary` extension does this in two parts: sphinx.environment.BuildEnvironment sphinx.util.relative_uri - The ``toctree`` option also signals to the :program:`sphinx-autogen` script - that stub pages should be generated for the entries listed in this - directive. The option accepts a directory name as an argument; - :program:`sphinx-autogen` will by default place its output in this - directory. If no argument is given, output is placed in the same directory - as the file that contains the directive. + The ``toctree`` option also signals to the :program:`sphinx-autogen` script + that stub pages should be generated for the entries listed in this + directive. The option accepts a directory name as an argument; + :program:`sphinx-autogen` will by default place its output in this + directory. If no argument is given, output is placed in the same directory + as the file that contains the directive. - You can also use ``caption`` option to give a caption to the toctree. + .. versionadded:: 0.6 - .. versionadded:: 3.1 + .. rst:directive:option:: caption: caption of ToC - caption option added. + Add a caption to the toctree. - * If you don't want the :rst:dir:`autosummary` to show function signatures in - the listing, include the ``nosignatures`` option:: + .. versionadded:: 3.1 - .. autosummary:: - :nosignatures: + .. rst:directive:option:: nosignatures - sphinx.environment.BuildEnvironment - sphinx.util.relative_uri + Do not show function signatures in the summary. - * You can specify a custom template with the ``template`` option. - For example, :: + .. versionadded:: 0.6 + + .. rst:directive:option:: template: filename + + Specify a custom template for rendering the summary. + For example, :: .. autosummary:: :template: mytemplate.rst sphinx.environment.BuildEnvironment - would use the template :file:`mytemplate.rst` in your - :confval:`templates_path` to generate the pages for all entries - listed. See `Customizing templates`_ below. + would use the template :file:`mytemplate.rst` in your + :confval:`templates_path` to generate the pages for all entries + listed. See `Customizing templates`_ below. + + .. versionadded:: 1.0 - .. versionadded:: 1.0 + .. rst:directive:option:: recursive - * You can specify the ``recursive`` option to generate documents for - modules and sub-packages recursively. It defaults to disabled. - For example, :: + Generate documents for modules and sub-packages recursively. + For example, :: .. autosummary:: :recursive: sphinx.environment.BuildEnvironment - .. versionadded:: 3.1 + .. versionadded:: 3.1 :program:`sphinx-autogen` -- generate autodoc stub pages diff --git a/doc/usage/extensions/index.rst b/doc/usage/extensions/index.rst index 929f2b604b2..4be426c3fe2 100644 --- a/doc/usage/extensions/index.rst +++ b/doc/usage/extensions/index.rst @@ -69,11 +69,14 @@ Where to put your own extensions? Extensions local to a project should be put within the project's directory structure. Set Python's module search path, ``sys.path``, accordingly so that Sphinx can find them. For example, if your extension ``foo.py`` lies in the -``exts`` subdirectory of the project root, put into :file:`conf.py`:: +``exts`` subdirectory of the project root, put into :file:`conf.py`: - import sys, os +.. code-block:: python - sys.path.append(os.path.abspath('exts')) + import sys + from pathlib import Path + + sys.path.append(str(Path('exts').resolve())) extensions = ['foo'] diff --git a/pyproject.toml b/pyproject.toml index 9c46ba94694..e3abc9d218d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ dependencies = [ "alabaster>=0.7.14", "imagesize>=1.3", "requests>=2.30.0", + "roman-numerals-py>=1.0.0", "packaging>=23.0", "colorama>=0.4.6; sys_platform == 'win32'", ] @@ -81,7 +82,7 @@ docs = [ ] lint = [ "flake8>=6.0", - "ruff==0.7.0", + "ruff==0.7.2", "mypy==1.13.0", "sphinx-lint>=0.9", "types-colorama==0.4.15.20240311", @@ -91,7 +92,7 @@ lint = [ "types-Pygments==2.18.0.20240506", "types-requests==2.32.0.20241016", # align with requests "types-urllib3==1.26.25.14", - "pyright==1.1.387", + "pyright==1.1.388", "pytest>=6.0", ] test = [ diff --git a/sphinx/_cli/__init__.py b/sphinx/_cli/__init__.py index 3160f08373c..270d9210e33 100644 --- a/sphinx/_cli/__init__.py +++ b/sphinx/_cli/__init__.py @@ -170,7 +170,7 @@ def _format_metavar( def error(self, message: str) -> NoReturn: sys.stderr.write( __( - '{0}: error: {1}\n' "Run '{0} --help' for information" # NoQA: COM812 + "{0}: error: {1}\nRun '{0} --help' for information" # NoQA: COM812 ).format(self.prog, message) ) raise SystemExit(2) diff --git a/sphinx/application.py b/sphinx/application.py index 2d650dc231f..5edc86afb71 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -11,7 +11,6 @@ import sys from collections import deque from io import StringIO -from os import path from typing import TYPE_CHECKING, overload from docutils.parsers.rst import Directive, roles @@ -182,11 +181,11 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st self.outdir = _StrPath(outdir).resolve() self.doctreedir = _StrPath(doctreedir).resolve() - if not path.isdir(self.srcdir): + if not self.srcdir.is_dir(): raise ApplicationError(__('Cannot find source directory (%s)') % self.srcdir) - if path.exists(self.outdir) and not path.isdir(self.outdir): + if self.outdir.exists() and not self.outdir.is_dir(): raise ApplicationError(__('Output directory (%s) is not a directory') % self.outdir) @@ -258,9 +257,9 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st # preload builder module (before init config values) self.preload_builder(buildername) - if not path.isdir(outdir): + if not self.outdir.is_dir(): with progress_message(__('making output directory')): - ensuredir(outdir) + ensuredir(self.outdir) # the config file itself can be an extension if self.config.setup: @@ -327,8 +326,8 @@ def _init_i18n(self) -> None: logger.info(__('not available for built-in messages')) def _init_env(self, freshenv: bool) -> BuildEnvironment: - filename = path.join(self.doctreedir, ENV_PICKLE_FILENAME) - if freshenv or not os.path.exists(filename): + filename = self.doctreedir / ENV_PICKLE_FILENAME + if freshenv or not filename.exists(): return self._create_fresh_env() else: return self._load_existing_env(filename) @@ -339,12 +338,12 @@ def _create_fresh_env(self) -> BuildEnvironment: return env @progress_message(__('loading pickled environment')) - def _load_existing_env(self, filename: str) -> BuildEnvironment: + def _load_existing_env(self, filename: Path) -> BuildEnvironment: try: with open(filename, 'rb') as f: env = pickle.load(f) - env.setup(self) - self._fresh_env_used = False + env.setup(self) + self._fresh_env_used = False except Exception as err: logger.info(__('failed: %s'), err) env = self._create_fresh_env() @@ -383,8 +382,8 @@ def build(self, force_all: bool = False, filenames: list[str] | None = None) -> self.events.emit('build-finished', None) except Exception as err: # delete the saved env to force a fresh build next time - envfile = path.join(self.doctreedir, ENV_PICKLE_FILENAME) - if path.isfile(envfile): + envfile = self.doctreedir / ENV_PICKLE_FILENAME + if envfile.is_file(): os.unlink(envfile) self.events.emit('build-finished', err) raise diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 21b73f873a0..1420417380d 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -3,11 +3,11 @@ from __future__ import annotations import codecs +import os.path import pickle import re import time from contextlib import nullcontext -from os import path from typing import TYPE_CHECKING, Any, Literal, final from docutils import nodes @@ -48,6 +48,7 @@ from sphinx.application import Sphinx from sphinx.config import Config from sphinx.events import EventManager + from sphinx.util._pathlib import _StrPath from sphinx.util.tags import Tags @@ -94,10 +95,10 @@ class Builder: supported_data_uri_images: bool = False def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: - self.srcdir = app.srcdir - self.confdir = app.confdir - self.outdir = app.outdir - self.doctreedir = app.doctreedir + self.srcdir: _StrPath = app.srcdir + self.confdir: _StrPath = app.confdir + self.outdir: _StrPath = app.outdir + self.doctreedir: _StrPath = app.doctreedir ensuredir(self.doctreedir) self.app: Sphinx = app @@ -203,7 +204,7 @@ def post_process_images(self, doctree: Node) -> None: image_uri = images.get_original_image_uri(node['uri']) if mimetypes: logger.warning( - __('a suitable image for %s builder not found: ' '%s (%s)'), + __('a suitable image for %s builder not found: %s (%s)'), self.name, mimetypes, image_uri, @@ -232,7 +233,7 @@ def compile_catalogs(self, catalogs: set[CatalogInfo], message: str) -> None: return def cat2relpath(cat: CatalogInfo) -> str: - return relpath(cat.mo_path, self.env.srcdir).replace(path.sep, SEP) + return relpath(cat.mo_path, self.env.srcdir).replace(os.path.sep, SEP) logger.info(bold(__('building [mo]: ')) + message) for catalog in status_iterator( @@ -259,7 +260,7 @@ def compile_all_catalogs(self) -> None: def compile_specific_catalogs(self, specified_files: list[str]) -> None: def to_domain(fpath: str) -> str | None: - docname = self.env.path2doc(path.abspath(fpath)) + docname = self.env.path2doc(os.path.abspath(fpath)) if docname: return docname_to_domain(docname, self.config.gettext_compact) else: @@ -306,9 +307,9 @@ def build_specific(self, filenames: list[str]) -> None: docnames: list[str] = [] for filename in filenames: - filename = path.normpath(path.abspath(filename)) + filename = os.path.normpath(os.path.abspath(filename)) - if not path.isfile(filename): + if not os.path.isfile(filename): logger.warning( __('file %r given on command line does not exist, '), filename ) @@ -399,7 +400,7 @@ def build( with ( progress_message(__('pickling environment')), - open(path.join(self.doctreedir, ENV_PICKLE_FILENAME), 'wb') as f, + open(os.path.join(self.doctreedir, ENV_PICKLE_FILENAME), 'wb') as f, ): pickle.dump(self.env, f, pickle.HIGHEST_PROTOCOL) @@ -607,8 +608,8 @@ def read_doc(self, docname: str, *, _cache: bool = True) -> None: self.env.prepare_settings(docname) # Add confdir/docutils.conf to dependencies list if exists - docutilsconf = path.join(self.confdir, 'docutils.conf') - if path.isfile(docutilsconf): + docutilsconf = os.path.join(self.confdir, 'docutils.conf') + if os.path.isfile(docutilsconf): self.env.note_dependency(docutilsconf) filename = str(self.env.doc2path(docname)) @@ -659,8 +660,8 @@ def write_doctree( doctree.settings.env = None doctree.settings.record_dependencies = None - doctree_filename = path.join(self.doctreedir, docname + '.doctree') - ensuredir(path.dirname(doctree_filename)) + doctree_filename = os.path.join(self.doctreedir, docname + '.doctree') + ensuredir(os.path.dirname(doctree_filename)) with open(doctree_filename, 'wb') as f: pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL) diff --git a/sphinx/builders/_epub_base.py b/sphinx/builders/_epub_base.py index ffab73634ae..32fb0e892c6 100644 --- a/sphinx/builders/_epub_base.py +++ b/sphinx/builders/_epub_base.py @@ -4,9 +4,9 @@ import html import os +import os.path import re import time -from os import path from typing import TYPE_CHECKING, Any, NamedTuple from urllib.parse import quote from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile @@ -235,7 +235,7 @@ def get_toc(self) -> None: self.config.root_doc, self, prune_toctrees=False, includehidden=True ) self.refnodes = self.get_refnodes(doctree, []) - master_dir = path.dirname(self.config.root_doc) + master_dir = os.path.dirname(self.config.root_doc) if master_dir: master_dir += '/' # XXX or os.sep? for item in self.refnodes: @@ -409,7 +409,7 @@ def fix_genindex(self, tree: list[tuple[str, list[tuple[str, Any]]]]) -> None: def is_vector_graphics(self, filename: str) -> bool: """Does the filename extension indicate a vector graphic format?""" - ext = path.splitext(filename)[-1] + ext = os.path.splitext(filename)[-1] return ext in VECTOR_GRAPHICS_EXTENSIONS def copy_image_files_pil(self) -> None: @@ -417,7 +417,7 @@ def copy_image_files_pil(self) -> None: The method tries to read and write the files with Pillow, converting the format and resizing the image if necessary/possible. """ - ensuredir(path.join(self.outdir, self.imagedir)) + ensuredir(os.path.join(self.outdir, self.imagedir)) for src in status_iterator( self.images, __('copying images... '), @@ -427,12 +427,12 @@ def copy_image_files_pil(self) -> None: ): dest = self.images[src] try: - img = Image.open(path.join(self.srcdir, src)) + img = Image.open(os.path.join(self.srcdir, src)) except OSError: if not self.is_vector_graphics(src): logger.warning( __('cannot read image file %r: copying it instead'), - path.join(self.srcdir, src), + os.path.join(self.srcdir, src), ) try: copyfile( @@ -443,7 +443,7 @@ def copy_image_files_pil(self) -> None: except OSError as err: logger.warning( __('cannot copy image file %r: %s'), - path.join(self.srcdir, src), + os.path.join(self.srcdir, src), err, ) continue @@ -459,11 +459,11 @@ def copy_image_files_pil(self) -> None: nh = round((height * nw) / width) img = img.resize((nw, nh), Image.BICUBIC) try: - img.save(path.join(self.outdir, self.imagedir, dest)) + img.save(os.path.join(self.outdir, self.imagedir, dest)) except OSError as err: logger.warning( __('cannot write image file %r: %s'), - path.join(self.srcdir, src), + os.path.join(self.srcdir, src), err, ) @@ -511,7 +511,7 @@ def build_mimetype(self) -> None: """Write the metainfo file mimetype.""" logger.info(__('writing mimetype file...')) copyfile( - path.join(self.template_dir, 'mimetype'), + os.path.join(self.template_dir, 'mimetype'), self.outdir / 'mimetype', force=True, ) @@ -522,7 +522,7 @@ def build_container(self, outname: str = 'META-INF/container.xml') -> None: outdir = self.outdir / 'META-INF' ensuredir(outdir) copyfile( - path.join(self.template_dir, 'container.xml'), + os.path.join(self.template_dir, 'container.xml'), outdir / 'container.xml', force=True, ) @@ -578,10 +578,10 @@ def build_content(self) -> None: for root, dirs, files in os.walk(self.outdir): dirs.sort() for fn in sorted(files): - filename = relpath(path.join(root, fn), self.outdir) + filename = relpath(os.path.join(root, fn), self.outdir) if filename in self.ignored_files: continue - ext = path.splitext(filename)[-1] + ext = os.path.splitext(filename)[-1] if ext not in self.media_types: # we always have JS and potentially OpenSearch files, don't # always warn about them @@ -636,7 +636,7 @@ def build_content(self) -> None: spine = Spine(html.escape(self.make_id(self.coverpage_name)), True) metadata['spines'].insert(0, spine) if self.coverpage_name not in self.files: - ext = path.splitext(self.coverpage_name)[-1] + ext = os.path.splitext(self.coverpage_name)[-1] self.files.append(self.coverpage_name) item = ManifestItem( html.escape(self.coverpage_name), @@ -645,7 +645,9 @@ def build_content(self) -> None: ) metadata['manifest_items'].append(item) ctx = {'image': html.escape(image), 'title': self.config.project} - self.handle_page(path.splitext(self.coverpage_name)[0], ctx, html_tmpl) + self.handle_page( + os.path.splitext(self.coverpage_name)[0], ctx, html_tmpl + ) spinefiles.add(self.coverpage_name) auto_add_cover = True @@ -681,7 +683,7 @@ def build_content(self) -> None: # write the project file copy_asset_file( - path.join(self.template_dir, 'content.opf.jinja'), + os.path.join(self.template_dir, 'content.opf.jinja'), self.outdir, context=metadata, force=True, @@ -775,7 +777,7 @@ def build_toc(self) -> None: level = max(item['level'] for item in self.refnodes) level = min(level, self.config.epub_tocdepth) copy_asset_file( - path.join(self.template_dir, 'toc.ncx.jinja'), + os.path.join(self.template_dir, 'toc.ncx.jinja'), self.outdir, context=self.toc_metadata(level, navpoints), force=True, @@ -789,10 +791,10 @@ def build_epub(self) -> None: """ outname = self.config.epub_basename + '.epub' logger.info(__('writing %s file...'), outname) - epub_filename = path.join(self.outdir, outname) + epub_filename = os.path.join(self.outdir, outname) with ZipFile(epub_filename, 'w', ZIP_DEFLATED) as epub: - epub.write(path.join(self.outdir, 'mimetype'), 'mimetype', ZIP_STORED) + epub.write(os.path.join(self.outdir, 'mimetype'), 'mimetype', ZIP_STORED) for filename in ('META-INF/container.xml', 'content.opf', 'toc.ncx'): - epub.write(path.join(self.outdir, filename), filename, ZIP_DEFLATED) + epub.write(os.path.join(self.outdir, filename), filename, ZIP_DEFLATED) for filename in self.files: - epub.write(path.join(self.outdir, filename), filename, ZIP_DEFLATED) + epub.write(os.path.join(self.outdir, filename), filename, ZIP_DEFLATED) diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py index cd40faf3ab7..918ed3bcaee 100644 --- a/sphinx/builders/changes.py +++ b/sphinx/builders/changes.py @@ -3,7 +3,7 @@ from __future__ import annotations import html -from os import path +import os.path from typing import TYPE_CHECKING from sphinx import package_dir @@ -108,9 +108,9 @@ def write_documents(self, _docnames: Set[str]) -> None: 'show_copyright': self.config.html_show_copyright, 'show_sphinx': self.config.html_show_sphinx, } - with open(path.join(self.outdir, 'index.html'), 'w', encoding='utf8') as f: + with open(os.path.join(self.outdir, 'index.html'), 'w', encoding='utf8') as f: f.write(self.templates.render('changes/frameset.html', ctx)) - with open(path.join(self.outdir, 'changes.html'), 'w', encoding='utf8') as f: + with open(os.path.join(self.outdir, 'changes.html'), 'w', encoding='utf8') as f: f.write(self.templates.render('changes/versionchanges.html', ctx)) hltext = [ @@ -140,8 +140,8 @@ def hl(no: int, line: str) -> str: __('could not read %r for changelog creation'), docname ) continue - targetfn = path.join(self.outdir, 'rst', os_path(docname)) + '.html' - ensuredir(path.dirname(targetfn)) + targetfn = os.path.join(self.outdir, 'rst', os_path(docname)) + '.html' + ensuredir(os.path.dirname(targetfn)) with open(targetfn, 'w', encoding='utf-8') as f: text = ''.join(hl(i + 1, line) for (i, line) in enumerate(lines)) ctx = { @@ -153,14 +153,16 @@ def hl(no: int, line: str) -> str: 'theme_' + key: val for (key, val) in self.theme.get_options({}).items() } copy_asset_file( - path.join(package_dir, 'themes', 'default', 'static', 'default.css.jinja'), + os.path.join( + package_dir, 'themes', 'default', 'static', 'default.css.jinja' + ), self.outdir, context=themectx, renderer=self.templates, force=True, ) copy_asset_file( - path.join(package_dir, 'themes', 'basic', 'static', 'basic.css'), + os.path.join(package_dir, 'themes', 'basic', 'static', 'basic.css'), self.outdir / 'basic.css', force=True, ) diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py index cbd50bd229e..44b2cb10e16 100644 --- a/sphinx/builders/epub3.py +++ b/sphinx/builders/epub3.py @@ -7,9 +7,9 @@ import html import os +import os.path import re import time -from os import path from typing import TYPE_CHECKING, Any, NamedTuple from sphinx import package_dir @@ -83,7 +83,7 @@ class Epub3Builder(_epub_base.EpubBuilder): epilog = __('The ePub file is in %(outdir)s.') supported_remote_images = False - template_dir = path.join(package_dir, 'templates', 'epub3') + template_dir = os.path.join(package_dir, 'templates', 'epub3') doctype = DOCTYPE html_tag = HTML_TAG use_meta_charset = True @@ -199,7 +199,7 @@ def build_navigation_doc(self) -> None: refnodes = self.refnodes navlist = self.build_navlist(refnodes) copy_asset_file( - path.join(self.template_dir, 'nav.xhtml.jinja'), + os.path.join(self.template_dir, 'nav.xhtml.jinja'), self.outdir, context=self.navigation_doc_metadata(navlist), force=True, diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index eacb333fbe9..9a1001fceaf 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -5,12 +5,12 @@ import contextlib import html import os +import os.path import posixpath import re import shutil import sys import warnings -from os import path from pathlib import Path from typing import TYPE_CHECKING, Any from urllib.parse import quote @@ -20,7 +20,6 @@ from docutils.core import Publisher from docutils.frontend import OptionParser from docutils.io import DocTreeInput, StringOutput -from docutils.utils import relative_path from sphinx import __display_version__, package_dir from sphinx import version_info as sphinx_version @@ -56,6 +55,7 @@ from sphinx.util.osutil import ( SEP, _last_modified_time, + _relative_path, copyfile, ensuredir, relative_uri, @@ -496,9 +496,9 @@ def prepare_writing(self, docnames: Set[str]) -> None: favicon = self.config.html_favicon or '' if not is_url(logo): - logo = path.basename(logo) + logo = os.path.basename(logo) if not is_url(favicon): - favicon = path.basename(favicon) + favicon = os.path.basename(favicon) self.relations = self.env.collect_relations() @@ -795,7 +795,7 @@ def copy_image_files(self) -> None: def copy_download_files(self) -> None: def to_relpath(f: str) -> str: - return relative_path(self.srcdir, f) + return _relative_path(Path(f), self.srcdir).as_posix() # copy downloadable files if self.env.dlfiles: diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py index db7adc5b0d7..09cffaaf92c 100644 --- a/sphinx/builders/latex/__init__.py +++ b/sphinx/builders/latex/__init__.py @@ -3,8 +3,8 @@ from __future__ import annotations import os +import os.path import warnings -from os import path from typing import TYPE_CHECKING, Any from docutils.frontend import OptionParser @@ -166,10 +166,7 @@ def init_document_data(self) -> None: docname = entry[0] if docname not in self.env.all_docs: logger.warning( - __( - '"latex_documents" config value references unknown ' - 'document %s' - ), + __('"latex_documents" config value references unknown document %s'), docname, ) continue @@ -202,7 +199,7 @@ def init_context(self) -> None: self.context['date'] = format_date(today_fmt, language=self.config.language) if self.config.latex_logo: - self.context['logofilename'] = path.basename(self.config.latex_logo) + self.context['logofilename'] = os.path.basename(self.config.latex_logo) # for compatibilities self.context['indexname'] = _('Index') @@ -279,7 +276,7 @@ def init_multilingual(self) -> None: def write_stylesheet(self) -> None: highlighter = highlighting.PygmentsBridge('latex', self.config.pygments_style) - stylesheet = path.join(self.outdir, 'sphinxhighlight.sty') + stylesheet = os.path.join(self.outdir, 'sphinxhighlight.sty') with open(stylesheet, 'w', encoding='utf-8') as f: f.write('\\NeedsTeXFormat{LaTeX2e}[1995/12/01]\n') f.write( @@ -320,7 +317,7 @@ def write_documents(self, _docnames: Set[str]) -> None: if len(entry) > 5: toctree_only = entry[5] destination = SphinxFileOutput( - destination_path=path.join(self.outdir, targetname), + destination_path=os.path.join(self.outdir, targetname), encoding='utf-8', overwrite_if_changed=True, ) @@ -446,11 +443,11 @@ def copy_support_files(self) -> None: 'xindy_lang_option': xindy_lang_option, 'xindy_cyrillic': xindy_cyrillic, } - staticdirname = path.join(package_dir, 'texinputs') + staticdirname = os.path.join(package_dir, 'texinputs') for filename in os.listdir(staticdirname): if not filename.startswith('.'): copy_asset_file( - path.join(staticdirname, filename), + os.path.join(staticdirname, filename), self.outdir, context=context, force=True, @@ -458,9 +455,9 @@ def copy_support_files(self) -> None: # use pre-1.6.x Makefile for make latexpdf on Windows if os.name == 'nt': - staticdirname = path.join(package_dir, 'texinputs_win') + staticdirname = os.path.join(package_dir, 'texinputs_win') copy_asset_file( - path.join(staticdirname, 'Makefile.jinja'), + os.path.join(staticdirname, 'Makefile.jinja'), self.outdir, context=context, force=True, @@ -498,11 +495,11 @@ def copy_image_files(self) -> None: except Exception as err: logger.warning( __('cannot copy image file %r: %s'), - path.join(self.srcdir, src), + os.path.join(self.srcdir, src), err, ) if self.config.latex_logo: - if not path.isfile(path.join(self.confdir, self.config.latex_logo)): + if not os.path.isfile(os.path.join(self.confdir, self.config.latex_logo)): raise SphinxError( __('logo file %r does not exist') % self.config.latex_logo ) @@ -525,7 +522,7 @@ def write_message_catalog(self) -> None: if self.context['babel'] or self.context['polyglossia']: context['addtocaptions'] = r'\addto\captions%s' % self.babel.get_language() - filename = path.join( + filename = os.path.join( package_dir, 'templates', 'latex', 'sphinxmessages.sty.jinja' ) copy_asset_file( diff --git a/sphinx/builders/latex/theming.py b/sphinx/builders/latex/theming.py index 50f03652fc8..de0540127df 100644 --- a/sphinx/builders/latex/theming.py +++ b/sphinx/builders/latex/theming.py @@ -3,7 +3,7 @@ from __future__ import annotations import configparser -from os import path +import os.path from typing import TYPE_CHECKING from sphinx.errors import ThemeError @@ -77,7 +77,7 @@ class UserTheme(Theme): def __init__(self, name: str, filename: str) -> None: super().__init__(name) self.config = configparser.RawConfigParser() - self.config.read(path.join(filename), encoding='utf-8') + self.config.read(os.path.join(filename), encoding='utf-8') for key in self.REQUIRED_CONFIG_KEYS: try: @@ -104,7 +104,7 @@ class ThemeFactory: def __init__(self, app: Sphinx) -> None: self.themes: dict[str, Theme] = {} self.theme_paths = [ - path.join(app.srcdir, p) for p in app.config.latex_theme_path + os.path.join(app.srcdir, p) for p in app.config.latex_theme_path ] self.config = app.config self.load_builtin_themes(app.config) @@ -127,8 +127,8 @@ def get(self, name: str) -> Theme: def find_user_theme(self, name: str) -> Theme | None: """Find a theme named as *name* from latex_theme_path.""" for theme_path in self.theme_paths: - config_path = path.join(theme_path, name, 'theme.conf') - if path.isfile(config_path): + config_path = os.path.join(theme_path, name, 'theme.conf') + if os.path.isfile(config_path): try: return UserTheme(name, config_path) except ThemeError as exc: diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index fcf994e8e03..82fc0acc4bc 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -4,12 +4,12 @@ import contextlib import json +import os.path import re import socket import time from enum import StrEnum from html.parser import HTMLParser -from os import path from queue import PriorityQueue, Queue from threading import Thread from typing import TYPE_CHECKING, NamedTuple, cast @@ -85,8 +85,8 @@ def finish(self) -> None: checker = HyperlinkAvailabilityChecker(self.config) logger.info('') - output_text = path.join(self.outdir, 'output.txt') - output_json = path.join(self.outdir, 'output.json') + output_text = os.path.join(self.outdir, 'output.txt') + output_json = os.path.join(self.outdir, 'output.json') with ( open(output_text, 'w', encoding='utf-8') as self.txt_outfile, open(output_json, 'w', encoding='utf-8') as self.json_outfile, @@ -450,8 +450,8 @@ def _check(self, docname: str, uri: str, hyperlink: Hyperlink) -> _URIProperties # Non-supported URI schemes (ex. ftp) return _Status.UNCHECKED, '', 0 - src_dir = path.dirname(hyperlink.docpath) - if path.exists(path.join(src_dir, uri)): + src_dir = os.path.dirname(hyperlink.docpath) + if os.path.exists(os.path.join(src_dir, uri)): return _Status.WORKING, '', 0 return _Status.BROKEN, '', 0 diff --git a/sphinx/builders/manpage.py b/sphinx/builders/manpage.py index 2e24486d174..98135d9801c 100644 --- a/sphinx/builders/manpage.py +++ b/sphinx/builders/manpage.py @@ -2,8 +2,8 @@ from __future__ import annotations +import os.path import warnings -from os import path from typing import TYPE_CHECKING, Any from docutils.frontend import OptionParser @@ -44,10 +44,7 @@ class ManualPageBuilder(Builder): def init(self) -> None: if not self.config.man_pages: logger.warning( - __( - 'no "man_pages" config value found; no manual pages ' - 'will be written' - ) + __('no "man_pages" config value found; no manual pages will be written') ) def get_outdated_docs(self) -> str | list[str]: @@ -73,7 +70,7 @@ def write_documents(self, _docnames: Set[str]) -> None: docname, name, description, authors, section = info if docname not in self.env.all_docs: logger.warning( - __('"man_pages" config value references unknown ' 'document %s'), + __('"man_pages" config value references unknown document %s'), docname, ) continue @@ -90,14 +87,15 @@ def write_documents(self, _docnames: Set[str]) -> None: if self.config.man_make_section_directory: dirname = 'man%s' % section - ensuredir(path.join(self.outdir, dirname)) + ensuredir(os.path.join(self.outdir, dirname)) targetname = f'{dirname}/{name}.{section}' else: targetname = f'{name}.{section}' logger.info(darkgreen(targetname) + ' { ') destination = FileOutput( - destination_path=path.join(self.outdir, targetname), encoding='utf-8' + destination_path=os.path.join(self.outdir, targetname), + encoding='utf-8', ) tree = self.env.get_doctree(docname) diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py index 2d428bb736b..7143e68c4f0 100644 --- a/sphinx/builders/texinfo.py +++ b/sphinx/builders/texinfo.py @@ -3,8 +3,8 @@ from __future__ import annotations import os +import os.path import warnings -from os import path from typing import TYPE_CHECKING, Any from docutils import nodes @@ -109,7 +109,8 @@ def write_documents(self, _docnames: Set[str]) -> None: if len(entry) > 7: toctree_only = entry[7] destination = FileOutput( - destination_path=path.join(self.outdir, targetname), encoding='utf-8' + destination_path=os.path.join(self.outdir, targetname), + encoding='utf-8', ) with progress_message(__('processing %s') % targetname, nonl=False): appendices = self.config.texinfo_appendices or [] @@ -216,7 +217,7 @@ def copy_image_files(self, targetname: str) -> None: except Exception as err: logger.warning( __('cannot copy image file %r: %s'), - path.join(self.srcdir, src), + os.path.join(self.srcdir, src), err, ) diff --git a/sphinx/builders/text.py b/sphinx/builders/text.py index 243e790124f..03e22d2c73e 100644 --- a/sphinx/builders/text.py +++ b/sphinx/builders/text.py @@ -2,7 +2,7 @@ from __future__ import annotations -from os import path +import os.path from typing import TYPE_CHECKING from docutils.io import StringOutput @@ -48,7 +48,7 @@ def get_outdated_docs(self) -> Iterator[str]: if docname not in self.env.all_docs: yield docname continue - targetname = path.join(self.outdir, docname + self.out_suffix) + targetname = os.path.join(self.outdir, docname + self.out_suffix) try: targetmtime = _last_modified_time(targetname) except Exception: @@ -72,8 +72,8 @@ def write_doc(self, docname: str, doctree: nodes.document) -> None: self.secnumbers = self.env.toc_secnumbers.get(docname, {}) destination = StringOutput(encoding='utf-8') self.writer.write(doctree, destination) - outfilename = path.join(self.outdir, os_path(docname) + self.out_suffix) - ensuredir(path.dirname(outfilename)) + outfilename = os.path.join(self.outdir, os_path(docname) + self.out_suffix) + ensuredir(os.path.dirname(outfilename)) try: with open(outfilename, 'w', encoding='utf-8') as f: f.write(self.writer.output) diff --git a/sphinx/builders/xml.py b/sphinx/builders/xml.py index dbb78772a00..173f1ee5c53 100644 --- a/sphinx/builders/xml.py +++ b/sphinx/builders/xml.py @@ -2,7 +2,7 @@ from __future__ import annotations -from os import path +import os.path from typing import TYPE_CHECKING from docutils import nodes @@ -52,7 +52,7 @@ def get_outdated_docs(self) -> Iterator[str]: if docname not in self.env.all_docs: yield docname continue - targetname = path.join(self.outdir, docname + self.out_suffix) + targetname = os.path.join(self.outdir, docname + self.out_suffix) try: targetmtime = _last_modified_time(targetname) except Exception: @@ -88,8 +88,8 @@ def write_doc(self, docname: str, doctree: nodes.document) -> None: value[i] = list(val) destination = StringOutput(encoding='utf-8') self.writer.write(doctree, destination) - outfilename = path.join(self.outdir, os_path(docname) + self.out_suffix) - ensuredir(path.dirname(outfilename)) + outfilename = os.path.join(self.outdir, os_path(docname) + self.out_suffix) + ensuredir(os.path.dirname(outfilename)) try: with open(outfilename, 'w', encoding='utf-8') as f: f.write(self.writer.output) diff --git a/sphinx/cmd/build.py b/sphinx/cmd/build.py index 3c3d8e4b4de..37f3f26c453 100644 --- a/sphinx/cmd/build.py +++ b/sphinx/cmd/build.py @@ -7,11 +7,10 @@ import contextlib import locale import multiprocessing -import os import pdb # NoQA: T100 import sys import traceback -from os import path +from pathlib import Path from typing import TYPE_CHECKING, Any, TextIO from docutils.utils import SystemMessage @@ -22,6 +21,7 @@ from sphinx.errors import SphinxError, SphinxParallelError from sphinx.locale import __ from sphinx.util._io import TeeStripANSI +from sphinx.util._pathlib import _StrPath from sphinx.util.console import color_terminal, nocolor, red, terminal_safe from sphinx.util.docutils import docutils_namespace, patch_docutils from sphinx.util.exceptions import format_exception_cut_frames, save_traceback @@ -217,14 +217,14 @@ def get_parser() -> argparse.ArgumentParser: '-a', action='store_true', dest='force_all', - help=__('write all files (default: only write new and ' 'changed files)'), + help=__('write all files (default: only write new and changed files)'), ) group.add_argument( '--fresh-env', '-E', action='store_true', dest='freshenv', - help=__("don't use a saved environment, always read " 'all files'), + help=__("don't use a saved environment, always read all files"), ) group = parser.add_argument_group(__('path options')) @@ -243,9 +243,7 @@ def get_parser() -> argparse.ArgumentParser: '-c', metavar='PATH', dest='confdir', - help=__( - 'directory for the configuration file (conf.py) ' '(default: SOURCE_DIR)' - ), + help=__('directory for the configuration file (conf.py) (default: SOURCE_DIR)'), ) group = parser.add_argument_group('build configuration options') @@ -392,10 +390,10 @@ def _parse_confdir(noconfig: bool, confdir: str, sourcedir: str) -> str | None: return confdir -def _parse_doctreedir(doctreedir: str, outputdir: str) -> str: +def _parse_doctreedir(doctreedir: str, outputdir: str) -> _StrPath: if doctreedir: - return doctreedir - return os.path.join(outputdir, '.doctrees') + return _StrPath(doctreedir) + return _StrPath(outputdir, '.doctrees') def _validate_filenames( @@ -431,12 +429,12 @@ def _parse_logging( warnfp = None if warning and warnfile: try: - warnfile = path.abspath(warnfile) - ensuredir(path.dirname(warnfile)) + warn_file = Path(warnfile).resolve() + ensuredir(warn_file.parent) # the caller is responsible for closing this file descriptor - warnfp = open(warnfile, 'w', encoding='utf-8') # NoQA: SIM115 + warnfp = open(warn_file, 'w', encoding='utf-8') # NoQA: SIM115 except Exception as exc: - parser.error(__('cannot open warning file %r: %s') % (warnfile, exc)) + parser.error(__("cannot open warning file '%s': %s") % (warn_file, exc)) warning = TeeStripANSI(warning, warnfp) # type: ignore[assignment] error = warning diff --git a/sphinx/cmd/make_mode.py b/sphinx/cmd/make_mode.py index ac22ba99822..5966b628a3e 100644 --- a/sphinx/cmd/make_mode.py +++ b/sphinx/cmd/make_mode.py @@ -13,11 +13,11 @@ import subprocess import sys from contextlib import chdir -from os import path from typing import TYPE_CHECKING import sphinx from sphinx.cmd.build import build_main +from sphinx.util._pathlib import _StrPath from sphinx.util.console import blue, bold, color_terminal, nocolor from sphinx.util.osutil import rmtree @@ -49,7 +49,7 @@ ( '', 'doctest', - 'to run all doctests embedded in the documentation ' '(if enabled)', + 'to run all doctests embedded in the documentation (if enabled)', ), ('', 'coverage', 'to run coverage check of the documentation (if enabled)'), ('', 'clean', 'to remove everything in the build directory'), @@ -57,30 +57,36 @@ class Make: - def __init__(self, *, source_dir: str, build_dir: str, opts: Sequence[str]) -> None: - self.source_dir = source_dir - self.build_dir = build_dir + def __init__( + self, + *, + source_dir: str | os.PathLike[str], + build_dir: str | os.PathLike[str], + opts: Sequence[str], + ) -> None: + self.source_dir = _StrPath(source_dir) + self.build_dir = _StrPath(build_dir) self.opts = [*opts] - def build_dir_join(self, *comps: str) -> str: - return path.join(self.build_dir, *comps) + def build_dir_join(self, *comps: str | os.PathLike[str]) -> _StrPath: + return self.build_dir.joinpath(*comps) def build_clean(self) -> int: - source_dir = path.abspath(self.source_dir) - build_dir = path.abspath(self.build_dir) - if not path.exists(self.build_dir): + source_dir = self.source_dir.resolve() + build_dir = self.build_dir.resolve() + if not self.build_dir.exists(): return 0 - elif not path.isdir(self.build_dir): - print('Error: %r is not a directory!' % self.build_dir) + elif not self.build_dir.is_dir(): + print("Error: '%s' is not a directory!" % self.build_dir) return 1 elif source_dir == build_dir: - print('Error: %r is same as source directory!' % self.build_dir) + print("Error: '%s' is same as source directory!" % self.build_dir) return 1 - elif path.commonpath([source_dir, build_dir]) == build_dir: - print('Error: %r directory contains source directory!' % self.build_dir) + elif source_dir.is_relative_to(build_dir): + print("Error: '%s' directory contains source directory!" % self.build_dir) return 1 - print('Removing everything under %r...' % self.build_dir) - for item in os.listdir(self.build_dir): + print("Removing everything under '%s'..." % self.build_dir) + for item in self.build_dir.iterdir(): rmtree(self.build_dir_join(item)) return 0 @@ -179,7 +185,9 @@ def build_gettext(self) -> int: return 1 return 0 - def run_generic_build(self, builder: str, doctreedir: str | None = None) -> int: + def run_generic_build( + self, builder: str, doctreedir: str | os.PathLike[str] | None = None + ) -> int: # compatibility with old Makefile paper_size = os.getenv('PAPER', '') if paper_size in {'a4', 'letter'}: @@ -191,9 +199,9 @@ def run_generic_build(self, builder: str, doctreedir: str | None = None) -> int: '--builder', builder, '--doctree-dir', - doctreedir, - self.source_dir, - self.build_dir_join(builder), + str(doctreedir), + str(self.source_dir), + str(self.build_dir_join(builder)), ] return build_main(args + self.opts) diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py index 1176dc14b40..214391ecf22 100644 --- a/sphinx/cmd/quickstart.py +++ b/sphinx/cmd/quickstart.py @@ -5,9 +5,9 @@ import argparse import locale import os +import os.path import sys import time -from os import path from typing import TYPE_CHECKING, Any # try to import readline, unix specific enhancement @@ -89,8 +89,8 @@ class ValidationError(Exception): def is_path(x: str) -> str: - x = path.expanduser(x) - if not path.isdir(x): + x = os.path.expanduser(x) + if not os.path.isdir(x): raise ValidationError(__('Please enter a valid path name.')) return x @@ -179,12 +179,14 @@ def _has_custom_template(self, template_name: str) -> bool: Note: Please don't use this function from extensions. It will be removed in the future without deprecation period. """ - template = path.join(self.templatedir, path.basename(template_name)) - return bool(self.templatedir) and path.exists(template) + template = os.path.join(self.templatedir, os.path.basename(template_name)) + return bool(self.templatedir) and os.path.exists(template) def render(self, template_name: str, context: dict[str, Any]) -> str: if self._has_custom_template(template_name): - custom_template = path.join(self.templatedir, path.basename(template_name)) + custom_template = os.path.join( + self.templatedir, os.path.basename(template_name) + ) return self.render_from_file(custom_template, context) else: return super().render(template_name, context) @@ -228,8 +230,8 @@ def ask_user(d: dict[str, Any]) -> None: print(__('Enter the root path for documentation.')) d['path'] = do_prompt(__('Root path for the documentation'), '.', is_path) - while path.isfile(path.join(d['path'], 'conf.py')) or path.isfile( - path.join(d['path'], 'source', 'conf.py') + while os.path.isfile(os.path.join(d['path'], 'conf.py')) or os.path.isfile( + os.path.join(d['path'], 'source', 'conf.py') ): print() print( @@ -341,8 +343,8 @@ def ask_user(d: dict[str, Any]) -> None: ) while ( - path.isfile(path.join(d['path'], d['master'] + d['suffix'])) - or path.isfile(path.join(d['path'], 'source', d['master'] + d['suffix'])) + os.path.isfile(os.path.join(d['path'], d['master'] + d['suffix'])) + or os.path.isfile(os.path.join(d['path'], 'source', d['master'] + d['suffix'])) ): # fmt: skip print() print( @@ -424,14 +426,14 @@ def generate( d['path'] = os.path.abspath(d['path']) ensuredir(d['path']) - srcdir = path.join(d['path'], 'source') if d['sep'] else d['path'] + srcdir = os.path.join(d['path'], 'source') if d['sep'] else d['path'] ensuredir(srcdir) if d['sep']: - builddir = path.join(d['path'], 'build') + builddir = os.path.join(d['path'], 'build') d['exclude_patterns'] = '' else: - builddir = path.join(srcdir, d['dot'] + 'build') + builddir = os.path.join(srcdir, d['dot'] + 'build') exclude_patterns = map( repr, [ @@ -442,11 +444,11 @@ def generate( ) d['exclude_patterns'] = ', '.join(exclude_patterns) ensuredir(builddir) - ensuredir(path.join(srcdir, d['dot'] + 'templates')) - ensuredir(path.join(srcdir, d['dot'] + 'static')) + ensuredir(os.path.join(srcdir, d['dot'] + 'templates')) + ensuredir(os.path.join(srcdir, d['dot'] + 'static')) def write_file(fpath: str, content: str, newline: str | None = None) -> None: - if overwrite or not path.isfile(fpath): + if overwrite or not os.path.isfile(fpath): if 'quiet' not in d: print(__('Creating file %s.') % fpath) with open(fpath, 'w', encoding='utf-8', newline=newline) as f: @@ -456,16 +458,16 @@ def write_file(fpath: str, content: str, newline: str | None = None) -> None: print(__('File %s already exists, skipping.') % fpath) conf_path = os.path.join(templatedir, 'conf.py.jinja') if templatedir else None - if not conf_path or not path.isfile(conf_path): + if not conf_path or not os.path.isfile(conf_path): conf_path = os.path.join( package_dir, 'templates', 'quickstart', 'conf.py.jinja' ) with open(conf_path, encoding='utf-8') as f: conf_text = f.read() - write_file(path.join(srcdir, 'conf.py'), template.render_string(conf_text, d)) + write_file(os.path.join(srcdir, 'conf.py'), template.render_string(conf_text, d)) - masterfile = path.join(srcdir, d['master'] + d['suffix']) + masterfile = os.path.join(srcdir, d['master'] + d['suffix']) if template._has_custom_template('quickstart/master_doc.rst.jinja'): write_file(masterfile, template.render('quickstart/master_doc.rst.jinja', d)) else: @@ -479,7 +481,7 @@ def write_file(fpath: str, content: str, newline: str | None = None) -> None: d['rbuilddir'] = 'build' if d['sep'] else d['dot'] + 'build' # use binary mode, to avoid writing \r\n on Windows write_file( - path.join(d['path'], 'Makefile'), + os.path.join(d['path'], 'Makefile'), template.render(makefile_template, d), '\n', ) @@ -488,7 +490,7 @@ def write_file(fpath: str, content: str, newline: str | None = None) -> None: d['rsrcdir'] = 'source' if d['sep'] else '.' d['rbuilddir'] = 'build' if d['sep'] else d['dot'] + 'build' write_file( - path.join(d['path'], 'make.bat'), + os.path.join(d['path'], 'make.bat'), template.render(batchfile_template, d), '\r\n', ) @@ -507,7 +509,7 @@ def write_file(fpath: str, content: str, newline: str | None = None) -> None: end='', ) if d['makefile'] or d['batchfile']: - print(__('Use the Makefile to build the docs, like so:\n' ' make builder')) + print(__('Use the Makefile to build the docs, like so:\n make builder')) else: print( __( @@ -527,9 +529,9 @@ def write_file(fpath: str, content: str, newline: str | None = None) -> None: def valid_dir(d: dict[str, Any]) -> bool: dir = d['path'] - if not path.exists(dir): + if not os.path.exists(dir): return True - if not path.isdir(dir): + if not os.path.isdir(dir): return False if {'Makefile', 'make.bat'} & set(os.listdir(dir)): @@ -537,9 +539,9 @@ def valid_dir(d: dict[str, Any]) -> bool: if d['sep']: dir = os.path.join('source', dir) - if not path.exists(dir): + if not os.path.exists(dir): return True - if not path.isdir(dir): + if not os.path.isdir(dir): return False reserved_names = [ diff --git a/sphinx/config.py b/sphinx/config.py index 8700ed30054..24b0ba2cd9c 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -7,14 +7,14 @@ import types import warnings from contextlib import chdir -from os import getenv, path +from os import getenv +from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, NamedTuple from sphinx.deprecation import RemovedInSphinx90Warning from sphinx.errors import ConfigError, ExtensionError from sphinx.locale import _, __ from sphinx.util import logging -from sphinx.util.osutil import fs_encoding if TYPE_CHECKING: import os @@ -304,8 +304,8 @@ def overrides(self) -> dict[str, Any]: def read(cls: type[Config], confdir: str | os.PathLike[str], overrides: dict | None = None, tags: Tags | None = None) -> Config: """Create a Config object from configuration file.""" - filename = path.join(confdir, CONFIG_FILENAME) - if not path.isfile(filename): + filename = Path(confdir, CONFIG_FILENAME) + if not filename.is_file(): raise ConfigError(__("config directory doesn't contain a conf.py file (%s)") % confdir) namespace = eval_config_file(filename, tags) @@ -510,18 +510,19 @@ def __setstate__(self, state: dict) -> None: self.__dict__.update(state) -def eval_config_file(filename: str, tags: Tags | None) -> dict[str, Any]: +def eval_config_file(filename: str | os.PathLike[str], tags: Tags | None) -> dict[str, Any]: """Evaluate a config file.""" + filename = Path(filename) + namespace: dict[str, Any] = {} - namespace['__file__'] = filename + namespace['__file__'] = str(filename) namespace['tags'] = tags - with chdir(path.dirname(filename)): + with chdir(filename.parent): # during executing config file, current dir is changed to ``confdir``. try: - with open(filename, 'rb') as f: - code = compile(f.read(), filename.encode(fs_encoding), 'exec') - exec(code, namespace) # NoQA: S102 + code = compile(filename.read_bytes(), filename, 'exec') + exec(code, namespace) # NoQA: S102 except SyntaxError as err: msg = __("There is a syntax error in your configuration file: %s\n") raise ConfigError(msg % err) from err diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 218bc6d5431..181c6f81a07 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -267,7 +267,7 @@ def run(self) -> list[Node]: finally: # Private attributes for ToC generation. Will be modified or removed # without notice. - if self.env.app.config.toc_object_entries: + if self.env.config.toc_object_entries: signode['_toc_parts'] = self._object_hierarchy_parts(signode) signode['_toc_name'] = self._toc_entry_name(signode) else: @@ -288,7 +288,7 @@ def run(self) -> list[Node]: content_node = addnodes.desc_content('', *content_children) node.append(content_node) self.transform_content(content_node) - self.env.app.emit( + self.env.events.emit( 'object-description-transform', self.domain, self.objtype, content_node ) DocFieldTransformer(self).transform_all(content_node) diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index e297ceb4b89..32fd3111a65 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -412,7 +412,7 @@ def _insert_input(include_lines: list[str], source: str) -> None: # Emit the "include-read" event arg = [text] - self.env.app.events.emit('include-read', path, docname, arg) + self.env.events.emit('include-read', path, docname, arg) text = arg[0] # Split back into lines and reattach the two marker lines @@ -424,7 +424,7 @@ def _insert_input(include_lines: list[str], source: str) -> None: return StateMachine.insert_input(self.state_machine, include_lines, source) # Only enable this patch if there are listeners for 'include-read'. - if self.env.app.events.listeners.get('include-read'): + if self.env.events.listeners.get('include-read'): # See https://github.com/python/mypy/issues/2427 for details on the mypy issue self.state_machine.insert_input = _insert_input diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py index 85cff2e6407..ff2989520d8 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from os import path +from pathlib import Path from typing import TYPE_CHECKING, ClassVar, cast from docutils import nodes @@ -16,7 +16,7 @@ from sphinx.util import logging from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import set_source_info -from sphinx.util.osutil import SEP, os_path, relpath +from sphinx.util.osutil import SEP, relpath if TYPE_CHECKING: from sphinx.application import Sphinx @@ -60,8 +60,8 @@ class CSVTable(tables.CSVTable): # type: ignore[misc] def run(self) -> list[Node]: if 'file' in self.options and self.options['file'].startswith((SEP, os.sep)): env = self.state.document.settings.env - filename = self.options['file'] - if path.exists(filename): + filename = Path(self.options['file']) + if filename.exists(): logger.warning( __( '":file:" option for csv-table directive now recognizes ' @@ -71,9 +71,9 @@ def run(self) -> list[Node]: location=(env.docname, self.lineno), ) else: - abspath = path.join(env.srcdir, os_path(self.options['file'][1:])) - docdir = path.dirname(env.doc2path(env.docname)) - self.options['file'] = relpath(abspath, docdir) + abspath = env.srcdir / self.options['file'][1:] + doc_dir = env.doc2path(env.docname).parent + self.options['file'] = relpath(abspath, doc_dir) return super().run() diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index 12a80e0d8d4..3f21078c6c5 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -223,7 +223,7 @@ def process_field_xref(self, pnode: pending_xref) -> None: def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, - ) -> Element | None: + ) -> nodes.reference | None: """Resolve the pending_xref *node* with the given *typ* and *target*. This method should return a new node, to replace the xref node, @@ -241,7 +241,7 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element, - ) -> list[tuple[str, Element]]: + ) -> list[tuple[str, nodes.reference]]: """Resolve the pending_xref *node* with the given *target*. The reference comes from an "any" or similar role, which means that we diff --git a/sphinx/domains/_domains_container.py b/sphinx/domains/_domains_container.py index b8e389c79e4..5578a92575b 100644 --- a/sphinx/domains/_domains_container.py +++ b/sphinx/domains/_domains_container.py @@ -22,6 +22,7 @@ from sphinx.environment import BuildEnvironment from sphinx.ext.duration import DurationDomain from sphinx.ext.todo import TodoDomain + from sphinx.registry import SphinxComponentRegistry class _DomainsContainer: @@ -71,8 +72,10 @@ class _DomainsContainer: }) @classmethod - def _from_environment(cls, env: BuildEnvironment, /) -> Self: - create_domains = env.app.registry.create_domains + def _from_environment( + cls, env: BuildEnvironment, /, *, registry: SphinxComponentRegistry + ) -> Self: + create_domains = registry.create_domains # Initialise domains if domains := {domain.name: domain for domain in create_domains(env)}: return cls(**domains) # type: ignore[arg-type] diff --git a/sphinx/domains/c/__init__.py b/sphinx/domains/c/__init__.py index e82912c167e..28d77227eee 100644 --- a/sphinx/domains/c/__init__.py +++ b/sphinx/domains/c/__init__.py @@ -771,9 +771,10 @@ def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> Non ourObjects[fullname] = (fn, id_, objtype) # no need to warn on duplicates, the symbol merge already does that - def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder, - typ: str, target: str, node: pending_xref, - contnode: Element) -> tuple[Element | None, str | None]: + def _resolve_xref_inner( + self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, contnode: Element + ) -> tuple[nodes.reference, str] | tuple[None, None]: parser = DefinitionParser(target, location=node, config=env.config) try: name = parser.parse_xref_object() @@ -810,13 +811,13 @@ def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, - contnode: Element) -> Element | None: + contnode: Element) -> nodes.reference | None: return self._resolve_xref_inner(env, fromdocname, builder, typ, target, node, contnode)[0] def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element, - ) -> list[tuple[str, Element]]: + ) -> list[tuple[str, nodes.reference]]: with logging.suppress_logging(): retnode, objtype = self._resolve_xref_inner(env, fromdocname, builder, 'any', target, node, contnode) @@ -836,7 +837,7 @@ def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: objectType = symbol.declaration.objectType docname = symbol.docname newestId = symbol.declaration.get_newest_id() - yield (name, dispname, objectType, docname, newestId, 1) + yield name, dispname, objectType, docname, newestId, 1 def setup(app: Sphinx) -> ExtensionMetadata: @@ -844,7 +845,9 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.add_config_value("c_id_attributes", [], 'env', types={list, tuple}) app.add_config_value("c_paren_attributes", [], 'env', types={list, tuple}) app.add_config_value("c_extra_keywords", _macroKeywords, 'env', types={set, list}) - app.add_config_value("c_maximum_signature_line_length", None, 'env', types={int, None}) + app.add_config_value( + "c_maximum_signature_line_length", None, 'env', types={int, type(None)} + ) app.add_post_transform(AliasTransform) return { diff --git a/sphinx/domains/c/_parser.py b/sphinx/domains/c/_parser.py index 2eeb4e7ef47..d6afb24ade0 100644 --- a/sphinx/domains/c/_parser.py +++ b/sphinx/domains/c/_parser.py @@ -465,7 +465,7 @@ def _parse_expression_fallback( brackets = {'(': ')', '{': '}', '[': ']'} symbols: list[str] = [] while not self.eof: - if (len(symbols) == 0 and self.current_char in end): + if len(symbols) == 0 and self.current_char in end: break if self.current_char in brackets: symbols.append(brackets[self.current_char]) diff --git a/sphinx/domains/citation.py b/sphinx/domains/citation.py index 58d55774f48..0bac6040c46 100644 --- a/sphinx/domains/citation.py +++ b/sphinx/domains/citation.py @@ -86,7 +86,7 @@ def check_consistency(self) -> None: def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, - ) -> Element | None: + ) -> nodes.reference | None: docname, labelid, lineno = self.citations.get(target, ('', '', 0)) if not docname: return None @@ -96,7 +96,7 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element, - ) -> list[tuple[str, Element]]: + ) -> list[tuple[str, nodes.reference]]: refnode = self.resolve_xref(env, fromdocname, builder, 'ref', target, node, contnode) if refnode is None: return [] diff --git a/sphinx/domains/cpp/__init__.py b/sphinx/domains/cpp/__init__.py index 45183611106..743aa0d1018 100644 --- a/sphinx/domains/cpp/__init__.py +++ b/sphinx/domains/cpp/__init__.py @@ -403,7 +403,7 @@ def _toc_entry_name(self, sig_node: desc_signature) -> str: if not sig_node.get('_toc_parts'): return '' - config = self.env.app.config + config = self.env.config objtype = sig_node.parent.get('objtype') if config.add_function_parentheses and objtype in {'function', 'method'}: parens = '()' @@ -962,9 +962,10 @@ def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> Non logger.debug("\tresult end") logger.debug("merge_domaindata end") - def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder, - typ: str, target: str, node: pending_xref, - contnode: Element) -> tuple[Element | None, str | None]: + def _resolve_xref_inner( + self, env: BuildEnvironment, fromdocname: str, builder: Builder, + typ: str, target: str, node: pending_xref, contnode: Element + ) -> tuple[nodes.reference, str] | tuple[None, None]: # add parens again for those that could be functions if typ in {'any', 'func'}: target += '()' @@ -1112,13 +1113,13 @@ def checkType() -> bool: def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, - ) -> Element | None: + ) -> nodes.reference | None: return self._resolve_xref_inner(env, fromdocname, builder, typ, target, node, contnode)[0] def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element, - ) -> list[tuple[str, Element]]: + ) -> list[tuple[str, nodes.reference]]: with logging.suppress_logging(): retnode, objtype = self._resolve_xref_inner(env, fromdocname, builder, 'any', target, node, contnode) @@ -1141,7 +1142,7 @@ def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: objectType = symbol.declaration.objectType docname = symbol.docname newestId = symbol.declaration.get_newest_id() - yield (name, dispname, objectType, docname, newestId, 1) + yield name, dispname, objectType, docname, newestId, 1 def get_full_qualified_name(self, node: Element) -> str | None: target = node.get('reftarget', None) @@ -1162,7 +1163,9 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.add_config_value("cpp_index_common_prefix", [], 'env') app.add_config_value("cpp_id_attributes", [], 'env', types={list, tuple}) app.add_config_value("cpp_paren_attributes", [], 'env', types={list, tuple}) - app.add_config_value("cpp_maximum_signature_line_length", None, 'env', types={int, None}) + app.add_config_value( + "cpp_maximum_signature_line_length", None, 'env', types={int, type(None)} + ) app.add_post_transform(AliasTransform) # debug stuff diff --git a/sphinx/domains/cpp/_parser.py b/sphinx/domains/cpp/_parser.py index 5eedd078d9b..09f9e69e0cd 100644 --- a/sphinx/domains/cpp/_parser.py +++ b/sphinx/domains/cpp/_parser.py @@ -795,7 +795,7 @@ def _parse_expression_fallback(self, end: list[str], brackets = {'(': ')', '{': '}', '[': ']', '<': '>'} symbols: list[str] = [] while not self.eof: - if (len(symbols) == 0 and self.current_char in end): + if len(symbols) == 0 and self.current_char in end: break if self.current_char in brackets: symbols.append(brackets[self.current_char]) diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index ec81375a6da..ca9b78b53b1 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -230,7 +230,7 @@ def _toc_entry_name(self, sig_node: desc_signature) -> str: if not sig_node.get('_toc_parts'): return '' - config = self.env.app.config + config = self.env.config objtype = sig_node.parent.get('objtype') if config.add_function_parentheses and objtype in {'function', 'method'}: parens = '()' @@ -466,7 +466,7 @@ def find_obj( def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, - ) -> Element | None: + ) -> nodes.reference | None: mod_name = node.get('js:module') prefix = node.get('js:object') searchorder = 1 if node.hasattr('refspecific') else 0 @@ -477,7 +477,7 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element, - ) -> list[tuple[str, Element]]: + ) -> list[tuple[str, nodes.reference]]: mod_name = node.get('js:module') prefix = node.get('js:object') name, obj = self.find_obj(env, mod_name, prefix, target, None, 1) diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py index 19e050739db..e59776d7a07 100644 --- a/sphinx/domains/math.py +++ b/sphinx/domains/math.py @@ -95,7 +95,7 @@ def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> Non def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, - ) -> Element | None: + ) -> nodes.reference | None: assert typ in {'eq', 'numref'} result = self.equations.get(target) if result: @@ -126,7 +126,7 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element, - ) -> list[tuple[str, Element]]: + ) -> list[tuple[str, nodes.reference]]: refnode = self.resolve_xref(env, fromdocname, builder, 'eq', target, node, contnode) if refnode is None: return [] diff --git a/sphinx/domains/python/__init__.py b/sphinx/domains/python/__init__.py index eaa3b158d38..24dfc2a5756 100644 --- a/sphinx/domains/python/__init__.py +++ b/sphinx/domains/python/__init__.py @@ -828,7 +828,7 @@ def find_obj(self, env: BuildEnvironment, modname: str, classname: str, def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, type: str, target: str, node: pending_xref, contnode: Element, - ) -> Element | None: + ) -> nodes.reference | None: modname = node.get('py:module') clsname = node.get('py:class') searchmode = 1 if node.hasattr('refspecific') else 0 @@ -875,10 +875,10 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element, - ) -> list[tuple[str, Element]]: + ) -> list[tuple[str, nodes.reference]]: modname = node.get('py:module') clsname = node.get('py:class') - results: list[tuple[str, Element]] = [] + results: list[tuple[str, nodes.reference]] = [] # always search in "refspecific" mode with the :any: role matches = self.find_obj(env, modname, clsname, target, None, 1) @@ -910,7 +910,7 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Bui return results def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str, - contnode: Node) -> Element: + contnode: Node) -> nodes.reference: # get additional info for modules module: ModuleEntry = self.modules[name] title_parts = [name] @@ -927,14 +927,14 @@ def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str, def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: for modname, mod in self.modules.items(): - yield (modname, modname, 'module', mod.docname, mod.node_id, 0) + yield modname, modname, 'module', mod.docname, mod.node_id, 0 for refname, obj in self.objects.items(): if obj.objtype != 'module': # modules are already handled if obj.aliased: # aliased names are not full-text searchable. - yield (refname, refname, obj.objtype, obj.docname, obj.node_id, -1) + yield refname, refname, obj.objtype, obj.docname, obj.node_id, -1 else: - yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1) + yield refname, refname, obj.objtype, obj.docname, obj.node_id, 1 def get_full_qualified_name(self, node: Element) -> str | None: modname = node.get('py:module') diff --git a/sphinx/domains/python/_object.py b/sphinx/domains/python/_object.py index ceb7f8bf83b..3e9049a1a27 100644 --- a/sphinx/domains/python/_object.py +++ b/sphinx/domains/python/_object.py @@ -411,7 +411,7 @@ def _toc_entry_name(self, sig_node: desc_signature) -> str: if not sig_node.get('_toc_parts'): return '' - config = self.env.app.config + config = self.env.config objtype = sig_node.parent.get('objtype') if config.add_function_parentheses and objtype in {'function', 'method'}: parens = '()' diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index 9eec281f3e6..4bed18e0c9b 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from collections.abc import Iterator, Set + from docutils import nodes from docutils.nodes import Element from sphinx.addnodes import desc_signature, pending_xref @@ -75,7 +76,7 @@ def _toc_entry_name(self, sig_node: desc_signature) -> str: if not sig_node.get('_toc_parts'): return '' - config = self.env.app.config + config = self.env.config objtype = sig_node.parent.get('objtype') *parents, name = sig_node['_toc_parts'] if objtype == 'directive:option': @@ -98,15 +99,15 @@ def parse_directive(d: str) -> tuple[str, str]: dir = d.strip() if not dir.startswith('.'): # Assume it is a directive without syntax - return (dir, '') + return dir, '' m = dir_sig_re.match(dir) if not m: - return (dir, '') + return dir, '' parsed_dir, parsed_args = m.groups() if parsed_args.strip(): - return (parsed_dir.strip(), ' ' + parsed_args.strip()) + return parsed_dir.strip(), ' ' + parsed_args.strip() else: - return (parsed_dir.strip(), '') + return parsed_dir.strip(), '' class ReSTDirective(ReSTMarkup): @@ -262,7 +263,7 @@ def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> Non def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, - ) -> Element | None: + ) -> nodes.reference | None: objtypes = self.objtypes_for_role(typ) if not objtypes: return None @@ -276,8 +277,8 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element, - ) -> list[tuple[str, Element]]: - results: list[tuple[str, Element]] = [] + ) -> list[tuple[str, nodes.reference]]: + results: list[tuple[str, nodes.reference]] = [] for objtype in self.object_types: result = self.objects.get((objtype, target)) if result: diff --git a/sphinx/domains/std/__init__.py b/sphinx/domains/std/__init__.py index 470a1c05428..0e214c92a5b 100644 --- a/sphinx/domains/std/__init__.py +++ b/sphinx/domains/std/__init__.py @@ -845,10 +845,11 @@ def add_program_option(self, program: str | None, name: str, self.progoptions[program, name] = (docname, labelid) def build_reference_node(self, fromdocname: str, builder: Builder, docname: str, - labelid: str, sectname: str, rolename: str, **options: Any, - ) -> Element: - nodeclass = options.pop('nodeclass', nodes.reference) - newnode = nodeclass('', '', internal=True, **options) + labelid: str, sectname: str, rolename: str, *, + node_class: type[nodes.reference] = nodes.reference, + **options: Any, + ) -> nodes.reference: + newnode = node_class('', '', internal=True, **options) innernode = nodes.inline(sectname, sectname) if innernode.get('classes') is not None: innernode['classes'].append('std') @@ -871,11 +872,11 @@ def build_reference_node(self, fromdocname: str, builder: Builder, docname: str, def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, - ) -> Element | None: + ) -> nodes.reference | None: if typ == 'ref': resolver = self._resolve_ref_xref elif typ == 'numref': - resolver = self._resolve_numref_xref + resolver = self._resolve_numref_xref # type: ignore[assignment] elif typ == 'keyword': resolver = self._resolve_keyword_xref elif typ == 'doc': @@ -891,7 +892,7 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder def _resolve_ref_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, - contnode: Element) -> Element | None: + contnode: Element) -> nodes.reference | None: if node['refexplicit']: # reference to anonymous label; the reference uses # the supplied link caption @@ -909,7 +910,8 @@ def _resolve_ref_xref(self, env: BuildEnvironment, fromdocname: str, def _resolve_numref_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, - node: pending_xref, contnode: Element) -> Element | None: + node: pending_xref, contnode: Element + ) -> nodes.reference | Element | None: if target in self.labels: docname, labelid, figname = self.labels.get(target, ('', '', '')) else: @@ -968,12 +970,12 @@ def _resolve_numref_xref(self, env: BuildEnvironment, fromdocname: str, return self.build_reference_node(fromdocname, builder, docname, labelid, newtitle, 'numref', - nodeclass=addnodes.number_reference, + node_class=addnodes.number_reference, title=title) def _resolve_keyword_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, - node: pending_xref, contnode: Element) -> Element | None: + node: pending_xref, contnode: Element) -> nodes.reference | None: # keywords are oddballs: they are referenced by named labels docname, labelid, _ = self.labels.get(target, ('', '', '')) if not docname: @@ -983,7 +985,7 @@ def _resolve_keyword_xref(self, env: BuildEnvironment, fromdocname: str, def _resolve_doc_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, - node: pending_xref, contnode: Element) -> Element | None: + node: pending_xref, contnode: Element) -> nodes.reference | None: # directly reference to document by source name; can be absolute or relative refdoc = node.get('refdoc', fromdocname) docname = docname_join(refdoc, node['reftarget']) @@ -1000,7 +1002,7 @@ def _resolve_doc_xref(self, env: BuildEnvironment, fromdocname: str, def _resolve_option_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, - node: pending_xref, contnode: Element) -> Element | None: + node: pending_xref, contnode: Element) -> nodes.reference | None: progname = node.get('std:program') target = target.strip() docname, labelid = self.progoptions.get((progname, target), ('', '')) @@ -1033,7 +1035,7 @@ def _resolve_option_xref(self, env: BuildEnvironment, fromdocname: str, def _resolve_term_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, - node: pending_xref, contnode: Element) -> Element | None: + node: pending_xref, contnode: Element) -> nodes.reference | None: result = self._resolve_obj_xref(env, fromdocname, builder, typ, target, node, contnode) if result: @@ -1048,7 +1050,7 @@ def _resolve_term_xref(self, env: BuildEnvironment, fromdocname: str, def _resolve_obj_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, - node: pending_xref, contnode: Element) -> Element | None: + node: pending_xref, contnode: Element) -> nodes.reference | None: objtypes = self.objtypes_for_role(typ) or [] for objtype in objtypes: if (objtype, target) in self.objects: @@ -1063,8 +1065,8 @@ def _resolve_obj_xref(self, env: BuildEnvironment, fromdocname: str, def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, - contnode: Element) -> list[tuple[str, Element]]: - results: list[tuple[str, Element]] = [] + contnode: Element) -> list[tuple[str, nodes.reference]]: + results: list[tuple[str, nodes.reference]] = [] ltarget = target.lower() # :ref: lowercases its target automatically for role in ('ref', 'option'): # do not try "keyword" res = self.resolve_xref(env, fromdocname, builder, role, @@ -1087,23 +1089,23 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: # handle the special 'doc' reference here for doc in self.env.all_docs: - yield (doc, clean_astext(self.env.titles[doc]), 'doc', doc, '', -1) + yield doc, clean_astext(self.env.titles[doc]), 'doc', doc, '', -1 for (prog, option), info in self.progoptions.items(): if prog: fullname = f'{prog}.{option}' - yield (fullname, fullname, 'cmdoption', info[0], info[1], 1) + yield fullname, fullname, 'cmdoption', info[0], info[1], 1 else: - yield (option, option, 'cmdoption', info[0], info[1], 1) + yield option, option, 'cmdoption', info[0], info[1], 1 for (type, name), info in self.objects.items(): yield (name, name, type, info[0], info[1], self.object_types[type].attrs['searchprio']) for name, (docname, labelid, sectionname) in self.labels.items(): - yield (name, sectionname, 'label', docname, labelid, -1) + yield name, sectionname, 'label', docname, labelid, -1 # add anonymous-only labels as well non_anon_labels = set(self.labels) for name, (docname, labelid) in self.anonlabels.items(): if name not in non_anon_labels: - yield (name, name, 'label', docname, labelid, -1) + yield name, name, 'label', docname, labelid, -1 def get_type_name(self, type: ObjType, primary: bool = False) -> str: # never prepend "Default" diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 34ef4cc8066..b17c1ee453b 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -7,7 +7,6 @@ import pickle from collections import defaultdict from copy import copy -from os import path from typing import TYPE_CHECKING from sphinx import addnodes @@ -28,11 +27,10 @@ from sphinx.util.docutils import LoggingReporter from sphinx.util.i18n import CatalogRepository, docname_to_domain from sphinx.util.nodes import is_translatable -from sphinx.util.osutil import _last_modified_time, canon_path, os_path +from sphinx.util.osutil import _last_modified_time, _relative_path, canon_path if TYPE_CHECKING: from collections.abc import Callable, Iterable, Iterator - from pathlib import Path from typing import Any, Literal from docutils import nodes @@ -102,8 +100,8 @@ class BuildEnvironment: def __init__(self, app: Sphinx) -> None: self.app: Sphinx = app - self.doctreedir: Path = app.doctreedir - self.srcdir: Path = app.srcdir + self.doctreedir: _StrPath = app.doctreedir + self.srcdir: _StrPath = app.srcdir self.config: Config = None # type: ignore[assignment] self.config_status: int = CONFIG_UNSET self.config_status_extra: str = '' @@ -220,7 +218,9 @@ def __init__(self, app: Sphinx) -> None: self._search_index_objnames: dict[int, tuple[str, str, str]] = {} # all the registered domains, set by the application - self.domains: _DomainsContainer = _DomainsContainer._from_environment(self) + self.domains: _DomainsContainer = _DomainsContainer._from_environment( + self, registry=app.registry + ) # set up environment self.setup(app) @@ -259,7 +259,9 @@ def setup(self, app: Sphinx) -> None: # initialise domains if self.domains is None: # if we are unpickling an environment, we need to recreate the domains - self.domains = _DomainsContainer._from_environment(self) + self.domains = _DomainsContainer._from_environment( + self, registry=app.registry + ) # setup domains (must do after all initialization) self.domains._setup() @@ -416,17 +418,15 @@ def relfn2path(self, filename: str, docname: str | None = None) -> tuple[str, st source dir, while relative filenames are relative to the dir of the containing document. """ - filename = os_path(filename) - if filename.startswith(('/', os.sep)): - rel_fn = filename[1:] + filename = canon_path(filename) + if filename.startswith('/'): + abs_fn = (self.srcdir / filename[1:]).resolve() else: - docdir = path.dirname(self.doc2path(docname or self.docname, base=False)) - rel_fn = path.join(docdir, filename) + doc_dir = self.doc2path(docname or self.docname, base=False).parent + abs_fn = (self.srcdir / doc_dir / filename).resolve() - return ( - canon_path(path.normpath(rel_fn)), - path.normpath(path.join(self.srcdir, rel_fn)), - ) + rel_fn = _relative_path(abs_fn, self.srcdir) + return canon_path(rel_fn), os.fspath(abs_fn) @property def found_docs(self) -> set[str]: @@ -463,7 +463,7 @@ def find_files(self, config: Config, builder: Builder) -> None: for docname in self.found_docs: domain = docname_to_domain(docname, self.config.gettext_compact) if domain in mo_paths: - self.dependencies[docname].add(str(mo_paths[domain])) + self.note_dependency(mo_paths[domain], docname=docname) except OSError as exc: raise DocumentError( __('Failed to scan documents in %s: %r') % (self.srcdir, exc) @@ -489,8 +489,8 @@ def get_outdated_files( added.add(docname) continue # if the doctree file is not there, rebuild - filename = path.join(self.doctreedir, docname + '.doctree') - if not path.isfile(filename): + filename = self.doctreedir / f'{docname}.doctree' + if not filename.is_file(): logger.debug('[build target] changed %r', docname) changed.add(docname) continue @@ -515,21 +515,21 @@ def get_outdated_files( for dep in self.dependencies[docname]: try: # this will do the right thing when dep is absolute too - deppath = path.join(self.srcdir, dep) - if not path.isfile(deppath): + dep_path = self.srcdir / dep + if not dep_path.is_file(): logger.debug( '[build target] changed %r missing dependency %r', docname, - deppath, + dep_path, ) changed.add(docname) break - depmtime = _last_modified_time(deppath) + depmtime = _last_modified_time(dep_path) if depmtime > mtime: logger.debug( '[build target] outdated %r from dependency %r: %s -> %s', docname, - deppath, + dep_path, _format_rfc3339_microseconds(mtime), _format_rfc3339_microseconds(depmtime), ) @@ -581,16 +581,20 @@ def new_serialno(self, category: str = '') -> int: self.temp_data[key] = cur + 1 return cur - def note_dependency(self, filename: str) -> None: + def note_dependency( + self, filename: str | os.PathLike[str], *, docname: str | None = None + ) -> None: """Add *filename* as a dependency of the current document. This means that the document will be rebuilt if this file changes. *filename* should be absolute or relative to the source directory. """ - self.dependencies[self.docname].add(filename) + if docname is None: + docname = self.docname + self.dependencies[docname].add(os.fspath(filename)) - def note_included(self, filename: str) -> None: + def note_included(self, filename: str | os.PathLike[str]) -> None: """Add *filename* as a included from other document. This means the document is not orphaned. @@ -625,7 +629,7 @@ def get_doctree(self, docname: str) -> nodes.document: try: serialised = self._pickled_doctree_cache[docname] except KeyError: - filename = path.join(self.doctreedir, docname + '.doctree') + filename = self.doctreedir / f'{docname}.doctree' with open(filename, 'rb') as f: serialised = self._pickled_doctree_cache[docname] = f.read() diff --git a/sphinx/environment/adapters/toctree.py b/sphinx/environment/adapters/toctree.py index edd873b5f44..3079c7dc543 100644 --- a/sphinx/environment/adapters/toctree.py +++ b/sphinx/environment/adapters/toctree.py @@ -315,7 +315,7 @@ def _toctree_entry( else: if ref in parents: logger.warning( - __('circular toctree references ' 'detected, ignoring: %s <- %s'), + __('circular toctree references detected, ignoring: %s <- %s'), ref, ' <- '.join(parents), location=ref, diff --git a/sphinx/environment/collectors/asset.py b/sphinx/environment/collectors/asset.py index 5096a9d1a68..44f72294520 100644 --- a/sphinx/environment/collectors/asset.py +++ b/sphinx/environment/collectors/asset.py @@ -3,12 +3,12 @@ from __future__ import annotations import os +import os.path from glob import glob -from os import path +from pathlib import Path from typing import TYPE_CHECKING from docutils import nodes -from docutils.utils import relative_path from sphinx import addnodes from sphinx.environment.collectors import EnvironmentCollector @@ -16,6 +16,7 @@ from sphinx.util import logging from sphinx.util.i18n import get_image_filename_for_language, search_image_for_language from sphinx.util.images import guess_mimetype +from sphinx.util.osutil import _relative_path if TYPE_CHECKING: from docutils.nodes import Node @@ -89,8 +90,8 @@ def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: # map image paths to unique image names (so that they can be put # into a single directory) for imgpath in candidates.values(): - app.env.dependencies[docname].add(imgpath) - if not os.access(path.join(app.srcdir, imgpath), os.R_OK): + app.env.note_dependency(imgpath) + if not os.access(os.path.join(app.srcdir, imgpath), os.R_OK): logger.warning( __('image file not readable: %s'), imgpath, @@ -110,14 +111,14 @@ def collect_candidates( ) -> None: globbed: dict[str, list[str]] = {} for filename in glob(imgpath): - new_imgpath = relative_path(path.join(env.srcdir, 'dummy'), filename) + new_imgpath = _relative_path(Path(filename), env.srcdir) try: mimetype = guess_mimetype(filename) if mimetype is None: - basename, suffix = path.splitext(filename) + basename, suffix = os.path.splitext(filename) mimetype = 'image/x-' + suffix[1:] if mimetype not in candidates: - globbed.setdefault(mimetype, []).append(new_imgpath) + globbed.setdefault(mimetype, []).append(new_imgpath.as_posix()) except OSError as err: logger.warning( __('image file %s not readable: %s'), @@ -154,7 +155,7 @@ def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: node['refuri'] = targetname else: rel_filename, filename = app.env.relfn2path(targetname, app.env.docname) - app.env.dependencies[app.env.docname].add(rel_filename) + app.env.note_dependency(rel_filename) if not os.access(filename, os.R_OK): logger.warning( __('download file not readable: %s'), diff --git a/sphinx/environment/collectors/dependencies.py b/sphinx/environment/collectors/dependencies.py index 46fbf323609..d77731218b1 100644 --- a/sphinx/environment/collectors/dependencies.py +++ b/sphinx/environment/collectors/dependencies.py @@ -2,14 +2,11 @@ from __future__ import annotations -import os -from os import path +from pathlib import Path from typing import TYPE_CHECKING -from docutils.utils import relative_path - from sphinx.environment.collectors import EnvironmentCollector -from sphinx.util.osutil import fs_encoding +from sphinx.util.osutil import _relative_path, fs_encoding if TYPE_CHECKING: from docutils import nodes @@ -38,8 +35,7 @@ def merge_other( def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: """Process docutils-generated dependency info.""" - cwd = os.getcwd() - frompath = path.join(path.normpath(app.srcdir), 'dummy') + cwd = Path.cwd() deps = doctree.settings.record_dependencies if not deps: return @@ -48,8 +44,8 @@ def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: # one relative to the srcdir if isinstance(dep, bytes): dep = dep.decode(fs_encoding) - relpath = relative_path(frompath, path.normpath(path.join(cwd, dep))) - app.env.dependencies[app.env.docname].add(relpath) + relpath = _relative_path(cwd / dep, app.srcdir) + app.env.note_dependency(relpath) def setup(app: Sphinx) -> ExtensionMetadata: diff --git a/sphinx/ext/apidoc.py b/sphinx/ext/apidoc.py index 0cab1d7f132..684eef57d38 100644 --- a/sphinx/ext/apidoc.py +++ b/sphinx/ext/apidoc.py @@ -16,11 +16,11 @@ import glob import locale import os +import os.path import re import sys from copy import copy from importlib.machinery import EXTENSION_SUFFIXES -from os import path from pathlib import Path from typing import TYPE_CHECKING, Any, Protocol @@ -50,7 +50,7 @@ PY_SUFFIXES = ('.py', '.pyx', *tuple(EXTENSION_SUFFIXES)) -template_dir = path.join(package_dir, 'templates', 'apidoc') +template_dir = os.path.join(package_dir, 'templates', 'apidoc') def is_initpy(filename: str | Path) -> bool: @@ -297,7 +297,7 @@ def recurse_tree( """ # check if the base directory is a package and get its name if is_packagedir(rootpath) or opts.implicit_namespaces: - root_package = rootpath.split(path.sep)[-1] + root_package = rootpath.split(os.path.sep)[-1] else: # otherwise, the base is a directory with packages root_package = None @@ -322,7 +322,7 @@ def recurse_tree( # we are in a package with something to document if subs or len(files) > 1 or not is_skipped_package(root, opts): subpackage = ( - root[len(rootpath) :].lstrip(path.sep).replace(path.sep, '.') + root[len(rootpath) :].lstrip(os.path.sep).replace(os.path.sep, '.') ) # if this is not a namespace or # a namespace and there is something there to document @@ -627,12 +627,12 @@ def main(argv: Sequence[str] = (), /) -> int: parser = get_parser() args: CliOptions = parser.parse_args(argv or sys.argv[1:]) - rootpath = path.abspath(args.module_path) + rootpath = os.path.abspath(args.module_path) # normalize opts if args.header is None: - args.header = rootpath.split(path.sep)[-1] + args.header = rootpath.split(os.path.sep)[-1] args.suffix = args.suffix.removeprefix('.') if not Path(rootpath).is_dir(): logger.error(__('%s is not a directory.'), rootpath) @@ -640,7 +640,7 @@ def main(argv: Sequence[str] = (), /) -> int: if not args.dryrun: ensuredir(args.destdir) excludes = tuple( - re.compile(fnmatch.translate(path.abspath(exclude))) + re.compile(fnmatch.translate(os.path.abspath(exclude))) for exclude in dict.fromkeys(args.exclude_pattern) ) written_files, modules = recurse_tree(rootpath, excludes, args, args.templatedir) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index c5d2b0b248b..60c31e2542e 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -584,9 +584,14 @@ def process_doc(self, docstrings: list[list[str]]) -> Iterator[str]: for docstringlines in docstrings: if self.env.app: # let extensions preprocess docstrings - self.env.app.emit('autodoc-process-docstring', - self.objtype, self.fullname, self.object, - self.options, docstringlines) + self.env.events.emit( + 'autodoc-process-docstring', + self.objtype, + self.fullname, + self.object, + self.options, + docstringlines, + ) if docstringlines and docstringlines[-1]: # append a blank line to the end of the docstring @@ -793,7 +798,7 @@ def is_filtered_inherited_member(name: str, obj: Any) -> bool: # should be skipped if self.env.app: # let extensions preprocess docstrings - skip_user = self.env.app.emit_firstresult( + skip_user = self.env.events.emit_firstresult( 'autodoc-skip-member', self.objtype, membername, member, not keep, self.options) if skip_user is not None: @@ -1325,7 +1330,7 @@ def format_args(self, **kwargs: Any) -> str: kwargs.setdefault('unqualified_typehints', True) try: - self.env.app.emit('autodoc-before-process-signature', self.object, False) + self.env.events.emit('autodoc-before-process-signature', self.object, False) sig = inspect.signature(self.object, type_aliases=self.config.autodoc_type_aliases) args = stringify_signature(sig, **kwargs) except TypeError as exc: @@ -1564,7 +1569,7 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: call = None if call is not None: - self.env.app.emit('autodoc-before-process-signature', call, True) + self.env.events.emit('autodoc-before-process-signature', call, True) try: sig = inspect.signature(call, bound_method=True, type_aliases=self.config.autodoc_type_aliases) @@ -1580,7 +1585,7 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: new = None if new is not None: - self.env.app.emit('autodoc-before-process-signature', new, True) + self.env.events.emit('autodoc-before-process-signature', new, True) try: sig = inspect.signature(new, bound_method=True, type_aliases=self.config.autodoc_type_aliases) @@ -1591,7 +1596,7 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: # Finally, we should have at least __init__ implemented init = get_user_defined_function_or_method(self.object, '__init__') if init is not None: - self.env.app.emit('autodoc-before-process-signature', init, True) + self.env.events.emit('autodoc-before-process-signature', init, True) try: sig = inspect.signature(init, bound_method=True, type_aliases=self.config.autodoc_type_aliases) @@ -1603,7 +1608,7 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: # handle it. # We don't know the exact method that inspect.signature will read # the signature from, so just pass the object itself to our hook. - self.env.app.emit('autodoc-before-process-signature', self.object, False) + self.env.events.emit('autodoc-before-process-signature', self.object, False) try: sig = inspect.signature(self.object, bound_method=False, type_aliases=self.config.autodoc_type_aliases) @@ -2198,11 +2203,13 @@ def format_args(self, **kwargs: Any) -> str: args = '()' else: if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): - self.env.app.emit('autodoc-before-process-signature', self.object, False) + self.env.events.emit( + 'autodoc-before-process-signature', self.object, False + ) sig = inspect.signature(self.object, bound_method=False, type_aliases=self.config.autodoc_type_aliases) else: - self.env.app.emit('autodoc-before-process-signature', self.object, True) + self.env.events.emit('autodoc-before-process-signature', self.object, True) sig = inspect.signature(self.object, bound_method=True, type_aliases=self.config.autodoc_type_aliases) args = stringify_signature(sig, **kwargs) @@ -2785,7 +2792,7 @@ def format_args(self, **kwargs: Any) -> str: return '' # update the annotations of the property getter - self.env.app.emit('autodoc-before-process-signature', func, False) + self.env.events.emit('autodoc-before-process-signature', func, False) # correctly format the arguments for a property return super().format_args(**kwargs) diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 4311d428870..ceefdcd9f5f 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -67,7 +67,7 @@ def should_ignore(name: str, value: Any) -> bool: def query(name: str, defining_class: type) -> tuple[str, type, Any] | None: value = attrgetter(enum_class, name, sentinel) if value is not sentinel: - return (name, defining_class, value) + return name, defining_class, value return None # attributes defined on a parent type, possibly shadowed later by diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 3959f329c45..e77dbe91c79 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -52,11 +52,11 @@ import inspect import operator import os +import os.path import posixpath import re import sys from inspect import Parameter -from os import path from types import ModuleType from typing import TYPE_CHECKING, Any, ClassVar, cast @@ -814,7 +814,7 @@ def process_generate_options(app: Sphinx) -> None: for genfile in genfiles] for entry in genfiles[:]: - if not path.isfile(path.join(app.srcdir, entry)): + if not os.path.isfile(os.path.join(app.srcdir, entry)): logger.warning(__('autosummary_generate: file not found: %s'), entry) genfiles.remove(entry) diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 5fbc51118bf..1d90438cfec 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -19,11 +19,11 @@ import inspect import locale import os +import os.path import pkgutil import pydoc import re import sys -from os import path from pathlib import Path from typing import TYPE_CHECKING, Any, NamedTuple @@ -853,7 +853,7 @@ def main(argv: Sequence[str] = (), /) -> None: args = get_parser().parse_args(argv or sys.argv[1:]) if args.templates: - app.config.templates_path.append(path.abspath(args.templates)) + app.config.templates_path.append(os.path.abspath(args.templates)) app.config.autosummary_ignore_module_all = not args.respect_module_all written_files = generate_autosummary_docs( diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index c9b997642d3..d1c4aea457c 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -8,12 +8,12 @@ import glob import inspect +import os.path import pickle import pkgutil import re import sys from importlib import import_module -from os import path from typing import IO, TYPE_CHECKING, Any, TextIO import sphinx @@ -161,12 +161,12 @@ class CoverageBuilder(Builder): name = 'coverage' epilog = __('Testing of coverage in the sources finished, look at the ' - 'results in %(outdir)s' + path.sep + 'python.txt.') + 'results in %(outdir)s' + os.path.sep + 'python.txt.') def init(self) -> None: self.c_sourcefiles: list[str] = [] for pattern in self.config.coverage_c_path: - pattern = path.join(self.srcdir, pattern) + pattern = os.path.join(self.srcdir, pattern) self.c_sourcefiles.extend(glob.glob(pattern)) self.c_regexes: list[tuple[str, re.Pattern[str]]] = [] @@ -230,7 +230,7 @@ def build_c_coverage(self) -> None: self.c_undoc[filename] = undoc def write_c_coverage(self) -> None: - output_file = path.join(self.outdir, 'c.txt') + output_file = os.path.join(self.outdir, 'c.txt') with open(output_file, 'w', encoding="utf-8") as op: if self.config.coverage_write_headline: write_header(op, 'Undocumented C API elements', '=') @@ -395,7 +395,7 @@ def _write_py_statistics(self, op: TextIO) -> None: op.write(f'{line}\n') def write_py_coverage(self) -> None: - output_file = path.join(self.outdir, 'python.txt') + output_file = os.path.join(self.outdir, 'python.txt') failed = [] with open(output_file, 'w', encoding="utf-8") as op: if self.config.coverage_write_headline: @@ -472,7 +472,7 @@ def write_py_coverage(self) -> None: def finish(self) -> None: # dump the coverage data to a pickle file too - picklepath = path.join(self.outdir, 'undoc.pickle') + picklepath = os.path.join(self.outdir, 'undoc.pickle') with open(picklepath, 'wb') as dumpfile: pickle.dump((self.py_undoc, self.c_undoc, self.py_undocumented, self.py_documented), dumpfile) diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index 0aead2db480..6c0327fab02 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -6,11 +6,11 @@ from __future__ import annotations import doctest +import os.path import re import sys import time from io import StringIO -from os import path from typing import TYPE_CHECKING, Any, ClassVar from docutils import nodes @@ -377,7 +377,7 @@ def get_line_number(node: Node) -> int | None: """Get the real line number or admit we don't know.""" # TODO: Work out how to store or calculate real (file-relative) # line numbers for doctest blocks in docstrings. - if ':docstring of ' in path.basename(node.source or ''): + if ':docstring of ' in os.path.basename(node.source or ''): # The line number is given relative to the stripped docstring, # not the file. This is correct where it is set, in # `docutils.nodes.Node.setup_child`, but Sphinx should report diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index 13bb5ea4111..eb85f2d4c99 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -3,13 +3,13 @@ from __future__ import annotations +import os.path import posixpath import re import subprocess import xml.etree.ElementTree as ET from hashlib import sha1 from itertools import chain -from os import path from subprocess import CalledProcessError from typing import TYPE_CHECKING, Any, ClassVar from urllib.parse import urlsplit, urlunsplit @@ -247,7 +247,7 @@ def fix_svg_relative_paths(self: HTML5Translator | LaTeXTranslator | TexinfoTran old_path = doc_dir / rel_uri img_path = doc_dir / self.builder.imgpath - new_path = path.relpath(old_path, start=img_path) + new_path = os.path.relpath(old_path, start=img_path) modified_url = urlunsplit((scheme, hostname, new_path, query, fragment)) element.set(href_name, modified_url) @@ -272,16 +272,16 @@ def render_dot(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator, fname = f'{prefix}-{sha1(hashkey, usedforsecurity=False).hexdigest()}.{format}' relfn = posixpath.join(self.builder.imgpath, fname) - outfn = path.join(self.builder.outdir, self.builder.imagedir, fname) + outfn = os.path.join(self.builder.outdir, self.builder.imagedir, fname) - if path.isfile(outfn): + if os.path.isfile(outfn): return relfn, outfn if (hasattr(self.builder, '_graphviz_warned_dot') and self.builder._graphviz_warned_dot.get(graphviz_dot)): return None, None - ensuredir(path.dirname(outfn)) + ensuredir(os.path.dirname(outfn)) dot_args = [graphviz_dot] dot_args.extend(self.builder.config.graphviz_dot_args) @@ -289,9 +289,9 @@ def render_dot(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator, docname = options.get('docname', 'index') if filename: - cwd = path.dirname(path.join(self.builder.srcdir, filename)) + cwd = os.path.dirname(os.path.join(self.builder.srcdir, filename)) else: - cwd = path.dirname(path.join(self.builder.srcdir, docname)) + cwd = os.path.dirname(os.path.join(self.builder.srcdir, docname)) if format == 'png': dot_args.extend(['-Tcmapx', '-o%s.map' % outfn]) @@ -309,7 +309,7 @@ def render_dot(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator, except CalledProcessError as exc: raise GraphvizError(__('dot exited with error:\n[stderr]\n%r\n' '[stdout]\n%r') % (exc.stderr, exc.stdout)) from exc - if not path.isfile(outfn): + if not os.path.isfile(outfn): raise GraphvizError(__('dot did not produce an output file:\n[stderr]\n%r\n' '[stdout]\n%r') % (ret.stderr, ret.stdout)) @@ -448,7 +448,7 @@ def man_visit_graphviz(self: ManualPageTranslator, node: graphviz) -> None: def on_config_inited(_app: Sphinx, config: Config) -> None: - css_path = path.join(sphinx.package_dir, 'templates', 'graphviz', 'graphviz.css') + css_path = os.path.join(sphinx.package_dir, 'templates', 'graphviz', 'graphviz.css') config.html_static_path.append(css_path) diff --git a/sphinx/ext/imgmath.py b/sphinx/ext/imgmath.py index aed8252a5cf..2a06dd208bb 100644 --- a/sphinx/ext/imgmath.py +++ b/sphinx/ext/imgmath.py @@ -6,12 +6,13 @@ import base64 import contextlib +import os +import os.path import re import shutil import subprocess import tempfile from hashlib import sha1 -from os import path from subprocess import CalledProcessError from typing import TYPE_CHECKING @@ -28,7 +29,6 @@ from sphinx.util.template import LaTeXRenderer if TYPE_CHECKING: - import os from docutils.nodes import Element @@ -40,7 +40,7 @@ logger = logging.getLogger(__name__) -templates_path = path.join(package_dir, 'templates', 'imgmath') +templates_path = os.path.join(package_dir, 'templates', 'imgmath') class MathExtError(SphinxError): @@ -109,8 +109,8 @@ def generate_latex_macro(image_format: str, for template_dir in config.templates_path: for template_suffix in ('.jinja', '_t'): - template = path.join(confdir, template_dir, template_name + template_suffix) - if path.exists(template): + template = os.path.join(confdir, template_dir, template_name + template_suffix) + if os.path.exists(template): return LaTeXRenderer().render(template, variables) return LaTeXRenderer(templates_path).render(template_name + '.jinja', variables) @@ -132,11 +132,11 @@ def ensure_tempdir(builder: Builder) -> str: def compile_math(latex: str, builder: Builder) -> str: """Compile LaTeX macros for math to DVI.""" tempdir = ensure_tempdir(builder) - filename = path.join(tempdir, 'math.tex') + filename = os.path.join(tempdir, 'math.tex') with open(filename, 'w', encoding='utf-8') as f: f.write(latex) - imgmath_latex_name = path.basename(builder.config.imgmath_latex) + imgmath_latex_name = os.path.basename(builder.config.imgmath_latex) # build latex command; old versions of latex don't have the # --output-directory option, so we have to manually chdir to the @@ -152,9 +152,9 @@ def compile_math(latex: str, builder: Builder) -> str: subprocess.run(command, capture_output=True, cwd=tempdir, check=True, encoding='ascii') if imgmath_latex_name in {'xelatex', 'tectonic'}: - return path.join(tempdir, 'math.xdv') + return os.path.join(tempdir, 'math.xdv') else: - return path.join(tempdir, 'math.dvi') + return os.path.join(tempdir, 'math.dvi') except OSError as exc: logger.warning(__('LaTeX command %r cannot be run (needed for math ' 'display), check the imgmath_latex setting'), @@ -251,9 +251,9 @@ def render_math( self.builder.confdir) filename = f"{sha1(latex.encode(), usedforsecurity=False).hexdigest()}.{image_format}" - generated_path = path.join(self.builder.outdir, self.builder.imagedir, 'math', filename) - ensuredir(path.dirname(generated_path)) - if path.isfile(generated_path): + generated_path = os.path.join(self.builder.outdir, self.builder.imagedir, 'math', filename) + ensuredir(os.path.dirname(generated_path)) + if os.path.isfile(generated_path): if image_format == 'png': depth = read_png_depth(generated_path) elif image_format == 'svg': @@ -308,7 +308,7 @@ def clean_up_files(app: Sphinx, exc: Exception) -> None: # in embed mode, the images are still generated in the math output dir # to be shared across workers, but are not useful to the final document with contextlib.suppress(Exception): - shutil.rmtree(path.join(app.builder.outdir, app.builder.imagedir, 'math')) + shutil.rmtree(os.path.join(app.builder.outdir, app.builder.imagedir, 'math')) def get_tooltip(self: HTML5Translator, node: Element) -> str: @@ -337,9 +337,9 @@ def html_visit_math(self: HTML5Translator, node: nodes.math) -> None: image_format = self.builder.config.imgmath_image_format.lower() img_src = render_maths_to_base64(image_format, rendered_path) else: - bname = path.basename(rendered_path) - relative_path = path.join(self.builder.imgpath, 'math', bname) - img_src = relative_path.replace(path.sep, '/') + bname = os.path.basename(rendered_path) + relative_path = os.path.join(self.builder.imgpath, 'math', bname) + img_src = relative_path.replace(os.path.sep, '/') c = f' Non image_format = self.builder.config.imgmath_image_format.lower() img_src = render_maths_to_base64(image_format, rendered_path) else: - bname = path.basename(rendered_path) - relative_path = path.join(self.builder.imgpath, 'math', bname) - img_src = relative_path.replace(path.sep, '/') + bname = os.path.basename(rendered_path) + relative_path = os.path.join(self.builder.imgpath, 'math', bname) + img_src = relative_path.replace(os.path.sep, '/') self.body.append(f'
\n') raise nodes.SkipNode diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index ed2a8c0936b..47dec78aa9f 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -33,10 +33,10 @@ class E(B): pass import builtins import hashlib import inspect +import os.path import re from collections.abc import Iterable, Sequence from importlib import import_module -from os import path from typing import TYPE_CHECKING, Any, ClassVar, cast from docutils import nodes @@ -422,7 +422,7 @@ def html_visit_inheritance_diagram(self: HTML5Translator, node: inheritance_diag # Create a mapping from fully-qualified class names to URLs. graphviz_output_format = self.builder.env.config.graphviz_output_format.upper() - current_filename = path.basename(self.builder.current_docname + self.builder.out_suffix) + current_filename = os.path.basename(self.builder.current_docname + self.builder.out_suffix) urls = {} pending_xrefs = cast(Iterable[addnodes.pending_xref], node) for child in pending_xrefs: diff --git a/sphinx/ext/intersphinx/_load.py b/sphinx/ext/intersphinx/_load.py index 27b11673465..eb8b46be807 100644 --- a/sphinx/ext/intersphinx/_load.py +++ b/sphinx/ext/intersphinx/_load.py @@ -3,10 +3,10 @@ from __future__ import annotations import concurrent.futures +import os.path import posixpath import time from operator import itemgetter -from os import path from typing import TYPE_CHECKING from urllib.parse import urlsplit, urlunsplit @@ -272,7 +272,7 @@ def _fetch_inventory_group( else: issues = '\n'.join(f[0] % f[1:] for f in failures) LOGGER.warning( - __('failed to reach any of the inventories ' 'with the following issues:') + __('failed to reach any of the inventories with the following issues:') + '\n' + issues ) @@ -303,7 +303,7 @@ def _fetch_inventory( if '://' in inv_location: f: _ReadableStream[bytes] = _read_from_url(inv_location, config=config) else: - f = open(path.join(srcdir, inv_location), 'rb') # NoQA: SIM115 + f = open(os.path.join(srcdir, inv_location), 'rb') # NoQA: SIM115 except Exception as err: err.args = ( 'intersphinx inventory %r not fetchable due to %s: %s', @@ -321,10 +321,10 @@ def _fetch_inventory( if target_uri in { inv_location, - path.dirname(inv_location), - path.dirname(inv_location) + '/', + os.path.dirname(inv_location), + os.path.dirname(inv_location) + '/', }: - target_uri = path.dirname(new_inv_location) + target_uri = os.path.dirname(new_inv_location) with f: try: invdata = InventoryFile.load(f, target_uri, posixpath.join) diff --git a/sphinx/ext/intersphinx/_resolve.py b/sphinx/ext/intersphinx/_resolve.py index 9387d1e1096..0dbab63dc69 100644 --- a/sphinx/ext/intersphinx/_resolve.py +++ b/sphinx/ext/intersphinx/_resolve.py @@ -2,12 +2,11 @@ from __future__ import annotations -import posixpath import re +from pathlib import Path from typing import TYPE_CHECKING, cast from docutils import nodes -from docutils.utils import relative_path from sphinx.addnodes import pending_xref from sphinx.deprecation import _deprecation_warning @@ -16,6 +15,7 @@ from sphinx.locale import _, __ from sphinx.transforms.post_transforms import ReferencesResolver from sphinx.util.docutils import CustomReSTDispatcher, SphinxRole +from sphinx.util.osutil import _relative_path if TYPE_CHECKING: from collections.abc import Iterable @@ -42,7 +42,7 @@ def _create_element_from_result( proj, version, uri, dispname = data if '://' not in uri and node.get('refdoc'): # get correct path in case of subdirectories - uri = posixpath.join(relative_path(node['refdoc'], '.'), uri) + uri = (_relative_path(Path(), Path(node['refdoc']).parent) / uri).as_posix() if version: reftitle = _('(in %s v%s)') % (proj, version) else: @@ -516,9 +516,9 @@ def get_role_name(self, name: str) -> tuple[str, str] | None: return None if domain and self.is_existent_role(domain, role): - return (domain, role) + return domain, role elif self.is_existent_role('std', role): - return ('std', role) + return 'std', role else: return None diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index 836dc8b864c..0625621b6db 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -97,7 +97,7 @@ def process_doc(self, env: BuildEnvironment, docname: str, document: nodes.document) -> None: todos = self.todos.setdefault(docname, []) for todo in document.findall(todo_node): - env.app.emit('todo-defined', todo) + env.events.emit('todo-defined', todo) todos.append(todo) if env.config.todo_emit_warnings: diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 9991cf5d426..b64d67bdad3 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -3,10 +3,10 @@ from __future__ import annotations import operator +import os.path import posixpath import traceback from importlib import import_module -from os import path from typing import TYPE_CHECKING, Any, cast from docutils import nodes @@ -29,6 +29,7 @@ from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.environment import BuildEnvironment + from sphinx.util._pathlib import _StrPath from sphinx.util.typing import ExtensionMetadata logger = logging.getLogger(__name__) @@ -207,7 +208,7 @@ def remove_viewcode_anchors(self) -> None: node.parent.remove(node) -def get_module_filename(app: Sphinx, modname: str) -> str | None: +def get_module_filename(app: Sphinx, modname: str) -> _StrPath | None: """Get module filename for *modname*.""" source_info = app.emit_firstresult('viewcode-find-source', modname) if source_info: @@ -229,7 +230,7 @@ def should_generate_module_page(app: Sphinx, modname: str) -> bool: builder = cast(StandaloneHTMLBuilder, app.builder) basename = modname.replace('.', '/') + builder.out_suffix - page_filename = path.join(app.outdir, '_modules/', basename) + page_filename = os.path.join(app.outdir, '_modules/', basename) try: if _last_modified_time(module_filename) <= _last_modified_time(page_filename): @@ -311,7 +312,7 @@ def collect_pages(app: Sphinx) -> Iterator[tuple[str, dict[str, Any], str]]: 'body': (_('