Repository: ipython/ipython

Issue:
__main__.py files not used in directory execution
Take the following directory structure:

```
test_dir
    __main__.py
```

where `__main__.py` reads

``` python
print("Hi")
```

Using the standard `python` command, I can:

```
$ python3 test_dir
Hi
```

Under `ipython`, this produces:

```
$ ipython3 test_dir
[TerminalIPythonApp] WARNING | File not found: 'test_dir'
```

This feature was added in Python 2.5. I suspect this error is simply because `ipython` hasn't implemented this functionality yet, but I don't see any open issues.

This feature is documented at https://docs.python.org/3/using/cmdline.html#interface-options.

Is there a specific reason why `ipython` doesn't use [runpy.run_path](https://docs.python.org/3/library/runpy.html#runpy.run_path) for running scripts passed as arguments?

If somebody could point me to a file in which I could patch this, I'd be happy to. I found [these lines](https://github.com/ipython/ipython/blob/master/IPython/core/interactiveshell.py#L2475-L2477) in `safe_execfile`, but `runpy.run_file` isn't entirely equivalent to `py3compat.execfile`, since it doesn't support the `locals` or `compiler` arguments.



Discussion:
Hi! Good catch; the code that runs a file from the command line argument is here: https://github.com/ipython/ipython/blob/0845fa68538de8d2843337928b415b96c36c3843/IPython/core/shellapp.py#L389

It's probably not quite as simple as swapping in the runpy function, because of various functionality that IPython allows, but it shouldn't be too hard to support a directory containing a `__main__.py`.

Ok. My thinking with runpy was just that it, as a part of Python, would get
any future changes that were made to this system as soon as the new Python
version was released. But adding this as manually works fine for now. I’ll
try to scrape together a PR right now.

On Tue, Jul 26, 2016 at 3:55 PM Thomas Kluyver notifications@github.com
wrote:

> Hi! Good catch; the code that runs a file from the command line argument
> is here:
> https://github.com/ipython/ipython/blob/0845fa68538de8d2843337928b415b96c36c3843/IPython/core/shellapp.py#L389
> 
> It's probably not quite as simple as swapping in the runpy function,
> because of various functionality that IPython allows, but it shouldn't be
> too hard to support a directory containing a **main**.py.
> 
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> https://github.com/ipython/ipython/issues/9812#issuecomment-235385237,
> or mute the thread
> https://github.com/notifications/unsubscribe-auth/AJ5YryxzWCxlGGP10ntXTliwNz1shEsaks5qZmY9gaJpZM4JVcnQ
> .


Resolving patch:
diff --git a/IPython/core/shellapp.py b/IPython/core/shellapp.py
--- a/IPython/core/shellapp.py
+++ b/IPython/core/shellapp.py
@@ -100,7 +100,7 @@
 
 class InteractiveShellApp(Configurable):
     """A Mixin for applications that start InteractiveShell instances.
-    
+
     Provides configurables for loading extensions and executing files
     as part of configuring a Shell environment.
 
@@ -126,7 +126,7 @@ class InteractiveShellApp(Configurable):
 
     # Extensions that are always loaded (not configurable)
     default_extensions = List(Unicode(), [u'storemagic']).tag(config=False)
-    
+
     hide_initial_ns = Bool(True,
         help="""Should variables loaded at startup (by startup files, exec_lines, etc.)
         be hidden from tools like %who?"""
@@ -166,7 +166,7 @@ class InteractiveShellApp(Configurable):
     pylab_import_all = Bool(True,
         help="""If true, IPython will populate the user namespace with numpy, pylab, etc.
         and an ``import *`` is done from numpy and pylab, when using pylab mode.
-        
+
         When False, pylab mode should not import any names into the user namespace.
         """
     ).tag(config=True)
@@ -174,7 +174,7 @@ class InteractiveShellApp(Configurable):
                      allow_none=True)
     # whether interact-loop should start
     interact = Bool(True)
-    
+
     user_ns = Instance(dict, args=None, allow_none=True)
     @observe('user_ns')
     def _user_ns_changed(self, change):
@@ -203,10 +203,10 @@ def init_gui_pylab(self):
         elif self.gui:
             enable = shell.enable_gui
             key = self.gui
-        
+
         if not enable:
             return
-        
+
         try:
             r = enable(key)
         except ImportError:
@@ -217,7 +217,7 @@ def init_gui_pylab(self):
             self.log.warning("GUI event loop or pylab initialization failed")
             self.shell.showtraceback()
             return
-            
+
         if isinstance(r, tuple):
             gui, backend = r[:2]
             self.log.info("Enabling GUI event loop integration, "
@@ -263,16 +263,16 @@ def init_code(self):
         self._run_startup_files()
         self._run_exec_lines()
         self._run_exec_files()
-        
+
         # Hide variables defined here from %who etc.
         if self.hide_initial_ns:
             self.shell.user_ns_hidden.update(self.shell.user_ns)
-        
+
         # command-line execution (ipython -i script.py, ipython -m module)
         # should *not* be excluded from %whos
         self._run_cmd_line_code()
         self._run_module()
-        
+
         # flush output, so itwon't be attached to the first cell
         sys.stdout.flush()
         sys.stderr.flush()
@@ -333,7 +333,7 @@ def _run_startup_files(self):
         """Run files from profile startup directory"""
         startup_dir = self.profile_dir.startup_dir
         startup_files = []
-        
+
         if self.exec_PYTHONSTARTUP and os.environ.get('PYTHONSTARTUP', False) and \
                 not (self.file_to_run or self.code_to_run or self.module_to_run):
             python_startup = os.environ['PYTHONSTARTUP']
@@ -343,12 +343,12 @@ def _run_startup_files(self):
             except:
                 self.log.warning("Unknown error in handling PYTHONSTARTUP file %s:", python_startup)
                 self.shell.showtraceback()
-        
+
         startup_files += glob.glob(os.path.join(startup_dir, '*.py'))
         startup_files += glob.glob(os.path.join(startup_dir, '*.ipy'))
         if not startup_files:
             return
-        
+
         self.log.debug("Running startup files from %s...", startup_dir)
         try:
             for fname in sorted(startup_files):
@@ -388,6 +388,8 @@ def _run_cmd_line_code(self):
         # Like Python itself, ignore the second if the first of these is present
         elif self.file_to_run:
             fname = self.file_to_run
+            if os.path.isdir(fname):
+                fname = os.path.join(fname, "__main__.py")
             try:
                 self._exec_file(fname, shell_futures=True)
             except:


Test patch: