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

A descriptor for contract attributes #30

Open yaniv256 opened 9 years ago

yaniv256 commented 9 years ago

Hi Andrea,

I wrote a descriptor for enforcing contracts. From the doc string:

ContractAttribute is a descriptor for object attributes that enforces a contract check on whatever object or function the attribute is set to. For a function, the function would not be passed the self argument (because that breaks encapsulation - you can still actively pass self if you want). Setting up an attribute like this is useful when an API that is expressed as an object requires a client function to work with. In such cases you want to both communicate expectations with regards to the needed function and verify that they are met.

Usage example:


from contracts import ContractAttribute, contract, ContractNotRespected

class spam(object): 
    f=ContractAttribute(contract(arg='float,>0')) #for functions
    x=ContractAttribute('float,>0')               #for scalars using string
    y=ContractAttribute(float)                    #for scalars using type

eggs=spam()                                                             

from math import log, exp, pi                                               
eggs.f=lambda (arg): log(arg)                                           

print "eggs.f(e)=" + str(eggs.f(exp(1.0)))                              

try:                                                                    
   print "eggs.f=" + str(eggs.f(-1.0))                                  
except ContractNotRespected as detail:                                  
   print detail                                                         

print "Attempting eggs.x=pi" 
eggs.x=pi
print "eggs.x=" + str(eggs.x) 

print "Attempting eggs.x=-pi" 
try:                                                                    
   eggs.x=-pi                                  
except ContractNotRespected as detail:                                  
   print detail                                                         

print "Attempting eggs.y=2*pi" 
eggs.y=2*pi
print "eggs.y=" + str(eggs.y) 
print "eggs.x=" + str(eggs.x) 

print "Attempting eggs.y=3" 
try:                                                                    
   eggs.y=3                                  
except ContractNotRespected as detail:                                  
   print str(detail)[:180]        

Output:

eggs.f(e)=1.0
Breach for argument 'arg' to <lambda>().
Condition -1.0 > 0 not respected
checking: >0         for value: Instance of float: -1.0   
checking: float,>0   for value: Instance of float: -1.0   
Attempting eggs.x=pi
eggs.x=3.14159265359
Attempting eggs.x=-pi
Condition -3.14159265359 > 0 not respected
checking: >0         for value: Instance of float: -3.141592653589793   
checking: float,>0   for value: Instance of float: -3.141592653589793   
Attempting eggs.y=2*pi
eggs.y=6.28318530718
eggs.x=3.14159265359
Attempting eggs.y=3
Could not satisfy any of the 3 clauses in Float|np_scalar_float|np_scalar,array(float).
 ---- Clause #1:   Float
 | Expected type 'float', got 'int'.
 | checking: Float   for value
AndreaCensi commented 9 years ago

Hi Yaniv, This looks like something very useful. I'm currently traveling for a few days so I will check it out in detail this weekend. A minor comment is that "ContractAttribute" is quite long. What's the smallest meaningful name for that?

I imagine that this depends on other libraries implementing attributes---I don't use this design pattern much so I wouldn't know.

a.

On Tuesday, November 18, 2014, Yaniv Ben-Ami notifications@github.com wrote:

Hi Andrea,

I wrote a descriptor for enforcing contracts. From the doc string:

A descriptor for object attributes that enforces a contract check on whatever object or function the attribute is set to. For a function, the function would not be passed the self argument (because that breaks encapsulation - you can still actively pass self if you want). Setting up an attribute like this is useful when an API that is expressed as an object requires a client function to work with. In such cases you want to both communicate expectations with regards to the needed function and verify that they are met.

Usage example:

from contracts import ContractAttribute, contract, ContractNotRespected class spam(object): f=ContractAttribute(contract(arg='float,>0')) #for functions x=ContractAttribute('float,>0') #for scalars using string y=ContractAttribute(float) #for scalars using type

eggs=spam() from math import log, exp, pi eggs.f=lambda (arg): log(arg) print "eggs.f(e)=" + str(eggs.f(exp(1.0))) try: print "eggs.f=" + str(eggs.f(-1.0)) except ContractNotRespected as detail: print detail print "Attempting eggs.x=pi" eggs.x=piprint "eggs.x=" + str(eggs.x) print "Attempting eggs.x=-pi" try: eggs.x=-pi except ContractNotRespected as detail: print detail print "Attempting eggs.y=2_pi" eggs.y=2_piprint "eggs.y=" + str(eggs.y) print "eggs.x=" + str(eggs.x) print "Attempting eggs.y=3" try: eggs.y=3 except ContractNotRespected as detail: print str(detail)[:180]

Output:

eggs.f(e)=1.0 Breach for argument 'arg' to (). Condition -1.0 > 0 not respected checking: >0 for value: Instance of float: -1.0 checking: float,>0 for value: Instance of float: -1.0 Attempting eggs.x=pi eggs.x=3.14159265359 Attempting eggs.x=-pi Condition -3.14159265359 > 0 not respected checking: >0 for value: Instance of float: -3.141592653589793 checking: float,>0 for value: Instance of float: -3.141592653589793 Attempting eggs.y=2*pi eggs.y=6.28318530718 eggs.x=3.14159265359 Attempting eggs.y=3 Could not satisfy any of the 3 clauses in Float|np_scalar_float|np_scalar,array(float). ---- Clause #1: Float | Expected type 'float', got 'int'. | checking: Float for value


You can merge this Pull Request by running

git pull https://github.com/yaniv256/contracts ContractAttribute

Or view, comment on, or merge it at:

https://github.com/AndreaCensi/contracts/pull/30 Commit Summary

  • before revert on backport.py
  • Added support for callable objects and bound methods
  • ContractAtribute added to main
  • Added doc string
  • Changed the name of the private attribute used to store wrapped values to contracts
  • a bit more info in doc string
  • Added support for scalar attributes

File Changes

Patch Links:

— Reply to this email directly or view it on GitHub https://github.com/AndreaCensi/contracts/pull/30.

(sent from mobile device, please pardon typos and brevity)

yaniv256 commented 9 years ago

Thanks!

I guess we could go with something like "verify", "guard", "watch" or "attach". It would be nice to have it sound like an original python keyword.

No, I used just functools and types, both from the standard library.

AndreaCensi commented 9 years ago

On Tue, Nov 18, 2014 at 8:07 PM, Yaniv Ben-Ami notifications@github.com wrote:

No, I used just functools and types, both from the standard library.

Sorry let me reword my comment: I meant, if there is a popular library implementing descriptors, then the syntax in PyContracts should be similar to it. I will look at the topic when I come back home.

thanks for the contribution!

A.

Andrea Censi | LIDS / MIT | http://censi.mit.edu

yaniv256 commented 9 years ago

BTW, I learned how to do descriptors from Chris Beaumont's excellent writeup (which I found via google).

Chris - I changed your design pattern a bit to hold the data in a dict on the instances, rather than on the descriptors. I hope it is as valid, but it would be good if you review.

AndreaCensi commented 9 years ago

@ChrisBeaumont you are the expert with descriptors. What do you think of this implementation?

ChrisBeaumont commented 9 years ago

This looks good to me! This is very similar to how Traits works (http://code.enthought.com/projects/traits/)

I have a few style comments that I'll leave inline.

ChrisBeaumont commented 9 years ago

@AndreaCensi regarding Traits and what to name these:

IPython has a "traitlets" module they use, whose API is similar to traits: http://ipython.org/ipython-doc/dev/api/generated/IPython.utils.traitlets.html

The problem is that both of these libraries are more "MyPy"-style, in that they have many special-purpose classes like Float, Boolean to express type info, rather than the DSL-style approach contracts uses. So they don't provide a precedent for whether to call these things Properties or something else.

I personally like Property or Attribute

yaniv256 commented 9 years ago

OK. For my own project I need functions, so I'll use my own fork until the arrow notation is ready. But for the pull request I scaled back function support. It's a trivial descriptor now, and should be fairly robust.

I also renamed to Attribute.

AndreaCensi commented 9 years ago

I tried to merge this, but the two changes you made to main.py (one involving using partial functions) make tests fail in nonobvious ways.

I'm working on this branch: https://github.com/AndreaCensi/contracts/tree/yaniv256-ContractAttribute You can set Yaniv = False or True alternatively to enable/disable your changes.

Run nosetests contracts to see what tests fail.