Recursively generate files (and thier stats) in the indicated directory

    Filter by the indicated file name extension (ext)

    Args:
        path (str):  Root/base path to search.
        ext (str or list of str):  File name extension(s).
            Only file paths that ".endswith()" this string will be returned
        level (int, optional): Depth of file tree to halt recursion at.
            None = full recursion to as deep as it goes
            0 = nonrecursive, just provide a list of files at the root level of the tree
            1 = one level of depth deeper in the tree
        typ (type):  output type (default: list). if a mapping type is provided the keys will be the full paths (unique)
        dirs (bool):  Whether to yield dir paths along with file paths (default: False)
        files (bool): Whether to yield file paths (default: True)
            `dirs=True`, `files=False` is equivalent to `ls -d`

    Returns:
        list of dicts: dict keys are { 'path', 'name', 'bytes', 'created', 'modified', 'accessed', 'permissions' }
        path (str): Full, absolute paths to file beneath the indicated directory and ending with `ext`
        name (str): File name only (everythin after the last slash in the path)
        size (int): File size in bytes
        changed_any (datetime): Timestamp for modification of either metadata (e.g. permissions) or content
        modified (datetime): File content modification timestamp from file system
        accessed (datetime): File access timestamp from file system
        permissions (int): File permissions bytes as a chown-style integer with a maximum of 4 digits
        type (str): One of 'file', 'dir', 'symlink->file', 'symlink->dir', 'symlink->broken'
                e.g.: 777 or 1755

    Examples:
        >>> 'util.py' in [d['name'] for d in generate_files(os.path.dirname(__file__), ext='.py', level=0)]
        True
        >>> next(d for d in generate_files(os.path.dirname(__file__), ext='.py')
        ...      if d['name'] == 'util.py')['size'] > 1000
        True
        >>> sorted(next(generate_files()).keys())
        ['accessed', 'changed_any', 'dir', 'mode', 'modified', 'name', 'path', 'size', 'type']

        There should be an __init__ file in the same directory as this script.
        And it should be at the top of the list.
        >>> sorted(d['name'] for d in generate_files(os.path.dirname(__file__), ext='.py', level=0))[0]
        '__init__.py'
        >>> len(list(generate_files(__file__, ext='.')))
        0
        >>> len(list(generate_files(__file__, ext=['invalidexttesting123', False])))
        0
        >>> len(list(generate_files(__file__, ext=['.py', '.pyc', 'invalidexttesting123', False]))) > 0
        True
        >>> sorted(generate_files(__file__))[0]['name'] == os.path.basename(__file__)
        True
        >>> sorted(list(generate_files())[0].keys())
        ['accessed', 'changed_any', 'dir', 'mode', 'modified', 'name', 'path', 'size', 'type']
        >>> all(d['type'] in ('file', 'dir',
        ...                   'symlink->file', 'symlink->dir', 'symlink->broken',
        ...                   'mount-point->file', 'mount-point->dir',
        ...                   'block-device', 'pipe', 'special', 'socket', 'unknown')
        ...     for d in generate_files(level=1, files=True, dirs=True))
        True

