Commit a2670565 authored by Alexey Izbyshev's avatar Alexey Izbyshev Committed by Victor Stinner

bpo-32236: open() emits RuntimeWarning if buffering=1 for binary mode (GH-4842)

If buffering=1 is specified for open() in binary mode, it is silently
treated as buffering=-1 (i.e., the default buffer size).
Coupled with the fact that line buffering is always supported in Python 2,
such behavior caused several issues (e.g., bpo-10344, bpo-21332).

Warn that line buffering is not supported if open() is called with
binary mode and buffering=1.
parent 4acf6c9d
...@@ -174,7 +174,7 @@ recommended approach for working with encoded text files, this module ...@@ -174,7 +174,7 @@ recommended approach for working with encoded text files, this module
provides additional utility functions and classes that allow the use of a provides additional utility functions and classes that allow the use of a
wider range of codecs when working with binary files: wider range of codecs when working with binary files:
.. function:: open(filename, mode='r', encoding=None, errors='strict', buffering=1) .. function:: open(filename, mode='r', encoding=None, errors='strict', buffering=-1)
Open an encoded file using the given *mode* and return an instance of Open an encoded file using the given *mode* and return an instance of
:class:`StreamReaderWriter`, providing transparent encoding/decoding. :class:`StreamReaderWriter`, providing transparent encoding/decoding.
...@@ -194,8 +194,8 @@ wider range of codecs when working with binary files: ...@@ -194,8 +194,8 @@ wider range of codecs when working with binary files:
*errors* may be given to define the error handling. It defaults to ``'strict'`` *errors* may be given to define the error handling. It defaults to ``'strict'``
which causes a :exc:`ValueError` to be raised in case an encoding error occurs. which causes a :exc:`ValueError` to be raised in case an encoding error occurs.
*buffering* has the same meaning as for the built-in :func:`open` function. It *buffering* has the same meaning as for the built-in :func:`open` function.
defaults to line buffered. It defaults to -1 which means that the default buffer size will be used.
.. function:: EncodedFile(file, data_encoding, file_encoding=None, errors='strict') .. function:: EncodedFile(file, data_encoding, file_encoding=None, errors='strict')
......
...@@ -198,6 +198,11 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None, ...@@ -198,6 +198,11 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
raise ValueError("binary mode doesn't take an errors argument") raise ValueError("binary mode doesn't take an errors argument")
if binary and newline is not None: if binary and newline is not None:
raise ValueError("binary mode doesn't take a newline argument") raise ValueError("binary mode doesn't take a newline argument")
if binary and buffering == 1:
import warnings
warnings.warn("line buffering (buffering=1) isn't supported in binary "
"mode, the default buffer size will be used",
RuntimeWarning, 2)
raw = FileIO(file, raw = FileIO(file,
(creating and "x" or "") + (creating and "x" or "") +
(reading and "r" or "") + (reading and "r" or "") +
......
...@@ -862,7 +862,7 @@ class StreamRecoder: ...@@ -862,7 +862,7 @@ class StreamRecoder:
### Shortcuts ### Shortcuts
def open(filename, mode='r', encoding=None, errors='strict', buffering=1): def open(filename, mode='r', encoding=None, errors='strict', buffering=-1):
""" Open an encoded file using the given mode and return """ Open an encoded file using the given mode and return
a wrapped version providing transparent encoding/decoding. a wrapped version providing transparent encoding/decoding.
...@@ -883,7 +883,8 @@ def open(filename, mode='r', encoding=None, errors='strict', buffering=1): ...@@ -883,7 +883,8 @@ def open(filename, mode='r', encoding=None, errors='strict', buffering=1):
encoding error occurs. encoding error occurs.
buffering has the same meaning as for the builtin open() API. buffering has the same meaning as for the builtin open() API.
It defaults to line buffered. It defaults to -1 which means that the default buffer size will
be used.
The returned wrapped file object provides an extra attribute The returned wrapped file object provides an extra attribute
.encoding which allows querying the used encoding. This .encoding which allows querying the used encoding. This
......
...@@ -743,12 +743,21 @@ class Popen(object): ...@@ -743,12 +743,21 @@ class Popen(object):
self._closed_child_pipe_fds = False self._closed_child_pipe_fds = False
if self.text_mode:
if bufsize == 1:
line_buffering = True
# Use the default buffer size for the underlying binary streams
# since they don't support line buffering.
bufsize = -1
else:
line_buffering = False
try: try:
if p2cwrite != -1: if p2cwrite != -1:
self.stdin = io.open(p2cwrite, 'wb', bufsize) self.stdin = io.open(p2cwrite, 'wb', bufsize)
if self.text_mode: if self.text_mode:
self.stdin = io.TextIOWrapper(self.stdin, write_through=True, self.stdin = io.TextIOWrapper(self.stdin, write_through=True,
line_buffering=(bufsize == 1), line_buffering=line_buffering,
encoding=encoding, errors=errors) encoding=encoding, errors=errors)
if c2pread != -1: if c2pread != -1:
self.stdout = io.open(c2pread, 'rb', bufsize) self.stdout = io.open(c2pread, 'rb', bufsize)
......
...@@ -107,7 +107,8 @@ __all__ = [ ...@@ -107,7 +107,8 @@ __all__ = [
# threads # threads
"threading_setup", "threading_cleanup", "reap_threads", "start_threads", "threading_setup", "threading_cleanup", "reap_threads", "start_threads",
# miscellaneous # miscellaneous
"check_warnings", "check_no_resource_warning", "EnvironmentVarGuard", "check_warnings", "check_no_resource_warning", "check_no_warnings",
"EnvironmentVarGuard",
"run_with_locale", "swap_item", "run_with_locale", "swap_item",
"swap_attr", "Matcher", "set_memlimit", "SuppressCrashReport", "sortdict", "swap_attr", "Matcher", "set_memlimit", "SuppressCrashReport", "sortdict",
"run_with_tz", "PGO", "missing_compiler_executable", "fd_count", "run_with_tz", "PGO", "missing_compiler_executable", "fd_count",
...@@ -1252,6 +1253,30 @@ def check_warnings(*filters, **kwargs): ...@@ -1252,6 +1253,30 @@ def check_warnings(*filters, **kwargs):
return _filterwarnings(filters, quiet) return _filterwarnings(filters, quiet)
@contextlib.contextmanager
def check_no_warnings(testcase, message='', category=Warning, force_gc=False):
"""Context manager to check that no warnings are emitted.
This context manager enables a given warning within its scope
and checks that no warnings are emitted even with that warning
enabled.
If force_gc is True, a garbage collection is attempted before checking
for warnings. This may help to catch warnings emitted when objects
are deleted, such as ResourceWarning.
Other keyword arguments are passed to warnings.filterwarnings().
"""
with warnings.catch_warnings(record=True) as warns:
warnings.filterwarnings('always',
message=message,
category=category)
yield
if force_gc:
gc_collect()
testcase.assertEqual(warns, [])
@contextlib.contextmanager @contextlib.contextmanager
def check_no_resource_warning(testcase): def check_no_resource_warning(testcase):
"""Context manager to check that no ResourceWarning is emitted. """Context manager to check that no ResourceWarning is emitted.
...@@ -1266,11 +1291,8 @@ def check_no_resource_warning(testcase): ...@@ -1266,11 +1291,8 @@ def check_no_resource_warning(testcase):
You must remove the object which may emit ResourceWarning before You must remove the object which may emit ResourceWarning before
the end of the context manager. the end of the context manager.
""" """
with warnings.catch_warnings(record=True) as warns: with check_no_warnings(testcase, category=ResourceWarning, force_gc=True):
warnings.filterwarnings('always', category=ResourceWarning)
yield yield
gc_collect()
testcase.assertEqual(warns, [])
class CleanImport(object): class CleanImport(object):
......
...@@ -169,10 +169,10 @@ class CmdLineTest(unittest.TestCase): ...@@ -169,10 +169,10 @@ class CmdLineTest(unittest.TestCase):
@contextlib.contextmanager @contextlib.contextmanager
def interactive_python(self, separate_stderr=False): def interactive_python(self, separate_stderr=False):
if separate_stderr: if separate_stderr:
p = spawn_python('-i', bufsize=1, stderr=subprocess.PIPE) p = spawn_python('-i', stderr=subprocess.PIPE)
stderr = p.stderr stderr = p.stderr
else: else:
p = spawn_python('-i', bufsize=1, stderr=subprocess.STDOUT) p = spawn_python('-i', stderr=subprocess.STDOUT)
stderr = p.stdout stderr = p.stdout
try: try:
# Drain stderr until prompt # Drain stderr until prompt
......
...@@ -169,22 +169,33 @@ class OtherFileTests: ...@@ -169,22 +169,33 @@ class OtherFileTests:
f.close() f.close()
self.fail("no error for invalid mode: %s" % bad_mode) self.fail("no error for invalid mode: %s" % bad_mode)
def _checkBufferSize(self, s):
try:
f = self.open(TESTFN, 'wb', s)
f.write(str(s).encode("ascii"))
f.close()
f.close()
f = self.open(TESTFN, 'rb', s)
d = int(f.read().decode("ascii"))
f.close()
f.close()
except OSError as msg:
self.fail('error setting buffer size %d: %s' % (s, str(msg)))
self.assertEqual(d, s)
def testSetBufferSize(self): def testSetBufferSize(self):
# make sure that explicitly setting the buffer size doesn't cause # make sure that explicitly setting the buffer size doesn't cause
# misbehaviour especially with repeated close() calls # misbehaviour especially with repeated close() calls
for s in (-1, 0, 1, 512): for s in (-1, 0, 512):
try: with support.check_no_warnings(self,
f = self.open(TESTFN, 'wb', s) message='line buffering',
f.write(str(s).encode("ascii")) category=RuntimeWarning):
f.close() self._checkBufferSize(s)
f.close()
f = self.open(TESTFN, 'rb', s) # test that attempts to use line buffering in binary mode cause
d = int(f.read().decode("ascii")) # a warning
f.close() with self.assertWarnsRegex(RuntimeWarning, 'line buffering'):
f.close() self._checkBufferSize(1)
except OSError as msg:
self.fail('error setting buffer size %d: %s' % (s, str(msg)))
self.assertEqual(d, s)
def testTruncateOnWindows(self): def testTruncateOnWindows(self):
# SF bug <http://www.python.org/sf/801631> # SF bug <http://www.python.org/sf/801631>
......
...@@ -593,7 +593,7 @@ class IOTest(unittest.TestCase): ...@@ -593,7 +593,7 @@ class IOTest(unittest.TestCase):
self.large_file_ops(f) self.large_file_ops(f)
def test_with_open(self): def test_with_open(self):
for bufsize in (0, 1, 100): for bufsize in (0, 100):
f = None f = None
with self.open(support.TESTFN, "wb", bufsize) as f: with self.open(support.TESTFN, "wb", bufsize) as f:
f.write(b"xxx") f.write(b"xxx")
......
...@@ -1136,7 +1136,8 @@ class ProcessTestCase(BaseTestCase): ...@@ -1136,7 +1136,8 @@ class ProcessTestCase(BaseTestCase):
# line is not flushed in binary mode with bufsize=1. # line is not flushed in binary mode with bufsize=1.
# we should get empty response # we should get empty response
line = b'line' + os.linesep.encode() # assume ascii-based locale line = b'line' + os.linesep.encode() # assume ascii-based locale
self._test_bufsize_equal_one(line, b'', universal_newlines=False) with self.assertWarnsRegex(RuntimeWarning, 'line buffering'):
self._test_bufsize_equal_one(line, b'', universal_newlines=False)
def test_leaking_fds_on_error(self): def test_leaking_fds_on_error(self):
# see bug #5179: Popen leaks file descriptors to PIPEs if # see bug #5179: Popen leaks file descriptors to PIPEs if
......
Warn that line buffering is not supported if :func:`open` is called with
binary mode and ``buffering=1``.
...@@ -363,6 +363,15 @@ _io_open_impl(PyObject *module, PyObject *file, const char *mode, ...@@ -363,6 +363,15 @@ _io_open_impl(PyObject *module, PyObject *file, const char *mode,
goto error; goto error;
} }
if (binary && buffering == 1) {
if (PyErr_WarnEx(PyExc_RuntimeWarning,
"line buffering (buffering=1) isn't supported in "
"binary mode, the default buffer size will be used",
1) < 0) {
goto error;
}
}
/* Create the Raw file stream */ /* Create the Raw file stream */
{ {
PyObject *RawIO_class = (PyObject *)&PyFileIO_Type; PyObject *RawIO_class = (PyObject *)&PyFileIO_Type;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment