Michael Merickel
2015-10-20 208e7b5e363b07476797d9f754962982c686e907
add pshell --list and default_shell ini options
5 files modified
226 ■■■■ changed files
docs/narr/commandline.rst 59 ●●●● patch | view | raw | blame | history
pyramid/scripts/pshell.py 66 ●●●● patch | view | raw | blame | history
pyramid/tests/test_scripts/dummy.py 3 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_scripts/test_pshell.py 97 ●●●●● patch | view | raw | blame | history
setup.py 1 ●●●● patch | view | raw | blame | history
docs/narr/commandline.rst
@@ -269,32 +269,39 @@
.. _ipython_or_bpython:
IPython or bpython
Alternative Shells
~~~~~~~~~~~~~~~~~~
If you have `IPython <http://en.wikipedia.org/wiki/IPython>`_ and/or `bpython
<http://bpython-interpreter.org/>`_ in the interpreter you use to invoke the
``pshell`` command, ``pshell`` will autodiscover and use the first one found,
in this order: IPython, bpython, standard Python interpreter. However you could
specifically invoke your choice with the ``-p choice`` or ``--python-shell
choice`` option.
``pshell`` command, ``pshell`` will autodiscover and use the first one found.
However you could specifically invoke your choice with the ``-p choice`` or
``--python-shell choice`` option.
.. code-block:: text
   $ $VENV/bin/pshell -p ipython | bpython | python development.ini#MyProject
   $ $VENV/bin/pshell -p ipython development.ini#MyProject
Alternative Shells
~~~~~~~~~~~~~~~~~~
You may use the ``--list-shells`` option to see the available shells.
.. code-block:: text
   $ $VENV/bin/pshell --list-shells
   Available shells:
     bpython  [not available]
     ipython
     python
If you want to use a shell that isn't supported out of the box, you can
introduce a new shell by registering an entry point in your setup.py:
.. code-block:: python
    setup(
        entry_points = """\
            [pyramid.pshell]
            myshell=my_app:ptpython_shell_factory
        """
        entry_points={
            'pyramid.pshell': [
              'myshell=my_app:ptpython_shell_factory',
            ],
        },
    )
And then your shell factory should return a function that accepts two
@@ -303,7 +310,12 @@
.. code-block:: python
    def ptpython_shell_factory():
        from ptpython.repl import embed
        try:
            from ptpython.repl import embed
        except ImportError:
            # ptpython is not installed
            return None
        def PTPShell(banner, **kwargs):
            print(banner)
            return embed(**kwargs)
@@ -313,6 +325,25 @@
        return shell
If the factory returns ``None`` then it is assumed that the shell is not
supported.
.. versionchanged:: 1.6
   User-defined shells may be registered using entry points. Prior to this
   the only supported shells were ``ipython``, ``bpython`` and ``python``.
Setting a Default Shell
~~~~~~~~~~~~~~~~~~~~~~~
You may use the ``default_shell`` option in your ``[pshell]`` ini section to
specify a list of preferred shells.
.. code-block:: ini
   :linenos:
   [pshell]
   default_shell = ptpython ipython bpython
