Completed
Pull Request — master (#223)
by Björn
02:44 queued 01:13
created

Session._enqueue_notification_and_stop()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.2963

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 3
ccs 1
cts 3
cp 0.3333
crap 1.2963
rs 10
c 0
b 0
f 0
1
"""Synchronous msgpack-rpc session layer."""
2 6
import logging
3 6
from collections import deque
4
5 6
from traceback import format_exc
6
7 6
import greenlet
0 ignored issues
show
Configuration introduced by
The import greenlet could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
8
9 6
logger = logging.getLogger(__name__)
10 6
error, debug, info, warn = (logger.error, logger.debug, logger.info,
11
                            logger.warning,)
12
13
14 6
class Session(object):
15
16
    """Msgpack-rpc session layer that uses coroutines for a synchronous API.
17
18
    This class provides the public msgpack-rpc API required by this library.
19
    It uses the greenlet module to handle requests and notifications coming
20
    from Nvim with a synchronous API.
21
    """
22
23 6
    def __init__(self, async_session):
24
        """Wrap `async_session` on a synchronous msgpack-rpc interface."""
25
        self._async_session = async_session
26
        self._request_cb = self._notification_cb = None
27
        self._pending_messages = deque()
28
        self._is_running = False
29
        self._setup_exception = None
30
31 6
    def threadsafe_call(self, fn, *args, **kwargs):
32
        """Wrapper around `AsyncSession.threadsafe_call`."""
33
        def handler():
34
            try:
35
                fn(*args, **kwargs)
36
            except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
37
                warn("error caught while excecuting async callback\n%s\n",
38
                     format_exc())
39
40
        def greenlet_wrapper():
41
            gr = greenlet.greenlet(handler)
42
            gr.switch()
43
44
        self._async_session.threadsafe_call(greenlet_wrapper)
45
46 6
    def next_message(self):
47
        """Block until a message(request or notification) is available.
48
49
        If any messages were previously enqueued, return the first in queue.
50
        If not, run the event loop until one is received.
51
        """
52
        if self._is_running:
53
            raise Exception('Event loop already running')
54
        if self._pending_messages:
55
            return self._pending_messages.popleft()
56
        self._async_session.run(self._enqueue_request_and_stop,
57
                                self._enqueue_notification_and_stop)
58
        if self._pending_messages:
59
            return self._pending_messages.popleft()
60
61 6
    def request(self, method, *args, **kwargs):
62
        """Send a msgpack-rpc request and block until as response is received.
63
64
        If the event loop is running, this method must have been called by a
65
        request or notification handler running on a greenlet. In that case,
66
        send the quest and yield to the parent greenlet until a response is
67
        available.
68
69
        When the event loop is not running, it will perform a blocking request
70
        like this:
71
        - Send the request
72
        - Run the loop until the response is available
73
        - Put requests/notifications received while waiting into a queue
74
75
        If the `async` flag is present and True, a asynchronous notification is
76
        sent instead. This will never block, and the return value or error is
77
        ignored.
78
        """
79
        async = kwargs.pop('async', False)
80
        if async:
81
            self._async_session.notify(method, args)
82
            return
83
84
        if kwargs:
85
            raise ValueError("request got unsupported keyword argument(s): {0}"
86
                             .format(', '.join(kwargs.keys())))
87
88
        if self._is_running:
89
            v = self._yielding_request(method, args)
90
        else:
91
            v = self._blocking_request(method, args)
92
        if not v:
93
            # EOF
94
            raise IOError('EOF')
95
        err, rv = v
0 ignored issues
show
Bug introduced by
The tuple unpacking with sequence defined at line 157 seems to be unbalanced; 2 value(s) for 0 label(s)

This happens when the amount of values does not equal the amount of labels:

a, b = ("a", "b", "c")  # only 2 labels for 3 values
Loading history...
96
        if err:
97
            info("'Received error: %s", err)
98
            raise self.error_wrapper(err)
0 ignored issues
show
Bug introduced by
The Instance of Session does not seem to have a member named error_wrapper.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
99
        return rv
100
101 6
    def run(self, request_cb, notification_cb, setup_cb=None):
102
        """Run the event loop to receive requests and notifications from Nvim.
103
104
        Like `AsyncSession.run()`, but `request_cb` and `notification_cb` are
105
        inside greenlets.
106
        """
107
        self._request_cb = request_cb
108
        self._notification_cb = notification_cb
109
        self._is_running = True
110
        self._setup_exception = None
111
112
        def on_setup():
113
            try:
114
                setup_cb()
115
            except Exception as e:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
116
                self._setup_exception = e
117
                self.stop()
118
119
        if setup_cb:
120
            # Create a new greenlet to handle the setup function
121
            gr = greenlet.greenlet(on_setup)
122
            gr.switch()
123
124
        if self._setup_exception:
125
            error('Setup error: {0}'.format(self._setup_exception))
126
            raise self._setup_exception
0 ignored issues
show
Bug introduced by
Raising NoneType while only classes or instances are allowed
Loading history...
127
128
        # Process all pending requests and notifications
129
        while self._pending_messages:
130
            msg = self._pending_messages.popleft()
131
            getattr(self, '_on_{0}'.format(msg[0]))(*msg[1:])
132
        self._async_session.run(self._on_request, self._on_notification)
133
        self._is_running = False
134
        self._request_cb = None
135
        self._notification_cb = None
136
137
        if self._setup_exception:
138
            raise self._setup_exception
0 ignored issues
show
Bug introduced by
Raising NoneType while only classes or instances are allowed
Loading history...
139
140 6
    def stop(self):
141
        """Stop the event loop."""
142
        self._async_session.stop()
143
144 6
    def _yielding_request(self, method, args):
145
        gr = greenlet.getcurrent()
146
        parent = gr.parent
147
148
        def response_cb(err, rv):
149
            debug('response is available for greenlet %s, switching back', gr)
150
            gr.switch(err, rv)
151
152
        self._async_session.request(method, args, response_cb)
153
        debug('yielding from greenlet %s to wait for response', gr)
154
        return parent.switch()
155
156 6
    def _blocking_request(self, method, args):
157
        result = []
158
159
        def response_cb(err, rv):
160
            result.extend([err, rv])
161
            self.stop()
162
163
        self._async_session.request(method, args, response_cb)
164
        self._async_session.run(self._enqueue_request,
165
                                self._enqueue_notification)
166
        return result
167
168 6
    def _enqueue_request_and_stop(self, name, args, response):
169
        self._enqueue_request(name, args, response)
170
        self.stop()
171
172 6
    def _enqueue_notification_and_stop(self, name, args):
173
        self._enqueue_notification(name, args)
174
        self.stop()
175
176 6
    def _enqueue_request(self, name, args, response):
177
        self._pending_messages.append(('request', name, args, response,))
178
179 6
    def _enqueue_notification(self, name, args):
180
        self._pending_messages.append(('notification', name, args,))
181
182 6
    def _on_request(self, name, args, response):
183
        def handler():
184
            try:
185
                rv = self._request_cb(name, args)
186
                debug('greenlet %s finished executing, ' +
187
                      'sending %s as response', gr, rv)
188
                response.send(rv)
189
            except ErrorResponse as err:
190
                warn("error response from request '%s %s': %s", name,
191
                     args, format_exc())
192
                response.send(err.args[0], error=True)
193
            except Exception as err:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
194
                warn("error caught while processing request '%s %s': %s", name,
195
                     args, format_exc())
196
                response.send(repr(err) + "\n" + format_exc(5), error=True)
197
            debug('greenlet %s is now dying...', gr)
198
199
        # Create a new greenlet to handle the request
200
        gr = greenlet.greenlet(handler)
201
        debug('received rpc request, greenlet %s will handle it', gr)
202
        gr.switch()
203
204 6
    def _on_notification(self, name, args):
205
        def handler():
206
            try:
207
                self._notification_cb(name, args)
208
                debug('greenlet %s finished executing', gr)
209
            except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
210
                warn("error caught while processing notification '%s %s': %s",
211
                     name, args, format_exc())
212
213
            debug('greenlet %s is now dying...', gr)
214
215
        gr = greenlet.greenlet(handler)
216
        debug('received rpc notification, greenlet %s will handle it', gr)
217
        gr.switch()
218
219
220 6
class ErrorResponse(BaseException):
221
222
    """Raise this in a request handler to respond with a given error message.
223
224
    Unlike when other exceptions are caught, this gives full control off the
225
    error response sent. When "ErrorResponse(msg)" is caught "msg" will be
226
    sent verbatim as the error response.No traceback will be appended.
227
    """
228
229
    pass
230