Skip to content

Commit 5156f46

Browse files
committed
gh-111495: Add PyFile tests
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 now redundant test_embed.StdPrinterTests.
1 parent 3a974e3 commit 5156f46

File tree

11 files changed

+477
-71
lines changed

11 files changed

+477
-71
lines changed

Lib/test/test_capi/test_file.py

Lines changed: 212 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,220 @@
1+
import io
12
import os
23
import unittest
4+
import warnings
35
from test import support
46
from test.support import import_helper, os_helper
57

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

9+
FIRST_LINE = 'import io\n' # First line of this file
10+
_testcapi = import_helper.import_module('_testcapi')
11+
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
12+
_io = import_helper.import_module('_io')
813
NULL = None
914

1015

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

15209
with open(__file__, "rb") as fp:
16210
source = fp.read()
17211

18212
for filename in (__file__, os.fsencode(__file__)):
19213
with self.subTest(filename=filename):
20-
data = _testcapi.py_fopen(filename, "rb")
214+
data = py_fopen(filename, "rb")
21215
self.assertEqual(data, source[:256])
22216

23-
data = _testcapi.py_fopen(os_helper.FakePath(filename), "rb")
217+
data = py_fopen(os_helper.FakePath(filename), "rb")
24218
self.assertEqual(data, source[:256])
25219

26220
filenames = [
@@ -43,41 +237,46 @@ def test_py_fopen(self):
43237
filename = None
44238
continue
45239
try:
46-
data = _testcapi.py_fopen(filename, "rb")
240+
data = py_fopen(filename, "rb")
47241
self.assertEqual(data, source[:256])
48242
finally:
49243
os_helper.unlink(filename)
50244

51245
# embedded null character/byte in the filename
52246
with self.assertRaises(ValueError):
53-
_testcapi.py_fopen("a\x00b", "rb")
247+
py_fopen("a\x00b", "rb")
54248
with self.assertRaises(ValueError):
55-
_testcapi.py_fopen(b"a\x00b", "rb")
249+
py_fopen(b"a\x00b", "rb")
56250

57251
# non-ASCII mode failing with "Invalid argument"
58252
with self.assertRaises(OSError):
59-
_testcapi.py_fopen(__file__, b"\xc2\x80")
253+
py_fopen(__file__, b"\xc2\x80")
60254
with self.assertRaises(OSError):
61255
# \x98 is invalid in cp1250, cp1251, cp1257
62256
# \x9d is invalid in cp1252-cp1255, cp1258
63-
_testcapi.py_fopen(__file__, b"\xc2\x98\xc2\x9d")
257+
py_fopen(__file__, b"\xc2\x98\xc2\x9d")
64258
# UnicodeDecodeError can come from the audit hook code
65259
with self.assertRaises((UnicodeDecodeError, OSError)):
66-
_testcapi.py_fopen(__file__, b"\x98\x9d")
260+
py_fopen(__file__, b"\x98\x9d")
67261

68262
# invalid filename type
69263
for invalid_type in (123, object()):
70264
with self.subTest(filename=invalid_type):
71265
with self.assertRaises(TypeError):
72-
_testcapi.py_fopen(invalid_type, "rb")
266+
py_fopen(invalid_type, "rb")
73267

74268
if support.MS_WINDOWS:
75269
with self.assertRaises(OSError):
76270
# On Windows, the file mode is limited to 10 characters
77-
_testcapi.py_fopen(__file__, "rt+, ccs=UTF-8")
271+
py_fopen(__file__, "rt+, ccs=UTF-8")
272+
273+
# CRASHES py_fopen(NULL, 'rb')
274+
# CRASHES py_fopen(__file__, NULL)
275+
276+
# TODO: Test Py_UniversalNewlineFgets()
78277

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

82281

83282
if __name__ == "__main__":

Lib/test/test_embed.py

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,56 +1983,5 @@ def test_presite(self):
19831983
self.assertIn("unique-python-message", out)
19841984

19851985

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

Modules/Setup.stdlib.in

Lines changed: 1 addition & 1 deletion
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
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

Modules/_testcapi/clinic/file.c.h

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)