Closed ChrisBeaumont closed 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
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 .
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.
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.
Ohhh, that's naughty.
What would be the definition of foo
in this case?
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)
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.
Nice, I can play around with this a bit more.
I was envisioning option 2 -- p is frozen at parse time
Option 2 seems less harmful --- at least, if there are problems, you catch them when the contract is defined.
This is addressed by #28
This is related to #23
I'd like do define a custom contract using something like
That is, I would like to put a parameter
x
in my contract definition, have the value ofx
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 usenew_contract
function/decorator to set up similar contracts, or do I need to build a manual Contract subclass?