Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e033e06

Browse files
committed
Issue #10093: ResourceWarnings are now issued when files and sockets are
deallocated without explicit closing. These warnings are silenced by default, except in pydebug mode.
1 parent 9cbdd75 commit e033e06

10 files changed

Lines changed: 189 additions & 25 deletions

File tree

Lib/socket.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def __repr__(self):
108108
if s.startswith("<socket object"):
109109
s = "<%s.%s%s%s" % (self.__class__.__module__,
110110
self.__class__.__name__,
111-
(self._closed and " [closed] ") or "",
111+
getattr(self, '_closed', False) and " [closed] " or "",
112112
s[7:])
113113
return s
114114

Lib/test/test_io.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import abc
3030
import signal
3131
import errno
32+
import warnings
3233
from itertools import cycle, count
3334
from collections import deque
3435
from test import support
@@ -2525,6 +2526,46 @@ def test_abc_inheritance_official(self):
25252526
# baseline "io" module.
25262527
self._check_abc_inheritance(io)
25272528

2529+
def _check_warn_on_dealloc(self, *args, **kwargs):
2530+
f = open(*args, **kwargs)
2531+
r = repr(f)
2532+
with self.assertWarns(ResourceWarning) as cm:
2533+
f = None
2534+
support.gc_collect()
2535+
self.assertIn(r, str(cm.warning.args[0]))
2536+
2537+
def test_warn_on_dealloc(self):
2538+
self._check_warn_on_dealloc(support.TESTFN, "wb", buffering=0)
2539+
self._check_warn_on_dealloc(support.TESTFN, "wb")
2540+
self._check_warn_on_dealloc(support.TESTFN, "w")
2541+
2542+
def _check_warn_on_dealloc_fd(self, *args, **kwargs):
2543+
fds = []
2544+
try:
2545+
r, w = os.pipe()
2546+
fds += r, w
2547+
self._check_warn_on_dealloc(r, *args, **kwargs)
2548+
# When using closefd=False, there's no warning
2549+
r, w = os.pipe()
2550+
fds += r, w
2551+
with warnings.catch_warnings(record=True) as recorded:
2552+
open(r, *args, closefd=False, **kwargs)
2553+
support.gc_collect()
2554+
self.assertEqual(recorded, [])
2555+
finally:
2556+
for fd in fds:
2557+
try:
2558+
os.close(fd)
2559+
except EnvironmentError as e:
2560+
if e.errno != errno.EBADF:
2561+
raise
2562+
2563+
def test_warn_on_dealloc_fd(self):
2564+
self._check_warn_on_dealloc_fd("rb", buffering=0)
2565+
self._check_warn_on_dealloc_fd("rb")
2566+
self._check_warn_on_dealloc_fd("r")
2567+
2568+
25282569
class CMiscIOTest(MiscIOTest):
25292570
io = io
25302571

Lib/test/test_socket.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,23 @@ def test_sendall_interrupted(self):
706706
def test_sendall_interrupted_with_timeout(self):
707707
self.check_sendall_interrupted(True)
708708

709+
def test_dealloc_warn(self):
710+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
711+
r = repr(sock)
712+
with self.assertWarns(ResourceWarning) as cm:
713+
sock = None
714+
support.gc_collect()
715+
self.assertIn(r, str(cm.warning.args[0]))
716+
# An open socket file object gets dereferenced after the socket
717+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
718+
f = sock.makefile('rb')
719+
r = repr(sock)
720+
sock = None
721+
support.gc_collect()
722+
with self.assertWarns(ResourceWarning):
723+
f = None
724+
support.gc_collect()
725+
709726

710727
@unittest.skipUnless(thread, 'Threading required for this test.')
711728
class BasicTCPTest(SocketConnectedTest):

Lib/xml/etree/ElementTree.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -662,17 +662,23 @@ def _setroot(self, element):
662662
# @exception ParseError If the parser fails to parse the document.
663663

