From 6655ced441ebc7a8ebad3649e5962a238bea20c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Linse?= Date: Sun, 6 Mar 2016 11:44:35 +0100 Subject: [PATCH] remove SessionHook: let the nvim object act as session wrapper --- neovim/__init__.py | 5 +- neovim/api/__init__.py | 4 +- neovim/api/common.py | 109 +--------------------------- neovim/api/nvim.py | 153 ++++++++++++++++++++++++--------------- neovim/plugin/host.py | 10 +-- test/test_client_rpc.py | 16 ++-- test/test_common.py | 2 +- test/test_concurrency.py | 8 +- test/test_events.py | 12 +-- 9 files changed, 126 insertions(+), 193 deletions(-) diff --git a/neovim/__init__.py b/neovim/__init__.py index 6fea7134..3fab764f 100644 --- a/neovim/__init__.py +++ b/neovim/__init__.py @@ -6,7 +6,7 @@ import os import sys -from .api import DecodeHook, Nvim, SessionHook +from .api import DecodeHook, Nvim from .msgpack_rpc import (ErrorResponse, child_session, socket_session, stdio_session, tcp_session) from .plugin import (Host, autocmd, command, encoding, function, plugin, @@ -16,8 +16,7 @@ __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session', 'start_host', 'autocmd', 'command', 'encoding', 'function', 'plugin', 'rpc_export', 'Host', 'DecodeHook', 'Nvim', - 'SessionHook', 'shutdown_hook', 'attach', 'setup_logging', - 'ErrorResponse') + 'shutdown_hook', 'attach', 'setup_logging', 'ErrorResponse') def start_host(session=None): diff --git a/neovim/api/__init__.py b/neovim/api/__init__.py index 10f2147f..144c4518 100644 --- a/neovim/api/__init__.py +++ b/neovim/api/__init__.py @@ -5,11 +5,11 @@ """ from .buffer import Buffer -from .common import DecodeHook, SessionHook +from .common import DecodeHook from .nvim import Nvim, NvimError from .tabpage import Tabpage from .window import Window -__all__ = ('Nvim', 'Buffer', 'Window', 'Tabpage', 'NvimError', 'SessionHook', +__all__ = ('Nvim', 'Buffer', 'Window', 'Tabpage', 'NvimError', 'DecodeHook') diff --git a/neovim/api/common.py b/neovim/api/common.py index 3a456e3a..fe520085 100644 --- a/neovim/api/common.py +++ b/neovim/api/common.py @@ -120,48 +120,7 @@ def _identity(obj, session, method, kind): return obj -class SessionHook(object): - - """Pair of functions to filter objects coming/going from/to Nvim. - - Filter functions receive the following arguments: - - - obj: The object to process - - session: The current session object - - method: The method name - - kind: Kind of filter, can be one of: - - 'request' for requests coming from Nvim - - 'notification' for notifications coming from Nvim - - 'out-request' for requests going to Nvim - - Whatever is returned from the function is used as a replacement for `obj`. - - This class also provides a `compose` method for composing hooks. - """ - - def __init__(self, from_nvim=_identity, to_nvim=_identity): - """Initialize a SessionHook with from/to filters.""" - self.from_nvim = from_nvim - self.to_nvim = to_nvim - - def compose(self, other): - """Compose two SessionHook instances. - - This works by composing the individual from/to filters and creating - a new SessionHook instance with the composed filters. - """ - def comp(f1, f2): - if f1 is _identity: - return f2 - if f2 is _identity: - return f1 - return lambda o, s, m, k: f1(f2(o, s, m, k), s, m, k) - - return SessionHook(comp(other.from_nvim, self.from_nvim), - comp(other.to_nvim, self.to_nvim)) - - -class DecodeHook(SessionHook): +class DecodeHook(object): """SessionHook subclass that decodes utf-8 strings coming from Nvim. @@ -173,9 +132,9 @@ def __init__(self, encoding='utf-8', encoding_errors='strict'): """Initialize with encoding and encoding errors policy.""" self.encoding = encoding self.encoding_errors = encoding_errors - super(DecodeHook, self).__init__(from_nvim=self._decode_if_bytes) - def _decode_if_bytes(self, obj, session, method, kind): + def decode_if_bytes(self, obj): + """Decode obj if it is bytes.""" if isinstance(obj, bytes): return obj.decode(self.encoding, errors=self.encoding_errors) return obj @@ -185,67 +144,7 @@ def walk(self, obj): Uses encoding and policy specified in constructor. """ - return walk(self._decode_if_bytes, obj, None, None, None) - - -class SessionFilter(object): - - """Wraps a session-like object with a SessionHook instance. - - This class can be used as a drop-in replacement for a sessions, the - difference is that a hook is applied to all data passing through a - SessionFilter instance. - """ - - def __init__(self, session, hook): - """Initialize with a Session(or SessionFilter) and a hook. - - If `session` is already a SessionFilter, it's hook will be extracted - and composed with `hook`. - """ - if isinstance(session, SessionFilter): - self._hook = session._hook.compose(hook) - self._session = session._session - else: - self._hook = hook - self._session = session - # Both filters are applied to `walk` so objects are transformed - # recursively - self._in = self._hook.from_nvim - self._out = self._hook.to_nvim - - def threadsafe_call(self, fn, *args, **kwargs): - """Wrapper for Session.threadsafe_call.""" - self._session.threadsafe_call(fn, *args, **kwargs) - - def next_message(self): - """Wrapper for Session.next_message.""" - msg = self._session.next_message() - if msg: - return walk(self._in, msg, self, msg[1], msg[0]) - - def request(self, name, *args, **kwargs): - """Wrapper for Session.request.""" - args = walk(self._out, args, self, name, 'out-request') - return walk(self._in, self._session.request(name, *args, **kwargs), - self, name, 'out-request') - - def run(self, request_cb, notification_cb, setup_cb=None): - """Wrapper for Session.run.""" - def filter_request_cb(name, args): - result = request_cb(self._in(name, self, name, 'request'), - walk(self._in, args, self, name, 'request')) - return walk(self._out, result, self, name, 'request') - - def filter_notification_cb(name, args): - notification_cb(self._in(name, self, name, 'notification'), - walk(self._in, args, self, name, 'notification')) - - self._session.run(filter_request_cb, filter_notification_cb, setup_cb) - - def stop(self): - """Wrapper for Session.stop.""" - self._session.stop() + return walk(self.decode_if_bytes, obj) def walk(fn, obj, *args): diff --git a/neovim/api/nvim.py b/neovim/api/nvim.py index 9650f315..dc7ce1ac 100644 --- a/neovim/api/nvim.py +++ b/neovim/api/nvim.py @@ -7,8 +7,7 @@ from msgpack import ExtType from .buffer import Buffer -from .common import (DecodeHook, Remote, RemoteMap, RemoteSequence, - SessionFilter, SessionHook, walk) +from .common import (DecodeHook, Remote, RemoteMap, RemoteSequence, walk) from .tabpage import Tabpage from .window import Window from ..compat import IS_PYTHON3 @@ -24,7 +23,7 @@ class Nvim(object): """Class that represents a remote Nvim instance. - This class is main entry point to Nvim remote API, it is a thin wrapper + This class is main entry point to Nvim remote API, it is a wrapper around Session instances. The constructor of this class must not be called directly. Instead, the @@ -49,9 +48,8 @@ def from_session(cls, session): channel_id, metadata = session.request(b'vim_get_api_info') if IS_PYTHON3: - hook = DecodeHook() # decode all metadata strings for python3 - metadata = walk(hook.from_nvim, metadata, None, None, None) + metadata = DecodeHook().walk(metadata) types = { metadata['types']['Buffer']['id']: Buffer, @@ -59,32 +57,87 @@ def from_session(cls, session): metadata['types']['Tabpage']['id']: Tabpage, } - return cls(session, channel_id, metadata).with_hook(ExtHook(types)) + return cls(session, channel_id, metadata, types) - def __init__(self, session, channel_id, metadata): + def __init__(self, session, channel_id, metadata, types, decodehook=None): """Initialize a new Nvim instance. This method is module-private.""" self._session = session self.channel_id = channel_id self.metadata = metadata - self.vars = RemoteMap(session, 'vim_get_var', 'vim_set_var') - self.vvars = RemoteMap(session, 'vim_get_vvar', None) - self.options = RemoteMap(session, 'vim_get_option', 'vim_set_option') - self.buffers = RemoteSequence(session, 'vim_get_buffers') - self.windows = RemoteSequence(session, 'vim_get_windows') - self.tabpages = RemoteSequence(session, 'vim_get_tabpages') - self.current = Current(session) + self.types = types + self.vars = RemoteMap(self, 'vim_get_var', 'vim_set_var') + self.vvars = RemoteMap(self, 'vim_get_vvar', None) + self.options = RemoteMap(self, 'vim_get_option', 'vim_set_option') + self.buffers = RemoteSequence(self, 'vim_get_buffers') + self.windows = RemoteSequence(self, 'vim_get_windows') + self.tabpages = RemoteSequence(self, 'vim_get_tabpages') + self.current = Current(self) self.funcs = Funcs(self) self.error = NvimError + self._decodehook = decodehook - def with_hook(self, hook): - """Initialize a new Nvim instance.""" - return Nvim(SessionFilter(self.session, hook), self.channel_id, - self.metadata) + def _from_nvim(self, obj): + if type(obj) is ExtType: + cls = self.types[obj.code] + return cls(self, (obj.code, obj.data)) + if self._decodehook is not None: + obj = self._decodehook.decode_if_bytes(obj) + return obj - @property - def session(self): - """Return the Session or SessionFilter for a Nvim instance.""" - return self._session + def _to_nvim(self, obj): + if isinstance(obj, Remote): + return ExtType(*obj.code_data) + return obj + + def request(self, name, *args, **kwargs): + """Send an API request or notification to nvim. + + It is rarely needed for a plugin to call this function directly, as high + As most API functions have python wrapper functions. + + Normally a blocking request will be sent. + If the `async` flag is present and True, a asynchronous notification is + sent instead. This will never block, and the return value or error is + ignored. + """ + args = walk(self._to_nvim, args) + res = self._session.request(name, *args, **kwargs) + return walk(self._from_nvim, res) + + def next_message(self): + """Block until a message(request or notification) is available. + + If any messages were previously enqueued, return the first in queue. + If not, run the event loop until one is received. + """ + msg = self._session.next_message() + if msg: + return walk(self._from_nvim, msg) + + def run_loop(self, request_cb, notification_cb, setup_cb=None): + """Run the event loop to receive requests and notifications from Nvim. + + This should not be called from a plugin running in the host, which + already runs the loop and dispatches events to plugins. + """ + def filter_request_cb(name, args): + args = walk(self._from_nvim, args) + result = request_cb(self._from_nvim(name), args) + return walk(self._to_nvim, result) + + def filter_notification_cb(name, args): + notification_cb(self._from_nvim(name), walk(self._from_nvim, args)) + + self._session.run(filter_request_cb, filter_notification_cb, setup_cb) + + def stop_loop(self): + """Stop the event loop being started with `run_loop`.""" + self._session.stop() + + def with_decodehook(self, hook): + """Initialize a new Nvim instance.""" + return Nvim(self._session, self.channel_id, + self.metadata, self.types, hook) def ui_attach(self, width, height, rgb): """Register as a remote UI. @@ -92,38 +145,38 @@ def ui_attach(self, width, height, rgb): After this method is called, the client will receive redraw notifications. """ - return self._session.request('ui_attach', width, height, rgb) + return self.request('ui_attach', width, height, rgb) def ui_detach(self): """Unregister as a remote UI.""" - return self._session.request('ui_detach') + return self.request('ui_detach') def ui_try_resize(self, width, height): """Notify nvim that the client window has resized. If possible, nvim will send a redraw request to resize. """ - return self._session.request('ui_try_resize', width, height) + return self.request('ui_try_resize', width, height) def subscribe(self, event): """Subscribe to a Nvim event.""" - return self._session.request('vim_subscribe', event) + return self.request('vim_subscribe', event) def unsubscribe(self, event): """Unsubscribe to a Nvim event.""" - return self._session.request('vim_unsubscribe', event) + return self.request('vim_unsubscribe', event) def command(self, string, async=False): """Execute a single ex command.""" - return self._session.request('vim_command', string, async=async) + return self.request('vim_command', string, async=async) def command_output(self, string): """Execute a single ex command and return the output.""" - return self._session.request('vim_command_output', string) + return self.request('vim_command_output', string) def eval(self, string, async=False): """Evaluate a vimscript expression.""" - return self._session.request('vim_eval', string, async=async) + return self.request('vim_eval', string, async=async) def call(self, name, *args, **kwargs): """Call a vimscript function.""" @@ -131,18 +184,18 @@ def call(self, name, *args, **kwargs): if k != "async": raise TypeError( "call() got an unexpected keyword argument '{}'".format(k)) - return self._session.request('vim_call_function', name, args, **kwargs) + return self.request('vim_call_function', name, args, **kwargs) def strwidth(self, string): """Return the number of display cells `string` occupies. Tab is counted as one cell. """ - return self._session.request('vim_strwidth', string) + return self.request('vim_strwidth', string) def list_runtime_paths(self): """Return a list of paths contained in the 'runtimepath' option.""" - return self._session.request('vim_list_runtime_paths') + return self.request('vim_list_runtime_paths') def foreach_rtp(self, cb): """Invoke `cb` for each path in 'runtimepath'. @@ -152,7 +205,7 @@ def foreach_rtp(self, cb): are no longer paths. If stopped in case callable returned non-None, vim.foreach_rtp function returns the value returned by callable. """ - for path in self._session.request('vim_list_runtime_paths'): + for path in self.request('vim_list_runtime_paths'): try: if cb(path) is not None: break @@ -162,7 +215,7 @@ def foreach_rtp(self, cb): def chdir(self, dir_path): """Run os.chdir, then all appropriate vim stuff.""" os_chdir(dir_path) - return self._session.request('vim_change_directory', dir_path) + return self.request('vim_change_directory', dir_path) def feedkeys(self, keys, options='', escape_csi=True): """Push `keys` to Nvim user input buffer. @@ -173,7 +226,7 @@ def feedkeys(self, keys, options='', escape_csi=True): - 't': Handle keys as if typed; otherwise they are handled as if coming from a mapping. This matters for undo, opening folds, etc. """ - return self._session.request('vim_feedkeys', keys, options, escape_csi) + return self.request('vim_feedkeys', keys, options, escape_csi) def input(self, bytes): """Push `bytes` to Nvim low level input buffer. @@ -183,7 +236,7 @@ def input(self, bytes): written(which can be less than what was requested if the buffer is full). """ - return self._session.request('vim_input', bytes) + return self.request('vim_input', bytes) def replace_termcodes(self, string, from_part=False, do_lt=True, special=True): @@ -199,16 +252,16 @@ def replace_termcodes(self, string, from_part=False, do_lt=True, The returned sequences can be used as input to `feedkeys`. """ - return self._session.request('vim_replace_termcodes', string, - from_part, do_lt, special) + return self.request('vim_replace_termcodes', string, + from_part, do_lt, special) def out_write(self, msg): """Print `msg` as a normal message.""" - return self._session.request('vim_out_write', msg) + return self.request('vim_out_write', msg) def err_write(self, msg, async=False): """Print `msg` as an error message.""" - return self._session.request('vim_err_write', msg, async=async) + return self.request('vim_err_write', msg, async=async) def quit(self, quit_command='qa!'): """Send a quit command to Nvim. @@ -304,23 +357,5 @@ def __getattr__(self, name): return functools.partial(self._nvim.call, name) -class ExtHook(SessionHook): - def __init__(self, types): - self.types = types - super(ExtHook, self).__init__(from_nvim=self.from_ext, - to_nvim=self.to_ext) - - def from_ext(self, obj, session, method, kind): - if type(obj) is ExtType: - cls = self.types[obj.code] - return cls(session, (obj.code, obj.data)) - return obj - - def to_ext(self, obj, session, method, kind): - if isinstance(obj, Remote): - return ExtType(*obj.code_data) - return obj - - class NvimError(Exception): pass diff --git a/neovim/plugin/host.py b/neovim/plugin/host.py index b328b961..fc3b74bc 100644 --- a/neovim/plugin/host.py +++ b/neovim/plugin/host.py @@ -47,14 +47,14 @@ def __init__(self, nvim): def start(self, plugins): """Start listening for msgpack-rpc requests and notifications.""" - self.nvim.session.run(self._on_request, - self._on_notification, - lambda: self._load(plugins)) + self.nvim.run_loop(self._on_request, + self._on_notification, + lambda: self._load(plugins)) def shutdown(self): """Shutdown the host.""" self._unload() - self.nvim.session.stop() + self.nvim.stop_loop() def _on_request(self, name, args): """Handle a msgpack-rpc request.""" @@ -215,5 +215,5 @@ def _configure_nvim_for(self, obj): encoding = getattr(obj, '_nvim_encoding', None) hook = self._decodehook_for(encoding) if hook is not None: - nvim = nvim.with_hook(hook) + nvim = nvim.with_decodehook(hook) return nvim diff --git a/test/test_client_rpc.py b/test/test_client_rpc.py index 5bf38ae2..c1607873 100644 --- a/test/test_client_rpc.py +++ b/test/test_client_rpc.py @@ -11,14 +11,14 @@ def setup_cb(): cmd = 'let g:result = rpcrequest(%d, "client-call", 1, 2, 3)' % cid vim.command(cmd) eq(vim.vars['result'], [4, 5, 6]) - vim.session.stop() + vim.stop_loop() def request_cb(name, args): eq(name, 'client-call') eq(args, [1, 2, 3]) return [4, 5, 6] - vim.session.run(request_cb, None, setup_cb) + vim.run_loop(request_cb, None, setup_cb) @with_setup(setup=cleanup) @@ -27,13 +27,13 @@ def setup_cb(): cmd = 'let g:result = rpcrequest(%d, "client-call2", 1, 2, 3)' % cid vim.command(cmd) eq(vim.vars['result'], [7, 8, 9]) - vim.session.stop() + vim.stop_loop() def request_cb(name, args): vim.command('let g:result2 = [7, 8, 9]') return vim.vars['result2'] - vim.session.run(request_cb, None, setup_cb) + vim.run_loop(request_cb, None, setup_cb) @with_setup(setup=cleanup) def test_async_call(): @@ -41,11 +41,11 @@ def test_async_call(): def request_cb(name, args): if name == "test-event": vim.vars['result'] = 17 - vim.session.stop() + vim.stop_loop() # this would have dead-locked if not async vim.funcs.rpcrequest(vim.channel_id, "test-event", async=True) - vim.session.run(request_cb, None, None) + vim.run_loop(request_cb, None, None) eq(vim.vars['result'], 17) @@ -62,7 +62,7 @@ def setup_cb(): eq(vim.vars['result2'], 8) eq(vim.vars['result3'], 16) eq(vim.vars['result4'], 32) - vim.session.stop() + vim.stop_loop() def request_cb(name, args): n = args[0] @@ -77,4 +77,4 @@ def request_cb(name, args): vim.command(cmd) return n - vim.session.run(request_cb, None, setup_cb) + vim.run_loop(request_cb, None, setup_cb) diff --git a/test/test_common.py b/test/test_common.py index d8801048..e4ac246b 100644 --- a/test/test_common.py +++ b/test/test_common.py @@ -16,7 +16,7 @@ if sys.version_info >= (3, 0): # For Python3 we decode binary strings as Unicode for compatibility # with Python2 - vim = vim.with_hook(neovim.DecodeHook()) + vim = vim.with_decodehook(neovim.DecodeHook()) cleanup_func = ''':function BeforeEachTest() set all& diff --git a/test/test_concurrency.py b/test/test_concurrency.py index 8bccde5e..b7a0a5dd 100644 --- a/test/test_concurrency.py +++ b/test/test_concurrency.py @@ -6,14 +6,14 @@ @with_setup(setup=cleanup) def test_interrupt_from_another_thread(): session = vim.session - timer = Timer(0.5, lambda: session.threadsafe_call(lambda: session.stop())) + timer = Timer(0.5, lambda: vim.async_call(lambda: session.stop())) timer.start() - eq(vim.session.next_message(), None) + eq(vim.next_message(), None) @with_setup(setup=cleanup) def test_exception_in_threadsafe_call(): # an exception in a threadsafe_call shouldn't crash the entire host vim.session.threadsafe_call(lambda: [vim.eval("3"), undefined_variable]) - vim.session.threadsafe_call(lambda: vim.session.stop()) - vim.session.run(None, None) + vim.session.threadsafe_call(lambda: vim.stop_loop()) + vim.run_loop(None, None, lambda: vim.input("")) diff --git a/test/test_events.py b/test/test_events.py index e6a76247..d273e0a8 100644 --- a/test/test_events.py +++ b/test/test_events.py @@ -6,13 +6,13 @@ @with_setup(setup=cleanup) def test_receiving_events(): vim.command('call rpcnotify(%d, "test-event", 1, 2, 3)' % vim.channel_id) - event = vim.session.next_message() + event = vim.next_message() eq(event[1], 'test-event') eq(event[2], [1, 2, 3]) vim.command('au FileType python call rpcnotify(%d, "py!", bufnr("$"))' % vim.channel_id) vim.command('set filetype=python') - event = vim.session.next_message() + event = vim.next_message() eq(event[1], 'py!') eq(event[2], [vim.current.buffer.number]) @@ -22,7 +22,7 @@ def test_sending_notify(): vim.command("let g:test = 3", async=True) cmd = 'call rpcnotify(%d, "test-event", g:test)' % vim.channel_id vim.command(cmd, async=True) - event = vim.session.next_message() + event = vim.next_message() eq(event[1], 'test-event') eq(event[2], [3]) @@ -37,16 +37,16 @@ def test_broadcast(): vim.command('call rpcnotify(0, "event1", 1, 2, 3)') vim.command('call rpcnotify(0, "event2", 4, 5, 6)') vim.command('call rpcnotify(0, "event2", 7, 8, 9)') - event = vim.session.next_message() + event = vim.next_message() eq(event[1], 'event2') eq(event[2], [4, 5, 6]) - event = vim.session.next_message() + event = vim.next_message() eq(event[1], 'event2') eq(event[2], [7, 8, 9]) vim.unsubscribe('event2') vim.subscribe('event1') vim.command('call rpcnotify(0, "event2", 10, 11, 12)') vim.command('call rpcnotify(0, "event1", 13, 14, 15)') - msg = vim.session.next_message() + msg = vim.next_message() eq(msg[1], 'event1') eq(msg[2], [13, 14, 15])