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

ScriptHost.python_chdir()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

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