Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b511537

Browse files
authored
_StrPath is dead; long live _StrPath (sphinx-doc#12690)
1 parent a80a11d commit b511537

9 files changed

Lines changed: 161 additions & 22 deletions

File tree

CHANGES.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,6 @@ Incompatible changes
6868
* #12096: Do not overwrite user-supplied files when copying assets
6969
unless forced with ``force=True``.
7070
Patch by Adam Turner.
71-
* #12650: Remove support for string methods on :py:class:`~pathlib.Path` objects.
72-
Use :py:func:`os.fspath` to convert :py:class:`~pathlib.Path` objects to strings,
73-
or :py:class:`~pathlib.Path`'s methods to work with path objects.
74-
Patch by Adam Turner.
7571
* #12646: Remove :py:func:`!sphinx.util.inspect.isNewType`.
7672
Patch by Adam Turner.
7773
* Remove the long-deprecated (since Sphinx 2) alias
@@ -88,6 +84,11 @@ Deprecated
8884
to ``sphinx.ext.intersphinx.validate_intersphinx_mapping``.
8985
The old name will be removed in Sphinx 10.
9086
Patch by Adam Turner.
87+
* #12650, #12686, #12690: Extend the deprecation for string methods on
88+
:py:class:`~pathlib.Path` objects to Sphinx 9.
89+
Use :py:func:`os.fspath` to convert :py:class:`~pathlib.Path` objects to strings,
90+
or :py:class:`~pathlib.Path`'s methods to work with path objects.
91+
Patch by Adam Turner.
9192

9293
Features added
9394
--------------

doc/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,12 @@
179179
('js:func', 'number'),
180180
('js:func', 'string'),
181181
('py:attr', 'srcline'),
182+
('py:class', '_StrPath'), # sphinx.environment.BuildEnvironment.doc2path
182183
('py:class', 'Element'), # sphinx.domains.Domain
183184
('py:class', 'Documenter'), # sphinx.application.Sphinx.add_autodocumenter
184185
('py:class', 'IndexEntry'), # sphinx.domains.IndexEntry
185186
('py:class', 'Node'), # sphinx.domains.Domain
186187
('py:class', 'NullTranslations'), # gettext.NullTranslations
187-
('py:class', 'Path'), # sphinx.environment.BuildEnvironment.doc2path
188188
('py:class', 'RoleFunction'), # sphinx.domains.Domain
189189
('py:class', 'RSTState'), # sphinx.utils.parsing.nested_parse_to_nodes
190190
('py:class', 'Theme'), # sphinx.application.TemplateBridge
@@ -213,6 +213,7 @@
213213
('py:class', 'sphinx.roles.XRefRole'),
214214
('py:class', 'sphinx.search.SearchLanguage'),
215215
('py:class', 'sphinx.theming.Theme'),
216+
('py:class', 'sphinx.util._pathlib._StrPath'), # sphinx.project.Project.doc2path
216217
('py:class', 'sphinxcontrib.websupport.errors.DocumentNotFoundError'),
217218
('py:class', 'sphinxcontrib.websupport.errors.UserNotAuthorizedError'),
218219
('py:exc', 'docutils.nodes.SkipNode'),

sphinx/application.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from collections.abc import Callable, Collection, Sequence # NoQA: TCH003
1414
from io import StringIO
1515
from os import path
16-
from pathlib import Path
1716
from typing import IO, TYPE_CHECKING, Any, Literal
1817

1918
from docutils.nodes import TextElement # NoQA: TCH002
@@ -32,6 +31,7 @@
3231
from sphinx.project import Project
3332
from sphinx.registry import SphinxComponentRegistry
3433
from sphinx.util import docutils, logging
34+
from sphinx.util._pathlib import _StrPath
3535
from sphinx.util.build_phase import BuildPhase
3636
from sphinx.util.console import bold
3737
from sphinx.util.display import progress_message
@@ -173,9 +173,9 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st
173173
self.registry = SphinxComponentRegistry()
174174

175175
# validate provided directories
176-
self.srcdir = Path(srcdir).resolve()
177-
self.outdir = Path(outdir).resolve()
178-
self.doctreedir = Path(doctreedir).resolve()
176+
self.srcdir = _StrPath(srcdir).resolve()
177+
self.outdir = _StrPath(outdir).resolve()
178+
self.doctreedir = _StrPath(doctreedir).resolve()
179179

180180
if not path.isdir(self.srcdir):
181181
raise ApplicationError(__('Cannot find source directory (%s)') %
@@ -231,7 +231,7 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st
231231
self.confdir = self.srcdir
232232
self.config = Config({}, confoverrides or {})
233233
else:
234-
self.confdir = Path(confdir).resolve()
234+
self.confdir = _StrPath(confdir).resolve()
235235
self.config = Config.read(self.confdir, confoverrides or {}, self.tags)
236236

237237
# set up translation infrastructure

sphinx/builders/linkcheck.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@
2828

2929
if TYPE_CHECKING:
3030
from collections.abc import Callable, Iterator
31-
from pathlib import Path
3231
from typing import Any
3332

3433
from requests import Response
3534

3635
from sphinx.application import Sphinx
3736
from sphinx.config import Config
37+
from sphinx.util._pathlib import _StrPath
3838
from sphinx.util.typing import ExtensionMetadata
3939

4040
logger = logging.getLogger(__name__)
@@ -150,7 +150,7 @@ def write_linkstat(self, data: dict[str, str | int]) -> None:
150150
self.json_outfile.write(json.dumps(data))
151151
self.json_outfile.write('\n')
152152

153-
def write_entry(self, what: str, docname: str, filename: Path, line: int,
153+
def write_entry(self, what: str, docname: str, filename: _StrPath, line: int,
154154
uri: str) -> None:
155155
self.txt_outfile.write(f'{filename}:{line}: [{what}] {uri}\n')
156156

@@ -226,7 +226,7 @@ def _add_uri(self, uri: str, node: nodes.Element) -> None:
226226
class Hyperlink(NamedTuple):
227227
uri: str
228228
docname: str
229-
docpath: Path
229+
docpath: _StrPath
230230
lineno: int
231231

232232

sphinx/environment/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from sphinx.domains import Domain
3737
from sphinx.events import EventManager
3838
from sphinx.project import Project
39+
from sphinx.util._pathlib import _StrPath
3940

4041
logger = logging.getLogger(__name__)
4142

@@ -413,7 +414,7 @@ def path2doc(self, filename: str | os.PathLike[str]) -> str | None:
413414
"""
414415
return self.project.path2doc(filename)
415416

416-
def doc2path(self, docname: str, base: bool = True) -> Path:
417+
def doc2path(self, docname: str, base: bool = True) -> _StrPath:
417418
"""Return the filename for the document name.
418419
419420
If *base* is True, return absolute path under self.srcdir.

sphinx/project.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from sphinx.locale import __
1111
from sphinx.util import logging
12+
from sphinx.util._pathlib import _StrPath
1213
from sphinx.util.matching import get_matching_files
1314
from sphinx.util.osutil import path_stabilize
1415

@@ -24,7 +25,7 @@ class Project:
2425

2526
def __init__(self, srcdir: str | os.PathLike[str], source_suffix: Iterable[str]) -> None:
2627
#: Source directory.
27-
self.srcdir = Path(srcdir)
28+
self.srcdir = _StrPath(srcdir)
2829

2930
#: source_suffix. Same as :confval:`source_suffix`.
3031
self.source_suffix = tuple(source_suffix)
@@ -106,7 +107,7 @@ def path2doc(self, filename: str | os.PathLike[str]) -> str | None:
106107
# the file does not have a docname
107108
return None
108109

109-
def doc2path(self, docname: str, absolute: bool) -> Path:
110+
def doc2path(self, docname: str, absolute: bool) -> _StrPath:
110111
"""Return the filename for the document name.
111112
112113
If *absolute* is True, return as an absolute path.
@@ -119,5 +120,5 @@ def doc2path(self, docname: str, absolute: bool) -> Path:
119120
filename = Path(docname + self._first_source_suffix)
120121

121122
if absolute:
122-
return self.srcdir / filename
123-
return filename
123+
return _StrPath(self.srcdir / filename)
124+
return _StrPath(filename)

sphinx/util/_pathlib.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""What follows is awful and will be gone in Sphinx 9.
2+
3+
Instances of _StrPath should not be constructed except in Sphinx itself.
4+
Consumers of Sphinx APIs should prefer using ``pathlib.Path`` objects
5+
where possible. _StrPath objects can be treated as equivalent to ``Path``,
6+
save that ``_StrPath.replace`` is overriden with ``str.replace``.
7+
8+
To continue treating path-like objects as strings, use ``os.fspath``,
9+
or explicit string coercion.
10+
11+
In Sphinx 9, ``Path`` objects will be expected and returned in all instances
12+
that ``_StrPath`` is currently used.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import sys
18+
import warnings
19+
from pathlib import Path, PosixPath, PurePath, WindowsPath
20+
from typing import Any
21+
22+
from sphinx.deprecation import RemovedInSphinx90Warning
23+
24+
_STR_METHODS = frozenset(str.__dict__)
25+
_PATH_NAME = Path().__class__.__name__
26+
27+
_MSG = (
28+
'Sphinx 8 will drop support for representing paths as strings. '
29+
'Use "pathlib.Path" or "os.fspath" instead.'
30+
)
31+
32+
# https://docs.python.org/3/library/stdtypes.html#typesseq-common
33+
# https://docs.python.org/3/library/stdtypes.html#string-methods
34+
35+
if sys.platform == 'win32':
36+
class _StrPath(WindowsPath):
37+
def replace( # type: ignore[override]
38+
self, old: str, new: str, count: int = -1, /,
39+
) -> str:
40+
# replace exists in both Path and str;
41+
# in Path it makes filesystem changes, so we use the safer str version
42+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
43+
return self.__str__().replace(old, new, count) # NoQA: PLC2801
44+
45+
def __getattr__(self, item: str) -> Any:
46+
if item in _STR_METHODS:
47+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
48+
return getattr(self.__str__(), item)
49+
msg = f'{_PATH_NAME!r} has no attribute {item!r}'
50+
raise AttributeError(msg)
51+
52+
def __add__(self, other: str) -> str:
53+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
54+
return self.__str__() + other
55+
56+
def __bool__(self) -> bool:
57+
if not self.__str__():
58+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
59+
return False
60+
return True
61+
62+
def __contains__(self, item: str) -> bool:
63+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
64+
return item in self.__str__()
65+
66+
def __eq__(self, other: object) -> bool:
67+
if isinstance(other, PurePath):
68+
return super().__eq__(other)
69+
if isinstance(other, str):
70+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
71+
return self.__str__() == other
72+
return NotImplemented
73+
74+
def __hash__(self) -> int:
75+
return super().__hash__()
76+
77+
def __getitem__(self, item: int | slice) -> str:
78+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
79+
return self.__str__()[item]
80+
81+
def __len__(self) -> int:
82+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
83+
return len(self.__str__())
84+
else:
85+
class _StrPath(PosixPath):
86+
def replace( # type: ignore[override]
87+
self, old: str, new: str, count: int = -1, /,
88+
) -> str:
89+
# replace exists in both Path and str;
90+
# in Path it makes filesystem changes, so we use the safer str version
91+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
92+
return self.__str__().replace(old, new, count) # NoQA: PLC2801
93+
94+
def __getattr__(self, item: str) -> Any:
95+
if item in _STR_METHODS:
96+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
97+
return getattr(self.__str__(), item)
98+
msg = f'{_PATH_NAME!r} has no attribute {item!r}'
99+
raise AttributeError(msg)
100+
101+
def __add__(self, other: str) -> str:
102+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
103+
return self.__str__() + other
104+
105+
def __bool__(self) -> bool:
106+
if not self.__str__():
107+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
108+
return False
109+
return True
110+
111+
def __contains__(self, item: str) -> bool:
112+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
113+
return item in self.__str__()
114+
115+
def __eq__(self, other: object) -> bool:
116+
if isinstance(other, PurePath):
117+
return super().__eq__(other)
118+
if isinstance(other, str):
119+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
120+
return self.__str__() == other
121+
return NotImplemented
122+
123+
def __hash__(self) -> int:
124+
return super().__hash__()
125+
126+
def __getitem__(self, item: int | slice) -> str:
127+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
128+
return self.__str__()[item]
129+
130+
def __len__(self) -> int:
131+
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
132+
return len(self.__str__())

tests/test_builders/test_build_html.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pytest
1111

1212
from sphinx.builders.html import validate_html_extra_path, validate_html_static_path
13+
from sphinx.deprecation import RemovedInSphinx90Warning
1314
from sphinx.errors import ConfigError
1415
from sphinx.util.console import strip_colors
1516
from sphinx.util.inventory import InventoryFile
@@ -329,7 +330,8 @@ def test_validate_html_extra_path(app):
329330
app.outdir, # outdir
330331
app.outdir / '_static', # inside outdir
331332
]
332-
validate_html_extra_path(app, app.config)
333+
with pytest.warns(RemovedInSphinx90Warning, match='Use "pathlib.Path" or "os.fspath" instead'):
334+
validate_html_extra_path(app, app.config)
333335
assert app.config.html_extra_path == ['_static']
334336

335337

@@ -342,7 +344,8 @@ def test_validate_html_static_path(app):
342344
app.outdir, # outdir
343345
app.outdir / '_static', # inside outdir
344346
]
345-
validate_html_static_path(app, app.config)
347+
with pytest.warns(RemovedInSphinx90Warning, match='Use "pathlib.Path" or "os.fspath" instead'):
348+
validate_html_static_path(app, app.config)
346349
assert app.config.html_static_path == ['_static']
347350

348351

tests/test_builders/test_build_linkcheck.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import wsgiref.handlers
1111
from base64 import b64encode
1212
from http.server import BaseHTTPRequestHandler
13-
from pathlib import Path
1413
from queue import Queue
1514
from typing import TYPE_CHECKING
1615
from unittest import mock
@@ -29,6 +28,7 @@
2928
compile_linkcheck_allowed_redirects,
3029
)
3130
from sphinx.util import requests
31+
from sphinx.util._pathlib import _StrPath
3232
from sphinx.util.console import strip_colors
3333

3434
from tests.utils import CERT_FILE, serve_application
@@ -1061,7 +1061,7 @@ def test_connection_contention(get_adapter, app, capsys):
10611061
wqueue: Queue[CheckRequest] = Queue()
10621062
rqueue: Queue[CheckResult] = Queue()
10631063
for _ in range(link_count):
1064-
wqueue.put(CheckRequest(0, Hyperlink(f"http://{address}", "test", Path("test.rst"), 1)))
1064+
wqueue.put(CheckRequest(0, Hyperlink(f"http://{address}", "test", _StrPath("test.rst"), 1)))
10651065

10661066
begin = time.time()
10671067
checked: list[CheckResult] = []

0 commit comments

Comments
 (0)