valid8r 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of valid8r might be problematic. Click here for more details.
- valid8r/__init__.py +27 -0
- valid8r/core/__init__.py +28 -0
- valid8r/core/combinators.py +89 -0
- valid8r/core/maybe.py +162 -0
- valid8r/core/parsers.py +1354 -0
- valid8r/core/validators.py +200 -0
- valid8r/prompt/__init__.py +8 -0
- valid8r/prompt/basic.py +190 -0
- valid8r/py.typed +0 -0
- valid8r/testing/__init__.py +32 -0
- valid8r/testing/assertions.py +67 -0
- valid8r/testing/generators.py +283 -0
- valid8r/testing/mock_input.py +84 -0
- valid8r-0.2.0.dist-info/METADATA +168 -0
- valid8r-0.2.0.dist-info/RECORD +17 -0
- valid8r-0.2.0.dist-info/WHEEL +4 -0
- valid8r-0.2.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Generators for test cases and test input data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
from typing import (
|
|
7
|
+
TYPE_CHECKING,
|
|
8
|
+
Any,
|
|
9
|
+
TypeVar,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from valid8r.core.maybe import Success
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from types import CellType
|
|
17
|
+
|
|
18
|
+
from valid8r.core.validators import Validator
|
|
19
|
+
|
|
20
|
+
T = TypeVar('T')
|
|
21
|
+
|
|
22
|
+
# Constants for test case generation
|
|
23
|
+
OFFSET_SMALL = 1
|
|
24
|
+
OFFSET_MEDIUM = 5
|
|
25
|
+
OFFSET_LARGE = 10
|
|
26
|
+
OFFSET_XLARGE = 100
|
|
27
|
+
MULTIPLIER_FAR = 2
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_numeric_value_from_closure(closure: Iterable[CellType]) -> Any | None: # noqa: ANN401
|
|
31
|
+
"""Extract a numeric value from a closure."""
|
|
32
|
+
for cell in closure:
|
|
33
|
+
value = cell.cell_contents
|
|
34
|
+
if isinstance(value, int | float):
|
|
35
|
+
return value
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _generate_minimum_validator_cases(min_value: float) -> tuple[list[Any], list[Any]]:
|
|
40
|
+
"""Generate test cases for a minimum validator."""
|
|
41
|
+
valid_cases = [
|
|
42
|
+
min_value, # Boundary
|
|
43
|
+
min_value + OFFSET_SMALL, # Just above
|
|
44
|
+
min_value + OFFSET_MEDIUM, # Above
|
|
45
|
+
min_value + OFFSET_LARGE, # Well above
|
|
46
|
+
min_value * OFFSET_LARGE if min_value > 0 else OFFSET_XLARGE, # Far above
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
invalid_cases = [
|
|
50
|
+
min_value - OFFSET_SMALL, # Just below
|
|
51
|
+
min_value - OFFSET_MEDIUM if min_value >= OFFSET_MEDIUM else 0, # Below
|
|
52
|
+
min_value - OFFSET_LARGE if min_value >= OFFSET_LARGE else -1, # Well below
|
|
53
|
+
-10 if min_value > 0 else min_value - 10, # Far below
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
return valid_cases, invalid_cases
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _generate_maximum_validator_cases(max_value: float) -> tuple[list[Any], list[Any]]:
|
|
60
|
+
"""Generate test cases for a maximum validator."""
|
|
61
|
+
valid_cases = [
|
|
62
|
+
max_value, # Boundary
|
|
63
|
+
max_value - OFFSET_SMALL, # Just below
|
|
64
|
+
max_value - OFFSET_MEDIUM if max_value >= OFFSET_MEDIUM else 0, # Below
|
|
65
|
+
max_value - OFFSET_LARGE if max_value >= OFFSET_LARGE else 0, # Well below
|
|
66
|
+
0 if max_value > 0 else max_value // MULTIPLIER_FAR, # Far below
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
invalid_cases = [
|
|
70
|
+
max_value + OFFSET_SMALL, # Just above
|
|
71
|
+
max_value + OFFSET_MEDIUM, # Above
|
|
72
|
+
max_value + OFFSET_LARGE, # Well above
|
|
73
|
+
max_value * MULTIPLIER_FAR, # Far above
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
return valid_cases, invalid_cases
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _generate_between_validator_cases(min_val: float, max_val: float) -> tuple[list[Any], list[Any]]:
|
|
80
|
+
"""Generate test cases for a between validator."""
|
|
81
|
+
range_size = max_val - min_val
|
|
82
|
+
valid_cases = [
|
|
83
|
+
min_val, # Min boundary
|
|
84
|
+
max_val, # Max boundary
|
|
85
|
+
(min_val + max_val) // 2, # Middle
|
|
86
|
+
min_val + range_size // 4, # First quarter
|
|
87
|
+
max_val - range_size // 4, # Last quarter
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
invalid_cases = [
|
|
91
|
+
min_val - 1, # Just below min
|
|
92
|
+
max_val + 1, # Just above max
|
|
93
|
+
min_val - 10, # Well below min
|
|
94
|
+
max_val + 10, # Well above max
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
return valid_cases, invalid_cases
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _identify_validator_type(validator: Validator[T]) -> tuple[str, Any, Any]:
|
|
101
|
+
"""Identify the type of validator and extract its parameters.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A tuple of (validator_type, first_param, second_param)
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
if not hasattr(validator.func, '__closure__') or not validator.func.__closure__:
|
|
108
|
+
return 'unknown', None, None
|
|
109
|
+
|
|
110
|
+
func_str = str(validator.func)
|
|
111
|
+
|
|
112
|
+
if 'minimum' in func_str:
|
|
113
|
+
min_value = _extract_numeric_value_from_closure(validator.func.__closure__)
|
|
114
|
+
return 'minimum', min_value, None
|
|
115
|
+
|
|
116
|
+
if 'maximum' in func_str:
|
|
117
|
+
max_value = _extract_numeric_value_from_closure(validator.func.__closure__)
|
|
118
|
+
return 'maximum', max_value, None
|
|
119
|
+
|
|
120
|
+
if 'between' in func_str:
|
|
121
|
+
values = _extract_two_numeric_values(validator.func.__closure__)
|
|
122
|
+
return 'between', values[0], values[1]
|
|
123
|
+
|
|
124
|
+
return 'unknown', None, None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _extract_two_numeric_values(closure: Iterable[CellType]) -> tuple[Any, Any]:
|
|
128
|
+
"""Extract two numeric values from a closure."""
|
|
129
|
+
values = []
|
|
130
|
+
for cell in closure:
|
|
131
|
+
value = cell.cell_contents
|
|
132
|
+
if isinstance(value, int | float):
|
|
133
|
+
values.append(value)
|
|
134
|
+
if len(values) >= 2: # noqa: PLR2004
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
if len(values) < 2: # noqa: PLR2004
|
|
138
|
+
return None, None
|
|
139
|
+
return values[0], values[1]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def generate_test_cases(validator: Validator[T]) -> dict[str, list[Any]]:
|
|
143
|
+
"""Generate test cases for a validator.
|
|
144
|
+
|
|
145
|
+
This function analyzes the validator and generates appropriate test cases
|
|
146
|
+
that should pass and fail the validation.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
validator: The validator to generate test cases for
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
A dictionary with 'valid' and 'invalid' lists of test cases
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
>>> test_cases = generate_test_cases(minimum(10))
|
|
156
|
+
>>> test_cases
|
|
157
|
+
{'valid': [10, 11, 15, 20, 100], 'invalid': [9, 5, 0, -10]}
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
# Identify validator type and extract parameters
|
|
161
|
+
validator_type, param1, param2 = _identify_validator_type(validator)
|
|
162
|
+
|
|
163
|
+
# Generate test cases based on validator type
|
|
164
|
+
valid_cases: list[Any] = []
|
|
165
|
+
invalid_cases: list[Any] = []
|
|
166
|
+
|
|
167
|
+
if validator_type == 'minimum' and param1 is not None:
|
|
168
|
+
valid_cases, invalid_cases = _generate_minimum_validator_cases(param1)
|
|
169
|
+
|
|
170
|
+
elif validator_type == 'maximum' and param1 is not None:
|
|
171
|
+
valid_cases, invalid_cases = _generate_maximum_validator_cases(param1)
|
|
172
|
+
|
|
173
|
+
elif validator_type == 'between' and param1 is not None and param2 is not None:
|
|
174
|
+
valid_cases, invalid_cases = _generate_between_validator_cases(param1, param2)
|
|
175
|
+
|
|
176
|
+
# Use generic cases if we couldn't determine specific ones
|
|
177
|
+
if not valid_cases:
|
|
178
|
+
valid_cases = [0, 1, 10, 42, 100]
|
|
179
|
+
if not invalid_cases:
|
|
180
|
+
invalid_cases = [-1, -10, -100]
|
|
181
|
+
|
|
182
|
+
# Verify categorization against the actual validator
|
|
183
|
+
actual_valid = []
|
|
184
|
+
actual_invalid = []
|
|
185
|
+
|
|
186
|
+
for case in valid_cases + invalid_cases:
|
|
187
|
+
result = validator(case)
|
|
188
|
+
if result.is_success():
|
|
189
|
+
actual_valid.append(case)
|
|
190
|
+
else:
|
|
191
|
+
actual_invalid.append(case)
|
|
192
|
+
|
|
193
|
+
return {'valid': actual_valid, 'invalid': actual_invalid}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def generate_random_inputs(
|
|
197
|
+
validator: Validator[T], count: int = 20, range_min: int = -100, range_max: int = 100
|
|
198
|
+
) -> list[T]:
|
|
199
|
+
"""Generate random inputs that include both valid and invalid cases.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
validator: The validator to test against
|
|
203
|
+
count: Number of inputs to generate
|
|
204
|
+
range_min: Minimum value for generated integers
|
|
205
|
+
range_max: Maximum value for generated integers
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
A list of random integers
|
|
209
|
+
|
|
210
|
+
Examples:
|
|
211
|
+
>>> inputs = generate_random_inputs(minimum(0), count=10)
|
|
212
|
+
>>> len(inputs)
|
|
213
|
+
10
|
|
214
|
+
|
|
215
|
+
"""
|
|
216
|
+
inputs: list[Any] = []
|
|
217
|
+
|
|
218
|
+
# Try to make sure we get both valid and invalid cases
|
|
219
|
+
for _ in range(count):
|
|
220
|
+
value = random.randint(range_min, range_max) # noqa: S311
|
|
221
|
+
inputs.append(value)
|
|
222
|
+
|
|
223
|
+
# Verify we have at least one valid and one invalid case
|
|
224
|
+
has_valid = False
|
|
225
|
+
has_invalid = False
|
|
226
|
+
|
|
227
|
+
for input_val in inputs:
|
|
228
|
+
result = validator(input_val)
|
|
229
|
+
match result:
|
|
230
|
+
case Success(_):
|
|
231
|
+
has_valid = True
|
|
232
|
+
case _:
|
|
233
|
+
has_invalid = True
|
|
234
|
+
|
|
235
|
+
if has_valid and has_invalid:
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
# If we're missing either valid or invalid cases, add them explicitly
|
|
239
|
+
if not has_valid or not has_invalid:
|
|
240
|
+
# Get test cases that are known to be valid/invalid
|
|
241
|
+
test_cases = generate_test_cases(validator)
|
|
242
|
+
|
|
243
|
+
if not has_valid and test_cases['valid']:
|
|
244
|
+
# Replace the first item with a valid case
|
|
245
|
+
inputs[0] = test_cases['valid'][0]
|
|
246
|
+
|
|
247
|
+
if not has_invalid and test_cases['invalid']:
|
|
248
|
+
# Replace the second item with an invalid case
|
|
249
|
+
inputs[1 if len(inputs) > 1 else 0] = test_cases['invalid'][0]
|
|
250
|
+
|
|
251
|
+
return inputs
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_validator_composition(validator: Validator[T]) -> bool:
|
|
255
|
+
"""Test a composed validator with various inputs to verify it works correctly.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
validator: The composed validator to test
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
True if the validator behaves as expected, False otherwise
|
|
262
|
+
|
|
263
|
+
Examples:
|
|
264
|
+
>>> is_valid_age = minimum(0) & maximum(120)
|
|
265
|
+
>>> test_validator_composition(is_valid_age) # Returns True
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
# Generate test cases
|
|
269
|
+
test_cases = generate_test_cases(validator)
|
|
270
|
+
|
|
271
|
+
# Check that all valid cases pass
|
|
272
|
+
for case in test_cases['valid']:
|
|
273
|
+
result = validator(case)
|
|
274
|
+
if not result.is_success():
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
# Check that all invalid cases fail
|
|
278
|
+
for case in test_cases['invalid']:
|
|
279
|
+
result = validator(case)
|
|
280
|
+
if not result.is_failure():
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
return True
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Utilities for mocking user input during tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
|
|
12
|
+
# Store the original input function
|
|
13
|
+
_original_input = builtins.input
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@contextmanager
|
|
17
|
+
def MockInputContext(inputs: list[str] | None = None) -> Iterator[None]: # noqa: N802
|
|
18
|
+
"""Context manager for mocking user input.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
inputs: A list of strings to be returned sequentially by input().
|
|
22
|
+
|
|
23
|
+
Yields:
|
|
24
|
+
None
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
>>> with MockInputContext(["yes", "42"]):
|
|
28
|
+
... answer = input("Continue? ") # returns "yes"
|
|
29
|
+
... number = input("Enter number: ") # returns "42"
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
input_values = [] if inputs is None else list(inputs)
|
|
33
|
+
|
|
34
|
+
def mock_input(prompt: object = '') -> str: # noqa: ARG001
|
|
35
|
+
"""Mock implementation of input function.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
prompt: The input prompt (ignored in mock)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The next string from the predefined inputs list
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
IndexError: If there are no more inputs available
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
if not input_values:
|
|
48
|
+
raise IndexError('No more mock inputs available')
|
|
49
|
+
return input_values.pop(0)
|
|
50
|
+
|
|
51
|
+
# Replace the builtin input function
|
|
52
|
+
builtins.input = mock_input
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
yield
|
|
56
|
+
finally:
|
|
57
|
+
# Restore the original input function
|
|
58
|
+
builtins.input = _original_input
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def configure_mock_input(inputs: list[str]) -> None:
|
|
62
|
+
"""Configure input to be mocked globally.
|
|
63
|
+
|
|
64
|
+
Unlike MockInputContext, this function replaces the input function
|
|
65
|
+
globally without restoring it automatically. Use for simple tests
|
|
66
|
+
where cleanup isn't critical.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
inputs: A list of strings to be returned sequentially by input().
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
>>> configure_mock_input(["yes", "42"])
|
|
73
|
+
>>> answer = input("Continue? ") # returns "yes"
|
|
74
|
+
>>> number = input("Enter number: ") # returns "42"
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
input_values = list(inputs) # Create a copy
|
|
78
|
+
|
|
79
|
+
def mock_input(prompt: object = '') -> str: # noqa: ARG001
|
|
80
|
+
if not input_values:
|
|
81
|
+
raise IndexError('No more mock inputs available')
|
|
82
|
+
return input_values.pop(0)
|
|
83
|
+
|
|
84
|
+
builtins.input = mock_input
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: valid8r
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Clean, flexible input validation for Python applications
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: validation,input,cli,maybe-monad
|
|
7
|
+
Author: Mike Lane
|
|
8
|
+
Author-email: mikelane@gmail.com
|
|
9
|
+
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Dist: email-validator (>=2.3.0,<3.0.0)
|
|
19
|
+
Requires-Dist: pydantic (>=2.0)
|
|
20
|
+
Requires-Dist: pydantic-core (>=2.27.0,<3.0.0)
|
|
21
|
+
Requires-Dist: uuid-utils (>=0.11.0,<0.12.0)
|
|
22
|
+
Project-URL: Repository, https://github.com/mikelane/valid8r
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Valid8r
|
|
26
|
+
|
|
27
|
+
A clean, flexible input validation library for Python applications.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **Clean Type Parsing**: Parse strings to various Python types with robust error handling
|
|
32
|
+
- **Flexible Validation**: Chain validators and create custom validation rules
|
|
33
|
+
- **Monadic Error Handling**: Use Maybe monad for clean error propagation
|
|
34
|
+
- **Input Prompting**: Prompt users for input with built-in validation
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install valid8r
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from valid8r import (
|
|
46
|
+
parsers,
|
|
47
|
+
prompt,
|
|
48
|
+
validators,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Simple validation
|
|
52
|
+
age = prompt.ask(
|
|
53
|
+
"Enter your age: ",
|
|
54
|
+
parser=parsers.parse_int,
|
|
55
|
+
validator=validators.minimum(0) & validators.maximum(120)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
print(f"Your age is {age}")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### IP parsing helpers
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from valid8r.core.maybe import Success, Failure
|
|
65
|
+
from valid8r.core import parsers
|
|
66
|
+
|
|
67
|
+
# IPv4 / IPv6 / generic IP
|
|
68
|
+
for text in ["192.168.0.1", "::1", " 10.0.0.1 "]:
|
|
69
|
+
match parsers.parse_ip(text):
|
|
70
|
+
case Success(addr):
|
|
71
|
+
print("Parsed:", addr)
|
|
72
|
+
case Failure(err):
|
|
73
|
+
print("Error:", err)
|
|
74
|
+
|
|
75
|
+
# CIDR (strict by default)
|
|
76
|
+
match parsers.parse_cidr("10.0.0.0/8"):
|
|
77
|
+
case Success(net):
|
|
78
|
+
print("Network:", net) # 10.0.0.0/8
|
|
79
|
+
case Failure(err):
|
|
80
|
+
print("Error:", err)
|
|
81
|
+
|
|
82
|
+
# Non-strict masks host bits
|
|
83
|
+
match parsers.parse_cidr("10.0.0.1/24", strict=False):
|
|
84
|
+
case Success(net):
|
|
85
|
+
assert str(net) == "10.0.0.0/24"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### URL and Email helpers
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from valid8r.core.maybe import Success, Failure
|
|
92
|
+
from valid8r.core import parsers
|
|
93
|
+
|
|
94
|
+
# URL parsing
|
|
95
|
+
match parsers.parse_url("https://alice:pw@example.com:8443/x?q=1#top"):
|
|
96
|
+
case Success(u):
|
|
97
|
+
print(u.scheme, u.username, u.password, u.host, u.port)
|
|
98
|
+
case Failure(err):
|
|
99
|
+
print("Error:", err)
|
|
100
|
+
|
|
101
|
+
# Email parsing
|
|
102
|
+
match parsers.parse_email("First.Last+tag@Example.COM"):
|
|
103
|
+
case Success(e):
|
|
104
|
+
print(e.local, e.domain) # First.Last+tag example.com
|
|
105
|
+
case Failure(err):
|
|
106
|
+
print("Error:", err)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Testing Support
|
|
110
|
+
|
|
111
|
+
Valid8r includes testing utilities to help you verify your validation logic:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from valid8r import (
|
|
115
|
+
Maybe,
|
|
116
|
+
validators,
|
|
117
|
+
parsers,
|
|
118
|
+
prompt,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
from valid8r.testing import (
|
|
122
|
+
MockInputContext,
|
|
123
|
+
assert_maybe_success,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def validate_age(age: int) -> Maybe[int]:
|
|
127
|
+
return validators.minimum(0) & validators.maximum(120)(age)
|
|
128
|
+
|
|
129
|
+
# Test prompts with mock input
|
|
130
|
+
with MockInputContext(["yes"]):
|
|
131
|
+
result = prompt.ask("Continue? ", parser=parsers.parse_bool)
|
|
132
|
+
assert result.is_success()
|
|
133
|
+
assert result.value_or(False) == True
|
|
134
|
+
|
|
135
|
+
# Test validation functions
|
|
136
|
+
result = validate_age(42)
|
|
137
|
+
assert assert_maybe_success(result, 42)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
For more information, see the [Testing with Valid8r](docs/user_guide/testing.rst) guide.
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
This project uses Poetry for dependency management and Tox for testing.
|
|
145
|
+
|
|
146
|
+
### Setup
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Install Poetry
|
|
150
|
+
curl -sSL https://install.python-poetry.org | python3 -
|
|
151
|
+
|
|
152
|
+
# Install dependencies
|
|
153
|
+
poetry install
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Running Tests
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# Run all tests
|
|
160
|
+
poetry run tox
|
|
161
|
+
|
|
162
|
+
# Run BDD tests
|
|
163
|
+
poetry run tox -e bdd
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
MIT
|
|
168
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
valid8r/__init__.py,sha256=Yg46vPVbLZvty-FSXkiJ3IkB6hg20pYcp6nWFidIfkk,447
|
|
2
|
+
valid8r/core/__init__.py,sha256=ASOdzqCtpZHbHjjYMZkb78Z-nKxtD26ruTY0bd43ImA,520
|
|
3
|
+
valid8r/core/combinators.py,sha256=KvRiDEqoZgH58cBYPO6SW9pdtkyijk0lS8aGSB5DbO4,2349
|
|
4
|
+
valid8r/core/maybe.py,sha256=xT1xbiLVKohZ2aeaDZoKjT0W6Vk_PPwKbZXpIfsP7hc,4359
|
|
5
|
+
valid8r/core/parsers.py,sha256=XlP9u0obe060wOJ29S5czCAQaJLObI4XxMax84R2m4k,43732
|
|
6
|
+
valid8r/core/validators.py,sha256=oCrRQ2wIPNkqQXy-hJ7sQ9mJAvxtEtGhoy7WvehWqTc,5756
|
|
7
|
+
valid8r/prompt/__init__.py,sha256=XYB3NEp-tmqT6fGmETVEeXd7Urj0M4ijlwdRAjj-rG8,175
|
|
8
|
+
valid8r/prompt/basic.py,sha256=fLWuN-oiVZyaLdbcW5GHWpoGQ82RG0j-1n7uMYDfOb8,6008
|
|
9
|
+
valid8r/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
valid8r/testing/__init__.py,sha256=8mk54zt0Ai2dK0a3GMOTfDPsVQWXaS6uvQJDrkRV9hs,779
|
|
11
|
+
valid8r/testing/assertions.py,sha256=9KGz1JooCoyikyxMX7VuXB9VYAtj-4H_LPYFGdvS-ps,1820
|
|
12
|
+
valid8r/testing/generators.py,sha256=kAV6NRO9x1gPy0BfGs07ETVxjpTIxOZyV9wH2BA1nHA,8791
|
|
13
|
+
valid8r/testing/mock_input.py,sha256=9GRT7h0PCh9Dea-OcQ5Uls7YqhsTdqMWuX6I6ZlW1aw,2334
|
|
14
|
+
valid8r-0.2.0.dist-info/METADATA,sha256=W0YFJg9Sd7E9UU1iyeC1e8_Pwzqzc1QU-tFFzma2A6s,4015
|
|
15
|
+
valid8r-0.2.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
16
|
+
valid8r-0.2.0.dist-info/entry_points.txt,sha256=H_24A4zUgnKIXAIRosJliIcntyqMfmcgKh5_Prl7W18,79
|
|
17
|
+
valid8r-0.2.0.dist-info/RECORD,,
|