pax_global_header00006660000000000000000000000064130070761260014514gustar00rootroot0000000000000052 comment=4851f8c82363113db551f86e612dc8d32b38692f python-glob2-0.5/000077500000000000000000000000001300707612600137045ustar00rootroot00000000000000python-glob2-0.5/.gitignore000066400000000000000000000001661300707612600156770ustar00rootroot00000000000000*.pyc /LOCAL_TODO # distutils/setuptools /dist/ *egg-info # IDEs *.wpr /.idea/ # Folder config file [Dd]esktop.ini python-glob2-0.5/.idea/000077500000000000000000000000001300707612600146645ustar00rootroot00000000000000python-glob2-0.5/.idea/encodings.xml000066400000000000000000000002461300707612600173610ustar00rootroot00000000000000 python-glob2-0.5/.idea/modules.xml000066400000000000000000000004261300707612600170600ustar00rootroot00000000000000 python-glob2-0.5/.idea/python-glob2.iml000066400000000000000000000010521300707612600177110ustar00rootroot00000000000000 python-glob2-0.5/CHANGES000066400000000000000000000004311300707612600146750ustar00rootroot000000000000000.5 (2016-11-04) - include_hidden option. - Python 3 fixes. - Publish a wheel. 0.4 (2013-05-08) - Support Python 3. 0.3 (2012-01-19) - Fix non-glob patterns (patch by Zalan). - Don't shadow internal "glob" module. 0.2 (2011-06-14) - Initial release. python-glob2-0.5/LICENSE000066400000000000000000000025171300707612600147160ustar00rootroot00000000000000Copyright (c) 2008, Michael Elsdörfer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.python-glob2-0.5/MANIFEST.in000066400000000000000000000000631300707612600154410ustar00rootroot00000000000000include README.rst CHANGES LICENSE include test.py python-glob2-0.5/README.rst000066400000000000000000000046511300707612600154010ustar00rootroot00000000000000python-glob2 ============ This is an extended version of Python's builtin glob module (http://docs.python.org/library/glob.html) which adds: - The ability to capture the text matched by glob patterns, and return those matches alongside the filenames. - A recursive '**' globbing syntax, akin for example to the ``globstar`` option of the bash shell. - The ability to replace the filesystem functions used, in order to glob on virtual filesystems. - Compatible with Python 2 and Python 3 (tested with 3.3). It's currently based on the glob code from Python 3.3.1. Examples -------- Matches being returned: ~~~~~~~~~~~~~~~~~~~~~~~ :: import glob2 for filename, (version,) in glob2.iglob('./binaries/project-*.zip', with_matches=True): print version Recursive glob: ~~~~~~~~~~~~~~~ :: >>> import glob2 >>> all_header_files = glob2.glob('src/**/*.h') ['src/fs.h', 'src/media/mp3.h', 'src/media/mp3/frame.h', ...] Note that ``**`` must appear on it's own as a directory element to have its special meaning. ``**h`` will not have the desired effect. ``**`` will match ".", so ``**/*.py`` returns Python files in the current directory. If this is not wanted, ``*/**/*.py`` should be used instead. Custom Globber: ~~~~~~~~~~~~~~~ :: from glob2 import Globber class VirtualStorageGlobber(Globber): def __init__(self, storage): self.storage = storage def listdir(self, path): # Must raise os.error if path is not a directory return self.storage.listdir(path) def exists(self, path): return self.storage.exists(path) def isdir(self, path): # Used only for trailing slash syntax (``foo/``). return self.storage.isdir(path) def islink(self, path): # Used only for recursive glob (``**``). return self.storage.islink(path) globber = VirtualStorageGlobber(sftp_storage) globber.glob('/var/www/**/*.js') If ``isdir`` and/or ``islink`` cannot be implemented for a storage, you can make them return a fixed value, with the following consequences: - If ``isdir`` returns ``True``, a glob expression ending with a slash will return all items, even non-directories, if it returns ``False``, the same glob expression will return nothing. - Return ``islink`` ``True``, the recursive globbing syntax ** will follow all links. If you return ``False``, it will not work at all. python-glob2-0.5/TODO000066400000000000000000000003621300707612600143750ustar00rootroot00000000000000Because our implementation of recursive directory search (**) using os.walk, and the matching using fnmatch, are both not using iterators, something like /** currently needs to read the whole filesystem into memory before returning anything. python-glob2-0.5/glob2/000077500000000000000000000000001300707612600147115ustar00rootroot00000000000000python-glob2-0.5/glob2/__init__.py000066400000000000000000000001221300707612600170150ustar00rootroot00000000000000from __future__ import absolute_import from .impl import * __version__ = (0, 5) python-glob2-0.5/glob2/compat.py000066400000000000000000000153131300707612600165510ustar00rootroot00000000000000# Back-port functools.lru_cache to Python 2 (and <= 3.2) # {{{ http://code.activestate.com/recipes/578078/ (r6) from collections import namedtuple from functools import update_wrapper from threading import RLock _CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) class _HashedSeq(list): __slots__ = 'hashvalue' def __init__(self, tup, hash=hash): self[:] = tup self.hashvalue = hash(tup) def __hash__(self): return self.hashvalue def _make_key(args, kwds, typed, kwd_mark = (object(),), fasttypes = set((int, str, frozenset, type(None))), sorted=sorted, tuple=tuple, type=type, len=len): 'Make a cache key from optionally typed positional and keyword arguments' key = args if kwds: sorted_items = sorted(kwds.items()) key += kwd_mark for item in sorted_items: key += item if typed: key += tuple(type(v) for v in args) if kwds: key += tuple(type(v) for k, v in sorted_items) elif len(key) == 1 and type(key[0]) in fasttypes: return key[0] return _HashedSeq(key) def lru_cache(maxsize=100, typed=False): """Least-recently-used cache decorator. If *maxsize* is set to None, the LRU features are disabled and the cache can grow without bound. If *typed* is True, arguments of different types will be cached separately. For example, f(3.0) and f(3) will be treated as distinct calls with distinct results. Arguments to the cached function must be hashable. View the cache statistics named tuple (hits, misses, maxsize, currsize) with f.cache_info(). Clear the cache and statistics with f.cache_clear(). Access the underlying function with f.__wrapped__. See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used """ # Users should only access the lru_cache through its public API: # cache_info, cache_clear, and f.__wrapped__ # The internals of the lru_cache are encapsulated for thread safety and # to allow the implementation to change (including a possible C version). def decorating_function(user_function): cache = dict() stats = [0, 0] # make statistics updateable non-locally HITS, MISSES = 0, 1 # names for the stats fields make_key = _make_key cache_get = cache.get # bound method to lookup key or return None _len = len # localize the global len() function lock = RLock() # because linkedlist updates aren't threadsafe root = [] # root of the circular doubly linked list root[:] = [root, root, None, None] # initialize by pointing to self nonlocal_root = [root] # make updateable non-locally PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields if maxsize == 0: def wrapper(*args, **kwds): # no caching, just do a statistics update after a successful call result = user_function(*args, **kwds) stats[MISSES] += 1 return result elif maxsize is None: def wrapper(*args, **kwds): # simple caching without ordering or size limit key = make_key(args, kwds, typed) result = cache_get(key, root) # root used here as a unique not-found sentinel if result is not root: stats[HITS] += 1 return result result = user_function(*args, **kwds) cache[key] = result stats[MISSES] += 1 return result else: def wrapper(*args, **kwds): # size limited caching that tracks accesses by recency key = make_key(args, kwds, typed) if kwds or typed else args with lock: link = cache_get(key) if link is not None: # record recent use of the key by moving it to the front of the list root, = nonlocal_root link_prev, link_next, key, result = link link_prev[NEXT] = link_next link_next[PREV] = link_prev last = root[PREV] last[NEXT] = root[PREV] = link link[PREV] = last link[NEXT] = root stats[HITS] += 1 return result result = user_function(*args, **kwds) with lock: root, = nonlocal_root if key in cache: # getting here means that this same key was added to the # cache while the lock was released. since the link # update is already done, we need only return the # computed result and update the count of misses. pass elif _len(cache) >= maxsize: # use the old root to store the new key and result oldroot = root oldroot[KEY] = key oldroot[RESULT] = result # empty the oldest link and make it the new root root = nonlocal_root[0] = oldroot[NEXT] oldkey = root[KEY] oldvalue = root[RESULT] root[KEY] = root[RESULT] = None # now update the cache dictionary for the new links del cache[oldkey] cache[key] = oldroot else: # put result in a new link at the front of the list last = root[PREV] link = [last, root, key, result] last[NEXT] = root[PREV] = cache[key] = link stats[MISSES] += 1 return result def cache_info(): """Report cache statistics""" with lock: return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) def cache_clear(): """Clear the cache and cache statistics""" with lock: cache.clear() root = nonlocal_root[0] root[:] = [root, root, None, None] stats[:] = [0, 0] wrapper.__wrapped__ = user_function wrapper.cache_info = cache_info wrapper.cache_clear = cache_clear return update_wrapper(wrapper, user_function) return decorating_functionpython-glob2-0.5/glob2/fnmatch.py000066400000000000000000000063461300707612600167140ustar00rootroot00000000000000"""Filename matching with shell patterns. fnmatch(FILENAME, PATTERN) matches according to the local convention. fnmatchcase(FILENAME, PATTERN) always takes case in account. The functions operate by translating the pattern into a regular expression. They cache the compiled regular expressions for speed. The function translate(PATTERN) returns a regular expression corresponding to PATTERN. (It does not compile it.) """ import os import posixpath import re try: from functools import lru_cache except ImportError: from .compat import lru_cache __all__ = ["filter", "fnmatch", "fnmatchcase", "translate"] def fnmatch(name, pat): """Test whether FILENAME matches PATTERN. Patterns are Unix shell style: * matches everything ? matches any single character [seq] matches any character in seq [!seq] matches any char not in seq An initial period in FILENAME is not special. Both FILENAME and PATTERN are first case-normalized if the operating system requires it. If you don't want this, use fnmatchcase(FILENAME, PATTERN). """ name = os.path.normcase(name) pat = os.path.normcase(pat) return fnmatchcase(name, pat) lru_cache(maxsize=256, typed=True) def _compile_pattern(pat): if isinstance(pat, bytes): pat_str = pat.decode('ISO-8859-1') res_str = translate(pat_str) res = res_str.encode('ISO-8859-1') else: res = translate(pat) return re.compile(res).match def filter(names, pat): """Return the subset of the list NAMES that match PAT.""" result = [] pat = os.path.normcase(pat) match = _compile_pattern(pat) if os.path is posixpath: # normcase on posix is NOP. Optimize it away from the loop. for name in names: m = match(name) if m: result.append((name, m.groups())) else: for name in names: m = match(os.path.normcase(name)) if m: result.append((name, m.groups())) return result def fnmatchcase(name, pat): """Test whether FILENAME matches PATTERN, including case. This is a version of fnmatch() which doesn't case-normalize its arguments. """ match = _compile_pattern(pat) return match(name) is not None def translate(pat): """Translate a shell PATTERN to a regular expression. There is no way to quote meta-characters. """ i, n = 0, len(pat) res = '' while i < n: c = pat[i] i = i+1 if c == '*': res = res + '(.*)' elif c == '?': res = res + '(.)' elif c == '[': j = i if j < n and pat[j] == '!': j = j+1 if j < n and pat[j] == ']': j = j+1 while j < n and pat[j] != ']': j = j+1 if j >= n: res = res + '\\[' else: stuff = pat[i:j].replace('\\','\\\\') i = j+1 if stuff[0] == '!': stuff = '^' + stuff[1:] elif stuff[0] == '^': stuff = '\\' + stuff res = '%s([%s])' % (res, stuff) else: res = res + re.escape(c) return res + '\Z(?ms)' python-glob2-0.5/glob2/impl.py000066400000000000000000000164751300707612600162410ustar00rootroot00000000000000"""Filename globbing utility.""" from __future__ import absolute_import import sys import os import re from . import fnmatch try: from itertools import imap except ImportError: imap = map class Globber(object): listdir = staticmethod(os.listdir) isdir = staticmethod(os.path.isdir) islink = staticmethod(os.path.islink) exists = staticmethod(os.path.lexists) def walk(self, top, followlinks=False): """A simplified version of os.walk (code copied) that uses ``self.listdir``, and the other local filesystem methods. Because we don't care about file/directory distinctions, only a single list is returned. """ try: names = self.listdir(top) except os.error as err: return items = [] for name in names: items.append(name) yield top, items for name in items: new_path = os.path.join(top, name) if followlinks or not self.islink(new_path): for x in self.walk(new_path, followlinks): yield x def glob(self, pathname, with_matches=False, include_hidden=False): """Return a list of paths matching a pathname pattern. The pattern may contain simple shell-style wildcards a la fnmatch. However, unlike fnmatch, filenames starting with a dot are special cases that are not matched by '*' and '?' patterns. If ``include_hidden`` is True, then files and folders starting with a dot are also returned. """ return list(self.iglob(pathname, with_matches, include_hidden)) def iglob(self, pathname, with_matches=False, include_hidden=False): """Return an iterator which yields the paths matching a pathname pattern. The pattern may contain simple shell-style wildcards a la fnmatch. However, unlike fnmatch, filenames starting with a dot are special cases that are not matched by '*' and '?' patterns. If ``with_matches`` is True, then for each matching path a 2-tuple will be returned; the second element if the tuple will be a list of the parts of the path that matched the individual wildcards. If ``include_hidden`` is True, then files and folders starting with a dot are also returned. """ result = self._iglob(pathname, include_hidden=include_hidden) if with_matches: return result return imap(lambda s: s[0], result) def _iglob(self, pathname, rootcall=True, include_hidden=False): """Internal implementation that backs :meth:`iglob`. ``rootcall`` is required to differentiate between the user's call to iglob(), and subsequent recursive calls, for the purposes of resolving certain special cases of ** wildcards. Specifically, "**" is supposed to include the current directory for purposes of globbing, but the directory itself should never be returned. So if ** is the lastmost part of the ``pathname`` given the user to the root call, we want to ignore the current directory. For this, we need to know which the root call is. """ # Short-circuit if no glob magic if not has_magic(pathname): if self.exists(pathname): yield pathname, () return # If no directory part is left, assume the working directory dirname, basename = os.path.split(pathname) # If the directory is globbed, recurse to resolve. # If at this point there is no directory part left, we simply # continue with dirname="", which will search the current dir. # `os.path.split()` returns the argument itself as a dirname if it is a # drive or UNC path. Prevent an infinite recursion if a drive or UNC path # contains magic characters (i.e. r'\\?\C:'). if dirname != pathname and has_magic(dirname): # Note that this may return files, which will be ignored # later when we try to use them as directories. # Prefiltering them here would only require more IO ops. dirs = self._iglob(dirname, False, include_hidden) else: dirs = [(dirname, ())] # Resolve ``basename`` expr for every directory found for dirname, dir_groups in dirs: for name, groups in self.resolve_pattern( dirname, basename, not rootcall, include_hidden): yield os.path.join(dirname, name), dir_groups + groups def resolve_pattern(self, dirname, pattern, globstar_with_root, include_hidden): """Apply ``pattern`` (contains no path elements) to the literal directory in ``dirname``. If pattern=='', this will filter for directories. This is a special case that happens when the user's glob expression ends with a slash (in which case we only want directories). It simpler and faster to filter here than in :meth:`_iglob`. """ if sys.version_info[0] == 3: if isinstance(pattern, bytes): dirname = bytes(os.curdir, 'ASCII') else: if isinstance(pattern, unicode) and not isinstance(dirname, unicode): dirname = unicode(dirname, sys.getfilesystemencoding() or sys.getdefaultencoding()) # If no magic, short-circuit, only check for existence if not has_magic(pattern): if pattern == '': if self.isdir(dirname): return [(pattern, ())] else: if self.exists(os.path.join(dirname, pattern)): return [(pattern, ())] return [] if not dirname: dirname = os.curdir try: if pattern == '**': # Include the current directory in **, if asked; by adding # an empty string as opposed to '.', we spare ourselves # having to deal with os.path.normpath() later. names = [''] if globstar_with_root else [] for top, entries in self.walk(dirname): _mkabs = lambda s: os.path.join(top[len(dirname)+1:], s) names.extend(map(_mkabs, entries)) # Reset pattern so that fnmatch(), which does not understand # ** specifically, will only return a single group match. pattern = '*' else: names = self.listdir(dirname) except os.error: return [] if not include_hidden and not _ishidden(pattern): # Remove hidden files, but take care to ensure # that the empty string we may have added earlier remains. # Do not filter out the '' that we might have added earlier names = filter(lambda x: not x or not _ishidden(x), names) return fnmatch.filter(names, pattern) default_globber = Globber() glob = default_globber.glob iglob = default_globber.iglob del default_globber magic_check = re.compile('[*?[]') magic_check_bytes = re.compile(b'[*?[]') def has_magic(s): if isinstance(s, bytes): match = magic_check_bytes.search(s) else: match = magic_check.search(s) return match is not None def _ishidden(path): return path[0] in ('.', b'.'[0]) python-glob2-0.5/setup.cfg000066400000000000000000000000341300707612600155220ustar00rootroot00000000000000[bdist_wheel] universal = 1 python-glob2-0.5/setup.py000077500000000000000000000023131300707612600154200ustar00rootroot00000000000000#!/usr/bin/env python import os from setuptools import setup, find_packages # Figure out the version import re here = os.path.dirname(os.path.abspath(__file__)) version_re = re.compile( r'__version__ = (\(.*?\))') fp = open(os.path.join(here, 'glob2', '__init__.py')) version = None for line in fp: match = version_re.search(line) if match: version = eval(match.group(1)) break else: raise Exception("Cannot find version in __init__.py") fp.close() setup( name = 'glob2', version = ".".join(map(str, version)), description = 'Version of the glob module that can capture patterns '+ 'and supports recursive wildcards', author = 'Michael Elsdoerfer', author_email = 'michael@elsdoerfer.com', license='BSD', url = 'http://github.com/miracle2k/python-glob2/', classifiers = [ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Libraries', ], packages = find_packages() ) python-glob2-0.5/test.py000066400000000000000000000117041300707612600152400ustar00rootroot00000000000000import os from os import path import shutil import tempfile import glob2 from glob2 import fnmatch class TestFnmatch(object): def test_filter_everything(self): names = ( 'fooABC', 'barABC', 'foo',) assert fnmatch.filter(names, 'foo*') == [ ('fooABC', ('ABC',)), ('foo', ('',)) ] assert fnmatch.filter(names, '*AB*') == [ ('fooABC', ('foo', 'C')), ('barABC', ('bar', 'C')) ] def test_filter_single_character(self): names = ( 'fooA', 'barA', 'foo',) assert fnmatch.filter(names, 'foo?') == [ ('fooA', ('A',)), ] assert fnmatch.filter(names, '???A') == [ ('fooA', ('f', 'o', 'o',)), ('barA', ('b', 'a', 'r',)), ] def test_sequence(self): names = ( 'fooA', 'fooB', 'fooC', 'foo',) assert fnmatch.filter(names, 'foo[AB]') == [ ('fooA', ('A',)), ('fooB', ('B',)), ] assert fnmatch.filter(names, 'foo[!AB]') == [ ('fooC', ('C',)), ] class BaseTest(object): def setup(self): self.basedir = tempfile.mkdtemp() self._old_cwd = os.getcwd() os.chdir(self.basedir) self.setup_files() def setup_files(self): pass def teardown(self): os.chdir(self._old_cwd) shutil.rmtree(self.basedir) def makedirs(self, *names): for name in names: os.makedirs(path.join(self.basedir, name)) def touch(self, *names): for name in names: open(path.join(self.basedir, name), 'w').close() class TestPatterns(BaseTest): def test(self): self.makedirs('dir1', 'dir22') self.touch( 'dir1/a-file', 'dir1/b-file', 'dir22/a-file', 'dir22/b-file') assert glob2.glob('dir?/a-*', True) == [ ('dir1/a-file', ('1', 'file')) ] class TestRecursive(BaseTest): def setup_files(self): self.makedirs('a', 'b', 'a/foo') self.touch('file.py', 'file.txt', 'a/bar.py', 'README', 'b/py', 'b/bar.py', 'a/foo/hello.py', 'a/foo/world.txt') def test_recursive(self): # ** includes the current directory assert sorted(glob2.glob('**/*.py', True)) == [ ('a/bar.py', ('a', 'bar')), ('a/foo/hello.py', ('a/foo', 'hello')), ('b/bar.py', ('b', 'bar')), ('file.py', ('', 'file')), ] def test_exclude_root_directory(self): # If files from the root directory should not be included, # this is the syntax to use: assert sorted(glob2.glob('*/**/*.py', True)) == [ ('a/bar.py', ('a', '', 'bar')), ('a/foo/hello.py', ('a', 'foo', 'hello')), ('b/bar.py', ('b', '', 'bar')) ] def test_only_directories(self): # Return directories only assert sorted(glob2.glob('**/', True)) == [ ('a/', ('a',)), ('a/foo/', ('a/foo',)), ('b/', ('b',)), ] def test_parent_dir(self): # Make sure ".." can be used os.chdir(path.join(self.basedir, 'b')) assert sorted(glob2.glob('../a/**/*.py', True)), [ ('../a/bar.py', ('', 'bar')), ('../a/foo/hello.py', ('foo', 'hello')) ] def test_fixed_basename(self): assert sorted(glob2.glob('**/bar.py', True)) == [ ('a/bar.py', ('a',)), ('b/bar.py', ('b',)), ] def test_all_files(self): # Return all files os.chdir(path.join(self.basedir, 'a')) assert sorted(glob2.glob('**', True)) == [ ('bar.py', ('bar.py',)), ('foo', ('foo',)), ('foo/hello.py', ('foo/hello.py',)), ('foo/world.txt', ('foo/world.txt',)), ] def test_root_directory_not_returned(self): # Ensure that a certain codepath (when the basename is globbed # with ** as opposed to the dirname) does not cause # the root directory to be part of the result. # -> b/ is NOT in the result! assert sorted(glob2.glob('b/**', True)) == [ ('b/bar.py', ('bar.py',)), ('b/py', ('py',)), ] def test_non_glob(self): # Test without patterns. assert glob2.glob(__file__, True) == [ (__file__, ()) ] assert glob2.glob(__file__) == [ (__file__) ] class TestIncludeHidden(BaseTest): def setup_files(self): self.makedirs('a', 'b', 'a/.foo') self.touch('file.py', 'file.txt', 'a/.bar', 'README', 'b/py', 'b/.bar', 'a/.foo/hello.py', 'a/.foo/world.txt') def test_hidden(self): # ** includes the current directory assert sorted(glob2.glob('*/*', True, include_hidden=True)), [ ('a/.bar', ('a', '.bar')), ('a/.foo', ('a', '.foo')), ('b/.bar', ('b', '.bar')), ('b/py', ('b', 'py')), ]