diff --git a/src/erc7730/convert/calldata/convert_erc7730_v2_input_to_calldata.py b/src/erc7730/convert/calldata/convert_erc7730_v2_input_to_calldata.py index 2fa53156..c769958c 100644 --- a/src/erc7730/convert/calldata/convert_erc7730_v2_input_to_calldata.py +++ b/src/erc7730/convert/calldata/convert_erc7730_v2_input_to_calldata.py @@ -101,6 +101,23 @@ ADDRESS_LENGTH = 20 +def _describe(input_descriptor: InputERC7730Descriptor, source: HttpUrl | None) -> str: + """Name a descriptor in a diagnostic. + + ``source`` is optional and is None for a local file, which is the usual case for the CLI, + so fall back to what the descriptor calls itself -- the same identifier the converted + output carries as its contract name. + + :param input_descriptor: descriptor being converted + :param source: source URL, if one was supplied + :return: the most specific identifier available + """ + name = input_descriptor.metadata.contractName or getattr(input_descriptor.context, "id", None) + if name is not None and source is not None: + return f"{name} ({source})" + return str(name or source or "") + + def erc7730_v2_descriptor_to_calldata_descriptors( input_descriptor: InputERC7730Descriptor, source: HttpUrl | None = None, @@ -190,7 +207,7 @@ def erc7730_v2_descriptor_to_calldata_descriptors( return output_descriptors except Exception as e: - out.warning(f"Error processing v2 ERC-7730 file {source}, skipping it") + out.warning(f"Error processing v2 ERC-7730 descriptor {_describe(input_descriptor, source)}, skipping it") exception_to_output(e, out) return [] diff --git a/tests/v2/convert/calldata/test_describe.py b/tests/v2/convert/calldata/test_describe.py new file mode 100644 index 00000000..6ccf2497 --- /dev/null +++ b/tests/v2/convert/calldata/test_describe.py @@ -0,0 +1,51 @@ +from typing import Any + +import pytest +from pydantic_string_url import HttpUrl + +from erc7730.convert.calldata.convert_erc7730_v2_input_to_calldata import _describe +from erc7730.model.input.v2.descriptor import InputERC7730Descriptor + +SOURCE = HttpUrl("https://example.com/calldata-MyContract.json") + + +def descriptor(*, context_id: str | None = None, contract_name: str | None = None) -> InputERC7730Descriptor: + context: dict[str, Any] = { + "contract": {"deployments": [{"chainId": 1, "address": "0x0000000000000000000000000000000000000001"}]} + } + if context_id is not None: + context["$id"] = context_id + metadata: dict[str, Any] = {"owner": "Someone"} + if contract_name is not None: + metadata["contractName"] = contract_name + return InputERC7730Descriptor.model_validate( + {"context": context, "metadata": metadata, "display": {"formats": {}}}, strict=True + ) + + +def test_the_contract_name_is_preferred() -> None: + assert _describe(descriptor(context_id="Fallback", contract_name="MyContract"), None) == "MyContract" + + +def test_the_context_id_is_the_fallback() -> None: + """A descriptor need not set contractName, and $id is what the output falls back to as well.""" + assert _describe(descriptor(context_id="MyContract"), None) == "MyContract" + + +def test_a_source_is_kept_beside_the_name() -> None: + """Both identify it, and a batch run over many files wants the path as well as the name.""" + assert _describe(descriptor(contract_name="MyContract"), SOURCE) == f"MyContract ({SOURCE})" + + +def test_the_source_alone_is_used_when_the_descriptor_names_nothing() -> None: + assert _describe(descriptor(), SOURCE) == str(SOURCE) + + +def test_neither_is_stated_rather_than_printing_none() -> None: + """The old message read "file None", which named nothing at all.""" + assert _describe(descriptor(), None) == "" + + +@pytest.mark.parametrize("source", [None, SOURCE]) +def test_it_never_returns_none_as_text(source: HttpUrl | None) -> None: + assert "None" not in _describe(descriptor(), source)