Skip to content

Commit

Permalink
Merge pull request #1137 from freakboy3742/combine-validators
Browse files Browse the repository at this point in the history
Make input validators a list.
  • Loading branch information
freakboy3742 authored Nov 17, 2020
2 parents 5ea7fa7 + ed7b10e commit caa0e10
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 81 deletions.
6 changes: 3 additions & 3 deletions examples/textinput/textinput/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,18 @@ def startup(self):
placeholder='Password...',
style=Pack(padding=10),
on_change=self.on_password_change,
validator=validators.Combine(
validators=[
validators.MinLength(10),
validators.ContainsUppercase(),
validators.ContainsLowercase(),
validators.ContainsSpecial(),
validators.ContainsDigit()
)
]
)
self.email_input = toga.TextInput(
placeholder='Email...',
style=Pack(padding=10),
validator=validators.Email()
validators=[validators.Email()]
)
self.number_input = toga.NumberInput(style=Pack(padding=10))
btn_extract = toga.Button(
Expand Down
13 changes: 6 additions & 7 deletions src/core/tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,17 @@ def test_validate_maximum_length(self):
self.check()

def test_validate_length_between(self):
too_short_error_message = "Input is too short (length should be at least 5)"
too_long_error_message = "Input is too long (length should be at most 10)"
default_error_message = "Input should be between 5 and 10 characters"

self.args = [5, 10]
self.validator_factory = validators.LengthBetween
self.valid_inputs = ["I am good", "right", "123456789"]
self.invalid_inputs = [
("I", too_short_error_message),
("am", too_short_error_message),
("tiny", too_short_error_message),
("I am way too long", too_long_error_message),
("are you serious now?", too_long_error_message),
("I", default_error_message),
("am", default_error_message),
("tiny", default_error_message),
("I am way too long", default_error_message),
("are you serious now?", default_error_message),
]

self.check()
Expand Down
22 changes: 11 additions & 11 deletions src/core/tests/widgets/test_textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def test_validator_run_in_constructor(self):
validator = Mock(return_value=None)
text_input = toga.TextInput(
initial=self.initial,
validator=validator,
validators=[validator],
factory=toga_dummy.factory
)
self.assertValueNotSet(text_input, "error")
Expand All @@ -98,7 +98,7 @@ def test_validator_run_after_set(self):

self.assertValueNotSet(text_input, "error")

text_input.validator = validator
text_input.validators = [validator]

self.assertValueSet(text_input, "error", message)
validator.assert_called_once_with(self.initial)
Expand All @@ -108,32 +108,32 @@ def test_text_input_with_no_validator_is_valid(self):
initial=self.initial,
factory=toga_dummy.factory
)
self.assertTrue(text_input.is_valid())
self.assertTrue(text_input.validate())

def test_is_valid_returns_true(self):
def test_validate_true_when_valid(self):
validator = Mock(return_value=None)
text_input = toga.TextInput(
initial=self.initial,
validator=validator,
validators=[validator],
factory=toga_dummy.factory
)
self.assertTrue(text_input.is_valid())
self.assertTrue(text_input.validate())

def test_is_valid_returns_false(self):
def test_validate_false_when_invalid(self):
message = "This is an error message"
validator = Mock(return_value=message)
text_input = toga.TextInput(
initial=self.initial,
validator=validator,
validators=[validator],
factory=toga_dummy.factory
)
self.assertFalse(text_input.is_valid())
self.assertFalse(text_input.validate())

def test_validate_passes(self):
validator = Mock(side_effect=[None, None])
text_input = toga.TextInput(
initial=self.initial,
validator=validator,
validators=[validator],
factory=toga_dummy.factory
)
self.assertValueNotSet(text_input, "error")
Expand All @@ -149,7 +149,7 @@ def test_validate_fails(self):
validator = Mock(side_effect=[None, message])
text_input = toga.TextInput(
initial=self.initial,
validator=validator,
validators=[validator],
factory=toga_dummy.factory
)
self.assertValueNotSet(text_input, "error")
Expand Down
73 changes: 40 additions & 33 deletions src/core/toga/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,6 @@
from string import ascii_uppercase, ascii_lowercase, digits


class Combine:
def __init__(self, *validators):
"""Use this method to combine multiple validators."""
self.validators = validators

def __call__(self, input_string):
for validator in self.validators:
error_message = validator(input_string)
if error_message is not None:
return error_message
return None


class BooleanValidator:
def __init__(self, error_message: str, allow_empty: bool = True):
self.error_message = error_message
Expand Down Expand Up @@ -65,47 +52,67 @@ def count(self, input_string: str):
)


