Module seedir.folderstructure

The primary algorithm for determining the folder structure returned by seedir() and FakeDir.seedir().

Expand source code
# -*- coding: utf-8 -*-
"""
The primary algorithm for determining the folder structure returned by
`seedir.realdir.seedir()` and `seedir.fakedir.FakeDir.seedir()`.

"""

import os
import warnings

import natsort

import seedir.printing as printing

def listdir_fullpath(path):
    '''Like `os.listdir()`, but returns absolute paths.'''
    return [os.path.join(path, f) for f in os.listdir(path)]

def formatter_update_args(formatter, item, args):
    '''
    Update a dictionary of style tokens based on a formatter function and
    an item.  Added in v 0.3.0 to support the addition of the formatter parameter
    for the seedir algorithm.

    Parameters
    ----------
    formatter : function
        Formatting function.
    item : file or folder item
        String or FakeItem produced in the seedir algorithm
    styleargs : dict
        Dictionary of seedir style tokens

    Returns
    -------
    styleargs : dict
        The input dictionary, after it may have been updated.

    '''
    newstyle = formatter(item)
    if newstyle is None:
        pass
    else:
        args.update(newstyle)

    return args

class FolderStructure:
    '''General class for determining folder strctures.  Implements
    the seedir folder-tree generating algorithm over arbitrary objects.'''

    def __init__(self, getname_func, isdir_func, listdir_func):
        '''
        Defines the functions used by `self` for generating folder structures.

        Parameters
        ----------
        getname_func : function
            Returns the name of the object.
        isdir_func : function
            Returns boolean of whether child object is a folder.
        listdir_func : function
            Function for returning the children of an object, given it is a
            "folder" as determined by `isdir_func`.  The child objects returned
            should be of similar type to `self`, such that the functions can
            be recursively applied.

        Returns
        -------
        None.

        '''
        self.listdir = listdir_func
        self.isdir = isdir_func
        self.getname = getname_func

    def __call__(self, folder, style='lines', printout=True, indent=2, uniform=None,
                 anystart=None, anyend=None, depthlimit=None, itemlimit=None,
                 beyond=None, first=None, sort=False, sort_reverse=False,
                 sort_key=None, include_folders=None, exclude_folders=None,
                 include_files=None, exclude_files=None, regex=False, mask=None,
                 formatter=None, sticky_formatter=False, slash=None, **kwargs):
        '''Call this on a folder object to generate the seedir output
        for that object.'''

        accept_kwargs = ['extend', 'split', 'space', 'final',
                         'folderstart', 'filestart', 'folderend', 'fileend']

        if any(i not in accept_kwargs for i in kwargs.keys()):
            bad_kwargs = [i for i in kwargs.keys() if i not in accept_kwargs]
            raise ValueError(f'kwargs must be any of {accept_kwargs}; '
                             f'unrecognized kwargs: {bad_kwargs}')

        styleargs = printing.get_styleargs(style)
        printing.format_indent(styleargs, indent=indent)

        if uniform is not None:
            for arg in ['extend', 'split', 'final', 'space']:
                styleargs[arg] = uniform

        if anystart is not None:
            styleargs['folderstart'] = anystart
            styleargs['filestart'] = anystart

        if anyend is not None:
            styleargs['folderend'] = anyend
            styleargs['fileend'] = anyend

        if slash is not None:
            warnings.warn("`slash` will removed in a future version; "
                          "pass `folderend` as a keyword argument instead.",
                          DeprecationWarning)
            if slash.lower() in ['sep', 'os.sep']:
                slash = os.sep
            styleargs['folderend'] = slash

        for k in kwargs:
            if k in styleargs:
                styleargs[k] = kwargs[k]

        if sort_key is not None or sort_reverse == True:
            sort = True

        default_args = dict(depthlimit=depthlimit,
                            itemlimit=itemlimit,
                            beyond=beyond,
                            first=first,
                            sort=sort,
                            sort_reverse=sort_reverse,
                            sort_key=sort_key,
                            include_folders=include_folders,
                            exclude_folders=exclude_folders,
                            include_files=include_files,
                            exclude_files=exclude_files,
                            regex=regex,
                            mask=mask,
                            formatter=formatter,
                            sticky_formatter=sticky_formatter,
                            **styleargs)

        current_args = default_args.copy()

        # apply formatter to top level
        if formatter is not None:
            formatter_update_args(formatter, folder, current_args)

        if sticky_formatter:
            default_args = current_args

        s = self._folderstructure_recurse(folder, default_args, **current_args).strip()

        if printout:
            print(s)
        else:
            return s

    def beyond_depth_str(self, items, beyond):
        '''
        Generates the text when using the 'beyond'
        parameter and ther are more items than the itemlimit or contents
        beyond the depthlimit.

        Parameters
        ----------
        beyond : str
            Style of beyond string to generate.  Options are:
                - 'ellipsis' ('...')
                - 'content' or 'contents' (the number of files and folders beyond)
                - a string starting with '_' (everything after the leading
                  underscore will be returned)
        items : collection of file paths, optional
            Path items of the items beyond the limit, used when the 'beyond'
            argeument is 'content' or 'contents'. The default is None.


        Returns
        -------
        str
            String indicating what lies beyond

        '''
        if beyond.lower() == 'ellipsis':
            return '...'
        elif beyond.lower() in ['contents','content']:
            folders = self.count_folders(items)
            files = self.count_files(items)
            return '{} folder(s), {} file(s)'.format(folders, files)
        elif beyond and beyond[0] == '_':
            return beyond[1:]
        else:
            s1 = '"beyond" must be "ellipsis", "content", or '
            s2 = 'a string starting with "_"'
            raise ValueError(s1 + s2)

    def count_files(self, items):
        '''
        Count the number of files in a collection of paths.

        Parameters
        ----------
        paths : list-like
            Collection of paths.

        Returns
        -------
        files : int
            Count of files.

        '''
        files = sum([not self.isdir(i) for i in items])
        return files

    def count_folders(self, items):
        '''
        Count the number of folders in a collection of paths.

        Parameters
        ----------
        paths : list-like
            Collection of paths.

        Returns
        -------
        files : int
            Count of folders.

        '''
        folders = sum([self.isdir(i) for i in items])
        return folders

    def filter_items(self, listdir, include_folders=None,
                      exclude_folders=None, include_files=None,
                      exclude_files=None, regex=False, mask=None):

        '''
        Filter for folder and file names in folder structures.  Removes or includes
        items matching filtering strings.

        Note the following priority of arguments:
            1. Mask (totally overwrites include/exclude)
            2. Include (saves items from being excluded)
            3. Exclude

        Parameters
        ----------
        listdir : list-like
            Collection of file/folder items.
        include_folders : str or list-like, optional
            Folder names to include. The default is None.
        exclude_folders : str or list-like, optional
            Folder names to exclude. The default is None.
        include_files : str or list-like, optional
            File names to include. The default is None.
        exclude_files : str or list-like, optional
            File names to exclude. The default is None.
        regex : bool, optional
            Interpret strings as regular expressions. The default is False.
        mask : function, optional
            Function for filtering items.  Absolute paths of each individual item
            are passed to the mask function.  If True is returned, the
            item is kept.  The default is None.

        Returns
        -------
        keep : list
            Filtered input.

        '''

        def _should_convert(x):
            return isinstance(x, str) or x is None

        filtered = []
        for item in listdir:

            name = self.getname(item)

            if self.isdir(item):
                inc = [include_folders] if _should_convert(include_folders) else include_folders
                exc = [exclude_folders] if _should_convert(exclude_folders) else exclude_folders
            else:
                inc = [include_files] if _should_convert(include_files) else include_files
                exc = [exclude_files] if _should_convert(exclude_files) else exclude_files

            # 1. check mask - which trumps include/exclude arguments
            if mask is not None:
                if mask(item):
                    filtered.append(item)
                continue

            # set default keep behavior
            # items are exluded if inclusion is passed
            keep = all([i is None for i in inc])

            # 2. apply exclusion
            for pat in exc:
                if pat is not None:
                    match = printing.is_match(pattern=pat, string=name, regex=regex)
                    if match:
                        keep = False

            # 3. apply inclusion (trumps exclusion)
            for pat in inc:
                if pat is not None:
                    match = printing.is_match(pattern=pat, string=name, regex=regex)
                    if match:
                        keep = True

            if keep:
                filtered.append(item)

        return filtered


    def _folderstructure_recurse(self, folder, default_args, depth=0, incomplete=None,
                                 extend='│ ', space='  ', split='├─', final='└─',
                                 filestart='', folderstart='', fileend='', folderend='/',
                                 depthlimit=None, itemlimit=None, beyond=None, first=None,
                                 sort=True, sort_reverse=False, sort_key=None,
                                 include_folders=None, exclude_folders=None,
                                 include_files=None, exclude_files=None,
                                 regex=False, mask=None, formatter=None, sticky_formatter=False):
        '''
        Main algorithm for creating folder structure string.  See
        `seedir.realdir.seedir()` or `seedir.fakedir.FakeDir.seedir()`
        for a description of the parameters.

        Note that since the expansion of the formatter parameter
        (version 0.3.1), the call & logic has been slightly updated.
        Primarily, the `default_args` parameter has been added, to represent
        arguments to default to when the formatter is not in use.  The actual
        keyword arguments are treated as the options to be used for `folder`,
        and can change between iterations.  The `default_args` stay the same,
        unless `sticky_formatter` is used in combination with `formatter` to
        permanently update the default arguments.

        **In almost all cases, users should not need to call this function
        to generate diagrams**.  Rather, they should use the `__call__`
        method of a `FolderStructure` instance.

        '''

        # initialize
        output = ''

        if incomplete is None:
            incomplete = []

        # root string
        if depth == 0:

            d0args = {
                'extend': extend,
                'space': space,
                'split': split,
                'final': final,
                'filestart': filestart,
                'folderstart': folderstart,
                'fileend' : fileend,
                'folderend': folderend
                }

            if formatter is not None:
                formatter_update_args(formatter, folder, d0args)
            output += (d0args['folderstart'] +
                       self.getname(folder) +
                       d0args['folderend'] +
                       '\n')


        current_itemlimit = itemlimit
        beyond_added = False

        # handle when depthlimit is reached
        if isinstance(depthlimit, int) and depth >= depthlimit:
            if beyond is None:
                return output
            else:
                current_itemlimit = 0

        # get all children, add depth to docket
        listdir = self.listdir(folder)
        incomplete.append(depth)

        # sort and filter the contents of listdir
        sortargs = {
            'first': first,
            'sort_reverse': sort_reverse,
            'sort_key': sort_key}

        filterargs = {
            'include_folders': include_folders,
            'exclude_folders': exclude_folders,
            'include_files' : include_files,
            'exclude_files': exclude_files,
            'mask': mask
            }

        if sort or first is not None:
            listdir = self.sort_dir(listdir, **sortargs)

        if any(arg is not None for arg in filterargs.values()):
            listdir = self.filter_items(listdir, **filterargs, regex=regex)

        # set current_itemlimit based on listdir size
        if current_itemlimit is None:
            current_itemlimit = len(listdir)
        else:
            current_itemlimit = min(current_itemlimit, len(listdir))

        # segment output into what can be included
        finalpaths = listdir[:current_itemlimit]
        rem = listdir[current_itemlimit:]

        # append beyond string if being used
        if beyond is not None:
            if rem or (depth == depthlimit):
                finalpaths += [self.beyond_depth_str(rem, beyond)]
                beyond_added = True

        # if empty, close the current depth
        if not finalpaths:
            incomplete.remove(depth)

        # get output for each item in folder
        for i, f in enumerate(finalpaths):

            current_args = default_args.copy() # used in this iteration
            next_default_args = default_args   # passed to child

            lastitem = (i == (len(finalpaths) - 1))
            isbeyondstr = lastitem and beyond_added
            name = f if isbeyondstr else self.getname(f)

            # update tokens with formatter if passed
            if not isbeyondstr and formatter is not None:
                formatter_update_args(formatter, f, current_args)

            if sticky_formatter:
                next_default_args = current_args

            # create header for the item
            base_header = self.get_base_header(incomplete,
                                               current_args['extend'],
                                               current_args['space'])

            if lastitem:
                branch = current_args['final']
                incomplete.remove(depth)
            else:
                branch = current_args['split']

            header = base_header + branch

            # concat to output and recurse if item is folder
            if not isbeyondstr and self.isdir(f):
                output += (header +
                           current_args['folderstart'] +
                           name +
                           current_args['folderend'] +
                           '\n')
                output += self._folderstructure_recurse(f, depth=depth + 1,
                                                        incomplete=incomplete,
                                                        default_args=next_default_args,
                                                        **current_args)

            # only concat to output if file
            else:
                output += (header +
                           current_args['filestart'] +
                           name +
                           current_args['fileend'] +
                           '\n')

        return output

    def get_base_header(self, incomplete, extend, space):
        '''
        For folder structures, generate the combination of extend and space
        tokens to prepend to file names when generating folder diagrams.


        The string generated here will still be missing the branch token
        (split or final) as well as any folderstart or filestart tokens.
        They are added within seedir.seedir().

        For any item included in a folder diagram, the combination of
        extend and space tokens is based on the depth of the item as well as the
        parent folders to that item which are not completed.  This information
        is symbolized with the `incomplete` argument.  The following illustrates
        the incomplete arguments passed to this function for an example folder
        tree:

        ```
        doc/
        ├─_static/                  [0]
        │ ├─embedded/               [0, 1]
        │ │ ├─deep_file             [0, 1, 2]
        │ │ └─very/                 [0, 1, 2]
        │ │   └─deep/               [0, 1, 3]
        │ │     └─folder/           [0, 1, 4]
        │ │       └─very_deep_file  [0, 1, 5]
        │ └─less_deep_file          [0, 1]
        └─index.rst                 [0]
        ```

        Parameters
        ----------
        incomplete : list-like
            List of integers denoting the depth of incomplete folders at the time
            of constructing the line for a given item.  Zero represents being
            inside the main folder, with increasing integers meaing increasing
            depth.
        extend : str
            Characters symbolizing the extension of a folder.
        space : str
            Characters providing the gap between items and earlier parents.

        Returns
        -------
        str
            Base header string.

        '''
        base_header = []
        max_i = max(incomplete)
        for p in range(max_i):
            if p in incomplete:
                base_header.append(extend)
            else:
                base_header.append(space)
        return "".join(base_header)

    def sort_dir(self, items, first=None, sort_reverse=False, sort_key=None):
        '''
        Sorting function used to sort contents when producing folder diagrams.

        Parameters
        ----------
        items : list-like
            Collection of folder contents.
        first : 'files' or 'folders', optional
            Sort either files or folders first. The default is None.
        sort_reverse : bool, optional
            Reverse the sort applied. The default is False.
        sort_key : function, optional
            Function to apply to sort the objs by their basename.  The function
            should take a single argument, of the type expected by
            this FolderStucture.

        Returns
        -------
        list
            Sorted input as a list.

        '''
        if sort_key is None:
            key = lambda x : self.getname(x)
        else:
            key = lambda x: sort_key(self.getname(x))

        if first in ['folders', 'files']:
            folders = [p for p in items if self.isdir(p)]
            files = [p for p in items if not self.isdir(p)]
            folders = natsort.natsorted(folders, reverse=sort_reverse, key=key)
            files = natsort.natsorted(files, reverse=sort_reverse, key=key)
            output = folders + files if first == 'folders' else files + folders
        elif first is None:
            output = list(natsort.natsorted(items, reverse=sort_reverse, key=key))
        else:
            raise ValueError("`first` must be 'folders', 'files', or None.")

        return output

slashes = os.sep + '/' + '//'

RealDirStructure = FolderStructure(getname_func = lambda x: os.path.basename(x.rstrip(slashes)),
                                   isdir_func = lambda x: os.path.isdir(x),
                                   listdir_func = listdir_fullpath)
"""Object for making real folder structures from string paths."""

PathlibStructure = FolderStructure(getname_func = lambda x: x.name,
                                   isdir_func = lambda x: x.is_dir(),
                                   listdir_func = lambda x: list(x.iterdir()))
"""Object for making real folder structures from pathlib objects."""

FakeDirStructure = FolderStructure(getname_func = lambda x: x.name,
                                   isdir_func = lambda x: x.isdir(),
                                   listdir_func = lambda x: x.listdir())
"""Object for making fake folder structures."""

Global variables

var FakeDirStructure

Object for making fake folder structures.

var PathlibStructure

Object for making real folder structures from pathlib objects.

var RealDirStructure

Object for making real folder structures from string paths.

Functions

def formatter_update_args(formatter, item, args)

Update a dictionary of style tokens based on a formatter function and an item. Added in v 0.3.0 to support the addition of the formatter parameter for the seedir algorithm.

Parameters

formatter : function
Formatting function.
item : file or folder item
String or FakeItem produced in the seedir algorithm
styleargs : dict
Dictionary of seedir style tokens

Returns

styleargs : dict
The input dictionary, after it may have been updated.
Expand source code
def formatter_update_args(formatter, item, args):
    '''
    Update a dictionary of style tokens based on a formatter function and
    an item.  Added in v 0.3.0 to support the addition of the formatter parameter
    for the seedir algorithm.

    Parameters
    ----------
    formatter : function
        Formatting function.
    item : file or folder item
        String or FakeItem produced in the seedir algorithm
    styleargs : dict
        Dictionary of seedir style tokens

    Returns
    -------
    styleargs : dict
        The input dictionary, after it may have been updated.

    '''
    newstyle = formatter(item)
    if newstyle is None:
        pass
    else:
        args.update(newstyle)

    return args
def listdir_fullpath(path)

Like os.listdir(), but returns absolute paths.

Expand source code
def listdir_fullpath(path):
    '''Like `os.listdir()`, but returns absolute paths.'''
    return [os.path.join(path, f) for f in os.listdir(path)]

Classes

class FolderStructure (getname_func, isdir_func, listdir_func)

General class for determining folder strctures. Implements the seedir folder-tree generating algorithm over arbitrary objects.

Defines the functions used by self for generating folder structures.

Parameters

getname_func : function
Returns the name of the object.
isdir_func : function
Returns boolean of whether child object is a folder.
listdir_func : function
Function for returning the children of an object, given it is a "folder" as determined by isdir_func. The child objects returned should be of similar type to self, such that the functions can be recursively applied.

Returns

None.

Expand source code
class FolderStructure:
    '''General class for determining folder strctures.  Implements
    the seedir folder-tree generating algorithm over arbitrary objects.'''

    def __init__(self, getname_func, isdir_func, listdir_func):
        '''
        Defines the functions used by `self` for generating folder structures.

        Parameters
        ----------
        getname_func : function
            Returns the name of the object.
        isdir_func : function
            Returns boolean of whether child object is a folder.
        listdir_func : function
            Function for returning the children of an object, given it is a
            "folder" as determined by `isdir_func`.  The child objects returned
            should be of similar type to `self`, such that the functions can
            be recursively applied.

        Returns
        -------
        None.

        '''
        self.listdir = listdir_func
        self.isdir = isdir_func
        self.getname = getname_func

    def __call__(self, folder, style='lines', printout=True, indent=2, uniform=None,
                 anystart=None, anyend=None, depthlimit=None, itemlimit=None,
                 beyond=None, first=None, sort=False, sort_reverse=False,
                 sort_key=None, include_folders=None, exclude_folders=None,
                 include_files=None, exclude_files=None, regex=False, mask=None,
                 formatter=None, sticky_formatter=False, slash=None, **kwargs):
        '''Call this on a folder object to generate the seedir output
        for that object.'''

        accept_kwargs = ['extend', 'split', 'space', 'final',
                         'folderstart', 'filestart', 'folderend', 'fileend']

        if any(i not in accept_kwargs for i in kwargs.keys()):
            bad_kwargs = [i for i in kwargs.keys() if i not in accept_kwargs]
            raise ValueError(f'kwargs must be any of {accept_kwargs}; '
                             f'unrecognized kwargs: {bad_kwargs}')

        styleargs = printing.get_styleargs(style)
        printing.format_indent(styleargs, indent=indent)

        if uniform is not None:
            for arg in ['extend', 'split', 'final', 'space']:
                styleargs[arg] = uniform

        if anystart is not None:
            styleargs['folderstart'] = anystart
            styleargs['filestart'] = anystart

        if anyend is not None:
            styleargs['folderend'] = anyend
            styleargs['fileend'] = anyend

        if slash is not None:
            warnings.warn("`slash` will removed in a future version; "
                          "pass `folderend` as a keyword argument instead.",
                          DeprecationWarning)
            if slash.lower() in ['sep', 'os.sep']:
                slash = os.sep
            styleargs['folderend'] = slash

        for k in kwargs:
            if k in styleargs:
                styleargs[k] = kwargs[k]

        if sort_key is not None or sort_reverse == True:
            sort = True

        default_args = dict(depthlimit=depthlimit,
                            itemlimit=itemlimit,
                            beyond=beyond,
                            first=first,
                            sort=sort,
                            sort_reverse=sort_reverse,
                            sort_key=sort_key,
                            include_folders=include_folders,
                            exclude_folders=exclude_folders,
                            include_files=include_files,
                            exclude_files=exclude_files,
                            regex=regex,
                            mask=mask,
                            formatter=formatter,
                            sticky_formatter=sticky_formatter,
                            **styleargs)

        current_args = default_args.copy()

        # apply formatter to top level
        if formatter is not None:
            formatter_update_args(formatter, folder, current_args)

        if sticky_formatter:
            default_args = current_args

        s = self._folderstructure_recurse(folder, default_args, **current_args).strip()

        if printout:
            print(s)
        else:
            return s

    def beyond_depth_str(self, items, beyond):
        '''
        Generates the text when using the 'beyond'
        parameter and ther are more items than the itemlimit or contents
        beyond the depthlimit.

        Parameters
        ----------
        beyond : str
            Style of beyond string to generate.  Options are:
                - 'ellipsis' ('...')
                - 'content' or 'contents' (the number of files and folders beyond)
                - a string starting with '_' (everything after the leading
                  underscore will be returned)
        items : collection of file paths, optional
            Path items of the items beyond the limit, used when the 'beyond'
            argeument is 'content' or 'contents'. The default is None.


        Returns
        -------
        str
            String indicating what lies beyond

        '''
        if beyond.lower() == 'ellipsis':
            return '...'
        elif beyond.lower() in ['contents','content']:
            folders = self.count_folders(items)
            files = self.count_files(items)
            return '{} folder(s), {} file(s)'.format(folders, files)
        elif beyond and beyond[0] == '_':
            return beyond[1:]
        else:
            s1 = '"beyond" must be "ellipsis", "content", or '
            s2 = 'a string starting with "_"'
            raise ValueError(s1 + s2)

    def count_files(self, items):
        '''
        Count the number of files in a collection of paths.

        Parameters
        ----------
        paths : list-like
            Collection of paths.

        Returns
        -------
        files : int
            Count of files.

        '''
        files = sum([not self.isdir(i) for i in items])
        return files

    def count_folders(self, items):
        '''
        Count the number of folders in a collection of paths.

        Parameters
        ----------
        paths : list-like
            Collection of paths.

        Returns
        -------
        files : int
            Count of folders.

        '''
        folders = sum([self.isdir(i) for i in items])
        return folders

    def filter_items(self, listdir, include_folders=None,
                      exclude_folders=None, include_files=None,
                      exclude_files=None, regex=False, mask=None):

        '''
        Filter for folder and file names in folder structures.  Removes or includes
        items matching filtering strings.

        Note the following priority of arguments:
            1. Mask (totally overwrites include/exclude)
            2. Include (saves items from being excluded)
            3. Exclude

        Parameters
        ----------
        listdir : list-like
            Collection of file/folder items.
        include_folders : str or list-like, optional
            Folder names to include. The default is None.
        exclude_folders : str or list-like, optional
            Folder names to exclude. The default is None.
        include_files : str or list-like, optional
            File names to include. The default is None.
        exclude_files : str or list-like, optional
            File names to exclude. The default is None.
        regex : bool, optional
            Interpret strings as regular expressions. The default is False.
        mask : function, optional
            Function for filtering items.  Absolute paths of each individual item
            are passed to the mask function.  If True is returned, the
            item is kept.  The default is None.

        Returns
        -------
        keep : list
            Filtered input.

        '''

        def _should_convert(x):
            return isinstance(x, str) or x is None

        filtered = []
        for item in listdir:

            name = self.getname(item)

            if self.isdir(item):
                inc = [include_folders] if _should_convert(include_folders) else include_folders
                exc = [exclude_folders] if _should_convert(exclude_folders) else exclude_folders
            else:
                inc = [include_files] if _should_convert(include_files) else include_files
                exc = [exclude_files] if _should_convert(exclude_files) else exclude_files

            # 1. check mask - which trumps include/exclude arguments
            if mask is not None:
                if mask(item):
                    filtered.append(item)
                continue

            # set default keep behavior
            # items are exluded if inclusion is passed
            keep = all([i is None for i in inc])

            # 2. apply exclusion
            for pat in exc:
                if pat is not None:
                    match = printing.is_match(pattern=pat, string=name, regex=regex)
                    if match:
                        keep = False

            # 3. apply inclusion (trumps exclusion)
            for pat in inc:
                if pat is not None:
                    match = printing.is_match(pattern=pat, string=name, regex=regex)
                    if match:
                        keep = True

            if keep:
                filtered.append(item)

        return filtered


    def _folderstructure_recurse(self, folder, default_args, depth=0, incomplete=None,
                                 extend='│ ', space='  ', split='├─', final='└─',
                                 filestart='', folderstart='', fileend='', folderend='/',
                                 depthlimit=None, itemlimit=None, beyond=None, first=None,
                                 sort=True, sort_reverse=False, sort_key=None,
                                 include_folders=None, exclude_folders=None,
                                 include_files=None, exclude_files=None,
                                 regex=False, mask=None, formatter=None, sticky_formatter=False):
        '''
        Main algorithm for creating folder structure string.  See
        `seedir.realdir.seedir()` or `seedir.fakedir.FakeDir.seedir()`
        for a description of the parameters.

        Note that since the expansion of the formatter parameter
        (version 0.3.1), the call & logic has been slightly updated.
        Primarily, the `default_args` parameter has been added, to represent
        arguments to default to when the formatter is not in use.  The actual
        keyword arguments are treated as the options to be used for `folder`,
        and can change between iterations.  The `default_args` stay the same,
        unless `sticky_formatter` is used in combination with `formatter` to
        permanently update the default arguments.

        **In almost all cases, users should not need to call this function
        to generate diagrams**.  Rather, they should use the `__call__`
        method of a `FolderStructure` instance.

        '''

        # initialize
        output = ''

        if incomplete is None:
            incomplete = []

        # root string
        if depth == 0:

            d0args = {
                'extend': extend,
                'space': space,
                'split': split,
                'final': final,
                'filestart': filestart,
                'folderstart': folderstart,
                'fileend' : fileend,
                'folderend': folderend
                }

            if formatter is not None:
                formatter_update_args(formatter, folder, d0args)
            output += (d0args['folderstart'] +
                       self.getname(folder) +
                       d0args['folderend'] +
                       '\n')


        current_itemlimit = itemlimit
        beyond_added = False

        # handle when depthlimit is reached
        if isinstance(depthlimit, int) and depth >= depthlimit:
            if beyond is None:
                return output
            else:
                current_itemlimit = 0

        # get all children, add depth to docket
        listdir = self.listdir(folder)
        incomplete.append(depth)

        # sort and filter the contents of listdir
        sortargs = {
            'first': first,
            'sort_reverse': sort_reverse,
            'sort_key': sort_key}

        filterargs = {
            'include_folders': include_folders,
            'exclude_folders': exclude_folders,
            'include_files' : include_files,
            'exclude_files': exclude_files,
            'mask': mask
            }

        if sort or first is not None:
            listdir = self.sort_dir(listdir, **sortargs)

        if any(arg is not None for arg in filterargs.values()):
            listdir = self.filter_items(listdir, **filterargs, regex=regex)

        # set current_itemlimit based on listdir size
        if current_itemlimit is None:
            current_itemlimit = len(listdir)
        else:
            current_itemlimit = min(current_itemlimit, len(listdir))

        # segment output into what can be included
        finalpaths = listdir[:current_itemlimit]
        rem = listdir[current_itemlimit:]

        # append beyond string if being used
        if beyond is not None:
            if rem or (depth == depthlimit):
                finalpaths += [self.beyond_depth_str(rem, beyond)]
                beyond_added = True

        # if empty, close the current depth
        if not finalpaths:
            incomplete.remove(depth)

        # get output for each item in folder
        for i, f in enumerate(finalpaths):

            current_args = default_args.copy() # used in this iteration
            next_default_args = default_args   # passed to child

            lastitem = (i == (len(finalpaths) - 1))
            isbeyondstr = lastitem and beyond_added
            name = f if isbeyondstr else self.getname(f)

            # update tokens with formatter if passed
            if not isbeyondstr and formatter is not None:
                formatter_update_args(formatter, f, current_args)

            if sticky_formatter:
                next_default_args = current_args

            # create header for the item
            base_header = self.get_base_header(incomplete,
                                               current_args['extend'],
                                               current_args['space'])

            if lastitem:
                branch = current_args['final']
                incomplete.remove(depth)
            else:
                branch = current_args['split']

            header = base_header + branch

            # concat to output and recurse if item is folder
            if not isbeyondstr and self.isdir(f):
                output += (header +
                           current_args['folderstart'] +
                           name +
                           current_args['folderend'] +
                           '\n')
                output += self._folderstructure_recurse(f, depth=depth + 1,
                                                        incomplete=incomplete,
                                                        default_args=next_default_args,
                                                        **current_args)

            # only concat to output if file
            else:
                output += (header +
                           current_args['filestart'] +
                           name +
                           current_args['fileend'] +
                           '\n')

        return output

    def get_base_header(self, incomplete, extend, space):
        '''
        For folder structures, generate the combination of extend and space
        tokens to prepend to file names when generating folder diagrams.


        The string generated here will still be missing the branch token
        (split or final) as well as any folderstart or filestart tokens.
        They are added within seedir.seedir().

        For any item included in a folder diagram, the combination of
        extend and space tokens is based on the depth of the item as well as the
        parent folders to that item which are not completed.  This information
        is symbolized with the `incomplete` argument.  The following illustrates
        the incomplete arguments passed to this function for an example folder
        tree:

        ```
        doc/
        ├─_static/                  [0]
        │ ├─embedded/               [0, 1]
        │ │ ├─deep_file             [0, 1, 2]
        │ │ └─very/                 [0, 1, 2]
        │ │   └─deep/               [0, 1, 3]
        │ │     └─folder/           [0, 1, 4]
        │ │       └─very_deep_file  [0, 1, 5]
        │ └─less_deep_file          [0, 1]
        └─index.rst                 [0]
        ```

        Parameters
        ----------
        incomplete : list-like
            List of integers denoting the depth of incomplete folders at the time
            of constructing the line for a given item.  Zero represents being
            inside the main folder, with increasing integers meaing increasing
            depth.
        extend : str
            Characters symbolizing the extension of a folder.
        space : str
            Characters providing the gap between items and earlier parents.

        Returns
        -------
        str
            Base header string.

        '''
        base_header = []
        max_i = max(incomplete)
        for p in range(max_i):
            if p in incomplete:
                base_header.append(extend)
            else:
                base_header.append(space)
        return "".join(base_header)

    def sort_dir(self, items, first=None, sort_reverse=False, sort_key=None):
        '''
        Sorting function used to sort contents when producing folder diagrams.

        Parameters
        ----------
        items : list-like
            Collection of folder contents.
        first : 'files' or 'folders', optional
            Sort either files or folders first. The default is None.
        sort_reverse : bool, optional
            Reverse the sort applied. The default is False.
        sort_key : function, optional
            Function to apply to sort the objs by their basename.  The function
            should take a single argument, of the type expected by
            this FolderStucture.

        Returns
        -------
        list
            Sorted input as a list.

        '''
        if sort_key is None:
            key = lambda x : self.getname(x)
        else:
            key = lambda x: sort_key(self.getname(x))

        if first in ['folders', 'files']:
            folders = [p for p in items if self.isdir(p)]
            files = [p for p in items if not self.isdir(p)]
            folders = natsort.natsorted(folders, reverse=sort_reverse, key=key)
            files = natsort.natsorted(files, reverse=sort_reverse, key=key)
            output = folders + files if first == 'folders' else files + folders
        elif first is None:
            output = list(natsort.natsorted(items, reverse=sort_reverse, key=key))
        else:
            raise ValueError("`first` must be 'folders', 'files', or None.")

        return output