664664
def parse(self, source, parser=None):
665+
close_source = False
665666
if not hasattr(source, "read"):
666667
source = open(source, "rb")
667-
if not parser:
668-
parser = XMLParser(target=TreeBuilder())
669-
while 1:
670-
data = source.read(65536)
671-
if not data:
672-
break
673-
parser.feed(data)
674-
self._root = parser.close()
675-
return self._root
668+
close_source = True
669+
try:
670+
if not parser:
671+
parser = XMLParser(target=TreeBuilder())
672+
while 1:
673+
data = source.read(65536)
674+
if not data:
675+
break
676+
parser.feed(data)
677+
self._root = parser.close()
678+
return self._root
679+
finally:
680+
if close_source:
681+
source.close()
676682

677683
##
678684
# Creates a tree iterator for the root element. The iterator loops
@@ -1226,16 +1232,19 @@ def parse(source, parser=None):
12261232
# @return A (event, elem) iterator.
12271233

12281234
def iterparse(source, events=None, parser=None):
1235+
close_source = False
12291236
if not hasattr(source, "read"):
12301237
source = open(source, "rb")
1238+
close_source = True
12311239
if not parser:
12321240
parser = XMLParser(target=TreeBuilder())
1233-
return _IterParseIterator(source, events, parser)
1241+
return _IterParseIterator(source, events, parser, close_source)
12341242

12351243
class _IterParseIterator:
12361244

1237-
def __init__(self, source, events, parser):
1245+
def __init__(self, source, events, parser, close_source=False):
12381246
self._file = source
1247+
self._close_file = close_source
12391248
self._events = []
12401249
self._index = 0
12411250
self.root = self._root = None
@@ -1282,6 +1291,8 @@ def __next__(self):
12821291
except IndexError:
12831292
if self._parser is None:
12841293
self.root = self._root
1294+
if self._close_file:
1295+
self._file.close()
12851296
raise StopIteration
12861297
# load event buffer
12871298
del self._events[:]

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ Core and Builtins
5454
Library
5555
-------
5656

57+
- Issue #10093: ResourceWarnings are now issued when files and sockets are
58+
deallocated without explicit closing. These warnings are silenced by
59+
default, except in pydebug mode.
60+
5761
- tarfile.py: Add support for all missing variants of the GNU sparse
5862
extensions and create files with holes when extracting sparse members.
5963

Modules/_elementtree.c

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2946,19 +2946,25 @@ PyInit__elementtree(void)
29462946

29472947
"class ElementTree(ET.ElementTree):\n" /* public */
29482948
" def parse(self, source, parser=None):\n"
2949+
" close_source = False\n"
29492950
" if not hasattr(source, 'read'):\n"
29502951
" source = open(source, 'rb')\n"
2951-
" if parser is not None:\n"
2952-
" while 1:\n"
2953-
" data = source.read(65536)\n"
2954-
" if not data:\n"
2955-
" break\n"
2956-
" parser.feed(data)\n"
2957-
" self._root = parser.close()\n"
2958-
" else:\n"
2959-
" parser = cElementTree.XMLParser()\n"
2960-
" self._root = parser._parse(source)\n"
2961-
" return self._root\n"
2952+
" close_source = True\n"
2953+
" try:\n"
2954+
" if parser is not None:\n"
2955+
" while 1:\n"
2956+
" data = source.read(65536)\n"
2957+
" if not data:\n"
2958+
" break\n"
2959+
" parser.feed(data)\n"
2960+
" self._root = parser.close()\n"
2961+
" else:\n"
2962+
" parser = cElementTree.XMLParser()\n"
2963+
" self._root = parser._parse(source)\n"
2964+
" return self._root\n"
2965+
" finally:\n"
2966+
" if close_source:\n"
2967+
" source.close()\n"
29622968
"cElementTree.ElementTree = ElementTree\n"
29632969

