Repository: hgrecco/pint

Issue:
Wraps crashes when using the same unit for different inputvalues
When I have this function:

```
@ureg.wraps(ureg.M, (ureg.degC, ureg.M, ureg.M))
def hco3_eq_from_tillmans_phreeqc(temperature, CO2, NaCl_conc=__NaCl_conc):
    pass
```
And call it as:

```
temp = ureg.Quantity(12, ureg.degC)
print(mdl.hco3_eq_from_tillmans_phreeqc(temp, 1*ureg.mM, NaCl_conc = 0.1 * ureg.mM))
```
Note how mM=mmol/L, a unit I added to my 

It gives me the following error:

```
Traceback (most recent call last):  File "C:\Users\korevma\.vscode\extensions\ms-python.python-2018.2.1\pythonFiles\PythonTools\visualstudio_py_launcher_nodebug.py", line 74, in run
    _vspu.exec_file(file, globals_obj)
  File "C:\Users\korevma\.vscode\extensions\ms-python.python-2018.2.1\pythonFiles\PythonTools\visualstudio_py_util.py", line 119, in exec_file
    exec_code(code, file, global_variables)
  File "C:\Users\korevma\.vscode\extensions\ms-python.python-2018.2.1\pythonFiles\PythonTools\visualstudio_py_util.py", line 95, in exec_code
    exec(code_obj, global_variables)
```

After removing the third argument, the wrapping works fine. Is it because the same unit is used twice? Does it have something to do with the usage of `unit_args_ndx` in the `_parse_wrap_args` function?

Im am using python 3

Discussion:

Patch:
diff --git a/docs/wrapping.rst b/docs/wrapping.rst
index a4913b3..dd9a39f 100644
--- a/docs/wrapping.rst
+++ b/docs/wrapping.rst
@@ -148,6 +148,37 @@ airmass::
     def solar_position(lat, lon, press, tamb, timestamp):
         return zenith, azimuth, airmass
 
+Optional arguments
+------------------
+
+For a function with named keywords with optional values, use a tuple for all
+arguments:
+
+.. doctest::
+
+    >>> @ureg.wraps(ureg.second, (ureg.meters, ureg.meters/ureg.second**2))
+    ... def calculate_time_to_fall(height, gravity=Q_(9.8, 'm/s^2'), verbose=False):
+    ...     """Calculate time to fall from a height h.
+    ...
+    ...     By default, the gravity is assumed to be earth gravity,
+    ...     but it can be modified.
+    ...
+    ...     d = .5 * g * t**2
+    ...     t = sqrt(2 * d / g)
+    ...     """
+    ...     t = sqrt(2 * height / gravity)
+    ...     if verbose: print(str(t) + " seconds to fall")
+    ...     return t
+    ...
+    >>> lunar_module_height = Q_(22, 'feet') + Q_(11, 'inches')
+    >>> calculate_time_to_fall(lunar_module_height, verbose=True)
+    1.1939473204801092 seconds to fall
+    <Quantity(1.1939473204801092, 'second')>
+    >>>
+    >>> moon_gravity = Q_(1.625, 'm/s^2')
+    >>> tcalculate_time_to_fall(lunar_module_height, moon_gravity)
+    <Quantity(2.932051001760214, 'second')>
+
 
 Specifying relations between arguments
 --------------------------------------
@@ -171,6 +202,19 @@ You can use more than one label:
     ... def some_function(x, y, z):
     ...     pass
 
+With optional arguments
+
+.. doctest::
+
+    >>> @ureg.wraps('=A*B', ('=A', '=B'))
+    ... def get_displacement(time, rate=Q_(1, 'm/s')):
+    ...     return time * rate
+    ...
+    >>> get_displacement(Q_(2, 's'))
+    <Quantity(2, 'meter')>
+    >>> get_displacement(Q_(2, 's'), Q_(1, 'deg/s'))
+    <Quantity(2, 'degree')>
+
 
 Ignoring an argument or return value
 ------------------------------------
diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py
index 1105038..4a8b862 100644
--- a/pint/registry_helpers.py
+++ b/pint/registry_helpers.py
@@ -15,6 +15,12 @@ from .compat import string_types, zip_longest
 from .errors import DimensionalityError
 from .util import to_units_container, UnitsContainer
 
+try:
+    from inspect import signature
+except ImportError:
+    # Python2 does not have the inspect library. Import the backport.
+    from funcsigs import signature
+
 
 def _replace_units(original_units, values_by_name):
     """Convert a unit compatible type to a UnitsContainer.
@@ -165,6 +171,19 @@ def wraps(ureg, ret, args, strict=True):
         @functools.wraps(func, assigned=assigned, updated=updated)
         def wrapper(*values, **kw):
 
+
+            # Named keywords may have been left blank. Wherever the named keyword is blank,
+            # fill it in with the default value.
+            sig = signature(func)
+            bound_arguments = sig.bind(*values, **kw)
+
+            for param in sig.parameters.values():
+                if param.name not in bound_arguments.arguments:
+                    bound_arguments.arguments[param.name] = param.default
+
+            values = [bound_arguments.arguments[key] for key in sig.parameters.keys()]
+            kw = {}
+                
             # In principle, the values are used as is
             # When then extract the magnitudes when needed.
             new_values, values_by_name = converter(ureg, values, strict)
diff --git a/setup.py b/setup.py
index 354a79d..1bcae3c 100644
--- a/setup.py
+++ b/setup.py
@@ -60,4 +60,10 @@ setup(
         'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
-    ])
+    ],
+    extras_require={
+        ':python_version == "2.7"': [
+            'funcsigs',
+        ],
+    },
+    )


Test patch:
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index 158fbe1..0cefce9 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -571,3 +571,57 @@ class TestIssuesNP(QuantityTestCase):
 
         self.assertEqual(f(ureg.Quantity(1, '')), 2)
         self.assertRaises(DimensionalityError, f, ureg.Quantity(1, 'm'))
+
+    def test_issue625a(self):
+        try:
+            from inspect import signature
+        except ImportError:
+            # Python2 does not have the inspect library. Import the backport.
+            from funcsigs import signature
+
+        ureg = UnitRegistry()
+        Q_ = ureg.Quantity
+        from math import sqrt
+
+        @ureg.wraps(ureg.second, (ureg.meters, ureg.meters/ureg.second**2))
+        def calculate_time_to_fall(height, gravity=Q_(9.8, 'm/s^2')):
+            """Calculate time to fall from a height h with a default gravity.
+
+            By default, the gravity is assumed to be earth gravity,
+            but it can be modified.
+
+            d = .5 * g * t**2
+            t = sqrt(2 * d / g)
+            """
+            return sqrt(2 * height / gravity)
+
+        lunar_module_height = Q_(10, 'm')
+        t1 = calculate_time_to_fall(lunar_module_height)
+        print(t1)
+        self.assertAlmostEqual(t1, Q_(1.4285714285714286, 's'))
+
+        moon_gravity = Q_(1.625, 'm/s^2')
+        t2 = calculate_time_to_fall(lunar_module_height, moon_gravity)
+        self.assertAlmostEqual(t2, Q_(3.508232077228117, 's'))
+
+    def test_issue625b(self):
+        try:
+            from inspect import signature
+        except ImportError:
+            # Python2 does not have the inspect library. Import the backport.
+            from funcsigs import signature
+
+        ureg = UnitRegistry()
+        Q_ = ureg.Quantity
+
+        @ureg.wraps('=A*B', ('=A', '=B'))
+        def get_displacement(time, rate=Q_(1, 'm/s')):
+            """Calculates displacement from a duration and default rate.
+            """
+            return time * rate
+
+        d1 = get_displacement(Q_(2, 's'))
+        self.assertAlmostEqual(d1, Q_(2, 'm'))
+
+        d2 = get_displacement(Q_(2, 's'), Q_(1, 'deg/s'))
+        self.assertAlmostEqual(d2, Q_(2,' deg'))