from functools import wraps
from types import FunctionType

def generate_properties_for_class(getter_suffix='_getter', setter_suffix='_setter', deleter_suffix='_deleter'):
    def decorator(cls):
        names = []
        getters = {}
        setters = {}
        deleters = {}

        # local_methods = dir(cls)
        local_methods = {x:y for x, y in cls.__dict__.items() if isinstance(y, (FunctionType, classmethod, staticmethod))}
        for name in local_methods:
            if name.endswith(getter_suffix):
                base_name = name[:-len(getter_suffix)]
                getters[base_name] = getattr(cls, name)
                names.append(base_name)

            if name.endswith(setter_suffix):
                base_name = name[:-len(setter_suffix)]
                setters[base_name] = getattr(cls, name)
                names.append(base_name)

            if name.endswith(deleter_suffix):
                base_name = name[:-len(deleter_suffix)]
                deleters[name[:-len(deleter_suffix)]] = getattr(cls, name)
                names.append(base_name)


        for name in names:
            getter = getters.get(name, None)
            setter = setters.get(name, None)
            deleter = deleters.get(name, None)

            if not getter:
                # print('make default getter for', cls, name, f'_{name}')
                def default_getter(self):
                    # print('use default getter', self, name)
                    return getattr(self, f'_{name}', None)
                getter = default_getter

            if not setter:
                def default_setter(self, value):
                    setattr(self, f'_{name}', value)
                setter = default_setter

            if not deleter:
                def default_deleter(self):
                    delattr(self, f'_{name}')
                deleter = default_deleter

            setattr(cls, name, property(getter, setter, deleter))

        return cls
    return decorator



if __name__ == '__main__':
    class Z:
        pass

    @generate_properties_for_class(getter_suffix='_getter', setter_suffix='_setter')
    class A:
        pass
        # def x_getter(self):
        #     print('get original x')
        #     return self._x

        # def x_setter(self, value):
        #     self._x = value
        #     print('A setter side effect')


    @generate_properties_for_class()
    class B(A):
        def __init__(self):
            super().__init__()

        def x_setter(self, value):
            self._x = value
            # super().x_setter(value) # enables you to use getters and setters with inheritance while keeping the parent class's behavior
            print('B setter side effect')
        # @x.setter
        # def x(self, value):
        #     setattr(super(), 'x', value)
        #     print('-----', )



    # how you'd do it without the property generator, using __getattr__ and __setattr__
    # class B(A):
    #     def __setattr__(self, name, value):
    #         super().__setattr__(name, value)
    #
    #         if name == 'x':
    #             print('custom x stuff!')


    e = B()
    e.x = 2
    print('xxxxxxxx', e.x)
    # del e.x
