From 61056e0245242a0f53357ce6438236ea58a2d740 Mon Sep 17 00:00:00 2001 From: Ian Lin Date: Sun, 29 Sep 2019 15:59:15 +0800 Subject: [PATCH] Add keep_empty_types to support keeping empty values (#18) * add doc8 * add keep_empty_types * update docstring * update README * move doc8 to dev dependencies --- README.rst | 25 +++++++- poetry.lock | 70 ++++++++++++++++++++- pyproject.toml | 1 + src/flatten_dict/flatten_dict.py | 32 +++++++--- src/flatten_dict/tests/flatten_dict_test.py | 45 +++++++++++++ 5 files changed, 163 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index dfbbfab..1df63e5 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ Flatten .. code-block:: python - def flatten(d, reducer='tuple', inverse=False, enumerate_types=()): + def flatten(d, reducer='tuple', inverse=False, enumerate_types=(), keep_empty_types=()): """Flatten `Mapping` object. Parameters @@ -44,6 +44,15 @@ Flatten Flatten these types using `enumerate`. For example, if we set `enumerate_types` to ``(list,)``, `list` indices become keys: ``{'a': ['b', 'c']}`` -> ``{('a', 0): 'b', ('a', 1): 'c'}``. + keep_empty_types : Sequence[type] + By default, ``flatten({1: 2, 3: {}})`` will give you ``{(1,): 2}``, that is, the key ``3`` + will disappear. + This is also applied for the types in `enumerate_types`, that is, + ``flatten({1: 2, 3: []}, enumerate_types=(list,))`` will give you ``{(1,): 2}``. + If you want to keep those empty values, you can specify the types in `keep_empty_types`: + + >>> flatten({1: 2, 3: {}}, keep_empty_types=(dict,)) + {(1,): 2, (3,): {}} Returns ------- @@ -145,6 +154,20 @@ We can even flatten a `list` directly: (1,): 2, (2,): 3} +If there is an empty dict in the values, by default, it will disappear after flattened: + +.. code-block:: python + + In [4]: flatten({1: 2, 3: {}}) + Out[4]: {(1,): 2} + +We can keep the empty dict in the result using ``keep_empty_types=(dict,)``: + +.. code-block:: python + + In [5]: flatten({1: 2, 3: {}}, keep_empty_types=(dict,)) + Out[5]: {(1,): 2, (3,): {}} + Unflatten ````````` diff --git a/poetry.lock b/poetry.lock index e39311a..a187206 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "19.1.0" +[[package]] +category = "dev" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + [[package]] category = "dev" description = "Cross-platform colored terminal text." @@ -49,6 +57,29 @@ optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" version = "4.5.4" +[[package]] +category = "dev" +description = "Style checker for Sphinx (or other) RST documentation" +name = "doc8" +optional = false +python-versions = "*" +version = "0.8.0" + +[package.dependencies] +chardet = "*" +docutils = "*" +restructuredtext-lint = ">=0.7" +six = "*" +stevedore = "*" + +[[package]] +category = "dev" +description = "Docutils -- Python Documentation Utilities" +name = "docutils" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.15.2" + [[package]] category = "dev" description = "Discover and load entry points from installed packages." @@ -200,6 +231,14 @@ six = "*" python = "<3.5" version = "*" +[[package]] +category = "dev" +description = "Python Build Reasonableness" +name = "pbr" +optional = false +python-versions = "*" +version = "5.4.3" + [[package]] category = "dev" description = "plugin and hook calling mechanisms for python" @@ -280,6 +319,17 @@ version = ">=1.0" python = "<3.6" version = ">=2.2.0" +[[package]] +category = "dev" +description = "reStructuredText linter" +name = "restructuredtext-lint" +optional = false +python-versions = "*" +version = "1.3.0" + +[package.dependencies] +docutils = ">=0.11,<1.0" + [[package]] category = "main" description = "scandir, a better directory iterator and faster os.walk()" @@ -297,6 +347,18 @@ optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" version = "1.12.0" +[[package]] +category = "dev" +description = "Manage dynamic plugins for Python applications" +name = "stevedore" +optional = false +python-versions = "*" +version = "1.31.0" + +[package.dependencies] +pbr = ">=2.0.0,<2.1.0 || >2.1.0" +six = ">=1.10.0" + [[package]] category = "dev" description = "Python Library for Tom's Obvious, Minimal Language" @@ -363,16 +425,19 @@ version = "0.6.0" more-itertools = "*" [metadata] -content-hash = "a023bd18c4deb50bf88c203452a01d60fb5afef839a492db51dbdccf4982f773" +content-hash = "28c473be28ffc3a37208c0c290cfd29d3313b10ea4cf50c8680f2ae0a38e1644" python-versions = "~2.7 || ^3.5" [metadata.hashes] atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] +chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] configparser = ["254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c", "c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df"] contextlib2 = ["7197aa736777caac513dbd800944c209a49765bf1979b12b037dce0277077ed3", "9d2c67f18c1f9b6db1b46317f7f784aa82789d2ee5dea5d9c0f0f2a764eb862e"] coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"] +doc8 = ["2df89f9c1a5abfb98ab55d0175fed633cae0cf45025b8b1e0ee5ea772be28543", "d12f08aa77a4a65eb28752f4bc78f41f611f9412c4155e2b03f1f5d4a45efe04"] +docutils = ["6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", "9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", "a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"] entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] enum34 = ["2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", "644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", "6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", "8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1"] filelock = ["18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", "929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"] @@ -384,14 +449,17 @@ mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "d more-itertools = ["38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", "c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", "fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9", "409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"] packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"] pathlib2 = ["0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db", "6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"] +pbr = ["2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", "b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9"] pluggy = ["0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", "fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"] py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"] pytest = ["8fc39199bdda3d9d025d3b1f4eb99a192c20828030ea7c9a0d2840721de7d347", "d100a02770f665f5dcf7e3f08202db29857fee6d15f34c942be0a511f39814f0"] +restructuredtext-lint = ["97b3da356d5b3a8514d8f1f9098febd8b41463bed6a1d9f126cf0a048b6fd908"] scandir = ["2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e", "2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022", "2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f", "2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f", "4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae", "67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173", "7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4", "8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32", "92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188", "b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d", "cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"] six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] +stevedore = ["01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", "e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14"] toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] tox = ["0bc216b6a2e6afe764476b4a07edf2c1dab99ed82bb146a1130b2e828f5bff5e", "c4f6b319c20ba4913dbfe71ebfd14ff95d1853c4231493608182f66e566ecfe1"] typing = ["91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", "c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", "f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"] diff --git a/pyproject.toml b/pyproject.toml index e414c47..a7ad5c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ pytest = "^4.6" flake8 = "^3.7" tox = "^3.14" coverage = "^4.5" +doc8 = "^0.8.0" [build-system] requires = ["poetry>=0.12"] diff --git a/src/flatten_dict/flatten_dict.py b/src/flatten_dict/flatten_dict.py index 5ac08e1..9e46e78 100644 --- a/src/flatten_dict/flatten_dict.py +++ b/src/flatten_dict/flatten_dict.py @@ -22,7 +22,7 @@ } -def flatten(d, reducer='tuple', inverse=False, enumerate_types=()): +def flatten(d, reducer='tuple', inverse=False, enumerate_types=(), keep_empty_types=()): """Flatten `Mapping` object. Parameters @@ -41,6 +41,15 @@ def flatten(d, reducer='tuple', inverse=False, enumerate_types=()): Flatten these types using `enumerate`. For example, if we set `enumerate_types` to ``(list,)``, `list` indices become keys: ``{'a': ['b', 'c']}`` -> ``{('a', 0): 'b', ('a', 1): 'c'}``. + keep_empty_types : Sequence[type] + By default, ``flatten({1: 2, 3: {}})`` will give you ``{(1,): 2}``, that is, the key ``3`` + will disappear. + This is also applied for the types in `enumerate_types`, that is, + ``flatten({1: 2, 3: []}, enumerate_types=(list,))`` will give you ``{(1,): 2}``. + If you want to keep those empty values, you can specify the types in `keep_empty_types`: + + >>> flatten({1: 2, 3: {}}, keep_empty_types=(dict,)) + {(1,): 2, (3,): {}} Returns ------- @@ -61,13 +70,20 @@ def _flatten(d, parent=None): for key, value in key_value_iterable: flat_key = reducer(parent, key) if isinstance(value, flattenable_types): - _flatten(value, flat_key) - else: - if inverse: - flat_key, value = value, flat_key - if flat_key in flat_dict: - raise ValueError("duplicated key '{}'".format(flat_key)) - flat_dict[flat_key] = value + if value: + # recursively build the result + _flatten(value, flat_key) + continue + elif not isinstance(value, keep_empty_types): + # ignore the key that has an empty value + continue + + # add an item to the result + if inverse: + flat_key, value = value, flat_key + if flat_key in flat_dict: + raise ValueError("duplicated key '{}'".format(flat_key)) + flat_dict[flat_key] = value _flatten(d) return flat_dict diff --git a/src/flatten_dict/tests/flatten_dict_test.py b/src/flatten_dict/tests/flatten_dict_test.py index a816141..bd82f12 100644 --- a/src/flatten_dict/tests/flatten_dict_test.py +++ b/src/flatten_dict/tests/flatten_dict_test.py @@ -214,3 +214,48 @@ def test_flatten_dict_with_list_with_enumerate_types( def test_flatten_list(): assert flatten([1, 2], enumerate_types=(list,)) == {(0,): 1, (1,): 2} + + +@pytest.fixture +def dict_with_empty_dict(): + return { + 'a': '0', + 'b': { + 'a': '1.0', + 'b': '1.1', + }, + 'c': { + 'a': '2.0', + 'b': { + 'a': '2.1.0', + 'b': '2.1.1', + 'c': {}, + }, + }, + } + + +@pytest.fixture +def flat_tuple_dict_with_empty_dict(): + return { + ('a',): '0', + ('b', 'a'): '1.0', + ('b', 'b'): '1.1', + ('c', 'a'): '2.0', + ('c', 'b', 'a'): '2.1.0', + ('c', 'b', 'b'): '2.1.1', + ('c', 'b', 'c'): {}, + } + + +def test_flatten_dict_with_empty_dict(dict_with_empty_dict, flat_tuple_dict): + assert flatten(dict_with_empty_dict) == flat_tuple_dict + + +def test_flatten_dict_with_empty_dict_kept(dict_with_empty_dict, flat_tuple_dict_with_empty_dict): + assert (flatten(dict_with_empty_dict, keep_empty_types=(dict,)) + == flat_tuple_dict_with_empty_dict) + + +def test_flatten_dict_with_keep_empty_types(normal_dict, flat_tuple_dict): + assert flatten(normal_dict, keep_empty_types=(dict, str)) == flat_tuple_dict