class MinLength(BooleanValidator):
class LengthBetween(BooleanValidator):
def __init__(
self, length: int, error_message: Optional[str] = None, allow_empty: bool = True
self,
min_value: int,
max_value: int,
error_message: Optional[str] = None,
allow_empty: bool = True,
):
if error_message is None:
error_message = "Input is too short (length should be at least {})".format(
length
error_message = "Input should be between {} and {} characters".format(
min_value, max_value
)
super().__init__(error_message=error_message, allow_empty=allow_empty)
self.length = length
self.min_value = min_value
self.max_value = max_value

def is_valid(self, input_string: str):
return len(input_string) >= self.length
if self.min_value:
if len(input_string) < self.min_value:
return False
if self.max_value:
if len(input_string) > self.max_value:
return False
return True


class MaxLength(BooleanValidator):
class MinLength(LengthBetween):
def __init__(
self, length: int, error_message: Optional[str] = None, allow_empty: bool = True
self,
length: int,
error_message: Optional[str] = None,
allow_empty: bool = True
):
if error_message is None:
error_message = "Input is too long (length should be at most {})".format(
error_message = "Input is too short (length should be at least {})".format(
length
)
super().__init__(error_message=error_message, allow_empty=allow_empty)
self.length = length

def is_valid(self, input_string: str):
return len(input_string) <= self.length
super().__init__(
min_value=length,
max_value=None,
error_message=error_message,
allow_empty=allow_empty
)


class LengthBetween(Combine):
class MaxLength(LengthBetween):
def __init__(
self,
min_value: int,
max_value: int,
length: int,
error_message: Optional[str] = None,
allow_empty: bool = True,
allow_empty: bool = True
):
if error_message is None:
error_message = "Input is too long (length should be at most {})".format(
length
)
super().__init__(
MinLength(min_value, error_message=error_message, allow_empty=allow_empty),
MaxLength(max_value, error_message=error_message, allow_empty=allow_empty),
min_value=None,
max_value=length,
error_message=error_message,
allow_empty=allow_empty
)


Expand Down
52 changes: 25 additions & 27 deletions src/core/toga/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class TextInput(Widget):
placeholder (str): If no input is present this text is shown.
readonly (bool): Whether a user can write into the text input, defaults to `False`.
on_change (Callable): Method to be called when text is changed in text box
validator (Callable): Validator to run on the value of the text box. Should
validators (list): list of validators to run on the value of the text box. Should
return None is value is valid and an error message if not.
on_change (``callable``): The handler to invoke when the text changes.
on_gain_focus (:obj:`callable`): Function to execute when get focused.
Expand All @@ -25,17 +25,17 @@ class TextInput(Widget):
MIN_WIDTH = 100

def __init__(
self,
id=None,
style=None,
factory=None,
initial=None,
placeholder=None,
readonly=False,
on_change=None,
on_gain_focus=None,
on_lose_focus=None,
validator=None
self,
id=None,
style=None,
factory=None,
initial=None,
placeholder=None,
readonly=False,
on_change=None,
on_gain_focus=None,
on_lose_focus=None,
validators=None
):
super().__init__(id=id, style=style, factory=factory)

Expand All @@ -48,7 +48,7 @@ def __init__(

# Set the actual value after on_change, as it may trigger change events, etc.
self.value = initial
self.validator = validator
self.validators = validators
self.on_lose_focus = on_lose_focus
self.on_gain_focus = on_gain_focus

Expand Down Expand Up @@ -128,12 +128,15 @@ def on_change(self, handler):
self._impl.set_on_change(self._on_change)

@property
def validator(self):
return self._validator
def validators(self):
return self._validators

@validator.setter
def validator(self, validator):
self._validator = validator
@validators.setter
def validators(self, validators):
if validators is None:
self._validators = []
else:
self._validators = validators
self.validate()

@property
Expand Down Expand Up @@ -165,19 +168,14 @@ def on_lose_focus(self, handler):
self._impl.set_on_lose_focus(self._on_lose_focus)

def validate(self):
if self.validator is None:
error_message = None
else:
error_message = self.validator(self.value)
error_message = None
for validator in self.validators:
if error_message is None:
error_message = validator(self.value)

if error_message is None:
self._impl.clear_error()
return True
else:
self._impl.set_error(error_message)
return False

def is_valid(self):
if self.validator is None:
return True
return self.validator(self.value) is None

0 comments on commit caa0e10

Please sign in to comment.