Fix Number and Float validation errors, allow underscores, add tests. Fix issue #4718 (#4784)

* Update Validation Test cases until not passing

* update Number and Integer validators

* Upgrade Number and Integer type regex to allow underscore, augment tests

* Force CHANGELOG update to prevent merge issues

* Update CHANGELOG.md for this PR

---------

Co-authored-by: Darren Burns <darrenb900@gmail.com>
This commit is contained in:
Charles Merriam
2024-09-05 00:20:39 -07:00
committed by GitHub
parent 61530d3691
commit 43dfe08117
5 changed files with 55 additions and 13 deletions

View File

@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added
- Input validation for floats and integers accept embedded underscores, e.g., "1_234_567" is valid. https://github.com/Textualize/textual/pull/4784
### Changed
- Input validation for integers no longer accepts scientific notation like '1.5e2'; must be castable to int. https://github.com/Textualize/textual/pull/4784
### Fixed
- Input validation of floats no longer accepts NaN (not a number). https://github.com/Textualize/textual/pull/4784
## [0.79.1] - 2024-08-31
### Fixed

View File

@@ -294,7 +294,7 @@ class Number(Validator):
except ValueError:
return ValidationResult.failure([Number.NotANumber(self, value)])
if float_value in {math.nan, math.inf, -math.inf}:
if math.isnan(float_value) or math.isinf(float_value):
return ValidationResult.failure([Number.NotANumber(self, value)])
if not self._validate_range(float_value):
@@ -354,10 +354,10 @@ class Integer(Number):
return number_validation_result
# We know it's a number, but is that number an integer?
is_integer = float(value).is_integer()
if not is_integer:
try:
int_value = int(value)
except ValueError:
return ValidationResult.failure([Integer.NotAnInteger(self, value)])
return self.success()
def describe_failure(self, failure: Failure) -> str | None:

View File

@@ -34,10 +34,9 @@ InputValidationOn = Literal["blur", "changed", "submitted"]
_POSSIBLE_VALIDATE_ON_VALUES = {"blur", "changed", "submitted"}
"""Set literal with the legal values for the type `InputValidationOn`."""
_RESTRICT_TYPES = {
"integer": r"[-+]?\d*",
"number": r"[-+]?\d*\.?\d*[eE]?[-+]?\d*",
"integer": r"[-+]?(?:\d*|\d+_)*",
"number": r"[-+]?(?:\d*|\d+_)*\.?(?:\d*|\d+_)*(?:\d[eE]?[-+]?(?:\d*|\d+_)*)?",
"text": None,
}
InputType = Literal["integer", "number", "text"]

View File

@@ -8,23 +8,35 @@ from textual.widgets._input import _RESTRICT_TYPES
def test_input_number_type():
"""Test number type regex."""
"""Test number type regex, value should be number or the prefix of a valid number"""
number = _RESTRICT_TYPES["number"]
assert re.fullmatch(number, "0")
assert re.fullmatch(number, "0.")
assert re.fullmatch(number, ".")
assert re.fullmatch(number, "-")
assert re.fullmatch(number, "+")
assert re.fullmatch(number, ".0")
assert re.fullmatch(number, "1.1")
assert re.fullmatch(number, "1_")
assert re.fullmatch(number, "1_2")
assert re.fullmatch(number, "-000_123_456.78e01_234")
assert re.fullmatch(number, "1e1")
assert re.fullmatch(number, "1")
assert re.fullmatch(number, "1.")
assert re.fullmatch(number, "1.2")
assert re.fullmatch(number, "1.2e")
assert re.fullmatch(number, "1.2e10")
assert re.fullmatch(number, "1.2E10")
assert re.fullmatch(number, "1.2e-")
assert re.fullmatch(number, "1.2e-1")
assert re.fullmatch(number, "1.2e-10")
assert re.fullmatch(number, "1.2E10")
assert not re.fullmatch(number, "1.2e10e")
assert not re.fullmatch(number, "-000_123_456.78e01_234.")
assert not re.fullmatch(number, "e") # float("e23") is not valid
assert not re.fullmatch(number, "1f2")
assert not re.fullmatch(number, "inf")
assert not re.fullmatch(number, "nan")
assert not re.fullmatch(number, "-inf")
def test_input_integer_type():
@@ -38,6 +50,13 @@ def test_input_integer_type():
assert re.fullmatch(integer, "+")
assert re.fullmatch(integer, "-1")
assert re.fullmatch(integer, "+2")
assert re.fullmatch(integer, "+0")
assert re.fullmatch(integer, "+0_")
assert re.fullmatch(integer, "+0_1")
assert re.fullmatch(integer, "+0_12")
assert re.fullmatch(integer, "+0_123")
assert not re.fullmatch(integer, "+_123")
assert not re.fullmatch(integer, "123.")
assert not re.fullmatch(integer, "+2e")
assert not re.fullmatch(integer, "foo")
@@ -125,9 +144,9 @@ async def test_restrict_type():
integer_input.focus()
await pilot.press("a")
assert integer_input.value == ""
assert not integer_input.value
await pilot.press("-")
assert integer_input.value == "-"
assert integer_input.is_valid is False
await pilot.press("1")

View File

@@ -114,6 +114,12 @@ def test_Failure_description_describe_and_description_inside_validate():
("99", 100, 200, False), # valid number but not in range
("201", 100, 200, False), # valid number but not in range
("1.23e4", 0, 50000, True), # valid scientific notation within range
("inf", None, None, False), # infinity never valid
("nan", None, None, False), # nan never valid
("-inf", None, None, False), # nan never valid
("-4", 0, 5, False), # valid negative number, out of range with zero
("2", -3, 0, False), # valid number out of range with zero
("-2", -3, 0, True), # negative in range
],
)
def test_Number_validate(value, minimum, maximum, expected_result):
@@ -154,7 +160,11 @@ def test_Regex_validate(regex, value, expected_result):
("123", 100, 200, True), # valid integer within range
("99", 100, 200, False), # valid integer but not in range
("201", 100, 200, False), # valid integer but not in range
("1.23e4", None, None, True), # valid integer in scientific notation
("1.23e4", None, None, False), # valid scientific notation, even resolving to an integer, is not valid
("123.", None, None, False), # periods not valid in integers
("123_456", None, None, True), # underscores are valid python
("_123_456", None, None, False), # leading underscores are not valid python
("-123", -123, -123, True), # valid negative number in minimal range
],
)
def test_Integer_validate(value, minimum, maximum, expected_result):