Completed
Pull Request — master (#178)
by Björn
23:15
created

neovim.plugin.Host   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 194
Duplicated Lines 0 %

Test Coverage

Coverage 9.92%
Metric Value
dl 0
loc 194
ccs 13
cts 131
cp 0.0992
rs 4.8387
wmc 58

15 Methods

Rating   Name   Duplication   Size   Complexity  
A predicate() 0 2 1
A _copy_attributes() 0 5 3
A start() 0 5 2
A missing_handler_error() 0 9 3
B _unload() 0 13 5
F _discover_functions() 0 39 12
B _on_notification() 0 19 5
B _decodehook_for() 0 7 5
A _discover_classes() 0 7 3
A _on_specs_request() 0 6 4
A __init__() 0 15 4
A _configure_nvim_for() 0 8 2
A _on_request() 0 14 4
B _load() 0 22 5
A shutdown() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like neovim.plugin.Host often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Implements a Nvim host for python plugins."""
2 6
import functools
3 6
import imp
4 6
import inspect
5 6
import logging
6 6
import os
7 6
import os.path
8
import re
9 6
10
from traceback import format_exc
11 6
12 6
from ..api import DecodeHook
13
from ..compat import IS_PYTHON3, find_module
14 6
from ..msgpack_rpc import ErrorResponse
15
16 6
__all__ = ('Host')
17 6
18
logger = logging.getLogger(__name__)
19
error, debug, info, warn = (logger.error, logger.debug, logger.info,
20
                            logger.warning,)
21 6
22
23
class Host(object):
24
25
    """Nvim host for python plugins.
26
27
    Takes care of loading/unloading plugins and routing msgpack-rpc
28
    requests/notifications to the appropriate handlers.
29 6
    """
30
31
    def __init__(self, nvim):
32
        """Set handlers for plugin_load/plugin_unload."""
33
        self.nvim = nvim
34
        self._specs = {}
35
        self._loaded = {}
36
        self._load_errors = {}
37
        self._notification_handlers = {}
38
        self._request_handlers = {
39
            'poll': lambda: 'ok',
40
            'specs': self._on_specs_request,
41
            'shutdown': self.shutdown
42
        }
43
        self._nvim_encoding = nvim.options['encoding']
44 6
        if IS_PYTHON3 and isinstance(self._nvim_encoding, bytes):
45
            self._nvim_encoding = self._nvim_encoding.decode('ascii')
46
47
    def start(self, plugins):
48
        """Start listening for msgpack-rpc requests and notifications."""
49
        self.nvim.session.run(self._on_request,
50 6
                              self._on_notification,
51
                              lambda: self._load(plugins))
52
53
    def shutdown(self):
54
        """Shutdown the host."""
55 6
        self._unload()
56
        self.nvim.session.stop()
57
58
    def _on_request(self, name, args):
59
        """Handle a msgpack-rpc request."""
60
        if IS_PYTHON3 and isinstance(name, bytes):
61
            name = name.decode(self._nvim_encoding)
62
        handler = self._request_handlers.get(name, None)
63
        if not handler:
64
            msg = self.missing_handler_error(name, "notification")
65
            error(msg)
66
            raise ErrorResponse(msg)
67
68
        debug('calling request handler for "%s", args: "%s"', name, args)
69
        rv = handler(*args)
70 6
        debug("request handler for '%s %s' returns: %s", name, args, rv)
71
        return rv
72
73
    def _on_notification(self, name, args):
74
        """Handle a msgpack-rpc notification."""
75
        if IS_PYTHON3 and isinstance(name, bytes):
76
            name = name.decode(self._nvim_encoding)
77
        handler = self._notification_handlers.get(name, None)
78
        if not handler:
79
            msg = self.missing_handler_error(name, "notification")
80
            error(msg)
81
            self.nvim.err_write(msg+"\n")
82
            return
83
84
        debug('calling notification handler for "%s", args: "%s"', name, args)
85
        try:
86
            handler(*args)
87
        except Exception as err:
88 6
            msg = ("error caught in async handler '{} {}':\n{!r}\n{}"
89
                   .format(name, args, err, format_exc(5)))
90
            self.nvim.err_write(msg, async=True)
91
            raise
92
93
    def missing_handler_error(self, name, kind):
94
        msg = 'no {} handler registered for "{}"'.format(kind, name)
95
        last_colon = name.rfind(':')
0 ignored issues
show
Unused Code introduced by
The variable last_colon seems to be unused.
Loading history...
96
        pathmatch = re.match(r'(.+):[^:]+:[^:]+', name)
97
        if pathmatch:
98
            loader_error = self._load_errors.get(pathmatch.group(1))
99
            if loader_error is not None:
100
                msg = msg + "\n" + loader_error
101
        return msg
102
103
    def _load(self, plugins):
104
        for path in plugins:
105
            err = None
106
            if path in self._loaded:
107
                error('{0} is already loaded'.format(path))
108
                continue
109
            directory, name = os.path.split(os.path.splitext(path)[0])
110
            file, pathname, description = find_module(name, [directory])
111 6
            handlers = []
112
            try:
113
                module = imp.load_module(name, file, pathname, description)
114
                self._discover_classes(module, handlers, path)
115
                self._discover_functions(module, handlers, path)
116
                if not handlers:
117
                    error('{0} exports no handlers'.format(path))
118
                    continue
119
                self._loaded[path] = {'handlers': handlers, 'module': module}
120
            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...
121
                err = ('Encountered {} loading plugin at {}: {}\n{}'
122
                        .format(type(e).__name__, path, e, format_exc(5)))
123
                error(err)
124
                self._load_errors[path] = err
125 6
126
    def _unload(self):
127
        for path, plugin in self._loaded.items():
0 ignored issues
show
Unused Code introduced by
The variable path seems to be unused.
Loading history...
128
            handlers = plugin['handlers']
129
            for handler in handlers:
130
                method_name = handler._nvim_rpc_method_name
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nvim_rpc_method_name was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
131
                if hasattr(handler, '_nvim_shutdown_hook'):
132
                    handler()
133 6
                elif handler._nvim_rpc_sync:
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nvim_rpc_sync was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
134
                    del self._request_handlers[method_name]
135
                else:
136
                    del self._notification_handlers[method_name]
137
        self._specs = {}
138
        self._loaded = {}
139
140
    def _discover_classes(self, module, handlers, plugin_path):
141
        for _, cls in inspect.getmembers(module, inspect.isclass):
142
            if getattr(cls, '_nvim_plugin', False):
143
                # create an instance of the plugin and pass the nvim object
144
                plugin = cls(self._configure_nvim_for(cls))
145
                # discover handlers in the plugin instance
146
                self._discover_functions(plugin, handlers, plugin_path)
147
148
    def _discover_functions(self, obj, handlers, plugin_path):
149
        def predicate(o):
150
            return hasattr(o, '_nvim_rpc_method_name')
151
        specs = []
152
        objenc = getattr(obj, '_nvim_encoding', None)
153
        for _, fn in inspect.getmembers(obj, predicate):
154
            enc = getattr(fn, '_nvim_encoding', objenc)
155
            if fn._nvim_bind:
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _nvim_bind was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
156
                # bind a nvim instance to the handler
157
                fn2 = functools.partial(fn, self._configure_nvim_for(fn))
158
                # copy _nvim_* attributes from the original function
159
                self._copy_attributes(fn, fn2)
160
                fn = fn2
161
            decodehook = self._decodehook_for(enc)
162
            if decodehook is not None:
163
                decoder = lambda fn, hook, *args: fn(*hook.walk(args))
164
                fn2 = functools.partial(decoder, fn, decodehook)
165
                self._copy_attributes(fn, fn2)
166
                fn = fn2
167
168
            # register in the rpc handler dict
169
            method = fn._nvim_rpc_method_name
0 ignored issues
show
Bug introduced by
The Function newfunc does not seem to have a member named _nvim_rpc_method_name.

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...
Coding Style Best Practice introduced by
It seems like _nvim_rpc_method_name was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
170
            if fn._nvim_prefix_plugin_path:
0 ignored issues
show
Bug introduced by
The Function newfunc does not seem to have a member named _nvim_prefix_plugin_path.

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...
Coding Style Best Practice introduced by
It seems like _nvim_prefix_plugin_path was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
171
                method = '{0}:{1}'.format(plugin_path, method)
172
            if fn._nvim_rpc_sync:
0 ignored issues
show
Bug introduced by
The Function newfunc does not seem to have a member named _nvim_rpc_sync.

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...
Coding Style Best Practice introduced by
It seems like _nvim_rpc_sync was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
173 6
                if method in self._request_handlers:
174
                    raise Exception(('Request handler for "{0}" is ' +
175
                                    'already registered').format(method))
176
                self._request_handlers[method] = fn
177
            else:
178
                if method in self._notification_handlers:
179 6
                    raise Exception(('Notification handler for "{0}" is ' +
180
                                    'already registered').format(method))
181
                self._notification_handlers[method] = fn
182
            if hasattr(fn, '_nvim_rpc_spec'):
183
                specs.append(fn._nvim_rpc_spec)
0 ignored issues
show
Bug introduced by
The Function newfunc does not seem to have a member named _nvim_rpc_spec.

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...
Coding Style Best Practice introduced by
It seems like _nvim_rpc_spec was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
184 6
            handlers.append(fn)
185
        if specs:
186
            self._specs[plugin_path] = specs
187
188
    def _copy_attributes(self, fn, fn2):
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...
189
        # Copy _nvim_* attributes from the original function
190
        for attr in dir(fn):
191
            if attr.startswith('_nvim_'):
192 6
                setattr(fn2, attr, getattr(fn, attr))
193
194
    def _on_specs_request(self, path):
195
        if IS_PYTHON3 and isinstance(path, bytes):
196
            path = path.decode(self._nvim_encoding)
197
        if self._load_errors:
198
            self.nvim.out_write('\n'.join(self._load_errors.values())+'\n')
199
        return self._specs.get(path, [])
200
201
    def _decodehook_for(self, encoding):
202
        if IS_PYTHON3 and encoding is None:
203
            encoding = True
204
        if encoding is True:
205
            encoding = self._nvim_encoding
206
        if encoding:
207
            return DecodeHook(encoding)
208
209
    def _configure_nvim_for(self, obj):
210
        # Configure a nvim instance for obj (checks encoding configuration)
211
        nvim = self.nvim
212
        encoding = getattr(obj, '_nvim_encoding', None)
213
        hook = self._decodehook_for(encoding)
214
        if hook is not None:
215
            nvim = nvim.with_hook(hook)
216
        return nvim
217