AndreaCensi / contracts

PyContracts is a Python package that allows to declare constraints on function parameters and return values. Contracts can be specified using Python3 annotations, or inside a docstring. PyContracts supports a basic type system, variables binding, arithmetic constraints, and has several specialized contracts and an extension API.
http://andreacensi.github.io/contracts/
Other
398 stars 62 forks source link

Contracts does not stop execution immediately having found an unexpected OR null input #65

Open AldarisPale opened 6 years ago

AldarisPale commented 6 years ago

Hi.

I think I have stumbled upon a bug or if not a bug then atleast an counterintuitive feature. Consider the following code:

#!/usr/bin/python3
# -*- coding: utf8 -*-

from contracts import contract

class ObjectThatWillSeeDeathBeforeBeingBorn(object):

    @contract
    def __init__(self, TestingString: 'str[>0]'):
        print('String given as parameter: %s.' % TestingString)
        print('ID of ObjectThatWillSeeDeathBeforeBeingBorn: %s.' % id(self))

    def __del__(self):
        print('ID of object being killed: %s.' % id(self))

NewObjectForTestingTheBug = ObjectThatWillSeeDeathBeforeBeingBorn()

When executed as listed above we will get this as output:

$ python3 contracts_possible_bug.py 
ID of object being killed: 139899159500840.
Traceback (most recent call last):
  File "contracts_possible_bug.py", line 17, in <module>
    NewObjectForTestingTheBug = ObjectThatWillSeeDeathBeforeBeingBorn()
TypeError: __init__() missing 1 required positional argument: 'TestingString'

The notable thing here is that we don't get any output from init but we do get output from del.

If we modify the last line so it looks like this: NewObjectForTestingTheBug = ObjectThatWillSeeDeathBeforeBeingBorn(42) the output looks like this:

$ python3 contracts_possible_bug.py 
Traceback (most recent call last):
  File "contracts_possible_bug.py", line 17, in <module>
    NewObjectForTestingTheBug = ObjectThatWillSeeDeathBeforeBeingBorn(42)
  File "<decorator-gen-1>", line 2, in __init__
  File "/usr/local/lib/python3.5/dist-packages/contracts/main.py", line 260, in contracts_checker
    raise e
  File "/usr/local/lib/python3.5/dist-packages/contracts/main.py", line 255, in contracts_checker
    accepts_parsed[arg]._check_contract(context, bound[arg], silent=False)
  File "/usr/local/lib/python3.5/dist-packages/contracts/interface.py", line 439, in _check_contract
    self.check_contract(context, value, silent)
  File "/usr/local/lib/python3.5/dist-packages/contracts/library/strings.py", line 19, in check_contract
    value=value, context=context)
contracts.interface.ContractNotRespected: Breach for argument 'TestingString' to ObjectThatWillSeeDeathBeforeBeingBorn:__init__().
Expected a string, got 'int'.
checking: str[>0]   for value: Instance of <class 'int'>: 42   
Variables bound in inner context:
- self: Instance of <class '__main__.ObjectThatWillSeeDeathBeforeBeingBorn'>: <__main__.ObjectThatWillSeeDeathBeforeBeingBorn object at 0x7fe... [clip]
ID of object being killed: 140638653347880.

I had expected that when a breach of contract has been found everything grinds to a halt but we can see that in both cases code still gets executed.

Last but not least, thank You for Your great work on contracts!

AldarisPale commented 6 years ago

Hi again.

After getting deeper into python and learning that __del__ is a bad idea anyway I have refactored the code:

#!/usr/bin/python3
# -*- coding: utf8 -*-

from contracts import contract

class ObjectThatWillSeeDeathBeforeBeingBorn(object):

    @contract
    def __init__(self, TestingString: 'str[>0]'):
        print('String given as parameter: %s.' % TestingString)
        print('ID of ObjectThatWillSeeDeathBeforeBeingBorn: %s.' % id(self))

    def __enter__(self):
        pass

    def __exit__(self, type, value, tb):
        print('ID of object being killed: %s.' % id(self))

with ObjectThatWillSeeDeathBeforeBeingBorn() as NewObjectForTestingTheBug:
    pass

we now get:

$ python3 contracts_possible_bug.py 
Traceback (most recent call last):
  File "contracts_possible_bug.py", line 20, in <module>
    with ObjectThatWillSeeDeathBeforeBeingBorn() as NewObjectForTestingTheBug:
TypeError: __init__() missing 1 required positional argument: 'TestingString'

Please note, that code in __exit__ did not get executed.

Lets try initating the object with a wrong type: with ObjectThatWillSeeDeathBeforeBeingBorn(42) as NewObjectForTestingTheBug:

Result:

$ python3 contracts_possible_bug.py 
Traceback (most recent call last):
  File "contracts_possible_bug.py", line 20, in <module>
    with ObjectThatWillSeeDeathBeforeBeingBorn(42) as NewObjectForTestingTheBug:
  File "<decorator-gen-1>", line 2, in __init__
  File "/usr/local/lib/python3.5/dist-packages/contracts/main.py", line 260, in contracts_checker
    raise e
  File "/usr/local/lib/python3.5/dist-packages/contracts/main.py", line 255, in contracts_checker
    accepts_parsed[arg]._check_contract(context, bound[arg], silent=False)
  File "/usr/local/lib/python3.5/dist-packages/contracts/interface.py", line 439, in _check_contract
    self.check_contract(context, value, silent)
  File "/usr/local/lib/python3.5/dist-packages/contracts/library/strings.py", line 19, in check_contract
    value=value, context=context)
contracts.interface.ContractNotRespected: Breach for argument 'TestingString' to ObjectThatWillSeeDeathBeforeBeingBorn:__init__().
Expected a string, got 'int'.
checking: str[>0]   for value: Instance of <class 'int'>: 42   
Variables bound in inner context:
- self: Instance of <class '__main__.ObjectThatWillSeeDeathBeforeBeingBorn'>: <__main__.ObjectThatWillSeeDeathBeforeBeingBorn object at 0x7f6... [clip]

Now let's try with the correct type: with ObjectThatWillSeeDeathBeforeBeingBorn("42") as NewObjectForTestingTheBug:

Result:

$ python3 contracts_possible_bug.py 
String given as parameter: 42.
ID of ObjectThatWillSeeDeathBeforeBeingBorn: 139824865924656.
ID of object being killed: 139824865924656.

So the lesson here is not to use __del__ when dealing with objects.