Commits

Michele Lacchia  committed 5532d80 Merge

Updated

  • Participants
  • Parent commits 657b41b, 236dcf1

Comments (0)

Files changed (7)

+035a6aa3e83aa1f3d2d7b16b8abae4fda9a2515a 0.5
+02c2082d237a86a3029426c621498edce6ca8e42 0.6
 Requirements
 ------------
 
-Python 3.2 or later is required.
+Python 3.2 or later is recommended, but pathlib is also usable with Python 2.7.
 
 Install
 -------
 The full documentation can be read at `Read the Docs
 <http://readthedocs.org/docs/pathlib/en/latest/>`_.
 
+
+Contributing
+------------
+
+The issue tracker and repository are hosted by `BitBucket
+<https://bitbucket.org/pitrou/pathlib/>`_.
+
+
+History
+-------
+
+Version 0.7
+^^^^^^^^^^^
+
+- Add a *target_is_directory* argument to Path.symlink_to()
+
+Version 0.6
+^^^^^^^^^^^
+
+- Add Path.is_file() and Path.is_symlink()
+- Add Path.glob() and Path.rglob()
+- Add PurePath.match()
+
+Version 0.5
+^^^^^^^^^^^
+
+- Add Path.mkdir().
+- Add Python 2.7 compatibility by Michele Lacchia.
+- Make parent() raise ValueError when the level is greater than the path
+  length.
-0.4
+0.6

File docs/index.rst

 This module offers a set of classes featuring all the common operations on
 paths in an easy, object-oriented way.
 
-This module requires Python 3.2 or later.  If using it with Python 3.3,
-you also have access to optional ``openat``-based filesystem operations.
+This module is best used with Python 3.2 or later, but it is also compatible
+with Python 2.7.  If using it with Python 3.3, you also have access to
+optional ``openat``-based filesystem operations.
 
 
 Download
       PureNTPath('c:\\Program Files')
 
 
+.. method:: PurePath.match(pattern)
+
+   Match this path against the provided glob-style pattern.  Return True
+   if matching is successful, False otherwise.
+
+   If *pattern* is relative, the path can be either relative or absolute,
+   and matching is done from the right::
+
+      >>> PurePath('a/b.py').match('*.py')
+      True
+      >>> PurePath('/a/b/c.py').match('b/*.py')
+      True
+      >>> PurePath('/a/b/c.py').match('a/*.py')
+      False
+
+   If *pattern* is absolute, the path must be absolute, and the whole path
+   must match::
+
+      >>> PurePath('/a.py').match('/*.py')
+      True
+      >>> PurePath('a/b.py').match('/*.py')
+      False
+
+   As with other methods, case-sensitivity is observed::
+
+      >>> PureNTPath('b.py').match('*.PY')
+      True
+
+
 .. method:: PurePath.normcase()
 
    Return a case-folded version of the path.  Calling this method is *not*
    needed before comparing path objects.
 
 
+.. method:: PurePath.parent(level=1)
+
+   Return the path's parent at the *level*'th level.  If *level* is not given,
+   return the path's immediate parent::
+
+      >>> p = PurePosixPath('/a/b/c/d')
+      >>> p.parent()
+      PurePosixPath('/a/b/c')
+      >>> p.parent(2)
+      PurePosixPath('/a/b')
+      >>> p.parent(3)
+      PurePosixPath('/a')
+      >>> p.parent(4)
+      PurePosixPath('/')
+
+   .. note::
+      This is a purely lexical operation, hence the following behaviour::
+
+         >>> p = PurePosixPath('foo/..')
+         >>> p.parent()
+         PurePosixPath('foo')
+
+      If you want to walk an arbitrary filesystem path upwards, it is
+      recommended to first call :meth:`Path.resolve` so as to resolve
+      symlinks and eliminate `".."` components.
+
+
+.. method:: PurePath.parents()
+
+   Iterate over the path's parents from the most to the least specific::
+
+      >>> for p in PureNTPath('c:/foo/bar/setup.py').parents(): p
+      ...
+      PureNTPath('c:\\foo\\bar')
+      PureNTPath('c:\\foo')
+      PureNTPath('c:\\')
+
+
 .. method:: PurePath.relative()
 
    Return the path object stripped of its drive and root, if any::
       ValueError: '/etc/passwd' does not start with '/usr'
 
 
