diff -r 17038de56fd4 Doc/library/functions.rst --- a/Doc/library/functions.rst Mon Jan 07 21:24:18 2013 +0100 +++ b/Doc/library/functions.rst Mon Jan 07 22:20:30 2013 +0100 @@ -852,6 +852,7 @@ are always available. They are listed h ``'w'`` open for writing, truncating the file first ``'x'`` open for exclusive creation, failing if the file already exists ``'a'`` open for writing, appending to the end of the file if it exists + ``'e'`` enable the close-on-exec flag ``'b'`` binary mode ``'t'`` text mode (default) ``'+'`` open a disk file for updating (reading and writing) @@ -989,6 +990,9 @@ are always available. They are listed h :exc:`FileExistsError` is now raised if the file opened in exclusive creation mode (``'x'``) already exists. + .. versionchanged:: 3.4 + ``'e'`` mode flag was added. + .. XXX works for bytes too, but should it? .. function:: ord(c) diff -r 17038de56fd4 Lib/_pyio.py --- a/Lib/_pyio.py Mon Jan 07 21:24:18 2013 +0100 +++ b/Lib/_pyio.py Mon Jan 07 22:20:30 2013 +0100 @@ -59,6 +59,7 @@ def open(file, mode="r", buffering=-1, e 'w' open for writing, truncating the file first 'x' create a new file and open it for writing 'a' open for writing, appending to the end of the file if it exists + 'e' enable the close-on-exec flag 'b' binary mode 't' text mode (default) '+' open a disk file for updating (reading and writing) @@ -163,6 +164,7 @@ def open(file, mode="r", buffering=-1, e if modes - set("axrwb+tU") or len(mode) > len(modes): raise ValueError("invalid mode: %r" % mode) creating = "x" in modes + cloexec = "e" in modes reading = "r" in modes writing = "w" in modes appending = "a" in modes @@ -190,6 +192,7 @@ def open(file, mode="r", buffering=-1, e (reading and "r" or "") + (writing and "w" or "") + (appending and "a" or "") + + (cloexec and "e" or "") + (updating and "+" or ""), closefd, opener=opener) line_buffering = False diff -r 17038de56fd4 Lib/test/test_builtin.py --- a/Lib/test/test_builtin.py Mon Jan 07 21:24:18 2013 +0100 +++ b/Lib/test/test_builtin.py Mon Jan 07 22:20:30 2013 +0100 @@ -16,10 +16,15 @@ import unittest import warnings from operator import neg from test.support import TESTFN, unlink, run_unittest, check_warnings +from test.script_helper import assert_python_ok try: import pty, signal except ImportError: pty = signal = None +try: + import fcntl +except ImportError: + fcntl = None class Squares: @@ -943,29 +948,24 @@ class BuiltinTest(unittest.TestCase): def write_testfile(self): # NB the first 4 lines are also used to test input, below fp = open(TESTFN, 'w') - try: + self.addCleanup(unlink, TESTFN) + with fp: fp.write('1+1\n') fp.write('The quick brown fox jumps over the lazy dog') fp.write('.\n') fp.write('Dear John\n') fp.write('XXX'*100) fp.write('YYY'*100) - finally: - fp.close() def test_open(self): self.write_testfile() - fp = open(TESTFN, 'r') - try: + with open(TESTFN, 'r') as fp: self.assertEqual(fp.readline(4), '1+1\n') self.assertEqual(fp.readline(), 'The quick brown fox jumps over the lazy dog.\n') self.assertEqual(fp.readline(4), 'Dear') self.assertEqual(fp.readline(100), ' John\n') self.assertEqual(fp.read(300), 'XXX'*100) self.assertEqual(fp.read(1000), 'YYY'*100) - finally: - fp.close() - unlink(TESTFN) def test_open_default_encoding(self): old_environ = dict(os.environ) @@ -979,16 +979,49 @@ class BuiltinTest(unittest.TestCase): self.write_testfile() current_locale_encoding = locale.getpreferredencoding(False) - fp = open(TESTFN, 'w') - try: + with open(TESTFN, 'w') as fp: self.assertEqual(fp.encoding, current_locale_encoding) - finally: - fp.close() - unlink(TESTFN) finally: os.environ.clear() os.environ.update(old_environ) + def test_open_close_exec(self): + code1 = """ +import errno, os, sys +fd = int(sys.argv[1]) +try: + os.fstat(fd) +except OSError as err: + if err.errno != errno.EBADF: + raise +else: + raise ValueError("file was not closed") + """ + args = [sys.executable, '-E', '-c', code1] + + self.write_testfile() + for mode in ('re', 'rbe'): + if fcntl: + with open(TESTFN, mode) as fileobj: + flags = fcntl.fcntl(fileobj.fileno(), fcntl.F_GETFD, 0) + self.assertTrue(flags & fcntl.FD_CLOEXEC) + + code2 = """ +import subprocess, sys + +fileobj = open(%a, %a) +fd = fileobj.fileno() +args = %a + [str(fd)] +exitcode = subprocess.call(args, close_fds=False) +fileobj.close() +sys.exit(exitcode) + """ % (TESTFN, mode, args) + assert_python_ok('-c', code2) + + def fake_opener(path, flags): + return 0 + self.assertRaises(ValueError, open, TESTFN, mode, opener=fake_opener) + def test_ord(self): self.assertEqual(ord(' '), 32) self.assertEqual(ord('A'), 65) diff -r 17038de56fd4 Modules/_io/_iomodule.c --- a/Modules/_io/_iomodule.c Mon Jan 07 21:24:18 2013 +0100 +++ b/Modules/_io/_iomodule.c Mon Jan 07 22:20:30 2013 +0100 @@ -123,6 +123,7 @@ PyDoc_STRVAR(open_doc, "'w' open for writing, truncating the file first\n" "'x' create a new file and open it for writing\n" "'a' open for writing, appending to the end of the file if it exists\n" +"'e' enable the close-on-exec flag\n" "'b' binary mode\n" "'t' text mode (default)\n" "'+' open a disk file for updating (reading and writing)\n" @@ -226,7 +227,8 @@ io_open(PyObject *self, PyObject *args, char *encoding = NULL, *errors = NULL, *newline = NULL; unsigned i; - int creating = 0, reading = 0, writing = 0, appending = 0, updating = 0; + int creating = 0, cloexec = 0; + int reading = 0, writing = 0, appending = 0, updating = 0; int text = 0, binary = 0, universal = 0; char rawmode[6], *m; @@ -282,6 +284,9 @@ io_open(PyObject *self, PyObject *args, universal = 1; reading = 1; break; + case 'e': + cloexec = 1; + break; default: goto invalid_mode; } @@ -300,6 +305,7 @@ io_open(PyObject *self, PyObject *args, if (reading) *(m++) = 'r'; if (writing) *(m++) = 'w'; if (appending) *(m++) = 'a'; + if (cloexec) *(m++) = 'e'; if (updating) *(m++) = '+'; *m = '\0'; diff -r 17038de56fd4 Modules/_io/fileio.c --- a/Modules/_io/fileio.c Mon Jan 07 21:24:18 2013 +0100 +++ b/Modules/_io/fileio.c Mon Jan 07 22:20:30 2013 +0100 @@ -202,6 +202,41 @@ check_fd(int fd) return 0; } +#if !defined(MS_WINDOWS) && defined(HAVE_FCNTL_H) +static void +set_cloexec(fileio *self) +{ + int fd_flags; +#ifdef O_CLOEXEC + static int cloexec_works = -1; + + if (cloexec_works == 1) + return; +#endif + + fd_flags = fcntl(self->fd, F_GETFD, 0); + if (fd_flags == -1) + goto error; + +#ifdef O_CLOEXEC + if (cloexec_works == -1) + cloexec_works = (fd_flags & FD_CLOEXEC); + if (cloexec_works) + return; + /* O_CLOEXEC doesn't work (ex: Linux kernel older than 2.6.23 */ +#endif + + fd_flags |= FD_CLOEXEC; + fd_flags = fcntl(self->fd, F_SETFD, fd_flags); + if (fd_flags == -1) + goto error; + return; + +error: + close(self->fd); + self->fd = -1; +} +#endif static int fileio_init(PyObject *oself, PyObject *args, PyObject *kwds) @@ -221,6 +256,7 @@ fileio_init(PyObject *oself, PyObject *a int fd = -1; int closefd = 1; int fd_is_own = 0; + int cloexec = 0; assert(PyFileIO_Check(oself)); if (self->fd >= 0) { @@ -291,6 +327,14 @@ fileio_init(PyObject *oself, PyObject *a self->writable = 1; flags |= O_EXCL | O_CREAT; break; + case 'e': + cloexec = 1; +#ifdef MS_WINDOWS + flags |= O_NOINHERIT; +#elif defined(O_CLOEXEC) && defined(HAVE_FCNTL_H) + flags |= O_CLOEXEC; +#endif + break; case 'r': if (rwa) goto bad_mode; @@ -360,6 +404,15 @@ fileio_init(PyObject *oself, PyObject *a goto error; } +#if !defined(MS_WINDOWS) && !defined(HAVE_FCNTL_H) + if (cloexec) { + PyErr_SetString(PyExc_NotImplementedError, + "close-on-exec ('e' mode) is not supported " + "by your platform"); + goto error; + } +#endif + errno = 0; if (opener == Py_None) { Py_BEGIN_ALLOW_THREADS @@ -369,10 +422,23 @@ fileio_init(PyObject *oself, PyObject *a else #endif self->fd = open(name, flags, 0666); + +#if !defined(MS_WINDOWS) + if (self->fd >= 0 && cloexec) + set_cloexec(self); +#endif Py_END_ALLOW_THREADS - } else { - PyObject *fdobj = PyObject_CallFunction( - opener, "Oi", nameobj, flags); + } + else { + PyObject *fdobj; + + if (cloexec) { + PyErr_SetString(PyExc_ValueError, + "'e' mode cannot be used with an opener"); + goto error; + } + + fdobj = PyObject_CallFunction(opener, "Oi", nameobj, flags); if (fdobj == NULL) goto error; if (!PyLong_Check(fdobj)) {