diff --git a/doc/pymode.txt b/doc/pymode.txt index 0962cd51..02f21a44 100644 --- a/doc/pymode.txt +++ b/doc/pymode.txt @@ -268,6 +268,9 @@ Set path to virtualenv manually *'g:pymode_virtualenv_path' Commands: *:PymodeRun* -- Run current buffer or selection +When a virtualenv is activated with |:PymodeVirtualenv|, *:PymodeRun* uses the +virtualenv's Python executable. + Turn on the run code script *'g:pymode_run'* > let g:pymode_run = 1 diff --git a/plugin/pymode.vim b/plugin/pymode.vim index 517f0af3..52371e02 100644 --- a/plugin/pymode.vim +++ b/plugin/pymode.vim @@ -99,6 +99,9 @@ call pymode#default('g:pymode_run', 1) " Key's map for run python code call pymode#default('g:pymode_run_bind', 'r') +" Timeout in seconds for :PymodeRun when using a virtualenv interpreter (0 = no limit) +call pymode#default('g:pymode_run_timeout', 0) + " }}} " CHECK CODE {{{ diff --git a/pymode/run.py b/pymode/run.py index bb83fa2c..18414c29 100644 --- a/pymode/run.py +++ b/pymode/run.py @@ -1,5 +1,8 @@ """ Code runnning support. """ +import os +import subprocess import sys +import tempfile from io import StringIO from re import compile as re @@ -25,39 +28,51 @@ def run_code(): elif encoding.match(lines[1]): lines.pop(1) - context = dict( - __name__='__main__', - __file__=env.var('expand("%:p")'), - input=env.user_input, - raw_input=env.user_input) + real_file = env.var('expand("%:p")') + python_cmd = __get_virtualenv_python() + if python_cmd: + result = __run_with_external_python(python_cmd, lines, real_file) + if result is not None: + output, err = result + else: + python_cmd = None + + if not python_cmd: + context = dict( + __name__='__main__', + __file__=real_file, + input=env.user_input, + raw_input=env.user_input) + + sys.stdout, stdout_ = StringIO(), sys.stdout + sys.stderr, stderr_ = StringIO(), sys.stderr + + try: + code = compile('\n'.join(lines) + '\n', env.curbuf.name, 'exec') + sys.path.insert(0, env.curdir) + exec(code, context) # noqa + sys.path.pop(0) + + except SystemExit as e: + if e.code: + # A non-false code indicates abnormal termination. + # A false code will be treated as a + # successful run, and the error will be hidden from Vim + sys.stdout, sys.stderr = stdout_, stderr_ + env.error("Script exited with code %s" % e.code) + return env.stop() + + except Exception: + import traceback + err = traceback.format_exc() + + else: + err = sys.stderr.getvalue() + + output = sys.stdout.getvalue() + sys.stdout, sys.stderr = stdout_, stderr_ - sys.stdout, stdout_ = StringIO(), sys.stdout - sys.stderr, stderr_ = StringIO(), sys.stderr - - try: - code = compile('\n'.join(lines) + '\n', env.curbuf.name, 'exec') - sys.path.insert(0, env.curdir) - exec(code, context) # noqa - sys.path.pop(0) - - except SystemExit as e: - if e.code: - # A non-false code indicates abnormal termination. - # A false code will be treated as a - # successful run, and the error will be hidden from Vim - env.error("Script exited with code %s" % e.code) - return env.stop() - - except Exception: - import traceback - err = traceback.format_exc() - - else: - err = sys.stderr.getvalue() - - output = sys.stdout.getvalue() output = env.prepare_value(output, dumps=False) - sys.stdout, sys.stderr = stdout_, stderr_ errors += [er for er in err.splitlines() if er and "" not in er] @@ -65,6 +80,66 @@ def run_code(): env.let('l:output', [s for s in output.splitlines()]) +def __get_virtualenv_python(): + path = env.var('g:pymode_virtualenv_enabled', silence=True, default='') + if not path: + return None + + if os.name == 'nt': + candidates = [ + os.path.join(path, 'Scripts', 'python.exe'), + os.path.join(path, 'Scripts', 'python'), + ] + else: + candidates = [os.path.join(path, 'bin', 'python')] + + for candidate in candidates: + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + + return None + + +def __run_with_external_python(python_cmd, lines, real_file): + # Inject __file__ so user code sees the real source path, not the temp file + header = '__file__ = %r\n' % real_file + source = header + '\n'.join(lines) + '\n' + temp_file_path = None + try: + with tempfile.NamedTemporaryFile( + mode='w', suffix='.py', delete=False, encoding='utf-8' + ) as temp_file: + temp_file.write(source) + temp_file_path = temp_file.name + + timeout = env.var('g:pymode_run_timeout', silence=True, default=0) or None + process = subprocess.Popen( + [python_cmd, temp_file_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=env.curdir, + text=True, + encoding='utf-8') + try: + output, err = process.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + process.kill() + output, err = process.communicate() + err += '\nProcess timed out after %s second(s).' % timeout + except OSError as exc: + env.debug('Failed to execute external python', python_cmd, exc) + return None + finally: + if temp_file_path and os.path.exists(temp_file_path): + os.unlink(temp_file_path) + + # Replace temp path in tracebacks so errors reference the real source file + if temp_file_path: + err = err.replace(temp_file_path, real_file) + + return output, err + + def __prepare_lines(line1, line2): lines = [l.rstrip() for l in env.lines[int(line1) - 1:int(line2)]] diff --git a/tests/vader/commands.vader b/tests/vader/commands.vader index d7a9c3d8..d4b9a25f 100644 --- a/tests/vader/commands.vader +++ b/tests/vader/commands.vader @@ -86,6 +86,45 @@ Execute (Test PymodeRun command): Assert 1, 'PymodeRun executed without producing a run buffer' endif +Execute (Test PymodeRun uses active virtualenv python): + if exists(':PymodeRun') && exists(':PymodeVirtualenv') + let pycmd = executable('python3') ? 'python3' : (executable('python') ? 'python' : '') + if !empty(pycmd) + let temp_dir = tempname() + call mkdir(temp_dir, 'p') + let venv_dir = temp_dir . '/.venv' + let sample_file = temp_dir . '/run_from_venv.py' + let saved_venv_enabled = get(g:, 'pymode_virtualenv_enabled', '') + let saved_venv = get(g:, 'pymode_virtualenv', 0) + + call writefile(['import sys', 'print(sys.executable)'], sample_file) + + call system(pycmd . ' -m venv ' . shellescape(venv_dir)) + if v:shell_error == 0 + execute 'edit ' . fnameescape(sample_file) + let g:pymode_virtualenv = 1 + execute 'PymodeVirtualenv ' . fnameescape(venv_dir) + PymodeRun + + let run_buffer = bufnr('__run__') + if run_buffer != -1 + execute 'buffer ' . run_buffer + let run_output = substitute(join(getline(1, '$'), "\n"), '\\', '/', 'g') + let expected_python = has('win32') || has('win64') ? venv_dir . '/Scripts/python.exe' : venv_dir . '/bin/python' + let expected_python = substitute(expected_python, '\\', '/', 'g') + Assert stridx(run_output, expected_python) >= 0, 'PymodeRun should execute with virtualenv python executable' + else + Assert 0, 'PymodeRun should create run buffer when using virtualenv' + endif + endif + + let g:pymode_virtualenv_enabled = saved_venv_enabled + let g:pymode_virtualenv = saved_venv + call delete(sample_file) + call delete(temp_dir, 'rf') + endif + endif + # Test PymodeLint command Given python (Python code with lint issues): import math, sys; @@ -275,4 +314,4 @@ Execute (Test PymodeRun with pymoderun_sample.py): endif else Assert 1, 'PymodeRun command not available - test skipped' - endif \ No newline at end of file + endif