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

How to define parametrized contracts? #26

Closed ChrisBeaumont closed 9 years ago

ChrisBeaumont commented 9 years ago

This is related to #23

I'd like do define a custom contract using something like


def checker(value, x):
     return value <= x  # dumb example

new_contract('foo(x)', checker)

minimum_value = 5

@contract(value='foo(minimum_value)')
def bar(value):
     sqrt(value - 5)

bar(6) # fine
bar(5) # fine
bar(4) # Contract violation

That is, I would like to put a parameter x in my contract definition, have the value of x extracted from the scope where the contract decorator is used, and pass both the value of the contract parameter and the value of the function input passed into my checker function.

This seems to be possible, given builtin contracts like isinstance(Foo). Is there a way to use new_contract function/decorator to set up similar contracts, or do I need to build a manual Contract subclass?

AndreaCensi commented 9 years ago

Hi Chris,

How about this?


    from contracts import contract, new_contract

    minimum_value = 5

    def checker(x):
         return minimum_value <= x

    new_contract('foo', checker)

    @contract(value='foo')
    def bar(value):
         pass

    bar(6) # fine
    bar(5) # fine
    bar(4) # Contract violation
ChrisBeaumont commented 9 years ago

The problem is I would want to use different values for the parameter for different contracts (a la the IsInstance contract) On Thu, Nov 13, 2014 at 6:48 PM Andrea Censi notifications@github.com wrote:

Hi Chris,

How about this?

from contracts import contract, new_contract

minimum_value = 5

def checker(x): return minimum_value <= x

new_contract('foo', checker)

@contract(value='foo') def bar(value): pass

bar(6) # fine bar(5) # fine bar(4) # Contract violation

— Reply to this email directly or view it on GitHub https://github.com/AndreaCensi/contracts/issues/26#issuecomment-62988259 .

AndreaCensi commented 9 years ago

The way isinstance(foo) works is by creating a Contract instance with parameter string "foo". Then the check is done by checking the name of the class of the object against "foo".

I cannot think of a way of implementing what you suggest without rewriting lots of PyCotnracts code, even if willing to implement a custom Contract.

In particular: how to refer to the globals? I suppose that on calling @contract one could save globals() in the function (e.g. bar._globals holds the globals)? however this would be bad for things like reference counting.

ChrisBeaumont commented 9 years ago

I understand if you don't want to move in this direction, but I do think that there's utility in being able to pass variables into the PyContracts dsl -- you made a good argument that MyPy-like syntax is awkward because you have to explicitly import objects like List, but that syntax has the benefit that you can easily pass data into those objects.

And it might be easier than you fear (but maybe not, I'm partially brainstorming here). Imagine something like this:

I setup a spec like this:

p = 5
@contract(x='foo(!p)')
def bar(x):
      pass

The '!' is a standin for some glyph that tells the parser that p should be extracted by name from the scope where @contract is invoked. The inspect module would let you look that up:

import inspect

def a():
     hidden_value = 10
     lookup('hidden_value')

def lookup(arg_name):
      callstack = inspect.getouterframes(inspect.currentframe())
      calling_frame = callstack[1][0]
      loc, glob = calling_frame.f_locals, calling_frame.f_globals
      arg_value = eval(arg_name, loc, glob)
      print "Value of %s is %s" % (arg_name, arg_value)

So the parser could look for variable names to extract, use inspect to look up those variables from the appropriate scope, and then pass those as tokens instead of the string literal !p. The most brittle part of that process is determining how many frames up the callstack to go, which will change as you refactor code. However, getouterframes also returns code snippets, which you could use to search for code like @contract.

I don't know if that interests you at all -- if it does, I could push on this further and put together a proposal PR.

AndreaCensi commented 9 years ago

Ohhh, that's naughty.

What would be the definition of foo in this case?

ChrisBeaumont commented 9 years ago

If I understand the architecture correctly, foo would be a keyword that identifies a Contract subclass which expects an argument in its __init__ -- so much like how isinstance(bar) eventually creates IsInstance('bar'), foo(!p) would eventually create (say) a Foo(value_of_p)

AndreaCensi commented 9 years ago

So the way to implement this would be to add a new contract expression called something like "GlobalVariableRef" which is triggered by "!". Look up classes "VariableRef" and "SimpleRValue".

Then in syntax.py add it to the possible r-values:

number = pi | floatnumber | integer
operand = number | int_variables_ref | misc_variables_ref
operand.setName('r-value')

rvalue << op(operand, [
            ('-', 1, opAssoc.RIGHT, Unary.parse_action),
             ('*', 2, opAssoc.LEFT, Binary.parse_action),
             ('-', 2, opAssoc.LEFT, Binary.parse_action),
             ('+', 2, opAssoc.LEFT, Binary.parse_action),
          ])

This will make it possible to parse expressions like "list[>!p]" or "list[x],x>!p".

Now the tricky part is the binding --- two possibilities: 1) The contract changes if p changes --- in this case we need to look at p during checking: 1a - There's a "context" dict being passed around. Just add two fields "locals" and "globals" for contracts to look at. 1b - Implement a function Contract.get_globals() which returns the list of global variables used by the contract. Then only add those to the context. 2) The contract doesn't change if p changes --- in this case we look at the stack when the contract is parsed. Then, right away, substitute the GlobalVariableRef in the contract's leaves with SimpleRValue instances.

ChrisBeaumont commented 9 years ago

Nice, I can play around with this a bit more.

I was envisioning option 2 -- p is frozen at parse time

AndreaCensi commented 9 years ago

Option 2 seems less harmful --- at least, if there are problems, you catch them when the contract is defined.

ChrisBeaumont commented 9 years ago

This is addressed by #28