Repository: Pylons/pyramid

Issue:
add PYTHONSTARTUP support to pshell
Currently pshell does not source any `.pystartup` file pointed to by the `PYTHONSTARTUP` environment variable. It'd be nice to support this.

https://docs.python.org/2/tutorial/interpreter.html#the-interactive-startup-file



Discussion:
So, it looks like it does source the file pointed to by `PYTHONSTARTUP`.  However, it looks like tabbed completion doesn't work on anything that is imported.

For reference, the `.pystartup` file I am using is:

``` python
try:
    import atexit
    import os
    import readline
    import rlcompleter

    readline.parse_and_bind('tab: complete')

    historyPath = os.path.expanduser("~/.pyhistory")

    def save_history(historyPath=historyPath):
        import readline
        readline.write_history_file(historyPath)

    if os.path.exists(historyPath):
        readline.read_history_file(historyPath)

    atexit.register(save_history)
    del os, atexit, readline, rlcompleter, save_history, historyPath
    print("Tabbed Completion Enabled")
except:
    pass
```

once `pshell` starts I can type `im<tab>` and it will complete to `import`.  If I then type `import os` and then `os.<tab>` it won't complete it.

OK, I found out why it was loading.  I tried sourcing it in a `setup` callable inside the `[pshell]` section of the `.ini` file (see [Extending the shell](http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/commandline.html#extending-the-shell)).  So back to square 1

As the original link above states, I don't see much of a problem with exec'ing the file at the start. The only trick is that I think the globals from that file are supposed to be injected into the interpreter.


Resolving patch:
diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py
--- a/pyramid/scripts/pshell.py
+++ b/pyramid/scripts/pshell.py
@@ -1,9 +1,11 @@
 from code import interact
 import optparse
+import os
 import sys
 import textwrap
 
 from pyramid.compat import configparser
+from pyramid.compat import exec_
 from pyramid.util import DottedNameResolver
 from pyramid.paster import bootstrap
 
@@ -51,6 +53,7 @@ class PShellCommand(object):
     loaded_objects = {}
     object_help = {}
     setup = None
+    pystartup = os.environ.get('PYTHONSTARTUP')
 
     def __init__(self, argv, quiet=False):
         self.quiet = quiet
@@ -144,6 +147,12 @@ def run(self, shell=None):
         if shell is None:
             shell = self.make_shell()
 
+        if self.pystartup and os.path.isfile(self.pystartup):
+            with open(self.pystartup, 'rb') as fp:
+                exec_(fp.read().decode('utf-8'), env)
+            if '__builtins__' in env:
+                del env['__builtins__']
+
         try:
             shell(env, help)
         finally:


Test patch:
diff --git a/pyramid/tests/test_scripts/pystartup.py b/pyramid/tests/test_scripts/pystartup.py
new file mode 100644
--- /dev/null
+++ b/pyramid/tests/test_scripts/pystartup.py
@@ -0,0 +1 @@
+foo = 1
diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py
--- a/pyramid/tests/test_scripts/test_pshell.py
+++ b/pyramid/tests/test_scripts/test_pshell.py
@@ -1,3 +1,4 @@
+import os
 import unittest
 from pyramid.tests.test_scripts import dummy
 
@@ -24,6 +25,9 @@ class Options(object): pass
             self.options.python_shell = ''
             self.options.setup = None
             cmd.options = self.options
+        # default to None to prevent side-effects from running tests in
+        # unknown environments
+        cmd.pystartup = None
         return cmd
 
     def test_make_default_shell(self):
@@ -369,6 +373,25 @@ def test_command_custom_section_override(self):
         self.assertTrue(self.bootstrap.closer.called)
         self.assertTrue(shell.help)
 
+    def test_command_loads_pythonstartup(self):
+        command = self._makeOne()
+        command.pystartup = (
+            os.path.abspath(
+                os.path.join(
+                    os.path.dirname(__file__),
+                    'pystartup.py')))
+        shell = dummy.DummyShell()
+        command.run(shell)
+        self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp')
+        self.assertEqual(shell.env, {
+            'app':self.bootstrap.app, 'root':self.bootstrap.root,
+            'registry':self.bootstrap.registry,
+            'request':self.bootstrap.request,
+            'root_factory':self.bootstrap.root_factory,
+            'foo':1,
+        })
+        self.assertTrue(self.bootstrap.closer.called)
+        self.assertTrue(shell.help)
 
 class Test_main(unittest.TestCase):
     def _callFUT(self, argv):