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,200 @@
1
+ """Core validators for validating values against specific criteria.
2
+
3
+ This module provides a collection of validator functions for common validation scenarios.
4
+ All validators follow the same pattern - they take a value and return a Maybe object
5
+ that either contains the validated value or an error message.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import (
11
+ TYPE_CHECKING,
12
+ Generic,
13
+ Protocol,
14
+ TypeVar,
15
+ )
16
+
17
+ from valid8r.core.combinators import (
18
+ and_then,
19
+ not_validator,
20
+ or_else,
21
+ )
22
+ from valid8r.core.maybe import Maybe
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable
26
+
27
+
28
+ class SupportsComparison(Protocol): # noqa: D101
29
+ def __le__(self, other: object, /) -> bool: ... # noqa: D105
30
+ def __lt__(self, other: object, /) -> bool: ... # noqa: D105
31
+ def __ge__(self, other: object, /) -> bool: ... # noqa: D105
32
+ def __gt__(self, other: object, /) -> bool: ... # noqa: D105
33
+ def __eq__(self, other: object, /) -> bool: ... # noqa: D105
34
+ def __ne__(self, other: object, /) -> bool: ... # noqa: D105
35
+ def __hash__(self, /) -> int: ... # noqa: D105
36
+
37
+
38
+ T = TypeVar('T')
39
+ U = TypeVar('U')
40
+ N = TypeVar('N', bound=SupportsComparison)
41
+
42
+
43
+ class Validator(Generic[T]):
44
+ """A wrapper class for validator functions that supports operator overloading."""
45
+
46
+ def __init__(self, func: Callable[[T], Maybe[T]]) -> None:
47
+ """Initialize a validator with a validation function.
48
+
49
+ Args:
50
+ func: A function that takes a value and returns a Maybe
51
+
52
+ """
53
+ self.func = func
54
+
55
+ def __call__(self, value: T) -> Maybe[T]:
56
+ """Apply the validator to a value.
57
+
58
+ Args:
59
+ value: The value to validate
60
+
61
+ Returns:
62
+ A Maybe containing either the validated value or an error
63
+
64
+ """
65
+ return self.func(value)
66
+
67
+ def __and__(self, other: Validator[T]) -> Validator[T]:
68
+ """Combine with another validator using logical AND.
69
+
70
+ Args:
71
+ other: Another validator to combine with
72
+
73
+ Returns:
74
+ A new validator that passes only if both validators pass
75
+
76
+ """
77
+ return Validator(lambda value: and_then(self.func, other.func)(value))
78
+
79
+ def __or__(self, other: Validator[T]) -> Validator[T]:
80
+ """Combine with another validator using logical OR.
81
+
82
+ Args:
83
+ other: Another validator to combine with
84
+
85
+ Returns:
86
+ A new validator that passes if either validator passes
87
+
88
+ """
89
+ return Validator(lambda value: or_else(self.func, other.func)(value))
90
+
91
+ def __invert__(self) -> Validator[T]:
92
+ """Negate this validator.
93
+
94
+ Returns:
95
+ A new validator that passes if this validator fails
96
+
97
+ """
98
+ return Validator(lambda value: not_validator(self.func, 'Negated validation failed')(value))
99
+
100
+
101
+ def minimum(min_value: N, error_message: str | None = None) -> Validator[N]:
102
+ """Create a validator that ensures a value is at least the minimum.
103
+
104
+ Args:
105
+ min_value: The minimum allowed value
106
+ error_message: Optional custom error message
107
+
108
+ Returns:
109
+ A validator function
110
+
111
+ """
112
+
113
+ def validator(value: N) -> Maybe[N]:
114
+ if value >= min_value:
115
+ return Maybe.success(value)
116
+ return Maybe.failure(error_message or f'Value must be at least {min_value}')
117
+
118
+ return Validator(validator)
119
+
120
+
121
+ def maximum(max_value: N, error_message: str | None = None) -> Validator[N]:
122
+ """Create a validator that ensures a value is at most the maximum.
123
+
124
+ Args:
125
+ max_value: The maximum allowed value
126
+ error_message: Optional custom error message
127
+
128
+ Returns:
129
+ A validator function
130
+
131
+ """
132
+
133
+ def validator(value: N) -> Maybe[N]:
134
+ if value <= max_value:
135
+ return Maybe.success(value)
136
+ return Maybe.failure(error_message or f'Value must be at most {max_value}')
137
+
138
+ return Validator(validator)
139
+
140
+
141
+ def between(min_value: N, max_value: N, error_message: str | None = None) -> Validator[N]:
142
+ """Create a validator that ensures a value is between minimum and maximum (inclusive).
143
+
144
+ Args:
145
+ min_value: The minimum allowed value
146
+ max_value: The maximum allowed value
147
+ error_message: Optional custom error message
148
+
149
+ Returns:
150
+ A validator function
151
+
152
+ """
153
+
154
+ def validator(value: N) -> Maybe[N]:
155
+ if min_value <= value <= max_value:
156
+ return Maybe.success(value)
157
+ return Maybe.failure(error_message or f'Value must be between {min_value} and {max_value}')
158
+
159
+ return Validator(validator)
160
+
161
+
162
+ def predicate(pred: Callable[[T], bool], error_message: str) -> Validator[T]:
163
+ """Create a validator using a custom predicate function.
164
+
165
+ Args:
166
+ pred: A function that takes a value and returns a boolean
167
+ error_message: Error message when validation fails
168
+
169
+ Returns:
170
+ A validator function
171
+
172
+ """
173
+
174
+ def validator(value: T) -> Maybe[T]:
175
+ if pred(value):
176
+ return Maybe.success(value)
177
+ return Maybe.failure(error_message)
178
+
179
+ return Validator(validator)
180
+
181
+
182
+ def length(min_length: int, max_length: int, error_message: str | None = None) -> Validator[str]:
183
+ """Create a validator that ensures a string's length is within bounds.
184
+
185
+ Args:
186
+ min_length: Minimum length of the string
187
+ max_length: Maximum length of the string
188
+ error_message: Optional custom error message
189
+
190
+ Returns:
191
+ A validator function
192
+
193
+ """
194
+
195
+ def validator(value: str) -> Maybe[str]:
196
+ if min_length <= len(value) <= max_length:
197
+ return Maybe.success(value)
198
+ return Maybe.failure(error_message or f'String length must be between {min_length} and {max_length}')
199
+
200
+ return Validator(validator)
@@ -0,0 +1,8 @@
1
+ # valid8r/prompt/__init__.py
2
+ """Input prompting functionality for command-line applications."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from .basic import ask
7
+
8
+ __all__ = ['ask']
@@ -0,0 +1,190 @@
1
+ """Basic input prompting functions with validation support.
2
+
3
+ This module provides functionality for prompting users for input via the command line
4
+ with built-in parsing, validation, and retry logic.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import (
11
+ TYPE_CHECKING,
12
+ Generic,
13
+ TypeVar,
14
+ cast,
15
+ )
16
+
17
+ from valid8r.core.maybe import (
18
+ Failure,
19
+ Maybe,
20
+ Success,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Callable
25
+
26
+ T = TypeVar('T')
27
+
28
+
29
+ @dataclass
30
+ class PromptConfig(Generic[T]):
31
+ """Configuration for the ask function."""
32
+
33
+ parser: Callable[[str], Maybe[T]] | None = None
34
+ validator: Callable[[T], Maybe[T]] | None = None
35
+ error_message: str | None = None
36
+ default: T | None = None
37
+ retry: bool | int = False
38
+ _test_mode: bool = False
39
+
40
+
41
+ def _handle_user_input(prompt_text: str, default: T | None) -> tuple[str, bool]:
42
+ """Handle getting user input and displaying the prompt.
43
+
44
+ Returns:
45
+ A tuple of (user_input, use_default) where use_default is True if the
46
+ default value should be used.
47
+
48
+ """
49
+ # Build prompt text with default if available
50
+ display_prompt = prompt_text
51
+ if default is not None:
52
+ display_prompt = f'{prompt_text} [{default}]: '
53
+
54
+ # Get user input
55
+ user_input = input(display_prompt)
56
+
57
+ # Check if we should use the default value
58
+ use_default = not user_input and default is not None
59
+
60
+ return user_input, use_default
61
+
62
+
63
+ def _process_input(user_input: str, parser: Callable[[str], Maybe[T]], validator: Callable[[T], Maybe[T]]) -> Maybe[T]:
64
+ """Process user input by parsing and validating."""
65
+ # Parse input
66
+ result = parser(user_input)
67
+
68
+ # Validate if parsing was successful
69
+ match result:
70
+ case Success(value):
71
+ return validator(value)
72
+ case Failure(_):
73
+ return result
74
+
75
+ return result # This line is unreachable but keeps type checkers happy pragma: no cover
76
+
77
+
78
+ def ask( # noqa: PLR0913
79
+ prompt_text: str,
80
+ *, # Force all other parameters to be keyword-only
81
+ parser: Callable[[str], Maybe[T]] | None = None,
82
+ validator: Callable[[T], Maybe[T]] | None = None,
83
+ error_message: str | None = None,
84
+ default: T | None = None,
85
+ retry: bool | int = False,
86
+ _test_mode: bool = False,
87
+ ) -> Maybe[T]:
88
+ """Prompt the user for input with validation.
89
+
90
+ Args:
91
+ prompt_text: The prompt to display to the user
92
+ parser: Function to convert string to desired type
93
+ validator: Function to validate the parsed value
94
+ error_message: Custom error message for invalid input
95
+ default: Default value to use if input is empty
96
+ retry: If True or an integer, retry on invalid input
97
+ _test_mode: Hidden parameter for testing the final return path
98
+
99
+ Returns:
100
+ A Maybe containing the validated input or an error
101
+
102
+ Examples:
103
+ >>> # This would prompt the user and validate their input
104
+ >>> from valid8r.core import parsers, validators
105
+ >>> age = ask(
106
+ ... "Enter your age: ",
107
+ ... parser=parsers.parse_int,
108
+ ... validator=validators.minimum(0),
109
+ ... retry=True
110
+ ... )
111
+
112
+ """
113
+ # Create a config object from the parameters
114
+ config = PromptConfig(
115
+ parser=parser,
116
+ validator=validator,
117
+ error_message=error_message,
118
+ default=default,
119
+ retry=retry,
120
+ _test_mode=_test_mode,
121
+ )
122
+
123
+ return _ask_with_config(prompt_text, config)
124
+
125
+
126
+ def _ask_with_config(prompt_text: str, config: PromptConfig[T]) -> Maybe[T]:
127
+ """Implement ask using a PromptConfig object."""
128
+ # For testing the final return path
129
+ if config._test_mode: # noqa: SLF001
130
+ return Maybe.failure(config.error_message or 'Maximum retry attempts reached')
131
+
132
+ # Set default parser and validator if not provided
133
+ def default_parser(s: str) -> Maybe[T]:
134
+ return Maybe.success(cast('T', s))
135
+
136
+ parser: Callable[[str], Maybe[T]] = config.parser if config.parser is not None else default_parser
137
+ validator = config.validator or (lambda v: Maybe.success(v))
138
+
139
+ # Calculate max retries
140
+ max_retries = config.retry if isinstance(config.retry, int) else float('inf') if config.retry else 0
141
+
142
+ return _run_prompt_loop(prompt_text, parser, validator, config.default, max_retries, config.error_message)
143
+
144
+
145
+ def _run_prompt_loop( # noqa: PLR0913
146
+ prompt_text: str,
147
+ parser: Callable[[str], Maybe[T]],
148
+ validator: Callable[[T], Maybe[T]],
149
+ default: T | None,
150
+ max_retries: float,
151
+ error_message: str | None,
152
+ ) -> Maybe[T]:
153
+ """Run the prompt loop with retries."""
154
+ attempt = 0
155
+
156
+ while attempt <= max_retries:
157
+ # Get user input
158
+ user_input, use_default = _handle_user_input(prompt_text, default)
159
+
160
+ # Use default if requested
161
+ if use_default:
162
+ if default is None:
163
+ return Maybe.failure('No default value provided')
164
+ return Maybe.success(default)
165
+ # Process the input
166
+ result = _process_input(user_input, parser, validator)
167
+
168
+ match result:
169
+ case Success(_):
170
+ return result
171
+ case Failure(error):
172
+ # Handle invalid input
173
+ attempt += 1
174
+ if attempt <= max_retries:
175
+ _display_error(error, error_message, max_retries, attempt)
176
+ else:
177
+ return result # Return the failed result after max retries
178
+
179
+ return Maybe.failure(error_message or 'Maximum retry attempts reached')
180
+
181
+
182
+ def _display_error(result_error: str, custom_error: str | None, max_retries: float, attempt: int) -> None:
183
+ """Display error message to the user."""
184
+ err_msg = custom_error or result_error
185
+ remaining = max_retries - attempt if max_retries < float('inf') else None
186
+
187
+ if remaining is not None:
188
+ print(f'Error: {err_msg} ({remaining} attempt(s) remaining)')
189
+ else:
190
+ print(f'Error: {err_msg}')
valid8r/py.typed ADDED
File without changes
@@ -0,0 +1,32 @@
1
+ # valid8r/testing/__init__.py
2
+ """Testing utilities for Valid8r.
3
+
4
+ This module provides tools for testing applications that use Valid8r,
5
+ making it easier to test validation logic, user prompts, and Maybe monads.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from valid8r.testing.assertions import (
11
+ assert_maybe_failure,
12
+ assert_maybe_success,
13
+ )
14
+ from valid8r.testing.generators import (
15
+ generate_random_inputs,
16
+ generate_test_cases,
17
+ test_validator_composition,
18
+ )
19
+ from valid8r.testing.mock_input import (
20
+ MockInputContext,
21
+ configure_mock_input,
22
+ )
23
+
24
+ __all__ = [
25
+ 'MockInputContext',
26
+ 'assert_maybe_failure',
27
+ 'assert_maybe_success',
28
+ 'configure_mock_input',
29
+ 'generate_random_inputs',
30
+ 'generate_test_cases',
31
+ 'test_validator_composition',
32
+ ]
@@ -0,0 +1,67 @@
1
+ """Assertion helpers for testing with Maybe monads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import (
6
+ Any,
7
+ TypeVar,
8
+ )
9
+
10
+ from valid8r.core.maybe import (
11
+ Failure,
12
+ Maybe,
13
+ Success,
14
+ )
15
+
16
+ T = TypeVar('T')
17
+
18
+
19
+ def assert_maybe_success(result: Maybe[T], expected_value: Any) -> bool: # noqa: ANN401
20
+ """Assert that a Maybe is a Success with the expected value.
21
+
22
+ Args:
23
+ result: The Maybe instance to check
24
+ expected_value: The expected value inside the Maybe
25
+
26
+ Returns:
27
+ True if result is a Success with the expected value, False otherwise
28
+
29
+ Examples:
30
+ >>> result = Maybe.success(42)
31
+ >>> assert_maybe_success(result, 42) # Returns True
32
+ >>> assert_maybe_success(result, 43) # Returns False
33
+
34
+ """
35
+ match result:
36
+ case Success(value):
37
+ return bool(value == expected_value)
38
+ case _:
39
+ return False
40
+
41
+
42
+ def assert_maybe_failure(result: Maybe[T], expected_error: str) -> bool:
43
+ """Assert that a Maybe is a Failure with the expected error message.
44
+
45
+ Args:
46
+ result: The Maybe instance to check
47
+ expected_error: The expected error message inside the Maybe
48
+
49
+ Returns:
50
+ True if result is a Failure with the expected error, False otherwise
51
+
52
+ Examples:
53
+ >>> result = Maybe.failure("Invalid input")
54
+ >>> assert_maybe_failure(result, "Invalid input") # Returns True
55
+ >>> assert_maybe_failure(result, "Other error") # Returns False
56
+
57
+ """
58
+ match result:
59
+ case Failure(error):
60
+ return error == expected_error
61
+ case _:
62
+ return False
63
+
64
+
65
+ def assert_error_equals(result: Maybe[T], expected_error: str, default: str = '') -> bool:
66
+ """Assert error via error_or helper."""
67
+ return result.error_or(default) == expected_error