From daef8ea4899d6c5110e8049181579e8f65d56094 Mon Sep 17 00:00:00 2001 From: Felipe Selmo Date: Wed, 12 Jan 2022 14:29:38 -0700 Subject: [PATCH] Don't require modules to inherit from the Module class --- docs/web3.main.rst | 42 +++++++++++++------ newsfragments/2304.feature.rst | 1 + tests/core/conftest.py | 37 ++++++++++++++++ tests/core/utilities/test_attach_modules.py | 35 ++++++++++++++++ tests/core/web3-module/test_attach_modules.py | 34 +++++++++++++++ web3/_utils/module.py | 15 +++++-- web3/main.py | 3 +- 7 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 newsfragments/2304.feature.rst diff --git a/docs/web3.main.rst b/docs/web3.main.rst index 6eae8850fa..91fbce5b33 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,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, @@ -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 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..3d5dec36b1 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: + 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: 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)