diff --git a/docs/web3.main.rst b/docs/web3.main.rst index 6eae8850fa..3feeefb684 100644 --- a/docs/web3.main.rst +++ b/docs/web3.main.rst @@ -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 @@ -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, @@ -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 diff --git a/newsfragments/2304.feature.rst b/newsfragments/2304.feature.rst new file mode 100644 index 0000000000..b0c995e6bf --- /dev/null +++ b/newsfragments/2304.feature.rst @@ -0,0 +1 @@ +external modules are no longer required to inherit from the ``web3.module.Module`` class \ No newline at end of file diff --git a/tests/core/conftest.py b/tests/core/conftest.py index c2b9aaff1f..cdaa362fce 100644 --- a/tests/core/conftest.py +++ b/tests/core/conftest.py @@ -4,6 +4,8 @@ Module, ) +# --- inherit from `web3.module.Module` class --- # + @pytest.fixture(scope='module') def module1(): @@ -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 diff --git a/tests/core/utilities/test_attach_modules.py b/tests/core/utilities/test_attach_modules.py index 36cb8a563a..60914dc21f 100644 --- a/tests/core/utilities/test_attach_modules.py +++ b/tests/core/utilities/test_attach_modules.py @@ -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) diff --git a/tests/core/web3-module/test_attach_modules.py b/tests/core/web3-module/test_attach_modules.py index 1b26c6d820..58b3d5aca3 100644 --- a/tests/core/web3-module/test_attach_modules.py +++ b/tests/core/web3-module/test_attach_modules.py @@ -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) diff --git a/web3/_utils/module.py b/web3/_utils/module.py index dd84e6e8c3..89142583de 100644 --- a/web3/_utils/module.py +++ b/web3/_utils/module.py @@ -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( @@ -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: diff --git a/web3/main.py b/web3/main.py index ec39c3c254..acbac1b9a3 100644 --- a/web3/main.py +++ b/web3/main.py @@ -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)