Skip to content

Commit 05bf9b5

Browse files
authored
Merge pull request RustPython#3529 from fanninpm/os-finesse
Align os and io modules more squarely with CPython 3.10
2 parents 372ed7f + b70eb89 commit 05bf9b5

File tree

9 files changed

+339
-146
lines changed

9 files changed

+339
-146
lines changed

Lib/_pyio.py

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,40 @@
3636
# Does io.IOBase finalizer log the exception if the close() method fails?
3737
# The exception is ignored silently by default in release build.
3838
_IOBASE_EMITS_UNRAISABLE = (hasattr(sys, "gettotalrefcount") or sys.flags.dev_mode)
39+
# Does open() check its 'errors' argument?
40+
_CHECK_ERRORS = _IOBASE_EMITS_UNRAISABLE
3941

4042

43+
def text_encoding(encoding, stacklevel=2):
44+
"""
45+
A helper function to choose the text encoding.
46+
47+
When encoding is not None, just return it.
48+
Otherwise, return the default text encoding (i.e. "locale").
49+
50+
This function emits an EncodingWarning if *encoding* is None and
51+
sys.flags.warn_default_encoding is true.
52+
53+
This can be used in APIs with an encoding=None parameter
54+
that pass it to TextIOWrapper or open.
55+
However, please consider using encoding="utf-8" for new APIs.
56+
"""
57+
if encoding is None:
58+
encoding = "locale"
59+
if sys.flags.warn_default_encoding:
60+
import warnings
61+
warnings.warn("'encoding' argument not specified.",
62+
EncodingWarning, stacklevel + 1)
63+
return encoding
64+
65+
66+
# Wrapper for builtins.open
67+
#
68+
# Trick so that open() won't become a bound method when stored
69+
# as a class variable (as dbm.dumb does).
70+
#
71+
# See init_set_builtins_open() in Python/pylifecycle.c.
72+
@staticmethod
4173
def open(file, mode="r", buffering=-1, encoding=None, errors=None,
4274
newline=None, closefd=True, opener=None):
4375

@@ -246,6 +278,7 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
246278
result = buffer
247279
if binary:
248280
return result
281+
encoding = text_encoding(encoding)
249282
text = TextIOWrapper(buffer, encoding, errors, newline, line_buffering)
250283
result = text
251284
text.mode = mode
@@ -278,27 +311,20 @@ def _open_code_with_warning(path):
278311
open_code = _open_code_with_warning
279312

280313

281-
class DocDescriptor:
282-
"""Helper for builtins.open.__doc__
283-
"""
284-
def __get__(self, obj, typ=None):
285-
return (
286-
"open(file, mode='r', buffering=-1, encoding=None, "
287-
"errors=None, newline=None, closefd=True)\n\n" +
288-
open.__doc__)
289-
290-
class OpenWrapper:
291-
"""Wrapper for builtins.open
292-
293-
Trick so that open won't become a bound method when stored
294-
as a class variable (as dbm.dumb does).
295-
296-
See initstdio() in Python/pylifecycle.c.
297-
"""
298-
__doc__ = DocDescriptor()
299-
300-
def __new__(cls, *args, **kwargs):
301-
return open(*args, **kwargs)
314+
def __getattr__(name):
315+
if name == "OpenWrapper":
316+
# bpo-43680: Until Python 3.9, _pyio.open was not a static method and
317+
# builtins.open was set to OpenWrapper to not become a bound method
318+
# when set to a class variable. _io.open is a built-in function whereas
319+
# _pyio.open is a Python function. In Python 3.10, _pyio.open() is now
320+
# a static method, and builtins.open() is now io.open().
321+
import warnings
322+
warnings.warn('OpenWrapper is deprecated, use open instead',
323+
DeprecationWarning, stacklevel=2)
324+
global OpenWrapper
325+
OpenWrapper = open
326+
return OpenWrapper
327+
raise AttributeError(name)
302328

303329

304330
# In normal operation, both `UnsupportedOperation`s should be bound to the
@@ -802,6 +828,9 @@ def tell(self):
802828
return pos
803829

