pythonclasscontracts

Reference local type in pycontract


I'm trying to use PyContracts within a web application, so I have lots of custom-defined classes being passed around that I simply want to type check alongside other more traditional argument types. I'd like to use contractual programming (PyContracts) to accomplish this, for the sake of cleanliness and forced documentation.

When I reference a locally visible class by name, PyContracts doesn't seem to be aware of the type. For example:

from contracts import contract

class SomeClass:
    pass

@contract
def f(a):
    """

    :param a: Just a parameter
    :type a: SomeClass
    """
    print(a)

my_a = SomeClass()
f(my_a)

Raises the following error:

ContractSyntaxError: Unknown identifier 'SomeClass'. Did you mean 'np_complex64'? (at char 0), (line:1, col:1)

I know I can use new_contract to custom-define names and bind them to classes, but that's a lot of hassle to do for every type. I want to use the docstring syntax for PyContracts if at all possible, and I definitely need to use the string-defined contract format since I'm using boolean type logic ("None|str|SomeClass"). How do I accomplish this with local types and minimal intrusion into the rest of my codebase?


Solution

  • I hacked together a magic decorator that adds types before creating the contract. For anyone that's interested, it seems to work, but it's probably pretty slow if you define a large number of functions:

    def magic_contract(*args, **kwargs):
        # Check if we got called without arguments, just the function
        func = None
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            func = args[0]
            args = tuple()
    
        def inner_decorator(f):
            for name, val in f.__globals__.items():
                if isinstance(val, type):
                    new_contract(name, val)
            return contract(*args, **kwargs)(f)
    
        if func:
            return inner_decorator(func)
        return inner_decorator
    

    And some test runs:

    In [3]: class SomeClass:
       ...:     pass
       ...:
    
    In [4]: @magic_contract
       ...: def f(a):
       ...:     """
       ...:
       ...:     :param a: Some parameter
       ...:     :type a: None|SomeClass
       ...:     """
       ...:     print(a)
       ...:
    
    In [5]: f(None)
    None
    
    In [6]: f(SomeClass())
    <__main__.SomeClass object at 0x7f1fa17c8b70>
    
    In [7]: f(2)
    ...
    ContractNotRespected: Breach for argument 'a' to f().
    ...
    
    In [8]: @magic_contract(b='int|SomeClass')
       ...: def g(b):
       ...:     print(type(b))
       ...:
    
    In [9]: g(2)
    <class 'int'>
    
    In [10]: g(SomeClass())
    <class '__main__.SomeClass'>
    
    In [11]: g(None)
    ...
    ContractNotRespected: Breach for argument 'b' to g().
    ...