Completed
Pull Request — master (#274)
by Lambda
51:31 queued 26:31
created

Session.__init__()   A

Complexity

Conditions 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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