From 52475abb22a849a8fe48de9e8e844c2f97828085 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 04:53:58 -0600 Subject: [PATCH 01/11] Decouple shell helpers --- tests/test_cli.py | 6 +-- tmuxp/cli.py | 130 +++++++--------------------------------------- tmuxp/shell.py | 84 ++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 113 deletions(-) create mode 100644 tmuxp/shell.py diff --git a/tests/test_cli.py b/tests/test_cli.py index 7c8cbf51881..651970cfa29 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -13,7 +13,7 @@ import libtmux from libtmux.common import has_lt_version from libtmux.exc import LibTmuxException -from tmuxp import cli, config +from tmuxp import cli, config, exc from tmuxp.cli import ( command_ls, get_config_dir, @@ -537,7 +537,7 @@ def test_shell( [], {}, {'session_name': 'nonexistant_session'}, - None, + exc.TmuxpException, 'Session not found: nonexistant_session', ), ( @@ -551,7 +551,7 @@ def test_shell( [], {}, {'window_name': 'nonexistant_window'}, - None, + exc.TmuxpException, 'Window not found: {WINDOW_NAME}', ), ], diff --git a/tmuxp/cli.py b/tmuxp/cli.py index 59306f3d450..f0f219f588e 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -16,10 +16,10 @@ from click.exceptions import FileError from libtmux.common import has_gte_version, has_minimum_version, which -from libtmux.exc import LibTmuxException, TmuxCommandNotFound +from libtmux.exc import TmuxCommandNotFound from libtmux.server import Server -from . import config, exc, log, util +from . import config, exc, log, shell, util from .__about__ import __version__ from ._compat import string_types from .workspacebuilder import WorkspaceBuilder, freeze @@ -682,64 +682,19 @@ def command_shell(session_name, window_name, socket_name, socket_path, command): """ server = Server(socket_name=socket_name, socket_path=socket_path) - try: - server.sessions - except LibTmuxException as e: - if 'No such file or directory' in str(e): - raise LibTmuxException( - 'no tmux session found. Start a tmux session and try again. \n' - 'Original error: ' + str(e) - ) - else: - raise e + shell.raise_if_tmux_not_running(server=server) - current_pane = None - if os.getenv('TMUX_PANE') is not None: - try: - current_pane = [ - p - for p in server._list_panes() - if p.get('pane_id') == os.getenv('TMUX_PANE') - ][0] - except IndexError: - pass - - try: - if session_name: - session = server.find_where({'session_name': session_name}) - elif current_pane is not None: - session = server.find_where({'session_id': current_pane['session_id']}) - else: - session = server.list_sessions()[0] - - if not session: - raise exc.TmuxpException('Session not found: %s' % session_name) - except exc.TmuxpException as e: - print(e) - return + current_pane = shell.get_current_pane(server=server) - try: - if window_name: - window = session.find_where({'window_name': window_name}) - if not window: - raise exc.TmuxpException('Window not found: %s' % window_name) - elif current_pane is not None: - window = session.find_where({'window_id': current_pane['window_id']}) - else: - window = session.list_windows()[0] + session = shell.get_session( + server=server, session_name=session_name, current_pane=current_pane + ) - except exc.TmuxpException as e: - print(e) - return + window = shell.get_window( + session=session, window_name=window_name, current_pane=current_pane + ) - try: - if current_pane is not None: - pane = window.find_where({'pane_id': current_pane['pane_id']}) # NOQA: F841 - else: - pane = window.attached_pane # NOQA: F841 - except exc.TmuxpException as e: - print(e) - return + pane = shell.get_pane(window=window, current_pane=current_pane) # NOQA: F841 if command is not None: exec(command) @@ -779,64 +734,19 @@ def command_shell_plus( """ server = Server(socket_name=socket_name, socket_path=socket_path) - try: - server.sessions - except LibTmuxException as e: - if 'No such file or directory' in str(e): - raise LibTmuxException( - 'no tmux session found. Start a tmux session and try again. \n' - 'Original error: ' + str(e) - ) - else: - raise e + shell.raise_if_tmux_not_running(server=server) - current_pane = None - if os.getenv('TMUX_PANE') is not None: - try: - current_pane = [ - p - for p in server._list_panes() - if p.get('pane_id') == os.getenv('TMUX_PANE') - ][0] - except IndexError: - pass - - try: - if session_name: - session = server.find_where({'session_name': session_name}) - elif current_pane is not None: - session = server.find_where({'session_id': current_pane['session_id']}) - else: - session = server.list_sessions()[0] - - if not session: - raise exc.TmuxpException('Session not found: %s' % session_name) - except exc.TmuxpException as e: - print(e) - return + current_pane = shell.get_current_pane(server=server) - try: - if window_name: - window = session.find_where({'window_name': window_name}) - if not window: - raise exc.TmuxpException('Window not found: %s' % window_name) - elif current_pane is not None: - window = session.find_where({'window_id': current_pane['window_id']}) - else: - window = session.list_windows()[0] + session = shell.get_session( + server=server, session_name=session_name, current_pane=current_pane + ) - except exc.TmuxpException as e: - print(e) - return + window = shell.get_window( + session=session, window_name=window_name, current_pane=current_pane + ) - try: - if current_pane is not None: - pane = window.find_where({'pane_id': current_pane['pane_id']}) # NOQA: F841 - else: - pane = window.attached_pane # NOQA: F841 - except exc.TmuxpException as e: - print(e) - return + pane = shell.get_pane(window=window, current_pane=current_pane) # NOQA: F841 if command is not None: exec(command) diff --git a/tmuxp/shell.py b/tmuxp/shell.py new file mode 100644 index 00000000000..ba994821bf9 --- /dev/null +++ b/tmuxp/shell.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""Command line tool for managing tmux workspaces and tmuxp configurations. + +tmuxp.shell +~~~~~~~~~~~ + +""" +from __future__ import absolute_import + +import logging +import os + +from libtmux.exc import LibTmuxException + +from . import exc + +logger = logging.getLogger(__name__) + + +def raise_if_tmux_not_running(server): + """Raise exception if not running. More descriptive error if no server found.""" + try: + server.sessions + except LibTmuxException as e: + if 'No such file or directory' in str(e): + raise LibTmuxException( + 'no tmux session found. Start a tmux session and try again. \n' + 'Original error: ' + str(e) + ) + else: + raise e + + +def get_current_pane(server): + """Return Pane if one found in env""" + if os.getenv('TMUX_PANE') is not None: + try: + return [ + p + for p in server._list_panes() + if p.get('pane_id') == os.getenv('TMUX_PANE') + ][0] + except IndexError: + pass + + +def get_session(server, session_name=None, current_pane=None): + if session_name: + session = server.find_where({'session_name': session_name}) + elif current_pane is not None: + session = server.find_where({'session_id': current_pane['session_id']}) + else: + session = server.list_sessions()[0] + + if not session: + raise exc.TmuxpException('Session not found: %s' % session_name) + + return session + + +def get_window(session, window_name=None, current_pane=None): + if window_name: + window = session.find_where({'window_name': window_name}) + if not window: + raise exc.TmuxpException('Window not found: %s' % window_name) + elif current_pane is not None: + window = session.find_where({'window_id': current_pane['window_id']}) + else: + window = session.list_windows()[0] + + return window + + +def get_pane(window, current_pane=None): + try: + if current_pane is not None: + pane = window.find_where({'pane_id': current_pane['pane_id']}) # NOQA: F841 + else: + pane = window.attached_pane # NOQA: F841 + except exc.TmuxpException as e: + print(e) + return + + return pane From 83659635ed5fe56adcb06133542c78ffab8624fe Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 04:56:36 -0600 Subject: [PATCH 02/11] Keep helper methods in util --- tmuxp/cli.py | 22 ++++++------- tmuxp/shell.py | 84 -------------------------------------------------- tmuxp/util.py | 69 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 95 deletions(-) delete mode 100644 tmuxp/shell.py diff --git a/tmuxp/cli.py b/tmuxp/cli.py index f0f219f588e..d9ee0ec2b8e 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -19,7 +19,7 @@ from libtmux.exc import TmuxCommandNotFound from libtmux.server import Server -from . import config, exc, log, shell, util +from . import config, exc, log, util from .__about__ import __version__ from ._compat import string_types from .workspacebuilder import WorkspaceBuilder, freeze @@ -682,19 +682,19 @@ def command_shell(session_name, window_name, socket_name, socket_path, command): """ server = Server(socket_name=socket_name, socket_path=socket_path) - shell.raise_if_tmux_not_running(server=server) + util.raise_if_tmux_not_running(server=server) - current_pane = shell.get_current_pane(server=server) + current_pane = util.get_current_pane(server=server) - session = shell.get_session( + session = util.get_session( server=server, session_name=session_name, current_pane=current_pane ) - window = shell.get_window( + window = util.get_window( session=session, window_name=window_name, current_pane=current_pane ) - pane = shell.get_pane(window=window, current_pane=current_pane) # NOQA: F841 + pane = util.get_pane(window=window, current_pane=current_pane) # NOQA: F841 if command is not None: exec(command) @@ -734,19 +734,19 @@ def command_shell_plus( """ server = Server(socket_name=socket_name, socket_path=socket_path) - shell.raise_if_tmux_not_running(server=server) + util.raise_if_tmux_not_running(server=server) - current_pane = shell.get_current_pane(server=server) + current_pane = util.get_current_pane(server=server) - session = shell.get_session( + session = util.get_session( server=server, session_name=session_name, current_pane=current_pane ) - window = shell.get_window( + window = util.get_window( session=session, window_name=window_name, current_pane=current_pane ) - pane = shell.get_pane(window=window, current_pane=current_pane) # NOQA: F841 + pane = util.get_pane(window=window, current_pane=current_pane) # NOQA: F841 if command is not None: exec(command) diff --git a/tmuxp/shell.py b/tmuxp/shell.py deleted file mode 100644 index ba994821bf9..00000000000 --- a/tmuxp/shell.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -"""Command line tool for managing tmux workspaces and tmuxp configurations. - -tmuxp.shell -~~~~~~~~~~~ - -""" -from __future__ import absolute_import - -import logging -import os - -from libtmux.exc import LibTmuxException - -from . import exc - -logger = logging.getLogger(__name__) - - -def raise_if_tmux_not_running(server): - """Raise exception if not running. More descriptive error if no server found.""" - try: - server.sessions - except LibTmuxException as e: - if 'No such file or directory' in str(e): - raise LibTmuxException( - 'no tmux session found. Start a tmux session and try again. \n' - 'Original error: ' + str(e) - ) - else: - raise e - - -def get_current_pane(server): - """Return Pane if one found in env""" - if os.getenv('TMUX_PANE') is not None: - try: - return [ - p - for p in server._list_panes() - if p.get('pane_id') == os.getenv('TMUX_PANE') - ][0] - except IndexError: - pass - - -def get_session(server, session_name=None, current_pane=None): - if session_name: - session = server.find_where({'session_name': session_name}) - elif current_pane is not None: - session = server.find_where({'session_id': current_pane['session_id']}) - else: - session = server.list_sessions()[0] - - if not session: - raise exc.TmuxpException('Session not found: %s' % session_name) - - return session - - -def get_window(session, window_name=None, current_pane=None): - if window_name: - window = session.find_where({'window_name': window_name}) - if not window: - raise exc.TmuxpException('Window not found: %s' % window_name) - elif current_pane is not None: - window = session.find_where({'window_id': current_pane['window_id']}) - else: - window = session.list_windows()[0] - - return window - - -def get_pane(window, current_pane=None): - try: - if current_pane is not None: - pane = window.find_where({'pane_id': current_pane['pane_id']}) # NOQA: F841 - else: - pane = window.attached_pane # NOQA: F841 - except exc.TmuxpException as e: - print(e) - return - - return pane diff --git a/tmuxp/util.py b/tmuxp/util.py index 5b7f3c73e6f..6cf66b68cec 100644 --- a/tmuxp/util.py +++ b/tmuxp/util.py @@ -13,6 +13,8 @@ import subprocess import sys +from libtmux.exc import LibTmuxException + from . import exc from ._compat import console_to_str @@ -74,3 +76,70 @@ def oh_my_zsh_auto_title(): 'Then create a new shell or type:\n\n' '\t$ source ~/.zshrc' ) + + +def raise_if_tmux_not_running(server): + """Raise exception if not running. More descriptive error if no server found.""" + try: + server.sessions + except LibTmuxException as e: + if 'No such file or directory' in str(e): + raise LibTmuxException( + 'no tmux session found. Start a tmux session and try again. \n' + 'Original error: ' + str(e) + ) + else: + raise e + + +def get_current_pane(server): + """Return Pane if one found in env""" + if os.getenv('TMUX_PANE') is not None: + try: + return [ + p + for p in server._list_panes() + if p.get('pane_id') == os.getenv('TMUX_PANE') + ][0] + except IndexError: + pass + + +def get_session(server, session_name=None, current_pane=None): + if session_name: + session = server.find_where({'session_name': session_name}) + elif current_pane is not None: + session = server.find_where({'session_id': current_pane['session_id']}) + else: + session = server.list_sessions()[0] + + if not session: + raise exc.TmuxpException('Session not found: %s' % session_name) + + return session + + +def get_window(session, window_name=None, current_pane=None): + if window_name: + window = session.find_where({'window_name': window_name}) + if not window: + raise exc.TmuxpException('Window not found: %s' % window_name) + elif current_pane is not None: + window = session.find_where({'window_id': current_pane['window_id']}) + else: + window = session.list_windows()[0] + + return window + + +def get_pane(window, current_pane=None): + try: + if current_pane is not None: + pane = window.find_where({'pane_id': current_pane['pane_id']}) # NOQA: F841 + else: + pane = window.attached_pane # NOQA: F841 + except exc.TmuxpException as e: + print(e) + return + + return pane From 63bc260c89df5bcecebfdb31e5f71bfe727b62b4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 04:58:16 -0600 Subject: [PATCH 03/11] docs(API): Add new util methods --- docs/api.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 031e3d9f1bf..c3b523ddd38 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -15,6 +15,18 @@ Internals .. automethod:: tmuxp.util.run_before_script +.. automethod:: tmuxp.util.oh_my_zsh_auto_title + +.. automethod:: tmuxp.util.raise_if_tmux_not_running + +.. automethod:: tmuxp.util.get_current_pane + +.. automethod:: tmuxp.util.get_session + +.. automethod:: tmuxp.util.get_window + +.. automethod:: tmuxp.util.get_pane + CLI --- From 663fcaa959ba62e5df06ab8abb65a94187409b79 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 05:30:48 -0600 Subject: [PATCH 04/11] Split out shell, launch into shell by default unless BREAKPOINT entered --- tmuxp/cli.py | 22 ++++++++++++--- tmuxp/shell.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 tmuxp/shell.py diff --git a/tmuxp/cli.py b/tmuxp/cli.py index d9ee0ec2b8e..d56f23ae2da 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -21,7 +21,7 @@ from . import config, exc, log, util from .__about__ import __version__ -from ._compat import string_types +from ._compat import PY3, PYMINOR, string_types from .workspacebuilder import WorkspaceBuilder, freeze logger = logging.getLogger(__name__) @@ -671,7 +671,15 @@ def startup(config_dir): 'command', help='Instead of opening shell, execute python code in libtmux and exit', ) -def command_shell(session_name, window_name, socket_name, socket_path, command): +@click.option( + '--use-pdb/--no-pdb', + 'use_pdb', + help='Use pdb / breakpoint() instead of code.interact()', + default=False, +) +def command_shell( + session_name, window_name, socket_name, socket_path, command, use_pdb +): """Launch python shell for tmux server, session, window and pane. Priority given to loaded session/wndow/pane objects: @@ -699,9 +707,15 @@ def command_shell(session_name, window_name, socket_name, socket_path, command): if command is not None: exec(command) else: - from ._compat import breakpoint as tmuxp_breakpoint + if use_pdb or (os.getenv('PYTHONBREAKPOINT') and PY3 and PYMINOR >= 7): + from ._compat import breakpoint as tmuxp_breakpoint + + tmuxp_breakpoint() + return + else: + from .shell import launch - tmuxp_breakpoint() + launch(server=server, session=session, window=window, pane=pane) @cli.command(name='shell_plus') diff --git a/tmuxp/shell.py b/tmuxp/shell.py new file mode 100644 index 00000000000..67ab226d2f7 --- /dev/null +++ b/tmuxp/shell.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +"""Utility and helper methods for tmuxp. + +tmuxp.shell +~~~~~~~~~~~ + +""" +from __future__ import absolute_import, unicode_literals + +import logging +import os + +logger = logging.getLogger(__name__) + + +def launch(shell=None, best=True, use_pythonrc=False, **kwargs): + import code + + import libtmux + + imported_objects = { + 'libtmux': libtmux, + 'Server': libtmux.Server, + 'Session': libtmux.Session, + 'Window': libtmux.Window, + 'Pane': libtmux.Pane, + 'server': kwargs.get('server'), + 'session': kwargs.get('session'), + 'window': kwargs.get('window'), + 'pane': kwargs.get('pane'), + } + + try: + # Try activating rlcompleter, because it's handy. + import readline + except ImportError: + pass + else: + # We don't have to wrap the following import in a 'try', because + # we already know 'readline' was imported successfully. + import rlcompleter + + readline.set_completer(rlcompleter.Completer(imported_objects).complete) + # Enable tab completion on systems using libedit (e.g. macOS). + # These lines are copied from Lib/site.py on Python 3.4. + readline_doc = getattr(readline, '__doc__', '') + if readline_doc is not None and 'libedit' in readline_doc: + readline.parse_and_bind("bind ^I rl_complete") + else: + readline.parse_and_bind("tab:complete") + + # We want to honor both $PYTHONSTARTUP and .pythonrc.py, so follow system + # conventions and get $PYTHONSTARTUP first then .pythonrc.py. + if use_pythonrc: + for pythonrc in set( + [os.environ.get("PYTHONSTARTUP"), os.path.expanduser('~/.pythonrc.py')] + ): + if not pythonrc: + continue + if not os.path.isfile(pythonrc): + continue + with open(pythonrc) as handle: + pythonrc_code = handle.read() + # Match the behavior of the cpython shell where an error in + # PYTHONSTARTUP prints an exception and continues. + try: + exec(compile(pythonrc_code, pythonrc, 'exec'), imported_objects) + except Exception: + import traceback + + traceback.print_exc() + + code.interact(local=imported_objects) From 477ec685c6b595495b1bfeddeda8819204fd5731 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 05:41:09 -0600 Subject: [PATCH 05/11] Remove shell_plus: Update tests --- tests/test_cli.py | 14 +++--- tmuxp/cli.py | 123 ++++++---------------------------------------- 2 files changed, 22 insertions(+), 115 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 651970cfa29..6352c07f6ce 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -407,7 +407,7 @@ def test_load_zsh_autotitle_warning(cli_args, tmpdir, monkeypatch): assert 'Please set' not in result.output -@pytest.mark.parametrize("cli_cmd", ['shell', 'shell_plus']) +@pytest.mark.parametrize("cli_cmd", ['shell', ('shell', '--use-pdb')]) @pytest.mark.parametrize( "cli_args,inputs,env,expected_output", [ @@ -501,7 +501,8 @@ def test_shell( SERVER_SOCKET_NAME=server.socket_name, ) - cli_args = [cli_cmd] + [cli_arg.format(**template_ctx) for cli_arg in cli_args] + cli_cmd = list(cli_cmd) if isinstance(cli_cmd, (list, tuple)) else [cli_cmd] + cli_args = cli_cmd + [cli_arg.format(**template_ctx) for cli_arg in cli_args] for k, v in env.items(): monkeypatch.setenv(k, v.format(**template_ctx)) @@ -515,7 +516,7 @@ def test_shell( assert expected_output.format(**template_ctx) in result.output -@pytest.mark.parametrize("cli_cmd", ['shell', 'shell_plus']) +@pytest.mark.parametrize("cli_cmd", ['shell', ('shell', '--use-pdb')]) @pytest.mark.parametrize( "cli_args,inputs,env,template_ctx,exception,message", [ @@ -583,7 +584,8 @@ def test_shell_target_missing( PANE_ID=template_ctx.get('pane_id'), SERVER_SOCKET_NAME=server.socket_name, ) - cli_args = [cli_cmd] + [cli_arg.format(**template_ctx) for cli_arg in cli_args] + cli_cmd = list(cli_cmd) if isinstance(cli_cmd, (list, tuple)) else [cli_cmd] + cli_args = cli_cmd + [cli_arg.format(**template_ctx) for cli_arg in cli_args] for k, v in env.items(): monkeypatch.setenv(k, v.format(**template_ctx)) @@ -608,7 +610,7 @@ def test_shell_target_missing( [ ( [ - 'shell_plus', + 'shell', '-L{SOCKET_NAME}', ], [], @@ -617,7 +619,7 @@ def test_shell_target_missing( ), ( [ - 'shell_plus', + 'shell', '-L{SOCKET_NAME}', ], [], diff --git a/tmuxp/cli.py b/tmuxp/cli.py index d56f23ae2da..29ddd411d46 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -677,8 +677,14 @@ def startup(config_dir): help='Use pdb / breakpoint() instead of code.interact()', default=False, ) +@click.option( + '--use-pythonrc/--no-startup', + 'use_pythonrc', + help='Load the PYTHONSTARTUP environment variable and ~/.pythonrc.py script.', + default=False, +) def command_shell( - session_name, window_name, socket_name, socket_path, command, use_pdb + session_name, window_name, socket_name, socket_path, command, use_pdb, use_pythonrc ): """Launch python shell for tmux server, session, window and pane. @@ -715,114 +721,13 @@ def command_shell( else: from .shell import launch - launch(server=server, session=session, window=window, pane=pane) - - -@cli.command(name='shell_plus') -@click.argument('session_name', nargs=1, required=False) -@click.argument('window_name', nargs=1, required=False) -@click.option('-S', 'socket_path', help='pass-through for tmux -S') -@click.option('-L', 'socket_name', help='pass-through for tmux -L') -@click.option( - '-c', - 'command', - help='Instead of opening shell, execute python code in libtmux and exit', -) -@click.option( - '--use-pythonrc/--no-startup', - 'use_pythonrc', - help='Load the PYTHONSTARTUP environment variable and ~/.pythonrc.py script.', - default=False, -) -def command_shell_plus( - session_name, - window_name, - socket_name, - socket_path, - command, - use_pythonrc, -): - """shell w/ tab completion. - - Credits: django-extensions shell_plus.py 51fef74 (MIT License) - """ - server = Server(socket_name=socket_name, socket_path=socket_path) - - util.raise_if_tmux_not_running(server=server) - - current_pane = util.get_current_pane(server=server) - - session = util.get_session( - server=server, session_name=session_name, current_pane=current_pane - ) - - window = util.get_window( - session=session, window_name=window_name, current_pane=current_pane - ) - - pane = util.get_pane(window=window, current_pane=current_pane) # NOQA: F841 - - if command is not None: - exec(command) - else: - # Using normal Python shell - import code - - import libtmux - - imported_objects = { - 'libtmux': libtmux, - 'Server': libtmux.Server, - 'Session': libtmux.Session, - 'Window': libtmux.Window, - 'Pane': libtmux.Pane, - 'server': server, - 'session': session, - 'window': window, - 'pane': pane, - } - - try: - # Try activating rlcompleter, because it's handy. - import readline - except ImportError: - pass - else: - # We don't have to wrap the following import in a 'try', because - # we already know 'readline' was imported successfully. - import rlcompleter - - readline.set_completer(rlcompleter.Completer(imported_objects).complete) - # Enable tab completion on systems using libedit (e.g. macOS). - # These lines are copied from Lib/site.py on Python 3.4. - readline_doc = getattr(readline, '__doc__', '') - if readline_doc is not None and 'libedit' in readline_doc: - readline.parse_and_bind("bind ^I rl_complete") - else: - readline.parse_and_bind("tab:complete") - - # We want to honor both $PYTHONSTARTUP and .pythonrc.py, so follow system - # conventions and get $PYTHONSTARTUP first then .pythonrc.py. - if use_pythonrc: - for pythonrc in set( - [os.environ.get("PYTHONSTARTUP"), os.path.expanduser('~/.pythonrc.py')] - ): - if not pythonrc: - continue - if not os.path.isfile(pythonrc): - continue - with open(pythonrc) as handle: - pythonrc_code = handle.read() - # Match the behavior of the cpython shell where an error in - # PYTHONSTARTUP prints an exception and continues. - try: - exec(compile(pythonrc_code, pythonrc, 'exec'), imported_objects) - except Exception: - import traceback - - traceback.print_exc() - - code.interact(local=imported_objects) + launch( + server=server, + session=session, + window=window, + pane=pane, + use_pythonrc=use_pythonrc, + ) @cli.command(name='freeze') From 5e6c56fb79a5b7f2f9c376bce399af1ee9dbe201 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 05:54:02 -0600 Subject: [PATCH 06/11] Checks for having python shells --- tmuxp/shell.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tmuxp/shell.py b/tmuxp/shell.py index 67ab226d2f7..c9c0c63f497 100644 --- a/tmuxp/shell.py +++ b/tmuxp/shell.py @@ -13,7 +13,45 @@ logger = logging.getLogger(__name__) -def launch(shell=None, best=True, use_pythonrc=False, **kwargs): +def has_ipython(): + try: + from IPython import start_ipython # NOQA F841 + except ImportError: + try: + from IPython.Shell import IPShell # NOQA F841 + except ImportError: + return False + + return True + + +def has_ptpython(): + try: + from ptpython.repl import embed, run_config # NOQA F841 + except ImportError: + try: + from prompt_toolkit.contrib.repl import embed, run_config # NOQA F841 + except ImportError: + return False + + return True + + +def has_ptipython(): + try: + from ptpython.ipython import embed # NOQA F841 + from ptpython.repl import run_config # NOQA F841 + except ImportError: + try: + from prompt_toolkit.contrib.ipython import embed # NOQA F841 + from prompt_toolkit.contrib.repl import run_config # NOQA F841 + except ImportError: + return False + + return True + + +def launch(shell='best', use_pythonrc=False, **kwargs): import code import libtmux From 36f37ea29aa8e59b46144d0ae4b5ca5930e7c625 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 07:24:44 -0600 Subject: [PATCH 07/11] Automatically detect the best shell --- tests/test_cli.py | 4 +- tmuxp/cli.py | 19 +++--- tmuxp/shell.py | 157 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 166 insertions(+), 14 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 6352c07f6ce..5b93a743c2b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -407,7 +407,7 @@ def test_load_zsh_autotitle_warning(cli_args, tmpdir, monkeypatch): assert 'Please set' not in result.output -@pytest.mark.parametrize("cli_cmd", ['shell', ('shell', '--use-pdb')]) +@pytest.mark.parametrize("cli_cmd", ['shell', ('shell', '--pdb')]) @pytest.mark.parametrize( "cli_args,inputs,env,expected_output", [ @@ -516,7 +516,7 @@ def test_shell( assert expected_output.format(**template_ctx) in result.output -@pytest.mark.parametrize("cli_cmd", ['shell', ('shell', '--use-pdb')]) +@pytest.mark.parametrize("cli_cmd", ['shell', ('shell', '--pdb')]) @pytest.mark.parametrize( "cli_args,inputs,env,template_ctx,exception,message", [ diff --git a/tmuxp/cli.py b/tmuxp/cli.py index 29ddd411d46..e5502b252d6 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -671,20 +671,22 @@ def startup(config_dir): 'command', help='Instead of opening shell, execute python code in libtmux and exit', ) -@click.option( - '--use-pdb/--no-pdb', - 'use_pdb', - help='Use pdb / breakpoint() instead of code.interact()', - default=False, -) @click.option( '--use-pythonrc/--no-startup', 'use_pythonrc', help='Load the PYTHONSTARTUP environment variable and ~/.pythonrc.py script.', default=False, ) +@click.option('--pdb', 'shell', flag_value='pdb', help='Use plain pdb') +@click.option( + '--code', 'shell', flag_value='code', help='Use stdlib\'s code.interact()' +) +@click.option('--ptipython', 'shell', flag_value='pdb', help='Use ptpython + ipython') +@click.option('--ptpython', 'shell', flag_value='pdb', help='Use ptpython') +@click.option('--ipython', 'shell', flag_value='pdb', help='Use ipython') +@click.option('--bpython', 'shell', flag_value='pdb', help='Use bpython') def command_shell( - session_name, window_name, socket_name, socket_path, command, use_pdb, use_pythonrc + session_name, window_name, socket_name, socket_path, command, shell, use_pythonrc ): """Launch python shell for tmux server, session, window and pane. @@ -713,7 +715,7 @@ def command_shell( if command is not None: exec(command) else: - if use_pdb or (os.getenv('PYTHONBREAKPOINT') and PY3 and PYMINOR >= 7): + if shell == 'pdb' or (os.getenv('PYTHONBREAKPOINT') and PY3 and PYMINOR >= 7): from ._compat import breakpoint as tmuxp_breakpoint tmuxp_breakpoint() @@ -727,6 +729,7 @@ def command_shell( window=window, pane=pane, use_pythonrc=use_pythonrc, + shell=shell, ) diff --git a/tmuxp/shell.py b/tmuxp/shell.py index c9c0c63f497..545bb2b9491 100644 --- a/tmuxp/shell.py +++ b/tmuxp/shell.py @@ -9,6 +9,7 @@ import logging import os +import traceback logger = logging.getLogger(__name__) @@ -51,12 +52,132 @@ def has_ptipython(): return True -def launch(shell='best', use_pythonrc=False, **kwargs): - import code +def has_bpython(): + try: + from bpython import embed # NOQA F841 + except ImportError: + return False + return True + + +def detect_best_shell(): + if has_ptipython(): + return 'ptipython' + elif has_ptpython(): + return 'ptpython' + elif has_ipython(): + return 'ipython' + elif has_bpython(): + return 'bpython' + return 'code' + + +def get_bpython(options, extra_args): + try: + from bpython import embed # NOQA F841 + except ImportError: + return traceback.format_exc() + + def launch_bpython(): + imported_objects = get_launch_args(**options) + kwargs = {} + if extra_args: + kwargs['args'] = extra_args + embed(imported_objects, **kwargs) + + return launch_bpython + + +def get_ipython_arguments(): + ipython_args = 'IPYTHON_ARGUMENTS' + return os.environ.get(ipython_args, '').split() + + +def get_ipython(options, **extra_args): + try: + from IPython import start_ipython + + def launch_ipython(): + imported_objects = get_launch_args(**options) + ipython_arguments = extra_args or get_ipython_arguments() + start_ipython(argv=ipython_arguments, user_ns=imported_objects) + + return launch_ipython + except ImportError: + str_exc = traceback.format_exc() + # IPython < 0.11 + # Explicitly pass an empty list as arguments, because otherwise + # IPython would use sys.argv from this script. + # Notebook not supported for IPython < 0.11. + try: + from IPython.Shell import IPShell + except ImportError: + return str_exc + "\n" + traceback.format_exc() + + def launch_ipython(): + imported_objects = get_launch_args(**options) + shell = IPShell(argv=[], user_ns=imported_objects) + shell.mainloop() + + return launch_ipython + +def get_ptpython(self, options): + try: + from ptpython.repl import embed, run_config + except ImportError: + tb = traceback.format_exc() + try: # prompt_toolkit < v0.27 + from prompt_toolkit.contrib.repl import embed, run_config + except ImportError: + return tb + + def run_ptpython(): + imported_objects = get_launch_args(**options) + history_filename = os.path.expanduser('~/.ptpython_history') + embed( + globals=imported_objects, + history_filename=history_filename, + vi_mode=options['vi_mode'], + configure=run_config, + ) + + return run_ptpython + + +def get_ptipython(options): + """Based on django-extensions + + Run renamed to launch, get_imported_objects renamed to get_launch_args + """ + try: + from ptpython.ipython import embed + from ptpython.repl import run_config + except ImportError: + tb = traceback.format_exc() + try: # prompt_toolkit < v0.27 + from prompt_toolkit.contrib.ipython import embed + from prompt_toolkit.contrib.repl import run_config + except ImportError: + return tb + + def launch_ptipython(): + imported_objects = get_launch_args(**options) + history_filename = os.path.expanduser('~/.ptpython_history') + embed( + user_ns=imported_objects, + history_filename=history_filename, + vi_mode=options['vi_mode'], + configure=run_config, + ) + + return launch_ptipython + + +def get_launch_args(**kwargs): import libtmux - imported_objects = { + return { 'libtmux': libtmux, 'Server': libtmux.Server, 'Session': libtmux.Session, @@ -68,6 +189,10 @@ def launch(shell='best', use_pythonrc=False, **kwargs): 'pane': kwargs.get('pane'), } + +def get_code(use_pythonrc, imported_objects): + import code + try: # Try activating rlcompleter, because it's handy. import readline @@ -108,4 +233,28 @@ def launch(shell='best', use_pythonrc=False, **kwargs): traceback.print_exc() - code.interact(local=imported_objects) + def launch_code(): + code.interact(local=imported_objects) + + return launch_code + + +def launch(shell='best', use_pythonrc=False, **kwargs): + # Also allowing passing shell='code' to force using code.interact + imported_objects = get_launch_args(**kwargs) + + if shell == 'best': + shell = detect_best_shell() + + if shell == 'ptipython': + launch = get_ptipython(options=kwargs) + elif shell == 'ptpython': + launch = get_ptpython(options=kwargs) + elif shell == 'ipython': + launch = get_ipython(options=kwargs) + elif shell == 'bpython': + launch = get_bpython(options=kwargs) + else: + launch = get_code(use_pythonrc=use_pythonrc, imported_objects=imported_objects) + + launch() From eddf45118afd5a8ba7c52d843540a32ec7cea354 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 07:48:35 -0600 Subject: [PATCH 08/11] Automatically detect the best shell --- tests/test_cli.py | 27 ++++++++++++++++++---- tmuxp/cli.py | 44 +++++++++++++++++++++++++++--------- tmuxp/shell.py | 57 +++++++++++++++++------------------------------ 3 files changed, 77 insertions(+), 51 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5b93a743c2b..2741a30a2f7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -516,7 +516,13 @@ def test_shell( assert expected_output.format(**template_ctx) in result.output -@pytest.mark.parametrize("cli_cmd", ['shell', ('shell', '--pdb')]) +@pytest.mark.parametrize( + "cli_cmd", + [ + 'shell', + ('shell', '--pdb'), + ], +) @pytest.mark.parametrize( "cli_args,inputs,env,template_ctx,exception,message", [ @@ -605,12 +611,23 @@ def test_shell_target_missing( assert message.format(**template_ctx) in result.output +@pytest.mark.parametrize( + "cli_cmd", + [ + # 'shell', + # ('shell', '--pdb'), + ('shell', '--code'), + # ('shell', '--bpython'), + # ('shell', '--ptipython'), + # ('shell', '--ptpython'), + # ('shell', '--ipython'), + ], +) @pytest.mark.parametrize( "cli_args,inputs,env,message", [ ( [ - 'shell', '-L{SOCKET_NAME}', ], [], @@ -619,7 +636,6 @@ def test_shell_target_missing( ), ( [ - 'shell', '-L{SOCKET_NAME}', ], [], @@ -629,6 +645,7 @@ def test_shell_target_missing( ], ) def test_shell_plus( + cli_cmd, cli_args, inputs, env, @@ -652,7 +669,9 @@ def test_shell_plus( SERVER_SOCKET_NAME=server.socket_name, ) - cli_args[:] = [cli_arg.format(**template_ctx) for cli_arg in cli_args] + cli_cmd = list(cli_cmd) if isinstance(cli_cmd, (list, tuple)) else [cli_cmd] + cli_args = cli_cmd + [cli_arg.format(**template_ctx) for cli_arg in cli_args] + for k, v in env.items(): monkeypatch.setenv(k, v.format(**template_ctx)) diff --git a/tmuxp/cli.py b/tmuxp/cli.py index e5502b252d6..6b1ac0cf5df 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -671,22 +671,44 @@ def startup(config_dir): 'command', help='Instead of opening shell, execute python code in libtmux and exit', ) +@click.option( + '--best', + 'shell', + flag_value='best', + help='Use best shell available in site packages', + default=True, +) +@click.option('--pdb', 'shell', flag_value='pdb', help='Use plain pdb') +@click.option( + '--code', 'shell', flag_value='code', help='Use stdlib\'s code.interact()' +) +@click.option( + '--ptipython', 'shell', flag_value='ptipython', help='Use ptpython + ipython' +) +@click.option('--ptpython', 'shell', flag_value='ptpython', help='Use ptpython') +@click.option('--ipython', 'shell', flag_value='ipython', help='Use ipython') +@click.option('--bpython', 'shell', flag_value='bpython', help='Use bpython') @click.option( '--use-pythonrc/--no-startup', 'use_pythonrc', - help='Load the PYTHONSTARTUP environment variable and ~/.pythonrc.py script.', + help='Load PYTHONSTARTUP env var and ~/.pythonrc.py script in --code', default=False, ) -@click.option('--pdb', 'shell', flag_value='pdb', help='Use plain pdb') @click.option( - '--code', 'shell', flag_value='code', help='Use stdlib\'s code.interact()' + '--use-vi-mode/--no-vi-mode', + 'use_vi_mode', + help='Use vi-mode in ptpython/ptipython', + default=False, ) -@click.option('--ptipython', 'shell', flag_value='pdb', help='Use ptpython + ipython') -@click.option('--ptpython', 'shell', flag_value='pdb', help='Use ptpython') -@click.option('--ipython', 'shell', flag_value='pdb', help='Use ipython') -@click.option('--bpython', 'shell', flag_value='pdb', help='Use bpython') def command_shell( - session_name, window_name, socket_name, socket_path, command, shell, use_pythonrc + session_name, + window_name, + socket_name, + socket_path, + command, + shell, + use_pythonrc, + use_vi_mode, ): """Launch python shell for tmux server, session, window and pane. @@ -724,12 +746,14 @@ def command_shell( from .shell import launch launch( + shell=shell, + use_pythonrc=use_pythonrc, # shell: code + use_vi_mode=use_vi_mode, # shell: ptpython, ptipython + # tmux environment / libtmux variables server=server, session=session, window=window, pane=pane, - use_pythonrc=use_pythonrc, - shell=shell, ) diff --git a/tmuxp/shell.py b/tmuxp/shell.py index 545bb2b9491..2920b590fdc 100644 --- a/tmuxp/shell.py +++ b/tmuxp/shell.py @@ -9,7 +9,6 @@ import logging import os -import traceback logger = logging.getLogger(__name__) @@ -72,11 +71,11 @@ def detect_best_shell(): return 'code' -def get_bpython(options, extra_args): - try: - from bpython import embed # NOQA F841 - except ImportError: - return traceback.format_exc() +def get_bpython(options, extra_args=None): + if extra_args is None: + extra_args = {} + + from bpython import embed # NOQA F841 def launch_bpython(): imported_objects = get_launch_args(**options) @@ -104,15 +103,11 @@ def launch_ipython(): return launch_ipython except ImportError: - str_exc = traceback.format_exc() # IPython < 0.11 # Explicitly pass an empty list as arguments, because otherwise # IPython would use sys.argv from this script. # Notebook not supported for IPython < 0.11. - try: - from IPython.Shell import IPShell - except ImportError: - return str_exc + "\n" + traceback.format_exc() + from IPython.Shell import IPShell def launch_ipython(): imported_objects = get_launch_args(**options) @@ -122,30 +117,26 @@ def launch_ipython(): return launch_ipython -def get_ptpython(self, options): +def get_ptpython(options, vi_mode=False): try: from ptpython.repl import embed, run_config except ImportError: - tb = traceback.format_exc() - try: # prompt_toolkit < v0.27 - from prompt_toolkit.contrib.repl import embed, run_config - except ImportError: - return tb + from prompt_toolkit.contrib.repl import embed, run_config - def run_ptpython(): + def launch_ptpython(): imported_objects = get_launch_args(**options) history_filename = os.path.expanduser('~/.ptpython_history') embed( globals=imported_objects, history_filename=history_filename, - vi_mode=options['vi_mode'], + vi_mode=vi_mode, configure=run_config, ) - return run_ptpython + return launch_ptpython -def get_ptipython(options): +def get_ptipython(options, vi_mode=False): """Based on django-extensions Run renamed to launch, get_imported_objects renamed to get_launch_args @@ -154,12 +145,9 @@ def get_ptipython(options): from ptpython.ipython import embed from ptpython.repl import run_config except ImportError: - tb = traceback.format_exc() - try: # prompt_toolkit < v0.27 - from prompt_toolkit.contrib.ipython import embed - from prompt_toolkit.contrib.repl import run_config - except ImportError: - return tb + # prompt_toolkit < v0.27 + from prompt_toolkit.contrib.ipython import embed + from prompt_toolkit.contrib.repl import run_config def launch_ptipython(): imported_objects = get_launch_args(**options) @@ -167,7 +155,7 @@ def launch_ptipython(): embed( user_ns=imported_objects, history_filename=history_filename, - vi_mode=options['vi_mode'], + vi_mode=vi_mode, configure=run_config, ) @@ -226,12 +214,7 @@ def get_code(use_pythonrc, imported_objects): pythonrc_code = handle.read() # Match the behavior of the cpython shell where an error in # PYTHONSTARTUP prints an exception and continues. - try: - exec(compile(pythonrc_code, pythonrc, 'exec'), imported_objects) - except Exception: - import traceback - - traceback.print_exc() + exec(compile(pythonrc_code, pythonrc, 'exec'), imported_objects) def launch_code(): code.interact(local=imported_objects) @@ -239,7 +222,7 @@ def launch_code(): return launch_code -def launch(shell='best', use_pythonrc=False, **kwargs): +def launch(shell='best', use_pythonrc=False, use_vi_mode=False, **kwargs): # Also allowing passing shell='code' to force using code.interact imported_objects = get_launch_args(**kwargs) @@ -247,9 +230,9 @@ def launch(shell='best', use_pythonrc=False, **kwargs): shell = detect_best_shell() if shell == 'ptipython': - launch = get_ptipython(options=kwargs) + launch = get_ptipython(options=kwargs, vi_mode=use_vi_mode) elif shell == 'ptpython': - launch = get_ptpython(options=kwargs) + launch = get_ptpython(options=kwargs, vi_mode=use_vi_mode) elif shell == 'ipython': launch = get_ipython(options=kwargs) elif shell == 'bpython': From 4f96c8f8fd403e29cc25505c5a2ff5c9004cf890 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 08:51:05 -0600 Subject: [PATCH 09/11] Test detect_best_shell, shell detection --- tests/test_shell.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/test_shell.py diff --git a/tests/test_shell.py b/tests/test_shell.py new file mode 100644 index 00000000000..9c700b50396 --- /dev/null +++ b/tests/test_shell.py @@ -0,0 +1,14 @@ +from tmuxp import shell +from tmuxp._compat import string_types + + +def test_detect_best_shell(): + result = shell.detect_best_shell() + assert isinstance(result, string_types) + + +def test_shell_detect(): + assert isinstance(shell.has_bpython(), bool) + assert isinstance(shell.has_ipython(), bool) + assert isinstance(shell.has_ptpython(), bool) + assert isinstance(shell.has_ptipython(), bool) From e4c07da33ee4cc9607cd3e03180b035364bff598 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 09:18:36 -0600 Subject: [PATCH 10/11] docs(CHANGES): Update for #641 --- CHANGES | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGES b/CHANGES index 6ffa7e38e14..fbf320e294d 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,28 @@ Here you can find the recent changes to tmuxp current ------- - *Insert changes/features/fixes for next release here* +- :issue:`641` Improvements to ``shell`` + + Thanks `django-extensions`_ (licensed MIT) for the shell detection abstraction. + + - Deprecate ``shell_plus`` + - ``tmuxp shell`` now detects the best shell available by default + - Python 3.7+ with ``PYTHONBREAKPOINT`` set in env will drop into ``pdb`` by + default + - Drop into ``code.interact`` by default instead of ``pdb`` if no third + party shells found + - New options, override: + + - ``--pdb``: Use plain old ``breakpoint()`` (python 3.7+) or + ``pdb.set_trace`` + - ``--code``: Drop into ``code.interact``, accepts ``--use-pythonrc`` + - ``--bpython``: Drop into bpython + - ``--ipython``: Drop into ipython + - ``--ptpython``: Drop into ptpython, accepts ``--use-vi-mode`` + - ``--ptipython``: Drop into ipython + ptpython, accepts + ``--use-vi-mode`` + +.. _django-extensions: https://github.com/django-extensions/django-extensions tmuxp 1.6.0 (2020-11-06) ------------------------ From 822675978af1e6a9c33d422ba3c325d4c47d3f15 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 7 Nov 2020 09:18:47 -0600 Subject: [PATCH 11/11] docs(cli): Update for new changes to rich shells --- docs/cli.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/cli.rst b/docs/cli.rst index 7a0dd8a9e10..59cb2cee60b 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -101,6 +101,20 @@ this via ``tmuxp -c``: .. _ipdb: https://pypi.org/project/ipdb/ .. _libtmux: https://libtmux.git-pull.com +Shell detection +~~~~~~~~~~~~~~~ + +``tmuxp shell`` detects the richest shell available in your *site packages*, you can also pick your shell via args: + +- ``--pdb``: Use plain old ``breakpoint()`` (python 3.7+) or + ``pdb.set_trace`` +- ``--code``: Drop into ``code.interact``, accepts ``--use-pythonrc`` +- ``--bpython``: Drop into bpython +- ``--ipython``: Drop into ipython +- ``--ptpython``: Drop into ptpython, accepts ``--use-vi-mode`` +- ``--ptipython``: Drop into ipython + ptpython, accepts + ``--use-vi-mode`` + .. _cli_freeze: Freeze sessions