Methods

def beyond_depth_str(self, items, beyond)

Generates the text when using the 'beyond' parameter and ther are more items than the itemlimit or contents beyond the depthlimit.

Parameters

beyond : str
Style of beyond string to generate. Options are: - 'ellipsis' ('…') - 'content' or 'contents' (the number of files and folders beyond) - a string starting with '_' (everything after the leading underscore will be returned)
items : collection of file paths, optional
Path items of the items beyond the limit, used when the 'beyond' argeument is 'content' or 'contents'. The default is None.

Returns

str
String indicating what lies beyond
Expand source code
def beyond_depth_str(self, items, beyond):
    '''
    Generates the text when using the 'beyond'
    parameter and ther are more items than the itemlimit or contents
    beyond the depthlimit.

    Parameters
    ----------
    beyond : str
        Style of beyond string to generate.  Options are:
            - 'ellipsis' ('...')
            - 'content' or 'contents' (the number of files and folders beyond)
            - a string starting with '_' (everything after the leading
              underscore will be returned)
    items : collection of file paths, optional
        Path items of the items beyond the limit, used when the 'beyond'
        argeument is 'content' or 'contents'. The default is None.


    Returns
    -------
    str
        String indicating what lies beyond

    '''
    if beyond.lower() == 'ellipsis':
        return '...'
    elif beyond.lower() in ['contents','content']:
        folders = self.count_folders(items)
        files = self.count_files(items)
        return '{} folder(s), {} file(s)'.format(folders, files)
    elif beyond and beyond[0] == '_':
        return beyond[1:]
    else:
        s1 = '"beyond" must be "ellipsis", "content", or '
        s2 = 'a string starting with "_"'
        raise ValueError(s1 + s2)
