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': (_('

Source code for %s

') % modname + '\n'.join(lines)), } - yield (pagename, context, 'page.html') + yield pagename, context, 'page.html' if not modnames: return @@ -339,7 +340,7 @@ def collect_pages(app: Sphinx) -> Iterator[tuple[str, dict[str, Any], str]]: ''.join(html)), } - yield (posixpath.join(OUTPUT_DIRNAME, 'index'), context, 'page.html') + yield posixpath.join(OUTPUT_DIRNAME, 'index'), context, 'page.html' def setup(app: Sphinx) -> ExtensionMetadata: diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py index 3621d417b94..909505fe5a2 100644 --- a/sphinx/jinja2glue.py +++ b/sphinx/jinja2glue.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from os import path +import os.path from pprint import pformat from typing import TYPE_CHECKING, Any @@ -127,12 +127,12 @@ def get_source( legacy_template = None for searchpath in self.searchpath: - filename = path.join(searchpath, template) + filename = os.path.join(searchpath, template) f = open_if_exists(filename) if f is not None: break if legacy_template is not None: - filename = path.join(searchpath, legacy_template) + filename = os.path.join(searchpath, legacy_template) f = open_if_exists(filename) if f is not None: break diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index d1a70d05b36..4379ea10367 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -141,7 +141,7 @@ def init( return translator, has_translation -_LOCALE_DIR = Path(__file__).parent.resolve() +_LOCALE_DIR = Path(__file__).resolve().parent def init_console( @@ -184,7 +184,7 @@ def get_translation(catalog: str, namespace: str = 'general') -> Callable[[str], The extension can use this API to translate the messages on the extension:: - import os + from pathlib import Path from sphinx.locale import get_translation MESSAGE_CATALOG_NAME = 'myextension' # name of *.pot, *.po and *.mo files @@ -193,8 +193,8 @@ def get_translation(catalog: str, namespace: str = 'general') -> Callable[[str], def setup(app): - package_dir = os.path.abspath(os.path.dirname(__file__)) - locale_dir = os.path.join(package_dir, 'locales') + package_dir = Path(__file__).resolve().parent + locale_dir = package_dir / 'locales' app.add_message_catalog(MESSAGE_CATALOG_NAME, locale_dir) With this code, sphinx searches a message catalog from diff --git a/sphinx/project.py b/sphinx/project.py index 01642999306..74499086034 100644 --- a/sphinx/project.py +++ b/sphinx/project.py @@ -96,10 +96,10 @@ def path2doc(self, filename: str | os.PathLike[str]) -> str | None: *filename* should be absolute or relative to the source directory. """ + path = Path(filename) try: - return self._path_to_docname[filename] # type: ignore[index] + return self._path_to_docname[path] except KeyError: - path = Path(filename) if path.is_absolute(): with contextlib.suppress(ValueError): path = path.relative_to(self.srcdir) diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index 4094d7a9b8b..0242cf9c8be 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -2,10 +2,8 @@ from __future__ import annotations -import os import tokenize from importlib import import_module -from os import path from typing import TYPE_CHECKING, Any, Literal from sphinx.errors import PycodeError @@ -13,6 +11,7 @@ from sphinx.util._pathlib import _StrPath if TYPE_CHECKING: + import os from inspect import Signature @@ -28,7 +27,7 @@ class ModuleAnalyzer: cache: dict[tuple[Literal['file', 'module'], str | _StrPath], Any] = {} @staticmethod - def get_module_source(modname: str) -> tuple[str | None, str | None]: + def get_module_source(modname: str) -> tuple[_StrPath | None, str | None]: """Try to find the source code for a module. Returns ('filename', 'source'). One of it can be None if @@ -39,14 +38,15 @@ def get_module_source(modname: str) -> tuple[str | None, str | None]: except Exception as err: raise PycodeError('error importing %r' % modname, err) from err loader = getattr(mod, '__loader__', None) - filename = getattr(mod, '__file__', None) + filename: str | None = getattr(mod, '__file__', None) if loader and getattr(loader, 'get_source', None): # prefer Native loader, as it respects #coding directive try: source = loader.get_source(modname) if source: + mod_path = None if filename is None else _StrPath(filename) # no exception and not None - it must be module source - return filename, source + return mod_path, source except ImportError: pass # Try other "source-mining" methods if filename is None and loader and getattr(loader, 'get_filename', None): @@ -60,24 +60,28 @@ def get_module_source(modname: str) -> tuple[str | None, str | None]: if filename is None: # all methods for getting filename failed, so raise... raise PycodeError('no source found for module %r' % modname) - filename = path.normpath(path.abspath(filename)) - if filename.lower().endswith(('.pyo', '.pyc')): - filename = filename[:-1] - if not path.isfile(filename) and path.isfile(filename + 'w'): - filename += 'w' - elif not filename.lower().endswith(('.py', '.pyw')): - raise PycodeError('source is not a .py file: %r' % filename) - - if not path.isfile(filename): - raise PycodeError('source file is not present: %r' % filename) - return filename, None + mod_path = _StrPath(filename).resolve() + if mod_path.suffix in {'.pyo', '.pyc'}: + mod_path_pyw = mod_path.with_suffix('.pyw') + if not mod_path.is_file() and mod_path_pyw.is_file(): + mod_path = mod_path_pyw + else: + mod_path = mod_path.with_suffix('.py') + elif mod_path.suffix not in {'.py', '.pyw'}: + msg = f'source is not a .py file: {mod_path!r}' + raise PycodeError(msg) + + if not mod_path.is_file(): + msg = f'source file is not present: {mod_path!r}' + raise PycodeError(msg) + return mod_path, None @classmethod def for_string( cls: type[ModuleAnalyzer], string: str, modname: str, - srcname: str = '', + srcname: str | os.PathLike[str] = '', ) -> ModuleAnalyzer: return cls(string, modname, srcname) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 82081cbe2e9..3f19d3663a0 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -10,13 +10,14 @@ import pickle import re from importlib import import_module -from os import path +from pathlib import Path from typing import IO, TYPE_CHECKING, Any from docutils import nodes from docutils.nodes import Element, Node from sphinx import addnodes, package_dir +from sphinx.util._pathlib import _StrPath from sphinx.util.index_entries import split_index_msg if TYPE_CHECKING: @@ -24,6 +25,9 @@ from sphinx.environment import BuildEnvironment +_NON_MINIFIED_JS_PATH = Path(package_dir, 'search', 'non-minified-js') +_MINIFIED_JS_PATH = Path(package_dir, 'search', 'minified-js') + class SearchLanguage: """ @@ -554,12 +558,12 @@ def context_for_searchtool(self) -> dict[str, Any]: 'search_word_splitter_code': js_splitter_code, } - def get_js_stemmer_rawcodes(self) -> list[str]: + def get_js_stemmer_rawcodes(self) -> list[_StrPath]: """Returns a list of non-minified stemmer JS files to copy.""" if self.lang.js_stemmer_rawcode: return [ - path.join(package_dir, 'search', 'non-minified-js', fname) - for fname in ('base-stemmer.js', self.lang.js_stemmer_rawcode) + _StrPath(_NON_MINIFIED_JS_PATH / 'base-stemmer.js'), + _StrPath(_NON_MINIFIED_JS_PATH / self.lang.js_stemmer_rawcode), ] else: return [] @@ -570,15 +574,10 @@ def get_js_stemmer_rawcode(self) -> str | None: def get_js_stemmer_code(self) -> str: """Returns JS code that will be inserted into language_data.js.""" if self.lang.js_stemmer_rawcode: - js_dir = path.join(package_dir, 'search', 'minified-js') - with open( - path.join(js_dir, 'base-stemmer.js'), encoding='utf-8' - ) as js_file: - base_js = js_file.read() - with open( - path.join(js_dir, self.lang.js_stemmer_rawcode), encoding='utf-8' - ) as js_file: - language_js = js_file.read() + base_js_path = _NON_MINIFIED_JS_PATH / 'base-stemmer.js' + language_js_path = _NON_MINIFIED_JS_PATH / self.lang.js_stemmer_rawcode + base_js = base_js_path.read_text(encoding='utf-8') + language_js = language_js_path.read_text(encoding='utf-8') return ( f'{base_js}\n{language_js}\nStemmer = {self.lang.language_name}Stemmer;' ) diff --git a/sphinx/testing/path.py b/sphinx/testing/path.py index 9792dcb7479..b469588ea6d 100644 --- a/sphinx/testing/path.py +++ b/sphinx/testing/path.py @@ -13,7 +13,7 @@ from collections.abc import Callable warnings.warn( - "'sphinx.testing.path' is deprecated. " "Use 'os.path' or 'pathlib' instead.", + "'sphinx.testing.path' is deprecated. Use 'os.path' or 'pathlib' instead.", RemovedInSphinx90Warning, stacklevel=2, ) diff --git a/sphinx/testing/restructuredtext.py b/sphinx/testing/restructuredtext.py index 1f89336db4b..620e8483492 100644 --- a/sphinx/testing/restructuredtext.py +++ b/sphinx/testing/restructuredtext.py @@ -1,5 +1,3 @@ -from os import path - from docutils import nodes from docutils.core import publish_doctree @@ -20,7 +18,7 @@ def parse(app: Sphinx, text: str, docname: str = 'index') -> nodes.document: with sphinx_domains(app.env): return publish_doctree( text, - path.join(app.srcdir, docname + '.rst'), + str(app.srcdir / f'{docname}.rst'), reader=reader, parser=parser, settings_overrides={ diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index 4d221133ffb..d95ffb46bc3 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -40,9 +40,9 @@ def assert_node(node: Node, cls: Any = None, xpath: str = '', **kwargs: Any) -> assert ( isinstance(node, nodes.Element) ), f'The node{xpath} does not have any children' # fmt: skip - assert ( - len(node) == 1 - ), f'The node{xpath} has {len(node)} child nodes, not one' + assert len(node) == 1, ( + f'The node{xpath} has {len(node)} child nodes, not one' + ) assert_node(node[0], cls[1:], xpath=xpath + '[0]', **kwargs) elif isinstance(cls, tuple): assert ( @@ -71,9 +71,9 @@ def assert_node(node: Node, cls: Any = None, xpath: str = '', **kwargs: Any) -> if (key := key.replace('_', '-')) not in node: msg = f'The node{xpath} does not have {key!r} attribute: {node!r}' raise AssertionError(msg) - assert ( - node[key] == value - ), f'The node{xpath}[{key}] is not {value!r}: {node[key]!r}' + assert node[key] == value, ( + f'The node{xpath}[{key}] is not {value!r}: {node[key]!r}' + ) # keep this to restrict the API usage and to have a correct return type diff --git a/sphinx/texinputs/sphinxlatexadmonitions.sty b/sphinx/texinputs/sphinxlatexadmonitions.sty index 76fef5a8c4f..0519903591b 100644 --- a/sphinx/texinputs/sphinxlatexadmonitions.sty +++ b/sphinx/texinputs/sphinxlatexadmonitions.sty @@ -32,7 +32,7 @@ % sphinxlatexshadowbox.sty, and handles both "with icon" and "without % icon" situations). % -% The sphinxlightbox environment is kept for backward compatiblity, for user +% The sphinxlightbox environment is kept for backward compatibility, for user % custom code which used it via custom definitions done in preamble or via % raw latex directive. % MEMO: here is for example how sphinxnote was formerly defined: diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 2dd9fbc9280..31542e81e5f 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -3,7 +3,6 @@ from __future__ import annotations import contextlib -from os import path from re import DOTALL, match from textwrap import indent from typing import TYPE_CHECKING, Any, TypeVar @@ -394,10 +393,8 @@ def apply(self, **kwargs: Any) -> None: textdomain = docname_to_domain(self.env.docname, self.config.gettext_compact) # fetch translations - dirs = [ - path.join(self.env.srcdir, directory) - for directory in self.config.locale_dirs - ] + srcdir = self.env.srcdir + dirs = [srcdir / directory for directory in self.config.locale_dirs] catalog, has_catalog = init_locale(dirs, self.config.language, textdomain) if not has_catalog: return diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index e642a95b134..cb590b77a10 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -139,7 +139,7 @@ def resolve_anyref( """Resolve reference generated by the "any" role.""" stddomain = self.env.domains.standard_domain target = node['reftarget'] - results: list[tuple[str, Element]] = [] + results: list[tuple[str, nodes.reference]] = [] # first, try resolving as :doc: doc_ref = stddomain.resolve_xref( self.env, refdoc, self.app.builder, 'doc', target, node, contnode diff --git a/sphinx/util/_pathlib.py b/sphinx/util/_pathlib.py index b44fec30285..31b47ce5a67 100644 --- a/sphinx/util/_pathlib.py +++ b/sphinx/util/_pathlib.py @@ -3,7 +3,7 @@ Instances of _StrPath should not be constructed except in Sphinx itself. Consumers of Sphinx APIs should prefer using ``pathlib.Path`` objects where possible. _StrPath objects can be treated as equivalent to ``Path``, -save that ``_StrPath.replace`` is overriden with ``str.replace``. +save that ``_StrPath.replace`` is overridden with ``str.replace``. To continue treating path-like objects as strings, use ``os.fspath``, or explicit string coercion. diff --git a/sphinx/util/docfields.py b/sphinx/util/docfields.py index bb8142570f0..33fda6f5dcd 100644 --- a/sphinx/util/docfields.py +++ b/sphinx/util/docfields.py @@ -132,7 +132,7 @@ def make_xrefs( ] def make_entry(self, fieldarg: str, content: list[Node]) -> tuple[str, list[Node]]: - return (fieldarg, content) + return fieldarg, content def make_field( self, diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index a4c1c67098e..30c87595f31 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -7,7 +7,7 @@ from collections.abc import Sequence # NoQA: TCH003 from contextlib import contextmanager from copy import copy -from os import path +from pathlib import Path from typing import IO, TYPE_CHECKING, Any, cast import docutils @@ -171,25 +171,23 @@ def patched_get_language( @contextmanager -def using_user_docutils_conf(confdir: str | None) -> Iterator[None]: +def using_user_docutils_conf(confdir: str | os.PathLike[str] | None) -> Iterator[None]: """Let docutils know the location of ``docutils.conf`` for Sphinx.""" try: - docutilsconfig = os.environ.get('DOCUTILSCONFIG', None) + docutils_config = os.environ.get('DOCUTILSCONFIG', None) if confdir: - os.environ['DOCUTILSCONFIG'] = path.join( - path.abspath(confdir), 'docutils.conf' - ) - + docutils_conf_path = Path(confdir, 'docutils.conf').resolve() + os.environ['DOCUTILSCONFIG'] = str(docutils_conf_path) yield finally: - if docutilsconfig is None: + if docutils_config is None: os.environ.pop('DOCUTILSCONFIG', None) else: - os.environ['DOCUTILSCONFIG'] = docutilsconfig + os.environ['DOCUTILSCONFIG'] = docutils_config @contextmanager -def patch_docutils(confdir: str | None = None) -> Iterator[None]: +def patch_docutils(confdir: str | os.PathLike[str] | None = None) -> Iterator[None]: """Patch to docutils temporarily.""" with ( patched_get_language(), @@ -718,7 +716,7 @@ def dispatch_visit(self, node: Node) -> None: 3. ``self.unknown_visit()`` """ for node_class in node.__class__.__mro__: - method = getattr(self, 'visit_%s' % (node_class.__name__), None) + method = getattr(self, 'visit_%s' % node_class.__name__, None) if method: method(node) break @@ -735,7 +733,7 @@ def dispatch_departure(self, node: Node) -> None: 3. ``self.unknown_departure()`` """ for node_class in node.__class__.__mro__: - method = getattr(self, 'depart_%s' % (node_class.__name__), None) + method = getattr(self, 'depart_%s' % node_class.__name__, None) if method: method(node) break diff --git a/sphinx/util/fileutil.py b/sphinx/util/fileutil.py index acd52b07674..d5e2e2692d5 100644 --- a/sphinx/util/fileutil.py +++ b/sphinx/util/fileutil.py @@ -7,11 +7,9 @@ from pathlib import Path from typing import TYPE_CHECKING, Any -from docutils.utils import relative_path - from sphinx.locale import __ from sphinx.util import logging -from sphinx.util.osutil import copyfile, ensuredir +from sphinx.util.osutil import _relative_path, copyfile, ensuredir if TYPE_CHECKING: from collections.abc import Callable @@ -125,7 +123,8 @@ def copy_asset( :param onerror: The error handler. :param bool force: Overwrite the destination file even if it exists. """ - if not os.path.exists(source): + source = Path(source) + if not source.exists(): return if renderer is None: @@ -134,14 +133,14 @@ def copy_asset( renderer = SphinxRenderer() ensuredir(destination) - if os.path.isfile(source): + if source.is_file(): copy_asset_file( source, destination, context=context, renderer=renderer, force=force ) return for root, dirs, files in os.walk(source, followlinks=True): - reldir = relative_path(source, root) + reldir = _relative_path(Path(root), source).as_posix() for dir in dirs.copy(): if excluded(posixpath.join(reldir, dir)): dirs.remove(dir) diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index cd5619a729e..826696c83e3 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -3,9 +3,9 @@ from __future__ import annotations import os +import os.path import re from datetime import datetime -from os import path from typing import TYPE_CHECKING import babel.dates @@ -151,7 +151,7 @@ def pofiles(self) -> Iterator[tuple[_StrPath, _StrPath]]: @property def catalogs(self) -> Iterator[CatalogInfo]: for basedir, filename in self.pofiles: - domain = canon_path(path.splitext(filename)[0]) + domain = canon_path(os.path.splitext(filename)[0]) yield CatalogInfo(basedir, domain, self.encoding) @@ -290,15 +290,15 @@ def get_image_filename_for_language( filename: str | os.PathLike[str], env: BuildEnvironment, ) -> str: - root, ext = path.splitext(filename) - dirname = path.dirname(root) - docpath = path.dirname(env.docname) + root, ext = os.path.splitext(filename) + dirname = os.path.dirname(root) + docpath = os.path.dirname(env.docname) try: return env.config.figure_language_filename.format( root=root, ext=ext, path=dirname and dirname + SEP, - basename=path.basename(root), + basename=os.path.basename(root), docpath=docpath and docpath + SEP, language=env.config.language, ) @@ -310,7 +310,7 @@ def get_image_filename_for_language( def search_image_for_language(filename: str, env: BuildEnvironment) -> str: translated = get_image_filename_for_language(filename, env) _, abspath = env.relfn2path(translated) - if path.exists(abspath): + if os.path.exists(abspath): return translated else: return filename diff --git a/sphinx/util/images.py b/sphinx/util/images.py index 4a6a58698e4..f1e7344eb7a 100644 --- a/sphinx/util/images.py +++ b/sphinx/util/images.py @@ -3,7 +3,7 @@ from __future__ import annotations import base64 -from os import path +from pathlib import Path from typing import TYPE_CHECKING, NamedTuple, overload import imagesize @@ -37,7 +37,8 @@ class DataURI(NamedTuple): data: bytes -def get_image_size(filename: str) -> tuple[int, int] | None: +def get_image_size(filename: str | PathLike[str]) -> tuple[int, int] | None: + filename = Path(filename) try: size = imagesize.get(filename) if size[0] == -1: @@ -68,10 +69,11 @@ def guess_mimetype( filename: PathLike[str] | str = '', default: str | None = None, ) -> str | None: - ext = path.splitext(filename)[1].lower() + filename = Path(filename) + ext = filename.suffix.lower() if ext in mime_suffixes: return mime_suffixes[ext] - if path.exists(filename): + if filename.exists(): try: imgtype = _image_type_from_file(filename) except ValueError: diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py index 758a48f8b0e..20293c8fabe 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -6,6 +6,7 @@ import logging.handlers from collections import defaultdict from contextlib import contextmanager, nullcontext +from os.path import abspath from typing import IO, TYPE_CHECKING, Any from docutils import nodes @@ -13,7 +14,6 @@ from sphinx.errors import SphinxWarning from sphinx.util.console import colorize -from sphinx.util.osutil import abspath if TYPE_CHECKING: from collections.abc import Iterator, Sequence, Set diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 4e092692bf0..779727b394b 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -5,12 +5,12 @@ import contextlib import filecmp import os +import os.path import re import shutil import sys import unicodedata from io import StringIO -from os import path from pathlib import Path from typing import TYPE_CHECKING @@ -28,12 +28,12 @@ def os_path(canonical_path: str, /) -> str: - return canonical_path.replace(SEP, path.sep) + return canonical_path.replace(SEP, os.path.sep) def canon_path(native_path: str | os.PathLike[str], /) -> str: """Return path in OS-independent form""" - return os.fspath(native_path).replace(path.sep, SEP) + return os.fspath(native_path).replace(os.path.sep, SEP) def path_stabilize(filepath: str | os.PathLike[str], /) -> str: @@ -169,11 +169,27 @@ def relpath( return str(path) +def _relative_path(path: Path, root: Path, /) -> Path: + """Return a relative filepath to *path* from the given *root* directory. + + This is an alternative of ``Path.relative_to``. + It returns the original path if *path* and *root* are on different drives, + which may happen on Windows. + """ + if path.anchor != root.anchor or '..' in root.parts: + # If the drives are different, no relative path exists. + # Path.relative_to() requires fully-resolved paths (no '..'). + return path + if sys.version_info[:2] < (3, 12): + return Path(os.path.relpath(path, root)) + return path.relative_to(root, walk_up=True) + + safe_relpath = relpath # for compatibility fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() -abspath = path.abspath +abspath = os.path.abspath class FileAvoidWrite: @@ -236,7 +252,7 @@ def __getattr__(self, name: str) -> Any: return getattr(self._io, name) -def rmtree(path: str) -> None: +def rmtree(path: str | os.PathLike[str], /) -> None: if os.path.isdir(path): shutil.rmtree(path) else: diff --git a/sphinx/util/template.py b/sphinx/util/template.py index 6e217d6177f..b428d3cf777 100644 --- a/sphinx/util/template.py +++ b/sphinx/util/template.py @@ -4,7 +4,7 @@ import os from functools import partial -from os import path +from pathlib import Path from typing import TYPE_CHECKING, Any from jinja2 import TemplateNotFound @@ -21,6 +21,9 @@ from jinja2.environment import Environment +_TEMPLATES_PATH = Path(package_dir, 'templates') +_LATEX_TEMPLATES_PATH = _TEMPLATES_PATH / 'latex' + class BaseRenderer: def __init__(self, loader: BaseLoader | None = None) -> None: @@ -49,11 +52,12 @@ def __init__(self, search_path: Sequence[str | os.PathLike[str]]) -> None: @classmethod def render_from_file( - cls: type[FileRenderer], filename: str, context: dict[str, Any] + cls: type[FileRenderer], + filename: str | os.PathLike[str], + context: dict[str, Any], ) -> str: - dirname = os.path.dirname(filename) - basename = os.path.basename(filename) - return cls(dirname).render(basename, context) + filename = Path(filename) + return cls((filename.parent,)).render(filename.name, context) class SphinxRenderer(FileRenderer): @@ -61,12 +65,14 @@ def __init__( self, template_path: Sequence[str | os.PathLike[str]] | None = None ) -> None: if template_path is None: - template_path = os.path.join(package_dir, 'templates') + template_path = (_TEMPLATES_PATH,) super().__init__(template_path) @classmethod def render_from_file( - cls: type[FileRenderer], filename: str, context: dict[str, Any] + cls: type[FileRenderer], + filename: str | os.PathLike[str], + context: dict[str, Any], ) -> str: return FileRenderer.render_from_file(filename, context) @@ -78,7 +84,7 @@ def __init__( latex_engine: str | None = None, ) -> None: if template_path is None: - template_path = [os.path.join(package_dir, 'templates', 'latex')] + template_path = (_LATEX_TEMPLATES_PATH,) super().__init__(template_path) # use texescape as escape filter @@ -126,8 +132,9 @@ def __init__( self.loaders = [] self.sysloaders = [] + conf_dir = Path(confdir) for templates_path in templates_paths: - loader = SphinxFileSystemLoader(path.join(confdir, templates_path)) + loader = SphinxFileSystemLoader(conf_dir / templates_path) self.loaders.append(loader) for templates_path in system_templates_paths: diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 506d7b5753d..778c7ab5da0 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -5,7 +5,6 @@ import pickle from itertools import product, zip_longest from operator import itemgetter -from os import path from typing import TYPE_CHECKING, Any from uuid import uuid4 @@ -160,8 +159,8 @@ def apply(self, **kwargs: Any) -> None: if env.versioning_compare: # get old doctree + filename = env.doctreedir / f'{env.docname}.doctree' try: - filename = path.join(env.doctreedir, env.docname + '.doctree') with open(filename, 'rb') as f: old_doctree = pickle.load(f) except OSError: diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index e002788fc97..608a84f34db 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -2,7 +2,6 @@ from __future__ import annotations -import os import posixpath import re import urllib.parse @@ -327,9 +326,9 @@ def visit_reference(self, node: Element) -> None: atts['href'] = self.cloak_mailto(atts['href']) self.in_mailto = True else: - assert ( - 'refid' in node - ), 'References must have "refuri" or "refid" attribute.' + assert 'refid' in node, ( + 'References must have "refuri" or "refid" attribute.' + ) atts['href'] = '#' + node['refid'] if not isinstance(node.parent, nodes.TextElement): assert len(node) == 1 and isinstance(node[0], nodes.image) # NoQA: PT018 @@ -379,7 +378,7 @@ def get_secnumber(self, node: Element) -> tuple[int, ...] | None: if isinstance(node.parent, nodes.section): if self.builder.name == 'singlehtml': docname = self.docnames[-1] - anchorname = f"{docname}/#{node.parent['ids'][0]}" + anchorname = f'{docname}/#{node.parent["ids"][0]}' if anchorname not in self.builder.secnumbers: # try first heading which has no anchor anchorname = f'{docname}/' @@ -752,8 +751,7 @@ def visit_image(self, node: Element) -> None: # but it tries the final file name, which does not necessarily exist # yet at the time the HTML file is written. if not ('width' in node and 'height' in node): - path = os.path.join(self.builder.srcdir, olduri) # type: ignore[has-type] - size = get_image_size(path) + size = get_image_size(self.builder.srcdir / olduri) if size is None: logger.warning( __('Could not obtain image size. :scale: option is ignored.'), diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index c98135efa7f..454543b043c 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -9,10 +9,11 @@ import re from collections import defaultdict from collections.abc import Iterable -from os import path +from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, cast from docutils import nodes, writers +from roman_numerals import RomanNumeral from sphinx import addnodes, highlighting from sphinx.errors import SphinxError @@ -24,12 +25,6 @@ from sphinx.util.template import LaTeXRenderer from sphinx.util.texescape import tex_replace_map -try: - from docutils.utils.roman import toRoman -except ImportError: - # In Debian/Ubuntu, roman package is provided as roman, not as docutils.utils.roman - from roman import toRoman # type: ignore[no-redef, import-not-found] - if TYPE_CHECKING: from docutils.nodes import Element, Node, Text @@ -583,18 +578,19 @@ def generate( def render(self, template_name: str, variables: dict[str, Any]) -> str: renderer = LaTeXRenderer(latex_engine=self.config.latex_engine) for template_dir in self.config.templates_path: - template = path.join(self.builder.confdir, template_dir, template_name) - if path.exists(template): - return renderer.render(template, variables) - elif template.endswith('.jinja'): - legacy_template = template.removesuffix('.jinja') + '_t' - if path.exists(legacy_template): + template = self.builder.confdir / template_dir / template_name + if template.exists(): + return renderer.render(str(template), variables) + elif template.suffix == '.jinja': + legacy_template_name = template.name.removesuffix('.jinja') + '_t' + legacy_template = template.with_name(legacy_template_name) + if legacy_template.exists(): logger.warning( __('template %s not found; loading from legacy %s instead'), template_name, legacy_template, ) - return renderer.render(legacy_template, variables) + return renderer.render(str(legacy_template), variables) return renderer.render(template_name, variables) @@ -1420,8 +1416,9 @@ def get_nested_level(node: Element) -> int: else: return get_nested_level(node.parent) - enum = 'enum%s' % toRoman(get_nested_level(node)).lower() - enumnext = 'enum%s' % toRoman(get_nested_level(node) + 1).lower() + nested_level = get_nested_level(node) + enum = f'enum{RomanNumeral(nested_level).to_lowercase()}' + enumnext = f'enum{RomanNumeral(nested_level + 1).to_lowercase()}' style = ENUMERATE_LIST_STYLE.get(get_enumtype(node)) prefix = node.get('prefix', '') suffix = node.get('suffix', '.') @@ -1648,7 +1645,9 @@ def visit_image(self, node: Element) -> None: options = '' if include_graphics_options: options = '[%s]' % ','.join(include_graphics_options) - base, ext = path.splitext(uri) + img_path = Path(uri) + base = img_path.with_suffix('') + ext = img_path.suffix if self.in_title and base: # Lowercase tokens forcely because some fncychap themes capitalize @@ -1657,8 +1656,8 @@ def visit_image(self, node: Element) -> None: else: cmd = rf'\sphinxincludegraphics{options}{{{{{base}}}{ext}}}' # escape filepath for includegraphics, https://tex.stackexchange.com/a/202714/41112 - if '#' in base: - cmd = r'{\catcode`\#=12' + cmd + '}' + if '#' in str(base): + cmd = rf'{{\catcode`\#=12{cmd}}}' self.body.append(cmd) self.body.extend(post) @@ -2452,7 +2451,7 @@ def visit_math(self, node: Element) -> None: def visit_math_block(self, node: Element) -> None: if node.get('label'): - label = f"equation:{node['docname']}:{node['label']}" + label = f'equation:{node["docname"]}:{node["label"]}' else: label = None @@ -2469,7 +2468,7 @@ def visit_math_block(self, node: Element) -> None: raise nodes.SkipNode def visit_math_reference(self, node: Element) -> None: - label = f"equation:{node['docname']}:{node['target']}" + label = f'equation:{node["docname"]}:{node["target"]}' eqref_format = self.config.math_eqref_format if eqref_format: try: diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py index 5bfc23481c0..7b7db13961f 100644 --- a/sphinx/writers/manpage.py +++ b/sphinx/writers/manpage.py @@ -117,7 +117,7 @@ def header(self) -> str: ' "%(date)s" "%(version)s" "%(manual_group)s"\n' ) if self._docinfo['subtitle']: - tmpl += '.SH NAME\n' '%(title)s \\- %(subtitle)s\n' + tmpl += '.SH NAME\n%(title)s \\- %(subtitle)s\n' return tmpl % self._docinfo def visit_start_of_file(self, node: Element) -> None: diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index 997561b16fd..396e48bf91a 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -2,10 +2,10 @@ from __future__ import annotations +import os.path import re import textwrap from collections.abc import Iterable, Iterator -from os import path from typing import TYPE_CHECKING, Any, ClassVar, cast from docutils import nodes, writers @@ -250,9 +250,10 @@ def init_settings(self) -> None: '(%s)' % elements['filename'], self.escape_arg(self.settings.texinfo_dir_description), ) - elements['direntry'] = ( - '@dircategory %s\n' '@direntry\n' '%s' '@end direntry\n' - ) % (self.escape_id(self.settings.texinfo_dir_category), entry) + elements['direntry'] = '@dircategory %s\n@direntry\n%s@end direntry\n' % ( + self.escape_id(self.settings.texinfo_dir_category), + entry, + ) elements['copying'] = COPYING % elements # allow the user to override them all elements.update(self.settings.texinfo_elements) @@ -448,10 +449,10 @@ def _add_detailed_menu(name: str) -> None: for subentry in entries: _add_detailed_menu(subentry) - self.body.append('\n@detailmenu\n' ' --- The Detailed Node Listing ---\n') + self.body.append('\n@detailmenu\n --- The Detailed Node Listing ---\n') for entry in entries: _add_detailed_menu(entry) - self.body.append('\n@end detailmenu\n' '@end menu\n') + self.body.append('\n@end detailmenu\n@end menu\n') def tex_image_length(self, width_str: str) -> str: match = re.match(r'(\d*\.?\d*)\s*(\S*)', width_str) @@ -1119,7 +1120,7 @@ def _visit_named_admonition(self, node: Element) -> None: def depart_admonition(self, node: Element) -> None: self.ensure_eol() - self.body.append('@end quotation\n' '@end cartouche\n') + self.body.append('@end quotation\n@end cartouche\n') visit_attention = _visit_named_admonition depart_attention = depart_admonition @@ -1231,12 +1232,12 @@ def visit_image(self, node: Element) -> None: if uri.find('://') != -1: # ignore remote images return - name, ext = path.splitext(uri) + name, ext = os.path.splitext(uri) # width and height ignored in non-tex output width = self.tex_image_length(node.get('width', '')) height = self.tex_image_length(node.get('height', '')) alt = self.escape_arg(node.get('alt', '')) - filename = f"{self.elements['filename'][:-5]}-figures/{name}" # type: ignore[index] + filename = f'{self.elements["filename"][:-5]}-figures/{name}' # type: ignore[index] self.body.append(f'\n@image{{{filename},{width},{height},{alt},{ext[1:]}}}\n') def depart_image(self, node: Element) -> None: @@ -1280,7 +1281,7 @@ def visit_substitution_definition(self, node: Element) -> None: def visit_system_message(self, node: Element) -> None: self.body.append( - '\n@verbatim\n' '\n' '@end verbatim\n' % node.astext() + '\n@verbatim\n\n@end verbatim\n' % node.astext() ) raise nodes.SkipNode diff --git a/tests/conftest.py b/tests/conftest.py index 8501825a025..b8c8befad32 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,7 +44,7 @@ def _init_console( @pytest.fixture(scope='session') def rootdir() -> Path: - return Path(__file__).parent.resolve() / 'roots' + return Path(__file__).resolve().parent / 'roots' def pytest_report_header(config: pytest.Config) -> str: diff --git a/tests/roots/test-util-copyasset_overwrite/myext.py b/tests/roots/test-util-copyasset_overwrite/myext.py index 6c2ad23395b..5ef9e69e645 100644 --- a/tests/roots/test-util-copyasset_overwrite/myext.py +++ b/tests/roots/test-util-copyasset_overwrite/myext.py @@ -9,7 +9,7 @@ def _copy_asset_overwrite_hook(app): assert css.read_text(encoding='utf-8') == '/* html_static_path */\n', 'invalid default text' # warning generated by here copy_asset( - Path(__file__).parent.joinpath('myext_static', 'custom-styles.css'), + Path(__file__).resolve().parent.joinpath('myext_static', 'custom-styles.css'), app.outdir / '_static', ) # This demonstrates that no overwriting occurs diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py index 1e82e70eaa1..097f895d7c8 100644 --- a/tests/test_builders/test_build_html.py +++ b/tests/test_builders/test_build_html.py @@ -300,9 +300,7 @@ def test_html_raw_directive(app): [ (".//link[@href='_static/persistent.css'][@rel='stylesheet']", '', True), ( - ".//link[@href='_static/default.css']" - "[@rel='stylesheet']" - "[@title='Default']", + ".//link[@href='_static/default.css'][@rel='stylesheet'][@title='Default']", '', True, ), @@ -338,8 +336,7 @@ def test_html_raw_directive(app): True, ), ( - ".//link[@href='_static/more_alternate2.css']" - "[@rel='alternate stylesheet']", + ".//link[@href='_static/more_alternate2.css'][@rel='alternate stylesheet']", '', True, ), diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py index a0cc15aeffa..a618fae1e45 100644 --- a/tests/test_builders/test_build_html_5_output.py +++ b/tests/test_builders/test_build_html_5_output.py @@ -162,8 +162,7 @@ def checker(nodes: Iterable[Element]) -> Literal[True]: ), ( 'markup.html', - ".//a[@href='#with']" - "[@class='reference internal']/code/span[@class='pre']", + ".//a[@href='#with'][@class='reference internal']/code/span[@class='pre']", '^with$', ), ( diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py index b6207bae60c..d95c5e4c286 100644 --- a/tests/test_builders/test_build_latex.py +++ b/tests/test_builders/test_build_latex.py @@ -389,8 +389,7 @@ def test_numref(app): print(app.status.getvalue()) print(app.warning.getvalue()) assert ( - '\\hyperref[\\detokenize{index:fig1}]' - '{Fig.\\@ \\ref{\\detokenize{index:fig1}}}' + '\\hyperref[\\detokenize{index:fig1}]{Fig.\\@ \\ref{\\detokenize{index:fig1}}}' ) in result assert ( '\\hyperref[\\detokenize{baz:fig22}]{Figure\\ref{\\detokenize{baz:fig22}}}' @@ -400,8 +399,7 @@ def test_numref(app): '{Table \\ref{\\detokenize{index:table-1}}}' ) in result assert ( - '\\hyperref[\\detokenize{baz:table22}]' - '{Table:\\ref{\\detokenize{baz:table22}}}' + '\\hyperref[\\detokenize{baz:table22}]{Table:\\ref{\\detokenize{baz:table22}}}' ) in result assert ( '\\hyperref[\\detokenize{index:code-1}]' @@ -462,8 +460,7 @@ def test_numref_with_prefix1(app): assert '\\ref{\\detokenize{index:code-1}}' in result assert '\\ref{\\detokenize{baz:code22}}' in result assert ( - '\\hyperref[\\detokenize{index:fig1}]' - '{Figure:\\ref{\\detokenize{index:fig1}}}' + '\\hyperref[\\detokenize{index:fig1}]{Figure:\\ref{\\detokenize{index:fig1}}}' ) in result assert ( '\\hyperref[\\detokenize{baz:fig22}]{Figure\\ref{\\detokenize{baz:fig22}}}' @@ -473,8 +470,7 @@ def test_numref_with_prefix1(app): '{Tab\\_\\ref{\\detokenize{index:table-1}}}' ) in result assert ( - '\\hyperref[\\detokenize{baz:table22}]' - '{Table:\\ref{\\detokenize{baz:table22}}}' + '\\hyperref[\\detokenize{baz:table22}]{Table:\\ref{\\detokenize{baz:table22}}}' ) in result assert ( '\\hyperref[\\detokenize{index:code-1}]' @@ -540,8 +536,7 @@ def test_numref_with_prefix2(app): '{Tab\\_\\ref{\\detokenize{index:table-1}}:}' ) in result assert ( - '\\hyperref[\\detokenize{baz:table22}]' - '{Table:\\ref{\\detokenize{baz:table22}}}' + '\\hyperref[\\detokenize{baz:table22}]{Table:\\ref{\\detokenize{baz:table22}}}' ) in result assert ( '\\hyperref[\\detokenize{index:code-1}]{Code\\sphinxhyphen{}\\ref{\\detokenize{index:code-1}} ' @@ -552,8 +547,7 @@ def test_numref_with_prefix2(app): '{Code\\sphinxhyphen{}\\ref{\\detokenize{baz:code22}}}' ) in result assert ( - '\\hyperref[\\detokenize{foo:foo}]' - '{SECTION\\_\\ref{\\detokenize{foo:foo}}\\_}' + '\\hyperref[\\detokenize{foo:foo}]{SECTION\\_\\ref{\\detokenize{foo:foo}}\\_}' ) in result assert ( '\\hyperref[\\detokenize{bar:bar-a}]' @@ -590,8 +584,7 @@ def test_numref_with_language_ja(app): print(app.status.getvalue()) print(app.warning.getvalue()) assert ( - '\\hyperref[\\detokenize{index:fig1}]' - '{\u56f3 \\ref{\\detokenize{index:fig1}}}' + '\\hyperref[\\detokenize{index:fig1}]{\u56f3 \\ref{\\detokenize{index:fig1}}}' ) in result assert ( '\\hyperref[\\detokenize{baz:fig22}]{Figure\\ref{\\detokenize{baz:fig22}}}' @@ -601,8 +594,7 @@ def test_numref_with_language_ja(app): '{\u8868 \\ref{\\detokenize{index:table-1}}}' ) in result assert ( - '\\hyperref[\\detokenize{baz:table22}]' - '{Table:\\ref{\\detokenize{baz:table22}}}' + '\\hyperref[\\detokenize{baz:table22}]{Table:\\ref{\\detokenize{baz:table22}}}' ) in result assert ( '\\hyperref[\\detokenize{index:code-1}]' @@ -937,8 +929,7 @@ def test_footnote(app): 'numbered\n%\n\\end{footnote}' ) in result assert ( - '\\begin{footnote}[2]\\sphinxAtStartFootnote\nauto numbered\n%\n' - '\\end{footnote}' + '\\begin{footnote}[2]\\sphinxAtStartFootnote\nauto numbered\n%\n\\end{footnote}' ) in result assert ( '\\begin{footnote}[3]\\sphinxAtStartFootnote\nnamed\n%\n\\end{footnote}' @@ -1880,8 +1871,7 @@ def test_latex_nested_enumerated_list(app): result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert ( - '\\sphinxsetlistlabels{\\arabic}{enumi}{enumii}{}{.}%\n' - '\\setcounter{enumi}{4}\n' + '\\sphinxsetlistlabels{\\arabic}{enumi}{enumii}{}{.}%\n\\setcounter{enumi}{4}\n' ) in result assert ( '\\sphinxsetlistlabels{\\alph}{enumii}{enumiii}{}{.}%\n' @@ -2201,9 +2191,9 @@ def test_duplicated_labels_before_module(app): ): tex_label_name = 'index:' + rst_label_name.replace('_', '-') tex_label_code = r'\phantomsection\label{\detokenize{%s}}' % tex_label_name - assert ( - content.count(tex_label_code) == 1 - ), f'duplicated label: {tex_label_name!r}' + assert content.count(tex_label_code) == 1, ( + f'duplicated label: {tex_label_name!r}' + ) tested_labels.add(tex_label_code) # ensure that we did not forget any label to check diff --git a/tests/test_builders/test_build_text.py b/tests/test_builders/test_build_text.py index 8007ea23157..2023f6f9a53 100644 --- a/tests/test_builders/test_build_text.py +++ b/tests/test_builders/test_build_text.py @@ -44,14 +44,7 @@ def test_lineblock(app): # regression test for #1109: need empty line after line block app.build() result = (app.outdir / 'lineblock.txt').read_text(encoding='utf8') - expect = ( - '* one\n' - '\n' - ' line-block 1\n' - ' line-block 2\n' - '\n' - 'followed paragraph.\n' - ) + expect = '* one\n\n line-block 1\n line-block 2\n\nfollowed paragraph.\n' assert result == expect @@ -265,16 +258,5 @@ def test_secnums(app): assert lines[5] == '' assert lines[6] == ' * Sub Bb' doc2 = (app.outdir / 'doc2.txt').read_text(encoding='utf8') - expect = ( - 'Section B\n' - '*********\n' - '\n' - '\n' - 'Sub Ba\n' - '======\n' - '\n' - '\n' - 'Sub Bb\n' - '======\n' - ) + expect = 'Section B\n*********\n\n\nSub Ba\n======\n\n\nSub Bb\n======\n' assert doc2 == expect diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index f1d6c12a0fd..78b35b83ab1 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -463,16 +463,15 @@ def test_config_eol(logger, tmp_path): ) def test_builtin_conf(app): warnings = app.warning.getvalue() - assert ( - 'root_doc' - ) in warnings, 'override on builtin "root_doc" should raise a type warning' + assert 'root_doc' in warnings, ( + 'override on builtin "root_doc" should raise a type warning' + ) assert 'language' not in warnings, ( 'explicitly permitted override on builtin "language" should NOT raise ' 'a type warning' ) assert 'primary_domain' not in warnings, ( - 'override to None on builtin "primary_domain" should NOT raise a type ' - 'warning' + 'override to None on builtin "primary_domain" should NOT raise a type warning' ) diff --git a/tests/test_directives/test_directive_code.py b/tests/test_directives/test_directive_code.py index dacf8a3b334..0b81b65c1d8 100644 --- a/tests/test_directives/test_directive_code.py +++ b/tests/test_directives/test_directive_code.py @@ -536,12 +536,7 @@ def test_literalinclude_pydecorators(app): assert actual == expect actual = literal_include[2].text - expect = ( - '@function_decorator\n' - '@other_decorator()\n' - 'def the_function():\n' - ' pass\n' - ) + expect = '@function_decorator\n@other_decorator()\ndef the_function():\n pass\n' assert actual == expect diff --git a/tests/test_directives/test_directive_only.py b/tests/test_directives/test_directive_only.py index de9230c04da..297f304dfdb 100644 --- a/tests/test_directives/test_directive_only.py +++ b/tests/test_directives/test_directive_only.py @@ -28,9 +28,9 @@ def testsects(prefix, sects, indent=0): assert prefix == parent_num, f'Section out of place: {title!r}' for i, subsect in enumerate(sects[1]): num = subsect[0].split()[0] - assert re.match( - '[0-9]+[.0-9]*[.]', num - ), f'Unnumbered section: {subsect[0]!r}' + assert re.match('[0-9]+[.0-9]*[.]', num), ( + f'Unnumbered section: {subsect[0]!r}' + ) testsects(prefix + str(i + 1) + '.', subsect, indent + 4) app.build(filenames=[app.srcdir / 'only.rst']) @@ -41,6 +41,6 @@ def testsects(prefix, sects, indent=0): for i, s in enumerate(parts): testsects(str(i + 1) + '.', s, 4) actual_headings = '\n'.join(p[0] for p in parts) - assert ( - len(parts) == 4 - ), f'Expected 4 document level headings, got:\n{actual_headings}' + assert len(parts) == 4, ( + f'Expected 4 document level headings, got:\n{actual_headings}' + ) diff --git a/tests/test_domains/test_domain_c.py b/tests/test_domains/test_domain_c.py index 81451f29845..d90bcd0b7b5 100644 --- a/tests/test_domains/test_domain_c.py +++ b/tests/test_domains/test_domain_c.py @@ -923,11 +923,7 @@ def test_domain_c_parse_cvar(app): @pytest.mark.sphinx('html', testroot='root') def test_domain_c_parse_no_index_entry(app): - text = ( - '.. c:function:: void f()\n' - '.. c:function:: void g()\n' - ' :no-index-entry:\n' - ) + text = '.. c:function:: void f()\n.. c:function:: void g()\n :no-index-entry:\n' doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, desc, addnodes.index, desc)) assert_node( diff --git a/tests/test_domains/test_domain_cpp.py b/tests/test_domains/test_domain_cpp.py index 6a7a7778d8c..a177991a535 100644 --- a/tests/test_domains/test_domain_cpp.py +++ b/tests/test_domains/test_domain_cpp.py @@ -159,7 +159,7 @@ def make_id_v2(): id1 = make_id_v1() id2 = make_id_v2() - input = f"void f({type_.replace(' ', ' ')} arg)" + input = f'void f({type_.replace(" ", " ")} arg)' output = f'void f({type_} arg)' check('function', input, {1: id1, 2: id2}, output=output) @@ -167,7 +167,7 @@ def make_id_v2(): # try permutations of all components tcs = type_.split() for p in itertools.permutations(tcs): - input = f"void f({' '.join(p)} arg)" + input = f'void f({" ".join(p)} arg)' check('function', input, {1: id1, 2: id2}) @@ -1347,8 +1347,7 @@ def test_domain_cpp_ast_template_args(): # from breathe#218 check( 'function', - 'template ' - 'void allow(F *f, typename func::type tt)', + 'template void allow(F *f, typename func::type tt)', { 2: 'I0E5allowP1FN4funcI1F1BXG != 1EE4typeE', 3: 'I0E5allowP1FN4funcI1F1BXne1GL1EEE4typeE', @@ -1906,9 +1905,7 @@ def test_domain_cpp_build_intersphinx(tmp_path, app): @pytest.mark.sphinx('html', testroot='root') def test_domain_cpp_parse_no_index_entry(app): text = ( - '.. cpp:function:: void f()\n' - '.. cpp:function:: void g()\n' - ' :no-index-entry:\n' + '.. cpp:function:: void f()\n.. cpp:function:: void g()\n :no-index-entry:\n' ) doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, desc, addnodes.index, desc)) diff --git a/tests/test_domains/test_domain_py_pyobject.py b/tests/test_domains/test_domain_py_pyobject.py index 8691e0e08cc..67d91d731e4 100644 --- a/tests/test_domains/test_domain_py_pyobject.py +++ b/tests/test_domains/test_domain_py_pyobject.py @@ -165,10 +165,7 @@ def test_pydata_with_union_type_operator(app): @pytest.mark.sphinx('html', testroot='root') def test_pyobject_prefix(app): text = ( - '.. py:class:: Foo\n' - '\n' - ' .. py:method:: Foo.say\n' - ' .. py:method:: FooBar.say' + '.. py:class:: Foo\n\n .. py:method:: Foo.say\n .. py:method:: FooBar.say' ) doctree = restructuredtext.parse(app, text) assert_node( diff --git a/tests/test_environment/test_environment.py b/tests/test_environment/test_environment.py index 64487b8c3f6..10e98584342 100644 --- a/tests/test_environment/test_environment.py +++ b/tests/test_environment/test_environment.py @@ -41,11 +41,12 @@ def test_config_status(make_app, app_params): # incremental build (config entry changed) app3 = make_app(*args, confoverrides={'root_doc': 'indexx'}, **kwargs) fname = app3.srcdir / 'index.rst' + other_fname = app3.srcdir / 'indexx.rst' assert fname.is_file() - shutil.move(fname, fname[:-4] + 'x.rst') + shutil.move(fname, other_fname) assert app3.env.config_status == CONFIG_CHANGED app3.build() - shutil.move(fname[:-4] + 'x.rst', fname) + shutil.move(other_fname, fname) output = strip_colors(app3.status.getvalue()) assert 'The configuration has changed' in output assert "[config changed ('master_doc')] 1 added," in output diff --git a/tests/test_extensions/test_ext_apidoc.py b/tests/test_extensions/test_ext_apidoc.py index 3886617c742..7e4edbdfce8 100644 --- a/tests/test_extensions/test_ext_apidoc.py +++ b/tests/test_extensions/test_ext_apidoc.py @@ -372,9 +372,9 @@ def test_toc_all_references_should_exist_pep420_enabled(apidoc): missing_files.append(filename) all_missing = ', '.join(missing_files) - assert ( - len(missing_files) == 0 - ), f'File(s) referenced in TOC not found: {all_missing}\nTOC:\n{toc}' + assert len(missing_files) == 0, ( + f'File(s) referenced in TOC not found: {all_missing}\nTOC:\n{toc}' + ) @pytest.mark.apidoc( @@ -403,9 +403,9 @@ def test_toc_all_references_should_exist_pep420_disabled(apidoc): missing_files.append(filename) all_missing = ', '.join(missing_files) - assert ( - len(missing_files) == 0 - ), f'File(s) referenced in TOC not found: {all_missing}\nTOC:\n{toc}' + assert len(missing_files) == 0, ( + f'File(s) referenced in TOC not found: {all_missing}\nTOC:\n{toc}' + ) def extract_toc(path): diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index f2fb0c8ec11..dbe0fbe1d71 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -1558,9 +1558,9 @@ def entry( def preamble_lookup( self, doc: str, *, indent: int = 0, **options: Any ) -> list[str]: - assert ( - doc - ), f'enumeration class {self.target!r} should have an explicit docstring' + assert doc, ( + f'enumeration class {self.target!r} should have an explicit docstring' + ) args = self._preamble_args(functional_constructor=False) return self._preamble(doc=doc, args=args, indent=indent, **options) @@ -1568,9 +1568,9 @@ def preamble_lookup( def preamble_constructor( self, doc: str, *, indent: int = 0, **options: Any ) -> list[str]: - assert ( - doc - ), f'enumeration class {self.target!r} should have an explicit docstring' + assert doc, ( + f'enumeration class {self.target!r} should have an explicit docstring' + ) args = self._preamble_args(functional_constructor=True) return self._preamble(doc=doc, args=args, indent=indent, **options) diff --git a/tests/test_extensions/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py index 81b13860278..5a96afdd3e3 100644 --- a/tests/test_extensions/test_ext_autosummary.py +++ b/tests/test_extensions/test_ext_autosummary.py @@ -192,9 +192,9 @@ def handler(app, what, name, obj, options, lines): 'C.C2': 'This is a nested inner class docstring', } for key, expected in expected_values.items(): - assert ( - autosummary_items[key][2] == expected - ), f'Summary for {key} was {autosummary_items[key]!r} - expected {expected!r}' + assert autosummary_items[key][2] == expected, ( + f'Summary for {key} was {autosummary_items[key]!r} - expected {expected!r}' + ) # check an item in detail assert 'func' in autosummary_items @@ -566,11 +566,7 @@ def test_autosummary_generate(app): Foo = path.read_text(encoding='utf8') assert '.. automethod:: __init__' in Foo assert ( - ' .. autosummary::\n' - ' \n' - ' ~Foo.__init__\n' - ' ~Foo.bar\n' - ' \n' + ' .. autosummary::\n \n ~Foo.__init__\n ~Foo.bar\n \n' ) in Foo assert ( ' .. autosummary::\n' @@ -591,9 +587,7 @@ def test_autosummary_generate(app): path = app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.value.rst' Foo_value = path.read_text(encoding='utf8') assert ( - '.. currentmodule:: autosummary_dummy_module\n' - '\n' - '.. autoattribute:: Foo.value' + '.. currentmodule:: autosummary_dummy_module\n\n.. autoattribute:: Foo.value' ) in Foo_value path = app.srcdir / 'generated' / 'autosummary_dummy_module.qux.rst' @@ -820,17 +814,10 @@ def test_autosummary_module_all(app): ).read_text(encoding='utf8') assert ' .. autosummary::\n \n PublicBar\n \n' in module assert ( - ' .. autosummary::\n' - ' \n' - ' public_foo\n' - ' public_baz\n' - ' \n' + ' .. autosummary::\n \n public_foo\n public_baz\n \n' ) in module assert ( - '.. autosummary::\n' - ' :toctree:\n' - ' :recursive:\n\n' - ' extra_dummy_module\n' + '.. autosummary::\n :toctree:\n :recursive:\n\n extra_dummy_module\n' ) in module finally: sys.modules.pop('autosummary_dummy_package_all', None) diff --git a/tests/test_extensions/test_ext_graphviz.py b/tests/test_extensions/test_ext_graphviz.py index 929dcfa63a0..4be01caf023 100644 --- a/tests/test_extensions/test_ext_graphviz.py +++ b/tests/test_extensions/test_ext_graphviz.py @@ -168,12 +168,7 @@ def test_graphviz_parse_mapfile(): assert cmap.generate_clickable_map() == '' # normal graph - code = ( - 'digraph {\n' - ' foo [href="https://www.google.com/"];\n' - ' foo -> bar;\n' - '}\n' - ) + code = 'digraph {\n foo [href="https://www.google.com/"];\n foo -> bar;\n}\n' content = ( '\n' ':1: ' 'WARNING: Inline literal start-string without end-string. \\[docutils\\]\n' ) - assert re.search( - warning_expr, warnings - ), f'{warning_expr!r} did not match {warnings!r}' + assert re.search(warning_expr, warnings), ( + f'{warning_expr!r} did not match {warnings!r}' + ) @sphinx_intl @@ -196,21 +196,21 @@ def test_text_inconsistency_warnings(app): 'translated': "\\['`I18N WITH REFS INCONSISTENCY`_'\\]", } ) - assert re.search( - expected_warning_expr, warnings - ), f'{expected_warning_expr!r} did not match {warnings!r}' + assert re.search(expected_warning_expr, warnings), ( + f'{expected_warning_expr!r} did not match {warnings!r}' + ) expected_citation_ref_warning_expr = '.*/refs_inconsistency.txt:\\d+: WARNING: Citation \\[ref2\\] is not referenced.' - assert re.search( - expected_citation_ref_warning_expr, warnings - ), f'{expected_citation_ref_warning_expr!r} did not match {warnings!r}' + assert re.search(expected_citation_ref_warning_expr, warnings), ( + f'{expected_citation_ref_warning_expr!r} did not match {warnings!r}' + ) expected_citation_warning_expr = ( '.*/refs_inconsistency.txt:\\d+: WARNING: citation not found: ref3' ) - assert re.search( - expected_citation_warning_expr, warnings - ), f'{expected_citation_warning_expr!r} did not match {warnings!r}' + assert re.search(expected_citation_warning_expr, warnings), ( + f'{expected_citation_warning_expr!r} did not match {warnings!r}' + ) @sphinx_intl @@ -261,9 +261,9 @@ def test_text_literalblock_warnings(app): expected_warning_expr = ( '.*/literalblock.txt:\\d+: WARNING: Literal block expected; none found.' ) - assert re.search( - expected_warning_expr, warnings - ), f'{expected_warning_expr!r} did not match {warnings!r}' + assert re.search(expected_warning_expr, warnings), ( + f'{expected_warning_expr!r} did not match {warnings!r}' + ) @sphinx_intl @@ -347,16 +347,16 @@ def test_text_glossary_term_inconsistencies(app): " original: \\[':term:`Some term`', ':term:`Some other term`'\\]," " translated: \\[':term:`SOME NEW TERM`'\\] \\[i18n.inconsistent_references\\]\n" ) - assert re.search( - expected_warning_expr, warnings - ), f'{expected_warning_expr!r} did not match {warnings!r}' + assert re.search(expected_warning_expr, warnings), ( + f'{expected_warning_expr!r} did not match {warnings!r}' + ) expected_warning_expr = ( '.*/glossary_terms_inconsistency.txt:\\d+::1: ' "WARNING: term not in glossary: 'TERM NOT IN GLOSSARY'" ) - assert re.search( - expected_warning_expr, warnings - ), f'{expected_warning_expr!r} did not match {warnings!r}' + assert re.search(expected_warning_expr, warnings), ( + f'{expected_warning_expr!r} did not match {warnings!r}' + ) @sphinx_intl @@ -1051,9 +1051,9 @@ def wrap_nest(parenttag, childtag, keyword): wrap_nest('li', 'ul', 'SEE'), ] for expr in expected_exprs: - assert re.search( - expr, result, re.MULTILINE - ), f'{expr!r} did not match {result!r}' + assert re.search(expr, result, re.MULTILINE), ( + f'{expr!r} did not match {result!r}' + ) @sphinx_intl @@ -1185,9 +1185,9 @@ def test_xml_footnotes(app): warnings = getwarning(app.warning) warning_expr = '.*/footnote.xml:\\d*: SEVERE: Duplicate ID: ".*".\n' - assert not re.search( - warning_expr, warnings - ), f'{warning_expr!r} did match {warnings!r}' + assert not re.search(warning_expr, warnings), ( + f'{warning_expr!r} did match {warnings!r}' + ) @sphinx_intl @@ -1569,7 +1569,7 @@ def test_additional_targets_should_be_translated(app): # [literalblock.txt] result = (app.outdir / 'literalblock.html').read_text(encoding='utf8') - # basic literal bloc should be translated + # basic literal block should be translated expected_expr = ( 'THIS IS\n' 'LITERAL BLOCK' diff --git a/tests/test_markup/test_markup.py b/tests/test_markup/test_markup.py index 1a8a074151c..9eb0b83fa89 100644 --- a/tests/test_markup/test_markup.py +++ b/tests/test_markup/test_markup.py @@ -403,10 +403,7 @@ def get(name): 'verify', ':kbd:`-`', '

