0
# Custom Parsers and Validation
1
2
Registration and management of custom parsing methods with marshmallow field integration, validation customization, and extensible parser patterns for specialized data types.
3
4
## Capabilities
5
6
### Custom Parser Registration
7
8
Register custom parsing functions to extend environs with domain-specific data types and validation logic.
9
10
```python { .api }
11
def add_parser(self, name: str, func: Callable):
12
"""
13
Register a new parser method with the given name.
14
15
Parameters:
16
- name: str, method name for the parser (must not conflict with existing methods)
17
- func: callable, parser function that receives the raw string value
18
19
Raises:
20
ParserConflictError: If name conflicts with existing method
21
22
Notes:
23
The parser function should accept a string value and return the parsed result.
24
It can raise EnvError or ValidationError for invalid input.
25
"""
26
```
27
28
Usage examples:
29
30
```python
31
import os
32
from environs import env, EnvError
33
import re
34
35
# Custom email validator parser
36
def parse_email(value):
37
"""Parse and validate email addresses."""
38
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
39
if not re.match(email_pattern, value):
40
raise EnvError(f"Invalid email format: {value}")
41
return value.lower()
42
43
# Register the custom parser
44
env.add_parser("email", parse_email)
45
46
# Use the custom parser
47
os.environ["ADMIN_EMAIL"] = "ADMIN@EXAMPLE.COM"
48
admin_email = env.email("ADMIN_EMAIL") # => "admin@example.com"
49
50
# Custom URL slug parser
51
def parse_slug(value):
52
"""Parse and validate URL slugs."""
53
slug_pattern = r'^[a-z0-9]+(?:-[a-z0-9]+)*$'
54
if not re.match(slug_pattern, value):
55
raise EnvError(f"Invalid slug format: {value}")
56
return value
57
58
env.add_parser("slug", parse_slug)
59
60
os.environ["ARTICLE_SLUG"] = "my-awesome-article"
61
slug = env.slug("ARTICLE_SLUG") # => "my-awesome-article"
62
63
# Custom coordinate parser
64
def parse_coordinates(value):
65
"""Parse latitude,longitude coordinates."""
66
try:
67
lat_str, lon_str = value.split(',')
68
lat, lon = float(lat_str.strip()), float(lon_str.strip())
69
if not (-90 <= lat <= 90):
70
raise ValueError("Latitude must be between -90 and 90")
71
if not (-180 <= lon <= 180):
72
raise ValueError("Longitude must be between -180 and 180")
73
return (lat, lon)
74
except (ValueError, TypeError) as e:
75
raise EnvError(f"Invalid coordinates format: {value}") from e
76
77
env.add_parser("coordinates", parse_coordinates)
78
79
os.environ["LOCATION"] = "40.7128, -74.0060"
80
location = env.coordinates("LOCATION") # => (40.7128, -74.0060)
81
```
82
83
### Parser Decorator
84
85
Use a decorator to register custom parsers for cleaner syntax and better organization.
86
87
```python { .api }
88
def parser_for(self, name: str):
89
"""
90
Decorator that registers a new parser method with the given name.
91
92
Parameters:
93
- name: str, method name for the parser
94
95
Returns:
96
Decorator function that registers the decorated function as a parser
97
98
Notes:
99
The decorated function should accept a string value and return parsed result.
100
"""
101
```
102
103
Usage examples:
104
105
```python
106
import os
107
from environs import env, EnvError
108
import ipaddress
109
110
# Register IP address parser using decorator
111
@env.parser_for("ip_address")
112
def parse_ip_address(value):
113
"""Parse and validate IP addresses."""
114
try:
115
return ipaddress.ip_address(value)
116
except ipaddress.AddressValueError as e:
117
raise EnvError(f"Invalid IP address: {value}") from e
118
119
# Register network parser
120
@env.parser_for("network")
121
def parse_network(value):
122
"""Parse and validate network CIDR notation."""
123
try:
124
return ipaddress.ip_network(value, strict=False)
125
except ipaddress.NetmaskValueError as e:
126
raise EnvError(f"Invalid network: {value}") from e
127
128
# Use custom parsers
129
os.environ["SERVER_IP"] = "192.168.1.100"
130
os.environ["ALLOWED_NETWORK"] = "10.0.0.0/8"
131
132
server_ip = env.ip_address("SERVER_IP") # => IPv4Address('192.168.1.100')
133
allowed_net = env.network("ALLOWED_NETWORK") # => IPv4Network('10.0.0.0/8')
134
135
print(f"Server IP: {server_ip}")
136
print(f"Network contains server: {server_ip in allowed_net}")
137
138
# Complex data structure parser
139
@env.parser_for("connection_string")
140
def parse_connection_string(value):
141
"""Parse database connection strings."""
142
# Format: "host:port/database?param1=value1¶m2=value2"
143
try:
144
# Split main parts
145
if '?' in value:
146
main_part, params_part = value.split('?', 1)
147
else:
148
main_part, params_part = value, ""
149
150
# Parse host:port/database
151
host_port, database = main_part.split('/', 1)
152
if ':' in host_port:
153
host, port = host_port.split(':', 1)
154
port = int(port)
155
else:
156
host, port = host_port, 5432
157
158
# Parse parameters
159
params = {}
160
if params_part:
161
for param in params_part.split('&'):
162
key, val = param.split('=', 1)
163
params[key] = val
164
165
return {
166
'host': host,
167
'port': port,
168
'database': database,
169
'params': params
170
}
171
except (ValueError, TypeError) as e:
172
raise EnvError(f"Invalid connection string: {value}") from e
173
174
# Use complex parser
175
os.environ["DATABASE_URL"] = "localhost:5432/myapp?ssl=require&timeout=30"
176
db_config = env.connection_string("DATABASE_URL")
177
# => {'host': 'localhost', 'port': 5432, 'database': 'myapp',
178
# 'params': {'ssl': 'require', 'timeout': '30'}}
179
```
180
181
### Marshmallow Field Integration
182
183
Register parsers from marshmallow fields for advanced validation and serialization capabilities.
184
185
```python { .api }
186
def add_parser_from_field(self, name: str, field_cls: Type[Field]):
187
"""
188
Register a new parser method from a marshmallow Field class.
189
190
Parameters:
191
- name: str, method name for the parser
192
- field_cls: marshmallow Field class or subclass
193
194
Notes:
195
The field class should implement _deserialize method for parsing.
196
Enables full marshmallow validation capabilities and error handling.
197
"""
198
```
199
200
Usage examples:
201
202
```python
203
import os
204
from environs import env
205
import marshmallow as ma
206
from marshmallow import ValidationError
207
import re
208
209
# Custom marshmallow field for phone numbers
210
class PhoneNumberField(ma.fields.Field):
211
"""Field for parsing and validating phone numbers."""
212
213
def _serialize(self, value, attr, obj, **kwargs):
214
if value is None:
215
return None
216
return str(value)
217
218
def _deserialize(self, value, attr, data, **kwargs):
219
if not isinstance(value, str):
220
raise ValidationError("Phone number must be a string")
221
222
# Remove all non-digit characters
223
digits = re.sub(r'\D', '', value)
224
225
# Validate US phone number format (10 digits)
226
if len(digits) != 10:
227
raise ValidationError("Phone number must have 10 digits")
228
229
# Format as (XXX) XXX-XXXX
230
return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
231
232
# Register marshmallow field as parser
233
env.add_parser_from_field("phone", PhoneNumberField)
234
235
# Use field-based parser
236
os.environ["SUPPORT_PHONE"] = "1-800-555-0123"
237
support_phone = env.phone("SUPPORT_PHONE") # => "(800) 555-0123"
238
239
# Custom field with validation options
240
class CreditCardField(ma.fields.Field):
241
"""Field for parsing credit card numbers with validation."""
242
243
def _deserialize(self, value, attr, data, **kwargs):
244
if not isinstance(value, str):
245
raise ValidationError("Credit card number must be a string")
246
247
# Remove spaces and dashes
248
digits = re.sub(r'[\s-]', '', value)
249
250
# Validate digits only
251
if not digits.isdigit():
252
raise ValidationError("Credit card number must contain only digits")
253
254
# Validate length (13-19 digits for most cards)
255
if not (13 <= len(digits) <= 19):
256
raise ValidationError("Credit card number must be 13-19 digits")
257
258
# Simple Luhn algorithm check
259
def luhn_check(card_num):
260
def digits_of(n):
261
return [int(d) for d in str(n)]
262
263
digits = digits_of(card_num)
264
odd_digits = digits[-1::-2]
265
even_digits = digits[-2::-2]
266
checksum = sum(odd_digits)
267
for d in even_digits:
268
checksum += sum(digits_of(d * 2))
269
return checksum % 10 == 0
270
271
if not luhn_check(digits):
272
raise ValidationError("Invalid credit card number")
273
274
# Mask all but last 4 digits
275
masked = '*' * (len(digits) - 4) + digits[-4:]
276
return masked
277
278
env.add_parser_from_field("credit_card", CreditCardField)
279
280
# Use with validation
281
os.environ["PAYMENT_CARD"] = "4532 1488 0343 6467" # Valid test number
282
card = env.credit_card("PAYMENT_CARD") # => "************6467"
283
```
284
285
### Validation Helpers
286
287
Use environs' built-in validation functions and marshmallow validators for robust input validation.
288
289
```python { .api }
290
# Import validation functions
291
from environs import validate
292
293
# Common validators available:
294
# validate.Length(min=None, max=None)
295
# validate.Range(min=None, max=None)
296
# validate.OneOf(choices)
297
# validate.Regexp(regex)
298
# validate.URL()
299
# validate.Email()
300
```
301
302
Usage examples:
303
304
```python
305
import os
306
from environs import env, validate
307
308
# String length validation
309
os.environ["USERNAME"] = "john_doe"
310
username = env.str("USERNAME", validate=validate.Length(min=3, max=20))
311
312
# Numeric range validation
313
os.environ["PORT"] = "8080"
314
port = env.int("PORT", validate=validate.Range(min=1024, max=65535))
315
316
# Choice validation
317
os.environ["LOG_LEVEL"] = "INFO"
318
log_level = env.str("LOG_LEVEL", validate=validate.OneOf(["DEBUG", "INFO", "WARNING", "ERROR"]))
319
320
# Regular expression validation
321
os.environ["VERSION"] = "1.2.3"
322
version = env.str("VERSION", validate=validate.Regexp(r'^\d+\.\d+\.\d+$'))
323
324
# Multiple validators
325
os.environ["API_KEY"] = "sk_live_123456789abcdef"
326
api_key = env.str("API_KEY", validate=[
327
validate.Length(min=20),
328
validate.Regexp(r'^sk_(live|test)_[a-zA-Z0-9]+$')
329
])
330
331
# Custom validation function
332
def validate_positive_even(value):
333
"""Validate that number is positive and even."""
334
if value <= 0:
335
raise ValidationError("Value must be positive")
336
if value % 2 != 0:
337
raise ValidationError("Value must be even")
338
return value
339
340
os.environ["BATCH_SIZE"] = "100"
341
batch_size = env.int("BATCH_SIZE", validate=validate_positive_even)
342
343
# Combining built-in and custom validators
344
os.environ["THREAD_COUNT"] = "8"
345
thread_count = env.int("THREAD_COUNT", validate=[
346
validate.Range(min=1, max=32),
347
validate_positive_even
348
])
349
```
350
351
## Error Handling
352
353
Handle parser conflicts and validation errors appropriately in custom parser registration.
354
355
```python
356
from environs import env, ParserConflictError, EnvValidationError
357
358
# Handle parser name conflicts
359
try:
360
env.add_parser("str", lambda x: x) # Conflicts with built-in
361
except ParserConflictError as e:
362
print(f"Parser conflict: {e}")
363
# Use a different name
364
env.add_parser("custom_str", lambda x: x.upper())
365
366
# Handle validation errors in custom parsers
367
@env.parser_for("positive_int")
368
def parse_positive_int(value):
369
try:
370
num = int(value)
371
if num <= 0:
372
raise EnvError("Value must be positive")
373
return num
374
except ValueError as e:
375
raise EnvError(f"Invalid integer: {value}") from e
376
377
# Use with error handling
378
os.environ["INVALID_POSITIVE"] = "-5"
379
try:
380
value = env.positive_int("INVALID_POSITIVE")
381
except EnvValidationError as e:
382
print(f"Validation failed: {e}")
383
```
384
385
## Types
386
387
```python { .api }
388
from typing import Callable, Any, Type
389
from marshmallow.fields import Field
390
from marshmallow import ValidationError
391
392
ParserFunction = Callable[[str], Any]
393
ValidatorFunction = Callable[[Any], Any]
394
```