def count_files(self, items)

Count the number of files in a collection of paths.

Parameters

paths : list-like
Collection of paths.

Returns

files : int
Count of files.
Expand source code
def count_files(self, items):
    '''
    Count the number of files in a collection of paths.

    Parameters
    ----------
    paths : list-like
        Collection of paths.

    Returns
    -------
    files : int
        Count of files.

    '''
    files = sum([not self.isdir(i) for i in items])
    return files
def count_folders(self, items)

Count the number of folders in a collection of paths.

Parameters

paths : list-like
Collection of paths.

Returns

files : int
Count of folders.
Expand source code
def count_folders(self, items):
    '''
    Count the number of folders in a collection of paths.

    Parameters
    ----------
    paths : list-like
        Collection of paths.

    Returns
    -------
    files : int
        Count of folders.

    '''
    folders = sum([self.isdir(i) for i in items])
    return folders
def filter_items(self, listdir, include_folders=None, exclude_folders=None, include_files=None, exclude_files=None, regex=False, mask=None)

Filter for folder and file names in folder structures. Removes or includes items matching filtering strings.

Note the following priority of arguments: 1. Mask (totally overwrites include/exclude) 2. Include (saves items from being excluded) 3. Exclude

Parameters

listdir : list-like
Collection of file/folder items.
include_folders : str or list-like, optional
Folder names to include. The default is None.
exclude_folders : str or list-like, optional
Folder names to exclude. The default is None.
include_files : str or list-like, optional
File names to include. The default is None.
exclude_files : str or list-like, optional
File names to exclude. The default is None.
regex : bool, optional
Interpret strings as regular expressions. The default is False.
mask : function, optional
Function for filtering items. Absolute paths of each individual item are passed to the mask function. If True is returned, the item is kept. The default is None.

