Repository: sunpy/sunpy

Issue:
Validity of 'override' kwarg of MultiMethod.add(...) method ignored when adding first function 
This is not a big problem, might want to clear up though. If this is the 
case then I can submit a fix along with some updated unit tests.
 
#### Description 

When I add a function to a `MultiMethod` instance, with *no* entries, and an invalid
value for the `override` keyword argument then the invalidity of this argument is
ignored and the function is successfully registered.

Adding a second function with an illegal value for `override` results in a 
`ValueError` as expected. Example:

```python

def eggs(): pass

def spam(): pass

mm = Multimethod(lambda *a: a)

# Does *not* raise ValueError('Invalid value for override'), it should
mm.add(spam, tuple(), override=56)

# Raises ValueError(...) as expected.
mm.add(eggs, tuple(), override=56)
```

It's a simple fix:

```python
# In sunpy/util/multimethod.py

def add(self, fun, types, override=SILENT)
    # ...
    # ...
    if override not in (SILENT, WARN, FAIL):
        raise ValueError("Invalid value '{0}' for override".format(override))

    # ...
    if override != SILENT:
        # ...
```

Not too keen about the duplication of the values for `override` but, can't think of a way around this.


Discussion:
ping @segfaulthunter 
Pinged florian.mayer@bitsrc.org
Looks like a small bug to me, yes.

On 9 Feb 2017 11:12 am, "Michael Charlton" <notifications@github.com> wrote:

> Pinged florian.mayer@bitsrc.org
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/sunpy/sunpy/issues/1991#issuecomment-278613003>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AABNchjsA3KsQ9PO8CAMph2fRZrVg-kqks5ravR-gaJpZM4L6z2k>
> .
>

please send the patch @m-charlton another good spot. It's almost like we have tests for a reason XD
No worries. Should have the patch and updated tests ready for today or tomorrow,
at the very latest.

As an aside looking at some of the unit tests, I think that it may be a good 
idea to start mocking some of them. This would involve installing [mock](https://pypi.python.org/pypi/mock),
for python < 3.3 and [pytest-mock](https://pypi.python.org/pypi/pytest-mock)

I'd be interested in yours and the other developers opinions regarding this. 

@m-charlton some of the tests are going to be much harder to mock than others, but if you are up for it I am!
Agreed, sometimes mocking can result in tightly coupling test and application
code, which we'd obviously want to avoid, in order to achieve the holy grail of
100% code coverage. 

I'll start to investigate this in an isolated branch and report back when I've
something substantial to submit.

Resolving patch:
diff --git a/sunpy/util/multimethod.py b/sunpy/util/multimethod.py
--- a/sunpy/util/multimethod.py
+++ b/sunpy/util/multimethod.py
@@ -78,8 +78,11 @@ def add(self, fun, types, override=SILENT):
             will be issued, and with FAIL a TypeError is raised when
             attempting to override an existing definition.
         """
+        if override not in (SILENT, WARN, FAIL):
+            raise ValueError("Invalid value '{0}' for override.".format(override))
+
         overriden = False
-        if override:
+        if override != SILENT:
             for signature, _ in self.methods:
                 if all(issubclass(a, b) for a, b in zip(types, signature)):
                     overriden = True
@@ -87,14 +90,10 @@ def add(self, fun, types, override=SILENT):
             raise TypeError
         elif overriden and override == WARN:
             # pylint: disable=W0631
-            warn(
-                'Definition ({0}) overrides prior definition ({1}).'.format(
-                _fmt_t(types), _fmt_t(signature)),
-                TypeWarning,
-                stacklevel=3
-            )
-        elif overriden:
-            raise ValueError('Invalid value for override.')
+            warn('Definition ({0}) overrides prior definition ({1}).'.format(_fmt_t(types),
+                                                                             _fmt_t(signature)),
+                 TypeWarning, stacklevel=3)
+
         self.methods.append((types, fun))
 
     def add_dec(self, *types, **kwargs):
@@ -103,6 +102,7 @@ def add_dec(self, *types, **kwargs):
         override to control overriding behaviour. Compare add.
         """
         self.cache = {}
+
         def _dec(fun):
             self.add(fun, types, kwargs.get('override', SILENT))
             return fun


Test patch:
diff --git a/sunpy/util/tests/test_multimethod.py b/sunpy/util/tests/test_multimethod.py
--- a/sunpy/util/tests/test_multimethod.py
+++ b/sunpy/util/tests/test_multimethod.py
@@ -4,20 +4,26 @@
 from __future__ import absolute_import, division, print_function
 
 import pytest
+import sys
 
 from sunpy.util.multimethod import MultiMethod, FAIL, WARN, TypeWarning
 
+
 def test_super():
     class String(str):
         pass
 
     mm = MultiMethod(lambda *a: a)
 
+    with pytest.raises(TypeError):
+        mm.super()
+
     @mm.add_dec(str, str)
     def foo(foo, bar):
         return 'String'
 
-    @mm.add_dec(String, str)
+    # Suppress pep8 warning "F811 redefinition of unused 'foo' ..."
+    @mm.add_dec(String, str)  # noqa
     def foo(foo, bar):
         return 'Fancy', mm.super(super(String, foo), bar)
 
@@ -42,3 +48,43 @@ def foo(foo, bar):
     mm.add_dec(String, str, override=WARN)(lambda x, y: None)
     w = recwarn.pop(TypeWarning)
     assert 'Definition (String, str) overrides prior definition (str, str).' in str(w.message)
+
+    # Illegal value for 'override'
+    pytest.raises(
+        ValueError, mm.add_dec(String, String, override=sys.maxsize), lambda x, y: None
+    )
+
+
+def test_invalid_arg_for_override_to_add_method():
+
+    def dummy_validator():
+        pass
+
+    mm = MultiMethod(lambda *a: a)
+
+    # See #1991
+    with pytest.raises(ValueError):
+        mm.add(dummy_validator, tuple(), override=sys.maxsize)
+
+
+def test_call_cached():
+
+    def sum_together(first, second):
+        return first + second
+
+    # Setup
+    mm = MultiMethod(lambda *a: a)
+    mm.add(sum_together, (int, int, ))
+    assert mm(3, 4) == 7
+
+    # The following call 'should' use the previously cached call
+    # You'll only see the result of this in the code coverage test though
+    assert mm(1, 2) == 3
+
+
+def test_no_registered_methods():
+
+    mm = MultiMethod(lambda *a: a)
+
+    with pytest.raises(TypeError):
+        mm(2)