././@PaxHeader 0000000 0000000 0000000 00000000032 00000000000 011450 x ustar 00 0000000 0000000 26 mtime=1579944220.18208
pynvim-0.4.1/ 0000755 0001750 0001750 00000000000 00000000000 013305 5 ustar 00bjorn bjorn 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1410857002.0
pynvim-0.4.1/LICENSE 0000644 0001750 0001750 00000026056 00000000000 014323 0 ustar 00bjorn bjorn 0000000 0000000 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 Thiago Arruda
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1573891020.0
pynvim-0.4.1/MANIFEST.in 0000644 0001750 0001750 00000000066 00000000000 015045 0 ustar 00bjorn bjorn 0000000 0000000 include README.md LICENSE
recursive-include test *.py
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1579944220.1854134
pynvim-0.4.1/PKG-INFO 0000644 0001750 0001750 00000000537 00000000000 014407 0 ustar 00bjorn bjorn 0000000 0000000 Metadata-Version: 2.1
Name: pynvim
Version: 0.4.1
Summary: Python client to neovim
Home-page: http://github.com/neovim/pynvim
Author: Thiago de Arruda
Author-email: tpadilha84@gmail.com
License: Apache
Download-URL: https://github.com/neovim/pynvim/archive/0.4.1.tar.gz
Description: UNKNOWN
Platform: UNKNOWN
Provides-Extra: pyuv
Provides-Extra: test
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579941126.0
pynvim-0.4.1/README.md 0000644 0001750 0001750 00000010200 00000000000 014555 0 ustar 00bjorn bjorn 0000000 0000000 ### Pynvim: Python client to [Neovim](https://github.com/neovim/neovim)
[](https://travis-ci.org/neovim/pynvim)
[](http://pynvim.readthedocs.io/en/latest/?badge=latest)
[](https://codecov.io/gh/neovim/pynvim)
Pynvim implements support for python plugins in Nvim. It also works as a library for
connecting to and scripting Nvim processes through its msgpack-rpc API.
#### Installation
Supports python 2.7, and 3.4 or later.
```sh
pip2 install pynvim
pip3 install pynvim
```
If you only use one of python2 or python3, it is enough to install that
version. You can install the package without being root by adding the `--user`
flag.
Anytime you upgrade Neovim, make sure to upgrade pynvim as well:
```sh
pip2 install --upgrade pynvim
pip3 install --upgrade pynvim
```
Alternatively, the master version could be installed by executing the following
in the root of this repository:
```sh
pip2 install .
pip3 install .
```
#### Python Plugin API
Pynvim supports python _remote plugins_ (via the language-agnostic Nvim rplugin
interface), as well as _Vim plugins_ (via the `:python[3]` interface). Thus when
pynvim is installed Neovim will report support for the `+python[3]` Vim feature.
The rplugin interface allows plugins to handle vimL function calls as well as
defining commands and autocommands, and such plugins can operate asynchronously
without blocking nvim. For details on the new rplugin interface,
see the [Remote Plugin](http://pynvim.readthedocs.io/en/latest/usage/remote-plugins.html) documentation.
Pynvim defines some extensions over the vim python API:
* Builtin and plugin vimL functions are available as `nvim.funcs`
* API functions are available as `vim.api` and for objects such as `buffer.api`
* Lua functions can be defined using `vim.exec_lua` and called with `vim.lua`
* Support for thread-safety and async requests.
See the [Python Plugin API](http://pynvim.readthedocs.io/en/latest/usage/python-plugin-api.html) documentation for usage of this new functionality.
#### Development
Use (and activate) a local virtualenv.
python3 -m venv env36
source env36/bin/activate
If you change the code, you must reinstall for the changes to take effect:
pip install .
Use `pytest` to run the tests. Invoking with `python -m` prepends the current
directory to `sys.path` (otherwise `pytest` might find other versions!):
python -m pytest
For details about testing and troubleshooting, see the
[development](http://pynvim.readthedocs.io/en/latest/development.html)
documentation.
#### Usage through the python REPL
A number of different transports are supported, but the simplest way to get
started is with the python REPL. First, start Nvim with a known address (or use
the `$NVIM_LISTEN_ADDRESS` of a running instance):
```sh
$ NVIM_LISTEN_ADDRESS=/tmp/nvim nvim
```
In another terminal, connect a python REPL to Nvim (note that the API is similar
to the one exposed by the [python-vim
bridge](http://vimdoc.sourceforge.net/htmldoc/if_pyth.html#python-vim)):
```python
>>> from pynvim import attach
# Create a python API session attached to unix domain socket created above:
>>> nvim = attach('socket', path='/tmp/nvim')
# Now do some work.
>>> buffer = nvim.current.buffer # Get the current buffer
>>> buffer[0] = 'replace first line'
>>> buffer[:] = ['replace whole buffer']
>>> nvim.command('vsplit')
>>> nvim.windows[1].width = 10
>>> nvim.vars['global_var'] = [1, 2, 3]
>>> nvim.eval('g:global_var')
[1, 2, 3]
```
You can embed Neovim into your python application instead of connecting to
a running Neovim instance.
```python
>>> from pynvim import attach
>>> nvim = attach('child', argv=["/bin/env", "nvim", "--embed", "--headless"])
```
- The ` --headless` argument tells `nvim` not to wait for a UI to connect.
- Alternatively, use `--embed` _without_ `--headless` if your client is a UI
and you want `nvim` to wait for your client to `nvim_ui_attach` before
continuing startup.
See the tests for more examples.
././@PaxHeader 0000000 0000000 0000000 00000000032 00000000000 011450 x ustar 00 0000000 0000000 26 mtime=1579944220.15208
pynvim-0.4.1/neovim/ 0000755 0001750 0001750 00000000000 00000000000 014602 5 ustar 00bjorn bjorn 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1544014247.0
pynvim-0.4.1/neovim/__init__.py 0000644 0001750 0001750 00000000256 00000000000 016716 0 ustar 00bjorn bjorn 0000000 0000000 """Python client for Nvim.
This is a transition package. New projects should instead import pynvim package.
"""
import pynvim
from pynvim import *
__all__ = pynvim.__all__
././@PaxHeader 0000000 0000000 0000000 00000000032 00000000000 011450 x ustar 00 0000000 0000000 26 mtime=1579944220.15208
pynvim-0.4.1/neovim/api/ 0000755 0001750 0001750 00000000000 00000000000 015353 5 ustar 00bjorn bjorn 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1544014247.0
pynvim-0.4.1/neovim/api/__init__.py 0000644 0001750 0001750 00000000261 00000000000 017463 0 ustar 00bjorn bjorn 0000000 0000000 """Nvim API subpackage.
This is a transition package. New projects should instead import pynvim.api.
"""
from pynvim import api
from pynvim.api import *
__all__ = api.__all__
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1579944220.1554134
pynvim-0.4.1/pynvim/ 0000755 0001750 0001750 00000000000 00000000000 014627 5 ustar 00bjorn bjorn 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/__init__.py 0000644 0001750 0001750 00000012633 00000000000 016745 0 ustar 00bjorn bjorn 0000000 0000000 """Python client for Nvim.
Client library for talking with Nvim processes via its msgpack-rpc API.
"""
import logging
import os
import sys
from .api import Nvim, NvimError
from .compat import IS_PYTHON3
from .msgpack_rpc import (ErrorResponse, child_session, socket_session,
stdio_session, tcp_session)
from .plugin import (Host, autocmd, command, decode, encoding, function,
plugin, rpc_export, shutdown_hook)
from .util import VERSION, Version
__all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session',
'start_host', 'autocmd', 'command', 'encoding', 'decode',
'function', 'plugin', 'rpc_export', 'Host', 'Nvim', 'NvimError',
'Version', 'VERSION', 'shutdown_hook', 'attach', 'setup_logging',
'ErrorResponse')
def start_host(session=None):
"""Promote the current process into python plugin host for Nvim.
Start msgpack-rpc event loop for `session`, listening for Nvim requests
and notifications. It registers Nvim commands for loading/unloading
python plugins.
The sys.stdout and sys.stderr streams are redirected to Nvim through
`session`. That means print statements probably won't work as expected
while this function doesn't return.
This function is normally called at program startup and could have been
defined as a separate executable. It is exposed as a library function for
testing purposes only.
"""
plugins = []
for arg in sys.argv:
_, ext = os.path.splitext(arg)
if ext == '.py':
plugins.append(arg)
elif os.path.isdir(arg):
init = os.path.join(arg, '__init__.py')
if os.path.isfile(init):
plugins.append(arg)
# This is a special case to support the old workaround of
# adding an empty .py file to make a package directory
# visible, and it should be removed soon.
for path in list(plugins):
dup = path + ".py"
if os.path.isdir(path) and dup in plugins:
plugins.remove(dup)
# Special case: the legacy scripthost receives a single relative filename
# while the rplugin host will receive absolute paths.
if plugins == ["script_host.py"]:
name = "script"
else:
name = "rplugin"
setup_logging(name)
if not session:
session = stdio_session()
nvim = Nvim.from_session(session)
if nvim.version.api_level < 1:
sys.stderr.write("This version of pynvim "
"requires nvim 0.1.6 or later")
sys.exit(1)
host = Host(nvim)
host.start(plugins)
def attach(session_type, address=None, port=None,
path=None, argv=None, decode=None):
"""Provide a nicer interface to create python api sessions.
Previous machinery to create python api sessions is still there. This only
creates a facade function to make things easier for the most usual cases.
Thus, instead of:
from pynvim import socket_session, Nvim
session = tcp_session(address=
, port=)
nvim = Nvim.from_session(session)
You can now do:
from pynvim import attach
nvim = attach('tcp', address=, port=)
And also:
nvim = attach('socket', path=)
nvim = attach('child', argv=)
nvim = attach('stdio')
When the session is not needed anymore, it is recommended to explicitly
close it:
nvim.close()
It is also possible to use the session as a context mangager:
with attach('socket', path=thepath) as nvim:
print(nvim.funcs.getpid())
print(nvim.current.line)
This will automatically close the session when you're done with it, or
when an error occured.
"""
session = (tcp_session(address, port) if session_type == 'tcp' else
socket_session(path) if session_type == 'socket' else
stdio_session() if session_type == 'stdio' else
child_session(argv) if session_type == 'child' else
None)
if not session:
raise Exception('Unknown session type "%s"' % session_type)
if decode is None:
decode = IS_PYTHON3
return Nvim.from_session(session).with_decode(decode)
def setup_logging(name):
"""Setup logging according to environment variables."""
logger = logging.getLogger(__name__)
if 'NVIM_PYTHON_LOG_FILE' in os.environ:
prefix = os.environ['NVIM_PYTHON_LOG_FILE'].strip()
major_version = sys.version_info[0]
logfile = '{}_py{}_{}'.format(prefix, major_version, name)
handler = logging.FileHandler(logfile, 'w', 'utf-8')
handler.formatter = logging.Formatter(
'%(asctime)s [%(levelname)s @ '
'%(filename)s:%(funcName)s:%(lineno)s] %(process)s - %(message)s')
logging.root.addHandler(handler)
level = logging.INFO
env_log_level = os.environ.get('NVIM_PYTHON_LOG_LEVEL', None)
if env_log_level is not None:
lvl = getattr(logging, env_log_level.strip(), None)
if isinstance(lvl, int):
level = lvl
else:
logger.warning('Invalid NVIM_PYTHON_LOG_LEVEL: %r, using INFO.',
env_log_level)
logger.setLevel(level)
# Required for python 2.6
class NullHandler(logging.Handler):
def emit(self, record):
pass
if not logging.root.handlers:
logging.root.addHandler(NullHandler())
././@PaxHeader 0000000 0000000 0000000 00000000032 00000000000 011450 x ustar 00 0000000 0000000 26 mtime=1579944220.16208
pynvim-0.4.1/pynvim/api/ 0000755 0001750 0001750 00000000000 00000000000 015400 5 ustar 00bjorn bjorn 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/api/__init__.py 0000644 0001750 0001750 00000000573 00000000000 017516 0 ustar 00bjorn bjorn 0000000 0000000 """Nvim API subpackage.
This package implements a higher-level API that wraps msgpack-rpc `Session`
instances.
"""
from .buffer import Buffer
from .common import decode_if_bytes, walk
from .nvim import Nvim, NvimError
from .tabpage import Tabpage
from .window import Window
__all__ = ('Nvim', 'Buffer', 'Window', 'Tabpage', 'NvimError',
'decode_if_bytes', 'walk')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/api/buffer.py 0000644 0001750 0001750 00000016026 00000000000 017230 0 ustar 00bjorn bjorn 0000000 0000000 """API for working with a Nvim Buffer."""
from .common import Remote
from ..compat import IS_PYTHON3, check_async
__all__ = ('Buffer')
if IS_PYTHON3:
basestring = str
def adjust_index(idx, default=None):
"""Convert from python indexing convention to nvim indexing convention."""
if idx is None:
return default
elif idx < 0:
return idx - 1
else:
return idx
class Buffer(Remote):
"""A remote Nvim buffer."""
_api_prefix = "nvim_buf_"
def __len__(self):
"""Return the number of lines contained in a Buffer."""
return self.request('nvim_buf_line_count')
def __getitem__(self, idx):
"""Get a buffer line or slice by integer index.
Indexes may be negative to specify positions from the end of the
buffer. For example, -1 is the last line, -2 is the line before that
and so on.
When retrieving slices, omiting indexes(eg: `buffer[:]`) will bring
the whole buffer.
"""
if not isinstance(idx, slice):
i = adjust_index(idx)
return self.request('nvim_buf_get_lines', i, i + 1, True)[0]
start = adjust_index(idx.start, 0)
end = adjust_index(idx.stop, -1)
return self.request('nvim_buf_get_lines', start, end, False)
def __setitem__(self, idx, item):
"""Replace a buffer line or slice by integer index.
Like with `__getitem__`, indexes may be negative.
When replacing slices, omiting indexes(eg: `buffer[:]`) will replace
the whole buffer.
"""
if not isinstance(idx, slice):
i = adjust_index(idx)
lines = [item] if item is not None else []
return self.request('nvim_buf_set_lines', i, i + 1, True, lines)
lines = item if item is not None else []
start = adjust_index(idx.start, 0)
end = adjust_index(idx.stop, -1)
return self.request('nvim_buf_set_lines', start, end, False, lines)
def __iter__(self):
"""Iterate lines of a buffer.
This will retrieve all lines locally before iteration starts. This
approach is used because for most cases, the gain is much greater by
minimizing the number of API calls by transfering all data needed to
work.
"""
lines = self[:]
for line in lines:
yield line
def __delitem__(self, idx):
"""Delete line or slice of lines from the buffer.
This is the same as __setitem__(idx, [])
"""
self.__setitem__(idx, None)
def __ne__(self, other):
"""Test inequality of Buffers.
Necessary for Python 2 compatibility.
"""
return not self.__eq__(other)
def append(self, lines, index=-1):
"""Append a string or list of lines to the buffer."""
if isinstance(lines, (basestring, bytes)):
lines = [lines]
return self.request('nvim_buf_set_lines', index, index, True, lines)
def mark(self, name):
"""Return (row, col) tuple for a named mark."""
return self.request('nvim_buf_get_mark', name)
def range(self, start, end):
"""Return a `Range` object, which represents part of the Buffer."""
return Range(self, start, end)
def add_highlight(self, hl_group, line, col_start=0,
col_end=-1, src_id=-1, async_=None,
**kwargs):
"""Add a highlight to the buffer."""
async_ = check_async(async_, kwargs, src_id != 0)
return self.request('nvim_buf_add_highlight', src_id, hl_group,
line, col_start, col_end, async_=async_)
def clear_highlight(self, src_id, line_start=0, line_end=-1, async_=None,
**kwargs):
"""Clear highlights from the buffer."""
async_ = check_async(async_, kwargs, True)
self.request('nvim_buf_clear_highlight', src_id,
line_start, line_end, async_=async_)
def update_highlights(self, src_id, hls, clear_start=0, clear_end=-1,
clear=False, async_=True):
"""Add or update highlights in batch to avoid unnecessary redraws.
A `src_id` must have been allocated prior to use of this function. Use
for instance `nvim.new_highlight_source()` to get a src_id for your
plugin.
`hls` should be a list of highlight items. Each item should be a list
or tuple on the form `("GroupName", linenr, col_start, col_end)` or
`("GroupName", linenr)` to highlight an entire line.
By default existing highlights are preserved. Specify a line range with
clear_start and clear_end to replace highlights in this range. As a
shorthand, use clear=True to clear the entire buffer before adding the
new highlights.
"""
if clear and clear_start is None:
clear_start = 0
lua = self._session._get_lua_private()
lua.update_highlights(self, src_id, hls, clear_start, clear_end,
async_=async_)
@property
def name(self):
"""Get the buffer name."""
return self.request('nvim_buf_get_name')
@name.setter
def name(self, value):
"""Set the buffer name. BufFilePre/BufFilePost are triggered."""
return self.request('nvim_buf_set_name', value)
@property
def valid(self):
"""Return True if the buffer still exists."""
return self.request('nvim_buf_is_valid')
@property
def number(self):
"""Get the buffer number."""
return self.handle
class Range(object):
def __init__(self, buffer, start, end):
self._buffer = buffer
self.start = start - 1
self.end = end - 1
def __len__(self):
return self.end - self.start + 1
def __getitem__(self, idx):
if not isinstance(idx, slice):
return self._buffer[self._normalize_index(idx)]
start = self._normalize_index(idx.start)
end = self._normalize_index(idx.stop)
if start is None:
start = self.start
if end is None:
end = self.end + 1
return self._buffer[start:end]
def __setitem__(self, idx, lines):
if not isinstance(idx, slice):
self._buffer[self._normalize_index(idx)] = lines
return
start = self._normalize_index(idx.start)
end = self._normalize_index(idx.stop)
if start is None:
start = self.start
if end is None:
end = self.end
self._buffer[start:end + 1] = lines
def __iter__(self):
for i in range(self.start, self.end + 1):
yield self._buffer[i]
def append(self, lines, i=None):
i = self._normalize_index(i)
if i is None:
i = self.end + 1
self._buffer.append(lines, i)
def _normalize_index(self, index):
if index is None:
return None
if index < 0:
index = self.end
else:
index += self.start
if index > self.end:
index = self.end
return index
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/api/common.py 0000644 0001750 0001750 00000014742 00000000000 017252 0 ustar 00bjorn bjorn 0000000 0000000 """Code shared between the API classes."""
import functools
from msgpack import unpackb
from ..compat import unicode_errors_default
__all__ = ()
class NvimError(Exception):
pass
class Remote(object):
"""Base class for Nvim objects(buffer/window/tabpage).
Each type of object has it's own specialized class with API wrappers around
the msgpack-rpc session. This implements equality which takes the remote
object handle into consideration.
"""
def __init__(self, session, code_data):
"""Initialize from session and code_data immutable object.
The `code_data` contains serialization information required for
msgpack-rpc calls. It must be immutable for Buffer equality to work.
"""
self._session = session
self.code_data = code_data
self.handle = unpackb(code_data[1])
self.api = RemoteApi(self, self._api_prefix)
self.vars = RemoteMap(self, self._api_prefix + 'get_var',
self._api_prefix + 'set_var',
self._api_prefix + 'del_var')
self.options = RemoteMap(self, self._api_prefix + 'get_option',
self._api_prefix + 'set_option')
def __repr__(self):
"""Get text representation of the object."""
return '<%s(handle=%r)>' % (
self.__class__.__name__,
self.handle,
)
def __eq__(self, other):
"""Return True if `self` and `other` are the same object."""
return (hasattr(other, 'code_data')
and other.code_data == self.code_data)
def __hash__(self):
"""Return hash based on remote object id."""
return self.code_data.__hash__()
def request(self, name, *args, **kwargs):
"""Wrapper for nvim.request."""
return self._session.request(name, self, *args, **kwargs)
class RemoteApi(object):
"""Wrapper to allow api methods to be called like python methods."""
def __init__(self, obj, api_prefix):
"""Initialize a RemoteApi with object and api prefix."""
self._obj = obj
self._api_prefix = api_prefix
def __getattr__(self, name):
"""Return wrapper to named api method."""
return functools.partial(self._obj.request, self._api_prefix + name)
def transform_keyerror(exc):
if isinstance(exc, NvimError):
if exc.args[0].startswith('Key not found:'):
return KeyError(exc.args[0])
if exc.args[0].startswith('Invalid option name:'):
return KeyError(exc.args[0])
return exc
class RemoteMap(object):
"""Represents a string->object map stored in Nvim.
This is the dict counterpart to the `RemoteSequence` class, but it is used
as a generic way of retrieving values from the various map-like data
structures present in Nvim.
It is used to provide a dict-like API to vim variables and options.
"""
_set = None
_del = None
def __init__(self, obj, get_method, set_method=None, del_method=None):
"""Initialize a RemoteMap with session, getter/setter."""
self._get = functools.partial(obj.request, get_method)
if set_method:
self._set = functools.partial(obj.request, set_method)
if del_method:
self._del = functools.partial(obj.request, del_method)
def __getitem__(self, key):
"""Return a map value by key."""
try:
return self._get(key)
except NvimError as exc:
raise transform_keyerror(exc)
def __setitem__(self, key, value):
"""Set a map value by key(if the setter was provided)."""
if not self._set:
raise TypeError('This dict is read-only')
self._set(key, value)
def __delitem__(self, key):
"""Delete a map value by associating None with the key."""
if not self._del:
raise TypeError('This dict is read-only')
try:
return self._del(key)
except NvimError as exc:
raise transform_keyerror(exc)
def __contains__(self, key):
"""Check if key is present in the map."""
try:
self._get(key)
return True
except Exception:
return False
def get(self, key, default=None):
"""Return value for key if present, else a default value."""
try:
return self.__getitem__(key)
except KeyError:
return default
class RemoteSequence(object):
"""Represents a sequence of objects stored in Nvim.
This class is used to wrap msgapck-rpc functions that work on Nvim
sequences(of lines, buffers, windows and tabpages) with an API that
is similar to the one provided by the python-vim interface.
For example, the 'windows' property of the `Nvim` class is a RemoteSequence
sequence instance, and the expression `nvim.windows[0]` is translated to
session.request('nvim_list_wins')[0].
One important detail about this class is that all methods will fetch the
sequence into a list and perform the necessary manipulation
locally(iteration, indexing, counting, etc).
"""
def __init__(self, session, method):
"""Initialize a RemoteSequence with session, method."""
self._fetch = functools.partial(session.request, method)
def __len__(self):
"""Return the length of the remote sequence."""
return len(self._fetch())
def __getitem__(self, idx):
"""Return a sequence item by index."""
if not isinstance(idx, slice):
return self._fetch()[idx]
return self._fetch()[idx.start:idx.stop]
def __iter__(self):
"""Return an iterator for the sequence."""
items = self._fetch()
for item in items:
yield item
def __contains__(self, item):
"""Check if an item is present in the sequence."""
return item in self._fetch()
def _identity(obj, session, method, kind):
return obj
def decode_if_bytes(obj, mode=True):
"""Decode obj if it is bytes."""
if mode is True:
mode = unicode_errors_default
if isinstance(obj, bytes):
return obj.decode("utf-8", errors=mode)
return obj
def walk(fn, obj, *args, **kwargs):
"""Recursively walk an object graph applying `fn`/`args` to objects."""
if type(obj) in [list, tuple]:
return list(walk(fn, o, *args) for o in obj)
if type(obj) is dict:
return dict((walk(fn, k, *args), walk(fn, v, *args)) for k, v in
obj.items())
return fn(obj, *args, **kwargs)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/api/nvim.py 0000644 0001750 0001750 00000046765 00000000000 016745 0 ustar 00bjorn bjorn 0000000 0000000 """Main Nvim interface."""
import os
import sys
import threading
from functools import partial
from traceback import format_stack
from msgpack import ExtType
from .buffer import Buffer
from .common import (NvimError, Remote, RemoteApi, RemoteMap, RemoteSequence,
decode_if_bytes, walk)
from .tabpage import Tabpage
from .window import Window
from ..compat import IS_PYTHON3
from ..util import Version, format_exc_skip
__all__ = ('Nvim')
os_chdir = os.chdir
lua_module = """
local a = vim.api
local function update_highlights(buf, src_id, hls, clear_first, clear_end)
if clear_first ~= nil then
a.nvim_buf_clear_highlight(buf, src_id, clear_first, clear_end)
end
for _,hl in pairs(hls) do
local group, line, col_start, col_end = unpack(hl)
if col_start == nil then
col_start = 0
end
if col_end == nil then
col_end = -1
end
a.nvim_buf_add_highlight(buf, src_id, group, line, col_start, col_end)
end
end
local chid = ...
local mod = {update_highlights=update_highlights}
_G["_pynvim_"..chid] = mod
"""
class Nvim(object):
"""Class that represents a remote Nvim instance.
This class is main entry point to Nvim remote API, it is a wrapper
around Session instances.
The constructor of this class must not be called directly. Instead, the
`from_session` class method should be used to create the first instance
from a raw `Session` instance.
Subsequent instances for the same session can be created by calling the
`with_decode` instance method to change the decoding behavior or
`SubClass.from_nvim(nvim)` where `SubClass` is a subclass of `Nvim`, which
is useful for having multiple `Nvim` objects that behave differently
without one affecting the other.
When this library is used on python3.4+, asyncio event loop is guaranteed
to be used. It is available as the "loop" attribute of this class. Note
that asyncio callbacks cannot make blocking requests, which includes
accessing state-dependent attributes. They should instead schedule another
callback using nvim.async_call, which will not have this restriction.
"""
@classmethod
def from_session(cls, session):
"""Create a new Nvim instance for a Session instance.
This method must be called to create the first Nvim instance, since it
queries Nvim metadata for type information and sets a SessionHook for
creating specialized objects from Nvim remote handles.
"""
session.error_wrapper = lambda e: NvimError(decode_if_bytes(e[1]))
channel_id, metadata = session.request(b'nvim_get_api_info')
if IS_PYTHON3:
# decode all metadata strings for python3
metadata = walk(decode_if_bytes, metadata)
types = {
metadata['types']['Buffer']['id']: Buffer,
metadata['types']['Window']['id']: Window,
metadata['types']['Tabpage']['id']: Tabpage,
}
return cls(session, channel_id, metadata, types)
@classmethod
def from_nvim(cls, nvim):
"""Create a new Nvim instance from an existing instance."""
return cls(nvim._session, nvim.channel_id, nvim.metadata,
nvim.types, nvim._decode, nvim._err_cb)
def __init__(self, session, channel_id, metadata, types,
decode=False, err_cb=None):
"""Initialize a new Nvim instance. This method is module-private."""
self._session = session
self.channel_id = channel_id
self.metadata = metadata
version = metadata.get("version", {"api_level": 0})
self.version = Version(**version)
self.types = types
self.api = RemoteApi(self, 'nvim_')
self.vars = RemoteMap(self, 'nvim_get_var', 'nvim_set_var', 'nvim_del_var')
self.vvars = RemoteMap(self, 'nvim_get_vvar', None, None)
self.options = RemoteMap(self, 'nvim_get_option', 'nvim_set_option')
self.buffers = Buffers(self)
self.windows = RemoteSequence(self, 'nvim_list_wins')
self.tabpages = RemoteSequence(self, 'nvim_list_tabpages')
self.current = Current(self)
self.session = CompatibilitySession(self)
self.funcs = Funcs(self)
self.lua = LuaFuncs(self)
self.error = NvimError
self._decode = decode
self._err_cb = err_cb
# only on python3.4+ we expose asyncio
if IS_PYTHON3:
self.loop = self._session.loop._loop
def _from_nvim(self, obj, decode=None):
if decode is None:
decode = self._decode
if type(obj) is ExtType:
cls = self.types[obj.code]
return cls(self, (obj.code, obj.data))
if decode:
obj = decode_if_bytes(obj, decode)
return obj
def _to_nvim(self, obj):
if isinstance(obj, Remote):
return ExtType(*obj.code_data)
return obj
def _get_lua_private(self):
if not getattr(self._session, "_has_lua", False):
self.exec_lua(lua_module, self.channel_id)
self._session._has_lua = True
return getattr(self.lua, "_pynvim_{}".format(self.channel_id))
def request(self, name, *args, **kwargs):
r"""Send an API request or notification to nvim.
It is rarely needed to call this function directly, as most API
functions have python wrapper functions. The `api` object can
be also be used to call API functions as methods:
vim.api.err_write('ERROR\n', async_=True)
vim.current.buffer.api.get_mark('.')
is equivalent to
vim.request('nvim_err_write', 'ERROR\n', async_=True)
vim.request('nvim_buf_get_mark', vim.current.buffer, '.')
Normally a blocking request will be sent. If the `async_` flag is
present and True, a asynchronous notification is sent instead. This
will never block, and the return value or error is ignored.
"""
if (self._session._loop_thread is not None
and threading.current_thread() != self._session._loop_thread):
msg = ("Request from non-main thread.\n"
"Requests from different threads should be wrapped "
"with nvim.async_call(cb, ...) \n{}\n"
.format('\n'.join(format_stack(None, 5)[:-1])))
self.async_call(self._err_cb, msg)
raise NvimError("request from non-main thread")
decode = kwargs.pop('decode', self._decode)
args = walk(self._to_nvim, args)
res = self._session.request(name, *args, **kwargs)
return walk(self._from_nvim, res, decode=decode)
def next_message(self):
"""Block until a message(request or notification) is available.
If any messages were previously enqueued, return the first in queue.
If not, run the event loop until one is received.
"""
msg = self._session.next_message()
if msg:
return walk(self._from_nvim, msg)
def run_loop(self, request_cb, notification_cb,
setup_cb=None, err_cb=None):
"""Run the event loop to receive requests and notifications from Nvim.
This should not be called from a plugin running in the host, which
already runs the loop and dispatches events to plugins.
"""
if err_cb is None:
err_cb = sys.stderr.write
self._err_cb = err_cb
def filter_request_cb(name, args):
name = self._from_nvim(name)
args = walk(self._from_nvim, args)
try:
result = request_cb(name, args)
except Exception:
msg = ("error caught in request handler '{} {}'\n{}\n\n"
.format(name, args, format_exc_skip(1)))
self._err_cb(msg)
raise
return walk(self._to_nvim, result)
def filter_notification_cb(name, args):
name = self._from_nvim(name)
args = walk(self._from_nvim, args)
try:
notification_cb(name, args)
except Exception:
msg = ("error caught in notification handler '{} {}'\n{}\n\n"
.format(name, args, format_exc_skip(1)))
self._err_cb(msg)
raise
self._session.run(filter_request_cb, filter_notification_cb, setup_cb)
def stop_loop(self):
"""Stop the event loop being started with `run_loop`."""
self._session.stop()
def close(self):
"""Close the nvim session and release its resources."""
self._session.close()
def __enter__(self):
"""Enter nvim session as a context manager."""
return self
def __exit__(self, *exc_info):
"""Exit nvim session as a context manager.
Closes the event loop.
"""
self.close()
def with_decode(self, decode=True):
"""Initialize a new Nvim instance."""
return Nvim(self._session, self.channel_id,
self.metadata, self.types, decode, self._err_cb)
def ui_attach(self, width, height, rgb=None, **kwargs):
"""Register as a remote UI.
After this method is called, the client will receive redraw
notifications.
"""
options = kwargs
if rgb is not None:
options['rgb'] = rgb
return self.request('nvim_ui_attach', width, height, options)
def ui_detach(self):
"""Unregister as a remote UI."""
return self.request('nvim_ui_detach')
def ui_try_resize(self, width, height):
"""Notify nvim that the client window has resized.
If possible, nvim will send a redraw request to resize.
"""
return self.request('ui_try_resize', width, height)
def subscribe(self, event):
"""Subscribe to a Nvim event."""
return self.request('nvim_subscribe', event)
def unsubscribe(self, event):
"""Unsubscribe to a Nvim event."""
return self.request('nvim_unsubscribe', event)
def command(self, string, **kwargs):
"""Execute a single ex command."""
return self.request('nvim_command', string, **kwargs)
def command_output(self, string):
"""Execute a single ex command and return the output."""
return self.request('nvim_command_output', string)
def eval(self, string, **kwargs):
"""Evaluate a vimscript expression."""
return self.request('nvim_eval', string, **kwargs)
def call(self, name, *args, **kwargs):
"""Call a vimscript function."""
return self.request('nvim_call_function', name, args, **kwargs)
def exec_lua(self, code, *args, **kwargs):
"""Execute lua code.
Additional parameters are available as `...` inside the lua chunk.
Only statements are executed. To evaluate an expression, prefix it
with `return`: `return my_function(...)`
There is a shorthand syntax to call lua functions with arguments:
nvim.lua.func(1,2)
nvim.lua.mymod.myfunction(data, async_=True)
is equivalent to
nvim.exec_lua("return func(...)", 1, 2)
nvim.exec_lua("mymod.myfunction(...)", data, async_=True)
Note that with `async_=True` there is no return value.
"""
return self.request('nvim_execute_lua', code, args, **kwargs)
def strwidth(self, string):
"""Return the number of display cells `string` occupies.
Tab is counted as one cell.
"""
return self.request('nvim_strwidth', string)
def list_runtime_paths(self):
"""Return a list of paths contained in the 'runtimepath' option."""
return self.request('nvim_list_runtime_paths')
def foreach_rtp(self, cb):
"""Invoke `cb` for each path in 'runtimepath'.
Call the given callable for each path in 'runtimepath' until either
callable returns something but None, the exception is raised or there
are no longer paths. If stopped in case callable returned non-None,
vim.foreach_rtp function returns the value returned by callable.
"""
for path in self.request('nvim_list_runtime_paths'):
try:
if cb(path) is not None:
break
except Exception:
break
def chdir(self, dir_path):
"""Run os.chdir, then all appropriate vim stuff."""
os_chdir(dir_path)
return self.request('nvim_set_current_dir', dir_path)
def feedkeys(self, keys, options='', escape_csi=True):
"""Push `keys` to Nvim user input buffer.
Options can be a string with the following character flags:
- 'm': Remap keys. This is default.
- 'n': Do not remap keys.
- 't': Handle keys as if typed; otherwise they are handled as if coming
from a mapping. This matters for undo, opening folds, etc.
"""
return self.request('nvim_feedkeys', keys, options, escape_csi)
def input(self, bytes):
"""Push `bytes` to Nvim low level input buffer.
Unlike `feedkeys()`, this uses the lowest level input buffer and the
call is not deferred. It returns the number of bytes actually
written(which can be less than what was requested if the buffer is
full).
"""
return self.request('nvim_input', bytes)
def replace_termcodes(self, string, from_part=False, do_lt=True,
special=True):
r"""Replace any terminal code strings by byte sequences.
The returned sequences are Nvim's internal representation of keys,
for example:
-> '\x1b'
-> '\r'
-> '\x0c'
-> '\x80ku'
The returned sequences can be used as input to `feedkeys`.
"""
return self.request('nvim_replace_termcodes', string,
from_part, do_lt, special)
def out_write(self, msg, **kwargs):
r"""Print `msg` as a normal message.
The message is buffered (won't display) until linefeed ("\n").
"""
return self.request('nvim_out_write', msg, **kwargs)
def err_write(self, msg, **kwargs):
r"""Print `msg` as an error message.
The message is buffered (won't display) until linefeed ("\n").
"""
if self._thread_invalid():
# special case: if a non-main thread writes to stderr
# i.e. due to an uncaught exception, pass it through
# without raising an additional exception.
self.async_call(self.err_write, msg, **kwargs)
return
return self.request('nvim_err_write', msg, **kwargs)
def _thread_invalid(self):
return (self._session._loop_thread is not None
and threading.current_thread() != self._session._loop_thread)
def quit(self, quit_command='qa!'):
"""Send a quit command to Nvim.
By default, the quit command is 'qa!' which will make Nvim quit without
saving anything.
"""
try:
self.command(quit_command)
except IOError:
# sending a quit command will raise an IOError because the
# connection is closed before a response is received. Safe to
# ignore it.
pass
def new_highlight_source(self):
"""Return new src_id for use with Buffer.add_highlight."""
return self.current.buffer.add_highlight("", 0, src_id=0)
def async_call(self, fn, *args, **kwargs):
"""Schedule `fn` to be called by the event loop soon.
This function is thread-safe, and is the only way code not
on the main thread could interact with nvim api objects.
This function can also be called in a synchronous
event handler, just before it returns, to defer execution
that shouldn't block neovim.
"""
call_point = ''.join(format_stack(None, 5)[:-1])
def handler():
try:
fn(*args, **kwargs)
except Exception as err:
msg = ("error caught while executing async callback:\n"
"{!r}\n{}\n \nthe call was requested at\n{}"
.format(err, format_exc_skip(1), call_point))
self._err_cb(msg)
raise
self._session.threadsafe_call(handler)
class Buffers(object):
"""Remote NVim buffers.
Currently the interface for interacting with remote NVim buffers is the
`nvim_list_bufs` msgpack-rpc function. Most methods fetch the list of
buffers from NVim.
Conforms to *python-buffers*.
"""
def __init__(self, nvim):
"""Initialize a Buffers object with Nvim object `nvim`."""
self._fetch_buffers = nvim.api.list_bufs
def __len__(self):
"""Return the count of buffers."""
return len(self._fetch_buffers())
def __getitem__(self, number):
"""Return the Buffer object matching buffer number `number`."""
for b in self._fetch_buffers():
if b.number == number:
return b
raise KeyError(number)
def __contains__(self, b):
"""Return whether Buffer `b` is a known valid buffer."""
return isinstance(b, Buffer) and b.valid
def __iter__(self):
"""Return an iterator over the list of buffers."""
return iter(self._fetch_buffers())
class CompatibilitySession(object):
"""Helper class for API compatibility."""
def __init__(self, nvim):
self.threadsafe_call = nvim.async_call
class Current(object):
"""Helper class for emulating vim.current from python-vim."""
def __init__(self, session):
self._session = session
self.range = None
@property
def line(self):
return self._session.request('nvim_get_current_line')
@line.setter
def line(self, line):
return self._session.request('nvim_set_current_line', line)
@line.deleter
def line(self):
return self._session.request('nvim_del_current_line')
@property
def buffer(self):
return self._session.request('nvim_get_current_buf')
@buffer.setter
def buffer(self, buffer):
return self._session.request('nvim_set_current_buf', buffer)
@property
def window(self):
return self._session.request('nvim_get_current_win')
@window.setter
def window(self, window):
return self._session.request('nvim_set_current_win', window)
@property
def tabpage(self):
return self._session.request('nvim_get_current_tabpage')
@tabpage.setter
def tabpage(self, tabpage):
return self._session.request('nvim_set_current_tabpage', tabpage)
class Funcs(object):
"""Helper class for functional vimscript interface."""
def __init__(self, nvim):
self._nvim = nvim
def __getattr__(self, name):
return partial(self._nvim.call, name)
class LuaFuncs(object):
"""Wrapper to allow lua functions to be called like python methods."""
def __init__(self, nvim, name=""):
self._nvim = nvim
self.name = name
def __getattr__(self, name):
"""Return wrapper to named api method."""
prefix = self.name + "." if self.name else ""
return LuaFuncs(self._nvim, prefix + name)
def __call__(self, *args, **kwargs):
# first new function after keyword rename, be a bit noisy
if 'async' in kwargs:
raise ValueError('"async" argument is not allowed. '
'Use "async_" instead.')
async_ = kwargs.get('async_', False)
pattern = "return {}(...)" if not async_ else "{}(...)"
code = pattern.format(self.name)
return self._nvim.exec_lua(code, *args, **kwargs)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/api/tabpage.py 0000644 0001750 0001750 00000001756 00000000000 017366 0 ustar 00bjorn bjorn 0000000 0000000 """API for working with Nvim tabpages."""
from .common import Remote, RemoteSequence
__all__ = ('Tabpage')
class Tabpage(Remote):
"""A remote Nvim tabpage."""
_api_prefix = "nvim_tabpage_"
def __init__(self, *args):
"""Initialize from session and code_data immutable object.
The `code_data` contains serialization information required for
msgpack-rpc calls. It must be immutable for Buffer equality to work.
"""
super(Tabpage, self).__init__(*args)
self.windows = RemoteSequence(self, 'nvim_tabpage_list_wins')
@property
def window(self):
"""Get the `Window` currently focused on the tabpage."""
return self.request('nvim_tabpage_get_win')
@property
def valid(self):
"""Return True if the tabpage still exists."""
return self.request('nvim_tabpage_is_valid')
@property
def number(self):
"""Get the tabpage number."""
return self.request('nvim_tabpage_get_number')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/api/window.py 0000644 0001750 0001750 00000003665 00000000000 017273 0 ustar 00bjorn bjorn 0000000 0000000 """API for working with Nvim windows."""
from .common import Remote
__all__ = ('Window')
class Window(Remote):
"""A remote Nvim window."""
_api_prefix = "nvim_win_"
@property
def buffer(self):
"""Get the `Buffer` currently being displayed by the window."""
return self.request('nvim_win_get_buf')
@property
def cursor(self):
"""Get the (row, col) tuple with the current cursor position."""
return self.request('nvim_win_get_cursor')
@cursor.setter
def cursor(self, pos):
"""Set the (row, col) tuple as the new cursor position."""
return self.request('nvim_win_set_cursor', pos)
@property
def height(self):
"""Get the window height in rows."""
return self.request('nvim_win_get_height')
@height.setter
def height(self, height):
"""Set the window height in rows."""
return self.request('nvim_win_set_height', height)
@property
def width(self):
"""Get the window width in rows."""
return self.request('nvim_win_get_width')
@width.setter
def width(self, width):
"""Set the window height in rows."""
return self.request('nvim_win_set_width', width)
@property
def row(self):
"""0-indexed, on-screen window position(row) in display cells."""
return self.request('nvim_win_get_position')[0]
@property
def col(self):
"""0-indexed, on-screen window position(col) in display cells."""
return self.request('nvim_win_get_position')[1]
@property
def tabpage(self):
"""Get the `Tabpage` that contains the window."""
return self.request('nvim_win_get_tabpage')
@property
def valid(self):
"""Return True if the window still exists."""
return self.request('nvim_win_is_valid')
@property
def number(self):
"""Get the window number."""
return self.request('nvim_win_get_number')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/compat.py 0000644 0001750 0001750 00000003202 00000000000 016461 0 ustar 00bjorn bjorn 0000000 0000000 """Code for compatibility across Python versions."""
import sys
import warnings
from imp import find_module as original_find_module
IS_PYTHON3 = sys.version_info >= (3, 0)
if IS_PYTHON3:
def find_module(fullname, path):
"""Compatibility wrapper for imp.find_module.
Automatically decodes arguments of find_module, in Python3
they must be Unicode
"""
if isinstance(fullname, bytes):
fullname = fullname.decode()
if isinstance(path, bytes):
path = path.decode()
elif isinstance(path, list):
newpath = []
for element in path:
if isinstance(element, bytes):
newpath.append(element.decode())
else:
newpath.append(element)
path = newpath
return original_find_module(fullname, path)
# There is no 'long' type in Python3 just int
long = int
unicode_errors_default = 'surrogateescape'
else:
find_module = original_find_module
unicode_errors_default = 'strict'
NUM_TYPES = (int, long, float)
def check_async(async_, kwargs, default):
"""Return a value of 'async' in kwargs or default when async_ is None.
This helper function exists for backward compatibility (See #274).
It shows a warning message when 'async' in kwargs is used to note users.
"""
if async_ is not None:
return async_
elif 'async' in kwargs:
warnings.warn(
'"async" attribute is deprecated. Use "async_" instead.',
DeprecationWarning,
)
return kwargs.pop('async')
else:
return default
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1579944220.1654134
pynvim-0.4.1/pynvim/msgpack_rpc/ 0000755 0001750 0001750 00000000000 00000000000 017120 5 ustar 00bjorn bjorn 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/msgpack_rpc/__init__.py 0000644 0001750 0001750 00000002616 00000000000 021236 0 ustar 00bjorn bjorn 0000000 0000000 """Msgpack-rpc subpackage.
This package implements a msgpack-rpc client. While it was designed for
handling some Nvim particularities(server->client requests for example), the
code here should work with other msgpack-rpc servers.
"""
from .async_session import AsyncSession
from .event_loop import EventLoop
from .msgpack_stream import MsgpackStream
from .session import ErrorResponse, Session
from ..util import get_client_info
__all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session',
'ErrorResponse')
def session(transport_type='stdio', *args, **kwargs):
loop = EventLoop(transport_type, *args, **kwargs)
msgpack_stream = MsgpackStream(loop)
async_session = AsyncSession(msgpack_stream)
session = Session(async_session)
session.request(b'nvim_set_client_info',
*get_client_info('client', 'remote', {}), async_=True)
return session
def tcp_session(address, port=7450):
"""Create a msgpack-rpc session from a tcp address/port."""
return session('tcp', address, port)
def socket_session(path):
"""Create a msgpack-rpc session from a unix domain socket."""
return session('socket', path)
def stdio_session():
"""Create a msgpack-rpc session from stdin/stdout."""
return session('stdio')
def child_session(argv):
"""Create a msgpack-rpc session from a new Nvim instance."""
return session('child', argv)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/msgpack_rpc/async_session.py 0000644 0001750 0001750 00000011677 00000000000 022366 0 ustar 00bjorn bjorn 0000000 0000000 """Asynchronous msgpack-rpc handling in the event loop pipeline."""
import logging
from traceback import format_exc
logger = logging.getLogger(__name__)
debug, info, warn = (logger.debug, logger.info, logger.warning,)
class AsyncSession(object):
"""Asynchronous msgpack-rpc layer that wraps a msgpack stream.
This wraps the msgpack stream interface for reading/writing msgpack
documents and exposes an interface for sending and receiving msgpack-rpc
requests and notifications.
"""
def __init__(self, msgpack_stream):
"""Wrap `msgpack_stream` on a msgpack-rpc interface."""
self._msgpack_stream = msgpack_stream
self._next_request_id = 1
self._pending_requests = {}
self._request_cb = self._notification_cb = None
self._handlers = {
0: self._on_request,
1: self._on_response,
2: self._on_notification
}
self.loop = msgpack_stream.loop
def threadsafe_call(self, fn):
"""Wrapper around `MsgpackStream.threadsafe_call`."""
self._msgpack_stream.threadsafe_call(fn)
def request(self, method, args, response_cb):
"""Send a msgpack-rpc request to Nvim.
A msgpack-rpc with method `method` and argument `args` is sent to
Nvim. The `response_cb` function is called with when the response
is available.
"""
request_id = self._next_request_id
self._next_request_id = request_id + 1
self._msgpack_stream.send([0, request_id, method, args])
self._pending_requests[request_id] = response_cb
def notify(self, method, args):
"""Send a msgpack-rpc notification to Nvim.
A msgpack-rpc with method `method` and argument `args` is sent to
Nvim. This will have the same effect as a request, but no response
will be recieved
"""
self._msgpack_stream.send([2, method, args])
def run(self, request_cb, notification_cb):
"""Run the event loop to receive requests and notifications from Nvim.
While the event loop is running, `request_cb` and `notification_cb`
will be called whenever requests or notifications are respectively
available.
"""
self._request_cb = request_cb
self._notification_cb = notification_cb
self._msgpack_stream.run(self._on_message)
self._request_cb = None
self._notification_cb = None
def stop(self):
"""Stop the event loop."""
self._msgpack_stream.stop()
def close(self):
"""Close the event loop."""
self._msgpack_stream.close()
def _on_message(self, msg):
try:
self._handlers.get(msg[0], self._on_invalid_message)(msg)
except Exception:
err_str = format_exc(5)
pass # replaces next logging statement
#warn(err_str)
self._msgpack_stream.send([1, 0, err_str, None])
def _on_request(self, msg):
# request
# - msg[1]: id
# - msg[2]: method name
# - msg[3]: arguments
pass # replaces next logging statement
#debug('received request: %s, %s', msg[2], msg[3])
self._request_cb(msg[2], msg[3], Response(self._msgpack_stream,
msg[1]))
def _on_response(self, msg):
# response to a previous request:
# - msg[1]: the id
# - msg[2]: error(if any)
# - msg[3]: result(if not errored)
pass # replaces next logging statement
#debug('received response: %s, %s', msg[2], msg[3])
self._pending_requests.pop(msg[1])(msg[2], msg[3])
def _on_notification(self, msg):
# notification/event
# - msg[1]: event name
# - msg[2]: arguments
pass # replaces next logging statement
#debug('received notification: %s, %s', msg[1], msg[2])
self._notification_cb(msg[1], msg[2])
def _on_invalid_message(self, msg):
error = 'Received invalid message %s' % msg
pass # replaces next logging statement
#warn(error)
self._msgpack_stream.send([1, 0, error, None])
class Response(object):
"""Response to a msgpack-rpc request that came from Nvim.
When Nvim sends a msgpack-rpc request, an instance of this class is
created for remembering state required to send a response.
"""
def __init__(self, msgpack_stream, request_id):
"""Initialize the Response instance."""
self._msgpack_stream = msgpack_stream
self._request_id = request_id
def send(self, value, error=False):
"""Send the response.
If `error` is True, it will be sent as an error.
"""
if error:
resp = [1, self._request_id, value, None]
else:
resp = [1, self._request_id, None, value]
pass # replaces next logging statement
#debug('sending response to request %d: %s', self._request_id, resp)
self._msgpack_stream.send(resp)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1579944220.1687467
pynvim-0.4.1/pynvim/msgpack_rpc/event_loop/ 0000755 0001750 0001750 00000000000 00000000000 021272 5 ustar 00bjorn bjorn 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/msgpack_rpc/event_loop/__init__.py 0000644 0001750 0001750 00000001311 00000000000 023377 0 ustar 00bjorn bjorn 0000000 0000000 """Event loop abstraction subpackage.
Tries to use pyuv as a backend, falling back to the asyncio implementation.
"""
from ...compat import IS_PYTHON3
# on python3 we only support asyncio, as we expose it to plugins
if IS_PYTHON3:
from .asyncio import AsyncioEventLoop
EventLoop = AsyncioEventLoop
else:
try:
# libuv is fully implemented in C, use it when available
from .uv import UvEventLoop
EventLoop = UvEventLoop
except ImportError:
# asyncio(trollius on python 2) is pure python and should be more
# portable across python implementations
from .asyncio import AsyncioEventLoop
EventLoop = AsyncioEventLoop
__all__ = ('EventLoop')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/msgpack_rpc/event_loop/asyncio.py 0000644 0001750 0001750 00000012704 00000000000 023315 0 ustar 00bjorn bjorn 0000000 0000000 """Event loop implementation that uses the `asyncio` standard module.
The `asyncio` module was added to python standard library on 3.4, and it
provides a pure python implementation of an event loop library. It is used
as a fallback in case pyuv is not available(on python implementations other
than CPython).
Earlier python versions are supported through the `trollius` package, which
is a backport of `asyncio` that works on Python 2.6+.
"""
from __future__ import absolute_import
import logging
import os
import sys
from collections import deque
try:
# For python 3.4+, use the standard library module
import asyncio
except (ImportError, SyntaxError):
# Fallback to trollius
import trollius as asyncio
from .base import BaseEventLoop
logger = logging.getLogger(__name__)
debug, info, warn = (logger.debug, logger.info, logger.warning,)
loop_cls = asyncio.SelectorEventLoop
if os.name == 'nt':
from asyncio.windows_utils import PipeHandle
import msvcrt
# On windows use ProactorEventLoop which support pipes and is backed by the
# more powerful IOCP facility
# NOTE: we override in the stdio case, because it doesn't work.
loop_cls = asyncio.ProactorEventLoop
class AsyncioEventLoop(BaseEventLoop, asyncio.Protocol,
asyncio.SubprocessProtocol):
"""`BaseEventLoop` subclass that uses `asyncio` as a backend."""
def connection_made(self, transport):
"""Used to signal `asyncio.Protocol` of a successful connection."""
self._transport = transport
self._raw_transport = transport
if isinstance(transport, asyncio.SubprocessTransport):
self._transport = transport.get_pipe_transport(0)
def connection_lost(self, exc):
"""Used to signal `asyncio.Protocol` of a lost connection."""
self._on_error(exc.args[0] if exc else 'EOF')
def data_received(self, data):
"""Used to signal `asyncio.Protocol` of incoming data."""
if self._on_data:
self._on_data(data)
return
self._queued_data.append(data)
def pipe_connection_lost(self, fd, exc):
"""Used to signal `asyncio.SubprocessProtocol` of a lost connection."""
self._on_error(exc.args[0] if exc else 'EOF')
def pipe_data_received(self, fd, data):
"""Used to signal `asyncio.SubprocessProtocol` of incoming data."""
if fd == 2: # stderr fd number
self._on_stderr(data)
elif self._on_data:
self._on_data(data)
else:
self._queued_data.append(data)
def process_exited(self):
"""Used to signal `asyncio.SubprocessProtocol` when the child exits."""
self._on_error('EOF')
def _init(self):
self._loop = loop_cls()
self._queued_data = deque()
self._fact = lambda: self
self._raw_transport = None
def _connect_tcp(self, address, port):
coroutine = self._loop.create_connection(self._fact, address, port)
self._loop.run_until_complete(coroutine)
def _connect_socket(self, path):
if os.name == 'nt':
coroutine = self._loop.create_pipe_connection(self._fact, path)
else:
coroutine = self._loop.create_unix_connection(self._fact, path)
self._loop.run_until_complete(coroutine)
def _connect_stdio(self):
if os.name == 'nt':
pipe = PipeHandle(msvcrt.get_osfhandle(sys.stdin.fileno()))
else:
pipe = sys.stdin
coroutine = self._loop.connect_read_pipe(self._fact, pipe)
self._loop.run_until_complete(coroutine)
pass # replaces next logging statement
#debug("native stdin connection successful")
# Make sure subprocesses don't clobber stdout,
# send the output to stderr instead.
rename_stdout = os.dup(sys.stdout.fileno())
os.dup2(sys.stderr.fileno(), sys.stdout.fileno())
if os.name == 'nt':
pipe = PipeHandle(msvcrt.get_osfhandle(rename_stdout))
else:
pipe = os.fdopen(rename_stdout, 'wb')
coroutine = self._loop.connect_write_pipe(self._fact, pipe)
self._loop.run_until_complete(coroutine)
pass # replaces next logging statement
#debug("native stdout connection successful")
def _connect_child(self, argv):
if os.name != 'nt':
self._child_watcher = asyncio.get_child_watcher()
self._child_watcher.attach_loop(self._loop)
coroutine = self._loop.subprocess_exec(self._fact, *argv)
self._loop.run_until_complete(coroutine)
def _start_reading(self):
pass
def _send(self, data):
self._transport.write(data)
def _run(self):
while self._queued_data:
self._on_data(self._queued_data.popleft())
self._loop.run_forever()
def _stop(self):
self._loop.stop()
def _close(self):
if self._raw_transport is not None:
self._raw_transport.close()
self._loop.close()
def _threadsafe_call(self, fn):
self._loop.call_soon_threadsafe(fn)
def _setup_signals(self, signals):
if os.name == 'nt':
# add_signal_handler is not supported in win32
self._signals = []
return
self._signals = list(signals)
for signum in self._signals:
self._loop.add_signal_handler(signum, self._on_signal, signum)
def _teardown_signals(self):
for signum in self._signals:
self._loop.remove_signal_handler(signum)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/msgpack_rpc/event_loop/base.py 0000644 0001750 0001750 00000016506 00000000000 022566 0 ustar 00bjorn bjorn 0000000 0000000 """Common code for event loop implementations."""
import logging
import signal
import threading
logger = logging.getLogger(__name__)
debug, info, warn = (logger.debug, logger.info, logger.warning,)
# When signals are restored, the event loop library may reset SIGINT to SIG_DFL
# which exits the program. To be able to restore the python interpreter to it's
# default state, we keep a reference to the default handler
default_int_handler = signal.getsignal(signal.SIGINT)
main_thread = threading.current_thread()
class BaseEventLoop(object):
"""Abstract base class for all event loops.
Event loops act as the bottom layer for Nvim sessions created by this
library. They hide system/transport details behind a simple interface for
reading/writing bytes to the connected Nvim instance.
This class exposes public methods for interacting with the underlying
event loop and delegates implementation-specific work to the following
methods, which subclasses are expected to implement:
- `_init()`: Implementation-specific initialization
- `_connect_tcp(address, port)`: connect to Nvim using tcp/ip
- `_connect_socket(path)`: Same as tcp, but use a UNIX domain socket or
named pipe.
- `_connect_stdio()`: Use stdin/stdout as the connection to Nvim
- `_connect_child(argv)`: Use the argument vector `argv` to spawn an
embedded Nvim that has its stdin/stdout connected to the event loop.
- `_start_reading()`: Called after any of _connect_* methods. Can be used
to perform any post-connection setup or validation.
- `_send(data)`: Send `data`(byte array) to Nvim. The data is only
- `_run()`: Runs the event loop until stopped or the connection is closed.
calling the following methods when some event happens:
actually sent when the event loop is running.
- `_on_data(data)`: When Nvim sends some data.
- `_on_signal(signum)`: When a signal is received.
- `_on_error(message)`: When a non-recoverable error occurs(eg:
connection lost)
- `_stop()`: Stop the event loop
- `_interrupt(data)`: Like `stop()`, but may be called from other threads
this.
- `_setup_signals(signals)`: Add implementation-specific listeners for
for `signals`, which is a list of OS-specific signal numbers.
- `_teardown_signals()`: Removes signal listeners set by `_setup_signals`
"""
def __init__(self, transport_type, *args):
"""Initialize and connect the event loop instance.
The only arguments are the transport type and transport-specific
configuration, like this:
>>> BaseEventLoop('tcp', '127.0.0.1', 7450)
Traceback (most recent call last):
...
AttributeError: 'BaseEventLoop' object has no attribute '_init'
>>> BaseEventLoop('socket', '/tmp/nvim-socket')
Traceback (most recent call last):
...
AttributeError: 'BaseEventLoop' object has no attribute '_init'
>>> BaseEventLoop('stdio')
Traceback (most recent call last):
...
AttributeError: 'BaseEventLoop' object has no attribute '_init'
>>> BaseEventLoop('child',
['nvim', '--embed', '--headless', '-u', 'NONE'])
Traceback (most recent call last):
...
AttributeError: 'BaseEventLoop' object has no attribute '_init'
This calls the implementation-specific initialization
`_init`, one of the `_connect_*` methods(based on `transport_type`)
and `_start_reading()`
"""
self._transport_type = transport_type
self._signames = dict((k, v) for v, k in signal.__dict__.items()
if v.startswith('SIG'))
self._on_data = None
self._error = None
self._init()
try:
getattr(self, '_connect_{}'.format(transport_type))(*args)
except Exception as e:
self.close()
raise e
self._start_reading()
def connect_tcp(self, address, port):
"""Connect to tcp/ip `address`:`port`. Delegated to `_connect_tcp`."""
pass # replaces next logging statement
#info('Connecting to TCP address: %s:%d', address, port)
self._connect_tcp(address, port)
def connect_socket(self, path):
"""Connect to socket at `path`. Delegated to `_connect_socket`."""
pass # replaces next logging statement
#info('Connecting to %s', path)
self._connect_socket(path)
def connect_stdio(self):
"""Connect using stdin/stdout. Delegated to `_connect_stdio`."""
pass # replaces next logging statement
#info('Preparing stdin/stdout for streaming data')
self._connect_stdio()
def connect_child(self, argv):
"""Connect a new Nvim instance. Delegated to `_connect_child`."""
pass # replaces next logging statement
#info('Spawning a new nvim instance')
self._connect_child(argv)
def send(self, data):
"""Queue `data` for sending to Nvim."""
pass # replaces next logging statement
#debug("Sending '%s'", data)
self._send(data)
def threadsafe_call(self, fn):
"""Call a function in the event loop thread.
This is the only safe way to interact with a session from other
threads.
"""
self._threadsafe_call(fn)
def run(self, data_cb):
"""Run the event loop."""
if self._error:
err = self._error
if isinstance(self._error, KeyboardInterrupt):
# KeyboardInterrupt is not destructive(it may be used in
# the REPL).
# After throwing KeyboardInterrupt, cleanup the _error field
# so the loop may be started again
self._error = None
raise err
self._on_data = data_cb
if threading.current_thread() == main_thread:
self._setup_signals([signal.SIGINT, signal.SIGTERM])
pass # replaces next logging statement
#debug('Entering event loop')
self._run()
pass # replaces next logging statement
#debug('Exited event loop')
if threading.current_thread() == main_thread:
self._teardown_signals()
signal.signal(signal.SIGINT, default_int_handler)
self._on_data = None
def stop(self):
"""Stop the event loop."""
self._stop()
pass # replaces next logging statement
#debug('Stopped event loop')
def close(self):
"""Stop the event loop."""
self._close()
pass # replaces next logging statement
#debug('Closed event loop')
def _on_signal(self, signum):
msg = 'Received {}'.format(self._signames[signum])
pass # replaces next logging statement
#debug(msg)
if signum == signal.SIGINT and self._transport_type == 'stdio':
# When the transport is stdio, we are probably running as a Nvim
# child process. In that case, we don't want to be killed by
# ctrl+C
return
cls = Exception
if signum == signal.SIGINT:
cls = KeyboardInterrupt
self._error = cls(msg)
self.stop()
def _on_error(self, error):
pass # replaces next logging statement
#debug(error)
self._error = IOError(error)
self.stop()
def _on_interrupt(self):
self.stop()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/msgpack_rpc/event_loop/uv.py 0000644 0001750 0001750 00000007676 00000000000 022316 0 ustar 00bjorn bjorn 0000000 0000000 """Event loop implementation that uses pyuv(libuv-python bindings)."""
import sys
from collections import deque
import pyuv
from .base import BaseEventLoop
class UvEventLoop(BaseEventLoop):
"""`BaseEventLoop` subclass that uses `pvuv` as a backend."""
def _init(self):
self._loop = pyuv.Loop()
self._async = pyuv.Async(self._loop, self._on_async)
self._connection_error = None
self._error_stream = None
self._callbacks = deque()
def _on_connect(self, stream, error):
self.stop()
if error:
msg = 'Cannot connect to {}: {}'.format(
self._connect_address, pyuv.errno.strerror(error))
self._connection_error = IOError(msg)
return
self._read_stream = self._write_stream = stream
def _on_read(self, handle, data, error):
if error or not data:
msg = pyuv.errno.strerror(error) if error else 'EOF'
self._on_error(msg)
return
if handle == self._error_stream:
return
self._on_data(data)
def _on_write(self, handle, error):
if error:
msg = pyuv.errno.strerror(error)
self._on_error(msg)
def _on_exit(self, handle, exit_status, term_signal):
self._on_error('EOF')
def _disconnected(self, *args):
raise IOError('Not connected to Nvim')
def _connect_tcp(self, address, port):
stream = pyuv.TCP(self._loop)
self._connect_address = '{}:{}'.format(address, port)
stream.connect((address, port), self._on_connect)
def _connect_socket(self, path):
stream = pyuv.Pipe(self._loop)
self._connect_address = path
stream.connect(path, self._on_connect)
def _connect_stdio(self):
self._read_stream = pyuv.Pipe(self._loop)
self._read_stream.open(sys.stdin.fileno())
self._write_stream = pyuv.Pipe(self._loop)
self._write_stream.open(sys.stdout.fileno())
def _connect_child(self, argv):
self._write_stream = pyuv.Pipe(self._loop)
self._read_stream = pyuv.Pipe(self._loop)
self._error_stream = pyuv.Pipe(self._loop)
stdin = pyuv.StdIO(self._write_stream,
flags=pyuv.UV_CREATE_PIPE + pyuv.UV_READABLE_PIPE)
stdout = pyuv.StdIO(self._read_stream,
flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE)
stderr = pyuv.StdIO(self._error_stream,
flags=pyuv.UV_CREATE_PIPE + pyuv.UV_WRITABLE_PIPE)
pyuv.Process.spawn(self._loop,
args=argv,
exit_callback=self._on_exit,
flags=pyuv.UV_PROCESS_WINDOWS_HIDE,
stdio=(stdin, stdout, stderr,))
self._error_stream.start_read(self._on_read)
def _start_reading(self):
if self._transport_type in ['tcp', 'socket']:
self._loop.run()
if self._connection_error:
self.run = self.send = self._disconnected
raise self._connection_error
self._read_stream.start_read(self._on_read)
def _send(self, data):
self._write_stream.write(data, self._on_write)
def _run(self):
self._loop.run(pyuv.UV_RUN_DEFAULT)
def _stop(self):
self._loop.stop()
def _close(self):
pass
def _threadsafe_call(self, fn):
self._callbacks.append(fn)
self._async.send()
def _on_async(self, handle):
while self._callbacks:
self._callbacks.popleft()()
def _setup_signals(self, signals):
self._signal_handles = []
def handler(h, signum):
self._on_signal(signum)
for signum in signals:
handle = pyuv.Signal(self._loop)
handle.start(handler, signum)
self._signal_handles.append(handle)
def _teardown_signals(self):
for handle in self._signal_handles:
handle.stop()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/msgpack_rpc/msgpack_stream.py 0000644 0001750 0001750 00000004204 00000000000 022472 0 ustar 00bjorn bjorn 0000000 0000000 """Msgpack handling in the event loop pipeline."""
import logging
from msgpack import Packer, Unpacker
from ..compat import unicode_errors_default
logger = logging.getLogger(__name__)
debug, info, warn = (logger.debug, logger.info, logger.warning,)
class MsgpackStream(object):
"""Two-way msgpack stream that wraps a event loop byte stream.
This wraps the event loop interface for reading/writing bytes and
exposes an interface for reading/writing msgpack documents.
"""
def __init__(self, event_loop):
"""Wrap `event_loop` on a msgpack-aware interface."""
self.loop = event_loop
self._packer = Packer(unicode_errors=unicode_errors_default)
self._unpacker = Unpacker()
self._message_cb = None
def threadsafe_call(self, fn):
"""Wrapper around `BaseEventLoop.threadsafe_call`."""
self.loop.threadsafe_call(fn)
def send(self, msg):
"""Queue `msg` for sending to Nvim."""
pass # replaces next logging statement
#debug('sent %s', msg)
self.loop.send(self._packer.pack(msg))
def run(self, message_cb):
"""Run the event loop to receive messages from Nvim.
While the event loop is running, `message_cb` will be called whenever
a message has been successfully parsed from the input stream.
"""
self._message_cb = message_cb
self.loop.run(self._on_data)
self._message_cb = None
def stop(self):
"""Stop the event loop."""
self.loop.stop()
def close(self):
"""Close the event loop."""
self.loop.close()
def _on_data(self, data):
self._unpacker.feed(data)
while True:
try:
pass # replaces next logging statement
#debug('waiting for message...')
msg = next(self._unpacker)
pass # replaces next logging statement
#debug('received message: %s', msg)
self._message_cb(msg)
except StopIteration:
pass # replaces next logging statement
#debug('unpacker needs more data...')
break
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/msgpack_rpc/session.py 0000644 0001750 0001750 00000021774 00000000000 021170 0 ustar 00bjorn bjorn 0000000 0000000 """Synchronous msgpack-rpc session layer."""
import logging
import threading
from collections import deque
from traceback import format_exc
import greenlet
from ..compat import check_async
logger = logging.getLogger(__name__)
error, debug, info, warn = (logger.error, logger.debug, logger.info,
logger.warning,)
class Session(object):
"""Msgpack-rpc session layer that uses coroutines for a synchronous API.
This class provides the public msgpack-rpc API required by this library.
It uses the greenlet module to handle requests and notifications coming
from Nvim with a synchronous API.
"""
def __init__(self, async_session):
"""Wrap `async_session` on a synchronous msgpack-rpc interface."""
self._async_session = async_session
self._request_cb = self._notification_cb = None
self._pending_messages = deque()
self._is_running = False
self._setup_exception = None
self.loop = async_session.loop
self._loop_thread = None
def threadsafe_call(self, fn, *args, **kwargs):
"""Wrapper around `AsyncSession.threadsafe_call`."""
def handler():
try:
fn(*args, **kwargs)
except Exception:
pass # replaces next logging statement
#warn("error caught while excecuting async callback\n%s\n",
#format_exc())
def greenlet_wrapper():
gr = greenlet.greenlet(handler)
gr.switch()
self._async_session.threadsafe_call(greenlet_wrapper)
def next_message(self):
"""Block until a message(request or notification) is available.
If any messages were previously enqueued, return the first in queue.
If not, run the event loop until one is received.
"""
if self._is_running:
raise Exception('Event loop already running')
if self._pending_messages:
return self._pending_messages.popleft()
self._async_session.run(self._enqueue_request_and_stop,
self._enqueue_notification_and_stop)
if self._pending_messages:
return self._pending_messages.popleft()
def request(self, method, *args, **kwargs):
"""Send a msgpack-rpc request and block until as response is received.
If the event loop is running, this method must have been called by a
request or notification handler running on a greenlet. In that case,
send the quest and yield to the parent greenlet until a response is
available.
When the event loop is not running, it will perform a blocking request
like this:
- Send the request
- Run the loop until the response is available
- Put requests/notifications received while waiting into a queue
If the `async_` flag is present and True, a asynchronous notification
is sent instead. This will never block, and the return value or error
is ignored.
"""
async_ = check_async(kwargs.pop('async_', None), kwargs, False)
if async_:
self._async_session.notify(method, args)
return
if kwargs:
raise ValueError("request got unsupported keyword argument(s): {}"
.format(', '.join(kwargs.keys())))
if self._is_running:
v = self._yielding_request(method, args)
else:
v = self._blocking_request(method, args)
if not v:
# EOF
raise IOError('EOF')
err, rv = v
if err:
pass # replaces next logging statement
#info("'Received error: %s", err)
raise self.error_wrapper(err)
return rv
def run(self, request_cb, notification_cb, setup_cb=None):
"""Run the event loop to receive requests and notifications from Nvim.
Like `AsyncSession.run()`, but `request_cb` and `notification_cb` are
inside greenlets.
"""
self._request_cb = request_cb
self._notification_cb = notification_cb
self._is_running = True
self._setup_exception = None
self._loop_thread = threading.current_thread()
def on_setup():
try:
setup_cb()
except Exception as e:
self._setup_exception = e
self.stop()
if setup_cb:
# Create a new greenlet to handle the setup function
gr = greenlet.greenlet(on_setup)
gr.switch()
if self._setup_exception:
pass # replaces next logging statement
#error('Setup error: {}'.format(self._setup_exception))
raise self._setup_exception
# Process all pending requests and notifications
while self._pending_messages:
msg = self._pending_messages.popleft()
getattr(self, '_on_{}'.format(msg[0]))(*msg[1:])
self._async_session.run(self._on_request, self._on_notification)
self._is_running = False
self._request_cb = None
self._notification_cb = None
self._loop_thread = None
if self._setup_exception:
raise self._setup_exception
def stop(self):
"""Stop the event loop."""
self._async_session.stop()
def close(self):
"""Close the event loop."""
self._async_session.close()
def _yielding_request(self, method, args):
gr = greenlet.getcurrent()
parent = gr.parent
def response_cb(err, rv):
pass # replaces next logging statement
#debug('response is available for greenlet %s, switching back', gr)
gr.switch(err, rv)
self._async_session.request(method, args, response_cb)
pass # replaces next logging statement
#debug('yielding from greenlet %s to wait for response', gr)
return parent.switch()
def _blocking_request(self, method, args):
result = []
def response_cb(err, rv):
result.extend([err, rv])
self.stop()
self._async_session.request(method, args, response_cb)
self._async_session.run(self._enqueue_request,
self._enqueue_notification)
return result
def _enqueue_request_and_stop(self, name, args, response):
self._enqueue_request(name, args, response)
self.stop()
def _enqueue_notification_and_stop(self, name, args):
self._enqueue_notification(name, args)
self.stop()
def _enqueue_request(self, name, args, response):
self._pending_messages.append(('request', name, args, response,))
def _enqueue_notification(self, name, args):
self._pending_messages.append(('notification', name, args,))
def _on_request(self, name, args, response):
def handler():
try:
rv = self._request_cb(name, args)
pass # replaces next logging statement
#debug('greenlet %s finished executing, '
#+ 'sending %s as response', gr, rv)
response.send(rv)
except ErrorResponse as err:
pass # replaces next logging statement
#warn("error response from request '%s %s': %s", name,
#args, format_exc())
response.send(err.args[0], error=True)
except Exception as err:
pass # replaces next logging statement
#warn("error caught while processing request '%s %s': %s", name,
#args, format_exc())
response.send(repr(err) + "\n" + format_exc(5), error=True)
pass # replaces next logging statement
#debug('greenlet %s is now dying...', gr)
# Create a new greenlet to handle the request
gr = greenlet.greenlet(handler)
pass # replaces next logging statement
#debug('received rpc request, greenlet %s will handle it', gr)
gr.switch()
def _on_notification(self, name, args):
def handler():
try:
self._notification_cb(name, args)
pass # replaces next logging statement
#debug('greenlet %s finished executing', gr)
except Exception:
pass # replaces next logging statement
#warn("error caught while processing notification '%s %s': %s",
#name, args, format_exc())
pass # replaces next logging statement
#debug('greenlet %s is now dying...', gr)
gr = greenlet.greenlet(handler)
pass # replaces next logging statement
#debug('received rpc notification, greenlet %s will handle it', gr)
gr.switch()
class ErrorResponse(BaseException):
"""Raise this in a request handler to respond with a given error message.
Unlike when other exceptions are caught, this gives full control off the
error response sent. When "ErrorResponse(msg)" is caught "msg" will be
sent verbatim as the error response.No traceback will be appended.
"""
pass
././@PaxHeader 0000000 0000000 0000000 00000000032 00000000000 011450 x ustar 00 0000000 0000000 26 mtime=1579944220.17208
pynvim-0.4.1/pynvim/plugin/ 0000755 0001750 0001750 00000000000 00000000000 016125 5 ustar 00bjorn bjorn 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/plugin/__init__.py 0000644 0001750 0001750 00000000477 00000000000 020246 0 ustar 00bjorn bjorn 0000000 0000000 """Nvim plugin/host subpackage."""
from .decorators import (autocmd, command, decode, encoding, function,
plugin, rpc_export, shutdown_hook)
from .host import Host
__all__ = ('Host', 'plugin', 'rpc_export', 'command', 'autocmd',
'function', 'encoding', 'decode', 'shutdown_hook')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/plugin/decorators.py 0000644 0001750 0001750 00000011016 00000000000 020643 0 ustar 00bjorn bjorn 0000000 0000000 """Decorators used by python host plugin system."""
import inspect
import logging
from ..compat import IS_PYTHON3, unicode_errors_default
logger = logging.getLogger(__name__)
debug, info, warn = (logger.debug, logger.info, logger.warning,)
__all__ = ('plugin', 'rpc_export', 'command', 'autocmd', 'function',
'encoding', 'decode', 'shutdown_hook')
def plugin(cls):
"""Tag a class as a plugin.
This decorator is required to make the class methods discoverable by the
plugin_load method of the host.
"""
cls._nvim_plugin = True
# the _nvim_bind attribute is set to True by default, meaning that
# decorated functions have a bound Nvim instance as first argument.
# For methods in a plugin-decorated class this is not required, because
# the class initializer will already receive the nvim object.
predicate = lambda fn: hasattr(fn, '_nvim_bind')
for _, fn in inspect.getmembers(cls, predicate):
if IS_PYTHON3:
fn._nvim_bind = False
else:
fn.im_func._nvim_bind = False
return cls
def rpc_export(rpc_method_name, sync=False):
"""Export a function or plugin method as a msgpack-rpc request handler."""
def dec(f):
f._nvim_rpc_method_name = rpc_method_name
f._nvim_rpc_sync = sync
f._nvim_bind = True
f._nvim_prefix_plugin_path = False
return f
return dec
def command(name, nargs=0, complete=None, range=None, count=None, bang=False,
register=False, sync=False, allow_nested=False, eval=None):
"""Tag a function or plugin method as a Nvim command handler."""
def dec(f):
f._nvim_rpc_method_name = 'command:{}'.format(name)
f._nvim_rpc_sync = sync
f._nvim_bind = True
f._nvim_prefix_plugin_path = True
opts = {}
if range is not None:
opts['range'] = '' if range is True else str(range)
elif count is not None:
opts['count'] = count
if bang:
opts['bang'] = ''
if register:
opts['register'] = ''
if nargs:
opts['nargs'] = nargs
if complete:
opts['complete'] = complete
if eval:
opts['eval'] = eval
if not sync and allow_nested:
rpc_sync = "urgent"
else:
rpc_sync = sync
f._nvim_rpc_spec = {
'type': 'command',
'name': name,
'sync': rpc_sync,
'opts': opts
}
return f
return dec
def autocmd(name, pattern='*', sync=False, allow_nested=False, eval=None):
"""Tag a function or plugin method as a Nvim autocommand handler."""
def dec(f):
f._nvim_rpc_method_name = 'autocmd:{}:{}'.format(name, pattern)
f._nvim_rpc_sync = sync
f._nvim_bind = True
f._nvim_prefix_plugin_path = True
opts = {
'pattern': pattern
}
if eval:
opts['eval'] = eval
if not sync and allow_nested:
rpc_sync = "urgent"
else:
rpc_sync = sync
f._nvim_rpc_spec = {
'type': 'autocmd',
'name': name,
'sync': rpc_sync,
'opts': opts
}
return f
return dec
def function(name, range=False, sync=False, allow_nested=False, eval=None):
"""Tag a function or plugin method as a Nvim function handler."""
def dec(f):
f._nvim_rpc_method_name = 'function:{}'.format(name)
f._nvim_rpc_sync = sync
f._nvim_bind = True
f._nvim_prefix_plugin_path = True
opts = {}
if range:
opts['range'] = '' if range is True else str(range)
if eval:
opts['eval'] = eval
if not sync and allow_nested:
rpc_sync = "urgent"
else:
rpc_sync = sync
f._nvim_rpc_spec = {
'type': 'function',
'name': name,
'sync': rpc_sync,
'opts': opts
}
return f
return dec
def shutdown_hook(f):
"""Tag a function or method as a shutdown hook."""
f._nvim_shutdown_hook = True
f._nvim_bind = True
return f
def decode(mode=unicode_errors_default):
"""Configure automatic encoding/decoding of strings."""
def dec(f):
f._nvim_decode = mode
return f
return dec
def encoding(encoding=True):
"""DEPRECATED: use pynvim.decode()."""
if isinstance(encoding, str):
encoding = True
def dec(f):
f._nvim_decode = encoding
return f
return dec
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/plugin/host.py 0000644 0001750 0001750 00000024773 00000000000 017471 0 ustar 00bjorn bjorn 0000000 0000000 """Implements a Nvim host for python plugins."""
import imp
import inspect
import logging
import os
import os.path
import re
from functools import partial
from traceback import format_exc
from . import script_host
from ..api import decode_if_bytes, walk
from ..compat import IS_PYTHON3, find_module
from ..msgpack_rpc import ErrorResponse
from ..util import format_exc_skip, get_client_info
__all__ = ('Host')
logger = logging.getLogger(__name__)
error, debug, info, warn = (logger.error, logger.debug, logger.info,
logger.warning,)
host_method_spec = {"poll": {}, "specs": {"nargs": 1}, "shutdown": {}}
class Host(object):
"""Nvim host for python plugins.
Takes care of loading/unloading plugins and routing msgpack-rpc
requests/notifications to the appropriate handlers.
"""
def __init__(self, nvim):
"""Set handlers for plugin_load/plugin_unload."""
self.nvim = nvim
self._specs = {}
self._loaded = {}
self._load_errors = {}
self._notification_handlers = {
'nvim_error_event': self._on_error_event
}
self._request_handlers = {
'poll': lambda: 'ok',
'specs': self._on_specs_request,
'shutdown': self.shutdown
}
# Decode per default for Python3
self._decode_default = IS_PYTHON3
def _on_async_err(self, msg):
# uncaught python exception
self.nvim.err_write(msg, async_=True)
def _on_error_event(self, kind, msg):
# error from nvim due to async request
# like nvim.command(..., async_=True)
errmsg = "{}: Async request caused an error:\n{}\n".format(
self.name, decode_if_bytes(msg))
self.nvim.err_write(errmsg, async_=True)
return errmsg
def start(self, plugins):
"""Start listening for msgpack-rpc requests and notifications."""
self.nvim.run_loop(self._on_request,
self._on_notification,
lambda: self._load(plugins),
err_cb=self._on_async_err)
def shutdown(self):
"""Shutdown the host."""
self._unload()
self.nvim.stop_loop()
def _wrap_delayed_function(self, cls, delayed_handlers, name, sync,
module_handlers, path, *args):
# delete the delayed handlers to be sure
for handler in delayed_handlers:
method_name = handler._nvim_registered_name
if handler._nvim_rpc_sync:
del self._request_handlers[method_name]
else:
del self._notification_handlers[method_name]
# create an instance of the plugin and pass the nvim object
plugin = cls(self._configure_nvim_for(cls))
# discover handlers in the plugin instance
self._discover_functions(plugin, module_handlers, path, False)
if sync:
self._request_handlers[name](*args)
else:
self._notification_handlers[name](*args)
def _wrap_function(self, fn, sync, decode, nvim_bind, name, *args):
if decode:
args = walk(decode_if_bytes, args, decode)
if nvim_bind is not None:
args.insert(0, nvim_bind)
try:
return fn(*args)
except Exception:
if sync:
msg = ("error caught in request handler '{} {}':\n{}"
.format(name, args, format_exc_skip(1)))
raise ErrorResponse(msg)
else:
msg = ("error caught in async handler '{} {}'\n{}\n"
.format(name, args, format_exc_skip(1)))
self._on_async_err(msg + "\n")
def _on_request(self, name, args):
"""Handle a msgpack-rpc request."""
if IS_PYTHON3:
name = decode_if_bytes(name)
handler = self._request_handlers.get(name, None)
if not handler:
msg = self._missing_handler_error(name, 'request')
pass # replaces next logging statement
#error(msg)
raise ErrorResponse(msg)
pass # replaces next logging statement
#debug('calling request handler for "%s", args: "%s"', name, args)
rv = handler(*args)
pass # replaces next logging statement
#debug("request handler for '%s %s' returns: %s", name, args, rv)
return rv
def _on_notification(self, name, args):
"""Handle a msgpack-rpc notification."""
if IS_PYTHON3:
name = decode_if_bytes(name)
handler = self._notification_handlers.get(name, None)
if not handler:
msg = self._missing_handler_error(name, 'notification')
pass # replaces next logging statement
#error(msg)
self._on_async_err(msg + "\n")
return
pass # replaces next logging statement
#debug('calling notification handler for "%s", args: "%s"', name, args)
handler(*args)
def _missing_handler_error(self, name, kind):
msg = 'no {} handler registered for "{}"'.format(kind, name)
pathmatch = re.match(r'(.+):[^:]+:[^:]+', name)
if pathmatch:
loader_error = self._load_errors.get(pathmatch.group(1))
if loader_error is not None:
msg = msg + "\n" + loader_error
return msg
def _load(self, plugins):
has_script = False
for path in plugins:
err = None
if path in self._loaded:
pass # replaces next logging statement
#error('{} is already loaded'.format(path))
continue
try:
if path == "script_host.py":
module = script_host
has_script = True
else:
directory, name = os.path.split(os.path.splitext(path)[0])
file, pathname, descr = find_module(name, [directory])
module = imp.load_module(name, file, pathname, descr)
handlers = []
self._discover_classes(module, handlers, path)
self._discover_functions(module, handlers, path, False)
if not handlers:
pass # replaces next logging statement
#error('{} exports no handlers'.format(path))
continue
self._loaded[path] = {'handlers': handlers, 'module': module}
except Exception as e:
err = ('Encountered {} loading plugin at {}: {}\n{}'
.format(type(e).__name__, path, e, format_exc(5)))
pass # replaces next logging statement
#error(err)
self._load_errors[path] = err
kind = ("script-host" if len(plugins) == 1 and has_script
else "rplugin-host")
info = get_client_info(kind, 'host', host_method_spec)
self.name = info[0]
self.nvim.api.set_client_info(*info, async_=True)
def _unload(self):
for path, plugin in self._loaded.items():
handlers = plugin['handlers']
for handler in handlers:
method_name = handler._nvim_registered_name
if hasattr(handler, '_nvim_shutdown_hook'):
handler()
elif handler._nvim_rpc_sync:
del self._request_handlers[method_name]
else:
del self._notification_handlers[method_name]
self._specs = {}
self._loaded = {}
def _discover_classes(self, module, handlers, plugin_path):
for _, cls in inspect.getmembers(module, inspect.isclass):
if getattr(cls, '_nvim_plugin', False):
# discover handlers in the plugin instance
self._discover_functions(cls, handlers, plugin_path, True)
def _discover_functions(self, obj, handlers, plugin_path, delay):
def predicate(o):
return hasattr(o, '_nvim_rpc_method_name')
cls_handlers = []
specs = []
objdecode = getattr(obj, '_nvim_decode', self._decode_default)
for _, fn in inspect.getmembers(obj, predicate):
method = fn._nvim_rpc_method_name
if fn._nvim_prefix_plugin_path:
method = '{}:{}'.format(plugin_path, method)
sync = fn._nvim_rpc_sync
if delay:
fn_wrapped = partial(self._wrap_delayed_function, obj,
cls_handlers, method, sync,
handlers, plugin_path)
else:
decode = getattr(fn, '_nvim_decode', objdecode)
nvim_bind = None
if fn._nvim_bind:
nvim_bind = self._configure_nvim_for(fn)
fn_wrapped = partial(self._wrap_function, fn,
sync, decode, nvim_bind, method)
self._copy_attributes(fn, fn_wrapped)
fn_wrapped._nvim_registered_name = method
# register in the rpc handler dict
if sync:
if method in self._request_handlers:
raise Exception(('Request handler for "{}" is '
+ 'already registered').format(method))
self._request_handlers[method] = fn_wrapped
else:
if method in self._notification_handlers:
raise Exception(('Notification handler for "{}" is '
+ 'already registered').format(method))
self._notification_handlers[method] = fn_wrapped
if hasattr(fn, '_nvim_rpc_spec'):
specs.append(fn._nvim_rpc_spec)
handlers.append(fn_wrapped)
cls_handlers.append(fn_wrapped)
if specs:
self._specs[plugin_path] = specs
def _copy_attributes(self, fn, fn2):
# Copy _nvim_* attributes from the original function
for attr in dir(fn):
if attr.startswith('_nvim_'):
setattr(fn2, attr, getattr(fn, attr))
def _on_specs_request(self, path):
if IS_PYTHON3:
path = decode_if_bytes(path)
if path in self._load_errors:
self.nvim.out_write(self._load_errors[path] + '\n')
return self._specs.get(path, 0)
def _configure_nvim_for(self, obj):
# Configure a nvim instance for obj (checks encoding configuration)
nvim = self.nvim
decode = getattr(obj, '_nvim_decode', self._decode_default)
if decode:
nvim = nvim.with_decode(decode)
return nvim
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/plugin/script_host.py 0000644 0001750 0001750 00000022164 00000000000 021045 0 ustar 00bjorn bjorn 0000000 0000000 """Legacy python/python3-vim emulation."""
import imp
import io
import logging
import os
import sys
from .decorators import plugin, rpc_export
from ..api import Nvim, walk
from ..compat import IS_PYTHON3
from ..msgpack_rpc import ErrorResponse
from ..util import format_exc_skip
__all__ = ('ScriptHost',)
logger = logging.getLogger(__name__)
debug, info, warn = (logger.debug, logger.info, logger.warn,)
if IS_PYTHON3:
basestring = str
if sys.version_info >= (3, 4):
from importlib.machinery import PathFinder
PYTHON_SUBDIR = 'python3'
else:
PYTHON_SUBDIR = 'python2'
@plugin
class ScriptHost(object):
"""Provides an environment for running python plugins created for Vim."""
def __init__(self, nvim):
"""Initialize the legacy python-vim environment."""
self.setup(nvim)
# context where all code will run
self.module = imp.new_module('__main__')
nvim.script_context = self.module
# it seems some plugins assume 'sys' is already imported, so do it now
exec('import sys', self.module.__dict__)
self.legacy_vim = LegacyVim.from_nvim(nvim)
sys.modules['vim'] = self.legacy_vim
# Handle DirChanged. #296
nvim.command(
'au DirChanged * call rpcnotify({}, "python_chdir", v:event.cwd)'
.format(nvim.channel_id), async_=True)
# XXX: Avoid race condition.
# https://github.com/neovim/pynvim/pull/296#issuecomment-358970531
# TODO(bfredl): when host initialization has been refactored,
# to make __init__ safe again, the following should work:
# os.chdir(nvim.eval('getcwd()', async_=False))
nvim.command('call rpcnotify({}, "python_chdir", getcwd())'
.format(nvim.channel_id), async_=True)
def setup(self, nvim):
"""Setup import hooks and global streams.
This will add import hooks for importing modules from runtime
directories and patch the sys module so 'print' calls will be
forwarded to Nvim.
"""
self.nvim = nvim
pass # replaces next logging statement
#info('install import hook/path')
self.hook = path_hook(nvim)
sys.path_hooks.append(self.hook)
nvim.VIM_SPECIAL_PATH = '_vim_path_'
sys.path.append(nvim.VIM_SPECIAL_PATH)
pass # replaces next logging statement
#info('redirect sys.stdout and sys.stderr')
self.saved_stdout = sys.stdout
self.saved_stderr = sys.stderr
sys.stdout = RedirectStream(lambda data: nvim.out_write(data))
sys.stderr = RedirectStream(lambda data: nvim.err_write(data))
def teardown(self):
"""Restore state modified from the `setup` call."""
nvim = self.nvim
pass # replaces next logging statement
#info('uninstall import hook/path')
sys.path.remove(nvim.VIM_SPECIAL_PATH)
sys.path_hooks.remove(self.hook)
pass # replaces next logging statement
#info('restore sys.stdout and sys.stderr')
sys.stdout = self.saved_stdout
sys.stderr = self.saved_stderr
@rpc_export('python_execute', sync=True)
def python_execute(self, script, range_start, range_stop):
"""Handle the `python` ex command."""
self._set_current_range(range_start, range_stop)
try:
exec(script, self.module.__dict__)
except Exception:
raise ErrorResponse(format_exc_skip(1))
@rpc_export('python_execute_file', sync=True)
def python_execute_file(self, file_path, range_start, range_stop):
"""Handle the `pyfile` ex command."""
self._set_current_range(range_start, range_stop)
with open(file_path) as f:
script = compile(f.read(), file_path, 'exec')
try:
exec(script, self.module.__dict__)
except Exception:
raise ErrorResponse(format_exc_skip(1))
@rpc_export('python_do_range', sync=True)
def python_do_range(self, start, stop, code):
"""Handle the `pydo` ex command."""
self._set_current_range(start, stop)
nvim = self.nvim
start -= 1
fname = '_vim_pydo'
# define the function
function_def = 'def %s(line, linenr):\n %s' % (fname, code,)
exec(function_def, self.module.__dict__)
# get the function
function = self.module.__dict__[fname]
while start < stop:
# Process batches of 5000 to avoid the overhead of making multiple
# API calls for every line. Assuming an average line length of 100
# bytes, approximately 488 kilobytes will be transferred per batch,
# which can be done very quickly in a single API call.
sstart = start
sstop = min(start + 5000, stop)
lines = nvim.current.buffer.api.get_lines(sstart, sstop, True)
exception = None
newlines = []
linenr = sstart + 1
for i, line in enumerate(lines):
result = function(line, linenr)
if result is None:
# Update earlier lines, and skip to the next
if newlines:
end = sstart + len(newlines) - 1
nvim.current.buffer.api.set_lines(sstart, end,
True, newlines)
sstart += len(newlines) + 1
newlines = []
pass
elif isinstance(result, basestring):
newlines.append(result)
else:
exception = TypeError('pydo should return a string '
+ 'or None, found %s instead'
% result.__class__.__name__)
break
linenr += 1
start = sstop
if newlines:
end = sstart + len(newlines)
nvim.current.buffer.api.set_lines(sstart, end, True, newlines)
if exception:
raise exception
# delete the function
del self.module.__dict__[fname]
@rpc_export('python_eval', sync=True)
def python_eval(self, expr):
"""Handle the `pyeval` vim function."""
return eval(expr, self.module.__dict__)
@rpc_export('python_chdir', sync=False)
def python_chdir(self, cwd):
"""Handle working directory changes."""
os.chdir(cwd)
def _set_current_range(self, start, stop):
current = self.legacy_vim.current
current.range = current.buffer.range(start, stop)
class RedirectStream(io.IOBase):
def __init__(self, redirect_handler):
self.redirect_handler = redirect_handler
def write(self, data):
self.redirect_handler(data)
def writelines(self, seq):
self.redirect_handler('\n'.join(seq))
if IS_PYTHON3:
num_types = (int, float)
else:
num_types = (int, long, float) # noqa: F821
def num_to_str(obj):
if isinstance(obj, num_types):
return str(obj)
else:
return obj
class LegacyVim(Nvim):
def eval(self, expr):
obj = self.request("vim_eval", expr)
return walk(num_to_str, obj)
# Copied/adapted from :help if_pyth.
def path_hook(nvim):
def _get_paths():
if nvim._thread_invalid():
return []
return discover_runtime_directories(nvim)
def _find_module(fullname, oldtail, path):
idx = oldtail.find('.')
if idx > 0:
name = oldtail[:idx]
tail = oldtail[idx + 1:]
fmr = imp.find_module(name, path)
module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr)
return _find_module(fullname, tail, module.__path__)
else:
return imp.find_module(fullname, path)
class VimModuleLoader(object):
def __init__(self, module):
self.module = module
def load_module(self, fullname, path=None):
# Check sys.modules, required for reload (see PEP302).
try:
return sys.modules[fullname]
except KeyError:
pass
return imp.load_module(fullname, *self.module)
class VimPathFinder(object):
@staticmethod
def find_module(fullname, path=None):
"""Method for Python 2.7 and 3.3."""
try:
return VimModuleLoader(
_find_module(fullname, fullname, path or _get_paths()))
except ImportError:
return None
@staticmethod
def find_spec(fullname, target=None):
"""Method for Python 3.4+."""
return PathFinder.find_spec(fullname, _get_paths(), target)
def hook(path):
if path == nvim.VIM_SPECIAL_PATH:
return VimPathFinder
else:
raise ImportError
return hook
def discover_runtime_directories(nvim):
rv = []
for rtp in nvim.list_runtime_paths():
if not os.path.exists(rtp):
continue
for subdir in ['pythonx', PYTHON_SUBDIR]:
path = os.path.join(rtp, subdir)
if os.path.exists(path):
rv.append(path)
return rv
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944217.0
pynvim-0.4.1/pynvim/util.py 0000644 0001750 0001750 00000002470 00000000000 016161 0 ustar 00bjorn bjorn 0000000 0000000 """Shared utility functions."""
import sys
from traceback import format_exception
def format_exc_skip(skip, limit=None):
"""Like traceback.format_exc but allow skipping the first frames."""
etype, val, tb = sys.exc_info()
for i in range(skip):
tb = tb.tb_next
return (''.join(format_exception(etype, val, tb, limit))).rstrip()
# Taken from SimpleNamespace in python 3
class Version:
"""Helper class for version info."""
def __init__(self, **kwargs):
"""Create the Version object."""
self.__dict__.update(kwargs)
def __repr__(self):
"""Return str representation of the Version."""
keys = sorted(self.__dict__)
items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys)
return "{}({})".format(type(self).__name__, ", ".join(items))
def __eq__(self, other):
"""Check if version is same as other."""
return self.__dict__ == other.__dict__
def get_client_info(kind, type_, method_spec):
"""Returns a tuple describing the client."""
name = "python{}-{}".format(sys.version_info[0], kind)
attributes = {"license": "Apache v2",
"website": "github.com/neovim/pynvim"}
return (name, VERSION.__dict__, type_, method_spec, attributes)
VERSION = Version(major=0, minor=4, patch=1, prerelease='')
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1579944220.1587467
pynvim-0.4.1/pynvim.egg-info/ 0000755 0001750 0001750 00000000000 00000000000 016321 5 ustar 00bjorn bjorn 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944220.0
pynvim-0.4.1/pynvim.egg-info/PKG-INFO 0000644 0001750 0001750 00000000537 00000000000 017423 0 ustar 00bjorn bjorn 0000000 0000000 Metadata-Version: 2.1
Name: pynvim
Version: 0.4.1
Summary: Python client to neovim
Home-page: http://github.com/neovim/pynvim
Author: Thiago de Arruda
Author-email: tpadilha84@gmail.com
License: Apache
Download-URL: https://github.com/neovim/pynvim/archive/0.4.1.tar.gz
Description: UNKNOWN
Platform: UNKNOWN
Provides-Extra: pyuv
Provides-Extra: test
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944220.0
pynvim-0.4.1/pynvim.egg-info/SOURCES.txt 0000644 0001750 0001750 00000002055 00000000000 020207 0 ustar 00bjorn bjorn 0000000 0000000 LICENSE
MANIFEST.in
README.md
setup.cfg
setup.py
neovim/__init__.py
neovim/api/__init__.py
pynvim/__init__.py
pynvim/compat.py
pynvim/util.py
pynvim.egg-info/PKG-INFO
pynvim.egg-info/SOURCES.txt
pynvim.egg-info/dependency_links.txt
pynvim.egg-info/not-zip-safe
pynvim.egg-info/requires.txt
pynvim.egg-info/top_level.txt
pynvim/api/__init__.py
pynvim/api/buffer.py
pynvim/api/common.py
pynvim/api/nvim.py
pynvim/api/tabpage.py
pynvim/api/window.py
pynvim/msgpack_rpc/__init__.py
pynvim/msgpack_rpc/async_session.py
pynvim/msgpack_rpc/msgpack_stream.py
pynvim/msgpack_rpc/session.py
pynvim/msgpack_rpc/event_loop/__init__.py
pynvim/msgpack_rpc/event_loop/asyncio.py
pynvim/msgpack_rpc/event_loop/base.py
pynvim/msgpack_rpc/event_loop/uv.py
pynvim/plugin/__init__.py
pynvim/plugin/decorators.py
pynvim/plugin/host.py
pynvim/plugin/script_host.py
test/conftest.py
test/test_buffer.py
test/test_client_rpc.py
test/test_concurrency.py
test/test_decorators.py
test/test_events.py
test/test_host.py
test/test_logging.py
test/test_tabpage.py
test/test_vim.py
test/test_window.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944220.0
pynvim-0.4.1/pynvim.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 00000000000 022367 0 ustar 00bjorn bjorn 0000000 0000000
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1520178846.0
pynvim-0.4.1/pynvim.egg-info/not-zip-safe 0000644 0001750 0001750 00000000001 00000000000 020547 0 ustar 00bjorn bjorn 0000000 0000000
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944220.0
pynvim-0.4.1/pynvim.egg-info/requires.txt 0000644 0001750 0001750 00000000102 00000000000 020712 0 ustar 00bjorn bjorn 0000000 0000000 msgpack>=0.5.0
greenlet
[pyuv]
pyuv>=1.0.0
[test]
pytest>=3.4.0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579944220.0
pynvim-0.4.1/pynvim.egg-info/top_level.txt 0000644 0001750 0001750 00000000016 00000000000 021050 0 ustar 00bjorn bjorn 0000000 0000000 neovim
pynvim
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1579944220.1854134
pynvim-0.4.1/setup.cfg 0000644 0001750 0001750 00000000424 00000000000 015126 0 ustar 00bjorn bjorn 0000000 0000000 [aliases]
test = pytest
[flake8]
extend-ignore = D211,E731,D401,W503
max-line-length = 88
per-file-ignores =
test/*:D1
application-import-names = pynvim
[isort]
known_first_party = pynvim
[tool:pytest]
testpaths = test
timeout = 10
[egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579943952.0
pynvim-0.4.1/setup.py 0000644 0001750 0001750 00000002614 00000000000 015022 0 ustar 00bjorn bjorn 0000000 0000000 import platform
import sys
import os
from setuptools import setup
install_requires = [
'msgpack>=0.5.0',
]
needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv)
pytest_runner = ['pytest-runner'] if needs_pytest else []
setup_requires = [
] + pytest_runner,
tests_require = [
'pytest>=3.4.0',
]
extras_require = {
'pyuv': ['pyuv>=1.0.0'],
'test': tests_require,
}
if sys.version_info < (3, 4):
if os.name == 'nt':
install_requires.append('pyuv>=1.0.0')
else:
# trollius is just a backport of 3.4 asyncio module
install_requires.append('trollius')
if platform.python_implementation() != 'PyPy':
# pypy already includes an implementation of the greenlet module
install_requires.append('greenlet')
setup(name='pynvim',
version='0.4.1',
description='Python client to neovim',
url='http://github.com/neovim/pynvim',
download_url='https://github.com/neovim/pynvim/archive/0.4.1.tar.gz',
author='Thiago de Arruda',
author_email='tpadilha84@gmail.com',
license='Apache',
packages=['pynvim', 'pynvim.api', 'pynvim.msgpack_rpc',
'pynvim.msgpack_rpc.event_loop', 'pynvim.plugin',
'neovim', 'neovim.api'],
install_requires=install_requires,
setup_requires=setup_requires,
tests_require=tests_require,
extras_require=extras_require,
zip_safe=False)
././@PaxHeader 0000000 0000000 0000000 00000000032 00000000000 011450 x ustar 00 0000000 0000000 26 mtime=1579944220.18208
pynvim-0.4.1/test/ 0000755 0001750 0001750 00000000000 00000000000 014264 5 ustar 00bjorn bjorn 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579941126.0
pynvim-0.4.1/test/conftest.py 0000644 0001750 0001750 00000001130 00000000000 016456 0 ustar 00bjorn bjorn 0000000 0000000 import json
import os
import pytest
import pynvim
pynvim.setup_logging("test")
@pytest.fixture
def vim():
child_argv = os.environ.get('NVIM_CHILD_ARGV')
listen_address = os.environ.get('NVIM_LISTEN_ADDRESS')
if child_argv is None and listen_address is None:
child_argv = '["nvim", "-u", "NONE", "--embed", "--headless"]'
if child_argv is not None:
editor = pynvim.attach('child', argv=json.loads(child_argv))
else:
assert listen_address is None or listen_address != ''
editor = pynvim.attach('socket', path=listen_address)
return editor
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1573891020.0
pynvim-0.4.1/test/test_buffer.py 0000644 0001750 0001750 00000015175 00000000000 017157 0 ustar 00bjorn bjorn 0000000 0000000 import os
import pytest
from pynvim.api import NvimError
from pynvim.compat import IS_PYTHON3
def test_repr(vim):
assert repr(vim.current.buffer) == ""
def test_get_length(vim):
assert len(vim.current.buffer) == 1
vim.current.buffer.append('line')
assert len(vim.current.buffer) == 2
vim.current.buffer.append('line')
assert len(vim.current.buffer) == 3
vim.current.buffer[-1] = None
assert len(vim.current.buffer) == 2
vim.current.buffer[-1] = None
vim.current.buffer[-1] = None
# There's always at least one line
assert len(vim.current.buffer) == 1
def test_get_set_del_line(vim):
assert vim.current.buffer[0] == ''
vim.current.buffer[0] = 'line1'
assert vim.current.buffer[0] == 'line1'
vim.current.buffer[0] = 'line2'
assert vim.current.buffer[0] == 'line2'
vim.current.buffer[0] = None
assert vim.current.buffer[0] == ''
# __delitem__
vim.current.buffer[:] = ['line1', 'line2', 'line3']
assert vim.current.buffer[2] == 'line3'
del vim.current.buffer[0]
assert vim.current.buffer[0] == 'line2'
assert vim.current.buffer[1] == 'line3'
del vim.current.buffer[-1]
assert vim.current.buffer[0] == 'line2'
assert len(vim.current.buffer) == 1
def test_get_set_del_slice(vim):
assert vim.current.buffer[:] == ['']
# Replace buffer
vim.current.buffer[:] = ['a', 'b', 'c']
assert vim.current.buffer[:] == ['a', 'b', 'c']
assert vim.current.buffer[1:] == ['b', 'c']
assert vim.current.buffer[1:2] == ['b']
assert vim.current.buffer[1:1] == []
assert vim.current.buffer[:-1] == ['a', 'b']
assert vim.current.buffer[1:-1] == ['b']
assert vim.current.buffer[-2:] == ['b', 'c']
vim.current.buffer[1:2] = ['a', 'b', 'c']
assert vim.current.buffer[:] == ['a', 'a', 'b', 'c', 'c']
vim.current.buffer[-1:] = ['a', 'b', 'c']
assert vim.current.buffer[:] == ['a', 'a', 'b', 'c', 'a', 'b', 'c']
vim.current.buffer[:-3] = None
assert vim.current.buffer[:] == ['a', 'b', 'c']
vim.current.buffer[:] = None
assert vim.current.buffer[:] == ['']
# __delitem__
vim.current.buffer[:] = ['a', 'b', 'c']
del vim.current.buffer[:]
assert vim.current.buffer[:] == ['']
vim.current.buffer[:] = ['a', 'b', 'c']
del vim.current.buffer[:1]
assert vim.current.buffer[:] == ['b', 'c']
del vim.current.buffer[:-1]
assert vim.current.buffer[:] == ['c']
def test_vars(vim):
vim.current.buffer.vars['python'] = [1, 2, {'3': 1}]
assert vim.current.buffer.vars['python'] == [1, 2, {'3': 1}]
assert vim.eval('b:python') == [1, 2, {'3': 1}]
assert vim.current.buffer.vars.get('python') == [1, 2, {'3': 1}]
del vim.current.buffer.vars['python']
with pytest.raises(KeyError):
vim.current.buffer.vars['python']
assert vim.eval('exists("b:python")') == 0
with pytest.raises(KeyError):
del vim.current.buffer.vars['python']
assert vim.current.buffer.vars.get('python', 'default') == 'default'
def test_api(vim):
vim.current.buffer.api.set_var('myvar', 'thetext')
assert vim.current.buffer.api.get_var('myvar') == 'thetext'
assert vim.eval('b:myvar') == 'thetext'
vim.current.buffer.api.set_lines(0, -1, True, ['alpha', 'beta'])
assert vim.current.buffer.api.get_lines(0, -1, True) == ['alpha', 'beta']
assert vim.current.buffer[:] == ['alpha', 'beta']
def test_options(vim):
assert vim.current.buffer.options['shiftwidth'] == 8
vim.current.buffer.options['shiftwidth'] = 4
assert vim.current.buffer.options['shiftwidth'] == 4
# global-local option
vim.current.buffer.options['define'] = 'test'
assert vim.current.buffer.options['define'] == 'test'
# Doesn't change the global value
assert vim.options['define'] == r'^\s*#\s*define'
with pytest.raises(KeyError) as excinfo:
vim.current.buffer.options['doesnotexist']
assert excinfo.value.args == ("Invalid option name: 'doesnotexist'",)
def test_number(vim):
curnum = vim.current.buffer.number
vim.command('new')
assert vim.current.buffer.number == curnum + 1
vim.command('new')
assert vim.current.buffer.number == curnum + 2
def test_name(vim):
vim.command('new')
assert vim.current.buffer.name == ''
new_name = vim.eval('resolve(tempname())')
vim.current.buffer.name = new_name
assert vim.current.buffer.name == new_name
vim.command('silent w!')
assert os.path.isfile(new_name)
os.unlink(new_name)
def test_valid(vim):
vim.command('new')
buffer = vim.current.buffer
assert buffer.valid
vim.command('bw!')
assert not buffer.valid
def test_append(vim):
vim.current.buffer.append('a')
assert vim.current.buffer[:] == ['', 'a']
vim.current.buffer.append('b', 0)
assert vim.current.buffer[:] == ['b', '', 'a']
vim.current.buffer.append(['c', 'd'])
assert vim.current.buffer[:] == ['b', '', 'a', 'c', 'd']
vim.current.buffer.append(['c', 'd'], 2)
assert vim.current.buffer[:] == ['b', '', 'c', 'd', 'a', 'c', 'd']
vim.current.buffer.append(b'bytes')
assert vim.current.buffer[:] == ['b', '', 'c', 'd', 'a', 'c', 'd', 'bytes']
def test_mark(vim):
vim.current.buffer.append(['a', 'bit of', 'text'])
vim.current.window.cursor = [3, 4]
vim.command('mark V')
assert vim.current.buffer.mark('V') == [3, 0]
def test_invalid_utf8(vim):
vim.command('normal "=printf("%c", 0xFF)\np')
assert vim.eval("char2nr(getline(1))") == 0xFF
assert vim.current.buffer[:] == ['\udcff'] if IS_PYTHON3 else ['\xff']
vim.current.line += 'x'
assert vim.eval("getline(1)", decode=False) == b'\xFFx'
assert vim.current.buffer[:] == ['\udcffx'] if IS_PYTHON3 else ['\xffx']
def test_get_exceptions(vim):
with pytest.raises(KeyError) as excinfo:
vim.current.buffer.options['invalid-option']
assert not isinstance(excinfo.value, NvimError)
assert excinfo.value.args == ("Invalid option name: 'invalid-option'",)
def test_set_items_for_range(vim):
vim.current.buffer[:] = ['a', 'b', 'c', 'd', 'e']
r = vim.current.buffer.range(1, 3)
r[1:3] = ['foo'] * 3
assert vim.current.buffer[:] == ['a', 'foo', 'foo', 'foo', 'd', 'e']
# NB: we can't easily test the effect of this. But at least run the lua
# function sync, so we know it runs without runtime error with simple args.
def test_update_highlights(vim):
vim.current.buffer[:] = ['a', 'b', 'c']
src_id = vim.new_highlight_source()
vim.current.buffer.update_highlights(
src_id, [["Comment", 0, 0, -1], ("String", 1, 0, 1)], clear=True, async_=False
)
def test_buffer_inequality(vim):
b = vim.current.buffer
assert not (b != vim.current.buffer)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1573891020.0
pynvim-0.4.1/test/test_client_rpc.py 0000644 0001750 0001750 00000004511 00000000000 020020 0 ustar 00bjorn bjorn 0000000 0000000 # -*- coding: utf-8 -*-
import time
def test_call_and_reply(vim):
cid = vim.channel_id
def setup_cb():
cmd = 'let g:result = rpcrequest(%d, "client-call", 1, 2, 3)' % cid
vim.command(cmd)
assert vim.vars['result'] == [4, 5, 6]
vim.stop_loop()
def request_cb(name, args):
assert name == 'client-call'
assert args == [1, 2, 3]
return [4, 5, 6]
vim.run_loop(request_cb, None, setup_cb)
def test_call_api_before_reply(vim):
cid = vim.channel_id
def setup_cb():
cmd = 'let g:result = rpcrequest(%d, "client-call2", 1, 2, 3)' % cid
vim.command(cmd)
assert vim.vars['result'] == [7, 8, 9]
vim.stop_loop()
def request_cb(name, args):
vim.command('let g:result2 = [7, 8, 9]')
return vim.vars['result2']
vim.run_loop(request_cb, None, setup_cb)
def test_async_call(vim):
def request_cb(name, args):
if name == "test-event":
vim.vars['result'] = 17
vim.stop_loop()
# this would have dead-locked if not async
vim.funcs.rpcrequest(vim.channel_id, "test-event", async_=True)
vim.run_loop(request_cb, None, None)
# TODO(blueyed): This sleep is required on Travis, where it hangs with
# "Entering event loop" otherwise (asyncio's EpollSelector._epoll.poll).
time.sleep(0.1)
assert vim.vars['result'] == 17
def test_recursion(vim):
cid = vim.channel_id
def setup_cb():
vim.vars['result1'] = 0
vim.vars['result2'] = 0
vim.vars['result3'] = 0
vim.vars['result4'] = 0
cmd = 'let g:result1 = rpcrequest(%d, "call", %d)' % (cid, 2,)
vim.command(cmd)
assert vim.vars['result1'] == 4
assert vim.vars['result2'] == 8
assert vim.vars['result3'] == 16
assert vim.vars['result4'] == 32
vim.stop_loop()
def request_cb(name, args):
n = args[0]
n *= 2
if n <= 16:
if n == 4:
cmd = 'let g:result2 = rpcrequest(%d, "call", %d)' % (cid, n,)
elif n == 8:
cmd = 'let g:result3 = rpcrequest(%d, "call", %d)' % (cid, n,)
elif n == 16:
cmd = 'let g:result4 = rpcrequest(%d, "call", %d)' % (cid, n,)
vim.command(cmd)
return n
vim.run_loop(request_cb, None, setup_cb)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1573891020.0
pynvim-0.4.1/test/test_concurrency.py 0000644 0001750 0001750 00000001211 00000000000 020222 0 ustar 00bjorn bjorn 0000000 0000000 from threading import Timer
def test_interrupt_from_another_thread(vim):
timer = Timer(0.5, lambda: vim.async_call(lambda: vim.stop_loop()))
timer.start()
assert vim.next_message() is None
def test_exception_in_threadsafe_call(vim):
# an exception in a threadsafe_call shouldn't crash the entire host
msgs = []
vim.async_call(lambda: [vim.eval("3"), undefined_variable]) # noqa: F821
timer = Timer(0.5, lambda: vim.async_call(lambda: vim.stop_loop()))
timer.start()
vim.run_loop(None, None, err_cb=msgs.append)
assert len(msgs) == 1
msgs[0].index('NameError')
msgs[0].index('undefined_variable')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1573891020.0
pynvim-0.4.1/test/test_decorators.py 0000644 0001750 0001750 00000001723 00000000000 020045 0 ustar 00bjorn bjorn 0000000 0000000 from pynvim.plugin.decorators import command
def test_command_count():
def function():
"""A dummy function to decorate."""
return
# ensure absence with default value of None
decorated = command('test')(function)
assert 'count' not in decorated._nvim_rpc_spec['opts']
# ensure absence with explicit value of None
count_value = None
decorated = command('test', count=count_value)(function)
assert 'count' not in decorated._nvim_rpc_spec['opts']
# Test presesence with value of 0
count_value = 0
decorated = command('test', count=count_value)(function)
assert 'count' in decorated._nvim_rpc_spec['opts']
assert decorated._nvim_rpc_spec['opts']['count'] == count_value
# Test presence with value of 1
count_value = 1
decorated = command('test', count=count_value)(function)
assert 'count' in decorated._nvim_rpc_spec['opts']
assert decorated._nvim_rpc_spec['opts']['count'] == count_value
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579941126.0
pynvim-0.4.1/test/test_events.py 0000644 0001750 0001750 00000003401 00000000000 017177 0 ustar 00bjorn bjorn 0000000 0000000 # -*- coding: utf-8 -*-
def test_receiving_events(vim):
vim.command('call rpcnotify(%d, "test-event", 1, 2, 3)' % vim.channel_id)
event = vim.next_message()
assert event[1] == 'test-event'
assert event[2] == [1, 2, 3]
vim.command('au FileType python call rpcnotify(%d, "py!", bufnr("$"))' %
vim.channel_id)
vim.command('set filetype=python')
event = vim.next_message()
assert event[1] == 'py!'
assert event[2] == [vim.current.buffer.number]
def test_sending_notify(vim):
# notify after notify
vim.command("let g:test = 3", async_=True)
cmd = 'call rpcnotify(%d, "test-event", g:test)' % vim.channel_id
vim.command(cmd, async_=True)
event = vim.next_message()
assert event[1] == 'test-event'
assert event[2] == [3]
# request after notify
vim.command("let g:data = 'xyz'", async_=True)
assert vim.eval('g:data') == 'xyz'
def test_async_error(vim):
# Invoke a bogus Ex command via notify (async).
vim.command("lolwut", async_=True)
event = vim.next_message()
assert event[1] == 'nvim_error_event'
def test_broadcast(vim):
vim.subscribe('event2')
vim.command('call rpcnotify(0, "event1", 1, 2, 3)')
vim.command('call rpcnotify(0, "event2", 4, 5, 6)')
vim.command('call rpcnotify(0, "event2", 7, 8, 9)')
event = vim.next_message()
assert event[1] == 'event2'
assert event[2] == [4, 5, 6]
event = vim.next_message()
assert event[1] == 'event2'
assert event[2] == [7, 8, 9]
vim.unsubscribe('event2')
vim.subscribe('event1')
vim.command('call rpcnotify(0, "event2", 10, 11, 12)')
vim.command('call rpcnotify(0, "event1", 13, 14, 15)')
msg = vim.next_message()
assert msg[1] == 'event1'
assert msg[2] == [13, 14, 15]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579941126.0
pynvim-0.4.1/test/test_host.py 0000644 0001750 0001750 00000001367 00000000000 016661 0 ustar 00bjorn bjorn 0000000 0000000 # -*- coding: utf-8 -*-
from pynvim.plugin.host import Host, host_method_spec
def test_host_clientinfo(vim):
h = Host(vim)
assert h._request_handlers.keys() == host_method_spec.keys()
assert 'remote' == vim.api.get_chan_info(vim.channel_id)['client']['type']
h._load([])
assert 'host' == vim.api.get_chan_info(vim.channel_id)['client']['type']
# Smoke test for Host._on_error_event(). #425
def test_host_async_error(vim):
h = Host(vim)
h._load([])
# Invoke a bogus Ex command via notify (async).
vim.command("lolwut", async_=True)
event = vim.next_message()
assert event[1] == 'nvim_error_event'
assert 'rplugin-host: Async request caused an error:\nboom\n' \
in h._on_error_event(None, 'boom')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1573891020.0
pynvim-0.4.1/test/test_logging.py 0000644 0001750 0001750 00000002141 00000000000 017321 0 ustar 00bjorn bjorn 0000000 0000000 import os
import sys
def test_setup_logging(monkeypatch, tmpdir, caplog):
from pynvim import setup_logging
major_version = sys.version_info[0]
setup_logging('name1')
assert caplog.messages == []
def get_expected_logfile(prefix, name):
return '{}_py{}_{}'.format(prefix, major_version, name)
prefix = tmpdir.join('testlog1')
monkeypatch.setenv('NVIM_PYTHON_LOG_FILE', str(prefix))
setup_logging('name2')
assert caplog.messages == []
logfile = get_expected_logfile(prefix, 'name2')
assert os.path.exists(logfile)
assert open(logfile, 'r').read() == ''
monkeypatch.setenv('NVIM_PYTHON_LOG_LEVEL', 'invalid')
setup_logging('name3')
assert caplog.record_tuples == [
('pynvim', 30, "Invalid NVIM_PYTHON_LOG_LEVEL: 'invalid', using INFO."),
]
logfile = get_expected_logfile(prefix, 'name2')
assert os.path.exists(logfile)
with open(logfile, 'r') as f:
lines = f.readlines()
assert len(lines) == 1
assert lines[0].endswith(
"- Invalid NVIM_PYTHON_LOG_LEVEL: 'invalid', using INFO.\n"
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1573891020.0
pynvim-0.4.1/test/test_tabpage.py 0000644 0001750 0001750 00000002627 00000000000 017307 0 ustar 00bjorn bjorn 0000000 0000000 import pytest
def test_windows(vim):
vim.command('tabnew')
vim.command('vsplit')
assert list(vim.tabpages[0].windows) == [vim.windows[0]]
assert list(vim.tabpages[1].windows) == [vim.windows[1], vim.windows[2]]
assert vim.tabpages[1].window == vim.windows[1]
vim.current.window = vim.windows[2]
assert vim.tabpages[1].window == vim.windows[2]
def test_vars(vim):
vim.current.tabpage.vars['python'] = [1, 2, {'3': 1}]
assert vim.current.tabpage.vars['python'] == [1, 2, {'3': 1}]
assert vim.eval('t:python') == [1, 2, {'3': 1}]
assert vim.current.tabpage.vars.get('python') == [1, 2, {'3': 1}]
del vim.current.tabpage.vars['python']
with pytest.raises(KeyError):
vim.current.tabpage.vars['python']
assert vim.eval('exists("t:python")') == 0
with pytest.raises(KeyError):
del vim.current.tabpage.vars['python']
assert vim.current.tabpage.vars.get('python', 'default') == 'default'
def test_valid(vim):
vim.command('tabnew')
tabpage = vim.tabpages[1]
assert tabpage.valid
vim.command('tabclose')
assert not tabpage.valid
def test_number(vim):
curnum = vim.current.tabpage.number
vim.command('tabnew')
assert vim.current.tabpage.number == curnum + 1
vim.command('tabnew')
assert vim.current.tabpage.number == curnum + 2
def test_repr(vim):
assert repr(vim.current.tabpage) == ""
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1579941126.0
pynvim-0.4.1/test/test_vim.py 0000644 0001750 0001750 00000014763 00000000000 016503 0 ustar 00bjorn bjorn 0000000 0000000 # -*- coding: utf-8 -*-
import os
import sys
import tempfile
import pytest
def source(vim, code):
fd, fname = tempfile.mkstemp()
with os.fdopen(fd, 'w') as f:
f.write(code)
vim.command('source ' + fname)
os.unlink(fname)
def test_clientinfo(vim):
assert 'remote' == vim.api.get_chan_info(vim.channel_id)['client']['type']
def test_command(vim):
fname = tempfile.mkstemp()[1]
vim.command('new')
vim.command('edit {}'.format(fname))
# skip the "press return" state, which does not handle deferred calls
vim.input('\r')
vim.command('normal itesting\npython\napi')
vim.command('w')
assert os.path.isfile(fname)
with open(fname) as f:
assert f.read() == 'testing\npython\napi\n'
os.unlink(fname)
def test_command_output(vim):
assert vim.command_output('echo "test"') == 'test'
def test_command_error(vim):
with pytest.raises(vim.error) as excinfo:
vim.current.window.cursor = -1, -1
assert excinfo.value.args == ('Cursor position outside buffer',)
def test_eval(vim):
vim.command('let g:v1 = "a"')
vim.command('let g:v2 = [1, 2, {"v3": 3}]')
g = vim.eval('g:')
assert g['v1'] == 'a'
assert g['v2'] == [1, 2, {'v3': 3}]
def test_call(vim):
assert vim.funcs.join(['first', 'last'], ', ') == 'first, last'
source(vim, """
function! Testfun(a,b)
return string(a:a).":".a:b
endfunction
""")
assert vim.funcs.Testfun(3, 'alpha') == '3:alpha'
def test_api(vim):
vim.api.command('let g:var = 3')
assert vim.api.eval('g:var') == 3
def test_strwidth(vim):
assert vim.strwidth('abc') == 3
# 6 + (neovim)
# 19 * 2 (each japanese character occupies two cells)
assert vim.strwidth('neovimのデザインかなりまともなのになってる。') == 44
def test_chdir(vim):
pwd = vim.eval('getcwd()')
root = os.path.abspath(os.sep)
# We can chdir to '/' on Windows, but then the pwd will be the root drive
vim.chdir('/')
assert vim.eval('getcwd()') == root
vim.chdir(pwd)
assert vim.eval('getcwd()') == pwd
def test_current_line(vim):
assert vim.current.line == ''
vim.current.line = 'abc'
assert vim.current.line == 'abc'
def test_current_line_delete(vim):
vim.current.buffer[:] = ['one', 'two']
assert len(vim.current.buffer[:]) == 2
del vim.current.line
assert len(vim.current.buffer[:]) == 1 and vim.current.buffer[0] == 'two'
del vim.current.line
assert len(vim.current.buffer[:]) == 1 and not vim.current.buffer[0]
def test_vars(vim):
vim.vars['python'] = [1, 2, {'3': 1}]
assert vim.vars['python'], [1, 2 == {'3': 1}]
assert vim.eval('g:python'), [1, 2 == {'3': 1}]
assert vim.vars.get('python') == [1, 2, {'3': 1}]
del vim.vars['python']
with pytest.raises(KeyError):
vim.vars['python']
assert vim.eval('exists("g:python")') == 0
with pytest.raises(KeyError):
del vim.vars['python']
assert vim.vars.get('python', 'default') == 'default'
def test_options(vim):
assert vim.options['background'] == 'dark'
vim.options['background'] = 'light'
assert vim.options['background'] == 'light'
def test_local_options(vim):
assert vim.windows[0].options['foldmethod'] == 'manual'
vim.windows[0].options['foldmethod'] = 'syntax'
assert vim.windows[0].options['foldmethod'] == 'syntax'
def test_buffers(vim):
buffers = []
# Number of elements
assert len(vim.buffers) == 1
# Indexing (by buffer number)
assert vim.buffers[vim.current.buffer.number] == vim.current.buffer
buffers.append(vim.current.buffer)
vim.command('new')
assert len(vim.buffers) == 2
buffers.append(vim.current.buffer)
assert vim.buffers[vim.current.buffer.number] == vim.current.buffer
vim.current.buffer = buffers[0]
assert vim.buffers[vim.current.buffer.number] == buffers[0]
# Membership test
assert buffers[0] in vim.buffers
assert buffers[1] in vim.buffers
assert {} not in vim.buffers
# Iteration
assert buffers == list(vim.buffers)
def test_windows(vim):
assert len(vim.windows) == 1
assert vim.windows[0] == vim.current.window
vim.command('vsplit')
vim.command('split')
assert len(vim.windows) == 3
assert vim.windows[0] == vim.current.window
vim.current.window = vim.windows[1]
assert vim.windows[1] == vim.current.window
def test_tabpages(vim):
assert len(vim.tabpages) == 1
assert vim.tabpages[0] == vim.current.tabpage
vim.command('tabnew')
assert len(vim.tabpages) == 2
assert len(vim.windows) == 2
assert vim.windows[1] == vim.current.window
assert vim.tabpages[1] == vim.current.tabpage
vim.current.window = vim.windows[0]
# Switching window also switches tabpages if necessary(this probably
# isn't the current behavior, but compatibility will be handled in the
# python client with an optional parameter)
assert vim.tabpages[0] == vim.current.tabpage
assert vim.windows[0] == vim.current.window
vim.current.tabpage = vim.tabpages[1]
assert vim.tabpages[1] == vim.current.tabpage
assert vim.windows[1] == vim.current.window
def test_hash(vim):
d = {}
d[vim.current.buffer] = "alpha"
assert d[vim.current.buffer] == 'alpha'
vim.command('new')
d[vim.current.buffer] = "beta"
assert d[vim.current.buffer] == 'beta'
vim.command('winc w')
assert d[vim.current.buffer] == 'alpha'
vim.command('winc w')
assert d[vim.current.buffer] == 'beta'
def test_cwd(vim, tmpdir):
pycmd = 'python'
if sys.version_info >= (3, 0):
pycmd = 'python3'
vim.command('{} import os'.format(pycmd))
cwd_before = vim.command_output('{} print(os.getcwd())'.format(pycmd))
vim.command('cd {}'.format(tmpdir.strpath))
cwd_vim = vim.command_output('pwd')
cwd_python = vim.command_output('{} print(os.getcwd())'.format(pycmd))
assert cwd_python == cwd_vim
assert cwd_python != cwd_before
lua_code = """
local a = vim.api
local y = ...
function pynvimtest_func(x)
return x+y
end
local function setbuf(buf,lines)
a.nvim_buf_set_lines(buf, 0, -1, true, lines)
end
local function getbuf(buf)
return a.nvim_buf_line_count(buf)
end
pynvimtest = {setbuf=setbuf, getbuf=getbuf}
return "eggspam"
"""
def test_lua(vim):
assert vim.exec_lua(lua_code, 7) == "eggspam"
assert vim.lua.pynvimtest_func(3) == 10
lua_module = vim.lua.pynvimtest
buf = vim.current.buffer
lua_module.setbuf(buf, ["a", "b", "c", "d"], async_=True)
assert lua_module.getbuf(buf) == 4
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1573891020.0
pynvim-0.4.1/test/test_window.py 0000644 0001750 0001750 00000007411 00000000000 017207 0 ustar 00bjorn bjorn 0000000 0000000 import pytest
def test_buffer(vim):
assert vim.current.buffer == vim.windows[0].buffer
vim.command('new')
vim.current.window = vim.windows[1]
assert vim.current.buffer == vim.windows[1].buffer
assert vim.windows[0].buffer != vim.windows[1].buffer
def test_cursor(vim):
assert vim.current.window.cursor == [1, 0]
vim.command('normal ityping\033o some text')
assert vim.current.buffer[:] == ['typing', ' some text']
assert vim.current.window.cursor == [2, 10]
vim.current.window.cursor = [2, 6]
vim.command('normal i dumb')
assert vim.current.buffer[:] == ['typing', ' some dumb text']
def test_height(vim):
vim.command('vsplit')
assert vim.windows[1].height == vim.windows[0].height
vim.current.window = vim.windows[1]
vim.command('split')
assert vim.windows[1].height == vim.windows[0].height // 2
vim.windows[1].height = 2
assert vim.windows[1].height == 2
def test_width(vim):
vim.command('split')
assert vim.windows[1].width == vim.windows[0].width
vim.current.window = vim.windows[1]
vim.command('vsplit')
assert vim.windows[1].width == vim.windows[0].width // 2
vim.windows[1].width = 2
assert vim.windows[1].width == 2
def test_vars(vim):
vim.current.window.vars['python'] = [1, 2, {'3': 1}]
assert vim.current.window.vars['python'] == [1, 2, {'3': 1}]
assert vim.eval('w:python') == [1, 2, {'3': 1}]
assert vim.current.window.vars.get('python') == [1, 2, {'3': 1}]
del vim.current.window.vars['python']
with pytest.raises(KeyError):
vim.current.window.vars['python']
assert vim.eval('exists("w:python")') == 0
with pytest.raises(KeyError):
del vim.current.window.vars['python']
assert vim.current.window.vars.get('python', 'default') == 'default'
def test_options(vim):
vim.current.window.options['colorcolumn'] = '4,3'
assert vim.current.window.options['colorcolumn'] == '4,3'
# global-local option
vim.current.window.options['statusline'] = 'window-status'
assert vim.current.window.options['statusline'] == 'window-status'
assert vim.options['statusline'] == ''
with pytest.raises(KeyError) as excinfo:
vim.current.window.options['doesnotexist']
assert excinfo.value.args == ("Invalid option name: 'doesnotexist'",)
def test_position(vim):
height = vim.windows[0].height
width = vim.windows[0].width
vim.command('split')
vim.command('vsplit')
assert (vim.windows[0].row, vim.windows[0].col) == (0, 0)
vsplit_pos = width / 2
split_pos = height / 2
assert vim.windows[1].row == 0
assert vsplit_pos - 1 <= vim.windows[1].col <= vsplit_pos + 1
assert split_pos - 1 <= vim.windows[2].row <= split_pos + 1
assert vim.windows[2].col == 0
def test_tabpage(vim):
vim.command('tabnew')
vim.command('vsplit')
assert vim.windows[0].tabpage == vim.tabpages[0]
assert vim.windows[1].tabpage == vim.tabpages[1]
assert vim.windows[2].tabpage == vim.tabpages[1]
def test_valid(vim):
vim.command('split')
window = vim.windows[1]
vim.current.window = window
assert window.valid
vim.command('q')
assert not window.valid
def test_number(vim):
curnum = vim.current.window.number
vim.command('bot split')
assert vim.current.window.number == curnum + 1
vim.command('bot split')
assert vim.current.window.number == curnum + 2
def test_handle(vim):
hnd1 = vim.current.window.handle
vim.command('bot split')
hnd2 = vim.current.window.handle
assert hnd2 != hnd1
vim.command('bot split')
hnd3 = vim.current.window.handle
assert hnd1 != hnd2 != hnd3
vim.command('wincmd w')
assert vim.current.window.handle == hnd1
def test_repr(vim):
assert repr(vim.current.window) == ""