Completed
Pull Request — master (#187)
by Björn
02:23
created

neovim.plugin.LegacyEvalHook   A

Complexity

Total Complexity 6

Size/Duplication

Total Lines 15
Duplicated Lines 0 %

Test Coverage

Coverage 27.27%
Metric Value
dl 0
loc 15
ccs 3
cts 11
cp 0.2727
rs 10
wmc 6

2 Methods

Rating   Name   Duplication   Size   Complexity  
B _string_eval() 0 8 5
A __init__() 0 2 1
1
"""Legacy python/python3-vim emulation."""
2 6
import imp
3 6
import io
4 6
import logging
5 6
import os
6 6
import sys
7
8 6
from .decorators import rpc_export, plugin
9 6
from ..api import SessionHook
10
11 6
__all__ = ('ScriptHost',)
12
13
14 6
logger = logging.getLogger(__name__)
15 6
debug, info, warn = (logger.debug, logger.info, logger.warn,)
16
17 6
IS_PYTHON3 = sys.version_info >= (3, 0)
18
19 6
if IS_PYTHON3:
20 3
    basestring = str
21
22 3
    if sys.version_info >= (3, 4):
23 2
        from importlib.machinery import PathFinder
24
25
26 6
@plugin
27 6
class ScriptHost(object):
28
29
    """Provides an environment for running python plugins created for Vim."""
30
31 6
    def __init__(self, nvim):
32
        """Initialize the legacy python-vim environment."""
33
        self.setup(nvim)
34
        # context where all code will run
35
        self.module = imp.new_module('__main__')
36
        nvim.script_context = self.module
37
        # it seems some plugins assume 'sys' is already imported, so do it now
38
        exec('import sys', self.module.__dict__)
0 ignored issues
show
Coding Style Security introduced by
The use of exec is discouraged.

Execution of dynamic code might introduce security vulnerabilities. It is generally recommended to use this feature with care and only when necessary.

Loading history...
39
        self.legacy_vim = nvim.with_hook(LegacyEvalHook())
40
        sys.modules['vim'] = self.legacy_vim
41
42 6
    def setup(self, nvim):
43
        """Setup import hooks and global streams.
44
45
        This will add import hooks for importing modules from runtime
46
        directories and patch the sys module so 'print' calls will be
47
        forwarded to Nvim.
48
        """
49
        self.nvim = nvim
50
        info('install import hook/path')
51
        self.hook = path_hook(nvim)
52
        sys.path_hooks.append(self.hook)
53
        nvim.VIM_SPECIAL_PATH = '_vim_path_'
54
        sys.path.append(nvim.VIM_SPECIAL_PATH)
55
        info('redirect sys.stdout and sys.stderr')
56
        self.saved_stdout = sys.stdout
57
        self.saved_stderr = sys.stderr
58
        sys.stdout = RedirectStream(lambda data: nvim.out_write(data))
0 ignored issues
show
Unused Code introduced by
This lambda might be unnecessary.
Loading history...
59
        sys.stderr = RedirectStream(lambda data: nvim.err_write(data))
0 ignored issues
show
Unused Code introduced by
This lambda might be unnecessary.
Loading history...
60
61 6
    def teardown(self):
62
        """Restore state modified from the `setup` call."""
63
        for plugin in self.installed_plugins:
0 ignored issues
show
Bug introduced by
The Instance of ScriptHost does not seem to have a member named installed_plugins.

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...
Comprehensibility Bug introduced by
plugin is re-defining a name which is already available in the outer-scope (previously defined on line 8).

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...
64
            if hasattr(plugin, 'on_teardown'):
65
                plugin.teardown()
66
        nvim = self.nvim
67
        info('uninstall import hook/path')
68
        sys.path.remove(nvim.VIM_SPECIAL_PATH)
69
        sys.path_hooks.remove(self.hook)
70
        info('restore sys.stdout and sys.stderr')
71
        sys.stdout = self.saved_stdout
72
        sys.stderr = self.saved_stderr
73
74 6
    @rpc_export('python_execute', sync=True)
75
    def python_execute(self, script, range_start, range_stop):
76
        """Handle the `python` ex command."""
77
        self._set_current_range(range_start, range_stop)
78
        exec(script, self.module.__dict__)
0 ignored issues
show
Coding Style Security introduced by
The use of exec is discouraged.

Execution of dynamic code might introduce security vulnerabilities. It is generally recommended to use this feature with care and only when necessary.

Loading history...
79
80 6
    @rpc_export('python_execute_file', sync=True)
81
    def python_execute_file(self, file_path, range_start, range_stop):
82
        """Handle the `pyfile` ex command."""
83
        self._set_current_range(range_start, range_stop)
84
        with open(file_path) as f:
85
            script = compile(f.read(), file_path, 'exec')
86
            exec(script, self.module.__dict__)
0 ignored issues
show
Coding Style Security introduced by
The use of exec is discouraged.

Execution of dynamic code might introduce security vulnerabilities. It is generally recommended to use this feature with care and only when necessary.

Loading history...
87
88 6
    @rpc_export('python_do_range', sync=True)
89
    def python_do_range(self, start, stop, code):
90
        """Handle the `pydo` ex command."""
91
        self._set_current_range(start, stop)
92
        nvim = self.nvim
93
        start -= 1
94
        stop -= 1
95
        fname = '_vim_pydo'
96
97
        # define the function
98
        function_def = 'def %s(line, linenr):\n %s' % (fname, code,)
99
        exec(function_def, self.module.__dict__)
0 ignored issues
show
Coding Style Security introduced by
The use of exec is discouraged.

Execution of dynamic code might introduce security vulnerabilities. It is generally recommended to use this feature with care and only when necessary.

Loading history...
100
        # get the function
101
        function = self.module.__dict__[fname]
102
        while start <= stop:
103
            # Process batches of 5000 to avoid the overhead of making multiple
104
            # API calls for every line. Assuming an average line length of 100
105
            # bytes, approximately 488 kilobytes will be transferred per batch,
106
            # which can be done very quickly in a single API call.
107
            sstart = start
108
            sstop = min(start + 5000, stop)
109
            lines = nvim.current.buffer.get_line_slice(sstart, sstop, True,
110
                                                       True)
111
112
            exception = None
113
            newlines = []
114
            linenr = sstart + 1
115
            for i, line in enumerate(lines):
0 ignored issues
show
Unused Code introduced by
The variable i seems to be unused.
Loading history...
116
                result = function(line, linenr)
117
                if result is None:
118
                    # Update earlier lines, and skip to the next
119
                    if newlines:
120
                        end = sstart + len(newlines) - 1
121
                        nvim.current.buffer.set_line_slice(sstart, end,
122
                                                           True, True,
123
                                                           newlines)
124
                    sstart += len(newlines) + 1
125
                    newlines = []
126
                    pass
0 ignored issues
show
Unused Code introduced by
Unnecessary pass statement
Loading history...
127
                elif isinstance(result, basestring):
128
                    newlines.append(result)
129
                else:
130
                    exception = TypeError('pydo should return a string ' +
131
                                          'or None, found %s instead'
132
                                          % result.__class__.__name__)
133
                    break
134
                linenr += 1
135
136
            start = sstop + 1
137
            if newlines:
138
                end = sstart + len(newlines) - 1
139
                nvim.current.buffer.set_line_slice(sstart, end, True, True,
140
                                                   newlines)
141
            if exception:
142
                raise exception
0 ignored issues
show
Bug introduced by
Raising NoneType while only classes or instances are allowed
Loading history...
143
        # delete the function
144
        del self.module.__dict__[fname]
145
146 6
    @rpc_export('python_eval', sync=True)
147
    def python_eval(self, expr):
148
        """Handle the `pyeval` vim function."""
149
        return eval(expr, self.module.__dict__)
0 ignored issues
show
Security Best Practice introduced by
eval should generally only be used if absolutely necessary.

The usage of eval might allow for executing arbitrary code if a user manages to inject dynamic input. Please use this language feature with care and only when you are sure of the input.

Loading history...
150
151 6
    def _set_current_range(self, start, stop):
152
        current = self.legacy_vim.current
153
        current.range = current.buffer.range(start, stop)
154
155
156 6
class RedirectStream(io.IOBase):
157 6
    def __init__(self, redirect_handler):
158
        self.redirect_handler = redirect_handler
159
160 6
    def write(self, data):
161
        self.redirect_handler(data)
162
163 6
    def writelines(self, seq):
164
        self.redirect_handler('\n'.join(seq))
165
166
167 6
class LegacyEvalHook(SessionHook):
168
169
    """Injects legacy `vim.eval` behavior to a Nvim instance."""
170
171 6
    def __init__(self):
172
        super(LegacyEvalHook, self).__init__(from_nvim=self._string_eval)
173
174 6
    def _string_eval(self, obj, session, method, kind):
0 ignored issues
show
Unused Code introduced by
The argument session seems to be unused.
Loading history...
Unused Code introduced by
The argument kind seems to be unused.
Loading history...
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...
175
        if method == 'vim_eval':
176
            if IS_PYTHON3:
177
                if isinstance(obj, (int, float)):
178
                    return str(obj)
179
            elif isinstance(obj, (int, long, float)):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'long'
Loading history...
180
                return str(obj)
181
        return obj
182
183
184
# This was copied/adapted from nvim-python help
185 6
def path_hook(nvim):
186
    def _get_paths():
187
        return discover_runtime_directories(nvim)
188
189
    def _find_module(fullname, oldtail, path):
190
        idx = oldtail.find('.')
191
        if idx > 0:
192
            name = oldtail[:idx]
193
            tail = oldtail[idx + 1:]
194
            fmr = imp.find_module(name, path)
195
            module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr)
