Completed
Pull Request — master (#296)
by Justin M.
43:20 queued 18:21
created

ScriptHost.python_execute_file()   A

Complexity

Conditions 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 7.608

Importance

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