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

BaseEventLoop.run()   B

Complexity

Conditions 5

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.3906

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
dl 0
loc 21
ccs 12
cts 16
cp 0.75
crap 5.3906
rs 8.439
c 1
b 0
f 0
1
"""Common code for event loop implementations."""
2 6
import logging
3 6
import signal
4 6
import threading
5
6
7 6
logger = logging.getLogger(__name__)
8 6
debug, info, warn = (logger.debug, logger.info, logger.warning,)
9
10
11
# When signals are restored, the event loop library may reset SIGINT to SIG_DFL
12
# which exits the program. To be able to restore the python interpreter to it's
13
# default state, we keep a reference to the default handler
14 6
default_int_handler = signal.getsignal(signal.SIGINT)
15 6
main_thread = threading.current_thread()
16
17
18 6
class BaseEventLoop(object):
19
20
    """Abstract base class for all event loops.
21
22
    Event loops act as the bottom layer for Nvim sessions created by this
23
    library. They hide system/transport details behind a simple interface for
24
    reading/writing bytes to the connected Nvim instance.
25
26
    This class exposes public methods for interacting with the underlying
27
    event loop and delegates implementation-specific work to the following
28
    methods, which subclasses are expected to implement:
29
30
    - `_init()`: Implementation-specific initialization
31
    - `_connect_tcp(address, port)`: connect to Nvim using tcp/ip
32
    - `_connect_socket(path)`: Same as tcp, but use a UNIX domain socket or
33
      or named pipe.
34
    - `_connect_stdio()`: Use stdin/stdout as the connection to Nvim
35
    - `_connect_child(argv)`: Use the argument vector `argv` to spawn an
36
      embedded Nvim that has it's stdin/stdout connected to the event loop.
37
    - `_start_reading()`: Called after any of _connect_* methods. Can be used
38
      to perform any post-connection setup or validation.
39
    - `_send(data)`: Send `data`(byte array) to Nvim. The data is only
40
    - `_run()`: Runs the event loop until stopped or the connection is closed.
41
      calling the following methods when some event happens:
42
      actually sent when the event loop is running.
43
      - `_on_data(data)`: When Nvim sends some data.
44
      - `_on_signal(signum)`: When a signal is received.
45
      - `_on_error(message)`: When a non-recoverable error occurs(eg:
46
        connection lost)
47
    - `_stop()`: Stop the event loop
48
    - `_interrupt(data)`: Like `stop()`, but may be called from other threads
49
      this.
50
    - `_setup_signals(signals)`: Add implementation-specific listeners for
51
      for `signals`, which is a list of OS-specific signal numbers.
52
    - `_teardown_signals()`: Removes signal listeners set by `_setup_signals`
53
    """
54
55 6
    def __init__(self, transport_type, *args):
56
        """Initialize and connect the event loop instance.
57
58
        The only arguments are the transport type and transport-specific
59
        configuration, like this:
60
61
        >>> BaseEventLoop('tcp', '127.0.0.1', 7450)
62
        Traceback (most recent call last):
63
            ...
64
        AttributeError: 'BaseEventLoop' object has no attribute '_init'
65
        >>> BaseEventLoop('socket', '/tmp/nvim-socket')
66
        Traceback (most recent call last):
67
            ...
68
        AttributeError: 'BaseEventLoop' object has no attribute '_init'
69
        >>> BaseEventLoop('stdio')
70
        Traceback (most recent call last):
71
            ...
72
        AttributeError: 'BaseEventLoop' object has no attribute '_init'
73
        >>> BaseEventLoop('child', ['nvim', '--embed', '-u', 'NONE'])
74
        Traceback (most recent call last):
75
            ...
76
        AttributeError: 'BaseEventLoop' object has no attribute '_init'
77
78
        This calls the implementation-specific initialization
79
        `_init`, one of the `_connect_*` methods(based on `transport_type`)
80
        and `_start_reading()`
81
        """
82 6
        self._transport_type = transport_type
83 6
        self._signames = dict((k, v) for v, k in signal.__dict__.items()
84
                              if v.startswith('SIG'))
85 6
        self._on_data = None
86 6
        self._error = None
87 6
        self._init()
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _init.

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...
88 6
        getattr(self, '_connect_{0}'.format(transport_type))(*args)
89 6
        self._start_reading()
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _start_reading.

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...
90
91 6
    def connect_tcp(self, address, port):
