Repository: ethereum/web3.py
Pull request #1998 (2021-05-17 21:54:01)

rm ens.utils.dict_copy
Signed-off-by: Harmouch101 <mahmoudddharmouchhh@gmail.com>

### What was wrong?

Related to Issue #1423

### How was it fixed?
By removing the `dict_copy` decorator.
### Todo:
- [x] Add entry to the [release notes](https://github.com/ethereum/web3.py/blob/master/newsfragments/README.md)

#### Cute Animal Picture

:cat2: 



Patch:
diff --git a/ens/main.py b/ens/main.py
index 4d62e356..2c836068 100644
--- a/ens/main.py
+++ b/ens/main.py
@@ -1,3 +1,6 @@
+from copy import (
+    deepcopy,
+)
 from functools import (
     wraps,
 )
@@ -39,7 +42,6 @@ from ens.utils import (
     address_in,
     address_to_reverse_domain,
     default,
-    dict_copy,
     init_web3,
     is_none_or_zero_address,
     is_valid_name,
@@ -149,12 +151,11 @@ class ENS:
         reversed_domain = address_to_reverse_domain(address)
         return self.resolve(reversed_domain, get='name')
 
-    @dict_copy
     def setup_address(
         self,
         name: str,
         address: Union[Address, ChecksumAddress, HexAddress] = cast(ChecksumAddress, default),
-        transact: "TxParams" = {}
+        transact: Optional["TxParams"] = None
     ) -> HexBytes:
         """
         Set up the name to point to the supplied address.
@@ -173,6 +174,9 @@ class ENS:
         :raises InvalidName: if ``name`` has invalid syntax
         :raises UnauthorizedError: if ``'from'`` in `transact` does not own `name`
         """
+        if not transact:
+            transact = {}
+        transact = deepcopy(transact)
         owner = self.setup_owner(name, transact=transact)
         self._assert_control(owner, name)
         if is_none_or_zero_address(address):
@@ -191,9 +195,11 @@ class ENS:
         resolver: 'Contract' = self._set_resolver(name, transact=transact)
         return resolver.functions.setAddr(raw_name_to_hash(name), address).transact(transact)
 
-    @dict_copy
     def setup_name(
-        self, name: str, address: ChecksumAddress = None, transact: "TxParams" = {}
+        self,
+        name: str,
+        address: Optional[ChecksumAddress] = None,
+        transact: Optional["TxParams"] = None
     ) -> HexBytes:
         """
         Set up the address for reverse lookup, aka "caller ID".
@@ -209,6 +215,9 @@ class ENS:
         :raises UnauthorizedError: if ``'from'`` in `transact` does not own `name`
         :raises UnownedName: if no one owns `name`
         """
+        if not transact:
+            transact = {}
+        transact = deepcopy(transact)
         if not name:
             self._assert_control(address, 'the reverse record')
             return self._setup_reverse(None, address, transact=transact)
@@ -273,12 +282,11 @@ class ENS:
         node = raw_name_to_hash(name)
         return self.ens.caller.owner(node)
 
-    @dict_copy
     def setup_owner(
         self,
         name: str,
         new_owner: ChecksumAddress = cast(ChecksumAddress, default),
-        transact: "TxParams" = {}
+        transact: Optional["TxParams"] = None
     ) -> ChecksumAddress:
         """
         Set the owner of the supplied name to `new_owner`.
@@ -303,6 +311,9 @@ class ENS:
         :raises UnauthorizedError: if ``'from'`` in `transact` does not own `name`
         :returns: the new owner's address
         """
+        if not transact:
+            transact = {}
+        transact = deepcopy(transact)
         (super_owner, unowned, owned) = self._first_owner(name)
         if new_owner is default:
             new_owner = super_owner
@@ -345,15 +356,17 @@ class ENS:
                 unowned.append(pieces.pop(0))
         return (owner, unowned, name)
 
-    @dict_copy
     def _claim_ownership(
         self,
         owner: ChecksumAddress,
         unowned: Sequence[str],
         owned: str,
-        old_owner: ChecksumAddress = None,
-        transact: "TxParams" = {}
+        old_owner: Optional[ChecksumAddress] = None,
+        transact: Optional["TxParams"] = None
     ) -> None:
+        if not transact:
+            transact = {}
+        transact = deepcopy(transact)
         transact['from'] = old_owner or owner
         for label in reversed(unowned):
             self.ens.functions.setSubnodeOwner(
@@ -363,10 +376,15 @@ class ENS:
             ).transact(transact)
             owned = "%s.%s" % (label, owned)
 
-    @dict_copy
     def _set_resolver(
-        self, name: str, resolver_addr: ChecksumAddress = None, transact: "TxParams" = {}
+        self,
+        name: str,
+        resolver_addr: Optional[ChecksumAddress] = None,
+        transact: Optional["TxParams"] = None
     ) -> 'Contract':
+        if not transact:
+            transact = {}
+        transact = deepcopy(transact)
         if is_none_or_zero_address(resolver_addr):
             resolver_addr = self.address('resolver.eth')
         namehash = raw_name_to_hash(name)
@@ -377,10 +395,12 @@ class ENS:
             ).transact(transact)
         return self._resolverContract(address=resolver_addr)
 
-    @dict_copy
     def _setup_reverse(
-        self, name: str, address: ChecksumAddress, transact: "TxParams" = {}
+        self, name: str, address: ChecksumAddress, transact: Optional["TxParams"] = None
     ) -> HexBytes:
+        if not transact:
+            transact = {}
+        transact = deepcopy(transact)
         if name:
             name = normalize_name(name)
         else:
diff --git a/ens/utils.py b/ens/utils.py
index bef9c319..43c223b5 100644
--- a/ens/utils.py
+++ b/ens/utils.py
@@ -1,16 +1,12 @@
-import copy
 import datetime
-import functools
 from typing import (
     TYPE_CHECKING,
     Any,
-    Callable,
     Collection,
     Optional,
     Sequence,
     Tuple,
     Type,
-    TypeVar,
     Union,
     cast,
 )
@@ -61,18 +57,6 @@ def Web3() -> Type['_Web3']:
     return Web3Main
 
 
-TFunc = TypeVar("TFunc", bound=Callable[..., Any])
-
-
-def dict_copy(func: TFunc) -> TFunc:
-    "copy dict keyword args, to avoid modifying caller's copy"
-    @functools.wraps(func)
-    def wrapper(*args: Any, **kwargs: Any) -> TFunc:
-        copied_kwargs = copy.deepcopy(kwargs)
-        return func(*args, **copied_kwargs)
-    return cast(TFunc, wrapper)
-
-
 def init_web3(
     provider: 'BaseProvider' = cast('BaseProvider', default),
     middlewares: Optional[Sequence[Tuple['Middleware', str]]] = None,
diff --git a/newsfragments/1423.bugfix.rst b/newsfragments/1423.bugfix.rst
new file mode 100644
index 00000000..5c465e86
--- /dev/null
+++ b/newsfragments/1423.bugfix.rst
@@ -0,0 +1,1 @@
+remove `ens.utils.dict_copy` decorator
diff --git a/newsfragments/2312.feature.rst b/newsfragments/2312.feature.rst
new file mode 100644
index 00000000..902b51b1
--- /dev/null
+++ b/newsfragments/2312.feature.rst
@@ -0,0 +1,1 @@
+Allow NamedTuples in ABI inputs
diff --git a/web3/_utils/abi.py b/web3/_utils/abi.py
index 28f1d0c7..698f4151 100644
--- a/web3/_utils/abi.py
+++ b/web3/_utils/abi.py
@@ -551,7 +551,10 @@ def _align_abi_input(arg_abi: ABIFunctionParams, arg: Any) -> Tuple[Any, ...]:
             ),
         )
 
