Skip to content

Commit a942503

Browse files
authored
Support the text argument to our subprocess helpers (#293)
Support the `text` argument to our subprocess helpers in order to ease conversion of the default utf8-encoded bytes output to native strings. Follow-up to issue #288.
2 parents 6511bfd + 36eb176 commit a942503

File tree

2 files changed

+108
-7
lines changed

2 files changed

+108
-7
lines changed

mig/shared/safeeval.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# --- BEGIN_HEADER ---
55
#
66
# safeeval - Safe evaluation of expressions and commands
7-
# Copyright (C) 2003-2023 The MiG Project lead by Brian Vinter
7+
# Copyright (C) 2003-2025 The MiG Project by the Science HPC Center at UCPH
88
#
99
# This file is part of MiG.
1010
#
@@ -290,7 +290,7 @@ def math_expr_eval(expr):
290290

291291

292292
def subprocess_check_output(command, stdin=None, stdout=None, stderr=None,
293-
env=None, cwd=None,
293+
env=None, text=None, cwd=None,
294294
only_sanitized_variables=False):
295295
"""Safe execution of command with output returned as byte string.
296296
The optional only_sanitized_variables option is used to override the
@@ -299,25 +299,31 @@ def subprocess_check_output(command, stdin=None, stdout=None, stderr=None,
299299
command comes from user-provided variables or file names that may contain
300300
control characters.
301301
"""
302-
return subprocess.check_output(command, stdin=stdin, env=env, cwd=cwd,
302+
# NOTE: python3.7 added text arg previously known as universal_newlines
303+
# TODO: rename universal_newlines to text once we drop python3.6 support
304+
return subprocess.check_output(command, stdin=stdin, env=env,
305+
universal_newlines=text, cwd=cwd,
303306
shell=only_sanitized_variables)
304307

305308

306309
def subprocess_call(command, stdin=None, stdout=None, stderr=None, env=None,
307-
cwd=None, only_sanitized_variables=False):
310+
text=None, cwd=None, only_sanitized_variables=False):
308311
"""Safe execution of command.
309312
The optional only_sanitized_variables option is used to override the
310313
default execution without shell interpretation of control characters.
311314
Please be really careful when using it especially if any parts of your
312315
command comes from user-provided variables or file names that may contain
313316
control characters.
314317
"""
318+
# NOTE: python3.7 added text arg previously known as universal_newlines
319+
# TODO: rename universal_newlines to text once we drop python3.6 support
315320
return subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
316-
env=env, cwd=cwd, shell=only_sanitized_variables)
321+
env=env, universal_newlines=text, cwd=cwd,
322+
shell=only_sanitized_variables)
317323

318324

319325
def subprocess_popen(command, stdin=None, stdout=None, stderr=None, env=None,
320-
cwd=None, only_sanitized_variables=False):
326+
text=None, cwd=None, only_sanitized_variables=False):
321327
"""Safe execution of command with full process control.
322328
The optional only_sanitized_variables option is used to override the
323329
default execution without shell interpretation of control characters.
@@ -326,8 +332,11 @@ def subprocess_popen(command, stdin=None, stdout=None, stderr=None, env=None,
326332
control characters.
327333
Returns a subprocess Popen object with wait method, returncode and so on.
328334
"""
335+
# NOTE: python3.7 added text arg previously known as universal_newlines
336+
# TODO: rename universal_newlines to text once we drop python3.6 support
329337
return subprocess.Popen(command, stdin=stdin, stdout=stdout, stderr=stderr,
330-
env=env, cwd=cwd, shell=only_sanitized_variables)
338+
env=env, universal_newlines=text, cwd=cwd,
339+
shell=only_sanitized_variables)
331340

332341

333342
def subprocess_list2cmdline(command):

tests/test_mig_shared_safeeval.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# --- BEGIN_HEADER ---
4+
#
5+
# test_mig_shared_safeeval - unit test of the corresponding mig shared module
6+
# Copyright (C) 2003-2025 The MiG Project by the Science HPC Center at UCPH
7+
#
8+
# This file is part of MiG.
9+
#
10+
# MiG is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation; either version 2 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# MiG is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program; if not, write to the Free Software
22+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
23+
# USA.
24+
#
25+
# --- END_HEADER ---
26+
#
27+
28+
"""Unit test safeeval functions"""
29+
30+
import os
31+
import sys
32+
33+
from tests.support import MigTestCase, testmain
34+
35+
from mig.shared.safeeval import *
36+
37+
38+
PWD_STR = os.getcwd()
39+
PWD_BYTES = PWD_STR.encode('utf8')
40+
41+
42+
class MigSharedSafeeval(MigTestCase):
43+
"""Wrap unit tests for the corresponding module"""
44+
45+
def test_subprocess_call(self):
46+
"""Check that pwd call without args succeeds"""
47+
retval = subprocess_call(['pwd'], stdout=subprocess_pipe)
48+
self.assertEqual(retval, 0, "unexpected subprocess call pwd retval")
49+
50+
def test_subprocess_call_invalid(self):
51+
"""Check that pwd call with invalid arg fails"""
52+
retval = subprocess_call(['pwd', '-h'], stderr=subprocess_pipe)
53+
self.assertNotEqual(retval, 0,
54+
"unexpected subprocess call nosuchcommand retval")
55+
56+
def test_subprocess_check_output(self):
57+
"""Check that pwd command output matches getcwd as bytes"""
58+
data = subprocess_check_output(['pwd'], stdout=subprocess_pipe,
59+
stderr=subprocess_pipe).strip()
60+
self.assertEqual(data, PWD_BYTES,
61+
"mismatch in subprocess check pwd output")
62+
63+
def test_subprocess_check_output_text(self):
64+
"""Check that pwd command output matches getcwd as string"""
65+
data = subprocess_check_output(['pwd'], stdout=subprocess_pipe,
66+
stderr=subprocess_pipe,
67+
text=True).strip()
68+
self.assertEqual(data, PWD_STR,
69+
"mismatch in subprocess check pwd output")
70+
71+
def test_subprocess_popen(self):
72+
"""Check that pwd popen output matches getcwd as bytes"""
73+
proc = subprocess_popen(['pwd'], stdout=subprocess_pipe,
74+
stderr=subprocess_stdout)
75+
retval = proc.wait()
76+
data = proc.stdout.read().strip()
77+
self.assertEqual(data, PWD_BYTES,
78+
"mismatch in subprocess popen pwd output")
79+
80+
def test_subprocess_popen_text(self):
81+
"""Check that pwd popen output matches getcwd as string"""
82+
orig = os.getcwd()
83+
proc = subprocess_popen(['pwd'], stdout=subprocess_pipe,
84+
stderr=subprocess_stdout, text=True)
85+
retval = proc.wait()
86+
data = proc.stdout.read().strip()
87+
self.assertEqual(data, PWD_STR,
88+
"mismatch in subprocess popen pwd output")
89+
90+
91+
if __name__ == '__main__':
92+
testmain()

0 commit comments

Comments
 (0)