././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1579944220.18208 pynvim-0.4.1/0000755000175000017500000000000000000000000013305 5ustar00bjornbjorn00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1410857002.0 pynvim-0.4.1/LICENSE0000644000175000017500000002605600000000000014323 0ustar00bjornbjorn00000000000000Apache 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573891020.0 pynvim-0.4.1/MANIFEST.in0000644000175000017500000000006600000000000015045 0ustar00bjornbjorn00000000000000include README.md LICENSE recursive-include test *.py ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579944220.1854134 pynvim-0.4.1/PKG-INFO0000644000175000017500000000053700000000000014407 0ustar00bjornbjorn00000000000000Metadata-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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579941126.0 pynvim-0.4.1/README.md0000644000175000017500000001020000000000000014555 0ustar00bjornbjorn00000000000000### Pynvim: Python client to [Neovim](https://github.com/neovim/neovim) [![Build Status](https://travis-ci.org/neovim/pynvim.svg?branch=master)](https://travis-ci.org/neovim/pynvim) [![Documentation Status](https://readthedocs.org/projects/pynvim/badge/?version=latest)](http://pynvim.readthedocs.io/en/latest/?badge=latest) [![Code coverage](https://codecov.io/gh/neovim/pynvim/branch/master/graph/badge.svg)](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. ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1579944220.15208 pynvim-0.4.1/neovim/0000755000175000017500000000000000000000000014602 5ustar00bjornbjorn00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1544014247.0 pynvim-0.4.1/neovim/__init__.py0000644000175000017500000000025600000000000016716 0ustar00bjornbjorn00000000000000"""Python client for Nvim. This is a transition package. New projects should instead import pynvim package. """ import pynvim from pynvim import * __all__ = pynvim.__all__ ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1579944220.15208 pynvim-0.4.1/neovim/api/0000755000175000017500000000000000000000000015353 5ustar00bjornbjorn00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1544014247.0 pynvim-0.4.1/neovim/api/__init__.py0000644000175000017500000000026100000000000017463 0ustar00bjornbjorn00000000000000"""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__ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579944220.1554134 pynvim-0.4.1/pynvim/0000755000175000017500000000000000000000000014627 5ustar00bjornbjorn00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/__init__.py0000644000175000017500000001263300000000000016745 0ustar00bjornbjorn00000000000000"""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()) ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1579944220.16208 pynvim-0.4.1/pynvim/api/0000755000175000017500000000000000000000000015400 5ustar00bjornbjorn00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/api/__init__.py0000644000175000017500000000057300000000000017516 0ustar00bjornbjorn00000000000000"""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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/api/buffer.py0000644000175000017500000001602600000000000017230 0ustar00bjornbjorn00000000000000"""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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/api/common.py0000644000175000017500000001474200000000000017252 0ustar00bjornbjorn00000000000000"""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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/api/nvim.py0000644000175000017500000004676500000000000016745 0ustar00bjornbjorn00000000000000"""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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/api/tabpage.py0000644000175000017500000000175600000000000017366 0ustar00bjornbjorn00000000000000"""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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/api/window.py0000644000175000017500000000366500000000000017273 0ustar00bjornbjorn00000000000000"""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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/compat.py0000644000175000017500000000320200000000000016461 0ustar00bjornbjorn00000000000000"""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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579944220.1654134 pynvim-0.4.1/pynvim/msgpack_rpc/0000755000175000017500000000000000000000000017120 5ustar00bjornbjorn00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/msgpack_rpc/__init__.py0000644000175000017500000000261600000000000021236 0ustar00bjornbjorn00000000000000"""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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/msgpack_rpc/async_session.py0000644000175000017500000001167700000000000022366 0ustar00bjornbjorn00000000000000"""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) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579944220.1687467 pynvim-0.4.1/pynvim/msgpack_rpc/event_loop/0000755000175000017500000000000000000000000021272 5ustar00bjornbjorn00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/msgpack_rpc/event_loop/__init__.py0000644000175000017500000000131100000000000023377 0ustar00bjornbjorn00000000000000"""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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/msgpack_rpc/event_loop/asyncio.py0000644000175000017500000001270400000000000023315 0ustar00bjornbjorn00000000000000"""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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/msgpack_rpc/event_loop/base.py0000644000175000017500000001650600000000000022566 0ustar00bjornbjorn00000000000000"""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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/msgpack_rpc/event_loop/uv.py0000644000175000017500000000767600000000000022316 0ustar00bjornbjorn00000000000000"""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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/msgpack_rpc/msgpack_stream.py0000644000175000017500000000420400000000000022472 0ustar00bjornbjorn00000000000000"""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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/msgpack_rpc/session.py0000644000175000017500000002177400000000000021170 0ustar00bjornbjorn00000000000000"""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 ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1579944220.17208 pynvim-0.4.1/pynvim/plugin/0000755000175000017500000000000000000000000016125 5ustar00bjornbjorn00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/plugin/__init__.py0000644000175000017500000000047700000000000020246 0ustar00bjornbjorn00000000000000"""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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/plugin/decorators.py0000644000175000017500000001101600000000000020643 0ustar00bjornbjorn00000000000000"""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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/plugin/host.py0000644000175000017500000002477300000000000017471 0ustar00bjornbjorn00000000000000"""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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/plugin/script_host.py0000644000175000017500000002216400000000000021045 0ustar00bjornbjorn00000000000000"""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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944217.0 pynvim-0.4.1/pynvim/util.py0000644000175000017500000000247000000000000016161 0ustar00bjornbjorn00000000000000"""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='') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579944220.1587467 pynvim-0.4.1/pynvim.egg-info/0000755000175000017500000000000000000000000016321 5ustar00bjornbjorn00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944220.0 pynvim-0.4.1/pynvim.egg-info/PKG-INFO0000644000175000017500000000053700000000000017423 0ustar00bjornbjorn00000000000000Metadata-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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944220.0 pynvim-0.4.1/pynvim.egg-info/SOURCES.txt0000644000175000017500000000205500000000000020207 0ustar00bjornbjorn00000000000000LICENSE 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././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944220.0 pynvim-0.4.1/pynvim.egg-info/dependency_links.txt0000644000175000017500000000000100000000000022367 0ustar00bjornbjorn00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1520178846.0 pynvim-0.4.1/pynvim.egg-info/not-zip-safe0000644000175000017500000000000100000000000020547 0ustar00bjornbjorn00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944220.0 pynvim-0.4.1/pynvim.egg-info/requires.txt0000644000175000017500000000010200000000000020712 0ustar00bjornbjorn00000000000000msgpack>=0.5.0 greenlet [pyuv] pyuv>=1.0.0 [test] pytest>=3.4.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579944220.0 pynvim-0.4.1/pynvim.egg-info/top_level.txt0000644000175000017500000000001600000000000021050 0ustar00bjornbjorn00000000000000neovim pynvim ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1579944220.1854134 pynvim-0.4.1/setup.cfg0000644000175000017500000000042400000000000015126 0ustar00bjornbjorn00000000000000[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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579943952.0 pynvim-0.4.1/setup.py0000644000175000017500000000261400000000000015022 0ustar00bjornbjorn00000000000000import 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) ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1579944220.18208 pynvim-0.4.1/test/0000755000175000017500000000000000000000000014264 5ustar00bjornbjorn00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579941126.0 pynvim-0.4.1/test/conftest.py0000644000175000017500000000113000000000000016456 0ustar00bjornbjorn00000000000000import 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573891020.0 pynvim-0.4.1/test/test_buffer.py0000644000175000017500000001517500000000000017157 0ustar00bjornbjorn00000000000000import 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573891020.0 pynvim-0.4.1/test/test_client_rpc.py0000644000175000017500000000451100000000000020020 0ustar00bjornbjorn00000000000000# -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573891020.0 pynvim-0.4.1/test/test_concurrency.py0000644000175000017500000000121100000000000020222 0ustar00bjornbjorn00000000000000from 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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573891020.0 pynvim-0.4.1/test/test_decorators.py0000644000175000017500000000172300000000000020045 0ustar00bjornbjorn00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579941126.0 pynvim-0.4.1/test/test_events.py0000644000175000017500000000340100000000000017177 0ustar00bjornbjorn00000000000000# -*- 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] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579941126.0 pynvim-0.4.1/test/test_host.py0000644000175000017500000000136700000000000016661 0ustar00bjornbjorn00000000000000# -*- 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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573891020.0 pynvim-0.4.1/test/test_logging.py0000644000175000017500000000214100000000000017321 0ustar00bjornbjorn00000000000000import 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" ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573891020.0 pynvim-0.4.1/test/test_tabpage.py0000644000175000017500000000262700000000000017307 0ustar00bjornbjorn00000000000000import 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) == "" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1579941126.0 pynvim-0.4.1/test/test_vim.py0000644000175000017500000001476300000000000016503 0ustar00bjornbjorn00000000000000# -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573891020.0 pynvim-0.4.1/test/test_window.py0000644000175000017500000000741100000000000017207 0ustar00bjornbjorn00000000000000import 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) == ""