valid8r 0.6.2__py3-none-any.whl → 0.6.3__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')
@@ -102,11 +102,25 @@ def minimum(min_value: N, error_message: str | None = None) -> Validator[N]:
102
102
  """Create a validator that ensures a value is at least the minimum.
103
103
 
104
104
  Args:
105
- min_value: The minimum allowed value
105
+ min_value: The minimum allowed value (inclusive)
106
106
  error_message: Optional custom error message
107
107
 
108
108
  Returns:
109
- A validator function
109
+ Validator[N]: A validator function that accepts values >= min_value
110
+
111
+ Examples:
112
+ >>> from valid8r.core.validators import minimum
113
+ >>> validator = minimum(0)
114
+ >>> validator(5)
115
+ Success(5)
116
+ >>> validator(0)
117
+ Success(0)
118
+ >>> validator(-1).is_failure()
119
+ True
120
+ >>> # With custom error message
121
+ >>> validator = minimum(18, error_message="Must be an adult")
122
+ >>> validator(17).error_or("")
123
+ 'Must be an adult'
110
124
 
111
125
  """
112
126
 
@@ -122,11 +136,25 @@ def maximum(max_value: N, error_message: str | None = None) -> Validator[N]:
122
136
  """Create a validator that ensures a value is at most the maximum.
123
137
 
124
138
  Args:
125
- max_value: The maximum allowed value
139
+ max_value: The maximum allowed value (inclusive)
126
140
  error_message: Optional custom error message
127
141
 
128
142
  Returns:
129
- A validator function
143
+ Validator[N]: A validator function that accepts values <= max_value
144
+
145
+ Examples:
146
+ >>> from valid8r.core.validators import maximum
147
+ >>> validator = maximum(100)
148
+ >>> validator(50)
149
+ Success(50)
150
+ >>> validator(100)
151
+ Success(100)
152
+ >>> validator(101).is_failure()
153
+ True
154
+ >>> # With custom error message
155
+ >>> validator = maximum(120, error_message="Age too high")
156
+ >>> validator(150).error_or("")
157
+ 'Age too high'
130
158
 
131
159
  """
132
160
 
@@ -142,12 +170,30 @@ def between(min_value: N, max_value: N, error_message: str | None = None) -> Val
142
170
  """Create a validator that ensures a value is between minimum and maximum (inclusive).
143
171
 
144
172
  Args:
145
- min_value: The minimum allowed value
146
- max_value: The maximum allowed value
173
+ min_value: The minimum allowed value (inclusive)
174
+ max_value: The maximum allowed value (inclusive)
147
175
  error_message: Optional custom error message
148
176
 
149
177
  Returns:
150
- A validator function
178
+ Validator[N]: A validator function that accepts values where min_value <= value <= max_value
179
+
180
+ Examples:
181
+ >>> from valid8r.core.validators import between
182
+ >>> validator = between(0, 100)
183
+ >>> validator(50)
184
+ Success(50)
185
+ >>> validator(0)
186
+ Success(0)
187
+ >>> validator(100)
188
+ Success(100)
189
+ >>> validator(-1).is_failure()
190
+ True
191
+ >>> validator(101).is_failure()
192
+ True
193
+ >>> # With custom error message
194
+ >>> validator = between(1, 10, error_message="Rating must be 1-10")
195
+ >>> validator(11).error_or("")
196
+ 'Rating must be 1-10'
151
197
 
152
198
  """
153
199
 
@@ -162,12 +208,30 @@ def between(min_value: N, max_value: N, error_message: str | None = None) -> Val
162
208
  def predicate(pred: Callable[[T], bool], error_message: str) -> Validator[T]:
163
209
  """Create a validator using a custom predicate function.
164
210
 
211
+ Allows creating custom validators for any validation logic by providing
212
+ a predicate function that returns True for valid values.
213
+
165
214
  Args:
166
- pred: A function that takes a value and returns a boolean
167
- error_message: Error message when validation fails
215
+ pred: A function that takes a value and returns True if valid, False otherwise
216
+ error_message: Error message to return when validation fails
168
217
 
169
218
  Returns:
170
- A validator function
219
+ Validator[T]: A validator function that applies the predicate
220
+
221
+ Examples:
222
+ >>> from valid8r.core.validators import predicate
223
+ >>> # Validate even numbers
224
+ >>> is_even = predicate(lambda x: x % 2 == 0, "Must be even")
225
+ >>> is_even(4)
226
+ Success(4)
227
+ >>> is_even(3).is_failure()
228
+ True
229
+ >>> # Validate string patterns
230
+ >>> starts_with_a = predicate(lambda s: s.startswith('a'), "Must start with 'a'")
231
+ >>> starts_with_a("apple")
232
+ Success('apple')
233
+ >>> starts_with_a("banana").error_or("")
234
+ "Must start with 'a'"
171
235
 
172
236
  """
