1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2019 Radim Rehurek <me@radimrehurek.com>
4#
5# This code is distributed under the terms and conditions
6# from the MIT License (MIT).
7#
8
9"""Common functions for working with docstrings.
10
11For internal use only.
12"""
13
14import contextlib
15import inspect
16import io
17import os.path
18import re
19
20from . import compression
21from . import transport
22
23PLACEHOLDER = ' smart_open/doctools.py magic goes here'
24
25
26def extract_kwargs(docstring):
27 """Extract keyword argument documentation from a function's docstring.
28
29 Parameters
30 ----------
31 docstring: str
32 The docstring to extract keyword arguments from.
33
34 Returns
35 -------
36 list of (str, str, list str)
37
38 str
39 The name of the keyword argument.
40 str
41 Its type.
42 str
43 Its documentation as a list of lines.
44
45 Notes
46 -----
47 The implementation is rather fragile. It expects the following:
48
49 1. The parameters are under an underlined Parameters section
50 2. Keyword parameters have the literal ", optional" after the type
51 3. Names and types are not indented
52 4. Descriptions are indented with 4 spaces
53 5. The Parameters section ends with an empty line.
54
55 Examples
56 --------
57
58 >>> docstring = '''The foo function.
59 ... Parameters
60 ... ----------
61 ... bar: str, optional
62 ... This parameter is the bar.
63 ... baz: int, optional
64 ... This parameter is the baz.
65 ...
66 ... '''
67 >>> kwargs = extract_kwargs(docstring)
68 >>> kwargs[0]
69 ('bar', 'str, optional', ['This parameter is the bar.'])
70
71 """
72 if not docstring:
73 return []
74
75 lines = inspect.cleandoc(docstring).split('\n')
76 kwargs = []
77
78 #
79 # 1. Find the underlined 'Parameters' section
80 # 2. Once there, continue parsing parameters until we hit an empty line
81 #
82 while lines and lines[0] != 'Parameters':
83 lines.pop(0)
84
85 if not lines:
86 return []
87
88 lines.pop(0)
89 lines.pop(0)
90
91 for line in lines:
92 if not line.strip(): # stop at the first empty line encountered
93 break
94 is_arg_line = not line.startswith(' ')
95 if is_arg_line:
96 name, type_ = line.split(':', 1)
97 name, type_, description = name.strip(), type_.strip(), []
98 kwargs.append([name, type_, description])
99 continue
100 is_description_line = line.startswith(' ')
101 if is_description_line:
102 kwargs[-1][-1].append(line.strip())
103
104 return kwargs
105
106
107def to_docstring(kwargs, lpad=''):
108 """Reconstruct a docstring from keyword argument info.
109
110 Basically reverses :func:`extract_kwargs`.
111
112 Parameters
113 ----------
114 kwargs: list
115 Output from the extract_kwargs function
116 lpad: str, optional
117 Padding string (from the left).
118
119 Returns
120 -------
121 str
122 The docstring snippet documenting the keyword arguments.
123
124 Examples
125 --------
126
127 >>> kwargs = [
128 ... ('bar', 'str, optional', ['This parameter is the bar.']),
129 ... ('baz', 'int, optional', ['This parameter is the baz.']),
130 ... ]
131 >>> print(to_docstring(kwargs), end='')
132 bar: str, optional
133 This parameter is the bar.
134 baz: int, optional
135 This parameter is the baz.
136
137 """
138 buf = io.StringIO()
139 for name, type_, description in kwargs:
140 buf.write('%s%s: %s\n' % (lpad, name, type_))
141 for line in description:
142 buf.write('%s %s\n' % (lpad, line))
143 return buf.getvalue()
144
145
146def extract_examples_from_readme_rst(indent=' '):
147 """Extract examples from this project's README.rst file.
148
149 Parameters
150 ----------
151 indent: str
152 Prepend each line with this string. Should contain some number of spaces.
153
154 Returns
155 -------
156 str
157 The examples.
158
159 Notes
160 -----
161 Quite fragile, depends on named labels inside the README.rst file.
162 """
163 curr_dir = os.path.dirname(os.path.abspath(__file__))
164 readme_path = os.path.join(curr_dir, '..', 'README.rst')
165 try:
166 with open(readme_path) as fin:
167 lines = list(fin)
168 start = lines.index('.. _doctools_before_examples:\n')
169 end = lines.index(".. _doctools_after_examples:\n")
170 lines = lines[start+4:end-2]
171 return ''.join([indent + re.sub('^ ', '', line) for line in lines])
172 except Exception:
173 return indent + 'See README.rst'
174
175
176def tweak_open_docstring(f):
177 buf = io.StringIO()
178 seen = set()
179
180 root_path = os.path.dirname(os.path.dirname(__file__))
181
182 with contextlib.redirect_stdout(buf):
183 print(' smart_open supports the following transport mechanisms:')
184 print()
185 for scheme, submodule in sorted(transport._REGISTRY.items()):
186 if scheme == transport.NO_SCHEME or submodule in seen:
187 continue
188 seen.add(submodule)
189
190 try:
191 schemes = submodule.SCHEMES
192 except AttributeError:
193 schemes = [scheme]
194
195 relpath = os.path.relpath(submodule.__file__, start=root_path)
196 heading = '%s (%s)' % ("/".join(schemes), relpath)
197 print(' %s' % heading)
198 print(' %s' % ('~' * len(heading)))
199 print(' %s' % submodule.__doc__.split('\n')[0])
200 print()
201
202 kwargs = extract_kwargs(submodule.open.__doc__)
203 if kwargs:
204 print(to_docstring(kwargs, lpad=u' '))
205
206 print(' Examples')
207 print(' --------')
208 print()
209 print(extract_examples_from_readme_rst())
210
211 print(' This function also supports transparent compression and decompression ')
212 print(' using the following codecs:')
213 print()
214 for extension in compression.get_supported_extensions():
215 print(' * %s' % extension)
216 print()
217 print(' The function depends on the file extension to determine the appropriate codec.')
218
219 #
220 # The docstring can be None if -OO was passed to the interpreter.
221 #
222 if f.__doc__:
223 f.__doc__ = f.__doc__.replace(PLACEHOLDER, buf.getvalue())
224
225
226def tweak_parse_uri_docstring(f):
227 buf = io.StringIO()
228 seen = set()
229 schemes = []
230 examples = []
231
232 for scheme, submodule in sorted(transport._REGISTRY.items()):
233 if scheme == transport.NO_SCHEME or submodule in seen:
234 continue
235
236 seen.add(submodule)
237
238 try:
239 examples.extend(submodule.URI_EXAMPLES)
240 except AttributeError:
241 pass
242
243 try:
244 schemes.extend(submodule.SCHEMES)
245 except AttributeError:
246 schemes.append(scheme)
247
248 with contextlib.redirect_stdout(buf):
249 print(' Supported URI schemes are:')
250 print()
251 for scheme in schemes:
252 print(' * %s' % scheme)
253 print()
254 print(' Valid URI examples::')
255 print()
256 for example in examples:
257 print(' * %s' % example)
258
259 if f.__doc__:
260 f.__doc__ = f.__doc__.replace(PLACEHOLDER, buf.getvalue())