Skip to content

Commit 5140b4e

Browse files
authored
Merge branch 'main' into masked_input
2 parents e0f01e8 + 43dfe08 commit 5140b4e

File tree

5 files changed

+51
-14
lines changed

5 files changed

+51
-14
lines changed

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8-
## Unreleased
8+
## [Unreleased]
99

1010
### Added
1111

1212
- Added `MaskedInput` widget https://github.com/Textualize/textual/pull/4783
13+
- Input validation for floats and integers accept embedded underscores, e.g., "1_234_567" is valid. https://github.com/Textualize/textual/pull/4784
14+
15+
### Changed
16+
17+
- Input validation for integers no longer accepts scientific notation like '1.5e2'; must be castable to int. https://github.com/Textualize/textual/pull/4784
18+
19+
### Fixed
20+
21+
- Input validation of floats no longer accepts NaN (not a number). https://github.com/Textualize/textual/pull/4784
1322

1423
## [0.79.1] - 2024-08-31
1524

src/textual/validation.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ def validate(self, value: str) -> ValidationResult:
294294
except ValueError:
295295
return ValidationResult.failure([Number.NotANumber(self, value)])
296296

297-
if float_value in {math.nan, math.inf, -math.inf}:
297+
if math.isnan(float_value) or math.isinf(float_value):
298298
return ValidationResult.failure([Number.NotANumber(self, value)])
299299

300300
if not self._validate_range(float_value):
@@ -354,10 +354,10 @@ def validate(self, value: str) -> ValidationResult:
354354
return number_validation_result
355355

356356
# We know it's a number, but is that number an integer?
357-
is_integer = float(value).is_integer()
358-
if not is_integer:
357+
try:
358+
int_value = int(value)
359+
except ValueError:
359360
return ValidationResult.failure([Integer.NotAnInteger(self, value)])
360-
361361
return self.success()
362362

363363
def describe_failure(self, failure: Failure) -> str | None:

src/textual/widgets/_input.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@
3434
_POSSIBLE_VALIDATE_ON_VALUES = {"blur", "changed", "submitted"}
3535
"""Set literal with the legal values for the type `InputValidationOn`."""
3636

37-
3837
_RESTRICT_TYPES = {
39-
"integer": r"[-+]?\d*",
40-
"number": r"[-+]?\d*\.?\d*[eE]?[-+]?\d*",
38+
"integer": r"[-+]?(?:\d*|\d+_)*",
39+
"number": r"[-+]?(?:\d*|\d+_)*\.?(?:\d*|\d+_)*(?:\d[eE]?[-+]?(?:\d*|\d+_)*)?",
4140
"text": None,
4241
}
4342
InputType = Literal["integer", "number", "text"]

tests/input/test_input_restrict.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,35 @@
88

99

1010
def test_input_number_type():
11-
"""Test number type regex."""
11+
"""Test number type regex, value should be number or the prefix of a valid number"""
1212
number = _RESTRICT_TYPES["number"]
1313
assert re.fullmatch(number, "0")
1414
assert re.fullmatch(number, "0.")
1515
assert re.fullmatch(number, ".")
16+
assert re.fullmatch(number, "-")
17+
assert re.fullmatch(number, "+")
1618
assert re.fullmatch(number, ".0")
1719
assert re.fullmatch(number, "1.1")
20+
assert re.fullmatch(number, "1_")
21+
assert re.fullmatch(number, "1_2")
22+
assert re.fullmatch(number, "-000_123_456.78e01_234")
1823
assert re.fullmatch(number, "1e1")
24+
assert re.fullmatch(number, "1")
25+
assert re.fullmatch(number, "1.")
26+
assert re.fullmatch(number, "1.2")
1927
assert re.fullmatch(number, "1.2e")
20-
assert re.fullmatch(number, "1.2e10")
21-
assert re.fullmatch(number, "1.2E10")
2228
assert re.fullmatch(number, "1.2e-")
29+
assert re.fullmatch(number, "1.2e-1")
2330
assert re.fullmatch(number, "1.2e-10")
31+
assert re.fullmatch(number, "1.2E10")
2432
assert not re.fullmatch(number, "1.2e10e")
33+
assert not re.fullmatch(number, "-000_123_456.78e01_234.")
34+
assert not re.fullmatch(number, "e") # float("e23") is not valid
2535
assert not re.fullmatch(number, "1f2")
2636
assert not re.fullmatch(number, "inf")
2737
assert not re.fullmatch(number, "nan")
38+
assert not re.fullmatch(number, "-inf")
39+
2840

2941

3042
def test_input_integer_type():
@@ -38,6 +50,13 @@ def test_input_integer_type():
3850
assert re.fullmatch(integer, "+")
3951
assert re.fullmatch(integer, "-1")
4052
assert re.fullmatch(integer, "+2")
53+
assert re.fullmatch(integer, "+0")
54+
assert re.fullmatch(integer, "+0_")
55+
assert re.fullmatch(integer, "+0_1")
56+
assert re.fullmatch(integer, "+0_12")
57+
assert re.fullmatch(integer, "+0_123")
58+
assert not re.fullmatch(integer, "+_123")
59+
assert not re.fullmatch(integer, "123.")
4160
assert not re.fullmatch(integer, "+2e")
4261
assert not re.fullmatch(integer, "foo")
4362

@@ -125,9 +144,9 @@ def compose(self) -> ComposeResult:
125144

126145
integer_input.focus()
127146
await pilot.press("a")
128-
assert integer_input.value == ""
129-
147+
assert not integer_input.value
130148
await pilot.press("-")
149+
assert integer_input.value == "-"
131150
assert integer_input.is_valid is False
132151

133152
await pilot.press("1")

tests/test_validation.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ def test_Failure_description_describe_and_description_inside_validate():
114114
("99", 100, 200, False), # valid number but not in range
115115
("201", 100, 200, False), # valid number but not in range
116116
("1.23e4", 0, 50000, True), # valid scientific notation within range
117+
("inf", None, None, False), # infinity never valid
118+
("nan", None, None, False), # nan never valid
119+
("-inf", None, None, False), # nan never valid
120+
("-4", 0, 5, False), # valid negative number, out of range with zero
121+
("2", -3, 0, False), # valid number out of range with zero
122+
("-2", -3, 0, True), # negative in range
117123
],
118124
)
119125
def test_Number_validate(value, minimum, maximum, expected_result):
@@ -154,7 +160,11 @@ def test_Regex_validate(regex, value, expected_result):
154160
("123", 100, 200, True), # valid integer within range
155161
("99", 100, 200, False), # valid integer but not in range
156162
("201", 100, 200, False), # valid integer but not in range
157-
("1.23e4", None, None, True), # valid integer in scientific notation
163+
("1.23e4", None, None, False), # valid scientific notation, even resolving to an integer, is not valid
164+
("123.", None, None, False), # periods not valid in integers
165+
("123_456", None, None, True), # underscores are valid python
166+
("_123_456", None, None, False), # leading underscores are not valid python
167+
("-123", -123, -123, True), # valid negative number in minimal range
158168
],
159169
)
160170
def test_Integer_validate(value, minimum, maximum, expected_result):

0 commit comments

Comments
 (0)