Repository: ethereum/web3.py

Issue:
Middleware order is inconsistent
* Version: 4

### What was wrong?

The order of the middleware as passed in to Web3, and treated as part of `add` or `insert`, and returned from `list(w3.middleware_stack)` is inconsistent:

1. in `add` (really, `append`), and `insert()`, the beginning is the bottom of the stack, and the end is the top of the stack
2. in `Web3(..., middlewares=[...])` and `list(w3.middleware_stack)`, the beginning is the top of the stack, and the end is the bottom of the stack

(where the bottom of the stack is the part closest to the node and the top of the stack is the part closest to the Web3 invocation)

This is confusing -- it even caused a bug in #648 .

### How can it be fixed?

~~I think the second order is more natural as part of the implementation, and the first order is more natural in semantic understanding (where you usually want to add new middlewares to the top of the stack, so when you append/add it to the end of the list, that should be the top of the stack).~~

~~Proposal: flip the order in `Web3(middlewares)` and `list(w3.middleware_stack)`.~~

Latest Proposal:
- Switch name from `insert` -> `inject` to avoid any confusion with the list API
- Switch to `inject(middleware, name=, layer=)`, where 0 is the inner-most layer
- Leave the ordering as-is on `Web3(middlewares)` and `list(w3.middleware_stack)`
- Remove all references to first/last/top/bottom/left/right in middleware docs. Always use outer or inner


Discussion:

Resolving patch:
diff --git a/web3/manager.py b/web3/manager.py
--- a/web3/manager.py
+++ b/web3/manager.py
@@ -20,7 +20,7 @@
     AutoProvider,
 )
 from web3.utils.datastructures import (
-    NamedElementStack,
+    NamedElementOnion,
 )
 from web3.utils.empty import (
     empty,
@@ -38,7 +38,7 @@ def __init__(self, web3, providers, middlewares=None):
         if middlewares is None:
             middlewares = self.default_middlewares(web3)
 
-        self.middleware_stack = NamedElementStack(middlewares)
+        self.middleware_stack = NamedElementOnion(middlewares)
         if providers is empty:
             self.providers = AutoProvider()
         else:
diff --git a/web3/providers/rpc.py b/web3/providers/rpc.py
--- a/web3/providers/rpc.py
+++ b/web3/providers/rpc.py
@@ -9,7 +9,7 @@
     http_retry_request_middleware,
 )
 from web3.utils.datastructures import (
-    NamedElementStack,
+    NamedElementOnion,
 )
 from web3.utils.http import (
     construct_user_agent,
@@ -33,7 +33,7 @@ class HTTPProvider(JSONBaseProvider):
     endpoint_uri = None
     _request_args = None
     _request_kwargs = None
-    _middlewares = NamedElementStack([(http_retry_request_middleware, 'http_retry_request')])
+    _middlewares = NamedElementOnion([(http_retry_request_middleware, 'http_retry_request')])
 
     def __init__(self, endpoint_uri=None, request_kwargs=None):
         if endpoint_uri is None:
diff --git a/web3/utils/datastructures.py b/web3/utils/datastructures.py
--- a/web3/utils/datastructures.py
+++ b/web3/utils/datastructures.py
@@ -97,7 +97,12 @@ def __eq__(self, other):
             return False
 
 
-class NamedElementStack(Mapping):
+class NamedElementOnion(Mapping):
+    '''
+    Add layers to an onion-shaped structure. Optionally, inject to a specific layer.
+    This structure is iterable, where the outermost layer is first, and innermost is last.
+    '''
+
     def __init__(self, init_elements, valid_element=callable):
         self._queue = OrderedDict()
         for element in reversed(init_elements):
@@ -118,32 +123,33 @@ def add(self, element, name=None):
 
         self._queue[name] = element
 
-    def insert(self, index, element, name=None):
+    def inject(self, element, name=None, layer=None):
         '''
-        Insert a named element to an arbitrary location in the stack.
+        Inject a named element to an arbitrary layer in the onion.
 
-        The current implementation only supports insertion at the beginning,
-        or at the end. Note that inserting to the end is equivalent to calling :meth:`add` .
+        The current implementation only supports insertion at the innermost layer,
+        or at the outermost layer. Note that inserting to the outermost is equivalent
+        to calling :meth:`add` .
         '''
-        if not is_integer(index):
-            raise TypeError("The index for insertion must be an int.")
-        elif index != 0 and index != len(self._queue):
+        if not is_integer(layer):
+            raise TypeError("The layer for insertion must be an int.")
+        elif layer != 0 and layer != len(self._queue):
             raise NotImplementedError(
                 "You can only insert to the beginning or end of a %s, currently. "
                 "You tried to insert to %d, but only 0 and %d are permitted. " % (
                     type(self),
-                    index,
+                    layer,
                     len(self._queue),
                 )
             )
 
         self.add(element, name=name)
 
-        if index == 0:
+        if layer == 0:
             if name is None:
                 name = element
-            self._queue.move_to_end(name)
-        elif index == len(self._queue):
+            self._queue.move_to_end(name, last=False)
+        elif layer == len(self._queue):
             return
         else:
             raise AssertionError("Impossible to reach: earlier validation raises an error")
@@ -186,11 +192,11 @@ def __iter__(self):
         return iter(reversed(elements))
 
     def __add__(self, other):
-        if not isinstance(other, NamedElementStack):
-            raise NotImplementedError("You can only combine with another Stack")
+        if not isinstance(other, NamedElementOnion):
+            raise NotImplementedError("You can only combine with another NamedElementOnion")
         combined = self._queue.copy()
         combined.update(other._queue)
-        return NamedElementStack(combined.items())
+        return NamedElementOnion(combined.items())
 
     def __contains__(self, element):
         return element in self._queue


Test patch:
diff --git a/tests/core/manager/test_middleware_add_and_clear_api.py b/tests/core/manager/test_middleware_add_and_clear_api.py
--- a/tests/core/manager/test_middleware_add_and_clear_api.py
+++ b/tests/core/manager/test_middleware_add_and_clear_api.py
@@ -114,18 +114,30 @@ def test_replace_middleware_without_name(middleware_factory):
     assert tuple(manager.middleware_stack) == (mw1, mw3)
 
 
-def test_bury_middleware(middleware_factory):
+def test_add_middleware(middleware_factory):
     mw1 = middleware_factory()
     mw2 = middleware_factory()
     mw3 = middleware_factory()
 
     manager = RequestManager(None, BaseProvider(), middlewares=[mw1, mw2])
 
-    manager.middleware_stack.insert(0, mw3)
+    manager.middleware_stack.add(mw3)
 
     assert tuple(manager.middleware_stack) == (mw3, mw1, mw2)
 
 
+def test_bury_middleware(middleware_factory):
+    mw1 = middleware_factory()
+    mw2 = middleware_factory()
+    mw3 = middleware_factory()
+
+    manager = RequestManager(None, BaseProvider(), middlewares=[mw1, mw2])
+
+    manager.middleware_stack.inject(mw3, layer=0)
+
+    assert tuple(manager.middleware_stack) == (mw1, mw2, mw3)
+
+
 def test_bury_named_middleware(middleware_factory):
     mw1 = middleware_factory()
     mw2 = middleware_factory()
@@ -133,11 +145,11 @@ def test_bury_named_middleware(middleware_factory):
 
     manager = RequestManager(None, BaseProvider(), middlewares=[mw1, mw2])
 
-    manager.middleware_stack.insert(0, mw3, name='middleware3')
+    manager.middleware_stack.inject(mw3, name='middleware3', layer=0)
 
-    assert tuple(manager.middleware_stack) == (mw3, mw1, mw2)
+    assert tuple(manager.middleware_stack) == (mw1, mw2, mw3)
 
-    # make sure middleware was inserted with correct name, by trying to remove
+    # make sure middleware was injected with correct name, by trying to remove
     # it by name.
     manager.middleware_stack.remove('middleware3')