Skip to content

Commit 4ca9fc0

Browse files
authored
gh-111495: Add PyFile tests (#129449)
Add tests for the following functions in test_capi.test_file: * PyFile_FromFd() * PyFile_GetLine() * PyFile_NewStdPrinter() * PyFile_WriteObject() * PyFile_WriteString() * PyObject_AsFileDescriptor() Add Modules/_testlimitedcapi/file.c file. Remove test_embed.StdPrinterTests which became redundant.
1 parent 4e47e05 commit 4ca9fc0

File tree

11 files changed

+500
-72
lines changed

11 files changed

+500
-72
lines changed

Lib/test/test_capi/test_file.py

+235-14
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,242 @@
1+
import io
12
import os
23
import unittest
4+
import warnings
35
from test import support
4-
from test.support import import_helper, os_helper
6+
from test.support import import_helper, os_helper, warnings_helper
57

6-
_testcapi = import_helper.import_module('_testcapi')
78

9+
_testcapi = import_helper.import_module('_testcapi')
10+
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
11+
_io = import_helper.import_module('_io')
812
NULL = None
13+
STDOUT_FD = 1
14+
15+
with open(__file__, 'rb') as fp:
16+
FIRST_LINE = next(fp).decode()
17+
FIRST_LINE_NORM = FIRST_LINE.rstrip() + '\n'
918

1019

1120
class CAPIFileTest(unittest.TestCase):
21+
def test_pyfile_fromfd(self):
22+
# Test PyFile_FromFd() which is a thin wrapper to _io.open()
23+
pyfile_fromfd = _testlimitedcapi.pyfile_fromfd
24+
filename = __file__
25+
with open(filename, "rb") as fp:
26+
fd = fp.fileno()
27+
28+
# FileIO
29+
fp.seek(0)
30+
obj = pyfile_fromfd(fd, filename, "rb", 0, NULL, NULL, NULL, 0)
31+
try:
32+
self.assertIsInstance(obj, _io.FileIO)
33+
self.assertEqual(obj.readline(), FIRST_LINE.encode())
34+
finally:
35+
obj.close()
36+
37+
# BufferedReader
38+
fp.seek(0)
39+
obj = pyfile_fromfd(fd, filename, "rb", 1024, NULL, NULL, NULL, 0)
40+
try:
41+
self.assertIsInstance(obj, _io.BufferedReader)
42+
self.assertEqual(obj.readline(), FIRST_LINE.encode())
43+
finally:
44+
obj.close()
45+
46+
# TextIOWrapper
47+
fp.seek(0)
48+
obj = pyfile_fromfd(fd, filename, "r", 1,
49+
"utf-8", "replace", NULL, 0)
50+
try:
51+
self.assertIsInstance(obj, _io.TextIOWrapper)
52+
self.assertEqual(obj.encoding, "utf-8")
53+
self.assertEqual(obj.errors, "replace")
54+
self.assertEqual(obj.readline(), FIRST_LINE_NORM)
55+
finally:
56+
obj.close()
57+
58+
def test_pyfile_getline(self):
59+
# Test PyFile_GetLine(file, n): call file.readline()
60+
# and strip "\n" suffix if n < 0.
61+
pyfile_getline = _testlimitedcapi.pyfile_getline
62+
63+
# Test Unicode
64+
with open(__file__, "r") as fp:
65+
fp.seek(0)
66+
self.assertEqual(pyfile_getline(fp, -1),
67+
FIRST_LINE_NORM.rstrip('\n'))
68+
fp.seek(0)
69+
self.assertEqual(pyfile_getline(fp, 0),
70+
FIRST_LINE_NORM)
71+
fp.seek(0)
72+
self.assertEqual(pyfile_getline(fp, 6),
73+
FIRST_LINE_NORM[:6])
74+
75+
# Test bytes
76+
with open(__file__, "rb") as fp:
77+
fp.seek(0)
78+
self.assertEqual(pyfile_getline(fp, -1),
79+
FIRST_LINE.rstrip('\n').encode())
80+
fp.seek(0)
81+
self.assertEqual(pyfile_getline(fp, 0),
82+
FIRST_LINE.encode())
83+
fp.seek(0)
84+
self.assertEqual(pyfile_getline(fp, 6),
85+
FIRST_LINE.encode()[:6])
86+
87+
def test_pyfile_writestring(self):
88+
# Test PyFile_WriteString(str, file): call file.write(str)
89+
writestr = _testlimitedcapi.pyfile_writestring
90+
91+
with io.StringIO() as fp:
92+
self.assertEqual(writestr("a\xe9\u20ac\U0010FFFF".encode(), fp), 0)
93+
with self.assertRaises(UnicodeDecodeError):
94+
writestr(b"\xff", fp)
95+
with self.assertRaises(UnicodeDecodeError):
96+
writestr("\udc80".encode("utf-8", "surrogatepass"), fp)
97+
98+
text = fp.getvalue()
99+
self.assertEqual(text, "a\xe9\u20ac\U0010FFFF")
100+
101+
with self.assertRaises(SystemError):
102+
writestr(b"abc", NULL)
103+
104+
def test_pyfile_writeobject(self):
105+
# Test PyFile_WriteObject(obj, file, flags):
106+
# - Call file.write(str(obj)) if flags equals Py_PRINT_RAW.
107+
# - Call file.write(repr(obj)) otherwise.
108+
writeobject = _testlimitedcapi.pyfile_writeobject
109+
Py_PRINT_RAW = 1
110+
111+
with io.StringIO() as fp:
112+
# Test flags=Py_PRINT_RAW
113+
self.assertEqual(writeobject("raw", fp, Py_PRINT_RAW), 0)
114+
writeobject(NULL, fp, Py_PRINT_RAW)
115+
116+
# Test flags=0
117+
self.assertEqual(writeobject("repr", fp, 0), 0)
118+
writeobject(NULL, fp, 0)
119+
120+
text = fp.getvalue()
121+
self.assertEqual(text, "raw<NULL>'repr'<NULL>")
122+
123+
# invalid file type
124+
for invalid_file in (123, "abc", object()):
125+
with self.subTest(file=invalid_file):
126+
with self.assertRaises(AttributeError):
127+
writeobject("abc", invalid_file, Py_PRINT_RAW)
128+
129+
with self.assertRaises(TypeError):
130+
writeobject("abc", NULL, 0)
131+
132+
def test_pyobject_asfiledescriptor(self):
133+
# Test PyObject_AsFileDescriptor(obj):
134+
# - Return obj if obj is an integer.
135+
# - Return obj.fileno() otherwise.
136+
# File descriptor must be >= 0.
137+
asfd = _testlimitedcapi.pyobject_asfiledescriptor
138+
139+
self.assertEqual(asfd(123), 123)
140+
self.assertEqual(asfd(0), 0)
141+
142+
with open(__file__, "rb") as fp:
143+
self.assertEqual(asfd(fp), fp.fileno())
144+
145+
# bool emits RuntimeWarning
146+
msg = r"bool is used as a file descriptor"
147+
with warnings_helper.check_warnings((msg, RuntimeWarning)):
148+
self.assertEqual(asfd(True), 1)
149+
150+
class FakeFile:
151+
def __init__(self, fd):
152+
self.fd = fd
153+
def fileno(self):
154+
return self.fd
155+
156+
# file descriptor must be positive
157+
with self.assertRaises(ValueError):
158+
asfd(-1)
159+
with self.assertRaises(ValueError):
160+
asfd(FakeFile(-1))
161+
162+
# fileno() result must be an integer
163+
with self.assertRaises(TypeError):
164+
asfd(FakeFile("text"))
165+
166+
# unsupported types
167+
for obj in ("string", ["list"], object()):
168+
with self.subTest(obj=obj):
169+
with self.assertRaises(TypeError):
170+
asfd(obj)
171+
172+
# CRASHES asfd(NULL)
173+
174+
def test_pyfile_newstdprinter(self):
175+
# Test PyFile_NewStdPrinter()
176+
pyfile_newstdprinter = _testcapi.pyfile_newstdprinter
177+
178+
file = pyfile_newstdprinter(STDOUT_FD)
179+
self.assertEqual(file.closed, False)
180+
self.assertIsNone(file.encoding)
181+
self.assertEqual(file.mode, "w")
182+
183+
self.assertEqual(file.fileno(), STDOUT_FD)
184+
self.assertEqual(file.isatty(), os.isatty(STDOUT_FD))
185+
186+
# flush() is a no-op
187+
self.assertIsNone(file.flush())
188+
189+
# close() is a no-op
190+
self.assertIsNone(file.close())
191+
self.assertEqual(file.closed, False)
192+
193+
support.check_disallow_instantiation(self, type(file))
194+
195+
def test_pyfile_newstdprinter_write(self):
196+
# Test the write() method of PyFile_NewStdPrinter()
197+
pyfile_newstdprinter = _testcapi.pyfile_newstdprinter
198+
199+
filename = os_helper.TESTFN
200+
self.addCleanup(os_helper.unlink, filename)
201+
202+
try:
203+
old_stdout = os.dup(STDOUT_FD)
204+
except OSError as exc:
205+
# os.dup(STDOUT_FD) is not supported on WASI
206+
self.skipTest(f"os.dup() failed with {exc!r}")
207+
208+
try:
209+
with open(filename, "wb") as fp:
210+
# PyFile_NewStdPrinter() only accepts fileno(stdout)
211+
# or fileno(stderr) file descriptor.
212+
fd = fp.fileno()
213+
os.dup2(fd, STDOUT_FD)
214+
215+
file = pyfile_newstdprinter(STDOUT_FD)
216+
self.assertEqual(file.write("text"), 4)
217+
# The surrogate character is encoded with
218+
# the "surrogateescape" error handler
219+
self.assertEqual(file.write("[\udc80]"), 8)
220+
finally:
221+
os.dup2(old_stdout, STDOUT_FD)
222+
os.close(old_stdout)
223+
224+
with open(filename, "r") as fp:
225+
self.assertEqual(fp.read(), "text[\\udc80]")
226+
12227
def test_py_fopen(self):
13228
# Test Py_fopen() and Py_fclose()
229+
py_fopen = _testcapi.py_fopen
14230

15231
with open(__file__, "rb") as fp:
16232
source = fp.read()
17233

18234
for filename in (__file__, os.fsencode(__file__)):
19235
with self.subTest(filename=filename):
20-
data = _testcapi.py_fopen(filename, "rb")
236+
data = py_fopen(filename, "rb")
21237
self.assertEqual(data, source[:256])
22238

23-
data = _testcapi.py_fopen(os_helper.FakePath(filename), "rb")
239+
data = py_fopen(os_helper.FakePath(filename), "rb")
24240
self.assertEqual(data, source[:256])
25241

26242
filenames = [
@@ -43,41 +259,46 @@ def test_py_fopen(self):
43259
filename = None
44260
continue
45261
try:
46-
data = _testcapi.py_fopen(filename, "rb")
262+
data = py_fopen(filename, "rb")
47263
self.assertEqual(data, source[:256])
48264
finally:
49265
os_helper.unlink(filename)
50266

51267
# embedded null character/byte in the filename
52268
with self.assertRaises(ValueError):
53-
_testcapi.py_fopen("a\x00b", "rb")
269+
py_fopen("a\x00b", "rb")
54270
with self.assertRaises(ValueError):
55-
_testcapi.py_fopen(b"a\x00b", "rb")
271+
py_fopen(b"a\x00b", "rb")
56272

57273
# non-ASCII mode failing with "Invalid argument"
58274
with self.assertRaises(OSError):
59-
_testcapi.py_fopen(__file__, b"\xc2\x80")
275+
py_fopen(__file__, b"\xc2\x80")
60276
with self.assertRaises(OSError):
61277
# \x98 is invalid in cp1250, cp1251, cp1257
62278
# \x9d is invalid in cp1252-cp1255, cp1258
63-
_testcapi.py_fopen(__file__, b"\xc2\x98\xc2\x9d")
279+
py_fopen(__file__, b"\xc2\x98\xc2\x9d")
64280
# UnicodeDecodeError can come from the audit hook code
65281
with self.assertRaises((UnicodeDecodeError, OSError)):
66-
_testcapi.py_fopen(__file__, b"\x98\x9d")
282+
py_fopen(__file__, b"\x98\x9d")
67283

68284
# invalid filename type
69285
for invalid_type in (123, object()):
70286
with self.subTest(filename=invalid_type):
71287
with self.assertRaises(TypeError):
72-
_testcapi.py_fopen(invalid_type, "rb")
288+
py_fopen(invalid_type, "rb")
73289

74290
if support.MS_WINDOWS:
75291
with self.assertRaises(OSError):
76292
# On Windows, the file mode is limited to 10 characters
77-
_testcapi.py_fopen(__file__, "rt+, ccs=UTF-8")
293+
py_fopen(__file__, "rt+, ccs=UTF-8")
294+
295+
# CRASHES py_fopen(NULL, 'rb')
296+
# CRASHES py_fopen(__file__, NULL)
297+
298+
# TODO: Test Py_UniversalNewlineFgets()
78299

79-
# CRASHES _testcapi.py_fopen(NULL, 'rb')
80-
# CRASHES _testcapi.py_fopen(__file__, NULL)
300+
# PyFile_SetOpenCodeHook() and PyFile_OpenCode() are tested by
301+
# test_embed.test_open_code_hook()
81302

82303

83304
if __name__ == "__main__":

Lib/test/test_embed.py

-51
Original file line numberDiff line numberDiff line change
@@ -1985,56 +1985,5 @@ def test_presite(self):
19851985
self.assertIn("unique-python-message", out)
19861986

19871987

1988-
class StdPrinterTests(EmbeddingTestsMixin, unittest.TestCase):
1989-
# Test PyStdPrinter_Type which is used by _PySys_SetPreliminaryStderr():
1990-
# "Set up a preliminary stderr printer until we have enough
1991-
# infrastructure for the io module in place."
1992-
1993-
STDOUT_FD = 1
1994-
1995-
def create_printer(self, fd):
1996-
ctypes = import_helper.import_module('ctypes')
1997-
PyFile_NewStdPrinter = ctypes.pythonapi.PyFile_NewStdPrinter
1998-
PyFile_NewStdPrinter.argtypes = (ctypes.c_int,)
1999-
PyFile_NewStdPrinter.restype = ctypes.py_object
2000-
return PyFile_NewStdPrinter(fd)
2001-
2002-
def test_write(self):
2003-
message = "unicode:\xe9-\u20ac-\udc80!\n"
2004-
2005-
stdout_fd = self.STDOUT_FD
2006-
stdout_fd_copy = os.dup(stdout_fd)
2007-
self.addCleanup(os.close, stdout_fd_copy)
2008-
2009-
rfd, wfd = os.pipe()
2010-
self.addCleanup(os.close, rfd)
2011-
self.addCleanup(os.close, wfd)
2012-
try:
2013-
# PyFile_NewStdPrinter() only accepts fileno(stdout)
2014-
# or fileno(stderr) file descriptor.
2015-
os.dup2(wfd, stdout_fd)
2016-
2017-
printer = self.create_printer(stdout_fd)
2018-
printer.write(message)
2019-
finally:
2020-
os.dup2(stdout_fd_copy, stdout_fd)
2021-
2022-
data = os.read(rfd, 100)
2023-
self.assertEqual(data, message.encode('utf8', 'backslashreplace'))
2024-
2025-
def test_methods(self):
2026-
fd = self.STDOUT_FD
2027-
printer = self.create_printer(fd)
2028-
self.assertEqual(printer.fileno(), fd)
2029-
self.assertEqual(printer.isatty(), os.isatty(fd))
2030-
printer.flush() # noop
2031-
printer.close() # noop
2032-
2033-
def test_disallow_instantiation(self):
2034-
fd = self.STDOUT_FD
2035-
printer = self.create_printer(fd)
2036-
support.check_disallow_instantiation(self, type(printer))
2037-
2038-
20391988
if __name__ == "__main__":
20401989
unittest.main()

Modules/Setup.stdlib.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@
163163
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
164164
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
165165
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c
166-
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c
166+
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c
167167
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
168168
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
169169

0 commit comments

Comments
 (0)