Completed
Push — master ( 1047bd...4dbfbd )
by Björn
10s
created

ScriptHost.python_eval()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.125

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
dl 0
loc 4
ccs 1
cts 2
cp 0.5
crap 1.125
rs 10
c 1
b 0
f 0
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 6
from ..msgpack_rpc import ErrorResponse
11 6
from ..util import format_exc_skip
12
13 6
__all__ = ('ScriptHost',)
14
15
16 6
logger = logging.getLogger(__name__)
17 6
debug, info, warn = (logger.debug, logger.info, logger.warn,)
18
19 6
IS_PYTHON3 = sys.version_info >= (3, 0)
20
21 6
if IS_PYTHON3:
22 3
    basestring = str
23
24 3
    if sys.version_info >= (3, 4):
25 2
        from importlib.machinery import PathFinder
26
27
28 6
@plugin
29 6
class ScriptHost(object):
30
31
    """Provides an environment for running python plugins created for Vim."""
32
33 6
    def __init__(self, nvim):
34
        """Initialize the legacy python-vim environment."""
35
        self.setup(nvim)
36
        # context where all code will run
37
        self.module = imp.new_module('__main__')
38
        nvim.script_context = self.module
39
        # it seems some plugins assume 'sys' is already imported, so do it now
40
        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...
41
        self.legacy_vim = LegacyVim.from_nvim(nvim)
42
        sys.modules['vim'] = self.legacy_vim
43
44 6
    def setup(self, nvim):
45
        """Setup import hooks and global streams.
46
47
        This will add import hooks for importing modules from runtime
48
        directories and patch the sys module so 'print' calls will be
49
        forwarded to Nvim.
50
        """
51
        self.nvim = nvim
52
        info('install import hook/path')
53
        self.hook = path_hook(nvim)
54
        sys.path_hooks.append(self.hook)
55
        nvim.VIM_SPECIAL_PATH = '_vim_path_'
56
        sys.path.append(nvim.VIM_SPECIAL_PATH)
57
        info('redirect sys.stdout and sys.stderr')
58
        self.saved_stdout = sys.stdout
59
        self.saved_stderr = sys.stderr
60
        sys.stdout = RedirectStream(lambda data: nvim.out_write(data))
0 ignored issues
show
Unused Code introduced by
This lambda might be unnecessary.
Loading history...
61
        sys.stderr = RedirectStream(lambda data: nvim.err_write(data))
0 ignored issues
show
Unused Code introduced by
This lambda might be unnecessary.
Loading history...
62
63 6
    def teardown(self):
64
        """Restore state modified from the `setup` call."""
65
        nvim = self.nvim
66
        info('uninstall import hook/path')
67
        sys.path.remove(nvim.VIM_SPECIAL_PATH)
68
        sys.path_hooks.remove(self.hook)
69
        info('restore sys.stdout and sys.stderr')
70
        sys.stdout = self.saved_stdout
71
        sys.stderr = self.saved_stderr
72
73 6
    @rpc_export('python_execute', sync=True)
74
    def python_execute(self, script, range_start, range_stop):
75
        """Handle the `python` ex command."""
76
        self._set_current_range(range_start, range_stop)
77
        try:
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
        except Exception:
80
            raise ErrorResponse(format_exc_skip(1))
81
82 6
    @rpc_export('python_execute_file', sync=True)
83
    def python_execute_file(self, file_path, range_start, range_stop):
84
        """Handle the `pyfile` ex command."""
85
        self._set_current_range(range_start, range_stop)
86
        with open(file_path) as f:
87
            script = compile(f.read(), file_path, 'exec')
88
            try:
89
                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...
90
            except Exception:
91
                raise ErrorResponse(format_exc_skip(1))
92
93 6
    @rpc_export('python_do_range', sync=True)
94
    def python_do_range(self, start, stop, code):
95
        """Handle the `pydo` ex command."""
96
        self._set_current_range(start, stop)
97
        nvim = self.nvim
98
        start -= 1
99
        fname = '_vim_pydo'
100
101
        # define the function
102
        function_def = 'def %s(line, linenr):\n %s' % (fname, code,)
103
        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...
104
        # get the function
105
        function = self.module.__dict__[fname]
106
        while start < stop:
107
            # Process batches of 5000 to avoid the overhead of making multiple
108
            # API calls for every line. Assuming an average line length of 100
109
            # bytes, approximately 488 kilobytes will be transferred per batch,
110
            # which can be done very quickly in a single API call.
111
            sstart = start
112
            sstop = min(start + 5000, stop)
113
            lines = nvim.current.buffer.api.get_lines(sstart, sstop, True)
114
115
            exception = None
116
            newlines = []
