Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/smart_open/doctools.py: 72%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

127 statements  

1# 

2# Copyright (C) 2019 Radim Rehurek <me@radimrehurek.com> 

3# 

4# This code is distributed under the terms and conditions 

5# from the MIT License (MIT). 

6# 

7 

8"""Common functions for working with docstrings. 

9 

10For internal use only. 

11""" 

12 

13from __future__ import annotations 

14 

15import contextlib 

16import inspect 

17import io 

18import sys 

19from pathlib import Path 

20from typing import TYPE_CHECKING, Any, cast 

21 

22from . import compression, transport 

23 

24if TYPE_CHECKING: 

25 from collections.abc import Callable 

26 

27# 

28# Python 3.13+ automatically trims docstrings (like inspect.cleandoc), 

29# so we need to adjust the placeholder and indentation accordingly. 

30# 

31if sys.version_info >= (3, 13): 

32 PLACEHOLDER = "smart_open/doctools.py magic goes here" 

33 LPAD = "" 

34else: 

35 PLACEHOLDER = " smart_open/doctools.py magic goes here" 

36 LPAD = " " 

37 

38 

39def extract_kwargs(docstring: str | None) -> list[list[Any]]: 

40 """Extract keyword argument documentation from a Google-style ``Args:`` section. 

41 

42 Args: 

43 docstring: The docstring to extract keyword arguments from. 

44 

45 Returns: 

46 A list of ``[name, type, description_lines]`` triples. ``type`` is 

47 always an empty string since Google-style docstrings in this codebase 

48 don't carry argument types, and ``description_lines`` is a list of 

49 lines. 

50 

51 Note: 

52 The implementation expects: 

53 

54 1. The parameters are under an ``Args:`` header 

55 2. Argument lines start with 4 spaces of indent (`` name: desc``) 

56 3. Continuation lines for a description are indented with 8 spaces 

57 4. The ``Args:`` section ends with an empty line or another section header. 

58 

59 Example: 

60 >>> docstring = '''The foo function. 

61 ... Args: 

62 ... bar: This parameter is the bar. 

63 ... baz: This parameter is the baz. 

64 ... 

65 ... ''' 

66 >>> kwargs = extract_kwargs(docstring) 

67 >>> kwargs[0] 

68 ['bar', '', ['This parameter is the bar.']] 

69 """ 

70 if not docstring: 

71 return [] 

72 

73 lines = inspect.cleandoc(docstring).split("\n") 

74 

75 for idx, line in enumerate(lines): 

76 if line.rstrip() == "Args:": 

77 lines = lines[idx + 1 :] 

78 break 

79 else: 

80 return [] 

81 

82 kwargs: list[list[Any]] = [] 

83 for line in lines: 

84 if not line.strip(): # stop at the first empty line encountered 

85 break 

86 # Argument line: 4-space indent, then ``name: description``. 

87 if line.startswith(" ") and not line.startswith(" "): 

88 stripped = line[4:] 

89 if ":" not in stripped: 

90 continue 

91 name, desc = stripped.split(":", 1) 

92 kwargs.append([name.strip(), "", [desc.strip()] if desc.strip() else []]) 

93 continue 

94 # Continuation line for the previous arg: 8-space (or deeper) indent. 

95 if line.startswith(" ") and kwargs: 

96 kwargs[-1][-1].append(line.strip()) 

97 

98 return kwargs 

99 

100 

101def to_docstring(kwargs: list[Any], lpad: str = "") -> str: 

102 """Reconstruct a docstring from keyword argument info. 

103 

104 Basically reverses :func:`extract_kwargs`. 

105 

106 Args: 

107 kwargs: Output from the :func:`extract_kwargs` function. 

108 lpad: Padding string (from the left). 

109 

110 Returns: 

111 The docstring snippet documenting the keyword arguments. 

112 

113 Example: 

114 >>> kwargs = [ 

115 ... ("bar", "str, optional", ["This parameter is the bar."]), 

116 ... ("baz", "int, optional", ["This parameter is the baz."]), 

117 ... ] 

118 >>> print(to_docstring(kwargs), end="") 

119 bar: str, optional 

120 This parameter is the bar. 

121 baz: int, optional 

122 This parameter is the baz. 

123 """ 

124 buf = io.StringIO() 

125 for name, type_, description in kwargs: 

126 if type_: 

127 buf.write(f"{lpad}{name}: {type_}\n") 

128 else: 

129 buf.write(f"{lpad}{name}:\n") 

130 for line in description: 

131 buf.write(f"{lpad} {line}\n") 

132 return buf.getvalue() 

133 

134 

135def extract_examples_from_readme(indent: str | None = None) -> str: 

