Skip to content

Commit

Permalink
Add support for external modules to web3 instance
Browse files Browse the repository at this point in the history
- added web3.attach_module() to attach a single module to a web3 instance e.g. w3.attach_module('module1', ModuleClass1)
- added support for initializing the instance with external modules e.g. w3 = Web3(..., external_modules={'module1': ModuleClass1})
- modules can be passed in without needing to be a tuple or list if they don't contain submodules i.e. pass in the class by itself instead a tuple with 1 argument, (ModuleClass1,)
- added tests for the above
- updated docs for web3 main to include usage examples for the above changes
  • Loading branch information
fselmo committed Jan 5, 2022
1 parent 10cf48d commit ee6c2e5
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 33 deletions.
2 changes: 1 addition & 1 deletion docs/providers.rst
Expand Up @@ -387,7 +387,7 @@ AsyncHTTPProvider
>>> from web3.net import AsyncNet
>>> w3 = Web3(AsyncHTTPProvider("http://127.0.0.1:8545"),
... modules={'eth': (AsyncEth,), 'net': (AsyncNet,)},
... modules={'eth': AsyncEth, 'net': AsyncNet},
... middlewares=[]) # See supported middleware section below for middleware options
Under the hood, the ``AsyncHTTPProvider`` uses the python
Expand Down
69 changes: 67 additions & 2 deletions docs/web3.main.rst
Expand Up @@ -11,7 +11,7 @@ Web3 API
.. py:class:: Web3(provider)
Each ``web3`` instance exposes the following APIs.
Each ``Web3`` instance exposes the following APIs.

Providers
~~~~~~~~~
Expand Down Expand Up @@ -388,7 +388,7 @@ Check Encodability
RPC APIS
~~~~~~~~

Each ``web3`` instance also exposes these namespaced APIs.
Each ``Web3`` instance also exposes these namespaced APIs.


.. py:attribute:: Web3.eth
Expand All @@ -410,3 +410,68 @@ Each ``web3`` instance also exposes these namespaced APIs.
.. py:attribute:: Web3.parity
See :doc:`./web3.parity`


Attaching Modules
~~~~~~~~~~~~~~~~~

Modules that inherit from the ``web3.module.Module`` class may be attached to the ``Web3`` instance either at
instantiation or by making use of the ``attach_module()`` method.

To instantiate the ``Web3`` instance with external modules:

.. code-block:: python
>>> from web3 import Web3, EthereumTesterProvider
>>> w3 = Web3(
... EthereumTesterProvider(),
... external_modules={
... # ModuleClass objects in this example inherit from the `web3.module.Module` class
... 'module1': ModuleClass1,
... 'module2': (ModuleClass2, {
... 'submodule1': ModuleClass3,
... 'submodule2': (ModuleClass4, {
... 'submodule2a': ModuleClass5 # submodule children may be nested further, if necessary
... })
... })
... }
... )
# `return_zero`, in this case, is an example attribute of the `ModuleClass1` object
>>> w3.module1.return_zero
0
>>> w3.module2.submodule1.return_one
1
>>> w3.module2.submodule2.submodule2a.return_two
2
.. py:method:: w3.attach_module(module_name, module)
The ``attach_module()`` method can be used to attach an external module after the ``Web3`` instance has been
instantiated.

.. code-block:: python
>>> from web3 import Web3, EthereumTesterProvider
>>> w3 = Web3(EthereumTesterProvider())
# attaching a single module - in this case, one with the attribute `return_zero`
>>> w3.attach_module('module1', ModuleClass1)
>>> w3.module1.return_zero
0
# attaching a module with submodules
>>> w3.attach_module(
... 'module2',
... (ModuleClass2, {
... 'submodule1': ModuleClass3,
... 'submodule2': (ModuleClass4, {
... 'submodule2a': ModuleClass5
... })
... })
... )
>>> w3.module2.submodule1.return_one
1
>>> w3.module2.submodule2.submodule2a.return_two
2
1 change: 1 addition & 0 deletions newsfragments/2288.feature.rst
@@ -0,0 +1 @@
Support for attaching external modules to the ``Web3`` instance when instantiating the ``Web3`` instance, via the ``external_modules`` argument, or via the new ``attach_module()`` method
41 changes: 41 additions & 0 deletions tests/core/conftest.py
@@ -0,0 +1,41 @@
import pytest

from web3.module import (
Module,
)


@pytest.fixture(scope='module')
def module1():
class Module1(Module):
a = 'a'

@property
def b(self):
return 'b'
return Module1


@pytest.fixture(scope='module')
def module2():
class Module2(Module):
c = 'c'

@staticmethod
def d():
return 'd'
return Module2


@pytest.fixture(scope='module')
def module3():
class Module3(Module):
e = 'e'
return Module3


@pytest.fixture(scope='module')
def module4():
class Module4(Module):
f = 'f'
return Module4
2 changes: 1 addition & 1 deletion tests/core/method-class/test_method.py
Expand Up @@ -265,7 +265,7 @@ class FakeModule(Module):
def dummy_w3():
return Web3(
EthereumTesterProvider(),
modules={'fake': (FakeModule,)},
modules={'fake': FakeModule},
middlewares=[])


Expand Down
2 changes: 1 addition & 1 deletion tests/core/method-class/test_result_formatters.py
Expand Up @@ -46,7 +46,7 @@ def dummy_w3():
w3 = Web3(
DummyProvider(),
middlewares=[result_middleware],
modules={"module": (ModuleForTest,)})
modules={"module": ModuleForTest})
return w3