Returns

keep : list
Filtered input.
Expand source code
def filter_items(self, listdir, include_folders=None,
                  exclude_folders=None, include_files=None,
                  exclude_files=None, regex=False, mask=None):

    '''
    Filter for folder and file names in folder structures.  Removes or includes
    items matching filtering strings.

    Note the following priority of arguments:
        1. Mask (totally overwrites include/exclude)
        2. Include (saves items from being excluded)
        3. Exclude

    Parameters
    ----------
    listdir : list-like
        Collection of file/folder items.
    include_folders : str or list-like, optional
        Folder names to include. The default is None.
    exclude_folders : str or list-like, optional
        Folder names to exclude. The default is None.
    include_files : str or list-like, optional
        File names to include. The default is None.
    exclude_files : str or list-like, optional
        File names to exclude. The default is None.
    regex : bool, optional
        Interpret strings as regular expressions. The default is False.
    mask : function, optional
        Function for filtering items.  Absolute paths of each individual item
        are passed to the mask function.  If True is returned, the
        item is kept.  The default is None.

    Returns
    -------
    keep : list
        Filtered input.

    '''

    def _should_convert(x):
        return isinstance(x, str) or x is None

    filtered = []
    for item in listdir:

        name = self.getname(item)

        if self.isdir(item):
            inc = [include_folders] if _should_convert(include_folders) else include_folders
            exc = [exclude_folders] if _should_convert(exclude_folders) else exclude_folders
        else:
            inc = [include_files] if _should_convert(include_files) else include_files
            exc = [exclude_files] if _should_convert(exclude_files) else exclude_files

        # 1. check mask - which trumps include/exclude arguments
        if mask is not None:
            if mask(item):
                filtered.append(item)
            continue

        # set default keep behavior
        # items are exluded if inclusion is passed
        keep = all([i is None for i in inc])

        # 2. apply exclusion
        for pat in exc:
            if pat is not None:
                match = printing.is_match(pattern=pat, string=name, regex=regex)
                if match:
                    keep = False

        # 3. apply inclusion (trumps exclusion)
        for pat in inc:
            if pat is not None:
                match = printing.is_match(pattern=pat, string=name, regex=regex)
                if match:
                    keep = True

        if keep:
            filtered.append(item)

    return filtered
