##// END OF EJS Templates
Use AssertPrints in tests for autoreload extension.
Thomas Kluyver -
Show More
@@ -1,307 +1,300 b''
1 1 import os
2 2 import sys
3 3 import tempfile
4 4 import shutil
5 5 import random
6 6 import time
7 7 from StringIO import StringIO
8 8
9 9 import nose.tools as nt
10 import IPython.testing.tools as tt
10 11
11 12 from IPython.extensions.autoreload import AutoreloadInterface
12 13 from IPython.core.hooks import TryNext
13 14
14 15 #-----------------------------------------------------------------------------
15 16 # Test fixture
16 17 #-----------------------------------------------------------------------------
17 18
18 19 class FakeShell(object):
19 20 def __init__(self):
20 21 self.ns = {}
21 22 self.reloader = AutoreloadInterface()
22 23
23 24 def run_code(self, code):
24 25 try:
25 26 self.reloader.pre_run_code_hook(self)
26 27 except TryNext:
27 28 pass
28 29 exec code in self.ns
29 30
30 31 def push(self, items):
31 32 self.ns.update(items)
32 33
33 34 def magic_autoreload(self, parameter):
34 35 self.reloader.magic_autoreload(self, parameter)
35 36
36 37 def magic_aimport(self, parameter, stream=None):
37 38 self.reloader.magic_aimport(self, parameter, stream=stream)
38 39
39 40
40 41 class Fixture(object):
41 42 """Fixture for creating test module files"""
42 43
43 44 test_dir = None
44 45 old_sys_path = None
45 46 filename_chars = "abcdefghijklmopqrstuvwxyz0123456789"
46 47
47 48 def setUp(self):
48 49 self.test_dir = tempfile.mkdtemp()
49 50 self.old_sys_path = list(sys.path)
50 51 sys.path.insert(0, self.test_dir)
51 52 self.shell = FakeShell()
52 53
53 54 def tearDown(self):
54 55 shutil.rmtree(self.test_dir)
55 56 sys.path = self.old_sys_path
56 57 self.shell.reloader.enabled = False
57 58
58 59 self.test_dir = None
59 60 self.old_sys_path = None
60 61 self.shell = None
61 62
62 63 def get_module(self):
63 64 module_name = "tmpmod_" + "".join(random.sample(self.filename_chars,20))
64 65 if module_name in sys.modules:
65 66 del sys.modules[module_name]
66 67 file_name = os.path.join(self.test_dir, module_name + ".py")
67 68 return module_name, file_name
68 69
69 70 def write_file(self, filename, content):
70 71 """
71 72 Write a file, and force a timestamp difference of at least one second
72 73
73 74 Notes
74 75 -----
75 76 Python's .pyc files record the timestamp of their compilation
76 77 with a time resolution of one second.
77 78
78 79 Therefore, we need to force a timestamp difference between .py
79 80 and .pyc, without having the .py file be timestamped in the
80 81 future, and without changing the timestamp of the .pyc file
81 82 (because that is stored in the file). The only reliable way
82 83 to achieve this seems to be to sleep.
83 84
84 85 """
85 86
86 87 # Sleep one second + eps
87 88 time.sleep(1.05)
88 89
89 90 # Write
90 91 f = open(filename, 'w')
91 92 try:
92 93 f.write(content)
93 94 finally:
94 95 f.close()
95 96
96 97 def new_module(self, code):
97 98 mod_name, mod_fn = self.get_module()
98 99 f = open(mod_fn, 'w')
99 100 try:
100 101 f.write(code)
101 102 finally:
102 103 f.close()
103 104 return mod_name, mod_fn
104 105
105 106 #-----------------------------------------------------------------------------
106 107 # Test automatic reloading
107 108 #-----------------------------------------------------------------------------
108 109
109 110 class TestAutoreload(Fixture):
110 111 def _check_smoketest(self, use_aimport=True):
111 112 """
112 113 Functional test for the automatic reloader using either
113 114 '%autoreload 1' or '%autoreload 2'
114 115 """
115 116
116 117 mod_name, mod_fn = self.new_module("""
117 118 x = 9
118 119
119 120 z = 123 # this item will be deleted
120 121
121 122 def foo(y):
122 123 return y + 3
123 124
124 125 class Baz(object):
125 126 def __init__(self, x):
126 127 self.x = x
127 128 def bar(self, y):
128 129 return self.x + y
129 130 @property
130 131 def quux(self):
131 132 return 42
132 133 def zzz(self):
133 134 '''This method will be deleted below'''
134 135 return 99
135 136
136 137 class Bar: # old-style class: weakref doesn't work for it on Python < 2.7
137 138 def foo(self):
138 139 return 1
139 140 """)
140 141
141 142 #
142 143 # Import module, and mark for reloading
143 144 #
144 145 if use_aimport:
145 146 self.shell.magic_autoreload("1")
146 147 self.shell.magic_aimport(mod_name)
147 148 stream = StringIO()
148 149 self.shell.magic_aimport("", stream=stream)
149 150 nt.assert_true(("Modules to reload:\n%s" % mod_name) in
150 151 stream.getvalue())
151 152
152 153 nt.assert_raises(
153 154 ImportError,
154 155 self.shell.magic_aimport, "tmpmod_as318989e89ds")
155 156 else:
156 157 self.shell.magic_autoreload("2")
157 158 self.shell.run_code("import %s" % mod_name)
158 159 stream = StringIO()
159 160 self.shell.magic_aimport("", stream=stream)
160 161 nt.assert_true("Modules to reload:\nall-except-skipped" in
161 162 stream.getvalue())
162 163 nt.assert_true(mod_name in self.shell.ns)
163 164
164 165 mod = sys.modules[mod_name]
165 166
166 167 #
167 168 # Test module contents
168 169 #
169 170 old_foo = mod.foo
170 171 old_obj = mod.Baz(9)
171 172 old_obj2 = mod.Bar()
172 173
173 174 def check_module_contents():
174 175 nt.assert_equal(mod.x, 9)
175 176 nt.assert_equal(mod.z, 123)
176 177
177 178 nt.assert_equal(old_foo(0), 3)
178 179 nt.assert_equal(mod.foo(0), 3)
179 180
180 181 obj = mod.Baz(9)
181 182 nt.assert_equal(old_obj.bar(1), 10)
182 183 nt.assert_equal(obj.bar(1), 10)
183 184 nt.assert_equal(obj.quux, 42)
184 185 nt.assert_equal(obj.zzz(), 99)
185 186
186 187 obj2 = mod.Bar()
187 188 nt.assert_equal(old_obj2.foo(), 1)
188 189 nt.assert_equal(obj2.foo(), 1)
189 190
190 191 check_module_contents()
191 192
192 193 #
193 194 # Simulate a failed reload: no reload should occur and exactly
194 195 # one error message should be printed
195 196 #
196 197 self.write_file(mod_fn, """
197 198 a syntax error
198 199 """)
199 200
200 old_stderr = sys.stderr
201 new_stderr = StringIO()
202 sys.stderr = new_stderr
203 try:
201 with tt.AssertPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'):
204 202 self.shell.run_code("pass") # trigger reload
203 with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'):
205 204 self.shell.run_code("pass") # trigger another reload
206 check_module_contents()
207 finally:
208 sys.stderr = old_stderr
209
210 nt.assert_true(('[autoreload of %s failed:' % mod_name) in
211 new_stderr.getvalue())
212 nt.assert_equal(new_stderr.getvalue().count('[autoreload of'), 1)
205 check_module_contents()
213 206
214 207 #
215 208 # Rewrite module (this time reload should succeed)
216 209 #
217 210 self.write_file(mod_fn, """
218 211 x = 10
219 212
220 213 def foo(y):
221 214 return y + 4
222 215
223 216 class Baz(object):
224 217 def __init__(self, x):
225 218 self.x = x
226 219 def bar(self, y):
227 220 return self.x + y + 1
228 221 @property
229 222 def quux(self):
230 223 return 43
231 224
232 225 class Bar: # old-style class
233 226 def foo(self):
234 227 return 2
235 228 """)
236 229
237 230 def check_module_contents():
238 231 nt.assert_equal(mod.x, 10)
239 232 nt.assert_false(hasattr(mod, 'z'))
240 233
241 234 nt.assert_equal(old_foo(0), 4) # superreload magic!
242 235 nt.assert_equal(mod.foo(0), 4)
243 236
244 237 obj = mod.Baz(9)
245 238 nt.assert_equal(old_obj.bar(1), 11) # superreload magic!
246 239 nt.assert_equal(obj.bar(1), 11)
247 240
248 241 nt.assert_equal(old_obj.quux, 43)
249 242 nt.assert_equal(obj.quux, 43)
250 243
251 244 nt.assert_false(hasattr(old_obj, 'zzz'))
252 245 nt.assert_false(hasattr(obj, 'zzz'))
253 246
254 247 obj2 = mod.Bar()
255 248 nt.assert_equal(old_obj2.foo(), 2)
256 249 nt.assert_equal(obj2.foo(), 2)
257 250
258 251 self.shell.run_code("pass") # trigger reload
259 252 check_module_contents()
260 253
261 254 #
262 255 # Another failure case: deleted file (shouldn't reload)
263 256 #
264 257 os.unlink(mod_fn)
265 258
266 259 self.shell.run_code("pass") # trigger reload
267 260 check_module_contents()
268 261
269 262 #
270 263 # Disable autoreload and rewrite module: no reload should occur
271 264 #
272 265 if use_aimport:
273 266 self.shell.magic_aimport("-" + mod_name)
274 267 stream = StringIO()
275 268 self.shell.magic_aimport("", stream=stream)
276 269 nt.assert_true(("Modules to skip:\n%s" % mod_name) in
277 270 stream.getvalue())
278 271
279 272 # This should succeed, although no such module exists
280 273 self.shell.magic_aimport("-tmpmod_as318989e89ds")
281 274 else:
282 275 self.shell.magic_autoreload("0")
283 276
284 277 self.write_file(mod_fn, """
285 278 x = -99
286 279 """)
287 280
288 281 self.shell.run_code("pass") # trigger reload
289 282 self.shell.run_code("pass")
290 283 check_module_contents()
291 284
292 285 #
293 286 # Re-enable autoreload: reload should now occur
294 287 #
295 288 if use_aimport:
296 289 self.shell.magic_aimport(mod_name)
297 290 else:
298 291 self.shell.magic_autoreload("")
299 292
300 293 self.shell.run_code("pass") # trigger reload
301 294 nt.assert_equal(mod.x, -99)
302 295
303 296 def test_smoketest_aimport(self):
304 297 self._check_smoketest(use_aimport=True)
305 298
306 299 def test_smoketest_autoreload(self):
307 300 self._check_smoketest(use_aimport=False)
@@ -1,388 +1,400 b''
1 1 """Generic testing tools that do NOT depend on Twisted.
2 2
3 3 In particular, this module exposes a set of top-level assert* functions that
4 4 can be used in place of nose.tools.assert* in method generators (the ones in
5 5 nose can not, at least as of nose 0.10.4).
6 6
7 7 Note: our testing package contains testing.util, which does depend on Twisted
8 8 and provides utilities for tests that manage Deferreds. All testing support
9 9 tools that only depend on nose, IPython or the standard library should go here
10 10 instead.
11 11
12 12
13 13 Authors
14 14 -------
15 15 - Fernando Perez <Fernando.Perez@berkeley.edu>
16 16 """
17 17
18 18 from __future__ import absolute_import
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Copyright (C) 2009 The IPython Development Team
22 22 #
23 23 # Distributed under the terms of the BSD License. The full license is in
24 24 # the file COPYING, distributed as part of this software.
25 25 #-----------------------------------------------------------------------------
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Imports
29 29 #-----------------------------------------------------------------------------
30 30
31 31 import os
32 32 import re
33 33 import sys
34 34 import tempfile
35 35
36 36 from contextlib import contextmanager
37 37 from io import StringIO
38 38
39 39 try:
40 40 # These tools are used by parts of the runtime, so we make the nose
41 41 # dependency optional at this point. Nose is a hard dependency to run the
42 42 # test suite, but NOT to use ipython itself.
43 43 import nose.tools as nt
44 44 has_nose = True
45 45 except ImportError:
46 46 has_nose = False
47 47
48 48 from IPython.config.loader import Config
49 49 from IPython.utils.process import find_cmd, getoutputerror
50 50 from IPython.utils.text import list_strings, getdefaultencoding
51 51 from IPython.utils.io import temp_pyfile, Tee
52 52 from IPython.utils import py3compat
53 53
54 54 from . import decorators as dec
55 55 from . import skipdoctest
56 56
57 57 #-----------------------------------------------------------------------------
58 58 # Globals
59 59 #-----------------------------------------------------------------------------
60 60
61 61 # Make a bunch of nose.tools assert wrappers that can be used in test
62 62 # generators. This will expose an assert* function for each one in nose.tools.
63 63
64 64 _tpl = """
65 65 def %(name)s(*a,**kw):
66 66 return nt.%(name)s(*a,**kw)
67 67 """
68 68
69 69 if has_nose:
70 70 for _x in [a for a in dir(nt) if a.startswith('assert')]:
71 71 exec _tpl % dict(name=_x)
72 72
73 73 #-----------------------------------------------------------------------------
74 74 # Functions and classes
75 75 #-----------------------------------------------------------------------------
76 76
77 77 # The docstring for full_path doctests differently on win32 (different path
78 78 # separator) so just skip the doctest there. The example remains informative.
79 79 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
80 80
81 81 @doctest_deco
82 82 def full_path(startPath,files):
83 83 """Make full paths for all the listed files, based on startPath.
84 84
85 85 Only the base part of startPath is kept, since this routine is typically
86 86 used with a script's __file__ variable as startPath. The base of startPath
87 87 is then prepended to all the listed files, forming the output list.
88 88
89 89 Parameters
90 90 ----------
91 91 startPath : string
92 92 Initial path to use as the base for the results. This path is split
93 93 using os.path.split() and only its first component is kept.
94 94
95 95 files : string or list
96 96 One or more files.
97 97
98 98 Examples
99 99 --------
100 100
101 101 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
102 102 ['/foo/a.txt', '/foo/b.txt']
103 103
104 104 >>> full_path('/foo',['a.txt','b.txt'])
105 105 ['/a.txt', '/b.txt']
106 106
107 107 If a single file is given, the output is still a list:
108 108 >>> full_path('/foo','a.txt')
109 109 ['/a.txt']
110 110 """
111 111
112 112 files = list_strings(files)
113 113 base = os.path.split(startPath)[0]
114 114 return [ os.path.join(base,f) for f in files ]
115 115
116 116
117 117 def parse_test_output(txt):
118 118 """Parse the output of a test run and return errors, failures.
119 119
120 120 Parameters
121 121 ----------
122 122 txt : str
123 123 Text output of a test run, assumed to contain a line of one of the
124 124 following forms::
125 125 'FAILED (errors=1)'
126 126 'FAILED (failures=1)'
127 127 'FAILED (errors=1, failures=1)'
128 128
129 129 Returns
130 130 -------
131 131 nerr, nfail: number of errors and failures.
132 132 """
133 133
134 134 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
135 135 if err_m:
136 136 nerr = int(err_m.group(1))
137 137 nfail = 0
138 138 return nerr, nfail
139 139
140 140 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
141 141 if fail_m:
142 142 nerr = 0
143 143 nfail = int(fail_m.group(1))
144 144 return nerr, nfail
145 145
146 146 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
147 147 re.MULTILINE)
148 148 if both_m:
149 149 nerr = int(both_m.group(1))
150 150 nfail = int(both_m.group(2))
151 151 return nerr, nfail
152 152
153 153 # If the input didn't match any of these forms, assume no error/failures
154 154 return 0, 0
155 155
156 156
157 157 # So nose doesn't think this is a test
158 158 parse_test_output.__test__ = False
159 159
160 160
161 161 def default_argv():
162 162 """Return a valid default argv for creating testing instances of ipython"""
163 163
164 164 return ['--quick', # so no config file is loaded
165 165 # Other defaults to minimize side effects on stdout
166 166 '--colors=NoColor', '--no-term-title','--no-banner',
167 167 '--autocall=0']
168 168
169 169
170 170 def default_config():
171 171 """Return a config object with good defaults for testing."""
172 172 config = Config()
173 173 config.TerminalInteractiveShell.colors = 'NoColor'
174 174 config.TerminalTerminalInteractiveShell.term_title = False,
175 175 config.TerminalInteractiveShell.autocall = 0
176 176 config.HistoryManager.hist_file = tempfile.mktemp(u'test_hist.sqlite')
177 177 config.HistoryManager.db_cache_size = 10000
178 178 return config
179 179
180 180
181 181 def ipexec(fname, options=None):
182 182 """Utility to call 'ipython filename'.
183 183
184 184 Starts IPython witha minimal and safe configuration to make startup as fast
185 185 as possible.
186 186
187 187 Note that this starts IPython in a subprocess!
188 188
189 189 Parameters
190 190 ----------
191 191 fname : str
192 192 Name of file to be executed (should have .py or .ipy extension).
193 193
194 194 options : optional, list
195 195 Extra command-line flags to be passed to IPython.
196 196
197 197 Returns
198 198 -------
199 199 (stdout, stderr) of ipython subprocess.
200 200 """
201 201 if options is None: options = []
202 202
203 203 # For these subprocess calls, eliminate all prompt printing so we only see
204 204 # output from script execution
205 205 prompt_opts = [ '--InteractiveShell.prompt_in1=""',
206 206 '--InteractiveShell.prompt_in2=""',
207 207 '--InteractiveShell.prompt_out=""'
208 208 ]
209 209 cmdargs = ' '.join(default_argv() + prompt_opts + options)
210 210
211 211 _ip = get_ipython()
212 212 test_dir = os.path.dirname(__file__)
213 213
214 214 ipython_cmd = find_cmd('ipython3' if py3compat.PY3 else 'ipython')
215 215 # Absolute path for filename
216 216 full_fname = os.path.join(test_dir, fname)
217 217 full_cmd = '%s %s %s' % (ipython_cmd, cmdargs, full_fname)
218 218 #print >> sys.stderr, 'FULL CMD:', full_cmd # dbg
219 219 out = getoutputerror(full_cmd)
220 220 # `import readline` causes 'ESC[?1034h' to be the first output sometimes,
221 221 # so strip that off the front of the first line if it is found
222 222 if out:
223 223 first = out[0]
224 224 m = re.match(r'\x1b\[[^h]+h', first)
225 225 if m:
226 226 # strip initial readline escape
227 227 out = list(out)
228 228 out[0] = first[len(m.group()):]
229 229 out = tuple(out)
230 230 return out
231 231
232 232
233 233 def ipexec_validate(fname, expected_out, expected_err='',
234 234 options=None):
235 235 """Utility to call 'ipython filename' and validate output/error.
236 236
237 237 This function raises an AssertionError if the validation fails.
238 238
239 239 Note that this starts IPython in a subprocess!
240 240
241 241 Parameters
242 242 ----------
243 243 fname : str
244 244 Name of the file to be executed (should have .py or .ipy extension).
245 245
246 246 expected_out : str
247 247 Expected stdout of the process.
248 248
249 249 expected_err : optional, str
250 250 Expected stderr of the process.
251 251
252 252 options : optional, list
253 253 Extra command-line flags to be passed to IPython.
254 254
255 255 Returns
256 256 -------
257 257 None
258 258 """
259 259
260 260 import nose.tools as nt
261 261
262 262 out, err = ipexec(fname)
263 263 #print 'OUT', out # dbg
264 264 #print 'ERR', err # dbg
265 265 # If there are any errors, we must check those befor stdout, as they may be
266 266 # more informative than simply having an empty stdout.
267 267 if err:
268 268 if expected_err:
269 269 nt.assert_equals(err.strip(), expected_err.strip())
270 270 else:
271 271 raise ValueError('Running file %r produced error: %r' %
272 272 (fname, err))
273 273 # If no errors or output on stderr was expected, match stdout
274 274 nt.assert_equals(out.strip(), expected_out.strip())
275 275
276 276
277 277 class TempFileMixin(object):
278 278 """Utility class to create temporary Python/IPython files.
279 279
280 280 Meant as a mixin class for test cases."""
281 281
282 282 def mktmp(self, src, ext='.py'):
283 283 """Make a valid python temp file."""
284 284 fname, f = temp_pyfile(src, ext)
285 285 self.tmpfile = f
286 286 self.fname = fname
287 287
288 288 def tearDown(self):
289 289 if hasattr(self, 'tmpfile'):
290 290 # If the tmpfile wasn't made because of skipped tests, like in
291 291 # win32, there's nothing to cleanup.
292 292 self.tmpfile.close()
293 293 try:
294 294 os.unlink(self.fname)
295 295 except:
296 296 # On Windows, even though we close the file, we still can't
297 297 # delete it. I have no clue why
298 298 pass
299 299
300 300 pair_fail_msg = ("Testing {0}\n\n"
301 301 "In:\n"
302 302 " {1!r}\n"
303 303 "Expected:\n"
304 304 " {2!r}\n"
305 305 "Got:\n"
306 306 " {3!r}\n")
307 307 def check_pairs(func, pairs):
308 308 """Utility function for the common case of checking a function with a
309 309 sequence of input/output pairs.
310 310
311 311 Parameters
312 312 ----------
313 313 func : callable
314 314 The function to be tested. Should accept a single argument.
315 315 pairs : iterable
316 316 A list of (input, expected_output) tuples.
317 317
318 318 Returns
319 319 -------
320 320 None. Raises an AssertionError if any output does not match the expected
321 321 value.
322 322 """
323 323 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
324 324 for inp, expected in pairs:
325 325 out = func(inp)
326 326 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
327 327
328 328 if py3compat.PY3:
329 329 MyStringIO = StringIO
330 330 else:
331 331 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
332 332 # so we need a class that can handle both.
333 333 class MyStringIO(StringIO):
334 334 def write(self, s):
335 335 s = py3compat.cast_unicode(s, encoding=getdefaultencoding())
336 336 super(MyStringIO, self).write(s)
337 337
338 338 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
339 339 {2!r}"""
340 340 class AssertPrints(object):
341 341 """Context manager for testing that code prints certain text.
342 342
343 343 Examples
344 344 --------
345 >>> with AssertPrints("abc"):
345 >>> with AssertPrints("abc", suppress=False):
346 346 ... print "abcd"
347 347 ... print "def"
348 348 ...
349 349 abcd
350 350 def
351 351 """
352 def __init__(self, s, channel='stdout'):
352 def __init__(self, s, channel='stdout', suppress=True):
353 353 self.s = s
354 354 self.channel = channel
355 self.suppress = suppress
355 356
356 357 def __enter__(self):
357 358 self.orig_stream = getattr(sys, self.channel)
358 359 self.buffer = MyStringIO()
359 360 self.tee = Tee(self.buffer, channel=self.channel)
360 setattr(sys, self.channel, self.tee)
361 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
361 362
362 363 def __exit__(self, etype, value, traceback):
363 364 self.tee.flush()
364 365 setattr(sys, self.channel, self.orig_stream)
365 366 printed = self.buffer.getvalue()
366 367 assert self.s in printed, notprinted_msg.format(self.s, self.channel, printed)
367 368 return False
369
370 class AssertNotPrints(AssertPrints):
371 """Context manager for checking that certain output *isn't* produced.
372
373 Counterpart of AssertPrints"""
374 def __exit__(self, etype, value, traceback):
375 self.tee.flush()
376 setattr(sys, self.channel, self.orig_stream)
377 printed = self.buffer.getvalue()
378 assert self.s not in printed, notprinted_msg.format(self.s, self.channel, printed)
379 return False
368 380
369 381 @contextmanager
370 382 def mute_warn():
371 383 from IPython.utils import warn
372 384 save_warn = warn.warn
373 385 warn.warn = lambda *a, **kw: None
374 386 try:
375 387 yield
376 388 finally:
377 389 warn.warn = save_warn
378 390
379 391 @contextmanager
380 392 def make_tempfile(name):
381 393 """ Create an empty, named, temporary file for the duration of the context.
382 394 """
383 395 f = open(name, 'w')
384 396 f.close()
385 397 try:
386 398 yield
387 399 finally:
388 400 os.unlink(name)
General Comments 0
You need to be logged in to leave comments. Login now