Completed
Pull Request — master (#179)
by Björn
21:25 queued 20:00
created

neovim.plugin.path_hook()   B

Complexity

Conditions 6

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42
Metric Value
cc 6
dl 0
loc 47
ccs 0
cts 34
cp 0
crap 42
rs 7.6129

7 Methods

Rating   Name   Duplication   Size   Complexity  
A neovim.plugin.hook() 0 5 2
A neovim.plugin.VimModuleLoader.__init__() 0 2 1
A neovim.plugin.VimModuleLoader.load_module() 0 5 2
A neovim.plugin.VimPathFinder.find_module() 0 8 2
A neovim.plugin.VimPathFinder.find_spec() 0 4 1
A neovim.plugin._find_module() 0 10 2
A neovim.plugin._get_paths() 0 2 1
1
"""Legacy python/python3-vim emulation."""
2
import imp
3
import io
4
import logging
5
import os
6
import sys
7
8
import neovim
9
10
__all__ = ('ScriptHost',)
11
12
13
logger = logging.getLogger(__name__)
14
debug, info, warn = (logger.debug, logger.info, logger.warn,)
15
16
IS_PYTHON3 = sys.version_info >= (3, 0)
17
18
if IS_PYTHON3:
19
    basestring = str
20
21
    if sys.version_info >= (3, 4):
22
        from importlib.machinery import PathFinder
23
24
25
@neovim.plugin
26
class ScriptHost(object):
27
28
    """Provides an environment for running python plugins created for Vim."""
29
30
    def __init__(self, nvim):
31
        """Initialize the legacy python-vim environment."""
32
        self.setup(nvim)
33
        # context where all code will run
34
        self.module = imp.new_module('__main__')
35
        nvim.script_context = self.module
36
        # it seems some plugins assume 'sys' is already imported, so do it now
37
        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...
38
        self.legacy_vim = LegacyVim.from_nvim(nvim)
39
        sys.modules['vim'] = self.legacy_vim
40
41
    def setup(self, nvim):
42
        """Setup import hooks and global streams.
43
44
        This will add import hooks for importing modules from runtime
45
        directories and patch the sys module so 'print' calls will be
46
        forwarded to Nvim.
47
        """
48
        self.nvim = nvim
49
        info('install import hook/path')
50
        self.hook = path_hook(nvim)
51
        sys.path_hooks.append(self.hook)
52
        nvim.VIM_SPECIAL_PATH = '_vim_path_'
53
        sys.path.append(nvim.VIM_SPECIAL_PATH)
54
        info('redirect sys.stdout and sys.stderr')
55
        self.saved_stdout = sys.stdout
56
        self.saved_stderr = sys.stderr
57
        sys.stdout = RedirectStream(lambda data: nvim.out_write(data))
0 ignored issues
show
Unused Code introduced by
This lambda might be unnecessary.
Loading history...
58
        sys.stderr = RedirectStream(lambda data: nvim.err_write(data))
0 ignored issues
show
Unused Code introduced by
This lambda might be unnecessary.
Loading history...
59
60
    def teardown(self):
61
        """Restore state modified from the `setup` call."""
62
        for plugin in self.installed_plugins:
0 ignored issues
show
Bug introduced by
The Instance of ScriptHost does not seem to have a member named installed_plugins.

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