29642970
"def iter(node, tag=None):\n" /* helper */
@@ -2988,8 +2994,10 @@ PyInit__elementtree(void)
29882994
"class iterparse:\n"
29892995
" root = None\n"
29902996
" def __init__(self, file, events=None):\n"
2997+
" self._close_file = False\n"
29912998
" if not hasattr(file, 'read'):\n"
29922999
" file = open(file, 'rb')\n"
3000+
" self._close_file = True\n"
29933001
" self._file = file\n"
29943002
" self._events = []\n"
29953003
" self._index = 0\n"
@@ -3004,6 +3012,8 @@ PyInit__elementtree(void)
30043012
" except IndexError:\n"
30053013
" if self._parser is None:\n"
30063014
" self.root = self._root\n"
3015+
" if self._close_file:\n"
3016+
" self._file.close()\n"
30073017
" raise StopIteration\n"
30083018
" # load event buffer\n"
30093019
" del self._events[:]\n"

Modules/_io/bufferedio.c

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ typedef struct {
197197
int detached;
198198
int readable;
199199
int writable;
200+
int deallocating;
200201

201202
/* True if this is a vanilla Buffered object (rather than a user derived
202203
class) *and* the raw stream is a vanilla FileIO object. */
@@ -342,6 +343,7 @@ typedef struct {
342343
static void
343344
buffered_dealloc(buffered *self)
344345
{
346+
self->deallocating = 1;
345347
if (self->ok && _PyIOBase_finalize((PyObject *) self) < 0)
346348
return;
347349
_PyObject_GC_UNTRACK(self);
@@ -382,6 +384,23 @@ buffered_clear(buffered *self)
382384
return 0;
383385
}
384386

387+
/* Because this can call arbitrary code, it shouldn't be called when
388+
the refcount is 0 (that is, not directly from tp_dealloc unless
389+
the refcount has been temporarily re-incremented). */
390+
PyObject *
391+
buffered_dealloc_warn(buffered *self, PyObject *source)
392+
{
393+
if (self->ok && self->raw) {
394+
PyObject *r;
395+
r = PyObject_CallMethod(self->raw, "_dealloc_warn", "O", source);
396+
if (r)
397+
Py_DECREF(r);
398+
else
399+
PyErr_Clear();
400+
}
401+
Py_RETURN_NONE;
402+
}
403+
385404
/*
386405
* _BufferedIOMixin methods
387406
* This is not a class, just a collection of methods that will be reused
@@ -435,6 +454,14 @@ buffered_close(buffered *self, PyObject *args)
435454
Py_INCREF(res);
436455
goto end;
437456
}
457+
458+
if (self->deallocating) {
459+
PyObject *r = buffered_dealloc_warn(self, (PyObject *) self);
460+
if (r)
461+
Py_DECREF(r);
462+
else
463+
PyErr_Clear();
464+
}
438465
/* flush() will most probably re-take the lock, so drop it first */
439466
LEAVE_BUFFERED(self)
440467
res = PyObject_CallMethodObjArgs((PyObject *)self, _PyIO_str_flush, NULL);
@@ -1461,6 +1488,7 @@ static PyMethodDef bufferedreader_methods[] = {
14611488
{"writable", (PyCFunction)buffered_writable, METH_NOARGS},
14621489
{"fileno", (PyCFunction)buffered_fileno, METH_NOARGS},
14631490
{"isatty", (PyCFunction)buffered_isatty, METH_NOARGS},
1491+
{"_dealloc_warn", (PyCFunction)buffered_dealloc_warn, METH_O},
14641492

14651493
{"read", (PyCFunction)buffered_read, METH_VARARGS},
14661494
{"peek", (PyCFunction)buffered_peek, METH_VARARGS},
@@ -1843,6 +1871,7 @@ static PyMethodDef bufferedwriter_methods[] = {
18431871
{"writable", (PyCFunction)buffered_writable, METH_NOARGS},
18441872
{"fileno", (PyCFunction)buffered_fileno, METH_NOARGS},
18451873
{"isatty", (PyCFunction)buffered_isatty, METH_NOARGS},
1874+
{"_dealloc_warn", (PyCFunction)buffered_dealloc_warn, METH_O},
18461875

18471876
{"write", (PyCFunction)bufferedwriter_write, METH_VARARGS},
18481877
{"truncate", (PyCFunction)buffered_truncate, METH_VARARGS},
@@ -2227,6 +2256,7 @@ static PyMethodDef bufferedrandom_methods[] = {
22272256
{"writable", (PyCFunction)buffered_writable, METH_NOARGS},
22282257
{"fileno", (PyCFunction)buffered_fileno, METH_NOARGS},
22292258
{"isatty", (PyCFunction)buffered_isatty, METH_NOARGS},
2259+
{"_dealloc_warn", (PyCFunction)buffered_dealloc_warn, METH_O},
22302260

22312261
{"flush", (PyCFunction)buffered_flush, METH_NOARGS},
22322262

@@ -2296,4 +2326,3 @@ PyTypeObject PyBufferedRandom_Type = {
22962326
0, /* tp_alloc */
22972327
PyType_GenericNew, /* tp_new */
22982328
};
2299-

Modules/_io/fileio.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#define PY_SSIZE_T_CLEAN
44
#include "Python.h"
5+
#include "structmember.h"
56
#ifdef HAVE_SYS_TYPES_H
67
#include <sys/types.h>
78
#endif
@@ -55,6 +56,7 @@ typedef struct {
5556
unsigned int writable : 1;
5657
signed int seekable : 2; /* -1 means unknown */
5758
unsigned int closefd : 1;
59+
unsigned int deallocating: 1;
5860
PyObject *weakreflist;
5961
PyObject *dict;
6062
} fileio;
@@ -69,6 +71,26 @@ _PyFileIO_closed(PyObject *self)
6971
return ((fileio *)self)->fd < 0;
7072
}
7173

74+
/* Because this can call arbitrary code, it shouldn't be called when
75+
the refcount is 0 (that is, not directly from tp_dealloc unless
76+
the refcount has been temporarily re-incremented). */
77+
static PyObject *
78+
fileio_dealloc_warn(fileio *self, PyObject *source)
79+
{
80+
if (self->fd >= 0 && self->closefd) {
81+
PyObject *exc, *val, *tb;
82+
PyErr_Fetch(&exc, &val, &tb);
83+
if (PyErr_WarnFormat(PyExc_ResourceWarning, 1,
84+
"unclosed file %R", source)) {
85+
/* Spurious errors can appear at shutdown */
86+
if (PyErr_ExceptionMatches(PyExc_Warning))
87+
PyErr_WriteUnraisable((PyObject *) self);
88+
}
89+
PyErr_Restore(exc, val, tb);
90+
}
91+
Py_RETURN_NONE;
92+
}
93+
7294
static PyObject *
7395
portable_lseek(int fd, PyObject *posobj, int whence);
7496

@@ -110,6 +132,13 @@ fileio_close(fileio *self)
110132
self->fd = -1;
111133
Py_RETURN_NONE;
112134
}
135+
if (self->deallocating) {
136+
PyObject *r = fileio_dealloc_warn(self, (PyObject *) self);
137+
if (r)
138+
Py_DECREF(r);
139+
else
140+
PyErr_Clear();
141+
}
113142
errno = internal_close(self);
114143
if (errno < 0)
115144
return NULL;
@@ -399,6 +428,7 @@ fileio_clear(fileio *self)
399428
static void
400429
fileio_dealloc(fileio *self)
401430
{
431+
self->deallocating = 1;
402432
if (_PyIOBase_finalize((PyObject *) self) < 0)
403433
return;
404434
_PyObject_GC_UNTRACK(self);
@@ -1008,6 +1038,7 @@ static PyMethodDef fileio_methods[] = {
10081038
{"writable", (PyCFunction)fileio_writable, METH_NOARGS, writable_doc},
10091039
{"fileno", (PyCFunction)fileio_fileno, METH_NOARGS, fileno_doc},
10101040
{"isatty", (PyCFunction)fileio_isatty, METH_NOARGS, isatty_doc},
1041+
{"_dealloc_warn", (PyCFunction)fileio_dealloc_warn, METH_O, NULL},
10111042
{NULL, NULL} /* sentinel */
10121043
};
10131044

0 commit comments

Comments
 (0)