Repository: rochacbruno/dynaconf
Pull request #736 (2022-04-11 16:33:41)

Added validate_only_current_env to validator (issue #734)
As discussed in [issue 734](https://github.com/rochacbruno/dynaconf/issues/734), added a new keyword for `validator.validate` and `Settings` class to only run validators on current env instead of all defined envs.

Hope everything is fine, feel free to mention any issue and I'll try to fix them.

codecov[bot]: # [Codecov](https://codecov.io/gh/rochacbruno/dynaconf/pull/736?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha) Report
> Merging [#736](https://codecov.io/gh/rochacbruno/dynaconf/pull/736?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha) (53b4154) into [master](https://codecov.io/gh/rochacbruno/dynaconf/commit/5f079841504f602cdc3d21e565a12afbb84245a8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha) (5f07984) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff            @@
##            master      #736   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines         1703      1707    +4     
=========================================
+ Hits          1703      1707    +4     
```


| [Impacted Files](https://codecov.io/gh/rochacbruno/dynaconf/pull/736?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha) | Coverage Δ | |
|---|---|---|
| [dynaconf/base.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/736/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha#diff-ZHluYWNvbmYvYmFzZS5weQ==) | `100.00% <ø> (ø)` | |
| [dynaconf/validator.py](https://codecov.io/gh/rochacbruno/dynaconf/pull/736/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha#diff-ZHluYWNvbmYvdmFsaWRhdG9yLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/rochacbruno/dynaconf/pull/736?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/rochacbruno/dynaconf/pull/736?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha). Last update [5f07984...53b4154](https://codecov.io/gh/rochacbruno/dynaconf/pull/736?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+Rocha).


Patch:
diff --git a/Makefile b/Makefile
index fb627a8..df49ba4 100644
--- a/Makefile
+++ b/Makefile
@@ -119,6 +119,7 @@ test_examples:
 	cd example/issues/494_using_pathlib;pwd;python app.py
 	cd example/issues/519_underscore_in_name;pwd;ATC_BLE__device_id=42 EXPECTED_VALUE=42 python app.py
 	cd example/issues/519_underscore_in_name;pwd;ATC_BLE__DEVICE_ID=42 EXPECTED_VALUE=42 python app.py
+	cd example/issues/734_validate_only_current_env;pwd;python app.py
 
 test_vault:
 	# @cd example/vault;pwd;python write.py
diff --git a/docs/validation.md b/docs/validation.md
index 35055f1..870ac59 100644
--- a/docs/validation.md
+++ b/docs/validation.md
@@ -184,7 +184,7 @@ This returns code 0 (success) if validation is ok.
     All values in dynaconf are parsed using toml format, TOML tries to be smart
     and infer the type of the settings variables, some variables will be automatically
     converted to integer:
-    
+
     ```
     FOO = "0x..."  # hexadecimal
     FOO = "0o..."  # Octal
@@ -263,5 +263,59 @@ settings.validators.validate(
     only=["module2"],
     exclude=["module2.bad"]
 )
+```
+
+> **Validate only current env**
+
+You can specify if you want to validate all environments defined for a validator (default behavior) or only the current environement. In the first case, the validators will run on all possible settings defined in their list of environment, while in the latter the validators with environments different from the current environment will be skipped.
+
+This is useful when your configuration for different environments (let's say `production` and `development`) comes from different files you don't necesseraly have access to during development. You would want to write different validators for your `development` and `production` environments, and only run the right validator for the current environment.
+
+Here is an example of the option using:
+
+- `settings.toml`
+```ini
+[development]
+version = "dev"
+age = 35
+name = "Bruno"
+servers = ['127.0.0.1', 'localhost', 'development.com']
+PORT = 80
 
+[production]
+version = "1.0.0"
+age = 35
+name = "Bruno"
+servers = ['production.com']
+PORT = 443
 ```
+
+- `.secrets.toml`
+```ini
+[production]
+api_key = 'secret_api_key'
+```
+
+You could then have these validators:
+
+```python
+from dynaconf import Dynaconf, Validator
+
+settings = Dynaconf(
+    settings_files=['setting.toml', '.secrets.toml'],
+    environments=True,
+    validators=[
+        # Ensure some parameters exists for both envs
+        Validator('VERSION', 'NAME', 'SERVERS', envs=['development', 'production'], must_exist=True),
+
+        # Ensure some parameter validate certain condition in dev env
+        Validator("SERVERS", env='development', cont='localhost'),
+
+        # Ensure some parameter exists in production env
+        Validator('API_KEY', env='production', must_exist=True),
+    ]
+)
+```
+And suppose during development, when `settings.current_env == 'development'`, you don't have the file `.secrets.toml`.
+
+Running `settings.validators.validate()` will fail even if `settings.current_env == 'development'`, because by default all validators will run on all of their environments, whether or not it is the current env. However you could create your settings with the parameter `validate_only_current_env=True`, and nothing will be raised if `settings.current_env == 'development'`. Still, if `settings.current_env == 'production'`, it will fail, forcing you to have `.secrets.toml` file in the directory in `production`, but not necessarily during `development`.
diff --git a/dynaconf/base.py b/dynaconf/base.py
index 93df514..039b9b5 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -218,6 +218,9 @@ class Settings:
         self._not_installed_warnings = []
         self._validate_only = kwargs.pop("validate_only", None)
         self._validate_exclude = kwargs.pop("validate_exclude", None)
+        self._validate_only_current_env = kwargs.pop(
+            "validate_only_current_env", False
+        )
 
         self.validators = ValidatorList(
             self, validators=kwargs.pop("validators", None)
@@ -233,7 +236,9 @@ class Settings:
         self.execute_loaders()
 
         self.validators.validate(
-            only=self._validate_only, exclude=self._validate_exclude
+            only=self._validate_only,
+            exclude=self._validate_exclude,
+            only_current_env=self._validate_only_current_env,
         )
 
     def __call__(self, *args, **kwargs):
@@ -1221,5 +1226,6 @@ RESERVED_ATTRS = (
         "validators",
         "_validate_only",
         "_validate_exclude",
+        "_validate_only_current_env",
     ]
 )
diff --git a/dynaconf/validator.py b/dynaconf/validator.py
index 225f5be..a21beb2 100644
--- a/dynaconf/validator.py
+++ b/dynaconf/validator.py
@@ -164,6 +164,7 @@ class Validator:
         settings: Any,
         only: Optional[Union[str, Sequence]] = None,
         exclude: Optional[Union[str, Sequence]] = None,
+        only_current_env: bool = False,
     ) -> None:
         """Raise ValidationError if invalid"""
         # If only or exclude are not set, this value always passes startswith
@@ -189,6 +190,15 @@ class Validator:
                 # if when is invalid, return canceling validation flow
                 return
 
+        if only_current_env:
+            if settings.current_env.upper() in map(
+                lambda s: s.upper(), self.envs
+            ):
+                self._validate_items(
+                    settings, settings.current_env, only=only, exclude=exclude
+                )
+            return
+
         # If only using current_env, skip using_env decoration (reload)
         if (
             len(self.envs) == 1
@@ -298,6 +308,7 @@ class CombinedValidator(Validator):
         settings: Any,
         only: Optional[Union[str, Sequence]] = None,
         exclude: Optional[Union[str, Sequence]] = None,
+        only_current_env: bool = False,
     ) -> None:  # pragma: no cover
         raise NotImplementedError(
             "subclasses OrValidator or AndValidator implements this method"
@@ -312,12 +323,18 @@ class OrValidator(CombinedValidator):
         settings: Any,
         only: Optional[Union[str, Sequence]] = None,
         exclude: Optional[Union[str, Sequence]] = None,
+        only_current_env: bool = False,
     ) -> None:
         """Ensure at least one of the validators are valid"""
         errors = []
         for validator in self.validators:
             try:
-                validator.validate(settings, only=only, exclude=exclude)
+                validator.validate(
+                    settings,
+                    only=only,
+                    exclude=exclude,
+                    only_current_env=only_current_env,
+                )
             except ValidationError as e:
                 errors.append(e)
                 continue
@@ -342,12 +359,18 @@ class AndValidator(CombinedValidator):
         settings: Any,
         only: Optional[Union[str, Sequence]] = None,
         exclude: Optional[Union[str, Sequence]] = None,
+        only_current_env: bool = False,
     ) -> None:
         """Ensure both the validators are valid"""
         errors = []
         for validator in self.validators:
             try:
-                validator.validate(settings, only=only, exclude=exclude)
+                validator.validate(
+                    settings,
+                    only=only,
+                    exclude=exclude,
+                    only_current_env=only_current_env,
+                )
             except ValidationError as e:
                 errors.append(e)
                 continue
@@ -412,6 +435,12 @@ class ValidatorList(list):
         self,
         only: Optional[Union[str, Sequence]] = None,
         exclude: Optional[Union[str, Sequence]] = None,
+        only_current_env: bool = False,
     ) -> None:
         for validator in self:
-            validator.validate(self.settings, only=only, exclude=exclude)
+            validator.validate(
+                self.settings,
+                only=only,
+                exclude=exclude,
+                only_current_env=only_current_env,
+            )
diff --git a/example/issues/734_validate_only_current_env/app.py b/example/issues/734_validate_only_current_env/app.py
new file mode 100644
index 0000000..44fd2c7
--- /dev/null
+++ b/example/issues/734_validate_only_current_env/app.py
@@ -0,0 +1,30 @@
+from dynaconf import Dynaconf
+from dynaconf import ValidationError
+from dynaconf import Validator
+
+settings_dev = Dynaconf(
+    settings_files=["settings.toml"],
+    environments=True,
+    force_env="development",
+)
+
+settings_prod = Dynaconf(
+    settings_files=["settings.toml"], environments=True, force_env="production"
+)
+
+settings_dev.validators.register(
+    Validator("API_KEY", env="production", must_exist=True)
+)
+
+settings_prod.validators.register(
+    Validator("API_KEY", env="production", must_exist=True)
+)
+
+# Will not fail event if production env has no API_KEY
+settings_dev.validators.validate(only_current_env=True)
+
+# Will fail as production env has no API_KEY and current env is production
+try:
+    settings_prod.validators.validate(only_current_env=True)
+except ValidationError as e:
+    print(e)
diff --git a/example/issues/734_validate_only_current_env/settings.toml b/example/issues/734_validate_only_current_env/settings.toml
new file mode 100644
index 0000000..ccbb8e5
--- /dev/null
+++ b/example/issues/734_validate_only_current_env/settings.toml
@@ -0,0 +1,13 @@
+[development]
+version = "dev"
+age = 35
+name = "Bruno"
+servers = ['127.0.0.1', 'localhost', 'development.com']
+PORT = 80
+
+[production]
+version = "1.0.0"
+age = 35
+name = "Bruno"
+servers = ['production.com']
+PORT = 443


Test patch:
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 4ada622..2d428dc 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -606,6 +606,36 @@ def test_validator_exclude_post_register(
     settings.app.path = "/tmp/app_startup"
 
 
+def test_validator_only_current_env_valid(tmpdir):
+    tmpfile = tmpdir.join("settings.toml")
+    tmpfile.write(TOML)
+    settings = LazySettings(
+        settings_file=str(tmpfile),
+        environments=True,
+        ENV_FOR_DYNACONF="DEVELOPMENT",
+    )
+    settings.validators.register(
+        Validator("IMAGE_1", env="production", must_exist=True)
+    )
+    settings.validators.validate(only_current_env=True)
+
+
+def test_raises_only_current_env_invalid(tmpdir):
+    tmpfile = tmpdir.join("settings.toml")
+    tmpfile.write(TOML)
+    settings = LazySettings(
+        settings_file=str(tmpfile),
+        environments=True,
+        ENV_FOR_DYNACONF="PRODUCTION",
+    )
+    settings.validators.register(
+        Validator("IMAGE_1", env="production", must_exist=True)
+    )
+
+    with pytest.raises(ValidationError):
+        settings.validators.validate(only_current_env=True)
+
+
 def test_raises_on_invalid_selective_args(tmpdir, yaml_validators_good):
     settings = LazySettings(validators=yaml_validators_good, validate_only=int)
     with pytest.raises(ValueError):