AndreaCensi/contracts

Simpler syntax for checking type name

Closed this issue · 5 comments

Suggested by @doctoryes:

One point we've agreed upon here: It'd be nice to not have to specify new_contract('Foo', Foo) for our own classes. Instead, it'd be nice if PyContracts could just search the namespace for the classname when it's specified in a contract.

There is a similar undocumented feature that uses the expression isinstance(classname):

 @contract(x="isinstance(BlockKey)")
 def f(x):
     pass

This checks that the name of the type of x or any of its base classes is BlockKey.

(String matching is good because this way PyContracts doesn't need to import modules or search namespaces.)

The current syntax isinstance(name) is too verbose, and this this is a common enough use case that it deserves a more intuitive syntax. The main reason that this feature is undocumented because I couldn't decide on a good compact syntax.

Some possibilities:

@contract(x="^BlockKey")
@contract(x="$BlockKey")
@contract(x="@BlockKey")

or, in general, <glyph>ClassName.

Some non-possibilities:

This one is intuitive enough but it clashes with "<" used as numerical comparison:

@contract(x="<BlockKey") 

We cannot use simply:

@contract(x="BlockKey")  

because it makes many expressions ambiguous; for example lisst becomes a valid contract (a class named lisst) instead of a mispelling of list.

+1 -- I've started using PyContracts after your excellent Boston Python talk, and this is the main pain point for me so far

I'm currently inclined to use ":" which looks clean enough though I'm not entirely convinced:

 @contract(x=":BlockKey")

If we want to get cute:

 @contract(x="a BlockKey")
 @contract(l="list(a BlockKey)")

The ':' prefix is my favorite of the above. It's similar to the C++ ::ClassName syntax indicating global scope.

The : syntax might get confusing when using docstrings for defining types. Such as:
:type x: :BlockKey

I am wondering if it would be so bad if "lisst" would become a valid contract. During runtime, when checking the contract, the method could then raise a NameError.
Unfortunately that would defer the error from the time of parsing to the execution time, but it might make the syntax more intuitive, i.e. @contract(x=BlockKey) would be the same as @contract(x="BlockKey").

When first using pycontracts I kind of assumed that's how it would work, which unfortunately was not the case.

I'm closing this. The "$" was implemented to refer to external variables, including types. E.g.

from module import MyClass

@contract(x='list($MyClass)')
def f(x):
  pass