1 # encoding: utf-8 2 """Tests for code execution (%run and related), which is particularly tricky. 3 4 Because of how %run manages namespaces, and the fact that we are trying here to 5 verify subtle object deletion and reference counting issues, the %run tests 6 will be kept in this separate file. This makes it easier to aggregate in one 7 place the tricks needed to handle it; most other magics are much easier to test 8 and we do so in a common test_magic file. 9 10 Note that any test using `run -i` should make sure to do a `reset` afterwards, 11 as otherwise it may influence later tests. 12 """ 13 14 # Copyright (c) IPython Development Team. 15 # Distributed under the terms of the Modified BSD License. 16 17 18 19 import functools 20 import os 21 import platform 22 import random 23 import string 24 import sys 25 import textwrap 26 import unittest 27 from os.path import join as pjoin 28 from unittest.mock import patch 29 30 import pytest 31 from tempfile import TemporaryDirectory 32 33 from IPython.core import debugger 34 from IPython.testing import decorators as dec 35 from IPython.testing import tools as tt 36 from IPython.utils.io import capture_output 37 38 39 def doctest_refbug(): 40 """Very nasty problem with references held by multiple runs of a script. 41 See: https://github.com/ipython/ipython/issues/141 42 43 In [1]: _ip.clear_main_mod_cache() 44 # random 45 46 In [2]: %run refbug 47 48 In [3]: call_f() 49 lowercased: hello 50 51 In [4]: %run refbug 52 53 In [5]: call_f() 54 lowercased: hello 55 lowercased: hello 56 """ 57 58 59 def doctest_run_builtins(): 60 r"""Check that %run doesn't damage __builtins__. 61 62 In [1]: import tempfile 63 64 In [2]: bid1 = id(__builtins__) 65 66 In [3]: fname = tempfile.mkstemp('.py')[1] 67 68 In [3]: f = open(fname, 'w', encoding='utf-8') 69 70 In [4]: dummy= f.write('pass\n') 71 72 In [5]: f.flush() 73 74 In [6]: t1 = type(__builtins__) 75 76 In [7]: %run $fname 77 78 In [7]: f.close() 79 80 In [8]: bid2 = id(__builtins__) 81 82 In [9]: t2 = type(__builtins__) 83 84 In [10]: t1 == t2 85 Out[10]: True 86 87 In [10]: bid1 == bid2 88 Out[10]: True 89 90 In [12]: try: 91 ....: os.unlink(fname) 92 ....: except: 93 ....: pass 94 ....: 95 """ 96 97 98 def doctest_run_option_parser(): 99 r"""Test option parser in %run. 100 101 In [1]: %run print_argv.py 102 [] 103 104 In [2]: %run print_argv.py print*.py 105 ['print_argv.py'] 106 107 In [3]: %run -G print_argv.py print*.py 108 ['print*.py'] 109 110 """ 111 112 113 @dec.skip_win32 114 def doctest_run_option_parser_for_posix(): 115 r"""Test option parser in %run (Linux/OSX specific). 116 117 You need double quote to escape glob in POSIX systems: 118 119 In [1]: %run print_argv.py print\\*.py 120 ['print*.py'] 121 122 You can't use quote to escape glob in POSIX systems: 123 124 In [2]: %run print_argv.py 'print*.py' 125 ['print_argv.py'] 126 127 """ 128 129 130 doctest_run_option_parser_for_posix.__skip_doctest__ = sys.platform == "win32" 131 132 133 @dec.skip_if_not_win32 134 def doctest_run_option_parser_for_windows(): 135 r"""Test option parser in %run (Windows specific). 136 137 In Windows, you can't escape ``*` `by backslash: 138 139 In [1]: %run print_argv.py print\\*.py 140 ['print\\\\*.py'] 141 142 You can use quote to escape glob: 143 144 In [2]: %run print_argv.py 'print*.py' 145 ["'print*.py'"] 146 147 """ 148 149 150 doctest_run_option_parser_for_windows.__skip_doctest__ = sys.platform != "win32" 151 152 153 def doctest_reset_del(): 154 """Test that resetting doesn't cause errors in __del__ methods. 155 156 In [2]: class A(object): 157 ...: def __del__(self): 158 ...: print(str("Hi")) 159 ...: 160 161 In [3]: a = A() 162 163 In [4]: get_ipython().reset(); import gc; x = gc.collect(0) 164 Hi 165 166 In [5]: 1+1 167 Out[5]: 2 168 """ 169 170 # For some tests, it will be handy to organize them in a class with a common 171 # setup that makes a temp file 172 173 class TestMagicRunPass(tt.TempFileMixin): 174 175 def setUp(self): 176 content = "a = [1,2,3]\nb = 1" 177 self.mktmp(content) 178 179 def run_tmpfile(self): 180 _ip = get_ipython() 181 # This fails on Windows if self.tmpfile.name has spaces or "~" in it. 182 # See below and ticket https://bugs.launchpad.net/bugs/366353 183 _ip.run_line_magic("run", self.fname) 184 185 def run_tmpfile_p(self): 186 _ip = get_ipython() 187 # This fails on Windows if self.tmpfile.name has spaces or "~" in it. 188 # See below and ticket https://bugs.launchpad.net/bugs/366353 189 _ip.run_line_magic("run", "-p %s" % self.fname) 190 191 def test_builtins_id(self): 192 """Check that %run doesn't damage __builtins__ """ 193 _ip = get_ipython() 194 # Test that the id of __builtins__ is not modified by %run 195 bid1 = id(_ip.user_ns['__builtins__']) 196 self.run_tmpfile() 197 bid2 = id(_ip.user_ns['__builtins__']) 198 assert bid1 == bid2 199 200 def test_builtins_type(self): 201 """Check that the type of __builtins__ doesn't change with %run. 202 203 However, the above could pass if __builtins__ was already modified to 204 be a dict (it should be a module) by a previous use of %run. So we 205 also check explicitly that it really is a module: 206 """ 207 _ip = get_ipython() 208 self.run_tmpfile() 209 assert type(_ip.user_ns["__builtins__"]) == type(sys) 210 211 def test_run_profile(self): 212 """Test that the option -p, which invokes the profiler, do not 213 crash by invoking execfile""" 214 self.run_tmpfile_p() 215 216 def test_run_debug_twice(self): 217 # https://github.com/ipython/ipython/issues/10028 218 _ip = get_ipython() 219 with tt.fake_input(["c"]): 220 _ip.run_line_magic("run", "-d %s" % self.fname) 221 with tt.fake_input(["c"]): 222 _ip.run_line_magic("run", "-d %s" % self.fname) 223 224 def test_run_debug_twice_with_breakpoint(self): 225 """Make a valid python temp file.""" 226 _ip = get_ipython() 227 with tt.fake_input(["b 2", "c", "c"]): 228 _ip.run_line_magic("run", "-d %s" % self.fname) 229 230 with tt.fake_input(["c"]): 231 with tt.AssertNotPrints("KeyError"): 232 _ip.run_line_magic("run", "-d %s" % self.fname) 233 234 235 class TestMagicRunSimple(tt.TempFileMixin): 236 237 def test_simpledef(self): 238 """Test that simple class definitions work.""" 239 src = ("class foo: pass\n" 240 "def f(): return foo()") 241 self.mktmp(src) 242 _ip.run_line_magic("run", str(self.fname)) 243 _ip.run_cell("t = isinstance(f(), foo)") 244 assert _ip.user_ns["t"] is True 245 246 @pytest.mark.xfail( 247 platform.python_implementation() == "PyPy", 248 reason="expecting __del__ call on exit is unreliable and doesn't happen on PyPy", 249 ) 250 def test_obj_del(self): 251 """Test that object's __del__ methods are called on exit.""" 252 src = ("class A(object):\n" 253 " def __del__(self):\n" 254 " print('object A deleted')\n" 255 "a = A()\n") 256 self.mktmp(src) 257 err = None 258 tt.ipexec_validate(self.fname, 'object A deleted', err) 259 260 def test_aggressive_namespace_cleanup(self): 261 """Test that namespace cleanup is not too aggressive GH-238 262 263 Returning from another run magic deletes the namespace""" 264 # see ticket https://github.com/ipython/ipython/issues/238 265 266 with tt.TempFileMixin() as empty: 267 empty.mktmp("") 268 # On Windows, the filename will have \users in it, so we need to use the 269 # repr so that the \u becomes \\u. 270 src = ( 271 "ip = get_ipython()\n" 272 "for i in range(5):\n" 273 " try:\n" 274 " ip.run_line_magic(%r, %r)\n" 275 " except NameError as e:\n" 276 " print(i)\n" 277 " break\n" % ("run", empty.fname) 278 ) 279 self.mktmp(src) 280 _ip.run_line_magic("run", str(self.fname)) 281 _ip.run_cell("ip == get_ipython()") 282 assert _ip.user_ns["i"] == 4 283 284 def test_run_second(self): 285 """Test that running a second file doesn't clobber the first, gh-3547""" 286 self.mktmp("avar = 1\n" "def afunc():\n" " return avar\n") 287 288 with tt.TempFileMixin() as empty: 289 empty.mktmp("") 290 291 _ip.run_line_magic("run", self.fname) 292 _ip.run_line_magic("run", empty.fname) 293 assert _ip.user_ns["afunc"]() == 1 294 295 def test_tclass(self): 296 mydir = os.path.dirname(__file__) 297 tc = os.path.join(mydir, "tclass") 298 src = f"""\ 299 import gc 300 %run "{tc}" C-first 301 gc.collect(0) 302 %run "{tc}" C-second 303 gc.collect(0) 304 %run "{tc}" C-third 305 gc.collect(0) 306 %reset -f 307 """ 308 self.mktmp(src, ".ipy") 309 out = """\ 310 ARGV 1-: ['C-first'] 311 ARGV 1-: ['C-second'] 312 tclass.py: deleting object: C-first 313 ARGV 1-: ['C-third'] 314 tclass.py: deleting object: C-second 315 tclass.py: deleting object: C-third 316 """ 317 err = None 318 tt.ipexec_validate(self.fname, out, err) 319 320 def test_run_i_after_reset(self): 321 """Check that %run -i still works after %reset (gh-693)""" 322 src = "yy = zz\n" 323 self.mktmp(src) 324 _ip.run_cell("zz = 23") 325 try: 326 _ip.run_line_magic("run", "-i %s" % self.fname) 327 assert _ip.user_ns["yy"] == 23 328 finally: 329 _ip.run_line_magic("reset", "-f") 330 331 _ip.run_cell("zz = 23") 332 try: 333 _ip.run_line_magic("run", "-i %s" % self.fname) 334 assert _ip.user_ns["yy"] == 23 335 finally: 336 _ip.run_line_magic("reset", "-f") 337 338 def test_unicode(self): 339 """Check that files in odd encodings are accepted.""" 340 mydir = os.path.dirname(__file__) 341 na = os.path.join(mydir, "nonascii.py") 342 _ip.run_line_magic("run", na) 343 assert _ip.user_ns["u"] == "ะัโะค" 344 345 def test_run_py_file_attribute(self): 346 """Test handling of `__file__` attribute in `%run <file>.py`.""" 347 src = "t = __file__\n" 348 self.mktmp(src) 349 _missing = object() 350 file1 = _ip.user_ns.get("__file__", _missing) 351 _ip.run_line_magic("run", self.fname) 352 file2 = _ip.user_ns.get("__file__", _missing) 353 354 # Check that __file__ was equal to the filename in the script's 355 # namespace. 356 assert _ip.user_ns["t"] == self.fname 357 358 # Check that __file__ was not leaked back into user_ns. 359 assert file1 == file2 360 361 def test_run_ipy_file_attribute(self): 362 """Test handling of `__file__` attribute in `%run <file.ipy>`.""" 363 src = "t = __file__\n" 364 self.mktmp(src, ext='.ipy') 365 _missing = object() 366 file1 = _ip.user_ns.get("__file__", _missing) 367 _ip.run_line_magic("run", self.fname) 368 file2 = _ip.user_ns.get("__file__", _missing) 369 370 # Check that __file__ was equal to the filename in the script's 371 # namespace. 372 assert _ip.user_ns["t"] == self.fname 373 374 # Check that __file__ was not leaked back into user_ns. 375 assert file1 == file2 376 377 def test_run_formatting(self): 378 """ Test that %run -t -N<N> does not raise a TypeError for N > 1.""" 379 src = "pass" 380 self.mktmp(src) 381 _ip.run_line_magic("run", "-t -N 1 %s" % self.fname) 382 _ip.run_line_magic("run", "-t -N 10 %s" % self.fname) 383 384 def test_ignore_sys_exit(self): 385 """Test the -e option to ignore sys.exit()""" 386 src = "import sys; sys.exit(1)" 387 self.mktmp(src) 388 with tt.AssertPrints("SystemExit"): 389 _ip.run_line_magic("run", self.fname) 390 391 with tt.AssertNotPrints("SystemExit"): 392 _ip.run_line_magic("run", "-e %s" % self.fname) 393 394 def test_run_nb(self): 395 """Test %run notebook.ipynb""" 396 pytest.importorskip("nbformat") 397 from nbformat import v4, writes 398 nb = v4.new_notebook( 399 cells=[ 400 v4.new_markdown_cell("The Ultimate Question of Everything"), 401 v4.new_code_cell("answer=42") 402 ] 403 ) 404 src = writes(nb, version=4) 405 self.mktmp(src, ext='.ipynb') 406 407 _ip.run_line_magic("run", self.fname) 408 409 assert _ip.user_ns["answer"] == 42 410 411 def test_run_nb_error(self): 412 """Test %run notebook.ipynb error""" 413 pytest.importorskip("nbformat") 414 from nbformat import v4, writes 415 416 # %run when a file name isn't provided 417 pytest.raises(Exception, _ip.magic, "run") 418 419 # %run when a file doesn't exist 420 pytest.raises(Exception, _ip.magic, "run foobar.ipynb") 421 422 # %run on a notebook with an error 423 nb = v4.new_notebook( 424 cells=[ 425 v4.new_code_cell("0/0") 426 ] 427 ) 428 src = writes(nb, version=4) 429 self.mktmp(src, ext='.ipynb') 430 pytest.raises(Exception, _ip.magic, "run %s" % self.fname) 431 432 def test_file_options(self): 433 src = ('import sys\n' 434 'a = " ".join(sys.argv[1:])\n') 435 self.mktmp(src) 436 test_opts = "-x 3 --verbose" 437 _ip.run_line_magic("run", "{0} {1}".format(self.fname, test_opts)) 438 assert _ip.user_ns["a"] == test_opts 439 440 441 class TestMagicRunWithPackage(unittest.TestCase): 442 443 def writefile(self, name, content): 444 path = os.path.join(self.tempdir.name, name) 445 d = os.path.dirname(path) 446 if not os.path.isdir(d): 447 os.makedirs(d) 448 with open(path, "w", encoding="utf-8") as f: 449 f.write(textwrap.dedent(content)) 450 451 def setUp(self): 452 self.package = package = 'tmp{0}'.format(''.join([random.choice(string.ascii_letters) for i in range(10)])) 453 """Temporary (probably) valid python package name.""" 454 455 self.value = int(random.random() * 10000) 456 457 self.tempdir = TemporaryDirectory() 458 self.__orig_cwd = os.getcwd() 459 sys.path.insert(0, self.tempdir.name) 460 461 self.writefile(os.path.join(package, '__init__.py'), '') 462 self.writefile(os.path.join(package, 'sub.py'), """ 463 x = {0!r} 464 """.format(self.value)) 465 self.writefile(os.path.join(package, 'relative.py'), """ 466 from .sub import x 467 """) 468 self.writefile(os.path.join(package, 'absolute.py'), """ 469 from {0}.sub import x 470 """.format(package)) 471 self.writefile(os.path.join(package, 'args.py'), """ 472 import sys 473 a = " ".join(sys.argv[1:]) 474 """.format(package)) 475 476 def tearDown(self): 477 os.chdir(self.__orig_cwd) 478 sys.path[:] = [p for p in sys.path if p != self.tempdir.name] 479 self.tempdir.cleanup() 480 481 def check_run_submodule(self, submodule, opts=""): 482 _ip.user_ns.pop("x", None) 483 _ip.run_line_magic( 484 "run", "{2} -m {0}.{1}".format(self.package, submodule, opts) 485 ) 486 self.assertEqual( 487 _ip.user_ns["x"], 488 self.value, 489 "Variable `x` is not loaded from module `{0}`.".format(submodule), 490 ) 491 492 def test_run_submodule_with_absolute_import(self): 493 self.check_run_submodule('absolute') 494 495 def test_run_submodule_with_relative_import(self): 496 """Run submodule that has a relative import statement (#2727).""" 497 self.check_run_submodule('relative') 498 499 def test_prun_submodule_with_absolute_import(self): 500 self.check_run_submodule('absolute', '-p') 501 502 def test_prun_submodule_with_relative_import(self): 503 self.check_run_submodule('relative', '-p') 504 505 def with_fake_debugger(func): 506 @functools.wraps(func) 507 def wrapper(*args, **kwds): 508 with patch.object(debugger.Pdb, 'run', staticmethod(eval)): 509 return func(*args, **kwds) 510 return wrapper 511 512 @with_fake_debugger 513 def test_debug_run_submodule_with_absolute_import(self): 514 self.check_run_submodule('absolute', '-d') 515 516 @with_fake_debugger 517 def test_debug_run_submodule_with_relative_import(self): 518 self.check_run_submodule('relative', '-d') 519 520 def test_module_options(self): 521 _ip.user_ns.pop("a", None) 522 test_opts = "-x abc -m test" 523 _ip.run_line_magic("run", "-m {0}.args {1}".format(self.package, test_opts)) 524 assert _ip.user_ns["a"] == test_opts 525 526 def test_module_options_with_separator(self): 527 _ip.user_ns.pop("a", None) 528 test_opts = "-x abc -m test" 529 _ip.run_line_magic("run", "-m {0}.args -- {1}".format(self.package, test_opts)) 530 assert _ip.user_ns["a"] == test_opts 531 532 533 def test_run__name__(): 534 with TemporaryDirectory() as td: 535 path = pjoin(td, "foo.py") 536 with open(path, "w", encoding="utf-8") as f: 537 f.write("q = __name__") 538 539 _ip.user_ns.pop("q", None) 540 _ip.run_line_magic("run", "{}".format(path)) 541 assert _ip.user_ns.pop("q") == "__main__" 542 543 _ip.run_line_magic("run", "-n {}".format(path)) 544 assert _ip.user_ns.pop("q") == "foo" 545 546 try: 547 _ip.run_line_magic("run", "-i -n {}".format(path)) 548 assert _ip.user_ns.pop("q") == "foo" 549 finally: 550 _ip.run_line_magic("reset", "-f") 551 552 553 def test_run_tb(): 554 """Test traceback offset in %run""" 555 with TemporaryDirectory() as td: 556 path = pjoin(td, "foo.py") 557 with open(path, "w", encoding="utf-8") as f: 558 f.write( 559 "\n".join( 560 [ 561 "def foo():", 562 " return bar()", 563 "def bar():", 564 " raise RuntimeError('hello!')", 565 "foo()", 566 ] 567 ) 568 ) 569 with capture_output() as io: 570 _ip.run_line_magic("run", "{}".format(path)) 571 out = io.stdout 572 assert "execfile" not in out 573 assert "RuntimeError" in out 574 assert out.count("---->") == 3 575 del ip.user_ns['bar'] 576 del ip.user_ns['foo'] 577 578 579 def test_multiprocessing_run(): 580 """Set we can run mutiprocesgin without messing up up main namespace 581 582 Note that import `nose.tools as nt` modify the values 583 sys.module['__mp_main__'] so we need to temporarily set it to None to test 584 the issue. 585 """ 586 with TemporaryDirectory() as td: 587 mpm = sys.modules.get('__mp_main__') 588 sys.modules['__mp_main__'] = None 589 try: 590 path = pjoin(td, "test.py") 591 with open(path, "w", encoding="utf-8") as f: 592 f.write("import multiprocessing\nprint('hoy')") 593 with capture_output() as io: 594 _ip.run_line_magic('run', path) 595 _ip.run_cell("i_m_undefined") 596 out = io.stdout 597 assert "hoy" in out 598 assert "AttributeError" not in out 599 assert "NameError" in out 600 assert out.count("---->") == 1 601 except: 602 raise 603 finally: 604 sys.modules['__mp_main__'] = mpm 605 606 607 def test_script_tb(): 608 """Test traceback offset in `ipython script.py`""" 609 with TemporaryDirectory() as td: 610 path = pjoin(td, "foo.py") 611 with open(path, "w", encoding="utf-8") as f: 612 f.write( 613 "\n".join( 614 [ 615 "def foo():", 616 " return bar()", 617 "def bar():", 618 " raise RuntimeError('hello!')", 619 "foo()", 620 ] 621 ) 622 ) 623 out, err = tt.ipexec(path) 624 assert "execfile" not in out 625 assert "RuntimeError" in out 626 assert out.count("---->") == 3 627