Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 245c8c4

Browse files
committedJun 27, 2017
Add ~ shortcut to negate shortcut
Sometimes negation is easier than the positive specification.
1 parent 8b7849e commit 245c8c4

File tree

9 files changed

+131
-5
lines changed

9 files changed

+131
-5
lines changed
 

‎README.md

+18
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,24 @@ ValueError: Expected an odd integer
198198
2
199199
~~~
200200

201+
The shortcuts can also be negated if specifying the "not" condition is too complicated:
202+
203+
~~~python
204+
import py_validate as pv
205+
206+
@pv.validate_inputs(a="~number") # The input must not be a number.
207+
def identity(a):
208+
return a
209+
210+
>>> identity(1)
211+
...
212+
py_validate.backend.shortcuts.NegateFailure: Failed validation for input 'a':
213+
Validation for 'number' passed when it shouldn't have
214+
215+
>>> identity("foo")
216+
'foo'
217+
~~~
218+
201219
When specifying validators for input variables, do note that once validators for a variable
202220
have been set, they cannot be changed. Doing so will cause an error to be raised:
203221

‎py_validate/backend/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
"""
44

55
from py_validate.backend.base import ValidatedFunction # noqa
6-
from py_validate.backend.shortcuts import get_shortcut # noqa
6+
from py_validate.backend.shortcuts import NegateShortcut, get_shortcut # noqa

‎py_validate/backend/base.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
from .helpers import DocSubstitution, FrozenDict
6-
from .shortcuts import get_shortcut
6+
from .shortcuts import NegateShortcut, get_shortcut
77

88

99
validator_doc = """If a string is provided, that means we are using a shortcut,
@@ -216,7 +216,10 @@ def raise_exception_failure(inp_name, e):
216216
raise type(e)(exception_failure.format(inp_name=inp_name) + str(e))
217217

218218
if isinstance(validator, str):
219-
validator = get_shortcut(validator)
219+
if validator.startswith("~"):
220+
validator = NegateShortcut(validator[1:])
221+
else:
222+
validator = get_shortcut(validator)
220223

221224
try:
222225
validator(val)

‎py_validate/backend/shortcuts.py

+26
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,29 @@ def get_shortcut(shortcut):
117117
raise ValueError(msg.format(shortcut=shortcut))
118118

119119
return shortcut_func
120+
121+
122+
class NegateFailure(Exception):
123+
"""
124+
Exception class for when a validation function passes when it shouldn't.
125+
"""
126+
127+
pass
128+
129+
130+
class NegateShortcut(object):
131+
132+
def __init__(self, shortcut):
133+
self.shortcut = shortcut
134+
self.func = get_shortcut(shortcut)
135+
self.msg = ("Validation for '{shortcut}' "
136+
"passed when it shouldn't have")
137+
138+
def __call__(self, x):
139+
try:
140+
self.func(x)
141+
raise NegateFailure(self.msg.format(shortcut=self.shortcut))
142+
except NegateFailure:
143+
raise
144+
except (TypeError, ValueError):
145+
pass

‎py_validate/tests/backend/test_base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def test_pv_namespace(self):
4141