173
237
 
@@ -183,12 +247,30 @@ def length(min_length: int, max_length: int, error_message: str | None = None) -
183
247
  """Create a validator that ensures a string's length is within bounds.
184
248
 
185
249
  Args:
186
- min_length: Minimum length of the string
187
- max_length: Maximum length of the string
250
+ min_length: Minimum length of the string (inclusive)
251
+ max_length: Maximum length of the string (inclusive)
188
252
  error_message: Optional custom error message
189
253
 
190
254
  Returns:
191
- A validator function
255
+ Validator[str]: A validator function that checks string length
256
+
257
+ Examples:
258
+ >>> from valid8r.core.validators import length
259
+ >>> validator = length(3, 10)
260
+ >>> validator("hello")
261
+ Success('hello')
262
+ >>> validator("abc")
263
+ Success('abc')
264
+ >>> validator("abcdefghij")
265
+ Success('abcdefghij')
266
+ >>> validator("ab").is_failure()
267
+ True
268
+ >>> validator("abcdefghijk").is_failure()
269
+ True
270
+ >>> # With custom error message
271
+ >>> validator = length(8, 20, error_message="Password must be 8-20 characters")
272
+ >>> validator("short").error_or("")
273
+ 'Password must be 8-20 characters'
192
274
 
193
275
  """
194
276
 
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.6.3
4
4
  Summary: Clean, flexible input validation for Python applications
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -168,21 +168,21 @@ from valid8r.core.maybe import Success, Failure
168
168
  from valid8r.core import parsers
169
169
 
170
170
  # Phone number parsing with NANP validation (PhoneNumber)
171
- match parsers.parse_phone("+1 (555) 123-4567"):
171
+ match parsers.parse_phone("+1 (415) 555-2671"):
172
172
  case Success(phone):
173
173
  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
174
+ print(f"Area: {phone.area_code}") # 415
175
+ print(f"Exchange: {phone.exchange}") # 555
176
+ print(f"Subscriber: {phone.subscriber}") # 2671
177
177
 
178
178
  # Format for display using properties
179
- print(f"E.164: {phone.e164}") # +15551234567
180
- print(f"National: {phone.national}") # (555) 123-4567
179
+ print(f"E.164: {phone.e164}") # +14155552671
180
+ print(f"National: {phone.national}") # (415) 555-2671
181
181
  case Failure(err):
182
182
  print("Error:", err)
183
183
 
184
184
  # Also accepts various formats
185
- for number in ["5551234567", "(555) 123-4567", "555-123-4567"]:
185
+ for number in ["4155552671", "(415) 555-2671", "415-555-2671"]:
186
186
  result = parsers.parse_phone(number)
187
187
  assert result.is_success()
188
188
  ```
@@ -212,24 +212,26 @@ assert assert_maybe_failure(result, "at least 0")
212
212
 
213
213
  # Test prompts with mock input
214
214
  with MockInputContext(["yes", "42", "invalid", "25"]):
215
- # First prompt
215
+ # First prompt - returns Maybe, unwrap with value_or()
216
216
  result = prompt.ask("Continue? ", parser=parsers.parse_bool)
217
217
  assert result.value_or(False) == True
218
218
 
219
- # Second prompt
220
- age = prompt.ask(
219
+ # Second prompt - unwrap the Maybe result
220
+ result = prompt.ask(
221
221
  "Age? ",
222
222
  parser=parsers.parse_int,
223
223
  validator=validate_age
224
224
  )
225
+ age = result.value_or(None)
225
226
  assert age == 42
226
227
 
227
- # Third prompt will fail, fourth succeeds
228
- age = prompt.ask(
228
+ # Third prompt will fail, fourth succeeds - unwrap result
229
+ result = prompt.ask(
229
230
  "Age again? ",
230
231
  parser=parsers.parse_int,
231
- retries=1 # Retry once after failure
232
+ retry=1 # Retry once after failure
232
233
  )
234
+ age = result.value_or(None)
233
235
  assert age == 25
234
236
  ```
235
237
 
@@ -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=owzY6s9UWLlC7qZQcu8NmjqzphlxPJpPuaIxujs1YmU,8784
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.6.3.dist-info/METADATA,sha256=nsJsH6nOR2YwQh4W6jBhFuodpfkODxN8t14Ryyghgf4,9246
15
+ valid8r-0.6.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
16
+ valid8r-0.6.3.dist-info/entry_points.txt,sha256=H_24A4zUgnKIXAIRosJliIcntyqMfmcgKh5_Prl7W18,79
17
+ valid8r-0.6.3.dist-info/licenses/LICENSE,sha256=JpEmJvRYOTIUt0UjgvpDrd3U94Wnbt_Grr5z-xU2jtk,1066
18
+ valid8r-0.6.3.dist-info/RECORD,,