Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't require modules to inherit from the Module class #2304

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 47 additions & 14 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,49 @@ Each ``Web3`` instance also exposes these namespaced APIs.
See :doc:`./web3.parity`


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

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
~~~~~~~~~~~~~~~~

External modules can be used to introduce custom or third-party APIs to your ``Web3`` instance. Adding external modules
can occur either at instantiation of the ``Web3`` instance or by making use of the ``attach_modules()`` method.

Unlike the native modules, external modules need not inherit from the ``web3.module.Module`` class. The only requirement
is that a Module must be a class and, if you'd like to make use of the parent ``Web3`` instance, it must be passed into
the ``__init__`` function. For example:

.. code-block:: python

>>> class ExampleModule():
...
... def __init__(self, w3):
... self.w3 = w3
...
... def print_balance_of_shaq(self):
... print(self.w3.eth.get_balance('shaq.eth'))


.. warning:: Given the flexibility of external modules, use caution and only import modules from trusted third parties
and open source code you've vetted!

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 +483,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)
19 changes: 14 additions & 5 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:
setattr(parent_module, module_name, module_class(parent_module))
w3 = parent_module
if 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 likely important for all modules internal to the library.
if w3 is None:
setattr(parent_module, module_name, module_class(parent_module))
w3 = parent_module
else:
setattr(parent_module, module_name, module_class(w3))
else:
setattr(parent_module, module_name, module_class(w3))
# An external `module_class` need not inherit from the `web3.module.Module` class.
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