Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A descriptor for contract attributes #30

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open

A descriptor for contract attributes #30

wants to merge 10 commits into from

Conversation

yaniv256
Copy link

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
Copy link
Owner

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 [email protected]
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:

#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
#30.

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

@yaniv256
Copy link
Author

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
Copy link
Owner

On Tue, Nov 18, 2014 at 8:07 PM, Yaniv Ben-Ami [email protected]
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
Copy link
Author

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
Copy link
Owner

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

@ChrisBeaumont
Copy link
Contributor

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.

"""
class_name=function_.__class__.__name__
function_=function_.__call__
function_.__dict__['__name__']=class_name +'.__call__'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not use function_.__name__ = class_name +'.__call__' here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason it generates an error:

AttributeError: 'instancemethod' object has no attribute 'name'

@ChrisBeaumont
Copy link
Contributor

@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
Copy link
Author

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
Copy link
Owner

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants