Repository: ethereum/web3.py

Issue:
ValueError in _dict_to_namedtuple on call contract function that returns 
### What was wrong?

We are getting exception when we call function on contract with 

When we use decode_tuple feature and call contract function that returns nothing, we got exception.

```
  File "/home/raman/.cache/pypoetry/virtualenvs/oracle-YM0-4bHH-py3.11/lib/python3.11/site-packages/web3/contract/utils.py", line 137, in call_contract_function
    normalized_data = recursive_dict_to_namedtuple(decoded)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/raman/.cache/pypoetry/virtualenvs/oracle-YM0-4bHH-py3.11/lib/python3.11/site-packages/web3/_utils/abi.py", line 963, in recursive_dict_to_namedtuple
    return recursive_map(_dict_to_namedtuple, data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/raman/.cache/pypoetry/virtualenvs/oracle-YM0-4bHH-py3.11/lib/python3.11/site-packages/web3/_utils/decorators.py", line 30, in wrapped
    wrapped_val = to_wrap(*args)
                  ^^^^^^^^^^^^^^
  File "/home/raman/.cache/pypoetry/virtualenvs/oracle-YM0-4bHH-py3.11/lib/python3.11/site-packages/web3/_utils/formatters.py", line 85, in recursive_map
    return func(items_mapped)
           ^^^^^^^^^^^^^^^^^^
  File "/home/raman/.cache/pypoetry/virtualenvs/oracle-YM0-4bHH-py3.11/lib/python3.11/site-packages/web3/_utils/abi.py", line 960, in _dict_to_namedtuple
    keys, values = zip(*value.items())
    ^^^^^^^^^^^^
ValueError: not enough values to unpack (expected 2, got 0)
```

### How can it be fixed?
In `call_contract_function` do:
```
if decode_tuples and fn_abi["outputs"]:
    decoded = named_tree(fn_abi["outputs"], normalized_data)
    normalized_data = recursive_dict_to_namedtuple(decoded)
```
Or in `recursive_dict_to_namedtuple` function
```
    def _dict_to_namedtuple(
        value: Union[Dict[str, Any], List[Any]]
    ) -> Union[Tuple[Any, ...], List[Any]]:
        if not isinstance(value, dict) or not value:
            return value

        keys, values = zip(*value.items())
        return abi_decoded_namedtuple_factory(keys)(values)
```



Discussion:

Resolving patch:
diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py
--- a/web3/_utils/abi.py
+++ b/web3/_utils/abi.py
@@ -957,7 +957,7 @@ def _dict_to_namedtuple(
         if not isinstance(value, dict):
             return value
 
-        keys, values = zip(*value.items())
+        keys, values = zip(*value.items()) if value else ((), ())
         return abi_decoded_namedtuple_factory(keys)(values)
 
     return recursive_map(_dict_to_namedtuple, data)


Test patch:
diff --git a/tests/core/utilities/test_abi.py b/tests/core/utilities/test_abi.py
--- a/tests/core/utilities/test_abi.py
+++ b/tests/core/utilities/test_abi.py
@@ -10,6 +10,7 @@
     get_aligned_abi_inputs,
     get_tuple_type_str_parts,
     map_abi_data,
+    recursive_dict_to_namedtuple,
 )
 from web3._utils.normalizers import (
     BASE_RETURN_NORMALIZERS,
@@ -357,3 +358,27 @@ def test_map_abi_data(types, data, funcs, expected):
 def test_exact_length_bytes_encoder_raises_on_non_multiples_of_8_bit_size(arg):
     with pytest.raises(ValueError, match="multiple of 8"):
         _ = ExactLengthBytesEncoder(None, data_byte_size=2, value_bit_size=arg)
+
+
+@pytest.mark.parametrize(
+    "input, expected_output",
+    [
+        ({"a": 1, "b": 2}, "ABIDecodedNamedTuple(a=1, b=2)"),
+        ({"a": 0}, "ABIDecodedNamedTuple(a=0)"),
+        ({"a": None}, "ABIDecodedNamedTuple(a=None)"),
+        ({"a": False}, "ABIDecodedNamedTuple(a=False)"),
+        ({}, "ABIDecodedNamedTuple()"),
+        ({"a": {}}, "ABIDecodedNamedTuple(a=ABIDecodedNamedTuple())"),
+        ({"a": []}, "ABIDecodedNamedTuple(a=[])"),
+        ({"a": [0]}, "ABIDecodedNamedTuple(a=[0])"),
+        ({"a": [{}]}, "ABIDecodedNamedTuple(a=[ABIDecodedNamedTuple()])"),
+        (
+            {"a": {"b": {}}},
+            "ABIDecodedNamedTuple(a=ABIDecodedNamedTuple(b=ABIDecodedNamedTuple()))",
+        ),
+    ],
+)
+def test_recursive_dict_to_namedtuple(input, expected_output):
+    named_tuple_output = recursive_dict_to_namedtuple(input)
+    output_repr = named_tuple_output.__repr__()
+    assert output_repr == expected_output