92
        """Connect to tcp/ip `address`:`port`. Delegated to `_connect_tcp`."""
93
        info('Connecting to TCP address: %s:%d', address, port)
94
        self._connect_tcp(address, port)
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _connect_tcp.

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...
95
96 6
    def connect_socket(self, path):
97
        """Connect to socket at `path`. Delegated to `_connect_socket`."""
98
        info('Connecting to %s', path)
99
        self._connect_socket(path)
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _connect_socket.

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...
100
101 6
    def connect_stdio(self):
102
        """Connect using stdin/stdout. Delegated to `_connect_stdio`."""
103
        info('Preparing stdin/stdout for streaming data')
104
        self._connect_stdio()
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _connect_stdio.

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...
105
106 6
    def connect_child(self, argv):
107
        """Connect a new Nvim instance. Delegated to `_connect_child`."""
108
        info('Spawning a new nvim instance')
109
        self._connect_child(argv)
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _connect_child.

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...
110
111 6
    def send(self, data):
112
        """Queue `data` for sending to Nvim."""
113 6
        debug("Sending '%s'", data)
114 6
        self._send(data)
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _send.

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...
115
116 6
    def threadsafe_call(self, fn):
117
        """Call a function in the event loop thread.
118
119
        This is the only safe way to interact with a session from other
120
        threads.
121
        """
122 6
        self._threadsafe_call(fn)
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _threadsafe_call.

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...
123
124 6
    def poll_fd(self, fd, on_readable=None, on_writable=None):
125
        """
126
        Invoke callbacks when the fd is ready for reading and/or writing. if
127
        `on_readable` is not None, it should be callback, which will be invoked
128
        (with no arguments) when the fd is ready for writing. Similarily if
129
        `on_writable` is not None it will be invoked when the fd is ready for
130
        writing.
131
132
        Only one callback (of each kind) can be registered on the same fd at a
133
        time. If both readability and writability should be monitored, both
134
        callbacks must be registered by the same `poll_fd` call.
135
136
        Returns a function that deactivates the callback(s).
137
        """
138
        if on_readable is None and on_writable is None:
139
            raise ValueError("poll_fd: At least one of `on_readable` and `on_writable` must be present")
140
        return self._poll_fd(fd, on_readable, on_writable)
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _poll_fd.

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...
141
142 6
    def run(self, data_cb):
143
        """Run the event loop."""
144 6
        if self._error:
145
            err = self._error
146
            if isinstance(self._error, KeyboardInterrupt):
147
                # KeyboardInterrupt is not destructive(it may be used in
148
                # the REPL).
149
                # After throwing KeyboardInterrupt, cleanup the _error field
150
                # so the loop may be started again
151
                self._error = None
152
            raise err
0 ignored issues
show
Bug introduced by
Raising NoneType while only classes or instances are allowed
Loading history...
153 6
        self._on_data = data_cb
154 6
        if threading.current_thread() == main_thread:
155 6
            self._setup_signals([signal.SIGINT, signal.SIGTERM])
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _setup_signals.

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...
156 6
        debug('Entering event loop')
157 6
        self._run()
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _run.

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...
158 6
        debug('Exited event loop')
159 6
        if threading.current_thread() == main_thread:
160 6
            self._teardown_signals()
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _teardown_signals.

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...
161 6
            signal.signal(signal.SIGINT, default_int_handler)
162 6
        self._on_data = None
163
164 6
    def stop(self):
165
        """Stop the event loop."""
166 6
        self._stop()
0 ignored issues
show
Bug introduced by
The Instance of BaseEventLoop does not seem to have a member named _stop.

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...
167 6
        debug('Stopped event loop')
168
169 6
    def _on_signal(self, signum):
170
        msg = 'Received {0}'.format(self._signames[signum])
171
        debug(msg)
172
        if signum == signal.SIGINT and self._transport_type == 'stdio':
173
            # When the transport is stdio, we are probably running as a Nvim
174
            # child process. In that case, we don't want to be killed by
175
            # ctrl+C
176
            return
177
        cls = Exception
178
        if signum == signal.SIGINT:
179
            cls = KeyboardInterrupt
180
        self._error = cls(msg)
181
        self.stop()
182
183 6
    def _on_error(self, error):
184
        debug(error)
185
        self._error = IOError(error)
186
        self.stop()
187
188 6
    def _on_interrupt(self):
189
        self.stop()
190