RussBaz / enforce

Python 3.5+ runtime type checking for integration testing and data validation
543 stars 21 forks source link

Sequence type and nested types are not handled correctly. #3

Closed RussBaz closed 7 years ago

RussBaz commented 8 years ago

Sequences do not trigger an exception if their contents are of incorrect type, i.e. any sequence is always a sequence and content type checks are ignored. This is a limitations of a current type parser and validator. This should be resolved when a new parser ( #2 ) is implemented.

TheDataLeek commented 8 years ago

I think a related bug is that function types are not handled correctly either. A minimum example is

import typing
import enforce

@enforce.runtime_validation
def foo(func: typing.Callable[[int], str], bar: int) -> str:
    return func(bar)

foo(lambda x: str(x), 5)

According to the typing documentation, that should be the correct syntax, but enforce thinks it's an error.

Edit: Here's the output error

Traceback (most recent call last):
  File "minexample.py", line 8, in <module>
    foo(lambda x: str(x), 5)
  File "/home/william/.local/lib/python3.5/site-packages/enforce/decorators.py", line 44, in wrapper
    raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError: Argument 'func' ('<function <lambda> at 0x7f393cbca620>') was not of type typing.Callable[[int], str]. Actual type was <class 'function'>.

Edit2: Further inspection shows that the reason behind this error is actually built into python.

import typing
type(lambda x: x) == typing.Callable

This returns False. Still digging away at this.

RussBaz commented 8 years ago
>>> import typing
>>> isinstance(lambda x: x, typing.Callable)
True

Anyway, you are doing a great job but I have totally forgotten to mention this in the README:

In the dev branch I implemented a recursive structure but there is no limit on a depth of recursion as it is using generators to avoid hitting this limit. A tree of type checking nodes (each node being a generator) is assigned for every parameter definition. The incoming argument is then passed to its own tree when a function is invoked. Whenever one of them fails, an exception is raised. In addition, it deals with a concept of Type variables by generating a single tree for every unique TypeVar (if any type T is used in more than one parameter, they will all point to the same tree) which has a special property of remembering the exact type passed to it last time. It will raise an exception if a different type is passed at the later stage.

In addition, I thought of evaluating contents of sequence structures lazily, i.e. the sequence is wrapped in a proxy object which will trigger type check on the content only when such content is accessed. In addition, it might be a good idea to check the first available element of every sequence before wrapping it in a proxy. Therefore, if the first one fails, there is no need for any extra overhead associated with the proxy object.

However, I have not yet implemented any special handling for the sequence, dictionary or callable type checks yet. I had enough time to work on this only during some of the major holidays because of my work. Please help me if you can.

I am especially concerned about checking the signature of callable objects. Honestly, I think the only realistic option would be to wrap it in a proxy object with an expected signature but it will prevent a detection of signature mismatch at the moment of passing such object into the function. As you can see, I do not have a working solution for this yet and hence I am very open to suggestions about how to make it better.

Thanks for all the work you did!

TheDataLeek commented 8 years ago

I've made a new issue for this specific problem of dealing with callable objects.

See #6.

RussBaz commented 7 years ago

I think, in its basic form, this bug was resolved. I am closing this issue till a more specific one is raised.