Issues (1)

pynvim/plugin/script_host.py (1 issue)

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