-    return type(aligned_arg)(
+    # convert NamedTuple to regular tuple
+    typing = tuple if isinstance(aligned_arg, tuple) else type(aligned_arg)
+
+    return typing(
         _align_abi_input(sub_abi, sub_arg)
         for sub_abi, sub_arg in zip(sub_abis, aligned_arg)
     )


Test patch:
diff --git a/tests/core/utilities/test_abi.py b/tests/core/utilities/test_abi.py
index 57158500..b861ee83 100644
--- a/tests/core/utilities/test_abi.py
+++ b/tests/core/utilities/test_abi.py
@@ -1,5 +1,8 @@
 import json
 import pytest
+from typing import (
+    NamedTuple,
+)
 
 from web3._utils.abi import (
     abi_data_tree,
@@ -41,6 +44,15 @@ def test_get_tuple_type_str_parts(input, expected):
     assert get_tuple_type_str_parts(input) == expected
 
 
+MyXYTuple = NamedTuple(
+    "MyXYTuple",
+    [
+        ("x", int),
+        ("y", int),
+    ]
+)
+
+
 TEST_FUNCTION_ABI_JSON = """
 {
   "constant": false,
@@ -165,6 +177,15 @@ GET_ABI_INPUTS_TESTS = (
         ),
         GET_ABI_INPUTS_OUTPUT,
     ),
+    (
+        TEST_FUNCTION_ABI,
+        {
+            's': {'a': 1, 'b': [2, 3, 4], 'c': [(5, 6), (7, 8), MyXYTuple(x=9, y=10)]},
+            't': MyXYTuple(x=11, y=12),
+            'a': 13,
+        },
+        GET_ABI_INPUTS_OUTPUT,
+    ),
     (
         {},
         (),