-.. method:: PurePath.parent(level=1)
-
-   Return the path's parent at the *level*'th level.  If *level* is not given,
-   return the path's immediate parent::
-
-      >>> p = PurePosixPath('/a/b/c/d')
-      >>> p.parent()
-      PurePosixPath('/a/b/c')
-      >>> p.parent(2)
-      PurePosixPath('/a/b')
-      >>> p.parent(3)
-      PurePosixPath('/a')
-      >>> p.parent(4)
-      PurePosixPath('/')
-
-
-.. method:: PurePath.parents()
-
-   Iterate over the path's parents from the most to the least specific::
-
-      >>> for p in PureNTPath('c:/foo/bar/setup.py').parents(): p
-      ...
-      PureNTPath('c:\\foo\\bar')
-      PureNTPath('c:\\foo')
-      PureNTPath('c:\\')
-
-
 Concrete paths
 --------------
 
       False
 
 
+.. method:: Path.glob(pattern)
+
+   Glob the given *pattern* in the directory represented by this path,
+   yielding all matching files (of any kind)::
+
+      >>> sorted(Path('.').glob('*.py'))
+      [PosixPath('pathlib.py'), PosixPath('setup.py'), PosixPath('test_pathlib.py')]
+      >>> sorted(Path('.').glob('*/*.py'))
+      [PosixPath('docs/conf.py')]
+
+
 .. method:: Path.is_dir()
 
-   Return True if the path points to a directory, False if it points to
-   another kind of file::
+   Return True if the path points to a directory (or a symbolic link
+   pointing to a directory), False if it points to another kind of file.
 
-      >>> Path('.').is_dir()
-      True
-      >>> Path('setup.py').is_dir()
-      False
+
+.. method:: Path.is_file()
+
+   Return True if the path points to a regular file (or a symbolic link
+   pointing to a regular file), False if it points to another kind of file.
+
+
+.. method:: Path.is_symlink()
+
+   Return True if the path points to a symbolic link, False otherwise.
 
 
 .. method:: Path.lchmod(mode)
       >>> p.resolve()
       PosixPath('/home/antoine/pathlib')
 
+   `".."` components are also eliminated (this is the only method to do so)::
+
+      >>> p = Path('docs/../setup.py')
+      >>> p.resolve()
+      PosixPath('/home/antoine/pathlib/setup.py')
+
    If the path doesn't exist, an :exc:`OSError` is raised.  If an infinite
    loop is encountered along the resolution path, :exc:`ValueError` is raised.
 
 
+.. method:: Path.rglob(pattern)
+
+   Like :meth:`glob`, but glob recursively from any subdirectories as well::
+
+      >>> sorted(Path().rglob("*.py"))
+      [PosixPath('build/lib/pathlib.py'),
+       PosixPath('docs/conf.py'),
+       PosixPath('pathlib.py'),
+       PosixPath('setup.py'),
+       PosixPath('test_pathlib.py')]
+
+
 .. method:: Path.rmdir()
 
    Remove this directory.  The directory must be empty.
 
 
-.. method:: Path.symlink_to(target)
+.. method:: Path.symlink_to(target, target_is_directory=False)
 
-   Make this path a symbolic link to *target*.
+   Make this path a symbolic link to *target*.  Under Windows,
+   *target_is_directory* must be True (default False) if the link's target
+   is a directory.  Under POSIX, *target_is_directory*'s value is ignored.
 
       >>> p = Path('mylink')
       >>> p.symlink_to('setup.py')
