From 980b07668590f25206edd5fe0cb2d4696b56137e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 22 May 2016 05:23:57 -0500 Subject: [PATCH 1/3] Decouple and refactor back-end API to libtmux --- libtmux/__init__.py | 4 + libtmux/_compat.py | 91 ++++++++ libtmux/common.py | 388 +++++++++++++++++++++++++++++++++ libtmux/exc.py | 14 ++ {tmuxp => libtmux}/formats.py | 0 {tmuxp => libtmux}/pane.py | 7 +- {tmuxp => libtmux}/server.py | 17 +- {tmuxp => libtmux}/session.py | 26 +-- {tmuxp => libtmux}/window.py | 17 +- setup.py | 2 +- tests/conftest.py | 13 +- tests/test_server.py | 2 +- tests/test_session.py | 2 +- tests/test_tmuxobject.py | 3 +- tests/test_util.py | 9 +- tests/test_window.py | 2 +- tests/test_workspacebuilder.py | 3 +- tmuxp/__init__.py | 7 +- tmuxp/cli.py | 3 +- tmuxp/common.py | 103 --------- tmuxp/util.py | 274 ----------------------- tmuxp/workspacebuilder.py | 9 +- 22 files changed, 563 insertions(+), 433 deletions(-) create mode 100644 libtmux/__init__.py create mode 100644 libtmux/_compat.py create mode 100644 libtmux/common.py create mode 100644 libtmux/exc.py rename {tmuxp => libtmux}/formats.py (100%) rename {tmuxp => libtmux}/pane.py (96%) rename {tmuxp => libtmux}/server.py (96%) rename {tmuxp => libtmux}/session.py (94%) rename {tmuxp => libtmux}/window.py (96%) delete mode 100644 tmuxp/common.py diff --git a/libtmux/__init__.py b/libtmux/__init__.py new file mode 100644 index 0000000000..17636a7d30 --- /dev/null +++ b/libtmux/__init__.py @@ -0,0 +1,4 @@ +from .pane import Pane +from .server import Server +from .session import Session +from .window import Window diff --git a/libtmux/_compat.py b/libtmux/_compat.py new file mode 100644 index 0000000000..ca238300d3 --- /dev/null +++ b/libtmux/_compat.py @@ -0,0 +1,91 @@ +# -*- coding: utf8 -*- +import sys + +PY2 = sys.version_info[0] == 2 + +_identity = lambda x: x + + +if PY2: + unichr = unichr + text_type = unicode + string_types = (str, unicode) + integer_types = (int, long) + from urllib import urlretrieve + + text_to_native = lambda s, enc: s.encode(enc) + + iterkeys = lambda d: d.iterkeys() + itervalues = lambda d: d.itervalues() + iteritems = lambda d: d.iteritems() + + from cStringIO import StringIO as BytesIO + from StringIO import StringIO + import cPickle as pickle + import ConfigParser as configparser + + from itertools import izip, imap + range_type = xrange + + cmp = cmp + + input = raw_input + from string import lower as ascii_lowercase + import urlparse + + exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') + + def implements_to_string(cls): + cls.__unicode__ = cls.__str__ + cls.__str__ = lambda x: x.__unicode__().encode('utf-8') + return cls + + def console_to_str(s): + return s.decode('utf_8') + +else: + unichr = chr + text_type = str + string_types = (str,) + integer_types = (int, ) + + text_to_native = lambda s, enc: s + + iterkeys = lambda d: iter(d.keys()) + itervalues = lambda d: iter(d.values()) + iteritems = lambda d: iter(d.items()) + + from io import StringIO, BytesIO + import pickle + import configparser + + izip = zip + imap = map + range_type = range + + cmp = lambda a, b: (a > b) - (a < b) + + input = input + from string import ascii_lowercase + import urllib.parse as urllib + import urllib.parse as urlparse + from urllib.request import urlretrieve + + console_encoding = sys.__stdout__.encoding + + implements_to_string = _identity + + def console_to_str(s): + """ From pypa/pip project, pip.backwardwardcompat. License MIT. """ + try: + return s.decode(console_encoding) + except UnicodeDecodeError: + return s.decode('utf_8') + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise(value.with_traceback(tb)) + raise value + + +number_types = integer_types + (float,) diff --git a/libtmux/common.py b/libtmux/common.py new file mode 100644 index 0000000000..7e3c5bbf54 --- /dev/null +++ b/libtmux/common.py @@ -0,0 +1,388 @@ +# -*- coding: utf-8 -*- + +import collections +import logging +import os +import re +import subprocess +from distutils.version import StrictVersion + +from . import exc +from ._compat import console_to_str + +logger = logging.getLogger(__name__) + + +class EnvironmentMixin(object): + + """Mixin class for managing session and server level environment + variables in tmux. + + """ + + _add_option = None + + def __init__(self, add_option=None): + self._add_option = add_option + + def set_environment(self, name, value): + """Set environment ``$ tmux set-environment ``. + + :param name: the environment variable name. such as 'PATH'. + :type option: string + :param value: environment value. + :type value: string + + """ + + args = ['set-environment'] + if self._add_option: + args += [self._add_option] + + args += [name, value] + + proc = self.cmd(*args) + + if proc.stderr: + if isinstance(proc.stderr, list) and len(proc.stderr) == int(1): + proc.stderr = proc.stderr[0] + raise ValueError('tmux set-environment stderr: %s' % proc.stderr) + + def unset_environment(self, name): + """Unset environment variable ``$ tmux set-environment -u ``. + + :param name: the environment variable name. such as 'PATH'. + :type option: string + """ + + args = ['set-environment'] + if self._add_option: + args += [self._add_option] + args += ['-u', name] + + proc = self.cmd(*args) + + if proc.stderr: + if isinstance(proc.stderr, list) and len(proc.stderr) == int(1): + proc.stderr = proc.stderr[0] + raise ValueError('tmux set-environment stderr: %s' % proc.stderr) + + def remove_environment(self, name): + """Remove environment variable ``$ tmux set-environment -r ``. + + :param name: the environment variable name. such as 'PATH'. + :type option: string + """ + + args = ['set-environment'] + if self._add_option: + args += [self._add_option] + args += ['-r', name] + + proc = self.cmd(*args) + + if proc.stderr: + if isinstance(proc.stderr, list) and len(proc.stderr) == int(1): + proc.stderr = proc.stderr[0] + raise ValueError('tmux set-environment stderr: %s' % proc.stderr) + + def show_environment(self, name=None): + """Show environment ``$tmux show-environment -t [session] ``. + + Return dict of environment variables for the session or the value of a + specific variable if the name is specified. + + :param name: the environment variable name. such as 'PATH'. + :type option: string + """ + tmux_args = ['show-environment'] + if self._add_option: + tmux_args += [self._add_option] + if name: + tmux_args += [name] + vars = self.cmd(*tmux_args).stdout + vars = [tuple(item.split('=', 1)) for item in vars] + vars_dict = {} + for t in vars: + if len(t) == 2: + vars_dict[t[0]] = t[1] + elif len(t) == 1: + vars_dict[t[0]] = True + else: + raise ValueError('unexpected variable %s', t) + + if name: + return vars_dict.get(name) + + return vars_dict + + +class tmux_cmd(object): + + """:term:`tmux(1)` command via :py:mod:`subprocess`. + + Usage:: + + proc = tmux_cmd('new-session', '-s%' % 'my session') + + if proc.stderr: + raise exc.LibTmuxException( + 'Command: %s returned error: %s' % (proc.cmd, proc.stderr) + ) + + print('tmux command returned %s' % proc.stdout) + + Equivalent to: + + .. code-block:: bash + + $ tmux new-session -s my session + + :versionchanged: 0.8 + Renamed from ``tmux`` to ``tmux_cmd``. + + """ + + def __init__(self, *args, **kwargs): + cmd = [which('tmux')] + cmd += args # add the command arguments to cmd + cmd = [str(c) for c in cmd] + + self.cmd = cmd + + try: + self.process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.process.wait() + stdout = self.process.stdout.read() + self.process.stdout.close() + stderr = self.process.stderr.read() + self.process.stderr.close() + except Exception as e: + logger.error( + 'Exception for %s: \n%s' % ( + subprocess.list2cmdline(cmd), + e + ) + ) + + self.stdout = console_to_str(stdout) + self.stdout = self.stdout.split('\n') + self.stdout = list(filter(None, self.stdout)) # filter empty values + + self.stderr = console_to_str(stderr) + self.stderr = self.stderr.split('\n') + self.stderr = list(filter(None, self.stderr)) # filter empty values + + if 'has-session' in cmd and len(self.stderr): + if not self.stdout: + self.stdout = self.stderr[0] + + logging.debug('self.stdout for %s: \n%s' % + (' '.join(cmd), self.stdout)) + + +class TmuxMappingObject(collections.MutableMapping): + + """Base: :py:class:`collections.MutableMapping`. + + Convenience container. Base class for :class:`Pane`, :class:`Window`, + :class:`Session` and :class:`Server`. + + Instance attributes for useful information :term:`tmux(1)` uses for + Session, Window, Pane, stored :attr:`self._TMUX`. For example, a + :class:`Window` will have a ``window_id`` and ``window_name``. + + """ + + def __getitem__(self, key): + return self._TMUX[key] + + def __setitem__(self, key, value): + self._TMUX[key] = value + self.dirty = True + + def __delitem__(self, key): + del self._TMUX[key] + self.dirty = True + + def keys(self): + """Return list of keys.""" + return self._TMUX.keys() + + def __iter__(self): + return self._TMUX.__iter__() + + def __len__(self): + return len(self._TMUX.keys()) + + +class TmuxRelationalObject(object): + + """Base Class for managing tmux object child entities. .. # NOQA + + Manages collection of child objects (a :class:`Server` has a collection of + :class:`Session` objects, a :class:`Session` has collection of + :class:`Window`) + + Children of :class:`TmuxRelationalObject` are going to have a + ``self.children``, ``self.childIdAttribute`` and ``self.list_children``. + + ================ ================== ===================== ============================ + Object ``.children`` ``.childIdAttribute`` method + ================ ================== ===================== ============================ + :class:`Server` ``self._sessions`` 'session_id' :meth:`Server.list_sessions` + :class:`Session` ``self._windows`` 'window_id' :meth:`Session.list_windows` + :class:`Window` ``self._panes`` 'pane_id' :meth:`Window.list_panes` + :class:`Pane` + ================ ================== ===================== ============================ + + """ + + def findWhere(self, attrs): + """Return object on first match. + + Based on `.findWhere()`_ from `underscore.js`_. + + .. _.findWhere(): http://underscorejs.org/#findWhere + .. _underscore.js: http://underscorejs.org/ + + """ + try: + return self.where(attrs)[0] + except IndexError: + return None + + def where(self, attrs, first=False): + """Return objects matching child objects properties. + + Based on `.where()`_ from `underscore.js`_. + + .. _.where(): http://underscorejs.org/#where + .. _underscore.js: http://underscorejs.org/ + + :param attrs: tmux properties to match + :type attrs: dict + :rtype: list + + """ + + # from https://github.com/serkanyersen/underscore.py + def by(val, *args): + for key, value in attrs.items(): + try: + if attrs[key] != val[key]: + return False + except KeyError: + return False + return True + + if first: + return list(filter(by, self.children))[0] + else: + return list(filter(by, self.children)) + + def getById(self, id): + """Return object based on ``childIdAttribute``. + + Based on `.get()`_ from `backbone.js`_. + + .. _backbone.js: http://backbonejs.org/ + .. _.get(): http://backbonejs.org/#Collection-get + + :param id: + :type id: string + :rtype: object + + """ + for child in self.children: + if child[self.childIdAttribute] == id: + return child + else: + continue + + return None + + +def which(exe=None): + """Return path of bin. Python clone of /usr/bin/which. + + from salt.util - https://www.github.com/saltstack/salt - license apache + + :param exe: Application to search PATHs for. + :type exe: string + :rtype: string + + """ + if exe: + if os.access(exe, os.X_OK): + return exe + + # default path based on busybox's default + search_path = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin' + + for path in search_path.split(os.pathsep): + full_path = os.path.join(path, exe) + if os.access(full_path, os.X_OK): + return full_path + raise exc.LibTmuxException( + '{0!r} could not be found in the following search ' + 'path: {1!r}'.format( + exe, search_path + ) + ) + logger.error('No executable was passed to be searched by which') + return None + + +def is_version(version): + """Return True if tmux version installed. + + :param version: version, '1.8' + :param type: string + :rtype: bool + + """ + proc = tmux_cmd('-V') + + if proc.stderr: + raise exc.LibTmuxException(proc.stderr) + + installed_version = proc.stdout[0].split('tmux ')[1] + + return StrictVersion(installed_version) == StrictVersion(version) + + +def has_required_tmux_version(version=None): + """Return if tmux meets version requirement. Version >1.8 or above. + + :versionchanged: 0.1.7 + Versions will now remove trailing letters per `Issue 55`_. + + .. _Issue 55: https://github.com/tony/tmuxp/issues/55. + + """ + + if not version: + proc = tmux_cmd('-V') + + if proc.stderr: + if proc.stderr[0] == 'tmux: unknown option -- V': + raise exc.LibTmuxException( + 'tmuxp supports tmux 1.8 and greater. This system' + ' is running tmux 1.3 or earlier.') + raise exc.LibTmuxException(proc.stderr) + + version = proc.stdout[0].split('tmux ')[1] + + version = re.sub(r'[a-z]', '', version) + + if StrictVersion(version) <= StrictVersion("1.7"): + raise exc.LibTmuxException( + 'tmuxp only supports tmux 1.8 and greater. This system' + ' has %s installed. Upgrade your tmux to use tmuxp.' % version + ) + return version diff --git a/libtmux/exc.py b/libtmux/exc.py new file mode 100644 index 0000000000..ee2d0f73b9 --- /dev/null +++ b/libtmux/exc.py @@ -0,0 +1,14 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals, with_statement) + + +class LibTmuxException(Exception): + + """Base Exception for Tmuxp Errors.""" + + +class TmuxSessionExists(LibTmuxException): + + """Session does not exist in the server.""" + + pass diff --git a/tmuxp/formats.py b/libtmux/formats.py similarity index 100% rename from tmuxp/formats.py rename to libtmux/formats.py diff --git a/tmuxp/pane.py b/libtmux/pane.py similarity index 96% rename from tmuxp/pane.py rename to libtmux/pane.py index 99d002e777..25688fe18f 100644 --- a/tmuxp/pane.py +++ b/libtmux/pane.py @@ -10,12 +10,13 @@ import logging -from . import exc, util +from . import exc +from .common import TmuxMappingObject, TmuxRelationalObject logger = logging.getLogger(__name__) -class Pane(util.TmuxMappingObject, util.TmuxRelationalObject): +class Pane(TmuxMappingObject, TmuxRelationalObject): """:term:`tmux(1)` :term:`pane`. @@ -151,7 +152,7 @@ def resize_pane(self, *args, **kwargs): proc = self.cmd('resize-pane', args[0]) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) self.server._update_panes() return self diff --git a/tmuxp/server.py b/libtmux/server.py similarity index 96% rename from tmuxp/server.py rename to libtmux/server.py index d46414965e..8c9f02e743 100644 --- a/tmuxp/server.py +++ b/libtmux/server.py @@ -12,9 +12,8 @@ import os from . import exc, formats -from .common import EnvironmentMixin +from .common import EnvironmentMixin, TmuxRelationalObject, tmux_cmd from .session import Session -from .util import TmuxRelationalObject, tmux_cmd logger = logging.getLogger(__name__) @@ -123,7 +122,7 @@ def _list_sessions(self): ) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) sformats = formats.SESSION_FORMATS tmux_formats = ['#{%s}' % format for format in sformats] @@ -186,7 +185,7 @@ def _list_windows(self): ) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) windows = proc.stdout @@ -248,7 +247,7 @@ def _list_panes(self): ) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) panes = proc.stdout @@ -350,7 +349,7 @@ def kill_session(self, target_session=None): proc = self.cmd('kill-session', '-t%s' % target_session) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) return self @@ -364,7 +363,7 @@ def switch_client(self, target_session): proc = self.cmd('switch-client', '-t%s' % target_session) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) def attach_session(self, target_session=None): """``$ tmux attach-session`` aka alias: ``$ tmux attach``. @@ -379,7 +378,7 @@ def attach_session(self, target_session=None): proc = self.cmd('attach-session', *tmux_args) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) def new_session(self, session_name=None, @@ -447,7 +446,7 @@ def new_session(self, ) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) session = proc.stdout[0] diff --git a/tmuxp/session.py b/libtmux/session.py similarity index 94% rename from tmuxp/session.py rename to libtmux/session.py index 6a67f6954e..0f32a069c4 100644 --- a/tmuxp/session.py +++ b/libtmux/session.py @@ -11,16 +11,16 @@ import logging import os -from . import exc, formats, util -from .common import EnvironmentMixin +from . import exc, formats +from .common import EnvironmentMixin, TmuxMappingObject, TmuxRelationalObject from .window import Window logger = logging.getLogger(__name__) class Session( - util.TmuxMappingObject, - util.TmuxRelationalObject, + TmuxMappingObject, + TmuxRelationalObject, EnvironmentMixin ): """:term:`tmux(1)` session. @@ -84,7 +84,7 @@ def attach_session(self, target_session=None): proc = self.cmd('attach-session', '-t%s' % self.get('session_id')) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) def kill_session(self): """``$ tmux kill-session``.""" @@ -92,7 +92,7 @@ def kill_session(self): proc = self.cmd('kill-session', '-t%s' % self.get('session_id')) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) def switch_client(self, target_session=None): """``$ tmux switch-client``. @@ -102,7 +102,7 @@ def switch_client(self, target_session=None): proc = self.cmd('switch-client', '-t%s' % self.get('session_id')) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) def rename_session(self, new_name): """Rename session and return new :class:`Session` object. @@ -119,7 +119,7 @@ def rename_session(self, new_name): ) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) return self @@ -197,7 +197,7 @@ def new_window(self, proc = self.cmd('new-window', *window_args) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) window = proc.stdout[0] @@ -231,7 +231,7 @@ def kill_window(self, target_window=None): proc = self.cmd('kill-window', target) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) self.server._update_windows() @@ -288,11 +288,11 @@ def attached_window(self): if len(active_windows) == int(1): return active_windows[0] else: - raise exc.TmuxpException( + raise exc.LibTmuxException( 'multiple active windows found. %s' % active_windows) if len(self._windows) == int(0): - raise exc.TmuxpException('No Windows') + raise exc.LibTmuxException('No Windows') def select_window(self, target_window): """Return :class:`Window` selected via ``$ tmux select-window``. @@ -311,7 +311,7 @@ def select_window(self, target_window): proc = self.cmd('select-window', target) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) return self.attached_window() diff --git a/tmuxp/window.py b/libtmux/window.py similarity index 96% rename from tmuxp/window.py rename to libtmux/window.py index ef64335927..0187981293 100644 --- a/tmuxp/window.py +++ b/libtmux/window.py @@ -11,13 +11,14 @@ import logging import os -from . import exc, formats, util +from . import exc, formats from .pane import Pane +from .common import TmuxMappingObject, TmuxRelationalObject logger = logging.getLogger(__name__) -class Window(util.TmuxMappingObject, util.TmuxRelationalObject): +class Window(TmuxMappingObject, TmuxRelationalObject): """:term:`tmux(1)` window.""" childIdAttribute = 'pane_id' @@ -120,7 +121,7 @@ def select_layout(self, layout=None): ) if proc.stderr: - raise exc.TmuxpException(proc.stderr) + raise exc.LibTmuxException(proc.stderr) def set_window_option(self, option, value): """Wrapper for ``$ tmux set-window-option