Commit ffe96ae1 authored by Serhiy Storchaka's avatar Serhiy Storchaka

Issue #25994: Added the close() method and the support of the context manager

protocol for the os.scandir() iterator.
parent 2feb6425
...@@ -1891,14 +1891,29 @@ features: ...@@ -1891,14 +1891,29 @@ features:
:attr:`~DirEntry.path` attributes of each :class:`DirEntry` will be of :attr:`~DirEntry.path` attributes of each :class:`DirEntry` will be of
the same type as *path*. the same type as *path*.
The :func:`scandir` iterator supports the :term:`context manager` protocol
and has the following method:
.. method:: scandir.close()
Close the iterator and free acquired resources.
This is called automatically when the iterator is exhausted or garbage
collected, or when an error happens during iterating. However it
is advisable to call it explicitly or use the :keyword:`with`
statement.
.. versionadded:: 3.6
The following example shows a simple use of :func:`scandir` to display all The following example shows a simple use of :func:`scandir` to display all
the files (excluding directories) in the given *path* that don't start with the files (excluding directories) in the given *path* that don't start with
``'.'``. The ``entry.is_file()`` call will generally not make an additional ``'.'``. The ``entry.is_file()`` call will generally not make an additional
system call:: system call::
for entry in os.scandir(path): with os.scandir(path) as it:
if not entry.name.startswith('.') and entry.is_file(): for entry in it:
print(entry.name) if not entry.name.startswith('.') and entry.is_file():
print(entry.name)
.. note:: .. note::
...@@ -1914,6 +1929,12 @@ features: ...@@ -1914,6 +1929,12 @@ features:
.. versionadded:: 3.5 .. versionadded:: 3.5
.. versionadded:: 3.6
Added support for the :term:`context manager` protocol and the
:func:`~scandir.close()` method. If a :func:`scandir` iterator is neither
exhausted nor explicitly closed a :exc:`ResourceWarning` will be emitted
in its destructor.
.. class:: DirEntry .. class:: DirEntry
......
...@@ -104,6 +104,17 @@ directives ``%G``, ``%u`` and ``%V``. ...@@ -104,6 +104,17 @@ directives ``%G``, ``%u`` and ``%V``.
(Contributed by Ashley Anderson in :issue:`12006`.) (Contributed by Ashley Anderson in :issue:`12006`.)
os
--
A new :meth:`~os.scandir.close` method allows explicitly closing a
:func:`~os.scandir` iterator. The :func:`~os.scandir` iterator now
supports the :term:`context manager` protocol. If a :func:`scandir`
iterator is neither exhausted nor explicitly closed a :exc:`ResourceWarning`
will be emitted in its destructor.
(Contributed by Serhiy Storchaka in :issue:`25994`.)
pickle pickle
------ ------
......
...@@ -374,46 +374,47 @@ def walk(top, topdown=True, onerror=None, followlinks=False): ...@@ -374,46 +374,47 @@ def walk(top, topdown=True, onerror=None, followlinks=False):
onerror(error) onerror(error)
return return
while True: with scandir_it:
try: while True:
try: try:
entry = next(scandir_it) try:
except StopIteration: entry = next(scandir_it)
break except StopIteration:
except OSError as error: break
if onerror is not None: except OSError as error:
onerror(error) if onerror is not None:
return onerror(error)
return
try:
is_dir = entry.is_dir()
except OSError:
# If is_dir() raises an OSError, consider that the entry is not
# a directory, same behaviour than os.path.isdir().
is_dir = False
if is_dir:
dirs.append(entry.name)
else:
nondirs.append(entry.name)
if not topdown and is_dir: try:
# Bottom-up: recurse into sub-directory, but exclude symlinks to is_dir = entry.is_dir()
# directories if followlinks is False except OSError:
if followlinks: # If is_dir() raises an OSError, consider that the entry is not
walk_into = True # a directory, same behaviour than os.path.isdir().
is_dir = False
if is_dir:
dirs.append(entry.name)
else: else:
try: nondirs.append(entry.name)
is_symlink = entry.is_symlink()
except OSError:
# If is_symlink() raises an OSError, consider that the
# entry is not a symbolic link, same behaviour than
# os.path.islink().
is_symlink = False
walk_into = not is_symlink
if walk_into: if not topdown and is_dir:
yield from walk(entry.path, topdown, onerror, followlinks) # Bottom-up: recurse into sub-directory, but exclude symlinks to
# directories if followlinks is False
if followlinks:
walk_into = True
else:
try:
is_symlink = entry.is_symlink()
except OSError:
# If is_symlink() raises an OSError, consider that the
# entry is not a symbolic link, same behaviour than
# os.path.islink().
is_symlink = False
walk_into = not is_symlink
if walk_into:
yield from walk(entry.path, topdown, onerror, followlinks)
# Yield before recursion if going top down # Yield before recursion if going top down
if topdown: if topdown:
...@@ -437,15 +438,30 @@ class _DummyDirEntry: ...@@ -437,15 +438,30 @@ class _DummyDirEntry:
def __init__(self, dir, name): def __init__(self, dir, name):
self.name = name self.name = name
self.path = path.join(dir, name) self.path = path.join(dir, name)
def is_dir(self): def is_dir(self):
return path.isdir(self.path) return path.isdir(self.path)
def is_symlink(self): def is_symlink(self):
return path.islink(self.path) return path.islink(self.path)
def _dummy_scandir(dir): class _dummy_scandir:
# listdir-based implementation for bytes patches on Windows # listdir-based implementation for bytes patches on Windows
for name in listdir(dir): def __init__(self, dir):
yield _DummyDirEntry(dir, name) self.dir = dir
self.it = iter(listdir(dir))
def __iter__(self):
return self
def __next__(self):
return _DummyDirEntry(self.dir, next(self.it))
def __enter__(self):
return self
def __exit__(self, *args):
self.it = iter(())
__all__.append("walk") __all__.append("walk")
......
...@@ -2808,6 +2808,8 @@ class ExportsTests(unittest.TestCase): ...@@ -2808,6 +2808,8 @@ class ExportsTests(unittest.TestCase):
class TestScandir(unittest.TestCase): class TestScandir(unittest.TestCase):
check_no_resource_warning = support.check_no_resource_warning
def setUp(self): def setUp(self):
self.path = os.path.realpath(support.TESTFN) self.path = os.path.realpath(support.TESTFN)
self.addCleanup(support.rmtree, self.path) self.addCleanup(support.rmtree, self.path)
...@@ -3030,6 +3032,56 @@ class TestScandir(unittest.TestCase): ...@@ -3030,6 +3032,56 @@ class TestScandir(unittest.TestCase):
for obj in [1234, 1.234, {}, []]: for obj in [1234, 1.234, {}, []]:
self.assertRaises(TypeError, os.scandir, obj) self.assertRaises(TypeError, os.scandir, obj)
def test_close(self):
self.create_file("file.txt")
self.create_file("file2.txt")
iterator = os.scandir(self.path)
next(iterator)
iterator.close()
# multiple closes
iterator.close()
with self.check_no_resource_warning():
del iterator
def test_context_manager(self):
self.create_file("file.txt")
self.create_file("file2.txt")
with os.scandir(self.path) as iterator:
next(iterator)
with self.check_no_resource_warning():
del iterator
def test_context_manager_close(self):
self.create_file("file.txt")
self.create_file("file2.txt")
with os.scandir(self.path) as iterator:
next(iterator)
iterator.close()
def test_context_manager_exception(self):
self.create_file("file.txt")
self.create_file("file2.txt")
with self.assertRaises(ZeroDivisionError):
with os.scandir(self.path) as iterator:
next(iterator)
1/0
with self.check_no_resource_warning():
del iterator
def test_resource_warning(self):
self.create_file("file.txt")
self.create_file("file2.txt")
iterator = os.scandir(self.path)
next(iterator)
with self.assertWarns(ResourceWarning):
del iterator
support.gc_collect()
# exhausted iterator
iterator = os.scandir(self.path)
list(iterator)
with self.check_no_resource_warning():
del iterator
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
...@@ -179,6 +179,9 @@ Core and Builtins ...@@ -179,6 +179,9 @@ Core and Builtins
Library Library
------- -------
- Issue #25994: Added the close() method and the support of the context manager
protocol for the os.scandir() iterator.
- Issue #23992: multiprocessing: make MapResult not fail-fast upon exception. - Issue #23992: multiprocessing: make MapResult not fail-fast upon exception.
- Issue #26243: Support keyword arguments to zlib.compress(). Patch by Aviv - Issue #26243: Support keyword arguments to zlib.compress(). Patch by Aviv
......
...@@ -11937,8 +11937,14 @@ typedef struct { ...@@ -11937,8 +11937,14 @@ typedef struct {
#ifdef MS_WINDOWS #ifdef MS_WINDOWS
static int
ScandirIterator_is_closed(ScandirIterator *iterator)
{
return iterator->handle == INVALID_HANDLE_VALUE;
}
static void static void
ScandirIterator_close(ScandirIterator *iterator) ScandirIterator_closedir(ScandirIterator *iterator)
{ {
if (iterator->handle == INVALID_HANDLE_VALUE) if (iterator->handle == INVALID_HANDLE_VALUE)
return; return;
...@@ -11956,7 +11962,7 @@ ScandirIterator_iternext(ScandirIterator *iterator) ...@@ -11956,7 +11962,7 @@ ScandirIterator_iternext(ScandirIterator *iterator)
BOOL success; BOOL success;
PyObject *entry; PyObject *entry;
/* Happens if the iterator is iterated twice */ /* Happens if the iterator is iterated twice, or closed explicitly */
if (iterator->handle == INVALID_HANDLE_VALUE) if (iterator->handle == INVALID_HANDLE_VALUE)
return NULL; return NULL;
...@@ -11987,14 +11993,20 @@ ScandirIterator_iternext(ScandirIterator *iterator) ...@@ -11987,14 +11993,20 @@ ScandirIterator_iternext(ScandirIterator *iterator)
} }
/* Error or no more files */ /* Error or no more files */
ScandirIterator_close(iterator); ScandirIterator_closedir(iterator);
return NULL; return NULL;
} }
#else /* POSIX */ #else /* POSIX */
static int
ScandirIterator_is_closed(ScandirIterator *iterator)
{
return !iterator->dirp;
}
static void static void
ScandirIterator_close(ScandirIterator *iterator) ScandirIterator_closedir(ScandirIterator *iterator)
{ {
if (!iterator->dirp) if (!iterator->dirp)
return; return;
...@@ -12014,7 +12026,7 @@ ScandirIterator_iternext(ScandirIterator *iterator) ...@@ -12014,7 +12026,7 @@ ScandirIterator_iternext(ScandirIterator *iterator)
int is_dot; int is_dot;
PyObject *entry; PyObject *entry;
/* Happens if the iterator is iterated twice */ /* Happens if the iterator is iterated twice, or closed explicitly */
if (!iterator->dirp) if (!iterator->dirp)
return NULL; return NULL;
...@@ -12051,21 +12063,67 @@ ScandirIterator_iternext(ScandirIterator *iterator) ...@@ -12051,21 +12063,67 @@ ScandirIterator_iternext(ScandirIterator *iterator)
} }
/* Error or no more files */ /* Error or no more files */
ScandirIterator_close(iterator); ScandirIterator_closedir(iterator);
return NULL; return NULL;
} }
#endif #endif
static PyObject *
ScandirIterator_close(ScandirIterator *self, PyObject *args)
{
ScandirIterator_closedir(self);
Py_RETURN_NONE;
}
static PyObject *
ScandirIterator_enter(PyObject *self, PyObject *args)
{
Py_INCREF(self);
return self;
}
static PyObject *
ScandirIterator_exit(ScandirIterator *self, PyObject *args)
{
ScandirIterator_closedir(self);
Py_RETURN_NONE;
}
static void static void
ScandirIterator_dealloc(ScandirIterator *iterator) ScandirIterator_dealloc(ScandirIterator *iterator)
{ {
ScandirIterator_close(iterator); if (!ScandirIterator_is_closed(iterator)) {
PyObject *exc, *val, *tb;
Py_ssize_t old_refcount = Py_REFCNT(iterator);
/* Py_INCREF/Py_DECREF cannot be used, because the refcount is
* likely zero, Py_DECREF would call again the destructor.
*/
++Py_REFCNT(iterator);
PyErr_Fetch(&exc, &val, &tb);
if (PyErr_WarnFormat(PyExc_ResourceWarning, 1,
"unclosed scandir iterator %R", iterator)) {
/* Spurious errors can appear at shutdown */
if (PyErr_ExceptionMatches(PyExc_Warning))
PyErr_WriteUnraisable((PyObject *) iterator);
}
PyErr_Restore(exc, val, tb);
Py_REFCNT(iterator) = old_refcount;
ScandirIterator_closedir(iterator);
}
Py_XDECREF(iterator->path.object); Py_XDECREF(iterator->path.object);
path_cleanup(&iterator->path); path_cleanup(&iterator->path);
Py_TYPE(iterator)->tp_free((PyObject *)iterator); Py_TYPE(iterator)->tp_free((PyObject *)iterator);
} }
static PyMethodDef ScandirIterator_methods[] = {
{"__enter__", (PyCFunction)ScandirIterator_enter, METH_NOARGS},
{"__exit__", (PyCFunction)ScandirIterator_exit, METH_VARARGS},
{"close", (PyCFunction)ScandirIterator_close, METH_NOARGS},
{NULL}
};
static PyTypeObject ScandirIteratorType = { static PyTypeObject ScandirIteratorType = {
PyVarObject_HEAD_INIT(NULL, 0) PyVarObject_HEAD_INIT(NULL, 0)
MODNAME ".ScandirIterator", /* tp_name */ MODNAME ".ScandirIterator", /* tp_name */
...@@ -12095,6 +12153,7 @@ static PyTypeObject ScandirIteratorType = { ...@@ -12095,6 +12153,7 @@ static PyTypeObject ScandirIteratorType = {
0, /* tp_weaklistoffset */ 0, /* tp_weaklistoffset */
PyObject_SelfIter, /* tp_iter */ PyObject_SelfIter, /* tp_iter */
(iternextfunc)ScandirIterator_iternext, /* tp_iternext */ (iternextfunc)ScandirIterator_iternext, /* tp_iternext */
ScandirIterator_methods, /* tp_methods */
}; };
static PyObject * static PyObject *
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment