Repository: more-itertools/more-itertools (python)

Issue:
Consider introducing starstarmap
### Description
`starstarmap` is the lost cousin of `itertools.starmap`, so whereas the relation between `map` and `starmap`  parallels the relation between `function(a,b)` and `function(*c)`, the relationship between `map` and `starstarmap` parallels the relation between `function(a,b)` and `function(**d)`.

And similar to how `starmap` can be implemented as:
```
def starmap(function, iterable):
    for args in iterable:
        yield function(*args)
```
`starstarmap` could be implemented as:
```
def starstarmap(function, iterable):
    for args in iterable:
        yield function(**args)
```

### References

The argument for inclusion would be the fact that it simply seems missing from `itertools`, and its usefulness follows from the same pattern as `starmap`.

### Examples
A REST interface returns a list of JSON objects:
```
[
    {"identifier": 1, "title": "The Cathedral and the Bazaar"},
    {"identifier": 2, "title": "Homesteading the Noosphere"},
   ...
]
```
Each object needs to be processed and transformed via a processing function:
```
def func(identifier, title):
    ...
    return Book(...)
```
Now to convert from the JSON-parsed list of dictionaries to processed books, we can simply do:
```
books = starstarmap(func, dictionaries)
```


Pull request:
Add doublestarmap (closes #679)
A continuation of work done by @Skeen in PR #680 on adding a doublestarmap function (see issue #679 for details). Hopefully this attempt will push it over the finish line. I fixed linting violations and improved the wording of the documentation.

Patch:
diff --git a/docs/api.rst b/docs/api.rst
index a24f35d..f179245 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -293,6 +293,7 @@ Others
 .. autoclass:: SequenceView
 .. autofunction:: time_limited
 .. autofunction:: map_if(iterable, pred, func, func_else=lambda x: x)
+.. autofunction:: doublestarmap
 
 ----
 
diff --git a/more_itertools/more.py b/more_itertools/more.py
index 6598435..72bd0ca 100755
--- a/more_itertools/more.py
+++ b/more_itertools/more.py
@@ -67,6 +67,7 @@ __all__ = [
     'distinct_permutations',
     'distribute',
     'divide',
+    'doublestarmap',
     'duplicates_everseen',
     'duplicates_justseen',
     'classify_unique',
@@ -4726,3 +4727,21 @@ def join_mappings(**field_to_map):
             ret[key][field_name] = value
 
     return dict(ret)
+
+
+def doublestarmap(func, iterable):
+    """Apply *func* to every item of *iterable* by dictionary unpacking
+    the item into *func*.
+
+    The difference between :func:`itertools.starmap` and :func:`doublestarmap`
+    parallels the distinction between ``func(*a)`` and ``func(**a)``.
+
+    >>> iterable = [{'a': 1, 'b': 2}, {'a': 40, 'b': 60}]
+    >>> list(doublestarmap(lambda a, b: a + b, iterable))
+    [3, 100]
+
+    ``TypeError`` will be raised if *func*'s signature doesn't match the
+    mapping contained in *iterable* or if *iterable* does not contain mappings.
+    """
+    for item in iterable:
+        yield func(**item)
diff --git a/more_itertools/more.pyi b/more_itertools/more.pyi
index dc559eb..ac10966 100644
--- a/more_itertools/more.pyi
+++ b/more_itertools/more.pyi
@@ -13,6 +13,7 @@ from typing import (
     Mapping,
     Iterable,
     Iterator,
+    Mapping,
     overload,
     Reversible,
     Sequence,
@@ -700,3 +701,7 @@ def powerset_of_sets(iterable: Iterable[_T]) -> Iterator[set[_T]]: ...
 def join_mappings(
     **field_to_map: Mapping[_T, _V]
 ) -> dict[_T, dict[str, _V]]: ...
+def doublestarmap(
+    func: Callable[..., _T],
+    iterable: Iterable[Mapping[str, Any]],
+) -> Iterator[_T]: ...


Test patch:
diff --git a/tests/test_more.py b/tests/test_more.py
index b3e2ade..7c19e0b 100644
--- a/tests/test_more.py
+++ b/tests/test_more.py
@@ -5803,3 +5803,48 @@ class JoinMappingTests(TestCase):
 
     def test_empty(self):
         self.assertEqual(dict(mi.join_mappings()), {})
+
+
+class DoubleStarMapTests(TestCase):
+    def test_construction(self):
+        iterable = [{'price': 1.23}, {'price': 42}, {'price': 0.1}]
+        actual = list(mi.doublestarmap('{price:.2f}'.format, iterable))
+        expected = ['1.23', '42.00', '0.10']
+        self.assertEqual(actual, expected)
+
+    def test_identity(self):
+        iterable = [{'x': 1}, {'x': 2}, {'x': 3}]
+        actual = list(mi.doublestarmap(lambda x: x, iterable))
+        expected = [1, 2, 3]
+        self.assertEqual(actual, expected)
+
+    def test_adding(self):
+        iterable = [{'a': 1, 'b': 2}, {'a': 3, 'b': 4}]
+        actual = list(mi.doublestarmap(lambda a, b: a + b, iterable))
+        expected = [3, 7]
+        self.assertEqual(actual, expected)
+
+    def test_mismatch_function_smaller(self):
+        iterable = [{'a': 1, 'b': 2}, {'a': 3, 'b': 4}]
+        with self.assertRaises(TypeError):
+            list(mi.doublestarmap(lambda a: a, iterable))
+
+    def test_mismatch_function_different(self):
+        iterable = [{'a': 1}, {'a': 2}]
+        with self.assertRaises(TypeError):
+            list(mi.doublestarmap(lambda x: x, iterable))
+
+    def test_mismatch_function_larger(self):
+        iterable = [{'a': 1}, {'a': 2}]
+        with self.assertRaises(TypeError):
+            list(mi.doublestarmap(lambda a, b: a + b, iterable))
+
+    def test_no_mapping(self):
+        iterable = [1, 2, 3, 4]
+        with self.assertRaises(TypeError):
+            list(mi.doublestarmap(lambda x: x, iterable))
+
+    def test_empty(self):
+        actual = list(mi.doublestarmap(lambda x: x, []))
+        expected = []
+        self.assertEqual(actual, expected)