Pack an arbitrary set of iterables and non-iterables into tuples.

    Function packs a set of inputs with arbitrary iterability into tuples.
    Iterability is tested with :func:`iterable`. Non-iterable inputs
    are repeated in each output tuple. Iterable inputs are expanded
    uniformly across the output tuples.  For consistency, all iterables must
    be the same length.

    The input arguments are parsed such that bare strings are treated as
    **NON-ITERABLE**, through the use of a local subclass of |str| that
    cripples the ``__iter__()`` method. Any strings passed are returned
    in the packed tuples as standard, **ITERABLE** instances of |str|, however.

    The order of the input arguments is retained within each output tuple.

    No structural conversion is attempted on the arguments.

    If all inputs are non-iterable, a list containing a single |tuple| will be
    returned.

    Parameters
    ----------
    \*args
        Arbitrary number of arbitrary mix of iterable and non-iterable
        objects to be packed into tuples.

    Returns
    -------
    tups
        |list| of |tuple| --
        Number of tuples returned is equal to the length of the iterables
        passed in `*args`

    Raises
    ------
    ~exceptions.ValueError
        If any iterable objects are of different lengths

def pack_tups(*args):
    """Pack an arbitrary set of iterables and non-iterables into tuples.

    Function packs a set of inputs with arbitrary iterability into tuples.
    Iterability is tested with :func:`iterable`. Non-iterable inputs
    are repeated in each output tuple. Iterable inputs are expanded
    uniformly across the output tuples.  For consistency, all iterables must
    be the same length.

    The input arguments are parsed such that bare strings are treated as
    **NON-ITERABLE**, through the use of a local subclass of |str| that
    cripples the ``__iter__()`` method. Any strings passed are returned
    in the packed tuples as standard, **ITERABLE** instances of |str|, however.

    The order of the input arguments is retained within each output tuple.

    No structural conversion is attempted on the arguments.

    If all inputs are non-iterable, a list containing a single |tuple| will be
    returned.

    Parameters
    ----------
    \*args
        Arbitrary number of arbitrary mix of iterable and non-iterable
        objects to be packed into tuples.

    Returns
    -------
    tups
        |list| of |tuple| --
        Number of tuples returned is equal to the length of the iterables
        passed in `*args`

    Raises
    ------
    ~exceptions.ValueError
        If any iterable objects are of different lengths

    """

    # Imports
    import numpy as np

    # Debug flag
    _DEBUG = False

    # Marker value for non-iterable items
    NOT_ITER = -1

    # Uninitialized test value
    UNINIT_VAL = -1

    # Print the input if in debug mode
    if _DEBUG: # pragma: no cover
        print("args = {0}".format(args))

    # Non-iterable subclass of str
    class StrNoIter(str):
        """ Non-iterable subclass of |str|. """
        def __iter__(self):
            raise NotImplementedError("Non-iterable string")
        ## end def __iter__
    ## end class StrNoIter

    # Re-wrap input arguments with non-iterable strings if required
    mod_args = [(StrNoIter(a) if isinstance(a, str) else a) for a in args]

    # Determine the length or non-iterable status of each item and store
    #  the maximum value (depends on NOT_ITER < 0)
    iterlens = [(len(a) if iterable(a) else NOT_ITER) for a in mod_args]
    maxiter = max(iterlens)

    # Check to ensure all iterables are the same length
    if not all(map(lambda v: v in (NOT_ITER, maxiter), iterlens)):
        raise ValueError("All iterable items must be of equal length")
    ## end if

    # If everything is non-iterable, just return the args tuple wrapped in
    #  a list (as above, depends on NOT_ITER < 0)
    if maxiter == NOT_ITER:
        return [args]
    ## end if

    # Swap any non-iterables for a suitable length repeat, and zip to
    #  tuples for return
    tups = list(zip(*[(np.repeat(a, maxiter) if l == NOT_ITER else a)
            for (a,l) in zip(mod_args, iterlens)]))

    # Dump the resulting tuples, if in debug mode
    if _DEBUG:  # pragma: no cover
        print("tups = {0}".format(tups))
    ## end if

    # Return the tuples
    return tups