196
            return _find_module(fullname, tail, module.__path__)
0 ignored issues
show
Bug introduced by
The Instance of tuple does not seem to have a member named __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...
197
        else:
198
            return imp.find_module(fullname, path)
199
200
    class VimModuleLoader(object):
201
        def __init__(self, module):
202
            self.module = module
203
204
        def load_module(self, fullname, path=None):
0 ignored issues
show
Unused Code introduced by
The argument path seems to be unused.
Loading history...
205
            # Check sys.modules, required for reload (see PEP302).
206
            if fullname in sys.modules:
207
                return sys.modules[fullname]
208
            return imp.load_module(fullname, *self.module)
209
210
    class VimPathFinder(object):
211
        @staticmethod
212
        def find_module(fullname, path=None):
213
            """Method for Python 2.7 and 3.3."""
214
            try:
215
                return VimModuleLoader(
216
                    _find_module(fullname, fullname, path or _get_paths()))
217
            except ImportError:
218
                return None
219
220
        @staticmethod
221
        def find_spec(fullname, path=None, target=None):
222
            """Method for Python 3.4+."""
223
            return PathFinder.find_spec(fullname, path or _get_paths(), target)
224
225
    def hook(path):
226
        if path == nvim.VIM_SPECIAL_PATH:
227
            return VimPathFinder
228
        else:
229
            raise ImportError
230
231
    return hook
232
233
234 6
def discover_runtime_directories(nvim):
235
    rv = []
236
    for path in nvim.list_runtime_paths():
237
        if not os.path.exists(path):
238
            continue
239
        path1 = os.path.join(path, 'pythonx')
240
        if IS_PYTHON3:
241
            path2 = os.path.join(path, 'python3')
242
        else:
243
            path2 = os.path.join(path, 'python2')
244
        if os.path.exists(path1):
245
            rv.append(path1)
246
        if os.path.exists(path2):
247
            rv.append(path2)
248
    return rv
249