Commits

Antoine Pitrou committed 3a0813a

Add Path.glob()

Comments (0)

Files changed (4)

 Version 0.6
 ^^^^^^^^^^^
 
+- Add Path.glob()
 - Add PurePath.match()
 
 Version 0.5
       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
 # 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]
 
         else:
             return '', '', part
 
+    def casefold(self, s):
+        return s
+
     def casefold_parts(self, parts):
         return parts
 
         """
         Return True if this path matches the given pattern.
         """
-        cf = self._flavour.casefold_parts
-        path_pattern, = cf((path_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,))[0]:
+        if drv and drv != cf(self._drv):
             return False
-        if root and root != cf((self._root,))[0]:
+        if root and root != cf(self._root):
             return False
         parts = self._cparts
         if drv or root:
         # A stub for the opener argument to built-in open()
         return self._accessor.open(self, flags, mode)
 
+    def _select_children(self, pattern_parts):
+        # Helper for globbing
+        if not pattern_parts:
+            yield self
+            return
+        if not self.is_dir():
+            return
+        pat = pattern_parts[0]
+        pattern_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(pattern_parts):
+                        yield p
+        else:
+            child_path = self._make_child_relpath(pat)
+            if child_path.exists():
+                for p in child_path._select_children(pattern_parts):
+                    yield p
+
     # Public API
 
     @classmethod
             return getattr(self._stat, name)
         return super(Path, self).__getattribute__(name)
 
+    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):
+            yield p
+
     def absolute(self):
         """Return an absolute version of this path.  This function works
         even if the path doesn't point to anything.
 
     using_openat = False
 
+    # (BASE)
+    #  |
+    #  |-- dirA/
+    #       |-- linkC -> "../dirB"
+    #  |-- dirB/
+    #  |    |-- fileB
+    #       |-- linkD -> "../dirB"
+    #  |-- fileA
+    #  |-- linkA -> "fileA"
+    #  |-- linkB -> "dirB"
+    #
+
     def setUp(self):
         os.mkdir(BASE)
         self.addCleanup(support.rmtree, BASE)
         self.assertIn(cm.exception.errno, (errno.ENOTDIR,
                                            errno.ENOENT, errno.EINVAL))
 
+    def test_glob(self):
+        P = self.cls
+        p = P(BASE)
+        it = p.glob("fileA")
+        self.assertIsInstance(it, collections.Iterator)
+        self.assertEqual(set(it), { P(BASE, "fileA") })
+        it = p.glob("fileB")
+        self.assertEqual(set(it), set())
+        it = p.glob("dir*/file*")
+        self.assertEqual(set(it), { P(BASE, "dirB/fileB") })
+        it = p.glob("*A")
+        expected = ['dirA', 'fileA']
+        if not symlink_skip_reason:
+            expected += ['linkA']
+        self.assertEqual(set(it), { P(BASE, q) for q in expected })
+        it = p.glob("*B/*")
+        expected = ['dirB/fileB']
+        if not symlink_skip_reason:
+            expected += ['dirB/linkD', 'linkB/fileB', 'linkB/linkD']
+        self.assertEqual(set(it), { P(BASE, q) for q in expected })
+        it = p.glob("*/fileB")
+        expected = ['dirB/fileB']
+        if not symlink_skip_reason:
+            expected += ['linkB/fileB']
+        self.assertEqual(set(it), { P(BASE, q) for q in expected })
+
+    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)