Completed
Pull Request — master (#296)
by
unknown
24:26
created

ScriptHost.python_chdir()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.125

Importance

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