Codebase list python-pynvim / 0cbf04b
Update upstream source from tag 'upstream/0.3.2' Update to upstream version '0.3.2' with Debian dir 50daf90f35e1043fc9ce20bc8eee4c5c62054af9 Víctor Cuadrado Juan 4 years ago
62 changed file(s) with 3135 addition(s) and 3077 deletion(s). Raw diff Collapse all Expand all
00 Metadata-Version: 2.1
1 Name: neovim
2 Version: 0.3.0
1 Name: pynvim
2 Version: 0.3.2
33 Summary: Python client to neovim
44 Home-page: http://github.com/neovim/python-client
55 Author: Thiago de Arruda
66 Author-email: tpadilha84@gmail.com
77 License: Apache
8 Download-URL: https://github.com/neovim/python-client/archive/0.3.0.tar.gz
8 Download-URL: https://github.com/neovim/python-client/archive/0.3.2.tar.gz
99 Description: UNKNOWN
1010 Platform: UNKNOWN
11 Provides-Extra: test
1112 Provides-Extra: pyuv
12 Provides-Extra: test
00 ### Pynvim: Python client to [Neovim](https://github.com/neovim/neovim)
11
2 [![Build Status](https://travis-ci.org/neovim/python-client.svg?branch=master)](https://travis-ci.org/neovim/python-client)
2 [![Build Status](https://travis-ci.org/neovim/pynvim.svg?branch=master)](https://travis-ci.org/neovim/pynvim)
33 [![Documentation Status](https://readthedocs.org/projects/pynvim/badge/?version=latest)](http://pynvim.readthedocs.io/en/latest/?badge=latest)
4 [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/neovim/python-client/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/neovim/python-client/?branch=master)
5 [![Code Coverage](https://scrutinizer-ci.com/g/neovim/python-client/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/neovim/python-client/?branch=master)
4 [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/neovim/pynvim/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/neovim/pynvim/?branch=master)
5 [![Code Coverage](https://scrutinizer-ci.com/g/neovim/pynvim/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/neovim/pynvim/?branch=master)
66
77 Pynvim implements support for python plugins in Nvim. It also works as a library for
88 connecting to and scripting Nvim processes through its msgpack-rpc API.
1212 Supports python 2.7, and 3.4 or later.
1313
1414 ```sh
15 pip2 install neovim
16 pip3 install neovim
15 pip2 install pynvim
16 pip3 install pynvim
1717 ```
1818
1919 If you only use one of python2 or python3, it is enough to install that
2020 version. You can install the package without being root by adding the `--user`
2121 flag.
2222
23 If you follow Neovim master, make sure to upgrade the python-client when you
24 upgrade neovim:
23 Anytime you upgrade Neovim, make sure to upgrade pynvim as well:
2524 ```sh
26 pip2 install --upgrade neovim
27 pip3 install --upgrade neovim
25 pip2 install --upgrade pynvim
26 pip3 install --upgrade pynvim
2827 ```
2928
3029 Alternatively, the master version could be installed by executing the following
5655
5756 #### Development
5857
59 If you change the code, you need to run
60 ```sh
61 pip2 install .
62 pip3 install .
63 ```
64 for the changes to have effect. For instructions of testing and troubleshooting,
65 see the [development](http://pynvim.readthedocs.io/en/latest/development.html) documentation.
58 Use (and activate) a local virtualenv.
59
60 python3 -m venv env36
61 source env36/bin/activate
62
63 If you change the code, you must reinstall for the changes to take effect:
64
65 pip install .
66
67 Use `pytest` to run the tests. Invoking with `python -m` prepends the current
68 directory to `sys.path` (otherwise `pytest` might find other versions!):
69
70 python -m pytest
71
72 For details about testing and troubleshooting, see the
73 [development](http://pynvim.readthedocs.io/en/latest/development.html)
74 documentation.
6675
6776 #### Usage through the python REPL
6877
7988 bridge](http://vimdoc.sourceforge.net/htmldoc/if_pyth.html#python-vim)):
8089
8190 ```python
82 >>> from neovim import attach
91 >>> from pynvim import attach
8392 # Create a python API session attached to unix domain socket created above:
8493 >>> nvim = attach('socket', path='/tmp/nvim')
8594 # Now do some work.
97106 running neovim instance.
98107
99108 ```python
100 >>> from neovim import attach
109 >>> from pynvim import attach
101110 >>> nvim = attach('child', argv=["/bin/env", "nvim", "--embed"])
102111 ```
103112
00 """Python client for Nvim.
11
2 Client library for talking with Nvim processes via it's msgpack-rpc API.
2 This is a transition package. New projects should instead import pynvim package.
33 """
4 import logging
5 import os
6 import sys
4 import pynvim
5 from pynvim import *
76
8 from .api import Nvim
9 from .compat import IS_PYTHON3
10 from .msgpack_rpc import (ErrorResponse, child_session, socket_session,
11 stdio_session, tcp_session)
12 from .plugin import (Host, autocmd, command, decode, encoding, function,
13 plugin, rpc_export, shutdown_hook)
14 from .util import VERSION, Version
15
16
17 __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session',
18 'start_host', 'autocmd', 'command', 'encoding', 'decode',
19 'function', 'plugin', 'rpc_export', 'Host', 'Nvim', 'Version',
20 'VERSION', 'shutdown_hook', 'attach', 'setup_logging',
21 'ErrorResponse')
22
23
24 def start_host(session=None):
25 """Promote the current process into python plugin host for Nvim.
26
27 Start msgpack-rpc event loop for `session`, listening for Nvim requests
28 and notifications. It registers Nvim commands for loading/unloading
29 python plugins.
30
31 The sys.stdout and sys.stderr streams are redirected to Nvim through
32 `session`. That means print statements probably won't work as expected
33 while this function doesn't return.
34
35 This function is normally called at program startup and could have been
36 defined as a separate executable. It is exposed as a library function for
37 testing purposes only.
38 """
39 plugins = []
40 for arg in sys.argv:
41 _, ext = os.path.splitext(arg)
42 if ext == '.py':
43 plugins.append(arg)
44 elif os.path.isdir(arg):
45 init = os.path.join(arg, '__init__.py')
46 if os.path.isfile(init):
47 plugins.append(arg)
48
49 # This is a special case to support the old workaround of
50 # adding an empty .py file to make a package directory
51 # visible, and it should be removed soon.
52 for path in list(plugins):
53 dup = path + ".py"
54 if os.path.isdir(path) and dup in plugins:
55 plugins.remove(dup)
56
57 # Special case: the legacy scripthost receives a single relative filename
58 # while the rplugin host will receive absolute paths.
59 if plugins == ["script_host.py"]:
60 name = "script"
61 else:
62 name = "rplugin"
63
64 setup_logging(name)
65
66 if not session:
67 session = stdio_session()
68 nvim = Nvim.from_session(session)
69
70 if nvim.version.api_level < 1:
71 sys.stderr.write("This version of the neovim python package "
72 "requires nvim 0.1.6 or later")
73 sys.exit(1)
74
75 host = Host(nvim)
76 host.start(plugins)
77
78
79 def attach(session_type, address=None, port=None,
80 path=None, argv=None, decode=None):
81 """Provide a nicer interface to create python api sessions.
82
83 Previous machinery to create python api sessions is still there. This only
84 creates a facade function to make things easier for the most usual cases.
85 Thus, instead of:
86 from neovim import socket_session, Nvim
87 session = tcp_session(address=<address>, port=<port>)
88 nvim = Nvim.from_session(session)
89 You can now do:
90 from neovim import attach
91 nvim = attach('tcp', address=<address>, port=<port>)
92 And also:
93 nvim = attach('socket', path=<path>)
94 nvim = attach('child', argv=<argv>)
95 nvim = attach('stdio')
96
97 When the session is not needed anymore, it is recommended to explicitly
98 close it:
99 nvim.close()
100 It is also possible to use the session as a context mangager:
101 with attach('socket', path=thepath) as nvim:
102 print(nvim.funcs.getpid())
103 print(nvim.current.line)
104 This will automatically close the session when you're done with it, or
105 when an error occured.
106
107
108 """
109 session = (tcp_session(address, port) if session_type == 'tcp' else
110 socket_session(path) if session_type == 'socket' else
111 stdio_session() if session_type == 'stdio' else
112 child_session(argv) if session_type == 'child' else
113 None)
114
115 if not session:
116 raise Exception('Unknown session type "%s"' % session_type)
117
118 if decode is None:
119 decode = IS_PYTHON3
120
121 return Nvim.from_session(session).with_decode(decode)
122
123
124 def setup_logging(name):
125 """Setup logging according to environment variables."""
126 logger = logging.getLogger(__name__)
127 if 'NVIM_PYTHON_LOG_FILE' in os.environ:
128 prefix = os.environ['NVIM_PYTHON_LOG_FILE'].strip()
129 major_version = sys.version_info[0]
130 logfile = '{}_py{}_{}'.format(prefix, major_version, name)
131 handler = logging.FileHandler(logfile, 'w', 'utf-8')
132 handler.formatter = logging.Formatter(
133 '%(asctime)s [%(levelname)s @ '
134 '%(filename)s:%(funcName)s:%(lineno)s] %(process)s - %(message)s')
135 logging.root.addHandler(handler)
136 level = logging.INFO
137 if 'NVIM_PYTHON_LOG_LEVEL' in os.environ:
138 lvl = getattr(logging,
139 os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(),
140 level)
141 if isinstance(lvl, int):
142 level = lvl
143 logger.setLevel(level)
144
145
146 # Required for python 2.6
147 class NullHandler(logging.Handler):
148 def emit(self, record):
149 pass
150
151
152 if not logging.root.handlers:
153 logging.root.addHandler(NullHandler())
7 __all__ = pynvim.__all__
00 """Nvim API subpackage.
11
2 This package implements a higher-level API that wraps msgpack-rpc `Session`
3 instances.
2 This is a transition package. New projects should instead import pynvim.api.
43 """
4 from pynvim import api
5 from pynvim.api import *
56
6 from .buffer import Buffer
7 from .common import decode_if_bytes, walk
8 from .nvim import Nvim, NvimError
9 from .tabpage import Tabpage
10 from .window import Window
11
12
13 __all__ = ('Nvim', 'Buffer', 'Window', 'Tabpage', 'NvimError',
14 'decode_if_bytes', 'walk')
7 __all__ = api.__all__
+0
-210
neovim/api/buffer.py less more
0 """API for working with a Nvim Buffer."""
1 from .common import Remote
2 from ..compat import IS_PYTHON3, check_async
3
4
5 __all__ = ('Buffer')
6
7
8 if IS_PYTHON3:
9 basestring = str
10
11
12 def adjust_index(idx, default=None):
13 """Convert from python indexing convention to nvim indexing convention."""
14 if idx is None:
15 return default
16 elif idx < 0:
17 return idx - 1
18 else:
19 return idx
20
21
22 class Buffer(Remote):
23
24 """A remote Nvim buffer."""
25
26 _api_prefix = "nvim_buf_"
27
28 def __len__(self):
29 """Return the number of lines contained in a Buffer."""
30 return self.request('nvim_buf_line_count')
31
32 def __getitem__(self, idx):
33 """Get a buffer line or slice by integer index.
34
35 Indexes may be negative to specify positions from the end of the
36 buffer. For example, -1 is the last line, -2 is the line before that
37 and so on.
38
39 When retrieving slices, omiting indexes(eg: `buffer[:]`) will bring
40 the whole buffer.
41 """
42 if not isinstance(idx, slice):
43 i = adjust_index(idx)
44 return self.request('nvim_buf_get_lines', i, i + 1, True)[0]
45 start = adjust_index(idx.start, 0)
46 end = adjust_index(idx.stop, -1)
47 return self.request('nvim_buf_get_lines', start, end, False)
48
49 def __setitem__(self, idx, item):
50 """Replace a buffer line or slice by integer index.
51
52 Like with `__getitem__`, indexes may be negative.
53
54 When replacing slices, omiting indexes(eg: `buffer[:]`) will replace
55 the whole buffer.
56 """
57 if not isinstance(idx, slice):
58 i = adjust_index(idx)
59 lines = [item] if item is not None else []
60 return self.request('nvim_buf_set_lines', i, i + 1, True, lines)
61 lines = item if item is not None else []
62 start = adjust_index(idx.start, 0)
63 end = adjust_index(idx.stop, -1)
64 return self.request('nvim_buf_set_lines', start, end, False, lines)
65
66 def __iter__(self):
67 """Iterate lines of a buffer.
68
69 This will retrieve all lines locally before iteration starts. This
70 approach is used because for most cases, the gain is much greater by
71 minimizing the number of API calls by transfering all data needed to
72 work.
73 """
74 lines = self[:]
75 for line in lines:
76 yield line
77
78 def __delitem__(self, idx):
79 """Delete line or slice of lines from the buffer.
80
81 This is the same as __setitem__(idx, [])
82 """
83 self.__setitem__(idx, None)
84
85 def append(self, lines, index=-1):
86 """Append a string or list of lines to the buffer."""
87 if isinstance(lines, (basestring, bytes)):
88 lines = [lines]
89 return self.request('nvim_buf_set_lines', index, index, True, lines)
90
91 def mark(self, name):
92 """Return (row, col) tuple for a named mark."""
93 return self.request('nvim_buf_get_mark', name)
94
95 def range(self, start, end):
96 """Return a `Range` object, which represents part of the Buffer."""
97 return Range(self, start, end)
98
99 def add_highlight(self, hl_group, line, col_start=0,
100 col_end=-1, src_id=-1, async_=None,
101 **kwargs):
102 """Add a highlight to the buffer."""
103 async_ = check_async(async_, kwargs, src_id != 0)
104 return self.request('nvim_buf_add_highlight', src_id, hl_group,
105 line, col_start, col_end, async_=async_)
106
107 def clear_highlight(self, src_id, line_start=0, line_end=-1, async_=None,
108 **kwargs):
109 """Clear highlights from the buffer."""
110 async_ = check_async(async_, kwargs, True)
111 self.request('nvim_buf_clear_highlight', src_id,
112 line_start, line_end, async_=async_)
113
114 def update_highlights(self, src_id, hls, clear_start=0, clear_end=-1,
115 clear=False, async_=True):
116 """Add or update highlights in batch to avoid unnecessary redraws.
117
118 A `src_id` must have been allocated prior to use of this function. Use
119 for instance `nvim.new_highlight_source()` to get a src_id for your
120 plugin.
121
122 `hls` should be a list of highlight items. Each item should be a list
123 or tuple on the form `("GroupName", linenr, col_start, col_end)` or
124 `("GroupName", linenr)` to highlight an entire line.
125
126 By default existing highlights are preserved. Specify a line range with
127 clear_start and clear_end to replace highlights in this range. As a
128 shorthand, use clear=True to clear the entire buffer before adding the
129 new highlights.
130 """
131 if clear and clear_start is None:
132 clear_start = 0
133 lua = self._session._get_lua_private()
134 lua.update_highlights(self, src_id, hls, clear_start, clear_end,
135 async_=async_)
136
137 @property
138 def name(self):
139 """Get the buffer name."""
140 return self.request('nvim_buf_get_name')
141
142 @name.setter
143 def name(self, value):
144 """Set the buffer name. BufFilePre/BufFilePost are triggered."""
145 return self.request('nvim_buf_set_name', value)
146
147 @property
148 def valid(self):
149 """Return True if the buffer still exists."""
150 return self.request('nvim_buf_is_valid')
151
152 @property
153 def number(self):
154 """Get the buffer number."""
155 return self.handle
156
157
158 class Range(object):
159 def __init__(self, buffer, start, end):
160 self._buffer = buffer
161 self.start = start - 1
162 self.end = end - 1
163
164 def __len__(self):
165 return self.end - self.start + 1
166
167 def __getitem__(self, idx):
168 if not isinstance(idx, slice):
169 return self._buffer[self._normalize_index(idx)]
170 start = self._normalize_index(idx.start)
171 end = self._normalize_index(idx.stop)
172 if start is None:
173 start = self.start
174 if end is None:
175 end = self.end + 1
176 return self._buffer[start:end]
177
178 def __setitem__(self, idx, lines):
179 if not isinstance(idx, slice):
180 self._buffer[self._normalize_index(idx)] = lines
181 return
182 start = self._normalize_index(idx.start)
183 end = self._normalize_index(idx.stop)
184 if start is None:
185 start = self.start
186 if end is None:
187 end = self.end
188 self._buffer[start:end + 1] = lines
189
190 def __iter__(self):
191 for i in range(self.start, self.end + 1):
192 yield self._buffer[i]
193
194 def append(self, lines, i=None):
195 i = self._normalize_index(i)
196 if i is None:
197 i = self.end + 1
198 self._buffer.append(lines, i)
199
200 def _normalize_index(self, index):
201 if index is None:
202 return None
203 if index < 0:
204 index = self.end
205 else:
206 index += self.start
207 if index > self.end:
208 index = self.end
209 return index
+0
-180
neovim/api/common.py less more
0 """Code shared between the API classes."""
1 import functools
2
3 from msgpack import unpackb
4
5 from ..compat import unicode_errors_default
6
7
8 class Remote(object):
9
10 """Base class for Nvim objects(buffer/window/tabpage).
11
12 Each type of object has it's own specialized class with API wrappers around
13 the msgpack-rpc session. This implements equality which takes the remote
14 object handle into consideration.
15 """
16
17 def __init__(self, session, code_data):
18 """Initialize from session and code_data immutable object.
19
20 The `code_data` contains serialization information required for
21 msgpack-rpc calls. It must be immutable for Buffer equality to work.
22 """
23 self._session = session
24 self.code_data = code_data
25 self.handle = unpackb(code_data[1])
26 self.api = RemoteApi(self, self._api_prefix)
27 self.vars = RemoteMap(self, self._api_prefix + 'get_var',
28 self._api_prefix + 'set_var')
29 self.options = RemoteMap(self, self._api_prefix + 'get_option',
30 self._api_prefix + 'set_option')
31
32 def __repr__(self):
33 """Get text representation of the object."""
34 return '<%s(handle=%r)>' % (
35 self.__class__.__name__,
36 self.handle,
37 )
38
39 def __eq__(self, other):
40 """Return True if `self` and `other` are the same object."""
41 return (hasattr(other, 'code_data') and
42 other.code_data == self.code_data)
43
44 def __hash__(self):
45 """Return hash based on remote object id."""
46 return self.code_data.__hash__()
47
48 def request(self, name, *args, **kwargs):
49 """Wrapper for nvim.request."""
50 return self._session.request(name, self, *args, **kwargs)
51
52
53 class RemoteApi(object):
54
55 """Wrapper to allow api methods to be called like python methods."""
56
57 def __init__(self, obj, api_prefix):
58 """Initialize a RemoteApi with object and api prefix."""
59 self._obj = obj
60 self._api_prefix = api_prefix
61
62 def __getattr__(self, name):
63 """Return wrapper to named api method."""
64 return functools.partial(self._obj.request, self._api_prefix + name)
65
66
67 class RemoteMap(object):
68
69 """Represents a string->object map stored in Nvim.
70
71 This is the dict counterpart to the `RemoteSequence` class, but it is used
72 as a generic way of retrieving values from the various map-like data
73 structures present in Nvim.
74
75 It is used to provide a dict-like API to vim variables and options.
76 """
77
78 def __init__(self, obj, get_method, set_method=None):
79 """Initialize a RemoteMap with session, getter/setter."""
80 self._get = functools.partial(obj.request, get_method)
81 self._set = None
82 if set_method:
83 self._set = functools.partial(obj.request, set_method)
84
85 def __getitem__(self, key):
86 """Return a map value by key."""
87 return self._get(key)
88
89 def __setitem__(self, key, value):
90 """Set a map value by key(if the setter was provided)."""
91 if not self._set:
92 raise TypeError('This dict is read-only')
93 self._set(key, value)
94
95 def __delitem__(self, key):
96 """Delete a map value by associating None with the key."""
97 if not self._set:
98 raise TypeError('This dict is read-only')
99 return self._set(key, None)
100
101 def __contains__(self, key):
102 """Check if key is present in the map."""
103 try:
104 self._get(key)
105 return True
106 except Exception:
107 return False
108
109 def get(self, key, default=None):
110 """Return value for key if present, else a default value."""
111 try:
112 return self._get(key)
113 except Exception:
114 return default
115
116
117 class RemoteSequence(object):
118
119 """Represents a sequence of objects stored in Nvim.
120
121 This class is used to wrap msgapck-rpc functions that work on Nvim
122 sequences(of lines, buffers, windows and tabpages) with an API that
123 is similar to the one provided by the python-vim interface.
124
125 For example, the 'windows' property of the `Nvim` class is a RemoteSequence
126 sequence instance, and the expression `nvim.windows[0]` is translated to
127 session.request('nvim_list_wins')[0].
128
129 One important detail about this class is that all methods will fetch the
130 sequence into a list and perform the necessary manipulation
131 locally(iteration, indexing, counting, etc).
132 """
133
134 def __init__(self, session, method):
135 """Initialize a RemoteSequence with session, method."""
136 self._fetch = functools.partial(session.request, method)
137
138 def __len__(self):
139 """Return the length of the remote sequence."""
140 return len(self._fetch())
141
142 def __getitem__(self, idx):
143 """Return a sequence item by index."""
144 if not isinstance(idx, slice):
145 return self._fetch()[idx]
146 return self._fetch()[idx.start:idx.stop]
147
148 def __iter__(self):
149 """Return an iterator for the sequence."""
150 items = self._fetch()
151 for item in items:
152 yield item
153
154 def __contains__(self, item):
155 """Check if an item is present in the sequence."""
156 return item in self._fetch()
157
158
159 def _identity(obj, session, method, kind):
160 return obj
161
162
163 def decode_if_bytes(obj, mode=True):
164 """Decode obj if it is bytes."""
165 if mode is True:
166 mode = unicode_errors_default
167 if isinstance(obj, bytes):
168 return obj.decode("utf-8", errors=mode)
169 return obj
170
171
172 def walk(fn, obj, *args, **kwargs):
173 """Recursively walk an object graph applying `fn`/`args` to objects."""
174 if type(obj) in [list, tuple]:
175 return list(walk(fn, o, *args) for o in obj)
176 if type(obj) is dict:
177 return dict((walk(fn, k, *args), walk(fn, v, *args)) for k, v in
178 obj.items())
179 return fn(obj, *args, **kwargs)
+0
-569
neovim/api/nvim.py less more
0 """Main Nvim interface."""
1 import os
2 import sys
3 import threading
4 from functools import partial
5 from traceback import format_stack
6
7 from msgpack import ExtType
8
9 from .buffer import Buffer
10 from .common import (Remote, RemoteApi, RemoteMap, RemoteSequence,
11 decode_if_bytes, walk)
12 from .tabpage import Tabpage
13 from .window import Window
14 from ..compat import IS_PYTHON3
15 from ..util import Version, format_exc_skip
16
17 __all__ = ('Nvim')
18
19
20 os_chdir = os.chdir
21
22 lua_module = """
23 local a = vim.api
24 local function update_highlights(buf, src_id, hls, clear_first, clear_end)
25 if clear_first ~= nil then
26 a.nvim_buf_clear_highlight(buf, src_id, clear_first, clear_end)
27 end
28 for _,hl in pairs(hls) do
29 local group, line, col_start, col_end = unpack(hl)
30 if col_start == nil then
31 col_start = 0
32 end
33 if col_end == nil then
34 col_end = -1
35 end
36 a.nvim_buf_add_highlight(buf, src_id, group, line, col_start, col_end)
37 end
38 end
39
40 local chid = ...
41 local mod = {update_highlights=update_highlights}
42 _G["_pynvim_"..chid] = mod
43 """
44
45
46 class Nvim(object):
47
48 """Class that represents a remote Nvim instance.
49
50 This class is main entry point to Nvim remote API, it is a wrapper
51 around Session instances.
52
53 The constructor of this class must not be called directly. Instead, the
54 `from_session` class method should be used to create the first instance
55 from a raw `Session` instance.
56
57 Subsequent instances for the same session can be created by calling the
58 `with_decode` instance method to change the decoding behavior or
59 `SubClass.from_nvim(nvim)` where `SubClass` is a subclass of `Nvim`, which
60 is useful for having multiple `Nvim` objects that behave differently
61 without one affecting the other.
62
63 When this library is used on python3.4+, asyncio event loop is guaranteed
64 to be used. It is available as the "loop" attribute of this class. Note
65 that asyncio callbacks cannot make blocking requests, which includes
66 accessing state-dependent attributes. They should instead schedule another
67 callback using nvim.async_call, which will not have this restriction.
68 """
69
70 @classmethod
71 def from_session(cls, session):
72 """Create a new Nvim instance for a Session instance.
73
74 This method must be called to create the first Nvim instance, since it
75 queries Nvim metadata for type information and sets a SessionHook for
76 creating specialized objects from Nvim remote handles.
77 """
78 session.error_wrapper = lambda e: NvimError(e[1])
79 channel_id, metadata = session.request(b'vim_get_api_info')
80
81 if IS_PYTHON3:
82 # decode all metadata strings for python3
83 metadata = walk(decode_if_bytes, metadata)
84
85 types = {
86 metadata['types']['Buffer']['id']: Buffer,
87 metadata['types']['Window']['id']: Window,
88 metadata['types']['Tabpage']['id']: Tabpage,
89 }
90
91 return cls(session, channel_id, metadata, types)
92
93 @classmethod
94 def from_nvim(cls, nvim):
95 """Create a new Nvim instance from an existing instance."""
96 return cls(nvim._session, nvim.channel_id, nvim.metadata,
97 nvim.types, nvim._decode, nvim._err_cb)
98
99 def __init__(self, session, channel_id, metadata, types,
100 decode=False, err_cb=None):
101 """Initialize a new Nvim instance. This method is module-private."""
102 self._session = session
103 self.channel_id = channel_id
104 self.metadata = metadata
105 version = metadata.get("version", {"api_level": 0})
106 self.version = Version(**version)
107 self.types = types
108 self.api = RemoteApi(self, 'nvim_')
109 self.vars = RemoteMap(self, 'nvim_get_var', 'nvim_set_var')
110 self.vvars = RemoteMap(self, 'nvim_get_vvar', None)
111 self.options = RemoteMap(self, 'nvim_get_option', 'nvim_set_option')
112 self.buffers = Buffers(self)
113 self.windows = RemoteSequence(self, 'nvim_list_wins')
114 self.tabpages = RemoteSequence(self, 'nvim_list_tabpages')
115 self.current = Current(self)
116 self.session = CompatibilitySession(self)
117 self.funcs = Funcs(self)
118 self.lua = LuaFuncs(self)
119 self.error = NvimError
120 self._decode = decode
121 self._err_cb = err_cb
122
123 # only on python3.4+ we expose asyncio
124 if IS_PYTHON3:
125 self.loop = self._session.loop._loop
126
127 def _from_nvim(self, obj, decode=None):
128 if decode is None:
129 decode = self._decode
130 if type(obj) is ExtType:
131 cls = self.types[obj.code]
132 return cls(self, (obj.code, obj.data))
133 if decode:
134 obj = decode_if_bytes(obj, decode)
135 return obj
136
137 def _to_nvim(self, obj):
138 if isinstance(obj, Remote):
139 return ExtType(*obj.code_data)
140 return obj
141
142 def _get_lua_private(self):
143 if not getattr(self._session, "_has_lua", False):
144 self.exec_lua(lua_module, self.channel_id)
145 self._session._has_lua = True
146 return getattr(self.lua, "_pynvim_{}".format(self.channel_id))
147
148 def request(self, name, *args, **kwargs):
149 r"""Send an API request or notification to nvim.
150
151 It is rarely needed to call this function directly, as most API
152 functions have python wrapper functions. The `api` object can
153 be also be used to call API functions as methods:
154
155 vim.api.err_write('ERROR\n', async_=True)
156 vim.current.buffer.api.get_mark('.')
157
158 is equivalent to
159
160 vim.request('nvim_err_write', 'ERROR\n', async_=True)
161 vim.request('nvim_buf_get_mark', vim.current.buffer, '.')
162
163
164 Normally a blocking request will be sent. If the `async_` flag is
165 present and True, a asynchronous notification is sent instead. This
166 will never block, and the return value or error is ignored.
167 """
168 if (self._session._loop_thread is not None and
169 threading.current_thread() != self._session._loop_thread):
170
171 msg = ("request from non-main thread:\n{}\n"
172 .format('\n'.join(format_stack(None, 5)[:-1])))
173
174 self.async_call(self._err_cb, msg)
175 raise NvimError("request from non-main thread")
176
177 decode = kwargs.pop('decode', self._decode)
178 args = walk(self._to_nvim, args)
179 res = self._session.request(name, *args, **kwargs)
180 return walk(self._from_nvim, res, decode=decode)
181
182 def next_message(self):
183 """Block until a message(request or notification) is available.
184
185 If any messages were previously enqueued, return the first in queue.
186 If not, run the event loop until one is received.
187 """
188 msg = self._session.next_message()
189 if msg:
190 return walk(self._from_nvim, msg)
191
192 def run_loop(self, request_cb, notification_cb,
193 setup_cb=None, err_cb=None):
194 """Run the event loop to receive requests and notifications from Nvim.
195
196 This should not be called from a plugin running in the host, which
197 already runs the loop and dispatches events to plugins.
198 """
199 if err_cb is None:
200 err_cb = sys.stderr.write
201 self._err_cb = err_cb
202
203 def filter_request_cb(name, args):
204 name = self._from_nvim(name)
205 args = walk(self._from_nvim, args)
206 try:
207 result = request_cb(name, args)
208 except Exception:
209 msg = ("error caught in request handler '{} {}'\n{}\n\n"
210 .format(name, args, format_exc_skip(1)))
211 self._err_cb(msg)
212 raise
213 return walk(self._to_nvim, result)
214
215 def filter_notification_cb(name, args):
216 name = self._from_nvim(name)
217 args = walk(self._from_nvim, args)
218 try:
219 notification_cb(name, args)
220 except Exception:
221 msg = ("error caught in notification handler '{} {}'\n{}\n\n"
222 .format(name, args, format_exc_skip(1)))
223 self._err_cb(msg)
224 raise
225
226 self._session.run(filter_request_cb, filter_notification_cb, setup_cb)
227
228 def stop_loop(self):
229 """Stop the event loop being started with `run_loop`."""
230 self._session.stop()
231
232 def close(self):
233 """Close the nvim session and release its resources."""
234 self._session.close()
235
236 def __enter__(self):
237 """Enter nvim session as a context manager."""
238 return self
239
240 def __exit__(self, *exc_info):
241 """Exit nvim session as a context manager.
242
243 Closes the event loop.
244 """
245 self.close()
246
247 def with_decode(self, decode=True):
248 """Initialize a new Nvim instance."""
249 return Nvim(self._session, self.channel_id,
250 self.metadata, self.types, decode, self._err_cb)
251
252 def ui_attach(self, width, height, rgb=None, **kwargs):
253 """Register as a remote UI.
254
255 After this method is called, the client will receive redraw
256 notifications.
257 """
258 options = kwargs
259 if rgb is not None:
260 options['rgb'] = rgb
261 return self.request('nvim_ui_attach', width, height, options)
262
263 def ui_detach(self):
264 """Unregister as a remote UI."""
265 return self.request('nvim_ui_detach')
266
267 def ui_try_resize(self, width, height):
268 """Notify nvim that the client window has resized.
269
270 If possible, nvim will send a redraw request to resize.
271 """
272 return self.request('ui_try_resize', width, height)
273
274 def subscribe(self, event):
275 """Subscribe to a Nvim event."""
276 return self.request('nvim_subscribe', event)
277
278 def unsubscribe(self, event):
279 """Unsubscribe to a Nvim event."""
280 return self.request('nvim_unsubscribe', event)
281
282 def command(self, string, **kwargs):
283 """Execute a single ex command."""
284 return self.request('nvim_command', string, **kwargs)
285
286 def command_output(self, string):
287 """Execute a single ex command and return the output."""
288 return self.request('nvim_command_output', string)
289
290 def eval(self, string, **kwargs):
291 """Evaluate a vimscript expression."""
292 return self.request('nvim_eval', string, **kwargs)
293
294 def call(self, name, *args, **kwargs):
295 """Call a vimscript function."""
296 return self.request('nvim_call_function', name, args, **kwargs)
297
298 def exec_lua(self, code, *args, **kwargs):
299 """Execute lua code.
300
301 Additional parameters are available as `...` inside the lua chunk.
302 Only statements are executed. To evaluate an expression, prefix it
303 with `return`: `return my_function(...)`
304
305 There is a shorthand syntax to call lua functions with arguments:
306
307 nvim.lua.func(1,2)
308 nvim.lua.mymod.myfunction(data, async_=True)
309
310 is equivalent to
311
312 nvim.exec_lua("return func(...)", 1, 2)
313 nvim.exec_lua("mymod.myfunction(...)", data, async_=True)
314
315 Note that with `async_=True` there is no return value.
316 """
317 return self.request('nvim_execute_lua', code, args, **kwargs)
318
319 def strwidth(self, string):
320 """Return the number of display cells `string` occupies.
321
322 Tab is counted as one cell.
323 """
324 return self.request('nvim_strwidth', string)
325
326 def list_runtime_paths(self):
327 """Return a list of paths contained in the 'runtimepath' option."""
328 return self.request('nvim_list_runtime_paths')
329
330 def foreach_rtp(self, cb):
331 """Invoke `cb` for each path in 'runtimepath'.
332
333 Call the given callable for each path in 'runtimepath' until either
334 callable returns something but None, the exception is raised or there
335 are no longer paths. If stopped in case callable returned non-None,
336 vim.foreach_rtp function returns the value returned by callable.
337 """
338 for path in self.request('nvim_list_runtime_paths'):
339 try:
340 if cb(path) is not None:
341 break
342 except Exception:
343 break
344
345 def chdir(self, dir_path):
346 """Run os.chdir, then all appropriate vim stuff."""
347 os_chdir(dir_path)
348 return self.request('nvim_set_current_dir', dir_path)
349
350 def feedkeys(self, keys, options='', escape_csi=True):
351 """Push `keys` to Nvim user input buffer.
352
353 Options can be a string with the following character flags:
354 - 'm': Remap keys. This is default.
355 - 'n': Do not remap keys.
356 - 't': Handle keys as if typed; otherwise they are handled as if coming
357 from a mapping. This matters for undo, opening folds, etc.
358 """
359 return self.request('nvim_feedkeys', keys, options, escape_csi)
360
361 def input(self, bytes):
362 """Push `bytes` to Nvim low level input buffer.
363
364 Unlike `feedkeys()`, this uses the lowest level input buffer and the
365 call is not deferred. It returns the number of bytes actually
366 written(which can be less than what was requested if the buffer is
367 full).
368 """
369 return self.request('nvim_input', bytes)
370
371 def replace_termcodes(self, string, from_part=False, do_lt=True,
372 special=True):
373 r"""Replace any terminal code strings by byte sequences.
374
375 The returned sequences are Nvim's internal representation of keys,
376 for example:
377
378 <esc> -> '\x1b'
379 <cr> -> '\r'
380 <c-l> -> '\x0c'
381 <up> -> '\x80ku'
382
383 The returned sequences can be used as input to `feedkeys`.
384 """
385 return self.request('nvim_replace_termcodes', string,
386 from_part, do_lt, special)
387
388 def out_write(self, msg, **kwargs):
389 """Print `msg` as a normal message."""
390 return self.request('nvim_out_write', msg, **kwargs)
391
392 def err_write(self, msg, **kwargs):
393 """Print `msg` as an error message."""
394 if self._thread_invalid():
395 # special case: if a non-main thread writes to stderr
396 # i.e. due to an uncaught exception, pass it through
397 # without raising an additional exception.
398 self.async_call(self.err_write, msg, **kwargs)
399 return
400 return self.request('nvim_err_write', msg, **kwargs)
401
402 def _thread_invalid(self):
403 return (self._session._loop_thread is not None and
404 threading.current_thread() != self._session._loop_thread)
405
406 def quit(self, quit_command='qa!'):
407 """Send a quit command to Nvim.
408
409 By default, the quit command is 'qa!' which will make Nvim quit without
410 saving anything.
411 """
412 try:
413 self.command(quit_command)
414 except IOError:
415 # sending a quit command will raise an IOError because the
416 # connection is closed before a response is received. Safe to
417 # ignore it.
418 pass
419
420 def new_highlight_source(self):
421 """Return new src_id for use with Buffer.add_highlight."""
422 return self.current.buffer.add_highlight("", 0, src_id=0)
423
424 def async_call(self, fn, *args, **kwargs):
425 """Schedule `fn` to be called by the event loop soon.
426
427 This function is thread-safe, and is the only way code not
428 on the main thread could interact with nvim api objects.
429
430 This function can also be called in a synchronous
431 event handler, just before it returns, to defer execution
432 that shouldn't block neovim.
433 """
434 call_point = ''.join(format_stack(None, 5)[:-1])
435
436 def handler():
437 try:
438 fn(*args, **kwargs)
439 except Exception as err:
440 msg = ("error caught while executing async callback:\n"
441 "{!r}\n{}\n \nthe call was requested at\n{}"
442 .format(err, format_exc_skip(1), call_point))
443 self._err_cb(msg)
444 raise
445 self._session.threadsafe_call(handler)
446
447
448 class Buffers(object):
449
450 """Remote NVim buffers.
451
452 Currently the interface for interacting with remote NVim buffers is the
453 `nvim_list_bufs` msgpack-rpc function. Most methods fetch the list of
454 buffers from NVim.
455
456 Conforms to *python-buffers*.
457 """
458
459 def __init__(self, nvim):
460 """Initialize a Buffers object with Nvim object `nvim`."""
461 self._fetch_buffers = nvim.api.list_bufs
462
463 def __len__(self):
464 """Return the count of buffers."""
465 return len(self._fetch_buffers())
466
467 def __getitem__(self, number):
468 """Return the Buffer object matching buffer number `number`."""
469 for b in self._fetch_buffers():
470 if b.number == number:
471 return b
472 raise KeyError(number)
473
474 def __contains__(self, b):
475 """Return whether Buffer `b` is a known valid buffer."""
476 return isinstance(b, Buffer) and b.valid
477
478 def __iter__(self):
479 """Return an iterator over the list of buffers."""
480 return iter(self._fetch_buffers())
481
482
483 class CompatibilitySession(object):
484
485 """Helper class for API compatibility."""
486
487 def __init__(self, nvim):
488 self.threadsafe_call = nvim.async_call
489
490
491 class Current(object):
492
493 """Helper class for emulating vim.current from python-vim."""
494
495 def __init__(self, session):
496 self._session = session
497 self.range = None
498
499 @property
500 def line(self):
501 return self._session.request('nvim_get_current_line')
502
503 @line.setter
504 def line(self, line):
505 return self._session.request('nvim_set_current_line', line)
506
507 @property
508 def buffer(self):
509 return self._session.request('nvim_get_current_buf')
510
511 @buffer.setter
512 def buffer(self, buffer):
513 return self._session.request('nvim_set_current_buf', buffer)
514
515 @property
516 def window(self):
517 return self._session.request('nvim_get_current_win')
518
519 @window.setter
520 def window(self, window):
521 return self._session.request('nvim_set_current_win', window)
522
523 @property
524 def tabpage(self):
525 return self._session.request('nvim_get_current_tabpage')
526
527 @tabpage.setter
528 def tabpage(self, tabpage):
529 return self._session.request('nvim_set_current_tabpage', tabpage)
530
531
532 class Funcs(object):
533
534 """Helper class for functional vimscript interface."""
535
536 def __init__(self, nvim):
537 self._nvim = nvim
538
539 def __getattr__(self, name):
540 return partial(self._nvim.call, name)
541
542
543 class LuaFuncs(object):
544
545 """Wrapper to allow lua functions to be called like python methods."""
546
547 def __init__(self, nvim, name=""):
548 self._nvim = nvim
549 self.name = name
550
551 def __getattr__(self, name):
552 """Return wrapper to named api method."""
553 prefix = self.name + "." if self.name else ""
554 return LuaFuncs(self._nvim, prefix + name)
555
556 def __call__(self, *args, **kwargs):
557 # first new function after keyword rename, be a bit noisy
558 if 'async' in kwargs:
559 raise ValueError('"async" argument is not allowed. '
560 'Use "async_" instead.')
561 async_ = kwargs.get('async_', False)
562 pattern = "return {}(...)" if not async_ else "{}(...)"
563 code = pattern.format(self.name)
564 return self._nvim.exec_lua(code, *args, **kwargs)
565
566
567 class NvimError(Exception):
568 pass
+0
-35
neovim/api/tabpage.py less more
0 """API for working with Nvim tabpages."""
1 from .common import Remote, RemoteSequence
2
3
4 __all__ = ('Tabpage')
5
6
7 class Tabpage(Remote):
8 """A remote Nvim tabpage."""
9
10 _api_prefix = "nvim_tabpage_"
11
12 def __init__(self, *args):
13 """Initialize from session and code_data immutable object.
14
15 The `code_data` contains serialization information required for
16 msgpack-rpc calls. It must be immutable for Buffer equality to work.
17 """
18 super(Tabpage, self).__init__(*args)
19 self.windows = RemoteSequence(self, 'nvim_tabpage_list_wins')
20
21 @property
22 def window(self):
23 """Get the `Window` currently focused on the tabpage."""
24 return self.request('nvim_tabpage_get_win')
25
26 @property
27 def valid(self):
28 """Return True if the tabpage still exists."""
29 return self.request('nvim_tabpage_is_valid')
30
31 @property
32 def number(self):
33 """Get the tabpage number."""
34 return self.request('nvim_tabpage_get_number')
+0
-72
neovim/api/window.py less more
0 """API for working with Nvim windows."""
1 from .common import Remote
2
3
4 __all__ = ('Window')
5
6
7 class Window(Remote):
8
9 """A remote Nvim window."""
10
11 _api_prefix = "nvim_win_"
12
13 @property
14 def buffer(self):
15 """Get the `Buffer` currently being displayed by the window."""
16 return self.request('nvim_win_get_buf')
17
18 @property
19 def cursor(self):
20 """Get the (row, col) tuple with the current cursor position."""
21 return self.request('nvim_win_get_cursor')
22
23 @cursor.setter
24 def cursor(self, pos):
25 """Set the (row, col) tuple as the new cursor position."""
26 return self.request('nvim_win_set_cursor', pos)
27
28 @property
29 def height(self):
30 """Get the window height in rows."""
31 return self.request('nvim_win_get_height')
32
33 @height.setter
34 def height(self, height):
35 """Set the window height in rows."""
36 return self.request('nvim_win_set_height', height)
37
38 @property
39 def width(self):
40 """Get the window width in rows."""
41 return self.request('nvim_win_get_width')
42
43 @width.setter
44 def width(self, width):
45 """Set the window height in rows."""
46 return self.request('nvim_win_set_width', width)
47
48 @property
49 def row(self):
50 """0-indexed, on-screen window position(row) in display cells."""
51 return self.request('nvim_win_get_position')[0]
52
53 @property
54 def col(self):
55 """0-indexed, on-screen window position(col) in display cells."""
56 return self.request('nvim_win_get_position')[1]
57
58 @property
59 def tabpage(self):
60 """Get the `Tabpage` that contains the window."""
61 return self.request('nvim_win_get_tabpage')
62
63 @property
64 def valid(self):
65 """Return True if the window still exists."""
66 return self.request('nvim_win_is_valid')
67
68 @property
69 def number(self):
70 """Get the window number."""
71 return self.request('nvim_win_get_number')
+0
-57
neovim/compat.py less more
0 """Code for supporting compatibility across python versions."""
1
2 import sys
3 import warnings
4 from imp import find_module as original_find_module
5
6
7 IS_PYTHON3 = sys.version_info >= (3, 0)
8
9
10 if IS_PYTHON3:
11 def find_module(fullname, path):
12 """Compatibility wrapper for imp.find_module.
13
14 Automatically decodes arguments of find_module, in Python3
15 they must be Unicode
16 """
17 if isinstance(fullname, bytes):
18 fullname = fullname.decode()
19 if isinstance(path, bytes):
20 path = path.decode()
21 elif isinstance(path, list):
22 newpath = []
23 for element in path:
24 if isinstance(element, bytes):
25 newpath.append(element.decode())
26 else:
27 newpath.append(element)
28 path = newpath
29 return original_find_module(fullname, path)
30
31 # There is no 'long' type in Python3 just int
32 long = int
33 unicode_errors_default = 'surrogateescape'
34 else:
35 find_module = original_find_module
36 unicode_errors_default = 'strict'
37
38 NUM_TYPES = (int, long, float)
39
40
41 def check_async(async_, kwargs, default):
42 """Return a value of 'async' in kwargs or default when async_ is None.
43
44 This helper function exists for backward compatibility (See #274).
45 It shows a warning message when 'async' in kwargs is used to note users.
46 """
47 if async_ is not None:
48 return async_
49 elif 'async' in kwargs:
50 warnings.warn(
51 '"async" attribute is deprecated. Use "async_" instead.',
52 DeprecationWarning,
53 )
54 return kwargs.pop('async')
55 else:
56 return default
+0
-42
neovim/msgpack_rpc/__init__.py less more
0 """Msgpack-rpc subpackage.
1
2 This package implements a msgpack-rpc client. While it was designed for
3 handling some Nvim particularities(server->client requests for example), the
4 code here should work with other msgpack-rpc servers.
5 """
6 from .async_session import AsyncSession
7 from .event_loop import EventLoop
8 from .msgpack_stream import MsgpackStream
9 from .session import ErrorResponse, Session
10
11
12 __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session',
13 'ErrorResponse')
14
15
16 def session(transport_type='stdio', *args, **kwargs):
17 loop = EventLoop(transport_type, *args, **kwargs)
18 msgpack_stream = MsgpackStream(loop)
19 async_session = AsyncSession(msgpack_stream)
20 session = Session(async_session)
21 return session
22
23
24 def tcp_session(address, port=7450):
25 """Create a msgpack-rpc session from a tcp address/port."""
26 return session('tcp', address, port)
27
28
29 def socket_session(path):
30 """Create a msgpack-rpc session from a unix domain socket."""
31 return session('socket', path)
32
33
34 def stdio_session():
35 """Create a msgpack-rpc session from stdin/stdout."""
36 return session('stdio')
37
38
39 def child_session(argv):
40 """Create a msgpack-rpc session from a new Nvim instance."""
41 return session('child', argv)
+0
-139
neovim/msgpack_rpc/async_session.py less more
0 """Asynchronous msgpack-rpc handling in the event loop pipeline."""
1 import logging
2 from traceback import format_exc
3
4
5 logger = logging.getLogger(__name__)
6 debug, info, warn = (logger.debug, logger.info, logger.warning,)
7
8
9 class AsyncSession(object):
10
11 """Asynchronous msgpack-rpc layer that wraps a msgpack stream.
12
13 This wraps the msgpack stream interface for reading/writing msgpack
14 documents and exposes an interface for sending and receiving msgpack-rpc
15 requests and notifications.
16 """
17
18 def __init__(self, msgpack_stream):
19 """Wrap `msgpack_stream` on a msgpack-rpc interface."""
20 self._msgpack_stream = msgpack_stream
21 self._next_request_id = 1
22 self._pending_requests = {}
23 self._request_cb = self._notification_cb = None
24 self._handlers = {
25 0: self._on_request,
26 1: self._on_response,
27 2: self._on_notification
28 }
29 self.loop = msgpack_stream.loop
30
31 def threadsafe_call(self, fn):
32 """Wrapper around `MsgpackStream.threadsafe_call`."""
33 self._msgpack_stream.threadsafe_call(fn)
34
35 def request(self, method, args, response_cb):
36 """Send a msgpack-rpc request to Nvim.
37
38 A msgpack-rpc with method `method` and argument `args` is sent to
39 Nvim. The `response_cb` function is called with when the response
40 is available.
41 """
42 request_id = self._next_request_id
43 self._next_request_id = request_id + 1
44 self._msgpack_stream.send([0, request_id, method, args])
45 self._pending_requests[request_id] = response_cb
46
47 def notify(self, method, args):
48 """Send a msgpack-rpc notification to Nvim.
49
50 A msgpack-rpc with method `method` and argument `args` is sent to
51 Nvim. This will have the same effect as a request, but no response
52 will be recieved
53 """
54 self._msgpack_stream.send([2, method, args])
55
56 def run(self, request_cb, notification_cb):
57 """Run the event loop to receive requests and notifications from Nvim.
58
59 While the event loop is running, `request_cb` and `_notification_cb`
60 will be called whenever requests or notifications are respectively
61 available.
62 """
63 self._request_cb = request_cb
64 self._notification_cb = notification_cb
65 self._msgpack_stream.run(self._on_message)
66 self._request_cb = None
67 self._notification_cb = None
68
69 def stop(self):
70 """Stop the event loop."""
71 self._msgpack_stream.stop()
72
73 def close(self):
74 """Close the event loop."""
75 self._msgpack_stream.close()
76
77 def _on_message(self, msg):
78 try:
79 self._handlers.get(msg[0], self._on_invalid_message)(msg)
80 except Exception:
81 err_str = format_exc(5)
82 warn(err_str)
83 self._msgpack_stream.send([1, 0, err_str, None])
84
85 def _on_request(self, msg):
86 # request
87 # - msg[1]: id
88 # - msg[2]: method name
89 # - msg[3]: arguments
90 debug('received request: %s, %s', msg[2], msg[3])
91 self._request_cb(msg[2], msg[3], Response(self._msgpack_stream,
92 msg[1]))
93
94 def _on_response(self, msg):
95 # response to a previous request:
96 # - msg[1]: the id
97 # - msg[2]: error(if any)
98 # - msg[3]: result(if not errored)
99 debug('received response: %s, %s', msg[2], msg[3])
100 self._pending_requests.pop(msg[1])(msg[2], msg[3])
101
102 def _on_notification(self, msg):
103 # notification/event
104 # - msg[1]: event name
105 # - msg[2]: arguments
106 debug('received notification: %s, %s', msg[1], msg[2])
107 self._notification_cb(msg[1], msg[2])
108
109 def _on_invalid_message(self, msg):
110 error = 'Received invalid message %s' % msg
111 warn(error)
112 self._msgpack_stream.send([1, 0, error, None])
113
114
115 class Response(object):
116
117 """Response to a msgpack-rpc request that came from Nvim.
118
119 When Nvim sends a msgpack-rpc request, an instance of this class is
120 created for remembering state required to send a response.
121 """
122
123 def __init__(self, msgpack_stream, request_id):
124 """Initialize the Response instance."""
125 self._msgpack_stream = msgpack_stream
126 self._request_id = request_id
127
128 def send(self, value, error=False):
129 """Send the response.
130
131 If `error` is True, it will be sent as an error.
132 """
133 if error:
134 resp = [1, self._request_id, value, None]
135 else:
136 resp = [1, self._request_id, None, value]
137 debug('sending response to request %d: %s', self._request_id, resp)
138 self._msgpack_stream.send(resp)
+0
-24
neovim/msgpack_rpc/event_loop/__init__.py less more
0 """Event loop abstraction subpackage.
1
2 Tries to use pyuv as a backend, falling back to the asyncio implementation.
3 """
4
5 from ...compat import IS_PYTHON3
6
7 # on python3 we only support asyncio, as we expose it to plugins
8 if IS_PYTHON3:
9 from .asyncio import AsyncioEventLoop
10 EventLoop = AsyncioEventLoop
11 else:
12 try:
13 # libuv is fully implemented in C, use it when available
14 from .uv import UvEventLoop
15 EventLoop = UvEventLoop
16 except ImportError:
17 # asyncio(trollius on python 2) is pure python and should be more
18 # portable across python implementations
19 from .asyncio import AsyncioEventLoop
20 EventLoop = AsyncioEventLoop
21
22
23 __all__ = ('EventLoop')
+0
-156
neovim/msgpack_rpc/event_loop/asyncio.py less more
0 """Event loop implementation that uses the `asyncio` standard module.
1
2 The `asyncio` module was added to python standard library on 3.4, and it
3 provides a pure python implementation of an event loop library. It is used
4 as a fallback in case pyuv is not available(on python implementations other
5 than CPython).
6
7 Earlier python versions are supported through the `trollius` package, which
8 is a backport of `asyncio` that works on Python 2.6+.
9 """
10 from __future__ import absolute_import
11
12 import logging
13 import os
14 import sys
15 from collections import deque
16
17 try:
18 # For python 3.4+, use the standard library module
19 import asyncio
20 except (ImportError, SyntaxError):
21 # Fallback to trollius
22 import trollius as asyncio
23
24 from .base import BaseEventLoop
25
26 logger = logging.getLogger(__name__)
27 debug, info, warn = (logger.debug, logger.info, logger.warning,)
28
29 loop_cls = asyncio.SelectorEventLoop
30 if os.name == 'nt':
31 from asyncio.windows_utils import PipeHandle
32 import msvcrt
33
34 # On windows use ProactorEventLoop which support pipes and is backed by the
35 # more powerful IOCP facility
36 # NOTE: we override in the stdio case, because it doesn't work.
37 loop_cls = asyncio.ProactorEventLoop
38
39
40 class AsyncioEventLoop(BaseEventLoop, asyncio.Protocol,
41 asyncio.SubprocessProtocol):
42
43 """`BaseEventLoop` subclass that uses `asyncio` as a backend."""
44
45 def connection_made(self, transport):
46 """Used to signal `asyncio.Protocol` of a successful connection."""
47 self._transport = transport
48 self._raw_transport = transport
49 if isinstance(transport, asyncio.SubprocessTransport):
50 self._transport = transport.get_pipe_transport(0)
51
52 def connection_lost(self, exc):
53 """Used to signal `asyncio.Protocol` of a lost connection."""
54 self._on_error(exc.args[0] if exc else 'EOF')
55
56 def data_received(self, data):
57 """Used to signal `asyncio.Protocol` of incoming data."""
58 if self._on_data:
59 self._on_data(data)
60 return
61 self._queued_data.append(data)
62
63 def pipe_connection_lost(self, fd, exc):
64 """Used to signal `asyncio.SubprocessProtocol` of a lost connection."""
65 self._on_error(exc.args[0] if exc else 'EOF')
66
67 def pipe_data_received(self, fd, data):
68 """Used to signal `asyncio.SubprocessProtocol` of incoming data."""
69 if fd == 2: # stderr fd number
70 self._on_stderr(data)
71 elif self._on_data:
72 self._on_data(data)
73 else:
74 self._queued_data.append(data)
75
76 def process_exited(self):
77 """Used to signal `asyncio.SubprocessProtocol` when the child exits."""
78 self._on_error('EOF')
79
80 def _init(self):
81 self._loop = loop_cls()
82 self._queued_data = deque()
83 self._fact = lambda: self
84 self._raw_transport = None
85
86 def _connect_tcp(self, address, port):
87 coroutine = self._loop.create_connection(self._fact, address, port)
88 self._loop.run_until_complete(coroutine)
89
90 def _connect_socket(self, path):
91 if os.name == 'nt':
92 coroutine = self._loop.create_pipe_connection(self._fact, path)
93 else:
94 coroutine = self._loop.create_unix_connection(self._fact, path)
95 self._loop.run_until_complete(coroutine)
96
97 def _connect_stdio(self):
98 if os.name == 'nt':
99 pipe = PipeHandle(msvcrt.get_osfhandle(sys.stdin.fileno()))
100 else:
101 pipe = sys.stdin
102 coroutine = self._loop.connect_read_pipe(self._fact, pipe)
103 self._loop.run_until_complete(coroutine)
104 debug("native stdin connection successful")
105
106 if os.name == 'nt':
107 pipe = PipeHandle(msvcrt.get_osfhandle(sys.stdout.fileno()))
108 else:
109 pipe = sys.stdout
110 coroutine = self._loop.connect_write_pipe(self._fact, pipe)
111 self._loop.run_until_complete(coroutine)
112 debug("native stdout connection successful")
113
114 def _connect_child(self, argv):
115 if os.name != 'nt':
116 self._child_watcher = asyncio.get_child_watcher()
117 self._child_watcher.attach_loop(self._loop)
118 coroutine = self._loop.subprocess_exec(self._fact, *argv)
119 self._loop.run_until_complete(coroutine)
120
121 def _start_reading(self):
122 pass
123
124 def _send(self, data):
125 self._transport.write(data)
126
127 def _run(self):
128 while self._queued_data:
129 self._on_data(self._queued_data.popleft())
130 self._loop.run_forever()
131
132 def _stop(self):
133 self._loop.stop()
134
135 def _close(self):
136 if self._raw_transport is not None:
137 self._raw_transport.close()
138 self._loop.close()
139
140 def _threadsafe_call(self, fn):
141 self._loop.call_soon_threadsafe(fn)
142
143 def _setup_signals(self, signals):
144 if os.name == 'nt':
145 # add_signal_handler is not supported in win32
146 self._signals = []
147 return
148
149 self._signals = list(signals)
150 for signum in self._signals:
151 self._loop.add_signal_handler(signum, self._on_signal, signum)
152
153 def _teardown_signals(self):
154 for signum in self._signals:
155 self._loop.remove_signal_handler(signum)
+0
-180
neovim/msgpack_rpc/event_loop/base.py less more
0 """Common code for event loop implementations."""
1 import logging
2 import signal
3 import threading
4
5
6 logger = logging.getLogger(__name__)
7 debug, info, warn = (logger.debug, logger.info, logger.warning,)
8
9
10 # When signals are restored, the event loop library may reset SIGINT to SIG_DFL
11 # which exits the program. To be able to restore the python interpreter to it's
12 # default state, we keep a reference to the default handler
13 default_int_handler = signal.getsignal(signal.SIGINT)
14 main_thread = threading.current_thread()
15
16
17 class BaseEventLoop(object):
18
19 """Abstract base class for all event loops.
20
21 Event loops act as the bottom layer for Nvim sessions created by this
22 library. They hide system/transport details behind a simple interface for
23 reading/writing bytes to the connected Nvim instance.
24
25 This class exposes public methods for interacting with the underlying
26 event loop and delegates implementation-specific work to the following
27 methods, which subclasses are expected to implement:
28
29 - `_init()`: Implementation-specific initialization
30 - `_connect_tcp(address, port)`: connect to Nvim using tcp/ip
31 - `_connect_socket(path)`: Same as tcp, but use a UNIX domain socket or
32 named pipe.
33 - `_connect_stdio()`: Use stdin/stdout as the connection to Nvim
34 - `_connect_child(argv)`: Use the argument vector `argv` to spawn an
35 embedded Nvim that has it's stdin/stdout connected to the event loop.
36 - `_start_reading()`: Called after any of _connect_* methods. Can be used
37 to perform any post-connection setup or validation.
38 - `_send(data)`: Send `data`(byte array) to Nvim. The data is only
39 - `_run()`: Runs the event loop until stopped or the connection is closed.
40 calling the following methods when some event happens:
41 actually sent when the event loop is running.
42 - `_on_data(data)`: When Nvim sends some data.
43 - `_on_signal(signum)`: When a signal is received.
44 - `_on_error(message)`: When a non-recoverable error occurs(eg:
45 connection lost)
46 - `_stop()`: Stop the event loop
47 - `_interrupt(data)`: Like `stop()`, but may be called from other threads
48 this.
49 - `_setup_signals(signals)`: Add implementation-specific listeners for
50 for `signals`, which is a list of OS-specific signal numbers.
51 - `_teardown_signals()`: Removes signal listeners set by `_setup_signals`
52 """
53
54 def __init__(self, transport_type, *args):
55 """Initialize and connect the event loop instance.
56
57 The only arguments are the transport type and transport-specific
58 configuration, like this:
59
60 >>> BaseEventLoop('tcp', '127.0.0.1', 7450)
61 Traceback (most recent call last):
62 ...
63 AttributeError: 'BaseEventLoop' object has no attribute '_init'
64 >>> BaseEventLoop('socket', '/tmp/nvim-socket')
65 Traceback (most recent call last):
66 ...
67 AttributeError: 'BaseEventLoop' object has no attribute '_init'
68 >>> BaseEventLoop('stdio')
69 Traceback (most recent call last):
70 ...
71 AttributeError: 'BaseEventLoop' object has no attribute '_init'
72 >>> BaseEventLoop('child', ['nvim', '--embed', '-u', 'NONE'])
73 Traceback (most recent call last):
74 ...
75 AttributeError: 'BaseEventLoop' object has no attribute '_init'
76
77 This calls the implementation-specific initialization
78 `_init`, one of the `_connect_*` methods(based on `transport_type`)
79 and `_start_reading()`
80 """
81 self._transport_type = transport_type
82 self._signames = dict((k, v) for v, k in signal.__dict__.items()
83 if v.startswith('SIG'))
84 self._on_data = None
85 self._error = None
86 self._init()
87 try:
88 getattr(self, '_connect_{}'.format(transport_type))(*args)
89 except Exception as e:
90 self.close()
91 raise e
92 self._start_reading()
93
94 def connect_tcp(self, address, port):
95 """Connect to tcp/ip `address`:`port`. Delegated to `_connect_tcp`."""
96 info('Connecting to TCP address: %s:%d', address, port)
97 self._connect_tcp(address, port)
98
99 def connect_socket(self, path):
100 """Connect to socket at `path`. Delegated to `_connect_socket`."""
101 info('Connecting to %s', path)
102 self._connect_socket(path)
103
104 def connect_stdio(self):
105 """Connect using stdin/stdout. Delegated to `_connect_stdio`."""
106 info('Preparing stdin/stdout for streaming data')
107 self._connect_stdio()
108
109 def connect_child(self, argv):
110 """Connect a new Nvim instance. Delegated to `_connect_child`."""
111 info('Spawning a new nvim instance')
112 self._connect_child(argv)
113
114 def send(self, data):
115 """Queue `data` for sending to Nvim."""
116 debug("Sending '%s'", data)
117 self._send(data)
118
119 def threadsafe_call(self, fn):
120 """Call a function in the event loop thread.
121
122 This is the only safe way to interact with a session from other
123 threads.
124 """
125 self._threadsafe_call(fn)
126
127 def run(self, data_cb):
128 """Run the event loop."""
129 if self._error:
130 err = self._error
131 if isinstance(self._error, KeyboardInterrupt):
132 # KeyboardInterrupt is not destructive(it may be used in
133 # the REPL).
134 # After throwing KeyboardInterrupt, cleanup the _error field
135 # so the loop may be started again
136 self._error = None
137 raise err
138 self._on_data = data_cb
139 if threading.current_thread() == main_thread:
140 self._setup_signals([signal.SIGINT, signal.SIGTERM])
141 debug('Entering event loop')
142 self._run()
143 debug('Exited event loop')
144 if threading.current_thread() == main_thread:
145 self._teardown_signals()
146 signal.signal(signal.SIGINT, default_int_handler)
147 self._on_data = None
148
149 def stop(self):
150 """Stop the event loop."""
151 self._stop()
152 debug('Stopped event loop')
153
154 def close(self):
155 """Stop the event loop."""
156 self._close()
157 debug('Closed event loop')
158
159 def _on_signal(self, signum):
160 msg = 'Received {}'.format(self._signames[signum])
161 debug(msg)
162 if signum == signal.SIGINT and self._transport_type == 'stdio':
163 # When the transport is stdio, we are probably running as a Nvim
164 # child process. In that case, we don't want to be killed by
165 # ctrl+C
166 return
167 cls = Exception
168 if signum == signal.SIGINT:
169 cls = KeyboardInterrupt
170 self._error = cls(msg)
171 self.stop()
172
173 def _on_error(self, error):
174 debug(error)
175 self._error = IOError(error)
176 self.stop()
177
178 def _on_interrupt(self):
179 self.stop()
+0
-124
neovim/msgpack_rpc/event_loop/uv.py less more
0 """Event loop implementation that uses pyuv(libuv-python bindings)."""
1 import sys
2 from collections import deque
3
4 import pyuv
5
6 from .base import BaseEventLoop
7
8
9 class UvEventLoop(BaseEventLoop):
10
11 """`BaseEventLoop` subclass that uses `pvuv` as a backend."""
12
13 def _init(self):
14 self._loop = pyuv.Loop()
15 self._async = pyuv.Async(self._loop, self._on_async)
16 self._connection_error = None
17 self._error_stream = None
18 self._callbacks = deque()
19
20 def _on_connect(self, stream, error):
21 self.stop()
22 if error:
23 msg = 'Cannot connect to {}: {}'.format(
24 self._connect_address, pyuv.errno.strerror(error))
25 self._connection_error = IOError(msg)
26 return
27 self._read_stream = self._write_stream = stream
28
29 def _on_read(self, handle, data, error):
30 if error or not data:
31 msg = pyuv.errno.strerror(error) if error else 'EOF'
32 self._on_error(msg)
33 return
34 if handle == self._error_stream:
35 return
36 self._on_data(data)
37
38 def _on_write(self, handle, error):
39 if error:
40 msg = pyuv.errno.strerror(error)
41 self._on_error(msg)
42
43 def _on_exit(self, handle, exit_status, term_signal):
44 self._on_error('EOF')
45
46 def _disconnected(self, *args):
47 raise IOError('Not connected to Nvim')
48
49 def _connect_tcp(self, address, port):
50 stream = pyuv.TCP(self._loop)
51 self._connect_address = '{}:{}'.format(address, port)
52 stream.connect((address, port), self._on_connect)
53
54 def _connect_socket(self, path):
55 stream = pyuv.Pipe(self._loop)
56 self._connect_address = path
57 stream.connect(path, self._on_connect)
58
59 def _connect_stdio(self):
60 self._read_stream = pyuv.Pipe(self._loop)
61 self._read_stream.open(sys.stdin.fileno())
62 self._write_stream = pyuv.Pipe(self._loop)
63 self._write_stream.open(sys.stdout.fileno())
64
65 def _connect_child(self, argv):
66 self._write_stream = pyuv.Pipe(self._loop)
67 self._read_stream = pyuv.Pipe(self._loop)
68 self._error_stream = pyuv.Pipe(self._loop)
69 stdin = pyuv.StdIO(self._write_stream,
70 flags=pyuv.UV_CREATE_PIPE + pyuv.UV_READABLE_PIPE)
71 stdout = pyuv.StdIO(self._read_stream,
72 flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE)
73 stderr = pyuv.StdIO(self._error_stream,
74 flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE)
75 pyuv.Process.spawn(self._loop,
76 args=argv,
77 exit_callback=self._on_exit,
78 flags=pyuv.UV_PROCESS_WINDOWS_HIDE,
79 stdio=(stdin, stdout, stderr,))
80 self._error_stream.start_read(self._on_read)
81
82 def _start_reading(self):
83 if self._transport_type in ['tcp', 'socket']:
84 self._loop.run()
85 if self._connection_error:
86 self.run = self.send = self._disconnected
87 raise self._connection_error
88 self._read_stream.start_read(self._on_read)
89
90 def _send(self, data):
91 self._write_stream.write(data, self._on_write)
92
93 def _run(self):
94 self._loop.run(pyuv.UV_RUN_DEFAULT)
95
96 def _stop(self):
97 self._loop.stop()
98
99 def _close(self):
100 pass
101
102 def _threadsafe_call(self, fn):
103 self._callbacks.append(fn)
104 self._async.send()
105
106 def _on_async(self, handle):
107 while self._callbacks:
108 self._callbacks.popleft()()
109
110 def _setup_signals(self, signals):
111 self._signal_handles = []
112
113 def handler(h, signum):
114 self._on_signal(signum)
115
116 for signum in signals:
117 handle = pyuv.Signal(self._loop)
118 handle.start(handler, signum)
119 self._signal_handles.append(handle)
120
121 def _teardown_signals(self):
122 for handle in self._signal_handles:
123 handle.stop()
+0
-65
neovim/msgpack_rpc/msgpack_stream.py less more
0 """Msgpack handling in the event loop pipeline."""
1 import logging
2
3 from msgpack import Packer, Unpacker
4
5 from ..compat import unicode_errors_default
6
7 logger = logging.getLogger(__name__)
8 debug, info, warn = (logger.debug, logger.info, logger.warning,)
9
10
11 class MsgpackStream(object):
12
13 """Two-way msgpack stream that wraps a event loop byte stream.
14
15 This wraps the event loop interface for reading/writing bytes and
16 exposes an interface for reading/writing msgpack documents.
17 """
18
19 def __init__(self, event_loop):
20 """Wrap `event_loop` on a msgpack-aware interface."""
21 self.loop = event_loop
22 self._packer = Packer(encoding='utf-8',
23 unicode_errors=unicode_errors_default)
24 self._unpacker = Unpacker()
25 self._message_cb = None
26
27 def threadsafe_call(self, fn):
28 """Wrapper around `BaseEventLoop.threadsafe_call`."""
29 self.loop.threadsafe_call(fn)
30
31 def send(self, msg):
32 """Queue `msg` for sending to Nvim."""
33 debug('sent %s', msg)
34 self.loop.send(self._packer.pack(msg))
35
36 def run(self, message_cb):
37 """Run the event loop to receive messages from Nvim.
38
39 While the event loop is running, `message_cb` will be called whenever
40 a message has been successfully parsed from the input stream.
41 """
42 self._message_cb = message_cb
43 self.loop.run(self._on_data)
44 self._message_cb = None
45
46 def stop(self):
47 """Stop the event loop."""
48 self.loop.stop()
49
50 def close(self):
51 """Close the event loop."""
52 self.loop.close()
53
54 def _on_data(self, data):
55 self._unpacker.feed(data)
56 while True:
57 try:
58 debug('waiting for message...')
59 msg = next(self._unpacker)
60 debug('received message: %s', msg)
61 self._message_cb(msg)
62 except StopIteration:
63 debug('unpacker needs more data...')
64 break
+0
-239
neovim/msgpack_rpc/session.py less more
0 """Synchronous msgpack-rpc session layer."""
1 import logging
2 import threading
3 from collections import deque
4 from traceback import format_exc
5
6 import greenlet
7
8 from ..compat import check_async
9
10 logger = logging.getLogger(__name__)
11 error, debug, info, warn = (logger.error, logger.debug, logger.info,
12 logger.warning,)
13
14
15 class Session(object):
16
17 """Msgpack-rpc session layer that uses coroutines for a synchronous API.
18
19 This class provides the public msgpack-rpc API required by this library.
20 It uses the greenlet module to handle requests and notifications coming
21 from Nvim with a synchronous API.
22 """
23
24 def __init__(self, async_session):
25 """Wrap `async_session` on a synchronous msgpack-rpc interface."""
26 self._async_session = async_session
27 self._request_cb = self._notification_cb = None
28 self._pending_messages = deque()
29 self._is_running = False
30 self._setup_exception = None
31 self.loop = async_session.loop
32 self._loop_thread = None
33
34 def threadsafe_call(self, fn, *args, **kwargs):
35 """Wrapper around `AsyncSession.threadsafe_call`."""
36 def handler():
37 try:
38 fn(*args, **kwargs)
39 except Exception:
40 warn("error caught while excecuting async callback\n%s\n",
41 format_exc())
42
43 def greenlet_wrapper():
44 gr = greenlet.greenlet(handler)
45 gr.switch()
46
47 self._async_session.threadsafe_call(greenlet_wrapper)
48
49 def next_message(self):
50 """Block until a message(request or notification) is available.
51
52 If any messages were previously enqueued, return the first in queue.
53 If not, run the event loop until one is received.
54 """
55 if self._is_running:
56 raise Exception('Event loop already running')
57 if self._pending_messages:
58 return self._pending_messages.popleft()
59 self._async_session.run(self._enqueue_request_and_stop,
60 self._enqueue_notification_and_stop)
61 if self._pending_messages:
62 return self._pending_messages.popleft()
63
64 def request(self, method, *args, **kwargs):
65 """Send a msgpack-rpc request and block until as response is received.
66
67 If the event loop is running, this method must have been called by a
68 request or notification handler running on a greenlet. In that case,
69 send the quest and yield to the parent greenlet until a response is
70 available.
71
72 When the event loop is not running, it will perform a blocking request
73 like this:
74 - Send the request
75 - Run the loop until the response is available
76 - Put requests/notifications received while waiting into a queue
77
78 If the `async_` flag is present and True, a asynchronous notification
79 is sent instead. This will never block, and the return value or error
80 is ignored.
81 """
82 async_ = check_async(kwargs.pop('async_', None), kwargs, False)
83 if async_:
84 self._async_session.notify(method, args)
85 return
86
87 if kwargs:
88 raise ValueError("request got unsupported keyword argument(s): {}"
89 .format(', '.join(kwargs.keys())))
90
91 if self._is_running:
92 v = self._yielding_request(method, args)
93 else:
94 v = self._blocking_request(method, args)
95 if not v:
96 # EOF
97 raise IOError('EOF')
98 err, rv = v
99 if err:
100 info("'Received error: %s", err)
101 raise self.error_wrapper(err)
102 return rv
103
104 def run(self, request_cb, notification_cb, setup_cb=None):
105 """Run the event loop to receive requests and notifications from Nvim.
106
107 Like `AsyncSession.run()`, but `request_cb` and `notification_cb` are
108 inside greenlets.
109 """
110 self._request_cb = request_cb
111 self._notification_cb = notification_cb
112 self._is_running = True
113 self._setup_exception = None
114 self._loop_thread = threading.current_thread()
115
116 def on_setup():
117 try:
118 setup_cb()
119 except Exception as e:
120 self._setup_exception = e
121 self.stop()
122
123 if setup_cb:
124 # Create a new greenlet to handle the setup function
125 gr = greenlet.greenlet(on_setup)
126 gr.switch()
127
128 if self._setup_exception:
129 error('Setup error: {}'.format(self._setup_exception))
130 raise self._setup_exception
131
132 # Process all pending requests and notifications
133 while self._pending_messages:
134 msg = self._pending_messages.popleft()
135 getattr(self, '_on_{}'.format(msg[0]))(*msg[1:])
136 self._async_session.run(self._on_request, self._on_notification)
137 self._is_running = False
138 self._request_cb = None
139 self._notification_cb = None
140 self._loop_thread = None
141
142 if self._setup_exception:
143 raise self._setup_exception
144
145 def stop(self):
146 """Stop the event loop."""
147 self._async_session.stop()
148
149 def close(self):
150 """Close the event loop."""
151 self._async_session.close()
152
153 def _yielding_request(self, method, args):
154 gr = greenlet.getcurrent()
155 parent = gr.parent
156
157 def response_cb(err, rv):
158 debug('response is available for greenlet %s, switching back', gr)
159 gr.switch(err, rv)
160
161 self._async_session.request(method, args, response_cb)
162 debug('yielding from greenlet %s to wait for response', gr)
163 return parent.switch()
164
165 def _blocking_request(self, method, args):
166 result = []
167
168 def response_cb(err, rv):
169 result.extend([err, rv])
170 self.stop()
171
172 self._async_session.request(method, args, response_cb)
173 self._async_session.run(self._enqueue_request,
174 self._enqueue_notification)
175 return result
176
177 def _enqueue_request_and_stop(self, name, args, response):
178 self._enqueue_request(name, args, response)
179 self.stop()
180
181 def _enqueue_notification_and_stop(self, name, args):
182 self._enqueue_notification(name, args)
183 self.stop()
184
185 def _enqueue_request(self, name, args, response):
186 self._pending_messages.append(('request', name, args, response,))
187
188 def _enqueue_notification(self, name, args):
189 self._pending_messages.append(('notification', name, args,))
190
191 def _on_request(self, name, args, response):
192 def handler():
193 try:
194 rv = self._request_cb(name, args)
195 debug('greenlet %s finished executing, ' +
196 'sending %s as response', gr, rv)
197 response.send(rv)
198 except ErrorResponse as err:
199 warn("error response from request '%s %s': %s", name,
200 args, format_exc())
201 response.send(err.args[0], error=True)
202 except Exception as err:
203 warn("error caught while processing request '%s %s': %s", name,
204 args, format_exc())
205 response.send(repr(err) + "\n" + format_exc(5), error=True)
206 debug('greenlet %s is now dying...', gr)
207
208 # Create a new greenlet to handle the request
209 gr = greenlet.greenlet(handler)
210 debug('received rpc request, greenlet %s will handle it', gr)
211 gr.switch()
212
213 def _on_notification(self, name, args):
214 def handler():
215 try:
216 self._notification_cb(name, args)
217 debug('greenlet %s finished executing', gr)
218 except Exception:
219 warn("error caught while processing notification '%s %s': %s",
220 name, args, format_exc())
221
222 debug('greenlet %s is now dying...', gr)
223
224 gr = greenlet.greenlet(handler)
225 debug('received rpc notification, greenlet %s will handle it', gr)
226 gr.switch()
227
228
229 class ErrorResponse(BaseException):
230
231 """Raise this in a request handler to respond with a given error message.
232
233 Unlike when other exceptions are caught, this gives full control off the
234 error response sent. When "ErrorResponse(msg)" is caught "msg" will be
235 sent verbatim as the error response.No traceback will be appended.
236 """
237
238 pass
+0
-9
neovim/plugin/__init__.py less more
0 """Nvim plugin/host subpackage."""
1
2 from .decorators import (autocmd, command, decode, encoding, function,
3 plugin, rpc_export, shutdown_hook)
4 from .host import Host
5
6
7 __all__ = ('Host', 'plugin', 'rpc_export', 'command', 'autocmd',
8 'function', 'encoding', 'decode', 'shutdown_hook')
+0
-175
neovim/plugin/decorators.py less more
0 """Decorators used by python host plugin system."""
1
2 import inspect
3 import logging
4
5 from ..compat import IS_PYTHON3, unicode_errors_default
6
7 logger = logging.getLogger(__name__)
8 debug, info, warn = (logger.debug, logger.info, logger.warning,)
9 __all__ = ('plugin', 'rpc_export', 'command', 'autocmd', 'function',
10 'encoding', 'decode', 'shutdown_hook')
11
12
13 def plugin(cls):
14 """Tag a class as a plugin.
15
16 This decorator is required to make the class methods discoverable by the
17 plugin_load method of the host.
18 """
19 cls._nvim_plugin = True
20 # the _nvim_bind attribute is set to True by default, meaning that
21 # decorated functions have a bound Nvim instance as first argument.
22 # For methods in a plugin-decorated class this is not required, because
23 # the class initializer will already receive the nvim object.
24 predicate = lambda fn: hasattr(fn, '_nvim_bind')
25 for _, fn in inspect.getmembers(cls, predicate):
26 if IS_PYTHON3:
27 fn._nvim_bind = False
28 else:
29 fn.im_func._nvim_bind = False
30 return cls
31
32
33 def rpc_export(rpc_method_name, sync=False):
34 """Export a function or plugin method as a msgpack-rpc request handler."""
35 def dec(f):
36 f._nvim_rpc_method_name = rpc_method_name
37 f._nvim_rpc_sync = sync
38 f._nvim_bind = True
39 f._nvim_prefix_plugin_path = False
40 return f
41 return dec
42
43
44 def command(name, nargs=0, complete=None, range=None, count=None, bang=False,
45 register=False, sync=False, allow_nested=False, eval=None):
46 """Tag a function or plugin method as a Nvim command handler."""
47 def dec(f):
48 f._nvim_rpc_method_name = 'command:{}'.format(name)
49 f._nvim_rpc_sync = sync
50 f._nvim_bind = True
51 f._nvim_prefix_plugin_path = True
52
53 opts = {}
54
55 if range is not None:
56 opts['range'] = '' if range is True else str(range)
57 elif count is not None:
58 opts['count'] = count
59
60 if bang:
61 opts['bang'] = ''
62
63 if register:
64 opts['register'] = ''
65
66 if nargs:
67 opts['nargs'] = nargs
68
69 if complete:
70 opts['complete'] = complete
71
72 if eval:
73 opts['eval'] = eval
74
75 if not sync and allow_nested:
76 rpc_sync = "urgent"
77 else:
78 rpc_sync = sync
79
80 f._nvim_rpc_spec = {
81 'type': 'command',
82 'name': name,
83 'sync': rpc_sync,
84 'opts': opts
85 }
86 return f
87 return dec
88
89
90 def autocmd(name, pattern='*', sync=False, allow_nested=False, eval=None):
91 """Tag a function or plugin method as a Nvim autocommand handler."""
92 def dec(f):
93 f._nvim_rpc_method_name = 'autocmd:{}:{}'.format(name, pattern)
94 f._nvim_rpc_sync = sync
95 f._nvim_bind = True
96 f._nvim_prefix_plugin_path = True
97
98 opts = {
99 'pattern': pattern
100 }
101
102 if eval:
103 opts['eval'] = eval
104
105 if not sync and allow_nested:
106 rpc_sync = "urgent"
107 else:
108 rpc_sync = sync
109
110 f._nvim_rpc_spec = {
111 'type': 'autocmd',
112 'name': name,
113 'sync': rpc_sync,
114 'opts': opts
115 }
116 return f
117 return dec
118
119
120 def function(name, range=False, sync=False, allow_nested=False, eval=None):
121 """Tag a function or plugin method as a Nvim function handler."""
122 def dec(f):
123 f._nvim_rpc_method_name = 'function:{}'.format(name)
124 f._nvim_rpc_sync = sync
125 f._nvim_bind = True
126 f._nvim_prefix_plugin_path = True
127
128 opts = {}
129
130 if range:
131 opts['range'] = '' if range is True else str(range)
132
133 if eval:
134 opts['eval'] = eval
135
136 if not sync and allow_nested:
137 rpc_sync = "urgent"
138 else:
139 rpc_sync = sync
140
141 f._nvim_rpc_spec = {
142 'type': 'function',
143 'name': name,
144 'sync': rpc_sync,
145 'opts': opts
146 }
147 return f
148 return dec
149
150
151 def shutdown_hook(f):
152 """Tag a function or method as a shutdown hook."""
153 f._nvim_shutdown_hook = True
154 f._nvim_bind = True
155 return f
156
157
158 def decode(mode=unicode_errors_default):
159 """Configure automatic encoding/decoding of strings."""
160 def dec(f):
161 f._nvim_decode = mode
162 return f
163 return dec
164
165
166 def encoding(encoding=True):
167 """DEPRECATED: use neovim.decode()."""
168 if isinstance(encoding, str):
169 encoding = True
170
171 def dec(f):
172 f._nvim_decode = encoding
173 return f
174 return dec
+0
-238
neovim/plugin/host.py less more
0 """Implements a Nvim host for python plugins."""
1 import imp
2 import inspect
3 import logging
4 import os
5 import os.path
6 import re
7 import sys
8 from functools import partial
9 from traceback import format_exc
10
11 from . import script_host
12 from ..api import decode_if_bytes, walk
13 from ..compat import IS_PYTHON3, find_module
14 from ..msgpack_rpc import ErrorResponse
15 from ..util import VERSION, format_exc_skip
16
17 __all__ = ('Host')
18
19 logger = logging.getLogger(__name__)
20 error, debug, info, warn = (logger.error, logger.debug, logger.info,
21 logger.warning,)
22
23 host_method_spec = {"poll": {}, "specs": {"nargs": 1}, "shutdown": {}}
24
25
26 class Host(object):
27
28 """Nvim host for python plugins.
29
30 Takes care of loading/unloading plugins and routing msgpack-rpc
31 requests/notifications to the appropriate handlers.
32 """
33
34 def __init__(self, nvim):
35 """Set handlers for plugin_load/plugin_unload."""
36 self.nvim = nvim
37 self._specs = {}
38 self._loaded = {}
39 self._load_errors = {}
40 self._notification_handlers = {}
41 self._request_handlers = {
42 'poll': lambda: 'ok',
43 'specs': self._on_specs_request,
44 'shutdown': self.shutdown
45 }
46
47 # Decode per default for Python3
48 self._decode_default = IS_PYTHON3
49
50 def _on_async_err(self, msg):
51 self.nvim.err_write(msg, async_=True)
52
53 def start(self, plugins):
54 """Start listening for msgpack-rpc requests and notifications."""
55 self.nvim.run_loop(self._on_request,
56 self._on_notification,
57 lambda: self._load(plugins),
58 err_cb=self._on_async_err)
59
60 def shutdown(self):
61 """Shutdown the host."""
62 self._unload()
63 self.nvim.stop_loop()
64
65 def _wrap_function(self, fn, sync, decode, nvim_bind, name, *args):
66 if decode:
67 args = walk(decode_if_bytes, args, decode)
68 if nvim_bind is not None:
69 args.insert(0, nvim_bind)
70 try:
71 return fn(*args)
72 except Exception:
73 if sync:
74 msg = ("error caught in request handler '{} {}':\n{}"
75 .format(name, args, format_exc_skip(1)))
76 raise ErrorResponse(msg)
77 else:
78 msg = ("error caught in async handler '{} {}'\n{}\n"
79 .format(name, args, format_exc_skip(1)))
80 self._on_async_err(msg + "\n")
81
82 def _on_request(self, name, args):
83 """Handle a msgpack-rpc request."""
84 if IS_PYTHON3:
85 name = decode_if_bytes(name)
86 handler = self._request_handlers.get(name, None)
87 if not handler:
88 msg = self._missing_handler_error(name, 'request')
89 error(msg)
90 raise ErrorResponse(msg)
91
92 debug('calling request handler for "%s", args: "%s"', name, args)
93 rv = handler(*args)
94 debug("request handler for '%s %s' returns: %s", name, args, rv)
95 return rv
96
97 def _on_notification(self, name, args):
98 """Handle a msgpack-rpc notification."""
99 if IS_PYTHON3:
100 name = decode_if_bytes(name)
101 handler = self._notification_handlers.get(name, None)
102 if not handler:
103 msg = self._missing_handler_error(name, 'notification')
104 error(msg)
105 self._on_async_err(msg + "\n")
106 return
107
108 debug('calling notification handler for "%s", args: "%s"', name, args)
109 handler(*args)
110
111 def _missing_handler_error(self, name, kind):
112 msg = 'no {} handler registered for "{}"'.format(kind, name)
113 pathmatch = re.match(r'(.+):[^:]+:[^:]+', name)
114 if pathmatch:
115 loader_error = self._load_errors.get(pathmatch.group(1))
116 if loader_error is not None:
117 msg = msg + "\n" + loader_error
118 return msg
119
120 def _load(self, plugins):
121 has_script = False
122 for path in plugins:
123 err = None
124 if path in self._loaded:
125 error('{} is already loaded'.format(path))
126 continue
127 try:
128 if path == "script_host.py":
129 module = script_host
130 has_script = True
131 else:
132 directory, name = os.path.split(os.path.splitext(path)[0])
133 file, pathname, descr = find_module(name, [directory])
134 module = imp.load_module(name, file, pathname, descr)
135 handlers = []
136 self._discover_classes(module, handlers, path)
137 self._discover_functions(module, handlers, path)
138 if not handlers:
139 error('{} exports no handlers'.format(path))
140 continue
141 self._loaded[path] = {'handlers': handlers, 'module': module}
142 except Exception as e:
143 err = ('Encountered {} loading plugin at {}: {}\n{}'
144 .format(type(e).__name__, path, e, format_exc(5)))
145 error(err)
146 self._load_errors[path] = err
147
148 if len(plugins) == 1 and has_script:
149 kind = "script"
150 else:
151 kind = "rplugin"
152 name = "python{}-{}-host".format(sys.version_info[0], kind)
153 attributes = {"license": "Apache v2",
154 "website": "github.com/neovim/python-client"}
155 self.nvim.api.set_client_info(
156 name, VERSION.__dict__, "host", host_method_spec,
157 attributes, async_=True)
158
159 def _unload(self):
160 for path, plugin in self._loaded.items():
161 handlers = plugin['handlers']
162 for handler in handlers:
163 method_name = handler._nvim_rpc_method_name
164 if hasattr(handler, '_nvim_shutdown_hook'):
165 handler()
166 elif handler._nvim_rpc_sync:
167 del self._request_handlers[method_name]
168 else:
169 del self._notification_handlers[method_name]
170 self._specs = {}
171 self._loaded = {}
172
173 def _discover_classes(self, module, handlers, plugin_path):
174 for _, cls in inspect.getmembers(module, inspect.isclass):
175 if getattr(cls, '_nvim_plugin', False):
176 # create an instance of the plugin and pass the nvim object
177 plugin = cls(self._configure_nvim_for(cls))
178 # discover handlers in the plugin instance
179 self._discover_functions(plugin, handlers, plugin_path)
180
181 def _discover_functions(self, obj, handlers, plugin_path):
182 def predicate(o):
183 return hasattr(o, '_nvim_rpc_method_name')
184
185 specs = []
186 objdecode = getattr(obj, '_nvim_decode', self._decode_default)
187 for _, fn in inspect.getmembers(obj, predicate):
188 sync = fn._nvim_rpc_sync
189 decode = getattr(fn, '_nvim_decode', objdecode)
190 nvim_bind = None
191 if fn._nvim_bind:
192 nvim_bind = self._configure_nvim_for(fn)
193
194 method = fn._nvim_rpc_method_name
195 if fn._nvim_prefix_plugin_path:
196 method = '{}:{}'.format(plugin_path, method)
197
198 fn_wrapped = partial(self._wrap_function, fn,
199 sync, decode, nvim_bind, method)
200 self._copy_attributes(fn, fn_wrapped)
201 # register in the rpc handler dict
202 if sync:
203 if method in self._request_handlers:
204 raise Exception(('Request handler for "{}" is ' +
205 'already registered').format(method))
206 self._request_handlers[method] = fn_wrapped
207 else:
208 if method in self._notification_handlers:
209 raise Exception(('Notification handler for "{}" is ' +
210 'already registered').format(method))
211 self._notification_handlers[method] = fn_wrapped
212 if hasattr(fn, '_nvim_rpc_spec'):
213 specs.append(fn._nvim_rpc_spec)
214 handlers.append(fn_wrapped)
215 if specs:
216 self._specs[plugin_path] = specs
217
218 def _copy_attributes(self, fn, fn2):
219 # Copy _nvim_* attributes from the original function
220 for attr in dir(fn):
221 if attr.startswith('_nvim_'):
222 setattr(fn2, attr, getattr(fn, attr))
223
224 def _on_specs_request(self, path):
225 if IS_PYTHON3:
226 path = decode_if_bytes(path)
227 if path in self._load_errors:
228 self.nvim.out_write(self._load_errors[path] + '\n')
229 return self._specs.get(path, 0)
230
231 def _configure_nvim_for(self, obj):
232 # Configure a nvim instance for obj (checks encoding configuration)
233 nvim = self.nvim
234 decode = getattr(obj, '_nvim_decode', self._decode_default)
235 if decode:
236 nvim = nvim.with_decode(decode)
237 return nvim
+0
-271
neovim/plugin/script_host.py less more
0 """Legacy python/python3-vim emulation."""
1 import imp
2 import io
3 import logging
4 import os
5 import sys
6
7 from .decorators import plugin, rpc_export
8 from ..api import Nvim, walk
9 from ..msgpack_rpc import ErrorResponse
10 from ..util import format_exc_skip
11
12 __all__ = ('ScriptHost',)
13
14
15 logger = logging.getLogger(__name__)
16 debug, info, warn = (logger.debug, logger.info, logger.warn,)
17
18 IS_PYTHON3 = sys.version_info >= (3, 0)
19
20 if IS_PYTHON3:
21 basestring = str
22
23 if sys.version_info >= (3, 4):
24 from importlib.machinery import PathFinder
25
26 PYTHON_SUBDIR = 'python3'
27 else:
28 PYTHON_SUBDIR = 'python2'
29
30
31 @plugin
32 class ScriptHost(object):
33
34 """Provides an environment for running python plugins created for Vim."""
35
36 def __init__(self, nvim):
37 """Initialize the legacy python-vim environment."""
38 self.setup(nvim)
39 # context where all code will run
40 self.module = imp.new_module('__main__')
41 nvim.script_context = self.module
42 # it seems some plugins assume 'sys' is already imported, so do it now
43 exec('import sys', self.module.__dict__)
44 self.legacy_vim = LegacyVim.from_nvim(nvim)
45 sys.modules['vim'] = self.legacy_vim
46
47 # Handle DirChanged. #296
48 nvim.command(
49 'au DirChanged * call rpcnotify({}, "python_chdir", v:event.cwd)'
50 .format(nvim.channel_id), async_=True)
51 # XXX: Avoid race condition.
52 # https://github.com/neovim/python-client/pull/296#issuecomment-358970531
53 # TODO(bfredl): when host initialization has been refactored,
54 # to make __init__ safe again, the following should work:
55 # os.chdir(nvim.eval('getcwd()', async_=False))
56 nvim.command('call rpcnotify({}, "python_chdir", getcwd())'
57 .format(nvim.channel_id), async_=True)
58
59 def setup(self, nvim):
60 """Setup import hooks and global streams.
61
62 This will add import hooks for importing modules from runtime
63 directories and patch the sys module so 'print' calls will be
64 forwarded to Nvim.
65 """
66 self.nvim = nvim
67 info('install import hook/path')
68 self.hook = path_hook(nvim)
69 sys.path_hooks.append(self.hook)
70 nvim.VIM_SPECIAL_PATH = '_vim_path_'
71 sys.path.append(nvim.VIM_SPECIAL_PATH)
72 info('redirect sys.stdout and sys.stderr')
73 self.saved_stdout = sys.stdout
74 self.saved_stderr = sys.stderr
75 sys.stdout = RedirectStream(lambda data: nvim.out_write(data))
76 sys.stderr = RedirectStream(lambda data: nvim.err_write(data))
77
78 def teardown(self):
79 """Restore state modified from the `setup` call."""
80 nvim = self.nvim
81 info('uninstall import hook/path')
82 sys.path.remove(nvim.VIM_SPECIAL_PATH)
83 sys.path_hooks.remove(self.hook)
84 info('restore sys.stdout and sys.stderr')
85 sys.stdout = self.saved_stdout
86 sys.stderr = self.saved_stderr
87
88 @rpc_export('python_execute', sync=True)
89 def python_execute(self, script, range_start, range_stop):
90 """Handle the `python` ex command."""
91 self._set_current_range(range_start, range_stop)
92 try:
93 exec(script, self.module.__dict__)
94 except Exception:
95 raise ErrorResponse(format_exc_skip(1))
96
97 @rpc_export('python_execute_file', sync=True)
98 def python_execute_file(self, file_path, range_start, range_stop):
99 """Handle the `pyfile` ex command."""
100 self._set_current_range(range_start, range_stop)
101 with open(file_path) as f:
102 script = compile(f.read(), file_path, 'exec')
103 try:
104 exec(script, self.module.__dict__)
105 except Exception:
106 raise ErrorResponse(format_exc_skip(1))
107
108 @rpc_export('python_do_range', sync=True)
109 def python_do_range(self, start, stop, code):
110 """Handle the `pydo` ex command."""
111 self._set_current_range(start, stop)
112 nvim = self.nvim
113 start -= 1
114 fname = '_vim_pydo'
115
116 # define the function
117 function_def = 'def %s(line, linenr):\n %s' % (fname, code,)
118 exec(function_def, self.module.__dict__)
119 # get the function
120 function = self.module.__dict__[fname]
121 while start < stop:
122 # Process batches of 5000 to avoid the overhead of making multiple
123 # API calls for every line. Assuming an average line length of 100
124 # bytes, approximately 488 kilobytes will be transferred per batch,
125 # which can be done very quickly in a single API call.
126 sstart = start
127 sstop = min(start + 5000, stop)
128 lines = nvim.current.buffer.api.get_lines(sstart, sstop, True)
129
130 exception = None
131 newlines = []
132 linenr = sstart + 1
133 for i, line in enumerate(lines):
134 result = function(line, linenr)
135 if result is None:
136 # Update earlier lines, and skip to the next
137 if newlines:
138 end = sstart + len(newlines) - 1
139 nvim.current.buffer.api.set_lines(sstart, end,
140 True, newlines)
141 sstart += len(newlines) + 1
142 newlines = []
143 pass
144 elif isinstance(result, basestring):
145 newlines.append(result)
146 else:
147 exception = TypeError('pydo should return a string ' +
148 'or None, found %s instead'
149 % result.__class__.__name__)
150 break
151 linenr += 1
152
153 start = sstop
154 if newlines:
155 end = sstart + len(newlines)
156 nvim.current.buffer.api.set_lines(sstart, end, True, newlines)
157 if exception:
158 raise exception
159 # delete the function
160 del self.module.__dict__[fname]
161
162 @rpc_export('python_eval', sync=True)
163 def python_eval(self, expr):
164 """Handle the `pyeval` vim function."""
165 return eval(expr, self.module.__dict__)
166
167 @rpc_export('python_chdir', sync=False)
168 def python_chdir(self, cwd):
169 """Handle working directory changes."""
170 os.chdir(cwd)
171
172 def _set_current_range(self, start, stop):
173 current = self.legacy_vim.current
174 current.range = current.buffer.range(start, stop)
175
176
177 class RedirectStream(io.IOBase):
178 def __init__(self, redirect_handler):
179 self.redirect_handler = redirect_handler
180
181 def write(self, data):
182 self.redirect_handler(data)
183
184 def writelines(self, seq):
185 self.redirect_handler('\n'.join(seq))
186
187
188 if IS_PYTHON3:
189 num_types = (int, float)
190 else:
191 num_types = (int, long, float) # noqa: F821
192
193
194 def num_to_str(obj):
195 if isinstance(obj, num_types):
196 return str(obj)
197 else:
198 return obj
199
200
201 class LegacyVim(Nvim):
202 def eval(self, expr):
203 obj = self.request("vim_eval", expr)
204 return walk(num_to_str, obj)
205
206
207 # This was copied/adapted from nvim-python help
208 def path_hook(nvim):
209 def _get_paths():
210 if nvim._thread_invalid():
211 return []
212 return discover_runtime_directories(nvim)
213
214 def _find_module(fullname, oldtail, path):
215 idx = oldtail.find('.')
216 if idx > 0:
217 name = oldtail[:idx]
218 tail = oldtail[idx + 1:]
219 fmr = imp.find_module(name, path)
220 module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr)
221 return _find_module(fullname, tail, module.__path__)
222 else:
223 return imp.find_module(fullname, path)
224
225 class VimModuleLoader(object):
226 def __init__(self, module):
227 self.module = module
228
229 def load_module(self, fullname, path=None):
230 # Check sys.modules, required for reload (see PEP302).
231 try:
232 return sys.modules[fullname]
233 except KeyError:
234 pass
235 return imp.load_module(fullname, *self.module)
236
237 class VimPathFinder(object):
238 @staticmethod
239 def find_module(fullname, path=None):
240 """Method for Python 2.7 and 3.3."""
241 try:
242 return VimModuleLoader(
243 _find_module(fullname, fullname, path or _get_paths()))
244 except ImportError:
245 return None
246
247 @staticmethod
248 def find_spec(fullname, target=None):
249 """Method for Python 3.4+."""
250 return PathFinder.find_spec(fullname, _get_paths(), target)
251
252 def hook(path):
253 if path == nvim.VIM_SPECIAL_PATH:
254 return VimPathFinder
255 else:
256 raise ImportError
257
258 return hook
259
260
261 def discover_runtime_directories(nvim):
262 rv = []
263 for rtp in nvim.list_runtime_paths():
264 if not os.path.exists(rtp):
265 continue
266 for subdir in ['pythonx', PYTHON_SUBDIR]:
267 path = os.path.join(rtp, subdir)
268 if os.path.exists(path):
269 rv.append(path)
270 return rv
+0
-35
neovim/util.py less more
0 """Shared utility functions."""
1
2 import sys
3 from traceback import format_exception
4
5
6 def format_exc_skip(skip, limit=None):
7 """Like traceback.format_exc but allow skipping the first frames."""
8 type, val, tb = sys.exc_info()
9 for i in range(skip):
10 tb = tb.tb_next
11 return ('\n'.join(format_exception(type, val, tb, limit))).rstrip()
12
13
14 # Taken from SimpleNamespace in python 3
15 class Version:
16
17 """Helper class for version info."""
18
19 def __init__(self, **kwargs):
20 """Create the Version object."""
21 self.__dict__.update(kwargs)
22
23 def __repr__(self):
24 """Return str representation of the Version."""
25 keys = sorted(self.__dict__)
26 items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys)
27 return "{}({})".format(type(self).__name__, ", ".join(items))
28
29 def __eq__(self, other):
30 """Check if version is same as other."""
31 return self.__dict__ == other.__dict__
32
33
34 VERSION = Version(major=0, minor=3, patch=0, prerelease='')
+0
-13
neovim.egg-info/PKG-INFO less more
0 Metadata-Version: 2.1
1 Name: neovim
2 Version: 0.3.0
3 Summary: Python client to neovim
4 Home-page: http://github.com/neovim/python-client
5 Author: Thiago de Arruda
6 Author-email: tpadilha84@gmail.com
7 License: Apache
8 Download-URL: https://github.com/neovim/python-client/archive/0.3.0.tar.gz
9 Description: UNKNOWN
10 Platform: UNKNOWN
11 Provides-Extra: pyuv
12 Provides-Extra: test
+0
-41
neovim.egg-info/SOURCES.txt less more
0 LICENSE
1 MANIFEST.in
2 README.md
3 setup.cfg
4 setup.py
5 neovim/__init__.py
6 neovim/compat.py
7 neovim/util.py
8 neovim.egg-info/PKG-INFO
9 neovim.egg-info/SOURCES.txt
10 neovim.egg-info/dependency_links.txt
11 neovim.egg-info/not-zip-safe
12 neovim.egg-info/requires.txt
13 neovim.egg-info/top_level.txt
14 neovim/api/__init__.py
15 neovim/api/buffer.py
16 neovim/api/common.py
17 neovim/api/nvim.py
18 neovim/api/tabpage.py
19 neovim/api/window.py
20 neovim/msgpack_rpc/__init__.py
21 neovim/msgpack_rpc/async_session.py
22 neovim/msgpack_rpc/msgpack_stream.py
23 neovim/msgpack_rpc/session.py
24 neovim/msgpack_rpc/event_loop/__init__.py
25 neovim/msgpack_rpc/event_loop/asyncio.py
26 neovim/msgpack_rpc/event_loop/base.py
27 neovim/msgpack_rpc/event_loop/uv.py
28 neovim/plugin/__init__.py
29 neovim/plugin/decorators.py
30 neovim/plugin/host.py
31 neovim/plugin/script_host.py
32 test/test_buffer.py
33 test/test_client_rpc.py
34 test/test_concurrency.py
35 test/test_decorators.py
36 test/test_events.py
37 test/test_host.py
38 test/test_tabpage.py
39 test/test_vim.py
40 test/test_window.py
+0
-1
neovim.egg-info/dependency_links.txt less more
0
+0
-1
neovim.egg-info/not-zip-safe less more
0
+0
-8
neovim.egg-info/requires.txt less more
0 msgpack>=0.5.0
1 greenlet
2
3 [pyuv]
4 pyuv>=1.0.0
5
6 [test]
7 pytest>=3.4.0
+0
-1
neovim.egg-info/top_level.txt less more
0 neovim
0 """Python client for Nvim.
1
2 Client library for talking with Nvim processes via its msgpack-rpc API.
3 """
4 import logging
5 import os
6 import sys
7
8 from .api import Nvim, NvimError
9 from .compat import IS_PYTHON3
10 from .msgpack_rpc import (ErrorResponse, child_session, socket_session,
11 stdio_session, tcp_session)
12 from .plugin import (Host, autocmd, command, decode, encoding, function,
13 plugin, rpc_export, shutdown_hook)
14 from .util import VERSION, Version
15
16
17 __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session',
18 'start_host', 'autocmd', 'command', 'encoding', 'decode',
19 'function', 'plugin', 'rpc_export', 'Host', 'Nvim', 'NvimError',
20 'Version', 'VERSION', 'shutdown_hook', 'attach', 'setup_logging',
21 'ErrorResponse')
22
23
24 def start_host(session=None):
25 """Promote the current process into python plugin host for Nvim.
26
27 Start msgpack-rpc event loop for `session`, listening for Nvim requests
28 and notifications. It registers Nvim commands for loading/unloading
29 python plugins.
30
31 The sys.stdout and sys.stderr streams are redirected to Nvim through
32 `session`. That means print statements probably won't work as expected
33 while this function doesn't return.
34
35 This function is normally called at program startup and could have been
36 defined as a separate executable. It is exposed as a library function for
37 testing purposes only.
38 """
39 plugins = []
40 for arg in sys.argv:
41 _, ext = os.path.splitext(arg)
42 if ext == '.py':
43 plugins.append(arg)
44 elif os.path.isdir(arg):
45 init = os.path.join(arg, '__init__.py')
46 if os.path.isfile(init):
47 plugins.append(arg)
48
49 # This is a special case to support the old workaround of
50 # adding an empty .py file to make a package directory
51 # visible, and it should be removed soon.
52 for path in list(plugins):
53 dup = path + ".py"
54 if os.path.isdir(path) and dup in plugins:
55 plugins.remove(dup)
56
57 # Special case: the legacy scripthost receives a single relative filename
58 # while the rplugin host will receive absolute paths.
59 if plugins == ["script_host.py"]:
60 name = "script"
61 else:
62 name = "rplugin"
63
64 setup_logging(name)
65
66 if not session:
67 session = stdio_session()
68 nvim = Nvim.from_session(session)
69
70 if nvim.version.api_level < 1:
71 sys.stderr.write("This version of pynvim "
72 "requires nvim 0.1.6 or later")
73 sys.exit(1)
74
75 host = Host(nvim)
76 host.start(plugins)
77
78
79 def attach(session_type, address=None, port=None,
80 path=None, argv=None, decode=None):
81 """Provide a nicer interface to create python api sessions.
82
83 Previous machinery to create python api sessions is still there. This only
84 creates a facade function to make things easier for the most usual cases.
85 Thus, instead of:
86 from pynvim import socket_session, Nvim
87 session = tcp_session(address=<address>, port=<port>)
88 nvim = Nvim.from_session(session)
89 You can now do:
90 from pynvim import attach
91 nvim = attach('tcp', address=<address>, port=<port>)
92 And also:
93 nvim = attach('socket', path=<path>)
94 nvim = attach('child', argv=<argv>)
95 nvim = attach('stdio')
96
97 When the session is not needed anymore, it is recommended to explicitly
98 close it:
99 nvim.close()
100 It is also possible to use the session as a context mangager:
101 with attach('socket', path=thepath) as nvim:
102 print(nvim.funcs.getpid())
103 print(nvim.current.line)
104 This will automatically close the session when you're done with it, or
105 when an error occured.
106
107
108 """
109 session = (tcp_session(address, port) if session_type == 'tcp' else
110 socket_session(path) if session_type == 'socket' else
111 stdio_session() if session_type == 'stdio' else
112 child_session(argv) if session_type == 'child' else
113 None)
114
115 if not session:
116 raise Exception('Unknown session type "%s"' % session_type)
117
118 if decode is None:
119 decode = IS_PYTHON3
120
121 return Nvim.from_session(session).with_decode(decode)
122
123
124 def setup_logging(name):
125 """Setup logging according to environment variables."""
126 logger = logging.getLogger(__name__)
127 if 'NVIM_PYTHON_LOG_FILE' in os.environ:
128 prefix = os.environ['NVIM_PYTHON_LOG_FILE'].strip()
129 major_version = sys.version_info[0]
130 logfile = '{}_py{}_{}'.format(prefix, major_version, name)
131 handler = logging.FileHandler(logfile, 'w', 'utf-8')
132 handler.formatter = logging.Formatter(
133 '%(asctime)s [%(levelname)s @ '
134 '%(filename)s:%(funcName)s:%(lineno)s] %(process)s - %(message)s')
135 logging.root.addHandler(handler)
136 level = logging.INFO
137 if 'NVIM_PYTHON_LOG_LEVEL' in os.environ:
138 lvl = getattr(logging,
139 os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(),
140 level)
141 if isinstance(lvl, int):
142 level = lvl
143 logger.setLevel(level)
144
145
146 # Required for python 2.6
147 class NullHandler(logging.Handler):
148 def emit(self, record):
149 pass
150
151
152 if not logging.root.handlers:
153 logging.root.addHandler(NullHandler())
0 """Nvim API subpackage.
1
2 This package implements a higher-level API that wraps msgpack-rpc `Session`
3 instances.
4 """
5
6 from .buffer import Buffer
7 from .common import decode_if_bytes, walk
8 from .nvim import Nvim, NvimError
9 from .tabpage import Tabpage
10 from .window import Window
11
12
13 __all__ = ('Nvim', 'Buffer', 'Window', 'Tabpage', 'NvimError',
14 'decode_if_bytes', 'walk')
0 """API for working with a Nvim Buffer."""
1 from .common import Remote
2 from ..compat import IS_PYTHON3, check_async
3
4
5 __all__ = ('Buffer')
6
7
8 if IS_PYTHON3:
9 basestring = str
10
11
12 def adjust_index(idx, default=None):
13 """Convert from python indexing convention to nvim indexing convention."""
14 if idx is None:
15 return default
16 elif idx < 0:
17 return idx - 1
18 else:
19 return idx
20
21
22 class Buffer(Remote):
23
24 """A remote Nvim buffer."""
25
26 _api_prefix = "nvim_buf_"
27
28 def __len__(self):
29 """Return the number of lines contained in a Buffer."""
30 return self.request('nvim_buf_line_count')
31
32 def __getitem__(self, idx):
33 """Get a buffer line or slice by integer index.
34
35 Indexes may be negative to specify positions from the end of the
36 buffer. For example, -1 is the last line, -2 is the line before that
37 and so on.
38
39 When retrieving slices, omiting indexes(eg: `buffer[:]`) will bring
40 the whole buffer.
41 """
42 if not isinstance(idx, slice):
43 i = adjust_index(idx)
44 return self.request('nvim_buf_get_lines', i, i + 1, True)[0]
45 start = adjust_index(idx.start, 0)
46 end = adjust_index(idx.stop, -1)
47 return self.request('nvim_buf_get_lines', start, end, False)
48
49 def __setitem__(self, idx, item):
50 """Replace a buffer line or slice by integer index.
51
52 Like with `__getitem__`, indexes may be negative.
53
54 When replacing slices, omiting indexes(eg: `buffer[:]`) will replace
55 the whole buffer.
56 """
57 if not isinstance(idx, slice):
58 i = adjust_index(idx)
59 lines = [item] if item is not None else []
60 return self.request('nvim_buf_set_lines', i, i + 1, True, lines)
61 lines = item if item is not None else []
62 start = adjust_index(idx.start, 0)
63 end = adjust_index(idx.stop, -1)
64 return self.request('nvim_buf_set_lines', start, end, False, lines)
65
66 def __iter__(self):
67 """Iterate lines of a buffer.
68
69 This will retrieve all lines locally before iteration starts. This
70 approach is used because for most cases, the gain is much greater by
71 minimizing the number of API calls by transfering all data needed to
72 work.
73 """
74 lines = self[:]
75 for line in lines:
76 yield line
77
78 def __delitem__(self, idx):
79 """Delete line or slice of lines from the buffer.
80
81 This is the same as __setitem__(idx, [])
82 """
83 self.__setitem__(idx, None)
84
85 def append(self, lines, index=-1):
86 """Append a string or list of lines to the buffer."""
87 if isinstance(lines, (basestring, bytes)):
88 lines = [lines]
89 return self.request('nvim_buf_set_lines', index, index, True, lines)
90
91 def mark(self, name):
92 """Return (row, col) tuple for a named mark."""
93 return self.request('nvim_buf_get_mark', name)
94
95 def range(self, start, end):
96 """Return a `Range` object, which represents part of the Buffer."""
97 return Range(self, start, end)
98
99 def add_highlight(self, hl_group, line, col_start=0,
100 col_end=-1, src_id=-1, async_=None,
101 **kwargs):
102 """Add a highlight to the buffer."""
103 async_ = check_async(async_, kwargs, src_id != 0)
104 return self.request('nvim_buf_add_highlight', src_id, hl_group,
105 line, col_start, col_end, async_=async_)
106
107 def clear_highlight(self, src_id, line_start=0, line_end=-1, async_=None,
108 **kwargs):
109 """Clear highlights from the buffer."""
110 async_ = check_async(async_, kwargs, True)
111 self.request('nvim_buf_clear_highlight', src_id,
112 line_start, line_end, async_=async_)
113
114 def update_highlights(self, src_id, hls, clear_start=0, clear_end=-1,
115 clear=False, async_=True):
116 """Add or update highlights in batch to avoid unnecessary redraws.
117
118 A `src_id` must have been allocated prior to use of this function. Use
119 for instance `nvim.new_highlight_source()` to get a src_id for your
120 plugin.
121
122 `hls` should be a list of highlight items. Each item should be a list
123 or tuple on the form `("GroupName", linenr, col_start, col_end)` or
124 `("GroupName", linenr)` to highlight an entire line.
125
126 By default existing highlights are preserved. Specify a line range with
127 clear_start and clear_end to replace highlights in this range. As a
128 shorthand, use clear=True to clear the entire buffer before adding the
129 new highlights.
130 """
131 if clear and clear_start is None:
132 clear_start = 0
133 lua = self._session._get_lua_private()
134 lua.update_highlights(self, src_id, hls, clear_start, clear_end,
135 async_=async_)
136
137 @property
138 def name(self):
139 """Get the buffer name."""
140 return self.request('nvim_buf_get_name')
141
142 @name.setter
143 def name(self, value):
144 """Set the buffer name. BufFilePre/BufFilePost are triggered."""
145 return self.request('nvim_buf_set_name', value)
146
147 @property
148 def valid(self):
149 """Return True if the buffer still exists."""
150 return self.request('nvim_buf_is_valid')
151
152 @property
153 def number(self):
154 """Get the buffer number."""
155 return self.handle
156
157
158 class Range(object):
159 def __init__(self, buffer, start, end):
160 self._buffer = buffer
161 self.start = start - 1
162 self.end = end - 1
163
164 def __len__(self):
165 return self.end - self.start + 1
166
167 def __getitem__(self, idx):
168 if not isinstance(idx, slice):
169 return self._buffer[self._normalize_index(idx)]
170 start = self._normalize_index(idx.start)
171 end = self._normalize_index(idx.stop)
172 if start is None:
173 start = self.start
174 if end is None:
175 end = self.end + 1
176 return self._buffer[start:end]
177
178 def __setitem__(self, idx, lines):
179 if not isinstance(idx, slice):
180 self._buffer[self._normalize_index(idx)] = lines
181 return
182 start = self._normalize_index(idx.start)
183 end = self._normalize_index(idx.stop)
184 if start is None:
185 start = self.start
186 if end is None:
187 end = self.end
188 self._buffer[start:end + 1] = lines
189
190 def __iter__(self):
191 for i in range(self.start, self.end + 1):
192 yield self._buffer[i]
193
194 def append(self, lines, i=None):
195 i = self._normalize_index(i)
196 if i is None:
197 i = self.end + 1
198 self._buffer.append(lines, i)
199
200 def _normalize_index(self, index):
201 if index is None:
202 return None
203 if index < 0:
204 index = self.end
205 else:
206 index += self.start
207 if index > self.end:
208 index = self.end
209 return index
0 """Code shared between the API classes."""
1 import functools
2
3 from msgpack import unpackb
4
5 from ..compat import unicode_errors_default
6
7
8 class Remote(object):
9
10 """Base class for Nvim objects(buffer/window/tabpage).
11
12 Each type of object has it's own specialized class with API wrappers around
13 the msgpack-rpc session. This implements equality which takes the remote
14 object handle into consideration.
15 """
16
17 def __init__(self, session, code_data):
18 """Initialize from session and code_data immutable object.
19
20 The `code_data` contains serialization information required for
21 msgpack-rpc calls. It must be immutable for Buffer equality to work.
22 """
23 self._session = session
24 self.code_data = code_data
25 self.handle = unpackb(code_data[1])
26 self.api = RemoteApi(self, self._api_prefix)
27 self.vars = RemoteMap(self, self._api_prefix + 'get_var',
28 self._api_prefix + 'set_var')
29 self.options = RemoteMap(self, self._api_prefix + 'get_option',
30 self._api_prefix + 'set_option')
31
32 def __repr__(self):
33 """Get text representation of the object."""
34 return '<%s(handle=%r)>' % (
35 self.__class__.__name__,
36 self.handle,
37 )
38
39 def __eq__(self, other):
40 """Return True if `self` and `other` are the same object."""
41 return (hasattr(other, 'code_data')
42 and other.code_data == self.code_data)
43
44 def __hash__(self):
45 """Return hash based on remote object id."""
46 return self.code_data.__hash__()
47
48 def request(self, name, *args, **kwargs):
49 """Wrapper for nvim.request."""
50 return self._session.request(name, self, *args, **kwargs)
51
52
53 class RemoteApi(object):
54
55 """Wrapper to allow api methods to be called like python methods."""
56
57 def __init__(self, obj, api_prefix):
58 """Initialize a RemoteApi with object and api prefix."""
59 self._obj = obj
60 self._api_prefix = api_prefix
61
62 def __getattr__(self, name):
63 """Return wrapper to named api method."""
64 return functools.partial(self._obj.request, self._api_prefix + name)
65
66
67 class RemoteMap(object):
68
69 """Represents a string->object map stored in Nvim.
70
71 This is the dict counterpart to the `RemoteSequence` class, but it is used
72 as a generic way of retrieving values from the various map-like data
73 structures present in Nvim.
74
75 It is used to provide a dict-like API to vim variables and options.
76 """
77
78 def __init__(self, obj, get_method, set_method=None):
79 """Initialize a RemoteMap with session, getter/setter."""
80 self._get = functools.partial(obj.request, get_method)
81 self._set = None
82 if set_method:
83 self._set = functools.partial(obj.request, set_method)
84
85 def __getitem__(self, key):
86 """Return a map value by key."""
87 return self._get(key)
88
89 def __setitem__(self, key, value):
90 """Set a map value by key(if the setter was provided)."""
91 if not self._set:
92 raise TypeError('This dict is read-only')
93 self._set(key, value)
94
95 def __delitem__(self, key):
96 """Delete a map value by associating None with the key."""
97 if not self._set:
98 raise TypeError('This dict is read-only')
99 return self._set(key, None)
100
101 def __contains__(self, key):
102 """Check if key is present in the map."""
103 try:
104 self._get(key)
105 return True
106 except Exception:
107 return False
108
109 def get(self, key, default=None):
110 """Return value for key if present, else a default value."""
111 try:
112 return self._get(key)
113 except Exception:
114 return default
115
116
117 class RemoteSequence(object):
118
119 """Represents a sequence of objects stored in Nvim.
120
121 This class is used to wrap msgapck-rpc functions that work on Nvim
122 sequences(of lines, buffers, windows and tabpages) with an API that
123 is similar to the one provided by the python-vim interface.
124
125 For example, the 'windows' property of the `Nvim` class is a RemoteSequence
126 sequence instance, and the expression `nvim.windows[0]` is translated to
127 session.request('nvim_list_wins')[0].
128
129 One important detail about this class is that all methods will fetch the
130 sequence into a list and perform the necessary manipulation
131 locally(iteration, indexing, counting, etc).
132 """
133
134 def __init__(self, session, method):
135 """Initialize a RemoteSequence with session, method."""
136 self._fetch = functools.partial(session.request, method)
137
138 def __len__(self):
139 """Return the length of the remote sequence."""
140 return len(self._fetch())
141
142 def __getitem__(self, idx):
143 """Return a sequence item by index."""
144 if not isinstance(idx, slice):
145 return self._fetch()[idx]
146 return self._fetch()[idx.start:idx.stop]
147
148 def __iter__(self):
149 """Return an iterator for the sequence."""
150 items = self._fetch()
151 for item in items:
152 yield item
153
154 def __contains__(self, item):
155 """Check if an item is present in the sequence."""
156 return item in self._fetch()
157
158
159 def _identity(obj, session, method, kind):
160 return obj
161
162
163 def decode_if_bytes(obj, mode=True):
164 """Decode obj if it is bytes."""
165 if mode is True:
166 mode = unicode_errors_default
167 if isinstance(obj, bytes):
168 return obj.decode("utf-8", errors=mode)
169 return obj
170
171
172 def walk(fn, obj, *args, **kwargs):
173 """Recursively walk an object graph applying `fn`/`args` to objects."""
174 if type(obj) in [list, tuple]:
175 return list(walk(fn, o, *args) for o in obj)
176 if type(obj) is dict:
177 return dict((walk(fn, k, *args), walk(fn, v, *args)) for k, v in
178 obj.items())
179 return fn(obj, *args, **kwargs)
0 """Main Nvim interface."""
1 import os
2 import sys
3 import threading
4 from functools import partial
5 from traceback import format_stack
6
7 from msgpack import ExtType
8
9 from .buffer import Buffer
10 from .common import (Remote, RemoteApi, RemoteMap, RemoteSequence,
11 decode_if_bytes, walk)
12 from .tabpage import Tabpage
13 from .window import Window
14 from ..compat import IS_PYTHON3
15 from ..util import Version, format_exc_skip
16
17 __all__ = ('Nvim')
18
19
20 os_chdir = os.chdir
21
22 lua_module = """
23 local a = vim.api
24 local function update_highlights(buf, src_id, hls, clear_first, clear_end)
25 if clear_first ~= nil then
26 a.nvim_buf_clear_highlight(buf, src_id, clear_first, clear_end)
27 end
28 for _,hl in pairs(hls) do
29 local group, line, col_start, col_end = unpack(hl)
30 if col_start == nil then
31 col_start = 0
32 end
33 if col_end == nil then
34 col_end = -1
35 end
36 a.nvim_buf_add_highlight(buf, src_id, group, line, col_start, col_end)
37 end
38 end
39
40 local chid = ...
41 local mod = {update_highlights=update_highlights}
42 _G["_pynvim_"..chid] = mod
43 """
44
45
46 class Nvim(object):
47
48 """Class that represents a remote Nvim instance.
49
50 This class is main entry point to Nvim remote API, it is a wrapper
51 around Session instances.
52
53 The constructor of this class must not be called directly. Instead, the
54 `from_session` class method should be used to create the first instance
55 from a raw `Session` instance.
56
57 Subsequent instances for the same session can be created by calling the
58 `with_decode` instance method to change the decoding behavior or
59 `SubClass.from_nvim(nvim)` where `SubClass` is a subclass of `Nvim`, which
60 is useful for having multiple `Nvim` objects that behave differently
61 without one affecting the other.
62
63 When this library is used on python3.4+, asyncio event loop is guaranteed
64 to be used. It is available as the "loop" attribute of this class. Note
65 that asyncio callbacks cannot make blocking requests, which includes
66 accessing state-dependent attributes. They should instead schedule another
67 callback using nvim.async_call, which will not have this restriction.
68 """
69
70 @classmethod
71 def from_session(cls, session):
72 """Create a new Nvim instance for a Session instance.
73
74 This method must be called to create the first Nvim instance, since it
75 queries Nvim metadata for type information and sets a SessionHook for
76 creating specialized objects from Nvim remote handles.
77 """
78 session.error_wrapper = lambda e: NvimError(e[1])
79 channel_id, metadata = session.request(b'vim_get_api_info')
80
81 if IS_PYTHON3:
82 # decode all metadata strings for python3
83 metadata = walk(decode_if_bytes, metadata)
84
85 types = {
86 metadata['types']['Buffer']['id']: Buffer,
87 metadata['types']['Window']['id']: Window,
88 metadata['types']['Tabpage']['id']: Tabpage,
89 }
90
91 return cls(session, channel_id, metadata, types)
92
93 @classmethod
94 def from_nvim(cls, nvim):
95 """Create a new Nvim instance from an existing instance."""
96 return cls(nvim._session, nvim.channel_id, nvim.metadata,
97 nvim.types, nvim._decode, nvim._err_cb)
98
99 def __init__(self, session, channel_id, metadata, types,
100 decode=False, err_cb=None):
101 """Initialize a new Nvim instance. This method is module-private."""
102 self._session = session
103 self.channel_id = channel_id
104 self.metadata = metadata
105 version = metadata.get("version", {"api_level": 0})
106 self.version = Version(**version)
107 self.types = types
108 self.api = RemoteApi(self, 'nvim_')
109 self.vars = RemoteMap(self, 'nvim_get_var', 'nvim_set_var')
110 self.vvars = RemoteMap(self, 'nvim_get_vvar', None)
111 self.options = RemoteMap(self, 'nvim_get_option', 'nvim_set_option')
112 self.buffers = Buffers(self)
113 self.windows = RemoteSequence(self, 'nvim_list_wins')
114 self.tabpages = RemoteSequence(self, 'nvim_list_tabpages')
115 self.current = Current(self)
116 self.session = CompatibilitySession(self)
117 self.funcs = Funcs(self)
118 self.lua = LuaFuncs(self)
119 self.error = NvimError
120 self._decode = decode
121 self._err_cb = err_cb
122
123 # only on python3.4+ we expose asyncio
124 if IS_PYTHON3:
125 self.loop = self._session.loop._loop
126
127 def _from_nvim(self, obj, decode=None):
128 if decode is None:
129 decode = self._decode
130 if type(obj) is ExtType:
131 cls = self.types[obj.code]
132 return cls(self, (obj.code, obj.data))
133 if decode:
134 obj = decode_if_bytes(obj, decode)
135 return obj
136
137 def _to_nvim(self, obj):
138 if isinstance(obj, Remote):
139 return ExtType(*obj.code_data)
140 return obj
141
142 def _get_lua_private(self):
143 if not getattr(self._session, "_has_lua", False):
144 self.exec_lua(lua_module, self.channel_id)
145 self._session._has_lua = True
146 return getattr(self.lua, "_pynvim_{}".format(self.channel_id))
147
148 def request(self, name, *args, **kwargs):
149 r"""Send an API request or notification to nvim.
150
151 It is rarely needed to call this function directly, as most API
152 functions have python wrapper functions. The `api` object can
153 be also be used to call API functions as methods:
154
155 vim.api.err_write('ERROR\n', async_=True)
156 vim.current.buffer.api.get_mark('.')
157
158 is equivalent to
159
160 vim.request('nvim_err_write', 'ERROR\n', async_=True)
161 vim.request('nvim_buf_get_mark', vim.current.buffer, '.')
162
163
164 Normally a blocking request will be sent. If the `async_` flag is
165 present and True, a asynchronous notification is sent instead. This
166 will never block, and the return value or error is ignored.
167 """
168 if (self._session._loop_thread is not None
169 and threading.current_thread() != self._session._loop_thread):
170
171 msg = ("Request from non-main thread.\n"
172 "Requests from different threads should be wrapped "
173 "with nvim.async_call(cb, ...) \n{}\n"
174 .format('\n'.join(format_stack(None, 5)[:-1])))
175
176 self.async_call(self._err_cb, msg)
177 raise NvimError("request from non-main thread")
178
179 decode = kwargs.pop('decode', self._decode)
180 args = walk(self._to_nvim, args)
181 res = self._session.request(name, *args, **kwargs)
182 return walk(self._from_nvim, res, decode=decode)
183
184 def next_message(self):
185 """Block until a message(request or notification) is available.
186
187 If any messages were previously enqueued, return the first in queue.
188 If not, run the event loop until one is received.
189 """
190 msg = self._session.next_message()
191 if msg:
192 return walk(self._from_nvim, msg)
193
194 def run_loop(self, request_cb, notification_cb,
195 setup_cb=None, err_cb=None):
196 """Run the event loop to receive requests and notifications from Nvim.
197
198 This should not be called from a plugin running in the host, which
199 already runs the loop and dispatches events to plugins.
200 """
201 if err_cb is None:
202 err_cb = sys.stderr.write
203 self._err_cb = err_cb
204
205 def filter_request_cb(name, args):
206 name = self._from_nvim(name)
207 args = walk(self._from_nvim, args)
208 try:
209 result = request_cb(name, args)
210 except Exception:
211 msg = ("error caught in request handler '{} {}'\n{}\n\n"
212 .format(name, args, format_exc_skip(1)))
213 self._err_cb(msg)
214 raise
215 return walk(self._to_nvim, result)
216
217 def filter_notification_cb(name, args):
218 name = self._from_nvim(name)
219 args = walk(self._from_nvim, args)
220 try:
221 notification_cb(name, args)
222 except Exception:
223 msg = ("error caught in notification handler '{} {}'\n{}\n\n"
224 .format(name, args, format_exc_skip(1)))
225 self._err_cb(msg)
226 raise
227
228 self._session.run(filter_request_cb, filter_notification_cb, setup_cb)
229
230 def stop_loop(self):
231 """Stop the event loop being started with `run_loop`."""
232 self._session.stop()
233
234 def close(self):
235 """Close the nvim session and release its resources."""
236 self._session.close()
237
238 def __enter__(self):
239 """Enter nvim session as a context manager."""
240 return self
241
242 def __exit__(self, *exc_info):
243 """Exit nvim session as a context manager.
244
245 Closes the event loop.
246 """
247 self.close()
248
249 def with_decode(self, decode=True):
250 """Initialize a new Nvim instance."""
251 return Nvim(self._session, self.channel_id,
252 self.metadata, self.types, decode, self._err_cb)
253
254 def ui_attach(self, width, height, rgb=None, **kwargs):
255 """Register as a remote UI.
256
257 After this method is called, the client will receive redraw
258 notifications.
259 """
260 options = kwargs
261 if rgb is not None:
262 options['rgb'] = rgb
263 return self.request('nvim_ui_attach', width, height, options)
264
265 def ui_detach(self):
266 """Unregister as a remote UI."""
267 return self.request('nvim_ui_detach')
268
269 def ui_try_resize(self, width, height):
270 """Notify nvim that the client window has resized.
271
272 If possible, nvim will send a redraw request to resize.
273 """
274 return self.request('ui_try_resize', width, height)
275
276 def subscribe(self, event):
277 """Subscribe to a Nvim event."""
278 return self.request('nvim_subscribe', event)
279
280 def unsubscribe(self, event):
281 """Unsubscribe to a Nvim event."""
282 return self.request('nvim_unsubscribe', event)
283
284 def command(self, string, **kwargs):
285 """Execute a single ex command."""
286 return self.request('nvim_command', string, **kwargs)
287
288 def command_output(self, string):
289 """Execute a single ex command and return the output."""
290 return self.request('nvim_command_output', string)
291
292 def eval(self, string, **kwargs):
293 """Evaluate a vimscript expression."""
294 return self.request('nvim_eval', string, **kwargs)
295
296 def call(self, name, *args, **kwargs):
297 """Call a vimscript function."""
298 return self.request('nvim_call_function', name, args, **kwargs)
299
300 def exec_lua(self, code, *args, **kwargs):
301 """Execute lua code.
302
303 Additional parameters are available as `...` inside the lua chunk.
304 Only statements are executed. To evaluate an expression, prefix it
305 with `return`: `return my_function(...)`
306
307 There is a shorthand syntax to call lua functions with arguments:
308
309 nvim.lua.func(1,2)
310 nvim.lua.mymod.myfunction(data, async_=True)
311
312 is equivalent to
313
314 nvim.exec_lua("return func(...)", 1, 2)
315 nvim.exec_lua("mymod.myfunction(...)", data, async_=True)
316
317 Note that with `async_=True` there is no return value.
318 """
319 return self.request('nvim_execute_lua', code, args, **kwargs)
320
321 def strwidth(self, string):
322 """Return the number of display cells `string` occupies.
323
324 Tab is counted as one cell.
325 """
326 return self.request('nvim_strwidth', string)
327
328 def list_runtime_paths(self):
329 """Return a list of paths contained in the 'runtimepath' option."""
330 return self.request('nvim_list_runtime_paths')
331
332 def foreach_rtp(self, cb):
333 """Invoke `cb` for each path in 'runtimepath'.
334
335 Call the given callable for each path in 'runtimepath' until either
336 callable returns something but None, the exception is raised or there
337 are no longer paths. If stopped in case callable returned non-None,
338 vim.foreach_rtp function returns the value returned by callable.
339 """
340 for path in self.request('nvim_list_runtime_paths'):
341 try:
342 if cb(path) is not None:
343 break
344 except Exception:
345 break
346
347 def chdir(self, dir_path):
348 """Run os.chdir, then all appropriate vim stuff."""
349 os_chdir(dir_path)
350 return self.request('nvim_set_current_dir', dir_path)
351
352 def feedkeys(self, keys, options='', escape_csi=True):
353 """Push `keys` to Nvim user input buffer.
354
355 Options can be a string with the following character flags:
356 - 'm': Remap keys. This is default.
357 - 'n': Do not remap keys.
358 - 't': Handle keys as if typed; otherwise they are handled as if coming
359 from a mapping. This matters for undo, opening folds, etc.
360 """
361 return self.request('nvim_feedkeys', keys, options, escape_csi)
362
363 def input(self, bytes):
364 """Push `bytes` to Nvim low level input buffer.
365
366 Unlike `feedkeys()`, this uses the lowest level input buffer and the
367 call is not deferred. It returns the number of bytes actually
368 written(which can be less than what was requested if the buffer is
369 full).
370 """
371 return self.request('nvim_input', bytes)
372
373 def replace_termcodes(self, string, from_part=False, do_lt=True,
374 special=True):
375 r"""Replace any terminal code strings by byte sequences.
376
377 The returned sequences are Nvim's internal representation of keys,
378 for example:
379
380 <esc> -> '\x1b'
381 <cr> -> '\r'
382 <c-l> -> '\x0c'
383 <up> -> '\x80ku'
384
385 The returned sequences can be used as input to `feedkeys`.
386 """
387 return self.request('nvim_replace_termcodes', string,
388 from_part, do_lt, special)
389
390 def out_write(self, msg, **kwargs):
391 """Print `msg` as a normal message."""
392 return self.request('nvim_out_write', msg, **kwargs)
393
394 def err_write(self, msg, **kwargs):
395 """Print `msg` as an error message."""
396 if self._thread_invalid():
397 # special case: if a non-main thread writes to stderr
398 # i.e. due to an uncaught exception, pass it through
399 # without raising an additional exception.
400 self.async_call(self.err_write, msg, **kwargs)
401 return
402 return self.request('nvim_err_write', msg, **kwargs)
403
404 def _thread_invalid(self):
405 return (self._session._loop_thread is not None
406 and threading.current_thread() != self._session._loop_thread)
407
408 def quit(self, quit_command='qa!'):
409 """Send a quit command to Nvim.
410
411 By default, the quit command is 'qa!' which will make Nvim quit without
412 saving anything.
413 """
414 try:
415 self.command(quit_command)
416 except IOError:
417 # sending a quit command will raise an IOError because the
418 # connection is closed before a response is received. Safe to
419 # ignore it.
420 pass
421
422 def new_highlight_source(self):
423 """Return new src_id for use with Buffer.add_highlight."""
424 return self.current.buffer.add_highlight("", 0, src_id=0)
425
426 def async_call(self, fn, *args, **kwargs):
427 """Schedule `fn` to be called by the event loop soon.
428
429 This function is thread-safe, and is the only way code not
430 on the main thread could interact with nvim api objects.
431
432 This function can also be called in a synchronous
433 event handler, just before it returns, to defer execution
434 that shouldn't block neovim.
435 """
436 call_point = ''.join(format_stack(None, 5)[:-1])
437
438 def handler():
439 try:
440 fn(*args, **kwargs)
441 except Exception as err:
442 msg = ("error caught while executing async callback:\n"
443 "{!r}\n{}\n \nthe call was requested at\n{}"
444 .format(err, format_exc_skip(1), call_point))
445 self._err_cb(msg)
446 raise
447 self._session.threadsafe_call(handler)
448
449
450 class Buffers(object):
451
452 """Remote NVim buffers.
453
454 Currently the interface for interacting with remote NVim buffers is the
455 `nvim_list_bufs` msgpack-rpc function. Most methods fetch the list of
456 buffers from NVim.
457
458 Conforms to *python-buffers*.
459 """
460
461 def __init__(self, nvim):
462 """Initialize a Buffers object with Nvim object `nvim`."""
463 self._fetch_buffers = nvim.api.list_bufs
464
465 def __len__(self):
466 """Return the count of buffers."""
467 return len(self._fetch_buffers())
468
469 def __getitem__(self, number):
470 """Return the Buffer object matching buffer number `number`."""
471 for b in self._fetch_buffers():
472 if b.number == number:
473 return b
474 raise KeyError(number)
475
476 def __contains__(self, b):
477 """Return whether Buffer `b` is a known valid buffer."""
478 return isinstance(b, Buffer) and b.valid
479
480 def __iter__(self):
481 """Return an iterator over the list of buffers."""
482 return iter(self._fetch_buffers())
483
484
485 class CompatibilitySession(object):
486
487 """Helper class for API compatibility."""
488
489 def __init__(self, nvim):
490 self.threadsafe_call = nvim.async_call
491
492
493 class Current(object):
494
495 """Helper class for emulating vim.current from python-vim."""
496
497 def __init__(self, session):
498 self._session = session
499 self.range = None
500
501 @property
502 def line(self):
503 return self._session.request('nvim_get_current_line')
504
505 @line.setter
506 def line(self, line):
507 return self._session.request('nvim_set_current_line', line)
508
509 @line.deleter
510 def line(self):
511 return self._session.request('nvim_del_current_line')
512
513 @property
514 def buffer(self):
515 return self._session.request('nvim_get_current_buf')
516
517 @buffer.setter
518 def buffer(self, buffer):
519 return self._session.request('nvim_set_current_buf', buffer)
520
521 @property
522 def window(self):
523 return self._session.request('nvim_get_current_win')
524
525 @window.setter
526 def window(self, window):
527 return self._session.request('nvim_set_current_win', window)
528
529 @property
530 def tabpage(self):
531 return self._session.request('nvim_get_current_tabpage')
532
533 @tabpage.setter
534 def tabpage(self, tabpage):
535 return self._session.request('nvim_set_current_tabpage', tabpage)
536
537
538 class Funcs(object):
539
540 """Helper class for functional vimscript interface."""
541
542 def __init__(self, nvim):
543 self._nvim = nvim
544
545 def __getattr__(self, name):
546 return partial(self._nvim.call, name)
547
548
549 class LuaFuncs(object):
550
551 """Wrapper to allow lua functions to be called like python methods."""
552
553 def __init__(self, nvim, name=""):
554 self._nvim = nvim
555 self.name = name
556
557 def __getattr__(self, name):
558 """Return wrapper to named api method."""
559 prefix = self.name + "." if self.name else ""
560 return LuaFuncs(self._nvim, prefix + name)
561
562 def __call__(self, *args, **kwargs):
563 # first new function after keyword rename, be a bit noisy
564 if 'async' in kwargs:
565 raise ValueError('"async" argument is not allowed. '
566 'Use "async_" instead.')
567 async_ = kwargs.get('async_', False)
568 pattern = "return {}(...)" if not async_ else "{}(...)"
569 code = pattern.format(self.name)
570 return self._nvim.exec_lua(code, *args, **kwargs)
571
572
573 class NvimError(Exception):
574 pass
0 """API for working with Nvim tabpages."""
1 from .common import Remote, RemoteSequence
2
3
4 __all__ = ('Tabpage')
5
6
7 class Tabpage(Remote):
8 """A remote Nvim tabpage."""
9
10 _api_prefix = "nvim_tabpage_"
11
12 def __init__(self, *args):
13 """Initialize from session and code_data immutable object.
14
15 The `code_data` contains serialization information required for
16 msgpack-rpc calls. It must be immutable for Buffer equality to work.
17 """
18 super(Tabpage, self).__init__(*args)
19 self.windows = RemoteSequence(self, 'nvim_tabpage_list_wins')
20
21 @property
22 def window(self):
23 """Get the `Window` currently focused on the tabpage."""
24 return self.request('nvim_tabpage_get_win')
25
26 @property
27 def valid(self):
28 """Return True if the tabpage still exists."""
29 return self.request('nvim_tabpage_is_valid')
30
31 @property
32 def number(self):
33 """Get the tabpage number."""
34 return self.request('nvim_tabpage_get_number')
0 """API for working with Nvim windows."""
1 from .common import Remote
2
3
4 __all__ = ('Window')
5
6
7 class Window(Remote):
8
9 """A remote Nvim window."""
10
11 _api_prefix = "nvim_win_"
12
13 @property
14 def buffer(self):
15 """Get the `Buffer` currently being displayed by the window."""
16 return self.request('nvim_win_get_buf')
17
18 @property
19 def cursor(self):
20 """Get the (row, col) tuple with the current cursor position."""
21 return self.request('nvim_win_get_cursor')
22
23 @cursor.setter
24 def cursor(self, pos):
25 """Set the (row, col) tuple as the new cursor position."""
26 return self.request('nvim_win_set_cursor', pos)
27
28 @property
29 def height(self):
30 """Get the window height in rows."""
31 return self.request('nvim_win_get_height')
32
33 @height.setter
34 def height(self, height):
35 """Set the window height in rows."""
36 return self.request('nvim_win_set_height', height)
37
38 @property
39 def width(self):
40 """Get the window width in rows."""
41 return self.request('nvim_win_get_width')
42
43 @width.setter
44 def width(self, width):
45 """Set the window height in rows."""
46 return self.request('nvim_win_set_width', width)
47
48 @property
49 def row(self):
50 """0-indexed, on-screen window position(row) in display cells."""
51 return self.request('nvim_win_get_position')[0]
52
53 @property
54 def col(self):
55 """0-indexed, on-screen window position(col) in display cells."""
56 return self.request('nvim_win_get_position')[1]
57
58 @property
59 def tabpage(self):
60 """Get the `Tabpage` that contains the window."""
61 return self.request('nvim_win_get_tabpage')
62
63 @property
64 def valid(self):
65 """Return True if the window still exists."""
66 return self.request('nvim_win_is_valid')
67
68 @property
69 def number(self):
70 """Get the window number."""
71 return self.request('nvim_win_get_number')
0 """Code for compatibility across Python versions."""
1
2 import sys
3 import warnings
4 from imp import find_module as original_find_module
5
6
7 IS_PYTHON3 = sys.version_info >= (3, 0)
8
9
10 if IS_PYTHON3:
11 def find_module(fullname, path):
12 """Compatibility wrapper for imp.find_module.
13
14 Automatically decodes arguments of find_module, in Python3
15 they must be Unicode
16 """
17 if isinstance(fullname, bytes):
18 fullname = fullname.decode()
19 if isinstance(path, bytes):
20 path = path.decode()
21 elif isinstance(path, list):
22 newpath = []
23 for element in path:
24 if isinstance(element, bytes):
25 newpath.append(element.decode())
26 else:
27 newpath.append(element)
28 path = newpath
29 return original_find_module(fullname, path)
30
31 # There is no 'long' type in Python3 just int
32 long = int
33 unicode_errors_default = 'surrogateescape'
34 else:
35 find_module = original_find_module
36 unicode_errors_default = 'strict'
37
38 NUM_TYPES = (int, long, float)
39
40
41 def check_async(async_, kwargs, default):
42 """Return a value of 'async' in kwargs or default when async_ is None.
43
44 This helper function exists for backward compatibility (See #274).
45 It shows a warning message when 'async' in kwargs is used to note users.
46 """
47 if async_ is not None:
48 return async_
49 elif 'async' in kwargs:
50 warnings.warn(
51 '"async" attribute is deprecated. Use "async_" instead.',
52 DeprecationWarning,
53 )
54 return kwargs.pop('async')
55 else:
56 return default
0 """Msgpack-rpc subpackage.
1
2 This package implements a msgpack-rpc client. While it was designed for
3 handling some Nvim particularities(server->client requests for example), the
4 code here should work with other msgpack-rpc servers.
5 """
6 from .async_session import AsyncSession
7 from .event_loop import EventLoop
8 from .msgpack_stream import MsgpackStream
9 from .session import ErrorResponse, Session
10
11
12 __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session',
13 'ErrorResponse')
14
15
16 def session(transport_type='stdio', *args, **kwargs):
17 loop = EventLoop(transport_type, *args, **kwargs)
18 msgpack_stream = MsgpackStream(loop)
19 async_session = AsyncSession(msgpack_stream)
20 session = Session(async_session)
21 return session
22
23
24 def tcp_session(address, port=7450):
25 """Create a msgpack-rpc session from a tcp address/port."""
26 return session('tcp', address, port)
27
28
29 def socket_session(path):
30 """Create a msgpack-rpc session from a unix domain socket."""
31 return session('socket', path)
32
33
34 def stdio_session():
35 """Create a msgpack-rpc session from stdin/stdout."""
36 return session('stdio')
37
38
39 def child_session(argv):
40 """Create a msgpack-rpc session from a new Nvim instance."""
41 return session('child', argv)
0 """Asynchronous msgpack-rpc handling in the event loop pipeline."""
1 import logging
2 from traceback import format_exc
3
4
5 logger = logging.getLogger(__name__)
6 debug, info, warn = (logger.debug, logger.info, logger.warning,)
7
8
9 class AsyncSession(object):
10
11 """Asynchronous msgpack-rpc layer that wraps a msgpack stream.
12
13 This wraps the msgpack stream interface for reading/writing msgpack
14 documents and exposes an interface for sending and receiving msgpack-rpc
15 requests and notifications.
16 """
17
18 def __init__(self, msgpack_stream):
19 """Wrap `msgpack_stream` on a msgpack-rpc interface."""
20 self._msgpack_stream = msgpack_stream
21 self._next_request_id = 1
22 self._pending_requests = {}
23 self._request_cb = self._notification_cb = None
24 self._handlers = {
25 0: self._on_request,
26 1: self._on_response,
27 2: self._on_notification
28 }
29 self.loop = msgpack_stream.loop
30
31 def threadsafe_call(self, fn):
32 """Wrapper around `MsgpackStream.threadsafe_call`."""
33 self._msgpack_stream.threadsafe_call(fn)
34
35 def request(self, method, args, response_cb):
36 """Send a msgpack-rpc request to Nvim.
37
38 A msgpack-rpc with method `method` and argument `args` is sent to
39 Nvim. The `response_cb` function is called with when the response
40 is available.
41 """
42 request_id = self._next_request_id
43 self._next_request_id = request_id + 1
44 self._msgpack_stream.send([0, request_id, method, args])
45 self._pending_requests[request_id] = response_cb
46
47 def notify(self, method, args):
48 """Send a msgpack-rpc notification to Nvim.
49
50 A msgpack-rpc with method `method` and argument `args` is sent to
51 Nvim. This will have the same effect as a request, but no response
52 will be recieved
53 """
54 self._msgpack_stream.send([2, method, args])
55
56 def run(self, request_cb, notification_cb):
57 """Run the event loop to receive requests and notifications from Nvim.
58
59 While the event loop is running, `request_cb` and `_notification_cb`
60 will be called whenever requests or notifications are respectively
61 available.
62 """
63 self._request_cb = request_cb
64 self._notification_cb = notification_cb
65 self._msgpack_stream.run(self._on_message)
66 self._request_cb = None
67 self._notification_cb = None
68
69 def stop(self):
70 """Stop the event loop."""
71 self._msgpack_stream.stop()
72
73 def close(self):
74 """Close the event loop."""
75 self._msgpack_stream.close()
76
77 def _on_message(self, msg):
78 try:
79 self._handlers.get(msg[0], self._on_invalid_message)(msg)
80 except Exception:
81 err_str = format_exc(5)
82 warn(err_str)
83 self._msgpack_stream.send([1, 0, err_str, None])
84
85 def _on_request(self, msg):
86 # request
87 # - msg[1]: id
88 # - msg[2]: method name
89 # - msg[3]: arguments
90 debug('received request: %s, %s', msg[2], msg[3])
91 self._request_cb(msg[2], msg[3], Response(self._msgpack_stream,
92 msg[1]))
93
94 def _on_response(self, msg):
95 # response to a previous request:
96 # - msg[1]: the id
97 # - msg[2]: error(if any)
98 # - msg[3]: result(if not errored)
99 debug('received response: %s, %s', msg[2], msg[3])
100 self._pending_requests.pop(msg[1])(msg[2], msg[3])
101
102 def _on_notification(self, msg):
103 # notification/event
104 # - msg[1]: event name
105 # - msg[2]: arguments
106 debug('received notification: %s, %s', msg[1], msg[2])
107 self._notification_cb(msg[1], msg[2])
108
109 def _on_invalid_message(self, msg):
110 error = 'Received invalid message %s' % msg
111 warn(error)
112 self._msgpack_stream.send([1, 0, error, None])
113
114
115 class Response(object):
116
117 """Response to a msgpack-rpc request that came from Nvim.
118
119 When Nvim sends a msgpack-rpc request, an instance of this class is
120 created for remembering state required to send a response.
121 """
122
123 def __init__(self, msgpack_stream, request_id):
124 """Initialize the Response instance."""
125 self._msgpack_stream = msgpack_stream
126 self._request_id = request_id
127
128 def send(self, value, error=False):
129 """Send the response.
130
131 If `error` is True, it will be sent as an error.
132 """
133 if error:
134 resp = [1, self._request_id, value, None]
135 else:
136 resp = [1, self._request_id, None, value]
137 debug('sending response to request %d: %s', self._request_id, resp)
138 self._msgpack_stream.send(resp)
0 """Event loop abstraction subpackage.
1
2 Tries to use pyuv as a backend, falling back to the asyncio implementation.
3 """
4
5 from ...compat import IS_PYTHON3
6
7 # on python3 we only support asyncio, as we expose it to plugins
8 if IS_PYTHON3:
9 from .asyncio import AsyncioEventLoop
10 EventLoop = AsyncioEventLoop
11 else:
12 try:
13 # libuv is fully implemented in C, use it when available
14 from .uv import UvEventLoop
15 EventLoop = UvEventLoop
16 except ImportError:
17 # asyncio(trollius on python 2) is pure python and should be more
18 # portable across python implementations
19 from .asyncio import AsyncioEventLoop
20 EventLoop = AsyncioEventLoop
21
22
23 __all__ = ('EventLoop')
0 """Event loop implementation that uses the `asyncio` standard module.
1
2 The `asyncio` module was added to python standard library on 3.4, and it
3 provides a pure python implementation of an event loop library. It is used
4 as a fallback in case pyuv is not available(on python implementations other
5 than CPython).
6
7 Earlier python versions are supported through the `trollius` package, which
8 is a backport of `asyncio` that works on Python 2.6+.
9 """
10 from __future__ import absolute_import
11
12 import logging
13 import os
14 import sys
15 from collections import deque
16
17 try:
18 # For python 3.4+, use the standard library module
19 import asyncio
20 except (ImportError, SyntaxError):
21 # Fallback to trollius
22 import trollius as asyncio
23
24 from .base import BaseEventLoop
25
26 logger = logging.getLogger(__name__)
27 debug, info, warn = (logger.debug, logger.info, logger.warning,)
28
29 loop_cls = asyncio.SelectorEventLoop
30 if os.name == 'nt':
31 from asyncio.windows_utils import PipeHandle
32 import msvcrt
33
34 # On windows use ProactorEventLoop which support pipes and is backed by the
35 # more powerful IOCP facility
36 # NOTE: we override in the stdio case, because it doesn't work.
37 loop_cls = asyncio.ProactorEventLoop
38
39
40 class AsyncioEventLoop(BaseEventLoop, asyncio.Protocol,
41 asyncio.SubprocessProtocol):
42
43 """`BaseEventLoop` subclass that uses `asyncio` as a backend."""
44
45 def connection_made(self, transport):
46 """Used to signal `asyncio.Protocol` of a successful connection."""
47 self._transport = transport
48 self._raw_transport = transport
49 if isinstance(transport, asyncio.SubprocessTransport):
50 self._transport = transport.get_pipe_transport(0)
51
52 def connection_lost(self, exc):
53 """Used to signal `asyncio.Protocol` of a lost connection."""
54 self._on_error(exc.args[0] if exc else 'EOF')
55
56 def data_received(self, data):
57 """Used to signal `asyncio.Protocol` of incoming data."""
58 if self._on_data:
59 self._on_data(data)
60 return
61 self._queued_data.append(data)
62
63 def pipe_connection_lost(self, fd, exc):
64 """Used to signal `asyncio.SubprocessProtocol` of a lost connection."""
65 self._on_error(exc.args[0] if exc else 'EOF')
66
67 def pipe_data_received(self, fd, data):
68 """Used to signal `asyncio.SubprocessProtocol` of incoming data."""
69 if fd == 2: # stderr fd number
70 self._on_stderr(data)
71 elif self._on_data:
72 self._on_data(data)
73 else:
74 self._queued_data.append(data)
75
76 def process_exited(self):
77 """Used to signal `asyncio.SubprocessProtocol` when the child exits."""
78 self._on_error('EOF')
79
80 def _init(self):
81 self._loop = loop_cls()
82 self._queued_data = deque()
83 self._fact = lambda: self
84 self._raw_transport = None
85
86 def _connect_tcp(self, address, port):
87 coroutine = self._loop.create_connection(self._fact, address, port)
88 self._loop.run_until_complete(coroutine)
89
90 def _connect_socket(self, path):
91 if os.name == 'nt':
92 coroutine = self._loop.create_pipe_connection(self._fact, path)
93 else:
94 coroutine = self._loop.create_unix_connection(self._fact, path)
95 self._loop.run_until_complete(coroutine)
96
97 def _connect_stdio(self):
98 if os.name == 'nt':
99 pipe = PipeHandle(msvcrt.get_osfhandle(sys.stdin.fileno()))
100 else:
101 pipe = sys.stdin
102 coroutine = self._loop.connect_read_pipe(self._fact, pipe)
103 self._loop.run_until_complete(coroutine)
104 debug("native stdin connection successful")
105
106 # Make sure subprocesses don't clobber stdout,
107 # send the output to stderr instead.
108 rename_stdout = os.dup(sys.stdout.fileno())
109 os.dup2(sys.stderr.fileno(), sys.stdout.fileno())
110
111 if os.name == 'nt':
112 pipe = PipeHandle(msvcrt.get_osfhandle(rename_stdout))
113 else:
114 pipe = os.fdopen(rename_stdout, 'wb')
115 coroutine = self._loop.connect_write_pipe(self._fact, pipe)
116 self._loop.run_until_complete(coroutine)
117 debug("native stdout connection successful")
118
119 def _connect_child(self, argv):
120 if os.name != 'nt':
121 self._child_watcher = asyncio.get_child_watcher()
122 self._child_watcher.attach_loop(self._loop)
123 coroutine = self._loop.subprocess_exec(self._fact, *argv)
124 self._loop.run_until_complete(coroutine)
125
126 def _start_reading(self):
127 pass
128
129 def _send(self, data):
130 self._transport.write(data)
131
132 def _run(self):
133 while self._queued_data:
134 self._on_data(self._queued_data.popleft())
135 self._loop.run_forever()
136
137 def _stop(self):
138 self._loop.stop()
139
140 def _close(self):
141 if self._raw_transport is not None:
142 self._raw_transport.close()
143 self._loop.close()
144
145 def _threadsafe_call(self, fn):
146 self._loop.call_soon_threadsafe(fn)
147
148 def _setup_signals(self, signals):
149 if os.name == 'nt':
150 # add_signal_handler is not supported in win32
151 self._signals = []
152 return
153
154 self._signals = list(signals)
155 for signum in self._signals:
156 self._loop.add_signal_handler(signum, self._on_signal, signum)
157
158 def _teardown_signals(self):
159 for signum in self._signals:
160 self._loop.remove_signal_handler(signum)
0 """Common code for event loop implementations."""
1 import logging
2 import signal
3 import threading
4
5
6 logger = logging.getLogger(__name__)
7 debug, info, warn = (logger.debug, logger.info, logger.warning,)
8
9
10 # When signals are restored, the event loop library may reset SIGINT to SIG_DFL
11 # which exits the program. To be able to restore the python interpreter to it's
12 # default state, we keep a reference to the default handler
13 default_int_handler = signal.getsignal(signal.SIGINT)
14 main_thread = threading.current_thread()
15
16
17 class BaseEventLoop(object):
18
19 """Abstract base class for all event loops.
20
21 Event loops act as the bottom layer for Nvim sessions created by this
22 library. They hide system/transport details behind a simple interface for
23 reading/writing bytes to the connected Nvim instance.
24
25 This class exposes public methods for interacting with the underlying
26 event loop and delegates implementation-specific work to the following
27 methods, which subclasses are expected to implement:
28
29 - `_init()`: Implementation-specific initialization
30 - `_connect_tcp(address, port)`: connect to Nvim using tcp/ip
31 - `_connect_socket(path)`: Same as tcp, but use a UNIX domain socket or
32 named pipe.
33 - `_connect_stdio()`: Use stdin/stdout as the connection to Nvim
34 - `_connect_child(argv)`: Use the argument vector `argv` to spawn an
35 embedded Nvim that has it's stdin/stdout connected to the event loop.
36 - `_start_reading()`: Called after any of _connect_* methods. Can be used
37 to perform any post-connection setup or validation.
38 - `_send(data)`: Send `data`(byte array) to Nvim. The data is only
39 - `_run()`: Runs the event loop until stopped or the connection is closed.
40 calling the following methods when some event happens:
41 actually sent when the event loop is running.
42 - `_on_data(data)`: When Nvim sends some data.
43 - `_on_signal(signum)`: When a signal is received.
44 - `_on_error(message)`: When a non-recoverable error occurs(eg:
45 connection lost)
46 - `_stop()`: Stop the event loop
47 - `_interrupt(data)`: Like `stop()`, but may be called from other threads
48 this.
49 - `_setup_signals(signals)`: Add implementation-specific listeners for
50 for `signals`, which is a list of OS-specific signal numbers.
51 - `_teardown_signals()`: Removes signal listeners set by `_setup_signals`
52 """
53
54 def __init__(self, transport_type, *args):
55 """Initialize and connect the event loop instance.
56
57 The only arguments are the transport type and transport-specific
58 configuration, like this:
59
60 >>> BaseEventLoop('tcp', '127.0.0.1', 7450)
61 Traceback (most recent call last):
62 ...
63 AttributeError: 'BaseEventLoop' object has no attribute '_init'
64 >>> BaseEventLoop('socket', '/tmp/nvim-socket')
65 Traceback (most recent call last):
66 ...
67 AttributeError: 'BaseEventLoop' object has no attribute '_init'
68 >>> BaseEventLoop('stdio')
69 Traceback (most recent call last):
70 ...
71 AttributeError: 'BaseEventLoop' object has no attribute '_init'
72 >>> BaseEventLoop('child', ['nvim', '--embed', '-u', 'NONE'])
73 Traceback (most recent call last):
74 ...
75 AttributeError: 'BaseEventLoop' object has no attribute '_init'
76
77 This calls the implementation-specific initialization
78 `_init`, one of the `_connect_*` methods(based on `transport_type`)
79 and `_start_reading()`
80 """
81 self._transport_type = transport_type
82 self._signames = dict((k, v) for v, k in signal.__dict__.items()
83 if v.startswith('SIG'))
84 self._on_data = None
85 self._error = None
86 self._init()
87 try:
88 getattr(self, '_connect_{}'.format(transport_type))(*args)
89 except Exception as e:
90 self.close()
91 raise e
92 self._start_reading()
93
94 def connect_tcp(self, address, port):
95 """Connect to tcp/ip `address`:`port`. Delegated to `_connect_tcp`."""
96 info('Connecting to TCP address: %s:%d', address, port)
97 self._connect_tcp(address, port)
98
99 def connect_socket(self, path):
100 """Connect to socket at `path`. Delegated to `_connect_socket`."""
101 info('Connecting to %s', path)
102 self._connect_socket(path)
103
104 def connect_stdio(self):
105 """Connect using stdin/stdout. Delegated to `_connect_stdio`."""
106 info('Preparing stdin/stdout for streaming data')
107 self._connect_stdio()
108
109 def connect_child(self, argv):
110 """Connect a new Nvim instance. Delegated to `_connect_child`."""
111 info('Spawning a new nvim instance')
112 self._connect_child(argv)
113
114 def send(self, data):
115 """Queue `data` for sending to Nvim."""
116 debug("Sending '%s'", data)
117 self._send(data)
118
119 def threadsafe_call(self, fn):
120 """Call a function in the event loop thread.
121
122 This is the only safe way to interact with a session from other
123 threads.
124 """
125 self._threadsafe_call(fn)
126
127 def run(self, data_cb):
128 """Run the event loop."""
129 if self._error:
130 err = self._error
131 if isinstance(self._error, KeyboardInterrupt):
132 # KeyboardInterrupt is not destructive(it may be used in
133 # the REPL).
134 # After throwing KeyboardInterrupt, cleanup the _error field
135 # so the loop may be started again
136 self._error = None
137 raise err
138 self._on_data = data_cb
139 if threading.current_thread() == main_thread:
140 self._setup_signals([signal.SIGINT, signal.SIGTERM])
141 debug('Entering event loop')
142 self._run()
143 debug('Exited event loop')
144 if threading.current_thread() == main_thread:
145 self._teardown_signals()
146 signal.signal(signal.SIGINT, default_int_handler)
147 self._on_data = None
148
149 def stop(self):
150 """Stop the event loop."""
151 self._stop()
152 debug('Stopped event loop')
153
154 def close(self):
155 """Stop the event loop."""
156 self._close()
157 debug('Closed event loop')
158
159 def _on_signal(self, signum):
160 msg = 'Received {}'.format(self._signames[signum])
161 debug(msg)
162 if signum == signal.SIGINT and self._transport_type == 'stdio':
163 # When the transport is stdio, we are probably running as a Nvim
164 # child process. In that case, we don't want to be killed by
165 # ctrl+C
166 return
167 cls = Exception
168 if signum == signal.SIGINT:
169 cls = KeyboardInterrupt
170 self._error = cls(msg)
171 self.stop()
172
173 def _on_error(self, error):
174 debug(error)
175 self._error = IOError(error)
176 self.stop()
177
178 def _on_interrupt(self):
179 self.stop()
0 """Event loop implementation that uses pyuv(libuv-python bindings)."""
1 import sys
2 from collections import deque
3
4 import pyuv
5
6 from .base import BaseEventLoop
7
8
9 class UvEventLoop(BaseEventLoop):
10
11 """`BaseEventLoop` subclass that uses `pvuv` as a backend."""
12
13 def _init(self):
14 self._loop = pyuv.Loop()
15 self._async = pyuv.Async(self._loop, self._on_async)
16 self._connection_error = None
17 self._error_stream = None
18 self._callbacks = deque()
19
20 def _on_connect(self, stream, error):
21 self.stop()
22 if error:
23 msg = 'Cannot connect to {}: {}'.format(
24 self._connect_address, pyuv.errno.strerror(error))
25 self._connection_error = IOError(msg)
26 return
27 self._read_stream = self._write_stream = stream
28
29 def _on_read(self, handle, data, error):
30 if error or not data:
31 msg = pyuv.errno.strerror(error) if error else 'EOF'
32 self._on_error(msg)
33 return
34 if handle == self._error_stream:
35 return
36 self._on_data(data)
37
38 def _on_write(self, handle, error):
39 if error:
40 msg = pyuv.errno.strerror(error)
41 self._on_error(msg)
42
43 def _on_exit(self, handle, exit_status, term_signal):
44 self._on_error('EOF')
45
46 def _disconnected(self, *args):
47 raise IOError('Not connected to Nvim')
48
49 def _connect_tcp(self, address, port):
50 stream = pyuv.TCP(self._loop)
51 self._connect_address = '{}:{}'.format(address, port)
52 stream.connect((address, port), self._on_connect)
53
54 def _connect_socket(self, path):
55 stream = pyuv.Pipe(self._loop)
56 self._connect_address = path
57 stream.connect(path, self._on_connect)
58
59 def _connect_stdio(self):
60 self._read_stream = pyuv.Pipe(self._loop)
61 self._read_stream.open(sys.stdin.fileno())
62 self._write_stream = pyuv.Pipe(self._loop)
63 self._write_stream.open(sys.stdout.fileno())
64
65 def _connect_child(self, argv):
66 self._write_stream = pyuv.Pipe(self._loop)
67 self._read_stream = pyuv.Pipe(self._loop)
68 self._error_stream = pyuv.Pipe(self._loop)
69 stdin = pyuv.StdIO(self._write_stream,
70 flags=pyuv.UV_CREATE_PIPE + pyuv.UV_READABLE_PIPE)
71 stdout = pyuv.StdIO(self._read_stream,
72 flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE)
73 stderr = pyuv.StdIO(self._error_stream,
74 flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE)
75 pyuv.Process.spawn(self._loop,
76 args=argv,
77 exit_callback=self._on_exit,
78 flags=pyuv.UV_PROCESS_WINDOWS_HIDE,
79 stdio=(stdin, stdout, stderr,))
80 self._error_stream.start_read(self._on_read)
81
82 def _start_reading(self):
83 if self._transport_type in ['tcp', 'socket']:
84 self._loop.run()
85 if self._connection_error:
86 self.run = self.send = self._disconnected
87 raise self._connection_error
88 self._read_stream.start_read(self._on_read)
89
90 def _send(self, data):
91 self._write_stream.write(data, self._on_write)
92
93 def _run(self):
94 self._loop.run(pyuv.UV_RUN_DEFAULT)
95
96 def _stop(self):
97 self._loop.stop()
98
99 def _close(self):
100 pass
101
102 def _threadsafe_call(self, fn):
103 self._callbacks.append(fn)
104 self._async.send()
105
106 def _on_async(self, handle):
107 while self._callbacks:
108 self._callbacks.popleft()()
109
110 def _setup_signals(self, signals):
111 self._signal_handles = []
112
113 def handler(h, signum):
114 self._on_signal(signum)
115
116 for signum in signals:
117 handle = pyuv.Signal(self._loop)
118 handle.start(handler, signum)
119 self._signal_handles.append(handle)
120
121 def _teardown_signals(self):
122 for handle in self._signal_handles:
123 handle.stop()
0 """Msgpack handling in the event loop pipeline."""
1 import logging
2
3 from msgpack import Packer, Unpacker
4
5 from ..compat import unicode_errors_default
6
7 logger = logging.getLogger(__name__)
8 debug, info, warn = (logger.debug, logger.info, logger.warning,)
9
10
11 class MsgpackStream(object):
12
13 """Two-way msgpack stream that wraps a event loop byte stream.
14
15 This wraps the event loop interface for reading/writing bytes and
16 exposes an interface for reading/writing msgpack documents.
17 """
18
19 def __init__(self, event_loop):
20 """Wrap `event_loop` on a msgpack-aware interface."""
21 self.loop = event_loop
22 self._packer = Packer(unicode_errors=unicode_errors_default)
23 self._unpacker = Unpacker()
24 self._message_cb = None
25
26 def threadsafe_call(self, fn):
27 """Wrapper around `BaseEventLoop.threadsafe_call`."""
28 self.loop.threadsafe_call(fn)
29
30 def send(self, msg):
31 """Queue `msg` for sending to Nvim."""
32 debug('sent %s', msg)
33 self.loop.send(self._packer.pack(msg))
34
35 def run(self, message_cb):
36 """Run the event loop to receive messages from Nvim.
37
38 While the event loop is running, `message_cb` will be called whenever
39 a message has been successfully parsed from the input stream.
40 """
41 self._message_cb = message_cb
42 self.loop.run(self._on_data)
43 self._message_cb = None
44
45 def stop(self):
46 """Stop the event loop."""
47 self.loop.stop()
48
49 def close(self):
50 """Close the event loop."""
51 self.loop.close()
52
53 def _on_data(self, data):
54 self._unpacker.feed(data)
55 while True:
56 try:
57 debug('waiting for message...')
58 msg = next(self._unpacker)
59 debug('received message: %s', msg)
60 self._message_cb(msg)
61 except StopIteration:
62 debug('unpacker needs more data...')
63 break
0 """Synchronous msgpack-rpc session layer."""
1 import logging
2 import threading
3 from collections import deque
4 from traceback import format_exc
5
6 import greenlet
7
8 from ..compat import check_async
9
10 logger = logging.getLogger(__name__)
11 error, debug, info, warn = (logger.error, logger.debug, logger.info,
12 logger.warning,)
13
14
15 class Session(object):
16
17 """Msgpack-rpc session layer that uses coroutines for a synchronous API.
18
19 This class provides the public msgpack-rpc API required by this library.
20 It uses the greenlet module to handle requests and notifications coming
21 from Nvim with a synchronous API.
22 """
23
24 def __init__(self, async_session):
25 """Wrap `async_session` on a synchronous msgpack-rpc interface."""
26 self._async_session = async_session
27 self._request_cb = self._notification_cb = None
28 self._pending_messages = deque()
29 self._is_running = False
30 self._setup_exception = None
31 self.loop = async_session.loop
32 self._loop_thread = None
33
34 def threadsafe_call(self, fn, *args, **kwargs):
35 """Wrapper around `AsyncSession.threadsafe_call`."""
36 def handler():
37 try:
38 fn(*args, **kwargs)
39 except Exception:
40 warn("error caught while excecuting async callback\n%s\n",
41 format_exc())
42
43 def greenlet_wrapper():
44 gr = greenlet.greenlet(handler)
45 gr.switch()
46
47 self._async_session.threadsafe_call(greenlet_wrapper)
48
49 def next_message(self):
50 """Block until a message(request or notification) is available.
51
52 If any messages were previously enqueued, return the first in queue.
53 If not, run the event loop until one is received.
54 """
55 if self._is_running:
56 raise Exception('Event loop already running')
57 if self._pending_messages:
58 return self._pending_messages.popleft()
59 self._async_session.run(self._enqueue_request_and_stop,
60 self._enqueue_notification_and_stop)
61 if self._pending_messages:
62 return self._pending_messages.popleft()
63
64 def request(self, method, *args, **kwargs):
65 """Send a msgpack-rpc request and block until as response is received.
66
67 If the event loop is running, this method must have been called by a
68 request or notification handler running on a greenlet. In that case,
69 send the quest and yield to the parent greenlet until a response is
70 available.
71
72 When the event loop is not running, it will perform a blocking request
73 like this:
74 - Send the request
75 - Run the loop until the response is available
76 - Put requests/notifications received while waiting into a queue
77
78 If the `async_` flag is present and True, a asynchronous notification
79 is sent instead. This will never block, and the return value or error
80 is ignored.
81 """
82 async_ = check_async(kwargs.pop('async_', None), kwargs, False)
83 if async_:
84 self._async_session.notify(method, args)
85 return
86
87 if kwargs:
88 raise ValueError("request got unsupported keyword argument(s): {}"
89 .format(', '.join(kwargs.keys())))
90
91 if self._is_running:
92 v = self._yielding_request(method, args)
93 else:
94 v = self._blocking_request(method, args)
95 if not v:
96 # EOF
97 raise IOError('EOF')
98 err, rv = v
99 if err:
100 info("'Received error: %s", err)
101 raise self.error_wrapper(err)
102 return rv
103
104 def run(self, request_cb, notification_cb, setup_cb=None):
105 """Run the event loop to receive requests and notifications from Nvim.
106
107 Like `AsyncSession.run()`, but `request_cb` and `notification_cb` are
108 inside greenlets.
109 """
110 self._request_cb = request_cb
111 self._notification_cb = notification_cb
112 self._is_running = True
113 self._setup_exception = None
114 self._loop_thread = threading.current_thread()
115
116 def on_setup():
117 try:
118 setup_cb()
119 except Exception as e:
120 self._setup_exception = e
121 self.stop()
122
123 if setup_cb:
124 # Create a new greenlet to handle the setup function
125 gr = greenlet.greenlet(on_setup)
126 gr.switch()
127
128 if self._setup_exception:
129 error('Setup error: {}'.format(self._setup_exception))
130 raise self._setup_exception
131
132 # Process all pending requests and notifications
133 while self._pending_messages:
134 msg = self._pending_messages.popleft()
135 getattr(self, '_on_{}'.format(msg[0]))(*msg[1:])
136 self._async_session.run(self._on_request, self._on_notification)
137 self._is_running = False
138 self._request_cb = None
139 self._notification_cb = None
140 self._loop_thread = None
141
142 if self._setup_exception:
143 raise self._setup_exception
144
145 def stop(self):
146 """Stop the event loop."""
147 self._async_session.stop()
148
149 def close(self):
150 """Close the event loop."""
151 self._async_session.close()
152
153 def _yielding_request(self, method, args):
154 gr = greenlet.getcurrent()
155 parent = gr.parent
156
157 def response_cb(err, rv):
158 debug('response is available for greenlet %s, switching back', gr)
159 gr.switch(err, rv)
160
161 self._async_session.request(method, args, response_cb)
162 debug('yielding from greenlet %s to wait for response', gr)
163 return parent.switch()
164
165 def _blocking_request(self, method, args):
166 result = []
167
168 def response_cb(err, rv):
169 result.extend([err, rv])
170 self.stop()
171
172 self._async_session.request(method, args, response_cb)
173 self._async_session.run(self._enqueue_request,
174 self._enqueue_notification)
175 return result
176
177 def _enqueue_request_and_stop(self, name, args, response):
178 self._enqueue_request(name, args, response)
179 self.stop()
180
181 def _enqueue_notification_and_stop(self, name, args):
182 self._enqueue_notification(name, args)
183 self.stop()
184
185 def _enqueue_request(self, name, args, response):
186 self._pending_messages.append(('request', name, args, response,))
187
188 def _enqueue_notification(self, name, args):
189 self._pending_messages.append(('notification', name, args,))
190
191 def _on_request(self, name, args, response):
192 def handler():
193 try:
194 rv = self._request_cb(name, args)
195 debug('greenlet %s finished executing, '
196 + 'sending %s as response', gr, rv)
197 response.send(rv)
198 except ErrorResponse as err:
199 warn("error response from request '%s %s': %s", name,
200 args, format_exc())
201 response.send(err.args[0], error=True)
202 except Exception as err:
203 warn("error caught while processing request '%s %s': %s", name,
204 args, format_exc())
205 response.send(repr(err) + "\n" + format_exc(5), error=True)
206 debug('greenlet %s is now dying...', gr)
207
208 # Create a new greenlet to handle the request
209 gr = greenlet.greenlet(handler)
210 debug('received rpc request, greenlet %s will handle it', gr)
211 gr.switch()
212
213 def _on_notification(self, name, args):
214 def handler():
215 try:
216 self._notification_cb(name, args)
217 debug('greenlet %s finished executing', gr)
218 except Exception:
219 warn("error caught while processing notification '%s %s': %s",
220 name, args, format_exc())
221
222 debug('greenlet %s is now dying...', gr)
223
224 gr = greenlet.greenlet(handler)
225 debug('received rpc notification, greenlet %s will handle it', gr)
226 gr.switch()
227
228
229 class ErrorResponse(BaseException):
230
231 """Raise this in a request handler to respond with a given error message.
232
233 Unlike when other exceptions are caught, this gives full control off the
234 error response sent. When "ErrorResponse(msg)" is caught "msg" will be
235 sent verbatim as the error response.No traceback will be appended.
236 """
237
238 pass
0 """Nvim plugin/host subpackage."""
1
2 from .decorators import (autocmd, command, decode, encoding, function,
3 plugin, rpc_export, shutdown_hook)
4 from .host import Host
5
6
7 __all__ = ('Host', 'plugin', 'rpc_export', 'command', 'autocmd',
8 'function', 'encoding', 'decode', 'shutdown_hook')
0 """Decorators used by python host plugin system."""
1
2 import inspect
3 import logging
4
5 from ..compat import IS_PYTHON3, unicode_errors_default
6
7 logger = logging.getLogger(__name__)
8 debug, info, warn = (logger.debug, logger.info, logger.warning,)
9 __all__ = ('plugin', 'rpc_export', 'command', 'autocmd', 'function',
10 'encoding', 'decode', 'shutdown_hook')
11
12
13 def plugin(cls):
14 """Tag a class as a plugin.
15
16 This decorator is required to make the class methods discoverable by the
17 plugin_load method of the host.
18 """
19 cls._nvim_plugin = True
20 # the _nvim_bind attribute is set to True by default, meaning that
21 # decorated functions have a bound Nvim instance as first argument.
22 # For methods in a plugin-decorated class this is not required, because
23 # the class initializer will already receive the nvim object.
24 predicate = lambda fn: hasattr(fn, '_nvim_bind')
25 for _, fn in inspect.getmembers(cls, predicate):
26 if IS_PYTHON3:
27 fn._nvim_bind = False
28 else:
29 fn.im_func._nvim_bind = False
30 return cls
31
32
33 def rpc_export(rpc_method_name, sync=False):
34 """Export a function or plugin method as a msgpack-rpc request handler."""
35 def dec(f):
36 f._nvim_rpc_method_name = rpc_method_name
37 f._nvim_rpc_sync = sync
38 f._nvim_bind = True
39 f._nvim_prefix_plugin_path = False
40 return f
41 return dec
42
43
44 def command(name, nargs=0, complete=None, range=None, count=None, bang=False,
45 register=False, sync=False, allow_nested=False, eval=None):
46 """Tag a function or plugin method as a Nvim command handler."""
47 def dec(f):
48 f._nvim_rpc_method_name = 'command:{}'.format(name)
49 f._nvim_rpc_sync = sync
50 f._nvim_bind = True
51 f._nvim_prefix_plugin_path = True
52
53 opts = {}
54
55 if range is not None:
56 opts['range'] = '' if range is True else str(range)
57 elif count is not None:
58 opts['count'] = count
59
60 if bang:
61 opts['bang'] = ''
62
63 if register:
64 opts['register'] = ''
65
66 if nargs:
67 opts['nargs'] = nargs
68
69 if complete:
70 opts['complete'] = complete
71
72 if eval:
73 opts['eval'] = eval
74
75 if not sync and allow_nested:
76 rpc_sync = "urgent"
77 else:
78 rpc_sync = sync
79
80 f._nvim_rpc_spec = {
81 'type': 'command',
82 'name': name,
83 'sync': rpc_sync,
84 'opts': opts
85 }
86 return f
87 return dec
88
89
90 def autocmd(name, pattern='*', sync=False, allow_nested=False, eval=None):
91 """Tag a function or plugin method as a Nvim autocommand handler."""
92 def dec(f):
93 f._nvim_rpc_method_name = 'autocmd:{}:{}'.format(name, pattern)
94 f._nvim_rpc_sync = sync
95 f._nvim_bind = True
96 f._nvim_prefix_plugin_path = True
97
98 opts = {
99 'pattern': pattern
100 }
101
102 if eval:
103 opts['eval'] = eval
104
105 if not sync and allow_nested:
106 rpc_sync = "urgent"
107 else:
108 rpc_sync = sync
109
110 f._nvim_rpc_spec = {
111 'type': 'autocmd',
112 'name': name,
113 'sync': rpc_sync,
114 'opts': opts
115 }
116 return f
117 return dec
118
119
120 def function(name, range=False, sync=False, allow_nested=False, eval=None):
121 """Tag a function or plugin method as a Nvim function handler."""
122 def dec(f):
123 f._nvim_rpc_method_name = 'function:{}'.format(name)
124 f._nvim_rpc_sync = sync
125 f._nvim_bind = True
126 f._nvim_prefix_plugin_path = True
127
128 opts = {}
129
130 if range:
131 opts['range'] = '' if range is True else str(range)
132
133 if eval:
134 opts['eval'] = eval
135
136 if not sync and allow_nested:
137 rpc_sync = "urgent"
138 else:
139 rpc_sync = sync
140
141 f._nvim_rpc_spec = {
142 'type': 'function',
143 'name': name,
144 'sync': rpc_sync,
145 'opts': opts
146 }
147 return f
148 return dec
149
150
151 def shutdown_hook(f):
152 """Tag a function or method as a shutdown hook."""
153 f._nvim_shutdown_hook = True
154 f._nvim_bind = True
155 return f
156
157
158 def decode(mode=unicode_errors_default):
159 """Configure automatic encoding/decoding of strings."""
160 def dec(f):
161 f._nvim_decode = mode
162 return f
163 return dec
164
165
166 def encoding(encoding=True):
167 """DEPRECATED: use pynvim.decode()."""
168 if isinstance(encoding, str):
169 encoding = True
170
171 def dec(f):
172 f._nvim_decode = encoding
173 return f
174 return dec
0 """Implements a Nvim host for python plugins."""
1 import imp
2 import inspect
3 import logging
4 import os
5 import os.path
6 import re
7 import sys
8 from functools import partial
9 from traceback import format_exc
10
11 from . import script_host
12 from ..api import decode_if_bytes, walk
13 from ..compat import IS_PYTHON3, find_module
14 from ..msgpack_rpc import ErrorResponse
15 from ..util import VERSION, format_exc_skip
16
17 __all__ = ('Host')
18
19 logger = logging.getLogger(__name__)
20 error, debug, info, warn = (logger.error, logger.debug, logger.info,
21 logger.warning,)
22
23 host_method_spec = {"poll": {}, "specs": {"nargs": 1}, "shutdown": {}}
24
25
26 class Host(object):
27
28 """Nvim host for python plugins.
29
30 Takes care of loading/unloading plugins and routing msgpack-rpc
31 requests/notifications to the appropriate handlers.
32 """
33
34 def __init__(self, nvim):
35 """Set handlers for plugin_load/plugin_unload."""
36 self.nvim = nvim
37 self._specs = {}
38 self._loaded = {}
39 self._load_errors = {}
40 self._notification_handlers = {
41 'nvim_error_event': self._on_error_event
42 }
43 self._request_handlers = {
44 'poll': lambda: 'ok',
45 'specs': self._on_specs_request,
46 'shutdown': self.shutdown
47 }
48
49 # Decode per default for Python3
50 self._decode_default = IS_PYTHON3
51
52 def _on_async_err(self, msg):
53 # uncaught python exception
54 self.nvim.err_write(msg, async_=True)
55
56 def _on_error_event(self, kind, msg):
57 # error from nvim due to async request
58 # like nvim.command(..., async_=True)
59 errmsg = "{}: Async request caused an error:\n{}\n".format(
60 self.name, decode_if_bytes(msg))
61 self.nvim.err_write(errmsg, async_=True)
62
63 def start(self, plugins):
64 """Start listening for msgpack-rpc requests and notifications."""
65 self.nvim.run_loop(self._on_request,
66 self._on_notification,
67 lambda: self._load(plugins),
68 err_cb=self._on_async_err)
69
70 def shutdown(self):
71 """Shutdown the host."""
72 self._unload()
73 self.nvim.stop_loop()
74
75 def _wrap_function(self, fn, sync, decode, nvim_bind, name, *args):
76 if decode:
77 args = walk(decode_if_bytes, args, decode)
78 if nvim_bind is not None:
79 args.insert(0, nvim_bind)
80 try:
81 return fn(*args)
82 except Exception:
83 if sync:
84 msg = ("error caught in request handler '{} {}':\n{}"
85 .format(name, args, format_exc_skip(1)))
86 raise ErrorResponse(msg)
87 else:
88 msg = ("error caught in async handler '{} {}'\n{}\n"
89 .format(name, args, format_exc_skip(1)))
90 self._on_async_err(msg + "\n")
91
92 def _on_request(self, name, args):
93 """Handle a msgpack-rpc request."""
94 if IS_PYTHON3:
95 name = decode_if_bytes(name)
96 handler = self._request_handlers.get(name, None)
97 if not handler:
98 msg = self._missing_handler_error(name, 'request')
99 error(msg)
100 raise ErrorResponse(msg)
101
102 debug('calling request handler for "%s", args: "%s"', name, args)
103 rv = handler(*args)
104 debug("request handler for '%s %s' returns: %s", name, args, rv)
105 return rv
106
107 def _on_notification(self, name, args):
108 """Handle a msgpack-rpc notification."""
109 if IS_PYTHON3:
110 name = decode_if_bytes(name)
111 handler = self._notification_handlers.get(name, None)
112 if not handler:
113 msg = self._missing_handler_error(name, 'notification')
114 error(msg)
115 self._on_async_err(msg + "\n")
116 return
117
118 debug('calling notification handler for "%s", args: "%s"', name, args)
119 handler(*args)
120
121 def _missing_handler_error(self, name, kind):
122 msg = 'no {} handler registered for "{}"'.format(kind, name)
123 pathmatch = re.match(r'(.+):[^:]+:[^:]+', name)
124 if pathmatch:
125 loader_error = self._load_errors.get(pathmatch.group(1))
126 if loader_error is not None:
127 msg = msg + "\n" + loader_error
128 return msg
129
130 def _load(self, plugins):
131 has_script = False
132 for path in plugins:
133 err = None
134 if path in self._loaded:
135 error('{} is already loaded'.format(path))
136 continue
137 try:
138 if path == "script_host.py":
139 module = script_host
140 has_script = True
141 else:
142 directory, name = os.path.split(os.path.splitext(path)[0])
143 file, pathname, descr = find_module(name, [directory])
144 module = imp.load_module(name, file, pathname, descr)
145 handlers = []
146 self._discover_classes(module, handlers, path)
147 self._discover_functions(module, handlers, path)
148 if not handlers:
149 error('{} exports no handlers'.format(path))
150 continue
151 self._loaded[path] = {'handlers': handlers, 'module': module}
152 except Exception as e:
153 err = ('Encountered {} loading plugin at {}: {}\n{}'
154 .format(type(e).__name__, path, e, format_exc(5)))
155 error(err)
156 self._load_errors[path] = err
157
158 if len(plugins) == 1 and has_script:
159 kind = "script"
160 else:
161 kind = "rplugin"
162 name = "python{}-{}-host".format(sys.version_info[0], kind)
163 attributes = {"license": "Apache v2",
164 "website": "github.com/neovim/pynvim"}
165 self.name = name
166 self.nvim.api.set_client_info(
167 name, VERSION.__dict__, "host", host_method_spec,
168 attributes, async_=True)
169
170 def _unload(self):
171 for path, plugin in self._loaded.items():
172 handlers = plugin['handlers']
173 for handler in handlers:
174 method_name = handler._nvim_rpc_method_name
175 if hasattr(handler, '_nvim_shutdown_hook'):
176 handler()
177 elif handler._nvim_rpc_sync:
178 del self._request_handlers[method_name]
179 else:
180 del self._notification_handlers[method_name]
181 self._specs = {}
182 self._loaded = {}
183
184 def _discover_classes(self, module, handlers, plugin_path):
185 for _, cls in inspect.getmembers(module, inspect.isclass):
186 if getattr(cls, '_nvim_plugin', False):
187 # create an instance of the plugin and pass the nvim object
188 plugin = cls(self._configure_nvim_for(cls))
189 # discover handlers in the plugin instance
190 self._discover_functions(plugin, handlers, plugin_path)
191
192 def _discover_functions(self, obj, handlers, plugin_path):
193 def predicate(o):
194 return hasattr(o, '_nvim_rpc_method_name')
195
196 specs = []
197 objdecode = getattr(obj, '_nvim_decode', self._decode_default)
198 for _, fn in inspect.getmembers(obj, predicate):
199 sync = fn._nvim_rpc_sync
200 decode = getattr(fn, '_nvim_decode', objdecode)
201 nvim_bind = None
202 if fn._nvim_bind:
203 nvim_bind = self._configure_nvim_for(fn)
204
205 method = fn._nvim_rpc_method_name
206 if fn._nvim_prefix_plugin_path:
207 method = '{}:{}'.format(plugin_path, method)
208
209 fn_wrapped = partial(self._wrap_function, fn,
210 sync, decode, nvim_bind, method)
211 self._copy_attributes(fn, fn_wrapped)
212 # register in the rpc handler dict
213 if sync:
214 if method in self._request_handlers:
215 raise Exception(('Request handler for "{}" is '
216 + 'already registered').format(method))
217 self._request_handlers[method] = fn_wrapped
218 else:
219 if method in self._notification_handlers:
220 raise Exception(('Notification handler for "{}" is '
221 + 'already registered').format(method))
222 self._notification_handlers[method] = fn_wrapped
223 if hasattr(fn, '_nvim_rpc_spec'):
224 specs.append(fn._nvim_rpc_spec)
225 handlers.append(fn_wrapped)
226 if specs:
227 self._specs[plugin_path] = specs
228
229 def _copy_attributes(self, fn, fn2):
230 # Copy _nvim_* attributes from the original function
231 for attr in dir(fn):
232 if attr.startswith('_nvim_'):
233 setattr(fn2, attr, getattr(fn, attr))
234
235 def _on_specs_request(self, path):
236 if IS_PYTHON3:
237 path = decode_if_bytes(path)
238 if path in self._load_errors:
239 self.nvim.out_write(self._load_errors[path] + '\n')
240 return self._specs.get(path, 0)
241
242 def _configure_nvim_for(self, obj):
243 # Configure a nvim instance for obj (checks encoding configuration)
244 nvim = self.nvim
245 decode = getattr(obj, '_nvim_decode', self._decode_default)
246 if decode:
247 nvim = nvim.with_decode(decode)
248 return nvim
0 """Legacy python/python3-vim emulation."""
1 import imp
2 import io
3 import logging
4 import os
5 import sys
6
7 from .decorators import plugin, rpc_export
8 from ..api import Nvim, walk
9 from ..compat import IS_PYTHON3
10 from ..msgpack_rpc import ErrorResponse
11 from ..util import format_exc_skip
12
13 __all__ = ('ScriptHost',)
14
15
16 logger = logging.getLogger(__name__)
17 debug, info, warn = (logger.debug, logger.info, logger.warn,)
18
19 if IS_PYTHON3:
20 basestring = str
21
22 if sys.version_info >= (3, 4):
23 from importlib.machinery import PathFinder
24
25 PYTHON_SUBDIR = 'python3'
26 else:
27 PYTHON_SUBDIR = 'python2'
28
29
30 @plugin
31 class ScriptHost(object):
32
33 """Provides an environment for running python plugins created for Vim."""
34
35 def __init__(self, nvim):
36 """Initialize the legacy python-vim environment."""
37 self.setup(nvim)
38 # context where all code will run
39 self.module = imp.new_module('__main__')
40 nvim.script_context = self.module
41 # it seems some plugins assume 'sys' is already imported, so do it now
42 exec('import sys', self.module.__dict__)
43 self.legacy_vim = LegacyVim.from_nvim(nvim)
44 sys.modules['vim'] = self.legacy_vim
45
46 # Handle DirChanged. #296
47 nvim.command(
48 'au DirChanged * call rpcnotify({}, "python_chdir", v:event.cwd)'
49 .format(nvim.channel_id), async_=True)
50 # XXX: Avoid race condition.
51 # https://github.com/neovim/pynvim/pull/296#issuecomment-358970531
52 # TODO(bfredl): when host initialization has been refactored,
53 # to make __init__ safe again, the following should work:
54 # os.chdir(nvim.eval('getcwd()', async_=False))
55 nvim.command('call rpcnotify({}, "python_chdir", getcwd())'
56 .format(nvim.channel_id), async_=True)
57
58 def setup(self, nvim):
59 """Setup import hooks and global streams.
60
61 This will add import hooks for importing modules from runtime
62 directories and patch the sys module so 'print' calls will be
63 forwarded to Nvim.
64 """
65 self.nvim = nvim
66 info('install import hook/path')
67 self.hook = path_hook(nvim)
68 sys.path_hooks.append(self.hook)
69 nvim.VIM_SPECIAL_PATH = '_vim_path_'
70 sys.path.append(nvim.VIM_SPECIAL_PATH)
71 info('redirect sys.stdout and sys.stderr')
72 self.saved_stdout = sys.stdout
73 self.saved_stderr = sys.stderr
74 sys.stdout = RedirectStream(lambda data: nvim.out_write(data))
75 sys.stderr = RedirectStream(lambda data: nvim.err_write(data))
76
77 def teardown(self):
78 """Restore state modified from the `setup` call."""
79 nvim = self.nvim
80 info('uninstall import hook/path')
81 sys.path.remove(nvim.VIM_SPECIAL_PATH)
82 sys.path_hooks.remove(self.hook)
83 info('restore sys.stdout and sys.stderr')
84 sys.stdout = self.saved_stdout
85 sys.stderr = self.saved_stderr
86
87 @rpc_export('python_execute', sync=True)
88 def python_execute(self, script, range_start, range_stop):
89 """Handle the `python` ex command."""
90 self._set_current_range(range_start, range_stop)
91 try:
92 exec(script, self.module.__dict__)
93 except Exception:
94 raise ErrorResponse(format_exc_skip(1))
95
96 @rpc_export('python_execute_file', sync=True)
97 def python_execute_file(self, file_path, range_start, range_stop):
98 """Handle the `pyfile` ex command."""
99 self._set_current_range(range_start, range_stop)
100 with open(file_path) as f:
101 script = compile(f.read(), file_path, 'exec')
102 try:
103 exec(script, self.module.__dict__)
104 except Exception:
105 raise ErrorResponse(format_exc_skip(1))
106
107 @rpc_export('python_do_range', sync=True)
108 def python_do_range(self, start, stop, code):
109 """Handle the `pydo` ex command."""
110 self._set_current_range(start, stop)
111 nvim = self.nvim
112 start -= 1
113 fname = '_vim_pydo'
114
115 # define the function
116 function_def = 'def %s(line, linenr):\n %s' % (fname, code,)
117 exec(function_def, self.module.__dict__)
118 # get the function
119 function = self.module.__dict__[fname]
120 while start < stop:
121 # Process batches of 5000 to avoid the overhead of making multiple
122 # API calls for every line. Assuming an average line length of 100
123 # bytes, approximately 488 kilobytes will be transferred per batch,
124 # which can be done very quickly in a single API call.
125 sstart = start
126 sstop = min(start + 5000, stop)
127 lines = nvim.current.buffer.api.get_lines(sstart, sstop, True)
128
129 exception = None
130 newlines = []
131 linenr = sstart + 1
132 for i, line in enumerate(lines):
133 result = function(line, linenr)
134 if result is None:
135 # Update earlier lines, and skip to the next
136 if newlines:
137 end = sstart + len(newlines) - 1
138 nvim.current.buffer.api.set_lines(sstart, end,
139 True, newlines)
140 sstart += len(newlines) + 1
141 newlines = []
142 pass
143 elif isinstance(result, basestring):
144 newlines.append(result)
145 else:
146 exception = TypeError('pydo should return a string '
147 + 'or None, found %s instead'
148 % result.__class__.__name__)
149 break
150 linenr += 1
151
152 start = sstop
153 if newlines:
154 end = sstart + len(newlines)
155 nvim.current.buffer.api.set_lines(sstart, end, True, newlines)
156 if exception:
157 raise exception
158 # delete the function
159 del self.module.__dict__[fname]
160
161 @rpc_export('python_eval', sync=True)
162 def python_eval(self, expr):
163 """Handle the `pyeval` vim function."""
164 return eval(expr, self.module.__dict__)
165
166 @rpc_export('python_chdir', sync=False)
167 def python_chdir(self, cwd):
168 """Handle working directory changes."""
169 os.chdir(cwd)
170
171 def _set_current_range(self, start, stop):
172 current = self.legacy_vim.current
173 current.range = current.buffer.range(start, stop)
174
175
176 class RedirectStream(io.IOBase):
177 def __init__(self, redirect_handler):
178 self.redirect_handler = redirect_handler
179
180 def write(self, data):
181 self.redirect_handler(data)
182
183 def writelines(self, seq):
184 self.redirect_handler('\n'.join(seq))
185
186
187 if IS_PYTHON3:
188 num_types = (int, float)
189 else:
190 num_types = (int, long, float) # noqa: F821
191
192
193 def num_to_str(obj):
194 if isinstance(obj, num_types):
195 return str(obj)
196 else:
197 return obj
198
199
200 class LegacyVim(Nvim):
201 def eval(self, expr):
202 obj = self.request("vim_eval", expr)
203 return walk(num_to_str, obj)
204
205
206 # Copied/adapted from :help if_pyth.
207 def path_hook(nvim):
208 def _get_paths():
209 if nvim._thread_invalid():
210 return []
211 return discover_runtime_directories(nvim)
212
213 def _find_module(fullname, oldtail, path):
214 idx = oldtail.find('.')
215 if idx > 0:
216 name = oldtail[:idx]
217 tail = oldtail[idx + 1:]
218 fmr = imp.find_module(name, path)
219 module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr)
220 return _find_module(fullname, tail, module.__path__)
221 else:
222 return imp.find_module(fullname, path)
223
224 class VimModuleLoader(object):
225 def __init__(self, module):
226 self.module = module
227
228 def load_module(self, fullname, path=None):
229 # Check sys.modules, required for reload (see PEP302).
230 try:
231 return sys.modules[fullname]
232 except KeyError:
233 pass
234 return imp.load_module(fullname, *self.module)
235
236 class VimPathFinder(object):
237 @staticmethod
238 def find_module(fullname, path=None):
239 """Method for Python 2.7 and 3.3."""
240 try:
241 return VimModuleLoader(
242 _find_module(fullname, fullname, path or _get_paths()))
243 except ImportError:
244 return None
245
246 @staticmethod
247 def find_spec(fullname, target=None):
248 """Method for Python 3.4+."""
249 return PathFinder.find_spec(fullname, _get_paths(), target)
250
251 def hook(path):
252 if path == nvim.VIM_SPECIAL_PATH:
253 return VimPathFinder
254 else:
255 raise ImportError
256
257 return hook
258
259
260 def discover_runtime_directories(nvim):
261 rv = []
262 for rtp in nvim.list_runtime_paths():
263 if not os.path.exists(rtp):
264 continue
265 for subdir in ['pythonx', PYTHON_SUBDIR]:
266 path = os.path.join(rtp, subdir)
267 if os.path.exists(path):
268 rv.append(path)
269 return rv
0 """Shared utility functions."""
1
2 import sys
3 from traceback import format_exception
4
5
6 def format_exc_skip(skip, limit=None):
7 """Like traceback.format_exc but allow skipping the first frames."""
8 etype, val, tb = sys.exc_info()
9 for i in range(skip):
10 tb = tb.tb_next
11 return (''.join(format_exception(etype, val, tb, limit))).rstrip()
12
13
14 # Taken from SimpleNamespace in python 3
15 class Version:
16
17 """Helper class for version info."""
18
19 def __init__(self, **kwargs):
20 """Create the Version object."""
21 self.__dict__.update(kwargs)
22
23 def __repr__(self):
24 """Return str representation of the Version."""
25 keys = sorted(self.__dict__)
26 items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys)
27 return "{}({})".format(type(self).__name__, ", ".join(items))
28
29 def __eq__(self, other):
30 """Check if version is same as other."""
31 return self.__dict__ == other.__dict__
32
33
34 VERSION = Version(major=0, minor=3, patch=2, prerelease='')
0 Metadata-Version: 2.1
1 Name: pynvim
2 Version: 0.3.2
3 Summary: Python client to neovim
4 Home-page: http://github.com/neovim/python-client
5 Author: Thiago de Arruda
6 Author-email: tpadilha84@gmail.com
7 License: Apache
8 Download-URL: https://github.com/neovim/python-client/archive/0.3.2.tar.gz
9 Description: UNKNOWN
10 Platform: UNKNOWN
11 Provides-Extra: test
12 Provides-Extra: pyuv
0 LICENSE
1 MANIFEST.in
2 README.md
3 setup.cfg
4 setup.py
5 neovim/__init__.py
6 neovim/api/__init__.py
7 pynvim/__init__.py
8 pynvim/compat.py
9 pynvim/util.py
10 pynvim.egg-info/PKG-INFO
11 pynvim.egg-info/SOURCES.txt
12 pynvim.egg-info/dependency_links.txt
13 pynvim.egg-info/not-zip-safe
14 pynvim.egg-info/requires.txt
15 pynvim.egg-info/top_level.txt
16 pynvim/api/__init__.py
17 pynvim/api/buffer.py
18 pynvim/api/common.py
19 pynvim/api/nvim.py
20 pynvim/api/tabpage.py
21 pynvim/api/window.py
22 pynvim/msgpack_rpc/__init__.py
23 pynvim/msgpack_rpc/async_session.py
24 pynvim/msgpack_rpc/msgpack_stream.py
25 pynvim/msgpack_rpc/session.py
26 pynvim/msgpack_rpc/event_loop/__init__.py
27 pynvim/msgpack_rpc/event_loop/asyncio.py
28 pynvim/msgpack_rpc/event_loop/base.py
29 pynvim/msgpack_rpc/event_loop/uv.py
30 pynvim/plugin/__init__.py
31 pynvim/plugin/decorators.py
32 pynvim/plugin/host.py
33 pynvim/plugin/script_host.py
34 test/test_buffer.py
35 test/test_client_rpc.py
36 test/test_concurrency.py
37 test/test_decorators.py
38 test/test_events.py
39 test/test_host.py
40 test/test_tabpage.py
41 test/test_vim.py
42 test/test_window.py
0 msgpack>=0.5.0
1 greenlet
2
3 [pyuv]
4 pyuv>=1.0.0
5
6 [test]
7 pytest>=3.4.0
00 [flake8]
1 ignore = D211,E731,D401
1 ignore = D211,E731,D401,W503
22
33 [tool:pytest]
44 testpaths = test
2727 # pypy already includes an implementation of the greenlet module
2828 install_requires.append('greenlet')
2929
30 setup(name='neovim',
31 version='0.3.0',
30 setup(name='pynvim',
31 version='0.3.2',
3232 description='Python client to neovim',
3333 url='http://github.com/neovim/python-client',
34 download_url='https://github.com/neovim/python-client/archive/0.3.0.tar.gz',
34 download_url='https://github.com/neovim/python-client/archive/0.3.2.tar.gz',
3535 author='Thiago de Arruda',
3636 author_email='tpadilha84@gmail.com',
3737 license='Apache',
38 packages=['neovim', 'neovim.api', 'neovim.msgpack_rpc',
39 'neovim.msgpack_rpc.event_loop', 'neovim.plugin'],
38 packages=['pynvim', 'pynvim.api', 'pynvim.msgpack_rpc',
39 'pynvim.msgpack_rpc.event_loop', 'pynvim.plugin',
40 'neovim', 'neovim.api'],
4041 install_requires=install_requires,
4142 tests_require=tests_require,
4243 extras_require=extras_require,
00 import os
11
2 from neovim.compat import IS_PYTHON3
2 from pynvim.compat import IS_PYTHON3
33
44
55 def test_repr(vim):
0 from neovim.plugin.decorators import command
0 from pynvim.plugin.decorators import command
11
22
33 def test_command_count():
00 # -*- coding: utf-8 -*-
11
2 from neovim.plugin.host import Host, host_method_spec
2 from pynvim.plugin.host import Host, host_method_spec
33
44 def test_host_method_spec(vim):
55 h = Host(vim)
7171 assert vim.current.line == ''
7272 vim.current.line = 'abc'
7373 assert vim.current.line == 'abc'
74
75
76 def test_current_line_delete(vim):
77 vim.current.buffer[:] = ['one', 'two']
78 assert len(vim.current.buffer[:]) == 2
79 del vim.current.line
80 assert len(vim.current.buffer[:]) == 1 and vim.current.buffer[0] == 'two'
81 del vim.current.line
82 assert len(vim.current.buffer[:]) == 1 and not vim.current.buffer[0]
7483
7584
7685 def test_vars(vim):