Expand Down
52 changes: 46 additions & 6 deletions tests/core/utilities/test_attach_modules.py
@@ -1,5 +1,9 @@
import pytest

from eth_utils import (
is_integer,
)

from web3 import Web3
from web3._utils.module import (
attach_modules,
Expand Down Expand Up @@ -37,10 +41,10 @@ def unlock_account(self):
def test_attach_modules():
mods = {
"geth": (MockGeth, {
"personal": (MockGethPersonal,),
"admin": (MockGethAdmin,),
"personal": MockGethPersonal,
"admin": MockGethAdmin,
}),
"eth": (MockEth,),
"eth": MockEth,
}
w3 = Web3(EthereumTesterProvider(), modules={})
attach_modules(w3, mods)
Expand All @@ -51,10 +55,10 @@ def test_attach_modules():

def test_attach_modules_multiple_levels_deep():
mods = {
"eth": (MockEth,),
"eth": MockEth,
"geth": (MockGeth, {
"personal": (MockGethPersonal, {
"admin": (MockGethAdmin,),
"admin": MockGethAdmin,
}),
}),
}
Expand All @@ -76,9 +80,45 @@ def test_attach_modules_with_wrong_module_format():

def test_attach_modules_with_existing_modules():
mods = {
"eth": (MockEth,),
"eth": MockEth,
}
w3 = Web3(EthereumTesterProvider, modules=mods)
with pytest.raises(AttributeError,
match="The web3 object already has an attribute with that name"):
attach_modules(w3, mods)


def test_attach_external_modules_multiple_levels_deep(module1, module2, module3, module4):
w3 = Web3(
EthereumTesterProvider(),
external_modules={
'module1': module1,
'module2': (module2, {
'submodule1': (module3, {
'submodule2': module4,
}),
})
}
)

assert w3.isConnected()

# assert instantiated with default modules
assert hasattr(w3, 'geth')
assert hasattr(w3, 'eth')
assert is_integer(w3.eth.chain_id)

# assert instantiated with module1
assert hasattr(w3, 'module1')
assert w3.module1.a == 'a'
assert w3.module1.b == 'b'

# assert instantiated with module2 + submodules
assert hasattr(w3, 'module2')
assert w3.module2.c == 'c'
assert w3.module2.d() == 'd'

assert hasattr(w3.module2, 'submodule1')
assert w3.module2.submodule1.e == 'e'
assert hasattr(w3.module2.submodule1, 'submodule2')
assert w3.module2.submodule1.submodule2.f == 'f'
35 changes: 35 additions & 0 deletions tests/core/web3-module/test_attach_module.py
@@ -0,0 +1,35 @@
from eth_utils import (
is_integer,
)


def test_attach_module(web3, module1, module2, module3, module4):
web3.attach_module('module1', module1)
web3.attach_module(
'module2',
(module2, {
'submodule1': (module3, {
'submodule2': module4,
})
})
)

# assert module1 attached
assert hasattr(web3, 'module1')
assert web3.module1.a == 'a'
assert web3.module1.b == 'b'

# assert module2 + submodules attached
assert hasattr(web3, 'module2')
assert web3.module2.c == 'c'
assert web3.module2.d() == 'd'

assert hasattr(web3.module2, 'submodule1')
assert web3.module2.submodule1.e == 'e'
assert hasattr(web3.module2.submodule1, 'submodule2')
assert web3.module2.submodule1.submodule2.f == 'f'

# assert default modules intact
assert hasattr(web3, 'geth')
assert hasattr(web3, 'eth')
assert is_integer(web3.eth.chain_id)
19 changes: 11 additions & 8 deletions web3/_utils/module.py
Expand Up @@ -18,11 +18,13 @@

def attach_modules(
parent_module: Union["Web3", "Module"],
module_definitions: Dict[str, Sequence[Any]],
module_definitions: Dict[str, Any],
w3: Optional[Union["Web3", "Module"]] = None
) -> None:
for module_name, module_info in module_definitions.items():
module_class = module_info[0]
module_info_is_list_like = isinstance(module_info, Sequence)

module_class = module_info[0] if module_info_is_list_like else module_info

if hasattr(parent_module, module_name):
raise AttributeError(
Expand All @@ -36,9 +38,10 @@ def attach_modules(
else:
setattr(parent_module, module_name, module_class(w3))

if len(module_info) == 2:
submodule_definitions = module_info[1]
module = getattr(parent_module, module_name)
attach_modules(module, submodule_definitions, w3)
elif len(module_info) != 1:
raise ValidationError("Module definitions can only have 1 or 2 elements.")
if module_info_is_list_like:
if len(module_info) == 2:
submodule_definitions = module_info[1]
module = getattr(parent_module, module_name)
attach_modules(module, submodule_definitions, w3)
elif len(module_info) != 1:
raise ValidationError("Module definitions can only have 1 or 2 elements.")
2 changes: 1 addition & 1 deletion web3/eth.py
Expand Up @@ -458,7 +458,7 @@ async def call(
return await self._call(transaction, block_identifier, state_override)


class Eth(BaseEth, Module):
class Eth(BaseEth):
account = Account()
defaultContractFactory: Type[Union[Contract, ConciseContract, ContractCaller]] = Contract # noqa: E704,E501
iban = Iban
Expand Down

0 comments on commit ee6c2e5

Please sign in to comment.