Skip to content

Commit 5e5856c

Browse files
committed
Merge pull request #34 from tarruda/restructure-library
Restructure library
2 parents ac76741 + 22cc1a8 commit 5e5856c

36 files changed

+1837
-1350
lines changed

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ language: python
22
python:
33
- "2.6"
44
- "2.7"
5+
- "3.2"
6+
- "3.3"
57
- "3.4"
8+
- "pypy"
69
before_install:
710
- sudo apt-get update -qq
811
- sudo apt-get install expect -q
912
- git clone --depth=1 -b master git://github.com/neovim/neovim nvim
1013
- sudo git clone --depth=1 git://github.com/neovim/deps /opt/neovim-deps
14+
- pip install -q flake8 flake8-import-order flake8-docstrings pep8-naming
1115
install:
1216
- pip install .
1317
- prefix="/opt/neovim-deps/64"
@@ -16,6 +20,8 @@ install:
1620
- export PKG_CONFIG_PATH="$prefix/usr/lib/pkgconfig"
1721
- export USE_BUNDLED_DEPS=OFF
1822
- cd nvim && make && cd ..
23+
before_script:
24+
- flake8 --exclude ./neovim/plugins neovim
1925
script:
2026
- ./nvim/scripts/run-api-tests.exp "nosetests --nologcapture --verbosity=2" "./nvim/build/bin/nvim -u NONE"
2127
- NVIM_SPAWN_ARGV='["./nvim/build/bin/nvim", "-u", "NONE", "--embed"]' nosetests --nologcapture --verbosity=2

README.md

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,43 @@
22