117
            linenr = sstart + 1
118
            for i, line in enumerate(lines):
0 ignored issues
show
Unused Code introduced by
The variable i seems to be unused.
Loading history...
119
                result = function(line, linenr)
120
                if result is None:
121
                    # Update earlier lines, and skip to the next
122
                    if newlines:
123
                        end = sstart + len(newlines) - 1
124
                        nvim.current.buffer.api.set_lines(sstart, end,
125
                                                          True, newlines)
126
                    sstart += len(newlines) + 1
127
                    newlines = []
128
                    pass
0 ignored issues
show
Unused Code introduced by
Unnecessary pass statement
Loading history...
129
                elif isinstance(result, basestring):
130
                    newlines.append(result)
131
                else:
132
                    exception = TypeError('pydo should return a string ' +
133
                                          'or None, found %s instead'
134
                                          % result.__class__.__name__)
135
                    break
136
                linenr += 1
137
138
            start = sstop
139
            if newlines:
140
                end = sstart + len(newlines)
141
                nvim.current.buffer.api.set_lines(sstart, end, True, newlines)
142
            if exception:
143
                raise exception
0 ignored issues
show
Bug introduced by
Raising NoneType while only classes or instances are allowed
Loading history...
144
        # delete the function
145
        del self.module.__dict__[fname]
146
147 6
    @rpc_export('python_eval', sync=True)
148
    def python_eval(self, expr):
149
        """Handle the `pyeval` vim function."""
150
        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...
151
152 6
    def _set_current_range(self, start, stop):
153
        current = self.legacy_vim.current
154
        current.range = current.buffer.range(start, stop)
155
156
157 6
class RedirectStream(io.IOBase):
158 6
    def __init__(self, redirect_handler):
159
        self.redirect_handler = redirect_handler
160
161 6
    def write(self, data):
162
        self.redirect_handler(data)
163
164 6
    def writelines(self, seq):
165
        self.redirect_handler('\n'.join(seq))
166
167
168 6
if IS_PYTHON3:
169 3
    num_types = (int, float)
170
else:
171 3
    num_types = (int, long, float)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'long'
Loading history...
172
173
174 6
def num_to_str(obj):
175
    if isinstance(obj, num_types):
176
        return str(obj)
177
    else:
178
        return obj
179
180
181 6
class LegacyVim(Nvim):
182 6
    def eval(self, expr):
183
        obj = self.request("vim_eval", expr)
184
        return walk(num_to_str, obj)
185
186
187
# This was copied/adapted from nvim-python help
188 6
def path_hook(nvim):
189
    def _get_paths():
190
        return discover_runtime_directories(nvim)
191
192
    def _find_module(fullname, oldtail, path):
193
        idx = oldtail.find('.')
194
        if idx > 0:
195
            name = oldtail[:idx]
196
            tail = oldtail[idx + 1:]
197
            fmr = imp.find_module(name, path)
198
            module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr)
199
            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...
200
        else:
201
            return imp.find_module(fullname, path)
202
203
    class VimModuleLoader(object):
204
        def __init__(self, module):
205
            self.module = module
206
207
        def load_module(self, fullname, path=None):
0 ignored issues
show
Unused Code introduced by
The argument path seems to be unused.
Loading history...
208
            # Check sys.modules, required for reload (see PEP302).
209
            if fullname in sys.modules:
210
                return sys.modules[fullname]
211
            return imp.load_module(fullname, *self.module)
212
213
    class VimPathFinder(object):
214
        @staticmethod
215
        def find_module(fullname, path=None):
216
            """Method for Python 2.7 and 3.3."""
217
            try:
218
                return VimModuleLoader(
219
                    _find_module(fullname, fullname, path or _get_paths()))
220
            except ImportError:
221
                return None
222
223
        @staticmethod
224
        def find_spec(fullname, path=None, target=None):
225
            """Method for Python 3.4+."""
226
            return PathFinder.find_spec(fullname, path or _get_paths(), target)
227
228
    def hook(path):
229
        if path == nvim.VIM_SPECIAL_PATH:
230
            return VimPathFinder
231
        else:
232
            raise ImportError
233
234
    return hook
235
236
237 6
def discover_runtime_directories(nvim):
238
    rv = []
239
    for path in nvim.list_runtime_paths():
240
        if not os.path.exists(path):
241
            continue
242
        path1 = os.path.join(path, 'pythonx')
243
        if IS_PYTHON3:
244
            path2 = os.path.join(path, 'python3')
245
        else:
246
            path2 = os.path.join(path, 'python2')
247
        if os.path.exists(path1):
248
            rv.append(path1)
249
        if os.path.exists(path2):
250
            rv.append(path2)
251
    return rv
252