"""
Prywatność dla atrybutów pobranych z instancji klas.
Przykłady użycia można znaleźć w kodzie testu samosprawdzającego na dole pliku.
Dekorator jest tym samym co: Doubler = Private('data', 'size')(Doubler).
Private zwraca onDecorator, onDecorator zwraca onInstance, a każda instancja onInstance osadza instancję Doubler.
"""

traceMe = False
def trace(*args):
    if traceMe: print(f'[{' '.join(map(str, args))}]')   # Literał f-string od Pythona 3.12

def Private(*privates):                              # privates w zakresie funkcji zawierającej
    def onDecorator(aClass):                         # aClass w zakresie funkcji zawierającej
        class onInstance:                            # Opakowane w atrybut instancji
            def __init__(self, *args, **kargs):
                self.wrapped = aClass(*args, **kargs)

            def __getattr__(self, attr):             # Moje atrybuty nie wywołują metody getattr
                trace('pobranie:', attr)                  # Inne zakładane w obiekcie wrapped
                if attr in privates:
                    raise TypeError('private attribute fetch, ' + attr)
                else:
                    return getattr(self.wrapped, attr)

            def __setattr__(self, attr, value):             # Próby dostępu z zewnątrz
                trace('ustawienie:', attr, value)                  # Pozostałe działają normalnie
                if attr == 'wrapped':                       # Pozwolenie na własne atrybuty
                    self.__dict__[attr] = value             # Uniknięcie pętli
                elif attr in privates:
                    raise TypeError('modyfikacja atrybutu prywatnego, ' + attr)
                else:
                    setattr(self.wrapped, attr, value)      # Opakowane atrybuty obiektu
        return onInstance
    return onDecorator


if __name__ == '__main__':
    traceMe = True

    @Private('data', 'size')                   # Doubler = Private(...)(Doubler)
    class Doubler:
        def __init__(self, label, start):
            self.label = label                 # Próby dostępu wewnątrz podmiotowej klasy
            self.data  = start                 # Nie zostaje przechwycony: wykonany normalnie
        def size(self):
            return len(self.data)              # Metody działają bez sprawdzania
        def double(self):                      # Ponieważ prywatność nie jest dziedziczona
            for i in range(self.size()):
                self.data[i] = self.data[i] * 2
        def display(self):
            print(f'{self.label} => {self.data}')

    print('Tworzenie instancji...')
    X = Doubler('X is', [1, 2, 3])
    Y = Doubler('Y is', [-10, -20, -30])

    # Poniższe testy kończą się powodzeniem

    print('\nBadanie instancji X...')
    print(X.label)                             # Próby dostępu spoza podmiotowej klasy
    X.display(); X.double(); X.display()       # Przechwycony: sprawdzony, wydelegowany

    print('\nBadanie instancji Y...')
    print(Y.label)
    Y.display(); Y.double()
    Y.label = 'Hakować'
    Y.display()

    # Poniższe testy wszystkie kończą się niepowodzeniem (zgodnie z zamierzeniami)
    """
    print(X.size())          # Wyświetla "TypeError: private attribute fetch, size"
    print(X.data)
    X.data = [1, 1, 1]       # Wyświetla "TypeError: private attribute change, data"
    X.size = lambda S: 0
    print(Y.data)
    print(Y.size())
    """

