Repository: ESSS/oop-ext

Issue:
Use `requires_declaration=True` as default in `IsImplementation`
As discussed in our meeting today with @nicoddemus, @tigarmo and @igortg we need to change back the default for `requires_declaration=True` because in the current state (`False`) provides "duck typing" interfaces, that is, a given class is considered an implementation of a given interface just because it contains the same methods. That brings problems in places where the interfaces are used as IDs, we don't just need objects that have X, Y and Z methods, besides that, we need objects that explicitly declared which interfaces were implemented.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/ESSS/oop-ext/pull/23?src=pr&el=h1) Report
> Merging [#23](https://codecov.io/gh/ESSS/oop-ext/pull/23?src=pr&el=desc) into [master](https://codecov.io/gh/ESSS/oop-ext/commit/a6b298e39a84fdc2344ca36857d3e31a0b741b29?src=pr&el=desc) will **increase** coverage by `0.03%`.
> The diff coverage is `100%`.


```diff
@@            Coverage Diff             @@
##           master      #23      +/-   ##
==========================================
+ Coverage   95.56%   95.59%   +0.03%     
==========================================
  Files          32       32              
  Lines        3225     3226       +1     
==========================================
+ Hits         3082     3084       +2     
+ Misses        143      142       -1
```


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 41e5a14..0de729f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,11 @@
-0.5.1 (2019-12-20)
+0.6.0 (2020-01-31)
 ==================
 
+* Change back the default value of ``requires_declaration`` to ``True`` and fix an error (#22) where the cache wasn 't properly cleared.
+
+0.5.1 (2019-12-20)
+------------------
+
 * Fixes an issue (#20) where mocked `classmethods` weren't considered a valid method during internal checks.
 
 0.5.0 (2019-12-12)
diff --git a/src/oop_ext/interface/_interface.py b/src/oop_ext/interface/_interface.py
index 5fd1ffa..d87f910 100644
--- a/src/oop_ext/interface/_interface.py
+++ b/src/oop_ext/interface/_interface.py
@@ -191,7 +191,7 @@ def _IsClass(obj):
     return isinstance(obj, type)
 
 
-def IsImplementation(class_or_instance, interface, *, requires_declaration=False):
+def IsImplementation(class_or_instance, interface, *, requires_declaration=True):
     """
     :type class_or_instance: type or classobj or object
 
@@ -199,9 +199,11 @@ def IsImplementation(class_or_instance, interface, *, requires_declaration=False
 
     :type requires_declaration: bool
         If `True`, the Interface must have been explicitly declared through :py:func:`ImplementsInterface`
-        for `class_or_interface` to be considered an implementation of `interface`. Alternatively, it's
-        possible to use :py:func:`DeclareClassImplements` from outside the class in order to tell that
-        interfaces are implemented.
+        for `class_or_interface` to be considered an implementation of `interface`. Otherwise it'd only
+        check if `class_or_interface` has all methods defined on `interface`.
+        Note that `class_or_instance` attributes are never evaluated.
+        Alternatively, it's possible to use :py:func:`DeclareClassImplements` from outside the class in
+        order to tell which interfaces are implemented.
 
     :rtype: bool
 
@@ -225,7 +227,7 @@ def IsImplementation(class_or_instance, interface, *, requires_declaration=False
     return is_implementation
 
 
-def IsImplementationOfAny(class_or_instance, interfaces, *, requires_declaration=False):
+def IsImplementationOfAny(class_or_instance, interfaces, *, requires_declaration=True):
     """
     Check if the class or instance implements any of the given interfaces
 
@@ -252,7 +254,7 @@ def IsImplementationOfAny(class_or_instance, interfaces, *, requires_declaration
     return False
 
 
-def AssertImplements(class_or_instance, interface, *, requires_declaration=False):
+def AssertImplements(class_or_instance, interface, *, requires_declaration=True):
     """
     If given a class, will try to match the class against a given interface. If given an object
     (instance), will try to match the class of the given object.
@@ -309,7 +311,7 @@ __ImplementsCache = __ResultsCache()
 __ImplementedInterfacesCache = __ResultsCache()
 
 
-def _CheckIfClassImplements(class_, interface, *, requires_declaration=False):
+def _CheckIfClassImplements(class_, interface, *, requires_declaration=True):
     """
     :type class_: type or classobj
     :param class_:
@@ -863,10 +865,11 @@ def DeclareClassImplements(class_, *interfaces):
     try:
         for interface in interfaces:
             # Forget any previous checks
-            __ImplementsCache.ForgetResult((class_, interface))
+            __ImplementsCache.ForgetResult((class_, interface, False))
+            __ImplementsCache.ForgetResult((class_, interface, True))
             __ImplementedInterfacesCache.ForgetResult(class_)
 
-            AssertImplements(class_, interface)
+            AssertImplements(class_, interface, requires_declaration=False)
     except:
         # Roll back...
         class_.__implements__ = old_implements


Test patch:
diff --git a/src/oop_ext/interface/_tests/test_interface.py b/src/oop_ext/interface/_tests/test_interface.py
index 8beb611..737a454 100644
--- a/src/oop_ext/interface/_tests/test_interface.py
+++ b/src/oop_ext/interface/_tests/test_interface.py
@@ -72,7 +72,7 @@ def testBasics():
 
     assert IsImplementation(C, I) == True  # OK
     # C2 shouldn't need to declare `@ImplementsInterface(I)`
-    assert IsImplementation(C2, I) == True
+    assert IsImplementation(C2, I, requires_declaration=False) == True
     assert IsImplementation(C2, I, requires_declaration=True) == False
     assert not IsImplementation(D, I) == True  # nope
 
@@ -87,7 +87,9 @@ def testBasics():
     # Now declare that C2 implements I
     DeclareClassImplements(C2, I)
 
-    assert IsImplementation(C2, I) == True  # Does not declare
+    assert (
+        IsImplementation(C2, I, requires_declaration=True) == True
+    )  # Even if it doesn't declare
 
 
 def testMissingMethod():
@@ -514,12 +516,16 @@ def testAdaptableInterface():
 
 
 def testNull():
-    AssertImplements(Null(), _InterfM2)  # Not raises BadImplementationError
+    AssertImplements(
+        Null(), _InterfM2, requires_declaration=False
+    )  # Not raises BadImplementationError
 
     class ExtendedNull(Null):
         ""
 
-    AssertImplements(ExtendedNull(), _InterfM2)  # Not raises BadImplementationError
+    AssertImplements(
+        ExtendedNull(), _InterfM2, requires_declaration=False
+    )  # Not raises BadImplementationError
 
 
 def testSetItem():
@@ -575,7 +581,7 @@ def testImplementorWithAny():
         def m3(self, *args, **kwargs):
             ""
 
-    AssertImplements(M3(), _InterfM3)
+    AssertImplements(M3(), _InterfM3, requires_declaration=False)
 
 
 def testInterfaceCheckRequiresInterface():
@@ -687,7 +693,7 @@ def testDeclareClassImplements():
     with pytest.raises(AssertionError):
         DeclareClassImplements(C0, I1)
 
-    assert IsImplementation(C1, I1) == True
+    assert IsImplementation(C1, I1, requires_declaration=False) == True
 
     DeclareClassImplements(C1, I1)
 
@@ -881,9 +887,13 @@ def testIsImplementationOfAny():
             ""
 
     a_obj = A()
-    AssertImplements(a_obj, _InterfM3)
-    assert IsImplementationOfAny(A, [_InterfM1, _InterfM2, _InterfM3, _InterfM4])
-    assert not IsImplementationOfAny(A, [_InterfM1, _InterfM2, _InterfM4])
+    AssertImplements(a_obj, _InterfM3, requires_declaration=False)
+    assert IsImplementationOfAny(
+        A, [_InterfM1, _InterfM2, _InterfM3, _InterfM4], requires_declaration=False
+    )
+    assert not IsImplementationOfAny(
+        A, [_InterfM1, _InterfM2, _InterfM4], requires_declaration=False
+    )
 
 
 def testClassMethodBug(mocker):