Repository: mtik00/yamicache

Issue:
default arguments are not handled
* yamicache version: 0.5.0
* Python version:  2.7
* Operating System:  Windows 10

### Description
This code shows two different cache hits.  The first call should include the default value.  It doesn't, so the call is made twice.
```python
from yamicache import Cache
c = Cache()
@c.cached()
def function1(argument, power=4):
    return argument ** power

function1(2)
function1(2, 4)
assert len(c) == 1
```

### What I Did

```
> python junk.py
Traceback (most recent call last):
  File "junk.py", line 9, in <module>
    assert len(c) == 1
AssertionError
```


Comments:

Patch:
diff --git a/yamicache/yamicache.py b/yamicache/yamicache.py
index 1fcde36..aa8d775 100644
--- a/yamicache/yamicache.py
+++ b/yamicache/yamicache.py
@@ -12,6 +12,7 @@ function calls.
 from __future__ import print_function
 import json
 import time
+import inspect
 import contextlib
 import collections
 from hashlib import sha224
@@ -227,9 +228,28 @@ class Cache(collections.MutableMapping):
 
         key = cached_key
         if not key:
-            key = dict(kwargs)
+            key = {}
+            # We need to grab the default arguments.  `inspect.getargspec()`
+            # returns the function argument names, and any defaults.  The
+            # defaults are always the last args.  For example:
+            # `args=['arg1', 'arg2'], defaults=(4,)` means that `arg2` has a
+            # default of 4.
+            fargs, _, _, defaults = inspect.getargspec(func)
+
+            # Load the defaults first, since they may not be in the calling
+            # spec.
+            if defaults:
+                key = dict(zip(fargs[-len(defaults):], defaults))
+
+            # Now load in the arguments.
+            key.update(kwargs)
             key.update(dict(zip(func.__code__.co_varnames, args)))
 
+            # This next issue is that Python may re-order the keys when we go
+            # to repr them.  This will cause invalid cache misses.  We can fix
+            # this by recreating a dictionary with a 'known' algorithm.
+            key = repr(dict(sorted(key.items())))
+
         return "{prefix}{name}{join}{formatted_key}".format(
             join=self._key_join,
             prefix=(self._prefix + self._key_join) if self._prefix else '',


Test patch:
diff --git a/tests/test_default_args.py b/tests/test_default_args.py
new file mode 100644
index 0000000..68274fe
--- /dev/null
+++ b/tests/test_default_args.py
@@ -0,0 +1,34 @@
+from __future__ import print_function
+from pprint import pprint as pp
+from yamicache import Cache
+
+
+c = Cache(hashing=False)
+
+
+@c.cached()
+def function1(argument, power=4, addition=0, division=2):
+    return argument ** power + addition / division
+
+
+def test_main():
+    '''use default args'''
+
+    # `function1` uses default arguments.  These calls are all equivalent, so
+    # there should only be 1 item in cache.
+    function1(1)
+    function1(1, 4)
+    function1(1, 4, 0)
+    function1(1, 4, addition=0, division=2)
+
+    assert len(c) == 1
+
+    pp(c._data_store)
+
+
+def main():
+    test_main()
+
+
+if __name__ == '__main__':
+    main()