Completed
Pull Request — master (#195)
by Björn
04:22
created

neovim.plugin.LegacyVim   A

Complexity

Total Complexity 1

Size/Duplication

Total Lines 4
Duplicated Lines 0 %

Test Coverage

Coverage 50%
Metric Value
dl 0
loc 4
ccs 2
cts 4
cp 0.5
rs 10
wmc 1

1 Method

Rating   Name   Duplication   Size   Complexity  
A eval() 0 3 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 plugin, rpc_export
9 6
from ..api import Nvim, walk
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 = LegacyVim.from_nvim(nvim)
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
        nvim = self.nvim
64
        info('uninstall import hook/path')
65
        sys.path.remove(nvim.VIM_SPECIAL_PATH)
66
        sys.path_hooks.remove(self.hook)
67
        info('restore sys.stdout and sys.stderr')
68
        sys.stdout = self.saved_stdout
69
        sys.stderr = self.saved_stderr
70
71 6
    @rpc_export('python_execute', sync=True)
72
    def python_execute(self, script, range_start, range_stop):
73
        """Handle the `python` ex command."""
74
        self._set_current_range(range_start, range_stop)
75
        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...
76
77 6
    @rpc_export('python_execute_file', sync=True)
78
    def python_execute_file(self, file_path, range_start, range_stop):
79
        """Handle the `pyfile` ex command."""
80
        self._set_current_range(range_start, range_stop)
81
        with open(file_path) as f:
82
            script = compile(f.read(), file_path, 'exec')
83
            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...
84
85 6
    @rpc_export('python_do_range', sync=True)
86
    def python_do_range(self, start, stop, code):
87
        """Handle the `pydo` ex command."""
88
        self._set_current_range(start, stop)
89
        nvim = self.nvim
90
        start -= 1
91
        stop -= 1
92
        fname = '_vim_pydo'
93
94
        # define the function
95
        function_def = 'def %s(line, linenr):\n %s' % (fname, code,)
96
        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...
97
        # get the function
98
        function = self.module.__dict__[fname]
99
        while start <= stop:
100
            # Process batches of 5000 to avoid the overhead of making multiple
101
            # API calls for every line. Assuming an average line length of 100
102
            # bytes, approximately 488 kilobytes will be transferred per batch,
103
            # which can be done very quickly in a single API call.
104
            sstart = start
105
            sstop = min(start + 5000, stop)
106
            lines = nvim.current.buffer.get_line_slice(sstart, sstop, True,
107
                                                       True)
108
109
            exception = None
110
            newlines = []
111
            linenr = sstart + 1
112
            for i, line in enumerate(lines):
0 ignored issues
show
Unused Code introduced by
The variable i seems to be unused.
Loading history...
113
                result = function(line, linenr)
114
                if result is None:
115
                    # Update earlier lines, and skip to the next
116
                    if newlines:
117
                        end = sstart + len(newlines) - 1
118
                        nvim.current.buffer.set_line_slice(sstart, end,
119
                                                           True, True,
120
                                                           newlines)
121
                    sstart += len(newlines) + 1
122
                    newlines = []
123
                    pass
0 ignored issues
show
Unused Code introduced by
Unnecessary pass statement
Loading history...
124
                elif isinstance(result, basestring):
125
                    newlines.append(result)
126
                else:
127
                    exception = TypeError('pydo should return a string ' +
128
                                          'or None, found %s instead'
129
                                          % result.__class__.__name__)
130
                    break
131
                linenr += 1
132
133
            start = sstop + 1
134
            if newlines:
135
                end = sstart + len(newlines) - 1
136
                nvim.current.buffer.set_line_slice(sstart, end, True, True,
137
                                                   newlines)
138
            if exception:
139
                raise exception
0 ignored issues
show
Bug introduced by
Raising NoneType while only classes or instances are allowed
Loading history...
140
        # delete the function
141
        del self.module.__dict__[fname]
142
143 6
    @rpc_export('python_eval', sync=True)
144
    def python_eval(self, expr):
145
        """Handle the `pyeval` vim function."""
146
        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...
147
148 6
    def _set_current_range(self, start, stop):
149
        current = self.legacy_vim.current
150
        current.range = current.buffer.range(start, stop)
151
152
153 6
class RedirectStream(io.IOBase):
154 6
    def __init__(self, redirect_handler):
155
        self.redirect_handler = redirect_handler
156
157 6
    def write(self, data):
158
        self.redirect_handler(data)
159
160 6
    def writelines(self, seq):
161
        self.redirect_handler('\n'.join(seq))
162
163
164 6
if IS_PYTHON3:
165 3
    num_types = (int, float)
166
else:
167 3
    num_types = (int, long, float)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'long'
Loading history...
168
169 6
def num_to_str(obj):
170
    if isinstance(obj, num_types):
171
        return str(obj)
172
173 6
class LegacyVim(Nvim):
174 6
    def eval(self, expr):
175
        obj = self.request("vim_eval", expr)
176
        return walk(num_to_str, obj)
177
178
179
# This was copied/adapted from nvim-python help
180 6
def path_hook(nvim):
181
    def _get_paths():
182
        return discover_runtime_directories(nvim)
183
184
    def _find_module(fullname, oldtail, path):
185
        idx = oldtail.find('.')
186
        if idx > 0:
187
            name = oldtail[:idx]
188
            tail = oldtail[idx + 1:]
189
            fmr = imp.find_module(name, path)
190
            module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr)
191
            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...
192
        else:
193
            return imp.find_module(fullname, path)
194
195
    class VimModuleLoader(object):
196
        def __init__(self, module):
197
            self.module = module
198
199
        def load_module(self, fullname, path=None):
0 ignored issues
show
Unused Code introduced by
The argument path seems to be unused.
Loading history...
200
            # Check sys.modules, required for reload (see PEP302).
201
            if fullname in sys.modules:
202
                return sys.modules[fullname]
203
            return imp.load_module(fullname, *self.module)
204
205
    class VimPathFinder(object):
206
        @staticmethod
207
        def find_module(fullname, path=None):
208
            """Method for Python 2.7 and 3.3."""
209
            try:
210
                return VimModuleLoader(
211
                    _find_module(fullname, fullname, path or _get_paths()))
212
            except ImportError:
213
                return None
214
215
        @staticmethod
216
        def find_spec(fullname, path=None, target=None):
217
            """Method for Python 3.4+."""
218
            return PathFinder.find_spec(fullname, path or _get_paths(), target)
219
220
    def hook(path):
221
        if path == nvim.VIM_SPECIAL_PATH:
222
            return VimPathFinder
223
        else:
224
            raise ImportError
225
226
    return hook
227
228
229 6
def discover_runtime_directories(nvim):
230
    rv = []
231
    for path in nvim.list_runtime_paths():
232
        if not os.path.exists(path):
233
            continue
234
        path1 = os.path.join(path, 'pythonx')
235
        if IS_PYTHON3:
236
            path2 = os.path.join(path, 'python3')
237
        else:
238
            path2 = os.path.join(path, 'python2')
239
        if os.path.exists(path1):
240
            rv.append(path1)
241
        if os.path.exists(path2):
242
            rv.append(path2)
243
    return rv
244