New upstream release.
Debian Janitor
2 years ago
0 | [run] | |
1 | source = . | |
2 | branch = true | |
3 | parallel = 1 | |
4 | ||
5 | [report] | |
6 | show_missing = true | |
7 | include = pynvim/*,test/* |
0 | *.egg-info/ | |
1 | __pycache__ | |
2 | neovim/ui/screen.c | |
3 | neovim/ui/screen.so | |
4 | build/ | |
5 | dist/ | |
6 | *.pyc | |
7 | .cache | |
8 | .eggs | |
9 | .tox | |
10 | ||
11 | # Sphinx documentation | |
12 | docs/_build/ |
0 | dist: xenial | |
1 | language: python | |
2 | env: | |
3 | global: | |
4 | - PYTEST_ADDOPTS="-vv --cov-append" | |
5 | matrix: | |
6 | - CI_TARGET=tests | |
7 | matrix: | |
8 | include: | |
9 | - python: pypy | |
10 | dist: trusty | |
11 | sudo: false | |
12 | - python: 3.6 | |
13 | env: CI_TARGET=checkqa TOXENV=checkqa,docs | |
14 | python: | |
15 | - 2.7 | |
16 | - 3.4 | |
17 | - 3.5 | |
18 | - 3.6 | |
19 | - 3.7 | |
20 | - 3.8 | |
21 | install: | |
22 | - if [ $CI_TARGET = tests ]; then | |
23 | eval "$(curl -Ss https://raw.githubusercontent.com/neovim/bot-ci/master/scripts/travis-setup.sh) nightly-x64"; | |
24 | pip install -q tox-travis; | |
25 | else | |
26 | pip install -q tox; | |
27 | fi | |
28 | script: | |
29 | - tox | |
30 | after_script: | |
31 | - if [ $CI_TARGET = tests ]; then | |
32 | set -x; | |
33 | pip install coverage; | |
34 | coverage combine; | |
35 | coverage report -m; | |
36 | coverage xml; | |
37 | bash <(curl --retry 5 --silent --fail https://codecov.io/bash) -f coverage.xml; | |
38 | set +x; | |
39 | fi |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: pynvim |
2 | Version: 0.4.2 | |
2 | Version: 0.4.3 | |
3 | 3 | Summary: Python client to neovim |
4 | 4 | Home-page: http://github.com/neovim/pynvim |
5 | 5 | Author: Thiago de Arruda |
6 | 6 | Author-email: tpadilha84@gmail.com |
7 | 7 | License: Apache |
8 | Download-URL: https://github.com/neovim/pynvim/archive/0.4.2.tar.gz | |
8 | Download-URL: https://github.com/neovim/pynvim/archive/0.4.3.tar.gz | |
9 | 9 | Description: UNKNOWN |
10 | 10 | Platform: UNKNOWN |
11 | 11 | Provides-Extra: pyuv |
0 | python-pynvim (0.4.2-2) UNRELEASED; urgency=medium | |
0 | python-pynvim (0.4.3-1) UNRELEASED; urgency=medium | |
1 | 1 | |
2 | 2 | * Bump debhelper from old 12 to 13. |
3 | 3 | * Remove obsolete field Name from debian/upstream/metadata (already present in |
4 | 4 | machine-readable debian/copyright). |
5 | 5 | * Update standards version to 4.5.1, no changes needed. |
6 | ||
7 | -- Debian Janitor <janitor@jelmer.uk> Mon, 23 Aug 2021 22:50:02 -0000 | |
6 | * New upstream release. | |
7 | ||
8 | -- Debian Janitor <janitor@jelmer.uk> Mon, 04 Oct 2021 02:16:03 -0000 | |
8 | 9 | |
9 | 10 | python-pynvim (0.4.2-1) unstable; urgency=medium |
10 | 11 |
0 | # Minimal makefile for Sphinx documentation | |
1 | # | |
2 | ||
3 | # You can set these variables from the command line. | |
4 | SPHINXOPTS = | |
5 | SPHINXBUILD = sphinx-build | |
6 | SPHINXPROJ = Neovim | |
7 | SOURCEDIR = . | |
8 | BUILDDIR = _build | |
9 | ||
10 | # Put it first so that "make" without argument is like "make help". | |
11 | help: | |
12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) | |
13 | ||
14 | .PHONY: help Makefile | |
15 | ||
16 | # Catch-all target: route all unknown targets to Sphinx using the new | |
17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). | |
18 | %: Makefile | |
19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)⏎ |
0 | Nvim Class | |
1 | ========== | |
2 | ||
3 | An instance of this class is used by remote plugins. | |
4 | ||
5 | .. autoclass:: pynvim.api.Nvim | |
6 | :members: |
0 | #!/usr/bin/env python3 | |
1 | # -*- coding: utf-8 -*- | |
2 | # | |
3 | # Neovim documentation build configuration file, created by | |
4 | # sphinx-quickstart on Sat Feb 3 12:15:22 2018. | |
5 | # | |
6 | # This file is execfile()d with the current directory set to its | |
7 | # containing dir. | |
8 | # | |
9 | # Note that not all possible configuration values are present in this | |
10 | # autogenerated file. | |
11 | # | |
12 | # All configuration values have a default; values that are commented out | |
13 | # serve to show the default. | |
14 | ||
15 | # If extensions (or modules to document with autodoc) are in another directory, | |
16 | # add these directories to sys.path here. If the directory is relative to the | |
17 | # documentation root, use os.path.abspath to make it absolute, like shown here. | |
18 | # | |
19 | # import os | |
20 | # import sys | |
21 | # sys.path.insert(0, os.path.abspath('./')) | |
22 | import datetime | |
23 | ||
24 | ||
25 | # -- General configuration ------------------------------------------------ | |
26 | ||
27 | # If your documentation needs a minimal Sphinx version, state it here. | |
28 | # | |
29 | # needs_sphinx = '1.0' | |
30 | ||
31 | # Add any Sphinx extension module names here, as strings. They can be | |
32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom | |
33 | # ones. | |
34 | extensions = ['sphinx.ext.autodoc', | |
35 | 'sphinx.ext.viewcode'] | |
36 | ||
37 | # Add any paths that contain templates here, relative to this directory. | |
38 | templates_path = ['_templates'] | |
39 | ||
40 | # The suffix(es) of source filenames. | |
41 | # You can specify multiple suffix as a list of string: | |
42 | # | |
43 | # source_suffix = ['.rst', '.md'] | |
44 | source_suffix = '.rst' | |
45 | ||
46 | # The master toctree document. | |
47 | master_doc = 'index' | |
48 | ||
49 | # General information about the project. | |
50 | project = 'Neovim Python Client' | |
51 | copyright = '2014 - {year}, Neovim'.format( | |
52 | year=datetime.datetime.now().year | |
53 | ) | |
54 | author = 'Neovim' | |
55 | ||
56 | # The version info for the project you're documenting, acts as replacement for | |
57 | # |version| and |release|, also used in various other places throughout the | |
58 | # built documents. | |
59 | # | |
60 | # The short X.Y version. | |
61 | version = '' | |
62 | # The full version, including alpha/beta/rc tags. | |
63 | release = '' | |
64 | ||
65 | # The language for content autogenerated by Sphinx. Refer to documentation | |
66 | # for a list of supported languages. | |
67 | # | |
68 | # This is also used if you do content translation via gettext catalogs. | |
69 | # Usually you set "language" from the command line for these cases. | |
70 | language = None | |
71 | ||
72 | # List of patterns, relative to source directory, that match files and | |
73 | # directories to ignore when looking for source files. | |
74 | # This patterns also effect to html_static_path and html_extra_path | |
75 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] | |
76 | ||
77 | # The name of the Pygments (syntax highlighting) style to use. | |
78 | pygments_style = 'sphinx' | |
79 | ||
80 | # If true, `todo` and `todoList` produce output, else they produce nothing. | |
81 | todo_include_todos = False | |
82 | ||
83 | ||
84 | # -- Options for HTML output ---------------------------------------------- | |
85 | ||
86 | # The theme to use for HTML and HTML Help pages. See the documentation for | |
87 | # a list of builtin themes. | |
88 | # | |
89 | html_theme = 'sphinx_rtd_theme' | |
90 | ||
91 | # Theme options are theme-specific and customize the look and feel of a theme | |
92 | # further. For a list of options available for each theme, see the | |
93 | # documentation. | |
94 | # | |
95 | # html_theme_options = {} | |
96 | ||
97 | # Add any paths that contain custom static files (such as style sheets) here, | |
98 | # relative to this directory. They are copied after the builtin static files, | |
99 | # so a file named "default.css" will overwrite the builtin "default.css". | |
100 | html_static_path = [] | |
101 | ||
102 | ||
103 | # -- Options for HTMLHelp output ------------------------------------------ | |
104 | ||
105 | # Output file base name for HTML help builder. | |
106 | htmlhelp_basename = 'Neovimdoc' | |
107 | ||
108 | ||
109 | # -- Options for LaTeX output --------------------------------------------- | |
110 | ||
111 | latex_elements = { | |
112 | # The paper size ('letterpaper' or 'a4paper'). | |
113 | # | |
114 | # 'papersize': 'letterpaper', | |
115 | ||
116 | # The font size ('10pt', '11pt' or '12pt'). | |
117 | # | |
118 | # 'pointsize': '10pt', | |
119 | ||
120 | # Additional stuff for the LaTeX preamble. | |
121 | # | |
122 | # 'preamble': '', | |
123 | ||
124 | # Latex figure (float) alignment | |
125 | # | |
126 | # 'figure_align': 'htbp', | |
127 | } | |
128 | ||
129 | # Grouping the document tree into LaTeX files. List of tuples | |
130 | # (source start file, target name, title, | |
131 | # author, documentclass [howto, manual, or own class]). | |
132 | latex_documents = [ | |
133 | (master_doc, 'Neovim.tex', 'Neovim Documentation', | |
134 | 'Neovim', 'manual'), | |
135 | ] | |
136 | ||
137 | ||
138 | # -- Options for manual page output --------------------------------------- | |
139 | ||
140 | # One entry per manual page. List of tuples | |
141 | # (source start file, name, description, authors, manual section). | |
142 | man_pages = [ | |
143 | (master_doc, 'neovim', 'Neovim Documentation', | |
144 | [author], 1) | |
145 | ] | |
146 | ||
147 | ||
148 | # -- Options for Texinfo output ------------------------------------------- | |
149 | ||
150 | # Grouping the document tree into Texinfo files. List of tuples | |
151 | # (source start file, target name, title, author, | |
152 | # dir menu entry, description, category) | |
153 | texinfo_documents = [ | |
154 | (master_doc, 'Neovim', 'Neovim Documentation', | |
155 | author, 'Neovim', 'One line description of project.', | |
156 | 'Miscellaneous'), | |
157 | ] | |
158 | ||
159 | ||
160 |
0 | Development | |
1 | =========== | |
2 | ||
3 | If you change the code, you need to run:: | |
4 | ||
5 | pip2 install . | |
6 | pip3 install . | |
7 | ||
8 | for the changes to have effect. | |
9 | Alternatively you could execute Neovim with the ``$PYTHONPATH`` environment variable:: | |
10 | ||
11 | PYTHONPATH=/path/to/pynvim nvim | |
12 | ||
13 | But note this is not completely reliable, | |
14 | as installed packages can appear before ``$PYTHONPATH`` in the python search path. | |
15 | ||
16 | You need to rerun this command if you have changed the code, | |
17 | in order for Neovim to use it for the plugin host. | |
18 | ||
19 | To run the tests execute:: | |
20 | ||
21 | python -m pytest | |
22 | ||
23 | This will run the tests in an embedded instance of Neovim, with the current | |
24 | directory added to ``sys.path``. | |
25 | ||
26 | If you want to test a different version than ``nvim`` in ``$PATH`` use:: | |
27 | ||
28 | NVIM_CHILD_ARGV='["/path/to/nvim", "-u", "NONE", "--embed", "--headless"]' pytest | |
29 | ||
30 | Alternatively, if you want to see the state of nvim, you could use:: | |
31 | ||
32 | export NVIM_LISTEN_ADDRESS=/tmp/nvimtest | |
33 | xterm -e "nvim -u NONE"& | |
34 | python -m pytest | |
35 | ||
36 | But note you need to restart Neovim every time you run the tests! | |
37 | Substitute your favorite terminal emulator for ``xterm``. | |
38 | ||
39 | Troubleshooting | |
40 | --------------- | |
41 | ||
42 | You can run the plugin host in Neovim with logging enabled to debug errors:: | |
43 | ||
44 | NVIM_PYTHON_LOG_FILE=logfile NVIM_PYTHON_LOG_LEVEL=DEBUG nvim | |
45 | ||
46 | As more than one Python host process might be started, | |
47 | the log filenames take the pattern ``logfile_pyX_KIND`` | |
48 | where ``X`` is the major python version (2 or 3) | |
49 | and ``KIND`` is either "rplugin" or "script" (for the ``:python[3]`` script interface). | |
50 | ||
51 | If the host cannot start at all, | |
52 | the error could be found in ``~/.nvimlog`` if ``nvim`` was compiled with logging. | |
53 | ||
54 | Usage through the Python REPL | |
55 | ----------------------------- | |
56 | ||
57 | A number of different transports are supported, | |
58 | but the simplest way to get started is with the python REPL. | |
59 | First, start Neovim with a known address (or use the ``$NVIM_LISTEN_ADDRESS`` of a running instance):: | |
60 | ||
61 | NVIM_LISTEN_ADDRESS=/tmp/nvim nvim | |
62 | ||
63 | In another terminal, | |
64 | connect a python REPL to Neovim (note that the API is similar to the one exposed by the `python-vim bridge`_): | |
65 | ||
66 | .. code-block:: python | |
67 | ||
68 | >>> from pynvim import attach | |
69 | # Create a python API session attached to unix domain socket created above: | |
70 | >>> nvim = attach('socket', path='/tmp/nvim') | |
71 | # Now do some work. | |
72 | >>> buffer = nvim.current.buffer # Get the current buffer | |
73 | >>> buffer[0] = 'replace first line' | |
74 | >>> buffer[:] = ['replace whole buffer'] | |
75 | >>> nvim.command('vsplit') | |
76 | >>> nvim.windows[1].width = 10 | |
77 | >>> nvim.vars['global_var'] = [1, 2, 3] | |
78 | >>> nvim.eval('g:global_var') | |
79 | [1, 2, 3] | |
80 | ||
81 | .. _`python-vim bridge`: http://vimdoc.sourceforge.net/htmldoc/if_pyth.html#python-vim | |
82 | ||
83 | You can embed Neovim into your python application instead of binding to a running neovim instance: | |
84 | ||
85 | .. code-block:: python | |
86 | ||
87 | >>> from pynvim import attach | |
88 | >>> nvim = attach('child', argv=["/bin/env", "nvim", "--embed", "--headless"]) | |
89 | ||
90 | The tests can be consulted for more examples. |
0 | Neovim Python Client | |
1 | ==================== | |
2 | ||
3 | Implements support for Python plugins in `Neovim`_. | |
4 | Also works as a library for connecting to and scripting Neovim processes through its msgpack-rpc API. | |
5 | ||
6 | .. _`Neovim`: http://neovim.io/ | |
7 | ||
8 | .. toctree:: | |
9 | :caption: Getting started | |
10 | :maxdepth: 2 | |
11 | ||
12 | installation | |
13 | ||
14 | .. toctree:: | |
15 | :caption: Usage | |
16 | :maxdepth: 2 | |
17 | ||
18 | usage/python-plugin-api | |
19 | usage/remote-plugins | |
20 | ||
21 | .. toctree:: | |
22 | :caption: API documentation | |
23 | :maxdepth: 2 | |
24 | ||
25 | plugin-decorators | |
26 | api/nvim | |
27 | api/buffer | |
28 | api/window | |
29 | api/tabpage | |
30 | ||
31 | .. toctree:: | |
32 | :caption: Development | |
33 | :maxdepth: 2 | |
34 | ||
35 | development |
0 | Installation | |
1 | ============ | |
2 | ||
3 | The Neovim Python client supports Python 2.7, and 3.4 or later. | |
4 | ||
5 | Using pip | |
6 | --------- | |
7 | ||
8 | You can install the package without being root by adding the ``--user`` flag:: | |
9 | ||
10 | pip2 install --user pynvim | |
11 | pip3 install --user pynvim | |
12 | ||
13 | .. note:: | |
14 | ||
15 | If you only use one of python2 or python3, | |
16 | it is enough to install that version. | |
17 | ||
18 | If you follow Neovim HEAD, make sure to upgrade ``pynvim`` when you upgrade | |
19 | Neovim:: | |
20 | ||
21 | pip2 install --upgrade pynvim | |
22 | pip3 install --upgrade pynvim | |
23 | ||
24 | Install from source | |
25 | ------------------- | |
26 | ||
27 | Clone the repository somewhere on your disk and enter to the repository:: | |
28 | ||
29 | git clone https://github.com/neovim/pynvim.git | |
30 | cd pynvim | |
31 | ||
32 | Now you can install it on your system:: | |
33 | ||
34 | pip2 install . | |
35 | pip3 install . |
0 | @ECHO OFF | |
1 | ||
2 | pushd %~dp0 | |
3 | ||
4 | REM Command file for Sphinx documentation | |
5 | ||
6 | if "%SPHINXBUILD%" == "" ( | |
7 | set SPHINXBUILD=sphinx-build | |
8 | ) | |
9 | set SOURCEDIR=. | |
10 | set BUILDDIR=_build | |
11 | set SPHINXPROJ=Neovim | |
12 | ||
13 | if "%1" == "" goto help | |
14 | ||
15 | %SPHINXBUILD% >NUL 2>NUL | |
16 | if errorlevel 9009 ( | |
17 | echo. | |
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx | |
19 | echo.installed, then set the SPHINXBUILD environment variable to point | |
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you | |
21 | echo.may add the Sphinx directory to PATH. | |
22 | echo. | |
23 | echo.If you don't have Sphinx installed, grab it from | |
24 | echo.http://sphinx-doc.org/ | |
25 | exit /b 1 | |
26 | ) | |
27 | ||
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% | |
29 | goto end | |
30 | ||
31 | :help | |
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% | |
33 | ||
34 | :end | |
35 | popd |
0 | Plugin Decorators | |
1 | ================= | |
2 | ||
3 | .. module:: pynvim.plugin | |
4 | ||
5 | Plugin decorators. | |
6 | ||
7 | Plugin | |
8 | ------ | |
9 | ||
10 | .. autofunction:: plugin | |
11 | ||
12 | Command | |
13 | ------- | |
14 | ||
15 | .. autofunction:: command | |
16 | ||
17 | Autocmd | |
18 | ------- | |
19 | ||
20 | .. autofunction:: autocmd | |
21 | ||
22 | Function | |
23 | -------- | |
24 | ||
25 | .. autofunction:: function |
0 | Python Plugin API | |
1 | ================= | |
2 | ||
3 | Neovim has a new mechanism for defining plugins, | |
4 | as well as a number of extensions to the python API. | |
5 | The API extensions are accessible no matter if the traditional ``:python`` interface or the new mechanism is used, | |
6 | as discussed on :doc:`remote-plugins`. | |
7 | ||
8 | Nvim API methods: ``vim.api`` | |
9 | ----------------------------- | |
10 | ||
11 | Exposes Neovim API methods. | |
12 | For instance to call ``nvim_strwidth``: | |
13 | ||
14 | .. code-block:: python | |
15 | ||
16 | result = vim.api.strwidth("some text") | |
17 | ||
18 | Note the initial ``nvim_`` is not included. | |
19 | Also, object methods can be called directly on their object: | |
20 | ||
21 | .. code-block:: python | |
22 | ||
23 | buf = vim.current.buffer | |
24 | length = buf.api.line_count() | |
25 | ||
26 | calls ``nvim_buf_line_count``. | |
27 | Alternatively msgpack requests can be invoked directly: | |
28 | ||
29 | .. code-block:: python | |
30 | ||
31 | result = vim.request("nvim_strwith", "some text") | |
32 | length = vim.request("nvim_buf_line_count", buf) | |
33 | ||
34 | Both ``vim.api`` and ``vim.request`` can take an ``async_=True`` keyword argument | |
35 | to instead send a msgpack notification. Nvim will execute the API method the | |
36 | same way, but python will not wait for it to finish, so the return value is | |
37 | unavailable. | |
38 | ||
39 | Vimscript functions: ``vim.funcs`` | |
40 | ---------------------------------- | |
41 | ||
42 | Exposes vimscript functions (both builtin and global user defined functions) as a python namespace. | |
43 | For instance to set the value of a register: | |
44 | ||
45 | .. code-block:: python | |
46 | ||
47 | vim.funcs.setreg('0', ["some", "text"], 'l') | |
48 | ||
49 | These functions can also take the ``async_=True`` keyword argument, just like API | |
50 | methods. | |
51 | ||
52 | Lua integration | |
53 | --------------- | |
54 | ||
55 | Python plugins can define and invoke lua code in Nvim's in-process lua | |
56 | interpreter. This is especially useful in asynchronous contexts, where an async | |
57 | event handler can schedule a complex operation with many api calls to be | |
58 | executed by nvim without interleaved processing of user input or other event | |
59 | sources (unless requested). | |
60 | ||
61 | The recommended usage is the following pattern. First use ``vim.exec_lua(code)`` | |
62 | to define a module with lua functions: | |
63 | ||
64 | .. code-block:: python | |
65 | ||
66 | vim.exec_lua(""" | |
67 | local a = vim.api | |
68 | local function add(a,b) | |
69 | return a+b | |
70 | end | |
71 | ||
72 | local function buffer_ticks() | |
73 | local ticks = {} | |
74 | for _, buf in ipairs(a.nvim_list_bufs()) do | |
75 | ticks[#ticks+1] = a.nvim_buf_get_changedtick(buf) | |
76 | end | |
77 | return ticks | |
78 | end | |
79 | ||
80 | _testplugin = {add=add, buffer_ticks=buffer_ticks} | |
81 | """) | |
82 | ||
83 | Alternatively, place the code in ``/lua/testplugin.lua`` under your plugin repo | |
84 | root, and use ``vim.exec_lua("_testplugin = require('testplugin')")``. | |
85 | In both cases, replace ``testplugin`` with a unique string based on your plugin | |
86 | name. | |
87 | ||
88 | Then, the module can be acessed as ``vim.lua._testplugin``. | |
89 | ||
90 | .. code-block:: python | |
91 | ||
92 | mod = vim.lua._testplugin | |
93 | mod.add(2,3) # => 5 | |
94 | mod.buffer_ticks() # => list of ticks | |
95 | ||
96 | These functions can also take the ``async_=True`` keyword argument, just like API | |
97 | methods. | |
98 | ||
99 | It is also possible to pass arguments directly to a code block. Using | |
100 | ``vim.exec_lua(code, args...)``, the arguments will be available in lua as ``...``. | |
101 | ||
102 | Async calls | |
103 | ----------- | |
104 | ||
105 | The API is not thread-safe in general. | |
106 | However, ``vim.async_call`` allows a spawned thread to schedule code to be executed on the main thread. | |
107 | This method could also be called from ``:python`` or a synchronous request handler, | |
108 | to defer some execution that shouldn't block Neovim: | |
109 | ||
110 | .. code-block:: vim | |
111 | ||
112 | :python vim.async_call(myfunc, args...) | |
113 | ||
114 | Note that this code will still block the plugin host if it does long-running computations. | |
115 | Intensive computations should be done in a separate thread (or process), | |
116 | and ``vim.async_call`` can be used to send results back to Neovim. | |
117 | ||
118 | Some methods accept an ``async_`` keyword argument: ``vim.eval``, | |
119 | ``vim.command``, ``vim.request`` as well as the ``vim.funcs``, ``vim.api` and | |
120 | ``vim.lua``` wrappers. When ``async_=True`` is passed the client will not wait | |
121 | for Neovim to complete the request (which also means that the return value is | |
122 | unavailable). |
0 | .. _remote-plugins: | |
1 | ||
2 | Remote (new-style) plugins | |
3 | ========================== | |
4 | ||
5 | Neovim allows Python 3 plugins to be defined by placing python files or packages in ``rplugin/python3/`` (in a ``runtimepath`` folder). | |
6 | Python 2 rplugins are also supported and placed in ``rplugin/python/``, | |
7 | but are considered deprecated. | |
8 | Further added library features will only be available on Python 3. | |
9 | Rplugins follow the structure of this example: | |
10 | ||
11 | .. code-block:: python | |
12 | ||
13 | import pynvim | |
14 | ||
15 | @pynvim.plugin | |
16 | class TestPlugin(object): | |
17 | ||
18 | def __init__(self, nvim): | |
19 | self.nvim = nvim | |
20 | ||
21 | @pynvim.function('TestFunction', sync=True) | |
22 | def testfunction(self, args): | |
23 | return 3 | |
24 | ||
25 | @pynvim.command('TestCommand', nargs='*', range='') | |
26 | def testcommand(self, args, range): | |
27 | self.nvim.current.line = ('Command with args: {}, range: {}' | |
28 | .format(args, range)) | |
29 | ||
30 | @pynvim.autocmd('BufEnter', pattern='*.py', eval='expand("<afile>")', sync=True) | |
31 | def on_bufenter(self, filename): | |
32 | self.nvim.out_write('testplugin is in ' + filename + '\n') | |
33 | ||
34 | If ``sync=True`` is supplied Neovim will wait for the handler to finish | |
35 | (this is required for function return values), | |
36 | but by default handlers are executed asynchronously. | |
37 | ||
38 | Normally async handlers (``sync=False``, the default) | |
39 | are blocked while a synchronous handler is running. | |
40 | This ensures that async handlers can call requests without Neovim confusing these requests with requests from a synchronous handler. | |
41 | To execute an asynchronous handler even when other handlers are running, | |
42 | add ``allow_nested=True`` to the decorator. | |
43 | This handler must then not make synchronous Neovim requests, | |
44 | but it can make asynchronous requests, i.e. passing ``async_=True``. | |
45 | ||
46 | .. note:: | |
47 | ||
48 | Plugin objects are constructed the first time any request of the class is | |
49 | invoked. Any error in ``__init__`` will be reported as an error from this | |
50 | first request. A well-behaved rplugin will not start executing until its | |
51 | functionality is requested by the user. Initialize the plugin when user | |
52 | invokes a command, or use an appropriate autocommand, e.g. FileType if it | |
53 | makes sense to automatically start the plugin for a given filetype. Plugins | |
54 | must not invoke API methods (or really do anything with non-trivial | |
55 | side-effects) in global module scope, as the module might be loaded as part | |
56 | of executing `UpdateRemotePlugins`. | |
57 | ||
58 | You need to run ``:UpdateRemotePlugins`` in Neovim for changes in the specifications to have effect. | |
59 | For details see ``:help remote-plugin`` in Neovim. | |
60 | ||
61 | For local plugin development, it's a good idea to use an isolated vimrc: | |
62 | ||
63 | .. code-block:: console | |
64 | ||
65 | cat vimrc | |
66 | let &runtimepath.=','.escape(expand('<sfile>:p:h'), '\,') | |
67 | ||
68 | That appends the current directory to the Nvim runtime path so Nvim can | |
69 | find your plugin. You can now invoke Neovim: | |
70 | ||
71 | .. code-block:: console | |
72 | ||
73 | nvim -u ./vimrc | |
74 | ||
75 | Then run ``:UpdateRemotePlugins`` and your plugin should be activated. | |
76 | ||
77 | In case you run into some issues, you can list your loaded plugins from inside | |
78 | Neovim by running ``:scriptnames`` like so.: | |
79 | ||
80 | .. code-block:: vim | |
81 | ||
82 | :scriptnames | |
83 | 1: ~/path/to/your/plugin-git-repo/vimrc | |
84 | 2: /usr/share/nvim/runtime/filetype.vim | |
85 | ... | |
86 | 25: /usr/share/nvim/runtime/plugin/zipPlugin.vim | |
87 | 26: ~/path/to/your/plugin-git-repo/plugin/lucid.vim | |
88 | ||
89 | You can also inspect the ``&runtimepath`` like this: | |
90 | ||
91 | .. code-block:: vim | |
92 | ||
93 | :set runtimepath | |
94 | runtimepath=~/.config/nvim,/etc/xdg/nvim,~/.local/share/nvim/site,..., | |
95 | ,~/g/path/to/your/plugin-git-repo | |
96 | ||
97 | " Or alternatively | |
98 | :echo &rtp |
0 | from setuptools import setup | |
1 | ||
2 | setup(name='neovim', | |
3 | version='0.3.1', | |
4 | description='Transition packgage for pynvim', | |
5 | url='http://github.com/neovim/python-client', | |
6 | author='Thiago de Arruda', | |
7 | author_email='tpadilha84@gmail.com', | |
8 | license='Apache', | |
9 | packages=[], | |
10 | install_requires=['pynvim>=0.3.1'], | |
11 | zip_safe=False) |
79 | 79 | self._handlers.get(msg[0], self._on_invalid_message)(msg) |
80 | 80 | except Exception: |
81 | 81 | err_str = format_exc(5) |
82 | pass # replaces next logging statement | |
83 | #warn(err_str) | |
82 | warn(err_str) | |
84 | 83 | self._msgpack_stream.send([1, 0, err_str, None]) |
85 | 84 | |
86 | 85 | def _on_request(self, msg): |
88 | 87 | # - msg[1]: id |
89 | 88 | # - msg[2]: method name |
90 | 89 | # - msg[3]: arguments |
91 | pass # replaces next logging statement | |
92 | #debug('received request: %s, %s', msg[2], msg[3]) | |
90 | debug('received request: %s, %s', msg[2], msg[3]) | |
93 | 91 | self._request_cb(msg[2], msg[3], Response(self._msgpack_stream, |
94 | 92 | msg[1])) |
95 | 93 | |
98 | 96 | # - msg[1]: the id |
99 | 97 | # - msg[2]: error(if any) |
100 | 98 | # - msg[3]: result(if not errored) |
101 | pass # replaces next logging statement | |
102 | #debug('received response: %s, %s', msg[2], msg[3]) | |
99 | debug('received response: %s, %s', msg[2], msg[3]) | |
103 | 100 | self._pending_requests.pop(msg[1])(msg[2], msg[3]) |
104 | 101 | |
105 | 102 | def _on_notification(self, msg): |
106 | 103 | # notification/event |
107 | 104 | # - msg[1]: event name |
108 | 105 | # - msg[2]: arguments |
109 | pass # replaces next logging statement | |
110 | #debug('received notification: %s, %s', msg[1], msg[2]) | |
106 | debug('received notification: %s, %s', msg[1], msg[2]) | |
111 | 107 | self._notification_cb(msg[1], msg[2]) |
112 | 108 | |
113 | 109 | def _on_invalid_message(self, msg): |
114 | 110 | error = 'Received invalid message %s' % msg |
115 | pass # replaces next logging statement | |
116 | #warn(error) | |
111 | warn(error) | |
117 | 112 | self._msgpack_stream.send([1, 0, error, None]) |
118 | 113 | |
119 | 114 | |
139 | 134 | resp = [1, self._request_id, value, None] |
140 | 135 | else: |
141 | 136 | resp = [1, self._request_id, None, value] |
142 | pass # replaces next logging statement | |
143 | #debug('sending response to request %d: %s', self._request_id, resp) | |
137 | debug('sending response to request %d: %s', self._request_id, resp) | |
144 | 138 | self._msgpack_stream.send(resp) |
101 | 101 | pipe = sys.stdin |
102 | 102 | coroutine = self._loop.connect_read_pipe(self._fact, pipe) |
103 | 103 | self._loop.run_until_complete(coroutine) |
104 | pass # replaces next logging statement | |
105 | #debug("native stdin connection successful") | |
104 | debug("native stdin connection successful") | |
106 | 105 | |
107 | 106 | # Make sure subprocesses don't clobber stdout, |
108 | 107 | # send the output to stderr instead. |
115 | 114 | pipe = os.fdopen(rename_stdout, 'wb') |
116 | 115 | coroutine = self._loop.connect_write_pipe(self._fact, pipe) |
117 | 116 | self._loop.run_until_complete(coroutine) |
118 | pass # replaces next logging statement | |
119 | #debug("native stdout connection successful") | |
117 | debug("native stdout connection successful") | |
120 | 118 | |
121 | 119 | def _connect_child(self, argv): |
122 | 120 | if os.name != 'nt': |
94 | 94 | |
95 | 95 | def connect_tcp(self, address, port): |
96 | 96 | """Connect to tcp/ip `address`:`port`. Delegated to `_connect_tcp`.""" |
97 | pass # replaces next logging statement | |
98 | #info('Connecting to TCP address: %s:%d', address, port) | |
97 | info('Connecting to TCP address: %s:%d', address, port) | |
99 | 98 | self._connect_tcp(address, port) |
100 | 99 | |
101 | 100 | def connect_socket(self, path): |
102 | 101 | """Connect to socket at `path`. Delegated to `_connect_socket`.""" |
103 | pass # replaces next logging statement | |
104 | #info('Connecting to %s', path) | |
102 | info('Connecting to %s', path) | |
105 | 103 | self._connect_socket(path) |
106 | 104 | |
107 | 105 | def connect_stdio(self): |
108 | 106 | """Connect using stdin/stdout. Delegated to `_connect_stdio`.""" |
109 | pass # replaces next logging statement | |
110 | #info('Preparing stdin/stdout for streaming data') | |
107 | info('Preparing stdin/stdout for streaming data') | |
111 | 108 | self._connect_stdio() |
112 | 109 | |
113 | 110 | def connect_child(self, argv): |
114 | 111 | """Connect a new Nvim instance. Delegated to `_connect_child`.""" |
115 | pass # replaces next logging statement | |
116 | #info('Spawning a new nvim instance') | |
112 | info('Spawning a new nvim instance') | |
117 | 113 | self._connect_child(argv) |
118 | 114 | |
119 | 115 | def send(self, data): |
120 | 116 | """Queue `data` for sending to Nvim.""" |
121 | pass # replaces next logging statement | |
122 | #debug("Sending '%s'", data) | |
117 | debug("Sending '%s'", data) | |
123 | 118 | self._send(data) |
124 | 119 | |
125 | 120 | def threadsafe_call(self, fn): |
144 | 139 | self._on_data = data_cb |
145 | 140 | if threading.current_thread() == main_thread: |
146 | 141 | self._setup_signals([signal.SIGINT, signal.SIGTERM]) |
147 | pass # replaces next logging statement | |
148 | #debug('Entering event loop') | |
142 | debug('Entering event loop') | |
149 | 143 | self._run() |
150 | pass # replaces next logging statement | |
151 | #debug('Exited event loop') | |
144 | debug('Exited event loop') | |
152 | 145 | if threading.current_thread() == main_thread: |
153 | 146 | self._teardown_signals() |
154 | 147 | signal.signal(signal.SIGINT, default_int_handler) |
157 | 150 | def stop(self): |
158 | 151 | """Stop the event loop.""" |
159 | 152 | self._stop() |
160 | pass # replaces next logging statement | |
161 | #debug('Stopped event loop') | |
153 | debug('Stopped event loop') | |
162 | 154 | |
163 | 155 | def close(self): |
164 | 156 | """Stop the event loop.""" |
165 | 157 | self._close() |
166 | pass # replaces next logging statement | |
167 | #debug('Closed event loop') | |
158 | debug('Closed event loop') | |
168 | 159 | |
169 | 160 | def _on_signal(self, signum): |
170 | 161 | msg = 'Received {}'.format(self._signames[signum]) |
171 | pass # replaces next logging statement | |
172 | #debug(msg) | |
162 | debug(msg) | |
173 | 163 | if signum == signal.SIGINT and self._transport_type == 'stdio': |
174 | 164 | # When the transport is stdio, we are probably running as a Nvim |
175 | 165 | # child process. In that case, we don't want to be killed by |
182 | 172 | self.stop() |
183 | 173 | |
184 | 174 | def _on_error(self, error): |
185 | pass # replaces next logging statement | |
186 | #debug(error) | |
175 | debug(error) | |
187 | 176 | self._error = OSError(error) |
188 | 177 | self.stop() |
189 | 178 |
29 | 29 | |
30 | 30 | def send(self, msg): |
31 | 31 | """Queue `msg` for sending to Nvim.""" |
32 | pass # replaces next logging statement | |
33 | #debug('sent %s', msg) | |
32 | debug('sent %s', msg) | |
34 | 33 | self.loop.send(self._packer.pack(msg)) |
35 | 34 | |
36 | 35 | def run(self, message_cb): |
55 | 54 | self._unpacker.feed(data) |
56 | 55 | while True: |
57 | 56 | try: |
58 | pass # replaces next logging statement | |
59 | #debug('waiting for message...') | |
57 | debug('waiting for message...') | |
60 | 58 | msg = next(self._unpacker) |
61 | pass # replaces next logging statement | |
62 | #debug('received message: %s', msg) | |
59 | debug('received message: %s', msg) | |
63 | 60 | self._message_cb(msg) |
64 | 61 | except StopIteration: |
65 | pass # replaces next logging statement | |
66 | #debug('unpacker needs more data...') | |
62 | debug('unpacker needs more data...') | |
67 | 63 | break |
37 | 37 | try: |
38 | 38 | fn(*args, **kwargs) |
39 | 39 | except Exception: |
40 | pass # replaces next logging statement | |
41 | #warn("error caught while excecuting async callback\n%s\n", | |
42 | #format_exc()) | |
40 | warn("error caught while excecuting async callback\n%s\n", | |
41 | format_exc()) | |
43 | 42 | |
44 | 43 | def greenlet_wrapper(): |
45 | 44 | gr = greenlet.greenlet(handler) |
98 | 97 | raise OSError('EOF') |
99 | 98 | err, rv = v |
100 | 99 | if err: |
101 | pass # replaces next logging statement | |
102 | #info("'Received error: %s", err) | |
100 | info("'Received error: %s", err) | |
103 | 101 | raise self.error_wrapper(err) |
104 | 102 | return rv |
105 | 103 | |
128 | 126 | gr.switch() |
129 | 127 | |
130 | 128 | if self._setup_exception: |
131 | pass # replaces next logging statement | |
132 | #error('Setup error: {}'.format(self._setup_exception)) | |
129 | error('Setup error: {}'.format(self._setup_exception)) | |
133 | 130 | raise self._setup_exception |
134 | 131 | |
135 | 132 | # Process all pending requests and notifications |
158 | 155 | parent = gr.parent |
159 | 156 | |
160 | 157 | def response_cb(err, rv): |
161 | pass # replaces next logging statement | |
162 | #debug('response is available for greenlet %s, switching back', gr) | |
158 | debug('response is available for greenlet %s, switching back', gr) | |
163 | 159 | gr.switch(err, rv) |
164 | 160 | |
165 | 161 | self._async_session.request(method, args, response_cb) |
166 | pass # replaces next logging statement | |
167 | #debug('yielding from greenlet %s to wait for response', gr) | |
162 | debug('yielding from greenlet %s to wait for response', gr) | |
168 | 163 | return parent.switch() |
169 | 164 | |
170 | 165 | def _blocking_request(self, method, args): |
197 | 192 | def handler(): |
198 | 193 | try: |
199 | 194 | rv = self._request_cb(name, args) |
200 | pass # replaces next logging statement | |
201 | #debug('greenlet %s finished executing, ' | |
202 | #+ 'sending %s as response', gr, rv) | |
195 | debug('greenlet %s finished executing, ' | |
196 | + 'sending %s as response', gr, rv) | |
203 | 197 | response.send(rv) |
204 | 198 | except ErrorResponse as err: |
205 | pass # replaces next logging statement | |
206 | #warn("error response from request '%s %s': %s", name, | |
207 | #args, format_exc()) | |
199 | warn("error response from request '%s %s': %s", name, | |
200 | args, format_exc()) | |
208 | 201 | response.send(err.args[0], error=True) |
209 | 202 | except Exception as err: |
210 | pass # replaces next logging statement | |
211 | #warn("error caught while processing request '%s %s': %s", name, | |
212 | #args, format_exc()) | |
203 | warn("error caught while processing request '%s %s': %s", name, | |
204 | args, format_exc()) | |
213 | 205 | response.send(repr(err) + "\n" + format_exc(5), error=True) |
214 | pass # replaces next logging statement | |
215 | #debug('greenlet %s is now dying...', gr) | |
206 | debug('greenlet %s is now dying...', gr) | |
216 | 207 | |
217 | 208 | # Create a new greenlet to handle the request |
218 | 209 | gr = greenlet.greenlet(handler) |
219 | pass # replaces next logging statement | |
220 | #debug('received rpc request, greenlet %s will handle it', gr) | |
210 | debug('received rpc request, greenlet %s will handle it', gr) | |
221 | 211 | gr.switch() |
222 | 212 | |
223 | 213 | def _on_notification(self, name, args): |
224 | 214 | def handler(): |
225 | 215 | try: |
226 | 216 | self._notification_cb(name, args) |
227 | pass # replaces next logging statement | |
228 | #debug('greenlet %s finished executing', gr) | |
217 | debug('greenlet %s finished executing', gr) | |
229 | 218 | except Exception: |
230 | pass # replaces next logging statement | |
231 | #warn("error caught while processing notification '%s %s': %s", | |
232 | #name, args, format_exc()) | |
233 | ||
234 | pass # replaces next logging statement | |
235 | #debug('greenlet %s is now dying...', gr) | |
219 | warn("error caught while processing notification '%s %s': %s", | |
220 | name, args, format_exc()) | |
221 | ||
222 | debug('greenlet %s is now dying...', gr) | |
236 | 223 | |
237 | 224 | gr = greenlet.greenlet(handler) |
238 | pass # replaces next logging statement | |
239 | #debug('received rpc notification, greenlet %s will handle it', gr) | |
225 | debug('received rpc notification, greenlet %s will handle it', gr) | |
240 | 226 | gr.switch() |
241 | 227 | |
242 | 228 |
116 | 116 | handler = self._request_handlers.get(name, None) |
117 | 117 | if not handler: |
118 | 118 | msg = self._missing_handler_error(name, 'request') |
119 | pass # replaces next logging statement | |
120 | #error(msg) | |
119 | error(msg) | |
121 | 120 | raise ErrorResponse(msg) |
122 | 121 | |
123 | pass # replaces next logging statement | |
124 | #debug('calling request handler for "%s", args: "%s"', name, args) | |
122 | debug('calling request handler for "%s", args: "%s"', name, args) | |
125 | 123 | rv = handler(*args) |
126 | pass # replaces next logging statement | |
127 | #debug("request handler for '%s %s' returns: %s", name, args, rv) | |
124 | debug("request handler for '%s %s' returns: %s", name, args, rv) | |
128 | 125 | return rv |
129 | 126 | |
130 | 127 | def _on_notification(self, name, args): |
134 | 131 | handler = self._notification_handlers.get(name, None) |
135 | 132 | if not handler: |
136 | 133 | msg = self._missing_handler_error(name, 'notification') |
137 | pass # replaces next logging statement | |
138 | #error(msg) | |
134 | error(msg) | |
139 | 135 | self._on_async_err(msg + "\n") |
140 | 136 | return |
141 | 137 | |
142 | pass # replaces next logging statement | |
143 | #debug('calling notification handler for "%s", args: "%s"', name, args) | |
138 | debug('calling notification handler for "%s", args: "%s"', name, args) | |
144 | 139 | handler(*args) |
145 | 140 | |
146 | 141 | def _missing_handler_error(self, name, kind): |
157 | 152 | for path in plugins: |
158 | 153 | err = None |
159 | 154 | if path in self._loaded: |
160 | pass # replaces next logging statement | |
161 | #error('{} is already loaded'.format(path)) | |
155 | error('{} is already loaded'.format(path)) | |
162 | 156 | continue |
163 | 157 | try: |
164 | 158 | if path == "script_host.py": |
172 | 166 | self._discover_classes(module, handlers, path) |
173 | 167 | self._discover_functions(module, handlers, path, False) |
174 | 168 | if not handlers: |
175 | pass # replaces next logging statement | |
176 | #error('{} exports no handlers'.format(path)) | |
169 | error('{} exports no handlers'.format(path)) | |
177 | 170 | continue |
178 | 171 | self._loaded[path] = {'handlers': handlers, 'module': module} |
179 | 172 | except Exception as e: |
180 | 173 | err = ('Encountered {} loading plugin at {}: {}\n{}' |
181 | 174 | .format(type(e).__name__, path, e, format_exc(5))) |
182 | pass # replaces next logging statement | |
183 | #error(err) | |
175 | error(err) | |
184 | 176 | self._load_errors[path] = err |
185 | 177 | |
186 | 178 | kind = ("script-host" if len(plugins) == 1 and has_script |
65 | 65 | forwarded to Nvim. |
66 | 66 | """ |
67 | 67 | self.nvim = nvim |
68 | pass # replaces next logging statement | |
69 | #info('install import hook/path') | |
68 | info('install import hook/path') | |
70 | 69 | self.hook = path_hook(nvim) |
71 | 70 | sys.path_hooks.append(self.hook) |
72 | 71 | nvim.VIM_SPECIAL_PATH = '_vim_path_' |
73 | 72 | sys.path.append(nvim.VIM_SPECIAL_PATH) |
74 | pass # replaces next logging statement | |
75 | #info('redirect sys.stdout and sys.stderr') | |
73 | info('redirect sys.stdout and sys.stderr') | |
76 | 74 | self.saved_stdout = sys.stdout |
77 | 75 | self.saved_stderr = sys.stderr |
78 | 76 | sys.stdout = RedirectStream(lambda data: nvim.out_write(data)) |
81 | 79 | def teardown(self): |
82 | 80 | """Restore state modified from the `setup` call.""" |
83 | 81 | nvim = self.nvim |
84 | pass # replaces next logging statement | |
85 | #info('uninstall import hook/path') | |
82 | info('uninstall import hook/path') | |
86 | 83 | sys.path.remove(nvim.VIM_SPECIAL_PATH) |
87 | 84 | sys.path_hooks.remove(self.hook) |
88 | pass # replaces next logging statement | |
89 | #info('restore sys.stdout and sys.stderr') | |
85 | info('restore sys.stdout and sys.stderr') | |
90 | 86 | sys.stdout = self.saved_stdout |
91 | 87 | sys.stderr = self.saved_stderr |
92 | 88 | |
103 | 99 | def python_execute_file(self, file_path, range_start, range_stop): |
104 | 100 | """Handle the `pyfile` ex command.""" |
105 | 101 | self._set_current_range(range_start, range_stop) |
106 | with open(file_path) as f: | |
102 | with open(file_path, 'rb') as f: | |
107 | 103 | script = compile(f.read(), file_path, 'exec') |
108 | 104 | try: |
109 | 105 | exec(script, self.module.__dict__) |
38 | 38 | return (name, VERSION.__dict__, type_, method_spec, attributes) |
39 | 39 | |
40 | 40 | |
41 | VERSION = Version(major=0, minor=4, patch=2, prerelease='') | |
41 | VERSION = Version(major=0, minor=4, patch=3, prerelease='') |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: pynvim |
2 | Version: 0.4.2 | |
2 | Version: 0.4.3 | |
3 | 3 | Summary: Python client to neovim |
4 | 4 | Home-page: http://github.com/neovim/pynvim |
5 | 5 | Author: Thiago de Arruda |
6 | 6 | Author-email: tpadilha84@gmail.com |
7 | 7 | License: Apache |
8 | Download-URL: https://github.com/neovim/pynvim/archive/0.4.2.tar.gz | |
8 | Download-URL: https://github.com/neovim/pynvim/archive/0.4.3.tar.gz | |
9 | 9 | Description: UNKNOWN |
10 | 10 | Platform: UNKNOWN |
11 | 11 | Provides-Extra: pyuv |
0 | .coveragerc | |
1 | .gitignore | |
2 | .readthedocs.yml | |
3 | .travis.yml | |
0 | 4 | LICENSE |
1 | 5 | MANIFEST.in |
2 | 6 | README.md |
7 | codecov.yml | |
3 | 8 | setup.cfg |
4 | 9 | setup.py |
10 | tox.ini | |
11 | docs/Makefile | |
12 | docs/conf.py | |
13 | docs/development.rst | |
14 | docs/index.rst | |
15 | docs/installation.rst | |
16 | docs/make.bat | |
17 | docs/plugin-decorators.rst | |
18 | docs/api/buffer.rst | |
19 | docs/api/nvim.rst | |
20 | docs/api/tabpage.rst | |
21 | docs/api/window.rst | |
22 | docs/usage/python-plugin-api.rst | |
23 | docs/usage/remote-plugins.rst | |
24 | dummy/setup.py | |
5 | 25 | neovim/__init__.py |
6 | 26 | neovim/api/__init__.py |
7 | 27 | pynvim/__init__.py |
31 | 51 | pynvim/plugin/decorators.py |
32 | 52 | pynvim/plugin/host.py |
33 | 53 | pynvim/plugin/script_host.py |
54 | scripts/disable_log_statements.sh | |
55 | scripts/enable_log_statements.sh | |
56 | scripts/logging_statement_modifier.py | |
57 | scripts/publish.sh | |
34 | 58 | test/conftest.py |
35 | 59 | test/test_buffer.py |
36 | 60 | test/test_client_rpc.py |
0 | #!/bin/sh -e | |
1 | ||
2 | cd pynvim | |
3 | find -name '*.py' | xargs -i{} ../scripts/logging_statement_modifier.py {} |
0 | #!/bin/sh -e | |
1 | ||
2 | cd pynvim | |
3 | find -name '*.py' | xargs -i{} ../scripts/logging_statement_modifier.py --restore {} |
0 | #!/usr/bin/env python | |
1 | ||
2 | """\ | |
3 | Logging Statement Modifier - replace logging calls with pass (or vice versa) | |
4 | Author: David Underhill <dgu@cs.stanford.edu> | |
5 | Version: 1.00 (06-Feb-2010) | |
6 | ||
7 | This script parses a Python file and comments out logging statements, replacing | |
8 | them with a pass statement (or vice versa). The purpose of commenting out these | |
9 | statements is to improve performance. Even if logging is disabled, arguments to | |
10 | logging method calls must still be evaluated, which can be expensive. | |
11 | ||
12 | This tool handles most common cases: | |
13 | * Log statements may span multiple lines. | |
14 | * Custom logging levels may be added (LEVELS, LEVEL_VALUES). | |
15 | * Integral logging levels & named logging levels (DEBUG, etc.) are recognized. | |
16 | * Logging statements log(), debug(), ..., critical() are all recognized. | |
17 | * Statements with unrecognized logging levels will be left as-is. | |
18 | * 'logging' is the assumed logging module name (LOGGING_MODULE_NAME). | |
19 | ||
20 | However, its ability to parse files is limited: | |
21 | * It only operates on logging statements in the form logging.log(<level>, ...) | |
22 | and logging.<level>(...). | |
23 | * The <level> must either be an integral constant or contain one of the names | |
24 | from the LEVELS constant below. | |
25 | * If a logging statement is made, it is assumed that no other statement is | |
26 | made on the same line as logging statement (except for statements made in | |
27 | between the open and close parenthesis of the logging call). For example, | |
28 | a semi-colon and then a second statement on the same line as a logging call | |
29 | will not be handled properly. | |
30 | * Logging methods must be called through SOME module, e.g., logging.log(), not | |
31 | just log(). | |
32 | * For simplicity, undoing the commenting process relies on a comment left by | |
33 | the program on the pass statements it adds when commenting out logging | |
34 | statements. (So don't change the comment it outputs by the pass statement). | |
35 | ||
36 | To run this command on all of the Python files in a particular folder and its | |
37 | sub-folders at once, try this (replace '/path/to' as appropriate): | |
38 | find . -name '*.py' | xargs -i{} /path/to/logging_statement_modifier.py {} | |
39 | """ | |
40 | ||
41 | import logging | |
42 | from optparse import OptionParser | |
43 | import re | |
44 | import sys | |
45 | ||
46 | # logging level names and values | |
47 | LEVELS = ['DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'CRITICAL'] | |
48 | LEVEL_VALUES = [logging.DEBUG, logging.INFO, logging.WARN, logging.WARNING, logging.ERROR, logging.CRITICAL] | |
49 | LEVELS_DICT = dict(zip(LEVELS, LEVEL_VALUES)) | |
50 | ||
51 | # names of methods in the logging module which perform logging | |
52 | LOGGING_METHODS_OF_INTEREST = ['log', 'debug', 'info', 'warn', 'warning', 'error', 'critical'] | |
53 | ||
54 | # name of the logging module | |
55 | LOGGING_MODULE_NAME = 'logging' | |
56 | ||
57 | # this matches logging.<method>([<first_arg>,] | |
58 | # STR_RE_LOGGING_CALL = r'%s.(\w+)[(](([^,\r\n]+),)?' % LOGGING_MODULE_NAME | |
59 | STR_RE_LOGGING_CALL = r'\b(' + '|'.join(LOGGING_METHODS_OF_INTEREST) + r')[(](([^,\r\n]+),)?' | |
60 | ||
61 | # contents of a pass line (not including prefixed whitespace) | |
62 | PASS_LINE_CONTENTS = 'pass # replaces next logging statement\n' | |
63 | ||
64 | # Match a logging call (must only be prefixed with whitespace). Capture groups | |
65 | # include the whitespace, the logging method called, and the first argument if | |
66 | # possible | |
67 | RE_LOGGING_START = re.compile(r'^(\s+)' + STR_RE_LOGGING_CALL) | |
68 | RE_LOGGING_START_IN_COMMENT = re.compile(r'^(\s+)#' + STR_RE_LOGGING_CALL) | |
69 | ||
70 | def main(argv=sys.argv[1:]): | |
71 | """Parses the command line comments.""" | |
72 | usage = 'usage: %prog [options] FILE\n\n' + __doc__ | |
73 | parser = OptionParser(usage) | |
74 | ||
75 | # options | |
76 | parser.add_option("-f", "--force", | |
77 | action='store_true', default=False, | |
78 | help="make changes even if they cannot undone before saving the new file") | |
79 | parser.add_option("-m", "--min_level", | |
80 | default='NONE', | |
81 | help="minimum level of logging statements to modify [default: no minimum]") | |
82 | parser.add_option("-M", "--max_level", | |
83 | default='NONE', | |
84 | help="maximum level of logging statements to modify [default: no maximum]") | |
85 | parser.add_option("-o", "--output-file", | |
86 | default=None, | |
87 | help="where to output the result [default: overwrite the input file]") | |
88 | parser.add_option("-r", "--restore", | |
89 | action='store_true', default=False, | |
90 | help="restore logging statements previously commented out and replaced with pass statements") | |
91 | parser.add_option("-v", "--verbose", | |
92 | action='store_true', default=False, | |
93 | help="print informational messages about changes made") | |
94 | ||
95 | (options, args) = parser.parse_args(argv) | |
96 | if len(args) != 1: | |
97 | parser.error("expected 1 argument but got %d arguments: %s" % (len(args), ' '.join(args))) | |
98 | input_fn = args[0] | |
99 | if not options.output_file: | |
100 | options.output_file = input_fn | |
101 | ||
102 | # validate min/max level | |
103 | LEVEL_CHOICES = LEVELS + ['NONE'] | |
104 | min_level_value = 0 if options.min_level == 'NONE' else get_level_value(options.min_level) | |
105 | if options.min_level is None: | |
106 | parser.error("min level must be an integer or one of these values: %s" % ', '.join(LEVEL_CHOICES)) | |
107 | max_level_value = 9000 if options.max_level == 'NONE' else get_level_value(options.max_level) | |
108 | if options.max_level is None: | |
109 | parser.error("max level must be an integer or one of these values: %s" % ', '.join(LEVEL_CHOICES)) | |
110 | ||
111 | if options.verbose: | |
112 | logging.getLogger().setLevel(logging.INFO) | |
113 | ||
114 | try: | |
115 | return modify_logging(input_fn, options.output_file, | |
116 | min_level_value, max_level_value, | |
117 | options.restore, options.force) | |
118 | except OSError as e: | |
119 | logging.error(str(e)) | |
120 | return -1 | |
121 | ||
122 | # matches two main groups: 1) leading whitespace and 2) all following text | |
123 | RE_LINE_SPLITTER_COMMENT = re.compile(r'^(\s*)((.|\n)*)$') | |
124 | def comment_lines(lines): | |
125 | """Comment out the given list of lines and return them. The hash mark will | |
126 | be inserted before the first non-whitespace character on each line.""" | |
127 | ret = [] | |
128 | for line in lines: | |
129 | ws_prefix, rest, ignore = RE_LINE_SPLITTER_COMMENT.match(line).groups() | |
130 | ret.append(ws_prefix + '#' + rest) | |
131 | return ''.join(ret) | |
132 | ||
133 | # matches two main groups: 1) leading whitespace and 2) all following text | |
134 | RE_LINE_SPLITTER_UNCOMMENT = re.compile(r'^(\s*)#((.|\n)*)$') | |
135 | def uncomment_lines(lines): | |
136 | """Uncomment the given list of lines and return them. The first hash mark | |
137 | following any amount of whitespace will be removed on each line.""" | |
138 | ret = [] | |
139 | for line in lines: | |
140 | ws_prefix, rest, ignore = RE_LINE_SPLITTER_UNCOMMENT.match(line).groups() | |
141 | ret.append(ws_prefix + rest) | |
142 | return ''.join(ret) | |
143 | ||
144 | def first_arg_to_level_name(arg): | |
145 | """Decide what level the argument specifies and return it. The argument | |
146 | must contain (case-insensitive) one of the values in LEVELS or be an integer | |
147 | constant. Otherwise None will be returned.""" | |
148 | try: | |
149 | return int(arg) | |
150 | except ValueError: | |
151 | arg = arg.upper() | |
152 | for level in LEVELS: | |
153 | if level in arg: | |
154 | return level | |
155 | return None | |
156 | ||
157 | def get_level_value(level): | |
158 | """Returns the logging value associated with a particular level name. The | |
159 | argument must be present in LEVELS_DICT or be an integer constant. | |
160 | Otherwise None will be returned.""" | |
161 | try: | |
162 | # integral constants also work: they are the level value | |
163 | return int(level) | |
164 | except ValueError: | |
165 | try: | |
166 | return LEVELS_DICT[level.upper()] | |
167 | except KeyError: | |
168 | logging.warning("level '%s' cannot be translated to a level value (not present in LEVELS_DICT)" % level) | |
169 | return None | |
170 | ||
171 | def get_logging_level(logging_stmt, commented_out=False): | |
172 | """Determines the level of logging in a given logging statement. The string | |
173 | representing this level is returned. False is returned if the method is | |
174 | not a logging statement and thus has no level. None is returned if a level | |
175 | should have been found but wasn't.""" | |
176 | regexp = RE_LOGGING_START_IN_COMMENT if commented_out else RE_LOGGING_START | |
177 | ret = regexp.match(logging_stmt) | |
178 | _, method_name, _, first_arg = ret.groups() | |
179 | if method_name not in LOGGING_METHODS_OF_INTEREST: | |
180 | logging.debug('skipping uninteresting logging call: %s' % method_name) | |
181 | return False | |
182 | ||
183 | if method_name != 'log': | |
184 | return method_name | |
185 | ||
186 | # if the method name did not specify the level, we must have a first_arg to extract the level from | |
187 | if not first_arg: | |
188 | logging.warning("logging.log statement found but we couldn't extract the first argument") | |
189 | return None | |
190 | ||
191 | # extract the level of logging from the first argument to the log() call | |
192 | level = first_arg_to_level_name(first_arg) | |
193 | if level is None: | |
194 | logging.warning("arg does not contain any known level '%s'\n" % first_arg) | |
195 | return None | |
196 | return level | |
197 | ||
198 | def level_is_between(level, min_level_value, max_level_value): | |
199 | """Returns True if level is between the specified min or max, inclusive.""" | |
200 | level_value = get_level_value(level) | |
201 | if level_value is None: | |
202 | # unknown level value | |
203 | return False | |
204 | return level_value >= min_level_value and level_value <= max_level_value | |
205 | ||
206 | def split_call(lines, open_paren_line=0): | |
207 | """Returns a 2-tuple where the first element is the list of lines from the | |
208 | first open paren in lines to the matching closed paren. The second element | |
209 | is all remaining lines in a list.""" | |
210 | num_open = 0 | |
211 | num_closed = 0 | |
212 | for i, line in enumerate(lines): | |
213 | c = line.count('(') | |
214 | num_open += c | |
215 | if not c and i==open_paren_line: | |
216 | raise Exception('Exception open parenthesis in line %d but there is not one there: %s' % (i, str(lines))) | |
217 | num_closed += line.count(')') | |
218 | ||
219 | if num_open == num_closed: | |
220 | return (lines[:i+1], lines[i+1:]) | |
221 | ||
222 | print(''.join(lines)) | |
223 | raise Exception('parenthesis are mismatched (%d open, %d closed found)' % (num_open, num_closed)) | |
224 | ||
225 | def modify_logging(input_fn, output_fn, min_level_value, max_level_value, restore, force): | |
226 | """Modifies logging statements in the specified file.""" | |
227 | # read in all the lines | |
228 | logging.info('reading in %s' % input_fn) | |
229 | fh = open(input_fn, 'r') | |
230 | lines = fh.readlines() | |
231 | fh.close() | |
232 | original_contents = ''.join(lines) | |
233 | ||
234 | if restore: | |
235 | forwards = restore_logging | |
236 | backwards = disable_logging | |
237 | else: | |
238 | forwards = disable_logging | |
239 | backwards = restore_logging | |
240 | ||
241 | # apply the requested action | |
242 | new_contents = forwards(lines, min_level_value, max_level_value) | |
243 | ||
244 | # quietly check to see if we can undo what we just did (if not, the text | |
245 | # contains something we cannot translate [bug or limitation with this code]) | |
246 | logging.disable(logging.CRITICAL) | |
247 | new_contents_undone = backwards(new_contents.splitlines(True), min_level_value, max_level_value) | |
248 | logging.disable(logging.DEBUG) | |
249 | if original_contents != new_contents_undone: | |
250 | base_str = 'We are unable to revert this action as expected' | |
251 | if force: | |
252 | logging.warning(base_str + " but -f was specified so we'll do it anyway.") | |
253 | else: | |
254 | logging.error(base_str + ', so we will not do it in the first place. Pass -f to override this and make the change anyway.') | |
255 | return -1 | |
256 | ||
257 | logging.info('writing the new contents to %s' % output_fn) | |
258 | fh = open(output_fn, 'w') | |
259 | fh.write(new_contents) | |
260 | fh.close() | |
261 | logging.info('done!') | |
262 | return 0 | |
263 | ||
264 | def check_level(logging_stmt, logging_stmt_is_commented_out, min_level_value, max_level_value): | |
265 | """Extracts the level of the logging statement and returns True if the | |
266 | level falls betwen min and max_level_value. If the level cannot be | |
267 | extracted, then a warning is logged.""" | |
268 | level = get_logging_level(logging_stmt, logging_stmt_is_commented_out) | |
269 | if level is None: | |
270 | logging.warning('skipping logging statement because the level could not be extracted: %s' % logging_stmt.strip()) | |
271 | return False | |
272 | elif level is False: | |
273 | return False | |
274 | elif level_is_between(level, min_level_value, max_level_value): | |
275 | return True | |
276 | else: | |
277 | logging.debug('keep this one as is (not in the specified level range): %s' % logging_stmt.strip()) | |
278 | return False | |
279 | ||
280 | def disable_logging(lines, min_level_value, max_level_value): | |
281 | """Disables logging statements in these lines whose logging level falls | |
282 | between the specified minimum and maximum levels.""" | |
283 | output = '' | |
284 | while lines: | |
285 | line = lines[0] | |
286 | ret = RE_LOGGING_START.match(line) | |
287 | if not ret: | |
288 | # no logging statement here, so just leave the line as-is and keep going | |
289 | output += line | |
290 | lines = lines[1:] | |
291 | else: | |
292 | # a logging call has started: find all the lines it includes and those it does not | |
293 | logging_lines, remaining_lines = split_call(lines) | |
294 | lines = remaining_lines | |
295 | logging_stmt = ''.join(logging_lines) | |
296 | ||
297 | # replace the logging statement if its level falls b/w min and max | |
298 | if not check_level(logging_stmt, False, min_level_value, max_level_value): | |
299 | output += logging_stmt | |
300 | else: | |
301 | # comment out this logging statement and replace it with pass | |
302 | prefix_ws = ret.group(1) | |
303 | pass_stmt = prefix_ws + PASS_LINE_CONTENTS | |
304 | commented_out_logging_lines = comment_lines(logging_lines) | |
305 | new_lines = pass_stmt + commented_out_logging_lines | |
306 | logging.info('replacing:\n%s\nwith this:\n%s' % (logging_stmt.rstrip(), new_lines.rstrip())) | |
307 | output += new_lines | |
308 | return output | |
309 | ||
310 | def restore_logging(lines, min_level_value, max_level_value): | |
311 | """Re-enables logging statements in these lines whose logging level falls | |
312 | between the specified minimum and maximum levels and which were disabled | |
313 | by disable_logging() before.""" | |
314 | output = '' | |
315 | while lines: | |
316 | line = lines[0] | |
317 | if line.lstrip() != PASS_LINE_CONTENTS: | |
318 | # not our pass statement here, so just leave the line as-is and keep going | |
319 | output += line | |
320 | lines = lines[1:] | |
321 | else: | |
322 | # a logging call will start on the next line: find all the lines it includes and those it does not | |
323 | logging_lines, remaining_lines = split_call(lines[1:]) | |
324 | lines = remaining_lines | |
325 | logging_stmt = ''.join(logging_lines) | |
326 | original_lines = line + logging_stmt | |
327 | ||
328 | # replace the logging statement if its level falls b/w min and max | |
329 | if not check_level(logging_stmt, True, min_level_value, max_level_value): | |
330 | output += logging_stmt | |
331 | else: | |
332 | # uncomment_lines of this logging statement and remove the pass line | |
333 | uncommented_logging_lines = uncomment_lines(logging_lines) | |
334 | logging.info('replacing:\n%s\nwith this:\n%s' % (original_lines.rstrip(), uncommented_logging_lines.rstrip())) | |
335 | output += uncommented_logging_lines | |
336 | return output | |
337 | ||
338 | if __name__ == "__main__": | |
339 | logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.WARN) | |
340 | sys.exit(main()) |
0 | #!/bin/sh -e | |
1 | ||
2 | ./scripts/disable_log_statements.sh | |
3 | python setup.py sdist upload -r pypi | |
4 | ./scripts/enable_log_statements.sh |
34 | 34 | install_requires.append('greenlet') |
35 | 35 | |
36 | 36 | setup(name='pynvim', |
37 | version='0.4.2', | |
37 | version='0.4.3', | |
38 | 38 | description='Python client to neovim', |
39 | 39 | url='http://github.com/neovim/pynvim', |
40 | download_url='https://github.com/neovim/pynvim/archive/0.4.2.tar.gz', | |
40 | download_url='https://github.com/neovim/pynvim/archive/0.4.3.tar.gz', | |
41 | 41 | author='Thiago de Arruda', |
42 | 42 | author_email='tpadilha84@gmail.com', |
43 | 43 | license='Apache', |
0 | [tox] | |
1 | envlist = | |
2 | py{27,34,35,36,37,38}-{asyncio,pyuv}-cov,pypy-cov | |
3 | checkqa | |
4 | ||
5 | [testenv] | |
6 | extras = test | |
7 | deps = | |
8 | pytest-timeout | |
9 | cov: pytest-cov | |
10 | pyuv: pyuv | |
11 | setenv = | |
12 | cov: PYTEST_ADDOPTS=--cov=. {env:PYTEST_ADDOPTS:} | |
13 | passenv = PYTEST_ADDOPTS | |
14 | commands = | |
15 | python -m pytest {posargs} | |
16 | ||
17 | [testenv:checkqa] | |
18 | deps = | |
19 | flake8 | |
20 | flake8-import-order | |
21 | flake8-docstrings | |
22 | pep8-naming | |
23 | commands = flake8 {posargs:pynvim test} | |
24 | ||
25 | [testenv:docs] | |
26 | deps = | |
27 | Sphinx | |
28 | sphinx_rtd_theme | |
29 | changedir = {toxinidir}/docs | |
30 | commands = | |
31 | sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html |