4

A read-only data descriptor is a descriptor that defines both __get__ and __set__, but __set__ raises AttributeError when called.

An example is a simple read-only property:

class Test():

    _i = 1

    @property
    def i(self):
        return self._i

assert hasattr(Test.i, '__get__')
assert hasattr(Test.i, '__set__')
t = Test()
t.i # 1
t.i = 2 # ERROR

If I have an instance of a class, I can determine if the instance attribute is a read-only data descriptor this way (although I don't like this at all):

def is_ro_data_descriptor_from_instance(instance, attr):
    temp = getattr(instance, attr)
    try:
        setattr(instance, attr, None)
    except AttributeError:
        return True
    else:
        setattr(instance, attr, temp)
        return False

If I know the class doesn't require any arguments to be instantiated, I can determine if its class attribute is a read-only data descriptor similar to the above:

def is_ro_data_descriptor_from_klass(klass, attr):
    try:
        setattr(klass(), attr, None)
    except AttributeError:
        return True
    else:
        return False

However, if I don't know the signature of the class ahead of time, and I try to instantiate a temporary object in this way, I could get an error:

class MyClass():
    i = 1
    def __init__(self, a, b, c):
        '''a, b, and c are required!'''
        pass

def is_ro_data_descriptor_from_klass(MyClass, 'i') # Error

What can be done to determine if a class attribute is a read-only data descriptor?

EDIT: Adding more information.

Below is the code I am trying to get working:

class StaticVarsMeta(type):
    '''A metaclass that will emulate the "static variable" behavior of
    other languages. For example: 

        class Test(metaclass = StaticVarsMeta):
            _i = 1
            @property
            def i(self):
                return self._i
        t = Test()
        assert t.i == Test.i'''
    statics = {}
    def __new__(meta, name, bases, dct):
        klass = super().__new__(meta, name, bases, dct)
        meta.statics[klass] = {}
        for key, value in dct.items():
            if "_" + key in dct:
                meta.statics[klass][key] = set()
                if hasattr(value, '__get__'):
                    meta.statics[klass][key].add('__get__')
                if hasattr(value, '__set__'):
                    try:
                        value.__set__(None, None)
                    except AttributeError:
                        continue
                    else:
                        meta.statics[klass][key].add('__set__')
        return klass
    def __getattribute__(klass, attr):
        if attr not in StaticVarsMeta.statics[klass]:
            return super().__getattribute__(attr)
        elif '__get__' not in StaticVarsMeta.statics[klass][attr]:
            return super().__getattribute__(attr)
        else:
            return getattr(klass, '_' + attr)
    def __setattr__(klass, attr, value):
        if attr not in StaticVarsMeta.statics[klass]:
            super().__setattr__(attr, value)
        elif '__set__' not in StaticVarsMeta.statics[klass][attr]:
            super().__setattr__(attr, value)
        else:
            setattr(klass, '_' + attr, value)

class Test(metaclass = StaticVarsMeta):
    _i = 1
    def get_i(self):
        return self._i
    i = property(get_i)

Note the following:

type(Test.i) # int
type(Test.__dict__['i']) # property
Test().i = 2 # ERROR, as expected
Test.i = 2 # NO ERROR - should produce an error
Rick
  • 43,029
  • 15
  • 76
  • 119
  • 1
    I've re-read the question and understand it better. I've removed the two implementations related to other types of descriptor, as I think they were only muddying the waters. – jonrsharpe Apr 14 '15 at 15:57
  • 1
    Out of curiosity, why to you *need* to know if it is read-only? What's the use-case here? – mgilson Apr 14 '15 at 15:57
  • It's complicated. I was adding on to [my answer to this question](http://stackoverflow.com/questions/68645/static-class-variables-in-python/27568860#27568860). I am writing a metaclass `StaticVarsMeta` that will emulate the static variable behavior of other languages. These classes would allow you to get and set a "static variable" via the class itself, or via the instance. I have most of it working, but realized I have to raise an `AttributeError` in `StaticVarsMeta.__setattr__` when the descriptor is a read-only data descriptor. – Rick Apr 14 '15 at 16:03
  • It's **really hacky**, but note that you get `AttributeError: can't set attribute` *whatever* "instance" object you pass to `Test.i.__set__`, so you could call e.g. `Test.i.__set__(None, None)` and see whether the `err.args` are what you expect (if it's not read-only, you still get an `AttributeError`, but now with `'NoneType' object has no attribute 'i'`). – jonrsharpe Apr 14 '15 at 16:05
  • @RickTeachey -- Maybe I'm not following, but it looks like raising AttributeError is the feature of a read-only descriptor that you're using to identify it as read-only -- So why not just pass through to the descriptor and let it raise the AttributeError? – mgilson Apr 14 '15 at 16:06
  • @mgilson Can't. If you pass through, i.e. `super().__setattr__(attr, value)` in the metaclass, it will overwrite the descriptor object. It won't raise an error. – Rick Apr 14 '15 at 16:08
  • @mgilson I've added some more information to the end. I left it out at first because I didn't think it was needed. – Rick Apr 14 '15 at 16:11
  • Note that, although it's conventional to do so, you aren't *required* to back a property `foo` with an attribute `_foo`. Checking for `__get__` and `__set__` attributes seems like a more robust thing to rely on. – jonrsharpe Apr 14 '15 at 16:14
  • @jonrsharpe That's a good point - my idea was that for this specific implementation, the requirement would be you pair `i` with `_i`. The pairing defines a "static variable". There might be a better way to do it. – Rick Apr 14 '15 at 16:15
  • @jonrsharpe This way also allows you to have properties/descriptors that AREN'T "static variables". But maybe that's confusing and all class attributes should be "static variables" for `StaticVarsMeta` classes. – Rick Apr 14 '15 at 16:20

2 Answers2

4

It seems super-awkward, but here's how you could implement it based on my comment:

class StaticVarsMeta(type):

    statics = {}

    def __new__(meta, name, bases, dct):
        cls = super().__new__(meta, name, bases, dct)
        meta.statics[cls] = {}
        for key, val in dct.items():
            if hasattr(val, '__get__') and hasattr(val, '__set__'):
                meta.statics[cls][key] = {'__get__'}
                try:
                    val.__set__(None, None)
                except AttributeError as err:
                    if "can't set attribute" in err.args:
                        continue
                meta.statics[cls][key].add('__set__')
        return cls

In use:

>>> class ReadOnly(metaclass=StaticVarsMeta):
    @property
    def foo(self):
        return None


>>> class ReadWrite(metaclass=StaticVarsMeta):
    @property
    def bar(self):
        return None
    @bar.setter
    def bar(self, val):
        pass


>>> StaticVarsMeta.statics
{<class '__main__.ReadOnly'>: {'foo': {'__get__'}}, 
 <class '__main__.ReadWrite'>: {'bar': {'__get__', '__set__'}}}

This is more of a "starter for 10", there must be a better way to do it...

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • Bravo. This is better than any idea I've been able to come up with. I don't think there is a way to do this that isn't going to be super-awkward. – Rick Apr 14 '15 at 16:30
  • 1
    @RickTeachey perhaps not, but I don't like relying on the specific error message like this. Anyway, hopefully someone will turn up and tell me why I'm wrong! – jonrsharpe Apr 14 '15 at 16:34
  • @RickTeachey hmm, the examples I show work for me. Which version are you using? – jonrsharpe Apr 14 '15 at 17:46
  • BTW my metaclass as written above is still broken, but now it has a different problem. – Rick Apr 14 '15 at 17:51
3

Your first solution can be made simpler and slightly more robust, by attempting to assign the value it already has. This way, no undoing is required (Still, this isn't thread-safe).

def is_ro_data_descriptor_from_instance(instance, attr):
    temp = getattr(instance, attr)
    try:
        setattr(instance, attr, temp)
    except AttributeError:
        return True
    else:
        return False
shx2
  • 61,779
  • 13
  • 130
  • 153
  • Although a good point, I'm not sure this answers the question, as it doesn't tell the OP how to find out from the *class* object if the property is read-only. – jonrsharpe Apr 14 '15 at 16:37
  • @jonrsharpe Not an answer to the question, but still a helpful idea. Thanks. – Rick Apr 14 '15 at 17:14