././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5453439
pynvim-0.5.0/ 0000755 0001750 0001750 00000000000 14533454457 013300 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1600170170.0
pynvim-0.5.0/LICENSE 0000644 0001750 0001750 00000026056 13730124272 014301 0 ustar 00jamessan jamessan Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 Thiago Arruda
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1600170170.0
pynvim-0.5.0/MANIFEST.in 0000644 0001750 0001750 00000000066 13730124272 015023 0 ustar 00jamessan jamessan include README.md LICENSE
recursive-include test *.py
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5453439
pynvim-0.5.0/PKG-INFO 0000644 0001750 0001750 00000000717 14533454457 014402 0 ustar 00jamessan jamessan Metadata-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"
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/README.md 0000644 0001750 0001750 00000011406 14533365244 014554 0 ustar 00jamessan jamessan Pynvim: Python client to [Neovim](https://github.com/neovim/neovim)
===================================================================
[](https://readthedocs.org/projects/pynvim/builds/)
[](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)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5333438
pynvim-0.5.0/neovim/ 0000755 0001750 0001750 00000000000 14533454457 014575 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1600170170.0
pynvim-0.5.0/neovim/__init__.py 0000644 0001750 0001750 00000000256 13730124272 016674 0 ustar 00jamessan jamessan """Python client for Nvim.
This is a transition package. New projects should instead import pynvim package.
"""
import pynvim
from pynvim import *
__all__ = pynvim.__all__
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5333438
pynvim-0.5.0/neovim/api/ 0000755 0001750 0001750 00000000000 14533454457 015346 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1600170170.0
pynvim-0.5.0/neovim/api/__init__.py 0000644 0001750 0001750 00000000261 13730124272 017441 0 ustar 00jamessan jamessan """Nvim API subpackage.
This is a transition package. New projects should instead import pynvim.api.
"""
from pynvim import api
from pynvim.api import *
__all__ = api.__all__
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5373437
pynvim-0.5.0/pynvim/ 0000755 0001750 0001750 00000000000 14533454457 014622 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/__init__.py 0000644 0001750 0001750 00000013756 14533454401 016734 0 ustar 00jamessan jamessan """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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730562.0
pynvim-0.5.0/pynvim/_version.py 0000644 0001750 0001750 00000000562 14533454402 017011 0 ustar 00jamessan jamessan """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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5373437
pynvim-0.5.0/pynvim/api/ 0000755 0001750 0001750 00000000000 14533454457 015373 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/api/__init__.py 0000644 0001750 0001750 00000000655 14533454401 017477 0 ustar 00jamessan jamessan """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')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/api/buffer.py 0000644 0001750 0001750 00000023576 14533454401 017220 0 ustar 00jamessan jamessan """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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/api/common.py 0000644 0001750 0001750 00000017723 14533454401 017234 0 ustar 00jamessan jamessan """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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/api/nvim.py 0000644 0001750 0001750 00000051667 14533454401 016722 0 ustar 00jamessan jamessan """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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/api/tabpage.py 0000644 0001750 0001750 00000002374 14533454401 017343 0 ustar 00jamessan jamessan """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')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/api/window.py 0000644 0001750 0001750 00000004367 14533454401 017253 0 ustar 00jamessan jamessan """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')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/compat.py 0000644 0001750 0001750 00000002701 14533454401 016444 0 ustar 00jamessan jamessan """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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5373437
pynvim-0.5.0/pynvim/msgpack_rpc/ 0000755 0001750 0001750 00000000000 14533454457 017113 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/msgpack_rpc/__init__.py 0000644 0001750 0001750 00000003176 14533454401 021220 0 ustar 00jamessan jamessan """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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/msgpack_rpc/async_session.py 0000644 0001750 0001750 00000011705 14533454401 022336 0 ustar 00jamessan jamessan """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)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5413437
pynvim-0.5.0/pynvim/msgpack_rpc/event_loop/ 0000755 0001750 0001750 00000000000 14533454457 021265 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/msgpack_rpc/event_loop/__init__.py 0000644 0001750 0001750 00000000462 14533454401 023365 0 ustar 00jamessan jamessan """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']
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/msgpack_rpc/event_loop/asyncio.py 0000644 0001750 0001750 00000014712 14533454401 023276 0 ustar 00jamessan jamessan """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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/msgpack_rpc/event_loop/base.py 0000644 0001750 0001750 00000021400 14533454401 022533 0 ustar 00jamessan jamessan """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()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/msgpack_rpc/event_loop/uv.py 0000644 0001750 0001750 00000007733 14533454401 022270 0 ustar 00jamessan jamessan """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()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/msgpack_rpc/msgpack_stream.py 0000644 0001750 0001750 00000004262 14533454401 022456 0 ustar 00jamessan jamessan """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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730561.0
pynvim-0.5.0/pynvim/msgpack_rpc/session.py 0000644 0001750 0001750 00000025137 14533454401 021145 0 ustar 00jamessan jamessan """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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5413437
pynvim-0.5.0/pynvim/plugin/ 0000755 0001750 0001750 00000000000 14533454457 016120 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730562.0
pynvim-0.5.0/pynvim/plugin/__init__.py 0000644 0001750 0001750 00000000604 14533454402 020217 0 ustar 00jamessan jamessan """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')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730562.0
pynvim-0.5.0/pynvim/plugin/decorators.py 0000644 0001750 0001750 00000014121 14533454402 020624 0 ustar 00jamessan jamessan """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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730562.0
pynvim-0.5.0/pynvim/plugin/host.py 0000644 0001750 0001750 00000026573 14533454402 017452 0 ustar 00jamessan jamessan # 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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730562.0
pynvim-0.5.0/pynvim/plugin/script_host.py 0000644 0001750 0001750 00000023202 14533454402 021020 0 ustar 00jamessan jamessan # 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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730562.0
pynvim-0.5.0/pynvim/util.py 0000644 0001750 0001750 00000001635 14533454402 016144 0 ustar 00jamessan jamessan """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)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5413437
pynvim-0.5.0/pynvim.egg-info/ 0000755 0001750 0001750 00000000000 14533454457 016314 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730606.0
pynvim-0.5.0/pynvim.egg-info/PKG-INFO 0000644 0001750 0001750 00000000717 14533454456 017415 0 ustar 00jamessan jamessan Metadata-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"
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730606.0
pynvim-0.5.0/pynvim.egg-info/SOURCES.txt 0000644 0001750 0001750 00000002423 14533454456 020200 0 ustar 00jamessan jamessan LICENSE
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 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730606.0
pynvim-0.5.0/pynvim.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 14533454456 022361 0 ustar 00jamessan jamessan
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730606.0
pynvim-0.5.0/pynvim.egg-info/requires.txt 0000644 0001750 0001750 00000000100 14533454456 020702 0 ustar 00jamessan jamessan msgpack>=0.5.0
greenlet>=3.0
[pyuv]
pyuv>=1.0.0
[test]
pytest
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701730606.0
pynvim-0.5.0/pynvim.egg-info/top_level.txt 0000644 0001750 0001750 00000000016 14533454456 021042 0 ustar 00jamessan jamessan neovim
pynvim
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/pyproject.toml 0000644 0001750 0001750 00000000120 14533365244 016200 0 ustar 00jamessan jamessan [build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta" ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5453439
pynvim-0.5.0/setup.cfg 0000644 0001750 0001750 00000001154 14533454457 015122 0 ustar 00jamessan jamessan [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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/setup.py 0000644 0001750 0001750 00000003177 14533365244 015015 0 ustar 00jamessan jamessan """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}},
)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5413437
pynvim-0.5.0/test/ 0000755 0001750 0001750 00000000000 14533454457 014257 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/__init__.py 0000644 0001750 0001750 00000000000 14533365244 016351 0 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/conftest.py 0000644 0001750 0001750 00000002615 14533365244 016455 0 ustar 00jamessan jamessan """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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5333438
pynvim-0.5.0/test/fixtures/ 0000755 0001750 0001750 00000000000 14533454457 016130 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5333438
pynvim-0.5.0/test/fixtures/module_plugin/ 0000755 0001750 0001750 00000000000 14533454457 020773 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5333438
pynvim-0.5.0/test/fixtures/module_plugin/rplugin/ 0000755 0001750 0001750 00000000000 14533454457 022453 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5333438
pynvim-0.5.0/test/fixtures/module_plugin/rplugin/python3/ 0000755 0001750 0001750 00000000000 14533454457 024057 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5413437
pynvim-0.5.0/test/fixtures/module_plugin/rplugin/python3/mymodule/ 0000755 0001750 0001750 00000000000 14533454457 025712 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/fixtures/module_plugin/rplugin/python3/mymodule/__init__.py 0000644 0001750 0001750 00000000501 14533365244 030012 0 ustar 00jamessan jamessan """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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/fixtures/module_plugin/rplugin/python3/mymodule/plugin.py 0000644 0001750 0001750 00000000475 14533365244 027563 0 ustar 00jamessan jamessan """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!'")
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5333438
pynvim-0.5.0/test/fixtures/simple_plugin/ 0000755 0001750 0001750 00000000000 14533454457 020777 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5333438
pynvim-0.5.0/test/fixtures/simple_plugin/rplugin/ 0000755 0001750 0001750 00000000000 14533454457 022457 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1701730606.5413437
pynvim-0.5.0/test/fixtures/simple_plugin/rplugin/python3/ 0000755 0001750 0001750 00000000000 14533454457 024063 5 ustar 00jamessan jamessan ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/fixtures/simple_plugin/rplugin/python3/simple_nvim.py 0000644 0001750 0001750 00000000432 14533365244 026751 0 ustar 00jamessan jamessan import 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!'")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_buffer.py 0000644 0001750 0001750 00000015472 14533365244 017145 0 ustar 00jamessan jamessan import 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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_client_rpc.py 0000644 0001750 0001750 00000005067 14533365244 020015 0 ustar 00jamessan jamessan # -*- 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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_concurrency.py 0000644 0001750 0001750 00000001462 14533365244 020220 0 ustar 00jamessan jamessan from 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')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_decorators.py 0000644 0001750 0001750 00000001762 14533365244 020036 0 ustar 00jamessan jamessan # 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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_events.py 0000644 0001750 0001750 00000003525 14533365244 017174 0 ustar 00jamessan jamessan # -*- 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]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_host.py 0000644 0001750 0001750 00000004330 14533365244 016640 0 ustar 00jamessan jamessan # 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()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_logging.py 0000644 0001750 0001750 00000002240 14533365244 017307 0 ustar 00jamessan jamessan import 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"
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_tabpage.py 0000644 0001750 0001750 00000002772 14533365244 017276 0 ustar 00jamessan jamessan import 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) == ""
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_version.py 0000644 0001750 0001750 00000000263 14533365244 017351 0 ustar 00jamessan jamessan import pynvim
def test_version() -> None:
assert pynvim.__version__
assert isinstance(pynvim.__version__, str)
print(f"pynvim.__version__ = '{pynvim.__version__}'")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_vim.py 0000644 0001750 0001750 00000021420 14533365244 016455 0 ustar 00jamessan jamessan import 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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1701702308.0
pynvim-0.5.0/test/test_window.py 0000644 0001750 0001750 00000007716 14533365244 017205 0 ustar 00jamessan jamessan import 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) == ""