-

', - ( - '\\sphinxAtStartPar\n' - '\\sphinxkeyboard{\\sphinxupquote{\\sphinxhyphen{}}}' - ), + '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{\\sphinxhyphen{}}}', ), ( # kbd role diff --git a/tests/test_pycode/test_pycode_parser.py b/tests/test_pycode/test_pycode_parser.py index c12ac4d9935..7883a8de3cf 100644 --- a/tests/test_pycode/test_pycode_parser.py +++ b/tests/test_pycode/test_pycode_parser.py @@ -31,10 +31,7 @@ def test_comment_picker_basic(): def test_comment_picker_location(): # multiple "before" comments source = ( - '#: comment before assignment1\n' - '#:\n' - '#: comment before assignment2\n' - 'a = 1 + 1\n' + '#: comment before assignment1\n#:\n#: comment before assignment2\na = 1 + 1\n' ) parser = Parser(source) parser.parse() diff --git a/tests/test_search.py b/tests/test_search.py index f74755b3b5b..600f66cb9f6 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -454,9 +454,9 @@ def assert_is_sorted( elif isinstance(item, list): if not is_title_tuple_type(item) and path not in lists_not_to_sort: # sort nulls last; http://stackoverflow.com/questions/19868767/ - assert item == sorted( - item, key=lambda x: (x is None, x) - ), f'{err_path} is not sorted' + assert item == sorted(item, key=lambda x: (x is None, x)), ( + f'{err_path} is not sorted' + ) for i, child in enumerate(item): assert_is_sorted(child, f'{path}[{i}]') diff --git a/tests/test_transforms/test_transforms_reorder_nodes.py b/tests/test_transforms/test_transforms_reorder_nodes.py index 5540755523a..22fdcf742cf 100644 --- a/tests/test_transforms/test_transforms_reorder_nodes.py +++ b/tests/test_transforms/test_transforms_reorder_nodes.py @@ -10,14 +10,7 @@ @pytest.mark.sphinx('html', testroot='root') def test_transforms_reorder_consecutive_target_and_index_nodes_preserve_order(app): - text = ( - '.. index:: abc\n' - '.. index:: def\n' - '.. index:: ghi\n' - '.. index:: jkl\n' - '\n' - 'text\n' - ) + text = '.. index:: abc\n.. index:: def\n.. index:: ghi\n.. index:: jkl\n\ntext\n' doctree = restructuredtext.parse(app, text) assert_node( doctree, diff --git a/tests/test_util/test_util_i18n.py b/tests/test_util/test_util_i18n.py index 973b054a1d8..95d0909f90a 100644 --- a/tests/test_util/test_util_i18n.py +++ b/tests/test_util/test_util_i18n.py @@ -19,16 +19,16 @@ def test_catalog_info_for_file_and_path(): cat = i18n.CatalogInfo('path', 'domain', 'utf-8') assert cat.po_file == 'domain.po' assert cat.mo_file == 'domain.mo' - assert cat.po_path == str(Path('path', 'domain.po')) - assert cat.mo_path == str(Path('path', 'domain.mo')) + assert cat.po_path == Path('path', 'domain.po') + assert cat.mo_path == Path('path', 'domain.mo') def test_catalog_info_for_sub_domain_file_and_path(): cat = i18n.CatalogInfo('path', 'sub/domain', 'utf-8') assert cat.po_file == 'sub/domain.po' assert cat.mo_file == 'sub/domain.mo' - assert cat.po_path == str(Path('path', 'sub', 'domain.po')) - assert cat.mo_path == str(Path('path', 'sub', 'domain.mo')) + assert cat.po_path == Path('path', 'sub', 'domain.po') + assert cat.mo_path == Path('path', 'sub', 'domain.mo') def test_catalog_outdated(tmp_path): @@ -108,7 +108,7 @@ def test_format_date_timezone(): assert fd_gmt == '2016-08-07 05:11:17' assert fd_gmt == iso_gmt - iso_local = dt.astimezone().isoformat(' ').split('+')[0] + iso_local = dt.astimezone().isoformat(' ')[:19] # strip the timezone fd_local = i18n.format_date(fmt, date=dt, language='en', local_time=True) assert fd_local == iso_local assert fd_local != fd_gmt @@ -178,8 +178,8 @@ def test_CatalogRepository(tmp_path): # for language xx repo = i18n.CatalogRepository(tmp_path, ['loc1', 'loc2'], 'xx', 'utf-8') assert list(repo.locale_dirs) == [ - str(tmp_path / 'loc1'), - str(tmp_path / 'loc2'), + tmp_path / 'loc1', + tmp_path / 'loc2', ] assert all(isinstance(c, i18n.CatalogInfo) for c in repo.catalogs) assert sorted(c.domain for c in repo.catalogs) == [ diff --git a/tests/utils.py b/tests/utils.py index ba4640aa33a..9ebe62ec2e6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -22,7 +22,7 @@ # Generated with: # $ openssl req -new -x509 -days 3650 -nodes -out cert.pem \ # -keyout cert.pem -addext "subjectAltName = DNS:localhost" -TESTS_ROOT: Final[Path] = Path(__file__).parent +TESTS_ROOT: Final[Path] = Path(__file__).resolve().parent CERT_FILE: Final[str] = str(TESTS_ROOT / 'certs' / 'cert.pem') diff --git a/utils/babel_runner.py b/utils/babel_runner.py index 970d6f3bfca..8c27f20e3e8 100644 --- a/utils/babel_runner.py +++ b/utils/babel_runner.py @@ -33,7 +33,7 @@ from jinja2.ext import babel_extract as extract_jinja2 IS_CI = 'CI' in environ -ROOT = Path(__file__).parent.parent.resolve() +ROOT = Path(__file__).resolve().parent.parent TEX_DELIMITERS = { 'variable_start_string': '<%=', 'variable_end_string': '%>', diff --git a/utils/bump_version.py b/utils/bump_version.py index 81824fc0a1c..7894ac9f85e 100755 --- a/utils/bump_version.py +++ b/utils/bump_version.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: from collections.abc import Iterator, Sequence -script_dir = Path(__file__).parent +script_dir = Path(__file__).resolve().parent package_dir = script_dir.parent