[Zope3-Users] Dynamic Typing: Are checks on interface compliance possible?

Marc Rijken marc at rijken.org
Thu May 18 03:11:00 EDT 2006


Hi Reinhold,

In addition to verifyClass and verifyObject, I am developing a module for doing 
realtime typechecks. I am using a first version of it for one application and it 
works great.

I have made a proposal and readme for it, which I have attached. Is this what you 
are looking for?

Marc

Op 18-5-2006 8:23, Reinhold Strobl schreef:
> Hi,
> 
> what I am looking for is something like PyChecker for Zope interfaces. I mean
> PyChecker aims to address the benefit of static typed languages and their
> compile-time checkings. 
> 
> In Zope, there is no forced interface compliance. But is there a possibility or
> program, which can check this?
> 
> Thanks!
> 
> _______________________________________________
> Zope3-users mailing list
> Zope3-users at zope.org
> http://mail.zope.org/mailman/listinfo/zope3-users

-------------- next part --------------
TypeCheck of Interfaces

  Status

    IsProposal

  Author

    Marc Rijken

  Problem/Proposal/Goals

    In interfaces the behaviour of methods and attributes is been described. It would be nice if this desciption
    could be used for checking the behaviour of implemented methods/attributes. In fact, my proposal consist
    of:

    1.  describe at the level of the implemented method/attribute the interface and not on the class level,
        so it is always clear which interface describes the intended behaviour of the implemented 
        method/attribute

    2.  use the docstring of the interface to extend the docstring of the method and put in a
        reference to the interface automatically based on 1.

    3.  use the interface for checking the behaviour of the method/attribute on runtime.    

  Proposed Solution

    1.  use 'implement' at the method level in stead of 'implements' at the class level. I.e.::


            class Test(object):
                @implement(ITest)
                def add(self, x, y):
                   return x+y
    
                @implement(ITest)
                def multiply(self, x, y):
                    return x*y

                @implementGetter(ITest['at'])
                def _getAt(self):
                    return self.i
    
                @implementSetter(ITest['at'])
                def _setAt(self, value):
                    self.i = value
    
                at = property(_getAt, _setAt)

 
        in stead of::


            class Test(object):
                implements(ITest)
    
                def add(self, x, y):
                    return x+y
    
                def multiply(self, x, y):
                    return x*y

                def _getAt(self):
                    return self.i
    
                def _setAt(self, value):
                    self.i = value
    
                at = property(_getAt, _setAt)

        The decorators `implement`, `implementGetter` and `implementSetter` acts like implements.


    2.  `implement` changs also the docstring of the implemented method. Ie::


            class ITest(Interface):
    
                def add(x, y):
                    """add two parameters"""
    
                def multiply(x, y):
                    """multiply two parameters"""

            class Test(object):
                @implement(ITest)
                def add(self, x, y):
                    return x+y
    
                @implement(ITest)
                def multiply(self, x, y):
                    """a test implementation"""
                    return x*y


        would give the following docstring for Test.add.__doc__::


            Interface ITest, method add

            add two parameters


        and would give the following docstring for Test.mulitply.__doc__::

            Interface ITest, method multiply

            multiply two parameters

            a test implementation


    3.  In order to use the interface for typechecking, I propose to use the TypeCheck
        module (see http://oakwinter.com/code/typecheck/). This can be used easely, when
        'implement' works like 'accepts' and 'returns'. The parameters for accepts and returns
        has to be added to the interface. For example::


            class ITest(Interface):
    
                @accepts(int, int)
                @returns(str)
                def add(x, y):
                    """add two parameters"""
    
                @accepts(float, float)
                @returns(float)
                def multiply(x, ysdx):
                    """multiply two parameters"""


        In accepts or returns you can give any type or class. The parameter of the 
        method will be checked for beiing an instance of that type or class. But it is
        also convenient when it is possible to give the interface which has to be
        implemented by the parameter. So the parameter is checked like in ITest.providedBy(par1).::


            class ITest2(Interface):
    
                @accepts(ITest, ITest)
                @returns(ITest)
                def add(x, y):
                    """add two parameters"""


        This usage can be a problem when the interface is not defined yet. In that
        case we can use the next.::


            class ITest(Interface):
    
                @accepts(ImplementedInterface('ITest'), ImplementedInterface('ITest'))
                @returns(ImplementedInterface('ITest'))
                def add(x, y):
                    """add two parameters"""
                    
        The previous is for methods, but for attributes it works exactly the same.

  Risks

    List possible risks or bottlenecks of the implementation of the proposal.
-------------- next part --------------
INTERFACE
=========

TypeCheck is a package which makes it possible to checks the types
of the parameters of a function/method and the return values of it.
These checks are usefull for assuring the correct functioning of
applications.

Of course these checks can also be part of doctests, but the typechecks
will assure that all situations are covered.

When using typechecks, it is not necessary anymore to perform test of
parameters within the function/methods.

In order to perform these checks, the checks has to be described and
added to the function/method. When zope.interface will be used, it
will be appropriate to describe the checks at the interface, so the
interface description will be complete and the implementation does not
contain any interface descriptions.

For well tested production software the checks can be commented out,
because the checks will cost some performance.

USING
=====

First start defining an interface

    >>> from zope.interface import Interface
    >>> from zope.schema import Int
    >>> from interface import implement, implementGetter, implementSetter, accepts, returns
    >>> class ITest(Interface):
    ...     @accepts(int)
    ...     @returns(str)
    ...     def test(j):
    ...         'This is an test method which convert integers to string'
    ...
    ...     at = Int(title=u'Test attribute')

Now the interface is defined, we can implement it

    >>> class Test(object):
    ...     def __init__(self, i):
    ...         self.i = i
    ...
    ...     @implement(ITest)
    ...     def test(self, j):
    ...         if self.i > 0:
    ...             return '%d %0.2f'% (self.i, j)
    ...         return self.i
    ...
    ...     @implementGetter(ITest['at'])
    ...     def _getAt(self):
    ...         return self.i
    ...
    ...     @implementSetter(ITest['at'])
    ...     def _setAt(self, value):
    ...         self.i = value
    ...
    ...     at = property(_getAt, _setAt)
    >>> ITest.implementedBy(Test)
    True
    >>> t=Test(2)
    >>> ITest.providedBy(t)
    True

You see that when using typechecks, it is always clear which interface is
implemented by both the method as the attribute getters and setters.

When the method is executed from an instance, the checks are performed.

    >>> t = Test(1)
    >>> t.test(2)
    '1 2.00'
    >>> t.at = 1

Executing test with an integer as parameter performs the method as normal.
But when a string is given as a paramater, an error will be raised.

    >>> t.test('2')
    Traceback (most recent call last):
        ...
    TypeCheckError: Argument j: for 2, expected <type 'int'>, got <type 'str'>
    
The same counts for setting an attribute (in fact the parameter of the setter) with a string 
in stead of an integer.

    >>> t.at = '1'
    Traceback (most recent call last):
        ...
    TypeCheckError: Argument value: for 1, expected <type 'int'>, got <type 'str'>

The same counts for a wrong return value.

    >>> t = Test(-1)
    >>> t.test(2)
    Traceback (most recent call last):
        ...
    TypeCheckError: Return value: for -1, expected <type 'str'>, got <type 'int'>
    >>> t = Test('str')
    
And also the attribute (in fact the return value of the getter) is beiing checked.    
    
    >>> t.at
    Traceback (most recent call last):
        ...
    TypeCheckError: Return value: for str, expected <type 'int'>, got <type 'str'>

DOCS
====

As a side effect the doc string of an interface will be copied to the
implemented method when that method does not have a docstring.
When a docstring exists, a string mentioning the origin of the interfacmethod
will be place before the existing docstring.

    >>> t = Test(1)
    >>> t.test.__doc__
    'Interface\n=========\n\nInterface ITest, method test \n\nThis is an test method which convert integers to stringImplementation\n==============\n\n'
    >>> class Test(object):
    ...     def __init__(self, i):
    ...         self.i = i
    ...
    ...     @implement(ITest)
    ...     def test(self, j):
    ...         'my own doc string'
    ...         if self.i > 0:
    ...             return '%d %0.2f'% (self.i, j)
    ...         return self.i

    >>> t = Test(1)
    >>> t.test.__doc__
    'Interface\n=========\n\nInterface ITest, method test \n\nThis is an test method which convert integers to stringImplementation\n==============\n\nmy own doc string'

Provided Interface as check
===========================

It is also possible to check whether an parameter or return value
implements a given interface.

    >>> from interface import ImplementedInterface

    >>> class IName(Interface):
    ...     def getName():
    ...         'This is an test method which return the name'

    >>> class Name(object):
    ...     def __init__(self, name):
    ...         self.name = name
    ...
    ...     @implement(IName)
    ...     def getName(self):
    ...         return self.name

We could have use zope.interface.implements instead of implement, because this
class/interface does not use the typecheck. But I think it is a goog habit to
always use implement at the method level instead of the implements at the class
level.

    >>> class ITest(Interface):
    ...     @accepts(ImplementedInterface(IName))
    ...     @returns(str)
    ...     def test(j):
    ...         'This is an test method which return the name of j'

    >>> class Test(object):
    ...     @implement(ITest)
    ...     def test(self, j):
    ...         return j.getName()

    >>> n = Name('Marc')
    >>> t = Test()
    >>> t.test(n)
    'Marc'

When accepts or returns get an interface as parameter, they will know it has
to be converted to ImplementedInterface. So the next works to and is even
more easy to write.

    >>> class ITest(Interface):
    ...     @accepts(IName)
    ...     @returns(str)
    ...     def test(j):
    ...         'This is an test method which return the name of j'

    >>> class Test(object):
    ...     @implement(ITest)
    ...     def test(self, j):
    ...         return j.getName()

    >>> n = Name('Marc')
    >>> t = Test()
    >>> t.test(n)
    'Marc'

When the parameter does not implement the given interface, an error is raised.

    >>> t.test('marc')
    Traceback (most recent call last):
        ...
    TypeCheckError: Argument j: for marc, expected <InterfaceClass __builtin__.IName>, got <type 'str'>


Disabling
=========

Type-checking can be turned on and off by toggling the value of the global 
enable_checking variable.

    >>> import typecheck
    >>> typecheck.enable_checking = False
    >>> class ITest(Interface):
    ...     @accepts(ImplementedInterface(IName))
    ...     @returns(str)
    ...     def test(j):
    ...         'This is an test method which return the name of j'

    >>> class Test(object):
    ...     @implement(ITest)
    ...     def test(self, j):
    ...         return j.getName()

    >>> n = Name('Marc')
    >>> t = Test()
    >>> t.test(n)
    'Marc'
    >>> t.test('Marc')
    Traceback (most recent call last):
        ...
    AttributeError: 'str' object has no attribute 'getName'
    
    Now not a TypeCheckError will be raise (because the type is not checked),
    but a AttributeError is raised (because test will not function when the 
    parameter has not a getName method).

    We set back the enable_checking for next usages.
    
    >>> typecheck.enable_checking = True



More information about the Zope3-users mailing list