804830
def truncate(self, pos=None):
831+
self._checkClosed()
832+
self._checkWritable()
833+
805834
# Flush the stream. We're mixing buffered I/O with lower-level I/O,
806835
# and a flush may be necessary to synch both views of the current
807836
# file state.
@@ -1571,7 +1600,7 @@ def __init__(self, file, mode='r', closefd=True, opener=None):
15711600
raise IsADirectoryError(errno.EISDIR,
15721601
os.strerror(errno.EISDIR), file)
15731602
except AttributeError:
1574-
# Ignore the AttribueError if stat.S_ISDIR or errno.EISDIR
1603+
# Ignore the AttributeError if stat.S_ISDIR or errno.EISDIR
15751604
# don't exist.
15761605
pass
15771606
self._blksize = getattr(fdfstat, 'st_blksize', 0)
@@ -1999,19 +2028,22 @@ class TextIOWrapper(TextIOBase):
19992028
def __init__(self, buffer, encoding=None, errors=None, newline=None,
20002029
line_buffering=False, write_through=False):
20012030
self._check_newline(newline)
2002-
if encoding is None:
2031+
encoding = text_encoding(encoding)
2032+
2033+
if encoding == "locale":
20032034
try:
2004-
encoding = os.device_encoding(buffer.fileno())
2035+
encoding = os.device_encoding(buffer.fileno()) or "locale"
20052036
except (AttributeError, UnsupportedOperation):
20062037
pass
2007-
if encoding is None:
2008-
try:
2009-
import locale
2010-
except ImportError:
2011-
# Importing locale may fail if Python is being built
2012-
encoding = "ascii"
2013-
else:
2014-
encoding = locale.getpreferredencoding(False)
2038+
2039+
if encoding == "locale":
2040+
try:
2041+
import locale
2042+
except ImportError:
2043+
# Importing locale may fail if Python is being built
2044+
encoding = "utf-8"
2045+
else:
2046+
encoding = locale.getpreferredencoding(False)
20152047

20162048
if not isinstance(encoding, str):
20172049
raise ValueError("invalid encoding: %r" % encoding)
@@ -2026,6 +2058,8 @@ def __init__(self, buffer, encoding=None, errors=None, newline=None,
20262058
else:
20272059
if not isinstance(errors, str):
20282060
raise ValueError("invalid errors: %r" % errors)
2061+
if _CHECK_ERRORS:
2062+
codecs.lookup_error(errors)
20292063

20302064
self._buffer = buffer
20312065
self._decoded_chars = '' # buffer for text returned from decoder
@@ -2295,7 +2329,7 @@ def _read_chunk(self):
22952329
return not eof
22962330

22972331
def _pack_cookie(self, position, dec_flags=0,
2298-
bytes_to_feed=0, need_eof=0, chars_to_skip=0):
2332+
bytes_to_feed=0, need_eof=False, chars_to_skip=0):
22992333
# The meaning of a tell() cookie is: seek to position, set the
23002334
# decoder flags to dec_flags, read bytes_to_feed bytes, feed them
23012335
# into the decoder with need_eof as the EOF flag, then skip
@@ -2309,7 +2343,7 @@ def _unpack_cookie(self, bigint):
23092343
rest, dec_flags = divmod(rest, 1<<64)
23102344
rest, bytes_to_feed = divmod(rest, 1<<64)
23112345
need_eof, chars_to_skip = divmod(rest, 1<<64)
2312-
return position, dec_flags, bytes_to_feed, need_eof, chars_to_skip
2346+
return position, dec_flags, bytes_to_feed, bool(need_eof), chars_to_skip
23132347

23142348
def tell(self):
23152349
if not self._seekable:
@@ -2383,7 +2417,7 @@ def tell(self):
23832417
# (a point where the decoder has nothing buffered, so seek()
23842418
# can safely start from there and advance to this location).
23852419
bytes_fed = 0
2386-
need_eof = 0
2420+
need_eof = False
23872421
# Chars decoded since `start_pos`
23882422
chars_decoded = 0
23892423
for i in range(skip_bytes, len(next_input)):
@@ -2400,7 +2434,7 @@ def tell(self):
24002434
else:
24012435
# We didn't get enough decoded data; signal EOF to get more.
24022436
chars_decoded += len(decoder.decode(b'', final=True))
2403-
need_eof = 1
2437+
need_eof = True
24042438
if chars_decoded < chars_to_skip:
24052439
raise OSError("can't reconstruct logical file position")
24062440

