forked from ucbds-infra/otter-grader
/
utils.py
271 lines (226 loc) · 8.72 KB
/
utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
"""
Utilities for Otter-Grader
"""
import os
import sys
import pathlib
import random
import string
import shutil
from contextlib import contextmanager, redirect_stdout
from IPython import get_ipython
@contextmanager
def block_print():
"""
Context manager that disables printing to stdout
"""
with open(os.devnull, "w") as f, redirect_stdout(f):
yield
def flush_inline_matplotlib_plots():
"""
Flush matplotlib plots immediately, rather than asynchronously
Basically, the inline backend only shows the plot after the entire cell executes, which means we
can't easily use a context manager to suppress displaying it. See https://github.com/jupyter-widgets/ipywidgets/issues/1181/
and https://github.com/ipython/ipython/issues/10376 for more details. This function displays flushes
any pending matplotlib plots if we are using the inline backend. Stolen from
https://github.com/jupyter-widgets/ipywidgets/blob/4cc15e66d5e9e69dac8fc20d1eb1d7db825d7aa2/ipywidgets/widgets/interaction.py#L35
"""
if 'matplotlib' not in sys.modules:
# matplotlib hasn't been imported, nothing to do.
return
try:
import matplotlib as mpl
from ipykernel.pylab.backend_inline import flush_figures
except ImportError:
return
# except KeyError:
# return
if mpl.get_backend() == 'module://ipykernel.pylab.backend_inline':
flush_figures()
@contextmanager
def hide_outputs():
"""
Context manager for hiding outputs from ``display()`` calls. IPython handles matplotlib outputs
specially, so those are supressed too.
"""
ipy = get_ipython()
if ipy is None:
# Not running inside ipython!
yield
return
old_formatters = ipy.display_formatter.formatters
ipy.display_formatter.formatters = {}
try:
yield
finally:
# flush_inline_matplotlib_plots()
ipy.display_formatter.formatters = old_formatters
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
"""
Used to generate a dynamic variable name for grading functions
This function generates a random name using the given length and character set.
Args:
size (``int``): length of output name
chars (``str``, optional): set of characters used to create function name
Returns:
``str``: randomized string name for grading function
"""
return ''.join(random.choice(chars) for _ in range(size))
def get_variable_type(obj):
"""
Returns the fully-qualified type string of an object ``obj``
Args:
obj (object): the object in question
Returns:
``str``: the fully-qualified type string
"""
return type(obj).__module__ + "." + type(obj).__name__
def get_relpath(src, dst):
"""
Returns the relative path from ``src`` to ``dst``
Args:
src (``pathlib.Path``): the source directory
dst (``pathlib.Path``): the destination directory
Returns:
``pathlib.Path``: the relative path
"""
# osrc = src
ups = 0
while True:
try:
dst.relative_to(src)
break
except ValueError:
src = src.parent
ups += 1
return pathlib.Path(("../" * ups) / dst.relative_to(src))
def get_source(cell):
"""
Returns the source code of a cell in a way that works for both nbformat and JSON
Args:
cell (``nbformat.NotebookNode``): notebook cell
Returns:
``list`` of ``str``: each line of the cell source stripped of ending line breaks
"""
source = cell.source
if isinstance(source, str):
return source.split('\n')
elif isinstance(source, list):
return [line.strip('\n') for line in source]
raise ValueError(f'unknown source type: {type(source)}')
@contextmanager
def nullcontext():
"""
Yields an empty context. Added because ``contextlib.nullcontext`` was added in Python 3.7, so
earlier versions of Python require this patch.
"""
yield
@contextmanager
def load_default_file(provided_fn, default_fn, default_disabled=False):
"""
Reads the contents of a file with an optional default path. If ``proivided_fn`` is not specified
and ``expected_fn`` is a existing file path, the contents of ``expected_fn`` are read in place
of ``provided_fn``. The use of ``expected_fn`` can be disabled by setting ``default_disabled``
to ``True``.
"""
if provided_fn is None and os.path.isfile(default_fn) and not default_disabled:
provided_fn = default_fn
if provided_fn is not None:
assert os.path.isfile(provided_fn), f"Could not find specified file: {provided_fn}"
with open(provided_fn) as f:
yield f.read()
else:
yield None
def print_full_width(char, mid_text="", whitespace=" ", ret_str=False, **kwargs):
"""
Prints a character at the full terminal width. If ``mid_text`` is supplied, this text is printed
in the middle of the terminal, surrounded by ``whitespace``. Additional kwargs passed to
``print``.
If ``ret_str`` is true, the string is returned; if not, it is printed directly to the console.
"""
cols, rows = shutil.get_terminal_size()
if mid_text:
left = cols - len(mid_text) - 2 * len(whitespace)
if left <= 0:
left = 2
l, r = left // 2, left // 2
if left % 2 == 1:
r += 1
out = char * l + whitespace + mid_text + whitespace + char * r
else:
out = char * cols
if ret_str:
return out
print(out, **kwargs)
def convert_config_description_dict(configs, include_required=False):
"""
Recursively converts a documented list of dictionary configurations into a dictionary with the
default values loaded.
Expects a list of the form:
.. code-block:: python
[
{
"key": "config_name",
"description": "a description of the config for documentation",
"default": True, # the default config value
},
{
"key": "required_config_name",
"description": "a description of the config for documentation",
"required": True, # indicates that this must be user-specified and has no default
},
{
"key": "nested_config_name",
"description": "a description of the config for documentation",
"default": [ # note that a list is used for nested dict configs
{
"key": "subconfig",
"description": "a nested key",
"default": None,
}
],
},
]
The list above gets converted to a dictionary mapping each ``key`` to each ``default``:
.. code-block:: python
{
"config_name": True,
"nested_config_name": {
"subconfig": None,
},
}
Any configurations with ``default`` unspecified have their default value set to ``None``. Any
configurations marked with ``"required": True`` are not included in the output dict (so that
they raise a ``KeyError`` if unspecified).
Args:
configs (``list[dict[str,object]]``): the configurations with the structure defined above
Returns:
``dict[str,object]``: a dictionary mapping keys to default values
"""
res = {}
for d in configs:
default = d.get("default", None)
if isinstance(default, list) and len(default) > 0 and \
all(isinstance(e, dict) for e in default):
default = convert_config_description_dict(d["default"])
if not d.get("required", False) or include_required:
res[d["key"]] = default
return res
def assert_path_exists(path_tuples):
"""
Ensure that a series of file paths exist and are of a specific type, or raise a ``ValueError``.
Elements of ``path_tuples`` should be 2-tuples where the first element is a string representing
the file path and the second element is ``True`` if the path should be a directory, ``False`` if
it should be a file, and ``None`` if it doesn't matter.
Args:
path_tuples (``list[tuple[str, bool]]``): the list of paths as described above
Raises:
``ValueError``: if the path does not exist or it is not of the correct type
"""
for path, is_dir in path_tuples:
if not os.path.exists(path):
raise ValueError(f"Path {path} does not exist")
if is_dir and not os.path.isdir(path):
raise ValueError(f"Path {path} is not a directory")
if is_dir is False and not os.path.isfile(path):
raise ValueError(f"Path {path} is not a file")