Format: text/x-python
โฌ‡ Download File (18.9 KB)
   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