Skip to content

GH-125413: Revert addition of pathlib.Path.scandir() method #127377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1289,35 +1289,6 @@ Reading directories
raised.


.. method:: Path.scandir()

When the path points to a directory, return an iterator of
:class:`os.DirEntry` objects corresponding to entries in the directory. The
returned iterator supports the :term:`context manager` protocol. It is
implemented using :func:`os.scandir` and gives the same guarantees.

Using :meth:`~Path.scandir` instead of :meth:`~Path.iterdir` can
significantly increase the performance of code that also needs file type or
file attribute information, because :class:`os.DirEntry` objects expose
this information if the operating system provides it when scanning a
directory.

The following example displays the names of subdirectories. The
``entry.is_dir()`` check will generally not make an additional system call::

>>> p = Path('docs')
>>> with p.scandir() as entries:
... for entry in entries:
... if entry.is_dir():
... entry.name
...
'_templates'
'_build'
'_static'

.. versionadded:: 3.14


.. method:: Path.glob(pattern, *, case_sensitive=None, recurse_symlinks=False)

Glob the given relative *pattern* in the directory represented by this path,
Expand Down
6 changes: 0 additions & 6 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -509,12 +509,6 @@ pathlib

(Contributed by Barney Gale in :gh:`73991`.)

* Add :meth:`pathlib.Path.scandir` to scan a directory and return an iterator
of :class:`os.DirEntry` objects. This is exactly equivalent to calling
:func:`os.scandir` on a path object.

(Contributed by Barney Gale in :gh:`125413`.)


pdb
---
Expand Down
15 changes: 7 additions & 8 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class PathGlobber(_GlobberBase):

lexists = operator.methodcaller('exists', follow_symlinks=False)
add_slash = operator.methodcaller('joinpath', '')
scandir = operator.methodcaller('scandir')
scandir = operator.methodcaller('_scandir')

@staticmethod
def concat_path(path, text):
Expand Down Expand Up @@ -640,23 +640,22 @@ def write_text(self, data, encoding=None, errors=None, newline=None):
with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f:
return f.write(data)

def scandir(self):
"""Yield os.DirEntry objects of the directory contents.
def _scandir(self):
"""Yield os.DirEntry-like objects of the directory contents.

The children are yielded in arbitrary order, and the
special entries '.' and '..' are not included.
"""
raise UnsupportedOperation(self._unsupported_msg('scandir()'))
import contextlib
return contextlib.nullcontext(self.iterdir())

def iterdir(self):
"""Yield path objects of the directory contents.

The children are yielded in arbitrary order, and the
special entries '.' and '..' are not included.
"""
with self.scandir() as entries:
names = [entry.name for entry in entries]
return map(self.joinpath, names)
raise UnsupportedOperation(self._unsupported_msg('iterdir()'))

def _glob_selector(self, parts, case_sensitive, recurse_symlinks):
if case_sensitive is None:
Expand Down Expand Up @@ -706,7 +705,7 @@ def walk(self, top_down=True, on_error=None, follow_symlinks=False):
if not top_down:
paths.append((path, dirnames, filenames))
try:
with path.scandir() as entries:
with path._scandir() as entries:
for entry in entries:
name = entry.name
try:
Expand Down
4 changes: 2 additions & 2 deletions Lib/pathlib/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,8 +627,8 @@ def _filter_trailing_slash(self, paths):
path_str = path_str[:-1]
yield path_str

def scandir(self):
"""Yield os.DirEntry objects of the directory contents.
def _scandir(self):
"""Yield os.DirEntry-like objects of the directory contents.

The children are yielded in arbitrary order, and the
special entries '.' and '..' are not included.
Expand Down
48 changes: 9 additions & 39 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import collections
import contextlib
import io
import os
import errno
Expand Down Expand Up @@ -1421,24 +1420,6 @@ def close(self):
'st_mode st_ino st_dev st_nlink st_uid st_gid st_size st_atime st_mtime st_ctime')


class DummyDirEntry:
"""
Minimal os.DirEntry-like object. Returned from DummyPath.scandir().
"""
__slots__ = ('name', '_is_symlink', '_is_dir')

def __init__(self, name, is_symlink, is_dir):
self.name = name
self._is_symlink = is_symlink
self._is_dir = is_dir

def is_symlink(self):
return self._is_symlink

def is_dir(self, *, follow_symlinks=True):
return self._is_dir and (follow_symlinks or not self._is_symlink)


class DummyPath(PathBase):
"""
Simple implementation of PathBase that keeps files and directories in
Expand Down Expand Up @@ -1506,25 +1487,14 @@ def open(self, mode='r', buffering=-1, encoding=None,
stream = io.TextIOWrapper(stream, encoding=encoding, errors=errors, newline=newline)
return stream

@contextlib.contextmanager
def scandir(self):
path = self.resolve()
path_str = str(path)
if path_str in self._files:
raise NotADirectoryError(errno.ENOTDIR, "Not a directory", path_str)
elif path_str in self._directories:
yield iter([path.joinpath(name)._dir_entry for name in self._directories[path_str]])
def iterdir(self):
path = str(self.resolve())
if path in self._files:
raise NotADirectoryError(errno.ENOTDIR, "Not a directory", path)
elif path in self._directories:
return iter([self / name for name in self._directories[path]])
else:
raise FileNotFoundError(errno.ENOENT, "File not found", path_str)

@property
def _dir_entry(self):
path_str = str(self)
is_symlink = path_str in self._symlinks
is_directory = (path_str in self._directories
if not is_symlink
else self._symlinks[path_str][1])
return DummyDirEntry(self.name, is_symlink, is_directory)
raise FileNotFoundError(errno.ENOENT, "File not found", path)

def mkdir(self, mode=0o777, parents=False, exist_ok=False):
path = str(self.parent.resolve() / self.name)
Expand Down Expand Up @@ -2217,9 +2187,9 @@ def test_iterdir_nodir(self):

def test_scandir(self):
p = self.cls(self.base)
with p.scandir() as entries:
with p._scandir() as entries:
self.assertTrue(list(entries))
with p.scandir() as entries:
with p._scandir() as entries:
for entry in entries:
child = p / entry.name
self.assertIsNotNone(entry)
Expand Down
2 changes: 1 addition & 1 deletion Misc/NEWS.d/3.14.0a2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ TypeError is now raised instead of ValueError for some logical errors.
.. nonce: Jat5kq
.. section: Library

Add :meth:`pathlib.Path.scandir` method to efficiently fetch directory
Add :meth:`!pathlib.Path.scandir` method to efficiently fetch directory
children and their file attributes. This is a trivial wrapper of
:func:`os.scandir`.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Revert addition of :meth:`!pathlib.Path.scandir`. This method was added in
3.14.0a2. The optimizations remain for file system paths, but other
subclasses should only have to implement :meth:`pathlib.Path.iterdir`.
Loading