Lib/io.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,29 @@
5555
open, open_code, BytesIO, StringIO, BufferedReader,
5656
BufferedWriter, BufferedRWPair, BufferedRandom,
5757
# XXX RUSTPYTHON TODO: IncrementalNewlineDecoder
58-
# IncrementalNewlineDecoder, TextIOWrapper)
59-
TextIOWrapper)
58+
# IncrementalNewlineDecoder, text_encoding, TextIOWrapper)
59+
text_encoding, TextIOWrapper)
6060

6161
try:
6262
from _io import FileIO
6363
except ImportError:
6464
pass
6565

66-
OpenWrapper = _io.open # for compatibility with _pyio
66+
def __getattr__(name):
67+
if name == "OpenWrapper":
68+
# bpo-43680: Until Python 3.9, _pyio.open was not a static method and
69+
# builtins.open was set to OpenWrapper to not become a bound method
70+
# when set to a class variable. _io.open is a built-in function whereas
71+
# _pyio.open is a Python function. In Python 3.10, _pyio.open() is now
72+
# a static method, and builtins.open() is now io.open().
73+
import warnings
74+
warnings.warn('OpenWrapper is deprecated, use open instead',
75+
DeprecationWarning, stacklevel=2)
76+
global OpenWrapper
77+
OpenWrapper = open
78+
return OpenWrapper
79+
raise AttributeError(name)
80+
6781

6882
# Pretend this exception was created here.
6983
UnsupportedOperation.__module__ = "io"

