Skip to content

Commit

Permalink
feat: support hex str selector and fix typing issues (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Nov 11, 2022
1 parent 641e09e commit f5f0a2d
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 117 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ repos:
- id: isort

- repo: https://github.com/psf/black
rev: 22.6.0
rev: 22.10.0
hooks:
- id: black
name: black

- repo: https://gitlab.com/pycqa/flake8
rev: 4.0.1
rev: 5.0.4
hooks:
- id: flake8

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.971
rev: v0.982
hooks:
- id: mypy
additional_dependencies: [types-requests]
Expand Down
15 changes: 5 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# Quick Start

EthPM is an Ethereum package manifest containing data types for contracts, deployments,
and source code using [EIP-2678](https://eips.ethereum.org/EIPS/eip-2678).
EthPM is an Ethereum package manifest containing data types for contracts, deployments, and source code using [EIP-2678](https://eips.ethereum.org/EIPS/eip-2678).
The library validates and serializes contract related data and provides JSON schemas.

## Dependencies

* [python3](https://www.python.org/downloads) version 3.7.2 or greater, python3-dev
* [python3](https://www.python.org/downloads) version 3.8 or greater, python3-dev

## Installation

Expand All @@ -30,15 +29,11 @@ python3 setup.py install

## Quick Usage

Starting with a dictionary of attribute data, such as a contract instance, you can
build an EthPM typed object.
Starting with a dictionary of attribute data, such as a contract instance, you can build an EthPM typed object.

```python
from ethpm_types import ContractInstance

# contract_dict assumes a pre-defined dictionary containing all required keywords/args
# contract_dict = {"keyword": "value",...}
contract = ContractInstance(**contract_dict)

print(contract.name)
contract = ContractInstance(contractType="ContractClassName", address="0x123...")
print(contract.contract_type)
```
7 changes: 1 addition & 6 deletions ethpm_types/abi.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
from typing import TYPE_CHECKING, List, Optional, Union
from typing import TYPE_CHECKING, List, Literal, Optional, Union

from pydantic import Extra, Field

from .base import BaseModel

try:
from typing import Literal # type: ignore
except ImportError:
from typing_extensions import Literal # type: ignore

if TYPE_CHECKING:
from ethpm_types.contract_type import ContractType

Expand Down
111 changes: 80 additions & 31 deletions ethpm_types/contract_type.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from typing import Callable, Dict, Iterator, List, Optional, Union
from functools import singledispatchmethod
from typing import Callable, Dict, Iterable, Iterator, List, Optional, TypeVar, Union

from eth_utils import add_0x_prefix
from eth_utils import add_0x_prefix, is_0x_prefixed
from hexbytes import HexBytes
from pydantic import Field, validator

from .abi import ABI, ConstructorABI, EventABI, FallbackABI, MethodABI
from .abi import ABI, ConstructorABI, ErrorABI, EventABI, FallbackABI, MethodABI, StructABI
from .base import BaseModel
from .utils import Hex, is_valid_hex

Expand Down Expand Up @@ -257,54 +258,102 @@ def parse(self) -> Dict[int, PCMapItem]:
return results


class ABIList(list):
T = TypeVar("T", bound=Union[MethodABI, EventABI, StructABI, ErrorABI])


class ABIList(List[T]):
"""
Adds selection by name, selector and keccak(selector).
"""

def __init__(
self,
iterable=(),
iterable: Optional[Iterable[T]] = None,
*,
selector_id_size=32,
selector_hash_fn: Optional[Callable[[str], bytes]] = None,
):
self._selector_id_size = selector_id_size
self._selector_hash_fn = selector_hash_fn
super().__init__(iterable)
super().__init__(iterable or ())

def __getitem__( # type: ignore
self, item: Union[int, slice, str, HexBytes, bytes, MethodABI, EventABI]
):
@singledispatchmethod
def __getitem__(self, selector):
raise NotImplementedError(f"Cannot use {type(selector)} as a selector.")

@__getitem__.register
def __getitem_int(self, selector: int):
return super().__getitem__(selector)

@__getitem__.register
def __getitem_slice(self, selector: slice):
return super().__getitem__(selector)

@__getitem__.register
def __getitem_str(self, selector: str):
try:
if "(" in selector:
# String-style selector e.g. `method(arg0)`.
return next(abi for abi in self if abi.selector == selector)

elif is_0x_prefixed(selector):
# Hashed bytes selector, but as a hex str.
return self.__getitem__(HexBytes(selector))

# Search by name (could be ambiguous()
return next(abi for abi in self if abi.name == selector)

except StopIteration:
raise KeyError(selector)

@__getitem__.register
def __getitem_bytes(self, selector: bytes):
try:
# selector
if isinstance(item, str) and "(" in item:
return next(abi for abi in self if abi.selector == item)
# name, could be ambiguous
elif isinstance(item, str):
return next(abi for abi in self if abi.name == item)
# hashed selector, like log.topics[0] or tx.data
# NOTE: Will fail with `ImportError` if `item` is `bytes` and `eth-hash` has no backend
elif isinstance(item, (bytes, HexBytes)) and self._selector_hash_fn:
if self._selector_hash_fn:
return next(
abi
for abi in self
if self._selector_hash_fn(abi.selector)[: self._selector_id_size]
== item[: self._selector_id_size]
== selector[: self._selector_id_size]
)
elif isinstance(item, (MethodABI, EventABI)):
return next(abi for abi in self if abi.selector == item.selector)

else:
raise KeyError(selector)

except StopIteration:
raise KeyError(item)
raise KeyError(selector)

# handle int, slice
return super().__getitem__(item) # type: ignore
@__getitem__.register
def __getitem_method_abi(self, selector: MethodABI):
return self.__getitem__(selector.selector)

def __contains__(self, item: Union[str, bytes]) -> bool: # type: ignore
if isinstance(item, (int, slice)):
return False
@__getitem__.register
def __getitem_event_abi(self, selector: EventABI):
return self.__getitem__(selector.selector)

@singledispatchmethod
def __contains__(self, selector):
raise NotImplementedError(f"Cannot use {type(selector)} as a selector.")

@__contains__.register
def __contains_str(self, selector: str) -> bool:
return self._contains(selector)

@__contains__.register
def __contains_bytes(self, selector: bytes) -> bool:
return self._contains(selector)

@__contains__.register
def __contains_method_abi(self, selector: MethodABI) -> bool:
return self._contains(selector)

@__contains__.register
def __contains_event_abi(self, selector: EventABI) -> bool:
return self._contains(selector)

def _contains(self, selector: Union[str, bytes, MethodABI, EventABI]) -> bool:
try:
self[item]
_ = self[selector]
return True
except (KeyError, IndexError):
return False
Expand Down Expand Up @@ -408,7 +457,7 @@ def fallback(self) -> FallbackABI:
return fallback_abi

@property
def view_methods(self) -> List[MethodABI]:
def view_methods(self) -> ABIList[MethodABI]:
"""
The call-methods (read-only method, non-payable methods) defined in a smart contract.
Returns:
Expand All @@ -428,7 +477,7 @@ def view_methods(self) -> List[MethodABI]:
)

@property
def mutable_methods(self) -> List[MethodABI]:
def mutable_methods(self) -> ABIList[MethodABI]:
"""
The transaction-methods (stateful or payable methods) defined in a smart contract.
Returns:
Expand All @@ -446,7 +495,7 @@ def mutable_methods(self) -> List[MethodABI]:
)

@property
def events(self) -> List[EventABI]:
def events(self) -> ABIList[EventABI]:
"""
The events defined in a smart contract.
Returns:
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
"pysha3>=1.0.2,<2.0.0", # Backend for eth-hash
],
"lint": [
"black>=22.6.0", # auto-formatter and linter
"black>=22.10.0", # auto-formatter and linter
"mypy==0.982", # Static type analyzer
"types-PyYAML", # NOTE: Needed due to mypy typeshed
"types-requests", # NOTE: Needed due to mypy typeshed
"flake8>=<4.0.1", # Style linter
"flake8>=5.0.4", # Style linter
"flake8-breakpoint>=1.1.0", # detect breakpoints left in code
"flake8-print>=4.0.0", # detect print statements left in code
"isort>=5.10.1", # Import sorting linter
Expand Down
23 changes: 23 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import json
from pathlib import Path

import pytest

from ethpm_types import PackageManifest


@pytest.fixture
def oz_package_manifest_dict():
oz_manifest_file = Path(__file__).parent / "data" / "OpenZeppelinContracts.json"
return json.loads(oz_manifest_file.read_text())


@pytest.fixture
def oz_package(oz_package_manifest_dict):
return PackageManifest.parse_obj(oz_package_manifest_dict)


@pytest.fixture
def oz_contract_type(oz_package):
# NOTE: AccessControl has events, view methods, and mutable methods.
return oz_package.contract_types["AccessControl"]
Loading

0 comments on commit f5f0a2d

Please sign in to comment.