Skip to content

Commit 2aec97c

Browse files
diraolclaude
andcommitted
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) <noreply@anthropic.com>
1 parent 892a362 commit 2aec97c

3 files changed

Lines changed: 29 additions & 13 deletions

File tree

plugin/pymode.vim

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ call pymode#default('g:pymode_run', 1)
9999
" Key's map for run python code
100100
call pymode#default('g:pymode_run_bind', '<leader>r')
101101

102+
" Timeout in seconds for :PymodeRun when using a virtualenv interpreter (0 = no limit)
103+
call pymode#default('g:pymode_run_timeout', 0)
104+
102105
" }}}
103106

104107
" CHECK CODE {{{

pymode/run.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ def run_code():
2828
elif encoding.match(lines[1]):
2929
lines.pop(1)
3030

31+
real_file = env.var('expand("%:p")')
3132
python_cmd = __get_virtualenv_python()
3233
if python_cmd:
33-
result = __run_with_external_python(python_cmd, lines)
34+
result = __run_with_external_python(python_cmd, lines, real_file)
3435
if result is not None:
3536
output, err = result
3637
else:
@@ -39,7 +40,7 @@ def run_code():
3940
if not python_cmd:
4041
context = dict(
4142
__name__='__main__',
42-
__file__=env.var('expand("%:p")'),
43+
__file__=real_file,
4344
input=env.user_input,
4445
raw_input=env.user_input)
4546

@@ -57,6 +58,7 @@ def run_code():
5758
# A non-false code indicates abnormal termination.
5859
# A false code will be treated as a
5960
# successful run, and the error will be hidden from Vim
61+
sys.stdout, sys.stderr = stdout_, stderr_
6062
env.error("Script exited with code %s" % e.code)
6163
return env.stop()
6264

@@ -98,8 +100,10 @@ def __get_virtualenv_python():
98100
return None
99101

100102

101-
def __run_with_external_python(python_cmd, lines):
102-
source = '\n'.join(lines) + '\n'
103+
def __run_with_external_python(python_cmd, lines, real_file):
104+
# Inject __file__ so user code sees the real source path, not the temp file
105+
header = '__file__ = %r\n' % real_file
106+
source = header + '\n'.join(lines) + '\n'
103107
temp_file_path = None
104108
try:
105109
with tempfile.NamedTemporaryFile(
@@ -108,20 +112,31 @@ def __run_with_external_python(python_cmd, lines):
108112
temp_file.write(source)
109113
temp_file_path = temp_file.name
110114

115+
timeout = env.var('g:pymode_run_timeout', silence=True, default=0) or None
111116
process = subprocess.Popen(
112117
[python_cmd, temp_file_path],
113118
stdout=subprocess.PIPE,
114119
stderr=subprocess.PIPE,
115120
cwd=env.curdir,
116-
text=True)
117-
output, err = process.communicate()
121+
text=True,
122+
encoding='utf-8')
123+
try:
124+
output, err = process.communicate(timeout=timeout)
125+
except subprocess.TimeoutExpired:
126+
process.kill()
127+
output, err = process.communicate()
128+
err += '\nProcess timed out after %s second(s).' % timeout
118129
except OSError as exc:
119130
env.debug('Failed to execute external python', python_cmd, exc)
120131
return None
121132
finally:
122133
if temp_file_path and os.path.exists(temp_file_path):
123134
os.unlink(temp_file_path)
124135

136+
# Replace temp path in tracebacks so errors reference the real source file
137+
if temp_file_path:
138+
err = err.replace(temp_file_path, real_file)
139+
125140
return output, err
126141

127142

tests/vader/commands.vader

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,16 @@ Execute (Test PymodeRun uses active virtualenv python):
9494
call mkdir(temp_dir, 'p')
9595
let venv_dir = temp_dir . '/.venv'
9696
let sample_file = temp_dir . '/run_from_venv.py'
97+
let saved_venv_enabled = get(g:, 'pymode_virtualenv_enabled', '')
98+
let saved_venv = get(g:, 'pymode_virtualenv', 0)
9799

98100
call writefile(['import sys', 'print(sys.executable)'], sample_file)
99101

100102
call system(pycmd . ' -m venv ' . shellescape(venv_dir))
101103
if v:shell_error == 0
102104
execute 'edit ' . fnameescape(sample_file)
103105
let g:pymode_virtualenv = 1
104-
execute 'PymodeVirtualenv ' . string(venv_dir)
106+
execute 'PymodeVirtualenv ' . fnameescape(venv_dir)
105107
PymodeRun
106108

107109
let run_buffer = bufnr('__run__')
@@ -114,17 +116,13 @@ Execute (Test PymodeRun uses active virtualenv python):
114116
else
115117
Assert 0, 'PymodeRun should create run buffer when using virtualenv'
116118
endif
117-
else
118-
Assert 1, 'Unable to create test virtualenv in this environment - test skipped'
119119
endif
120120

121+
let g:pymode_virtualenv_enabled = saved_venv_enabled
122+
let g:pymode_virtualenv = saved_venv
121123
call delete(sample_file)
122124
call delete(temp_dir, 'rf')
123-
else
124-
Assert 1, 'No python executable available to create virtualenv - test skipped'
125125
endif
126-
else
127-
Assert 1, 'PymodeRun/PymodeVirtualenv command not available - test skipped'
128126
endif
129127

130128
# Test PymodeLint command

0 commit comments

Comments
 (0)