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