Issue #8513: os.get_exec_path() supports b'PATH' key and bytes value.

subprocess.Popen() and os._execvpe() support bytes program name. Add
os.supports_bytes_environ flag: True if the native OS type of the environment
is bytes (eg. False on Windows).
This commit is contained in:
Victor Stinner 2010-05-18 17:17:23 +00:00
parent 04b5684d00
commit b745a74c99
6 changed files with 129 additions and 22 deletions

View File

@ -142,7 +142,8 @@ process and user.
synchronized (modify :data:`environb` updates :data:`environ`, and vice synchronized (modify :data:`environb` updates :data:`environ`, and vice
versa). versa).
Availability: Unix. :data:`environb` is only available if :data:`supports_bytes_environ` is
True.
.. versionadded:: 3.2 .. versionadded:: 3.2
@ -457,6 +458,12 @@ process and user.
Availability: Unix, Windows. Availability: Unix, Windows.
.. data:: supports_bytes_environ
True if the native OS type of the environment is bytes (eg. False on
Windows).
.. function:: umask(mask) .. function:: umask(mask)
Set the current numeric umask and return the previous umask. Set the current numeric umask and return the previous umask.

View File

@ -355,7 +355,11 @@ def _execvpe(file, args, env=None):
return return
last_exc = saved_exc = None last_exc = saved_exc = None
saved_tb = None saved_tb = None
for dir in get_exec_path(env): path_list = get_exec_path(env)
if name != 'nt':
file = fsencode(file)
path_list = map(fsencode, path_list)
for dir in path_list:
fullname = path.join(dir, file) fullname = path.join(dir, file)
try: try:
exec_func(fullname, *argrest) exec_func(fullname, *argrest)
@ -380,7 +384,30 @@ def get_exec_path(env=None):
""" """
if env is None: if env is None:
env = environ env = environ
return env.get('PATH', defpath).split(pathsep)
try:
path_list = env.get('PATH')
except TypeError:
path_list = None
if supports_bytes_environ:
try:
path_listb = env[b'PATH']
except (KeyError, TypeError):
pass
else:
if path_list is not None:
raise ValueError(
"env cannot contain 'PATH' and b'PATH' keys")
path_list = path_listb
if path_list is not None and isinstance(path_list, bytes):
path_list = path_list.decode(sys.getfilesystemencoding(),
'surrogateescape')
if path_list is None:
path_list = defpath
return path_list.split(pathsep)
# Change environ to automatically call putenv(), unsetenv if they exist. # Change environ to automatically call putenv(), unsetenv if they exist.
@ -482,9 +509,11 @@ def getenv(key, default=None):
The optional second argument can specify an alternate default. The optional second argument can specify an alternate default.
key, default and the result are str.""" key, default and the result are str."""
return environ.get(key, default) return environ.get(key, default)
__all__.append("getenv")
if name not in ('os2', 'nt'): supports_bytes_environ = name not in ('os2', 'nt')
__all__.extend(("getenv", "supports_bytes_environ"))
if supports_bytes_environ:
def _check_bytes(value): def _check_bytes(value):
if not isinstance(value, bytes): if not isinstance(value, bytes):
raise TypeError("bytes expected, not %s" % type(value).__name__) raise TypeError("bytes expected, not %s" % type(value).__name__)

View File

