././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5453439 pynvim-0.5.0/0000755000175000017500000000000014533454457013300 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1600170170.0 pynvim-0.5.0/LICENSE0000644000175000017500000002605613730124272014301 0ustar00jamessanjamessanApache 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1600170170.0 pynvim-0.5.0/MANIFEST.in0000644000175000017500000000006613730124272015023 0ustar00jamessanjamessaninclude README.md LICENSE recursive-include test *.py ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5453439 pynvim-0.5.0/PKG-INFO0000644000175000017500000000071714533454457014402 0ustar00jamessanjamessanMetadata-Version: 2.1 Name: pynvim Version: 0.5.0 Summary: Python client for Neovim Home-page: http://github.com/neovim/pynvim Download-URL: https://github.com/neovim/pynvim/archive/0.5.0.tar.gz Author: Neovim Authors License: Apache Requires-Python: >=3.7 License-File: LICENSE Requires-Dist: msgpack>=0.5.0 Requires-Dist: greenlet>=3.0 Provides-Extra: pyuv Requires-Dist: pyuv>=1.0.0; extra == "pyuv" Provides-Extra: test Requires-Dist: pytest; extra == "test" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/README.md0000644000175000017500000001140614533365244014554 0ustar00jamessanjamessanPynvim: Python client to [Neovim](https://github.com/neovim/neovim) =================================================================== [![Documentation Status](https://readthedocs.org/projects/pynvim/badge/?version=latest)](https://readthedocs.org/projects/pynvim/builds/) [![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. Install ------- Supports python 3.7 or later. pip3 install pynvim You can install the package without being root by adding the `--user` flag. Anytime you upgrade Neovim, make sure to upgrade pynvim as well: pip3 install --upgrade pynvim Alternatively, you can install the development version by cloning this repository and executing the following at the top level: pip3 install . Python Plugin API ----------------- Pynvim supports python _remote plugins_ (via the language-agnostic Nvim rplugin interface), as well as _Vim plugins_ (via the `:python3` interface). Thus when pynvim is installed Neovim will report support for the `+python3` 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, for example: python3 -m virtualenv venv source venv/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 from 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 >>> import pynvim # Create a python API session attached to unix domain socket created above: >>> nvim = pynvim.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 >>> import pynvim >>> nvim = pynvim.attach('child', argv=["/usr/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](https://github.com/neovim/pynvim/tree/master/test) for more examples. Release ------- 1. Create a release commit with title `Pynvim x.y.z` - list significant changes in the commit message - bump the version in `pynvim/_version.py` 2. Make a release on GitHub with the same commit/version tag and copy the message. 3. Run `scripts/disable_log_statements.sh` 4. Run `python -m build` - diff the release tarball `dist/pynvim-x.y.z.tar.gz` against the previous one. 5. Run `twine upload -r pypi dist/*` - Assumes you have a pypi account with permissions. 6. Run `scripts/enable_log_statements.sh` or `git reset --hard` to restore the working dir. 7. Bump up to the next development version in `pynvim/_version.py`, with `prerelease` suffix `dev0`. License ------- [Apache License 2.0](https://github.com/neovim/pynvim/blob/master/LICENSE) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5333438 pynvim-0.5.0/neovim/0000755000175000017500000000000014533454457014575 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1600170170.0 pynvim-0.5.0/neovim/__init__.py0000644000175000017500000000025613730124272016674 0ustar00jamessanjamessan"""Python client for Nvim. This is a transition package. New projects should instead import pynvim package. """ import pynvim from pynvim import * __all__ = pynvim.__all__ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5333438 pynvim-0.5.0/neovim/api/0000755000175000017500000000000014533454457015346 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1600170170.0 pynvim-0.5.0/neovim/api/__init__.py0000644000175000017500000000026113730124272017441 0ustar00jamessanjamessan"""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__ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5373437 pynvim-0.5.0/pynvim/0000755000175000017500000000000014533454457014622 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/__init__.py0000644000175000017500000001375614533454401016734 0ustar00jamessanjamessan"""Python client for Nvim. Client library for talking with Nvim processes via its msgpack-rpc API. """ import logging import os import sys from types import SimpleNamespace as Version from typing import List, Optional, cast, overload from pynvim._version import VERSION, __version__ from pynvim.api import Nvim, NvimError from pynvim.msgpack_rpc import (ErrorResponse, Session, TTransportType, child_session, socket_session, stdio_session, tcp_session) from pynvim.plugin import (Host, autocmd, command, decode, encoding, function, plugin, rpc_export, shutdown_hook) if sys.version_info < (3, 8): from typing_extensions import Literal else: from typing import Literal __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session', 'start_host', 'autocmd', 'command', 'encoding', 'decode', 'function', 'plugin', 'rpc_export', 'Host', 'Nvim', 'NvimError', 'Version', 'VERSION', '__version__', 'shutdown_hook', 'attach', 'setup_logging', 'ErrorResponse', ) def start_host(session: Optional[Session] = None) -> 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) @overload def attach(session_type: Literal['tcp'], address: str, port: int = 7450) -> Nvim: ... @overload def attach(session_type: Literal['socket'], *, path: str) -> Nvim: ... @overload def attach(session_type: Literal['child'], *, argv: List[str]) -> Nvim: ... @overload def attach(session_type: Literal['stdio']) -> Nvim: ... def attach( session_type: TTransportType, address: Optional[str] = None, port: int = 7450, path: Optional[str] = None, argv: Optional[List[str]] = None, decode: Literal[True] = True ) -> Nvim: """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 manager: 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 occurred. """ session = ( tcp_session(cast(str, address), port) if session_type == 'tcp' else socket_session(cast(str, path)) if session_type == 'socket' else stdio_session() if session_type == 'stdio' else child_session(cast(List[str], argv)) if session_type == 'child' else None ) if not session: raise Exception('Unknown session type "%s"' % session_type) return Nvim.from_session(session).with_decode(decode) def setup_logging(name: str) -> None: """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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730562.0 pynvim-0.5.0/pynvim/_version.py0000644000175000017500000000056214533454402017011 0ustar00jamessanjamessan"""Specifies pynvim version.""" # pylint: disable=consider-using-f-string from types import SimpleNamespace # see also setup.py VERSION = SimpleNamespace(major=0, minor=5, patch=0, prerelease="") # e.g. "0.5.0", "0.5.0.dev0" (PEP-440) __version__ = '{major}.{minor}.{patch}'.format(**vars(VERSION)) if VERSION.prerelease: __version__ += '.' + VERSION.prerelease ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5373437 pynvim-0.5.0/pynvim/api/0000755000175000017500000000000014533454457015373 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/api/__init__.py0000644000175000017500000000065514533454401017477 0ustar00jamessanjamessan"""Nvim API subpackage. This package implements a higher-level API that wraps msgpack-rpc `Session` instances. """ from pynvim.api.buffer import Buffer from pynvim.api.common import decode_if_bytes, walk from pynvim.api.nvim import Nvim, NvimError from pynvim.api.tabpage import Tabpage from pynvim.api.window import Window __all__ = ('Nvim', 'Buffer', 'Window', 'Tabpage', 'NvimError', 'decode_if_bytes', 'walk') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/api/buffer.py0000644000175000017500000002357614533454401017220 0ustar00jamessanjamessan"""API for working with a Nvim Buffer.""" from typing import (Any, Iterator, List, Optional, TYPE_CHECKING, Tuple, Union, cast, overload) from pynvim.api.common import Remote from pynvim.compat import check_async if TYPE_CHECKING: from pynvim.api import Nvim __all__ = ('Buffer',) @overload def adjust_index(idx: int, default: Optional[int] = None) -> int: ... @overload def adjust_index(idx: Optional[int], default: int) -> int: ... @overload def adjust_index(idx: Optional[int], default: Optional[int] = None) -> Optional[int]: ... def adjust_index(idx: Optional[int], default: Optional[int] = None) -> Optional[int]: """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_" _session: "Nvim" def __init__(self, session: "Nvim", code_data: Tuple[int, Any]): """Initialize from Nvim and code_data immutable object.""" super().__init__(session, code_data) def __len__(self) -> int: """Return the number of lines contained in a Buffer.""" return self.request('nvim_buf_line_count') @overload def __getitem__(self, idx: int) -> str: # noqa: D105 ... @overload def __getitem__(self, idx: slice) -> List[str]: # noqa: D105 ... def __getitem__(self, idx: Union[int, slice]) -> Union[str, List[str]]: """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, omitting 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) @overload def __setitem__(self, idx: int, item: Optional[str]) -> None: # noqa: D105 ... @overload def __setitem__( # noqa: D105 self, idx: slice, item: Optional[Union[List[str], str]] ) -> None: ... def __setitem__( self, idx: Union[int, slice], item: Union[None, str, List[str]] ) -> None: """Replace a buffer line or slice by integer index. Like with `__getitem__`, indexes may be negative. When replacing slices, omitting indexes(eg: `buffer[:]`) will replace the whole buffer. """ if not isinstance(idx, slice): assert not isinstance(item, list) i = adjust_index(idx) lines = [item] if item is not None else [] return self.request('nvim_buf_set_lines', i, i + 1, True, lines) if item is None: lines = [] elif isinstance(item, str): lines = [item] else: lines = item 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) -> Iterator[str]: """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 transferring all data needed to work. """ lines = self[:] for line in lines: yield line def __delitem__(self, idx: Union[int, slice]) -> None: """Delete line or slice of lines from the buffer. This is the same as __setitem__(idx, []) """ self.__setitem__(idx, None) def __ne__(self, other: Any) -> bool: """Test inequality of Buffers. Necessary for Python 2 compatibility. """ return not self.__eq__(other) def append( self, lines: Union[str, bytes, List[Union[str, bytes]]], index: int = -1 ) -> None: """Append a string or list of lines to the buffer.""" if isinstance(lines, (str, bytes)): lines = [lines] return self.request('nvim_buf_set_lines', index, index, True, lines) def mark(self, name: str) -> Tuple[int, int]: """Return (row, col) tuple for a named mark.""" return cast(Tuple[int, int], tuple(self.request('nvim_buf_get_mark', name))) def range(self, start: int, end: int) -> "Range": """Return a `Range` object, which represents part of the Buffer.""" return Range(self, start, end) def add_highlight( self, hl_group: str, line: int, col_start: int = 0, col_end: int = -1, src_id: int = -1, async_: Optional[bool] = None, **kwargs: Any ) -> int: """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: int, line_start: int = 0, line_end: int = -1, async_: Optional[bool] = None, **kwargs: Any ) -> None: """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: int, hls: List[Union[Tuple[str, int], Tuple[str, int, int, int]]], clear_start: Optional[int] = None, clear_end: int = -1, clear: bool = False, async_: bool = True, ) -> None: """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) -> str: """Get the buffer name.""" return self.request('nvim_buf_get_name') @name.setter def name(self, value: str) -> None: """Set the buffer name. BufFilePre/BufFilePost are triggered.""" return self.request('nvim_buf_set_name', value) @property def valid(self) -> bool: """Return True if the buffer still exists.""" return self.request('nvim_buf_is_valid') @property def loaded(self) -> bool: """Return True if the buffer is valid and loaded.""" return self.request('nvim_buf_is_loaded') @property def number(self) -> int: """Get the buffer number.""" return self.handle class Range(object): def __init__(self, buffer: Buffer, start: int, end: int): self._buffer = buffer self.start = start - 1 self.end = end - 1 def __len__(self) -> int: return self.end - self.start + 1 @overload def __getitem__(self, idx: int) -> str: ... @overload def __getitem__(self, idx: slice) -> List[str]: ... def __getitem__(self, idx: Union[int, slice]) -> Union[str, List[str]]: 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] @overload def __setitem__(self, idx: int, lines: Optional[str]) -> None: ... @overload def __setitem__(self, idx: slice, lines: Optional[List[str]]) -> None: ... def __setitem__( self, idx: Union[int, slice], lines: Union[None, str, List[str]] ) -> None: if not isinstance(idx, slice): assert not isinstance(lines, list) 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) -> Iterator[str]: for i in range(self.start, self.end + 1): yield self._buffer[i] def append( self, lines: Union[str, bytes, List[Union[str, bytes]]], i: Optional[int] = None ) -> None: i = self._normalize_index(i) if i is None: i = self.end + 1 self._buffer.append(lines, i) @overload def _normalize_index(self, index: int) -> int: ... @overload def _normalize_index(self, index: None) -> None: ... def _normalize_index(self, index: Optional[int]) -> Optional[int]: 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/api/common.py0000644000175000017500000001772314533454401017234 0ustar00jamessanjamessan"""Code shared between the API classes.""" import functools import sys from abc import ABC, abstractmethod from typing import (Any, Callable, Generic, Iterator, List, Optional, Tuple, TypeVar, Union, overload) from msgpack import unpackb if sys.version_info < (3, 8): from typing_extensions import Literal, Protocol else: from typing import Literal, Protocol from pynvim.compat import unicode_errors_default __all__ = () T = TypeVar('T') TDecodeMode = Union[Literal[True], str] class NvimError(Exception): pass class IRemote(Protocol): def request(self, name: str, *args: Any, **kwargs: Any) -> Any: raise NotImplementedError class Remote(ABC): """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: IRemote, code_data: Tuple[int, Any]): """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') @property @abstractmethod def _api_prefix(self) -> str: raise NotImplementedError() def __repr__(self) -> str: """Get text representation of the object.""" return '<%s(handle=%r)>' % ( self.__class__.__name__, self.handle, ) def __eq__(self, other: Any) -> bool: """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) -> int: """Return hash based on remote object id.""" return self.code_data.__hash__() def request(self, name: str, *args: Any, **kwargs: Any) -> Any: """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: IRemote, api_prefix: str): """Initialize a RemoteApi with object and api prefix.""" self._obj = obj self._api_prefix = api_prefix def __getattr__(self, name: str) -> Callable[..., Any]: """Return wrapper to named api method.""" return functools.partial(self._obj.request, self._api_prefix + name) E = TypeVar('E', bound=Exception) def transform_keyerror(exc: E) -> Union[E, KeyError]: 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: IRemote, get_method: str, set_method: Optional[str] = None, del_method: Optional[str] = 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: str) -> Any: """Return a map value by key.""" try: return self._get(key) except NvimError as exc: raise transform_keyerror(exc) def __setitem__(self, key: str, value: Any) -> None: """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: str) -> None: """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: str) -> bool: """Check if key is present in the map.""" try: self._get(key) return True except Exception: return False @overload def get(self, key: str, default: T) -> T: ... @overload def get(self, key: str, default: Optional[T] = None) -> Optional[T]: ... def get(self, key: str, default: Optional[T] = None) -> Optional[T]: """Return value for key if present, else a default value.""" try: return self.__getitem__(key) except KeyError: return default class RemoteSequence(Generic[T]): """Represents a sequence of objects stored in Nvim. This class is used to wrap msgpack-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: IRemote, method: str): """Initialize a RemoteSequence with session, method.""" self._fetch = functools.partial(session.request, method) def __len__(self) -> int: """Return the length of the remote sequence.""" return len(self._fetch()) @overload def __getitem__(self, idx: int) -> T: ... @overload def __getitem__(self, idx: slice) -> List[T]: ... def __getitem__(self, idx: Union[slice, int]) -> Union[T, List[T]]: """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) -> Iterator[T]: """Return an iterator for the sequence.""" items = self._fetch() for item in items: yield item def __contains__(self, item: T) -> bool: """Check if an item is present in the sequence.""" return item in self._fetch() @overload def decode_if_bytes(obj: bytes, mode: TDecodeMode = True) -> str: ... @overload def decode_if_bytes(obj: T, mode: TDecodeMode = True) -> Union[T, str]: ... def decode_if_bytes(obj: T, mode: TDecodeMode = True) -> Union[T, str]: """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: Callable[..., Any], obj: Any, *args: Any, **kwargs: Any) -> Any: """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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/api/nvim.py0000644000175000017500000005166714533454401016722 0ustar00jamessanjamessan"""Main Nvim interface.""" import os import sys import threading from functools import partial from traceback import format_stack from types import SimpleNamespace from typing import (Any, AnyStr, Callable, Dict, Iterator, List, Optional, TYPE_CHECKING, Union) from msgpack import ExtType from pynvim.api.buffer import Buffer from pynvim.api.common import (NvimError, Remote, RemoteApi, RemoteMap, RemoteSequence, TDecodeMode, decode_if_bytes, walk) from pynvim.api.tabpage import Tabpage from pynvim.api.window import Window from pynvim.util import format_exc_skip if TYPE_CHECKING: from pynvim.msgpack_rpc import Session if sys.version_info < (3, 8): from typing_extensions import Literal else: from typing import Literal __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: 'Session') -> 'Nvim': """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') 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: 'Nvim') -> '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: 'Session', channel_id: int, metadata: Dict[str, Any], types: Dict[int, Any], decode: TDecodeMode = True, err_cb: Optional[Callable[[str], None]] = 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 = SimpleNamespace(**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[Window] = RemoteSequence(self, 'nvim_list_wins') self.tabpages: RemoteSequence[Tabpage] = 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 if err_cb is None: self._err_cb: Callable[[str], Any] = lambda _: None else: self._err_cb = err_cb self.loop = self._session.loop._loop def _from_nvim(self, obj: Any, decode: Optional[TDecodeMode] = None) -> Any: 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: Any) -> Any: if isinstance(obj, Remote): return ExtType(*obj.code_data) return obj def _get_lua_private(self) -> 'LuaFuncs': if not getattr(self._session, "_has_lua", False): self.exec_lua(lua_module, self.channel_id) self._session._has_lua = True # type: ignore[attr-defined] return getattr(self.lua, "_pynvim_{}".format(self.channel_id)) def request(self, name: str, *args: Any, **kwargs: Any) -> Any: 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) -> Any: """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: Optional[Callable[[str, List[Any]], Any]], notification_cb: Optional[Callable[[str, List[Any]], Any]], setup_cb: Optional[Callable[[], None]] = None, err_cb: Optional[Callable[[str], Any]] = None ) -> 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: str, args: Any) -> Any: name = self._from_nvim(name) args = walk(self._from_nvim, args) try: result = request_cb(name, args) # type: ignore[misc] 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: str, args: Any) -> None: name = self._from_nvim(name) args = walk(self._from_nvim, args) try: notification_cb(name, args) # type: ignore[misc] 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) -> None: """Stop the event loop being started with `run_loop`.""" self._session.stop() def close(self) -> None: """Close the nvim session and release its resources.""" self._session.close() def __enter__(self) -> 'Nvim': """Enter nvim session as a context manager.""" return self def __exit__(self, *exc_info: Any) -> None: """Exit nvim session as a context manager. Closes the event loop. """ self.close() def with_decode(self, decode: Literal[True] = True) -> 'Nvim': """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: int, height: int, rgb: Optional[bool] = None, **kwargs: Any ) -> None: """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) -> None: """Unregister as a remote UI.""" return self.request('nvim_ui_detach') def ui_try_resize(self, width: int, height: int) -> None: """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: str) -> None: """Subscribe to a Nvim event.""" return self.request('nvim_subscribe', event) def unsubscribe(self, event: str) -> None: """Unsubscribe to a Nvim event.""" return self.request('nvim_unsubscribe', event) def command(self, string: str, **kwargs: Any) -> None: """Execute a single ex command.""" return self.request('nvim_command', string, **kwargs) def command_output(self, string: str) -> str: """Execute a single ex command and return the output.""" return self.request('nvim_command_output', string) def eval(self, string: str, **kwargs: Any) -> Any: """Evaluate a vimscript expression.""" return self.request('nvim_eval', string, **kwargs) def call(self, name: str, *args: Any, **kwargs: Any) -> Any: """Call a vimscript function.""" return self.request('nvim_call_function', name, args, **kwargs) def exec_lua(self, code: str, *args: Any, **kwargs: Any) -> Any: """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: str) -> int: """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) -> List[str]: """Return a list of paths contained in the 'runtimepath' option.""" return self.request('nvim_list_runtime_paths') def foreach_rtp(self, cb: Callable[[str], Any]) -> None: """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.list_runtime_paths(): try: if cb(path) is not None: break except Exception: break def chdir(self, dir_path: str) -> None: """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: str, options: str = '', escape_csi: bool = True) -> None: """Push `keys` to Nvim user input buffer.""" return self.request('nvim_feedkeys', keys, options, escape_csi) def input(self, bytes: AnyStr) -> int: """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: str, from_part: bool = False, do_lt: bool = True, special: bool = True ) -> str: 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: str, **kwargs: Any) -> None: 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: str, **kwargs: Any) -> None: 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) -> bool: return (self._session._loop_thread is not None and threading.current_thread() != self._session._loop_thread) def quit(self, quit_command: str = 'qa!') -> None: """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 OSError: # 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) -> int: """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: Callable[..., Any], *args: Any, **kwargs: Any) -> None: """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() -> None: 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: Nvim): """Initialize a Buffers object with Nvim object `nvim`.""" self._fetch_buffers = nvim.api.list_bufs def __len__(self) -> int: """Return the count of buffers.""" return len(self._fetch_buffers()) def __getitem__(self, number: int) -> Buffer: """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: Buffer) -> bool: """Return whether Buffer `b` is a known valid buffer.""" return isinstance(b, Buffer) and b.valid def __iter__(self) -> Iterator[Buffer]: """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: Nvim): self.threadsafe_call = nvim.async_call class Current(object): """Helper class for emulating vim.current from python-vim.""" def __init__(self, session: Nvim): self._session = session self.range = None @property def line(self) -> str: return self._session.request('nvim_get_current_line') @line.setter def line(self, line: str) -> None: return self._session.request('nvim_set_current_line', line) @line.deleter def line(self) -> None: return self._session.request('nvim_del_current_line') @property def buffer(self) -> Buffer: return self._session.request('nvim_get_current_buf') @buffer.setter def buffer(self, buffer: Union[Buffer, int]) -> None: return self._session.request('nvim_set_current_buf', buffer) @property def window(self) -> Window: return self._session.request('nvim_get_current_win') @window.setter def window(self, window: Union[Window, int]) -> None: return self._session.request('nvim_set_current_win', window) @property def tabpage(self) -> Tabpage: return self._session.request('nvim_get_current_tabpage') @tabpage.setter def tabpage(self, tabpage: Union[Tabpage, int]) -> None: return self._session.request('nvim_set_current_tabpage', tabpage) class Funcs(object): """Helper class for functional vimscript interface.""" def __init__(self, nvim: Nvim): self._nvim = nvim def __getattr__(self, name: str) -> Callable[..., Any]: return partial(self._nvim.call, name) class LuaFuncs(object): """Wrapper to allow lua functions to be called like python methods.""" def __init__(self, nvim: Nvim, name: str = ""): self._nvim = nvim self.name = name def __getattr__(self, name: str) -> 'LuaFuncs': """Return wrapper to named api method.""" prefix = self.name + "." if self.name else "" return LuaFuncs(self._nvim, prefix + name) def __call__(self, *args: Any, **kwargs: Any) -> Any: # 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/api/tabpage.py0000644000175000017500000000237414533454401017343 0ustar00jamessanjamessan"""API for working with Nvim tabpages.""" from typing import Any, TYPE_CHECKING, Tuple from pynvim.api.common import Remote, RemoteSequence from pynvim.api.window import Window if TYPE_CHECKING: from pynvim.api.nvim import Nvim __all__ = ['Tabpage'] class Tabpage(Remote): """A remote Nvim tabpage.""" _api_prefix = "nvim_tabpage_" def __init__(self, session: 'Nvim', code_data: Tuple[int, Any]): """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__(session, code_data) self.windows: RemoteSequence[Window] = RemoteSequence( self, "nvim_tabpage_list_wins" ) @property def window(self) -> Window: """Get the `Window` currently focused on the tabpage.""" return self.request('nvim_tabpage_get_win') @property def valid(self) -> bool: """Return True if the tabpage still exists.""" return self.request('nvim_tabpage_is_valid') @property def number(self) -> int: """Get the tabpage number.""" return self.request('nvim_tabpage_get_number') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/api/window.py0000644000175000017500000000436714533454401017253 0ustar00jamessanjamessan"""API for working with Nvim windows.""" from typing import TYPE_CHECKING, Tuple, cast from pynvim.api.buffer import Buffer from pynvim.api.common import Remote if TYPE_CHECKING: from pynvim.api.tabpage import Tabpage __all__ = ['Window'] class Window(Remote): """A remote Nvim window.""" _api_prefix = "nvim_win_" @property def buffer(self) -> Buffer: """Get the `Buffer` currently being displayed by the window.""" return self.request('nvim_win_get_buf') @property def cursor(self) -> Tuple[int, int]: """Get the (row, col) tuple with the current cursor position.""" return cast(Tuple[int, int], tuple(self.request('nvim_win_get_cursor'))) @cursor.setter def cursor(self, pos: Tuple[int, int]) -> None: """Set the (row, col) tuple as the new cursor position.""" return self.request('nvim_win_set_cursor', pos) @property def height(self) -> int: """Get the window height in rows.""" return self.request('nvim_win_get_height') @height.setter def height(self, height: int) -> None: """Set the window height in rows.""" return self.request('nvim_win_set_height', height) @property def width(self) -> int: """Get the window width in rows.""" return self.request('nvim_win_get_width') @width.setter def width(self, width: int) -> None: """Set the window height in rows.""" return self.request('nvim_win_set_width', width) @property def row(self) -> int: """0-indexed, on-screen window position(row) in display cells.""" return self.request('nvim_win_get_position')[0] @property def col(self) -> int: """0-indexed, on-screen window position(col) in display cells.""" return self.request('nvim_win_get_position')[1] @property def tabpage(self) -> 'Tabpage': """Get the `Tabpage` that contains the window.""" return self.request('nvim_win_get_tabpage') @property def valid(self) -> bool: """Return True if the window still exists.""" return self.request('nvim_win_is_valid') @property def number(self) -> int: """Get the window number.""" return self.request('nvim_win_get_number') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/compat.py0000644000175000017500000000270114533454401016444 0ustar00jamessanjamessan"""Code for compatibility across Python versions.""" import warnings from typing import Any, Dict, Optional def find_module(fullname, path): # type: ignore """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 from imp import find_module as original_find_module return original_find_module(fullname, path) unicode_errors_default = 'surrogateescape' NUM_TYPES = (int, float) def check_async(async_: Optional[bool], kwargs: Dict[str, Any], default: bool) -> bool: """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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5373437 pynvim-0.5.0/pynvim/msgpack_rpc/0000755000175000017500000000000014533454457017113 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/msgpack_rpc/__init__.py0000644000175000017500000000317614533454401021220 0ustar00jamessanjamessan"""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 typing import Any, List from pynvim.msgpack_rpc.async_session import AsyncSession from pynvim.msgpack_rpc.event_loop import EventLoop, TTransportType from pynvim.msgpack_rpc.msgpack_stream import MsgpackStream from pynvim.msgpack_rpc.session import ErrorResponse, Session from pynvim.util import get_client_info __all__ = ('tcp_session', 'socket_session', 'stdio_session', 'child_session', 'ErrorResponse') def session( transport_type: TTransportType = 'stdio', *args: Any, **kwargs: Any ) -> Session: 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: str, port: int = 7450) -> Session: """Create a msgpack-rpc session from a tcp address/port.""" return session('tcp', address, port) def socket_session(path: str) -> Session: """Create a msgpack-rpc session from a unix domain socket.""" return session('socket', path) def stdio_session() -> Session: """Create a msgpack-rpc session from stdin/stdout.""" return session('stdio') def child_session(argv: List[str]) -> Session: """Create a msgpack-rpc session from a new Nvim instance.""" return session('child', argv) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/msgpack_rpc/async_session.py0000644000175000017500000001170514533454401022336 0ustar00jamessanjamessan"""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 received """ 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5413437 pynvim-0.5.0/pynvim/msgpack_rpc/event_loop/0000755000175000017500000000000014533454457021265 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/msgpack_rpc/event_loop/__init__.py0000644000175000017500000000046214533454401023365 0ustar00jamessanjamessan"""Event loop abstraction subpackage. Tries to use pyuv as a backend, falling back to the asyncio implementation. """ from pynvim.msgpack_rpc.event_loop.asyncio import AsyncioEventLoop as EventLoop from pynvim.msgpack_rpc.event_loop.base import TTransportType __all__ = ['EventLoop', 'TTransportType'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/msgpack_rpc/event_loop/asyncio.py0000644000175000017500000001471214533454401023276 0ustar00jamessanjamessan"""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). """ from __future__ import absolute_import import asyncio import logging import os import sys from collections import deque from signal import Signals from typing import Any, Callable, Deque, List, Optional from pynvim.msgpack_rpc.event_loop.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 # type: ignore[attr-defined] 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 # type: ignore[attr-defined,misc] class AsyncioEventLoop(BaseEventLoop, asyncio.Protocol, asyncio.SubprocessProtocol): """`BaseEventLoop` subclass that uses `asyncio` as a backend.""" _queued_data: Deque[bytes] if os.name != 'nt': _child_watcher: Optional['asyncio.AbstractChildWatcher'] 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: bytes) -> None: """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.""" pass # replaces next logging statement # debug("pipe_connection_lost: fd = %s, exc = %s", fd, exc) if os.name == 'nt' and fd == 2: # stderr # On windows, ignore piped stderr being closed immediately (#505) return 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 # Ignore stderr message, log only for debugging pass # replaces next logging statement # debug("stderr: %s", str(data)) elif self._on_data: self._on_data(data) else: self._queued_data.append(data) def process_exited(self) -> None: """Used to signal `asyncio.SubprocessProtocol` when the child exits.""" self._on_error('EOF') def _init(self) -> None: self._loop = loop_cls() self._queued_data = deque() self._fact = lambda: self self._raw_transport = None self._child_watcher = None def _connect_tcp(self, address: str, port: int) -> None: coroutine = self._loop.create_connection(self._fact, address, port) self._loop.run_until_complete(coroutine) def _connect_socket(self, path: str) -> None: if os.name == 'nt': coroutine = self._loop.create_pipe_connection( # type: ignore[attr-defined] self._fact, path ) else: coroutine = self._loop.create_unix_connection(self._fact, path) self._loop.run_until_complete(coroutine) def _connect_stdio(self) -> None: if os.name == 'nt': pipe: Any = PipeHandle( msvcrt.get_osfhandle(sys.stdin.fileno()) # type: ignore[attr-defined] ) 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) # type: ignore[attr-defined] ) else: pipe = os.fdopen(rename_stdout, 'wb') coroutine = self._loop.connect_write_pipe(self._fact, pipe) # type: ignore[assignment] self._loop.run_until_complete(coroutine) pass # replaces next logging statement # debug("native stdout connection successful") def _connect_child(self, argv: List[str]) -> None: 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) -> None: pass def _send(self, data: bytes) -> None: self._transport.write(data) def _run(self) -> None: while self._queued_data: data = self._queued_data.popleft() if self._on_data is not None: self._on_data(data) self._loop.run_forever() def _stop(self) -> None: self._loop.stop() def _close(self) -> None: if self._raw_transport is not None: self._raw_transport.close() self._loop.close() if self._child_watcher is not None: self._child_watcher.close() self._child_watcher = None def _threadsafe_call(self, fn: Callable[[], Any]) -> None: self._loop.call_soon_threadsafe(fn) def _setup_signals(self, signals: List[Signals]) -> None: 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) -> None: for signum in self._signals: self._loop.remove_signal_handler(signum) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/msgpack_rpc/event_loop/base.py0000644000175000017500000002140014533454401022533 0ustar00jamessanjamessan"""Common code for event loop implementations.""" import logging import signal import sys import threading from abc import ABC, abstractmethod from typing import Any, Callable, List, Optional, Type, Union if sys.version_info < (3, 8): from typing_extensions import Literal else: from typing import Literal 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() TTransportType = Union[ Literal['stdio'], Literal['socket'], Literal['tcp'], Literal['child'] ] class BaseEventLoop(ABC): """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: TTransportType, *args: Any, **kwargs: Any): """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: Optional[Callable[[bytes], None]] = None self._error: Optional[BaseException] = None self._init() try: getattr(self, '_connect_{}'.format(transport_type))(*args, **kwargs) except Exception as e: self.close() raise e self._start_reading() @abstractmethod def _init(self) -> None: raise NotImplementedError() @abstractmethod def _start_reading(self) -> None: raise NotImplementedError() @abstractmethod def _send(self, data: bytes) -> None: raise NotImplementedError() def connect_tcp(self, address: str, port: int) -> None: """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) @abstractmethod def _connect_tcp(self, address: str, port: int) -> None: raise NotImplementedError() def connect_socket(self, path: str) -> None: """Connect to socket at `path`. Delegated to `_connect_socket`.""" pass # replaces next logging statement # info('Connecting to %s', path) self._connect_socket(path) @abstractmethod def _connect_socket(self, path: str) -> None: raise NotImplementedError() def connect_stdio(self) -> None: """Connect using stdin/stdout. Delegated to `_connect_stdio`.""" pass # replaces next logging statement # info('Preparing stdin/stdout for streaming data') self._connect_stdio() @abstractmethod def _connect_stdio(self) -> None: raise NotImplementedError() 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) @abstractmethod def _connect_child(self, argv: List[str]) -> None: raise NotImplementedError() def send(self, data: bytes) -> None: """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) -> None: """Stop the event loop.""" self._stop() pass # replaces next logging statement # debug('Stopped event loop') @abstractmethod def _stop(self) -> None: raise NotImplementedError() def close(self) -> None: """Stop the event loop.""" self._close() pass # replaces next logging statement # debug('Closed event loop') @abstractmethod def _close(self) -> None: raise NotImplementedError() def _on_signal(self, signum: signal.Signals) -> None: 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: Type[BaseException] = Exception if signum == signal.SIGINT: cls = KeyboardInterrupt self._error = cls(msg) self.stop() def _on_error(self, error: str) -> None: pass # replaces next logging statement # debug(error) self._error = OSError(error) self.stop() def _on_interrupt(self) -> None: self.stop() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/msgpack_rpc/event_loop/uv.py0000644000175000017500000000773314533454401022270 0ustar00jamessanjamessan"""Event loop implementation that uses pyuv(libuv-python bindings).""" import sys from collections import deque import pyuv from pynvim.msgpack_rpc.event_loop.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 = OSError(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 OSError('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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/msgpack_rpc/msgpack_stream.py0000644000175000017500000000426214533454401022456 0ustar00jamessanjamessan"""Msgpack handling in the event loop pipeline.""" import logging from msgpack import Packer, Unpacker from pynvim.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(unicode_errors=unicode_errors_default) 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730561.0 pynvim-0.5.0/pynvim/msgpack_rpc/session.py0000644000175000017500000002513714533454401021145 0ustar00jamessanjamessan"""Synchronous msgpack-rpc session layer.""" import logging import sys import threading from collections import deque from traceback import format_exc from typing import (Any, AnyStr, Callable, Deque, List, NamedTuple, Optional, Sequence, Tuple, Union, cast) import greenlet from pynvim.compat import check_async from pynvim.msgpack_rpc.async_session import AsyncSession if sys.version_info < (3, 8): from typing_extensions import Literal else: from typing import Literal logger = logging.getLogger(__name__) error, debug, info, warn = (logger.error, logger.debug, logger.info, logger.warning,) class Request(NamedTuple): """A request from Nvim.""" type: Literal['request'] name: str args: List[Any] response: Any class Notification(NamedTuple): """A notification from Nvim.""" type: Literal['notification'] name: str args: List[Any] Message = Union[Request, Notification] 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: AsyncSession): """Wrap `async_session` on a synchronous msgpack-rpc interface.""" self._async_session = async_session self._request_cb: Optional[Callable[[str, List[Any]], None]] = None self._notification_cb: Optional[Callable[[str, List[Any]], None]] = None self._pending_messages: Deque[Message] = deque() self._is_running = False self._setup_exception: Optional[Exception] = None self.loop = async_session.loop self._loop_thread: Optional[threading.Thread] = None self.error_wrapper: Callable[[Tuple[int, str]], Exception] = \ lambda e: Exception(e[1]) def threadsafe_call( self, fn: Callable[..., Any], *args: Any, **kwargs: Any ) -> None: """Wrapper around `AsyncSession.threadsafe_call`.""" def handler(): try: fn(*args, **kwargs) except Exception: pass # replaces next logging statement # warn("error caught while executing 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) -> Optional[Message]: """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() return None def request(self, method: AnyStr, *args: Any, **kwargs: Any) -> Any: """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 OSError('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: Callable[[str, List[Any]], None], notification_cb: Callable[[str, List[Any]], None], setup_cb: Optional[Callable[[], None]] = None) -> 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() -> None: try: setup_cb() # type: ignore[misc] 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( # type: ignore[unreachable] # '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) -> None: """Stop the event loop.""" self._async_session.stop() def close(self) -> None: """Close the event loop.""" self._async_session.close() def _yielding_request( self, method: AnyStr, args: Sequence[Any] ) -> Tuple[Tuple[int, str], Any]: 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: AnyStr, args: Sequence[Any] ) -> Tuple[Tuple[int, str], Any]: 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 cast(Tuple[Tuple[int, str], Any], tuple(result)) def _enqueue_request_and_stop( self, name: str, args: List[Any], response: Any ) -> None: self._enqueue_request(name, args, response) self.stop() def _enqueue_notification_and_stop(self, name: str, args: List[Any]) -> None: self._enqueue_notification(name, args) self.stop() def _enqueue_request(self, name: str, args: List[Any], response: Any) -> None: self._pending_messages.append(Request('request', name, args, response,)) def _enqueue_notification(self, name: str, args: List[Any]) -> None: self._pending_messages.append(Notification('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 # debug("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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5413437 pynvim-0.5.0/pynvim/plugin/0000755000175000017500000000000014533454457016120 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730562.0 pynvim-0.5.0/pynvim/plugin/__init__.py0000644000175000017500000000060414533454402020217 0ustar00jamessanjamessan"""Nvim plugin/host subpackage.""" from pynvim.plugin.decorators import (autocmd, command, decode, encoding, function, plugin, rpc_export, shutdown_hook) from pynvim.plugin.host import Host # type: ignore[attr-defined] __all__ = ('Host', 'plugin', 'rpc_export', 'command', 'autocmd', 'function', 'encoding', 'decode', 'shutdown_hook') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730562.0 pynvim-0.5.0/pynvim/plugin/decorators.py0000644000175000017500000001412114533454402020624 0ustar00jamessanjamessan"""Decorators used by python host plugin system.""" import inspect import logging import sys from typing import Any, Callable, Dict, Optional, TypeVar, Union from pynvim.compat import unicode_errors_default if sys.version_info < (3, 8): from typing_extensions import Literal else: from typing import Literal logger = logging.getLogger(__name__) debug, info, warn = (logger.debug, logger.info, logger.warning,) __all__ = ('plugin', 'rpc_export', 'command', 'autocmd', 'function', 'encoding', 'decode', 'shutdown_hook') T = TypeVar('T') F = TypeVar('F', bound=Callable[..., Any]) def plugin(cls: T) -> T: """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 # type: ignore[attr-defined] # 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): fn._nvim_bind = False return cls def rpc_export(rpc_method_name: str, sync: bool = False) -> Callable[[F], F]: """Export a function or plugin method as a msgpack-rpc request handler.""" def dec(f: F) -> F: f._nvim_rpc_method_name = rpc_method_name # type: ignore[attr-defined] f._nvim_rpc_sync = sync # type: ignore[attr-defined] f._nvim_bind = True # type: ignore[attr-defined] f._nvim_prefix_plugin_path = False # type: ignore[attr-defined] return f return dec def command( name: str, nargs: Union[str, int] = 0, complete: Optional[str] = None, range: Optional[Union[str, int]] = None, count: Optional[int] = None, bang: bool = False, register: bool = False, sync: bool = False, allow_nested: bool = False, eval: Optional[str] = None ) -> Callable[[F], F]: """Tag a function or plugin method as a Nvim command handler.""" def dec(f: F) -> F: f._nvim_rpc_method_name = ( # type: ignore[attr-defined] 'command:{}'.format(name) ) f._nvim_rpc_sync = sync # type: ignore[attr-defined] f._nvim_bind = True # type: ignore[attr-defined] f._nvim_prefix_plugin_path = True # type: ignore[attr-defined] opts: Dict[str, Any] = {} 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: Union[bool, Literal['urgent']] = "urgent" else: rpc_sync = sync f._nvim_rpc_spec = { # type: ignore[attr-defined] 'type': 'command', 'name': name, 'sync': rpc_sync, 'opts': opts } return f return dec def autocmd( name: str, pattern: str = '*', sync: bool = False, allow_nested: bool = False, eval: Optional[str] = None ) -> Callable[[F], F]: """Tag a function or plugin method as a Nvim autocommand handler.""" def dec(f: F) -> F: f._nvim_rpc_method_name = ( # type: ignore[attr-defined] 'autocmd:{}:{}'.format(name, pattern) ) f._nvim_rpc_sync = sync # type: ignore[attr-defined] f._nvim_bind = True # type: ignore[attr-defined] f._nvim_prefix_plugin_path = True # type: ignore[attr-defined] opts = { 'pattern': pattern } if eval: opts['eval'] = eval if not sync and allow_nested: rpc_sync: Union[bool, Literal['urgent']] = "urgent" else: rpc_sync = sync f._nvim_rpc_spec = { # type: ignore[attr-defined] 'type': 'autocmd', 'name': name, 'sync': rpc_sync, 'opts': opts } return f return dec def function( name: str, range: Union[bool, str, int] = False, sync: bool = False, allow_nested: bool = False, eval: Optional[str] = None ) -> Callable[[F], F]: """Tag a function or plugin method as a Nvim function handler.""" def dec(f: F) -> F: f._nvim_rpc_method_name = ( # type: ignore[attr-defined] 'function:{}'.format(name) ) f._nvim_rpc_sync = sync # type: ignore[attr-defined] f._nvim_bind = True # type: ignore[attr-defined] f._nvim_prefix_plugin_path = True # type: ignore[attr-defined] 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: Union[bool, Literal['urgent']] = "urgent" else: rpc_sync = sync f._nvim_rpc_spec = { # type: ignore[attr-defined] 'type': 'function', 'name': name, 'sync': rpc_sync, 'opts': opts } return f return dec def shutdown_hook(f: F) -> F: """Tag a function or method as a shutdown hook.""" f._nvim_shutdown_hook = True # type: ignore[attr-defined] f._nvim_bind = True # type: ignore[attr-defined] return f def decode(mode: str = unicode_errors_default) -> Callable[[F], F]: """Configure automatic encoding/decoding of strings.""" def dec(f: F) -> F: f._nvim_decode = mode # type: ignore[attr-defined] return f return dec def encoding(encoding: Union[bool, str] = True) -> Callable[[F], F]: """DEPRECATED: use pynvim.decode().""" if isinstance(encoding, str): encoding = True def dec(f: F) -> F: f._nvim_decode = encoding # type: ignore[attr-defined] return f return dec ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730562.0 pynvim-0.5.0/pynvim/plugin/host.py0000644000175000017500000002657314533454402017452 0ustar00jamessanjamessan# type: ignore """Implements a Nvim host for python plugins.""" import importlib import inspect import logging import os import os.path import re import sys from functools import partial from traceback import format_exc from typing import Any, Sequence from pynvim.api import Nvim, decode_if_bytes, walk from pynvim.msgpack_rpc import ErrorResponse from pynvim.plugin import script_host from pynvim.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": {}} def _handle_import(path: str, name: str): """Import python module `name` from a known file path or module directory. The path should be the base directory from which the module can be imported. To support python 3.12, the use of `imp` should be avoided. @see https://docs.python.org/3.12/whatsnew/3.12.html#imp """ if not name: raise ValueError("Missing module name.") sys.path.append(path) return importlib.import_module(name) 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: 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 } self._decode_default = True def _on_async_err(self, msg: str) -> None: # uncaught python exception self.nvim.err_write(msg, async_=True) def _on_error_event(self, kind: Any, msg: str) -> None: # 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) -> None: """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: return self._request_handlers[name](*args) else: return 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: str, args: Sequence[Any]) -> None: """Handle a msgpack-rpc request.""" 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: str, args: Sequence[Any]) -> None: """Handle a msgpack-rpc notification.""" 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: Sequence[str]) -> None: """Load the remote plugins and register handlers defined in the plugins. Args: plugins: List of plugin paths to rplugin python modules registered by remote#host#RegisterPlugin('python3', ...) (see the generated rplugin.vim manifest) """ # self.nvim.err_write("host init _load\n", async_=True) has_script = False for path in plugins: path = os.path.normpath(path) # normalize path err = None if path in self._loaded: pass # replaces next logging statement # warn('{} 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]) module = _handle_import(directory, name) 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) -> None: 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): 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730562.0 pynvim-0.5.0/pynvim/plugin/script_host.py0000644000175000017500000002320214533454402021020 0ustar00jamessanjamessan# type: ignore """Legacy python/python3-vim emulation.""" import io import logging import os import sys from importlib.machinery import PathFinder from types import ModuleType from pynvim.api import Nvim, walk from pynvim.msgpack_rpc import ErrorResponse from pynvim.plugin.decorators import plugin, rpc_export from pynvim.util import format_exc_skip __all__ = ('ScriptHost',) logger = logging.getLogger(__name__) debug, info, warn = (logger.debug, logger.info, logger.warn,) @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 = ModuleType('__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 # mimic Vim by importing vim module by default. exec('import vim', self.module.__dict__) # 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) if script.startswith('='): # Handle ":py= ...". Evaluate as an expression and print. # (note: a valid python statement can't start with "=") expr = script[1:] print(self.python_eval(expr)) return try: # pylint: disable-next=exec-used exec(script, self.module.__dict__) except Exception as exc: raise ErrorResponse(format_exc_skip(1)) from exc @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, 'rb') as f: script = compile(f.read(), file_path, 'exec') try: # pylint: disable-next=exec-used exec(script, self.module.__dict__) except Exception as exc: raise ErrorResponse(format_exc_skip(1)) from exc @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, str): 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.""" try: # pylint: disable-next=eval-used return eval(expr, self.module.__dict__) except Exception as exc: raise ErrorResponse(format_exc_skip(1)) from exc @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)) num_types = (int, float) def num_to_str(obj): if isinstance(obj, num_types) and not isinstance(obj, bool): 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): import imp 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 import imp 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', 'python3']: path = os.path.join(rtp, subdir) if os.path.exists(path): rv.append(path) return rv ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730562.0 pynvim-0.5.0/pynvim/util.py0000644000175000017500000000163514533454402016144 0ustar00jamessanjamessan"""Shared utility functions.""" import sys from traceback import format_exception from typing import Any, Dict, Optional, Tuple, TypeVar from pynvim._version import VERSION def format_exc_skip(skip: int, limit: Optional[int] = None) -> str: """Like traceback.format_exc but allow skipping the first frames.""" etype, val, tb = sys.exc_info() for _ in range(skip): if tb is not None: tb = tb.tb_next return ("".join(format_exception(etype, val, tb, limit))).rstrip() T1 = TypeVar("T1") T2 = TypeVar("T2") def get_client_info( kind: str, type_: T1, method_spec: T2 ) -> Tuple[str, Dict[str, Any], T1, T2, Dict[str, str]]: """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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5413437 pynvim-0.5.0/pynvim.egg-info/0000755000175000017500000000000014533454457016314 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730606.0 pynvim-0.5.0/pynvim.egg-info/PKG-INFO0000644000175000017500000000071714533454456017415 0ustar00jamessanjamessanMetadata-Version: 2.1 Name: pynvim Version: 0.5.0 Summary: Python client for Neovim Home-page: http://github.com/neovim/pynvim Download-URL: https://github.com/neovim/pynvim/archive/0.5.0.tar.gz Author: Neovim Authors License: Apache Requires-Python: >=3.7 License-File: LICENSE Requires-Dist: msgpack>=0.5.0 Requires-Dist: greenlet>=3.0 Provides-Extra: pyuv Requires-Dist: pyuv>=1.0.0; extra == "pyuv" Provides-Extra: test Requires-Dist: pytest; extra == "test" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730606.0 pynvim-0.5.0/pynvim.egg-info/SOURCES.txt0000644000175000017500000000242314533454456020200 0ustar00jamessanjamessanLICENSE MANIFEST.in README.md pyproject.toml setup.cfg setup.py neovim/__init__.py neovim/api/__init__.py pynvim/__init__.py pynvim/_version.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/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/__init__.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_version.py test/test_vim.py test/test_window.py test/fixtures/module_plugin/rplugin/python3/mymodule/__init__.py test/fixtures/module_plugin/rplugin/python3/mymodule/plugin.py test/fixtures/simple_plugin/rplugin/python3/simple_nvim.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730606.0 pynvim-0.5.0/pynvim.egg-info/dependency_links.txt0000644000175000017500000000000114533454456022361 0ustar00jamessanjamessan ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730606.0 pynvim-0.5.0/pynvim.egg-info/requires.txt0000644000175000017500000000010014533454456020702 0ustar00jamessanjamessanmsgpack>=0.5.0 greenlet>=3.0 [pyuv] pyuv>=1.0.0 [test] pytest ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701730606.0 pynvim-0.5.0/pynvim.egg-info/top_level.txt0000644000175000017500000000001614533454456021042 0ustar00jamessanjamessanneovim pynvim ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/pyproject.toml0000644000175000017500000000012014533365244016200 0ustar00jamessanjamessan[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta"././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5453439 pynvim-0.5.0/setup.cfg0000644000175000017500000000115414533454457015122 0ustar00jamessanjamessan[aliases] test = pytest [flake8] extend-ignore = D211,E731,D401,W503 max-line-length = 100 per-file-ignores = test/*:D1 application-import-names = pynvim [isort] known_first_party = pynvim [tool:pytest] testpaths = test [mypy] disallow_untyped_calls = true disallow_untyped_defs = true disallow_incomplete_defs = true disallow_untyped_decorators = true ignore_missing_imports = true warn_redundant_casts = true warn_unused_ignores = true warn_unreachable = true strict_equality = true [mypy-pynvim.msgpack_rpc.*] disallow_untyped_calls = false disallow_untyped_defs = false [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/setup.py0000644000175000017500000000317714533365244015015 0ustar00jamessanjamessan"""setup.py for pynvim.""" import os.path import platform import sys __PATH__ = os.path.abspath(os.path.dirname(__file__)) 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', ] extras_require = { 'pyuv': ['pyuv>=1.0.0'], 'test': tests_require, } if platform.python_implementation() != 'PyPy': # pypy already includes an implementation of the greenlet module install_requires.append('greenlet>=3.0') if sys.version_info < (3, 8): install_requires.append('typing-extensions') # __version__: see pynvim/_version.py with open(os.path.join(__PATH__, "pynvim/_version.py"), "r", encoding="utf-8") as fp: _version_env = {} exec(fp.read(), _version_env) # pylint: disable=exec-used version = _version_env['__version__'] setup(name='pynvim', version=version, description='Python client for Neovim', url='http://github.com/neovim/pynvim', download_url=f'https://github.com/neovim/pynvim/archive/{version}.tar.gz', author='Neovim Authors', license='Apache', packages=['pynvim', 'pynvim.api', 'pynvim.msgpack_rpc', 'pynvim.msgpack_rpc.event_loop', 'pynvim.plugin', 'neovim', 'neovim.api'], python_requires=">=3.7", install_requires=install_requires, setup_requires=setup_requires, tests_require=tests_require, extras_require=extras_require, options={"bdist_wheel": {"universal": True}}, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5413437 pynvim-0.5.0/test/0000755000175000017500000000000014533454457014257 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/__init__.py0000644000175000017500000000000014533365244016351 0ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/conftest.py0000644000175000017500000000261514533365244016455 0ustar00jamessanjamessan"""Configs for pytest.""" import gc import json import os import sys from typing import Generator import pytest import pynvim pynvim.setup_logging("test") @pytest.fixture def vim() -> Generator[pynvim.Nvim, None, None]: """Create an embedded, sub-process Nvim fixture instance.""" editor: pynvim.Nvim 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 = json.dumps([ "nvim", "--clean", # no config and plugins (-u NONE), no SHADA "-n", # no swap file "--embed", "--headless", # Always use the same exact python executable regardless of $PATH "--cmd", f"let g:python3_host_prog='{sys.executable}'", ]) if child_argv is not None: editor = pynvim.attach('child', argv=json.loads(child_argv)) else: assert listen_address is not None and listen_address != '' editor = pynvim.attach('socket', path=listen_address) try: yield editor finally: # Ensure all internal resources (pipes, transports, etc.) are always # closed properly. Otherwise, during GC finalizers (__del__) will raise # "Event loop is closed" error. editor.close() gc.collect() # force-run GC, to early-detect potential leakages ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5333438 pynvim-0.5.0/test/fixtures/0000755000175000017500000000000014533454457016130 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5333438 pynvim-0.5.0/test/fixtures/module_plugin/0000755000175000017500000000000014533454457020773 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5333438 pynvim-0.5.0/test/fixtures/module_plugin/rplugin/0000755000175000017500000000000014533454457022453 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5333438 pynvim-0.5.0/test/fixtures/module_plugin/rplugin/python3/0000755000175000017500000000000014533454457024057 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5413437 pynvim-0.5.0/test/fixtures/module_plugin/rplugin/python3/mymodule/0000755000175000017500000000000014533454457025712 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/fixtures/module_plugin/rplugin/python3/mymodule/__init__.py0000644000175000017500000000050114533365244030012 0ustar00jamessanjamessan"""The `mymodule` package for the fixture module plugin.""" # pylint: disable=all # Somehow the plugin might be using relative imports. from .plugin import MyPlugin as MyPlugin # ... or absolute import (assuming this is the root package) import mymodule.plugin # noqa: I100 assert mymodule.plugin.MyPlugin is MyPlugin ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/fixtures/module_plugin/rplugin/python3/mymodule/plugin.py0000644000175000017500000000047514533365244027563 0ustar00jamessanjamessan"""Actual implement lies here.""" import pynvim as neovim import pynvim.api @neovim.plugin class MyPlugin: def __init__(self, nvim: pynvim.api.Nvim): self.nvim = nvim @neovim.command("ModuleHelloWorld") def hello_world(self) -> None: self.nvim.command("echom 'MyPlugin: Hello World!'") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5333438 pynvim-0.5.0/test/fixtures/simple_plugin/0000755000175000017500000000000014533454457020777 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5333438 pynvim-0.5.0/test/fixtures/simple_plugin/rplugin/0000755000175000017500000000000014533454457022457 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1701730606.5413437 pynvim-0.5.0/test/fixtures/simple_plugin/rplugin/python3/0000755000175000017500000000000014533454457024063 5ustar00jamessanjamessan././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/fixtures/simple_plugin/rplugin/python3/simple_nvim.py0000644000175000017500000000043214533365244026751 0ustar00jamessanjamessanimport neovim import pynvim.api @neovim.plugin class SimplePlugin: def __init__(self, nvim: pynvim.api.Nvim): self.nvim = nvim @neovim.command("SimpleHelloWorld") def hello_world(self) -> None: self.nvim.command("echom 'SimplePlugin: Hello World!'") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_buffer.py0000644000175000017500000001547214533365244017145 0ustar00jamessanjamessanimport os import pytest from pynvim.api import Nvim, NvimError def test_repr(vim: Nvim) -> None: assert repr(vim.current.buffer) == "" def test_get_length(vim: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: assert vim.current.buffer.options['shiftwidth'] == 8 vim.current.buffer.options['shiftwidth'] = 4 assert vim.current.buffer.options['shiftwidth'] == 4 # global-local option global_define = vim.options['define'] vim.current.buffer.options['define'] = 'test' assert vim.current.buffer.options['define'] == 'test' # Doesn't change the global value assert vim.options['define'] == global_define with pytest.raises(KeyError) as excinfo: vim.current.buffer.options['doesnotexist'] assert excinfo.value.args == ("Invalid option name: 'doesnotexist'",) def test_number(vim: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: vim.command('new') buffer = vim.current.buffer assert buffer.valid vim.command('bw!') assert not buffer.valid def test_append(vim: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: vim.command('normal "=printf("%c", 0xFF)\np') assert vim.eval("char2nr(getline(1))") == 0xFF assert vim.current.buffer[:] == ['\udcff'] vim.current.line += 'x' assert vim.eval("getline(1)", decode=False) == '\udcffx' assert vim.current.buffer[:] == ['\udcffx'] def test_get_exceptions(vim: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: b = vim.current.buffer assert not (b != vim.current.buffer) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_client_rpc.py0000644000175000017500000000506714533365244020015 0ustar00jamessanjamessan# -*- coding: utf-8 -*- import time from typing import List from pynvim.api import Nvim def test_call_and_reply(vim: Nvim) -> None: cid = vim.channel_id def setup_cb() -> None: 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: str, args: List[int]) -> List[int]: 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: Nvim) -> None: cid = vim.channel_id def setup_cb() -> None: 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: str, args: List[int]) -> List[int]: 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: Nvim) -> None: def request_cb(name: str, args: List[int]) -> None: 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: Nvim) -> None: cid = vim.channel_id def setup_cb() -> None: 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: str, args: List[int]) -> int: 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_concurrency.py0000644000175000017500000000146214533365244020220 0ustar00jamessanjamessanfrom threading import Timer from typing import List from pynvim.api import Nvim def test_interrupt_from_another_thread(vim: Nvim) -> None: 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: Nvim) -> None: # an exception in a threadsafe_call shouldn't crash the entire host msgs: List[str] = [] vim.async_call( lambda: [ vim.eval("3"), undefined_variable # type: ignore[name-defined] # 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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_decorators.py0000644000175000017500000000176214533365244020036 0ustar00jamessanjamessan# type: ignore from pynvim.plugin.decorators import command def test_command_count() -> None: def function() -> None: """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 precedence 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_events.py0000644000175000017500000000352514533365244017174 0ustar00jamessanjamessan# -*- coding: utf-8 -*- from pynvim.api import Nvim def test_receiving_events(vim: Nvim) -> None: 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: Nvim) -> None: # 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: Nvim) -> None: # 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: Nvim) -> None: 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] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_host.py0000644000175000017500000000433014533365244016640 0ustar00jamessanjamessan# type: ignore # pylint: disable=protected-access import os from typing import Sequence from pynvim.plugin.host import Host, host_method_spec from pynvim.plugin.script_host import ScriptHost __PATH__ = os.path.abspath(os.path.dirname(__file__)) def test_host_imports(vim): h = ScriptHost(vim) try: assert h.module.__dict__['vim'] assert h.module.__dict__['vim'] == h.legacy_vim assert h.module.__dict__['sys'] finally: h.teardown() def test_host_import_rplugin_modules(vim): # Test whether a Host can load and import rplugins (#461). # See also $VIMRUNTIME/autoload/provider/pythonx.vim. h = Host(vim) plugins: Sequence[str] = [ # plugin paths like real rplugins os.path.join(__PATH__, "./fixtures/simple_plugin/rplugin/python3/simple_nvim.py"), os.path.join(__PATH__, "./fixtures/module_plugin/rplugin/python3/mymodule/"), os.path.join(__PATH__, "./fixtures/module_plugin/rplugin/python3/mymodule"), # duplicate ] h._load(plugins) assert len(h._loaded) == 2 # pylint: disable-next=unbalanced-tuple-unpacking simple_nvim, mymodule = list(h._loaded.values()) assert simple_nvim['module'].__name__ == 'simple_nvim' assert mymodule['module'].__name__ == 'mymodule' 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') def test_legacy_vim_eval(vim): h = ScriptHost(vim) try: assert h.legacy_vim.eval('1') == '1' assert h.legacy_vim.eval('v:null') is None assert h.legacy_vim.eval('v:true') is True assert h.legacy_vim.eval('v:false') is False finally: h.teardown() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_logging.py0000644000175000017500000000224014533365244017307 0ustar00jamessanjamessanimport os import sys from typing import Any def test_setup_logging(monkeypatch: Any, tmpdir: str, caplog: Any) -> None: from pynvim import setup_logging major_version = sys.version_info[0] setup_logging('name1') assert caplog.messages == [] def get_expected_logfile(prefix: str, name: str) -> str: 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" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_tabpage.py0000644000175000017500000000277214533365244017276 0ustar00jamessanjamessanimport pytest from pynvim.api import Nvim def test_windows(vim: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: vim.command('tabnew') tabpage = vim.tabpages[1] assert tabpage.valid vim.command('tabclose') assert not tabpage.valid def test_number(vim: Nvim) -> None: 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: Nvim) -> None: assert repr(vim.current.tabpage) == "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_version.py0000644000175000017500000000026314533365244017351 0ustar00jamessanjamessanimport pynvim def test_version() -> None: assert pynvim.__version__ assert isinstance(pynvim.__version__, str) print(f"pynvim.__version__ = '{pynvim.__version__}'") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_vim.py0000644000175000017500000002142014533365244016455 0ustar00jamessanjamessanimport os import sys import tempfile import textwrap from pathlib import Path import pytest from pynvim.api import Nvim, NvimError def source(vim: Nvim, code: str) -> None: 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: Nvim) -> None: assert 'remote' == vim.api.get_chan_info(vim.channel_id)['client']['type'] def test_command(vim: Nvim) -> None: 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' try: os.unlink(fname) except OSError: pass # on windows, this can be flaky; ignore it def test_command_output(vim: Nvim) -> None: assert vim.command_output('echo "test"') == 'test' # can capture multi-line outputs vim.command("let g:multiline_string = join(['foo', 'bar'], nr2char(10))") assert vim.command_output('echo g:multiline_string') == "foo\nbar" def test_command_error(vim: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: vim.api.command('let g:var = 3') assert vim.api.eval('g:var') == 3 def test_strwidth(vim: Nvim) -> None: assert vim.strwidth('abc') == 3 # 6 + (neovim) # 19 * 2 (each japanese character occupies two cells) assert vim.strwidth('neovimのデザインかなりまともなのになってる。') == 44 def test_chdir(vim: Nvim) -> None: 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: Nvim) -> None: assert vim.current.line == '' vim.current.line = 'abc' assert vim.current.line == 'abc' def test_current_line_delete(vim: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: assert vim.options['background'] == 'dark' vim.options['background'] = 'light' assert vim.options['background'] == 'light' def test_local_options(vim: Nvim) -> None: assert vim.windows[0].options['foldmethod'] == 'manual' vim.windows[0].options['foldmethod'] = 'syntax' assert vim.windows[0].options['foldmethod'] == 'syntax' def test_buffers(vim: Nvim) -> None: 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 # type: ignore[operator] # Iteration assert buffers == list(vim.buffers) def test_windows(vim: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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_python3(vim: Nvim) -> None: """Tests whether python3 host can load.""" python3_prog = vim.command_output('echom provider#python3#Prog()') python3_err = vim.command_output('echom provider#python3#Error()') assert python3_prog != "", python3_err assert python3_prog == sys.executable assert sys.executable == vim.command_output( 'python3 import sys; print(sys.executable)') assert 1 == vim.eval('has("python3")') def test_python3_ex_eval(vim: Nvim) -> None: assert '42' == vim.command_output('python3 =42') assert '42' == vim.command_output('python3 = 42 ') assert '42' == vim.command_output('py3= 42 ') assert '42' == vim.command_output('py=42') # On syntax error or evaluation error, stacktrace information is printed # Note: the pynvim API command_output() throws an exception on error # because the Ex command :python will throw (wrapped with provider#python3#Call) with pytest.raises(NvimError) as excinfo: vim.command('py3= 1/0') assert textwrap.dedent('''\ Traceback (most recent call last): File "", line 1, in ZeroDivisionError: division by zero ''').strip() in excinfo.value.args[0] vim.command('python3 def raise_error(): raise RuntimeError("oops")') with pytest.raises(NvimError) as excinfo: vim.command_output('python3 =print("nooo", raise_error())') assert textwrap.dedent('''\ Traceback (most recent call last): File "", line 1, in File "", line 1, in raise_error RuntimeError: oops ''').strip() in excinfo.value.args[0] assert 'nooo' not in vim.command_output(':messages') def test_python_cwd(vim: Nvim, tmp_path: Path) -> None: vim.command('python3 import os') cwd_before = vim.command_output('python3 print(os.getcwd())') # handle DirChanged #296 vim.command('cd {}'.format(str(tmp_path))) cwd_vim = vim.command_output('pwd') cwd_python = vim.command_output('python3 print(os.getcwd())') 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: Nvim) -> None: 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701702308.0 pynvim-0.5.0/test/test_window.py0000644000175000017500000000771614533365244017205 0ustar00jamessanjamessanimport pytest from pynvim.api import Nvim def test_buffer(vim: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: 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: Nvim) -> None: assert repr(vim.current.window) == ""