+import fnmatch
 import io
 import ntpath
 import os
 from functools import wraps
 from itertools import chain, count
 from operator import attrgetter
-from stat import S_ISDIR
+from stat import S_ISDIR, S_ISLNK, S_ISREG
 
 
 supports_symlinks = True
 # Internals
 #
 
+def _is_wildcard_pattern(pat):
+    # Whether this pattern needs actual matching using fnmatch, or can
+    # be looked up directly as a file.
+    return "*" in pat or "?" in pat or "[" in pat
+
+
 class _Flavour(object):
     """A flavour implements a particular (platform-specific) set of path
     semantics."""
             part = part.lstrip(sep)
         return drv, root, part
 
+    def casefold(self, s):
+        return s.lower()
+
     def casefold_parts(self, parts):
         return [p.lower() for p in parts]
 
             return os.getcwd()
         if _getfinalpathname is not None:
             return self._ext_to_normal(_getfinalpathname(s))
-        # Means fallback on abspath
+        # Means fallback on absolute
         return None
 
     def _ext_to_normal(self, s):
         else:
             return '', '', part
 
+    def casefold(self, s):
+        return s
+
     def casefold_parts(self, parts):
         return parts
 
 
         rename = _wrap_binary_atfunc(os.renameat)
 
-        def symlink(self, target, pathobj):
+        def symlink(self, target, pathobj, target_is_directory):
             parent_fd, name = _fdnamepair(pathobj)
             os.symlinkat(str(target), parent_fd, name)
 
 
     rename = _wrap_binary_strfunc(os.rename)
 
-    symlink = _wrap_binary_strfunc(os.symlink)
+    if nt:
+        symlink = _wrap_binary_strfunc(os.symlink)
+    else:
+        # Under POSIX, os.symlink() takes two args
+        @staticmethod
+        def symlink(a, b, target_is_directory):
+            return os.symlink(str(a), str(b))
 
     def init_path(self, pathobj):
         pass
         drv = self._drv
         root = self._root
         parts = self._parts[:-level]
