Completed
Pull Request — master (#72)
by Björn
18:06 queued 16:50
created

Session._wrap_greenlet()   B

Complexity

Conditions 5

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.3906

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
dl 0
loc 16
ccs 9
cts 12
cp 0.75
crap 5.3906
rs 8.5454
c 1
b 0
f 0

2 Methods

Rating   Name   Duplication   Size   Complexity  
A Session.greenlet_wrapper() 0 3 1
A Session.handler() 0 6 2
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 6
        self._async_session = async_session
26 6
        self._request_cb = self._notification_cb = None
27 6
        self._pending_messages = deque()
28 6
        self._is_running = False
29 6
        self._setup_exception = None
30
31 6
    def _wrap_greenlet(self, fn, *args, **kwargs):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
32 6
        if fn is None:
33
            return None
34
35 6
        def handler():
36 6
            try:
37 6
                fn(*args, **kwargs)
38
            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...
39
                warn("error caught while excecuting async callback\n%s\n",
40
                     format_exc())
41
42 6
        def greenlet_wrapper():
43 6
            gr = greenlet.greenlet(handler)
44 6
            gr.switch()
45
46 6
        return greenlet_wrapper
47
48 6
    def threadsafe_call(self, fn, *args, **kwargs):
49
        """Wrapper around `AsyncSession.threadsafe_call`."""
50
51 6
        greenlet_wrapper = self._wrap_greenlet(fn, *args, **kwargs)
52 6
        self._async_session.threadsafe_call(greenlet_wrapper)
53
54 6
    def poll_fd(self, fd, on_readable, on_writable, greenlet=True):
0 ignored issues
show
Comprehensibility Bug introduced by
greenlet is re-defining a name which is already available in the outer-scope (previously defined on line 7).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

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