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.

@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ docs-build=scripts.docs:build
3
+ docs-serve=scripts.docs:serve
4
+