##// END OF EJS Templates
cleanup unused bits
Paul Ivanov -
Show More
@@ -1,417 +1,414 b''
1 """Generic testing tools.
1 """Generic testing tools.
2
2
3 Authors
3 Authors
4 -------
4 -------
5 - Fernando Perez <Fernando.Perez@berkeley.edu>
5 - Fernando Perez <Fernando.Perez@berkeley.edu>
6 """
6 """
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Copyright (C) 2009-2011 The IPython Development Team
11 # Copyright (C) 2009-2011 The IPython Development Team
12 #
12 #
13 # Distributed under the terms of the BSD License. The full license is in
13 # Distributed under the terms of the BSD License. The full license is in
14 # the file COPYING, distributed as part of this software.
14 # the file COPYING, distributed as part of this software.
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Imports
18 # Imports
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 import os
21 import os
22 import pipes
23 import re
22 import re
24 import sys
23 import sys
25 import tempfile
24 import tempfile
26
25
27 from contextlib import contextmanager
26 from contextlib import contextmanager
28 from io import StringIO
27 from io import StringIO
29 from subprocess import Popen, PIPE
28 from subprocess import Popen, PIPE
30
29
31 try:
30 try:
32 # These tools are used by parts of the runtime, so we make the nose
31 # These tools are used by parts of the runtime, so we make the nose
33 # dependency optional at this point. Nose is a hard dependency to run the
32 # dependency optional at this point. Nose is a hard dependency to run the
34 # test suite, but NOT to use ipython itself.
33 # test suite, but NOT to use ipython itself.
35 import nose.tools as nt
34 import nose.tools as nt
36 has_nose = True
35 has_nose = True
37 except ImportError:
36 except ImportError:
38 has_nose = False
37 has_nose = False
39
38
40 from IPython.config.loader import Config
39 from IPython.config.loader import Config
41 from IPython.utils.process import getoutputerror
42 from IPython.utils.text import list_strings
40 from IPython.utils.text import list_strings
43 from IPython.utils.io import temp_pyfile, Tee
41 from IPython.utils.io import temp_pyfile, Tee
44 from IPython.utils import py3compat
42 from IPython.utils import py3compat
45 from IPython.utils.encoding import DEFAULT_ENCODING
43 from IPython.utils.encoding import DEFAULT_ENCODING
46
44
47 from . import decorators as dec
45 from . import decorators as dec
48 from . import skipdoctest
46 from . import skipdoctest
49
47
50 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
51 # Functions and classes
49 # Functions and classes
52 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
53
51
54 # The docstring for full_path doctests differently on win32 (different path
52 # The docstring for full_path doctests differently on win32 (different path
55 # separator) so just skip the doctest there. The example remains informative.
53 # separator) so just skip the doctest there. The example remains informative.
56 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
54 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
57
55
58 @doctest_deco
56 @doctest_deco
59 def full_path(startPath,files):
57 def full_path(startPath,files):
60 """Make full paths for all the listed files, based on startPath.
58 """Make full paths for all the listed files, based on startPath.
61
59
62 Only the base part of startPath is kept, since this routine is typically
60 Only the base part of startPath is kept, since this routine is typically
63 used with a script's __file__ variable as startPath. The base of startPath
61 used with a script's __file__ variable as startPath. The base of startPath
64 is then prepended to all the listed files, forming the output list.
62 is then prepended to all the listed files, forming the output list.
65
63
66 Parameters
64 Parameters
67 ----------
65 ----------
68 startPath : string
66 startPath : string
69 Initial path to use as the base for the results. This path is split
67 Initial path to use as the base for the results. This path is split
70 using os.path.split() and only its first component is kept.
68 using os.path.split() and only its first component is kept.
71
69
72 files : string or list
70 files : string or list
73 One or more files.
71 One or more files.
74
72
75 Examples
73 Examples
76 --------
74 --------
77
75
78 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
76 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
79 ['/foo/a.txt', '/foo/b.txt']
77 ['/foo/a.txt', '/foo/b.txt']
80
78
81 >>> full_path('/foo',['a.txt','b.txt'])
79 >>> full_path('/foo',['a.txt','b.txt'])
82 ['/a.txt', '/b.txt']
80 ['/a.txt', '/b.txt']
83
81
84 If a single file is given, the output is still a list:
82 If a single file is given, the output is still a list:
85 >>> full_path('/foo','a.txt')
83 >>> full_path('/foo','a.txt')
86 ['/a.txt']
84 ['/a.txt']
87 """
85 """
88
86
89 files = list_strings(files)
87 files = list_strings(files)
90 base = os.path.split(startPath)[0]
88 base = os.path.split(startPath)[0]
91 return [ os.path.join(base,f) for f in files ]
89 return [ os.path.join(base,f) for f in files ]
92
90
93
91
94 def parse_test_output(txt):
92 def parse_test_output(txt):
95 """Parse the output of a test run and return errors, failures.
93 """Parse the output of a test run and return errors, failures.
96
94
97 Parameters
95 Parameters
98 ----------
96 ----------
99 txt : str
97 txt : str
100 Text output of a test run, assumed to contain a line of one of the
98 Text output of a test run, assumed to contain a line of one of the
101 following forms::
99 following forms::
102
100
103 'FAILED (errors=1)'
101 'FAILED (errors=1)'
104 'FAILED (failures=1)'
102 'FAILED (failures=1)'
105 'FAILED (errors=1, failures=1)'
103 'FAILED (errors=1, failures=1)'
106
104
107 Returns
105 Returns
108 -------
106 -------
109 nerr, nfail: number of errors and failures.
107 nerr, nfail: number of errors and failures.
110 """
108 """
111
109
112 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
110 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
113 if err_m:
111 if err_m:
114 nerr = int(err_m.group(1))
112 nerr = int(err_m.group(1))
115 nfail = 0
113 nfail = 0
116 return nerr, nfail
114 return nerr, nfail
117
115
118 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
116 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
119 if fail_m:
117 if fail_m:
120 nerr = 0
118 nerr = 0
121 nfail = int(fail_m.group(1))
119 nfail = int(fail_m.group(1))
122 return nerr, nfail
120 return nerr, nfail
123
121
124 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
122 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
125 re.MULTILINE)
123 re.MULTILINE)
126 if both_m:
124 if both_m:
127 nerr = int(both_m.group(1))
125 nerr = int(both_m.group(1))
128 nfail = int(both_m.group(2))
126 nfail = int(both_m.group(2))
129 return nerr, nfail
127 return nerr, nfail
130
128
131 # If the input didn't match any of these forms, assume no error/failures
129 # If the input didn't match any of these forms, assume no error/failures
132 return 0, 0
130 return 0, 0
133
131
134
132
135 # So nose doesn't think this is a test
133 # So nose doesn't think this is a test
136 parse_test_output.__test__ = False
134 parse_test_output.__test__ = False
137
135
138
136
139 def default_argv():
137 def default_argv():
140 """Return a valid default argv for creating testing instances of ipython"""
138 """Return a valid default argv for creating testing instances of ipython"""
141
139
142 return ['--quick', # so no config file is loaded
140 return ['--quick', # so no config file is loaded
143 # Other defaults to minimize side effects on stdout
141 # Other defaults to minimize side effects on stdout
144 '--colors=NoColor', '--no-term-title','--no-banner',
142 '--colors=NoColor', '--no-term-title','--no-banner',
145 '--autocall=0']
143 '--autocall=0']
146
144
147
145
148 def default_config():
146 def default_config():
149 """Return a config object with good defaults for testing."""
147 """Return a config object with good defaults for testing."""
150 config = Config()
148 config = Config()
151 config.TerminalInteractiveShell.colors = 'NoColor'
149 config.TerminalInteractiveShell.colors = 'NoColor'
152 config.TerminalTerminalInteractiveShell.term_title = False,
150 config.TerminalTerminalInteractiveShell.term_title = False,
153 config.TerminalInteractiveShell.autocall = 0
151 config.TerminalInteractiveShell.autocall = 0
154 config.HistoryManager.hist_file = tempfile.mktemp(u'test_hist.sqlite')
152 config.HistoryManager.hist_file = tempfile.mktemp(u'test_hist.sqlite')
155 config.HistoryManager.db_cache_size = 10000
153 config.HistoryManager.db_cache_size = 10000
156 return config
154 return config
157
155
158
156
159 def get_ipython_cmd(as_string=False):
157 def get_ipython_cmd(as_string=False):
160 """
158 """
161 Return appropriate IPython command line name. By default, this will return
159 Return appropriate IPython command line name. By default, this will return
162 a list that can be used with subprocess.Popen, for example, but passing
160 a list that can be used with subprocess.Popen, for example, but passing
163 `as_string=True` allows for returning the IPython command as a string.
161 `as_string=True` allows for returning the IPython command as a string.
164
162
165 Parameters
163 Parameters
166 ----------
164 ----------
167 as_string: bool
165 as_string: bool
168 Flag to allow to return the command as a string.
166 Flag to allow to return the command as a string.
169 """
167 """
170 # FIXME: remove workaround for 2.6 support
168 # FIXME: remove workaround for 2.6 support
171 if sys.version_info[:2] > (2,6):
169 if sys.version_info[:2] > (2,6):
172 ipython_cmd = [sys.executable, "-m", "IPython"]
170 ipython_cmd = [sys.executable, "-m", "IPython"]
173 else:
171 else:
174 ipython_cmd = ["ipython"]
172 ipython_cmd = ["ipython"]
175
173
176 if as_string:
174 if as_string:
177 ipython_cmd = " ".join(ipython_cmd)
175 ipython_cmd = " ".join(ipython_cmd)
178
176
179 return ipython_cmd
177 return ipython_cmd
180
178
181 def ipexec(fname, options=None):
179 def ipexec(fname, options=None):
182 """Utility to call 'ipython filename'.
180 """Utility to call 'ipython filename'.
183
181
184 Starts IPython with a minimal and safe configuration to make startup as fast
182 Starts IPython with a minimal and safe configuration to make startup as fast
185 as possible.
183 as possible.
186
184
187 Note that this starts IPython in a subprocess!
185 Note that this starts IPython in a subprocess!
188
186
189 Parameters
187 Parameters
190 ----------
188 ----------
191 fname : str
189 fname : str
192 Name of file to be executed (should have .py or .ipy extension).
190 Name of file to be executed (should have .py or .ipy extension).
193
191
194 options : optional, list
192 options : optional, list
195 Extra command-line flags to be passed to IPython.
193 Extra command-line flags to be passed to IPython.
196
194
197 Returns
195 Returns
198 -------
196 -------
199 (stdout, stderr) of ipython subprocess.
197 (stdout, stderr) of ipython subprocess.
200 """
198 """
201 if options is None: options = []
199 if options is None: options = []
202
200
203 # For these subprocess calls, eliminate all prompt printing so we only see
201 # For these subprocess calls, eliminate all prompt printing so we only see
204 # output from script execution
202 # output from script execution
205 prompt_opts = [ '--PromptManager.in_template=""',
203 prompt_opts = [ '--PromptManager.in_template=""',
206 '--PromptManager.in2_template=""',
204 '--PromptManager.in2_template=""',
207 '--PromptManager.out_template=""'
205 '--PromptManager.out_template=""'
208 ]
206 ]
209 cmdargs = default_argv() + prompt_opts + options
207 cmdargs = default_argv() + prompt_opts + options
210
208
211 _ip = get_ipython()
212 test_dir = os.path.dirname(__file__)
209 test_dir = os.path.dirname(__file__)
213
210
214 ipython_cmd = get_ipython_cmd()
211 ipython_cmd = get_ipython_cmd()
215 # Absolute path for filename
212 # Absolute path for filename
216 full_fname = os.path.join(test_dir, fname)
213 full_fname = os.path.join(test_dir, fname)
217 full_cmd = ipython_cmd + cmdargs + [full_fname]
214 full_cmd = ipython_cmd + cmdargs + [full_fname]
218 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE)
215 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE)
219 out, err = p.communicate()
216 out, err = p.communicate()
220 out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err)
217 out, err = py3compat.bytes_to_str(out), py3compat.bytes_to_str(err)
221 # `import readline` causes 'ESC[?1034h' to be output sometimes,
218 # `import readline` causes 'ESC[?1034h' to be output sometimes,
222 # so strip that out before doing comparisons
219 # so strip that out before doing comparisons
223 if out:
220 if out:
224 out = re.sub(r'\x1b\[[^h]+h', '', out)
221 out = re.sub(r'\x1b\[[^h]+h', '', out)
225 return out, err
222 return out, err
226
223
227
224
228 def ipexec_validate(fname, expected_out, expected_err='',
225 def ipexec_validate(fname, expected_out, expected_err='',
229 options=None):
226 options=None):
230 """Utility to call 'ipython filename' and validate output/error.
227 """Utility to call 'ipython filename' and validate output/error.
231
228
232 This function raises an AssertionError if the validation fails.
229 This function raises an AssertionError if the validation fails.
233
230
234 Note that this starts IPython in a subprocess!
231 Note that this starts IPython in a subprocess!
235
232
236 Parameters
233 Parameters
237 ----------
234 ----------
238 fname : str
235 fname : str
239 Name of the file to be executed (should have .py or .ipy extension).
236 Name of the file to be executed (should have .py or .ipy extension).
240
237
241 expected_out : str
238 expected_out : str
242 Expected stdout of the process.
239 Expected stdout of the process.
243
240
244 expected_err : optional, str
241 expected_err : optional, str
245 Expected stderr of the process.
242 Expected stderr of the process.
246
243
247 options : optional, list
244 options : optional, list
248 Extra command-line flags to be passed to IPython.
245 Extra command-line flags to be passed to IPython.
249
246
250 Returns
247 Returns
251 -------
248 -------
252 None
249 None
253 """
250 """
254
251
255 import nose.tools as nt
252 import nose.tools as nt
256
253
257 out, err = ipexec(fname, options)
254 out, err = ipexec(fname, options)
258 #print 'OUT', out # dbg
255 #print 'OUT', out # dbg
259 #print 'ERR', err # dbg
256 #print 'ERR', err # dbg
260 # If there are any errors, we must check those befor stdout, as they may be
257 # If there are any errors, we must check those befor stdout, as they may be
261 # more informative than simply having an empty stdout.
258 # more informative than simply having an empty stdout.
262 if err:
259 if err:
263 if expected_err:
260 if expected_err:
264 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
261 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
265 else:
262 else:
266 raise ValueError('Running file %r produced error: %r' %
263 raise ValueError('Running file %r produced error: %r' %
267 (fname, err))
264 (fname, err))
268 # If no errors or output on stderr was expected, match stdout
265 # If no errors or output on stderr was expected, match stdout
269 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
266 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
270
267
271
268
272 class TempFileMixin(object):
269 class TempFileMixin(object):
273 """Utility class to create temporary Python/IPython files.
270 """Utility class to create temporary Python/IPython files.
274
271
275 Meant as a mixin class for test cases."""
272 Meant as a mixin class for test cases."""
276
273
277 def mktmp(self, src, ext='.py'):
274 def mktmp(self, src, ext='.py'):
278 """Make a valid python temp file."""
275 """Make a valid python temp file."""
279 fname, f = temp_pyfile(src, ext)
276 fname, f = temp_pyfile(src, ext)
280 self.tmpfile = f
277 self.tmpfile = f
281 self.fname = fname
278 self.fname = fname
282
279
283 def tearDown(self):
280 def tearDown(self):
284 if hasattr(self, 'tmpfile'):
281 if hasattr(self, 'tmpfile'):
285 # If the tmpfile wasn't made because of skipped tests, like in
282 # If the tmpfile wasn't made because of skipped tests, like in
286 # win32, there's nothing to cleanup.
283 # win32, there's nothing to cleanup.
287 self.tmpfile.close()
284 self.tmpfile.close()
288 try:
285 try:
289 os.unlink(self.fname)
286 os.unlink(self.fname)
290 except:
287 except:
291 # On Windows, even though we close the file, we still can't
288 # On Windows, even though we close the file, we still can't
292 # delete it. I have no clue why
289 # delete it. I have no clue why
293 pass
290 pass
294
291
295 pair_fail_msg = ("Testing {0}\n\n"
292 pair_fail_msg = ("Testing {0}\n\n"
296 "In:\n"
293 "In:\n"
297 " {1!r}\n"
294 " {1!r}\n"
298 "Expected:\n"
295 "Expected:\n"
299 " {2!r}\n"
296 " {2!r}\n"
300 "Got:\n"
297 "Got:\n"
301 " {3!r}\n")
298 " {3!r}\n")
302 def check_pairs(func, pairs):
299 def check_pairs(func, pairs):
303 """Utility function for the common case of checking a function with a
300 """Utility function for the common case of checking a function with a
304 sequence of input/output pairs.
301 sequence of input/output pairs.
305
302
306 Parameters
303 Parameters
307 ----------
304 ----------
308 func : callable
305 func : callable
309 The function to be tested. Should accept a single argument.
306 The function to be tested. Should accept a single argument.
310 pairs : iterable
307 pairs : iterable
311 A list of (input, expected_output) tuples.
308 A list of (input, expected_output) tuples.
312
309
313 Returns
310 Returns
314 -------
311 -------
315 None. Raises an AssertionError if any output does not match the expected
312 None. Raises an AssertionError if any output does not match the expected
316 value.
313 value.
317 """
314 """
318 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
315 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
319 for inp, expected in pairs:
316 for inp, expected in pairs:
320 out = func(inp)
317 out = func(inp)
321 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
318 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
322
319
323
320
324 if py3compat.PY3:
321 if py3compat.PY3:
325 MyStringIO = StringIO
322 MyStringIO = StringIO
326 else:
323 else:
327 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
324 # In Python 2, stdout/stderr can have either bytes or unicode written to them,
328 # so we need a class that can handle both.
325 # so we need a class that can handle both.
329 class MyStringIO(StringIO):
326 class MyStringIO(StringIO):
330 def write(self, s):
327 def write(self, s):
331 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
328 s = py3compat.cast_unicode(s, encoding=DEFAULT_ENCODING)
332 super(MyStringIO, self).write(s)
329 super(MyStringIO, self).write(s)
333
330
334 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
331 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
335 -------
332 -------
336 {2!s}
333 {2!s}
337 -------
334 -------
338 """
335 """
339
336
340 class AssertPrints(object):
337 class AssertPrints(object):
341 """Context manager for testing that code prints certain text.
338 """Context manager for testing that code prints certain text.
342
339
343 Examples
340 Examples
344 --------
341 --------
345 >>> with AssertPrints("abc", suppress=False):
342 >>> with AssertPrints("abc", suppress=False):
346 ... print "abcd"
343 ... print "abcd"
347 ... print "def"
344 ... print "def"
348 ...
345 ...
349 abcd
346 abcd
350 def
347 def
351 """
348 """
352 def __init__(self, s, channel='stdout', suppress=True):
349 def __init__(self, s, channel='stdout', suppress=True):
353 self.s = s
350 self.s = s
354 self.channel = channel
351 self.channel = channel
355 self.suppress = suppress
352 self.suppress = suppress
356
353
357 def __enter__(self):
354 def __enter__(self):
358 self.orig_stream = getattr(sys, self.channel)
355 self.orig_stream = getattr(sys, self.channel)
359 self.buffer = MyStringIO()
356 self.buffer = MyStringIO()
360 self.tee = Tee(self.buffer, channel=self.channel)
357 self.tee = Tee(self.buffer, channel=self.channel)
361 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
358 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
362
359
363 def __exit__(self, etype, value, traceback):
360 def __exit__(self, etype, value, traceback):
364 self.tee.flush()
361 self.tee.flush()
365 setattr(sys, self.channel, self.orig_stream)
362 setattr(sys, self.channel, self.orig_stream)
366 printed = self.buffer.getvalue()
363 printed = self.buffer.getvalue()
367 assert self.s in printed, notprinted_msg.format(self.s, self.channel, printed)
364 assert self.s in printed, notprinted_msg.format(self.s, self.channel, printed)
368 return False
365 return False
369
366
370 printed_msg = """Found {0!r} in printed output (on {1}):
367 printed_msg = """Found {0!r} in printed output (on {1}):
371 -------
368 -------
372 {2!s}
369 {2!s}
373 -------
370 -------
374 """
371 """
375
372
376 class AssertNotPrints(AssertPrints):
373 class AssertNotPrints(AssertPrints):
377 """Context manager for checking that certain output *isn't* produced.
374 """Context manager for checking that certain output *isn't* produced.
378
375
379 Counterpart of AssertPrints"""
376 Counterpart of AssertPrints"""
380 def __exit__(self, etype, value, traceback):
377 def __exit__(self, etype, value, traceback):
381 self.tee.flush()
378 self.tee.flush()
382 setattr(sys, self.channel, self.orig_stream)
379 setattr(sys, self.channel, self.orig_stream)
383 printed = self.buffer.getvalue()
380 printed = self.buffer.getvalue()
384 assert self.s not in printed, printed_msg.format(self.s, self.channel, printed)
381 assert self.s not in printed, printed_msg.format(self.s, self.channel, printed)
385 return False
382 return False
386
383
387 @contextmanager
384 @contextmanager
388 def mute_warn():
385 def mute_warn():
389 from IPython.utils import warn
386 from IPython.utils import warn
390 save_warn = warn.warn
387 save_warn = warn.warn
391 warn.warn = lambda *a, **kw: None
388 warn.warn = lambda *a, **kw: None
392 try:
389 try:
393 yield
390 yield
394 finally:
391 finally:
395 warn.warn = save_warn
392 warn.warn = save_warn
396
393
397 @contextmanager
394 @contextmanager
398 def make_tempfile(name):
395 def make_tempfile(name):
399 """ Create an empty, named, temporary file for the duration of the context.
396 """ Create an empty, named, temporary file for the duration of the context.
400 """
397 """
401 f = open(name, 'w')
398 f = open(name, 'w')
402 f.close()
399 f.close()
403 try:
400 try:
404 yield
401 yield
405 finally:
402 finally:
406 os.unlink(name)
403 os.unlink(name)
407
404
408
405
409 @contextmanager
406 @contextmanager
410 def monkeypatch(obj, name, attr):
407 def monkeypatch(obj, name, attr):
411 """
408 """
412 Context manager to replace attribute named `name` in `obj` with `attr`.
409 Context manager to replace attribute named `name` in `obj` with `attr`.
413 """
410 """
414 orig = getattr(obj, name)
411 orig = getattr(obj, name)
415 setattr(obj, name, attr)
412 setattr(obj, name, attr)
416 yield
413 yield
417 setattr(obj, name, orig)
414 setattr(obj, name, orig)
General Comments 0
You need to be logged in to leave comments. Login now