# Decorators for exposing constructor inner functions as object
# methods or properties.
#
# Jesper Larsson, Malmö University, 2020

# First, the method decorator. Intended use is like this, where foo is
# the method to expose:
#
#    @clo_method(self)
#    def foo(x, y):
#        pass
#
# Or, alternatively like this, if you can't def 'foo' because it
# clashes with a local variable or something:
#
#    @clo_method(self, 'foo')
#    def _(x, y):
#        pass

def clo_method(obj, name=None):
    def defmeth(f):
        obj.__dict__[name or f.__name__] = f
        return f
    return defmeth

# The property decorator is more convoluted. It works by adding a
# __getattr__ method to the class, and a __clprops dictionary to each
# object. The getter method is supposed to take zero arguments.
#
#    @clo_property(self)
#    def bar():
#        return 'computation of bar value'
#
# The alternatively form with a name is useful if the bar property is
# actually a local variable with the same name, which should be quite
# common:
#
#    @clo_property(self, 'bar')
#    def _():
#        return bar
#
# There can be a setter as well, which must be defined after the getter:
#
#    @clo_setter(self, 'bar')
#    def _(value):
#        nonlocal bar
#        bar = value

def _cantsetattr(a):
    raise AttributeError("no setter for closure property") from None

def _getattr(obj, a):
    try:
        return (obj.__clprops[a])()
    except KeyError:
        raise AttributeError("no such attribute") from None

def _setattr(obj, a, v):
    try:
        return (obj.__clprops[a].__setter)(v)
    except KeyError:
        super(obj.__class__, obj).__setattr__(a, v)

def clo_property(obj, name=None):
    if not '__clprops' in obj.__dict__:
        obj.__dict__['__clprops'] = {}
        obj.__class__.__getattr__ = _getattr
        obj.__class__.__setattr__ = _setattr
    
    def defprop(f):
        f.__setter = _cantsetattr
        obj.__clprops[name or f.__name__] = f
        return f

    return defprop

def clo_setter(obj):
    def defsetter(f):
        try:
            obj.__clprops[f.__name__].__setter = f
        except KeyError:
            raise NameError("closure property '%s' is not defined" % f.__name__)

    return defsetter