@ -1096,15 +1096,14 @@ class Popen(object):
for k, v in env.items()] for k, v in env.items()]
else: else:
env_list = None # Use execv instead of execve. env_list = None # Use execv instead of execve.
executable = os.fsencode(executable)
if os.path.dirname(executable): if os.path.dirname(executable):
executable_list = (os.fsencode(executable),) executable_list = (executable,)
else: else:
# This matches the behavior of os._execvpe(). # This matches the behavior of os._execvpe().
path_list = os.get_exec_path(env) executable_list = tuple(
executable_list = (os.path.join(dir, executable) os.path.join(os.fsencode(dir), executable)
for dir in path_list) for dir in os.get_exec_path(env))
executable_list = tuple(os.fsencode(exe)
for exe in executable_list)
self.pid = _posixsubprocess.fork_exec( self.pid = _posixsubprocess.fork_exec(
args, executable_list, args, executable_list,
close_fds, cwd, env_list, close_fds, cwd, env_list,

View File

@ -370,7 +370,7 @@ class EnvironTests(mapping_tests.BasicTestMappingProtocol):
def setUp(self): def setUp(self):
self.__save = dict(os.environ) self.__save = dict(os.environ)
if os.name not in ('os2', 'nt'): if os.supports_bytes_environ:
self.__saveb = dict(os.environb) self.__saveb = dict(os.environb)
for key, value in self._reference().items(): for key, value in self._reference().items():
os.environ[key] = value os.environ[key] = value
@ -378,7 +378,7 @@ class EnvironTests(mapping_tests.BasicTestMappingProtocol):
def tearDown(self): def tearDown(self):
os.environ.clear() os.environ.clear()
os.environ.update(self.__save) os.environ.update(self.__save)
if os.name not in ('os2', 'nt'): if os.supports_bytes_environ:
os.environb.clear() os.environb.clear()
os.environb.update(self.__saveb) os.environb.update(self.__saveb)
@ -445,7 +445,21 @@ class EnvironTests(mapping_tests.BasicTestMappingProtocol):
# Supplied PATH environment variable # Supplied PATH environment variable
self.assertSequenceEqual(test_path, os.get_exec_path(test_env)) self.assertSequenceEqual(test_path, os.get_exec_path(test_env))
@unittest.skipIf(sys.platform == "win32", "POSIX specific test") if os.supports_bytes_environ:
# env cannot contain 'PATH' and b'PATH' keys
self.assertRaises(ValueError,
os.get_exec_path, {'PATH': '1', b'PATH': b'2'})
# bytes key and/or value
self.assertSequenceEqual(os.get_exec_path({b'PATH': b'abc'}),
['abc'])
self.assertSequenceEqual(os.get_exec_path({b'PATH': 'abc'}),
['abc'])
self.assertSequenceEqual(os.get_exec_path({'PATH': b'abc'}),
['abc'])
@unittest.skipUnless(os.supports_bytes_environ,
"os.environb required for this test.")
def test_environb(self): def test_environb(self):
# os.environ -> os.environb # os.environ -> os.environb
value = 'euro\u20ac' value = 'euro\u20ac'
@ -669,22 +683,54 @@ class ExecTests(unittest.TestCase):
@unittest.skipUnless(hasattr(os, '_execvpe'), @unittest.skipUnless(hasattr(os, '_execvpe'),
"No internal os._execvpe function to test.") "No internal os._execvpe function to test.")
def test_internal_execvpe(self): def _test_internal_execvpe(self, test_type):
program_path = os.sep+'absolutepath' program_path = os.sep + 'absolutepath'
program = 'executable' if test_type is bytes:
fullpath = os.path.join(program_path, program) program = b'executable'
arguments = ['progname', 'arg1', 'arg2'] fullpath = os.path.join(os.fsencode(program_path), program)
native_fullpath = fullpath
arguments = [b'progname', 'arg1', 'arg2']
else:
program = 'executable'
arguments = ['progname', 'arg1', 'arg2']
fullpath = os.path.join(program_path, program)
if os.name != "nt":
native_fullpath = os.fsencode(fullpath)
else:
native_fullpath = fullpath
env = {'spam': 'beans'} env = {'spam': 'beans'}
# test os._execvpe() with an absolute path
with _execvpe_mockup() as calls: with _execvpe_mockup() as calls:
self.assertRaises(RuntimeError, os._execvpe, fullpath, arguments) self.assertRaises(RuntimeError,
os._execvpe, fullpath, arguments)
self.assertEqual(len(calls), 1) self.assertEqual(len(calls), 1)
self.assertEqual(calls[0], ('execv', fullpath, (arguments,))) self.assertEqual(calls[0], ('execv', fullpath, (arguments,)))
# test os._execvpe() with a relative path:
# os.get_exec_path() returns defpath
with _execvpe_mockup(defpath=program_path) as calls: with _execvpe_mockup(defpath=program_path) as calls:
self.assertRaises(OSError, os._execvpe, program, arguments, env=env) self.assertRaises(OSError,
os._execvpe, program, arguments, env=env)
self.assertEqual(len(calls), 1) self.assertEqual(len(calls), 1)
self.assertEqual(calls[0], ('execve', fullpath, (arguments, env))) self.assertSequenceEqual(calls[0],
('execve', native_fullpath, (arguments, env)))
# test os._execvpe() with a relative path:
# os.get_exec_path() reads the 'PATH' variable
with _execvpe_mockup() as calls:
env_path = env.copy()
env_path['PATH'] = program_path
self.assertRaises(OSError,
os._execvpe, program, arguments, env=env_path)
self.assertEqual(len(calls), 1)
self.assertSequenceEqual(calls[0],
('execve', native_fullpath, (arguments, env_path)))
def test_internal_execvpe_str(self):
self._test_internal_execvpe(str)
if os.name != "nt":
self._test_internal_execvpe(bytes)
class Win32ErrorTests(unittest.TestCase): class Win32ErrorTests(unittest.TestCase):

View File

@ -825,6 +825,27 @@ class POSIXProcessTestCase(BaseTestCase):
stdout = stdout.rstrip(b'\n\r') stdout = stdout.rstrip(b'\n\r')
self.assertEquals(stdout.decode('ascii'), repr(value)) self.assertEquals(stdout.decode('ascii'), repr(value))
def test_bytes_program(self):
abs_program = os.fsencode(sys.executable)
path, program = os.path.split(sys.executable)
program = os.fsencode(program)
# absolute bytes path
exitcode = subprocess.call([abs_program, "-c", "pass"])
self.assertEquals(exitcode, 0)
# bytes program, unicode PATH
env = os.environ.copy()
env["PATH"] = path
exitcode = subprocess.call([program, "-c", "pass"], env=env)
self.assertEquals(exitcode, 0)
# bytes program, bytes PATH
envb = os.environb.copy()
envb[b"PATH"] = os.fsencode(path)
exitcode = subprocess.call([program, "-c", "pass"], env=envb)
self.assertEquals(exitcode, 0)
@unittest.skipUnless(mswindows, "Windows specific tests") @unittest.skipUnless(mswindows, "Windows specific tests")
class Win32ProcessTestCase(BaseTestCase): class Win32ProcessTestCase(BaseTestCase):

View File

@ -366,6 +366,11 @@ C-API
Library Library
------- -------
- Issue #8513: os.get_exec_path() supports b'PATH' key and bytes value.
subprocess.Popen() and os._execvpe() support bytes program name. Add
os.supports_bytes_environ flag: True if the native OS type of the environment
is bytes (eg. False on Windows).
- Issue #8633: tarfile is now able to read and write archives with "raw" binary - Issue #8633: tarfile is now able to read and write archives with "raw" binary
pax headers as described in POSIX.1-2008. pax headers as described in POSIX.1-2008.