Xion/pyduck — Issue #7: @expects to accept boolean predicates
Author: Xion
Created: 2011-10-16 15:58:13+00:00
State: open

It would seem to be quite useful if @expects was able to check arguments against arbitrary predicates. An obvious example is the following:
```python
@expects(callable)
def decorator(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
# ...
return wrapped
```
versus the cliunky explicit if:
```python
def decorator(func):
if not callable(func):
raise TypeError, "decorator expects a callable"
@functools.wraps(func):
def wrapped(*args, **kwargs):
# ...
return wrapped
```
So it is pretty straightforward. There is, however, one problem now when @expects specifications can be compared against themselves as a part of "does object X implemenets interface Y" check. Such comparison relies on ability to establish an inclusion relation between arguments' constraints (e.g. if a certain type is expected as parameter of interface's method, an object whose method expect a *subtype* is conforming).
In theory, this would be possible in some cases:
```python
@expects(callable)
def decor1(arg):
pass

@expects(inspect.isfunction)
def decor2(arg):
pass
```
We know that constraint imposed on decor2 is tighter than the one enforced on decor1, but there is no way the library could know about that. Therefore, some compromise would have to be struck about what happens when one method has @expects(X, ...) specification and other has @expects(Y, ...), and they are checked for conformance when X or Y is a boolean predicate. Some ideas (assuming the "template" method has X, and the checked method has Y):

1. If Y is boolean predicate, test fails unless X is Any or equal to Y. If X is boolean predicate, test fails unless Y is equal to X.
2. If X is boolean predicate, it is ignored in test (i.e. treated as Any which accepts everything).
3. If X is boolean predicate, Y must also be boolean predicate but doesn't have to be equal to X.
4. If X is boolean predicate, Y must *not* be boolean predicate unless it is equal to X.

I don't exactly feel (2) is good solution because it allows to totally unrelated argument types in checked method to slip past if "template" method has a boolean predicate in particular place.
As for (1), it is logically consistent but discourages usage of boolean predicates in @expects declarations applied on interface methods since once they are used, we're stuck with them (can't even make them more specific later on, like in the callable -> inspect.isfunction example).
(3) mandates usage of boolean predicates once we introduce them and thus may prohibit turning the particular constraint into type/interface check later on. Of course that's not absolute since type/interface check is a boolean predicate in itself, but this would require further workarounds, e.g. introducing a kind of is_(type) predicate that would behave like isinstance but remember the type and cooperate with issubclass when needed).
(4) seems easiest to implement from the get-go but it also prohibit more fine-grained predicates to be introduced further down the road. In this manner it's similar to (1) but slightly less constraining as we might forego using boolean predicates somewhere and we're not stuck with them if we introduces them to our interfaces.

At the end of the day, (2) might actually be sensible for starters. An intended solution would be something which is a modified (3) that operates **only** on boolean predicates everywhere, and where normal type constraints are automatically converted to is_-like predicates.

So for now, let's try to do (2) maybe :)