Repository: ipython/ipython

Issue:
%run -m doesn't support relative imports
I'm trying to use `%run -m` to run modules within some packages, but it doesn't work quite like `python -m`: it doesn't support relative imports.

For example, I have the following two files:
- ./foo/**init**.py
  
  ```
  x = 1
  ```
- ./foo/bar.py
  
  ```
  from . import x
  print x
  ```

With `python -m foo.bar` or `ipython -m foo.bar`  the output is `x`. With `%run`, however:

```
>>> run -m test.bar
Traceback (most recent call last):
  File "/Library/Python/2.7/site-packages/IPython/utils/py3compat.py", line 179, in execfile
    __builtin__.execfile(filename, *where)
  File "/private/tmp/test/bar.py", line 1, in <module>
    from . import x
ValueError: Attempted relative import in non-package
```



Discussion:
Yes, confirmed bug.  The `%run` code is pretty involved, so it might be a bit of a project to fix this.

As a temporary solution (and inspiration for an eventual patch) look at [runpy.run_module](http://docs.python.org/2/library/runpy.html) which you could use as:

```
In [6]: import runpy

In [7]: runpy.run_module('foo.bar');
1
```


Resolving patch:
diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py
--- a/IPython/core/magics/execution.py
+++ b/IPython/core/magics/execution.py
@@ -77,8 +77,7 @@ def profile_missing_notice(self, *args, **kwargs):
 
     @skip_doctest
     @line_cell_magic
-    def prun(self, parameter_s='', cell=None, user_mode=True,
-                   opts=None,arg_lst=None,prog_ns=None):
+    def prun(self, parameter_s='', cell=None):
 
         """Run a statement through the python code profiler.
 
@@ -178,38 +177,33 @@ def prun(self, parameter_s='', cell=None, user_mode=True,
 
           In [1]: import profile; profile.help()
         """
+        opts, arg_str = self.parse_options(parameter_s, 'D:l:rs:T:q',
+                                           list_all=True, posix=False)
+        if cell is not None:
+            arg_str += '\n' + cell
+        return self._run_with_profiler(arg_str, opts, self.shell.user_ns)
 
-        opts_def = Struct(D=[''],l=[],s=['time'],T=[''])
+    def _run_with_profiler(self, code, opts, namespace):
+        """
+        Run `code` with profiler.  Used by ``%prun`` and ``%run -p``.
 
-        if user_mode:  # regular user call
-            opts,arg_str = self.parse_options(parameter_s,'D:l:rs:T:q',
-                                              list_all=True, posix=False)
-            namespace = self.shell.user_ns
-            if cell is not None:
-                arg_str += '\n' + cell
-        else:  # called to run a program by %run -p
-            try:
-                filename = get_py_filename(arg_lst[0])
-            except IOError as e:
-                try:
-                    msg = str(e)
-                except UnicodeError:
-                    msg = e.message
-                error(msg)
-                return
+        Parameters
+        ----------
+        code : str
+            Code to be executed.
+        opts : Struct
+            Options parsed by `self.parse_options`.
+        namespace : dict
+            A dictionary for Python namespace (e.g., `self.shell.user_ns`).
 
-            arg_str = 'execfile(filename,prog_ns)'
-            namespace = {
-                'execfile': self.shell.safe_execfile,
-                'prog_ns': prog_ns,
-                'filename': filename
-                }
+        """
 
-        opts.merge(opts_def)
+        # Fill default values for unspecified options:
+        opts.merge(Struct(D=[''], l=[], s=['time'], T=['']))
 
         prof = profile.Profile()
         try:
-            prof = prof.runctx(arg_str,namespace,namespace)
+            prof = prof.runctx(code, namespace, namespace)
             sys_exit = ''
         except SystemExit:
             sys_exit = """*** SystemExit exception caught in code being profiled."""
@@ -327,8 +321,10 @@ def run(self, parameter_s='', runner=None,
                   file_finder=get_py_filename):
         """Run the named file inside IPython as a program.
 
-        Usage:\\
-          %run [-n -i -t [-N<N>] -d [-b<N>] -p [profile options] -G] file [args]
+        Usage:
+          %run [-n -i -e -G]
+               [( -t [-N<N>] | -d [-b<N>] | -p [profile options] )]
+               ( -m mod | file ) [args]
 
         Parameters after the filename are passed as command-line arguments to
         the program (put in sys.argv). Then, control returns to IPython's
@@ -541,62 +537,45 @@ def run(self, parameter_s='', runner=None,
         # every single object ever created.
         sys.modules[main_mod_name] = main_mod
 
+        if 'p' in opts or 'd' in opts:
+            if 'm' in opts:
+                code = 'run_module(modulename, prog_ns)'
+                code_ns = {
+                    'run_module': self.shell.safe_run_module,
+                    'prog_ns': prog_ns,
+                    'modulename': modulename,
+                }
+            else:
+                code = 'execfile(filename, prog_ns)'
+                code_ns = {
+                    'execfile': self.shell.safe_execfile,
+                    'prog_ns': prog_ns,
+                    'filename': get_py_filename(filename),
+                }
+
         try:
             stats = None
             with self.shell.readline_no_record:
                 if 'p' in opts:
-                    stats = self.prun('', None, False, opts, arg_lst, prog_ns)
+                    stats = self._run_with_profiler(code, opts, code_ns)
                 else:
                     if 'd' in opts:
-                        deb = debugger.Pdb(self.shell.colors)
-                        # reset Breakpoint state, which is moronically kept
-                        # in a class
-                        bdb.Breakpoint.next = 1
-                        bdb.Breakpoint.bplist = {}
-                        bdb.Breakpoint.bpbynumber = [None]
-                        # Set an initial breakpoint to stop execution
-                        maxtries = 10
-                        bp_file, bp_line = parse_breakpoint(opts.get('b', ['1'])[0], filename)
-                        checkline = deb.checkline(bp_file, bp_line)
-                        if not checkline:
-                            for bp in range(bp_line + 1, bp_line + maxtries + 1):
-                                if deb.checkline(bp_file, bp):
-                                    break
-                            else:
-                                msg = ("\nI failed to find a valid line to set "
-                                       "a breakpoint\n"
-                                       "after trying up to line: %s.\n"
-                                       "Please set a valid breakpoint manually "
-                                       "with the -b option." % bp)
-                                error(msg)
-                                return
-                        # if we find a good linenumber, set the breakpoint
-                        deb.do_break('%s:%s' % (bp_file, bp_line))
-
-                        # Mimic Pdb._runscript(...)
-                        deb._wait_for_mainpyfile = True
-                        deb.mainpyfile = deb.canonic(filename)
-
-                        # Start file run
-                        print "NOTE: Enter 'c' at the",
-                        print "%s prompt to start your script." % deb.prompt
-                        ns = {'execfile': py3compat.execfile, 'prog_ns': prog_ns}
-                        try:
-                            #save filename so it can be used by methods on the deb object
-                            deb._exec_filename = filename
-                            deb.run('execfile("%s", prog_ns)' % filename, ns)
-
-                        except:
-                            etype, value, tb = sys.exc_info()
-                            # Skip three frames in the traceback: the %run one,
-                            # one inside bdb.py, and the command-line typed by the
-                            # user (run by exec in pdb itself).
-                            self.shell.InteractiveTB(etype, value, tb, tb_offset=3)
+                        self._run_with_debugger(
+                            code, code_ns, opts.get('b', ['1'])[0], filename)
                     else:
-                        if runner is None:
-                            runner = self.default_runner
-                        if runner is None:
-                            runner = self.shell.safe_execfile
+                        if 'm' in opts:
+                            def run():
+                                self.shell.safe_run_module(modulename, prog_ns)
+                        else:
+                            if runner is None:
+                                runner = self.default_runner
+                            if runner is None:
+                                runner = self.shell.safe_execfile
+
+                            def run():
+                                runner(filename, prog_ns, prog_ns,
+                                       exit_ignore=exit_ignore)
+
                         if 't' in opts:
                             # timed execution
                             try:
@@ -606,37 +585,10 @@ def run(self, parameter_s='', runner=None,
                                     return
                             except (KeyError):
                                 nruns = 1
-                            twall0 = time.time()
-                            if nruns == 1:
-                                t0 = clock2()
-                                runner(filename, prog_ns, prog_ns,
-                                       exit_ignore=exit_ignore)
-                                t1 = clock2()
-                                t_usr = t1[0] - t0[0]
-                                t_sys = t1[1] - t0[1]
-                                print "\nIPython CPU timings (estimated):"
-                                print "  User   : %10.2f s." % t_usr
-                                print "  System : %10.2f s." % t_sys
-                            else:
-                                runs = range(nruns)
-                                t0 = clock2()
-                                for nr in runs:
-                                    runner(filename, prog_ns, prog_ns,
-                                           exit_ignore=exit_ignore)
-                                t1 = clock2()
-                                t_usr = t1[0] - t0[0]
-                                t_sys = t1[1] - t0[1]
-                                print "\nIPython CPU timings (estimated):"
-                                print "Total runs performed:", nruns
-                                print "  Times  : %10s   %10s" % ('Total', 'Per run')
-                                print "  User   : %10.2f s, %10.2f s." % (t_usr, t_usr / nruns)
-                                print "  System : %10.2f s, %10.2f s." % (t_sys, t_sys / nruns)
-                            twall1 = time.time()
-                            print "Wall time: %10.2f s." % (twall1 - twall0)
-
+                            self._run_with_timing(run, nruns)
                         else:
                             # regular execution
-                            runner(filename, prog_ns, prog_ns, exit_ignore=exit_ignore)
+                            run()
 
                 if 'i' in opts:
                     self.shell.user_ns['__name__'] = __name__save
@@ -676,7 +628,114 @@ def run(self, parameter_s='', runner=None,
                 del sys.modules[main_mod_name]
 
         return stats
-    
+
+    def _run_with_debugger(self, code, code_ns, break_point, filename):
+        """
+        Run `code` in debugger with a break point.
+
+        Parameters
+        ----------
+        code : str
+            Code to execute.
+        code_ns : dict
+            A namespace in which `code` is executed.
+        break_point : str
+            Line number in the file specified by `filename` argument
+            or a string in the format ``file:line``.  In the latter
+            case, `filename` is ignored.
+            See also :func:`.parse_breakpoint`.
+        filename : str
+            Path to the file in which break point is specified.
+
+        Raises
+        ------
+        UsageError
+            If no meaningful break point is given by `break_point` and
+            `filename`.
+
+        """
+        deb = debugger.Pdb(self.shell.colors)
+        # reset Breakpoint state, which is moronically kept
+        # in a class
+        bdb.Breakpoint.next = 1
+        bdb.Breakpoint.bplist = {}
+        bdb.Breakpoint.bpbynumber = [None]
+        # Set an initial breakpoint to stop execution
+        maxtries = 10
+        bp_file, bp_line = parse_breakpoint(break_point, filename)
+        checkline = deb.checkline(bp_file, bp_line)
+        if not checkline:
+            for bp in range(bp_line + 1, bp_line + maxtries + 1):
+                if deb.checkline(bp_file, bp):
+                    break
+            else:
+                msg = ("\nI failed to find a valid line to set "
+                       "a breakpoint\n"
+                       "after trying up to line: %s.\n"
+                       "Please set a valid breakpoint manually "
+                       "with the -b option." % bp)
+                raise UsageError(msg)
+        # if we find a good linenumber, set the breakpoint
+        deb.do_break('%s:%s' % (bp_file, bp_line))
+
+        # Mimic Pdb._runscript(...)
+        deb._wait_for_mainpyfile = True
+        deb.mainpyfile = deb.canonic(filename)
+
+        # Start file run
+        print "NOTE: Enter 'c' at the",
+        print "%s prompt to start your script." % deb.prompt
+        try:
+            #save filename so it can be used by methods on the deb object
+            deb._exec_filename = filename
+            deb.run(code, code_ns)
+
+        except:
+            etype, value, tb = sys.exc_info()
+            # Skip three frames in the traceback: the %run one,
+            # one inside bdb.py, and the command-line typed by the
+            # user (run by exec in pdb itself).
+            self.shell.InteractiveTB(etype, value, tb, tb_offset=3)
+
+    @staticmethod
+    def _run_with_timing(run, nruns):
+        """
+        Run function `run` and print timing information.
+
+        Parameters
+        ----------
+        run : callable
+            Any callable object which takes no argument.
+        nruns : int
+            Number of times to execute `run`.
+
+        """
+        twall0 = time.time()
+        if nruns == 1:
+            t0 = clock2()
+            run()
+            t1 = clock2()
+            t_usr = t1[0] - t0[0]
+            t_sys = t1[1] - t0[1]
+            print "\nIPython CPU timings (estimated):"
+            print "  User   : %10.2f s." % t_usr
+            print "  System : %10.2f s." % t_sys
+        else:
+            runs = range(nruns)
+            t0 = clock2()
+            for nr in runs:
+                run()
+            t1 = clock2()
+            t_usr = t1[0] - t0[0]
+            t_sys = t1[1] - t0[1]
+            print "\nIPython CPU timings (estimated):"
+            print "Total runs performed:", nruns
+            print "  Times  : %10s   %10s" % ('Total', 'Per run')
+            print "  User   : %10.2f s, %10.2f s." % (t_usr, t_usr / nruns)
+            print "  System : %10.2f s, %10.2f s." % (t_sys, t_sys / nruns)
+        twall1 = time.time()
+        print "Wall time: %10.2f s." % (twall1 - twall0)
+
     @skip_doctest
     @line_cell_magic
     def timeit(self, line='', cell=None):


Test patch:
diff --git a/IPython/core/tests/test_run.py b/IPython/core/tests/test_run.py
--- a/IPython/core/tests/test_run.py
+++ b/IPython/core/tests/test_run.py
@@ -13,9 +13,13 @@
 # Imports
 #-----------------------------------------------------------------------------
 
+import functools
 import os
+import random
 import sys
 import tempfile
+import textwrap
+import unittest
 
 import nose.tools as nt
 from nose import SkipTest
@@ -23,6 +27,8 @@
 from IPython.testing import decorators as dec
 from IPython.testing import tools as tt
 from IPython.utils import py3compat
+from IPython.utils.tempdir import TemporaryDirectory
+from IPython.core import debugger
 
 #-----------------------------------------------------------------------------
 # Test functions begin
@@ -337,3 +343,74 @@ def test_run_formatting(self):
         self.mktmp(src)
         _ip.magic('run -t -N 1 %s' % self.fname)
         _ip.magic('run -t -N 10 %s' % self.fname)
+
+
+class TestMagicRunWithPackage(unittest.TestCase):
+
+    def writefile(self, name, content):
+        path = os.path.join(self.tempdir.name, name)
+        d = os.path.dirname(path)
+        if not os.path.isdir(d):
+            os.makedirs(d)
+        with open(path, 'w') as f:
+            f.write(textwrap.dedent(content))
+
+    def setUp(self):
+        self.package = package = 'tmp{0}'.format(repr(random.random())[2:])
+        """Temporary valid python package name."""
+
+        self.value = int(random.random() * 10000)
+
+        self.tempdir = TemporaryDirectory()
+        self.__orig_cwd = os.getcwdu()
+        sys.path.insert(0, self.tempdir.name)
+
+        self.writefile(os.path.join(package, '__init__.py'), '')
+        self.writefile(os.path.join(package, 'sub.py'), """
+        x = {0!r}
+        """.format(self.value))
+        self.writefile(os.path.join(package, 'relative.py'), """
+        from .sub import x
+        """)
+        self.writefile(os.path.join(package, 'absolute.py'), """
+        from {0}.sub import x
+        """.format(package))
+
+    def tearDown(self):
+        os.chdir(self.__orig_cwd)
+        sys.path[:] = [p for p in sys.path if p != self.tempdir.name]
+        self.tempdir.cleanup()
+
+    def check_run_submodule(self, submodule, opts=''):
+        _ip.magic('run {2} -m {0}.{1}'.format(self.package, submodule, opts))
+        self.assertEqual(_ip.user_ns['x'], self.value,
+                         'Variable `x` is not loaded from module `{0}`.'
+                         .format(submodule))
+
+    def test_run_submodule_with_absolute_import(self):
+        self.check_run_submodule('absolute')
+
+    def test_run_submodule_with_relative_import(self):
+        """Run submodule that has a relative import statement (#2727)."""
+        self.check_run_submodule('relative')
+
+    def test_prun_submodule_with_absolute_import(self):
+        self.check_run_submodule('absolute', '-p')
+
+    def test_prun_submodule_with_relative_import(self):
+        self.check_run_submodule('relative', '-p')
+
+    def with_fake_debugger(func):
+        @functools.wraps(func)
+        def wrapper(*args, **kwds):
+            with tt.monkeypatch(debugger.Pdb, 'run', staticmethod(eval)):
+                return func(*args, **kwds)
+        return wrapper
+
+    @with_fake_debugger
+    def test_debug_run_submodule_with_absolute_import(self):
+        self.check_run_submodule('absolute', '-d')
+
+    @with_fake_debugger
+    def test_debug_run_submodule_with_relative_import(self):
+        self.check_run_submodule('relative', '-d')