russbaz / enforce Goto Github PK
View Code? Open in Web Editor NEWPython 3.5+ runtime type checking for integration testing and data validation
Python 3.5+ runtime type checking for integration testing and data validation
Cool project. Here's a minor issue I ran into:
E enforce.exceptions.RuntimeTypeError:
E The following runtime type errors were encountered:
E Return value was not of type typing.Tuple[str, str, datetime.datetime]. Actual type was <class 'tuple'>
Which is misleading since the issue wasn't with the tuple type, but with the contents of the tuple. The actual message should have been something like:
"Return value was not of type Tuple[str, str, dt]. Actual type was Tuple[str, int, dt].
Code that ran into this issue:
@enforce.runtime_validation
def parse_fn(fn: str) -> Tuple[str, str, dt]:
# feed-foo_cust-acme_sen-290_sver-1_letime-20161201T041042_uuid-243c5a95d14f449dbc9b4e62b8a241d9.csv.gz
fn = basename(fn)
try:
kvparts = fn.split('.')[0].split('_')
cust_name = kvparts[1].split('-')[1]
sensor_id = int(kvparts[2].split('-')[1])
insert_dt = dt.strptime(kvparts[4].split('-')[1], '%Y%m%dT%H%M%S')
except IndexError:
raise ValueError('invalid filename: {}'.format(fn))
else:
return cust_name, sensor_id, insert_dt
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.
It would be very practical to be able to enforce PEP484 type validation package/module-wide, typically when the package/module developer thinks it should be enforced for his package/module to work properly.
In this case he could write something in his __init__
file (for packages), or at the end of his python file (for modules), to automatically decorate all symbols of the package/module with PEP484 type validation.
Currently, if one were to enforce typing on a function that took a list as input, and the list was especially nested and/or large, it would iterate/recurse over every object asserting that the typing matches.
For example:
import enforce
@enforce.runtime_validation
def foo(bar: List[int]) -> int:
return bar[0]
foo(range(10000000))
An easy workaround (currently) is to just define the input type as Any
, which prevents further iteration
import enforce
@enforce.runtime_validation
def foo(bar: Any) -> int:
return bar[0]
foo(range(10000000))
This has the downside of losing the type annotations. I propose the following syntax:
import enforce
@enforce.runtime_validation(recursion_limit=0) # not necessarily the name
def foo(bar: List[int]) -> int:
return bar[0]
foo(range(10000000))
Setting the recursion_limit
to zero would mean that just the top level nodes are parsed, while one would mean that the top level nodes and each of their children would be parsed, etc. In the above example, this would mean that we would enforce the type of bar
as a List
, but we wouldn't check the types of its elements (basically turning it from List[int]
to List[Any]
).
This change would have huge benefits for time and space complexity, as currently large arrays are completely parsed.
This change would also require a minor rework of the core decorator in order to use arguments.
The metadata on the [https://pypi.python.org/pypi/enforce](pypi page) says that the enforce
package is licensed under MIT license - but the license requires that there should be a copy of its text included in the repository.
It would be great if such file was included.
Lots of talk going on here about typing. Potentially now is a really good time to showcase what we've got.
How close would you say we are to production release? It's my current understanding that we cover probably 90% of typing use cases, with some uncommon cases not quite fully supported.
I'm running into odd issues with threading in which it appears that arguements and results are being replaced with None.
It runs fine with enforce with a single worker, and runs with with multiple workers and enforce disabled. But consistently breaks with multiple workers and enforce enabled.
Here's the code:
#!/usr/bin/env python3.5
import os, sys, concurrent.futures, enforce
from typing import Union, Any, List, Tuple
try:
WORKERS = int(sys.argv[1])
ENFORCE_ENABLED = True if sys.argv[2] == 'true' else False
ASSERT_ENABLED = True if sys.argv[3] == 'true' else False
except:
raise ValueError('invalid args, should be: \n'
' arg 1 (int) = number of workers,\n'
' arg 2 (true or false) = enforce enabling,\n'
' arg 3 (true or false) = assertions enabling')
enforce.config({'enabled': ENFORCE_ENABLED})
def main():
for i in range(100):
test_runner(100)
print('.', end='', flush=True)
print('test success')
def test_runner(widget_number):
widgets = [ x for x in range(widget_number) ]
with concurrent.futures.ThreadPoolExecutor(max_workers=WORKERS) as executor:
for score in executor.map(test_one_widget,
widgets):
if ASSERT_ENABLED:
assert isinstance(score, int)
@enforce.runtime_validation
def test_one_widget(widget_id: int) -> int:
score = widget_inspector(widget_id, a='foo', b=4, c='bar')
return score
@enforce.runtime_validation
def widget_inspector(widget_id: int, a: str, b: int, c: str) -> int:
if ASSERT_ENABLED:
assert isinstance(widget_id, int)
assert isinstance(a, str)
assert isinstance(b, int)
assert isinstance(c, str)
return b
if __name__ == '__main__':
main()
Test results with 1 worker or multiple workers but enforce off:
(enforce):~/Envs/enforce/enforcechk$ ./threading_test.py 1 false true
....................................................................................................test success
(enforce):~/Envs/enforce/enforcechk$ ./threading_test.py 1 true true
....................................................................................................test success
(enforce):~/Envs/enforce/enforcechk$ ./threading_test.py 40 false true
....................................................................................................test success
Test results with multiple workers and enforce on:
(enforce) kenfar@darth560:~/Envs/enforce/enforcechk$ ./threading_test.py 40 true true
..Traceback (most recent call last):
File "./threading_test.py", line 47, in <module>
main()
File "./threading_test.py", line 19, in main
test_runner(100)
File "./threading_test.py", line 28, in test_runner
widgets):
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 556, in result_iterator
yield future.result()
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 398, in result
return self.__get_result()
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 357, in __get_result
raise self._exception
File "/usr/lib/python3.5/concurrent/futures/thread.py", line 55, in run
result = self.fn(*self.args, **self.kwargs)
File "/home/kenfar/Envs/enforce/lib/python3.5/site-packages/enforce/decorators.py", line 105, in universal
result = wrapped(*_args, **_kwargs)
File "./threading_test.py", line 34, in test_one_widget
score = widget_inspector(widget_id, a='foo', b=4, c='bar')
File "/home/kenfar/Envs/enforce/lib/python3.5/site-packages/enforce/decorators.py", line 105, in universal
result = wrapped(*_args, **_kwargs)
File "./threading_test.py", line 42, in widget_inspector
assert isinstance(b, int)
AssertionError
(enforce) kenfar@darth560:~/Envs/enforce/enforcechk$ ./threading_test.py 40 true true
......Traceback (most recent call last):
File "./threading_test.py", line 47, in <module>
main()
File "./threading_test.py", line 19, in main
test_runner(100)
File "./threading_test.py", line 28, in test_runner
widgets):
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 556, in result_iterator
yield future.result()
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 398, in result
return self.__get_result()
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 357, in __get_result
raise self._exception
File "/usr/lib/python3.5/concurrent/futures/thread.py", line 55, in run
result = self.fn(*self.args, **self.kwargs)
File "/home/kenfar/Envs/enforce/lib/python3.5/site-packages/enforce/decorators.py", line 111, in universal
return enforcer.validate_outputs(result)
File "/home/kenfar/Envs/enforce/lib/python3.5/site-packages/enforce/enforcers.py", line 98, in validate_outputs
raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError:
The following runtime type errors were encountered:
Return value was not of type <class 'int'>. Actual type was NoneType.
(enforce) kenfar@darth560:~/Envs/enforce/enforcechk$ ./threading_test.py 40 true false
..................Traceback (most recent call last):
File "./threading_test.py", line 47, in <module>
main()
File "./threading_test.py", line 19, in main
test_runner(100)
File "./threading_test.py", line 28, in test_runner
widgets):
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 556, in result_iterator
yield future.result()
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 398, in result
return self.__get_result()
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 357, in __get_result
raise self._exception
File "/usr/lib/python3.5/concurrent/futures/thread.py", line 55, in run
result = self.fn(*self.args, **self.kwargs)
File "/home/kenfar/Envs/enforce/lib/python3.5/site-packages/enforce/decorators.py", line 105, in universal
result = wrapped(*_args, **_kwargs)
File "./threading_test.py", line 34, in test_one_widget
score = widget_inspector(widget_id, a='foo', b=4, c='bar')
File "/home/kenfar/Envs/enforce/lib/python3.5/site-packages/enforce/decorators.py", line 97, in universal
_args, _kwargs, _ = enforcer.validate_inputs(parameters)
File "/home/kenfar/Envs/enforce/lib/python3.5/site-packages/enforce/enforcers.py", line 86, in validate_inputs
raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError:
The following runtime type errors were encountered:
Argument 'widget_id' was not of type <class 'int'>. Actual type was NoneType.
The results show failures in various areas running the same code. Sometimes caught by enforce, othertimes caught by assertions.
Run the following code:
import enforce
class Foo:
@staticmethod
def fun() -> None:
print(42)
class Bar(Foo):
@staticmethod
def fun() -> None:
print(84)
@enforce.runtime_validation
def meta(x: Foo) -> None:
x.fun()
meta(Foo())
meta(Bar())
This errors out with:
42
Traceback (most recent call last):
File "/tmp/test.py", line 19, in <module>
meta(Bar())
File "/home/marcin/venv3/golem/lib/python3.6/site-packages/enforce/decorators.py", line 104, in universal
_args, _kwargs, _ = enforcer.validate_inputs(parameters)
File "/home/marcin/venv3/golem/lib/python3.6/site-packages/enforce/enforcers.py", line 86, in validate_inputs
raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError:
The following runtime type errors were encountered:
Argument 'x' was not of type <class '__main__.Foo'>. Actual type was Bar.
The code is validated properly by mypy --strict
.
There is a need for a new type parser and validator to handle recursive type checks for container types as well as cross checking for type variables.
Hey there, I found this nice bug while developing valid8
With enforce 0.3.4,
@runtime_validation
class Foo:
def __init__(self, fun: Callable):
self.fun = fun
a = Foo(next)
assert a.fun.__name__ == 'next' # => raises an AssertionError
Indeed a.fun
does not hold your callable, but a proxy to it. This could be ok, but that proxy does not act as a transparent decorator. I would recommend using the very nice decorator library, which I use in valid8 and autoclass
Hi there.
You seems to be quite reactive (#39) so I'll bother you with a nitpick as I was testing a couple of type-checking libraries the other day.
Here is the simple script:
from typing import Tuple, Dict
from enforce import runtime_validation
@runtime_validation
def example1(param: Tuple[int, int, int]) -> None:
return None
@runtime_validation
def example2(param: Dict[str, int]) -> None:
return None
example1((42, 42)) # Argument 'param' was not of type typing.Tuple[int, int, int]. Actual type was tuple.
example2({42: 42}) # Argument 'param' was not of type typing.Dict[str, int]. Actual type was typing.Dict[int, int].
As you can see with the comments, for example2
/dict
the error message is quite clear.
For example1
/tuple
the message lacks a bit of precision.
It could be nice to introspect the passed tuple here but it's not clear for me why nodes.TupleNode.validate_data
uses input_type.__name__
as type_name
instead of relying on something like get_actual_data_type
.
Hi there, I got some typing errors and I'm not sure where the problems came from, could you give me your thoughts about the following examples?
Feel free to close the issue if there is nothing wrong on enforce's side.
Version used, pipe show enforce
:
Name: enforce
Version: 0.3.4
...
In this case, there is no problem if the callback is not a bound method.
Code:
from typing import Optional, Callable, Any, Dict
import enforce
class Example:
@enforce.runtime_validation
def method(self, d: Dict):
pass
@enforce.runtime_validation
def bound_callback_example(callback: Optional[Callable[[Dict], Any]]=None):
pass
example = Example()
bound_callback_example(example.method)
This code may raise two different tracebacks, the difference between the two being the type of the function:
Traceback (most recent call last):
File "enforce_examples.py", line 14, in <module>
bound_callback_example(example.method)
File ".../venv/lib/python3.6/site-packages/enforce/decorators.py", line 104, in universal
_args, _kwargs, _ = enforcer.validate_inputs(parameters)
File ".../venv/lib/python3.6/site-packages/enforce/enforcers.py", line 86, in validate_inputs
raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError:
The following runtime type errors were encountered:
Argument 'callback' was not of type typing.Union[typing.Callable[[typing.Dict], typing.Any], NoneType]. Actual type was BoundFunctionWrapper.
Traceback (most recent call last):
File "enforce_examples.py", line 14, in <module>
bound_callback_example(example.method)
File ".../venv/lib/python3.6/site-packages/enforce/decorators.py", line 104, in universal
_args, _kwargs, _ = enforcer.validate_inputs(parameters)
File ".../venv/lib/python3.6/site-packages/enforce/enforcers.py", line 86, in validate_inputs
raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError:
The following runtime type errors were encountered:
Argument 'callback' was not of type typing.Union[typing.Callable[[typing.Dict], typing.Any], NoneType]. Actual type was typing.Callable.
The errors go away when the callback
type is less strict:
@enforce.runtime_validation
def bound_callback_example(callback: Optional[Callable]=None):
pass
Is this the expected behavior?
The traceback makes me think this may be an error on enforce's side.
Code:
from typing import Generator
import enforce
@enforce.runtime_validation
def generator() -> Generator[int, None, None]:
i = 0
while True:
yield i
i += 1
g = generator()
print(next(g))
print(next(g))
Traceback:
Traceback (most recent call last):
File ".../venv/lib/python3.6/site-packages/enforce/nodes.py", line 539, in preprocess_data
enforcer = data.__enforcer__
AttributeError: 'generator' object has no attribute '__enforcer__'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "enforce_examples.py", line 11, in <module>
g = generator()
File ".../venv/lib/python3.6/site-packages/enforce/decorators.py", line 118, in universal
return enforcer.validate_outputs(result)
File ".../venv/lib/python3.6/site-packages/enforce/enforcers.py", line 96, in validate_outputs
if not self.validator.validate(output_data, 'return'):
File ".../venv/lib/python3.6/site-packages/enforce/validator.py", line 26, in validate
validation_result = visit(validation_tree)
File ".../venv/lib/python3.6/site-packages/enforce/utils.py", line 17, in visit
stack.append(last.send(last_result))
File ".../venv/lib/python3.6/site-packages/enforce/nodes.py", line 64, in validate
clean_data = self.preprocess_data(validator, data)
File ".../venv/lib/python3.6/site-packages/enforce/nodes.py", line 541, in preprocess_data
return GenericProxy(data)
File ".../venv/lib/python3.6/site-packages/enforce/enforcers.py", line 130, in __init__
raise TypeError('Only generics can be wrapped in GenericProxy')
TypeError: Only generics can be wrapped in GenericProxy
There is a similar problem when a function returns a Coroutine
.
from typing import Optional, Tuple
from enforce import runtime_validation
@runtime_validation
def foo(arg: Optional[Tuple[str, str]] = None):
pass
foo()
Yields TypeError: 'NoneType' object is not iterable
. Note that the result is the same when the argument has no default value and None
is explicitly passed.
Trying to dig-in the source it seems that at some point the None
value is propagated to the Tuple
node, which might be ok. I will provide a pull request with a fix.
What are the performance considerations for users wishing to use this in a production environment?
Version used:
pip show enforce
Name: enforce
Version: 0.3.3
...
Simplest reproducible example:
from typing import Optional
from enforce import config, runtime_validation
config({'mode': 'covariant'})
@runtime_validation
def example() -> Optional[str]:
return None
example()
Note that if I comment out the config
call, it works as expected.
Of course, the covariant mode isn't useful here, but I do need it in a larger script.
The traceback itself, for completeness:
Traceback (most recent call last):
File "example.py", line 10, in <module>
example()
File ".../venv/lib/python3.6/site-packages/enforce/decorators.py", line 118, in universal
return enforcer.validate_outputs(result)
File ".../venv/lib/python3.6/site-packages/enforce/enforcers.py", line 96, in validate_outputs
if not self.validator.validate(output_data, 'return'):
File ".../venv/lib/python3.6/site-packages/enforce/validator.py", line 26, in validate
validation_result = visit(validation_tree)
File ".../venv/lib/python3.6/site-packages/enforce/utils.py", line 17, in visit
stack.append(last.send(last_result))
File ".../venv/lib/python3.6/site-packages/enforce/nodes.py", line 54, in validate
self_validation_result = self.validate_data(validator, clean_data, force)
File ".../venv/lib/python3.6/site-packages/enforce/nodes.py", line 210, in validate_data
result = is_type_of_type(input_type, expected_data_type, covariant=covariant, contravariant=contravariant)
File ".../venv/lib/python3.6/site-packages/enforce/types.py", line 206, in is_type_of_type
subclass_check = perform_subclasscheck(data, data_type, covariant, contravariant)
File ".../venv/lib/python3.6/site-packages/enforce/types.py", line 234, in perform_subclasscheck
result = result or data_type.__subclasscheck__(reversed_data)
TypeError: issubclass() arg 1 must be a class
While Python 3.6 is in beta we're changing the way typing.py represents types. I can't make any promises that it'll stay stable until the 4th beta (after all PEP 484 is still provisional) but I figured I should give you a heads up that we're changing things around. You can follow the fun in our repo: https://github.com/python/typing/
from typing import Union, List
from enforce.decorators import runtime_validation
I18ableMessage = List[str]
I18ableMessages = Union[str,List[I18ableMessage]]
@runtime_validation
def test(msg: I18ableMessages):
print(msg)
test([["a","b"],["x","y"]])
Traceback (most recent call last):
File "test.py", line 11, in <module>
test([["a","b"],["x","y"]])
File "/usr/local/lib/python3.5/dist-packages/enforce/decorators.py", line 109, in universal
_args, _kwargs, _ = enforcer.validate_inputs(parameters)
File "/usr/local/lib/python3.5/dist-packages/enforce/enforcers.py", line 77, in validate_inputs
if not self.validator.validate(argument, name):
File "/usr/local/lib/python3.5/dist-packages/enforce/validator.py", line 26, in validate
result = visit(validation_tree)
File "/usr/local/lib/python3.5/dist-packages/enforce/utils.py", line 17, in visit
stack.append(last.send(last_result))
File "/usr/local/lib/python3.5/dist-packages/enforce/nodes.py", line 84, in validate_children
result = yield child.validate(propagated_data[i], validator, self.type_var)
IndexError: list index out of range
As requested, I am opening this issue instead of #44.
Seen with enforce 0.3.4
.
Example code with a generator:
from typing import Generator
import enforce
@enforce.runtime_validation
def generator() -> Generator[int, None, None]:
i = 0
while True:
yield i
i += 1
g = generator()
print(next(g))
print(next(g))
An exception will occur in enforce:
Traceback (most recent call last):
File ".../venv/lib/python3.6/site-packages/enforce/nodes.py", line 539, in preprocess_data
enforcer = data.__enforcer__
AttributeError: 'generator' object has no attribute '__enforcer__'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "enforce_examples.py", line 11, in <module>
g = generator()
File ".../venv/lib/python3.6/site-packages/enforce/decorators.py", line 118, in universal
return enforcer.validate_outputs(result)
File ".../venv/lib/python3.6/site-packages/enforce/enforcers.py", line 96, in validate_outputs
if not self.validator.validate(output_data, 'return'):
File ".../venv/lib/python3.6/site-packages/enforce/validator.py", line 26, in validate
validation_result = visit(validation_tree)
File ".../venv/lib/python3.6/site-packages/enforce/utils.py", line 17, in visit
stack.append(last.send(last_result))
File ".../venv/lib/python3.6/site-packages/enforce/nodes.py", line 64, in validate
clean_data = self.preprocess_data(validator, data)
File ".../venv/lib/python3.6/site-packages/enforce/nodes.py", line 541, in preprocess_data
return GenericProxy(data)
File ".../venv/lib/python3.6/site-packages/enforce/enforcers.py", line 130, in __init__
raise TypeError('Only generics can be wrapped in GenericProxy')
TypeError: Only generics can be wrapped in GenericProxy
There is a similar problem when a function returns a Coroutine
.
This seems normal for now as there isn't any real support for these objects.
I'm attempting to use enforce
over a class. However, one of the methods in this class is static, and marked so by:
@staticmethod
When attempting to run, i get the following:
In [7]: Keyboard._parse_modifier(2)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-7-2977a0e8d3a6> in <module>()
----> 1 Keyboard._parse_modifier(2)
~/.virtualenvs/usb_pcap/lib/python3.5/site-packages/enforce/decorators.py in universal(wrapped, instance, args, kwargs)
83 """
84 with RunLock:
---> 85 enforcer = wrapped.__enforcer__
86 skip = False
87
AttributeError: 'functools.partial' object has no attribute '__enforcer__'
Same error occurs if i remove my type hints.
Any idea what that might mean?
argument 'pinfo' was not of type <class 'sos.PathInfo'>. Actual type was untyped PathInfo
We are in sos.py
with a class PathInfo
.
< Release plan discussion goes here >
The question says it all.
I will try fixing the backward compatibility with those versions, but I think I should drop it as soon as it starts giving me too much headache.
A 'Generic' is any mutable object which is a subclass of typing.Generic
.
Parametrised Generic is a Generic which accepts one or more type parameters. These parameters must be instances of typing.TypeVar
.
Restricted Generic is a Parametrised Generic class that specifies which types it accepts and at least one of the required types must be more specific than typing.Any
.
Typed Generic is a Generic which has runtime type checking applied to it.
Type Safe Method/Function is any method/function with a runtime type checking applied to it.
A Proxy object is an object which wraps another mutable object and proxies all its attribute accesses to the wrapped object. If a proxy was defined with an attribute named identically to the wrapped one, then the proxy attribute will be accessed instead. In all other cases it should mimic the wrapped object as much as possible.
A Generic Proxy is a proxy object for a type safe access to the type un-safe Generic.
Safe Generic is either an instance of a typed generic or a generic proxy.
Enforcing generic type safety means ensuring that only safe generics are acceptable. If any non-typed restricted generics are encountered, then they must be silently wrapped in a generic proxy.
Methods, lambdas and functions can be collectively called callables.
Type Safe Callables (or just safe callables) are callables with a runtime type checking applied.
Only callables can have a type safety checking.
If a safe callable expects a restricted generic anywhere in its input, then a generic type safety must be enforced on such input.
If a safe callable expects to return a restricted generic anywhere in its output data, then it also must enforce a generic type safety on its output.
TypeVars are invariant by default (PEP 484).
Parameters of Generics are always TypeVars (PEP 484).
Immutable types must always be treated invariantly.
TypeVars can be covariant, contravariant, both or neither (PEP 484).
Mutable types are covariant unless they are parametrised generics.
Parametrised generics are evaluated against their own type invariantly and against their type parameters according to their definition.
PyPi does not support markdown descriptions.
There are two options:
Which one to choose . . .
What should we do about this? I am a bit unsure where to fit this in the current model.
Should this hook replace the entire inheritance check? Should this work only in covariant mode? Or should this be used for all cases other than invariant and expect users to know about this?
Hey, I just discovered this via pycoder's weekly ๐
It'd be nice to have some way to run the type checks automatically on any function call. I'm sure this will be slow as hell, but it'd be nice to run the tests with it once in a while, to discover things a static checker might not discover.
I'm not sure what the best way to implement this would be - sys.settrace
and bytecode rewriting come to mind, but I'm not sure how hard this is...
Do you support forward references? I'm getting this behavior:
import enforce
import typing
MyType = typing.List[typing.Union[int, 'MyType']]
@enforce.runtime_validation()
def foo(x: MyType):
pass
foo([1, 2]) # Good
foo([1, [2]]) # RuntimeTypeError
The error states: Argument 'x' was not of type typing.List<~T>[typing.Union[int, _ForwardRef('MyType')]]. Actual type was typing.List[int, typing.List].
Are forward references not supported, is this a bug, or am I doing something wrong? Thanks :)
Continuation of Issue #36 - threading issues.
So, does anyone know how to make it work with multiprocessing? I cannot grasp where exactly the issue is arising. Is it because of 'wrapt'?
Working with 0.3.1, installed from PyPI
Example code:
import enforce from typing import Set @enforce.runtime_validation def test() -> Set[int]: return set([1,2,3]) test()
Error:
Traceback (most recent call last): File "/tmp/x/py35/venv/lib/python3.5/site-packages/enforce/nodes.py", line 403, in preprocess_data enforcer = data.__enforcer__ AttributeError: 'list' object has no attribute '__enforcer__' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "test.py", line 9, in test() File "/tmp/x/py35/venv/lib/python3.5/site-packages/enforce/decorators.py", line 123, in universal return enforcer.validate_outputs(result) File "/tmp/x/py35/venv/lib/python3.5/site-packages/enforce/enforcers.py", line 95, in validate_outputs if not self.validator.validate(output_data, 'return'): File "/tmp/x/py35/venv/lib/python3.5/site-packages/enforce/validator.py", line 26, in validate result = visit(validation_tree) File "/tmp/x/py35/venv/lib/python3.5/site-packages/enforce/utils.py", line 17, in visit stack.append(last.send(last_result)) File "/tmp/x/py35/venv/lib/python3.5/site-packages/enforce/nodes.py", line 45, in validate clean_data = self.preprocess_data(validator, data) File "/tmp/x/py35/venv/lib/python3.5/site-packages/enforce/nodes.py", line 405, in preprocess_data return GenericProxy(data) File "/tmp/x/py35/venv/lib/python3.5/site-packages/enforce/enforcers.py", line 129, in __init__ raise TypeError('Only generics can be wrapped in GenericProxy') TypeError: Only generics can be wrapped in GenericProxy
If we change Set to List then it works fine though:
import enforce from typing import List @enforce.runtime_validation def test() -> List[int]: return [1,2,3] test()
See the wiki page: https://github.com/RussBaz/enforce/wiki/Configuration
In a nutshell, the built in functionality with wrapt lets us disable these checks and skip over them, however since this happens at the bytecode level it needs to occur right as the file is read.
This behavior isn't ideal, as we would like to be able to dynamically disable functions based on possibly arguments, or other things. This currently isn't possible...
This is a low priority issue, however we still need to keep track of it. (I'll try to get to it soon, but if someone beats me to it that would also be great.)
I have
from enforce import runtime_validation
class A(object):
pass
class B(A):
pass
class C(B):
pass
@runtime_validation
def fct(a: A):
pass
c= C()
fct(c)
and I get Argument 'a' was not of type <class '__main__.A'>. Actual type was C.
I was expecting this to work (c is an instance of A). Is it a bug or did I miss something?
On dev
branch.
from typing import Any, List
import enforce
@enforce.runtime_validation
def foo(a: Any) -> Any:
return 10
foo([10,20])
will result in
enforce.exceptions.RuntimeTypeError:
The following runtime type errors were encountered:
Argument 'a' was not of type typing.Any. Actual type was typing.List[int].
Any
should match against a List[int]
, I believe.
All the types are currently checked invariantly (i.e. it has to be the same as specified or it fails otherwise). This sometimes might not what our users expect. I think we should implement a global switch to change the type checking mode.
This switch should be able to specify whether type checking is: invariant, covariant, contravariant or bivariant.
It would be nice to be able to configure some basic settings globally, rather than having to set them on a per-function basis. This would extend #7 and #4.
Possible syntax is
# set recursion limit to 10
# do not disable
enforce.config(recursion_limit=10, disable=False)
This would allow to build programs where type checking can be enabled through command line arguments.
I'm not sure how the code to implement this would work, but I can start looking into it. It might not also hurt to have "tagged" functions so that you can configure groups of functions at a time. For example
import enforce
from enforce import runtime_validation
@runtime_validation(group='foo')
def foo1(a:str)->str:
return a
@runtime_validation(group='foo')
def foo2(a:str)->str:
return a
@runtime_validation(group='bar')
def bar(a:str)->str:
return a
# Disable all "foo" group checks
enforce.config(disable=False)
enforce.group('foo', disable=True)
I'm open to suggestions on syntax, especially for the function group config.
I think a great feature would be the option to attempt a typecast if @enforce.runtime_validation
fails, either as a separate decorator @enforce.runtime_typecasting
or as an optional parameter @enforce.runtime_validation(coerce=True)
A simple example might be
@enforce.runtime_typecasting
def elongator(s: str):
return s * 10
...
elongator(5)
# returns '5555555555'
For the simple primitives, this could be a simple task. For more complicated objects, an attempt to pass the argument itself to the required type as a single arg, as *args
, and as **kwargs
could be attempted in sequence.
class SimpleClass:
def __init__(self, x: int):
self.value = x
@enforce.runtime_typecasting
def SillySimpleClassFactory(x: SimpleClass):
return x
...
SillySimpleClassFactory(10)
# returns SimpleClass(10)
class ComplicatedClass:
def __init__(self, first_arg, second_arg, third_arg):
...
...
@enforce.runtime_typecasting
def ComplicatedClassFactory(x: ComplicatedClass):
return x
ComplicatedClassFactory((1, 2, 3))
# returns ComplicatedClass(1, 2, 3)
ComplicatedClassFactory({'first_arg' : 4, 'third_arg' : 6, 'second_arg' : 5})
# returns ComplicatedClass(4, 5, 6)
I imagine Callable
wouldn't be properly supported. Probably others.
I would be happy to contribute this addition myself in a month or two.
First, thanks for this awesome project!
Can this project be used with typing.NamedTuple
? I tried this but I couldn't get it to work.
import enforce
import typing
MyNamedTuple = typing.NamedTuple('MyNamedTuple', [('my_int', int)])
MyNamedTuple = enforce.runtime_validation(MyNamedTuple)
# I assumed enforce.runtime_validation would work on any callable
MyNamedTuple(5) # For me it crashes here already on a "missing a required argument: 'my_int'"
MyNamedTuple(my_int=5) # Now it gives a "missing a required argument: '_cls'", I give up
MyNamedTuple(my_int='hello') # I never got here
I use version typing=0.3.1 and python=3.5.2.
This works fine with Python 3.5:
import enforce from typing import List @enforce.runtime_validation def test() -> List[int]: return [1,2,3] test()
But under Python 3.6 it has this error:
File "test.py", line 1, in import enforce File "/tmp/x/py36/venv/lib/python3.6/site-packages/enforce/__init__.py", line 1, in from .decorators import runtime_validation File "/tmp/x/py36/venv/lib/python3.6/site-packages/enforce/decorators.py", line 9, in from .enforcers import apply_enforcer, Parameters, GenericProxy File "/tmp/x/py36/venv/lib/python3.6/site-packages/enforce/enforcers.py", line 7, in from .types import EnhancedTypeVar, is_type_of_type File "/tmp/x/py36/venv/lib/python3.6/site-packages/enforce/types.py", line 5, in from typing import Optional, Union, UnionMeta, Any, TypeVar, Tuple, Generic ImportError: cannot import name 'UnionMeta'
As requested, I am opening this issue instead of #44.
Seen with enforce 0.3.4
.
Example code where a bound method is used:
from typing import Optional, Callable, Any, Dict
import enforce
class Example:
@enforce.runtime_validation
def method(self, d: Dict):
pass
@enforce.runtime_validation
def bound_callback_example(callback: Optional[Callable[[Dict], Any]]=None):
pass
example = Example()
bound_callback_example(example.method)
Funnily two different exceptions may be raised by the above code:
Traceback (most recent call last):
File "enforce_examples.py", line 14, in <module>
bound_callback_example(example.method)
File ".../venv/lib/python3.6/site-packages/enforce/decorators.py", line 104, in universal
_args, _kwargs, _ = enforcer.validate_inputs(parameters)
File ".../venv/lib/python3.6/site-packages/enforce/enforcers.py", line 86, in validate_inputs
raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError:
The following runtime type errors were encountered:
Argument 'callback' was not of type typing.Union[typing.Callable[[typing.Dict], typing.Any], NoneType]. Actual type was BoundFunctionWrapper.
Traceback (most recent call last):
File "enforce_examples.py", line 14, in <module>
bound_callback_example(example.method)
File ".../venv/lib/python3.6/site-packages/enforce/decorators.py", line 104, in universal
_args, _kwargs, _ = enforcer.validate_inputs(parameters)
File ".../venv/lib/python3.6/site-packages/enforce/enforcers.py", line 86, in validate_inputs
raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError:
The following runtime type errors were encountered:
Argument 'callback' was not of type typing.Union[typing.Callable[[typing.Dict], typing.Any], NoneType]. Actual type was typing.Callable.
For now, the problem can be avoided by using a less strict type for the callback
, to the detriment of self-documenting code and pushing the burden of type verification to the callable itself (if needed):
@enforce.runtime_validation
def bound_callback_example(callback: Optional[Callable]=None):
pass
I get an NameError
when I try to use ABCs from the numbers
module, e.g.
from typing import NamedTuple, Text, Tuple
from numbers import Integral
Interval = Tuple[Integral, Integral]
Annotation = NamedTuple("Annotation", [("source", Text), ("start", Integral),
("end", Integral), ("text", Text),
("cls", Text)])
Using these annotations in any function gives me:
Traceback (most recent call last):
File "/Applications/PyCharm.app/Contents/helpers/pydev/pydevd.py", line 1599, in <module>
globals = debugger.run(setup['file'], None, None, is_module)
File "/Applications/PyCharm.app/Contents/helpers/pydev/pydevd.py", line 1026, in run
pydev_imports.execfile(file, globals, locals) # execute the script
File "/Applications/PyCharm.app/Contents/helpers/pydev/_pydev_imps/_pydev_execfile.py", line 18, in execfile
exec(compile(contents+"\n", file, 'exec'), glob, loc)
File "/Applications/PyCharm.app/Contents/helpers/pycharm/docrunner.py", line 297, in <module>
modules = [loadSource(a[0])]
File "/Applications/PyCharm.app/Contents/helpers/pycharm/docrunner.py", line 229, in loadSource
module = imp.load_source(moduleName, fileName)
File "/Users/ilia/.venvs/py3/lib/python3.5/imp.py", line 172, in load_source
module = _load(spec)
File "<frozen importlib._bootstrap>", line 693, in _load
File "<frozen importlib._bootstrap>", line 673, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 665, in exec_module
File "<frozen importlib._bootstrap>", line 222, in _call_with_frames_removed
File "/Users/ilia/OneDrive/GitHub/skoblov-lab/chempred/chempred/chemdner.py", line 52, in <module>
-> List[Tuple[int, List[Annotation], List[Annotation]]]:
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/decorators.py", line 58, in runtime_validation
return generate_decorated()
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/decorators.py", line 158, in build_wrapper
return decorate(wrapped, configuration, None)
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/decorators.py", line 70, in decorate
data = apply_enforcer(data, parent_root=parent_root, settings=configuration)
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/enforcers.py", line 159, in apply_enforcer
func.__enforcer__ = generate_new_enforcer(func, generic, parent_root, instance_of, settings)
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/enforcers.py", line 227, in generate_new_enforcer
validator = init_validator(hints, parent_root)
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/validator.py", line 71, in init_validator
syntax_tree = visit(root_parser)
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/utils.py", line 17, in visit
stack.append(last.send(last_result))
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/parsers.py", line 54, in _parse_namedtuple
new_node = yield nodes.NamedTupleNode(hint)
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/nodes.py", line 390, in __init__
super().__init__(runtime_validation(data_type), is_sequence=True, is_container=True, **kwargs)
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/decorators.py", line 46, in runtime_validation
return get_typed_namedtuple(configuration, data, fields, field_types)
File "/Users/ilia/.venvs/py3/lib/python3.5/site-packages/enforce/decorators.py", line 180, in get_typed_namedtuple
exec(new_init_template, context)
File "<string>", line 1, in <module>
NameError: name 'Integral' is not defined
Callables are not currently working, as referenced in #3.
Thinking about callable objects, I think the best way to handle them is as follows.
Let's say we have some function that takes another function as an argument with correct type hinting syntax.
def foo(func: Callable[[int, int], float], s: str) -> str:
return s + str(func(5, 5))
I think that the best way to handle this is to also require that the argument passed in func
needs to also have complete type annotation.
def func(x: int, y: int) -> float:
return float(x * y)
With func
possibly also checked at runtime depending on whether or not the enforce.runtime_validation
decorator is applied. The only thing that the high level function foo
checks is that the call signature of func
matches the type hinting specified.
How's that sound?
I use the Sequence
generic ABC in my codebase a lot, but runtime_validation
doesn't seem to handle it. Here is a small reproducible example:
from typing import Sequence, List
from enforce import runtime_validation
@runtime_validation
def add_one(arg: Sequence[int]) -> List[int]:
return [arg + 1]
add_one(range(10))
The traceback:
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
~/.venvs/py3/lib/python3.5/site-packages/enforce/nodes.py in preprocess_data(self, validator, data)
538 try:
--> 539 enforcer = data.__enforcer__
540 except AttributeError:
AttributeError: 'range' object has no attribute '__enforcer__'
During handling of the above exception, another exception occurred:
TypeError Traceback (most recent call last)
<ipython-input-4-5dfdb081f76a> in <module>()
----> 1 add_one(range(10))
~/.venvs/py3/lib/python3.5/site-packages/enforce/decorators.py in universal(wrapped, instance, args, kwargs)
102
103 # First, check argument types (every key not labelled 'return')
--> 104 _args, _kwargs, _ = enforcer.validate_inputs(parameters)
105
106 if instance_method:
~/.venvs/py3/lib/python3.5/site-packages/enforce/enforcers.py in validate_inputs(self, input_data)
76 if name != 'return':
77 argument = binded_arguments.arguments.get(name)
---> 78 if not self.validator.validate(argument, name):
79 break
80 binded_arguments.arguments[name] = self.validator.data_out[name]
~/.venvs/py3/lib/python3.5/site-packages/enforce/validator.py in validate(self, data, param_name)
24 validation_tree = hint_validator.validate(data, self)
25
---> 26 validation_result = visit(validation_tree)
27
28 self.data_out[param_name] = self.roots[param_name].data_out
~/.venvs/py3/lib/python3.5/site-packages/enforce/utils.py in visit(generator)
15 last = stack[-1]
16 if isinstance(last, typing.Generator):
---> 17 stack.append(last.send(last_result))
18 last_result = None
19 else:
~/.venvs/py3/lib/python3.5/site-packages/enforce/nodes.py in validate(self, data, validator, force)
62
63 # 1
---> 64 clean_data = self.preprocess_data(validator, data)
65
66 # 2
~/.venvs/py3/lib/python3.5/site-packages/enforce/nodes.py in preprocess_data(self, validator, data)
539 enforcer = data.__enforcer__
540 except AttributeError:
--> 541 return GenericProxy(data)
542 else:
543 covariant = self.covariant or validator.settings.covariant
~/.venvs/py3/lib/python3.5/site-packages/enforce/enforcers.py in __init__(self, wrapped)
128 apply_enforcer(self, generic=True)
129 else:
--> 130 raise TypeError('Only generics can be wrapped in GenericProxy')
131
132 def __call__(self, *args, **kwargs):
TypeError: Only generics can be wrapped in GenericProxy
Hmmm. I would like a way to disable these checks globally. For example with a configured variable or by invoking python with the optimize flag (python -o). I don't see any switches in the code. Is this possible?
I have had this obscure problem, which I would call a bug since one would expect to get a proper error message like Python 3 normally does.
I use
Python 3.6.1
andenforce==0.3.4
class A:
def hello():
print("Hello, World!")
a = A()
# Traceback (most recent call last):
# File "example.py", line 13, in <module>
# a.hello()
# File "example.py", line 4, in hello
# print("Hello, " + b.world())
# TypeError: world() takes 0 positional arguments but 1 was given
try:
a.hello()
except TypeError:
print("Error caught correctly")
# But now
import enforce
@enforce.runtime_validation
class BadA:
def hello():
print("Hello, ")
a = BadA()
a.hello()
will output
Error caught correctly
Traceback (most recent call last):
File "example.py", line 30, in <module>
a.hello()
File "/Users/XXX/enforce-test/env/lib/python3.6/site-packages/enforce/decorators.py", line 104, in universal
_args, _kwargs, _ = enforcer.validate_inputs(parameters)
File "/Users/XXX/enforce-test/env/lib/python3.6/site-packages/enforce/enforcers.py", line 71, in validate_inputs
binded_arguments = self.signature.bind(*args, **kwargs)
File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/inspect.py", line 2933, in bind
return args[0]._bind(args[1:], kwargs)
File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/inspect.py", line 2854, in _bind
raise TypeError('too many positional arguments') from None
TypeError: too many positional arguments
In the current iteration of the dev
branch, Union
type hints are not enforced correctly in certain situations (generally ones where there is nesting involved). The following example does not throw an error.
@runtime_validation
def test_func(x: typing.Union[float, typing.List[str]]) -> int:
return 5
test_func([1, 2, 3, 4])
I'm working on debugging it as well.
It's caused by import pypandoc
in setup.py.
It's external dependency, and to install enforce
user must firstly manually install pypandoc
and it's dependencies (which are huge, by the way), because pip install git+https://github.com/RussBaz/enforce.git@dev
won't run.
Do you really need that? It looks like not really. Goal which you want to achieve may be done better way.
from setuptools import setup
from pathlib import Path
import pypandoc
ROOT = Path('.')
README_PATH = ROOT / 'README.md'
with README_PATH.open(encoding='utf-8') as f:
LONG_DESCRIPTION = pypandoc.convert_text(f.read(), 'rst', format='md')
setup(
name='enforce',
version='0.3.5',
description='Python 3.5+ library for integration testing and data validation through configurable and optional runtime type hint enforcement.',
long_description=LONG_DESCRIPTION,
...
In mypy you can use this
import typing
def returns_dict() -> typing.Mapping:
return dict()
returns_dict()
And it validates just fine.
But with enforce I get a strange result
import enforce
import typing
@enforce.runtime_validation
def returns_dict() -> typing.Mapping:
return dict
returns_dict()
Seemingly, the generic type handling is different in enforce.
Traceback (most recent call last):
File "/Users/XXX/enforce-test/env/lib/python3.6/site-packages/enforce/nodes.py", line 539, in preprocess_data
enforcer = data.__enforcer__
AttributeError: type object 'dict' has no attribute '__enforcer__'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "test.py", line 10, in <module>
returns_dict()
File "/Users/XXX/enforce-test/env/lib/python3.6/site-packages/enforce/decorators.py", line 118, in universal
return enforcer.validate_outputs(result)
File "/Users/XXX/enforce-test/env/lib/python3.6/site-packages/enforce/enforcers.py", line 96, in validate_outputs
if not self.validator.validate(output_data, 'return'):
File "/Users/XXX/enforce-test/env/lib/python3.6/site-packages/enforce/validator.py", line 26, in validate
validation_result = visit(validation_tree)
File "/Users/XXX/enforce-test/env/lib/python3.6/site-packages/enforce/utils.py", line 17, in visit
stack.append(last.send(last_result))
File "/Users/XXX/enforce-test/env/lib/python3.6/site-packages/enforce/nodes.py", line 64, in validate
clean_data = self.preprocess_data(validator, data)
File "/Users/XXX/enforce-test/env/lib/python3.6/site-packages/enforce/nodes.py", line 541, in preprocess_data
return GenericProxy(data)
File "/Users/XXX/enforce-test/env/lib/python3.6/site-packages/enforce/enforcers.py", line 130, in __init__
raise TypeError('Only generics can be wrapped in GenericProxy')
TypeError: Only generics can be wrapped in GenericProxy
What's the best way to get a similar behavior to mypy?
config
function. Reset to default functionality was also added.issubclass
is covariant)__subclasscheck__
, __subclasshook__
and ABCs (#21, #23)__no_type_check__
is now respected (#26)A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.