Repository: ianlini/flatten-dict

Issue:
Feature Proposition: Underscores as native separators
Hey there!

Can I make a pull request for native inclusion of the underscores as a separator? 

Something like: 

`
flatten(normal_dict, reducer="_")
`

which would output the expanded dict with underscores as separators directly. 

What do you think? 

Cheers and thanks for a great little utility 😄 



Discussion:
duarteocarmo: had to add another commit (rst. and main function docstring..)
duarteocarmo: Alright! Patched everything, let me know what you think 
ianlini: LGTM now. I will release this with 0.2.0 recently.
Thanks for your contribution!

Patch:
diff --git a/README.rst b/README.rst
index 7590f88..68a1910 100644
--- a/README.rst
+++ b/README.rst
@@ -30,11 +30,12 @@ Flatten
        ----------
        d : dict-like object
            The dict that will be flattened.
-       reducer : {'tuple', 'path', Callable}
+       reducer : {'tuple', 'path', 'underscore', Callable}
            The key joining method. If a `Callable` is given, the `Callable` will be
            used to reduce.
            'tuple': The resulting key will be tuple of the original keys.
            'path': Use `os.path.join` to join keys.
+           'underscore': Use underscores to join keys.
        inverse : bool
            Whether you want invert the resulting key and value.
        enumerate_types : Sequence[type]
@@ -154,11 +155,12 @@ Unflatten
        ----------
        d : dict-like object
            The dict that will be unflattened.
-       splitter : {'tuple', 'path', Callable}
+       splitter : {'tuple', 'path', 'underscore', Callable}
            The key splitting method. If a Callable is given, the Callable will be
            used to split.
            'tuple': Use each element in the tuple key as the key of the unflattened dict.
            'path': Use `pathlib.Path.parts` to split keys.
+           'underscore': Use underscores to split keys.
        inverse : bool
            Whether you want to invert the key and value before flattening.
 
diff --git a/flatten_dict/flatten_dict.py b/flatten_dict/flatten_dict.py
index 63a11e0..110ffbb 100644
--- a/flatten_dict/flatten_dict.py
+++ b/flatten_dict/flatten_dict.py
@@ -2,18 +2,20 @@ from collections import Mapping
 
 import six
 
-from .reducer import tuple_reducer, path_reducer
-from .splitter import tuple_splitter, path_splitter
+from .reducer import tuple_reducer, path_reducer, underscore_reducer
+from .splitter import tuple_splitter, path_splitter, underscore_splitter
 
 
 REDUCER_DICT = {
     'tuple': tuple_reducer,
     'path': path_reducer,
+    'underscore': underscore_reducer,
 }
 
 SPLITTER_DICT = {
     'tuple': tuple_splitter,
     'path': path_splitter,
+    'underscore': underscore_splitter,
 }
 
 
@@ -24,11 +26,12 @@ def flatten(d, reducer='tuple', inverse=False, enumerate_types=()):
     ----------
     d : dict-like object
         The dict that will be flattened.
-    reducer : {'tuple', 'path', Callable}
+    reducer : {'tuple', 'path', 'underscore', Callable}
         The key joining method. If a `Callable` is given, the `Callable` will be
         used to reduce.
         'tuple': The resulting key will be tuple of the original keys.
         'path': Use `os.path.join` to join keys.
+        'underscore': Use underscores to join keys.
     inverse : bool
         Whether you want invert the resulting key and value.
     enumerate_types : Sequence[type]
@@ -94,11 +97,12 @@ def unflatten(d, splitter='tuple', inverse=False):
     ----------
     d : dict-like object
         The dict that will be unflattened.
-    splitter : {'tuple', 'path', Callable}
+    splitter : {'tuple', 'path', 'underscore', Callable}
         The key splitting method. If a Callable is given, the Callable will be
         used to split.
         'tuple': Use each element in the tuple key as the key of the unflattened dict.
         'path': Use `pathlib.Path.parts` to split keys.
+        'underscore': Use underscores to join keys.
     inverse : bool
         Whether you want to invert the key and value before flattening.
 
diff --git a/flatten_dict/reducer.py b/flatten_dict/reducer.py
index 5eee3f3..bd81d3e 100644
--- a/flatten_dict/reducer.py
+++ b/flatten_dict/reducer.py
@@ -11,3 +11,10 @@ def path_reducer(k1, k2):
         return k2
     else:
         return os.path.join(k1, k2)
+
+
+def underscore_reducer(k1, k2):
+    if k1 is None:
+        return k2
+    else:
+        return k1 + "_" + k2
diff --git a/flatten_dict/splitter.py b/flatten_dict/splitter.py
index d36faf2..5cf9f7e 100644
--- a/flatten_dict/splitter.py
+++ b/flatten_dict/splitter.py
@@ -8,3 +8,8 @@ def tuple_splitter(flat_key):
 def path_splitter(flat_key):
     keys = PurePath(flat_key).parts
     return keys
+
+
+def underscore_splitter(flat_key):
+    keys = tuple(flat_key.split("_"))
+    return keys


Test patch:
diff --git a/flatten_dict/tests.py b/flatten_dict/tests.py
index 89af964..a66d7fd 100644
--- a/flatten_dict/tests.py
+++ b/flatten_dict/tests.py
@@ -57,6 +57,11 @@ def test_flatten_dict_path():
     assert flatten(normal_dict, reducer='path') == flat_path_dict
 
 
+def test_flatten_dict_underscore():
+    flat_underscore_dict = {'_'.join(k): v for k, v in six.viewitems(flat_normal_dict)}
+    assert flatten(normal_dict, reducer='underscore') == flat_underscore_dict
+
+
 def test_flatten_dict_inverse_with_duplicated_value():
     dup_val_dict = normal_dict.copy()
     dup_val_dict['a'] = '2.1.1'
@@ -82,6 +87,11 @@ def test_unflatten_dict_path():
     assert unflatten(flat_path_dict, splitter='path') == normal_dict
 
 
+def test_unflatten_dict_underscore():
+    flat_underscore_dict = {'_'.join(k): v for k, v in six.viewitems(flat_normal_dict)}
+    assert unflatten(flat_underscore_dict, splitter='underscore') == normal_dict
+
+
 def test_unflatten_dict_inverse_with_duplicated_value():
     inv_flat_normal_dict = {v: k for k, v in six.viewitems(flat_normal_dict)}
     inv_flat_normal_dict['2.1.1'] = ('c', 'b', 'a')
@@ -128,6 +138,11 @@ def test_flatten_dict_with_list_path():
     assert flatten(dict_with_list, reducer='path') == flat_path_dict
 
 
+def test_flatten_dict_with_list_underscore():
+    flat_underscore_dict = {'_'.join(k): v for k, v in six.viewitems(flat_dict_with_list)}
+    assert flatten(dict_with_list, reducer='underscore') == flat_underscore_dict
+
+
 def test_unflatten_dict_with_list():
     assert unflatten(flat_dict_with_list) == dict_with_list
 
@@ -141,6 +156,11 @@ def test_unflatten_dict_with_list_path():
     assert unflatten(flat_path_dict, splitter='path') == dict_with_list
 
 
+def test_unflatten_dict_with_list_underscore():
+    flat_underscore_dict = {'_'.join(k): v for k, v in six.viewitems(flat_dict_with_list)}
+    assert unflatten(flat_underscore_dict, splitter='underscore') == dict_with_list
+
+
 flat_dict_with_enumerated_list = {
     ('a',): '0',
     ('b', 'a'): '1.0',