Source code for geowatch.utils.util_chmod

"""
This logic was ported to ub.Path.chmod in 1.3.5, can remove and depend on that
when it is ready.
"""


def _parse_chmod_code(code):
    """
    Expand a chmod code into a list of actions.

    Args:
        code (str): of the form: [ugoa…][-+=]perms…[,…]
            perms is either zero or more letters from the set rwxXst, or a
            single letter from the set ugo.

    Yields:
        Tuple[str, str, str]: target, op, and perms.

            The target is modified by the operation using the value.
            target -- specified 'u' for user, 'g' for group, 'o' for other.
            op -- specified as '+' to add, '-' to remove, or '=' to assign.
            val -- specified as 'r' for read, 'w' for write, or 'x' for execute.

    Example:
        >>> print(list(_parse_chmod_code('ugo+rw,+r,g=rwx')))
        >>> print(list(_parse_chmod_code('o+x')))
        >>> print(list(_parse_chmod_code('u-x')))
        >>> print(list(_parse_chmod_code('x')))
        >>> print(list(_parse_chmod_code('ugo+rwx')))
        [('ugo', '+', 'rw'), ('ugo', '+', 'r'), ('g', '=', 'rwx')]
        [('o', '+', 'x')]
        [('u', '-', 'x')]
        [('u', '+', 'x')]
        [('ugo', '+', 'rwx')]
        >>> import pytest
        >>> with pytest.raises(ValueError):
        >>>     list(_parse_chmod_code('a+b+c'))
    """
    import re
    pat = re.compile(r'([\+\-\=])')
    parts = code.split(',')
    for part in parts:
        ab = pat.split(part)
        len_ab = len(ab)
        if len_ab == 3:
            targets, op, perms = ab
        elif len_ab == 1:
            perms = ab[0]
            op = '+'
            targets = 'u'
        else:
            raise ValueError('unknown chmod code pattern: part={part}')
        if targets == '' or targets == 'a':
            targets = 'ugo'
        yield (targets, op, perms)


def _resolve_chmod_code(old_mode, code):
    """
    Modifies integer stat permissions based on a string code.

    Args:
        old_mode (int): old mode from st_stat
        code (str): chmod style codeold mode from st_stat

    Returns:
        int : new code

    Example:
        >>> print(oct(_resolve_chmod_code(0, '+rwx')))
        >>> print(oct(_resolve_chmod_code(0, 'ugo+rwx')))
        >>> print(oct(_resolve_chmod_code(0, 'a-rwx')))
        >>> print(oct(_resolve_chmod_code(0, 'u+rw,go+r,go-wx')))
        >>> print(oct(_resolve_chmod_code(0o777, 'u+rw,go+r,go-wx')))
        0o777
        0o777
        0o0
        0o644
        0o744
        >>> import pytest
        >>> with pytest.raises(NotImplementedError):
        >>>     print(oct(_resolve_chmod_code(0, 'u=rw')))
        >>> with pytest.raises(ValueError):
        >>>     _resolve_chmod_code(0, 'u?w')
    """
    import stat
    import itertools as it
    action_lut = {
        # TODO: handle suid, sgid, and sticky?
        # suid = stat.S_ISUID
        # sgid = stat.S_ISGID
        # sticky = stat.S_ISVTX
        'ur' : stat.S_IRUSR,
        'uw' : stat.S_IWUSR,
        'ux' : stat.S_IXUSR,

        'gr' : stat.S_IRGRP,
        'gw' : stat.S_IWGRP,
        'gx' : stat.S_IXGRP,

        'or' : stat.S_IROTH,
        'ow' : stat.S_IWOTH,
        'ox' : stat.S_IXOTH,
    }
    actions = _parse_chmod_code(code)
    new_mode = int(old_mode)  # (could optimize to modify inplace if needed)
    for action in actions:
        targets, op, perms = action
        try:
            action_keys = (target + perm for target, perm in it.product(targets, perms))
            action_values = (action_lut[key] for key in action_keys)
            action_values = list(action_values)
            if op == '+':
                for val in action_values:
                    new_mode |= val
            elif op == '-':
                for val in action_values:
                    new_mode &= (~val)
            elif op == '=':
                raise NotImplementedError(f'new chmod code for op={op}')
            else:
                raise AssertionError(
                    f'should not be able to get here. unknown op code: op={op}')
        except KeyError:
            # Give a better error message if something goes wrong
            raise ValueError(f'Unknown action: {action}')
    return new_mode


def _encode_chmod_int(int_code):
    """
    Convert a chmod integer code to a string

    Currently unused, but may be useful in the future.

    Example:
        >>> int_code = 0o744
        >>> print(_encode_chmod_int(int_code))
        u=rwx,g=r,o=r
    """
    import stat
    action_lut = {
        'ur' : stat.S_IRUSR,
        'uw' : stat.S_IWUSR,
        'ux' : stat.S_IXUSR,

        'gr' : stat.S_IRGRP,
        'gw' : stat.S_IWGRP,
        'gx' : stat.S_IXGRP,

        'or' : stat.S_IROTH,
        'ow' : stat.S_IWOTH,
        'ox' : stat.S_IXOTH,
    }
    from collections import defaultdict
    target_to_perms = defaultdict(list)
    for key, val in action_lut.items():
        target, perm = key
        if int_code & val:
            target_to_perms[target].append(perm)
    parts = [k + '=' + ''.join(vs) for k, vs in target_to_perms.items()]
    code = ','.join(parts)
    return code


[docs] def new_chmod(path, code): """ dpath = ub.Path.appdir('util/chmod/test').ensuredir() path = (dpath / 'file').touch() code = 'g+x,g+r' new_chmod(path, code) import stat stat.filemode(path.stat().st_mode) """ old_mode = path.stat().st_mode new_mode = _resolve_chmod_code(old_mode, code) path.chmod(new_mode)