Skip to content

Commit f4f3bf5

Browse files
committed
Add support for proper deletion, raising KeyError where appropriate
Fixes #402
1 parent 3fb2f94 commit f4f3bf5

File tree

6 files changed

+96
-18
lines changed

6 files changed

+96
-18
lines changed

pynvim/api/common.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from ..compat import unicode_errors_default
77

88

9+
class NvimError(Exception):
10+
pass
11+
12+
913
class Remote(object):
1014

1115
"""Base class for Nvim objects(buffer/window/tabpage).
@@ -26,7 +30,8 @@ def __init__(self, session, code_data):
2630
self.handle = unpackb(code_data[1])
2731
self.api = RemoteApi(self, self._api_prefix)
2832
self.vars = RemoteMap(self, self._api_prefix + 'get_var',
29-
self._api_prefix + 'set_var')
33+
self._api_prefix + 'set_var',
34+
self._api_prefix + 'del_var')
3035
self.options = RemoteMap(self, self._api_prefix + 'get_option',
3136
self._api_prefix + 'set_option')
3237

@@ -65,8 +70,16 @@ def __getattr__(self, name):
6570
return functools.partial(self._obj.request, self._api_prefix + name)
6671

6772

68-
class RemoteMap(object):
73+
def transform_keyerror(exc):
74+
if isinstance(exc, NvimError):
75+
if exc.args[0].startswith('Key not found:'):
76+
return KeyError(exc.args[0])
77+
if exc.args[0].startswith('Invalid option name:'):
78+
return KeyError(exc.args[0])
79+
return exc
6980

81+
82+
class RemoteMap(object):
7083
"""Represents a string->object map stored in Nvim.
7184
7285
This is the dict counterpart to the `RemoteSequence` class, but it is used
@@ -75,17 +88,23 @@ class RemoteMap(object):
7588
7689
It is used to provide a dict-like API to vim variables and options.
7790
"""
91+
_set = None
92+
_del = None
7893

79-
def __init__(self, obj, get_method, set_method=None):
94+
def __init__(self, obj, get_method, set_method=None, del_method=None):
8095
"""Initialize a RemoteMap with session, getter/setter."""
8196
self._get = functools.partial(obj.request, get_method)
82-
self._set = None
8397
if set_method:
8498
self._set = functools.partial(obj.request, set_method)
99+
if del_method:
100+
self._del = functools.partial(obj.request, del_method)
85101

86102
def __getitem__(self, key):
87103
"""Return a map value by key."""
88-
return self._get(key)
104+
try:
105+
return self._get(key)
106+
except NvimError as exc:
107+
raise transform_keyerror(exc)
89108

90109
def __setitem__(self, key, value):
91110
"""Set a map value by key(if the setter was provided)."""
@@ -95,9 +114,12 @@ def __setitem__(self, key, value):
95114

96115
def __delitem__(self, key):
97116
"""Delete a map value by associating None with the key."""
98-
if not self._set:
117+
if not self._del:
99118
raise TypeError('This dict is read-only')
100-
return self._set(key, None)
119+
try:
120+
return self._del(key)
121+
except NvimError as exc:
122+
raise transform_keyerror(exc)
101123

102124
def __contains__(self, key):
103125
"""Check if key is present in the map."""
@@ -110,8 +132,8 @@ def __contains__(self, key):
110132
def get(self, key, default=None):
111133
"""Return value for key if present, else a default value."""
112134
try:
113-
return self._get(key)
114-
except Exception:
135+
return self.__getitem__(key)
136+
except KeyError:
115137
return default
116138

117139

pynvim/api/nvim.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from msgpack import ExtType
99

1010
from .buffer import Buffer
11-
from .common import (Remote, RemoteApi, RemoteMap, RemoteSequence,
11+
from .common import (NvimError, Remote, RemoteApi, RemoteMap, RemoteSequence,
1212
decode_if_bytes, walk)
1313
from .tabpage import Tabpage
1414
from .window import Window
@@ -107,8 +107,8 @@ def __init__(self, session, channel_id, metadata, types,
107107
self.version = Version(**version)
108108
self.types = types
109109
self.api = RemoteApi(self, 'nvim_')
110-
self.vars = RemoteMap(self, 'nvim_get_var', 'nvim_set_var')
111-
self.vvars = RemoteMap(self, 'nvim_get_vvar', None)
110+
self.vars = RemoteMap(self, 'nvim_get_var', 'nvim_set_var', 'nvim_del_var')
111+
self.vvars = RemoteMap(self, 'nvim_get_vvar', None, None)
112112
self.options = RemoteMap(self, 'nvim_get_option', 'nvim_set_option')
113113
self.buffers = Buffers(self)
114114
self.windows = RemoteSequence(self, 'nvim_list_wins')
@@ -575,7 +575,3 @@ def __call__(self, *args, **kwargs):
575575
pattern = "return {}(...)" if not async_ else "{}(...)"
576576
code = pattern.format(self.name)
577577
return self._nvim.exec_lua(code, *args, **kwargs)
578-
579-
580-
class NvimError(Exception):
581-
pass

test/test_buffer.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22

3+
import pytest
4+
from pynvim.api import NvimError
35
from pynvim.compat import IS_PYTHON3
46

57

@@ -74,6 +76,17 @@ def test_vars(vim):
7476
vim.current.buffer.vars['python'] = [1, 2, {'3': 1}]
7577
assert vim.current.buffer.vars['python'] == [1, 2, {'3': 1}]
7678
assert vim.eval('b:python') == [1, 2, {'3': 1}]
79+
assert vim.current.buffer.vars.get('python') == [1, 2, {'3': 1}]
80+
81+
del vim.current.buffer.vars['python']
82+
with pytest.raises(KeyError):
83+
vim.current.buffer.vars['python']
84+
assert vim.eval('exists("b:python")') == 0
85+
86+
with pytest.raises(KeyError):
87+
del vim.current.buffer.vars['python']
88+
89+
assert vim.current.buffer.vars.get('python', 'default') == 'default'
7790

7891

7992
def test_api(vim):
@@ -95,6 +108,10 @@ def test_options(vim):
95108
# Doesn't change the global value
96109
assert vim.options['define'] == r'^\s*#\s*define'
97110

111+
with pytest.raises(KeyError) as excinfo:
112+
vim.current.buffer.options['doesnotexist']
113+
assert excinfo.value.args == ("Invalid option name: 'doesnotexist'",)
114+
98115

99116
def test_number(vim):
100117
curnum = vim.current.buffer.number
@@ -154,10 +171,10 @@ def test_invalid_utf8(vim):
154171

155172

156173
def test_get_exceptions(vim):
157-
with pytest.raises(vim.error) as excinfo:
174+
with pytest.raises(KeyError) as excinfo:
158175
vim.current.buffer.options['invalid-option']
159176

160-
assert isinstance(excinfo.value, KeyError)
177+
assert not isinstance(excinfo.value, NvimError)
161178
assert excinfo.value.args == ("Invalid option name: 'invalid-option'",)
162179

163180

test/test_tabpage.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import pytest
2+
3+
14
def test_windows(vim):
25
vim.command('tabnew')
36
vim.command('vsplit')
@@ -12,6 +15,17 @@ def test_vars(vim):
1215
vim.current.tabpage.vars['python'] = [1, 2, {'3': 1}]
1316
assert vim.current.tabpage.vars['python'] == [1, 2, {'3': 1}]
1417
assert vim.eval('t:python') == [1, 2, {'3': 1}]
18+
assert vim.current.tabpage.vars.get('python') == [1, 2, {'3': 1}]
19+
20+
del vim.current.tabpage.vars['python']
21+
with pytest.raises(KeyError):
22+
vim.current.tabpage.vars['python']
23+
assert vim.eval('exists("t:python")') == 0
24+
25+
with pytest.raises(KeyError):
26+
del vim.current.tabpage.vars['python']
27+
28+
assert vim.current.tabpage.vars.get('python', 'default') == 'default'
1529

1630

1731
def test_valid(vim):

test/test_vim.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,17 @@ def test_vars(vim):
9999
vim.vars['python'] = [1, 2, {'3': 1}]
100100
assert vim.vars['python'], [1, 2 == {'3': 1}]
101101
assert vim.eval('g:python'), [1, 2 == {'3': 1}]
102+
assert vim.vars.get('python') == [1, 2, {'3': 1}]
103+
104+
del vim.vars['python']
105+
with pytest.raises(KeyError):
106+
vim.vars['python']
107+
assert vim.eval('exists("g:python")') == 0
108+
109+
with pytest.raises(KeyError):
110+
del vim.vars['python']
111+
112+
assert vim.vars.get('python', 'default') == 'default'
102113

103114

104115
def test_options(vim):

test/test_window.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import pytest
2+
3+
14
def test_buffer(vim):
25
assert vim.current.buffer == vim.windows[0].buffer
36
vim.command('new')
@@ -40,6 +43,17 @@ def test_vars(vim):
4043
vim.current.window.vars['python'] = [1, 2, {'3': 1}]
4144
assert vim.current.window.vars['python'] == [1, 2, {'3': 1}]
4245
assert vim.eval('w:python') == [1, 2, {'3': 1}]
46+
assert vim.current.window.vars.get('python') == [1, 2, {'3': 1}]
47+
48+
del vim.current.window.vars['python']
49+
with pytest.raises(KeyError):
50+
vim.current.window.vars['python']
51+
assert vim.eval('exists("w:python")') == 0
52+
53+
with pytest.raises(KeyError):
54+
del vim.current.window.vars['python']
55+
56+
assert vim.current.window.vars.get('python', 'default') == 'default'
4357

4458

4559
def test_options(vim):
@@ -50,6 +64,10 @@ def test_options(vim):
5064
assert vim.current.window.options['statusline'] == 'window-status'
5165
assert vim.options['statusline'] == ''
5266

67+
with pytest.raises(KeyError) as excinfo:
68+
vim.current.window.options['doesnotexist']
69+
assert excinfo.value.args == ("Invalid option name: 'doesnotexist'",)
70+
5371

5472
def test_position(vim):
5573
height = vim.windows[0].height

0 commit comments

Comments
 (0)