Completed
Pull Request — master (#190)
by Björn
02:21
created

neovim.plugin.ScriptHost.__init__()   A

Complexity

Conditions 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.6296
Metric Value
cc 1
dl 0
loc 10
ccs 1
cts 7
cp 0.1429
crap 1.6296
rs 9.4285
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
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
        fname = '_vim_pydo'
92
93
        # define the function
94
        function_def = 'def %s(line, linenr):\n %s' % (fname, code,)
95
        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...
96
        # get the function
97
        function = self.module.__dict__[fname]
98
        while start < stop:
99
            # Process batches of 5000 to avoid the overhead of making multiple
100
            # API calls for every line. Assuming an average line length of 100
101
            # bytes, approximately 488 kilobytes will be transferred per batch,
102
            # which can be done very quickly in a single API call.
103
            sstart = start
104
            sstop = min(start + 5000, stop)
105
            lines = nvim.current.buffer.api.get_lines(sstart, sstop, True)
106
107
            exception = None
108
            newlines = []
109
            linenr = sstart + 1
110
            for i, line in enumerate(lines):
0 ignored issues
show
Unused Code introduced by
The variable i seems to be unused.
Loading history...
111
                result = function(line, linenr)
112
                if result is None:
113
                    # Update earlier lines, and skip to the next
114
                    if newlines:
115
                        end = sstart + len(newlines) - 1
116
                        nvim.current.buffer.api.set_lines(sstart, end,
117
                                                      True, newlines)
118
                    sstart += len(newlines) + 1
119
                    newlines = []
120
                    pass
0 ignored issues
show
Unused Code introduced by
Unnecessary pass statement
Loading history...
121
                elif isinstance(result, basestring):
122
                    newlines.append(result)
123
                else:
124
                    exception = TypeError('pydo should return a string ' +
125
                                          'or None, found %s instead'
126
                                          % result.__class__.__name__)
127
                    break
128
                linenr += 1
129
130
            start = sstop
131
            if newlines:
132
                end = sstart + len(newlines)
133
                nvim.current.buffer.api.set_lines(sstart, end, True, newlines)
134
            if exception:
135
                raise exception
0 ignored issues
show
Bug introduced by
Raising NoneType while only classes or instances are allowed
Loading history...
136
        # delete the function
137
        del self.module.__dict__[fname]
138
139 6
    @rpc_export('python_eval', sync=True)
140
    def python_eval(self, expr):
141
        """Handle the `pyeval` vim function."""
142
        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...
143
144 6
    def _set_current_range(self, start, stop):
145
        current = self.legacy_vim.current
146
        current.range = current.buffer.range(start, stop)
147
148
149 6
class RedirectStream(io.IOBase):
150 6
    def __init__(self, redirect_handler):
151
        self.redirect_handler = redirect_handler
152
153 6
    def write(self, data):
154
        self.redirect_handler(data)
155
156 6
    def writelines(self, seq):
157
        self.redirect_handler('\n'.join(seq))
158
159
160 6
class LegacyVim(Nvim):
161 6
    def eval(self, expr):
0 ignored issues
show
Bug introduced by
Arguments number differs from overridden 'eval' method
Loading history...
162
        obj = self.request("vim_eval", expr)
163
        if IS_PYTHON3:
164
            if isinstance(obj, (int, float)):
165
                return str(obj)
166
        elif isinstance(obj, (int, long, float)):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'long'
Loading history...
167
            return str(obj)
168
        return obj
169
170
171
# This was copied/adapted from nvim-python help
172 6
def path_hook(nvim):
173
    def _get_paths():
174
        return discover_runtime_directories(nvim)
175
176
    def _find_module(fullname, oldtail, path):
177
        idx = oldtail.find('.')
178
        if idx > 0:
179
            name = oldtail[:idx]
180
            tail = oldtail[idx + 1:]
181
            fmr = imp.find_module(name, path)
182
            module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr)
183
            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...
184
        else:
185
            return imp.find_module(fullname, path)
186
187
    class VimModuleLoader(object):
188
        def __init__(self, module):
189
            self.module = module
190
191
        def load_module(self, fullname, path=None):
0 ignored issues
show
Unused Code introduced by
The argument path seems to be unused.
Loading history...
192
            # Check sys.modules, required for reload (see PEP302).
193
            if fullname in sys.modules:
194
                return sys.modules[fullname]
195
            return imp.load_module(fullname, *self.module)
196
197
    class VimPathFinder(object):
198
        @staticmethod
199
        def find_module(fullname, path=None):
200
            """Method for Python 2.7 and 3.3."""
201
            try:
202
                return VimModuleLoader(
203
                    _find_module(fullname, fullname, path or _get_paths()))
204
            except ImportError:
205
                return None
206
207
        @staticmethod
208
        def find_spec(fullname, path=None, target=None):
209
            """Method for Python 3.4+."""
210
            return PathFinder.find_spec(fullname, path or _get_paths(), target)
211
212
    def hook(path):
213
        if path == nvim.VIM_SPECIAL_PATH:
214
            return VimPathFinder
215
        else:
216
            raise ImportError
217
218
    return hook
219
220
221 6
def discover_runtime_directories(nvim):
222
    rv = []
223
    for path in nvim.list_runtime_paths():
224
        if not os.path.exists(path):
225
            continue
226
        path1 = os.path.join(path, 'pythonx')
227
        if IS_PYTHON3:
228
            path2 = os.path.join(path, 'python3')
229
        else:
230
            path2 = os.path.join(path, 'python2')
231
        if os.path.exists(path1):
232
            rv.append(path1)
233
        if os.path.exists(path2):
234
            rv.append(path2)
235
    return rv
236