33
[![Build Status](https://travis-ci.org/neovim/python-client.svg?branch=master)](https://travis-ci.org/neovim/python-client)
44

5-
Library aims to emulate the current python-vim interface through Neovim
6-
msgpack-rpc API
5+
Library for scripting Nvim processes through it's msgpack-rpc API.
76

87
#### Installation
98

109
```sh
1110
pip install neovim
1211
```
1312

14-
#### Usage
13+
#### Usage through the python REPL
1514

16-
Start Neovim with a known address or query the value of $NVIM_LISTEN_ADDRESS
17-
after startup:
15+
A number of different transports are supported, but the simplest way to get
16+
started is with the python REPL. First, start Nvim with a known address(or
17+
query the value of $NVIM_LISTEN_ADDRESS of a running instance):
1818

1919
```sh
20-
$ NVIM_LISTEN_ADDRESS=/tmp/neovim nvim
20+
$ NVIM_LISTEN_ADDRESS=/tmp/nvim nvim
2121
```
2222

23-
Open the python REPL with another terminal connect to Neovim:
23+
Open the python REPL with another terminal connect to Nvim(Note that the API is
24+
similar to the one exposed by the [python-vim
25+
bridge](http://vimdoc.sourceforge.net/htmldoc/if_pyth.html#python-vim))
2426

2527
```python
26-
>>> import neovim
27-
>>> vim = neovim.connect('/tmp/neovim')
28-
>>> buffer = vim.buffers[0] # get the first buffer
28+
>>> from neovim import socket_session, Nvim
29+
# Create a msgpack-rpc session to the unix domain socket created by Nvim:
30+
>>> session = socket_session('/tmp/nvim')
31+
# Create a Nvim instance from the session(don't call Nvim constructor!):
32+
>>> nvim = Nvim.from_session(session)
33+
# Now do some work.
34+
>>> buffer = nvim.buffers[0] # Get the first buffer
2935
>>> buffer[0] = 'replace first line'
3036
>>> buffer[:] = ['replace whole buffer']
31-
>>> vim.command('vsplit')
32-
>>> vim.windows[1].width = 10
33-
>>> vim.vars['global_var'] = [1, 2, 3]
34-
>>> vim.eval('g:global_var')
37+
>>> nvim.command('vsplit')
38+
>>> nvim.windows[1].width = 10
39+
>>> nvim.vars['global_var'] = [1, 2, 3]
40+
>>> nvim.eval('g:global_var')
3541
[1, 2, 3]
3642
```
3743

38-
See the test subdirectory for more examples
39-
40-
This is still alpha and incomplete, use only for testing purposes
44+
The tests can be consulted for more examples.

neovim/__init__.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
1-
from .client import Client
2-
from .script_host import ScriptHost
3-
from .plugin_host import PluginHost
4-
from .uv_stream import UvStream
5-
from .msgpack_stream import MsgpackStream
6-
from .rpc_stream import RPCStream
7-
from time import sleep
8-
import logging, os
1+
"""Python client for Nvim.
92
10-
__all__ = ['connect', 'start_host', 'ScriptHost', 'PluginHost']
3+
Client library for talking with Nvim processes via it's msgpack-rpc API.
4+
"""
5+
import logging
6+
import os
117

8+
from .api import DecodeHook, Nvim, SessionHook
9+
from .msgpack_rpc import (socket_session, spawn_session, stdio_session,
10+
tcp_session)
11+
from .plugins import PluginHost, ScriptHost
1212

13-
# Required for python 2.6
14-
class NullHandler(logging.Handler):
15-
def emit(self, record):
16-
pass
1713

14+
__all__ = ('tcp_session', 'socket_session', 'stdio_session', 'spawn_session',
15+
'start_host', 'DecodeHook', 'Nvim', 'SessionHook')
1816

19-
def connect(address=None, port=None, vim_compatible=False, decode_str=False):
20-
client = Client(RPCStream(MsgpackStream(UvStream(address, port)), decode_str=decode_str),
21-
vim_compatible)
22-
return client.vim
2317

18+
def start_host(session=None):
19+
"""Promote the current process into python plugin host for Nvim.
2420
25-
def spawn(argv, decode_str=False):
26-
client = Client(RPCStream(MsgpackStream(UvStream(spawn_argv=argv)), decode_str=decode_str))
27-
return client.vim
21+
Start msgpack-rpc event loop for `session`, listening for Nvim requests
22+
and notifications. It registers Nvim commands for loading/unloading
23+
python plugins.
2824
25+
The sys.stdout and sys.stderr streams are redirected to Nvim through
26+
`session`. That means print statements probably won't work as expected
27+
while this function doesn't return.
2928
30-
def start_host(address=None, port=None):
31-
logging.root.addHandler(NullHandler())
29+
This function is normally called at program startup and could have been
30+
defined as a separate executable. It is exposed as a library function for
31+
testing purposes only.
32+
"""
3233
logger = logging.getLogger(__name__)
33-
info = logger.info
3434
if 'NVIM_PYTHON_LOG_FILE' in os.environ:
3535
logfile = os.environ['NVIM_PYTHON_LOG_FILE'].strip()
3636
handler = logging.FileHandler(logfile, 'w')
@@ -46,9 +46,18 @@ def start_host(address=None, port=None):
4646
if isinstance(l, int):
4747
level = l
4848
logger.setLevel(level)
49-
info('connecting to neovim')
50-
vim = connect(address, port, vim_compatible=True)
51-
info('connected to neovim')
52-
with PluginHost(vim, discovered_plugins=[ScriptHost]) as host:
49+
if not session:
50+
session = stdio_session()
51+
nvim = Nvim.from_session(session)
52+
with PluginHost(nvim, preloaded=[ScriptHost]) as host:
5353
host.run()
5454

55+
56+
# Required for python 2.6
57+
class NullHandler(logging.Handler):
58+
def emit(self, record):
59+
pass
60+
61+
62+
if not logging.root.handlers:
63+
logging.root.addHandler(NullHandler())

neovim/api/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Nvim API subpackage.
2+
3+
This package implements a higher-level API that wraps msgpack-rpc `Session`
4+
instances.
5+
"""
6+
7+
from .buffer import Buffer
8+
from .common import DecodeHook, SessionHook
9+
from .nvim import Nvim, NvimError
10+
from .tabpage import Tabpage
11+
from .window import Window
12+
13+
14+
__all__ = ('Nvim', 'Buffer', 'Window', 'Tabpage', 'NvimError', 'SessionHook',
15+
'DecodeHook')

neovim/api/buffer.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""API for working with Nvim buffers."""
2+
from .common import Remote, RemoteMap
3+
from ..compat import IS_PYTHON3
4+
5+
6+
__all__ = ('Buffer')
7+
8+
9+
if IS_PYTHON3:
10+
basestring = str
11+
12+
13+
class Buffer(Remote):
14+
15+
"""A remote Nvim buffer."""
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.vars = RemoteMap(session, 'buffer_get_var', 'buffer_set_var',
26+
self)
27+
self.options = RemoteMap(session, 'buffer_get_option',
28+
'buffer_set_option', self)
29+
30+
def __len__(self):
31+
"""Return the number of lines contained in a Buffer."""
32+
return self._session.request('buffer_get_length', self)
33+
34+
def __getitem__(self, idx):
35+
"""Get a buffer line or slice by integer index.
36+
37+
Indexes may be negative to specify positions from the end of the
38+
buffer. For example, -1 is the last line, -2 is the line before that
39+
and so on.
40+
41+
When retrieving slices, omiting indexes(eg: `buffer[:]`) will bring
42+
the whole buffer.
43+
"""
44+
if not isinstance(idx, slice):
45+
return self._session.request('buffer_get_line', self, idx)
46+
include_end = False
47+
start = idx.start
48+
end = idx.stop
49+
if start is None:
50+
start = 0
51+
if end is None:
52+
end = -1
53+
include_end = True
54+
return self._session.request('buffer_get_slice', self, start, end,
55+
True, include_end)
56+
57+
def __setitem__(self, idx, lines):
58+
"""Replace a buffer line or slice by integer index.
59+
60+
Like with `__getitem__`, indexes may be negative.
61+
62+
When replacing slices, omiting indexes(eg: `buffer[:]`) will replace
63+
the whole buffer.
64+
"""
65+
if not isinstance(idx, slice):
66+
if lines is None:
67+
return self._session.request('buffer_del_line', self, idx)
68+
else:
69+
return self._session.request('buffer_set_line', self, idx,
70+
lines)
71+
if lines is None:
72+
lines = []
73+
include_end = False
74+
start = idx.start
75+
end = idx.stop
76+
if start is None:
77+
start = 0
78+
if end is None:
79+
end = -1
80+
include_end = True
81+
return self._session.request('buffer_set_slice', self, start, end,
82+
True, include_end, lines)
83+
84+
def __iter__(self):
85+
"""Iterate lines of a buffer.
86+
87+
This will retrieve all lines locally before iteration starts. This
88+
approach is used because for most cases, the gain is much greater by
89+
minimizing the number of API calls by transfering all data needed to
90+
work.
91+
"""
92+
lines = self[:]
93+
for line in lines:
94+
yield line
95+
96+
def get_slice(self, start, stop, start_incl, end_incl):
97+
"""More flexible wrapper for retrieving slices."""
98+
return self._session.request('buffer_get_slice', self, start, stop,
99+
start_incl, end_incl)
100+
101+
def set_slice(self, start, stop, start_incl, end_incl, lines):
102+
"""More flexible wrapper for replacing slices."""
103+
return self._session.request('buffer_set_slice', self, start, stop,
104+
start_incl, end_incl, lines)
105+
106+
def append(self, lines, index=-1):
107+
"""Append a string or list of lines to the buffer."""
108+
if isinstance(lines, basestring):
109+
lines = [lines]
110+
return self._session.request('buffer_insert', self, index, lines)
111+
112+
def mark(self, name):
113+
"""Return (row, col) tuple for a named mark."""
114+
return self._session.request('buffer_get_mark', self, name)
115+
116+
def range(self, start, end):
117+
"""Return a `Range` object, which represents part of the Buffer."""
118+
return Range(self, start, end)
119+
120+
@property
121+
def name(self):
122+
"""Get the buffer name."""
123+
return self._session.request('buffer_get_name', self)
124+
125+
@name.setter
126+
def name(self, value):
127+
"""Set the buffer name. BufFilePre/BufFilePost are triggered."""
128+
return self._session.request('buffer_set_name', self, value)
129+
130+
@property
131+
def valid(self):
132+
"""Return True if the buffer still exists."""
133+
return self._session.request('buffer_is_valid', self)
134+
135+
@property
136+
def number(self):
137+
"""Get the buffer number."""
138+
return self._session.request('buffer_get_number', self)
139+
140+
141+
class Range(object):
142+
def __init__(self, buffer, start, end):
143+
self._buffer = buffer
144+
self.start = start - 1
145+
self.end = end
146+
147+
def __len__(self):
148+
return self.end - self.start
149+
150+
def __getitem__(self, idx):
151+
if not isinstance(idx, slice):
152+
return self._buffer[self._normalize_index(idx)]
153+
start = self._normalize_index(idx.start)
154+
end = self._normalize_index(idx.stop)
155+
if start is None:
156+
start = self.start
157+
if end is None:
158+
end = self.end
159+
return self._buffer[start:end]
160+
161+
def __setitem__(self, idx, lines):
162+
if not isinstance(idx, slice):
163+
self._buffer[self._normalize_index(idx)] = lines
164+
return
165+
start = self._normalize_index(idx.start)
166+
end = self._normalize_index(idx.stop)
167+
if start is None:
168+
start = self.start
169+
if end is None:
170+
end = self.end
171+
self._buffer[start:end] = lines
172+
173+
def __iter__(self):
174+
for i in range(self.start, self.end):
175+
yield self._buffer[i]
176+
177+
def append(self, lines, i=None):
178+
i = self._normalize_index(i)
179+
if i is None:
180+
i = self.end
181+
self._buffer.append(lines, i)
182+
183+
def _normalize_index(self, index):
184+
if index is None:
185+
return None
186+
if index < 0:
187+
index = self.end - 1
188+
else:
189+
index += self.start
190+
if index >= self.end:
191+
index = self.end - 1
192+
return index

0 commit comments

Comments
 (0)