valid8r 0.6.2__py3-none-any.whl → 0.7.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/core/maybe.py CHANGED
@@ -108,6 +108,10 @@ class Success(Maybe[T]):
108
108
  """Get a string representation."""
109
109
  return f'Success({self.value})'
110
110
 
111
+ def __repr__(self) -> str:
112
+ """Get a repr representation for debugging and doctests."""
113
+ return f'Success({self.value!r})'
114
+
111
115
 
112
116
  class Failure(Maybe[T]):
113
117
  """Represents a failed computation with an error message."""
@@ -160,3 +164,7 @@ class Failure(Maybe[T]):
160
164
  def __str__(self) -> str:
161
165
  """Get a string representation."""
162
166
  return f'Failure({self.error})'
167
+
168
+ def __repr__(self) -> str:
169
+ """Get a repr representation for debugging and doctests."""
170
+ return f'Failure({self.error!r})'
valid8r/core/parsers.py CHANGED
@@ -80,7 +80,30 @@ _PHONE_DIGIT_EXTRACTION_PATTERN = re.compile(r'\D')
80
80
 
81
81
 
82
82
  def parse_int(input_value: str, error_message: str | None = None) -> Maybe[int]:
83
- """Parse a string to an integer."""
83
+ """Parse a string to an integer.
84
+
85
+ Converts string representations of integers to Python int values.
86
+ Handles whitespace trimming and accepts whole numbers in float notation (e.g., "42.0").
87
+
88
+ Args:
89
+ input_value: String to parse (leading/trailing whitespace is stripped)
90
+ error_message: Optional custom error message for parsing failures
91
+
92
+ Returns:
93
+ Maybe[int]: Success(int) if parsing succeeds, Failure(str) with error message otherwise
94
+
95
+ Examples:
96
+ >>> parse_int("42")
97
+ Success(42)
98
+ >>> parse_int(" -17 ")
99
+ Success(-17)
100
+ >>> parse_int("42.0")
101
+ Success(42)
102
+ >>> parse_int("42.5").is_failure()
103
+ True
104
+ >>> parse_int("not a number").is_failure()
105
+ True
106
+ """
84
107
  if not input_value:
85
108
  return Maybe.failure('Input must not be empty')
86
109
 
@@ -102,7 +125,28 @@ def parse_int(input_value: str, error_message: str | None = None) -> Maybe[int]:
102
125
 
103
126
 
104
127
  def parse_float(input_value: str, error_message: str | None = None) -> Maybe[float]:
105
- """Parse a string to a float."""
128
+ """Parse a string to a floating-point number.
129
+
130
+ Converts string representations of numbers to Python float values.
131
+ Handles whitespace trimming and scientific notation.
132
+
133
+ Args:
134
+ input_value: String to parse (leading/trailing whitespace is stripped)
135
+ error_message: Optional custom error message for parsing failures
136
+
137
+ Returns:
138
+ Maybe[float]: Success(float) if parsing succeeds, Failure(str) with error message otherwise
139
+
140
+ Examples:
141
+ >>> parse_float("3.14")
142
+ Success(3.14)
143
+ >>> parse_float(" -2.5 ")
144
+ Success(-2.5)
145
+ >>> parse_float("1e-3")
146
+ Success(0.001)
147
+ >>> parse_float("not a number").is_failure()
148
+ True
149
+ """
106
150
  if not input_value:
107
151
  return Maybe.failure('Input must not be empty')
108
152
 
