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

Skip to content

Commit ffe96ae

Browse files
Issue #25994: Added the close() method and the support of the context manager
protocol for the os.scandir() iterator.
1 parent 2feb642 commit ffe96ae

6 files changed

Lines changed: 211 additions & 49 deletions

File tree

Doc/library/os.rst

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,14 +1891,29 @@ features:
18911891
:attr:`~DirEntry.path` attributes of each :class:`DirEntry` will be of
18921892
the same type as *path*.
18931893

1894+
The :func:`scandir` iterator supports the :term:`context manager` protocol
1895+
and has the following method:
1896+
1897+
.. method:: scandir.close()
1898+
1899+
Close the iterator and free acquired resources.
1900+
1901+
This is called automatically when the iterator is exhausted or garbage
1902+
collected, or when an error happens during iterating. However it
1903+
is advisable to call it explicitly or use the :keyword:`with`
1904+
statement.
1905+
1906+
.. versionadded:: 3.6
1907+
18941908
The following example shows a simple use of :func:`scandir` to display all
18951909
the files (excluding directories) in the given *path* that don't start with
18961910
``'.'``. The ``entry.is_file()`` call will generally not make an additional
18971911
system call::
18981912

1899-
for entry in os.scandir(path):
1900-
if not entry.name.startswith('.') and entry.is_file():
1901-
print(entry.name)
1913+
with os.scandir(path) as it:
1914+
for entry in it:
1915+
if not entry.name.startswith('.') and entry.is_file():
1916+
print(entry.name)
19021917

19031918
.. note::
19041919

@@ -1914,6 +1929,12 @@ features:
19141929

19151930
.. versionadded:: 3.5
19161931

1932+
.. versionadded:: 3.6
1933+
Added support for the :term:`context manager` protocol and the
1934+
:func:`~scandir.close()` method. If a :func:`scandir` iterator is neither
1935+
exhausted nor explicitly closed a :exc:`ResourceWarning` will be emitted
1936+
in its destructor.
1937+
19171938

19181939
.. class:: DirEntry
19191940

Doc/whatsnew/3.6.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,17 @@ directives ``%G``, ``%u`` and ``%V``.
104104
(Contributed by Ashley Anderson in :issue:`12006`.)
105105

106106

107+
os
108+
--
109+
110+
A new :meth:`~os.scandir.close` method allows explicitly closing a
111+
:func:`~os.scandir` iterator. The :func:`~os.scandir` iterator now
112+
supports the :term:`context manager` protocol. If a :func:`scandir`
113+
iterator is neither exhausted nor explicitly closed a :exc:`ResourceWarning`
114+
will be emitted in its destructor.
115+
(Contributed by Serhiy Storchaka in :issue:`25994`.)
116+
117+
107118
pickle
108119
------
109120

Lib/os.py

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -374,46 +374,47 @@ def walk(top, topdown=True, onerror=None, followlinks=False):
374374
onerror(error)
375375
return
376376

377-
while True:
378-
try:
377+
with scandir_it:
378+
while True:
379379
try:
380-
entry = next(scandir_it)
381-
except StopIteration:
382-
break
383-
except OSError as error:
384-
if onerror is not None:
385-
onerror(error)
386-
return
387-
388-
try:
389-
is_dir = entry.is_dir()
390-
except OSError:
391-
# If is_dir() raises an OSError, consider that the entry is not
392-
# a directory, same behaviour than os.path.isdir().
393-
is_dir = False
394-
395-
if is_dir:
396-
dirs.append(entry.name)
397-
else:
398-
nondirs.append(entry.name)
380+
try:
381+
entry = next(scandir_it)
382+
except StopIteration:
383+
break
384+
except OSError as error:
385+
if onerror is not None:
386+
onerror(error)
387+
return
399388

400-
if not topdown and is_dir:
401-
# Bottom-up: recurse into sub-directory, but exclude symlinks to
402-
# directories if followlinks is False
403-
if followlinks:
404-
walk_into = True
389+
try:
390+
is_dir = entry.is_dir()
391+
except OSError:
392+
# If is_dir() raises an OSError, consider that the entry is not
393+
# a directory, same behaviour than os.path.isdir().
394+
is_dir = False
395+
396+
if is_dir:
397+
dirs.append(entry.name)
405398
else:
406-
try:
407-
is_symlink = entry.is_symlink()
408-
except OSError:
409-
# If is_symlink() raises an OSError, consider that the
410-
# entry is not a symbolic link, same behaviour than
411-
# os.path.islink().
412-
is_symlink = False
413-
walk_into = not is_symlink
399+
nondirs.append(entry.name)
414400

415-
if walk_into:
416-
yield from walk(entry.path, topdown, onerror, followlinks)
401+
if not topdown and is_dir:
402+
# Bottom-up: recurse into sub-directory, but exclude symlinks to
403+
# directories if followlinks is False
404+
if followlinks:
405+
walk_into = True
406+
else:
407+
try:
408+
is_symlink = entry.is_symlink()
409+
except OSError:
410+
# If is_symlink() raises an OSError, consider that the
411+
# entry is not a symbolic link, same behaviour than
412+
# os.path.islink().
413+
is_symlink = False
414+
walk_into = not is_symlink
415+
416+
if walk_into:
417+
yield from walk(entry.path, topdown, onerror, followlinks)
417418

