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
![]() |
|||
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 |