.. versionadded:: 1.6
.. index::
pyramid/scripts/pshell.py
@@ -12,6 +12,8 @@
from pyramid.paster import setup_logging
from pyramid.settings import aslist
from pyramid.scripts.common import parse_vars
def main(argv=sys.argv, quiet=False):
@@ -43,7 +45,14 @@
        )
    parser.add_option('-p', '--python-shell',
                      action='store', type='string', dest='python_shell',
                      default='', help='ipython | bpython | python')
                      default='',
                      help=('Select the shell to use. A list of possible '
                            'shells is available using the --list-shells '
                            'option.'))
    parser.add_option('-l', '--list-shells',
                      dest='list',
                      action='store_true',
                      help='List all available shells.')
    parser.add_option('--setup',
                      dest='setup',
                      help=("A callable that will be passed the environment "
@@ -55,6 +64,7 @@
    loaded_objects = {}
    object_help = {}
    preferred_shells = []
    setup = None
    pystartup = os.environ.get('PYTHONSTARTUP')
@@ -64,6 +74,7 @@
    def pshell_file_config(self, filename):
        config = self.ConfigParser()
        config.optionxform = str
        config.read(filename)
        try:
            items = config.items('pshell')
@@ -77,6 +88,8 @@
        for k, v in items:
            if k == 'setup':
                self.setup = v
            elif k == 'default_shell':
                self.preferred_shells = [x.lower() for x in aslist(v)]
            else:
                self.loaded_objects[k] = resolver.maybe_resolve(v)
                self.object_help[k] = v
@@ -86,6 +99,8 @@
            print(msg)
    def run(self, shell=None):
        if self.options.list:
            return self.show_shells()
        if not self.args:
            self.out('Requires a config file argument')
            return 2
@@ -169,19 +184,50 @@
        finally:
            self.closer()
    def make_shell(self):
        shells = {}
    def show_shells(self):
        shells = self.find_all_shells()
        sorted_shells = sorted(shells.items(), key=lambda x: x[0].lower())
        max_name = max([len(s) for s in shells])
        self.out('Available shells:')
        for name, factory in sorted_shells:
            shell = factory()
            if shell is not None:
                self.out('  %s' % (name,))
            else:
                self.out('  %s%s  [not available]' % (
                    name,
                    ' ' * (max_name - len(name))))
        return 0
    def find_all_shells(self):
        shells = {}
        for ep in self.pkg_resources.iter_entry_points('pyramid.pshell'):
            name = ep.name
            shell_module = ep.load()
            shells[name] = shell_module
            shell_factory = ep.load()
            shells[name] = shell_factory
        return shells
    def make_shell(self):
        shells = self.find_all_shells()
        shell = None
        user_shell = self.options.python_shell.lower()
        if not user_shell:
            sorted_shells = sorted(shells.items(), key=lambda x: x[0])
            preferred_shells = self.preferred_shells
            if not preferred_shells:
                # by default prioritize all shells above python
                preferred_shells = [k for k in shells.keys() if k != 'python']
            max_weight = len(preferred_shells)
            def order(x):
                # invert weight to reverse sort the list
                # (closer to the front is higher priority)
                try:
                    return preferred_shells.index(x[0].lower()) - max_weight
                except ValueError:
                    return 1
            sorted_shells = sorted(shells.items(), key=order)
            for name, factory in sorted_shells:
                shell = factory()
@@ -192,7 +238,8 @@
            if factory is not None:
                shell = factory()
            else:
            if shell is None:
                raise ValueError(
                    'could not find a shell named "%s"' % user_shell
                )
@@ -202,7 +249,8 @@
        return shell
    def make_default_shell(self, interact=interact):
    @classmethod
    def make_python_shell(cls, interact=interact):
        def shell(env, help):
            cprt = 'Type "help" for more information.'
            banner = "Python %s on %s\n%s" % (sys.version, sys.platform, cprt)
@@ -210,6 +258,8 @@
            interact(banner, local=env)
        return shell
    make_default_shell = make_python_shell
    @classmethod
    def make_bpython_shell(cls, BPShell=None):
        if BPShell is None: # pragma: no cover
pyramid/tests/test_scripts/dummy.py
@@ -21,10 +21,12 @@
class DummyShell(object):
    env = {}
    help = ''
    called = False
    def __call__(self, env, help):
        self.env = env
        self.help = help
        self.called = True
class DummyInteractor:
    def __call__(self, banner, local):
@@ -35,6 +37,7 @@
    def __call__(self, locals_, banner):
        self.locals_ = locals_
        self.banner = banner
        self.called = True
class DummyIPShell(object):
    IP = Dummy()
pyramid/tests/test_scripts/test_pshell.py
@@ -26,6 +26,7 @@
            self.options = Options()
            self.options.python_shell = ''
            self.options.setup = None
            self.options.list = None
            cmd.options = self.options
        # default to None to prevent side-effects from running tests in
@@ -52,7 +53,7 @@
        self.assertEqual(bpython.locals_, {'foo': 'bar'})
        self.assertTrue('a help message' in bpython.banner)
    def test_make_ipython_v1_1_shell(self):
    def test_make_ipython_shell(self):
        command = self._makeOne()
        ipshell_factory = dummy.DummyIPShellFactory()
        shell = command.make_ipython_shell(ipshell_factory)
@@ -69,6 +70,7 @@
            {
                'ipython': lambda: None,
                'bpython': lambda: None,
                'python': lambda: None,
            }
        )
@@ -87,7 +89,7 @@
        self.assertTrue(self.bootstrap.closer.called)
        self.assertTrue(shell.help)
    def test_command_loads_default_shell_with_unknown_shell(self):
    def test_command_errors_with_unknown_shell(self):
        command = self._makeOne()
        out_calls = []
@@ -120,9 +122,10 @@
        self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp')
        self.assertTrue(self.bootstrap.closer.called)
    def test_command_loads_ipython_v1_1(self):
    def test_command_loads_ipython(self):
        command = self._makeOne()
        shell = dummy.DummyShell()
        bad_shell = dummy.DummyShell()
        self._makeEntryPoints(
            command,
            {
@@ -190,7 +193,7 @@
        shell = command.make_shell()
        self.assertEqual(shell, dshell)
    def test_shell_ordering(self):
    def test_shell_override(self):
        command = self._makeOne()
        ipshell = dummy.DummyShell()
        bpshell = dummy.DummyShell()
@@ -210,12 +213,10 @@
        self.assertEqual(shell, dshell)
        command.options.python_shell = 'ipython'
        shell = command.make_shell()
        self.assertEqual(shell, dshell)
        self.assertRaises(ValueError, command.make_shell)
        command.options.python_shell = 'bpython'
        shell = command.make_shell()
        self.assertEqual(shell, dshell)
        self.assertRaises(ValueError, command.make_shell)
        self._makeEntryPoints(
            command,
@@ -235,6 +236,35 @@
        self.assertEqual(shell, bpshell)
        command.options.python_shell = 'python'
        shell = command.make_shell()
        self.assertEqual(shell, dshell)
    def test_shell_ordering(self):
        command = self._makeOne()
        ipshell = dummy.DummyShell()
        bpshell = dummy.DummyShell()
        dshell = dummy.DummyShell()
        self._makeEntryPoints(
            command,
            {
                'ipython': lambda: ipshell,
                'bpython': lambda: bpshell,
                'python': lambda: dshell,
            }
        )
        command.make_default_shell = lambda: dshell
        command.preferred_shells = ['ipython', 'bpython']
        shell = command.make_shell()
        self.assertEqual(shell, ipshell)
        command.preferred_shells = ['bpython', 'python']
        shell = command.make_shell()
        self.assertEqual(shell, bpshell)
        command.preferred_shells = ['python', 'ipython']
        shell = command.make_shell()
        self.assertEqual(shell, dshell)
@@ -281,6 +311,27 @@
        })
        self.assertTrue(self.bootstrap.closer.called)
        self.assertTrue(shell.help)
    def test_command_default_shell_option(self):
        command = self._makeOne()
        ipshell = dummy.DummyShell()
        dshell = dummy.DummyShell()
        self._makeEntryPoints(
            command,
            {
                'ipython': lambda: ipshell,
                'bpython': lambda: None,
                'python': lambda: dshell,
            }
        )
        self.config_factory.items = [
            ('default_shell', 'bpython python\nipython')]
        command.run()
        self.assertTrue(self.config_factory.parser)
        self.assertEqual(self.config_factory.parser.filename,
                         '/foo/bar/myapp.ini')
        self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp')
        self.assertTrue(dshell.called)
    def test_command_loads_check_variable_override_order(self):
        command = self._makeOne()
@@ -369,6 +420,36 @@
        self.assertTrue(self.bootstrap.closer.called)
        self.assertTrue(shell.help)
    def test_list_shells(self):
        command = self._makeOne()
        dshell = dummy.DummyShell()
        out_calls = []
        def out(msg):
            out_calls.append(msg)
        command.out = out
        self._makeEntryPoints(
            command,
            {
                'ipython': lambda: dshell,
                'bpython': lambda: None,
                'python': lambda: dshell,
            }
        )
        command.options.list = True
        result = command.run()
        self.assertEqual(result, 0)
        self.assertEqual(out_calls, [
            'Available shells:',
            '  bpython  [not available]',
            '  ipython',
            '  python',
        ])
class Test_main(unittest.TestCase):
    def _callFUT(self, argv):
        from pyramid.scripts.pshell import main
setup.py
@@ -114,6 +114,7 @@
        [pyramid.pshell]
        ipython=pyramid.scripts.pshell:PShellCommand.make_ipython_shell
        bpython=pyramid.scripts.pshell:PShellCommand.make_bpython_shell
        python=pyramid.scripts.pshell:PShellCommand.make_python_shell
        [console_scripts]
        pcreate = pyramid.scripts.pcreate:main
        pserve = pyramid.scripts.pserve:main