@@ -114,7 +158,33 @@ def parse_float(input_value: str, error_message: str | None = None) -> Maybe[flo
114
158
 
115
159
 
116
160
  def parse_bool(input_value: str, error_message: str | None = None) -> Maybe[bool]:
117
- """Parse a string to a boolean."""
161
+ """Parse a string to a boolean value.
162
+
163
+ Accepts various common representations of true/false values.
164
+ Case-insensitive and handles whitespace.
165
+
166
+ Recognized true values: 'true', 't', 'yes', 'y', '1'
167
+ Recognized false values: 'false', 'f', 'no', 'n', '0'
168
+
169
+ Args:
170
+ input_value: String to parse (leading/trailing whitespace is stripped, case-insensitive)
171
+ error_message: Optional custom error message for parsing failures
172
+
173
+ Returns:
174
+ Maybe[bool]: Success(bool) if parsing succeeds, Failure(str) with error message otherwise
175
+
176
+ Examples:
177
+ >>> parse_bool("true")
178
+ Success(True)
179
+ >>> parse_bool("YES")
180
+ Success(True)
181
+ >>> parse_bool("n")
182
+ Success(False)
183
+ >>> parse_bool(" 0 ")
184
+ Success(False)
185
+ >>> parse_bool("maybe").is_failure()
186
+ True
187
+ """
118
188
  if not input_value:
119
189
  return Maybe.failure('Input must not be empty')
120
190
 
@@ -133,7 +203,27 @@ def parse_bool(input_value: str, error_message: str | None = None) -> Maybe[bool
133
203
 
134
204
 
135
205
  def parse_date(input_value: str, date_format: str | None = None, error_message: str | None = None) -> Maybe[date]:
136
- """Parse a string to a date."""
206
+ """Parse a string to a date object.
207
+
208
+ Parses date strings using ISO 8601 format (YYYY-MM-DD) by default,
209
+ or a custom format if specified.
210
+
211
+ Args:
212
+ input_value: String to parse (leading/trailing whitespace is stripped)
213
+ date_format: Optional strftime format string (e.g., '%Y-%m-%d', '%m/%d/%Y')
214
+ error_message: Optional custom error message for parsing failures
215
+
216
+ Returns:
217
+ Maybe[date]: Success(date) if parsing succeeds, Failure(str) with error message otherwise
218
+
219
+ Examples:
220
+ >>> parse_date("2025-01-15")
221
+ Success(datetime.date(2025, 1, 15))
222
+ >>> parse_date("01/15/2025", date_format="%m/%d/%Y")
223
+ Success(datetime.date(2025, 1, 15))
224
+ >>> parse_date("invalid").is_failure()
225
+ True
226
+ """
137
227
  if not input_value:
138
228
  return Maybe.failure('Input must not be empty')
139
229
 
@@ -157,7 +247,30 @@ def parse_date(input_value: str, date_format: str | None = None, error_message:
157
247
 
158
248
 
159
249
  def parse_complex(input_value: str, error_message: str | None = None) -> Maybe[complex]:
160
- """Parse a string to a complex number."""
250
+ """Parse a string to a complex number.
251
+
252
+ Accepts various complex number representations including both 'j' and 'i' notation.
253
+ Handles parentheses and spaces in the input.
254
+
255
+ Args:
256
+ input_value: String to parse (whitespace is stripped, both 'i' and 'j' accepted)
257
+ error_message: Optional custom error message for parsing failures
258
+
259
+ Returns:
260
+ Maybe[complex]: Success(complex) if parsing succeeds, Failure(str) with error message otherwise
261
+
262
+ Examples:
263
+ >>> parse_complex("3+4j")
264
+ Success((3+4j))
265
+ >>> parse_complex("3 + 4i")
266
+ Success((3+4j))
267
+ >>> parse_complex("(2-3j)")
268
+ Success((2-3j))
269
+ >>> parse_complex("5j")
270
+ Success(5j)
271
+ >>> parse_complex("invalid").is_failure()
272
+ True
273
+ """
161
274
  if not input_value:
162
275
  return Maybe.failure('Input must not be empty')
163
276
 
@@ -187,7 +300,10 @@ def parse_complex(input_value: str, error_message: str | None = None) -> Maybe[c
187
300
 
188
301
 
189
302
  def parse_decimal(input_value: str, error_message: str | None = None) -> Maybe[Decimal]:
190
- """Parse a string to a Decimal.
303
+ """Parse a string to a Decimal for precise decimal arithmetic.
304
+
305
+ Uses Python's Decimal type for arbitrary-precision decimal arithmetic,
306
+ avoiding floating-point rounding errors. Ideal for financial calculations.
191
307
 
192
308
  Args:
193
309
  input_value: String representation of a decimal number
@@ -196,6 +312,15 @@ def parse_decimal(input_value: str, error_message: str | None = None) -> Maybe[D
196
312
  Returns:
197
313
  Maybe[Decimal]: Success with Decimal value or Failure with an error message
198
314
 
315
+ Examples:
316
+ >>> parse_decimal("3.14159")
317
+ Success(Decimal('3.14159'))
318
+ >>> parse_decimal(" 0.1 ")
319
+ Success(Decimal('0.1'))
320
+ >>> parse_decimal("-99.99")
321
+ Success(Decimal('-99.99'))
322
+ >>> parse_decimal("not a number").is_failure()
323
+ True
199
324
  """
200
325
  if not input_value:
201
326
  return Maybe.failure('Input must not be empty')
@@ -229,7 +354,34 @@ def _find_enum_by_name(enum_class: type[E], value: str) -> E | None:
229
354
 
230
355
 
231
356
  def parse_enum(input_value: str, enum_class: type[E], error_message: str | None = None) -> Maybe[object]:
232
- """Parse a string to an enum value."""
357
+ """Parse a string to an enum member.
358
+
359
+ Matches input against enum member values and names (case-insensitive for names).
360
+ Handles whitespace trimming and supports enums with empty string values.
361
+
362
+ Args:
363
+ input_value: String to parse (whitespace is stripped for non-exact matches)
364
+ enum_class: The Enum class to parse into
365
+ error_message: Optional custom error message for parsing failures
366
+
367
+ Returns:
368
+ Maybe[object]: Success with enum member if valid, Failure(str) with error message otherwise
369
+
370
+ Examples:
371
+ >>> from enum import Enum
372
+ >>> class Color(Enum):
373
+ ... RED = 'red'
374
+ ... GREEN = 'green'
375
+ ... BLUE = 'blue'
376
+ >>> parse_enum("red", Color)
377
+ Success(<Color.RED: 'red'>)
378
+ >>> parse_enum("RED", Color)
379
+ Success(<Color.RED: 'red'>)
380
+ >>> parse_enum(" green ", Color)
381
+ Success(<Color.GREEN: 'green'>)
382
+ >>> parse_enum("yellow", Color).is_failure()
383
+ True
384
+ """
233
385
  if not isinstance(enum_class, type) or not issubclass(enum_class, Enum):
234
386
  return Maybe.failure(error_message or 'Invalid enum class provided')
235
387
 
@@ -269,15 +421,27 @@ def parse_list(
269
421
  ) -> Maybe[list[T]]:
270
422
  """Parse a string to a list using the specified element parser and separator.
271
423
 
424
+ Splits the input string by the separator and parses each element using the element parser.
425
+ If no element parser is provided, elements are returned as trimmed strings.
426
+
272
427
  Args:
273
428
  input_value: The string to parse
274
- element_parser: A function that parses individual elements
275
- separator: The string that separates elements
429
+ element_parser: A function that parses individual elements (default: strips whitespace)
430
+ separator: The string that separates elements (default: ',')
276
431
  error_message: Custom error message for parsing failures
277
432
 
278
433
  Returns:
279
- A Maybe containing the parsed list or an error message
434
+ Maybe[list[T]]: Success with parsed list or Failure with error message
280
435
 
436
+ Examples:
437
+ >>> parse_list("a,b,c")
438
+ Success(['a', 'b', 'c'])
439
+ >>> parse_list("1, 2, 3", element_parser=parse_int)
440
+ Success([1, 2, 3])
441
+ >>> parse_list("apple|banana|cherry", separator="|")
442
+ Success(['apple', 'banana', 'cherry'])
443
+ >>> parse_list("1,2,invalid", element_parser=parse_int).is_failure()
444
+ True
281
445
  """
282
446
  if not input_value:
283
447
  return Maybe.failure('Input must not be empty')
@@ -358,7 +522,32 @@ def parse_dict( # noqa: PLR0913
358
522
  key_value_separator: str = ':',
359
523
  error_message: str | None = None,
360
524
  ) -> Maybe[dict[K, V]]:
361
- """Parse a string to a dictionary using the specified parsers and separators."""
525
+ """Parse a string to a dictionary using the specified parsers and separators.
526
+
527
+ Splits the input string by pair_separator, then splits each pair by key_value_separator.
528
+ Parses keys and values using the provided parsers (defaults to trimmed strings).
529
+
530
+ Args:
531
+ input_value: The string to parse
532
+ key_parser: A function that parses keys (default: strips whitespace)
533
+ value_parser: A function that parses values (default: strips whitespace)
534
+ pair_separator: The string that separates key-value pairs (default: ',')
535
+ key_value_separator: The string that separates keys from values (default: ':')
536
+ error_message: Custom error message for parsing failures
537
+
538
+ Returns:
539
+ Maybe[dict[K, V]]: Success with parsed dictionary or Failure with error message
540
+
541
+ Examples:
542
+ >>> parse_dict("a:1,b:2,c:3")
543
+ Success({'a': '1', 'b': '2', 'c': '3'})
544
+ >>> parse_dict("x:10, y:20", value_parser=parse_int)
545
+ Success({'x': 10, 'y': 20})
546
+ >>> parse_dict("name=Alice|age=30", pair_separator="|", key_value_separator="=")
547
+ Success({'name': 'Alice', 'age': '30'})
548
+ >>> parse_dict("a:1,b:invalid", value_parser=parse_int).is_failure()
549
+ True
550
+ """
362
551
  if not input_value:
363
552
  return Maybe.failure('Input must not be empty')
364
553
 
@@ -402,15 +591,33 @@ def parse_set(
402
591
  ) -> Maybe[set[T]]:
403
592
  """Parse a string to a set using the specified element parser and separator.
404
593
 
594
+ Splits the input string by the separator and parses each element using the element parser.
595
+ Automatically removes duplicate values. If no element parser is provided, elements are
596
+ returned as trimmed strings.
597
+
405
598
  Args:
406
599
  input_value: The string to parse
407
- element_parser: A function that parses individual elements
408
- separator: The string that separates elements
600
+ element_parser: A function that parses individual elements (default: strips whitespace)
601
+ separator: The string that separates elements (default: ',')
409
602
  error_message: Custom error message for parsing failures
410
603
 
411
604
  Returns:
412
- A Maybe containing the parsed set or an error message
605
+ Maybe[set[T]]: Success with parsed set or Failure with error message
413
606
 
607
+ Examples:
608
+ >>> result = parse_set("a,b,c")
609
+ >>> result.is_success()
610
+ True
611
+ >>> sorted(result.value_or(set()))
612
+ ['a', 'b', 'c']
613
+ >>> result = parse_set("1, 2, 3, 2, 1", element_parser=parse_int)
614
+ >>> sorted(result.value_or(set()))
615
+ [1, 2, 3]
616
+ >>> result = parse_set("red|blue|green|red", separator="|")
617
+ >>> sorted(result.value_or(set()))
618
+ ['blue', 'green', 'red']
619
+ >>> parse_set("1,2,invalid", element_parser=parse_int).is_failure()
620
+ True
414
621
  """
415
622
  if separator is None:
416
623
  separator = ','
@@ -433,7 +640,10 @@ def parse_int_with_validation(
433
640
  max_value: int | None = None,
434
641
  error_message: str | None = None,
435
642
  ) -> Maybe[int]:
436
- """Parse a string to an integer with validation.
643
+ """Parse a string to an integer with range validation.
644
+
645
+ Combines parsing and validation in a single step. First parses the string to an integer,
646
+ then validates it falls within the specified range.
437
647
 
438
648
  Args:
439
649
  input_value: The string to parse
@@ -442,8 +652,17 @@ def parse_int_with_validation(
442
652
  error_message: Custom error message for parsing failures
443
653
 
444
654
  Returns:
445
- A Maybe containing the parsed integer or an error message
655
+ Maybe[int]: Success with validated integer or Failure with error message
446
656
 
657
+ Examples:
658
+ >>> parse_int_with_validation("42", min_value=0, max_value=100)
659
+ Success(42)
660
+ >>> parse_int_with_validation("5", min_value=10).is_failure()
661
+ True
662
+ >>> parse_int_with_validation("150", max_value=100).is_failure()
663
+ True
664
+ >>> parse_int_with_validation("50", min_value=0, max_value=100)
665
+ Success(50)
447
666
  """
448
667
  result = parse_int(input_value, error_message)
449
668
  if result.is_failure():
@@ -469,7 +688,10 @@ def parse_list_with_validation( # noqa: PLR0913
469
688
  max_length: int | None = None,
470
689
  error_message: str | None = None,
471
690
  ) -> Maybe[list[T]]:
472
- """Parse a string to a list with validation.
691
+ """Parse a string to a list with length validation.
692
+
693
+ Combines parsing and validation in a single step. First parses the string to a list,
694
+ then validates it has an acceptable number of elements.
473
695
 
474
696
  Args:
475
697
  input_value: The string to parse
@@ -480,8 +702,17 @@ def parse_list_with_validation( # noqa: PLR0913
480
702
  error_message: Custom error message for parsing failures
481
703
 
482
704
  Returns:
483
- A Maybe containing the parsed list or an error message
705
+ Maybe[list[T]]: Success with validated list or Failure with error message
484
706
 
707
+ Examples:
708
+ >>> parse_list_with_validation("a,b,c", min_length=2, max_length=5)
709
+ Success(['a', 'b', 'c'])
710
+ >>> parse_list_with_validation("1,2", element_parser=parse_int, min_length=3).is_failure()
711
+ True
712
+ >>> parse_list_with_validation("1,2,3,4,5,6", max_length=5).is_failure()
713
+ True
714
+ >>> parse_list_with_validation("10,20,30", element_parser=parse_int, min_length=1)
715
+ Success([10, 20, 30])
485
716
  """
486
717
  result = parse_list(input_value, element_parser, separator, error_message)
487
718
  if result.is_failure():
@@ -508,7 +739,10 @@ def parse_dict_with_validation( # noqa: PLR0913
508
739
  required_keys: list[str] | None = None,
509
740
  error_message: str | None = None,
510
741
  ) -> Maybe[dict[K, V]]:
511
- """Parse a string to a dictionary with validation.
742
+ """Parse a string to a dictionary with required keys validation.
743
+
744
+ Combines parsing and validation in a single step. First parses the string to a dictionary,
745
+ then validates that all required keys are present.
512
746
 
513
747
  Args:
514
748
  input_value: The string to parse
@@ -520,8 +754,16 @@ def parse_dict_with_validation( # noqa: PLR0913
520
754
  error_message: Custom error message for parsing failures
521
755
 
522
756
  Returns:
523
- A Maybe containing the parsed dictionary or an error message
757
+ Maybe[dict[K, V]]: Success with validated dictionary or Failure with error message
524
758
 
759
+ Examples:
760
+ >>> parse_dict_with_validation("name:Alice,age:30", required_keys=["name", "age"])
761
+ Success({'name': 'Alice', 'age': '30'})
762
+ >>> parse_dict_with_validation("name:Bob", required_keys=["name", "age"]).is_failure()
763
+ True
764
+ >>> result = parse_dict_with_validation("x:10,y:20", value_parser=parse_int, required_keys=["x"])
765
+ >>> result.value_or({})
766
+ {'x': 10, 'y': 20}
525
767
  """
526
768
  result = parse_dict(input_value, key_parser, value_parser, pair_separator, key_value_separator, error_message)
527
769
  if result.is_failure():
@@ -705,13 +947,24 @@ def parse_uuid(text: str, version: int | None = None, strict: bool = True) -> Ma
705
947
  def parse_ipv4(text: str) -> Maybe[IPv4Address]:
706
948
  """Parse an IPv4 address string.
707
949
 
708
- Trims surrounding whitespace only. Returns Success with a concrete
709
- IPv4Address on success, or Failure with a deterministic error message.
950
+ Validates and parses IPv4 addresses in dotted-decimal notation.
951
+ Trims surrounding whitespace.
952
+
953
+ Args:
954
+ text: String containing an IPv4 address (whitespace is stripped)
955
+
956
+ Returns:
957
+ Maybe[IPv4Address]: Success(IPv4Address) if valid, Failure(str) with error message otherwise
710
958
 
711
- Error messages:
712
- - value must be a string
713
- - value is empty
714
- - not a valid IPv4 address
959
+ Examples:
960
+ >>> parse_ipv4("192.168.1.1")
961
+ Success(IPv4Address('192.168.1.1'))
962
+ >>> parse_ipv4(" 10.0.0.1 ")
963
+ Success(IPv4Address('10.0.0.1'))
964
+ >>> parse_ipv4("256.1.1.1").is_failure()
965
+ True
966
+ >>> parse_ipv4("not an ip").is_failure()
967
+ True
715
968
  """
716
969
  if not isinstance(text, str):
717
970
  return Maybe.failure('Input must be a string')
@@ -734,13 +987,24 @@ def parse_ipv4(text: str) -> Maybe[IPv4Address]:
734
987
  def parse_ipv6(text: str) -> Maybe[IPv6Address]:
735
988
  """Parse an IPv6 address string.
736
989
 
737
- Trims surrounding whitespace only. Returns Success with a concrete
738
- IPv6Address on success, or Failure with a deterministic error message.
990
+ Validates and parses IPv6 addresses in standard notation.
991
+ Rejects scope IDs (e.g., %eth0). Trims surrounding whitespace.
992
+
993
+ Args:
994
+ text: String containing an IPv6 address (whitespace is stripped)
995
+
996
+ Returns:
997
+ Maybe[IPv6Address]: Success(IPv6Address) if valid, Failure(str) with error message otherwise
739
998
 
740
- Error messages:
741
- - value must be a string
742
- - value is empty
743
- - not a valid IPv6 address
999
+ Examples:
1000
+ >>> parse_ipv6("::1")
1001
+ Success(IPv6Address('::1'))
1002
+ >>> parse_ipv6("2001:0db8:85a3::8a2e:0370:7334")
1003
+ Success(IPv6Address('2001:db8:85a3::8a2e:370:7334'))
1004
+ >>> parse_ipv6(" fe80::1 ")
1005
+ Success(IPv6Address('fe80::1'))
1006
+ >>> parse_ipv6("192.168.1.1").is_failure()
1007
+ True
744
1008
  """
745
1009
  if not isinstance(text, str):
746
1010
  return Maybe.failure('Input must be a string')
@@ -767,12 +1031,27 @@ def parse_ipv6(text: str) -> Maybe[IPv6Address]:
767
1031
  def parse_ip(text: str) -> Maybe[IPv4Address | IPv6Address]:
768
1032
  """Parse a string as either an IPv4 or IPv6 address.
769
1033
 
770
- Trims surrounding whitespace only.
1034
+ Automatically detects and parses either IPv4 or IPv6 addresses.
1035
+ Trims surrounding whitespace.
771
1036
 
772
- Error messages:
773
- - value must be a string
774
- - value is empty
775
- - not a valid IP address
1037
+ Args:
1038
+ text: String containing an IP address (IPv4 or IPv6, whitespace is stripped)
1039
+
1040
+ Returns:
1041
+ Maybe[IPv4Address | IPv6Address]: Success with IPv4Address or IPv6Address if valid,
1042
+ Failure(str) with error message otherwise
1043
+
1044
+ Examples:
1045
+ >>> result = parse_ip("192.168.1.1")
1046
+ >>> result.is_success()
1047
+ True
1048
+ >>> result = parse_ip("::1")
1049
+ >>> result.is_success()
1050
+ True
1051
+ >>> parse_ip(" 10.0.0.1 ")
1052
+ Success(IPv4Address('10.0.0.1'))
1053
+ >>> parse_ip("not an ip").is_failure()
1054
+ True
776
1055
  """
777
1056
  if not isinstance(text, str):
778
1057
  return Maybe.failure('Input must be a string')
@@ -799,14 +1078,32 @@ def parse_ip(text: str) -> Maybe[IPv4Address | IPv6Address]:
799
1078
  def parse_cidr(text: str, *, strict: bool = True) -> Maybe[IPv4Network | IPv6Network]:
800
1079
  """Parse a CIDR network string (IPv4 or IPv6).
801
1080
 
802
- Uses ipaddress.ip_network under the hood. By default ``strict=True``
803
- so host bits set will fail. With ``strict=False``, host bits are masked.
1081
+ Validates and parses network addresses in CIDR notation (e.g., 192.168.1.0/24).
1082
+ By default, validates that host bits are not set (strict mode).
1083
+ With strict=False, host bits are masked to the network address.
1084
+
1085
+ Args:
1086
+ text: String containing a CIDR network (whitespace is stripped)
1087
+ strict: If True, reject networks with host bits set; if False, mask them (default: True)
1088
+
1089
+ Returns:
1090
+ Maybe[IPv4Network | IPv6Network]: Success with IPv4Network or IPv6Network if valid,
1091
+ Failure(str) with error message otherwise
804
1092
 
805
- Error messages:
806
- - value must be a string
807
- - value is empty
808
- - has host bits set (when strict and host bits are present)
809
- - not a valid network (all other parsing failures)
1093
+ Examples:
1094
+ >>> parse_cidr("192.168.1.0/24")
1095
+ Success(IPv4Network('192.168.1.0/24'))
1096
+ >>> parse_cidr("10.0.0.0/8")
1097
+ Success(IPv4Network('10.0.0.0/8'))
1098
+ >>> parse_cidr("2001:db8::/32")
1099
+ Success(IPv6Network('2001:db8::/32'))
1100
+ >>> # Strict mode rejects host bits
1101
+ >>> parse_cidr("192.168.1.5/24").is_failure()
1102
+ True
1103
+ >>> # Non-strict mode masks host bits
1104
+ >>> result = parse_cidr("192.168.1.5/24", strict=False)
1105
+ >>> str(result.value_or(None))
1106
+ '192.168.1.0/24'
810
1107
  """
811
1108
  if not isinstance(text, str):
812
1109
  return Maybe.failure('Input must be a string')
@@ -7,6 +7,7 @@ that either contains the validated value or an error message.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import re
10
11
  from typing import (
11
12
  TYPE_CHECKING,
12
13
  Generic,
@@ -102,11 +103,25 @@ def minimum(min_value: N, error_message: str | None = None) -> Validator[N]:
102
103
  """Create a validator that ensures a value is at least the minimum.
103
104
 
104
105
  Args:
105
- min_value: The minimum allowed value
106
+ min_value: The minimum allowed value (inclusive)
106
107
  error_message: Optional custom error message
107
108
 
108
109
  Returns:
109
- A validator function
110
+ Validator[N]: A validator function that accepts values >= min_value
111
+
112
+ Examples:
113
+ >>> from valid8r.core.validators import minimum
114
+ >>> validator = minimum(0)
115
+ >>> validator(5)
116
+ Success(5)
117
+ >>> validator(0)
118
+ Success(0)
119
+ >>> validator(-1).is_failure()
120
+ True
121
+ >>> # With custom error message
122
+ >>> validator = minimum(18, error_message="Must be an adult")
123
+ >>> validator(17).error_or("")
124
+ 'Must be an adult'
110
125
 
111
126
  """
112
127
 
@@ -122,11 +137,25 @@ def maximum(max_value: N, error_message: str | None = None) -> Validator[N]:
122
137
  """Create a validator that ensures a value is at most the maximum.
123
138
 
124
139
  Args:
125
- max_value: The maximum allowed value
140
+ max_value: The maximum allowed value (inclusive)
126
141
  error_message: Optional custom error message
127
142
 
128
143
  Returns:
129
- A validator function
144
+ Validator[N]: A validator function that accepts values <= max_value
145
+
146
+ Examples:
147
+ >>> from valid8r.core.validators import maximum
148
+ >>> validator = maximum(100)
149
+ >>> validator(50)
150
+ Success(50)
151
+ >>> validator(100)
152
+ Success(100)
153
+ >>> validator(101).is_failure()
154
+ True
155
+ >>> # With custom error message
156
+ >>> validator = maximum(120, error_message="Age too high")
157
+ >>> validator(150).error_or("")
158
+ 'Age too high'
130
159
 
131
160
  """
132
161
 
@@ -142,12 +171,30 @@ def between(min_value: N, max_value: N, error_message: str | None = None) -> Val
142
171
  """Create a validator that ensures a value is between minimum and maximum (inclusive).
143
172
 
144
173
  Args:
145
- min_value: The minimum allowed value
146
- max_value: The maximum allowed value
174
+ min_value: The minimum allowed value (inclusive)
175
+ max_value: The maximum allowed value (inclusive)
147
176
  error_message: Optional custom error message
148
177
 
149
178
  Returns:
150
- A validator function
179
+ Validator[N]: A validator function that accepts values where min_value <= value <= max_value
180
+
181
+ Examples:
182
+ >>> from valid8r.core.validators import between
183
+ >>> validator = between(0, 100)
184
+ >>> validator(50)
185
+ Success(50)
186
+ >>> validator(0)
187
+ Success(0)
188
+ >>> validator(100)
189
+ Success(100)
190
+ >>> validator(-1).is_failure()
191
+ True
192
+ >>> validator(101).is_failure()
193
+ True
194
+ >>> # With custom error message
195
+ >>> validator = between(1, 10, error_message="Rating must be 1-10")
196
+ >>> validator(11).error_or("")
197
+ 'Rating must be 1-10'
151
198
 
152
199
  """
153
200
 
@@ -162,12 +209,30 @@ def between(min_value: N, max_value: N, error_message: str | None = None) -> Val
162
209
  def predicate(pred: Callable[[T], bool], error_message: str) -> Validator[T]:
163
210
  """Create a validator using a custom predicate function.
164
211
 
212
+ Allows creating custom validators for any validation logic by providing
213
+ a predicate function that returns True for valid values.
214
+
165
215
  Args:
166
- pred: A function that takes a value and returns a boolean
167
- error_message: Error message when validation fails
216
+ pred: A function that takes a value and returns True if valid, False otherwise
217
+ error_message: Error message to return when validation fails
168
218
 
169
219
  Returns:
170
- A validator function
220
+ Validator[T]: A validator function that applies the predicate
221
+
222
+ Examples:
223
+ >>> from valid8r.core.validators import predicate
224
+ >>> # Validate even numbers
225
+ >>> is_even = predicate(lambda x: x % 2 == 0, "Must be even")
226
+ >>> is_even(4)
227
+ Success(4)
228
+ >>> is_even(3).is_failure()
229
+ True
230
+ >>> # Validate string patterns
231
+ >>> starts_with_a = predicate(lambda s: s.startswith('a'), "Must start with 'a'")
232
+ >>> starts_with_a("apple")
233
+ Success('apple')
234
+ >>> starts_with_a("banana").error_or("")
235
+ "Must start with 'a'"
171
236
 
172
237
  """
173
238
 
@@ -183,12 +248,30 @@ def length(min_length: int, max_length: int, error_message: str | None = None) -
183
248
  """Create a validator that ensures a string's length is within bounds.
184
249
 
185
250
  Args:
186
- min_length: Minimum length of the string
187
- max_length: Maximum length of the string
251
+ min_length: Minimum length of the string (inclusive)
252
+ max_length: Maximum length of the string (inclusive)
188
253
  error_message: Optional custom error message
189
254
 
190
255
  Returns:
191
- A validator function
256
+ Validator[str]: A validator function that checks string length
257
+
258
+ Examples:
259
+ >>> from valid8r.core.validators import length
260
+ >>> validator = length(3, 10)
261
+ >>> validator("hello")
262
+ Success('hello')
263
+ >>> validator("abc")
264
+ Success('abc')
265
+ >>> validator("abcdefghij")
266
+ Success('abcdefghij')
267
+ >>> validator("ab").is_failure()
268
+ True
269
+ >>> validator("abcdefghijk").is_failure()
270
+ True
271
+ >>> # With custom error message
272
+ >>> validator = length(8, 20, error_message="Password must be 8-20 characters")
273
+ >>> validator("short").error_or("")
274
+ 'Password must be 8-20 characters'
192
275
 
193
276
  """
194
277
 
@@ -198,3 +281,285 @@ def length(min_length: int, max_length: int, error_message: str | None = None) -
198
281
  return Maybe.failure(error_message or f'String length must be between {min_length} and {max_length}')
199
282
 
200
283
  return Validator(validator)
284
+
285
+
286
+ def matches_regex(pattern: str | re.Pattern[str], error_message: str | None = None) -> Validator[str]:
287
+ r"""Create a validator that ensures a string matches a regular expression pattern.
288
+
289
+ Args:
290
+ pattern: Regular expression pattern (string or compiled Pattern object)
291
+ error_message: Optional custom error message
292
+
293
+ Returns:
294
+ Validator[str]: A validator function that checks pattern matching
295
+
296
+ Examples:
297
+ >>> from valid8r.core.validators import matches_regex
298
+ >>> import re
299
+ >>> # String pattern
300
+ >>> validator = matches_regex(r'^\\d{3}-\\d{2}-\\d{4}$')
301
+ >>> validator('123-45-6789')
302
+ Success('123-45-6789')
303
+ >>> validator('invalid').is_failure()
304
+ True
305
+ >>> # Compiled regex pattern
306
+ >>> pattern = re.compile(r'^[A-Z][a-z]+$')
307
+ >>> validator = matches_regex(pattern)
308
+ >>> validator('Hello')
309
+ Success('Hello')
310
+ >>> validator('hello').is_failure()
311
+ True
312
+ >>> # With custom error message
313
+ >>> validator = matches_regex(r'^\\d{5}$', error_message='Must be a 5-digit ZIP code')
314
+ >>> validator('1234').error_or('')
315
+ 'Must be a 5-digit ZIP code'
316
+
317
+ """
318
+ compiled_pattern = re.compile(pattern) if isinstance(pattern, str) else pattern
319
+
320
+ def validator(value: str) -> Maybe[str]:
321
+ if compiled_pattern.match(value):
322
+ return Maybe.success(value)
323
+ return Maybe.failure(error_message or f'Value must match pattern {compiled_pattern.pattern}')
324
+
325
+ return Validator(validator)
326
+
327
+
328
+ def in_set(allowed_values: set[T], error_message: str | None = None) -> Validator[T]:
329
+ """Create a validator that ensures a value is in a set of allowed values.
330
+
331
+ Args:
332
+ allowed_values: Set of allowed values
333
+ error_message: Optional custom error message
334
+
335
+ Returns:
336
+ Validator[T]: A validator function that checks membership
337
+
338
+ Examples:
339
+ >>> from valid8r.core.validators import in_set
340
+ >>> # String values
341
+ >>> validator = in_set({'red', 'green', 'blue'})
342
+ >>> validator('red')
343
+ Success('red')
344
+ >>> validator('yellow').is_failure()
345
+ True
346
+ >>> # Numeric values
347
+ >>> validator = in_set({1, 2, 3, 4, 5})
348
+ >>> validator(3)
349
+ Success(3)
350
+ >>> validator(10).is_failure()
351
+ True
352
+ >>> # With custom error message
353
+ >>> validator = in_set({'small', 'medium', 'large'}, error_message='Size must be S, M, or L')
354
+ >>> validator('extra-large').error_or('')
355
+ 'Size must be S, M, or L'
356
+
357
+ """
358
+
359
+ def validator(value: T) -> Maybe[T]:
360
+ if value in allowed_values:
361
+ return Maybe.success(value)
362
+ return Maybe.failure(error_message or f'Value must be one of {allowed_values}')
363
+
364
+ return Validator(validator)
365
+
366
+
367
+ def non_empty_string(error_message: str | None = None) -> Validator[str]:
368
+ """Create a validator that ensures a string is not empty.
369
+
370
+ Validates that a string contains at least one non-whitespace character.
371
+ Both empty strings and whitespace-only strings are rejected.
372
+
373
+ Args:
374
+ error_message: Optional custom error message
375
+
376
+ Returns:
377
+ Validator[str]: A validator function that checks for non-empty strings
378
+
379
+ Examples:
380
+ >>> from valid8r.core.validators import non_empty_string
381
+ >>> validator = non_empty_string()
382
+ >>> validator('hello')
383
+ Success('hello')
384
+ >>> validator(' hello ')
385
+ Success(' hello ')
386
+ >>> validator('').is_failure()
387
+ True
388
+ >>> validator(' ').is_failure()
389
+ True
390
+ >>> # With custom error message
391
+ >>> validator = non_empty_string(error_message='Name is required')
392
+ >>> validator('').error_or('')
393
+ 'Name is required'
394
+
395
+ """
396
+
397
+ def validator(value: str) -> Maybe[str]:
398
+ if value.strip():
399
+ return Maybe.success(value)
400
+ return Maybe.failure(error_message or 'String must not be empty')
401
+
402
+ return Validator(validator)
403
+
404
+
405
+ def unique_items(error_message: str | None = None) -> Validator[list[T]]:
406
+ """Create a validator that ensures all items in a list are unique.
407
+
408
+ Validates that a list contains no duplicate elements by comparing
409
+ the list length to the set length.
410
+
411
+ Args:
412
+ error_message: Optional custom error message
413
+
414
+ Returns:
415
+ Validator[list[T]]: A validator function that checks for unique items
416
+
417
+ Examples:
418
+ >>> from valid8r.core.validators import unique_items
419
+ >>> validator = unique_items()
420
+ >>> validator([1, 2, 3, 4, 5])
421
+ Success([1, 2, 3, 4, 5])
422
+ >>> validator([1, 2, 2, 3]).is_failure()
423
+ True
424
+ >>> # Works with strings
425
+ >>> validator(['a', 'b', 'c'])
426
+ Success(['a', 'b', 'c'])
427
+ >>> validator(['a', 'b', 'a']).is_failure()
428
+ True
429
+ >>> # With custom error message
430
+ >>> validator = unique_items(error_message='Duplicate items found')
431
+ >>> validator([1, 1, 2]).error_or('')
432
+ 'Duplicate items found'
433
+
434
+ """
435
+
436
+ def validator(value: list[T]) -> Maybe[list[T]]:
437
+ if len(value) == len(set(value)):
438
+ return Maybe.success(value)
439
+ return Maybe.failure(error_message or 'All items must be unique')
440
+
441
+ return Validator(validator)
442
+
443
+
444
+ def subset_of(allowed_set: set[T], error_message: str | None = None) -> Validator[set[T]]:
445
+ """Create a validator that ensures a set is a subset of allowed values.
446
+
447
+ Validates that all elements in the input set are contained within
448
+ the allowed set. An empty set is always a valid subset.
449
+
450
+ Args:
451
+ allowed_set: The set of allowed values
452
+ error_message: Optional custom error message
453
+
454
+ Returns:
455
+ Validator[set[T]]: A validator function that checks subset relationship
456
+
457
+ Examples:
458
+ >>> from valid8r.core.validators import subset_of
459
+ >>> validator = subset_of({1, 2, 3, 4, 5})
460
+ >>> validator({1, 2, 3})
461
+ Success({1, 2, 3})
462
+ >>> validator({1, 2, 3, 4, 5, 6}).is_failure()
463
+ True
464
+ >>> # Empty set is valid subset
465
+ >>> validator(set())
466
+ Success(set())
467
+ >>> # With custom error message
468
+ >>> validator = subset_of({'a', 'b', 'c'}, error_message='Invalid characters')
469
+ >>> validator({'a', 'd'}).error_or('')
470
+ 'Invalid characters'
471
+
472
+ """
473
+
474
+ def validator(value: set[T]) -> Maybe[set[T]]:
475
+ if value.issubset(allowed_set):
476
+ return Maybe.success(value)
477
+ return Maybe.failure(error_message or f'Value must be a subset of {allowed_set}')
478
+
479
+ return Validator(validator)
480
+
481
+
482
+ def superset_of(required_set: set[T], error_message: str | None = None) -> Validator[set[T]]:
483
+ """Create a validator that ensures a set is a superset of required values.
484
+
485
+ Validates that the input set contains all elements from the required set.
486
+ The input set may contain additional elements beyond those required.
487
+
488
+ Args:
489
+ required_set: The set of required values
490
+ error_message: Optional custom error message
491
+
492
+ Returns:
493
+ Validator[set[T]]: A validator function that checks superset relationship
494
+
495
+ Examples:
496
+ >>> from valid8r.core.validators import superset_of
497
+ >>> validator = superset_of({1, 2, 3})
498
+ >>> validator({1, 2, 3, 4, 5})
499
+ Success({1, 2, 3, 4, 5})
500
+ >>> validator({1, 2}).is_failure()
501
+ True
502
+ >>> # Exact match is valid
503
+ >>> validator({1, 2, 3})
504
+ Success({1, 2, 3})
505
+ >>> # With custom error message
506
+ >>> validator = superset_of({'read', 'write'}, error_message='Missing required permissions')
507
+ >>> validator({'read'}).error_or('')
508
+ 'Missing required permissions'
509
+
510
+ """
511
+
512
+ def validator(value: set[T]) -> Maybe[set[T]]:
513
+ if value.issuperset(required_set):
514
+ return Maybe.success(value)
515
+ return Maybe.failure(error_message or f'Value must be a superset of {required_set}')
516
+
517
+ return Validator(validator)
518
+
519
+
520
+ def is_sorted(*, reverse: bool = False, error_message: str | None = None) -> Validator[list[N]]:
521
+ """Create a validator that ensures a list is sorted.
522
+
523
+ Validates that a list is sorted in either ascending or descending order.
524
+ Uses keyword-only parameters to avoid boolean trap anti-pattern.
525
+
526
+ Args:
527
+ reverse: If True, checks for descending order; otherwise ascending (default)
528
+ error_message: Optional custom error message
529
+
530
+ Returns:
531
+ Validator[list[N]]: A validator function that checks if list is sorted
532
+
533
+ Examples:
534
+ >>> from valid8r.core.validators import is_sorted
535
+ >>> # Ascending order (default)
536
+ >>> validator = is_sorted()
537
+ >>> validator([1, 2, 3, 4, 5])
538
+ Success([1, 2, 3, 4, 5])
539
+ >>> validator([3, 1, 4, 2]).is_failure()
540
+ True
541
+ >>> # Descending order
542
+ >>> validator = is_sorted(reverse=True)
543
+ >>> validator([5, 4, 3, 2, 1])
544
+ Success([5, 4, 3, 2, 1])
545
+ >>> validator([1, 2, 3]).is_failure()
546
+ True
547
+ >>> # Works with strings
548
+ >>> validator = is_sorted()
549
+ >>> validator(['a', 'b', 'c'])
550
+ Success(['a', 'b', 'c'])
551
+ >>> # With custom error message
552
+ >>> validator = is_sorted(error_message='List must be in order')
553
+ >>> validator([3, 1, 2]).error_or('')
554
+ 'List must be in order'
555
+
556
+ """
557
+
558
+ def validator(value: list[N]) -> Maybe[list[N]]:
559
+ sorted_value = sorted(value, reverse=reverse)
560
+ if value == sorted_value:
561
+ return Maybe.success(value)
562
+ direction = 'descending' if reverse else 'ascending'
563
+ return Maybe.failure(error_message or f'List must be sorted in {direction} order')
564
+
565
+ return Validator(validator)
valid8r/prompt/basic.py CHANGED
@@ -85,29 +85,68 @@ def ask( # noqa: PLR0913
85
85
  retry: bool | int = False,
86
86
  _test_mode: bool = False,
87
87
  ) -> Maybe[T]:
88
- """Prompt the user for input with validation.
88
+ """Prompt the user for input with parsing and validation.
89
+
90
+ Displays a prompt to the user, parses their input using the provided parser,
91
+ validates the result, and optionally retries on failure. Returns a Maybe monad
92
+ containing either the validated input or an error message.
89
93
 
90
94
  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
95
+ prompt_text: The prompt message to display to the user
96
+ parser: Function to convert string input to desired type (default: returns string as-is)
97
+ validator: Function to validate the parsed value (default: accepts any value)
98
+ error_message: Custom error message to display on validation failure
99
+ default: Default value to use if user provides empty input (displays in prompt)
100
+ retry: Enable retry on failure - True for unlimited, integer for max attempts, False to disable
101
+ _test_mode: Internal testing parameter (do not use)
98
102
 
99
103
  Returns:
100
- A Maybe containing the validated input or an error
104
+ Maybe[T]: Success with validated input, or Failure with error message
101
105
 
102
106
  Examples:
103
- >>> # This would prompt the user and validate their input
104
107
  >>> from valid8r.core import parsers, validators
105
- >>> age = ask(
108
+ >>> from valid8r.prompt import ask
109
+ >>>
110
+ >>> # Basic integer input with validation
111
+ >>> result = ask(
106
112
  ... "Enter your age: ",
107
113
  ... parser=parsers.parse_int,
108
- ... validator=validators.minimum(0),
114
+ ... validator=validators.between(0, 120),
109
115
  ... retry=True
110
116
  ... )
117
+ >>> # User enters "25" -> Success(25)
118
+ >>> # User enters "invalid" -> prompts again with error message
119
+ >>>
120
+ >>> # Input with default value
121
+ >>> result = ask(
122
+ ... "Enter port: ",
123
+ ... parser=parsers.parse_int,
124
+ ... default=8080
125
+ ... )
126
+ >>> # User presses Enter -> Success(8080)
127
+ >>> # User enters "3000" -> Success(3000)
128
+ >>>
129
+ >>> # Limited retries with custom error
130
+ >>> result = ask(
131
+ ... "Email: ",
132
+ ... parser=parsers.parse_email,
133
+ ... error_message="Invalid email format",
134
+ ... retry=3
135
+ ... )
136
+ >>> # User has 3 attempts to enter valid email
137
+ >>>
138
+ >>> # Boolean input with retry
139
+ >>> result = ask(
140
+ ... "Continue? (yes/no): ",
141
+ ... parser=parsers.parse_bool,
142
+ ... retry=True
143
+ ... )
144
+ >>> # User enters "yes" -> Success(True)
145
+ >>> # User enters "maybe" -> error, retry prompt
146
+
147
+ Note:
148
+ The returned Maybe must be unwrapped to access the value.
149
+ Use pattern matching or .value_or() to extract the result.
111
150
 
112
151
  """
113
152
  # Create a config object from the parameters
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: valid8r
3
- Version: 0.6.2
3
+ Version: 0.7.0
4
4
  Summary: Clean, flexible input validation for Python applications
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -78,6 +78,30 @@ A clean, flexible input validation library for Python applications.
78
78
  - **Enums**: `parse_enum` (type-safe enum parsing)
79
79
  - **Custom**: `create_parser`, `make_parser`, `validated_parser` (parser factories)
80
80
 
81
+ ## Available Validators
82
+
83
+ ### Numeric Validators
84
+ - **`minimum(min_value)`** - Ensures value is at least the minimum (inclusive)
85
+ - **`maximum(max_value)`** - Ensures value is at most the maximum (inclusive)
86
+ - **`between(min_value, max_value)`** - Ensures value is within range (inclusive)
87
+
88
+ ### String Validators
89
+ - **`non_empty_string()`** - Rejects empty strings and whitespace-only strings
90
+ - **`matches_regex(pattern)`** - Validates string matches regex pattern (string or compiled)
91
+ - **`length(min_length, max_length)`** - Validates string length within bounds
92
+
93
+ ### Collection Validators
94
+ - **`in_set(allowed_values)`** - Ensures value is in set of allowed values
95
+ - **`unique_items()`** - Ensures all items in a list are unique
96
+ - **`subset_of(allowed_set)`** - Validates set is subset of allowed values
97
+ - **`superset_of(required_set)`** - Validates set is superset of required values
98
+ - **`is_sorted(reverse=False)`** - Ensures list is sorted (ascending or descending)
99
+
100
+ ### Custom Validators
101
+ - **`predicate(func, error_message)`** - Create custom validator from any predicate function
102
+
103
+ **Note**: All validators support custom error messages and can be combined using `&` (and), `|` (or), and `~` (not) operators.
104
+
81
105
  ## Installation
82
106
 
83
107
  **Requirements**: Python 3.11 or higher
@@ -168,21 +192,21 @@ from valid8r.core.maybe import Success, Failure
168
192
  from valid8r.core import parsers
169
193
 
170
194
  # Phone number parsing with NANP validation (PhoneNumber)
171
- match parsers.parse_phone("+1 (555) 123-4567"):
195
+ match parsers.parse_phone("+1 (415) 555-2671"):
172
196
  case Success(phone):
173
197
  print(f"Country: {phone.country_code}") # 1
174
- print(f"Area: {phone.area_code}") # 555
175
- print(f"Exchange: {phone.exchange}") # 123
176
- print(f"Subscriber: {phone.subscriber}") # 4567
198
+ print(f"Area: {phone.area_code}") # 415
199
+ print(f"Exchange: {phone.exchange}") # 555
200
+ print(f"Subscriber: {phone.subscriber}") # 2671
177
201
 
178
202
  # Format for display using properties
179
- print(f"E.164: {phone.e164}") # +15551234567
180
- print(f"National: {phone.national}") # (555) 123-4567
203
+ print(f"E.164: {phone.e164}") # +14155552671
204
+ print(f"National: {phone.national}") # (415) 555-2671
181
205
  case Failure(err):
182
206
  print("Error:", err)
183
207
 
184
208
  # Also accepts various formats
185
- for number in ["5551234567", "(555) 123-4567", "555-123-4567"]:
209
+ for number in ["4155552671", "(415) 555-2671", "415-555-2671"]:
186
210
  result = parsers.parse_phone(number)
187
211
  assert result.is_success()
188
212
  ```
@@ -212,24 +236,26 @@ assert assert_maybe_failure(result, "at least 0")
212
236
 
213
237
  # Test prompts with mock input
214
238
  with MockInputContext(["yes", "42", "invalid", "25"]):
215
- # First prompt
239
+ # First prompt - returns Maybe, unwrap with value_or()
216
240
  result = prompt.ask("Continue? ", parser=parsers.parse_bool)
217
241
  assert result.value_or(False) == True
218
242
 
219
- # Second prompt
220
- age = prompt.ask(
243
+ # Second prompt - unwrap the Maybe result
244
+ result = prompt.ask(
221
245
  "Age? ",
222
246
  parser=parsers.parse_int,
223
247
  validator=validate_age
224
248
  )
249
+ age = result.value_or(None)
225
250
  assert age == 42
226
251
 
227
- # Third prompt will fail, fourth succeeds
228
- age = prompt.ask(
252
+ # Third prompt will fail, fourth succeeds - unwrap result
253
+ result = prompt.ask(
229
254
  "Age again? ",
230
255
  parser=parsers.parse_int,
231
- retries=1 # Retry once after failure
256
+ retry=1 # Retry once after failure
232
257
  )
258
+ age = result.value_or(None)
233
259
  assert age == 25
234
260
  ```
235
261
 
@@ -1,18 +1,18 @@
1
1
  valid8r/__init__.py,sha256=2fzSl6XtKX44sdqzf0GBTn4oEaCvhmyGkdsPDMJjZz8,447
2
2
  valid8r/core/__init__.py,sha256=ASOdzqCtpZHbHjjYMZkb78Z-nKxtD26ruTY0bd43ImA,520
3
3
  valid8r/core/combinators.py,sha256=KvRiDEqoZgH58cBYPO6SW9pdtkyijk0lS8aGSB5DbO4,2349
4
- valid8r/core/maybe.py,sha256=xT1xbiLVKohZ2aeaDZoKjT0W6Vk_PPwKbZXpIfsP7hc,4359
5
- valid8r/core/parsers.py,sha256=U-fszQfRmUzT_PMhmVVlUnTpY7CooeG9rr3Lxv5fIfs,55866
6
- valid8r/core/validators.py,sha256=oCrRQ2wIPNkqQXy-hJ7sQ9mJAvxtEtGhoy7WvehWqTc,5756
4
+ valid8r/core/maybe.py,sha256=ifz15tDMwRaQLsYvU3pWGBZBxJ2JyyOW-g5YpOw_-3w,4643
5
+ valid8r/core/parsers.py,sha256=WYAgwCIh8HyrXr0AoZeqa0c43-_iVIK_S3wb2lha73c,67456
6
+ valid8r/core/validators.py,sha256=e3B_fi7ch5m0Zczg87r8AhrgEdBTcbz-aygrKCvi0dg,18537
7
7
  valid8r/prompt/__init__.py,sha256=XYB3NEp-tmqT6fGmETVEeXd7Urj0M4ijlwdRAjj-rG8,175
8
- valid8r/prompt/basic.py,sha256=fLWuN-oiVZyaLdbcW5GHWpoGQ82RG0j-1n7uMYDfOb8,6008
8
+ valid8r/prompt/basic.py,sha256=fFARuy5nGTE7xM3dB1jpRC3OPNmp4WwaymFMz7BSgdo,7635
9
9
  valid8r/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  valid8r/testing/__init__.py,sha256=8mk54zt0Ai2dK0a3GMOTfDPsVQWXaS6uvQJDrkRV9hs,779
11
11
  valid8r/testing/assertions.py,sha256=9KGz1JooCoyikyxMX7VuXB9VYAtj-4H_LPYFGdvS-ps,1820
12
12
  valid8r/testing/generators.py,sha256=kAV6NRO9x1gPy0BfGs07ETVxjpTIxOZyV9wH2BA1nHA,8791
13
13
  valid8r/testing/mock_input.py,sha256=9GRT7h0PCh9Dea-OcQ5Uls7YqhsTdqMWuX6I6ZlW1aw,2334
14
- valid8r-0.6.2.dist-info/METADATA,sha256=-uKoFnQMura7JXCYVDu7F4JxR1owUyErOBgc5IJM9f8,9096
15
- valid8r-0.6.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
16
- valid8r-0.6.2.dist-info/entry_points.txt,sha256=H_24A4zUgnKIXAIRosJliIcntyqMfmcgKh5_Prl7W18,79
17
- valid8r-0.6.2.dist-info/licenses/LICENSE,sha256=JpEmJvRYOTIUt0UjgvpDrd3U94Wnbt_Grr5z-xU2jtk,1066
18
- valid8r-0.6.2.dist-info/RECORD,,
14
+ valid8r-0.7.0.dist-info/METADATA,sha256=X7tG_qUhsJ3LE0M28nIDMjERAiTOp_xcHnSen9RbXgU,10458
15
+ valid8r-0.7.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
16
+ valid8r-0.7.0.dist-info/entry_points.txt,sha256=H_24A4zUgnKIXAIRosJliIcntyqMfmcgKh5_Prl7W18,79
17
+ valid8r-0.7.0.dist-info/licenses/LICENSE,sha256=JpEmJvRYOTIUt0UjgvpDrd3U94Wnbt_Grr5z-xU2jtk,1066
18
+ valid8r-0.7.0.dist-info/RECORD,,