Skip to content

Commit c03e053

Browse files
committed
pythongh-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 c03e053

File tree

11 files changed

+468
-71
lines changed

11 files changed

+468
-71
lines changed

Lib/test/test_capi/test_file.py

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

15200
with open(__file__, "rb") as fp:
16201
source = fp.read()
17202

18203
for filename in (__file__, os.fsencode(__file__)):
19204
with self.subTest(filename=filename):
20-
data = _testcapi.py_fopen(filename, "rb")
205+
data = py_fopen(filename, "rb")
21206
self.assertEqual(data, source[:256])
22207

23-
data = _testcapi.py_fopen(os_helper.FakePath(filename), "rb")
208+
data = py_fopen(os_helper.FakePath(filename), "rb")
24209
self.assertEqual(data, source[:256])
25210

26211
filenames = [
@@ -43,41 +228,46 @@ def test_py_fopen(self):
43228
filename = None
44229
continue
45230
try:
46-
data = _testcapi.py_fopen(filename, "rb")
231+
data = py_fopen(filename, "rb")
47232
self.assertEqual(data, source[:256])
48233
finally:
49234
os_helper.unlink(filename)
50235

51236
# embedded null character/byte in the filename
52237
with self.assertRaises(ValueError):
53-
_testcapi.py_fopen("a\x00b", "rb")
238+
py_fopen("a\x00b", "rb")
54239
with self.assertRaises(ValueError):
55-
_testcapi.py_fopen(b"a\x00b", "rb")
240+
py_fopen(b"a\x00b", "rb")
56241

57242
# non-ASCII mode failing with "Invalid argument"
58243
with self.assertRaises(OSError):
59-
_testcapi.py_fopen(__file__, b"\xc2\x80")
244+
py_fopen(__file__, b"\xc2\x80")
60245
with self.assertRaises(OSError):
61246
# \x98 is invalid in cp1250, cp1251, cp1257
62247
# \x9d is invalid in cp1252-cp1255, cp1258
63-
_testcapi.py_fopen(__file__, b"\xc2\x98\xc2\x9d")
248+
py_fopen(__file__, b"\xc2\x98\xc2\x9d")
64249
# UnicodeDecodeError can come from the audit hook code
65250
with self.assertRaises((UnicodeDecodeError, OSError)):
66-
_testcapi.py_fopen(__file__, b"\x98\x9d")
251+
py_fopen(__file__, b"\x98\x9d")
67252

68253
# invalid filename type
69254
for invalid_type in (123, object()):
70255
with self.subTest(filename=invalid_type):
71256
with self.assertRaises(TypeError):
72-
_testcapi.py_fopen(invalid_type, "rb")
257+
py_fopen(invalid_type, "rb")
73258

74259
if support.MS_WINDOWS:
75260
with self.assertRaises(OSError):
76261
# On Windows, the file mode is limited to 10 characters
77-
_testcapi.py_fopen(__file__, "rt+, ccs=UTF-8")
262+
py_fopen(__file__, "rt+, ccs=UTF-8")
263+
264+
# CRASHES py_fopen(NULL, 'rb')
265+
# CRASHES py_fopen(__file__, NULL)
266+
267+
# TODO: Test Py_UniversalNewlineFgets()
78268

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

82272

83273
if __name__ == "__main__":

Lib/test/test_embed.py

-51
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

+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
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

+28-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)