Skip to content

Commit 1696737

Browse files
authored
feat: Ex command ":py=" evaluate and print expression #548
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__ ```
1 parent b8ef69a commit 1696737

File tree

3 files changed

+58
-10
lines changed

3 files changed

+58
-10
lines changed

pynvim/msgpack_rpc/session.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,12 +243,12 @@ def handler():
243243
+ 'sending %s as response', gr, rv)
244244
response.send(rv)
245245
except ErrorResponse as err:
246-
warn("error response from request '%s %s': %s", name,
247-
args, format_exc())
246+
debug("error response from request '%s %s': %s",
247+
name, args, format_exc())
248248
response.send(err.args[0], error=True)
249249
except Exception as err:
250-
warn("error caught while processing request '%s %s': %s", name,
251-
args, format_exc())
250+
warn("error caught while processing request '%s %s': %s",
251+
name, args, format_exc())
252252
response.send(repr(err) + "\n" + format_exc(5), error=True)
253253
debug('greenlet %s is now dying...', gr)
254254

pynvim/plugin/script_host.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,19 @@ def teardown(self):
8181
def python_execute(self, script, range_start, range_stop):
8282
"""Handle the `python` ex command."""
8383
self._set_current_range(range_start, range_stop)
84+
85+
if script.startswith('='):
86+
# Handle ":py= ...". Evaluate as an expression and print.
87+
# (note: a valid python statement can't start with "=")
88+
expr = script[1:]
89+
print(self.python_eval(expr))
90+
return
91+
8492
try:
93+
# pylint: disable-next=exec-used
8594
exec(script, self.module.__dict__)
86-
except Exception:
87-
raise ErrorResponse(format_exc_skip(1))
95+
except Exception as exc:
96+
raise ErrorResponse(format_exc_skip(1)) from exc
8897

8998
@rpc_export('python_execute_file', sync=True)
9099
def python_execute_file(self, file_path, range_start, range_stop):
@@ -93,9 +102,10 @@ def python_execute_file(self, file_path, range_start, range_stop):
93102
with open(file_path, 'rb') as f:
94103
script = compile(f.read(), file_path, 'exec')
95104
try:
105+
# pylint: disable-next=exec-used
96106
exec(script, self.module.__dict__)
97-
except Exception:
98-
raise ErrorResponse(format_exc_skip(1))
107+
except Exception as exc:
108+
raise ErrorResponse(format_exc_skip(1)) from exc
99109

100110
@rpc_export('python_do_range', sync=True)
101111
def python_do_range(self, start, stop, code):
@@ -154,7 +164,11 @@ def python_do_range(self, start, stop, code):
154164
@rpc_export('python_eval', sync=True)
155165
def python_eval(self, expr):
156166
"""Handle the `pyeval` vim function."""
157-
return eval(expr, self.module.__dict__)
167+
try:
168+
# pylint: disable-next=eval-used
169+
return eval(expr, self.module.__dict__)
170+
except Exception as exc:
171+
raise ErrorResponse(format_exc_skip(1)) from exc
158172

159173
@rpc_export('python_chdir', sync=False)
160174
def python_chdir(self, cwd):

test/test_vim.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import os
22
import sys
33
import tempfile
4+
import textwrap
45
from pathlib import Path
56

67
import pytest
78

8-
from pynvim.api import Nvim
9+
from pynvim.api import Nvim, NvimError
910

1011

1112
def source(vim: Nvim, code: str) -> None:
@@ -40,6 +41,10 @@ def test_command(vim: Nvim) -> None:
4041
def test_command_output(vim: Nvim) -> None:
4142
assert vim.command_output('echo "test"') == 'test'
4243

44+
# can capture multi-line outputs
45+
vim.command("let g:multiline_string = join(['foo', 'bar'], nr2char(10))")
46+
assert vim.command_output('echo g:multiline_string') == "foo\nbar"
47+
4348

4449
def test_command_error(vim: Nvim) -> None:
4550
with pytest.raises(vim.error) as excinfo:
@@ -213,6 +218,35 @@ def test_python3(vim: Nvim) -> None:
213218
assert 1 == vim.eval('has("python3")')
214219

215220

221+
def test_python3_ex_eval(vim: Nvim) -> None:
222+
assert '42' == vim.command_output('python3 =42')
223+
assert '42' == vim.command_output('python3 = 42 ')
224+
assert '42' == vim.command_output('py3= 42 ')
225+
assert '42' == vim.command_output('py=42')
226+
227+
# On syntax error or evaluation error, stacktrace information is printed
228+
# Note: the pynvim API command_output() throws an exception on error
229+
# because the Ex command :python will throw (wrapped with provider#python3#Call)
230+
with pytest.raises(NvimError) as excinfo:
231+
vim.command('py3= 1/0')
232+
assert textwrap.dedent('''\
233+
Traceback (most recent call last):
234+
File "<string>", line 1, in <module>
235+
ZeroDivisionError: division by zero
236+
''').strip() in excinfo.value.args[0]
237+
238+
vim.command('python3 def raise_error(): raise RuntimeError("oops")')
239+
with pytest.raises(NvimError) as excinfo:
240+
vim.command_output('python3 =print("nooo", raise_error())')
241+
assert textwrap.dedent('''\
242+
Traceback (most recent call last):
243+
File "<string>", line 1, in <module>
244+
File "<string>", line 1, in raise_error
245+
RuntimeError: oops
246+
''').strip() in excinfo.value.args[0]
247+
assert 'nooo' not in vim.command_output(':messages')
248+
249+
216250
def test_python_cwd(vim: Nvim, tmp_path: Path) -> None:
217251
vim.command('python3 import os')
218252
cwd_before = vim.command_output('python3 print(os.getcwd())')

0 commit comments

Comments
 (0)