Repository: sloria/environs (python)

Issue:
Expose marshmallow's `validate` module from environs
so that you can do

```python
from environs import env, validate

node_env = env.str("NODE_ENV", validate=validate.OneOf(["production", "development"])
```

Pull request description:
Re-export validate module from marshmallow
closes #385 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e795a0..fecadd9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ Features:
 - Add `prefix` parameter to `Env` constructor ([#384](https://github.com/sloria/environs/issues/384)).
   Thanks [arthurc0102](https://github.com/sloria/environs/issues/384)
   for the suggestion.
+- Re-export `validate` module from marshmallow ([#385](https://github.com/sloria/environs/issues/385)).
 
 Bug fixes:
 
diff --git a/README.md b/README.md
index 8ba4fc6..124fd73 100644
--- a/README.md
+++ b/README.md
@@ -219,34 +219,35 @@ year = env.int("YEAR")  # =>2020
 # export NODE_ENV='invalid'
 # export EMAIL='^_^'
 
-from environs import env, ValidationError
-from marshmallow.validate import OneOf, Length, Email
+from environs import env, validate, ValidationError
 
 
-# simple validator
-def validator(n):
-    if n <= 0:
-        raise ValidationError("Invalid value.")
-
-
-env.int("TTL", validate=validator)
-# => Environment variable "TTL" invalid: ['Invalid value.']
-
-
-# using marshmallow validators
+# built-in validators (provided by marshmallow)
 env.str(
     "NODE_ENV",
-    validate=OneOf(
+    validate=validate.OneOf(
         ["production", "development"], error="NODE_ENV must be one of: {choices}"
     ),
 )
 # => Environment variable "NODE_ENV" invalid: ['NODE_ENV must be one of: production, development']
 
 # multiple validators
-env.str("EMAIL", validate=[Length(min=4), Email()])
+env.str("EMAIL", validate=[validate.Length(min=4), validate.Email()])
 # => Environment variable "EMAIL" invalid: ['Shorter than minimum length 4.', 'Not a valid email address.']
+
+
+# custom validator
+def validator(n):
+    if n <= 0:
+        raise ValidationError("Invalid value.")
+
+
+env.int("TTL", validate=validator)
+# => Environment variable "TTL" invalid: ['Invalid value.']
 ```
 
+`environs.validate` is equivalent to [`marshmallow.validate`](https://marshmallow.readthedocs.io/en/stable/marshmallow.validate.html), so you can use any of the validators provided by that module.
+
 ## Deferred validation
 
 By default, a validation error is raised immediately upon calling a parser method for an invalid environment variable.
diff --git a/examples/deferred_validation_example.py b/examples/deferred_validation_example.py
index 140e242..7a786ac 100644
--- a/examples/deferred_validation_example.py
+++ b/examples/deferred_validation_example.py
@@ -1,9 +1,7 @@
 import os
 from pprint import pprint
 
-from marshmallow.validate import Email, Length, OneOf, Range
-
-from environs import Env, EnvValidationError
+from environs import Env, EnvValidationError, validate
 
 os.environ["TTL"] = "-2"
 os.environ["NODE_ENV"] = "invalid"
@@ -11,14 +9,14 @@ os.environ["EMAIL"] = "^_^"
 
 
 env = Env(eager=False)
-TTL = env.int("TTL", validate=Range(min=0, max=100))
+TTL = env.int("TTL", validate=validate.Range(min=0, max=100))
 NODE_ENV = env.str(
     "NODE_ENV",
-    validate=OneOf(
+    validate=validate.OneOf(
         ["production", "development"], error="NODE_ENV must be one of: {choices}"
     ),
 )
-EMAIL = env.str("EMAIL", validate=[Length(min=4), Email()])
+EMAIL = env.str("EMAIL", validate=[validate.Length(min=4), validate.Email()])
 
 # This will raise an error with the combined validation messages
 try:
diff --git a/examples/django_example.py b/examples/django_example.py
index bf46f81..856e53d 100644
--- a/examples/django_example.py
+++ b/examples/django_example.py
@@ -10,9 +10,8 @@ To run this example:
 import os
 from pprint import pprint
 
-import environs
+from environs import env
 
-env = environs.Env()
 env.read_env()
 
 BASE_DIR = os.path.dirname(os.path.abspath(__file__))
diff --git a/examples/marshmallow_example.py b/examples/marshmallow_example.py
index 41969e5..2b4faff 100644
--- a/examples/marshmallow_example.py
+++ b/examples/marshmallow_example.py
@@ -3,7 +3,7 @@ import pathlib
 
 import marshmallow as ma
 
-from environs import Env
+from environs import env
 
 os.environ["STATIC_PATH"] = "app/static"
 
@@ -16,7 +16,6 @@ class PathField(ma.fields.Field):
         return str(value)
 
 
-env = Env()
 env.parser_from_field("path", PathField)
 
 static_path = env.path("STATIC_PATH")
diff --git a/examples/plugin_example.py b/examples/plugin_example.py
index 5d3824b..ace14f4 100644
--- a/examples/plugin_example.py
+++ b/examples/plugin_example.py
@@ -3,7 +3,7 @@ import os
 from furl import furl as Furl
 from yarl import URL
 
-from environs import Env
+from environs import env
 
 ##### This is the beginning of the plugin code #####
 
@@ -28,7 +28,6 @@ os.environ["GITHUB_URL"] = "https://github.com/sloria/environs"
 
 # Our application activates the plugin using the setup function
 
-env = Env()
 setup(env)
 
 # We now have the 'furl' and 'yurl' methods available
diff --git a/examples/validation_example.py b/examples/validation_example.py
index 3b61fa3..6af1a39 100644
--- a/examples/validation_example.py
+++ b/examples/validation_example.py
@@ -1,27 +1,16 @@
 import os
 
-from marshmallow.validate import Email, Length, OneOf
-
-from environs import Env, EnvError
+from environs import EnvError, ValidationError, env, validate
 
 os.environ["TTL"] = "-2"
 os.environ["NODE_ENV"] = "invalid"
 os.environ["EMAIL"] = "^_^"
 
-
-env = Env()
-
-# simple validator
-try:
-    env.int("TTL", validate=lambda n: n > 0)
-except EnvError as err:
-    print(err)
-
-# marshmallow validator
+# built-in validator
 try:
     env.str(
         "NODE_ENV",
-        validate=OneOf(
+        validate=validate.OneOf(
             ["production", "development"], error="NODE_ENV must be one of: {choices}"
         ),
     )
@@ -31,6 +20,19 @@ except EnvError as err:
 
 # multiple validators
 try:
-    env.str("EMAIL", validate=[Length(min=4), Email()])
+    env.str("EMAIL", validate=[validate.Length(min=4), validate.Email()])
+except EnvError as err:
+    print(err)
+
+
+# custom validator
+def validate_ttl(value):
+    if value <= 0:
+        raise ValidationError("TTL must be greater than 0")
+    return value
+
+
+try:
+    env.int("TTL", validate=validate_ttl)
 except EnvError as err:
     print(err)
diff --git a/src/environs/__init__.py b/src/environs/__init__.py
index aa24300..2484924 100644
--- a/src/environs/__init__.py
+++ b/src/environs/__init__.py
@@ -54,7 +54,9 @@ _DictType = dict
 _EXPANDED_VAR_PATTERN = re.compile(r"(?<!\\)\$\{([A-Za-z0-9_]+)(:-[^\}:]*)?\}")
 
 
-# Reexport marshmallow's ValidationError. Custom validators should raise this for invalid input.
+# Reexport marshmallow's validate module and ValidationError.
+validate = ma.validate
+# Custom validators should raise this for invalid input.
 ValidationError = ma.ValidationError
 
 


Test patch:
diff --git a/tests/test_environs.py b/tests/test_environs.py
index b9c1edd..2bc036a 100644
--- a/tests/test_environs.py
+++ b/tests/test_environs.py
@@ -18,10 +18,11 @@ import dj_email_url
 import django_cache_url
 import marshmallow as ma
 import pytest
-from marshmallow import fields, validate
+from marshmallow import fields
 from packaging.version import Version
 
 import environs
+from environs import validate
 
 HERE = pathlib.Path(__file__).parent
 MARSHMALLOW_VERSION = Version(importlib.metadata.version("marshmallow"))