Skip to content

Commit 0a84966

Browse files
authored
Merge pull request #405 from blueyed/keyerror
Add support for proper deletion, raising KeyError where appropriate
2 parents 97b343b + 574441d commit 0a84966

File tree

8 files changed

+105
-22
lines changed

8 files changed

+105
-22
lines changed

pynvim/api/common.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
from ..compat import unicode_errors_default
77

8+
__all__ = ()
9+
10+
11+
class NvimError(Exception):
12+
pass
13+
814

915
class Remote(object):
1016

@@ -26,7 +32,8 @@ def __init__(self, session, code_data):
2632
self.handle = unpackb(code_data[1])
2733
self.api = RemoteApi(self, self._api_prefix)
2834
self.vars = RemoteMap(self, self._api_prefix + 'get_var',
29-
self._api_prefix + 'set_var')
35+
self._api_prefix + 'set_var',
36+
self._api_prefix + 'del_var')
3037
self.options = RemoteMap(self, self._api_prefix + 'get_option',
3138
self._api_prefix + 'set_option')
3239

@@ -65,8 +72,16 @@ def __getattr__(self, name):
6572
return functools.partial(self._obj.request, self._api_prefix + name)
6673

6774

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

83+
84+
class RemoteMap(object):
7085
"""Represents a string->object map stored in Nvim.
7186
7287
This is the dict counterpart to the `RemoteSequence` class, but it is used
@@ -76,16 +91,23 @@ class RemoteMap(object):
7691
It is used to provide a dict-like API to vim variables and options.
7792
"""
7893

79-
def __init__(self, obj, get_method, set_method=None):
94+
_set = None
95+
_del = None
96+
97+
def __init__(self, obj, get_method, set_method=None, del_method=None):
8098
"""Initialize a RemoteMap with session, getter/setter."""
8199
self._get = functools.partial(obj.request, get_method)
82-
self._set = None
83100
if set_method:
84101
self._set = functools.partial(obj.request, set_method)
102+
if del_method:
103+
self._del = functools.partial(obj.request, del_method)
85104

86105
def __getitem__(self, key):
87106
"""Return a map value by key."""
88-
return self._get(key)
107+
try:
108+
return self._get(key)
109+
except NvimError as exc:
110+
raise transform_keyerror(exc)
89111

90112
def __setitem__(self, key, value):
91113
"""Set a map value by key(if the setter was provided)."""
@@ -95,9 +117,12 @@ def __setitem__(self, key, value):
95117

96118
def __delitem__(self, key):
97119
"""Delete a map value by associating None with the key."""
98-
if not self._set:
120+
if not self._del:
99121
raise TypeError('This dict is read-only')
100-
return self._set(key, None)
122+
try:
123+
return self._del(key)
124+
except NvimError as exc:
125+
raise transform_keyerror(exc)
101126

102127
def __contains__(self, key):
103128
"""Check if key is present in the map."""
@@ -110,8 +135,8 @@ def __contains__(self, key):
110135
def get(self, key, default=None):
111136
"""Return value for key if present, else a default value."""
112137
try:
113-
return self._get(key)
114-
except Exception:
138+
return self.__getitem__(key)
139+
except KeyError:
115140
return default
116141

117142

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

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ extend-ignore = D211,E731,D401,W503
66
max-line-length = 88
77
per-file-ignores =
88
test/*:D1
9+
application-import-names = pynvim
910

1011
[isort]
1112
known_first_party = pynvim

test/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import os
33
import textwrap
44

5-
import pynvim
6-
75
import pytest
86

7+
import pynvim
8+
99
pynvim.setup_logging("test")
1010

1111

test/test_buffer.py

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

3+
import pytest
4+
5+
from pynvim.api import NvimError
36
from pynvim.compat import IS_PYTHON3
47

58

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

7892

7993
def test_api(vim):
@@ -95,6 +109,10 @@ def test_options(vim):
95109
# Doesn't change the global value
96110
assert vim.options['define'] == r'^\s*#\s*define'
97111

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

99117
def test_number(vim):
100118
curnum = vim.current.buffer.number
@@ -154,11 +172,11 @@ def test_invalid_utf8(vim):
154172

155173

156174
def test_get_exceptions(vim):
157-
try:
175+
with pytest.raises(KeyError) as excinfo:
158176
vim.current.buffer.options['invalid-option']
159-
assert False
160-
except vim.error:
161-
pass
177+
178+
assert not isinstance(excinfo.value, NvimError)
179+
assert excinfo.value.args == ("Invalid option name: 'invalid-option'",)
162180

163181

164182
def test_set_items_for_range(vim):

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)