def get_base_header(self, incomplete, extend, space)

For folder structures, generate the combination of extend and space tokens to prepend to file names when generating folder diagrams.

The string generated here will still be missing the branch token (split or final) as well as any folderstart or filestart tokens. They are added within seedir.seedir().

For any item included in a folder diagram, the combination of extend and space tokens is based on the depth of the item as well as the parent folders to that item which are not completed. This information is symbolized with the incomplete argument. The following illustrates the incomplete arguments passed to this function for an example folder tree:

doc/
├─_static/                  [0]
│ ├─embedded/               [0, 1]
│ │ ├─deep_file             [0, 1, 2]
│ │ └─very/                 [0, 1, 2]
│ │   └─deep/               [0, 1, 3]
│ │     └─folder/           [0, 1, 4]
│ │       └─very_deep_file  [0, 1, 5]
│ └─less_deep_file          [0, 1]
└─index.rst                 [0]

Parameters

incomplete : list-like
List of integers denoting the depth of incomplete folders at the time of constructing the line for a given item. Zero represents being inside the main folder, with increasing integers meaing increasing depth.
extend : str
Characters symbolizing the extension of a folder.
space : str
Characters providing the gap between items and earlier parents.

Returns

str
Base header string.
Expand source code
def get_base_header(self, incomplete, extend, space):
    '''
    For folder structures, generate the combination of extend and space
    tokens to prepend to file names when generating folder diagrams.


    The string generated here will still be missing the branch token
    (split or final) as well as any folderstart or filestart tokens.
    They are added within seedir.seedir().

    For any item included in a folder diagram, the combination of
    extend and space tokens is based on the depth of the item as well as the
    parent folders to that item which are not completed.  This information
    is symbolized with the `incomplete` argument.  The following illustrates
    the incomplete arguments passed to this function for an example folder
    tree:

    ```
    doc/
    ├─_static/                  [0]
    │ ├─embedded/               [0, 1]
    │ │ ├─deep_file             [0, 1, 2]
    │ │ └─very/                 [0, 1, 2]
    │ │   └─deep/               [0, 1, 3]
    │ │     └─folder/           [0, 1, 4]
    │ │       └─very_deep_file  [0, 1, 5]
    │ └─less_deep_file          [0, 1]
    └─index.rst                 [0]
    ```

    Parameters
    ----------
    incomplete : list-like
        List of integers denoting the depth of incomplete folders at the time
        of constructing the line for a given item.  Zero represents being
        inside the main folder, with increasing integers meaing increasing
        depth.
    extend : str
        Characters symbolizing the extension of a folder.
    space : str
        Characters providing the gap between items and earlier parents.

    Returns
    -------
    str
        Base header string.

    '''
    base_header = []
    max_i = max(incomplete)
    for p in range(max_i):
        if p in incomplete:
            base_header.append(extend)
        else:
            base_header.append(space)
    return "".join(base_header)
def sort_dir(self, items, first=None, sort_reverse=False, sort_key=None)

Sorting function used to sort contents when producing folder diagrams.

Parameters

items : list-like
Collection of folder contents.
first : 'files' or 'folders', optional
Sort either files or folders first. The default is None.
sort_reverse : bool, optional
Reverse the sort applied. The default is False.
sort_key : function, optional
Function to apply to sort the objs by their basename. The function should take a single argument, of the type expected by this FolderStucture.

Returns

list
Sorted input as a list.
Expand source code
def sort_dir(self, items, first=None, sort_reverse=False, sort_key=None):
    '''
    Sorting function used to sort contents when producing folder diagrams.

    Parameters
    ----------
    items : list-like
        Collection of folder contents.
    first : 'files' or 'folders', optional
        Sort either files or folders first. The default is None.
    sort_reverse : bool, optional
        Reverse the sort applied. The default is False.
    sort_key : function, optional
        Function to apply to sort the objs by their basename.  The function
        should take a single argument, of the type expected by
        this FolderStucture.

    Returns
    -------
    list
        Sorted input as a list.

    '''
    if sort_key is None:
        key = lambda x : self.getname(x)
    else:
        key = lambda x: sort_key(self.getname(x))

    if first in ['folders', 'files']:
        folders = [p for p in items if self.isdir(p)]
        files = [p for p in items if not self.isdir(p)]
        folders = natsort.natsorted(folders, reverse=sort_reverse, key=key)
        files = natsort.natsorted(files, reverse=sort_reverse, key=key)
        output = folders + files if first == 'folders' else files + folders
    elif first is None:
        output = list(natsort.natsorted(items, reverse=sort_reverse, key=key))
    else:
        raise ValueError("`first` must be 'folders', 'files', or None.")

    return output