Skip to content

Commit 9a59a51

Browse files
authored
[3.13] gh-111495: Add PyFile tests (#129449) (#129477)
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. (cherry picked from commit 4ca9fc0)
1 parent a853e2f commit 9a59a51

File tree

11 files changed

+506
-57
lines changed

11 files changed

+506
-57
lines changed

Lib/test/test_capi/test_file.py

+234
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import io
2+
import os
3+
import unittest
4+
import warnings
5+
from test import support
6+
from test.support import import_helper, os_helper, warnings_helper
7+
8+
9+
_testcapi = import_helper.import_module('_testcapi')
10+
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
11+
_io = import_helper.import_module('_io')
12+
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'
18+
19+
20+
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+
227+
# TODO: Test Py_UniversalNewlineFgets()
228+
229+
# PyFile_SetOpenCodeHook() and PyFile_OpenCode() are tested by
230+
# test_embed.test_open_code_hook()
231+
232+
233+
if __name__ == "__main__":
234+
unittest.main()

Lib/test/test_embed.py

-51
Original file line numberDiff line numberDiff line change
@@ -1955,56 +1955,5 @@ def test_presite(self):
19551955
self.assertIn("cmd", out)
19561956

19571957

1958-
class StdPrinterTests(EmbeddingTestsMixin, unittest.TestCase):
1959-
# Test PyStdPrinter_Type which is used by _PySys_SetPreliminaryStderr():
1960-
# "Set up a preliminary stderr printer until we have enough
1961-
# infrastructure for the io module in place."
1962-
1963-
STDOUT_FD = 1
1964-
1965-
def create_printer(self, fd):
1966-
ctypes = import_helper.import_module('ctypes')
1967-
PyFile_NewStdPrinter = ctypes.pythonapi.PyFile_NewStdPrinter
1968-
PyFile_NewStdPrinter.argtypes = (ctypes.c_int,)
1969-
PyFile_NewStdPrinter.restype = ctypes.py_object
1970-
return PyFile_NewStdPrinter(fd)
1971-
1972-
def test_write(self):
1973-
message = "unicode:\xe9-\u20ac-\udc80!\n"
1974-
1975-
stdout_fd = self.STDOUT_FD
1976-
stdout_fd_copy = os.dup(stdout_fd)
1977-
self.addCleanup(os.close, stdout_fd_copy)
1978-
1979-
rfd, wfd = os.pipe()
1980-
self.addCleanup(os.close, rfd)
1981-
self.addCleanup(os.close, wfd)
1982-
try:
1983-
# PyFile_NewStdPrinter() only accepts fileno(stdout)
1984-
# or fileno(stderr) file descriptor.
1985-
os.dup2(wfd, stdout_fd)
1986-
1987-
printer = self.create_printer(stdout_fd)
1988-
printer.write(message)
1989-
finally:
1990-
os.dup2(stdout_fd_copy, stdout_fd)
1991-
1992-
data = os.read(rfd, 100)
1993-
self.assertEqual(data, message.encode('utf8', 'backslashreplace'))
1994-
1995-
def test_methods(self):
1996-
fd = self.STDOUT_FD
1997-
printer = self.create_printer(fd)
1998-
self.assertEqual(printer.fileno(), fd)
1999-
self.assertEqual(printer.isatty(), os.isatty(fd))
2000-
printer.flush() # noop
2001-
printer.close() # noop
2002-
2003-
def test_disallow_instantiation(self):
2004-
fd = self.STDOUT_FD
2005-
printer = self.create_printer(fd)
2006-
support.check_disallow_instantiation(self, type(printer))
2007-
2008-
20091958
if __name__ == "__main__":
20101959
unittest.main()

Modules/Setup.stdlib.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
165165
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
166166
@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
167-
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.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
167+
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.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/file.c
168168
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
169169
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
170170

Modules/_testcapi/clinic/file.c.h

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

Modules/_testcapi/file.c

+25-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,37 @@
11
#include "parts.h"
22
#include "util.h"
3+
#include "clinic/file.c.h"
4+
5+
6+
/*[clinic input]
7+
module _testcapi
8+
[clinic start generated code]*/
9+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=6361033e795369fc]*/
10+
11+
12+
/*[clinic input]
13+
_testcapi.pyfile_newstdprinter
14+
15+
fd: int
16+
/
17+
18+
[clinic start generated code]*/
19+
20+
static PyObject *
21+
_testcapi_pyfile_newstdprinter_impl(PyObject *module, int fd)
22+
/*[clinic end generated code: output=8a2d1c57b6892db3 input=442f1824142262ea]*/
23+
{
24+
return PyFile_NewStdPrinter(fd);
25+
}
326

427

528
static PyMethodDef test_methods[] = {
29+
_TESTCAPI_PYFILE_NEWSTDPRINTER_METHODDEF
630
{NULL},
731
};
832

933
int
1034
_PyTestCapi_Init_File(PyObject *m)
1135
{
12-
if (PyModule_AddFunctions(m, test_methods) < 0){
13-
return -1;
14-
}
15-
16-
return 0;
36+
return PyModule_AddFunctions(m, test_methods);
1737
}

Modules/_testlimitedcapi.c

+3
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,8 @@ PyInit__testlimitedcapi(void)
8383
if (_PyTestLimitedCAPI_Init_VectorcallLimited(mod) < 0) {
8484
return NULL;
8585
}
86+
if (_PyTestLimitedCAPI_Init_File(mod) < 0) {
87+
return NULL;
88+
}
8689
return mod;
8790
}

0 commit comments

Comments
 (0)