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
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
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#
8"""Common functions for working with docstrings.
10For internal use only.
11"""
13from __future__ import annotations
15import contextlib
16import inspect
17import io
18import sys
19from pathlib import Path
20from typing import TYPE_CHECKING, Any, cast
22from . import compression, transport
24if TYPE_CHECKING:
25 from collections.abc import Callable
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 = " "
39def extract_kwargs(docstring: str | None) -> list[list[Any]]:
40 """Extract keyword argument documentation from a Google-style ``Args:`` section.
42 Args:
43 docstring: The docstring to extract keyword arguments from.
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.
51 Note:
52 The implementation expects:
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.
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 []
73 lines = inspect.cleandoc(docstring).split("\n")
75 for idx, line in enumerate(lines):
76 if line.rstrip() == "Args:":
77 lines = lines[idx + 1 :]
78 break
79 else:
80 return []
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())
98 return kwargs
101def to_docstring(kwargs: list[Any], lpad: str = "") -> str:
102 """Reconstruct a docstring from keyword argument info.
104 Basically reverses :func:`extract_kwargs`.
106 Args:
107 kwargs: Output from the :func:`extract_kwargs` function.
108 lpad: Padding string (from the left).
110 Returns:
111 The docstring snippet documenting the keyword arguments.
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()
135def extract_examples_from_readme(indent: str | None = None) -> str:
136 """Extract examples from this project's README.md file.
138 Args:
139 indent: Prepend each line with this string. Should contain some number
140 of spaces.
142 Returns:
143 The examples as a single string.
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"
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()
169 root_path = Path(__file__).parent.parent
170 body_pad = LPAD + " "
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)
179 try:
180 schemes = submodule.SCHEMES
181 except AttributeError:
182 schemes = [scheme]
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)
191 kwargs = extract_kwargs(submodule.open.__doc__)
192 if kwargs:
193 print(to_docstring(kwargs, lpad=body_pad), file=buf)
195 print(f"{LPAD}Examples:", file=buf)
196 print(file=buf)
197 print(extract_examples_from_readme(indent=body_pad), file=buf)
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)
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())
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 = []
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
227 seen.add(submodule)
229 with contextlib.suppress(AttributeError):
230 examples.extend(submodule.URI_EXAMPLES)
232 try:
233 schemes.extend(submodule.SCHEMES)
234 except AttributeError:
235 schemes.append(scheme)
237 body_pad = LPAD + " "
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)
249 if f.__doc__:
250 f.__doc__ = f.__doc__.replace(PLACEHOLDER, buf.getvalue())