Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support string methods on path objects #11619

Merged
merged 3 commits into from Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES
Expand Up @@ -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)
=====================================
Expand Down
10 changes: 5 additions & 5 deletions sphinx/application.py
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)') %
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions sphinx/builders/html/_assets.py
Expand Up @@ -9,7 +9,7 @@
from sphinx.errors import ThemeError

if TYPE_CHECKING:
from pathlib import Path
from sphinx.util._pathlib import _StrPath


class _CascadingStyleSheet:
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions sphinx/environment/__init__.py
Expand Up @@ -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
Expand All @@ -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__)

Expand Down Expand Up @@ -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 = ''
Expand Down
3 changes: 1 addition & 2 deletions sphinx/jinja2glue.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions sphinx/testing/util.py
Expand Up @@ -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'


Expand Down Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions sphinx/util/_pathlib.py
@@ -0,0 +1,48 @@
"""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(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. '
'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 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. '
'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)