diff options
| -rw-r--r-- | CHANGES.txt | 21 | ||||
| -rw-r--r-- | docs/narr/commandline.rst | 72 | ||||
| -rw-r--r-- | pyramid/scripts/pshell.py | 116 | ||||
| -rw-r--r-- | pyramid/tests/test_scripts/dummy.py | 20 | ||||
| -rw-r--r-- | pyramid/tests/test_scripts/test_pshell.py | 202 | ||||
| -rw-r--r-- | setup.py | 5 |
6 files changed, 242 insertions, 194 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 4396c4356..8b63cf847 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,19 @@ 1.6 (2015-04-14) ================ +Backward Incompatibilities +-------------------------- + +- IPython and BPython support have been removed from pshell in the core. + To continue using them on Pyramid 1.6+ you must install the binding + packages explicitly:: + + $ pip install pyramid_ipython + + or + + $ pip install pyramid_bpython + Features -------- @@ -135,7 +148,8 @@ Features https://github.com/Pylons/pyramid/pull/1610 - Additional shells for ``pshell`` can now be registered as entrypoints. See - https://github.com/Pylons/pyramid/pull/1891 + https://github.com/Pylons/pyramid/pull/1891 and + https://github.com/Pylons/pyramid/pull/2012 - The variables injected into ``pshell`` are now displayed with their docstrings instead of the default ``str(obj)`` when possible. @@ -213,6 +227,11 @@ Bug Fixes WebOb 1.5. See https://github.com/Pylons/pyramid/pull/1865 +- ``pshell`` will now preserve the capitalization of variables in the + ``[pshell]`` section of the INI file. This makes exposing classes to the + shell a little more straightfoward. + See https://github.com/Pylons/pyramid/pull/1883 + Deprecations ------------ diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 9db92b669..430641a50 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -107,9 +107,7 @@ found* message. .. index:: single: interactive shell - single: IPython single: pshell - single: bpython .. _interactive_shell: @@ -263,38 +261,40 @@ request is configured to generate urls from the host >>> request.route_url('home') 'https://www.example.com/' -.. index:: - single: IPython - single: bpython +Alternative Shells +~~~~~~~~~~~~~~~~~~ -.. _ipython_or_bpython: +The ``pshell`` command can be easily extended with alternate REPLs if the +default python REPL is not satisfactory. Assuming you have a binding +installed such as ``pyramid_ipython`` it will normally be auto-selected and +used. You may also specifically invoke your choice with the ``-p choice`` or +``--python-shell choice`` option. -IPython or bpython -~~~~~~~~~~~~~~~~~~ +.. code-block:: text + + $ $VENV/bin/pshell -p ipython development.ini#MyProject -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. +You may use the ``--list-shells`` option to see the available shells. .. code-block:: text - $ $VENV/bin/pshell -p ipython | bpython | python development.ini#MyProject + $ $VENV/bin/pshell --list-shells + Available shells: + bpython + ipython + python -Alternative Shells -~~~~~~~~~~~~~~~~~~ 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_runner': [ + 'myshell=my_app:ptpython_shell_factory', + ], + }, ) And then your shell factory should return a function that accepts two @@ -302,16 +302,30 @@ arguments, ``env`` and ``help``, which would look like this: .. code-block:: python - def ptpython_shell_factory(): - from ptpython.repl import embed - def PTPShell(banner, **kwargs): - print(banner) - return embed(**kwargs) + from ptpython.repl import embed - def shell(env, help): - PTPShell(banner=help, locals=env) + def ptpython_shell_runner(env, help): + print(help) + return embed(locals=env) - return shell +.. versionchanged:: 1.6 + User-defined shells may be registered using entry points. Prior to this + the only supported shells were ``ipython``, ``bpython`` and ``python``. + + ``ipython`` and ``bpython`` have been moved into their respective + packages ``pyramid_ipython`` and ``pyramid_bpython``. + +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 diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index 5913220fc..0a7cfbbe5 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -12,6 +12,8 @@ from pyramid.paster import bootstrap 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): @@ -19,6 +21,13 @@ def main(argv=sys.argv, quiet=False): return command.run() +def python_shell_runner(env, help, interact=interact): + cprt = 'Type "help" for more information.' + banner = "Python %s on %s\n%s" % (sys.version, sys.platform, cprt) + banner += '\n\n' + help + '\n' + interact(banner, local=env) + + class PShellCommand(object): usage = '%prog config_uri' description = """\ @@ -43,7 +52,14 @@ class PShellCommand(object): ) 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 " @@ -52,9 +68,11 @@ class PShellCommand(object): "[pshell] ini section.")) ConfigParser = configparser.ConfigParser # testing + default_runner = python_shell_runner # testing loaded_objects = {} object_help = {} + preferred_shells = [] setup = None pystartup = os.environ.get('PYTHONSTARTUP') @@ -64,6 +82,7 @@ class PShellCommand(object): def pshell_file_config(self, filename): config = self.ConfigParser() + config.optionxform = str config.read(filename) try: items = config.items('pshell') @@ -77,6 +96,8 @@ class PShellCommand(object): 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 +107,8 @@ class PShellCommand(object): 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,71 +192,64 @@ class PShellCommand(object): finally: self.closer() - def make_shell(self): - shells = {} + def show_shells(self): + shells = self.find_all_shells() + sorted_names = sorted(shells.keys(), key=lambda x: x.lower()) - for ep in self.pkg_resources.iter_entry_points('pyramid.pshell'): + self.out('Available shells:') + for name in sorted_names: + self.out(' %s' % (name,)) + return 0 + + def find_all_shells(self): + pkg_resources = self.pkg_resources + + shells = {} + for ep in pkg_resources.iter_entry_points('pyramid.pshell_runner'): 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]) - for name, factory in sorted_shells: - shell = factory() + 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) + + if len(sorted_shells) > 0: + shell = sorted_shells[0][1] - if shell is not None: - break else: - factory = shells.get(user_shell) + runner = shells.get(user_shell) - if factory is not None: - shell = factory() - else: + if runner is not None: + shell = runner + + if shell is None: raise ValueError( 'could not find a shell named "%s"' % user_shell ) if shell is None: - shell = self.make_default_shell() - - return shell - - def make_default_shell(self, interact=interact): - def shell(env, help): - cprt = 'Type "help" for more information.' - banner = "Python %s on %s\n%s" % (sys.version, sys.platform, cprt) - banner += '\n\n' + help + '\n' - interact(banner, local=env) - return shell + # should never happen, but just incase entry points are borked + shell = self.default_runner - @classmethod - def make_bpython_shell(cls, BPShell=None): - if BPShell is None: # pragma: no cover - try: - from bpython import embed - BPShell = embed - except ImportError: - return None - def shell(env, help): - BPShell(locals_=env, banner=help + '\n') - return shell - - @classmethod - def make_ipython_shell(cls, IPShellFactory=None): - if IPShellFactory is None: # pragma: no cover - try: - from IPython.terminal.embed import ( - InteractiveShellEmbed) - IPShellFactory = InteractiveShellEmbed - except ImportError: - return None - def shell(env, help): - IPShell = IPShellFactory(banner2=help + '\n', user_ns=env) - IPShell() return shell diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py index 2788c0b32..a872e197c 100644 --- a/pyramid/tests/test_scripts/dummy.py +++ b/pyramid/tests/test_scripts/dummy.py @@ -21,34 +21,18 @@ dummy_registry = DummyRegistry() 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): self.banner = banner self.local = local -class DummyBPythonShell: - def __call__(self, locals_, banner): - self.locals_ = locals_ - self.banner = banner - -class DummyIPShell(object): - IP = Dummy() - IP.BANNER = 'foo' - - def __call__(self): - self.called = True - -class DummyIPShellFactory(object): - def __call__(self, **kw): - self.kw = kw - self.shell = DummyIPShell() - return self.shell - class DummyApp: def __init__(self): self.registry = dummy_registry diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py index 034f2109d..f98ded6d3 100644 --- a/pyramid/tests/test_scripts/test_pshell.py +++ b/pyramid/tests/test_scripts/test_pshell.py @@ -26,6 +26,7 @@ class TestPShellCommand(unittest.TestCase): 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 @@ -36,43 +37,12 @@ class TestPShellCommand(unittest.TestCase): def _makeEntryPoints(self, command, shells): command.pkg_resources = dummy.DummyPkgResources(shells) - def test_make_default_shell(self): - command = self._makeOne() - interact = dummy.DummyInteractor() - shell = command.make_default_shell(interact) - shell({'foo': 'bar'}, 'a help message') - self.assertEqual(interact.local, {'foo': 'bar'}) - self.assertTrue('a help message' in interact.banner) - - def test_make_bpython_shell(self): - command = self._makeOne() - bpython = dummy.DummyBPythonShell() - shell = command.make_bpython_shell(bpython) - shell({'foo': 'bar'}, 'a help message') - self.assertEqual(bpython.locals_, {'foo': 'bar'}) - self.assertTrue('a help message' in bpython.banner) - - def test_make_ipython_v1_1_shell(self): - command = self._makeOne() - ipshell_factory = dummy.DummyIPShellFactory() - shell = command.make_ipython_shell(ipshell_factory) - shell({'foo': 'bar'}, 'a help message') - self.assertEqual(ipshell_factory.kw['user_ns'], {'foo': 'bar'}) - self.assertTrue('a help message' in ipshell_factory.kw['banner2']) - self.assertTrue(ipshell_factory.shell.called) - def test_command_loads_default_shell(self): command = self._makeOne() shell = dummy.DummyShell() - self._makeEntryPoints( - command, - { - 'ipython': lambda: None, - 'bpython': lambda: None, - } - ) + self._makeEntryPoints(command, {}) - command.make_default_shell = lambda: shell + command.default_runner = shell command.run() self.assertTrue(self.config_factory.parser) self.assertEqual(self.config_factory.parser.filename, @@ -87,7 +57,7 @@ class TestPShellCommand(unittest.TestCase): 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 = [] @@ -97,17 +67,10 @@ class TestPShellCommand(unittest.TestCase): command.out = out shell = dummy.DummyShell() - bad_shell = dummy.DummyShell() - self._makeEntryPoints( - command, - { - 'ipython': lambda: bad_shell, - 'bpython': lambda: bad_shell, - } - ) + self._makeEntryPoints(command, {}) - command.make_default_shell = lambda: shell + command.default_runner = shell command.options.python_shell = 'unknown_python_shell' result = command.run() self.assertEqual(result, 1) @@ -120,14 +83,15 @@ class TestPShellCommand(unittest.TestCase): 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, { - 'ipython': lambda: shell, - 'bpython': lambda: bad_shell, + 'ipython': shell, + 'bpython': bad_shell, } ) @@ -147,33 +111,6 @@ class TestPShellCommand(unittest.TestCase): self.assertTrue(self.bootstrap.closer.called) self.assertTrue(shell.help) - def test_command_loads_bpython_shell(self): - command = self._makeOne() - shell = dummy.DummyBPythonShell() - - self._makeEntryPoints( - command, - { - 'ipython': lambda: None, - 'bpython': lambda: shell, - } - ) - - command.options.python_shell = 'bpython' - 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.assertEqual(shell.locals_, { - 'app':self.bootstrap.app, 'root':self.bootstrap.root, - 'registry':self.bootstrap.registry, - 'request':self.bootstrap.request, - 'root_factory':self.bootstrap.root_factory, - }) - self.assertTrue(self.bootstrap.closer.called) - self.assertTrue(shell.banner) - def test_shell_entry_points(self): command = self._makeOne() dshell = dummy.DummyShell() @@ -181,67 +118,86 @@ class TestPShellCommand(unittest.TestCase): self._makeEntryPoints( command, { - 'ipython': lambda: dshell, - 'bpython': lambda: dshell, + 'ipython': dshell, + 'bpython': dshell, } ) - command.make_default_shell = lambda: None + command.default_runner = None 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() dshell = dummy.DummyShell() + self._makeEntryPoints(command, {}) + + command.default_runner = dshell + + shell = command.make_shell() + self.assertEqual(shell, dshell) + + command.options.python_shell = 'ipython' + self.assertRaises(ValueError, command.make_shell) + self._makeEntryPoints( command, { - 'ipython': lambda: None, - 'bpython': lambda: None, + 'ipython': ipshell, + 'bpython': bpshell, + 'python': dshell, } ) - command.make_default_shell = lambda: dshell - - shell = command.make_shell() - self.assertEqual(shell, dshell) - command.options.python_shell = 'ipython' shell = command.make_shell() - self.assertEqual(shell, dshell) + self.assertEqual(shell, ipshell) command.options.python_shell = 'bpython' shell = command.make_shell() + 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, + 'ipython': ipshell, + 'bpython': bpshell, + 'python': dshell, } ) - command.options.python_shell = 'ipython' + command.default_runner = dshell + + command.preferred_shells = ['ipython', 'bpython'] shell = command.make_shell() self.assertEqual(shell, ipshell) - command.options.python_shell = 'bpython' + command.preferred_shells = ['bpython', 'python'] shell = command.make_shell() self.assertEqual(shell, bpshell) - command.options.python_shell = 'python' + command.preferred_shells = ['python', 'ipython'] shell = command.make_shell() self.assertEqual(shell, dshell) def test_command_loads_custom_items(self): command = self._makeOne() model = dummy.Dummy() - self.config_factory.items = [('m', model)] + user = dummy.Dummy() + self.config_factory.items = [('m', model), ('User', user)] shell = dummy.DummyShell() command.run(shell) self.assertTrue(self.config_factory.parser) @@ -254,6 +210,7 @@ class TestPShellCommand(unittest.TestCase): 'request':self.bootstrap.request, 'root_factory':self.bootstrap.root_factory, 'm':model, + 'User': user, }) self.assertTrue(self.bootstrap.closer.called) self.assertTrue(shell.help) @@ -282,6 +239,26 @@ class TestPShellCommand(unittest.TestCase): 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': ipshell, + 'python': 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() model = dummy.Dummy() @@ -369,6 +346,46 @@ class TestPShellCommand(unittest.TestCase): 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': dshell, + 'python': dshell, + } + ) + + command.options.list = True + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(out_calls, [ + 'Available shells:', + ' ipython', + ' python', + ]) + + +class Test_python_shell_runner(unittest.TestCase): + def _callFUT(self, env, help, interact): + from pyramid.scripts.pshell import python_shell_runner + return python_shell_runner(env, help, interact=interact) + + def test_it(self): + interact = dummy.DummyInteractor() + self._callFUT({'foo': 'bar'}, 'a help message', interact) + self.assertEqual(interact.local, {'foo': 'bar'}) + self.assertTrue('a help message' in interact.banner) + class Test_main(unittest.TestCase): def _callFUT(self, argv): from pyramid.scripts.pshell import main @@ -377,4 +394,3 @@ class Test_main(unittest.TestCase): def test_it(self): result = self._callFUT(['pshell']) self.assertEqual(result, 2) - @@ -111,9 +111,8 @@ setup(name='pyramid', starter=pyramid.scaffolds:StarterProjectTemplate zodb=pyramid.scaffolds:ZODBProjectTemplate alchemy=pyramid.scaffolds:AlchemyProjectTemplate - [pyramid.pshell] - ipython=pyramid.scripts.pshell:PShellCommand.make_ipython_shell - bpython=pyramid.scripts.pshell:PShellCommand.make_bpython_shell + [pyramid.pshell_runner] + python=pyramid.scripts.pshell:python_shell_runner [console_scripts] pcreate = pyramid.scripts.pcreate:main pserve = pyramid.scripts.pserve:main |
