Skip to content

Commit

Permalink
feat[lang]: export interfaces (#3919)
Browse files Browse the repository at this point in the history
this commit allows exporting of `module.<interface>`, and also adds
`module.__interface__` which gives the interface type of the module.
in particular, this makes it easier for users to export all functions
from a module, since they do not need to list out every single function
manually.

note that since `module.__interface__` is actually an interface type, it
can theoretically be used in type expressions, e.g.,
```vyper
    x: module.__interface__ = module.__interface__(msg.sender)
```

however, it doesn't work yet as some additional work is required to
properly thread the type into the type analysis system
(see related: GH #3943).

this commit includes the restriction that only `implement`ed interfaces
can be exported, this makes the most sense from a UX / user intuition
perspective.

---------

Co-authored-by: cyberthirst <[email protected]>
  • Loading branch information
charles-cooper and cyberthirst authored Apr 14, 2024
1 parent cb94068 commit 6f09e29
Show file tree
Hide file tree
Showing 4 changed files with 498 additions and 24 deletions.
293 changes: 293 additions & 0 deletions tests/functional/codegen/modules/test_exports.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import pytest

from vyper.compiler import compile_code
from vyper.utils import method_id


def test_simple_export(make_input_bundle, get_contract):
lib1 = """
@external
Expand Down Expand Up @@ -147,3 +153,290 @@ def foo() -> uint256:
c = get_contract(main, input_bundle=input_bundle)

assert c.foo() == 5


@pytest.fixture
def simple_library(make_input_bundle):
ifoo = """
@external
def foo() -> uint256:
...
@external
def bar() -> uint256:
...
"""
ibar = """
@external
def bar() -> uint256:
...
@external
def qux() -> uint256:
...
"""
lib1 = """
import ifoo
import ibar
implements: ifoo
implements: ibar
@external
def foo() -> uint256:
return 1
@external
def bar() -> uint256:
return 2
@external
def qux() -> uint256:
return 3
"""
return make_input_bundle({"lib1.vy": lib1, "ifoo.vyi": ifoo, "ibar.vyi": ibar})


@pytest.fixture
def send_failing_tx_to_signature(w3, tx_failed):
def _send_transaction(c, method_sig):
data = method_id(method_sig)
with tx_failed():
w3.eth.send_transaction({"to": c.address, "data": data})

return _send_transaction


def test_exports_interface_simple(get_contract, simple_library):
main = """
import lib1
exports: lib1.__interface__
"""
c = get_contract(main, input_bundle=simple_library)
assert c.foo() == 1
assert c.bar() == 2
assert c.qux() == 3


def test_exports_interface2(get_contract, send_failing_tx_to_signature, simple_library):
main = """
import lib1
exports: lib1.ifoo
"""
out = compile_code(
main, output_formats=["abi"], contract_path="main.vy", input_bundle=simple_library
)
fnames = [item["name"] for item in out["abi"]]
assert fnames == ["foo", "bar"]

c = get_contract(main, input_bundle=simple_library)
assert c.foo() == 1
assert c.bar() == 2
assert not hasattr(c, "qux")
send_failing_tx_to_signature(c, "qux()")


def test_exported_fun_part_of_interface(get_contract, make_input_bundle):
main = """
import lib2
exports: lib2.__interface__
"""
lib1 = """
@external
def bar() -> uint256:
return 1
"""
lib2 = """
import lib1
@external
def foo() -> uint256:
return 2
exports: lib1.bar
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2})
c = get_contract(main, input_bundle=input_bundle)
assert c.bar() == 1
assert c.foo() == 2


def test_imported_module_not_part_of_interface(
send_failing_tx_to_signature, get_contract, make_input_bundle
):
main = """
import lib2
exports: lib2.__interface__
"""
lib1 = """
@external
def bar() -> uint256:
return 1
"""
lib2 = """
import lib1
@external
def foo() -> uint256:
return 2
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2})
c = get_contract(main, input_bundle=input_bundle)
assert c.foo() == 2
send_failing_tx_to_signature(c, "bar()")


def test_export_unimplemented_function(
send_failing_tx_to_signature, get_contract, make_input_bundle
):
ifoo = """
@external
def foo() -> uint256:
...
"""
lib1 = """
import ifoo
implements: ifoo
@external
def foo() -> uint256:
return 1
@external
def bar() -> uint256:
return 2
"""
main = """
import lib1
exports: lib1.ifoo
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "ifoo.vyi": ifoo})
c = get_contract(main, input_bundle=input_bundle)
assert c.foo() == 1
send_failing_tx_to_signature(c, "bar()")


# sanity check that when multiple modules implement an interface, the
# correct one (specified by the user) gets selected for export.
def test_export_interface_multiple_choices(get_contract, make_input_bundle):
ifoo = """
@external
def foo() -> uint256:
...
"""
lib1 = """
import ifoo
implements: ifoo
@external
def foo() -> uint256:
return 1
"""
lib2 = """
import ifoo
implements: ifoo
@external
def foo() -> uint256:
return 2
"""
main = """
import lib1
import lib2
exports: lib1.ifoo
"""
main2 = """
import lib1
import lib2
exports: lib2.ifoo
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2, "ifoo.vyi": ifoo})

c = get_contract(main, input_bundle=input_bundle)
assert c.foo() == 1

c = get_contract(main2, input_bundle=input_bundle)
assert c.foo() == 2


def test_export_module_with_init(get_contract, make_input_bundle):
lib1 = """
@deploy
def __init__():
pass
@external
def foo() -> uint256:
return 1
"""
main = """
import lib1
exports: lib1.__interface__
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
c = get_contract(main, input_bundle=input_bundle)
assert c.foo() == 1


def test_export_module_with_getter(get_contract, make_input_bundle):
lib1 = """
counter: public(uint256)
@external
def foo():
self.counter += 1
"""
main = """
import lib1
initializes: lib1
exports: lib1.__interface__
@deploy
def __init__():
lib1.counter = 100
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
c = get_contract(main, input_bundle=input_bundle)
assert c.counter() == 100
c.foo(transact={})
assert c.counter() == 101


def test_export_module_with_default(w3, get_contract, make_input_bundle):
lib1 = """
counter: public(uint256)
@external
def foo() -> uint256:
return 1
@external
def __default__():
self.counter += 1
"""
main = """
import lib1
initializes: lib1
@deploy
def __init__():
lib1.counter = 5
exports: lib1.__interface__
"""
input_bundle = make_input_bundle({"lib1.vy": lib1})
c = get_contract(main, input_bundle=input_bundle)
assert c.foo() == 1
assert c.counter() == 5
# call `c.__default__()`
w3.eth.send_transaction({"to": c.address})
assert c.counter() == 6
Loading

0 comments on commit 6f09e29

Please sign in to comment.