Reference local type in pycontract

368 views Asked by At

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?

1

There are 1 answers

0
scnerd On BEST ANSWER

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().
...