418419
# Yield before recursion if going top down
419420
if topdown:
@@ -437,15 +438,30 @@ class _DummyDirEntry:
437438
def __init__(self, dir, name):
438439
self.name = name
439440
self.path = path.join(dir, name)
441+
440442
def is_dir(self):
441443
return path.isdir(self.path)
444+
442445
def is_symlink(self):
443446
return path.islink(self.path)
444447

445-
def _dummy_scandir(dir):
448+
class _dummy_scandir:
446449
# listdir-based implementation for bytes patches on Windows
447-
for name in listdir(dir):
448-
yield _DummyDirEntry(dir, name)
450+
def __init__(self, dir):
451+
self.dir = dir
452+
self.it = iter(listdir(dir))
453+
454+
def __iter__(self):
455+
return self
456+
457+
def __next__(self):
458+
return _DummyDirEntry(self.dir, next(self.it))
459+
460+
def __enter__(self):
461+
return self
462+
463+
def __exit__(self, *args):
464+
self.it = iter(())
449465

450466
__all__.append("walk")
451467

Lib/test/test_os.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2808,6 +2808,8 @@ def test_os_all(self):
28082808

28092809

28102810
class TestScandir(unittest.TestCase):
2811+
check_no_resource_warning = support.check_no_resource_warning
2812+
28112813
def setUp(self):
28122814
self.path = os.path.realpath(support.TESTFN)
28132815
self.addCleanup(support.rmtree, self.path)
@@ -3030,6 +3032,56 @@ def test_bad_path_type(self):
30303032
for obj in [1234, 1.234, {}, []]:
30313033
self.assertRaises(TypeError, os.scandir, obj)
30323034

3035+
def test_close(self):
3036+
self.create_file("file.txt")
3037+
self.create_file("file2.txt")
3038+
iterator = os.scandir(self.path)
3039+
next(iterator)
3040+
iterator.close()
3041+
# multiple closes
3042+
iterator.close()
3043+
with self.check_no_resource_warning():
3044+
del iterator
3045+
3046+
def test_context_manager(self):
3047+
self.create_file("file.txt")
3048+
self.create_file("file2.txt")
3049+
with os.scandir(self.path) as iterator:
3050+
next(iterator)
3051+
with self.check_no_resource_warning():
3052+
del iterator
3053+
3054+
def test_context_manager_close(self):
3055+
self.create_file("file.txt")
3056+
self.create_file("file2.txt")
3057+
with os.scandir(self.path) as iterator:
3058+
next(iterator)
3059+
iterator.close()
3060+
3061+
def test_context_manager_exception(self):
3062+
self.create_file("file.txt")
3063+
self.create_file("file2.txt")
3064+
with self.assertRaises(ZeroDivisionError):
3065+
with os.scandir(self.path) as iterator:
3066+
next(iterator)
3067+
1/0
3068+
with self.check_no_resource_warning():
3069+
del iterator
3070+
3071+
def test_resource_warning(self):
3072+
self.create_file("file.txt")
3073+
self.create_file("file2.txt")
3074+
iterator = os.scandir(self.path)
3075+
next(iterator)
3076+
with self.assertWarns(ResourceWarning):
3077+
del iterator
3078+
support.gc_collect()
3079+
# exhausted iterator
3080+
iterator = os.scandir(self.path)
3081+
list(iterator)
3082+
with self.check_no_resource_warning():
3083+
del iterator
3084+
30333085

30343086
if __name__ == "__main__":
30353087
unittest.main()

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ Core and Builtins
179179
Library
180180
-------
181181

182+
- Issue #25994: Added the close() method and the support of the context manager
183+
protocol for the os.scandir() iterator.
184+
182185
- Issue #23992: multiprocessing: make MapResult not fail-fast upon exception.
183186

184187
- Issue #26243: Support keyword arguments to zlib.compress(). Patch by Aviv

Modules/posixmodule.c

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11937,8 +11937,14 @@ typedef struct {
1193711937

1193811938
#ifdef MS_WINDOWS
1193911939

11940+
static int
11941+
ScandirIterator_is_closed(ScandirIterator *iterator)
11942+
{
11943+
return iterator->handle == INVALID_HANDLE_VALUE;
11944+
}
11945+
1194011946
static void
11941-
ScandirIterator_close(ScandirIterator *iterator)
11947+
ScandirIterator_closedir(ScandirIterator *iterator)
1194211948
{
1194311949
if (iterator->handle == INVALID_HANDLE_VALUE)
1194411950
return;
@@ -11956,7 +11962,7 @@ ScandirIterator_iternext(ScandirIterator *iterator)
1195611962
BOOL success;
1195711963
PyObject *entry;
1195811964

11959-
/* Happens if the iterator is iterated twice */
11965+
/* Happens if the iterator is iterated twice, or closed explicitly */
1196011966
if (iterator->handle == INVALID_HANDLE_VALUE)
1196111967
return NULL;
1196211968

@@ -11987,14 +11993,20 @@ ScandirIterator_iternext(ScandirIterator *iterator)
1198711993
}
1198811994

1198911995
/* Error or no more files */
11990-
ScandirIterator_close(iterator);
11996+
ScandirIterator_closedir(iterator);
1199111997
return NULL;
1199211998
}
1199311999