Lib/os.py

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,8 @@ def fwalk(top=".", topdown=True, onerror=None, *, follow_symlinks=False, dir_fd=
461461
dirs.remove('CVS') # don't visit CVS directories
462462
"""
463463
sys.audit("os.fwalk", top, topdown, onerror, follow_symlinks, dir_fd)
464-
top = fspath(top)
464+
if not isinstance(top, int) or not hasattr(top, '__index__'):
465+
top = fspath(top)
465466
# Note: To guard against symlink races, we use the standard
466467
# lstat()/open()/fstat() trick.
467468
if not follow_symlinks:
@@ -703,11 +704,9 @@ def __len__(self):
703704
return len(self._data)
704705

705706
def __repr__(self):
706-
formatted_items = ", ".join(
707-
f"{self.decodekey(key)!r}: {self.decodevalue(value)!r}"
708-
for key, value in self._data.items()
709-
)
710-
return f"environ({{{formatted_items}}})"
707+
return 'environ({{{}}})'.format(', '.join(
708+
('{!r}: {!r}'.format(self.decodekey(key), self.decodevalue(value))
709+
for key, value in self._data.items())))
711710

712711
def copy(self):
713712
return dict(self)
@@ -973,7 +972,7 @@ def spawnlpe(mode, file, *args):
973972
# VxWorks has no user space shell provided. As a result, running
974973
# command in a shell can't be supported.
975974
if sys.platform != 'vxworks':
976-
# Supply os.popen()
975+
# Supply os.popen()
977976
def popen(cmd, mode="r", buffering=-1):
978977
if not isinstance(cmd, str):
979978
raise TypeError("invalid cmd type (%s, expected string)" % type(cmd))
@@ -995,28 +994,28 @@ def popen(cmd, mode="r", buffering=-1):
995994
bufsize=buffering)
996995
return _wrap_close(proc.stdin, proc)
997996

998-
# Helper for popen() -- a proxy for a file whose close waits for the process
999-
class _wrap_close:
1000-
def __init__(self, stream, proc):
1001-
self._stream = stream
1002-
self._proc = proc
1003-
def close(self):
1004-
self._stream.close()
1005-
returncode = self._proc.wait()
1006-
if returncode == 0:
1007-
return None
1008-
if name == 'nt':
1009-
return returncode
1010-
else:
1011-
return returncode << 8 # Shift left to match old behavior
1012-
def __enter__(self):
1013-
return self
1014-
def __exit__(self, *args):
1015-
self.close()
1016-
def __getattr__(self, name):
1017-
return getattr(self._stream, name)
1018-
def __iter__(self):
1019-
return iter(self._stream)
997+
# Helper for popen() -- a proxy for a file whose close waits for the process
998+
class _wrap_close:
999+
def __init__(self, stream, proc):
1000+
self._stream = stream
1001+
self._proc = proc
1002+
def close(self):
1003+
self._stream.close()
1004+
returncode = self._proc.wait()
1005+
if returncode == 0:
1006+
return None
1007+
if name == 'nt':
1008+
return returncode
1009+
else:
1010+
return returncode << 8 # Shift left to match old behavior
1011+
def __enter__(self):
1012+
return self
1013+
def __exit__(self, *args):
1014+
self.close()
1015+
def __getattr__(self, name):
1016+
return getattr(self._stream, name)
1017+
def __iter__(self):
1018+
return iter(self._stream)
10201019

10211020
__all__.append("popen")
10221021

@@ -1026,9 +1025,7 @@ def fdopen(fd, mode="r", buffering=-1, encoding=None, *args, **kwargs):
10261025
raise TypeError("invalid fd type (%s, expected integer)" % type(fd))
10271026
import io
10281027
if "b" not in mode:
1029-
# TODO: RUSTPYTHON (module 'io' has no attribute 'text_encoding')
1030-
# encoding = io.text_encoding(encoding)
1031-
pass
1028+
encoding = io.text_encoding(encoding)
10321029
return io.open(fd, mode, buffering, encoding, *args, **kwargs)
10331030

10341031

Lib/test/test_io.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,8 +1537,6 @@ def test_read_on_closed(self):
15371537
self.assertRaises(ValueError, b.peek)
15381538
self.assertRaises(ValueError, b.read1, 1)
15391539

1540-
# TODO: RUSTPYTHON, AssertionError: UnsupportedOperation not raised by truncate
1541-
@unittest.expectedFailure
15421540
def test_truncate_on_read_only(self):
15431541
rawio = self.MockFileIO(b"abc")
15441542
bufio = self.tp(rawio)
@@ -1625,6 +1623,11 @@ def test_bad_readinto_type(self):
16251623
def test_flush_error_on_close(self):
16261624
super().test_flush_error_on_close()
16271625

1626+
# TODO: RUSTPYTHON, AssertionError: UnsupportedOperation not raised by truncate
1627+
@unittest.expectedFailure
1628+
def test_truncate_on_read_only(self): # TODO: RUSTPYTHON, remove when this passes
1629+
super().test_truncate_on_read_only() # TODO: RUSTPYTHON, remove when this passes
1630+
16281631

16291632
class PyBufferedReaderTest(BufferedReaderTest):
16301633
tp = pyio.BufferedReader
@@ -4417,8 +4420,6 @@ def test_open_allargs(self):
44174420
# there used to be a buffer overflow in the parser for rawmode
44184421
self.assertRaises(ValueError, self.open, os_helper.TESTFN, 'rwax+', encoding="utf-8")
44194422

4420-
# TODO: RUSTPYTHON, AssertionError: 22 != 10 : _PythonRunResult(rc=22, out=b'', err=b'')
4421-
@unittest.expectedFailure
44224423
def test_check_encoding_errors(self):
44234424
# bpo-37388: open() and TextIOWrapper must check encoding and errors
44244425
# arguments in dev mode
@@ -4557,6 +4558,11 @@ def test_daemon_threads_shutdown_stdout_deadlock(self):
45574558
def test_daemon_threads_shutdown_stderr_deadlock(self):
45584559
self.check_daemon_threads_shutdown_deadlock('stderr')
45594560

4561+
# TODO: RUSTPYTHON, AssertionError: 22 != 10 : _PythonRunResult(rc=22, out=b'', err=b'')
4562+
@unittest.expectedFailure
4563+
def test_check_encoding_errors(self): # TODO: RUSTPYTHON, remove when this passes
4564+
super().test_check_encoding_errors() # TODO: RUSTPYTHON, remove when this passes
4565+
45604566

45614567
class PyMiscIOTest(MiscIOTest):
45624568
io = pyio

0 commit comments

Comments
 (0)