From c7a45cc69ba162ababde4eb04d44a745b0fd40d1 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 18 Aug 2023 18:59:30 +0100 Subject: [PATCH 1/3] Support string methods on path objects --- sphinx/application.py | 10 +++++----- sphinx/builders/html/_assets.py | 4 ++-- sphinx/environment/__init__.py | 6 +++--- sphinx/jinja2glue.py | 3 +-- sphinx/testing/util.py | 7 ++++--- sphinx/util/_pathlib.py | 34 +++++++++++++++++++++++++++++++++ 6 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 sphinx/util/_pathlib.py diff --git a/sphinx/application.py b/sphinx/application.py index 73c157dea29..d5fbaa9f30a 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -13,7 +13,6 @@ from collections.abc import Sequence # NoQA: TCH003 from io import StringIO from os import path -from pathlib import Path from typing import IO, TYPE_CHECKING, Any, Callable from docutils.nodes import TextElement # NoQA: TCH002 @@ -32,6 +31,7 @@ from sphinx.project import Project from sphinx.registry import SphinxComponentRegistry from sphinx.util import docutils, logging +from sphinx.util._pathlib import _StrPath from sphinx.util.build_phase import BuildPhase from sphinx.util.console import bold # type: ignore[attr-defined] from sphinx.util.display import progress_message @@ -149,9 +149,9 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st self.registry = SphinxComponentRegistry() # validate provided directories - self.srcdir = Path(srcdir).resolve() - self.outdir = Path(outdir).resolve() - self.doctreedir = Path(doctreedir).resolve() + self.srcdir = _StrPath(srcdir).resolve() + self.outdir = _StrPath(outdir).resolve() + self.doctreedir = _StrPath(doctreedir).resolve() if not path.isdir(self.srcdir): raise ApplicationError(__('Cannot find source directory (%s)') % @@ -207,7 +207,7 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st self.confdir = self.srcdir self.config = Config({}, confoverrides or {}) else: - self.confdir = Path(confdir).resolve() + self.confdir = _StrPath(confdir).resolve() self.config = Config.read(self.confdir, confoverrides or {}, self.tags) # initialize some limited config variables before initialize i18n and loading diff --git a/sphinx/builders/html/_assets.py b/sphinx/builders/html/_assets.py index a72c5000bbc..2fb8dcc28f8 100644 --- a/sphinx/builders/html/_assets.py +++ b/sphinx/builders/html/_assets.py @@ -9,7 +9,7 @@ from sphinx.errors import ThemeError if TYPE_CHECKING: - from pathlib import Path + from sphinx.util._pathlib import _StrPath class _CascadingStyleSheet: @@ -124,7 +124,7 @@ def __getitem__(self, key): return os.fspath(self.filename)[key] -def _file_checksum(outdir: Path, filename: str | os.PathLike[str]) -> str: +def _file_checksum(outdir: _StrPath, filename: str | os.PathLike[str]) -> str: filename = os.fspath(filename) # Don't generate checksums for HTTP URIs if '://' in filename: diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 9b9e9dd306a..a6f97ee0137 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -24,7 +24,6 @@ if TYPE_CHECKING: from collections.abc import Generator, Iterator - from pathlib import Path from docutils import nodes from docutils.nodes import Node @@ -35,6 +34,7 @@ from sphinx.domains import Domain from sphinx.events import EventManager from sphinx.project import Project + from sphinx.util._pathlib import _StrPath logger = logging.getLogger(__name__) @@ -148,8 +148,8 @@ class BuildEnvironment: def __init__(self, app: Sphinx): 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 = '' diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py index 88040c6feac..cfe92b07538 100644 --- a/sphinx/jinja2glue.py +++ b/sphinx/jinja2glue.py @@ -2,7 +2,6 @@ from __future__ import annotations -import pathlib from os import path from pprint import pformat from typing import TYPE_CHECKING, Any, Callable @@ -122,7 +121,7 @@ class SphinxFileSystemLoader(FileSystemLoader): def get_source(self, environment: Environment, template: str) -> tuple[str, str, Callable]: for searchpath in self.searchpath: - filename = str(pathlib.Path(searchpath, template)) + filename = path.join(searchpath, template) f = open_if_exists(filename) if f is not None: break diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index e76d4010c49..de8f523f95b 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -17,10 +17,11 @@ if TYPE_CHECKING: from io import StringIO - from pathlib import Path from docutils.nodes import Node + from sphinx.util._pathlib import _StrPath + __all__ = 'SphinxTestApp', 'SphinxTestAppWrapperForSkipBuilding' @@ -81,8 +82,8 @@ class SphinxTestApp(application.Sphinx): def __init__( self, buildername: str = 'html', - srcdir: Path | None = None, - builddir: Path | None = None, + srcdir: _StrPath | None = None, + builddir: _StrPath | None = None, freshenv: bool = False, confoverrides: dict | None = None, status: IO | None = None, diff --git a/sphinx/util/_pathlib.py b/sphinx/util/_pathlib.py new file mode 100644 index 00000000000..e73e104b5f2 --- /dev/null +++ b/sphinx/util/_pathlib.py @@ -0,0 +1,34 @@ +"""What follows is awful and will be gone in Sphinx 8""" + +from __future__ import annotations + +import sys +import warnings +from pathlib import Path, PosixPath, WindowsPath + +from sphinx.deprecation import RemovedInSphinx80Warning + +_STR_METHODS = frozenset(dir('')) +_PATH_NAME = Path().__class__.__name__ + + +if sys.platform == 'win32': + class _StrPath(WindowsPath): + def __getattr__(self, item): + if item in _STR_METHODS: + warnings.warn('Sphinx 8 will drop support for representing paths as strings. ' + 'Use "pathlib.Path" or "os.fspath" instead.', + RemovedInSphinx80Warning, stacklevel=2) + return getattr(str(self), item) + msg = f'{_PATH_NAME!r} has no attribute {item!r}' + raise AttributeError(msg) +else: + class _StrPath(PosixPath): + def __getattr__(self, item): + if item in _STR_METHODS: + warnings.warn('Sphinx 8 will drop support for representing paths as strings. ' + 'Use "pathlib.Path" or "os.fspath" instead.', + RemovedInSphinx80Warning, stacklevel=2) + return getattr(str(self), item) + msg = f'{_PATH_NAME!r} has no attribute {item!r}' + raise AttributeError(msg) From 955b553c690b51c010d7136c5bb16ae7e5639855 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 23 Aug 2023 16:57:02 +0100 Subject: [PATCH 2/3] replace --- sphinx/util/_pathlib.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sphinx/util/_pathlib.py b/sphinx/util/_pathlib.py index e73e104b5f2..91a1d2cabe2 100644 --- a/sphinx/util/_pathlib.py +++ b/sphinx/util/_pathlib.py @@ -8,12 +8,20 @@ from sphinx.deprecation import RemovedInSphinx80Warning -_STR_METHODS = frozenset(dir('')) +_STR_METHODS = frozenset(str.__dict__) _PATH_NAME = Path().__class__.__name__ if sys.platform == 'win32': class _StrPath(WindowsPath): + def replace(self, old, new, count=-1, /): + # replace exists in both Path and str; + # in Path it makes filesystem changes, so we use the safer str version + warnings.warn('Sphinx 8 will drop support for representing paths as strings. ' + 'Use "pathlib.Path" or "os.fspath" instead.', + RemovedInSphinx80Warning, stacklevel=2) + return str(self).replace(old, new, count) + def __getattr__(self, item): if item in _STR_METHODS: warnings.warn('Sphinx 8 will drop support for representing paths as strings. ' @@ -24,6 +32,12 @@ def __getattr__(self, item): raise AttributeError(msg) else: class _StrPath(PosixPath): + def replace(self, old, new, count=-1, /): + warnings.warn('Sphinx 8 will drop support for representing paths as strings. ' + 'Use "pathlib.Path" or "os.fspath" instead.', + RemovedInSphinx80Warning, stacklevel=2) + return str(self).replace(old, new, count) + def __getattr__(self, item): if item in _STR_METHODS: warnings.warn('Sphinx 8 will drop support for representing paths as strings. ' From 597d16aeebba9297b6f5544ce5c15b8f52bfc669 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 23 Aug 2023 17:28:01 +0100 Subject: [PATCH 3/3] CHANGES --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index 738e9018f03..3ea34fa41d2 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,10 @@ Bugs fixed * Fixed a type error in ``SingleFileHTMLBuilder._get_local_toctree``, ``includehidden`` may be passed as a string or a boolean. * Fix ``:noindex:`` for ``PyModule`` and JSModule``. +* Restore support string methods on path objects. + This is deprecated and will be removed in Sphinx 8. + Use :py:func`os.fspath` to convert :py:class:~`pathlib.Path` objects to strings, + or :py:class:~`pathlib.Path`'s methods to work with path objects. Release 7.2.1 (released Aug 17, 2023) =====================================