from __future__ import annotations import functools import itertools import operator import re from collections.abc import Iterable, Iterator from typing import Any, Callable # from jaraco.functools def compose(*funcs: Callable[..., Any]) -> Callable[..., Any]: compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) # noqa return functools.reduce(compose_two, funcs) # from jaraco.structures.binary def gen_bit_values(number: int) -> Iterator[int]: """ Return a zero or one for each bit of a numeric value up to the most significant 1 bit, beginning with the least significant bit. >>> list(gen_bit_values(16)) [0, 0, 0, 0, 1] """ digits = bin(number)[2:] return map(int, reversed(digits)) # from more_itertools def padded( iterable: Iterable[Any], fillvalue: Any | None = None, n: int | None = None, next_multiple: bool = False, ) -> Iterator[Any]: """Yield the elements from *iterable*, followed by *fillvalue*, such that at least *n* items are emitted. >>> list(padded([1, 2, 3], '?', 5)) [1, 2, 3, '?', '?'] If *next_multiple* is ``True``, *fillvalue* will be emitted until the number of items emitted is a multiple of *n*:: >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) [1, 2, 3, 4, None, None] If *n* is ``None``, *fillvalue* will be emitted indefinitely. """ it = iter(iterable) if n is None: yield from itertools.chain(it, itertools.repeat(fillvalue)) elif n < 1: raise ValueError('n must be at least 1') else: item_count = 0 for item in it: yield item item_count += 1 remaining = (n - item_count) % n if next_multiple else n - item_count for _ in range(remaining): yield fillvalue def compound(mode: str) -> Callable[[int], int]: """ Support multiple, comma-separated Unix chmod symbolic modes. >>> oct(compound('a=r,u+w')(0)) '0o644' """ return compose(*map(simple, reversed(mode.split(',')))) def simple(mode: str) -> Callable[[int], int]: """ Convert a Unix chmod symbolic mode like ``'ugo+rwx'`` to a function suitable for applying to a mask to affect that change. >>> mask = simple('ugo+rwx') >>> mask(0o554) == 0o777 True >>> simple('go-x')(0o777) == 0o766 True >>> simple('o-x')(0o445) == 0o444 True >>> simple('a+x')(0) == 0o111 True >>> simple('a=rw')(0o057) == 0o666 True >>> simple('u=x')(0o666) == 0o166 True >>> simple('g=')(0o157) == 0o107 True >>> simple('gobbledeegook') Traceback (most recent call last): ValueError: ('Unrecognized symbolic mode', 'gobbledeegook') """ # parse the symbolic mode parsed = re.match('(?P[ugoa]+)(?P[-+=])(?P[rwx]*)$', mode) if not parsed: raise ValueError("Unrecognized symbolic mode", mode) # generate a mask representing the specified permission spec_map = dict(r=4, w=2, x=1) specs = (spec_map[perm] for perm in parsed.group('what')) spec = functools.reduce(operator.or_, specs, 0) # now apply spec to each subject in who shift_map = dict(u=6, g=3, o=0) who = parsed.group('who').replace('a', 'ugo') masks = (spec << shift_map[subj] for subj in who) mask = functools.reduce(operator.or_, masks) op = parsed.group('op') # if op is -, invert the mask if op == '-': mask ^= 0o777 # if op is =, retain extant values for unreferenced subjects if op == '=': masks = (0o7 << shift_map[subj] for subj in who) retain = functools.reduce(operator.or_, masks) ^ 0o777 op_map = { '+': operator.or_, '-': operator.and_, '=': lambda mask, target: target & retain ^ mask, } return functools.partial(op_map[op], mask) class Permissions(int): """ >>> perms = Permissions(0o764) >>> oct(perms) '0o764' >>> perms.symbolic 'rwxrw-r--' >>> str(perms) 'rwxrw-r--' >>> str(Permissions(0o222)) '-w--w--w-' """ @property def symbolic(self) -> str: return ''.join( ['-', val][bit] for val, bit in zip(itertools.cycle('rwx'), self.bits) ) @property def bits(self) -> Iterator[int]: return reversed(tuple(padded(gen_bit_values(self), 0, n=9))) def __str__(self) -> str: return self.symbolic