4242
def test_pv_backend_namespace(self):
4343
import py_validate.backend as backend
44-
expected = {"ValidatedFunction", "base",
44+
expected = {"NegateShortcut", "ValidatedFunction", "base",
4545
"get_shortcut", "helpers", "shortcuts"}
4646

4747
self._check_namespace(backend, expected)

‎py_validate/tests/backend/test_shortcuts.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import py_validate.backend.shortcuts as shortcuts
1+
from py_validate.backend.shortcuts import NegateShortcut
22
from py_validate.tests import assert_raises
33

4+
import py_validate.backend.shortcuts as shortcuts
45
import pytest
56

67

@@ -109,3 +110,37 @@ def test_invalid_odd_not_int(self, invalid):
109110
def test_invalid_even_not_even(self, invalid):
110111
msg = "Expected an odd integer"
111112
assert_raises(ValueError, msg, shortcuts.check_odd, invalid)
113+
114+
115+
class TestCheckNegateShortcut(object):
116+
117+
@pytest.mark.parametrize("invalid", [
118+
"foo", "bar", "baz"
119+
])
120+
def test_invalid_shortcut_negate(self, invalid):
121+
msg = "Unknown shortcut"
122+
assert_raises(ValueError, msg, NegateShortcut, invalid)
123+
124+
@pytest.mark.parametrize("valid,value", [
125+
("number", "foo"),
126+
("integer", 1.0),
127+
("even", 3),
128+
("odd", 2)
129+
])
130+
def test_valid_negate(self, valid, value):
131+
132+
# No Exception should be raised.
133+
negate_check = NegateShortcut(valid)
134+
negate_check(value)
135+
136+
@pytest.mark.parametrize("valid,value", [
137+
("number", 1.5),
138+
("integer", 1),
139+
("even", 2),
140+
("odd", 3)
141+
])
142+
def test_invalid_negate(self, valid, value):
143+
msg = "passed when it shouldn't have"
144+
negate_check = NegateShortcut(valid)
145+
146+
assert_raises(shortcuts.NegateFailure, msg, negate_check, value)

‎py_validate/tests/validator/test_inputs.py

+12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Unittests for the input validator decorator.
33
"""
44

5+
from py_validate.backend.shortcuts import NegateFailure
56
from py_validate.validator import validate_inputs
67
from py_validate.tests import assert_raises
78

@@ -186,3 +187,14 @@ def wrapper(a):
186187
msg = "Expected an odd integer"
187188
assert_raises(ValueError, msg, wrapper, 2)
188189
assert_raises(ValueError, msg, wrapper, 4)
190+
191+
def test_negate(self):
192+
@validate_inputs(a="~number")
193+
def wrapper(a):
194+
return a
195+
196+
assert wrapper("foo") == "foo"
197+
assert wrapper((1, 2, 3)) == (1, 2, 3)
198+
199+
msg = "'number' passed when it shouldn't have"
200+
assert_raises(NegateFailure, msg, wrapper, 1)

‎py_validate/tests/validator/test_outputs.py

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Unittests for the output validator decorator.
33
"""
44

5+
from py_validate.backend.shortcuts import NegateFailure
56
from py_validate.validator import validate_outputs
67
from py_validate.tests import assert_raises
78

@@ -175,3 +176,15 @@ def wrapper(a):
175176

176177
msg = "Expected an odd integer"
177178
assert_raises(ValueError, msg, wrapper, 2)
179+
180+
def test_negate(self):
181+
@validate_outputs(-1, "~integer")
182+
def wrapper(a):
183+
return a
184+
185+
assert wrapper(1.5) == 1.5
186+
assert wrapper("foo") == "foo"
187+
assert wrapper((1, 2, 3)) == (1, 2, 3)
188+
189+
msg = "'integer' passed when it shouldn't have"
190+
assert_raises(NegateFailure, msg, wrapper, 1)

‎py_validate/tests/validator/test_stacking.py

+19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
from py_validate.validator import validate_inputs, validate_outputs
7+
from py_validate.backend.shortcuts import NegateFailure
78
from py_validate.tests import assert_raises
89

910

@@ -188,3 +189,21 @@ def wrapper(a):
188189

189190
msg = "Expected an even integer"
190191
assert_raises(ValueError, msg, wrapper, 1)
192+
193+
194+
def test_negate_stack():
195+
@validate_inputs(a="integer")
196+
@validate_inputs(b="~number")
197+
def wrapper(a, b):
198+
return a * b
199+
200+
assert wrapper(2, "bar") == "barbar"
201+
assert wrapper(3, [1, 2]) == [1, 2, 1, 2, 1, 2]
202+
203+
msg = "Expected an integer"
204+
assert_raises(TypeError, msg, wrapper, "foo", "bar")
205+
assert_raises(TypeError, msg, wrapper, [1, 2, 3], "bar")
206+
207+
msg = "'number' passed when it shouldn't have"
208+
assert_raises(NegateFailure, msg, wrapper, 5, 5)
209+
assert_raises(NegateFailure, msg, wrapper, 5, 12.1)

0 commit comments

Comments
 (0)
Please sign in to comment.