pythonpython-descriptors

Descriptors in python for implementing perl's tie scalar operation


I need some help with descriptors in python. I wrote an automatic translator from perl to python (Pythonizer) and I'm trying to implement tied scalars, which is basically an object that acts as a scalar, but has FETCH and STORE operations that are called appropriately. I'm using a dynamic class namespace 'main' to store the variable values. I'm attempting to define __get__ and __set__ operations for the object, but they are not working. Any help will be appreciated!

main = type('main', tuple(), dict())

class TiedScalar(dict):    # Generated code - can't be changed
    FETCH_CALLED_v = 0
    STORE_CALLED_v = 0

    def STORE(*_args):
        _args = list(_args)
        self = _args.pop(0) if _args else None
        TiedScalar.STORE_CALLED_v = TiedScalar.STORE_CALLED_v + 1
        self["value"] = _args.pop(0) if _args else None
        return self.get("value")
    
    
    #TiedScalar.STORE = lambda *_args, **_kwargs: perllib.tie_call(STORE, _args, _kwargs)
    
    
    def FETCH(*_args):
        _args = list(_args)
        self = _args.pop(0) if _args else None
        TiedScalar.FETCH_CALLED_v = TiedScalar.FETCH_CALLED_v + 1
        return self.get("value")
    
    
    #TiedScalar.FETCH = lambda *_args, **_kwargs: perllib.tie_call(FETCH, _args, _kwargs)
    
    
    @classmethod
    def TIESCALAR(*_args):
        _args = list(_args)
        class_ = _args.pop(0) if _args else None
        self = {"value": (_args.pop(0) if _args else None)}
        #self = perllib.bless(self, class_)
        #return perllib.add_tie_methods(self)
        return add_tie_methods(class_(self))

    def __init__(self, d):
        for k, v in d.items():
            self[k] = v
            #setattr(self, k, v)

def add_tie_methods(obj):    # This code is part of perllib and can be changed
    cls = obj.__class__
    classname = cls.__name__
    result = type(classname, (cls,), dict())
    cls.__TIE_subclass__ = result

    def __get__(self, obj, objtype=None):
        return self.FETCH()
    result.__get__ =  __get__

    def __set__(self, obj, value):
        return self.STORE(value)
    result.__set__ = __set__

    obj.__class__ = result
    return obj

main.tied_scalar_v = TiedScalar.TIESCALAR(42)  # Generated code
assert main.tied_scalar_v == 42
main.tied_scalar_v = 100
assert main.tied_scalar_v == 100
print(TiedScalar.FETCH_CALLED_v)
print(TiedScalar.STORE_CALLED_v)

Here the 2 print statements print 1 and 0, so esp STORE is not being called, and fetch is not being called enough times based on the code. Note that 'main' stores all of the user's variables, also the TiedScalar class is (mostly) generated from the user’s perl code.


Solution

  • Ok - with your help, I was able to figure out how to do it. Basically I have to create a metaclass for my main, then use that class to store the initial object for tied scalars. Here is my updated code with changes marked # new:

    meta = type('mainmeta', (type,), {
        '__init__': lambda cls, name, bases, attrs: type.__init__(cls, name, bases, attrs)
    })      # new
    #main = type('main', tuple(), dict())
    main = meta('main', tuple(), dict())    # new
    
    class TiedScalar(dict):    # Generated code - can't be changed
        FETCH_CALLED_v = 0
        STORE_CALLED_v = 0
    
        def STORE(*_args):
            _args = list(_args)
            self = _args.pop(0) if _args else None
            TiedScalar.STORE_CALLED_v = TiedScalar.STORE_CALLED_v + 1
            self["value"] = _args.pop(0) if _args else None
            return self.get("value")
        
        
        #TiedScalar.STORE = lambda *_args, **_kwargs: perllib.tie_call(STORE, _args, _kwargs)
        
        
        def FETCH(*_args):
            _args = list(_args)
            self = _args.pop(0) if _args else None
            TiedScalar.FETCH_CALLED_v = TiedScalar.FETCH_CALLED_v + 1
            return self.get("value")
        
        
        #TiedScalar.FETCH = lambda *_args, **_kwargs: perllib.tie_call(FETCH, _args, _kwargs)
        
        
        @classmethod
        def TIESCALAR(*_args):
            _args = list(_args)
            class_ = _args.pop(0) if _args else None
            self = {"value": (_args.pop(0) if _args else None)}
            #self = perllib.bless(self, class_)
            #return perllib.add_tie_methods(self)
            return add_tie_methods(class_(self))
    
        def __init__(self, d):
            for k, v in d.items():
                self[k] = v
                #setattr(self, k, v)
    
    def add_tie_methods(obj):    # This code is part of perllib and can be changed
        cls = obj.__class__
        classname = cls.__name__
        result = type(classname, (cls,), dict())
        cls.__TIE_subclass__ = result
    
        def __get__(self, obj, objtype=None):
            #print(f'__get__({self}, {obj}, {objtype})')
            return self.FETCH()
        result.__get__ =  __get__
    
        def __set__(self, obj, value):
            #print(f'__set__({self}, {obj}, {value})')
            return self.STORE(value)
        result.__set__ = __set__
    
        obj.__class__ = result
        return obj
    
    def assign_meta(cls, var, value):       # new
        meta = cls.__class__
        setattr(meta, var, value)
        return value
    
    # main.tied_scalar_v = TiedScalar.TIESCALAR(42)
    assign_meta(main, 'tied_scalar_v', TiedScalar.TIESCALAR(42))    # new
    assert main.tied_scalar_v == 42
    main.tied_scalar_v = 100
    assert main.tied_scalar_v == 100
    print(TiedScalar.FETCH_CALLED_v)
    print(TiedScalar.STORE_CALLED_v)