From b875597b7e0d3c2aae9f5b62ea50a830bb741fff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:37:20 +0000 Subject: [PATCH 1/5] Initial plan From 593ac513974326fa8959ad46b7e54187b50d2394 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:44:14 +0000 Subject: [PATCH 2/5] fix: run PymodeRun with activated virtualenv interpreter Agent-Logs-Url: https://github.com/python-mode/python-mode/sessions/990a6238-68c1-475c-b384-5afbd8f07a7f Co-authored-by: diraol <192702+diraol@users.noreply.github.com> --- doc/pymode.txt | 3 + pymode/run.py | 121 +++++++++++++++++++++++++++---------- tests/vader/commands.vader | 42 ++++++++++++- 3 files changed, 134 insertions(+), 32 deletions(-) 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/pymode/run.py b/pymode/run.py index bb83fa2c..d7b41d0c 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,49 @@ 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) + python_cmd = __get_virtualenv_python() + if python_cmd: + result = __run_with_external_python(python_cmd, lines) + if result is not None: + output, err = result + else: + python_cmd = None + + if not python_cmd: + context = dict( + __name__='__main__', + __file__=env.var('expand("%:p")'), + 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 + 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 +78,52 @@ 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): + source = '\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 + + process = subprocess.Popen( + [python_cmd, temp_file_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=env.curdir, + universal_newlines=True) + output, err = process.communicate() + except OSError: + return None + finally: + if temp_file_path and os.path.exists(temp_file_path): + os.unlink(temp_file_path) + + 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..e423bc35 100644 --- a/tests/vader/commands.vader +++ b/tests/vader/commands.vader @@ -86,6 +86,46 @@ 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' + + 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 ' . string(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' + 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 + else + Assert 1, 'Unable to create test virtualenv in this environment - test skipped' + endif + + call delete(sample_file) + call delete(temp_dir, 'rf') + else + Assert 1, 'No python executable available to create virtualenv - test skipped' + endif + else + Assert 1, 'PymodeRun/PymodeVirtualenv command not available - test skipped' + endif + # Test PymodeLint command Given python (Python code with lint issues): import math, sys; @@ -275,4 +315,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 From 09e2f234a444a2c287d6f6b36d391520bb9742e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:46:08 +0000 Subject: [PATCH 3/5] test: normalize expected virtualenv path in run command test Agent-Logs-Url: https://github.com/python-mode/python-mode/sessions/990a6238-68c1-475c-b384-5afbd8f07a7f Co-authored-by: diraol <192702+diraol@users.noreply.github.com> --- tests/vader/commands.vader | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/vader/commands.vader b/tests/vader/commands.vader index e423bc35..e4bb74a2 100644 --- a/tests/vader/commands.vader +++ b/tests/vader/commands.vader @@ -109,6 +109,7 @@ Execute (Test PymodeRun uses active virtualenv python): 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' From 892a3626d06d1f7c1f4fb4a8924ea6b110e9977a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:47:00 +0000 Subject: [PATCH 4/5] chore: improve subprocess run compatibility and debug logging Agent-Logs-Url: https://github.com/python-mode/python-mode/sessions/990a6238-68c1-475c-b384-5afbd8f07a7f Co-authored-by: diraol <192702+diraol@users.noreply.github.com> --- pymode/run.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pymode/run.py b/pymode/run.py index d7b41d0c..f1e64859 100644 --- a/pymode/run.py +++ b/pymode/run.py @@ -113,9 +113,10 @@ def __run_with_external_python(python_cmd, lines): stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=env.curdir, - universal_newlines=True) + text=True) output, err = process.communicate() - except OSError: + 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): From 2aec97cea6733747af776c139a269f61c9bd485d Mon Sep 17 00:00:00 2001 From: Diego Rabatone Oliveira Date: Sat, 23 May 2026 22:39:38 -0300 Subject: [PATCH 5/5] Fix review issues in virtualenv PymodeRun integration - Set __file__ correctly in subprocess so user code referencing it sees the real source path instead of a temp file path - Rewrite temp file paths in subprocess tracebacks to the real source file - Add explicit UTF-8 encoding to subprocess Popen call - Add g:pymode_run_timeout support with TimeoutExpired handling to prevent Vim from hanging on non-terminating scripts - Restore sys.stdout/sys.stderr before returning on SystemExit with non-false exit code (pre-existing leak) - Fix test: use fnameescape() instead of string() for PymodeVirtualenv argument (string() wraps in quotes, producing a malformed path) - Save and restore g:pymode_virtualenv_enabled/g:pymode_virtualenv in the test to avoid polluting subsequent tests - Remove misleading Assert 1 "skip" patterns; drop unreachable else branches so cleanup runs unconditionally Co-Authored-By: Claude Sonnet 4.6 (1M context) --- plugin/pymode.vim | 3 +++ pymode/run.py | 27 +++++++++++++++++++++------ tests/vader/commands.vader | 12 +++++------- 3 files changed, 29 insertions(+), 13 deletions(-) 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 f1e64859..18414c29 100644 --- a/pymode/run.py +++ b/pymode/run.py @@ -28,9 +28,10 @@ def run_code(): elif encoding.match(lines[1]): lines.pop(1) + real_file = env.var('expand("%:p")') python_cmd = __get_virtualenv_python() if python_cmd: - result = __run_with_external_python(python_cmd, lines) + result = __run_with_external_python(python_cmd, lines, real_file) if result is not None: output, err = result else: @@ -39,7 +40,7 @@ def run_code(): if not python_cmd: context = dict( __name__='__main__', - __file__=env.var('expand("%:p")'), + __file__=real_file, input=env.user_input, raw_input=env.user_input) @@ -57,6 +58,7 @@ def run_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() @@ -98,8 +100,10 @@ def __get_virtualenv_python(): return None -def __run_with_external_python(python_cmd, lines): - source = '\n'.join(lines) + '\n' +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( @@ -108,13 +112,20 @@ def __run_with_external_python(python_cmd, lines): 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) - output, err = process.communicate() + 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 @@ -122,6 +133,10 @@ def __run_with_external_python(python_cmd, lines): 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 diff --git a/tests/vader/commands.vader b/tests/vader/commands.vader index e4bb74a2..d4b9a25f 100644 --- a/tests/vader/commands.vader +++ b/tests/vader/commands.vader @@ -94,6 +94,8 @@ Execute (Test PymodeRun uses active virtualenv python): 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) @@ -101,7 +103,7 @@ Execute (Test PymodeRun uses active virtualenv python): if v:shell_error == 0 execute 'edit ' . fnameescape(sample_file) let g:pymode_virtualenv = 1 - execute 'PymodeVirtualenv ' . string(venv_dir) + execute 'PymodeVirtualenv ' . fnameescape(venv_dir) PymodeRun let run_buffer = bufnr('__run__') @@ -114,17 +116,13 @@ Execute (Test PymodeRun uses active virtualenv python): else Assert 0, 'PymodeRun should create run buffer when using virtualenv' endif - else - Assert 1, 'Unable to create test virtualenv in this environment - test skipped' endif + let g:pymode_virtualenv_enabled = saved_venv_enabled + let g:pymode_virtualenv = saved_venv call delete(sample_file) call delete(temp_dir, 'rf') - else - Assert 1, 'No python executable available to create virtualenv - test skipped' endif - else - Assert 1, 'PymodeRun/PymodeVirtualenv command not available - test skipped' endif # Test PymodeLint command