1199412000
#else /* POSIX */
1199512001

12002+
static int
12003+
ScandirIterator_is_closed(ScandirIterator *iterator)
12004+
{
12005+
return !iterator->dirp;
12006+
}
12007+
1199612008
static void
11997-
ScandirIterator_close(ScandirIterator *iterator)
12009+
ScandirIterator_closedir(ScandirIterator *iterator)
1199812010
{
1199912011
if (!iterator->dirp)
1200012012
return;
@@ -12014,7 +12026,7 @@ ScandirIterator_iternext(ScandirIterator *iterator)
1201412026
int is_dot;
1201512027
PyObject *entry;
1201612028

12017-
/* Happens if the iterator is iterated twice */
12029+
/* Happens if the iterator is iterated twice, or closed explicitly */
1201812030
if (!iterator->dirp)
1201912031
return NULL;
1202012032

@@ -12051,21 +12063,67 @@ ScandirIterator_iternext(ScandirIterator *iterator)
1205112063
}
1205212064

1205312065
/* Error or no more files */
12054-
ScandirIterator_close(iterator);
12066+
ScandirIterator_closedir(iterator);
1205512067
return NULL;
1205612068
}
1205712069

1205812070
#endif
1205912071

12072+
static PyObject *
12073+
ScandirIterator_close(ScandirIterator *self, PyObject *args)
12074+
{
12075+
ScandirIterator_closedir(self);
12076+
Py_RETURN_NONE;
12077+
}
12078+
12079+
static PyObject *
12080+
ScandirIterator_enter(PyObject *self, PyObject *args)
12081+
{
12082+
Py_INCREF(self);
12083+
return self;
12084+
}
12085+
12086+
static PyObject *
12087+
ScandirIterator_exit(ScandirIterator *self, PyObject *args)
12088+
{
12089+
ScandirIterator_closedir(self);
12090+
Py_RETURN_NONE;
12091+
}
12092+
1206012093
static void
1206112094
ScandirIterator_dealloc(ScandirIterator *iterator)
1206212095
{
12063-
ScandirIterator_close(iterator);
12096+
if (!ScandirIterator_is_closed(iterator)) {
12097+
PyObject *exc, *val, *tb;
12098+
Py_ssize_t old_refcount = Py_REFCNT(iterator);
12099+
/* Py_INCREF/Py_DECREF cannot be used, because the refcount is
12100+
* likely zero, Py_DECREF would call again the destructor.
12101+
*/
12102+
++Py_REFCNT(iterator);
12103+
PyErr_Fetch(&exc, &val, &tb);
12104+
if (PyErr_WarnFormat(PyExc_ResourceWarning, 1,
12105+
"unclosed scandir iterator %R", iterator)) {
12106+
/* Spurious errors can appear at shutdown */
12107+
if (PyErr_ExceptionMatches(PyExc_Warning))
12108+
PyErr_WriteUnraisable((PyObject *) iterator);
12109+
}
12110+
PyErr_Restore(exc, val, tb);
12111+
Py_REFCNT(iterator) = old_refcount;
12112+
12113+
ScandirIterator_closedir(iterator);
12114+
}
1206412115
Py_XDECREF(iterator->path.object);
1206512116
path_cleanup(&iterator->path);
1206612117
Py_TYPE(iterator)->tp_free((PyObject *)iterator);
1206712118
}
1206812119

12120+
static PyMethodDef ScandirIterator_methods[] = {
12121+
{"__enter__", (PyCFunction)ScandirIterator_enter, METH_NOARGS},
12122+
{"__exit__", (PyCFunction)ScandirIterator_exit, METH_VARARGS},
12123+
{"close", (PyCFunction)ScandirIterator_close, METH_NOARGS},
12124+
{NULL}
12125+
};
12126+
1206912127
static PyTypeObject ScandirIteratorType = {
1207012128
PyVarObject_HEAD_INIT(NULL, 0)
1207112129
MODNAME ".ScandirIterator", /* tp_name */
@@ -12095,6 +12153,7 @@ static PyTypeObject ScandirIteratorType = {
1209512153
0, /* tp_weaklistoffset */
1209612154
PyObject_SelfIter, /* tp_iter */
1209712155
(iternextfunc)ScandirIterator_iternext, /* tp_iternext */
12156+
ScandirIterator_methods, /* tp_methods */
1209812157
};
1209912158

1210012159
static PyObject *

0 commit comments

Comments
 (0)