Repository: dynaconf/dynaconf

Issue:
[bug] Only one CombinedValidator is registered - subsequent are silently ignored
**Describe the bug**
If validators are added through `settings.validators.register()`, only first CombinedValidator is registered - subsequent are silently skipped.

**Analysis**
The root cause is `Validator.__eq__()` method. `ValidatorList.register()` will go through provided validators and add them, but only if they aren't already on the list (`validator not in self`). `in` will return "`True` if an item of *s* is equal to *x*" ([docs](https://docs.python.org/3.8/library/stdtypes.html#common-sequence-operations)). That logic was added in #256 .

When `Validator.__eq__()` compares different objects, it looks into various `Validator` properties and compares them in pair. If they are all the same, `__eq__()` will assume these are two instances of effectively the same validation rule.

The problem is, `CombinedValidator` doesn't have any of these properties, so two completely different `CombinedValidator` will appear to be the same for `__eq__()` method.


**To Reproduce**
In python shell:
```
>>> from dynaconf import Validator
>>> (Validator("foo.foo") | Validator("foo.bar")) == (Validator("bar.foo") & Validator("bar.bar"))
True
```

This should return False, as these two `CombinedValidator`s have nothing in common.

**Environment (please complete the following information):**
 - OS: Linux/Fedora32
 - Dynaconf master (6c568d687e29ca5ed9806a74f1f4fb7e4b96be2f), 3.0.1

**Additional context**
I might try working on patch, but I'm not sure about best approach. Perhaps we need type comparison inside `__eq__()` (so comparing AndValidator to OrValidator can return early). But how do we compare two `AndValidator`s? Look into combined validators properties recursively?


Discussion:
I think we can add a property that combines the attributes of all the validators and  then we compare.

I like your idea of returning earlier.

Resolving patch:
diff --git a/dynaconf/validator.py b/dynaconf/validator.py
--- a/dynaconf/validator.py
+++ b/dynaconf/validator.py
@@ -5,6 +5,16 @@
 from dynaconf.utils.functional import empty
 
 
+EQUALITY_ATTRS = (
+    "names",
+    "must_exist",
+    "when",
+    "condition",
+    "operations",
+    "envs",
+)
+
+
 class ValidationError(Exception):
     pass
 
@@ -125,16 +135,12 @@ def __eq__(self, other):
         if self is other:
             return True
 
+        if type(self).__name__ != type(other).__name__:
+            return False
+
         identical_attrs = (
             getattr(self, attr) == getattr(other, attr)
-            for attr in (
-                "names",
-                "must_exist",
-                "when",
-                "condition",
-                "operations",
-                "envs",
-            )
+            for attr in EQUALITY_ATTRS
         )
         if all(identical_attrs):
             return True
@@ -229,6 +235,12 @@ def __init__(self, validator_a, validator_b, *args, **kwargs):
         """Takes 2 validators and combines the validation"""
         self.validators = (validator_a, validator_b)
         super().__init__(*args, **kwargs)
+        for attr in EQUALITY_ATTRS:
+            if not getattr(self, attr, None):
+                value = tuple(
+                    getattr(validator, attr) for validator in self.validators
+                )
+                setattr(self, attr, value)
 
     def validate(self, settings):  # pragma: no cover
         raise NotImplementedError(


Test patch:
diff --git a/tests/test_validators.py b/tests/test_validators.py
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -211,21 +211,53 @@ def test_no_reload_on_single_env(tmpdir, mocker):
     assert using_env.call_count == 1
 
 
-def test_equality():
-    validator1 = Validator("VERSION", "AGE", "NAME", must_exist=True)
-    validator2 = Validator("VERSION", "AGE", "NAME", must_exist=True)
+@pytest.mark.parametrize(
+    "this_validator,another_validator",
+    [
+        (
+            Validator("VERSION", "AGE", "NAME", must_exist=True),
+            Validator("VERSION", "AGE", "NAME", must_exist=True),
+        ),
+        (
+            Validator("VERSION") | Validator("AGE"),
+            Validator("VERSION") | Validator("AGE"),
+        ),
+        (
+            Validator("VERSION") & Validator("AGE"),
+            Validator("VERSION") & Validator("AGE"),
+        ),
+    ],
+)
+def test_equality(this_validator, another_validator):
+    assert this_validator == another_validator
+    assert this_validator is not another_validator
 
-    assert validator1 == validator2
-    assert validator1 is not validator2
 
-    validator3 = (
-        Validator("IMAGE_1", when=Validator("BASE_IMAGE", must_exist=True)),
-    )
-    validator4 = (
-        Validator("IMAGE_1", when=Validator("MYSQL_HOST", must_exist=True)),
-    )
-
-    assert validator3 != validator4
+@pytest.mark.parametrize(
+    "this_validator,another_validator",
+    [
+        (
+            Validator(
+                "IMAGE_1", when=Validator("BASE_IMAGE", must_exist=True)
+            ),
+            Validator(
+                "IMAGE_1", when=Validator("MYSQL_HOST", must_exist=True)
+            ),
+        ),
+        (Validator("VERSION"), Validator("VERSION") & Validator("AGE")),
+        (Validator("VERSION"), Validator("VERSION") | Validator("AGE")),
+        (
+            Validator("VERSION") | Validator("AGE"),
+            Validator("VERSION") & Validator("AGE"),
+        ),
+        (
+            Validator("VERSION") | Validator("AGE"),
+            Validator("NAME") | Validator("BASE_IMAGE"),
+        ),
+    ],
+)
+def test_inequality(this_validator, another_validator):
+    assert this_validator != another_validator
 
 
 def test_ignoring_duplicate_validators(tmpdir):