Skip to content

Commit

Permalink
Don't require modules to inherit from the Module class
Browse files Browse the repository at this point in the history
  • Loading branch information
fselmo committed Jan 17, 2022
1 parent 6a90a26 commit daef8ea
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 17 deletions.
42 changes: 30 additions & 12 deletions docs/web3.main.rst
Expand Up @@ -385,10 +385,10 @@ Check Encodability
False


RPC APIS
~~~~~~~~
RPC API Modules
~~~~~~~~~~~~~~~

Each ``Web3`` instance also exposes these namespaced APIs.
Each ``Web3`` instance also exposes these namespaced API modules.


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


These internal modules inherit from the ``web3.module.Module`` class which give them some configurations internal to the
web3.py library.


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_modules()`` method.
External modules may be attached to the ``Web3`` instance either at instantiation or by making use of the
``attach_modules()`` method. External modules need not inherit from the ``web3.module.Module`` class. This allows for a
good deal of flexibility even in defining what a module means to the user. An external module could technically provide
any bit of functionality that the user desires. Consequently, external modules need not be RPC APIs - though this does
provide the ability to attach an RPC API library, specific to your chain of choice, to the ``Web3`` instance.

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

.. code-block:: python
>>> from web3 import Web3, EthereumTesterProvider
>>> from web3 import Web3, HTTPProvider
>>> from external_module_library import (
... ModuleClass1,
... ModuleClass2,
... ModuleClass3,
... ModuleClass4,
... ModuleClass5,
... )
>>> w3 = Web3(
... EthereumTesterProvider(),
... HTTPProvider(provider_uri),
... external_modules={
... # ModuleClass objects in this example inherit from the `web3.module.Module` class
... 'module1': ModuleClass1,
... 'module2': (ModuleClass2, {
... 'submodule1': ModuleClass3,
Expand Down Expand Up @@ -455,12 +468,17 @@ To instantiate the ``Web3`` instance with external modules:
themselves, if there are no submodules, or two-item tuples with the module class as the 0th index and a similarly
built `dict` containing the submodule information as the 1st index. This pattern may be repeated as necessary.

.. note:: Module classes must inherit from the ``web3.module.Module`` class.

.. code-block:: python
>>> from web3 import Web3, EthereumTesterProvider
>>> w3 = Web3(EthereumTesterProvider())
>>> from web3 import Web3, HTTPProvider
>>> from external_module_library import (
... ModuleClass1,
... ModuleClass2,
... ModuleClass3,
... ModuleClass4,
... ModuleClass5,
... )
>>> w3 = Web3(HTTPProvider(provider_uri))
>>> w3.attach_modules({
... 'module1': ModuleClass1, # the module class itself may be used for a single module with no submodules
Expand Down
1 change: 1 addition & 0 deletions newsfragments/2304.feature.rst
@@ -0,0 +1 @@
external modules are no longer required to inherit from the ``web3.module.Module`` class
37 changes: 37 additions & 0 deletions tests/core/conftest.py
Expand Up @@ -4,6 +4,8 @@
Module,
)

# --- inherit from `web3.module.Module` class --- #


@pytest.fixture(scope='module')
def module1():
Expand Down Expand Up @@ -39,3 +41,38 @@ def module4():
class Module4(Module):
f = 'f'
return Module4


# --- do not inherit from `web3.module.Module` class --- #


@pytest.fixture(scope='module')
def module1_unique():
class Module1:
a = 'a'
return Module1


@pytest.fixture(scope='module')
def module2_unique():
class Module2:
b = 'b'

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


@pytest.fixture(scope='module')
def module3_unique():
class Module3:
d = 'd'
return Module3


@pytest.fixture(scope='module')
def module4_unique():
class Module4:
e = 'e'
return Module4
35 changes: 35 additions & 0 deletions tests/core/utilities/test_attach_modules.py
Expand Up @@ -127,3 +127,38 @@ def test_attach_external_modules_multiple_levels_deep(module1, module2, module3,
assert w3.module2.submodule1.e == 'e'
assert hasattr(w3.module2.submodule1, 'submodule2')
assert w3.module2.submodule1.submodule2.f == 'f'


def test_attach_external_modules_that_do_not_inherit_from_module_class(
module1_unique, module2_unique, module3_unique, module4_unique,
):
w3 = Web3(
EthereumTesterProvider(),
external_modules={
'module1': module1_unique,
'module2': (module2_unique, {
'submodule1': (module3_unique, {
'submodule2': module4_unique,
}),
})
}
)

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

# assert module2 + submodules attached
assert hasattr(w3, 'module2')
assert w3.module2.b == 'b'
assert w3.module2.c() == 'c'

assert hasattr(w3.module2, 'submodule1')
assert w3.module2.submodule1.d == 'd'
assert hasattr(w3.module2.submodule1, 'submodule2')
assert w3.module2.submodule1.submodule2.e == 'e'

# assert default modules intact
assert hasattr(w3, 'geth')
assert hasattr(w3, 'eth')
assert is_integer(w3.eth.chain_id)
34 changes: 34 additions & 0 deletions tests/core/web3-module/test_attach_modules.py
Expand Up @@ -32,3 +32,37 @@ def test_attach_modules(web3, module1, module2, module3, module4):
assert hasattr(web3, 'geth')
assert hasattr(web3, 'eth')
assert is_integer(web3.eth.chain_id)


def test_attach_modules_that_do_not_inherit_from_module_class(
web3, module1_unique, module2_unique, module3_unique, module4_unique,
):
web3.attach_modules(
{
'module1': module1_unique,
'module2': (module2_unique, {
'submodule1': (module3_unique, {
'submodule2': module4_unique,
}),
})
}
)

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

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

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

# assert default modules intact
assert hasattr(web3, 'geth')
assert hasattr(web3, 'eth')
assert is_integer(web3.eth.chain_id)
15 changes: 12 additions & 3 deletions web3/_utils/module.py
Expand Up @@ -10,10 +10,12 @@
from web3.exceptions import (
ValidationError,
)
from web3.module import (
Module,
)

if TYPE_CHECKING:
from web3 import Web3 # noqa: F401
from web3.module import Module # noqa: F401


def attach_modules(
Expand All @@ -32,11 +34,18 @@ def attach_modules(
"already has an attribute with that name"
)

if w3 is None:
if w3 is None and issubclass(module_class, Module):
# If the `module_class` inherits from the `web3.module.Module` class, it has access to
# caller functions internal to the web3.py library and sets up a proper codec. This
# is important for likely all modules internal to the library.
setattr(parent_module, module_name, module_class(parent_module))
w3 = parent_module
else:
elif w3 is not None and issubclass(module_class, Module):
setattr(parent_module, module_name, module_class(w3))
else:
# An external `module_class` need not inherit from the `web3.module.Module` class.
# This provides a good deal of flexibility for attaching external modules.
setattr(parent_module, module_name, module_class)

if module_info_is_list_like:
if len(module_info) == 2:
Expand Down
3 changes: 1 addition & 2 deletions web3/main.py
Expand Up @@ -335,8 +335,7 @@ def attach_modules(
self, modules: Optional[Dict[str, Union[Type[Module], Sequence[Any]]]]
) -> None:
"""
Attach modules to the `Web3` instance. Modules should inherit from the `web3.module.Module`
class.
Attach modules to the `Web3` instance.
"""
_attach_modules(self, modules)

Expand Down

0 comments on commit daef8ea

Please sign in to comment.