def generate_files(path='', ext='', level=None, dirs=False, files=True, verbosity=0):
    """ Recursively generate files (and thier stats) in the indicated directory

    Filter by the indicated file name extension (ext)

    Args:
        path (str):  Root/base path to search.
        ext (str or list of str):  File name extension(s).
            Only file paths that ".endswith()" this string will be returned
        level (int, optional): Depth of file tree to halt recursion at.
            None = full recursion to as deep as it goes
            0 = nonrecursive, just provide a list of files at the root level of the tree
            1 = one level of depth deeper in the tree
        typ (type):  output type (default: list). if a mapping type is provided the keys will be the full paths (unique)
        dirs (bool):  Whether to yield dir paths along with file paths (default: False)
        files (bool): Whether to yield file paths (default: True)
            `dirs=True`, `files=False` is equivalent to `ls -d`

    Returns:
        list of dicts: dict keys are { 'path', 'name', 'bytes', 'created', 'modified', 'accessed', 'permissions' }
        path (str): Full, absolute paths to file beneath the indicated directory and ending with `ext`
        name (str): File name only (everythin after the last slash in the path)
        size (int): File size in bytes
        changed_any (datetime): Timestamp for modification of either metadata (e.g. permissions) or content
        modified (datetime): File content modification timestamp from file system
        accessed (datetime): File access timestamp from file system
        permissions (int): File permissions bytes as a chown-style integer with a maximum of 4 digits
        type (str): One of 'file', 'dir', 'symlink->file', 'symlink->dir', 'symlink->broken'
                e.g.: 777 or 1755

    Examples:
        >>> 'util.py' in [d['name'] for d in generate_files(os.path.dirname(__file__), ext='.py', level=0)]
        True
        >>> next(d for d in generate_files(os.path.dirname(__file__), ext='.py')
        ...      if d['name'] == 'util.py')['size'] > 1000
        True
        >>> sorted(next(generate_files()).keys())
        ['accessed', 'changed_any', 'dir', 'mode', 'modified', 'name', 'path', 'size', 'type']

        There should be an __init__ file in the same directory as this script.
        And it should be at the top of the list.
        >>> sorted(d['name'] for d in generate_files(os.path.dirname(__file__), ext='.py', level=0))[0]
        '__init__.py'
        >>> len(list(generate_files(__file__, ext='.')))
        0
        >>> len(list(generate_files(__file__, ext=['invalidexttesting123', False])))
        0
        >>> len(list(generate_files(__file__, ext=['.py', '.pyc', 'invalidexttesting123', False]))) > 0
        True
        >>> sorted(generate_files(__file__))[0]['name'] == os.path.basename(__file__)
        True
        >>> sorted(list(generate_files())[0].keys())
        ['accessed', 'changed_any', 'dir', 'mode', 'modified', 'name', 'path', 'size', 'type']
        >>> all(d['type'] in ('file', 'dir',
        ...                   'symlink->file', 'symlink->dir', 'symlink->broken',
        ...                   'mount-point->file', 'mount-point->dir',
        ...                   'block-device', 'pipe', 'special', 'socket', 'unknown')
        ...     for d in generate_files(level=1, files=True, dirs=True))
        True
    """
    path = expand_path(path or '.')
    # None interpreted as '', False is interpreted as '.' (no ext will be accepted)
    ext = '.' if ext is False else ext
    # multiple extensions can be specified in a list or tuple
    ext = ext if ext and isinstance(ext, (list, tuple)) else [ext]
    # case-insensitive extensions, '.' ext means only no-extensions are accepted
    ext = set(x.lower() if x else '.' if x is False else '' for x in ext)

    if os.path.isfile(path):
        fn = os.path.basename(path)
        # only yield the stat dict if the extension is among those that match or files without any ext are desired
        if not ext or any(path.lower().endswith(x) or (x == '.' and '.' not in fn) for x in ext):
            yield path_status(os.path.dirname(path), os.path.basename(path), verbosity=verbosity)
    else:
        for dir_path, dir_names, filenames in walk_level(path, level=level):
            if verbosity > 0:
                print('Checking path "{}"'.format(dir_path))
            if files:
                for fn in filenames:  # itertools.chain(filenames, dir_names)
                    if ext and not any((fn.lower().endswith(x) or (x == '.' and x not in fn) for x in ext)):
                        continue
                    stat = path_status(dir_path, fn, verbosity=verbosity)
                    if stat and stat['name'] and stat['path']:
                        yield stat
            if dirs:
                for fn in dir_names:
                    if ext and not any((fn.lower().endswith(x) or (x == '.' and x not in fn) for x in ext)):
                        continue
                    yield path_status(dir_path, fn, verbosity=verbosity)