-        if not parts and (drv or root):
-            # If the path is absolute, we keep it absolute
-            parts = [self._parts[0]]
+        if not parts:
+            if level > len(self._parts) - bool(drv or root):
+                raise ValueError("level greater than path length")
         return self._from_parsed_parts(drv, root, parts)
 
     def parents(self):
         by the system, if any."""
         return self._flavour.is_reserved(self._parts)
 
+    def match(self, path_pattern):
+        """
+        Return True if this path matches the given pattern.
+        """
+        cf = self._flavour.casefold
+        path_pattern = cf(path_pattern)
+        drv, root, pat_parts = self._flavour.parse_parts((path_pattern,))
+        if not pat_parts:
+            raise ValueError("empty pattern")
+        if drv and drv != cf(self._drv):
+            return False
+        if root and root != cf(self._root):
+            return False
+        parts = self._cparts
+        if drv or root:
+            if len(pat_parts) != len(parts):
+                return False
+            pat_parts = pat_parts[1:]
+        elif len(pat_parts) > len(parts):
+            return False
+        for part, pat in zip(reversed(parts), reversed(pat_parts)):
+            if not fnmatch.fnmatchcase(part, pat):
+                return False
+        return True
+
 
 class PurePosixPath(PurePath):
     _flavour = _posix_flavour
         # A stub for the opener argument to built-in open()
         return self._accessor.open(self, flags, mode)
 
+    def _select_children(self, pattern_parts, recursive):
+        # Helper for globbing
+        # XXX symlink loops
+        if not pattern_parts:
+            yield self
+            return
+        if not self.is_dir():
+            return
+        pat = pattern_parts[0]
+        child_parts = pattern_parts[1:]
+        if _is_wildcard_pattern(pat):
+            cf = self._flavour.casefold
+            for name in self._accessor.listdir(self):
+                name = cf(name)
+                if fnmatch.fnmatchcase(name, pat):
+                    child_path = self._make_child_relpath(name)
+                    for p in child_path._select_children(child_parts, False):
+                        yield p
+                elif recursive:
+                    child_path = self._make_child_relpath(name)
+                    for p in child_path._select_children(pattern_parts, recursive):
+                        yield p
+        else:
+            child_path = self._make_child_relpath(pat)
+            if child_path.exists():
+                for p in child_path._select_children(child_parts, False):
+                    yield p
+            if recursive:
+                for name in self._accessor.listdir(self):
+                    child_path = self._make_child_relpath(name)
+                    for p in child_path._select_children(pattern_parts, recursive):
+                        yield p
+
     # Public API
 
     @classmethod
             return getattr(self._stat, name)
         return super(Path, self).__getattribute__(name)
 
-    def abspath(self):
+    def glob(self, pattern):
+        """Iterate over this subtree and yield all existing files (of any
+        kind, including directories) matching the given pattern.
+        """
+        pattern = self._flavour.casefold(pattern)
+        drv, root, pattern_parts = self._flavour.parse_parts((pattern,))
+        if drv or root:
+            raise NotImplementedError("Non-relative patterns are unsupported")
+        for p in self._select_children(pattern_parts, recursive=False):
+            yield p
+
+    def rglob(self, pattern):
+        """Recursively yield all existing files (of any kind, including
+        directories) matching the given pattern, anywhere in this subtree.
+        """
+        pattern = self._flavour.casefold(pattern)
+        drv, root, pattern_parts = self._flavour.parse_parts((pattern,))
+        if drv or root:
+            raise NotImplementedError("Non-relative patterns are unsupported")
+        for p in self._select_children(pattern_parts, recursive=True):
+            yield p
+
+    def absolute(self):
         """Return an absolute version of this path.  This function works
         even if the path doesn't point to anything.
 
             # No symlink resolution => for consistency, raise an error if
             # the path doesn't exist or is forbidden
             self._stat
-            s = str(self.abspath())
+            s = str(self.absolute())
         # Now we have no symlinks in the path, it's safe to normalize it.
         normed = self._flavour.pathmod.normpath(s)
         obj = self._from_parts((normed,), init=False)
             self._raise_closed()
         self._accessor.rename(self, target)
 
-    def symlink_to(self, target):
+    def symlink_to(self, target, target_is_directory=False):
         """
         Make this path a symlink pointing to the given path.
         Note the order of arguments (self, target) is the reverse of os.symlink's.
         """
         if self._closed:
             self._raise_closed()