136 """Extract examples from this project's README.md file. 

137 

138 Args: 

139 indent: Prepend each line with this string. Should contain some number 

140 of spaces. 

141 

142 Returns: 

143 The examples as a single string. 

144 

145 Note: 

146 Quite fragile, depends on the example markers and the fenced code block 

147 inside the README.md file. 

148 """ 

149 if indent is None: 

150 indent = LPAD 

151 readme_path = Path(__file__).resolve().parent.parent / "README.md" 

152 try: 

153 text = readme_path.read_text(encoding="utf-8") 

154 body = text.split("<!-- doctools_before_examples -->", 1)[1] 

155 body = body.split("<!-- doctools_after_examples -->", 1)[0] 

156 # keep only the contents of the ```python ... ``` fenced code block 

157 body = body.split("```python", 1)[1].rsplit("```", 1)[0] 

158 lines = body.strip("\n").split("\n") 

159 return "".join(indent + line + "\n" for line in lines) 

160 except Exception: # noqa: BLE001 # README parsing is best-effort; any failure falls back gracefully 

161 return indent + "See README.md" 

162 

163 

164def tweak_open_docstring(f: Callable[..., Any]) -> None: 

165 """Inject transport, compression and example sections into ``f``'s docstring.""" 

166 buf = io.StringIO() 

167 seen = set() 

168 

169 root_path = Path(__file__).parent.parent 

170 body_pad = LPAD + " " 

171 

172 print(f"{LPAD}Transports:", file=buf) 

173 print(file=buf) 

174 for scheme, submodule in sorted(transport._REGISTRY.items()): # noqa: SLF001 # intra-package coupling 

175 if scheme == transport.NO_SCHEME or submodule in seen: 

176 continue 

177 seen.add(submodule) 

178 

179 try: 

180 schemes = submodule.SCHEMES 

181 except AttributeError: 

182 schemes = [scheme] 

183 

184 relpath = Path(cast("str", submodule.__file__)).relative_to(root_path) 

185 heading = "{} ({})".format("/".join(schemes), relpath) 

186 print(f"{body_pad}{heading}", file=buf) 

187 print(f"{body_pad}{'~' * len(heading)}", file=buf) 

188 print(f"{body_pad}{(submodule.__doc__ or '').split(chr(10))[0]}", file=buf) 

189 print(file=buf) 

190 

191 kwargs = extract_kwargs(submodule.open.__doc__) 

192 if kwargs: 

193 print(to_docstring(kwargs, lpad=body_pad), file=buf) 

194 

195 print(f"{LPAD}Examples:", file=buf) 

196 print(file=buf) 

197 print(extract_examples_from_readme(indent=body_pad), file=buf) 

198 

199 print(f"{LPAD}Codecs:", file=buf) 

200 print(file=buf) 

201 print(f"{body_pad}smart_open supports transparent compression and decompression for files", file=buf) 

202 print(f"{body_pad}with the following extensions:", file=buf) 

203 print(file=buf) 

204 for extension in compression.get_supported_extensions(): 

205 print(f"{body_pad}* {extension}", file=buf) 

206 print(file=buf) 

207 print(f"{body_pad}The codec is selected based on the file extension.", file=buf) 

208 

209 # 

210 # The docstring can be None if -OO was passed to the interpreter. 

211 # 

212 if f.__doc__: 

213 f.__doc__ = f.__doc__.replace(PLACEHOLDER, buf.getvalue()) 

214 

215 

216def tweak_parse_uri_docstring(f: Callable[..., Any]) -> None: 

217 """Inject supported schemes and example URIs into ``f``'s docstring.""" 

218 buf = io.StringIO() 

219 seen = set() 

220 schemes = [] 

221 examples = [] 

222 

223 for scheme, submodule in sorted(transport._REGISTRY.items()): # noqa: SLF001 # intra-package coupling 

224 if scheme == transport.NO_SCHEME or submodule in seen: 

225 continue 

226 

227 seen.add(submodule) 

228 

229 with contextlib.suppress(AttributeError): 

230 examples.extend(submodule.URI_EXAMPLES) 

231 

232 try: 

233 schemes.extend(submodule.SCHEMES) 

234 except AttributeError: 

235 schemes.append(scheme) 

236 

237 body_pad = LPAD + " " 

238 

239 print(f"{LPAD}Schemes:", file=buf) 

240 print(file=buf) 

241 for scheme in schemes: 

242 print(f"{body_pad}* {scheme}", file=buf) 

243 print(file=buf) 

244 print(f"{LPAD}Examples:", file=buf) 

245 print(file=buf) 

246 for example in examples: 

247 print(f"{body_pad}* {example}", file=buf) 

248 

249 if f.__doc__: 

250 f.__doc__ = f.__doc__.replace(PLACEHOLDER, buf.getvalue())