File: test_validation.py

package info (click to toggle)
textual 2.1.2-1.1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 55,080 kB
  • sloc: python: 85,423; lisp: 1,669; makefile: 101
file content (234 lines) | stat: -rw-r--r-- 8,907 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
from __future__ import annotations

import pytest

from textual.validation import (
    URL,
    Failure,
    Function,
    Integer,
    Length,
    Number,
    Regex,
    ValidationResult,
    Validator,
)

VALIDATOR = Function(lambda value: True)


def test_ValidationResult_merge_successes():
    results = [ValidationResult.success(), ValidationResult.success()]
    assert ValidationResult.merge(results) == ValidationResult.success()


def test_ValidationResult_merge_failures():
    failure_one = Failure(VALIDATOR, "1")
    failure_two = Failure(VALIDATOR, "2")
    results = [
        ValidationResult.failure([failure_one]),
        ValidationResult.failure([failure_two]),
        ValidationResult.success(),
    ]
    expected_result = ValidationResult.failure([failure_one, failure_two])
    assert ValidationResult.merge(results) == expected_result


def test_ValidationResult_failure_descriptions():
    result = ValidationResult.failure(
        [
            Failure(VALIDATOR, description="One"),
            Failure(VALIDATOR, description="Two"),
            Failure(VALIDATOR, description="Three"),
        ],
    )
    assert result.failure_descriptions == ["One", "Two", "Three"]


class ValidatorWithDescribeFailure(Validator):
    def validate(self, value: str) -> ValidationResult:
        return self.failure()

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


def test_Failure_description_priorities_parameter_only():
    number_validator = Number(failure_description="ABC")
    non_number_value = "x"
    result = number_validator.validate(non_number_value)
    # The inline value takes priority over the describe_failure.
    assert result.failures[0].description == "ABC"


def test_Failure_description_priorities_parameter_and_describe_failure():
    validator = ValidatorWithDescribeFailure(failure_description="ABC")
    result = validator.validate("x")
    # Even though the validator has a `describe_failure`, we've provided it
    # inline and the inline value should take priority.
    assert result.failures[0].description == "ABC"


def test_Failure_description_priorities_describe_failure_only():
    validator = ValidatorWithDescribeFailure()
    result = validator.validate("x")
    assert result.failures[0].description == "describe_failure"


class ValidatorWithFailureMessageAndNoDescribe(Validator):
    def validate(self, value: str) -> ValidationResult:
        return self.failure(description="ABC")


def test_Failure_description_parameter_and_description_inside_validate():
    validator = ValidatorWithFailureMessageAndNoDescribe()
    result = validator.validate("x")
    assert result.failures[0].description == "ABC"


class ValidatorWithFailureMessageAndDescribe(Validator):
    def validate(self, value: str) -> ValidationResult:
        return self.failure(value=value, description="ABC")

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


def test_Failure_description_describe_and_description_inside_validate():
    # This is kind of a weird case - there's no reason to supply both of
    # these but lets still make sure we're sensible about how we handle it.
    validator = ValidatorWithFailureMessageAndDescribe()
    result = validator.validate("x")
    assert result.failures == [Failure(validator, "x", "ABC")]


@pytest.mark.parametrize(
    "value, minimum, maximum, expected_result",
    [
        ("123", None, None, True),  # valid number, no range
        ("-123", None, None, True),  # valid negative number, no range
        ("123.45", None, None, True),  # valid float, no range
        ("1.23e-4", None, None, True),  # valid scientific notation, no range
        ("abc", None, None, False),  # non-numeric string, no range
        ("123", 100, 200, True),  # valid number within range
        ("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):
    validator = Number(minimum=minimum, maximum=maximum)
    result = validator.validate(value)
    assert result.is_valid == expected_result


@pytest.mark.parametrize(
    "regex, value, expected_result",
    [
        (r"\d+", "123", True),  # matches regex for one or more digits
        (r"\d+", "abc", False),  # does not match regex for one or more digits
        (r"[a-z]+", "abc", True),  # matches regex for one or more lowercase letters
        (
            r"[a-z]+",
            "ABC",
            False,
        ),  # does not match regex for one or more lowercase letters
        (r"\w+", "abc123", True),  # matches regex for one or more word characters
        (r"\w+", "!@#", False),  # does not match regex for one or more word characters
    ],
)
def test_Regex_validate(regex, value, expected_result):
    validator = Regex(regex)
    result = validator.validate(value)
    assert result.is_valid == expected_result


@pytest.mark.parametrize(
    "value, minimum, maximum, expected_result",
    [
        ("123", None, None, True),  # valid integer, no range
        ("-123", None, None, True),  # valid negative integer, no range
        ("123.45", None, None, False),  # float, not a valid integer
        ("1.23e-4", None, None, False),  # scientific notation, not a valid integer
        ("abc", None, None, False),  # non-numeric string, not a valid integer
        ("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, 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):
    validator = Integer(minimum=minimum, maximum=maximum)
    result = validator.validate(value)
    assert result.is_valid == expected_result


@pytest.mark.parametrize(
    "value, min_length, max_length, expected_result",
    [
        ("", None, None, True),  # empty string
        ("test", None, None, True),  # any string with no restrictions
        ("test", 5, None, False),  # shorter than minimum length
        ("test", None, 3, False),  # longer than maximum length
        ("test", 4, 4, True),  # exactly matches minimum and maximum length
        ("test", 2, 6, True),  # within length range
    ],
)
def test_Length_validate(value, min_length, max_length, expected_result):
    validator = Length(minimum=min_length, maximum=max_length)
    result = validator.validate(value)
    assert result.is_valid == expected_result


@pytest.mark.parametrize(
    "value, expected_result",
    [
        ("http://example.com", True),  # valid URL
        ("https://example.com", True),  # valid URL with https
        ("www.example.com", False),  # missing scheme
        ("://example.com", False),  # invalid URL (no scheme)
        ("https:///path", False),  # missing netloc
        (
            "redis://username:pass[word@localhost:6379/0",
            False,
        ),  # invalid URL characters
        ("", False),  # empty string
    ],
)
def test_URL_validate(value, expected_result):
    validator = URL()
    result = validator.validate(value)
    assert result.is_valid == expected_result


@pytest.mark.parametrize(
    "function, failure_description, is_valid",
    [
        ((lambda value: True), None, True),
        ((lambda value: False), "failure!", False),
    ],
)
def test_Function_validate(function, failure_description, is_valid):
    validator = Function(function, failure_description)
    result = validator.validate("x")
    assert result.is_valid is is_valid
    if result.failure_descriptions:
        assert result.failure_descriptions[0] == failure_description


def test_Integer_failure_description_when_NotANumber():
    """Regression test for https://github.com/Textualize/textual/issues/4413"""
    validator = Integer()
    result = validator.validate("x")
    assert result.is_valid is False
    assert result.failure_descriptions[0] == "Must be a valid integer."