Completed
Pull Request — master (#303)
by Björn
24:34
created

Session.stop()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
"""Synchronous msgpack-rpc session layer."""
2 5
import logging
3 5
from collections import deque
4
from traceback import format_exc
5 5
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...
7 5
8
logger = logging.getLogger(__name__)
9 5
error, debug, info, warn = (logger.error, logger.debug, logger.info,
10 5
                            logger.warning,)
11
12
13
class Session(object):
14 5
15
    """Msgpack-rpc session layer that uses coroutines for a synchronous API.
16
17
    This class provides the public msgpack-rpc API required by this library.
18
    It uses the greenlet module to handle requests and notifications coming
19
    from Nvim with a synchronous API.
20
    """
21
22
    def __init__(self, async_session):
23 5
        """Wrap `async_session` on a synchronous msgpack-rpc interface."""
24
        self._async_session = async_session
25 5
        self._request_cb = self._notification_cb = None
26 5
        self._pending_messages = deque()
27 5
        self._is_running = False
28 5
        self._setup_exception = None
29 5
30
    def threadsafe_call(self, fn, *args, **kwargs):
31 5
        """Wrapper around `AsyncSession.threadsafe_call`."""
32
        def handler():
33 5
            try:
34 5
                fn(*args, **kwargs)
35 5
            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...
36
                warn("error caught while excecuting async callback\n%s\n",
37
                     format_exc())
38
39
        def greenlet_wrapper():
40 5
            gr = greenlet.greenlet(handler)
41 5
            gr.switch()
42 5
43
        self._async_session.threadsafe_call(greenlet_wrapper)
44 5
45
    def next_message(self):
46 5
        """Block until a message(request or notification) is available.
47
48
        If any messages were previously enqueued, return the first in queue.
49
        If not, run the event loop until one is received.
50
        """
51
        if self._is_running:
52 5
            raise Exception('Event loop already running')
53 5
        if self._pending_messages:
54 5
            return self._pending_messages.popleft()
55 5
        self._async_session.run(self._enqueue_request_and_stop,
56 5
                                self._enqueue_notification_and_stop)
57
        if self._pending_messages:
58 5
            return self._pending_messages.popleft()
59 5
60
    def request(self, method, *args, **kwargs):
61 5
        """Send a msgpack-rpc request and block until as response is received.
62
63
        If the event loop is running, this method must have been called by a
64
        request or notification handler running on a greenlet. In that case,
65
        send the quest and yield to the parent greenlet until a response is
66
        available.
67
68
        When the event loop is not running, it will perform a blocking request
69
        like this:
70
        - Send the request
71
        - Run the loop until the response is available
72
        - Put requests/notifications received while waiting into a queue
73
74
        If the `async` flag is present and True, a asynchronous notification is
75
        sent instead. This will never block, and the return value or error is
76
        ignored.
77
        """
78
        async = kwargs.pop('async', False)
79 5
        if async:
80 5
            self._async_session.notify(method, args)
81 5
            return
82 5
83
        if kwargs:
84 5
            raise ValueError("request got unsupported keyword argument(s): {}"
85
                             .format(', '.join(kwargs.keys())))
86
87
        if self._is_running:
88 5
            v = self._yielding_request(method, args)
89 5
        else:
90
            v = self._blocking_request(method, args)
91 5
        if not v:
92 5
            # EOF
93
            raise IOError('EOF')
94
        err, rv = v
0 ignored issues
show
Bug introduced by
The tuple unpacking with sequence defined at line 160 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...
95 5
        if err:
96 5
            info("'Received error: %s", err)
97 5
            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...
98 5
        return rv
99 5
100
    def run(self, request_cb, notification_cb, setup_cb=None):
101 5
        """Run the event loop to receive requests and notifications from Nvim.
102
103
        Like `AsyncSession.run()`, but `request_cb` and `notification_cb` are
104
        inside greenlets.
105
        """
106
        self._request_cb = request_cb
107 5
        self._notification_cb = notification_cb
108 5
        self._is_running = True
109 5
        self._setup_exception = None
110 5
111
        def on_setup():
112 5
            try:
113 5
                setup_cb()
114 5
            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...
115
                self._setup_exception = e
116
                self.stop()
117
118
        if setup_cb:
119 5
            # Create a new greenlet to handle the setup function
120
            gr = greenlet.greenlet(on_setup)
121 5
            gr.switch()
122 5
123
        if self._setup_exception:
124 5
            error('Setup error: {}'.format(self._setup_exception))
125
            raise self._setup_exception
0 ignored issues
show
Bug introduced by
Raising NoneType while only classes or instances are allowed
Loading history...
126
127
        # Process all pending requests and notifications
128
        while self._pending_messages:
129 5
            msg = self._pending_messages.popleft()
130
            getattr(self, '_on_{}'.format(msg[0]))(*msg[1:])
131 1
        self._async_session.run(self._on_request, self._on_notification)
132 5
        self._is_running = False
133 5
        self._request_cb = None
134 5
        self._notification_cb = None
135 5
136
        if self._setup_exception:
137 5
            raise self._setup_exception
0 ignored issues
show
Bug introduced by
Raising NoneType while only classes or instances are allowed
Loading history...
138
139
    def stop(self):
140 5
        """Stop the event loop."""
141
        self._async_session.stop()
142 5
143
    def close(self):
144 5
        """Close the event loop."""
145 5
        self._async_session.close()
146 5
147
    def _yielding_request(self, method, args):
148 5
        gr = greenlet.getcurrent()
149 5
        parent = gr.parent
150 5
151
        def response_cb(err, rv):
152 5
            debug('response is available for greenlet %s, switching back', gr)
153 5
            gr.switch(err, rv)
154 5
155
        self._async_session.request(method, args, response_cb)
156 5
        debug('yielding from greenlet %s to wait for response', gr)
157 5
        return parent.switch()
158
159 5
    def _blocking_request(self, method, args):
160 5
        result = []
161 5
162
        def response_cb(err, rv):
163 5
            result.extend([err, rv])
164 5
            self.stop()
165
166 5
        self._async_session.request(method, args, response_cb)
167
        self._async_session.run(self._enqueue_request,
168 5
                                self._enqueue_notification)
169
        return result
170
171
    def _enqueue_request_and_stop(self, name, args, response):
172 5
        self._enqueue_request(name, args, response)
173 5
        self.stop()
174 5
175
    def _enqueue_notification_and_stop(self, name, args):
176 5
        self._enqueue_notification(name, args)
177
        self.stop()
178
179 5
    def _enqueue_request(self, name, args, response):
180 5
        self._pending_messages.append(('request', name, args, response,))
181
182 5
    def _enqueue_notification(self, name, args):
183 5
        self._pending_messages.append(('notification', name, args,))
184 5
185 5
    def _on_request(self, name, args, response):
186 5
        def handler():
187
            try:
188 5
                rv = self._request_cb(name, args)
189
                debug('greenlet %s finished executing, ' +
190
                      'sending %s as response', gr, rv)
191
                response.send(rv)
192
            except ErrorResponse as err:
193
                warn("error response from request '%s %s': %s", name,
194
                     args, format_exc())
195
                response.send(err.args[0], error=True)
196
            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...
197 5
                warn("error caught while processing request '%s %s': %s", name,
198
                     args, format_exc())
199
                response.send(repr(err) + "\n" + format_exc(5), error=True)
200 5
            debug('greenlet %s is now dying...', gr)
201 5
202 5
        # Create a new greenlet to handle the request
203
        gr = greenlet.greenlet(handler)
204 5
        debug('received rpc request, greenlet %s will handle it', gr)
205
        gr.switch()
206
207
    def _on_notification(self, name, args):
208
        def handler():
209
            try:
210
                self._notification_cb(name, args)
211
                debug('greenlet %s finished executing', gr)
212
            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...
213
                warn("error caught while processing notification '%s %s': %s",
214
                     name, args, format_exc())
215
216
            debug('greenlet %s is now dying...', gr)
217
218
        gr = greenlet.greenlet(handler)
219
        debug('received rpc notification, greenlet %s will handle it', gr)
220 5
        gr.switch()
221
222
223
class ErrorResponse(BaseException):
224
225
    """Raise this in a request handler to respond with a given error message.
226
227
    Unlike when other exceptions are caught, this gives full control off the
228
    error response sent. When "ErrorResponse(msg)" is caught "msg" will be
229 5
    sent verbatim as the error response.No traceback will be appended.
230
    """
231
232
    pass
233