Skip to content

Commit

Permalink
feat: Ex command ":py=" evaluate and print expression #548
Browse files Browse the repository at this point in the history
The Ex command `:py`, `:python`, :`py3`, etc. can evaluate the line
as an expression rather than a statement if the line starts with `=`,
just like `:lua=`.

`:py= <expr>` is equivalent as `:py print(<expr>)`.

```vim
:py3= sys.version_info[:3]
:python3 =pynvim.__version__
```
  • Loading branch information
wookayin committed Nov 13, 2023
1 parent b8ef69a commit 1696737
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 10 deletions.
8 changes: 4 additions & 4 deletions pynvim/msgpack_rpc/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,12 @@ def handler():
+ 'sending %s as response', gr, rv)
response.send(rv)
except ErrorResponse as err:
warn("error response from request '%s %s': %s", name,
args, format_exc())
debug("error response from request '%s %s': %s",
name, args, format_exc())
response.send(err.args[0], error=True)
except Exception as err:
warn("error caught while processing request '%s %s': %s", name,
args, format_exc())
warn("error caught while processing request '%s %s': %s",
name, args, format_exc())
response.send(repr(err) + "\n" + format_exc(5), error=True)
debug('greenlet %s is now dying...', gr)

Expand Down
24 changes: 19 additions & 5 deletions pynvim/plugin/script_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,19 @@ def teardown(self):
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:
raise ErrorResponse(format_exc_skip(1))
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):
Expand All @@ -93,9 +102,10 @@ def python_execute_file(self, file_path, 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:
raise ErrorResponse(format_exc_skip(1))
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):
Expand Down Expand Up @@ -154,7 +164,11 @@ def python_do_range(self, start, stop, code):
@rpc_export('python_eval', sync=True)
def python_eval(self, expr):
"""Handle the `pyeval` vim function."""
return eval(expr, self.module.__dict__)
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):
Expand Down
36 changes: 35 additions & 1 deletion test/test_vim.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import os
import sys
import tempfile
import textwrap
from pathlib import Path

import pytest

from pynvim.api import Nvim
from pynvim.api import Nvim, NvimError


def source(vim: Nvim, code: str) -> None:
Expand Down Expand Up @@ -40,6 +41,10 @@ def test_command(vim: Nvim) -> None:
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:
Expand Down Expand Up @@ -213,6 +218,35 @@ def test_python3(vim: Nvim) -> None:
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 "<string>", line 1, in <module>
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 "<string>", line 1, in <module>
File "<string>", 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())')
Expand Down

0 comments on commit 1696737

Please sign in to comment.