Skip to content

Commit

Permalink
Merge branch 'main' into feat/38312
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Aug 19, 2024
2 parents f0d0be2 + 5f678f4 commit 03c250a
Show file tree
Hide file tree
Showing 16 changed files with 355 additions and 65 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,16 @@ from ethpm_types import ContractInstance
contract = ContractInstance(contractType="ContractClassName", address="0x123...")
print(contract.contract_type)
```

You can also parse `ethpm_types.abi` objects using the `.from_signature` classmethod:

```py
from ethpm_types.abi import MethodABI, EventABI

>>> MethodABI.from_signature("function_name(uint256 arg1)")
MethodABI(type='function', name='function_name', inputs=[...], ...)

>>> EventABI.from_signature("Transfer(address indexed from, address indexed to, uint256 value)")
EventABI(type='event', name='Transfer', inputs=[...], ...)
```

58 changes: 37 additions & 21 deletions ethpm_types/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,47 @@
from pydantic import BaseModel as _BaseModel


class BaseModel(_BaseModel):
def model_dump(self, *args, **kwargs) -> dict:
# NOTE: We do this to accommodate the aliases needed for EIP-2678 compatibility
if "by_alias" not in kwargs:
kwargs["by_alias"] = True
def _set_dict_defaults(**kwargs) -> dict:
# NOTE: We do this to accommodate the aliases needed for EIP-2678 compatibility
if "by_alias" not in kwargs:
kwargs["by_alias"] = True

# EIP-2678: skip empty fields (at least by default)
if "exclude_none" not in kwargs:
kwargs["exclude_none"] = True

return kwargs


def _to_json_str(model, *args, **kwargs) -> str:
# NOTE: When serializing to IPFS, the canonical representation must be repeatable

# EIP-2678: skip empty fields (at least by default)
if "exclude_none" not in kwargs:
kwargs["exclude_none"] = True
# EIP-2678: minified representation (at least by default)
separators = kwargs.pop("separators", (",", ":"))

# EIP-2678: sort keys (at least by default)
sort_keys = kwargs.pop("sort_keys", True)

# TODO: Find a better way to handle sorting the keys and custom separators.
# or open an issue(s) with pydantic. `super().model_dump_json()` does not
# support `sort_keys` or `separators`.
kwargs["by_alias"] = True
kwargs["mode"] = "json"
result_dict = model.model_dump(*args, **kwargs)
return json.dumps(result_dict, sort_keys=sort_keys, separators=separators)


class BaseModel(_BaseModel):
def model_dump(self, *args, **kwargs) -> Dict:
kwargs = _set_dict_defaults(**kwargs)
return super().model_dump(*args, **kwargs)

def model_dump_json(self, *args, **kwargs) -> str:
# NOTE: When serializing to IPFS, the canonical representation must be repeatable

# EIP-2678: minified representation (at least by default)
separators = kwargs.pop("separators", (",", ":"))
return _to_json_str(self, *args, **kwargs)

# EIP-2678: sort keys (at least by default)
sort_keys = kwargs.pop("sort_keys", True)
def dict(self, *args, **kwargs) -> Dict:
kwargs = _set_dict_defaults(**kwargs)
return super().dict(*args, **kwargs)

# TODO: Find a better way to handle sorting the keys and custom separators.
# or open an issue(s) with pydantic. `super().model_dump_json()` does not
# support `sort_keys` or `separators`.
kwargs["by_alias"] = True
kwargs["mode"] = "json"
result_dict = self.model_dump(*args, **kwargs)
return json.dumps(result_dict, sort_keys=sort_keys, separators=separators)
def json(self, *args, **kwargs) -> str:
return _to_json_str(self, *args, **kwargs)
84 changes: 84 additions & 0 deletions ethpm_types/contract_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,16 @@ class ContractType(BaseModel):
"""

userdoc: Optional[dict] = None
"""
Documentation for the end-user, generated from NatSpecs
found in the contract source file.
"""

devdoc: Optional[dict] = None
"""
Documentation for the contract maintainers, generated from NatSpecs
found in the contract source file.
"""

def __repr__(self) -> str:
repr_id = self.__class__.__name__
Expand Down Expand Up @@ -470,6 +479,38 @@ def structs(self) -> ABIList:
"""
return self._get_abis(filter_fn=lambda a: isinstance(a, StructABI))

@property
def natspecs(self) -> Dict[str, str]:
"""
A mapping of ABI selectors to their natspec documentation.
"""
return {
**self._method_natspecs,
**self._event_natspecs,
**self._error_natspecs,
**self._struct_natspecs,
}

@cached_property
def _method_natspecs(self) -> Dict[str, str]:
# NOTE: Both Solidity and Vyper support this!
return _extract_natspec(self.devdoc or {}, "methods", self.methods)

@cached_property
def _event_natspecs(self) -> Dict[str, str]:
# NOTE: Only supported in Solidity (at time of writing this).
return _extract_natspec(self.devdoc or {}, "events", self.events)

@cached_property
def _error_natspecs(self) -> Dict[str, str]:
# NOTE: Only supported in Solidity (at time of writing this).
return _extract_natspec(self.devdoc or {}, "errors", self.errors)

@cached_property
def _struct_natspecs(self) -> Dict[str, str]:
# NOTE: Not supported in Solidity or Vyper at the time of writing this.
return _extract_natspec(self.devdoc or {}, "structs", self.structs)

@classmethod
def _selector_hash_fn(cls, selector: str) -> bytes:
# keccak is the default on most ecosystems, other ecosystems can subclass to override it
Expand Down Expand Up @@ -517,3 +558,46 @@ def get_id(aitem: ABI_W_SELECTOR_T) -> str:
list[ABI_W_SELECTOR_T], [x for x in self.abi if hasattr(x, "selector")]
)
return [(x, get_id(x)) for x in abis_with_selector]


def _extract_natspec(devdoc: dict, devdoc_key: str, abis: ABIList) -> Dict[str, str]:
result: Dict[str, str] = {}
devdocs = devdoc.get(devdoc_key, {})
for abi in abis:
dev_fields = devdocs.get(abi.selector, {})
if isinstance(dev_fields, dict):
if spec := _extract_natspec_from_dict(dev_fields, abi):
result[abi.selector] = "\n".join(spec)

elif isinstance(dev_fields, list):
for dev_field_ls_item in dev_fields:
if not isinstance(dev_field_ls_item, dict):
# Not sure.
continue

if spec := _extract_natspec_from_dict(dev_field_ls_item, abi):
result[abi.selector] = "\n".join(spec)

return result


def _extract_natspec_from_dict(data: Dict, abi: ABI) -> List[str]:
info_parts: list[str] = []

for field_key, field_doc in data.items():
if isinstance(field_doc, str):
info_parts.append(f"@{field_key} {field_doc}")
elif isinstance(field_doc, dict):
if field_key != "params":
# Not sure!
continue

for param_name, param_doc in field_doc.items():
param_type_matches = [i for i in getattr(abi, "inputs", []) if i.name == param_name]
if not param_type_matches:
continue # Unlikely?

param_type = str(param_type_matches[0].type)
info_parts.append(f"@param {param_name} {param_type} {param_doc}")

return info_parts
2 changes: 1 addition & 1 deletion ethpm_types/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ def unpack_sources(self, destination: Path):
# Create nested directories as needed.
source_path.parent.mkdir(parents=True, exist_ok=True)

source_path.write_text(content)
source_path.write_text(content, encoding="utf8")

def get_compiler(self, name: str, version: str) -> Optional[Compiler]:
"""
Expand Down
8 changes: 6 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ def fn(name: str) -> ContractType:
@pytest.fixture
def get_source_path():
def fn(name: str, base: Path = SOURCE_BASE) -> Path:
for path in base.iterdir():
contracts_path = base / "contracts"
if not contracts_path.is_dir():
raise AssertionError("test setup failed - contracts directory not found")

for path in contracts_path.iterdir():
if path.stem == name:
return path

raise AssertionError("test setup failed - path not found")
raise AssertionError("test setup failed - test file '{name}' not found")

return fn

Expand Down
2 changes: 1 addition & 1 deletion tests/data/Compiled/SolidityContract.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/data/Compiled/VyperContract.json

Large diffs are not rendered by default.

File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ contract SolidityContract {

uint256 constant MAX_FOO = 5;

/**
* @dev This is a doc for an error
*/
error ACustomError();

/**
* @dev Emitted when number is changed.
*
* `newNum` is the new number from the call.
* Expected every time number changes.
*/
event NumberChange(
bytes32 b,
uint256 prevNum,
Expand All @@ -32,9 +43,28 @@ contract SolidityContract {
uint256 indexed bar
);

event EventWithStruct(
MyStruct a_struct
);

event EventWithAddressArray(
uint32 indexed some_id,
address indexed some_address,
address[] participants,
address[1] agents
);

event EventWithUintArray(
uint256[1] agents
);

/**
* @dev This is the doc for MyStruct
**/
struct MyStruct {
address a;
bytes32 b;
uint256 c;
}

struct NestedStruct1 {
Expand Down Expand Up @@ -85,6 +115,15 @@ contract SolidityContract {
emit BarHappened(1);
}


/**
* @notice Sets a new number, with restrictions and event emission
* @dev Only the owner can call this function. The new number cannot be 5.
* @param num The new number to be set
* @custom:require num Must not be equal to 5
* @custom:modifies Sets the `myNumber` state variable
* @custom:emits Emits a `NumberChange` event with the previous number, the new number, and the previous block hash
*/
function setNumber(uint256 num) public onlyOwner {
require(num != 5);
prevNumber = myNumber;
Expand All @@ -111,7 +150,7 @@ contract SolidityContract {
}

function getStruct() public view returns(MyStruct memory) {
return MyStruct(msg.sender, blockhash(block.number - 1));
return MyStruct(msg.sender, blockhash(block.number - 1), 244);
}

function getNestedStruct1() public view returns(NestedStruct1 memory) {
Expand Down Expand Up @@ -278,4 +317,22 @@ contract SolidityContract {
function setStructArray(MyStruct[2] memory _my_struct_array) public pure {

}

function logStruct() public {
bytes32 _bytes = 0x1234567890abcdef0123456789abcdef0123456789abcdef0123456789abcdef;
MyStruct memory _struct = MyStruct(msg.sender, _bytes, 244);
emit EventWithStruct(_struct);
}

function logAddressArray() public {
address[] memory ppl = new address[](1);
ppl[0] = msg.sender;
address[1] memory agts = [msg.sender];
emit EventWithAddressArray(1001, msg.sender, ppl, agts);
}

function logUintArray() public {
uint256[1] memory agts = [uint256(1)];
emit EventWithUintArray(agts);
}
}
File renamed without changes.
Loading

0 comments on commit 03c250a

Please sign in to comment.