-        # XXX how about target_is_directory?
-        self._accessor.symlink(target, self)
+        self._accessor.symlink(target, self, target_is_directory)
 
     # Convenience functions for querying the stat results
 
         """
         return S_ISDIR(self._stat.st_mode)
 
+    def is_file(self):
+        """
+        Whether this path is a regular file (also True for symlinks pointing
+        to regular files).
+        """
+        return S_ISREG(self._stat.st_mode)
+
+    def is_symlink(self):
+        """
+        Whether this path is a symbolic link.
+        """
+        st = self.lstat()
+        return S_ISLNK(st.st_mode)
+
 
 class PosixPath(Path, PurePosixPath):
     __slots__ = ()
         'License :: OSI Approved :: MIT License',
         'Operating System :: OS Independent',
         'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3.2',
         'Topic :: Software Development :: Libraries',
         'Topic :: System :: Filesystems',

File test_pathlib.py

             self.assertEqual(P(pathstr).as_posix(), pathstr)
         # Other tests for as_posix() are in test_equivalences()
 
-    @unittest.skipIf(sys.version_info[:2] < (3, 2),
+    @unittest.skipIf(sys.version_info < (3, 2),
                      'os.fsencode has been introduced in version 3.2')
     def test_as_bytes_common(self):
-        try:
-            sep = os.fsencode(self.sep)
-        except AttributeError:
-            sys.stdout.write('Skipping test_as_bytes_common, as os.fsencode ' \
-                             'has been introduced in version 3.2\n')
-            return
+        sep = os.fsencode(self.sep)
         P = self.cls
         self.assertEqual(P('a/b').as_bytes(), b'a' + sep + b'b')
         self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b')
         self.assertNotEqual(P('/a/b'), P('/'))
         self.assertNotEqual(P(), P('/'))
 
+    def test_match_common(self):
+        P = self.cls
+        self.assertRaises(ValueError, P('a').match, '')
+        self.assertRaises(ValueError, P('a').match, '.')
+        # Simple relative pattern
+        self.assertTrue(P('b.py').match('b.py'))
+        self.assertTrue(P('a/b.py').match('b.py'))
+        self.assertTrue(P('/a/b.py').match('b.py'))
+        self.assertFalse(P('a.py').match('b.py'))
+        self.assertFalse(P('b/py').match('b.py'))
+        self.assertFalse(P('/a.py').match('b.py'))
+        self.assertFalse(P('b.py/c').match('b.py'))
+        # Wilcard relative pattern
+        self.assertTrue(P('b.py').match('*.py'))
+        self.assertTrue(P('a/b.py').match('*.py'))
+        self.assertTrue(P('/a/b.py').match('*.py'))
+        self.assertFalse(P('b.pyc').match('*.py'))
+        self.assertFalse(P('b./py').match('*.py'))
+        self.assertFalse(P('b.py/c').match('*.py'))
+        # Multi-part relative pattern
+        self.assertTrue(P('ab/c.py').match('a*/*.py'))
+        self.assertTrue(P('/d/ab/c.py').match('a*/*.py'))
+        self.assertFalse(P('a.py').match('a*/*.py'))
+        self.assertFalse(P('/dab/c.py').match('a*/*.py'))
+        self.assertFalse(P('ab/c.py/d').match('a*/*.py'))
+        # Absolute pattern
+        self.assertTrue(P('/b.py').match('/*.py'))
+        self.assertFalse(P('b.py').match('/*.py'))
+        self.assertFalse(P('a/b.py').match('/*.py'))
+        self.assertFalse(P('/a/b.py').match('/*.py'))
+        # Multi-part absolute pattern
+        self.assertTrue(P('/a/b.py').match('/a/*.py'))
+        self.assertFalse(P('/ab.py').match('/a/*.py'))
+        self.assertFalse(P('/a/b/c.py').match('/a/*.py'))
+
     def test_ordering_common(self):
         # Ordering is tuple-alike
         def assertLess(a, b):
         self.assertEqual(p.parent(), P('a/b'))
         self.assertEqual(p.parent(2), P('a'))
         self.assertEqual(p.parent(3), P())
-        self.assertEqual(p.parent(4), P())
+        self.assertRaises(ValueError, p.parent, 4)
         # Anchored
         p = P('/a/b/c')
         self.assertEqual(p.parent(), P('/a/b'))
         self.assertEqual(p.parent(2), P('/a'))
         self.assertEqual(p.parent(3), P('/'))
-        self.assertEqual(p.parent(4), P('/'))
+        self.assertRaises(ValueError, p.parent, 4)
+        # Invalid level values
+        self.assertRaises(ValueError, p.parent, 0)
+        self.assertRaises(ValueError, p.parent, -1)
 
     def test_parents_common(self):
         # Relative
         P = self.cls
         self.assertNotEqual(P('a/b'), P('A/b'))
 
+    def test_match(self):
+        P = self.cls
+        self.assertFalse(P('A.py').match('a.PY'))
+
     def test_is_absolute(self):
         P = self.cls
         self.assertFalse(P().is_absolute())
         self.assertEqual(P('C:a/B'), P('c:A/b'))
         self.assertEqual(P('//Some/SHARE/a/B'), P('//somE/share/A/b'))
 
+    def test_match_common(self):
+        P = self.cls
+        # Absolute patterns
+        self.assertTrue(P('c:/b.py').match('/*.py'))
+        self.assertTrue(P('c:/b.py').match('c:*.py'))
+        self.assertTrue(P('c:/b.py').match('c:/*.py'))
+        self.assertFalse(P('d:/b.py').match('c:/*.py'))  # wrong drive
+        self.assertFalse(P('b.py').match('/*.py'))
+        self.assertFalse(P('b.py').match('c:*.py'))
+        self.assertFalse(P('b.py').match('c:/*.py'))
+        self.assertFalse(P('c:b.py').match('/*.py'))
+        self.assertFalse(P('c:b.py').match('c:/*.py'))
+        self.assertFalse(P('/b.py').match('c:*.py'))
+        self.assertFalse(P('/b.py').match('c:/*.py'))
+        # UNC patterns
+        self.assertTrue(P('//some/share/a.py').match('/*.py'))
+        self.assertTrue(P('//some/share/a.py').match('//some/share/*.py'))
+        self.assertFalse(P('//other/share/a.py').match('//some/share/*.py'))
+        self.assertFalse(P('//some/share/a/b.py').match('//some/share/*.py'))
+        # Case-insensitivity
+        self.assertTrue(P('B.py').match('b.PY'))
+        self.assertTrue(P('c:/a/B.Py').match('C:/A/*.pY'))
+        self.assertTrue(P('//Some/Share/B.Py').match('//somE/sharE/*.pY'))
+
     def test_ordering_common(self):
         # Case-insensitivity
         def assertOrderedEqual(a, b):
         self.assertEqual(p.parent(), P('z:a/b'))
         self.assertEqual(p.parent(2), P('z:a'))
         self.assertEqual(p.parent(3), P('z:'))
-        self.assertEqual(p.parent(4), P('z:'))
+        self.assertRaises(ValueError, p.parent, 4)
         p = P('z:/a/b/c')
         self.assertEqual(p.parent(), P('z:/a/b'))
         self.assertEqual(p.parent(2), P('z:/a'))
         self.assertEqual(p.parent(3), P('z:/'))
-        self.assertEqual(p.parent(4), P('z:/'))
+        self.assertRaises(ValueError, p.parent, 4)
         p = P('//a/b/c/d')
         self.assertEqual(p.parent(), P('//a/b/c'))
         self.assertEqual(p.parent(2), P('//a/b'))
-        self.assertEqual(p.parent(3), P('//a/b'))
+        self.assertRaises(ValueError, p.parent, 3)
 
     def test_parents(self):
         # Anchored
         q = pathlib.PureNTPath('a')
         self.assertNotEqual(p, q)
 
-    @unittest.skipIf(sys.version_info[:2] < (3, 0),
+    @unittest.skipIf(sys.version_info < (3, 0),
                      'Most types are orderable in Python 2')
     def test_different_flavours_unordered(self):
         p = pathlib.PurePosixPath('a')
 
     using_openat = False
 
+    # (BASE)
+    #  |
+    #  |-- dirA/
+    #       |-- linkC -> "../dirB"
+    #  |-- dirB/
+    #  |    |-- fileB
+    #       |-- linkD -> "../dirB"
+    #  |-- dirC/
+    #  |    |-- fileC
+    #  |    |-- fileD
+    #  |-- fileA
+    #  |-- linkA -> "fileA"
+    #  |-- linkB -> "dirB"
+    #
+
     def setUp(self):
         os.mkdir(BASE)
         self.addCleanup(support.rmtree, BASE)
         os.mkdir(join('dirA'))
         os.mkdir(join('dirB'))
+        os.mkdir(join('dirC'))
+        os.mkdir(join('dirC', 'dirD'))
         with open(join('fileA'), 'wb') as f:
             f.write(b"this is file A\n")
         with open(join('dirB', 'fileB'), 'wb') as f:
             f.write(b"this is file B\n")
+        with open(join('dirC', 'fileC'), 'wb') as f:
+            f.write(b"this is file C\n")
+        with open(join('dirC', 'dirD', 'fileD'), 'wb') as f:
+            f.write(b"this is file D\n")
         if not symlink_skip_reason:
             if os.name == 'nt':
                 # Workaround for http://bugs.python.org/issue13772
         self.assertIs(True, p.exists())
         self.assertIs(True, p['dirA'].exists())
         self.assertIs(True, p['fileA'].exists())
-        self.assertIs(True, p['linkA'].exists())
-        self.assertIs(True, p['linkB'].exists())
+        if not symlink_skip_reason:
+            self.assertIs(True, p['linkA'].exists())
+            self.assertIs(True, p['linkB'].exists())
         self.assertIs(False, p['foo'].exists())
         self.assertIs(False, P('/xyzzy').exists())
 
         self.assertIsInstance(p, collections.Iterable)
         it = iter(p)
         paths = set(it)
-        expected = ['dirA', 'dirB', 'fileA']
+        expected = ['dirA', 'dirB', 'dirC', 'fileA']
         if not symlink_skip_reason:
             expected += ['linkA', 'linkB']
         self.assertEqual(paths, { P(BASE, q) for q in expected })
         self.assertIn(cm.exception.errno, (errno.ENOTDIR,
                                            errno.ENOENT, errno.EINVAL))
 
+    def test_glob_common(self):
+        def _check(glob, expected):
+            self.assertEqual(set(glob), { P(BASE, q) for q in expected })
+        P = self.cls
+        p = P(BASE)
+        it = p.glob("fileA")
+        self.assertIsInstance(it, collections.Iterator)
+        _check(it, ["fileA"])
+        _check(p.glob("fileB"), [])
+        _check(p.glob("dir*/file*"), ["dirB/fileB", "dirC/fileC"])
+        if symlink_skip_reason:
+            _check(p.glob("*A"), ['dirA', 'fileA'])
+        else:
+            _check(p.glob("*A"), ['dirA', 'fileA', 'linkA'])
+        if symlink_skip_reason:
+            _check(p.glob("*B/*"), ['dirB/fileB'])
+        else:
+            _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD',
+                                    'linkB/fileB', 'linkB/linkD'])
+        if symlink_skip_reason:
+            _check(p.glob("*/fileB"), ['dirB/fileB'])
+        else:
+            _check(p.glob("*/fileB"), ['dirB/fileB', 'linkB/fileB'])
+
+    def test_rglob_common(self):
+        def _check(glob, expected):
+            self.assertEqual(set(glob), { P(BASE, q) for q in expected })
+        P = self.cls
+        p = P(BASE)
+        it = p.rglob("fileA")
+        self.assertIsInstance(it, collections.Iterator)
+        # XXX cannot test because of symlink loops in the test setup
+        #_check(it, ["fileA"])
+        #_check(p.rglob("fileB"), ["dirB/fileB"])
+        #_check(p.rglob("*/fileA"), [""])
+        #_check(p.rglob("*/fileB"), ["dirB/fileB"])
+        #_check(p.rglob("file*"), ["fileA", "dirB/fileB"])
+        # No symlink loops here
+        p = P(BASE, "dirC")
+        _check(p.rglob("file*"), ["dirC/fileC", "dirC/dirD/fileD"])
+        _check(p.rglob("*/*"), ["dirC/dirD/fileD"])
+
+    def test_glob_dotdot(self):
+        # ".." is not special in globs
+        P = self.cls
+        p = P(BASE)
+        self.assertEqual(set(p.glob("..")), { P(BASE, "..") })
+        self.assertEqual(set(p.glob("dirA/../file*")), { P(BASE, "dirA/../fileA") })
+        self.assertEqual(set(p.glob("../xyzzy")), set())
+
     def _check_resolve_relative(self, p, expected):
         q = p.resolve()
         self.assertEqual(q, expected)
         self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB'))
         # Now create absolute symlinks
         d = tempfile.mkdtemp(suffix='-dirD')
+        self.addCleanup(support.rmtree, d)
         os.symlink(os.path.join(d), join('dirA', 'linkX'))
         os.symlink(join('dirB'), os.path.join(d, 'linkY'))
         p = P(BASE, 'dirA', 'linkX', 'linkY', 'fileB')
         self._check_resolve_absolute(p, P(BASE, 'dirB', 'fileB'))
-        self.addCleanup(support.rmtree, d)
 
     def test_with(self):
         p = self.cls(BASE)
         self.assertRaises(ValueError, p.open)
         self.assertRaises(ValueError, p.raw_open, os.O_RDONLY)
         self.assertRaises(ValueError, p.resolve)
-        self.assertRaises(ValueError, p.abspath)
+        self.assertRaises(ValueError, p.absolute)
         self.assertRaises(ValueError, p.__enter__)
 
     def test_chmod(self):
         link.symlink_to(str(target))
         self.assertEqual(link.stat(), target.stat())
         self.assertNotEqual(link.lstat(), target.stat())
+        self.assertFalse(link.is_dir())
+        # Symlinking to a directory
+        target = P['dirB']
+        link = P['dirA', 'linkAAAA']
+        link.symlink_to(target, target_is_directory=True)
+        self.assertEqual(link.stat(), target.stat())
+        self.assertNotEqual(link.lstat(), target.stat())
+        self.assertTrue(link.is_dir())
+        self.assertTrue(list(link))
 
     def test_is_dir(self):
         P = self.cls(BASE)
         self.assertTrue(P['dirA'].is_dir())
         self.assertFalse(P['fileA'].is_dir())
+        if not symlink_skip_reason:
+            self.assertFalse(P['linkA'].is_dir())
+            self.assertTrue(P['linkB'].is_dir())
+
+    def test_is_file(self):
+        P = self.cls(BASE)
+        self.assertTrue(P['fileA'].is_file())
+        self.assertFalse(P['dirA'].is_file())
+        if not symlink_skip_reason:
+            self.assertTrue(P['linkA'].is_file())
+            self.assertFalse(P['linkB'].is_file())
+
+    def test_is_symlink(self):
+        P = self.cls(BASE)
+        self.assertFalse(P['fileA'].is_symlink())
+        self.assertFalse(P['dirA'].is_symlink())
+        if not symlink_skip_reason:
+            self.assertTrue(P['linkA'].is_symlink())
+            self.assertTrue(P['linkB'].is_symlink())
 
 
 class PathTest(_BasePathTest):
         os.symlink(join('linkW/../linkW'), join('linkW'))
         self._check_symlink_loop(BASE, 'linkW')
 
+    def test_glob(self):
+        P = self.cls
+        p = P(BASE)
+        self.assertEqual(set(p.glob("FILEa")), set())
+
+    def test_rglob(self):
+        P = self.cls
+        p = P(BASE, "dirC")
+        self.assertEqual(set(p.rglob("FILEd")), set())
+
 
 if pathlib.supports_openat:
     class _RecordingOpenatAccessor(pathlib._OpenatAccessor):
 class NTPathTest(_BasePathTest):
     cls = pathlib.NTPath
 
+    def test_glob(self):
+        P = self.cls
+        p = P(BASE)
+        self.assertEqual(set(p.glob("FILEa")), { P(BASE, "fileA") })
+
+    def test_rglob(self):
+        P = self.cls
+        p = P(BASE, "dirC")
+        self.assertEqual(set(p.rglob("FILEd")), { P(BASE, "dirC/dirD/fileD") })
+
 
 def test_main():
     support.run_unittest(