Add `pathlib.Path.info` attribute, which stores an object implementing the `pathlib.types.PathInfo` protocol (also new). The object supports querying the file type and internally caching `os.stat()` results. Path objects generated by `Path.iterdir()` are initialised with status information from `os.DirEntry` objects, which is gleaned from scanning the parent directory.
The `PathInfo` protocol has four methods: `exists()`, `is_dir()`, `is_file()` and `is_symlink()`.
.. versionadded:: 3.5
+.. attribute:: Path.info
+
+ A :class:`~pathlib.types.PathInfo` object that supports querying file type
+ information. The object exposes methods that cache their results, which can
+ help reduce the number of system calls needed when switching on file type.
+ For example::
+
+ >>> p = Path('src')
+ >>> if p.info.is_symlink():
+ ... print('symlink')
+ ... elif p.info.is_dir():
+ ... print('directory')
+ ... elif p.info.exists():
+ ... print('something else')
+ ... else:
+ ... print('not found')
+ ...
+ directory
+
+ If the path was generated from :meth:`Path.iterdir` then this attribute is
+ initialized with some information about the file type gleaned from scanning
+ the parent directory. Merely accessing :attr:`Path.info` does not perform
+ any filesystem queries.
+
+ To fetch up-to-date information, it's best to call :meth:`Path.is_dir`,
+ :meth:`~Path.is_file` and :meth:`~Path.is_symlink` rather than methods of
+ this attribute. There is no way to reset the cache; instead you can create
+ a new path object with an empty info cache via ``p = Path(p)``.
+
+ .. versionadded:: 3.14
+
+
Reading and writing files
^^^^^^^^^^^^^^^^^^^^^^^^^
.. [4] :func:`os.walk` always follows symlinks when categorizing paths into
*dirnames* and *filenames*, whereas :meth:`Path.walk` categorizes all
symlinks into *filenames* when *follow_symlinks* is false (the default.)
+
+
+Protocols
+---------
+
+.. module:: pathlib.types
+ :synopsis: pathlib types for static type checking
+
+
+The :mod:`pathlib.types` module provides types for static type checking.
+
+.. versionadded:: 3.14
+
+
+.. class:: PathInfo()
+
+ A :class:`typing.Protocol` describing the
+ :attr:`Path.info <pathlib.Path.info>` attribute. Implementations may
+ return cached results from their methods.
+
+ .. method:: exists(*, follow_symlinks=True)
+
+ Return ``True`` if the path is an existing file or directory, or any
+ other kind of file; return ``False`` if the path doesn't exist.
+
+ If *follow_symlinks* is ``False``, return ``True`` for symlinks without
+ checking if their targets exist.
+
+ .. method:: is_dir(*, follow_symlinks=True)
+
+ Return ``True`` if the path is a directory, or a symbolic link pointing
+ to a directory; return ``False`` if the path is (or points to) any other
+ kind of file, or if it doesn't exist.
+
+ If *follow_symlinks* is ``False``, return ``True`` only if the path
+ is a directory (without following symlinks); return ``False`` if the
+ path is any other kind of file, or if it doesn't exist.
+
+ .. method:: is_file(*, follow_symlinks=True)
+
+ Return ``True`` if the path is a file, or a symbolic link pointing to
+ a file; return ``False`` if the path is (or points to) a directory or
+ other non-file, or if it doesn't exist.
+
+ If *follow_symlinks* is ``False``, return ``True`` only if the path
+ is a file (without following symlinks); return ``False`` if the path
+ is a directory or other other non-file, or if it doesn't exist.
+
+ .. method:: is_symlink()
+
+ Return ``True`` if the path is a symbolic link (even if broken); return
+ ``False`` if the path is a directory or any kind of file, or if it
+ doesn't exist.
(Contributed by Barney Gale in :gh:`73991`.)
+* Add :attr:`pathlib.Path.info` attribute, which stores an object
+ implementing the :class:`pathlib.types.PathInfo` protocol (also new). The
+ object supports querying the file type and internally caching
+ :func:`~os.stat` results. Path objects generated by
+ :meth:`~pathlib.Path.iterdir` are initialized with file type information
+ gleaned from scanning the parent directory.
+
+ (Contributed by Barney Gale in :gh:`125413`.)
+
pdb
---
@staticmethod
def scandir(path):
- """Implements os.scandir().
+ """Like os.scandir(), but generates (entry, name, path) tuples.
"""
raise NotImplementedError
def select_wildcard(path, exists=False):
try:
- # We must close the scandir() object before proceeding to
- # avoid exhausting file descriptors when globbing deep trees.
- with self.scandir(path) as scandir_it:
- entries = list(scandir_it)
+ entries = self.scandir(path)
except OSError:
pass
else:
- prefix = self.add_slash(path)
- for entry in entries:
- if match is None or match(entry.name):
+ for entry, entry_name, entry_path in entries:
+ if match is None or match(entry_name):
if dir_only:
try:
if not entry.is_dir():
continue
except OSError:
continue
- entry_path = self.concat_path(prefix, entry.name)
if dir_only:
yield from select_next(entry_path, exists=True)
else:
def select_recursive_step(stack, match_pos):
path = stack.pop()
try:
- # We must close the scandir() object before proceeding to
- # avoid exhausting file descriptors when globbing deep trees.
- with self.scandir(path) as scandir_it:
- entries = list(scandir_it)
+ entries = self.scandir(path)
except OSError:
pass
else:
- prefix = self.add_slash(path)
- for entry in entries:
+ for entry, _entry_name, entry_path in entries:
is_dir = False
try:
if entry.is_dir(follow_symlinks=follow_symlinks):
pass
if is_dir or not dir_only:
- entry_path = self.concat_path(prefix, entry.name)
if match is None or match(str(entry_path), match_pos):
if dir_only:
yield from select_next(entry_path, exists=True)
"""Provides shell-style pattern matching and globbing for string paths.
"""
lexists = staticmethod(os.path.lexists)
- scandir = staticmethod(os.scandir)
concat_path = operator.add
+ @staticmethod
+ def scandir(path):
+ # We must close the scandir() object before proceeding to
+ # avoid exhausting file descriptors when globbing deep trees.
+ with os.scandir(path) as scandir_it:
+ entries = list(scandir_it)
+ return ((entry, entry.name, entry.path) for entry in entries)
+
if os.name == 'nt':
@staticmethod
def add_slash(pathname):
if not pathname or pathname[-1] == '/':
return pathname
return f'{pathname}/'
+
+
+class _PathGlobber(_GlobberBase):
+ """Provides shell-style pattern matching and globbing for pathlib paths.
+ """
+
+ lexists = operator.methodcaller('exists', follow_symlinks=False)
+ add_slash = operator.methodcaller('joinpath', '')
+
+ @staticmethod
+ def scandir(path):
+ return ((child.info, child.name, child) for child in path.iterdir())
+
+ @staticmethod
+ def concat_path(path, text):
+ return path.with_segments(str(path) + text)
import functools
import io
-import operator
import posixpath
from errno import EINVAL
-from glob import _GlobberBase, _no_recurse_symlinks
+from glob import _PathGlobber, _no_recurse_symlinks
from pathlib._os import copyfileobj
raise TypeError(f"{cls.__name__} can't be opened with mode {mode!r}")
-class PathGlobber(_GlobberBase):
- """
- Class providing shell-style globbing for path objects.
- """
-
- lexists = operator.methodcaller('exists', follow_symlinks=False)
- add_slash = operator.methodcaller('joinpath', '')
- scandir = operator.methodcaller('_scandir')
-
- @staticmethod
- def concat_path(path, text):
- """Appends text to the given path."""
- return path.with_segments(str(path) + text)
-
-
class CopyReader:
"""
Class that implements the "read" part of copying between path objects.
pattern = self.with_segments(pattern)
if case_sensitive is None:
case_sensitive = _is_case_sensitive(self.parser)
- globber = PathGlobber(pattern.parser.sep, case_sensitive, recursive=True)
+ globber = _PathGlobber(pattern.parser.sep, case_sensitive, recursive=True)
match = globber.compile(str(pattern))
return match(str(self)) is not None
"""
__slots__ = ()
+ @property
+ def info(self):
+ """
+ A PathInfo object that exposes the file type and other file attributes
+ of this path.
+ """
+ raise NotImplementedError
+
def exists(self, *, follow_symlinks=True):
"""
Whether this path exists.
This method normally follows symlinks; to check whether a symlink exists,
add the argument follow_symlinks=False.
"""
- raise NotImplementedError
+ info = self.joinpath().info
+ return info.exists(follow_symlinks=follow_symlinks)
def is_dir(self, *, follow_symlinks=True):
"""
Whether this path is a directory.
"""
- raise NotImplementedError
+ info = self.joinpath().info
+ return info.is_dir(follow_symlinks=follow_symlinks)
def is_file(self, *, follow_symlinks=True):
"""
Whether this path is a regular file (also True for symlinks pointing
to regular files).
"""
- raise NotImplementedError
+ info = self.joinpath().info
+ return info.is_file(follow_symlinks=follow_symlinks)
def is_symlink(self):
"""
Whether this path is a symbolic link.
"""
- raise NotImplementedError
+ info = self.joinpath().info
+ return info.is_symlink()
def __open_rb__(self, buffering=-1):
"""
with magic_open(self, mode='r', encoding=encoding, errors=errors, newline=newline) as f:
return f.read()
- 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.
- """
- import contextlib
- return contextlib.nullcontext(self.iterdir())
-
def iterdir(self):
"""Yield path objects of the directory contents.
else:
case_pedantic = True
recursive = True if recurse_symlinks else _no_recurse_symlinks
- globber = PathGlobber(self.parser.sep, case_sensitive, case_pedantic, recursive)
+ globber = _PathGlobber(self.parser.sep, case_sensitive, case_pedantic, recursive)
select = globber.selector(parts)
return select(self)
if not top_down:
paths.append((path, dirnames, filenames))
try:
- with path._scandir() as entries:
- for entry in entries:
- name = entry.name
- try:
- if entry.is_dir(follow_symlinks=follow_symlinks):
- if not top_down:
- paths.append(path.joinpath(name))
- dirnames.append(name)
- else:
- filenames.append(name)
- except OSError:
- filenames.append(name)
+ for child in path.iterdir():
+ try:
+ if child.info.is_dir(follow_symlinks=follow_symlinks):
+ if not top_down:
+ paths.append(child)
+ dirnames.append(child.name)
+ else:
+ filenames.append(child.name)
+ except OSError:
+ filenames.append(child.name)
except OSError as error:
if on_error is not None:
on_error(error)
except ImportError:
grp = None
-from pathlib._os import copyfile
+from pathlib._os import copyfile, PathInfo, DirEntryInfo
from pathlib._abc import CopyReader, CopyWriter, JoinablePath, ReadablePath, WritablePath
object. You can also instantiate a PosixPath or WindowsPath directly,
but cannot instantiate a WindowsPath on a POSIX system or vice versa.
"""
- __slots__ = ()
+ __slots__ = ('_info',)
def __new__(cls, *args, **kwargs):
if cls is Path:
cls = WindowsPath if os.name == 'nt' else PosixPath
return object.__new__(cls)
+ @property
+ def info(self):
+ """
+ A PathInfo object that exposes the file type and other file attributes
+ of this path.
+ """
+ try:
+ return self._info
+ except AttributeError:
+ self._info = PathInfo(self)
+ return self._info
+
def stat(self, *, follow_symlinks=True):
"""
Return the result of the stat() system call on this path, like
path_str = path_str[:-1]
yield path_str
- 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.
- """
- return os.scandir(self)
+ def _from_dir_entry(self, dir_entry, path_str):
+ path = self.with_segments(path_str)
+ path._str = path_str
+ path._info = DirEntryInfo(dir_entry)
+ return path
def iterdir(self):
"""Yield path objects of the directory contents.
"""
root_dir = str(self)
with os.scandir(root_dir) as scandir_it:
- paths = [entry.path for entry in scandir_it]
+ entries = list(scandir_it)
if root_dir == '.':
- paths = map(self._remove_leading_dot, paths)
- return map(self._from_parsed_string, paths)
+ return (self._from_dir_entry(e, e.name) for e in entries)
+ else:
+ return (self._from_dir_entry(e, e.path) for e in entries)
def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=False):
"""Iterate over this subtree and yield all existing files (of any
"""
from errno import *
+from stat import S_ISDIR, S_ISREG, S_ISLNK
import os
import sys
try:
write_target = target_f.write
while buf := read_source(1024 * 1024):
write_target(buf)
+
+
+class _PathInfoBase:
+ __slots__ = ()
+
+ def __repr__(self):
+ path_type = "WindowsPath" if os.name == "nt" else "PosixPath"
+ return f"<{path_type}.info>"
+
+
+class _WindowsPathInfo(_PathInfoBase):
+ """Implementation of pathlib.types.PathInfo that provides status
+ information for Windows paths. Don't try to construct it yourself."""
+ __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink')
+
+ def __init__(self, path):
+ self._path = str(path)
+
+ def exists(self, *, follow_symlinks=True):
+ """Whether this path exists."""
+ if not follow_symlinks and self.is_symlink():
+ return True
+ try:
+ return self._exists
+ except AttributeError:
+ if os.path.exists(self._path):
+ self._exists = True
+ return True
+ else:
+ self._exists = self._is_dir = self._is_file = False
+ return False
+
+ def is_dir(self, *, follow_symlinks=True):
+ """Whether this path is a directory."""
+ if not follow_symlinks and self.is_symlink():
+ return False
+ try:
+ return self._is_dir
+ except AttributeError:
+ if os.path.isdir(self._path):
+ self._is_dir = self._exists = True
+ return True
+ else:
+ self._is_dir = False
+ return False
+
+ def is_file(self, *, follow_symlinks=True):
+ """Whether this path is a regular file."""
+ if not follow_symlinks and self.is_symlink():
+ return False
+ try:
+ return self._is_file
+ except AttributeError:
+ if os.path.isfile(self._path):
+ self._is_file = self._exists = True
+ return True
+ else:
+ self._is_file = False
+ return False
+
+ def is_symlink(self):
+ """Whether this path is a symbolic link."""
+ try:
+ return self._is_symlink
+ except AttributeError:
+ self._is_symlink = os.path.islink(self._path)
+ return self._is_symlink
+
+
+class _PosixPathInfo(_PathInfoBase):
+ """Implementation of pathlib.types.PathInfo that provides status
+ information for POSIX paths. Don't try to construct it yourself."""
+ __slots__ = ('_path', '_mode')
+
+ def __init__(self, path):
+ self._path = str(path)
+ self._mode = [None, None]
+
+ def _get_mode(self, *, follow_symlinks=True):
+ idx = bool(follow_symlinks)
+ mode = self._mode[idx]
+ if mode is None:
+ try:
+ st = os.stat(self._path, follow_symlinks=follow_symlinks)
+ except (OSError, ValueError):
+ mode = 0
+ else:
+ mode = st.st_mode
+ if follow_symlinks or S_ISLNK(mode):
+ self._mode[idx] = mode
+ else:
+ # Not a symlink, so stat() will give the same result
+ self._mode = [mode, mode]
+ return mode
+
+ def exists(self, *, follow_symlinks=True):
+ """Whether this path exists."""
+ return self._get_mode(follow_symlinks=follow_symlinks) > 0
+
+ def is_dir(self, *, follow_symlinks=True):
+ """Whether this path is a directory."""
+ return S_ISDIR(self._get_mode(follow_symlinks=follow_symlinks))
+
+ def is_file(self, *, follow_symlinks=True):
+ """Whether this path is a regular file."""
+ return S_ISREG(self._get_mode(follow_symlinks=follow_symlinks))
+
+ def is_symlink(self):
+ """Whether this path is a symbolic link."""
+ return S_ISLNK(self._get_mode(follow_symlinks=False))
+
+
+PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo
+
+
+class DirEntryInfo(_PathInfoBase):
+ """Implementation of pathlib.types.PathInfo that provides status
+ information by querying a wrapped os.DirEntry object. Don't try to
+ construct it yourself."""
+ __slots__ = ('_entry', '_exists')
+
+ def __init__(self, entry):
+ self._entry = entry
+
+ def exists(self, *, follow_symlinks=True):
+ """Whether this path exists."""
+ if not follow_symlinks:
+ return True
+ try:
+ return self._exists
+ except AttributeError:
+ try:
+ self._entry.stat()
+ except OSError:
+ self._exists = False
+ else:
+ self._exists = True
+ return self._exists
+
+ def is_dir(self, *, follow_symlinks=True):
+ """Whether this path is a directory."""
+ try:
+ return self._entry.is_dir(follow_symlinks=follow_symlinks)
+ except OSError:
+ return False
+
+ def is_file(self, *, follow_symlinks=True):
+ """Whether this path is a regular file."""
+ try:
+ return self._entry.is_file(follow_symlinks=follow_symlinks)
+ except OSError:
+ return False
+
+ def is_symlink(self):
+ """Whether this path is a symbolic link."""
+ try:
+ return self._entry.is_symlink()
+ except OSError:
+ return False
@runtime_checkable
-class Parser(Protocol):
+class _PathParser(Protocol):
"""Protocol for path parsers, which do low-level path manipulation.
Path parsers provide a subset of the os.path API, specifically those
def split(self, path: str) -> tuple[str, str]: ...
def splitext(self, path: str) -> tuple[str, str]: ...
def normcase(self, path: str) -> str: ...
+
+
+@runtime_checkable
+class PathInfo(Protocol):
+ """Protocol for path info objects, which support querying the file type.
+ Methods may return cached results.
+ """
+ def exists(self, *, follow_symlinks: bool = True) -> bool: ...
+ def is_dir(self, *, follow_symlinks: bool = True) -> bool: ...
+ def is_file(self, *, follow_symlinks: bool = True) -> bool: ...
+ def is_symlink(self) -> bool: ...
with self.assertRaises(pathlib.UnsupportedOperation):
q.symlink_to(p)
+ @needs_symlinks
+ def test_info_is_symlink_caching(self):
+ p = self.cls(self.base)
+ q = p / 'mylink'
+ self.assertFalse(q.info.is_symlink())
+ q.symlink_to('blah')
+ self.assertFalse(q.info.is_symlink())
+
+ q = p / 'mylink' # same path, new instance.
+ self.assertTrue(q.info.is_symlink())
+ q.unlink()
+ self.assertTrue(q.info.is_symlink())
+
def test_stat(self):
statA = self.cls(self.base).joinpath('fileA').stat()
statB = self.cls(self.base).joinpath('dirB', 'fileB').stat()
import unittest
from pathlib._abc import JoinablePath, ReadablePath, WritablePath, magic_open
-from pathlib._types import Parser
+from pathlib.types import _PathParser, PathInfo
import posixpath
from test.support.os_helper import TESTFN
self.altsep = self.parser.altsep
def test_parser(self):
- self.assertIsInstance(self.cls.parser, Parser)
+ self.assertIsInstance(self.cls.parser, _PathParser)
def test_constructor_common(self):
P = self.cls
super().close()
-class DummyReadablePath(ReadablePath, DummyJoinablePath):
- """
- Simple implementation of DummyReadablePath that keeps files and
- directories in memory.
- """
- __slots__ = ()
+class DummyReadablePathInfo:
+ __slots__ = ('_is_dir', '_is_file')
- _files = {}
- _directories = {}
+ def __init__(self, is_dir, is_file):
+ self._is_dir = is_dir
+ self._is_file = is_file
def exists(self, *, follow_symlinks=True):
- return self.is_dir() or self.is_file()
+ return self._is_dir or self._is_file
def is_dir(self, *, follow_symlinks=True):
- return str(self).rstrip('/') in self._directories
+ return self._is_dir
def is_file(self, *, follow_symlinks=True):
- return str(self) in self._files
+ return self._is_file
def is_symlink(self):
return False
+
+class DummyReadablePath(ReadablePath, DummyJoinablePath):
+ """
+ Simple implementation of DummyReadablePath that keeps files and
+ directories in memory.
+ """
+ __slots__ = ('_info')
+
+ _files = {}
+ _directories = {}
+
+ def __init__(self, *segments):
+ super().__init__(*segments)
+ self._info = None
+
+ @property
+ def info(self):
+ if self._info is None:
+ path_str = str(self)
+ self._info = DummyReadablePathInfo(
+ is_dir=path_str.rstrip('/') in self._directories,
+ is_file=path_str in self._files)
+ return self._info
+
def __open_rb__(self, buffering=-1):
path = str(self)
if path in self._directories:
self.assertIn(cm.exception.errno, (errno.ENOTDIR,
errno.ENOENT, errno.EINVAL))
- def test_scandir(self):
+ def test_iterdir_info(self):
p = self.cls(self.base)
- with p._scandir() as entries:
- self.assertTrue(list(entries))
- with p._scandir() as entries:
- for entry in entries:
- child = p / entry.name
- self.assertIsNotNone(entry)
- self.assertEqual(entry.name, child.name)
- self.assertEqual(entry.is_symlink(),
- child.is_symlink())
- self.assertEqual(entry.is_dir(follow_symlinks=False),
- child.is_dir(follow_symlinks=False))
- if entry.name != 'brokenLinkLoop':
- self.assertEqual(entry.is_dir(), child.is_dir())
+ for child in p.iterdir():
+ info = child.info
+ self.assertIsInstance(info, PathInfo)
+ self.assertEqual(info.exists(), child.exists())
+ self.assertEqual(info.is_dir(), child.is_dir())
+ self.assertEqual(info.is_file(), child.is_file())
+ self.assertEqual(info.is_symlink(), child.is_symlink())
+ self.assertTrue(info.exists(follow_symlinks=False))
+ self.assertEqual(info.is_dir(follow_symlinks=False),
+ child.is_dir(follow_symlinks=False))
+ self.assertEqual(info.is_file(follow_symlinks=False),
+ child.is_file(follow_symlinks=False))
def test_glob_common(self):
def _check(glob, expected):
self.assertEqual(set(p.rglob("FILEd")), { P(self.base, "dirC/dirD/fileD") })
self.assertEqual(set(p.rglob("*\\")), { P(self.base, "dirC/dirD/") })
+ def test_info_exists(self):
+ p = self.cls(self.base)
+ self.assertTrue(p.info.exists())
+ self.assertTrue((p / 'dirA').info.exists())
+ self.assertTrue((p / 'dirA').info.exists(follow_symlinks=False))
+ self.assertTrue((p / 'fileA').info.exists())
+ self.assertTrue((p / 'fileA').info.exists(follow_symlinks=False))
+ self.assertFalse((p / 'non-existing').info.exists())
+ self.assertFalse((p / 'non-existing').info.exists(follow_symlinks=False))
+ if self.can_symlink:
+ self.assertTrue((p / 'linkA').info.exists())
+ self.assertTrue((p / 'linkA').info.exists(follow_symlinks=False))
+ self.assertTrue((p / 'linkB').info.exists())
+ self.assertTrue((p / 'linkB').info.exists(follow_symlinks=True))
+ self.assertFalse((p / 'brokenLink').info.exists())
+ self.assertTrue((p / 'brokenLink').info.exists(follow_symlinks=False))
+ self.assertFalse((p / 'brokenLinkLoop').info.exists())
+ self.assertTrue((p / 'brokenLinkLoop').info.exists(follow_symlinks=False))
+ self.assertFalse((p / 'fileA\udfff').info.exists())
+ self.assertFalse((p / 'fileA\udfff').info.exists(follow_symlinks=False))
+ self.assertFalse((p / 'fileA\x00').info.exists())
+ self.assertFalse((p / 'fileA\x00').info.exists(follow_symlinks=False))
+
+ def test_info_exists_caching(self):
+ p = self.cls(self.base)
+ q = p / 'myfile'
+ self.assertFalse(q.info.exists())
+ self.assertFalse(q.info.exists(follow_symlinks=False))
+ if isinstance(self.cls, WritablePath):
+ q.write_text('hullo')
+ self.assertFalse(q.info.exists())
+ self.assertFalse(q.info.exists(follow_symlinks=False))
+
+ def test_info_is_dir(self):
+ p = self.cls(self.base)
+ self.assertTrue((p / 'dirA').info.is_dir())
+ self.assertTrue((p / 'dirA').info.is_dir(follow_symlinks=False))
+ self.assertFalse((p / 'fileA').info.is_dir())
+ self.assertFalse((p / 'fileA').info.is_dir(follow_symlinks=False))
+ self.assertFalse((p / 'non-existing').info.is_dir())
+ self.assertFalse((p / 'non-existing').info.is_dir(follow_symlinks=False))
+ if self.can_symlink:
+ self.assertFalse((p / 'linkA').info.is_dir())
+ self.assertFalse((p / 'linkA').info.is_dir(follow_symlinks=False))
+ self.assertTrue((p / 'linkB').info.is_dir())
+ self.assertFalse((p / 'linkB').info.is_dir(follow_symlinks=False))
+ self.assertFalse((p / 'brokenLink').info.is_dir())
+ self.assertFalse((p / 'brokenLink').info.is_dir(follow_symlinks=False))
+ self.assertFalse((p / 'brokenLinkLoop').info.is_dir())
+ self.assertFalse((p / 'brokenLinkLoop').info.is_dir(follow_symlinks=False))
+ self.assertFalse((p / 'dirA\udfff').info.is_dir())
+ self.assertFalse((p / 'dirA\udfff').info.is_dir(follow_symlinks=False))
+ self.assertFalse((p / 'dirA\x00').info.is_dir())
+ self.assertFalse((p / 'dirA\x00').info.is_dir(follow_symlinks=False))
+
+ def test_info_is_dir_caching(self):
+ p = self.cls(self.base)
+ q = p / 'mydir'
+ self.assertFalse(q.info.is_dir())
+ self.assertFalse(q.info.is_dir(follow_symlinks=False))
+ if isinstance(self.cls, WritablePath):
+ q.mkdir()
+ self.assertFalse(q.info.is_dir())
+ self.assertFalse(q.info.is_dir(follow_symlinks=False))
+
+ def test_info_is_file(self):
+ p = self.cls(self.base)
+ self.assertTrue((p / 'fileA').info.is_file())
+ self.assertTrue((p / 'fileA').info.is_file(follow_symlinks=False))
+ self.assertFalse((p / 'dirA').info.is_file())
+ self.assertFalse((p / 'dirA').info.is_file(follow_symlinks=False))
+ self.assertFalse((p / 'non-existing').info.is_file())
+ self.assertFalse((p / 'non-existing').info.is_file(follow_symlinks=False))
+ if self.can_symlink:
+ self.assertTrue((p / 'linkA').info.is_file())
+ self.assertFalse((p / 'linkA').info.is_file(follow_symlinks=False))
+ self.assertFalse((p / 'linkB').info.is_file())
+ self.assertFalse((p / 'linkB').info.is_file(follow_symlinks=False))
+ self.assertFalse((p / 'brokenLink').info.is_file())
+ self.assertFalse((p / 'brokenLink').info.is_file(follow_symlinks=False))
+ self.assertFalse((p / 'brokenLinkLoop').info.is_file())
+ self.assertFalse((p / 'brokenLinkLoop').info.is_file(follow_symlinks=False))
+ self.assertFalse((p / 'fileA\udfff').info.is_file())
+ self.assertFalse((p / 'fileA\udfff').info.is_file(follow_symlinks=False))
+ self.assertFalse((p / 'fileA\x00').info.is_file())
+ self.assertFalse((p / 'fileA\x00').info.is_file(follow_symlinks=False))
+
+ def test_info_is_file_caching(self):
+ p = self.cls(self.base)
+ q = p / 'myfile'
+ self.assertFalse(q.info.is_file())
+ self.assertFalse(q.info.is_file(follow_symlinks=False))
+ if isinstance(self.cls, WritablePath):
+ q.write_text('hullo')
+ self.assertFalse(q.info.is_file())
+ self.assertFalse(q.info.is_file(follow_symlinks=False))
+
+ def test_info_is_symlink(self):
+ p = self.cls(self.base)
+ self.assertFalse((p / 'fileA').info.is_symlink())
+ self.assertFalse((p / 'dirA').info.is_symlink())
+ self.assertFalse((p / 'non-existing').info.is_symlink())
+ if self.can_symlink:
+ self.assertTrue((p / 'linkA').info.is_symlink())
+ self.assertTrue((p / 'linkB').info.is_symlink())
+ self.assertTrue((p / 'brokenLink').info.is_symlink())
+ self.assertFalse((p / 'linkA\udfff').info.is_symlink())
+ self.assertFalse((p / 'linkA\x00').info.is_symlink())
+ self.assertTrue((p / 'brokenLinkLoop').info.is_symlink())
+ self.assertFalse((p / 'fileA\udfff').info.is_symlink())
+ self.assertFalse((p / 'fileA\x00').info.is_symlink())
+
def test_is_dir(self):
P = self.cls(self.base)
self.assertTrue((P / 'dirA').is_dir())
--- /dev/null
+Add :attr:`pathlib.Path.info` attribute, which stores an object
+implementing the :class:`pathlib.types.PathInfo` protocol (also new). The
+object supports querying the file type and internally caching
+:func:`~os.stat` results. Path objects generated by
+:meth:`~pathlib.Path.iterdir` are initialized with file type information
+gleaned from scanning the parent directory.