python-interface icon indicating copy to clipboard operation
python-interface copied to clipboard

Default implementation for Interface properties

Open aaronsnoswell opened this issue 4 years ago • 1 comments

Hello! Thanks for a great Python module.

I'm using this to define an interface with class properties (member variables). I would like to provide a default value for these variables (e.g. None), but I can't see a way to do this.

My attempt;

import interface


class IMyInterface(interface.Interface):
    @property
    def member_variable(self):
        pass

    @property
    @interface.default
    def optional_member_variable(self):
        return None


class Concrete(interface.implements(IMyInterface)):
    @property
    def member_variable(self):
        return 1.0


if __name__ == "__main__":
    c = Concrete()

Gives the error traceback;

Traceback (most recent call last):
  File "C:\Users\uqasnosw\AppData\Local\Continuum\Miniconda3\envs\python36\lib\site-packages\interface\interface.py", line 114, in __new__
    signature = TypedSignature(v)
  File "C:\Users\uqasnosw\AppData\Local\Continuum\Miniconda3\envs\python36\lib\site-packages\interface\typed_signature.py", line 28, in __init__
    self._signature = signature(extract_func(obj))
  File "C:\Users\uqasnosw\AppData\Local\Continuum\Miniconda3\envs\python36\lib\inspect.py", line 3065, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
  File "C:\Users\uqasnosw\AppData\Local\Continuum\Miniconda3\envs\python36\lib\inspect.py", line 2815, in from_callable
    follow_wrapper_chains=follow_wrapped)
  File "C:\Users\uqasnosw\AppData\Local\Continuum\Miniconda3\envs\python36\lib\inspect.py", line 2193, in _signature_from_callable
    raise TypeError('{!r} is not a callable object'.format(obj))
TypeError: default(<function IMyInterface.optional_member_variable at 0x00000168036D5BF8>) is not a callable object

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "D:/Development/unimodal-irl/unimodal_irl/envs/explicit_env.py", line 3, in <module>
    class IMyInterface(interface.Interface):
  File "C:\Users\uqasnosw\AppData\Local\Continuum\Miniconda3\envs\python36\lib\site-packages\interface\interface.py", line 122, in __new__
    raise_from(TypeError(errmsg), e)
  File "<string>", line 1, in raise_from
TypeError: Couldn't parse signature for field IMyInterface.optional_member_variable of type property.

If you can provide a little guidance on what form a fix might take, I'm happy to attempt a fix and submit a pull request!

Thank you,

aaronsnoswell avatar Jul 27 '20 23:07 aaronsnoswell

@aaronsnoswell apologies for the late response, but I think the issue here is just the order in which you're applying the @property and @default decorators. If you change your first definition to:

In [5]: class IFace(interface.Interface):
class IFace(interface.Interface):
    @interface.default
    @property
    def optional_member(self):
        return 3

The reason order matters here is that when you create a new interface, the interface machinery goes through all the function or property-like attributes of your class and tries to extract function signatures from the attributes using the standard library inspect module. In your example, you have a property that wraps a default object, and when we go to extract the signature from the default, inspect blows up because default isn't actually callable: it's a sentinel object that we unwrap during class creation.

If you switch the order of the decorators, then you end up with a default wrapping a property instead of a property wrapping a default, which means interface sees the default first, and knows how to handle it.

ssanderson avatar Oct 29 '20 13:10 ssanderson