From 71d8e7d11aa3c309ad515f8d9c0c3ebf9264dafc Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Mon, 22 Jun 2020 08:30:00 +0900 Subject: [PATCH 01/40] Refactor Score inspect code --- iconservice/iconscore/typing/function.py | 75 +++++++++ iconservice/utils/typing/__init__.py | 0 iconservice/utils/typing/conversion.py | 185 +++++++++++++++++++++++ iconservice/utils/typing/definition.py | 43 ++++++ 4 files changed, 303 insertions(+) create mode 100644 iconservice/iconscore/typing/function.py create mode 100644 iconservice/utils/typing/__init__.py create mode 100644 iconservice/utils/typing/conversion.py create mode 100644 iconservice/utils/typing/definition.py diff --git a/iconservice/iconscore/typing/function.py b/iconservice/iconscore/typing/function.py new file mode 100644 index 000000000..c01dd98bb --- /dev/null +++ b/iconservice/iconscore/typing/function.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import TYPE_CHECKING, List +from inspect import signature, Signature, Parameter + +from iconservice.iconscore.icon_score_constant import ( + CONST_BIT_FLAG, + ConstBitFlag, + STR_FALLBACK, +) +from iconservice.utils.typing.conversion import is_base_type + + +def normalize_signature(sig: Signature) -> Signature: + return sig + + +def normalize_parameter(param: Parameter) -> Parameter: + annotation = param.annotation + + # BaseType + if is_base_type(annotation): + return param + + # No annotation + if annotation is Parameter.empty: + return param.replace(annotation=str) + if annotation is list: + return param.replace(annotation=List[str]) + + origin = getattr(annotation, "__origin__", None) + + if origin is list: + return param.replace(annotation=List[str]) + + raise TypeError(f"Unsupported type hint: {annotation}") + + +class Function(object): + def __init__(self, func: callable): + self._func = func + self._signature: Signature = normalize_signature(signature(func)) + + @property + def name(self) -> str: + return self._func.__name__ + + @property + def flags(self) -> int: + return getattr(self._func, CONST_BIT_FLAG, 0) + + @property + def is_external(self) -> bool: + return bool(self.flags & ConstBitFlag.External) + + @property + def is_payable(self) -> bool: + return bool(self.flags & ConstBitFlag.Payable) + + @property + def is_fallback(self) -> bool: + return self.name == STR_FALLBACK and self.is_payable diff --git a/iconservice/utils/typing/__init__.py b/iconservice/utils/typing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/iconservice/utils/typing/conversion.py b/iconservice/utils/typing/conversion.py new file mode 100644 index 000000000..c8ee69afa --- /dev/null +++ b/iconservice/utils/typing/conversion.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional, Dict, Union, Type, List, ForwardRef, Any + +from iconservice.base.address import Address +from iconservice.base.exception import InvalidParamsException + +BaseObject = Union[bool, bytes, int, str, 'Address'] +BaseObjectType = Type[BaseObject] +CommonObject = Union[bool, bytes, int, str, 'Address', Dict[str, BaseObject]] +CommonType = Type[CommonObject] + +BASE_TYPES = {bool, bytes, int, str, Address} +TYPE_NAME_TO_TYPE = { + "bool": bool, + "bytes": bytes, + "int": int, + "str": str, + "Address": Address, +} + + +def is_base_type(value: type) -> bool: + try: + return value in BASE_TYPES + except: + return False + + +def type_name_to_type(type_name: str) -> BaseObjectType: + return TYPE_NAME_TO_TYPE[type_name] + + +def str_to_int(value: str) -> int: + if isinstance(value, int): + return value + + base = 16 if is_hex(value) else 10 + return int(value, base) + + +def base_object_to_str(value: Any) -> str: + if isinstance(value, Address): + return str(value) + elif isinstance(value, int): + return hex(value) + elif isinstance(value, bytes): + return bytes_to_hex(value) + elif isinstance(value, bool): + return "0x1" if value else "0x0" + elif isinstance(value, str): + return value + + raise TypeError(f"Unsupported type: {type(value)}") + + +def object_to_str(value: Any) -> Union[List, Dict, CommonObject]: + try: + return base_object_to_str(value) + except TypeError: + pass + + if isinstance(value, list): + return object_to_str_in_list(value) + elif isinstance(value, dict): + return object_to_str_in_dict(value) + + raise TypeError(f"Unsupported type: {type(value)}") + + +def str_to_base_object_by_type_name(type_name: str, value: str) -> BaseObject: + return str_to_base_object(type_name_to_type(type_name), value) + + +def str_to_base_object(type_hint: BaseObjectType, value: str) -> BaseObject: + if type_hint is bool: + return bool(str_to_int(value)) + if type_hint is bytes: + return hex_to_bytes(value) + if type_hint is int: + return str_to_int(value) + if type_hint is str: + return value + if type_hint is Address: + return Address.from_string(value) + + raise TypeError(f"Unknown type: {type_hint}") + + +def str_to_object(type_hint, value): + if isinstance(value, dict): + return str_to_object_in_typed_dict(type_hint, value) + else: + return str_to_base_object(type_hint, value) + + +def bytes_to_hex(value: bytes, prefix: str = "0x") -> str: + return f"{prefix}{value.hex()}" + + +def hex_to_bytes(value: Optional[str]) -> Optional[bytes]: + if value is None: + return None + + if value.startswith("0x"): + value = value[2:] + + return bytes.fromhex(value) + + +def is_hex(value: str) -> bool: + return value.startswith("0x") or value.startswith("-0x") + + +def str_to_object_in_typed_dict(type_hints, value: Dict[str, str]) -> Dict[str, BaseObject]: + annotations = type_hints.__annotations__ + return {k: str_to_base_object(annotations[k], value[k]) for k in annotations} + + +def object_to_str_in_dict(value: Dict[str, BaseObject]) -> Dict[str, str]: + return {k: object_to_str(value[k]) for k in value} + + +def str_to_object_in_list(type_hint, value: List[Any]) -> List[CommonObject]: + assert len(type_hint.__args__) == 1 + + args_type_hint = type_hint.__args__[0] + return [str_to_object(args_type_hint, i) for i in value] + + +def object_to_str_in_list(value: List[CommonObject]) -> List[Union[str, Dict[str, str]]]: + """Return a copied list from a given list + + All items in the origin list are copied to a copied list and converted in string format + There is no change in a given list + """ + return [object_to_str(i) for i in value] + + +def type_hint_to_type_template(type_hint) -> Any: + """Convert type_hint to type_template consisting of base_object_types, list and dict + + :param type_hint: + :return: + """ + if isinstance(type_hint, ForwardRef): + type_hint = type_name_to_type(type_hint.__forward_arg__) + elif isinstance(type_hint, str): + type_hint = type_name_to_type(type_hint) + + if is_base_type(type_hint): + return type_hint + + if type_hint is List: + raise InvalidParamsException(f"No arguments: {type_hint}") + + # If type_hint is a subclass of TypedDict + attr = "__annotations__" + if hasattr(type_hint, attr): + # annotations is a dictionary containing filed_name(str) as a key and type as a value + annotations = getattr(type_hint, attr) + return {k: type_hint_to_type_template(v) for k, v in annotations.items()} + + try: + origin = getattr(type_hint, "__origin__") + if origin is list: + args = getattr(type_hint, "__args__") + return [type_hint_to_type_template(args[0])] + except: + pass + + raise InvalidParamsException(f"Unsupported type: {type_hint}") diff --git a/iconservice/utils/typing/definition.py b/iconservice/utils/typing/definition.py new file mode 100644 index 000000000..45495bf86 --- /dev/null +++ b/iconservice/utils/typing/definition.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List, Dict + +from .conversion import is_base_type +from ...base.exception import IllegalFormatException + + +def get_type_name(type_template: type): + if is_base_type(type(type_template)): + return type_template.__name__ + + if isinstance(type_template, list): + item = type_template[0] + name = "struct" if isinstance(item, dict) else item.__name__ + return f"[]{name}" + + if isinstance(type_template, dict): + return "struct" + + +def get_fields(type_template: type) -> List[Dict[str, str]]: + if isinstance(type_template, list): + item = type_template[0] + elif isinstance(type_template, dict): + item = type_template + else: + raise IllegalFormatException(f"Invalid type: {type(type_template)}") + + return [{"name": k, "type": item[k].__name__} for k in item] From 5bfcfeb69809ee795b66c7b45ab0d70ac694b6ac Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Mon, 22 Jun 2020 10:10:18 +0900 Subject: [PATCH 02/40] Define Delegation parameter type hint with TypedDict in SystemScore --- iconservice/iconscore/system_score.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/iconservice/iconscore/system_score.py b/iconservice/iconscore/system_score.py index 28a3dc6e1..33e2daa1c 100644 --- a/iconservice/iconscore/system_score.py +++ b/iconservice/iconscore/system_score.py @@ -17,9 +17,11 @@ """ from inspect import currentframe -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List from iconcommons.logger import Logger +from typing_extensions import TypedDict + from .icon_score_base import IconScoreBase, interface, external, payable from .icon_score_base2 import InterfaceScore from ..base.address import Address @@ -33,6 +35,11 @@ from ..iiss.storage import RewardRate +class Delegation(TypedDict): + address: Address + value: int + + class SystemScore(IconScoreBase): def __init__(self, db: 'IconScoreDatabase') -> None: super().__init__(db) @@ -52,7 +59,7 @@ def getStake(self, address: Address) -> dict: return self._context.engine.iiss.query(*self._get_params(locals_params=locals())) @external - def setDelegation(self, delegations: list = None) -> None: + def setDelegation(self, delegations: List[Delegation] = None) -> None: self._context.engine.iiss.invoke(*self._get_params(locals_params=locals())) @external(readonly=True) From 4064f594c4d384b368c4ae3ceb6bd55e026f1346 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Mon, 22 Jun 2020 18:05:13 +0900 Subject: [PATCH 03/40] Under development * Put TODO comments to the locations of some codes that are needed to be fixed for score param conversion --- iconservice/iconscore/icon_score_base.py | 15 +++- iconservice/iconscore/icon_score_engine.py | 6 +- iconservice/iconscore/system_score.py | 2 +- iconservice/iconscore/typing/function.py | 40 ++++++----- iconservice/iiss/engine.py | 7 +- iconservice/utils/typing/type_hint.py | 69 +++++++++++++++++++ tests/unittest/iconscore/typing/__init__.py | 0 .../iconscore/typing/test_function.py | 62 +++++++++++++++++ 8 files changed, 177 insertions(+), 24 deletions(-) create mode 100644 iconservice/utils/typing/type_hint.py create mode 100644 tests/unittest/iconscore/typing/__init__.py create mode 100644 tests/unittest/iconscore/typing/test_function.py diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index 377ac72c0..4456df0a1 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -36,6 +36,7 @@ from ..base.exception import * from ..database.db import IconScoreDatabase, DatabaseObserver from ..icon_constant import ICX_TRANSFER_EVENT_LOG, Revision, IconScoreContextType +from ..iconscore.typing.function import Function from ..utils import get_main_type_from_annotations_type if TYPE_CHECKING: @@ -313,17 +314,25 @@ def on_update(self, **kwargs) -> None: class IconScoreBaseMeta(ABCMeta): def __new__(mcs, name, bases, namespace, **kwargs): - if IconScoreObject in bases or name == "IconSystemScoreBase": - return super().__new__(mcs, name, bases, namespace, **kwargs) - cls = super().__new__(mcs, name, bases, namespace, **kwargs) + if IconScoreObject in bases or name == "IconSystemScoreBase": + return cls + if not isinstance(namespace, dict): raise InvalidParamsException('namespace is not dict!') custom_funcs = [value for key, value in getmembers(cls, predicate=isfunction) if not key.startswith('__')] + # TODO: Normalize type hints of score parameters by goldworm + # Create funcs dict containing Function objects + # funcs = { + # name: Function(func) + # for name, func in getmembers(cls, predicate=isfunction) + # if not name.startswith('__') + # } + external_funcs = {func.__name__: signature(func) for func in custom_funcs if getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.External} payable_funcs = [func for func in custom_funcs diff --git a/iconservice/iconscore/icon_score_engine.py b/iconservice/iconscore/icon_score_engine.py index af1135139..ef3154418 100644 --- a/iconservice/iconscore/icon_score_engine.py +++ b/iconservice/iconscore/icon_score_engine.py @@ -105,9 +105,10 @@ def _call(context: 'IconScoreContext', icon_score = IconScoreEngine._get_icon_score(context, icon_score_address) - converted_params = IconScoreEngine._convert_score_params_by_annotations(context, icon_score, func_name, kw_params) + converted_params = IconScoreEngine._convert_score_params_by_annotations( + context, icon_score, func_name, kw_params) context.set_func_type_by_icon_score(icon_score, func_name) - context.current_address: 'Address' = icon_score_address + context.current_address = icon_score_address score_func = getattr(icon_score, ATTR_SCORE_CALL) ret = score_func(func_name=func_name, kw_params=converted_params) @@ -130,6 +131,7 @@ def _convert_score_params_by_annotations(context: 'IconScoreContext', remove_invalid_params = True score_func = getattr(icon_score, func_name) + # TODO: Implement type conversion considering TypedDict by goldworm TypeConverter.adjust_params_to_method(score_func, tmp_params, remove_invalid_params) return tmp_params diff --git a/iconservice/iconscore/system_score.py b/iconservice/iconscore/system_score.py index 33e2daa1c..81d705206 100644 --- a/iconservice/iconscore/system_score.py +++ b/iconservice/iconscore/system_score.py @@ -160,7 +160,7 @@ def getStake(self, address: Address) -> dict: pass def estimateUnstakeLockPeriod(self) -> dict: pass @interface - def setDelegation(self, delegations: list = None): pass + def setDelegation(self, delegations: List[Delegation] = None): pass @interface def getDelegation(self, address: Address) -> dict: pass diff --git a/iconservice/iconscore/typing/function.py b/iconservice/iconscore/typing/function.py index c01dd98bb..7c71cc0ab 100644 --- a/iconservice/iconscore/typing/function.py +++ b/iconservice/iconscore/typing/function.py @@ -13,40 +13,48 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, List from inspect import signature, Signature, Parameter +from collections import OrderedDict from iconservice.iconscore.icon_score_constant import ( CONST_BIT_FLAG, ConstBitFlag, STR_FALLBACK, ) -from iconservice.utils.typing.conversion import is_base_type +from iconservice.utils.typing.type_hint import normalize_type_hint def normalize_signature(sig: Signature) -> Signature: + params = sig.parameters + new_params = [] + + normalized = False + for k in params: + new_param = normalize_parameter(params[k]) + new_params.append(new_param) + + if params[k] != new_params: + normalized = True + + if normalized: + sig = sig.replace(parameters=new_params) + return sig def normalize_parameter(param: Parameter) -> Parameter: annotation = param.annotation - # BaseType - if is_base_type(annotation): - return param - - # No annotation - if annotation is Parameter.empty: - return param.replace(annotation=str) - if annotation is list: - return param.replace(annotation=List[str]) + if annotation == Parameter.empty: + type_hint = str + else: + type_hint = normalize_type_hint(annotation) - origin = getattr(annotation, "__origin__", None) - - if origin is list: - return param.replace(annotation=List[str]) + if type_hint == annotation: + # Nothing to update + return param - raise TypeError(f"Unsupported type hint: {annotation}") + return param.replace(annotation=type_hint) class Function(object): diff --git a/iconservice/iiss/engine.py b/iconservice/iiss/engine.py index a6bae2b81..bf9a22b22 100644 --- a/iconservice/iiss/engine.py +++ b/iconservice/iiss/engine.py @@ -39,6 +39,7 @@ from ..iconscore.icon_score_context import IconScoreContext from ..iconscore.icon_score_event_log import EventLogEmitter from ..iconscore.icon_score_step import StepType +from ..iconscore.system_score import Delegation from ..icx import Intent from ..icx.icx_account import Account from ..icx.issue.issue_formula import IssueFormula @@ -391,7 +392,7 @@ def handle_estimate_unstake_lock_period(self, context: 'IconScoreContext'): "unstakeLockPeriod": unstake_lock_period } - def handle_set_delegation(self, context: 'IconScoreContext', delegations: list): + def handle_set_delegation(self, context: 'IconScoreContext', delegations: List[Delegation]): """Handles setDelegation JSON-RPC API request """ # SCORE can stake via SCORE inter-call @@ -442,7 +443,8 @@ def _check_delegation_count(cls, @classmethod def _convert_params_of_set_delegation(cls, context: 'IconScoreContext', - delegations: Optional[List]) -> Tuple[int, List[Tuple['Address', int]]]: + delegations: Optional[List[Delegation]] + ) -> Tuple[int, List[Tuple['Address', int]]]: """Convert delegations format [{"address": "hxe7af5fcfd8dfc67530a01a0e403882687528dfcb", "value", "0xde0b6b3a7640000"}, ...] -> @@ -458,6 +460,7 @@ def _convert_params_of_set_delegation(cls, cls._check_delegation_count(context, delegations) + # TODO: Remove type conversion by goldworm temp_delegations: list = TypeConverter.convert(delegations, ParamType.IISS_SET_DELEGATION) total_delegating: int = 0 converted_delegations: List[Tuple['Address', int]] = [] diff --git a/iconservice/utils/typing/type_hint.py b/iconservice/utils/typing/type_hint.py new file mode 100644 index 000000000..a60583e51 --- /dev/null +++ b/iconservice/utils/typing/type_hint.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List + +from typing_extensions import TypedDict + + +def normalize_type_hint(type_hint) -> type: + return type_hint + +# def _normalize_type_hint(): +# # BaseType +# if is_base_type(annotation): +# return param +# +# # No annotation +# if annotation is Parameter.empty: +# return param.replace(annotation=str) +# if annotation is list: +# return param.replace(annotation=List[str]) +# +# origin = getattr(annotation, "__origin__", None) +# +# if origin is list: +# return param.replace(annotation=List[str]) +# +# raise TypeError(f"Unsupported type hint: {annotation}") + + +# def normalize_list_type_hint(type_hint) -> type: +# """ +# 1. list -> List[str] +# 2. List -> List[str] +# 3. List[int] -> List[int] +# 4. List[Custom] -> List[Custom] +# 5. List["Custom"] -> exception +# 6. List[Union[str, int]] -> exception +# +# :param type_hint: +# :return: +# """ +# if type_hint is list: +# return List[str] +# +# attr = "__args__" +# if not hasattr(type_hint, attr): +# return List[str] +# +# args = getattr(type_hint, "__args__") +# if len(args) > 1: +# raise TypeError(f"Unsupported type hint: {type_hint}") +# +# if is_base_type(args[0]) or issubclass(args[0], TypedDict): +# return type_hint +# +# raise TypeError(f"Unsupported type hint: {type_hint}") diff --git a/tests/unittest/iconscore/typing/__init__.py b/tests/unittest/iconscore/typing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unittest/iconscore/typing/test_function.py b/tests/unittest/iconscore/typing/test_function.py new file mode 100644 index 000000000..d1b82cd60 --- /dev/null +++ b/tests/unittest/iconscore/typing/test_function.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +from inspect import signature +from typing import List, Dict, Optional, Union + +import pytest + +from typing_extensions import TypedDict + +from iconservice.iconscore.typing.function import ( + normalize_signature, +) + + +class Person(TypedDict): + name: str + age: int + + +# Allowed list +def func0(name: str, age: int) -> int: pass +def func1(name: "str", age: int) -> str: pass +def func2(a: list): pass +def func3(a: List): pass +def func4(a: List[int]): pass +def func5(a: List["int"]): pass +def func6(a: List[Person]): pass +def func7(a: List["Person"]): pass + +_ALLOWED_LIST = [func0, func1, func2, func3, func4, func5, func6, func7] + +# Denied list +def func0_error(a: "int"): pass +def func1_error(a: Dict): pass +def func2_error(a: Dict[str, str]): pass +def func3_error(a: Union[int, str]): pass +def func4_error(a: Optional[str]): pass + +_DENIED_LIST = [ + func0_error, + func1_error, + func2_error, + func3_error, + func4_error, +] + + +@pytest.mark.parametrize("func", _ALLOWED_LIST) +def test_normalize_signature_with_allowed_func(func): + sig = signature(func) + new_sig = normalize_signature(sig) + assert new_sig == sig + + +@pytest.mark.parametrize("func", _DENIED_LIST) +def test_normalize_signature_with_denied_func(func): + sig = signature(func) + new_sig = normalize_signature(sig) + assert new_sig != sig + + new_sig2 = normalize_signature(new_sig) + assert new_sig2 == new_sig From 9d186a030be938a98b414c23f87a4a336db26031 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Tue, 23 Jun 2020 08:34:23 +0900 Subject: [PATCH 04/40] Under development --- iconservice/__init__.py | 2 ++ iconservice/iconscore/typing/__init__.py | 24 +++++++++++++++++++ .../{utils => iconscore}/typing/conversion.py | 0 .../{utils => iconscore}/typing/definition.py | 22 +++++++++++++++-- iconservice/iconscore/typing/function.py | 3 +-- .../{utils => iconscore}/typing/type_hint.py | 4 ---- iconservice/utils/typing/__init__.py | 0 .../sample_system_score_intercall.py | 7 +++++- 8 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 iconservice/iconscore/typing/__init__.py rename iconservice/{utils => iconscore}/typing/conversion.py (100%) rename iconservice/{utils => iconscore}/typing/definition.py (68%) rename iconservice/{utils => iconscore}/typing/type_hint.py (96%) delete mode 100644 iconservice/utils/typing/__init__.py diff --git a/iconservice/__init__.py b/iconservice/__init__.py index c30d3cdcf..77994ade9 100644 --- a/iconservice/__init__.py +++ b/iconservice/__init__.py @@ -15,8 +15,10 @@ from abc import ABCMeta, abstractmethod, ABC from functools import wraps from inspect import isfunction +from typing import List from iconcommons.logger import Logger +from typing_extensions import TypedDict from .base.address import Address, AddressPrefix, SYSTEM_SCORE_ADDRESS, ZERO_SCORE_ADDRESS from .base.exception import IconScoreException diff --git a/iconservice/iconscore/typing/__init__.py b/iconservice/iconscore/typing/__init__.py new file mode 100644 index 000000000..f054c2fd2 --- /dev/null +++ b/iconservice/iconscore/typing/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Tuple + + +def get_origin(type_hint: type) -> type: + return getattr(type_hint, "__origin__", None) + + +def get_args(type_hint: type) -> Tuple[type, ...]: + return getattr(type_hint, "__args__", ()) \ No newline at end of file diff --git a/iconservice/utils/typing/conversion.py b/iconservice/iconscore/typing/conversion.py similarity index 100% rename from iconservice/utils/typing/conversion.py rename to iconservice/iconscore/typing/conversion.py diff --git a/iconservice/utils/typing/definition.py b/iconservice/iconscore/typing/definition.py similarity index 68% rename from iconservice/utils/typing/definition.py rename to iconservice/iconscore/typing/definition.py index 45495bf86..fc626ffd8 100644 --- a/iconservice/utils/typing/definition.py +++ b/iconservice/iconscore/typing/definition.py @@ -13,10 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Dict +from typing import List, Dict, Tuple +from iconservice.base.exception import IllegalFormatException from .conversion import is_base_type -from ...base.exception import IllegalFormatException +from . import get_origin, get_args def get_type_name(type_template: type): @@ -32,6 +33,23 @@ def get_type_name(type_template: type): return "struct" +def get_type_name_by_type_hint(type_hint: type): + # If type hint is a base type, just return its name + # Ex: bool, bytes, int, str, Address + if is_base_type(type_hint): + return type_hint.__name__ + + origin: type = get_origin(type_hint) + args: Tuple[type, ...] = get_args(type_hint) + + if isinstance(origin, list): + name = "struct" if isinstance(args[0], dict) else item.__name__ + return f"[]{name}" + + if isinstance(origin, dict): + return "struct" + + def get_fields(type_template: type) -> List[Dict[str, str]]: if isinstance(type_template, list): item = type_template[0] diff --git a/iconservice/iconscore/typing/function.py b/iconservice/iconscore/typing/function.py index 7c71cc0ab..e7a6659e0 100644 --- a/iconservice/iconscore/typing/function.py +++ b/iconservice/iconscore/typing/function.py @@ -14,14 +14,13 @@ # limitations under the License. from inspect import signature, Signature, Parameter -from collections import OrderedDict from iconservice.iconscore.icon_score_constant import ( CONST_BIT_FLAG, ConstBitFlag, STR_FALLBACK, ) -from iconservice.utils.typing.type_hint import normalize_type_hint +from iconservice.iconscore.typing.type_hint import normalize_type_hint def normalize_signature(sig: Signature) -> Signature: diff --git a/iconservice/utils/typing/type_hint.py b/iconservice/iconscore/typing/type_hint.py similarity index 96% rename from iconservice/utils/typing/type_hint.py rename to iconservice/iconscore/typing/type_hint.py index a60583e51..ec5bc3837 100644 --- a/iconservice/utils/typing/type_hint.py +++ b/iconservice/iconscore/typing/type_hint.py @@ -13,10 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List - -from typing_extensions import TypedDict - def normalize_type_hint(type_hint) -> type: return type_hint diff --git a/iconservice/utils/typing/__init__.py b/iconservice/utils/typing/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/integrate_test/samples/sample_internal_call_scores/sample_system_score_intercall/sample_system_score_intercall.py b/tests/integrate_test/samples/sample_internal_call_scores/sample_system_score_intercall/sample_system_score_intercall.py index e6d3a844d..dc3cae353 100644 --- a/tests/integrate_test/samples/sample_internal_call_scores/sample_system_score_intercall/sample_system_score_intercall.py +++ b/tests/integrate_test/samples/sample_internal_call_scores/sample_system_score_intercall/sample_system_score_intercall.py @@ -1,6 +1,11 @@ from iconservice import * +class Delegation(TypedDict): + address: Address + value: int + + class SampleSystemScoreInterCall(IconScoreBase): def __init__(self, db: IconScoreDatabase) -> None: super().__init__(db) @@ -52,7 +57,7 @@ def call_estimateUnstakeLockPeriod(self) -> dict: func_name="estimateUnstakeLockPeriod", kw_dict=self._get_kw_dict(locals())) @external - def call_setDelegation(self, delegations: list): + def call_setDelegation(self, delegations: List[Delegation]): use_interface = self.use_interface.get() if use_interface: test_interface = self.create_interface_score(SYSTEM_SCORE_ADDRESS, InterfaceSystemScore) From b64eb9788f573845cf6ed82664f84b77ea6bf2e8 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Tue, 23 Jun 2020 22:55:49 +0900 Subject: [PATCH 05/40] Under development --- iconservice/iconscore/typing/__init__.py | 38 +++++++++- iconservice/iconscore/typing/conversion.py | 2 +- iconservice/iconscore/typing/definition.py | 71 +++++++++++-------- iconservice/iconscore/typing/type_hint.py | 5 ++ tests/unittest/iconscore/typing/__init__.py | 14 ++++ .../unittest/iconscore/typing/test__init__.py | 41 +++++++++++ .../iconscore/typing/test_definition.py | 51 +++++++++++++ .../iconscore/typing/test_type_hint.py | 27 +++++++ 8 files changed, 216 insertions(+), 33 deletions(-) create mode 100644 tests/unittest/iconscore/typing/test__init__.py create mode 100644 tests/unittest/iconscore/typing/test_definition.py create mode 100644 tests/unittest/iconscore/typing/test_type_hint.py diff --git a/iconservice/iconscore/typing/__init__.py b/iconservice/iconscore/typing/__init__.py index f054c2fd2..f6966834d 100644 --- a/iconservice/iconscore/typing/__init__.py +++ b/iconservice/iconscore/typing/__init__.py @@ -13,12 +13,48 @@ # See the License for the specific language governing permissions and # limitations under the License. +__all__ = ( + "is_base_type", + "get_origin", + "get_args", + "is_struct", +) + from typing import Tuple +from iconservice.base.address import Address + +BASE_TYPES = {bool, bytes, int, str, Address} +TYPE_NAME_TO_TYPE = {_type.__name__: _type for _type in BASE_TYPES} + + +def is_base_type(value: type) -> bool: + try: + return value in BASE_TYPES + except: + return False + def get_origin(type_hint: type) -> type: + """ + Dict[str, int].__origin__ == dict + List[str].__origin__ == list + + :param type_hint: + :return: + """ + if is_base_type(type_hint) or is_struct(type_hint): + return type_hint + return getattr(type_hint, "__origin__", None) def get_args(type_hint: type) -> Tuple[type, ...]: - return getattr(type_hint, "__args__", ()) \ No newline at end of file + return getattr(type_hint, "__args__", ()) + + +def is_struct(type_hint) -> bool: + try: + return type_hint.__class__.__name__ == "_TypedDictMeta" + except: + return False diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index c8ee69afa..84ae9d670 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -170,7 +170,7 @@ def type_hint_to_type_template(type_hint) -> Any: # If type_hint is a subclass of TypedDict attr = "__annotations__" if hasattr(type_hint, attr): - # annotations is a dictionary containing filed_name(str) as a key and type as a value + # annotations is a dictionary containing field_name(str) as a key and type as a value annotations = getattr(type_hint, attr) return {k: type_hint_to_type_template(v) for k, v in annotations.items()} diff --git a/iconservice/iconscore/typing/definition.py b/iconservice/iconscore/typing/definition.py index fc626ffd8..524c3dc17 100644 --- a/iconservice/iconscore/typing/definition.py +++ b/iconservice/iconscore/typing/definition.py @@ -13,49 +13,58 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Dict, Tuple +from typing import List from iconservice.base.exception import IllegalFormatException +from . import get_origin, get_args, is_struct from .conversion import is_base_type -from . import get_origin, get_args -def get_type_name(type_template: type): - if is_base_type(type(type_template)): - return type_template.__name__ +def get_input(name: str, type_hint: type) -> dict: + _input = {"name": name} - if isinstance(type_template, list): - item = type_template[0] - name = "struct" if isinstance(item, dict) else item.__name__ - return f"[]{name}" + types: List[type] = [] + _get_type(type_hint, types) + _input["type"] = types_to_name(types) - if isinstance(type_template, dict): - return "struct" + return _input -def get_type_name_by_type_hint(type_hint: type): - # If type hint is a base type, just return its name - # Ex: bool, bytes, int, str, Address - if is_base_type(type_hint): - return type_hint.__name__ - +def _get_type(type_hint: type, types: List[type]): origin: type = get_origin(type_hint) - args: Tuple[type, ...] = get_args(type_hint) + types.append(origin) + + if origin is list: + args = get_args(type_hint) + if len(args) != 1: + raise IllegalFormatException(f"Invalid type: {type_hint}") + + _get_type(args[0], types) + + +def types_to_name(types: List[type]) -> str: + def func(): + for _type in types: + if _type is list: + yield "[]" + elif is_base_type(_type): + yield _type.__name__ + elif is_struct(_type): + yield "struct" + + return "".join(func()) - if isinstance(origin, list): - name = "struct" if isinstance(args[0], dict) else item.__name__ - return f"[]{name}" - if isinstance(origin, dict): - return "struct" +def get_fields(type_hint: type): + """Returns fields info from struct + :param type_hint: struct type + :return: + """ -def get_fields(type_template: type) -> List[Dict[str, str]]: - if isinstance(type_template, list): - item = type_template[0] - elif isinstance(type_template, dict): - item = type_template - else: - raise IllegalFormatException(f"Invalid type: {type(type_template)}") + annotations = getattr(type_hint, "__annotations__", None) + if annotations is None: + raise IllegalFormatException(f"Not struct type: {type_hint}") - return [{"name": k, "type": item[k].__name__} for k in item] + # annotations is a dictionary containing key-type pair which has field_name as a key and type as a value + return [{"name": k, "type": v.__name__} for k, v in annotations.items()] diff --git a/iconservice/iconscore/typing/type_hint.py b/iconservice/iconscore/typing/type_hint.py index ec5bc3837..63b1dfe66 100644 --- a/iconservice/iconscore/typing/type_hint.py +++ b/iconservice/iconscore/typing/type_hint.py @@ -13,6 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import List, Dict, get_type_hints +from typing_extensions import TypedDict + +from iconservice.iconscore.typing import get_origin, get_args, is_struct + def normalize_type_hint(type_hint) -> type: return type_hint diff --git a/tests/unittest/iconscore/typing/__init__.py b/tests/unittest/iconscore/typing/__init__.py index e69de29bb..8291f7cea 100644 --- a/tests/unittest/iconscore/typing/__init__.py +++ b/tests/unittest/iconscore/typing/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/unittest/iconscore/typing/test__init__.py b/tests/unittest/iconscore/typing/test__init__.py new file mode 100644 index 000000000..21be897bb --- /dev/null +++ b/tests/unittest/iconscore/typing/test__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + + +from typing import List, Dict, Optional, Union, get_type_hints +from typing_extensions import TypedDict + +import pytest + +from iconservice.iconscore.typing import get_origin, get_args +from iconservice.base.address import Address + + +class Person(TypedDict): + name: str + age: int + single: bool + + +TYPE_HINTS = ( + # bool, bytes, int, str, Address, + # list, List, List[int], List[Person], List["Person"], + # Optional[str], Optional[List[str]], Optional[Dict[str, str]], + # Dict, Dict[str, int], Optional[Dict], + Union[str, int], Union[str, int] +) + +GET_ORIGIN_RESULTS = ( + Union, Union +) + + +def func(person: Person): + pass + + +@pytest.mark.parametrize("type_hint", TYPE_HINTS) +@pytest.mark.parametrize("result", GET_ORIGIN_RESULTS) +def test_get_origin(type_hint, result): + type_hints = get_type_hints(func) + origin = get_origin(type_hints["person"]) + assert origin == Person diff --git a/tests/unittest/iconscore/typing/test_definition.py b/tests/unittest/iconscore/typing/test_definition.py new file mode 100644 index 000000000..10933d8e4 --- /dev/null +++ b/tests/unittest/iconscore/typing/test_definition.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, List +from typing_extensions import TypedDict + +from iconservice.base.address import Address +from iconservice.iconscore.typing.definition import ( + get_input, + _get_type, +) + + +class Person(TypedDict): + name: str + age: int + single: bool + wallet: Address + data: bytes + + +def test__get_type(): + types: List[type] = [] + _get_type(List[List[int]], types) + assert types == [list, list, int] + + +def test_get_fields_by_type_hints(): + fields = [ + ("name", str), + ("age", int), + ("single", bool), + ("wallet", Address), + ("data", bytes), + ] + expected = [{"name": field[0], "type": field[1].__name__} for field in fields] + + ret = get_fields_from_typed_dict(Person) + assert ret == expected diff --git a/tests/unittest/iconscore/typing/test_type_hint.py b/tests/unittest/iconscore/typing/test_type_hint.py new file mode 100644 index 000000000..c35b3df51 --- /dev/null +++ b/tests/unittest/iconscore/typing/test_type_hint.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from typing import List, Optional, Dict, Union + +import pytest +from typing_extensions import TypedDict + +from iconservice.base.address import Address + + +class Person(TypedDict): + name: str + age: int + + +type_hints = ( + bool, bytes, int, str, Address, + list, List, List[int], List[Person], List["Person"], + Optional[str], Optional[List[str]], Optional[Dict[str, str]], + Dict, Dict[str, int], Optional[Dict], + Union[str], Union[str, int] +) + + +@pytest.mark.parametrize("type_hint", type_hints) +def test_normalize_type_hint(type_hint): + pass From 3115b07745d1241e55d584c394b8f08220d6f8f1 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Wed, 24 Jun 2020 02:20:04 +0900 Subject: [PATCH 06/40] Implement getScoreApi functions --- iconservice/iconscore/typing/definition.py | 153 +++++++++++++++--- .../unittest/iconscore/typing/test__init__.py | 69 +++++--- .../iconscore/typing/test_definition.py | 107 ++++++++++-- .../iconscore/typing/test_function.py | 1 + 4 files changed, 271 insertions(+), 59 deletions(-) diff --git a/iconservice/iconscore/typing/definition.py b/iconservice/iconscore/typing/definition.py index 524c3dc17..b0f6b1dc2 100644 --- a/iconservice/iconscore/typing/definition.py +++ b/iconservice/iconscore/typing/definition.py @@ -13,38 +13,120 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List +__all__ = "get_inputs" + +from inspect import signature, Signature, Parameter +from typing import List, Dict, Mapping -from iconservice.base.exception import IllegalFormatException from . import get_origin, get_args, is_struct from .conversion import is_base_type +from ..icon_score_constant import ConstBitFlag, CONST_BIT_FLAG, STR_FALLBACK +from ...base.exception import IllegalFormatException, InvalidParamsException + + +def get_functions(funcs: List[callable]) -> List: + ret = [] + + for func in funcs: + const_bit_flag = getattr(func, CONST_BIT_FLAG, 0) + is_readonly = const_bit_flag & ConstBitFlag.ReadOnly == ConstBitFlag.ReadOnly + is_payable = const_bit_flag & ConstBitFlag.Payable == ConstBitFlag.Payable + + ret.append(_get_function(func, is_readonly, is_payable)) + + return ret + + +def _get_function(func: callable, is_readonly: bool, is_payable: bool) -> Dict: + if _is_fallback(func, is_payable): + return _get_fallback_function() + else: + return _get_normal_function(func, is_readonly, is_payable) + + +def _get_normal_function(func: callable, is_readonly: bool, is_payable: bool) -> Dict: + sig = signature(func) + + ret = { + "name": func.__name__, + "type": "function", + "inputs": get_inputs(sig.parameters), + "outputs": get_outputs(sig.return_annotation) + } + + if is_readonly: + ret["readonly"] = True + + if is_payable: + ret["payable"] = True + + return ret + + +def _is_fallback(func: callable, is_payable: bool) -> bool: + ret: bool = func.__name__ == STR_FALLBACK and is_payable + if ret: + sig = signature(func) + if len(sig.parameters) > 1: + raise InvalidParamsException("Invalid fallback signature") + + return_annotation = sig.return_annotation + if return_annotation not in (None, Signature.empty): + raise InvalidParamsException("Invalid fallback signature") + + return ret + +def _get_fallback_function() -> Dict: + return { + "name": STR_FALLBACK, + "type": STR_FALLBACK, + "payable": True, + } -def get_input(name: str, type_hint: type) -> dict: + +def get_inputs(params: Mapping[str, Parameter]) -> list: + inputs = [] + + for name, param in params.items(): + annotation = param.annotation + type_hint = str if annotation is Parameter.empty else annotation + inputs.append(_get_input(name, type_hint)) + + return inputs + + +def _get_input(name: str, type_hint: type) -> Dict: _input = {"name": name} - types: List[type] = [] - _get_type(type_hint, types) - _input["type"] = types_to_name(types) + type_hints: List[type] = split_type_hint(type_hint) + _input["type"] = _type_hints_to_name(type_hints) + + last_type_hint: type = type_hints[-1] + + if is_struct(last_type_hint): + _input["fields"] = _get_fields(last_type_hint) return _input -def _get_type(type_hint: type, types: List[type]): +def split_type_hint(type_hint: type) -> List[type]: origin: type = get_origin(type_hint) - types.append(origin) + ret = [origin] if origin is list: args = get_args(type_hint) if len(args) != 1: raise IllegalFormatException(f"Invalid type: {type_hint}") - _get_type(args[0], types) + ret += split_type_hint(args[0]) + return ret -def types_to_name(types: List[type]) -> str: + +def _type_hints_to_name(type_hints: List[type]) -> str: def func(): - for _type in types: + for _type in type_hints: if _type is list: yield "[]" elif is_base_type(_type): @@ -55,16 +137,51 @@ def func(): return "".join(func()) -def get_fields(type_hint: type): +def _type_hint_to_name(type_hint: type) -> str: + if is_base_type(type_hint): + return type_hint.__name__ + elif is_struct(type_hint): + return "struct" + + raise IllegalFormatException(f"Invalid type: {type_hint}") + + +def _get_fields(struct: type) -> List[dict]: """Returns fields info from struct - :param type_hint: struct type + :param struct: struct type :return: """ + # annotations is a dictionary containing key-type pair + # which has field_name as a key and type as a value + annotations = struct.__annotations__ + + fields = [] + for name, type_hint in annotations.items(): + field = {"name": name} + + type_hints: List[type] = split_type_hint(type_hint) + field["type"] = _type_hints_to_name(type_hints) + + last_type_hint: type = type_hints[-1] + if is_struct(last_type_hint): + field["fields"] = _get_fields(last_type_hint) + + fields.append(field) + + return fields + + +def get_outputs(type_hint: type) -> List: + origin = get_origin(type_hint) - annotations = getattr(type_hint, "__annotations__", None) - if annotations is None: - raise IllegalFormatException(f"Not struct type: {type_hint}") + if is_base_type(origin): + type_name = origin.__name__ + elif is_struct(origin) or origin is dict: + type_name = "{}" + elif origin is list: + type_name = "[]" + else: + raise IllegalFormatException(f"Invalid output type: {type_hint}") - # annotations is a dictionary containing key-type pair which has field_name as a key and type as a value - return [{"name": k, "type": v.__name__} for k, v in annotations.items()] + return [{"type": type_name}] diff --git a/tests/unittest/iconscore/typing/test__init__.py b/tests/unittest/iconscore/typing/test__init__.py index 21be897bb..011a515e5 100644 --- a/tests/unittest/iconscore/typing/test__init__.py +++ b/tests/unittest/iconscore/typing/test__init__.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- -from typing import List, Dict, Optional, Union, get_type_hints -from typing_extensions import TypedDict +from typing import Union, List, Dict, Optional import pytest +from typing_extensions import TypedDict -from iconservice.iconscore.typing import get_origin, get_args from iconservice.base.address import Address +from iconservice.iconscore.typing import ( + get_origin, + get_args, +) class Person(TypedDict): @@ -16,26 +19,44 @@ class Person(TypedDict): single: bool -TYPE_HINTS = ( - # bool, bytes, int, str, Address, - # list, List, List[int], List[Person], List["Person"], - # Optional[str], Optional[List[str]], Optional[Dict[str, str]], - # Dict, Dict[str, int], Optional[Dict], - Union[str, int], Union[str, int] +@pytest.mark.parametrize( + "type_hint,expected", + [ + (bool, bool), + (bytes, bytes), + (int, int), + (str, str), + (Address, Address), + (List[int], list), + (List[List[str]], list), + (Dict, dict), + (Dict[str, int], dict), + (Union[int, str], Union), + (Optional[int], Union), + (Person, Person), + ] ) - -GET_ORIGIN_RESULTS = ( - Union, Union +def test_get_origin(type_hint, expected): + origin = get_origin(type_hint) + assert origin == expected + + +@pytest.mark.parametrize( + "type_hint,expected", + [ + (bool, ()), + (bytes, ()), + (int, ()), + (str, ()), + (Address, ()), + (List[int], (int,)), + (List[List[str]], (List[str],)), + (Dict[str, int], (str, int)), + (Union[int, str, Address], (int, str, Address)), + (Optional[int], (int, type(None))), + (List[Person], (Person,)), + ] ) - - -def func(person: Person): - pass - - -@pytest.mark.parametrize("type_hint", TYPE_HINTS) -@pytest.mark.parametrize("result", GET_ORIGIN_RESULTS) -def test_get_origin(type_hint, result): - type_hints = get_type_hints(func) - origin = get_origin(type_hints["person"]) - assert origin == Person +def test_get_args(type_hint, expected): + args = get_args(type_hint) + assert args == expected diff --git a/tests/unittest/iconscore/typing/test_definition.py b/tests/unittest/iconscore/typing/test_definition.py index 10933d8e4..85fa06161 100644 --- a/tests/unittest/iconscore/typing/test_definition.py +++ b/tests/unittest/iconscore/typing/test_definition.py @@ -13,16 +13,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, List +from inspect import signature +from typing import List + +import pytest from typing_extensions import TypedDict from iconservice.base.address import Address from iconservice.iconscore.typing.definition import ( - get_input, - _get_type, + get_inputs, + split_type_hint, ) +class Delegation(TypedDict): + address: Address + value: int + + class Person(TypedDict): name: str age: int @@ -31,21 +39,86 @@ class Person(TypedDict): data: bytes -def test__get_type(): - types: List[type] = [] - _get_type(List[List[int]], types) - assert types == [list, list, int] +class Company(TypedDict): + name: str + delegation: Delegation + workers: List[Person] + + +def test_get_inputs_with_list_of_struct(): + expected = [ + { + "name": "_persons", + "type": "[]struct", + "fields": [ + {"name": "name", "type": "str"}, + {"name": "age", "type": "int"}, + {"name": "single", "type": "bool"}, + {"name": "wallet", "type": "Address"}, + {"name": "data", "type": "bytes"}, + ] + } + ] + + def func(_persons: List[Person]): + pass + + sig = signature(func) + inputs = get_inputs(sig.parameters) + assert inputs == expected -def test_get_fields_by_type_hints(): - fields = [ - ("name", str), - ("age", int), - ("single", bool), - ("wallet", Address), - ("data", bytes), +def test_get_inputs_with_list_of_struct_nesting_struct(): + expected = [ + { + "name": "_company", + "type": "struct", + "fields": [ + {"name": "name", "type": "str"}, + { + "name": "delegation", + "type": "struct", + "fields": [ + {"name": "address", "type": "Address"}, + {"name": "value", "type": "int"}, + ] + }, + { + "name": "workers", + "type": "[]struct", + "fields": [ + {"name": "name", "type": "str"}, + {"name": "age", "type": "int"}, + {"name": "single", "type": "bool"}, + {"name": "wallet", "type": "Address"}, + {"name": "data", "type": "bytes"}, + ] + }, + ] + } ] - expected = [{"name": field[0], "type": field[1].__name__} for field in fields] - ret = get_fields_from_typed_dict(Person) - assert ret == expected + def func(_company: Company): + pass + + sig = signature(func) + inputs = get_inputs(sig.parameters) + assert inputs == expected + + +@pytest.mark.parametrize( + "type_hint,expected", + [ + (bool, [bool]), + (bytes, [bytes]), + (int, [int]), + (str, [str]), + (Address, [Address]), + (List[int], [list, int]), + (List[List[str]], [list, list, str]), + (List[List[List[Person]]], [list, list, list, Person]), + ] +) +def test__get_type(type_hint, expected): + types: List[type] = split_type_hint(type_hint) + assert types == expected diff --git a/tests/unittest/iconscore/typing/test_function.py b/tests/unittest/iconscore/typing/test_function.py index d1b82cd60..b87fad393 100644 --- a/tests/unittest/iconscore/typing/test_function.py +++ b/tests/unittest/iconscore/typing/test_function.py @@ -52,6 +52,7 @@ def test_normalize_signature_with_allowed_func(func): assert new_sig == sig +@pytest.mark.skip("Not implemented") @pytest.mark.parametrize("func", _DENIED_LIST) def test_normalize_signature_with_denied_func(func): sig = signature(func) From 3a9fc20656a00dabe1d25a21672f159c93882598 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Wed, 24 Jun 2020 22:44:21 +0900 Subject: [PATCH 07/40] get_score_api is under development * Unittest does not work --- iconservice/iconscore/icon_score_base.py | 64 ++++++++--- iconservice/iconscore/icon_score_constant.py | 2 + iconservice/iconscore/typing/__init__.py | 9 +- iconservice/iconscore/typing/conversion.py | 38 ++----- iconservice/iconscore/typing/definition.py | 104 +++++++++++++----- .../typing/{function.py => element.py} | 58 ++++++++-- .../iconscore/typing/test_definition.py | 22 +++- .../iconscore/typing/test_function.py | 2 +- 8 files changed, 222 insertions(+), 77 deletions(-) rename iconservice/iconscore/typing/{function.py => element.py} (58%) diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index 4456df0a1..a78a7beb3 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -18,25 +18,37 @@ from abc import abstractmethod, ABC, ABCMeta from functools import partial, wraps from inspect import isfunction, getmembers, signature, Parameter -from typing import TYPE_CHECKING, Callable, Any, List, Tuple +from typing import TYPE_CHECKING, Callable, Any, List, Tuple, Union, Dict from .context.context import ContextGetter, ContextContainer -from .icon_score_api_generator import ScoreApiGenerator from .icon_score_base2 import InterfaceScore, revert, Block -from .icon_score_constant import CONST_INDEXED_ARGS_COUNT, FORMAT_IS_NOT_FUNCTION_OBJECT, CONST_BIT_FLAG, \ - ConstBitFlag, FORMAT_DECORATOR_DUPLICATED, FORMAT_IS_NOT_DERIVED_OF_OBJECT, STR_FALLBACK, CONST_CLASS_EXTERNALS, \ - CONST_CLASS_PAYABLES, CONST_CLASS_API, T, BaseType +from .icon_score_constant import ( + CONST_INDEXED_ARGS_COUNT, + FORMAT_IS_NOT_FUNCTION_OBJECT, + CONST_BIT_FLAG, + ConstBitFlag, + FORMAT_DECORATOR_DUPLICATED, + FORMAT_IS_NOT_DERIVED_OF_OBJECT, + STR_FALLBACK, + CONST_CLASS_EXTERNALS, + CONST_CLASS_PAYABLES, + CONST_CLASS_API, + CONST_CLASS_ELEMENTS, + BaseType, + T, +) from .icon_score_context_util import IconScoreContextUtil from .icon_score_event_log import EventLogEmitter from .icon_score_step import StepType from .icx import Icx from .internal_call import InternalCall +from .typing.definition import get_score_api +from .typing.element import Function, EventLog from ..base.address import Address from ..base.address import GOVERNANCE_SCORE_ADDRESS from ..base.exception import * from ..database.db import IconScoreDatabase, DatabaseObserver from ..icon_constant import ICX_TRANSFER_EVENT_LOG, Revision, IconScoreContextType -from ..iconscore.typing.function import Function from ..utils import get_main_type_from_annotations_type if TYPE_CHECKING: @@ -299,6 +311,7 @@ def __wrapper(calling_obj: Any, *args, **kwargs): return __wrapper + class IconScoreObject(ABC): def __init__(self, *args, **kwargs) -> None: @@ -311,6 +324,33 @@ def on_update(self, **kwargs) -> None: pass +def create_score_elements(cls) -> Dict: + elements = {} + flags = ( + ConstBitFlag.ReadOnly | + ConstBitFlag.External | + ConstBitFlag.Payable | + ConstBitFlag.EventLog + ) + + for name, func in getmembers(cls, predicate=isfunction): + if name.startswith("__"): + continue + if getattr(func, CONST_BIT_FLAG, 0) & flags: + elements[name] = create_score_element(func) + + return elements + + +def create_score_element(element: callable) -> Union[Function, EventLog]: + flags = getattr(element, CONST_BIT_FLAG, 0) + + if flags & ConstBitFlag.EventLog: + return EventLog(element) + else: + return Function(element) + + class IconScoreBaseMeta(ABCMeta): def __new__(mcs, name, bases, namespace, **kwargs): @@ -326,12 +366,8 @@ def __new__(mcs, name, bases, namespace, **kwargs): if not key.startswith('__')] # TODO: Normalize type hints of score parameters by goldworm - # Create funcs dict containing Function objects - # funcs = { - # name: Function(func) - # for name, func in getmembers(cls, predicate=isfunction) - # if not name.startswith('__') - # } + elements = create_score_elements(cls) + setattr(cls, CONST_CLASS_ELEMENTS, elements) external_funcs = {func.__name__: signature(func) for func in custom_funcs if getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.External} @@ -350,7 +386,9 @@ def __new__(mcs, name, bases, namespace, **kwargs): payable_funcs = {func.__name__: signature(func) for func in payable_funcs} setattr(cls, CONST_CLASS_PAYABLES, payable_funcs) - api_list = ScoreApiGenerator.generate(custom_funcs) + # TODO: Replace it with a new list supporting struct and list + # api_list = ScoreApiGenerator.generate(custom_funcs) + api_list = get_score_api(elements) setattr(cls, CONST_CLASS_API, api_list) return cls diff --git a/iconservice/iconscore/icon_score_constant.py b/iconservice/iconscore/icon_score_constant.py index f2587abd7..374fe4fa6 100644 --- a/iconservice/iconscore/icon_score_constant.py +++ b/iconservice/iconscore/icon_score_constant.py @@ -27,6 +27,7 @@ CONST_CLASS_PAYABLES = '__payables' CONST_CLASS_INDEXES = '__indexes' CONST_CLASS_API = '__api' +CONST_CLASS_ELEMENTS = '__elements' CONST_BIT_FLAG = '__bit_flag' CONST_INDEXED_ARGS_COUNT = '__indexed_args_count' @@ -52,3 +53,4 @@ class ConstBitFlag(IntEnum): Payable = 4 EventLog = 8 Interface = 16 + diff --git a/iconservice/iconscore/typing/__init__.py b/iconservice/iconscore/typing/__init__.py index f6966834d..a9404f4a4 100644 --- a/iconservice/iconscore/typing/__init__.py +++ b/iconservice/iconscore/typing/__init__.py @@ -20,10 +20,13 @@ "is_struct", ) -from typing import Tuple +from typing import Tuple, Union, Type from iconservice.base.address import Address +BaseObject = Union[bool, bytes, int, str, 'Address'] +BaseObjectType = Type[BaseObject] + BASE_TYPES = {bool, bytes, int, str, Address} TYPE_NAME_TO_TYPE = {_type.__name__: _type for _type in BASE_TYPES} @@ -35,6 +38,10 @@ def is_base_type(value: type) -> bool: return False +def name_to_type(type_name: str) -> BaseObjectType: + return TYPE_NAME_TO_TYPE[type_name] + + def get_origin(type_hint: type) -> type: """ Dict[str, int].__origin__ == dict diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index 84ae9d670..61f531f46 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -15,34 +15,18 @@ from typing import Optional, Dict, Union, Type, List, ForwardRef, Any -from iconservice.base.address import Address -from iconservice.base.exception import InvalidParamsException +from . import ( + BaseObject, + BaseObjectType, + is_base_type, + name_to_type, +) +from ...base.address import Address +from ...base.exception import InvalidParamsException -BaseObject = Union[bool, bytes, int, str, 'Address'] -BaseObjectType = Type[BaseObject] CommonObject = Union[bool, bytes, int, str, 'Address', Dict[str, BaseObject]] CommonType = Type[CommonObject] -BASE_TYPES = {bool, bytes, int, str, Address} -TYPE_NAME_TO_TYPE = { - "bool": bool, - "bytes": bytes, - "int": int, - "str": str, - "Address": Address, -} - - -def is_base_type(value: type) -> bool: - try: - return value in BASE_TYPES - except: - return False - - -def type_name_to_type(type_name: str) -> BaseObjectType: - return TYPE_NAME_TO_TYPE[type_name] - def str_to_int(value: str) -> int: if isinstance(value, int): @@ -82,7 +66,7 @@ def object_to_str(value: Any) -> Union[List, Dict, CommonObject]: def str_to_base_object_by_type_name(type_name: str, value: str) -> BaseObject: - return str_to_base_object(type_name_to_type(type_name), value) + return str_to_base_object(name_to_type(type_name), value) def str_to_base_object(type_hint: BaseObjectType, value: str) -> BaseObject: @@ -157,9 +141,9 @@ def type_hint_to_type_template(type_hint) -> Any: :return: """ if isinstance(type_hint, ForwardRef): - type_hint = type_name_to_type(type_hint.__forward_arg__) + type_hint = name_to_type(type_hint.__forward_arg__) elif isinstance(type_hint, str): - type_hint = type_name_to_type(type_hint) + type_hint = name_to_type(type_hint) if is_base_type(type_hint): return type_hint diff --git a/iconservice/iconscore/typing/definition.py b/iconservice/iconscore/typing/definition.py index b0f6b1dc2..c987d3b96 100644 --- a/iconservice/iconscore/typing/definition.py +++ b/iconservice/iconscore/typing/definition.py @@ -15,40 +15,67 @@ __all__ = "get_inputs" -from inspect import signature, Signature, Parameter -from typing import List, Dict, Mapping +from inspect import Signature, Parameter +from typing import List, Dict, Mapping, Iterable from . import get_origin, get_args, is_struct from .conversion import is_base_type -from ..icon_score_constant import ConstBitFlag, CONST_BIT_FLAG, STR_FALLBACK -from ...base.exception import IllegalFormatException, InvalidParamsException +from .element import ScoreElement, Function, EventLog +from ..icon_score_constant import STR_FALLBACK +from ...base.exception import ( + IllegalFormatException, + InvalidParamsException, + InternalServiceErrorException, +) -def get_functions(funcs: List[callable]) -> List: - ret = [] +def get_score_api(elements: Iterable[ScoreElement]) -> List: + """Returns score api used in icx_getScoreApi JSON-RPC method - for func in funcs: - const_bit_flag = getattr(func, CONST_BIT_FLAG, 0) - is_readonly = const_bit_flag & ConstBitFlag.ReadOnly == ConstBitFlag.ReadOnly - is_payable = const_bit_flag & ConstBitFlag.Payable == ConstBitFlag.Payable + :param elements: + :return: + """ - ret.append(_get_function(func, is_readonly, is_payable)) + api = [] - return ret + for element in elements: + if isinstance(element, Function): + func: Function = element + item = get_function(func.name, func.signature, func.is_readonly, func.is_payable) + elif isinstance(element, EventLog): + eventlog: EventLog = element + item = _get_eventlog(eventlog.name, eventlog.signature, eventlog.indexed_args_count) + else: + raise InternalServiceErrorException(f"Invalid score element: {element}") + + api.append(item) + return api -def _get_function(func: callable, is_readonly: bool, is_payable: bool) -> Dict: - if _is_fallback(func, is_payable): + +# def get_functions(funcs: List[callable]) -> List: +# ret = [] +# +# for func in funcs: +# const_bit_flag = getattr(func, CONST_BIT_FLAG, 0) +# is_readonly = const_bit_flag & ConstBitFlag.ReadOnly == ConstBitFlag.ReadOnly +# is_payable = const_bit_flag & ConstBitFlag.Payable == ConstBitFlag.Payable +# +# ret.append(get_function(func, is_readonly, is_payable)) +# +# return ret + + +def get_function(func_name: str, sig: Signature, is_readonly: bool, is_payable: bool) -> Dict: + if _is_fallback(func_name, sig, is_payable): return _get_fallback_function() else: - return _get_normal_function(func, is_readonly, is_payable) + return _get_normal_function(func_name, sig, is_readonly, is_payable) -def _get_normal_function(func: callable, is_readonly: bool, is_payable: bool) -> Dict: - sig = signature(func) - +def _get_normal_function(func_name: str, sig: Signature, is_readonly: bool, is_payable: bool) -> Dict: ret = { - "name": func.__name__, + "name": func_name, "type": "function", "inputs": get_inputs(sig.parameters), "outputs": get_outputs(sig.return_annotation) @@ -63,10 +90,9 @@ def _get_normal_function(func: callable, is_readonly: bool, is_payable: bool) -> return ret -def _is_fallback(func: callable, is_payable: bool) -> bool: - ret: bool = func.__name__ == STR_FALLBACK and is_payable +def _is_fallback(func_name: str, sig: Signature, is_payable: bool) -> bool: + ret: bool = func_name == STR_FALLBACK and is_payable if ret: - sig = signature(func) if len(sig.parameters) > 1: raise InvalidParamsException("Invalid fallback signature") @@ -97,17 +123,17 @@ def get_inputs(params: Mapping[str, Parameter]) -> list: def _get_input(name: str, type_hint: type) -> Dict: - _input = {"name": name} + inp = {"name": name} type_hints: List[type] = split_type_hint(type_hint) - _input["type"] = _type_hints_to_name(type_hints) + inp["type"] = _type_hints_to_name(type_hints) last_type_hint: type = type_hints[-1] if is_struct(last_type_hint): - _input["fields"] = _get_fields(last_type_hint) + inp["fields"] = _get_fields(last_type_hint) - return _input + return inp def split_type_hint(type_hint: type) -> List[type]: @@ -185,3 +211,29 @@ def get_outputs(type_hint: type) -> List: raise IllegalFormatException(f"Invalid output type: {type_hint}") return [{"type": type_name}] + + +def _get_eventlog(func_name: str, sig: Signature, indexed_args_count: int) -> Dict: + params = sig.parameters + + inputs = [] + for name, param in params.items(): + if not _is_param_valid(param): + continue + + annotation = param.annotation + type_hint = str if annotation is Parameter.empty else annotation + inp: Dict = _get_input(name, type_hint) + inp["indexed"] = len(inputs) < indexed_args_count + + inputs.append(inp) + + return { + "name": func_name, + "type": "eventlog", + "inputs": inputs + } + + +def _is_param_valid(param: Parameter) -> bool: + return param.name not in ("self", "cls") diff --git a/iconservice/iconscore/typing/function.py b/iconservice/iconscore/typing/element.py similarity index 58% rename from iconservice/iconscore/typing/function.py rename to iconservice/iconscore/typing/element.py index e7a6659e0..382a4a436 100644 --- a/iconservice/iconscore/typing/function.py +++ b/iconservice/iconscore/typing/element.py @@ -15,12 +15,14 @@ from inspect import signature, Signature, Parameter -from iconservice.iconscore.icon_score_constant import ( +from .type_hint import normalize_type_hint +from ..icon_score_constant import ( CONST_BIT_FLAG, ConstBitFlag, STR_FALLBACK, + CONST_INDEXED_ARGS_COUNT, ) -from iconservice.iconscore.typing.type_hint import normalize_type_hint +from ...base.exception import IllegalFormatException def normalize_signature(sig: Signature) -> Signature: @@ -56,18 +58,45 @@ def normalize_parameter(param: Parameter) -> Parameter: return param.replace(annotation=type_hint) -class Function(object): - def __init__(self, func: callable): - self._func = func - self._signature: Signature = normalize_signature(signature(func)) +class ScoreElement(object): + def __init__(self, element: callable): + self._verify(element) + self._element = element + self._signature: Signature = normalize_signature(signature(element)) + + @property + def element(self) -> callable: + return self._element @property def name(self) -> str: - return self._func.__name__ + return self._element.__name__ @property def flags(self) -> int: - return getattr(self._func, CONST_BIT_FLAG, 0) + return getattr(self.element, CONST_BIT_FLAG, 0) + + @property + def signature(self) -> Signature: + return self._signature + + @classmethod + def _verify(cls, element: callable): + """Check whether the flags of the element is valid + + :param element: + :return: + """ + flags = getattr(element, CONST_BIT_FLAG, 0) + counterpart = ConstBitFlag.ReadOnly | ConstBitFlag.Payable + + if (flags & counterpart) == counterpart: + raise IllegalFormatException(f"Payable method cannot be readonly") + + +class Function(ScoreElement): + def __init__(self, func: callable): + super().__init__(func) @property def is_external(self) -> bool: @@ -77,6 +106,19 @@ def is_external(self) -> bool: def is_payable(self) -> bool: return bool(self.flags & ConstBitFlag.Payable) + @property + def is_readonly(self) -> bool: + return bool(self.flags & ConstBitFlag.ReadOnly) + @property def is_fallback(self) -> bool: return self.name == STR_FALLBACK and self.is_payable + + +class EventLog(ScoreElement): + def __init__(self, eventlog: callable): + super().__init__(eventlog) + + @property + def indexed_args_count(self) -> int: + return getattr(self.element, CONST_INDEXED_ARGS_COUNT, 0) diff --git a/tests/unittest/iconscore/typing/test_definition.py b/tests/unittest/iconscore/typing/test_definition.py index 85fa06161..3d3e8a2c7 100644 --- a/tests/unittest/iconscore/typing/test_definition.py +++ b/tests/unittest/iconscore/typing/test_definition.py @@ -23,6 +23,7 @@ from iconservice.iconscore.typing.definition import ( get_inputs, split_type_hint, + _get_eventlog ) @@ -119,6 +120,25 @@ def func(_company: Company): (List[List[List[Person]]], [list, list, list, Person]), ] ) -def test__get_type(type_hint, expected): +def test_split_type_hint(type_hint, expected): types: List[type] = split_type_hint(type_hint) assert types == expected + + +def test__get_eventlog(): + expected = { + "name": "ICXTransfer", + "type": "eventlog", + "inputs": [ + {"name": "to", "type": "Address", "indexed": True}, + {"name": "amount", "type": "int", "indexed": False}, + {"name": "data", "type": "bytes", "indexed": False}, + ] + } + + indexed_args_count = 1 + def ICXTransfer(to: Address, amount: int, data: bytes): + pass + + ret = _get_eventlog(ICXTransfer.__name__, signature(ICXTransfer), indexed_args_count) + assert ret == expected diff --git a/tests/unittest/iconscore/typing/test_function.py b/tests/unittest/iconscore/typing/test_function.py index b87fad393..8accb25df 100644 --- a/tests/unittest/iconscore/typing/test_function.py +++ b/tests/unittest/iconscore/typing/test_function.py @@ -7,7 +7,7 @@ from typing_extensions import TypedDict -from iconservice.iconscore.typing.function import ( +from iconservice.iconscore.typing.element import ( normalize_signature, ) From cd561ebcf13422c722b682a8a763e44edc258af4 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Thu, 25 Jun 2020 08:48:52 +0900 Subject: [PATCH 08/40] Exception handling on get_score_api --- iconservice/iconscore/icon_score_base.py | 2 +- iconservice/iconscore/typing/__init__.py | 8 +- iconservice/iconscore/typing/definition.py | 23 +++-- .../test_integrate_get_score_api.py | 86 +++++++++---------- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index a78a7beb3..070dca24a 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -388,7 +388,7 @@ def __new__(mcs, name, bases, namespace, **kwargs): # TODO: Replace it with a new list supporting struct and list # api_list = ScoreApiGenerator.generate(custom_funcs) - api_list = get_score_api(elements) + api_list = get_score_api(elements.values()) setattr(cls, CONST_CLASS_API, api_list) return cls diff --git a/iconservice/iconscore/typing/__init__.py b/iconservice/iconscore/typing/__init__.py index a9404f4a4..f81067265 100644 --- a/iconservice/iconscore/typing/__init__.py +++ b/iconservice/iconscore/typing/__init__.py @@ -50,7 +50,13 @@ def get_origin(type_hint: type) -> type: :param type_hint: :return: """ - if is_base_type(type_hint) or is_struct(type_hint): + # if ( + # is_base_type(type_hint) + # or is_struct(type_hint) + # or type_hint in (list, dict) + # ): + # return type_hint + if isinstance(type_hint, type): return type_hint return getattr(type_hint, "__origin__", None) diff --git a/iconservice/iconscore/typing/definition.py b/iconservice/iconscore/typing/definition.py index c987d3b96..0aa820da6 100644 --- a/iconservice/iconscore/typing/definition.py +++ b/iconservice/iconscore/typing/definition.py @@ -16,7 +16,7 @@ __all__ = "get_inputs" from inspect import Signature, Parameter -from typing import List, Dict, Mapping, Iterable +from typing import List, Dict, Mapping, Iterable, Any from . import get_origin, get_args, is_struct from .conversion import is_base_type @@ -46,7 +46,7 @@ def get_score_api(elements: Iterable[ScoreElement]) -> List: eventlog: EventLog = element item = _get_eventlog(eventlog.name, eventlog.signature, eventlog.indexed_args_count) else: - raise InternalServiceErrorException(f"Invalid score element: {element}") + raise InternalServiceErrorException(f"Invalid score element: {element} {type(element)}") api.append(item) @@ -115,16 +115,27 @@ def get_inputs(params: Mapping[str, Parameter]) -> list: inputs = [] for name, param in params.items(): + if not _is_param_valid(param): + continue + annotation = param.annotation type_hint = str if annotation is Parameter.empty else annotation - inputs.append(_get_input(name, type_hint)) + + inputs.append(_get_input(name, type_hint, param.default)) return inputs -def _get_input(name: str, type_hint: type) -> Dict: +def _get_input(name: str, type_hint: type, default: Any) -> Dict: inp = {"name": name} + # Add default parameter value to score api + if default is not Parameter.empty: + if default is not None and not isinstance(default, type_hint): + raise InvalidParamsException(f"Default params type mismatch. value: {default} type: {type_hint}") + + inp["default"] = default + type_hints: List[type] = split_type_hint(type_hint) inp["type"] = _type_hints_to_name(type_hints) @@ -208,7 +219,7 @@ def get_outputs(type_hint: type) -> List: elif origin is list: type_name = "[]" else: - raise IllegalFormatException(f"Invalid output type: {type_hint}") + return [] return [{"type": type_name}] @@ -223,7 +234,7 @@ def _get_eventlog(func_name: str, sig: Signature, indexed_args_count: int) -> Di annotation = param.annotation type_hint = str if annotation is Parameter.empty else annotation - inp: Dict = _get_input(name, type_hint) + inp: Dict = _get_input(name, type_hint, param.default) inp["indexed"] = len(inputs) < indexed_args_count inputs.append(inp) diff --git a/tests/integrate_test/test_integrate_get_score_api.py b/tests/integrate_test/test_integrate_get_score_api.py index 13f4c2bcd..fe34b5193 100644 --- a/tests/integrate_test/test_integrate_get_score_api.py +++ b/tests/integrate_test/test_integrate_get_score_api.py @@ -48,6 +48,17 @@ def test_get_score_api(self): response2: dict = self.get_score_api(score_addr2) expect_value1 = [ + { + 'type': 'eventlog', + 'name': 'Changed', + 'inputs': [ + { + 'name': 'value', + 'type': 'int', + 'indexed': True + } + ] + }, { 'type': 'function', 'name': 'base_value', @@ -85,6 +96,10 @@ def test_get_score_api(self): ], 'readonly': True }, + ] + self.assertEqual(response1, expect_value1) + + expect_value2 = [ { 'type': 'eventlog', 'name': 'Changed', @@ -95,11 +110,7 @@ def test_get_score_api(self): 'indexed': True } ] - } - ] - self.assertEqual(response1, expect_value1) - - expect_value2 = [ + }, { 'type': 'function', 'name': 'base_value', @@ -148,17 +159,6 @@ def test_get_score_api(self): ], 'readonly': True }, - { - 'type': 'eventlog', - 'name': 'Changed', - 'inputs': [ - { - 'name': 'value', - 'type': 'int', - 'indexed': True - } - ] - } ] self.assertEqual(response2, expect_value2) @@ -190,6 +190,17 @@ def test_get_score_api_update(self): response2: dict = self.get_score_api(score_addr2) expect_value1 = [ + { + 'type': 'eventlog', + 'name': 'Changed', + 'inputs': [ + { + 'name': 'value', + 'type': 'int', + 'indexed': True + } + ] + }, { 'type': 'function', 'name': 'base_value1', @@ -259,6 +270,10 @@ def test_get_score_api_update(self): ], 'readonly': True }, + ] + self.assertEqual(response1, expect_value1) + + expect_value2 = [ { 'type': 'eventlog', 'name': 'Changed', @@ -269,11 +284,7 @@ def test_get_score_api_update(self): 'indexed': True } ] - } - ] - self.assertEqual(response1, expect_value1) - - expect_value2 = [ + }, { 'type': 'function', 'name': 'base_value1', @@ -322,17 +333,6 @@ def test_get_score_api_update(self): ], 'readonly': True }, - { - 'type': 'eventlog', - 'name': 'Changed', - 'inputs': [ - { - 'name': 'value', - 'type': 'int', - 'indexed': True - } - ] - } ] self.assertEqual(response2, expect_value2) @@ -345,6 +345,17 @@ def test_get_score_no_fallback(self): response1: dict = self.get_score_api(score_addr1) expect_value1 = [ + { + 'type': 'eventlog', + 'name': 'Changed', + 'inputs': [ + { + 'name': 'value', + 'type': 'int', + 'indexed': True + } + ] + }, { 'type': 'function', 'name': 'get_value', @@ -361,17 +372,6 @@ def test_get_score_no_fallback(self): ], 'readonly': True }, - { - 'type': 'eventlog', - 'name': 'Changed', - 'inputs': [ - { - 'name': 'value', - 'type': 'int', - 'indexed': True - } - ] - } ] self.assertEqual(response1, expect_value1) From 46ed9eed14309985a7e70316929eda0b3963e78a Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Thu, 25 Jun 2020 17:46:25 +0900 Subject: [PATCH 09/40] Move create_score_element() to typing/element.py --- iconservice/iconscore/icon_score_base.py | 31 +--------- iconservice/iconscore/icon_score_constant.py | 7 ++- iconservice/iconscore/typing/element.py | 63 +++++++++++++++----- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index 070dca24a..6aa7cd48f 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -18,7 +18,7 @@ from abc import abstractmethod, ABC, ABCMeta from functools import partial, wraps from inspect import isfunction, getmembers, signature, Parameter -from typing import TYPE_CHECKING, Callable, Any, List, Tuple, Union, Dict +from typing import TYPE_CHECKING, Callable, Any, List, Tuple from .context.context import ContextGetter, ContextContainer from .icon_score_base2 import InterfaceScore, revert, Block @@ -43,7 +43,7 @@ from .icx import Icx from .internal_call import InternalCall from .typing.definition import get_score_api -from .typing.element import Function, EventLog +from .typing.element import create_score_elements from ..base.address import Address from ..base.address import GOVERNANCE_SCORE_ADDRESS from ..base.exception import * @@ -324,33 +324,6 @@ def on_update(self, **kwargs) -> None: pass -def create_score_elements(cls) -> Dict: - elements = {} - flags = ( - ConstBitFlag.ReadOnly | - ConstBitFlag.External | - ConstBitFlag.Payable | - ConstBitFlag.EventLog - ) - - for name, func in getmembers(cls, predicate=isfunction): - if name.startswith("__"): - continue - if getattr(func, CONST_BIT_FLAG, 0) & flags: - elements[name] = create_score_element(func) - - return elements - - -def create_score_element(element: callable) -> Union[Function, EventLog]: - flags = getattr(element, CONST_BIT_FLAG, 0) - - if flags & ConstBitFlag.EventLog: - return EventLog(element) - else: - return Function(element) - - class IconScoreBaseMeta(ABCMeta): def __new__(mcs, name, bases, namespace, **kwargs): diff --git a/iconservice/iconscore/icon_score_constant.py b/iconservice/iconscore/icon_score_constant.py index 374fe4fa6..6dccb7e6f 100644 --- a/iconservice/iconscore/icon_score_constant.py +++ b/iconservice/iconscore/icon_score_constant.py @@ -48,9 +48,14 @@ @unique class ConstBitFlag(IntEnum): NonFlag = 0 + + # Used for external function ReadOnly = 1 External = 2 Payable = 4 + + # Used for eventlog declaration in score EventLog = 8 - Interface = 16 + # Used for interface declaration in score + Interface = 16 diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 382a4a436..b5cf1fd9c 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -13,7 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from inspect import signature, Signature, Parameter +from inspect import ( + isfunction, + getmembers, + signature, + Signature, + Parameter, +) +from typing import Dict, Union from .type_hint import normalize_type_hint from ..icon_score_constant import ( @@ -58,9 +65,21 @@ def normalize_parameter(param: Parameter) -> Parameter: return param.replace(annotation=type_hint) +def verify_score_flags(func: callable): + """Check if score flag combination is valid + + If the combination is not valid, raise an exception + """ + flags = getattr(func, CONST_BIT_FLAG, 0) + counterpart = ConstBitFlag.ReadOnly | ConstBitFlag.Payable + + if (flags & counterpart) == counterpart: + raise IllegalFormatException(f"Payable method cannot be readonly") + + class ScoreElement(object): def __init__(self, element: callable): - self._verify(element) + verify_score_flags(element) self._element = element self._signature: Signature = normalize_signature(signature(element)) @@ -80,19 +99,6 @@ def flags(self) -> int: def signature(self) -> Signature: return self._signature - @classmethod - def _verify(cls, element: callable): - """Check whether the flags of the element is valid - - :param element: - :return: - """ - flags = getattr(element, CONST_BIT_FLAG, 0) - counterpart = ConstBitFlag.ReadOnly | ConstBitFlag.Payable - - if (flags & counterpart) == counterpart: - raise IllegalFormatException(f"Payable method cannot be readonly") - class Function(ScoreElement): def __init__(self, func: callable): @@ -122,3 +128,30 @@ def __init__(self, eventlog: callable): @property def indexed_args_count(self) -> int: return getattr(self.element, CONST_INDEXED_ARGS_COUNT, 0) + + +def create_score_elements(cls) -> Dict: + elements = {} + flags = ( + ConstBitFlag.ReadOnly | + ConstBitFlag.External | + ConstBitFlag.Payable | + ConstBitFlag.EventLog + ) + + for name, func in getmembers(cls, predicate=isfunction): + if name.startswith("__"): + continue + if getattr(func, CONST_BIT_FLAG, 0) & flags: + elements[name] = create_score_element(func) + + return elements + + +def create_score_element(element: callable) -> Union[Function, EventLog]: + flags = getattr(element, CONST_BIT_FLAG, 0) + + if flags & ConstBitFlag.EventLog: + return EventLog(element) + else: + return Function(element) From 835f467ca0fd9edc82804f8e015d3a314fefa33e Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Thu, 25 Jun 2020 20:27:11 +0900 Subject: [PATCH 10/40] Refactor ConstBitFlag for score * Rename ConstBitFlag to ScoreFlag * Change type from IntEnum to Flag * Add functions for ScoreFlag management --- .../iconscore/icon_score_api_generator.py | 27 +++++---- iconservice/iconscore/icon_score_base.py | 52 ++++++++++------- iconservice/iconscore/icon_score_constant.py | 20 +++---- iconservice/iconscore/typing/element.py | 57 +++++++++++++------ 4 files changed, 97 insertions(+), 59 deletions(-) diff --git a/iconservice/iconscore/icon_score_api_generator.py b/iconservice/iconscore/icon_score_api_generator.py index d42e4ec16..4d2d49e4a 100644 --- a/iconservice/iconscore/icon_score_api_generator.py +++ b/iconservice/iconscore/icon_score_api_generator.py @@ -17,8 +17,9 @@ from inspect import signature, Signature, Parameter, isclass, getmembers, isfunction from typing import Any, Optional, TYPE_CHECKING -from .icon_score_constant import ConstBitFlag, CONST_BIT_FLAG, CONST_INDEXED_ARGS_COUNT, BaseType, \ +from .icon_score_constant import ScoreFlag, CONST_INDEXED_ARGS_COUNT, BaseType, \ STR_FALLBACK, STR_ON_INSTALL, STR_ON_UPDATE +from .typing.element import get_score_flag, is_any_score_flag_on from ..base.address import Address from ..base.exception import IllegalFormatException, InvalidParamsException from ..base.type_converter import TypeConverter @@ -77,12 +78,12 @@ def __check_on_deploy_function(context: 'IconScoreContext', sig_info: 'Signature @staticmethod def __generate_functions(src: list, score_funcs: list) -> None: for func in score_funcs: - const_bit_flag = getattr(func, CONST_BIT_FLAG, 0) - is_readonly = const_bit_flag & ConstBitFlag.ReadOnly == ConstBitFlag.ReadOnly - is_payable = const_bit_flag & ConstBitFlag.Payable == ConstBitFlag.Payable + score_flag = get_score_flag(func) + is_readonly = bool(score_flag & ScoreFlag.READONLY) + is_payable = bool(score_flag & ScoreFlag.PAYABLE) try: - if const_bit_flag & ConstBitFlag.External: + if score_flag & ScoreFlag.EXTERNAL: src.append(ScoreApiGenerator.__generate_normal_function( func.__name__, is_readonly, is_payable, signature(func))) elif func.__name__ == ScoreApiGenerator.__API_TYPE_FALLBACK: @@ -130,12 +131,16 @@ def __generate_fallback_function(func_name: str, is_payable: bool, sig_info: 'Si @staticmethod def __generate_events(src: list, score_funcs: list) -> None: - event_funcs = {func.__name__: signature(func) for func in score_funcs - if getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.EventLog} - - indexed_args_counts = {func.__name__: getattr(func, CONST_INDEXED_ARGS_COUNT, 0) - for func in score_funcs - if getattr(func, CONST_INDEXED_ARGS_COUNT, 0)} + event_funcs = { + func.__name__: signature(func) for func in score_funcs + if is_any_score_flag_on(func, ScoreFlag.EVENTLOG) + } + + indexed_args_counts = { + func.__name__: getattr(func, CONST_INDEXED_ARGS_COUNT, 0) + for func in score_funcs + if getattr(func, CONST_INDEXED_ARGS_COUNT, 0) + } for func_name, event in event_funcs.items(): index_args_count = indexed_args_counts.get(func_name, 0) diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index 6aa7cd48f..a22966e79 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -25,8 +25,7 @@ from .icon_score_constant import ( CONST_INDEXED_ARGS_COUNT, FORMAT_IS_NOT_FUNCTION_OBJECT, - CONST_BIT_FLAG, - ConstBitFlag, + ScoreFlag, FORMAT_DECORATOR_DUPLICATED, FORMAT_IS_NOT_DERIVED_OF_OBJECT, STR_FALLBACK, @@ -44,6 +43,10 @@ from .internal_call import InternalCall from .typing.definition import get_score_api from .typing.element import create_score_elements +from .typing.element import ( + set_score_flag_on, + is_any_score_flag_on, +) from ..base.address import Address from ..base.address import GOVERNANCE_SCORE_ADDRESS from ..base.exception import * @@ -70,11 +73,10 @@ def interface(func): if not isfunction(func): raise IllegalFormatException(FORMAT_IS_NOT_FUNCTION_OBJECT.format(func, cls_name)) - if getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.Interface: + if is_any_score_flag_on(func, ScoreFlag.INTERFACE): raise InvalidInterfaceException(FORMAT_DECORATOR_DUPLICATED.format('interface', func_name, cls_name)) - bit_flag = getattr(func, CONST_BIT_FLAG, 0) | ConstBitFlag.Interface - setattr(func, CONST_BIT_FLAG, bit_flag) + set_score_flag_on(func, ScoreFlag.INTERFACE) @wraps(func) def __wrapper(calling_obj: "InterfaceScore", *args, **kwargs): @@ -128,13 +130,11 @@ def eventlog(func=None, *, indexed=0): if len(parameters) - 1 < indexed: raise InvalidEventLogException("Index exceeds the number of parameters") - if getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.EventLog: + if is_any_score_flag_on(func, ScoreFlag.EVENTLOG): raise InvalidEventLogException(FORMAT_DECORATOR_DUPLICATED.format('eventlog', func_name, cls_name)) - bit_flag = getattr(func, CONST_BIT_FLAG, 0) | ConstBitFlag.EventLog - setattr(func, CONST_BIT_FLAG, bit_flag) + set_score_flag_on(func, ScoreFlag.EVENTLOG) setattr(func, CONST_INDEXED_ARGS_COUNT, indexed) - event_signature = __retrieve_event_signature(func_name, parameters) @wraps(func) @@ -265,11 +265,13 @@ def external(func=None, *, readonly=False): if func_name == STR_FALLBACK: raise InvalidExternalException(f"{func_name} cannot be declared as external") - if getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.External: + if is_any_score_flag_on(func, ScoreFlag.EXTERNAL): raise InvalidExternalException(FORMAT_DECORATOR_DUPLICATED.format('external', func_name, cls_name)) - bit_flag = getattr(func, CONST_BIT_FLAG, 0) | ConstBitFlag.External | int(readonly) - setattr(func, CONST_BIT_FLAG, bit_flag) + score_flag = ScoreFlag.EXTERNAL + if readonly: + score_flag |= ScoreFlag.READONLY + set_score_flag_on(func, score_flag) @wraps(func) def __wrapper(calling_obj: Any, *args, **kwargs): @@ -294,11 +296,10 @@ def payable(func): if not isfunction(func): raise IllegalFormatException(FORMAT_IS_NOT_FUNCTION_OBJECT.format(func, cls_name)) - if getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.Payable: + if is_any_score_flag_on(func, ScoreFlag.PAYABLE): raise InvalidPayableException(FORMAT_DECORATOR_DUPLICATED.format('payable', func_name, cls_name)) - bit_flag = getattr(func, CONST_BIT_FLAG, 0) | ConstBitFlag.Payable - setattr(func, CONST_BIT_FLAG, bit_flag) + set_score_flag_on(func, ScoreFlag.PAYABLE) @wraps(func) def __wrapper(calling_obj: Any, *args, **kwargs): @@ -342,13 +343,20 @@ def __new__(mcs, name, bases, namespace, **kwargs): elements = create_score_elements(cls) setattr(cls, CONST_CLASS_ELEMENTS, elements) - external_funcs = {func.__name__: signature(func) for func in custom_funcs - if getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.External} - payable_funcs = [func for func in custom_funcs - if getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.Payable] + external_funcs = { + func.__name__: signature(func) for func in custom_funcs + if is_any_score_flag_on(func, ScoreFlag.EXTERNAL) + } + + payable_funcs = [ + func for func in custom_funcs + if is_any_score_flag_on(func, ScoreFlag.PAYABLE) + ] - readonly_payables = [func for func in payable_funcs - if getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.ReadOnly] + readonly_payables = [ + func for func in payable_funcs + if is_any_score_flag_on(func, ScoreFlag.READONLY) + ] if bool(readonly_payables): raise IllegalFormatException(f"Payable method cannot be readonly") @@ -481,7 +489,7 @@ def __is_func_readonly(self, func_name: str) -> bool: return False func = getattr(self, func_name) - return bool(getattr(func, CONST_BIT_FLAG, 0) & ConstBitFlag.ReadOnly) + return is_any_score_flag_on(func, ScoreFlag.READONLY) # noinspection PyUnusedLocal @staticmethod diff --git a/iconservice/iconscore/icon_score_constant.py b/iconservice/iconscore/icon_score_constant.py index 6dccb7e6f..0d584aefe 100644 --- a/iconservice/iconscore/icon_score_constant.py +++ b/iconservice/iconscore/icon_score_constant.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from enum import IntEnum, unique +from enum import Flag from typing import TypeVar from ..base.address import Address @@ -29,7 +29,7 @@ CONST_CLASS_API = '__api' CONST_CLASS_ELEMENTS = '__elements' -CONST_BIT_FLAG = '__bit_flag' +CONST_SCORE_FLAG = '__score_flag' CONST_INDEXED_ARGS_COUNT = '__indexed_args_count' FORMAT_IS_NOT_FUNCTION_OBJECT = "isn't function object: {}, cls: {}" @@ -45,17 +45,17 @@ ATTR_SCORE_VALIDATATE_EXTERNAL_METHOD = "_IconScoreBase__validate_external_method" -@unique -class ConstBitFlag(IntEnum): - NonFlag = 0 +class ScoreFlag(Flag): + NONE = 0 # Used for external function - ReadOnly = 1 - External = 2 - Payable = 4 + READONLY = 0x01 + EXTERNAL = 0x02 + PAYABLE = 0x04 + FUNC = 0xFF # Used for eventlog declaration in score - EventLog = 8 + EVENTLOG = 0x100 # Used for interface declaration in score - Interface = 16 + INTERFACE = 0x10000 diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index b5cf1fd9c..1a1e3a1d3 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -24,8 +24,8 @@ from .type_hint import normalize_type_hint from ..icon_score_constant import ( - CONST_BIT_FLAG, - ConstBitFlag, + CONST_SCORE_FLAG, + ScoreFlag, STR_FALLBACK, CONST_INDEXED_ARGS_COUNT, ) @@ -70,8 +70,8 @@ def verify_score_flags(func: callable): If the combination is not valid, raise an exception """ - flags = getattr(func, CONST_BIT_FLAG, 0) - counterpart = ConstBitFlag.ReadOnly | ConstBitFlag.Payable + flags = getattr(func, CONST_SCORE_FLAG, 0) + counterpart = ScoreFlag.READONLY | ScoreFlag.PAYABLE if (flags & counterpart) == counterpart: raise IllegalFormatException(f"Payable method cannot be readonly") @@ -92,8 +92,8 @@ def name(self) -> str: return self._element.__name__ @property - def flags(self) -> int: - return getattr(self.element, CONST_BIT_FLAG, 0) + def flag(self) -> ScoreFlag: + return get_score_flag(self._element) @property def signature(self) -> Signature: @@ -106,15 +106,15 @@ def __init__(self, func: callable): @property def is_external(self) -> bool: - return bool(self.flags & ConstBitFlag.External) + return bool(self.flag & ScoreFlag.EXTERNAL) @property def is_payable(self) -> bool: - return bool(self.flags & ConstBitFlag.Payable) + return bool(self.flag & ScoreFlag.PAYABLE) @property def is_readonly(self) -> bool: - return bool(self.flags & ConstBitFlag.ReadOnly) + return bool(self.flag & ScoreFlag.READONLY) @property def is_fallback(self) -> bool: @@ -133,25 +133,50 @@ def indexed_args_count(self) -> int: def create_score_elements(cls) -> Dict: elements = {} flags = ( - ConstBitFlag.ReadOnly | - ConstBitFlag.External | - ConstBitFlag.Payable | - ConstBitFlag.EventLog + ScoreFlag.READONLY | + ScoreFlag.EXTERNAL | + ScoreFlag.PAYABLE | + ScoreFlag.EVENTLOG ) for name, func in getmembers(cls, predicate=isfunction): if name.startswith("__"): continue - if getattr(func, CONST_BIT_FLAG, 0) & flags: + + # Collect the only functions with one or more of the above 4 score flags + if is_any_score_flag_on(func, flags): elements[name] = create_score_element(func) return elements def create_score_element(element: callable) -> Union[Function, EventLog]: - flags = getattr(element, CONST_BIT_FLAG, 0) + flags = getattr(element, CONST_SCORE_FLAG, 0) - if flags & ConstBitFlag.EventLog: + if flags & ScoreFlag.EVENTLOG: return EventLog(element) else: return Function(element) + + +def get_score_flag(obj: callable, default: ScoreFlag = ScoreFlag.NONE) -> ScoreFlag: + return getattr(obj, CONST_SCORE_FLAG, default) + + +def set_score_flag(obj: callable, flag: ScoreFlag) -> ScoreFlag: + setattr(obj, CONST_SCORE_FLAG, flag) + return flag + + +def set_score_flag_on(obj: callable, flag: ScoreFlag) -> ScoreFlag: + flag |= get_score_flag(obj) + set_score_flag(obj, flag) + return flag + + +def is_all_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: + return get_score_flag(obj) & flag == flag + + +def is_any_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: + return bool(get_score_flag(obj) & flag) From 27ce54333327f804323dcd08cc4b4c011be9be61 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Thu, 25 Jun 2020 22:41:17 +0900 Subject: [PATCH 11/40] Add ScoreElementContainer * Fix a minor bug in IconScoreBase.__is_func_readonly() --- iconservice/iconscore/icon_score_base.py | 31 ++++--- iconservice/iconscore/icon_score_constant.py | 4 +- iconservice/iconscore/typing/definition.py | 41 ++++----- iconservice/iconscore/typing/element.py | 93 +++++++++++++++++--- 4 files changed, 118 insertions(+), 51 deletions(-) diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index a22966e79..0772e3a59 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -18,7 +18,7 @@ from abc import abstractmethod, ABC, ABCMeta from functools import partial, wraps from inspect import isfunction, getmembers, signature, Parameter -from typing import TYPE_CHECKING, Callable, Any, List, Tuple +from typing import TYPE_CHECKING, Callable, Any, List, Tuple, Mapping, Union from .context.context import ContextGetter, ContextContainer from .icon_score_base2 import InterfaceScore, revert, Block @@ -42,11 +42,14 @@ from .icx import Icx from .internal_call import InternalCall from .typing.definition import get_score_api -from .typing.element import create_score_elements from .typing.element import ( + ScoreElementContainer, + ScoreElement, + Function, set_score_flag_on, is_any_score_flag_on, ) +from .typing.element import create_score_elements from ..base.address import Address from ..base.address import GOVERNANCE_SCORE_ADDRESS from ..base.exception import * @@ -340,7 +343,7 @@ def __new__(mcs, name, bases, namespace, **kwargs): if not key.startswith('__')] # TODO: Normalize type hints of score parameters by goldworm - elements = create_score_elements(cls) + elements: Mapping[str, ScoreElement] = create_score_elements(cls) setattr(cls, CONST_CLASS_ELEMENTS, elements) external_funcs = { @@ -410,7 +413,8 @@ def __init__(self, db: 'IconScoreDatabase') -> None: self.__owner = IconScoreContextUtil.get_owner(self._context, self.__address) self.__icx = None - if not self.__get_attr_dict(CONST_CLASS_EXTERNALS): + elements: ScoreElementContainer = self.__get_attr_dict(CONST_CLASS_ELEMENTS) + if elements.externals == 0: raise InvalidExternalException('There is no external method in the SCORE') self.__db.set_observer(self.__create_db_observer()) @@ -440,7 +444,7 @@ def __validate_external_method(self, func_name: str) -> None: f"Method not found: {type(self).__name__}.{func_name}") @classmethod - def __get_attr_dict(cls, attr: str) -> dict: + def __get_attr_dict(cls, attr: str) -> Union[dict, ScoreElementContainer]: return getattr(cls, attr, {}) def __create_db_observer(self) -> 'DatabaseObserver': @@ -479,19 +483,20 @@ def __check_payable(self, func_name: str): f"Method not payable: {type(self).__name__}.{func_name}") def __is_external_method(self, func_name) -> bool: - return func_name in self.__get_attr_dict(CONST_CLASS_EXTERNALS) + elements = self.__get_attr_dict(CONST_CLASS_ELEMENTS) + func: Function = elements.get(func_name) + return isinstance(func, Function) and func.is_external def __is_payable_method(self, func_name) -> bool: - return func_name in self.__get_attr_dict(CONST_CLASS_PAYABLES) + elements = self.__get_attr_dict(CONST_CLASS_ELEMENTS) + func: Function = elements.get(func_name) + return isinstance(func, Function) and func.is_payable def __is_func_readonly(self, func_name: str) -> bool: - if not self.__is_external_method(func_name): - return False + elements = self.__get_attr_dict(CONST_CLASS_ELEMENTS) + func: Function = elements.get(func_name) + return isinstance(func, Function) and func.is_readonly - func = getattr(self, func_name) - return is_any_score_flag_on(func, ScoreFlag.READONLY) - - # noinspection PyUnusedLocal @staticmethod def __on_db_get(context: 'IconScoreContext', key: bytes, diff --git a/iconservice/iconscore/icon_score_constant.py b/iconservice/iconscore/icon_score_constant.py index 0d584aefe..556cfe1bd 100644 --- a/iconservice/iconscore/icon_score_constant.py +++ b/iconservice/iconscore/icon_score_constant.py @@ -52,10 +52,12 @@ class ScoreFlag(Flag): READONLY = 0x01 EXTERNAL = 0x02 PAYABLE = 0x04 - FUNC = 0xFF + FUNC = 0xff # Used for eventlog declaration in score EVENTLOG = 0x100 # Used for interface declaration in score INTERFACE = 0x10000 + + ALL = 0xffffff diff --git a/iconservice/iconscore/typing/definition.py b/iconservice/iconscore/typing/definition.py index 0aa820da6..a64ac1cd3 100644 --- a/iconservice/iconscore/typing/definition.py +++ b/iconservice/iconscore/typing/definition.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__all__ = "get_inputs" +__all__ = "get_score_api" from inspect import Signature, Parameter from typing import List, Dict, Mapping, Iterable, Any @@ -28,6 +28,9 @@ InternalServiceErrorException, ) +"""Utils to support icx_getScoreApi method +""" + def get_score_api(elements: Iterable[ScoreElement]) -> List: """Returns score api used in icx_getScoreApi JSON-RPC method @@ -41,7 +44,7 @@ def get_score_api(elements: Iterable[ScoreElement]) -> List: for element in elements: if isinstance(element, Function): func: Function = element - item = get_function(func.name, func.signature, func.is_readonly, func.is_payable) + item = _get_function(func.name, func.signature, func.is_readonly, func.is_payable) elif isinstance(element, EventLog): eventlog: EventLog = element item = _get_eventlog(eventlog.name, eventlog.signature, eventlog.indexed_args_count) @@ -53,20 +56,7 @@ def get_score_api(elements: Iterable[ScoreElement]) -> List: return api -# def get_functions(funcs: List[callable]) -> List: -# ret = [] -# -# for func in funcs: -# const_bit_flag = getattr(func, CONST_BIT_FLAG, 0) -# is_readonly = const_bit_flag & ConstBitFlag.ReadOnly == ConstBitFlag.ReadOnly -# is_payable = const_bit_flag & ConstBitFlag.Payable == ConstBitFlag.Payable -# -# ret.append(get_function(func, is_readonly, is_payable)) -# -# return ret - - -def get_function(func_name: str, sig: Signature, is_readonly: bool, is_payable: bool) -> Dict: +def _get_function(func_name: str, sig: Signature, is_readonly: bool, is_payable: bool) -> Dict: if _is_fallback(func_name, sig, is_payable): return _get_fallback_function() else: @@ -77,8 +67,8 @@ def _get_normal_function(func_name: str, sig: Signature, is_readonly: bool, is_p ret = { "name": func_name, "type": "function", - "inputs": get_inputs(sig.parameters), - "outputs": get_outputs(sig.return_annotation) + "inputs": _get_inputs(sig.parameters), + "outputs": _get_outputs(sig.return_annotation) } if is_readonly: @@ -111,7 +101,7 @@ def _get_fallback_function() -> Dict: } -def get_inputs(params: Mapping[str, Parameter]) -> list: +def _get_inputs(params: Mapping[str, Parameter]) -> list: inputs = [] for name, param in params.items(): @@ -132,11 +122,12 @@ def _get_input(name: str, type_hint: type, default: Any) -> Dict: # Add default parameter value to score api if default is not Parameter.empty: if default is not None and not isinstance(default, type_hint): - raise InvalidParamsException(f"Default params type mismatch. value: {default} type: {type_hint}") + raise InvalidParamsException( + f"Default params type mismatch. value: {default} type: {type_hint}") inp["default"] = default - type_hints: List[type] = split_type_hint(type_hint) + type_hints: List[type] = _split_type_hint(type_hint) inp["type"] = _type_hints_to_name(type_hints) last_type_hint: type = type_hints[-1] @@ -147,7 +138,7 @@ def _get_input(name: str, type_hint: type, default: Any) -> Dict: return inp -def split_type_hint(type_hint: type) -> List[type]: +def _split_type_hint(type_hint: type) -> List[type]: origin: type = get_origin(type_hint) ret = [origin] @@ -156,7 +147,7 @@ def split_type_hint(type_hint: type) -> List[type]: if len(args) != 1: raise IllegalFormatException(f"Invalid type: {type_hint}") - ret += split_type_hint(args[0]) + ret += _split_type_hint(args[0]) return ret @@ -197,7 +188,7 @@ def _get_fields(struct: type) -> List[dict]: for name, type_hint in annotations.items(): field = {"name": name} - type_hints: List[type] = split_type_hint(type_hint) + type_hints: List[type] = _split_type_hint(type_hint) field["type"] = _type_hints_to_name(type_hints) last_type_hint: type = type_hints[-1] @@ -209,7 +200,7 @@ def _get_fields(struct: type) -> List[dict]: return fields -def get_outputs(type_hint: type) -> List: +def _get_outputs(type_hint: type) -> List: origin = get_origin(type_hint) if is_base_type(origin): diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 1a1e3a1d3..76348414d 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from collections.abc import MutableMapping from inspect import ( isfunction, getmembers, @@ -20,7 +21,7 @@ Signature, Parameter, ) -from typing import Dict, Union +from typing import Union, Mapping from .type_hint import normalize_type_hint from ..icon_score_constant import ( @@ -29,7 +30,7 @@ STR_FALLBACK, CONST_INDEXED_ARGS_COUNT, ) -from ...base.exception import IllegalFormatException +from ...base.exception import IllegalFormatException, InternalServiceErrorException def normalize_signature(sig: Signature) -> Signature: @@ -65,21 +66,33 @@ def normalize_parameter(param: Parameter) -> Parameter: return param.replace(annotation=type_hint) -def verify_score_flags(func: callable): +def verify_score_flag(func: callable): """Check if score flag combination is valid If the combination is not valid, raise an exception """ - flags = getattr(func, CONST_SCORE_FLAG, 0) - counterpart = ScoreFlag.READONLY | ScoreFlag.PAYABLE + flag: ScoreFlag = get_score_flag(func) - if (flags & counterpart) == counterpart: - raise IllegalFormatException(f"Payable method cannot be readonly") + if flag & ScoreFlag.READONLY: + # READONLY cannot be combined with PAYABLE + if flag & ScoreFlag.PAYABLE: + raise IllegalFormatException(f"Payable method cannot be readonly") + # READONLY cannot be set alone without EXTERNAL + elif not (flag & ScoreFlag.EXTERNAL): + raise IllegalFormatException(f"Invalid score flag: {flag}") + + # EVENTLOG cannot be combined with other flags + if flag & ScoreFlag.EVENTLOG and flag != ScoreFlag.EVENTLOG: + raise IllegalFormatException(f"Invalid score flag: {flag}") + + # INTERFACE cannot be combined with other flags + if flag & ScoreFlag.INTERFACE and flag != ScoreFlag.INTERFACE: + raise IllegalFormatException(f"Invalid score flag: {flag}") class ScoreElement(object): def __init__(self, element: callable): - verify_score_flags(element) + verify_score_flag(element) self._element = element self._signature: Signature = normalize_signature(signature(element)) @@ -130,8 +143,63 @@ def indexed_args_count(self) -> int: return getattr(self.element, CONST_INDEXED_ARGS_COUNT, 0) -def create_score_elements(cls) -> Dict: - elements = {} +class ScoreElementContainer(MutableMapping): + def __init__(self): + self._elements = {} + self._externals = 0 + self._eventlogs = 0 + self._readonly = False + + @property + def externals(self) -> int: + return self._externals + + @property + def eventlogs(self) -> int: + return self._eventlogs + + def __getitem__(self, k: str) -> ScoreElement: + return self._elements[k] + + def __setitem__(self, k: str, v: ScoreElement) -> None: + self._check_writable() + self._elements[k] = v + + if isinstance(v, Function): + self._externals += 1 + elif isinstance(v, EventLog): + self._eventlogs += 1 + else: + raise InternalServiceErrorException(f"Invalid element: {v}") + + def __iter__(self): + for k in self._elements: + yield k + + def __len__(self) -> int: + return len(self._elements) + + def __delitem__(self, k: str) -> None: + self._check_writable() + + element = self._elements[k] + del self._elements[k] + + if is_any_score_flag_on(element, ScoreFlag.EVENTLOG): + self._eventlogs -= 1 + else: + self._externals -= 1 + + def _check_writable(self): + if self._readonly: + raise InternalServiceErrorException("ScoreElementContainer not writable") + + def freeze(self): + self._readonly = True + + +def create_score_elements(cls) -> Mapping: + elements = ScoreElementContainer() flags = ( ScoreFlag.READONLY | ScoreFlag.EXTERNAL | @@ -147,11 +215,12 @@ def create_score_elements(cls) -> Dict: if is_any_score_flag_on(func, flags): elements[name] = create_score_element(func) + elements.freeze() return elements def create_score_element(element: callable) -> Union[Function, EventLog]: - flags = getattr(element, CONST_SCORE_FLAG, 0) + flags = get_score_flag(element) if flags & ScoreFlag.EVENTLOG: return EventLog(element) @@ -175,7 +244,7 @@ def set_score_flag_on(obj: callable, flag: ScoreFlag) -> ScoreFlag: def is_all_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: - return get_score_flag(obj) & flag == flag + return (get_score_flag(obj) & flag) == flag def is_any_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: From ed22d47bfd1dbffa8764b2b463d67e42b07c7f8d Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Thu, 25 Jun 2020 22:42:30 +0900 Subject: [PATCH 12/40] Rename tests/unittest to tests/unit_test --- .../iiss/prevote/test_iiss_unstake_lock_period.py | 2 +- .../sample_event_log_score/sample_event_log_score.py | 2 +- tests/integrate_test/test_integrate_event_log.py | 2 +- tests/{unittest => unit_test}/__init__.py | 0 tests/{unittest => unit_test}/base/__init__.py | 0 tests/{unittest => unit_test}/base/test_address.py | 0 tests/{unittest => unit_test}/base/test_block.py | 0 .../base/test_type_converter.py | 0 .../base/test_type_converter_func.py | 0 .../base/test_type_converter_iiss.py | 0 tests/{unittest => unit_test}/deploy/__init__.py | 0 .../deploy/test_icon_score_deploy_engine.py | 0 tests/{unittest => unit_test}/fee/__init__.py | 0 tests/{unittest => unit_test}/fee/test_deposit.py | 0 tests/{unittest => unit_test}/fee/test_deposit_meta.py | 0 tests/{unittest => unit_test}/fee/test_fee_storage.py | 0 .../fee/test_virtual_step_calculator.py | 0 tests/{unittest => unit_test}/hashing/__init__.py | 0 .../hashing/test_hash_generator.py | 0 tests/{unittest => unit_test}/iconscore/__init__.py | 0 .../iconscore/test_external_payable_call_method.py | 0 .../iconscore/test_icon_container_db.py | 0 .../iconscore/test_icon_score_api.py | 0 .../iconscore/test_icon_score_context.py | 0 .../iconscore/test_icon_score_event_log.py | 0 .../iconscore/test_icon_score_step.py | 0 .../iconscore/test_icon_score_trace.py | 0 .../iconscore/typing/__init__.py | 0 .../iconscore/typing/test__init__.py | 0 .../iconscore/typing/test_definition.py | 10 +++++----- .../iconscore/typing/test_function.py | 0 .../iconscore/typing/test_type_hint.py | 0 tests/{unittest => unit_test}/iconservice.json | 0 tests/{unittest => unit_test}/iiss/test_iiss_engine.py | 0 .../iiss/test_reward_calc_data_storage.py | 0 tests/{unittest => unit_test}/inv/__init__.py | 0 tests/{unittest => unit_test}/inv/data/test_value.py | 0 tests/{unittest => unit_test}/inv/test_container.py | 0 tests/{unittest => unit_test}/inv/test_engine.py | 0 tests/{unittest => unit_test}/inv/test_storage.py | 0 .../inv/test_value_converter.py | 0 tests/{unittest => unit_test}/score_loader/__init__.py | 0 .../score_loader/test_icon_score_class_loader.py | 0 tests/{unittest => unit_test}/test_icon_config.py | 0 .../{unittest => unit_test}/test_icon_inner_service.py | 0 .../test_icon_service_engine.py | 0 .../test_precommit_data_manager.py | 2 +- 47 files changed, 9 insertions(+), 9 deletions(-) rename tests/{unittest => unit_test}/__init__.py (100%) rename tests/{unittest => unit_test}/base/__init__.py (100%) rename tests/{unittest => unit_test}/base/test_address.py (100%) rename tests/{unittest => unit_test}/base/test_block.py (100%) rename tests/{unittest => unit_test}/base/test_type_converter.py (100%) rename tests/{unittest => unit_test}/base/test_type_converter_func.py (100%) rename tests/{unittest => unit_test}/base/test_type_converter_iiss.py (100%) rename tests/{unittest => unit_test}/deploy/__init__.py (100%) rename tests/{unittest => unit_test}/deploy/test_icon_score_deploy_engine.py (100%) rename tests/{unittest => unit_test}/fee/__init__.py (100%) rename tests/{unittest => unit_test}/fee/test_deposit.py (100%) rename tests/{unittest => unit_test}/fee/test_deposit_meta.py (100%) rename tests/{unittest => unit_test}/fee/test_fee_storage.py (100%) rename tests/{unittest => unit_test}/fee/test_virtual_step_calculator.py (100%) rename tests/{unittest => unit_test}/hashing/__init__.py (100%) rename tests/{unittest => unit_test}/hashing/test_hash_generator.py (100%) rename tests/{unittest => unit_test}/iconscore/__init__.py (100%) rename tests/{unittest => unit_test}/iconscore/test_external_payable_call_method.py (100%) rename tests/{unittest => unit_test}/iconscore/test_icon_container_db.py (100%) rename tests/{unittest => unit_test}/iconscore/test_icon_score_api.py (100%) rename tests/{unittest => unit_test}/iconscore/test_icon_score_context.py (100%) rename tests/{unittest => unit_test}/iconscore/test_icon_score_event_log.py (100%) rename tests/{unittest => unit_test}/iconscore/test_icon_score_step.py (100%) rename tests/{unittest => unit_test}/iconscore/test_icon_score_trace.py (100%) rename tests/{unittest => unit_test}/iconscore/typing/__init__.py (100%) rename tests/{unittest => unit_test}/iconscore/typing/test__init__.py (100%) rename tests/{unittest => unit_test}/iconscore/typing/test_definition.py (95%) rename tests/{unittest => unit_test}/iconscore/typing/test_function.py (100%) rename tests/{unittest => unit_test}/iconscore/typing/test_type_hint.py (100%) rename tests/{unittest => unit_test}/iconservice.json (100%) rename tests/{unittest => unit_test}/iiss/test_iiss_engine.py (100%) rename tests/{unittest => unit_test}/iiss/test_reward_calc_data_storage.py (100%) rename tests/{unittest => unit_test}/inv/__init__.py (100%) rename tests/{unittest => unit_test}/inv/data/test_value.py (100%) rename tests/{unittest => unit_test}/inv/test_container.py (100%) rename tests/{unittest => unit_test}/inv/test_engine.py (100%) rename tests/{unittest => unit_test}/inv/test_storage.py (100%) rename tests/{unittest => unit_test}/inv/test_value_converter.py (100%) rename tests/{unittest => unit_test}/score_loader/__init__.py (100%) rename tests/{unittest => unit_test}/score_loader/test_icon_score_class_loader.py (100%) rename tests/{unittest => unit_test}/test_icon_config.py (100%) rename tests/{unittest => unit_test}/test_icon_inner_service.py (100%) rename tests/{unittest => unit_test}/test_icon_service_engine.py (100%) rename tests/{unittest => unit_test}/test_precommit_data_manager.py (98%) diff --git a/tests/integrate_test/iiss/prevote/test_iiss_unstake_lock_period.py b/tests/integrate_test/iiss/prevote/test_iiss_unstake_lock_period.py index ba0256246..754262322 100644 --- a/tests/integrate_test/iiss/prevote/test_iiss_unstake_lock_period.py +++ b/tests/integrate_test/iiss/prevote/test_iiss_unstake_lock_period.py @@ -18,7 +18,7 @@ """ from iconservice.icon_constant import Revision, ICX_IN_LOOP, ConfigKey, IISS_DAY_BLOCK -from tests.unittest.iiss.test_iiss_engine import EXPECTED_LOCK_PERIOD_PRE_STAKE_PERCENT +from tests.unit_test.iiss.test_iiss_engine import EXPECTED_LOCK_PERIOD_PRE_STAKE_PERCENT from tests.integrate_test.iiss.test_iiss_base import TestIISSBase from tests.integrate_test.test_integrate_base import TOTAL_SUPPLY diff --git a/tests/integrate_test/samples/sample_event_log_scores/sample_event_log_score/sample_event_log_score.py b/tests/integrate_test/samples/sample_event_log_scores/sample_event_log_score/sample_event_log_score.py index d2f531377..a57c372fd 100644 --- a/tests/integrate_test/samples/sample_event_log_scores/sample_event_log_score/sample_event_log_score.py +++ b/tests/integrate_test/samples/sample_event_log_scores/sample_event_log_score/sample_event_log_score.py @@ -54,7 +54,7 @@ def set_value(self, value: str): self._value.set(value) @external(readonly=True) - def call_even_log_in_read_only_method(self) -> str: + def call_event_log_in_read_only_method(self) -> str: self.NormalEventLog("1", "2", "3") return "test" diff --git a/tests/integrate_test/test_integrate_event_log.py b/tests/integrate_test/test_integrate_event_log.py index f9e15b5cf..19b47d411 100644 --- a/tests/integrate_test/test_integrate_event_log.py +++ b/tests/integrate_test/test_integrate_event_log.py @@ -120,7 +120,7 @@ def test_call_event_log_in_read_only_method(self): tx_results: List['TransactionResult'] = self.score_call(from_=self._accounts[0], to_=score_addr1, - func_name="call_even_log_in_read_only_method", + func_name="call_event_log_in_read_only_method", expected_status=False) self.assertEqual(1, len(tx_results)) diff --git a/tests/unittest/__init__.py b/tests/unit_test/__init__.py similarity index 100% rename from tests/unittest/__init__.py rename to tests/unit_test/__init__.py diff --git a/tests/unittest/base/__init__.py b/tests/unit_test/base/__init__.py similarity index 100% rename from tests/unittest/base/__init__.py rename to tests/unit_test/base/__init__.py diff --git a/tests/unittest/base/test_address.py b/tests/unit_test/base/test_address.py similarity index 100% rename from tests/unittest/base/test_address.py rename to tests/unit_test/base/test_address.py diff --git a/tests/unittest/base/test_block.py b/tests/unit_test/base/test_block.py similarity index 100% rename from tests/unittest/base/test_block.py rename to tests/unit_test/base/test_block.py diff --git a/tests/unittest/base/test_type_converter.py b/tests/unit_test/base/test_type_converter.py similarity index 100% rename from tests/unittest/base/test_type_converter.py rename to tests/unit_test/base/test_type_converter.py diff --git a/tests/unittest/base/test_type_converter_func.py b/tests/unit_test/base/test_type_converter_func.py similarity index 100% rename from tests/unittest/base/test_type_converter_func.py rename to tests/unit_test/base/test_type_converter_func.py diff --git a/tests/unittest/base/test_type_converter_iiss.py b/tests/unit_test/base/test_type_converter_iiss.py similarity index 100% rename from tests/unittest/base/test_type_converter_iiss.py rename to tests/unit_test/base/test_type_converter_iiss.py diff --git a/tests/unittest/deploy/__init__.py b/tests/unit_test/deploy/__init__.py similarity index 100% rename from tests/unittest/deploy/__init__.py rename to tests/unit_test/deploy/__init__.py diff --git a/tests/unittest/deploy/test_icon_score_deploy_engine.py b/tests/unit_test/deploy/test_icon_score_deploy_engine.py similarity index 100% rename from tests/unittest/deploy/test_icon_score_deploy_engine.py rename to tests/unit_test/deploy/test_icon_score_deploy_engine.py diff --git a/tests/unittest/fee/__init__.py b/tests/unit_test/fee/__init__.py similarity index 100% rename from tests/unittest/fee/__init__.py rename to tests/unit_test/fee/__init__.py diff --git a/tests/unittest/fee/test_deposit.py b/tests/unit_test/fee/test_deposit.py similarity index 100% rename from tests/unittest/fee/test_deposit.py rename to tests/unit_test/fee/test_deposit.py diff --git a/tests/unittest/fee/test_deposit_meta.py b/tests/unit_test/fee/test_deposit_meta.py similarity index 100% rename from tests/unittest/fee/test_deposit_meta.py rename to tests/unit_test/fee/test_deposit_meta.py diff --git a/tests/unittest/fee/test_fee_storage.py b/tests/unit_test/fee/test_fee_storage.py similarity index 100% rename from tests/unittest/fee/test_fee_storage.py rename to tests/unit_test/fee/test_fee_storage.py diff --git a/tests/unittest/fee/test_virtual_step_calculator.py b/tests/unit_test/fee/test_virtual_step_calculator.py similarity index 100% rename from tests/unittest/fee/test_virtual_step_calculator.py rename to tests/unit_test/fee/test_virtual_step_calculator.py diff --git a/tests/unittest/hashing/__init__.py b/tests/unit_test/hashing/__init__.py similarity index 100% rename from tests/unittest/hashing/__init__.py rename to tests/unit_test/hashing/__init__.py diff --git a/tests/unittest/hashing/test_hash_generator.py b/tests/unit_test/hashing/test_hash_generator.py similarity index 100% rename from tests/unittest/hashing/test_hash_generator.py rename to tests/unit_test/hashing/test_hash_generator.py diff --git a/tests/unittest/iconscore/__init__.py b/tests/unit_test/iconscore/__init__.py similarity index 100% rename from tests/unittest/iconscore/__init__.py rename to tests/unit_test/iconscore/__init__.py diff --git a/tests/unittest/iconscore/test_external_payable_call_method.py b/tests/unit_test/iconscore/test_external_payable_call_method.py similarity index 100% rename from tests/unittest/iconscore/test_external_payable_call_method.py rename to tests/unit_test/iconscore/test_external_payable_call_method.py diff --git a/tests/unittest/iconscore/test_icon_container_db.py b/tests/unit_test/iconscore/test_icon_container_db.py similarity index 100% rename from tests/unittest/iconscore/test_icon_container_db.py rename to tests/unit_test/iconscore/test_icon_container_db.py diff --git a/tests/unittest/iconscore/test_icon_score_api.py b/tests/unit_test/iconscore/test_icon_score_api.py similarity index 100% rename from tests/unittest/iconscore/test_icon_score_api.py rename to tests/unit_test/iconscore/test_icon_score_api.py diff --git a/tests/unittest/iconscore/test_icon_score_context.py b/tests/unit_test/iconscore/test_icon_score_context.py similarity index 100% rename from tests/unittest/iconscore/test_icon_score_context.py rename to tests/unit_test/iconscore/test_icon_score_context.py diff --git a/tests/unittest/iconscore/test_icon_score_event_log.py b/tests/unit_test/iconscore/test_icon_score_event_log.py similarity index 100% rename from tests/unittest/iconscore/test_icon_score_event_log.py rename to tests/unit_test/iconscore/test_icon_score_event_log.py diff --git a/tests/unittest/iconscore/test_icon_score_step.py b/tests/unit_test/iconscore/test_icon_score_step.py similarity index 100% rename from tests/unittest/iconscore/test_icon_score_step.py rename to tests/unit_test/iconscore/test_icon_score_step.py diff --git a/tests/unittest/iconscore/test_icon_score_trace.py b/tests/unit_test/iconscore/test_icon_score_trace.py similarity index 100% rename from tests/unittest/iconscore/test_icon_score_trace.py rename to tests/unit_test/iconscore/test_icon_score_trace.py diff --git a/tests/unittest/iconscore/typing/__init__.py b/tests/unit_test/iconscore/typing/__init__.py similarity index 100% rename from tests/unittest/iconscore/typing/__init__.py rename to tests/unit_test/iconscore/typing/__init__.py diff --git a/tests/unittest/iconscore/typing/test__init__.py b/tests/unit_test/iconscore/typing/test__init__.py similarity index 100% rename from tests/unittest/iconscore/typing/test__init__.py rename to tests/unit_test/iconscore/typing/test__init__.py diff --git a/tests/unittest/iconscore/typing/test_definition.py b/tests/unit_test/iconscore/typing/test_definition.py similarity index 95% rename from tests/unittest/iconscore/typing/test_definition.py rename to tests/unit_test/iconscore/typing/test_definition.py index 3d3e8a2c7..3ead0599c 100644 --- a/tests/unittest/iconscore/typing/test_definition.py +++ b/tests/unit_test/iconscore/typing/test_definition.py @@ -21,8 +21,8 @@ from iconservice.base.address import Address from iconservice.iconscore.typing.definition import ( - get_inputs, - split_type_hint, + _get_inputs, + _split_type_hint, _get_eventlog ) @@ -65,7 +65,7 @@ def func(_persons: List[Person]): pass sig = signature(func) - inputs = get_inputs(sig.parameters) + inputs = _get_inputs(sig.parameters) assert inputs == expected @@ -103,7 +103,7 @@ def func(_company: Company): pass sig = signature(func) - inputs = get_inputs(sig.parameters) + inputs = _get_inputs(sig.parameters) assert inputs == expected @@ -121,7 +121,7 @@ def func(_company: Company): ] ) def test_split_type_hint(type_hint, expected): - types: List[type] = split_type_hint(type_hint) + types: List[type] = _split_type_hint(type_hint) assert types == expected diff --git a/tests/unittest/iconscore/typing/test_function.py b/tests/unit_test/iconscore/typing/test_function.py similarity index 100% rename from tests/unittest/iconscore/typing/test_function.py rename to tests/unit_test/iconscore/typing/test_function.py diff --git a/tests/unittest/iconscore/typing/test_type_hint.py b/tests/unit_test/iconscore/typing/test_type_hint.py similarity index 100% rename from tests/unittest/iconscore/typing/test_type_hint.py rename to tests/unit_test/iconscore/typing/test_type_hint.py diff --git a/tests/unittest/iconservice.json b/tests/unit_test/iconservice.json similarity index 100% rename from tests/unittest/iconservice.json rename to tests/unit_test/iconservice.json diff --git a/tests/unittest/iiss/test_iiss_engine.py b/tests/unit_test/iiss/test_iiss_engine.py similarity index 100% rename from tests/unittest/iiss/test_iiss_engine.py rename to tests/unit_test/iiss/test_iiss_engine.py diff --git a/tests/unittest/iiss/test_reward_calc_data_storage.py b/tests/unit_test/iiss/test_reward_calc_data_storage.py similarity index 100% rename from tests/unittest/iiss/test_reward_calc_data_storage.py rename to tests/unit_test/iiss/test_reward_calc_data_storage.py diff --git a/tests/unittest/inv/__init__.py b/tests/unit_test/inv/__init__.py similarity index 100% rename from tests/unittest/inv/__init__.py rename to tests/unit_test/inv/__init__.py diff --git a/tests/unittest/inv/data/test_value.py b/tests/unit_test/inv/data/test_value.py similarity index 100% rename from tests/unittest/inv/data/test_value.py rename to tests/unit_test/inv/data/test_value.py diff --git a/tests/unittest/inv/test_container.py b/tests/unit_test/inv/test_container.py similarity index 100% rename from tests/unittest/inv/test_container.py rename to tests/unit_test/inv/test_container.py diff --git a/tests/unittest/inv/test_engine.py b/tests/unit_test/inv/test_engine.py similarity index 100% rename from tests/unittest/inv/test_engine.py rename to tests/unit_test/inv/test_engine.py diff --git a/tests/unittest/inv/test_storage.py b/tests/unit_test/inv/test_storage.py similarity index 100% rename from tests/unittest/inv/test_storage.py rename to tests/unit_test/inv/test_storage.py diff --git a/tests/unittest/inv/test_value_converter.py b/tests/unit_test/inv/test_value_converter.py similarity index 100% rename from tests/unittest/inv/test_value_converter.py rename to tests/unit_test/inv/test_value_converter.py diff --git a/tests/unittest/score_loader/__init__.py b/tests/unit_test/score_loader/__init__.py similarity index 100% rename from tests/unittest/score_loader/__init__.py rename to tests/unit_test/score_loader/__init__.py diff --git a/tests/unittest/score_loader/test_icon_score_class_loader.py b/tests/unit_test/score_loader/test_icon_score_class_loader.py similarity index 100% rename from tests/unittest/score_loader/test_icon_score_class_loader.py rename to tests/unit_test/score_loader/test_icon_score_class_loader.py diff --git a/tests/unittest/test_icon_config.py b/tests/unit_test/test_icon_config.py similarity index 100% rename from tests/unittest/test_icon_config.py rename to tests/unit_test/test_icon_config.py diff --git a/tests/unittest/test_icon_inner_service.py b/tests/unit_test/test_icon_inner_service.py similarity index 100% rename from tests/unittest/test_icon_inner_service.py rename to tests/unit_test/test_icon_inner_service.py diff --git a/tests/unittest/test_icon_service_engine.py b/tests/unit_test/test_icon_service_engine.py similarity index 100% rename from tests/unittest/test_icon_service_engine.py rename to tests/unit_test/test_icon_service_engine.py diff --git a/tests/unittest/test_precommit_data_manager.py b/tests/unit_test/test_precommit_data_manager.py similarity index 98% rename from tests/unittest/test_precommit_data_manager.py rename to tests/unit_test/test_precommit_data_manager.py index bafd52dba..edaada718 100644 --- a/tests/unittest/test_precommit_data_manager.py +++ b/tests/unit_test/test_precommit_data_manager.py @@ -12,7 +12,7 @@ from iconservice.precommit_data_manager import PrecommitData, PrecommitDataWriter from iconservice.utils import bytes_to_hex from tests import create_hash_256, create_timestamp, create_block_hash, create_address -from tests.unittest.iiss.test_reward_calc_data_storage import DUMMY_BLOCK_HEIGHT, CONFIG_MAIN_PREP_COUNT, \ +from tests.unit_test.iiss.test_reward_calc_data_storage import DUMMY_BLOCK_HEIGHT, CONFIG_MAIN_PREP_COUNT, \ CONFIG_SUB_PREP_COUNT PRECOMMIT_LOG_PATH = os.path.join(os.getcwd(), PrecommitDataWriter.DIR_NAME) From 7a49b2a050dc9a35ff9194d1f714004c52319a3c Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Fri, 26 Jun 2020 00:21:41 +0900 Subject: [PATCH 13/40] Add typing_extensions to requirements.txt for TypedDict --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f211280e4..fb5553b1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ earlgrey~=0.0.4 iconcommons~=1.1.3 msgpack~=1.0.0 iso3166~=1.0.1 +typing_extensions~=3.7.4.2 From ff444c28566f762cb726e995997483a1f539aeb1 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Fri, 26 Jun 2020 00:35:37 +0900 Subject: [PATCH 14/40] Fix minor typo --- iconservice/iconscore/icon_score_constant.py | 2 +- iconservice/iconscore/icon_score_engine.py | 4 ++-- tests/legacy_unittest/test_icon_score_engine.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/iconservice/iconscore/icon_score_constant.py b/iconservice/iconscore/icon_score_constant.py index 556cfe1bd..ec3724b3d 100644 --- a/iconservice/iconscore/icon_score_constant.py +++ b/iconservice/iconscore/icon_score_constant.py @@ -42,7 +42,7 @@ ATTR_SCORE_GET_API = "_IconScoreBase__get_api" ATTR_SCORE_CALL = "_IconScoreBase__call" -ATTR_SCORE_VALIDATATE_EXTERNAL_METHOD = "_IconScoreBase__validate_external_method" +ATTR_SCORE_VALIDATE_EXTERNAL_METHOD = "_IconScoreBase__validate_external_method" class ScoreFlag(Flag): diff --git a/iconservice/iconscore/icon_score_engine.py b/iconservice/iconscore/icon_score_engine.py index ef3154418..0e92efa6b 100644 --- a/iconservice/iconscore/icon_score_engine.py +++ b/iconservice/iconscore/icon_score_engine.py @@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Any from .icon_score_constant import STR_FALLBACK, ATTR_SCORE_GET_API, ATTR_SCORE_CALL, \ - ATTR_SCORE_VALIDATATE_EXTERNAL_METHOD + ATTR_SCORE_VALIDATE_EXTERNAL_METHOD from .icon_score_context import IconScoreContext from .icon_score_context_util import IconScoreContextUtil from ..base.address import Address, SYSTEM_SCORE_ADDRESS @@ -123,7 +123,7 @@ def _convert_score_params_by_annotations(context: 'IconScoreContext', kw_params: dict) -> dict: tmp_params = deepcopy(kw_params) - validate_external_method = getattr(icon_score, ATTR_SCORE_VALIDATATE_EXTERNAL_METHOD) + validate_external_method = getattr(icon_score, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD) validate_external_method(func_name) remove_invalid_params = False diff --git a/tests/legacy_unittest/test_icon_score_engine.py b/tests/legacy_unittest/test_icon_score_engine.py index 35f6f3d3c..d1d5c4455 100644 --- a/tests/legacy_unittest/test_icon_score_engine.py +++ b/tests/legacy_unittest/test_icon_score_engine.py @@ -24,7 +24,7 @@ from iconservice.base.address import AddressPrefix, SYSTEM_SCORE_ADDRESS, Address from iconservice.base.exception import ScoreNotFoundException, InvalidParamsException from iconservice.iconscore.icon_score_constant import ATTR_SCORE_GET_API, ATTR_SCORE_CALL, \ - ATTR_SCORE_VALIDATATE_EXTERNAL_METHOD + ATTR_SCORE_VALIDATE_EXTERNAL_METHOD from iconservice.iconscore.icon_score_context import IconScoreContext from iconservice.iconscore.icon_score_context import IconScoreContextType from iconservice.iconscore.icon_score_engine import IconScoreEngine @@ -199,12 +199,12 @@ def test_method(address: Address, integer: int): context = Mock(spec=IconScoreContext) score_object = Mock(spec=IconScoreBase) - setattr(score_object, ATTR_SCORE_VALIDATATE_EXTERNAL_METHOD, Mock()) + setattr(score_object, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD, Mock()) setattr(score_object, 'test_method', test_method) converted_params = \ IconScoreEngine._convert_score_params_by_annotations(context, score_object, 'test_method', primitive_params) - validate_external_method = getattr(score_object, ATTR_SCORE_VALIDATATE_EXTERNAL_METHOD) + validate_external_method = getattr(score_object, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD) validate_external_method.assert_called() # primitive_params must not be changed. self.assertEqual(type(primitive_params["address"]), str) @@ -219,7 +219,7 @@ def test_method(address: Address, integer: int): context, score_object, 'test_method', not_matching_type_params) # not enough number of params inputted, - validate_external_method = getattr(score_object, ATTR_SCORE_VALIDATATE_EXTERNAL_METHOD) + validate_external_method = getattr(score_object, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD) validate_external_method.reset_mock() insufficient_params = {"address": str(create_address(AddressPrefix.EOA))} From 433877d4113f4db5bedd7a6a7fa24fd31f0a4c6d Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Fri, 26 Jun 2020 01:18:06 +0900 Subject: [PATCH 15/40] Remove unused codes from iconscore package --- iconservice/iconscore/icon_score_base.py | 72 ++++++++++---------- iconservice/iconscore/icon_score_constant.py | 7 +- iconservice/iconscore/typing/element.py | 17 ++++- 3 files changed, 51 insertions(+), 45 deletions(-) diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index 0772e3a59..bb4662f50 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -17,8 +17,8 @@ import warnings from abc import abstractmethod, ABC, ABCMeta from functools import partial, wraps -from inspect import isfunction, getmembers, signature, Parameter -from typing import TYPE_CHECKING, Callable, Any, List, Tuple, Mapping, Union +from inspect import isfunction, signature, Parameter +from typing import TYPE_CHECKING, Callable, Any, List, Tuple, Mapping from .context.context import ContextGetter, ContextContainer from .icon_score_base2 import InterfaceScore, revert, Block @@ -29,8 +29,6 @@ FORMAT_DECORATOR_DUPLICATED, FORMAT_IS_NOT_DERIVED_OF_OBJECT, STR_FALLBACK, - CONST_CLASS_EXTERNALS, - CONST_CLASS_PAYABLES, CONST_CLASS_API, CONST_CLASS_ELEMENTS, BaseType, @@ -315,7 +313,6 @@ def __wrapper(calling_obj: Any, *args, **kwargs): return __wrapper - class IconScoreObject(ABC): def __init__(self, *args, **kwargs) -> None: @@ -339,36 +336,36 @@ def __new__(mcs, name, bases, namespace, **kwargs): if not isinstance(namespace, dict): raise InvalidParamsException('namespace is not dict!') - custom_funcs = [value for key, value in getmembers(cls, predicate=isfunction) - if not key.startswith('__')] - # TODO: Normalize type hints of score parameters by goldworm elements: Mapping[str, ScoreElement] = create_score_elements(cls) setattr(cls, CONST_CLASS_ELEMENTS, elements) - external_funcs = { - func.__name__: signature(func) for func in custom_funcs - if is_any_score_flag_on(func, ScoreFlag.EXTERNAL) - } - - payable_funcs = [ - func for func in custom_funcs - if is_any_score_flag_on(func, ScoreFlag.PAYABLE) - ] - - readonly_payables = [ - func for func in payable_funcs - if is_any_score_flag_on(func, ScoreFlag.READONLY) - ] - - if bool(readonly_payables): - raise IllegalFormatException(f"Payable method cannot be readonly") - - if external_funcs: - setattr(cls, CONST_CLASS_EXTERNALS, external_funcs) - if payable_funcs: - payable_funcs = {func.__name__: signature(func) for func in payable_funcs} - setattr(cls, CONST_CLASS_PAYABLES, payable_funcs) + # custom_funcs = [value for key, value in getmembers(cls, predicate=isfunction) + # if not key.startswith('__')] + # + # external_funcs = { + # func.__name__: signature(func) for func in custom_funcs + # if is_any_score_flag_on(func, ScoreFlag.EXTERNAL) + # } + # + # payable_funcs = [ + # func for func in custom_funcs + # if is_any_score_flag_on(func, ScoreFlag.PAYABLE) + # ] + # + # readonly_payables = [ + # func for func in payable_funcs + # if is_any_score_flag_on(func, ScoreFlag.READONLY) + # ] + # + # if bool(readonly_payables): + # raise IllegalFormatException(f"Payable method cannot be readonly") + # + # if external_funcs: + # setattr(cls, CONST_CLASS_EXTERNALS, external_funcs) + # if payable_funcs: + # payable_funcs = {func.__name__: signature(func) for func in payable_funcs} + # setattr(cls, CONST_CLASS_PAYABLES, payable_funcs) # TODO: Replace it with a new list supporting struct and list # api_list = ScoreApiGenerator.generate(custom_funcs) @@ -413,7 +410,7 @@ def __init__(self, db: 'IconScoreDatabase') -> None: self.__owner = IconScoreContextUtil.get_owner(self._context, self.__address) self.__icx = None - elements: ScoreElementContainer = self.__get_attr_dict(CONST_CLASS_ELEMENTS) + elements: ScoreElementContainer = self.__get_score_elements() if elements.externals == 0: raise InvalidExternalException('There is no external method in the SCORE') @@ -444,8 +441,8 @@ def __validate_external_method(self, func_name: str) -> None: f"Method not found: {type(self).__name__}.{func_name}") @classmethod - def __get_attr_dict(cls, attr: str) -> Union[dict, ScoreElementContainer]: - return getattr(cls, attr, {}) + def __get_score_elements(cls) -> ScoreElementContainer: + return getattr(cls, CONST_CLASS_ELEMENTS) def __create_db_observer(self) -> 'DatabaseObserver': return DatabaseObserver( @@ -483,20 +480,21 @@ def __check_payable(self, func_name: str): f"Method not payable: {type(self).__name__}.{func_name}") def __is_external_method(self, func_name) -> bool: - elements = self.__get_attr_dict(CONST_CLASS_ELEMENTS) + elements = self.__get_score_elements() func: Function = elements.get(func_name) return isinstance(func, Function) and func.is_external def __is_payable_method(self, func_name) -> bool: - elements = self.__get_attr_dict(CONST_CLASS_ELEMENTS) + elements = self.__get_score_elements() func: Function = elements.get(func_name) return isinstance(func, Function) and func.is_payable def __is_func_readonly(self, func_name: str) -> bool: - elements = self.__get_attr_dict(CONST_CLASS_ELEMENTS) + elements = self.__get_score_elements() func: Function = elements.get(func_name) return isinstance(func, Function) and func.is_readonly + # noinspection PyUnusedLocal @staticmethod def __on_db_get(context: 'IconScoreContext', key: bytes, diff --git a/iconservice/iconscore/icon_score_constant.py b/iconservice/iconscore/icon_score_constant.py index ec3724b3d..409a22be4 100644 --- a/iconservice/iconscore/icon_score_constant.py +++ b/iconservice/iconscore/icon_score_constant.py @@ -23,9 +23,6 @@ # TODO add checking function for list, dict type BaseType = TypeVar("BaseType", bool, int, str, bytes, list, dict, Address) -CONST_CLASS_EXTERNALS = '__externals' -CONST_CLASS_PAYABLES = '__payables' -CONST_CLASS_INDEXES = '__indexes' CONST_CLASS_API = '__api' CONST_CLASS_ELEMENTS = '__elements' @@ -52,7 +49,7 @@ class ScoreFlag(Flag): READONLY = 0x01 EXTERNAL = 0x02 PAYABLE = 0x04 - FUNC = 0xff + FUNC = 0xFF # Used for eventlog declaration in score EVENTLOG = 0x100 @@ -60,4 +57,4 @@ class ScoreFlag(Flag): # Used for interface declaration in score INTERFACE = 0x10000 - ALL = 0xffffff + ALL = 0xFFFFFF diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 76348414d..6227069a1 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from collections import OrderedDict from collections.abc import MutableMapping from inspect import ( isfunction, @@ -30,6 +31,7 @@ STR_FALLBACK, CONST_INDEXED_ARGS_COUNT, ) +from ... import utils from ...base.exception import IllegalFormatException, InternalServiceErrorException @@ -114,6 +116,9 @@ def signature(self) -> Signature: class Function(ScoreElement): + """Represents a exposed function of SCORE + + """ def __init__(self, func: callable): super().__init__(func) @@ -135,6 +140,9 @@ def is_fallback(self) -> bool: class EventLog(ScoreElement): + """Represents an eventlog declared in a SCORE + """ + def __init__(self, eventlog: callable): super().__init__(eventlog) @@ -144,8 +152,11 @@ def indexed_args_count(self) -> int: class ScoreElementContainer(MutableMapping): + """Container which has score elements like function and eventlog + """ + def __init__(self): - self._elements = {} + self._elements = OrderedDict() self._externals = 0 self._eventlogs = 0 self._readonly = False @@ -244,8 +255,8 @@ def set_score_flag_on(obj: callable, flag: ScoreFlag) -> ScoreFlag: def is_all_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: - return (get_score_flag(obj) & flag) == flag + return utils.is_all_flag_on(get_score_flag(obj), flag) def is_any_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: - return bool(get_score_flag(obj) & flag) + return utils.is_any_flag_on(get_score_flag(obj), flag) From 6100f6fc8304759270c6e00f2af9bfb20c88e88b Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Fri, 26 Jun 2020 21:30:41 +0900 Subject: [PATCH 16/40] Make normalize_type_hint robust * Remove useless unittests --- iconservice/iconscore/typing/__init__.py | 22 ++-- iconservice/iconscore/typing/conversion.py | 115 ++++++------------ iconservice/iconscore/typing/element.py | 44 +++++-- iconservice/iconscore/typing/type_hint.py | 70 ----------- .../iconscore/typing/test__init__.py | 15 +++ .../iconscore/typing/test_convertion.py | 95 +++++++++++++++ .../iconscore/typing/test_element.py | 66 ++++++++++ .../iconscore/typing/test_function.py | 63 ---------- .../iconscore/typing/test_type_hint.py | 27 ---- 9 files changed, 263 insertions(+), 254 deletions(-) delete mode 100644 iconservice/iconscore/typing/type_hint.py create mode 100644 tests/unit_test/iconscore/typing/test_convertion.py create mode 100644 tests/unit_test/iconscore/typing/test_element.py delete mode 100644 tests/unit_test/iconscore/typing/test_function.py delete mode 100644 tests/unit_test/iconscore/typing/test_type_hint.py diff --git a/iconservice/iconscore/typing/__init__.py b/iconservice/iconscore/typing/__init__.py index f81067265..47c299851 100644 --- a/iconservice/iconscore/typing/__init__.py +++ b/iconservice/iconscore/typing/__init__.py @@ -15,12 +15,14 @@ __all__ = ( "is_base_type", + "name_to_type", "get_origin", "get_args", "is_struct", + "get_annotations", ) -from typing import Tuple, Union, Type +from typing import Tuple, Union, Type, Dict, Any, Optional from iconservice.base.address import Address @@ -42,20 +44,16 @@ def name_to_type(type_name: str) -> BaseObjectType: return TYPE_NAME_TO_TYPE[type_name] -def get_origin(type_hint: type) -> type: +def get_origin(type_hint: type) -> Optional[type]: """ - Dict[str, int].__origin__ == dict - List[str].__origin__ == list + Dict[str, int] -> dict + List[str] -> list + subclass of type -> itself + subclass of TypedDict -> itself :param type_hint: :return: """ - # if ( - # is_base_type(type_hint) - # or is_struct(type_hint) - # or type_hint in (list, dict) - # ): - # return type_hint if isinstance(type_hint, type): return type_hint @@ -71,3 +69,7 @@ def is_struct(type_hint) -> bool: return type_hint.__class__.__name__ == "_TypedDictMeta" except: return False + + +def get_annotations(obj: Any, default: Any) -> Dict[str, type]: + return getattr(obj, "__annotations__", default) diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index 61f531f46..d64b4a0b3 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -13,29 +13,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional, Dict, Union, Type, List, ForwardRef, Any +from inspect import Signature +from typing import Optional, Dict, Union, Type, List, Any from . import ( BaseObject, BaseObjectType, is_base_type, + is_struct, name_to_type, + get_origin, + get_args, + get_annotations, ) from ...base.address import Address -from ...base.exception import InvalidParamsException CommonObject = Union[bool, bytes, int, str, 'Address', Dict[str, BaseObject]] CommonType = Type[CommonObject] -def str_to_int(value: str) -> int: - if isinstance(value, int): - return value - - base = 16 if is_hex(value) else 10 - return int(value, base) - - def base_object_to_str(value: Any) -> str: if isinstance(value, Address): return str(value) @@ -52,24 +48,31 @@ def base_object_to_str(value: Any) -> str: def object_to_str(value: Any) -> Union[List, Dict, CommonObject]: - try: + if is_base_type(type(value)): return base_object_to_str(value) - except TypeError: - pass if isinstance(value, list): - return object_to_str_in_list(value) - elif isinstance(value, dict): - return object_to_str_in_dict(value) + return [object_to_str(i) for i in value] + + if isinstance(value, dict): + return {k: object_to_str(value[k]) for k in value} raise TypeError(f"Unsupported type: {type(value)}") def str_to_base_object_by_type_name(type_name: str, value: str) -> BaseObject: - return str_to_base_object(name_to_type(type_name), value) + return str_to_base_object(value, name_to_type(type_name)) + + +def str_to_int(value: str) -> int: + if isinstance(value, int): + return value + base = 16 if is_hex(value) else 10 + return int(value, base) -def str_to_base_object(type_hint: BaseObjectType, value: str) -> BaseObject: + +def str_to_base_object(value: str, type_hint: type) -> BaseObject: if type_hint is bool: return bool(str_to_int(value)) if type_hint is bytes: @@ -84,13 +87,6 @@ def str_to_base_object(type_hint: BaseObjectType, value: str) -> BaseObject: raise TypeError(f"Unknown type: {type_hint}") -def str_to_object(type_hint, value): - if isinstance(value, dict): - return str_to_object_in_typed_dict(type_hint, value) - else: - return str_to_base_object(type_hint, value) - - def bytes_to_hex(value: bytes, prefix: str = "0x") -> str: return f"{prefix}{value.hex()}" @@ -109,61 +105,28 @@ def is_hex(value: str) -> bool: return value.startswith("0x") or value.startswith("-0x") -def str_to_object_in_typed_dict(type_hints, value: Dict[str, str]) -> Dict[str, BaseObject]: - annotations = type_hints.__annotations__ - return {k: str_to_base_object(annotations[k], value[k]) for k in annotations} - - -def object_to_str_in_dict(value: Dict[str, BaseObject]) -> Dict[str, str]: - return {k: object_to_str(value[k]) for k in value} - - -def str_to_object_in_list(type_hint, value: List[Any]) -> List[CommonObject]: - assert len(type_hint.__args__) == 1 - - args_type_hint = type_hint.__args__[0] - return [str_to_object(args_type_hint, i) for i in value] - - -def object_to_str_in_list(value: List[CommonObject]) -> List[Union[str, Dict[str, str]]]: - """Return a copied list from a given list - - All items in the origin list are copied to a copied list and converted in string format - There is no change in a given list - """ - return [object_to_str(i) for i in value] - +def convert_score_parameters(params: Dict[str, Any], sig: Signature): + return { + name: str_to_object(param, sig.parameters[name].annotation) + for name, param in params.items() + } -def type_hint_to_type_template(type_hint) -> Any: - """Convert type_hint to type_template consisting of base_object_types, list and dict - :param type_hint: - :return: - """ - if isinstance(type_hint, ForwardRef): - type_hint = name_to_type(type_hint.__forward_arg__) - elif isinstance(type_hint, str): - type_hint = name_to_type(type_hint) +def str_to_object(value: Union[str, list, dict], type_hint: type) -> Any: + origin = get_origin(type_hint) - if is_base_type(type_hint): - return type_hint + if is_base_type(origin): + return str_to_base_object(value, origin) - if type_hint is List: - raise InvalidParamsException(f"No arguments: {type_hint}") + if is_struct(origin): + annotations = get_annotations(origin, None) + return {k: str_to_object(v, annotations[k]) for k, v in value.items()} - # If type_hint is a subclass of TypedDict - attr = "__annotations__" - if hasattr(type_hint, attr): - # annotations is a dictionary containing field_name(str) as a key and type as a value - annotations = getattr(type_hint, attr) - return {k: type_hint_to_type_template(v) for k, v in annotations.items()} + args = get_args(type_hint) - try: - origin = getattr(type_hint, "__origin__") - if origin is list: - args = getattr(type_hint, "__args__") - return [type_hint_to_type_template(args[0])] - except: - pass + if origin is list: + return [str_to_object(i, args[0]) for i in value] - raise InvalidParamsException(f"Unsupported type: {type_hint}") + if origin is dict: + type_hint = args[1] + return {k: str_to_object(v, type_hint) for k, v in value.items()} diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 6227069a1..beb0b340e 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -22,9 +22,15 @@ Signature, Parameter, ) -from typing import Union, Mapping - -from .type_hint import normalize_type_hint +from typing import Union, Mapping, List, Dict + +from . import ( + is_base_type, + is_struct, + get_origin, + get_args, + name_to_type, +) from ..icon_score_constant import ( CONST_SCORE_FLAG, ScoreFlag, @@ -68,13 +74,33 @@ def normalize_parameter(param: Parameter) -> Parameter: return param.replace(annotation=type_hint) -def verify_score_flag(func: callable): +def normalize_type_hint(type_hint) -> type: + # If type hint is str, convert it to type hint + if isinstance(type_hint, str): + type_hint = name_to_type(type_hint) + + origin = get_origin(type_hint) + + if is_base_type(origin) or is_struct(origin): + return type_hint + + args = get_args(type_hint) + size = len(args) + + if origin is list and size == 1: + return List[normalize_type_hint(args[0])] + + if origin is dict and size == 2 and args[0] is str: + return Dict[str, normalize_type_hint(args[1])] + + raise IllegalFormatException(f"Unsupported type hint: {type_hint}") + + +def verify_score_flag(flag: ScoreFlag): """Check if score flag combination is valid If the combination is not valid, raise an exception """ - flag: ScoreFlag = get_score_flag(func) - if flag & ScoreFlag.READONLY: # READONLY cannot be combined with PAYABLE if flag & ScoreFlag.PAYABLE: @@ -94,7 +120,6 @@ def verify_score_flag(func: callable): class ScoreElement(object): def __init__(self, element: callable): - verify_score_flag(element) self._element = element self._signature: Signature = normalize_signature(signature(element)) @@ -223,7 +248,10 @@ def create_score_elements(cls) -> Mapping: continue # Collect the only functions with one or more of the above 4 score flags - if is_any_score_flag_on(func, flags): + flag = get_score_flag(func) + + if utils.is_any_flag_on(flag, flags): + verify_score_flag(flag) elements[name] = create_score_element(func) elements.freeze() diff --git a/iconservice/iconscore/typing/type_hint.py b/iconservice/iconscore/typing/type_hint.py deleted file mode 100644 index 63b1dfe66..000000000 --- a/iconservice/iconscore/typing/type_hint.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2020 ICON Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import List, Dict, get_type_hints -from typing_extensions import TypedDict - -from iconservice.iconscore.typing import get_origin, get_args, is_struct - - -def normalize_type_hint(type_hint) -> type: - return type_hint - -# def _normalize_type_hint(): -# # BaseType -# if is_base_type(annotation): -# return param -# -# # No annotation -# if annotation is Parameter.empty: -# return param.replace(annotation=str) -# if annotation is list: -# return param.replace(annotation=List[str]) -# -# origin = getattr(annotation, "__origin__", None) -# -# if origin is list: -# return param.replace(annotation=List[str]) -# -# raise TypeError(f"Unsupported type hint: {annotation}") - - -# def normalize_list_type_hint(type_hint) -> type: -# """ -# 1. list -> List[str] -# 2. List -> List[str] -# 3. List[int] -> List[int] -# 4. List[Custom] -> List[Custom] -# 5. List["Custom"] -> exception -# 6. List[Union[str, int]] -> exception -# -# :param type_hint: -# :return: -# """ -# if type_hint is list: -# return List[str] -# -# attr = "__args__" -# if not hasattr(type_hint, attr): -# return List[str] -# -# args = getattr(type_hint, "__args__") -# if len(args) > 1: -# raise TypeError(f"Unsupported type hint: {type_hint}") -# -# if is_base_type(args[0]) or issubclass(args[0], TypedDict): -# return type_hint -# -# raise TypeError(f"Unsupported type hint: {type_hint}") diff --git a/tests/unit_test/iconscore/typing/test__init__.py b/tests/unit_test/iconscore/typing/test__init__.py index 011a515e5..9fae92e80 100644 --- a/tests/unit_test/iconscore/typing/test__init__.py +++ b/tests/unit_test/iconscore/typing/test__init__.py @@ -49,6 +49,7 @@ def test_get_origin(type_hint, expected): (int, ()), (str, ()), (Address, ()), + (Person, ()), (List[int], (int,)), (List[List[str]], (List[str],)), (Dict[str, int], (str, int)), @@ -60,3 +61,17 @@ def test_get_origin(type_hint, expected): def test_get_args(type_hint, expected): args = get_args(type_hint) assert args == expected + + +def test_get_args_with_struct(): + expected = { + "name": str, + "age": int, + "single": bool, + } + + annotations = Person.__annotations__ + assert len(annotations) == len(expected) + + for name, type_hint in annotations.items(): + assert type_hint == expected[name] diff --git a/tests/unit_test/iconscore/typing/test_convertion.py b/tests/unit_test/iconscore/typing/test_convertion.py new file mode 100644 index 000000000..79dc8e803 --- /dev/null +++ b/tests/unit_test/iconscore/typing/test_convertion.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import os +from typing import List, Dict + +import pytest + +from typing_extensions import TypedDict + +from iconservice.base.exception import IllegalFormatException +from iconservice.base.address import Address, AddressPrefix +from iconservice.iconscore.typing.conversion import ( + convert_score_parameters, + object_to_str, +) +from iconservice.iconscore.typing.element import Function + + +class User(TypedDict): + name: str + age: int + single: bool + wallet: Address + + +def test_convert_score_parameters(): + def func( + _bool: bool, + _bytes: bytes, + _int: int, + _str: str, + _address: Address, + _list: List[int], + _dict: Dict[str, int], + _struct: User, + _list_of_struct: List[User], + _dict_of_str_and_struct: Dict[str, User], + ): + pass + + address = Address.from_data(AddressPrefix.EOA, os.urandom(20)) + + params = { + "_bool": True, + "_bytes": b"hello", + "_int": 100, + "_str": "world", + "_address": address, + "_list": [0, 1, 2, 3, 4, 5], + "_dict": {"0": 0, "1": 1, "2": 2}, + "_struct": {"name": "hello", "age": 30, "single": True, "wallet": address}, + "_list_of_struct": [ + {"name": "hello", "age": 30, "single": True, "wallet": address}, + {"name": "world", "age": 40, "single": False}, + ], + "_dict_of_str_and_struct": { + "a": {"name": "hello", "age": 30, "single": True, "wallet": address}, + "b": {"age": 27}, + }, + } + + params_in_str = object_to_str(params) + params_in_object = convert_score_parameters(params_in_str, inspect.signature(func)) + assert params_in_object == params + + +def test_convert_score_parameters_without_no_type_hint(): + def func(_a, _b, _c, _d: List, _e: Dict): + pass + + params = { + "_a": "0x1234", + "_b": "hello", + "_c": "0x1", + "_d": ["0", "1", "2"], + "_e": {"a": "a", "b": "b"}, + } + + with pytest.raises(IllegalFormatException): + function = Function(func) + convert_score_parameters(params, function.signature) diff --git a/tests/unit_test/iconscore/typing/test_element.py b/tests/unit_test/iconscore/typing/test_element.py new file mode 100644 index 000000000..063d9706f --- /dev/null +++ b/tests/unit_test/iconscore/typing/test_element.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List, Dict, Union, Optional + +import pytest +from typing_extensions import TypedDict + +from iconservice.base.address import Address +from iconservice.base.exception import IllegalFormatException +from iconservice.iconscore.typing.element import normalize_type_hint + + +class Person(TypedDict): + name: str + age: int + + +@pytest.mark.parametrize( + "type_hint,expected", + [ + (bool, bool), + (bytes, bytes), + (int, int), + (str, str), + (Address, Address), + (list, None), + (List, None), + (List[int], List[int]), + (List[Person], List[Person]), + (List["Person"], None), + (Dict, None), + (Dict[str, int], Dict[str, int]), + (Dict[str, Person], Dict[str, Person]), + (Dict[int, str], None), + (Optional[str], None), + (Optional[List[str]], None), + (Optional[Dict[str, str]], None), + (Optional[Dict], None), + (Union[str], str), + (Union[str, int], None), + ] +) +def test_normalize_abnormal_type_hint(type_hint, expected): + print(type_hint) + + try: + ret = normalize_type_hint(type_hint) + except IllegalFormatException: + ret = None + except TypeError: + ret = None + + assert ret == expected diff --git a/tests/unit_test/iconscore/typing/test_function.py b/tests/unit_test/iconscore/typing/test_function.py deleted file mode 100644 index 8accb25df..000000000 --- a/tests/unit_test/iconscore/typing/test_function.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- - -from inspect import signature -from typing import List, Dict, Optional, Union - -import pytest - -from typing_extensions import TypedDict - -from iconservice.iconscore.typing.element import ( - normalize_signature, -) - - -class Person(TypedDict): - name: str - age: int - - -# Allowed list -def func0(name: str, age: int) -> int: pass -def func1(name: "str", age: int) -> str: pass -def func2(a: list): pass -def func3(a: List): pass -def func4(a: List[int]): pass -def func5(a: List["int"]): pass -def func6(a: List[Person]): pass -def func7(a: List["Person"]): pass - -_ALLOWED_LIST = [func0, func1, func2, func3, func4, func5, func6, func7] - -# Denied list -def func0_error(a: "int"): pass -def func1_error(a: Dict): pass -def func2_error(a: Dict[str, str]): pass -def func3_error(a: Union[int, str]): pass -def func4_error(a: Optional[str]): pass - -_DENIED_LIST = [ - func0_error, - func1_error, - func2_error, - func3_error, - func4_error, -] - - -@pytest.mark.parametrize("func", _ALLOWED_LIST) -def test_normalize_signature_with_allowed_func(func): - sig = signature(func) - new_sig = normalize_signature(sig) - assert new_sig == sig - - -@pytest.mark.skip("Not implemented") -@pytest.mark.parametrize("func", _DENIED_LIST) -def test_normalize_signature_with_denied_func(func): - sig = signature(func) - new_sig = normalize_signature(sig) - assert new_sig != sig - - new_sig2 = normalize_signature(new_sig) - assert new_sig2 == new_sig diff --git a/tests/unit_test/iconscore/typing/test_type_hint.py b/tests/unit_test/iconscore/typing/test_type_hint.py deleted file mode 100644 index c35b3df51..000000000 --- a/tests/unit_test/iconscore/typing/test_type_hint.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- - -from typing import List, Optional, Dict, Union - -import pytest -from typing_extensions import TypedDict - -from iconservice.base.address import Address - - -class Person(TypedDict): - name: str - age: int - - -type_hints = ( - bool, bytes, int, str, Address, - list, List, List[int], List[Person], List["Person"], - Optional[str], Optional[List[str]], Optional[Dict[str, str]], - Dict, Dict[str, int], Optional[Dict], - Union[str], Union[str, int] -) - - -@pytest.mark.parametrize("type_hint", type_hints) -def test_normalize_type_hint(type_hint): - pass From 00f56295dc6611e6f0746be6e6d7617deb1668c5 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Sat, 27 Jun 2020 15:38:27 +0900 Subject: [PATCH 17/40] Minor update * Fix minor bugs in test_element.py * Optimize code --- iconservice/iconscore/typing/element.py | 8 +------- tests/unit_test/iconscore/typing/test_element.py | 5 +++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index beb0b340e..598411055 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -236,12 +236,6 @@ def freeze(self): def create_score_elements(cls) -> Mapping: elements = ScoreElementContainer() - flags = ( - ScoreFlag.READONLY | - ScoreFlag.EXTERNAL | - ScoreFlag.PAYABLE | - ScoreFlag.EVENTLOG - ) for name, func in getmembers(cls, predicate=isfunction): if name.startswith("__"): @@ -250,7 +244,7 @@ def create_score_elements(cls) -> Mapping: # Collect the only functions with one or more of the above 4 score flags flag = get_score_flag(func) - if utils.is_any_flag_on(flag, flags): + if utils.is_any_flag_on(flag, ScoreFlag.FUNC | ScoreFlag.EVENTLOG): verify_score_flag(flag) elements[name] = create_score_element(func) diff --git a/tests/unit_test/iconscore/typing/test_element.py b/tests/unit_test/iconscore/typing/test_element.py index 063d9706f..e09d072a3 100644 --- a/tests/unit_test/iconscore/typing/test_element.py +++ b/tests/unit_test/iconscore/typing/test_element.py @@ -36,11 +36,14 @@ class Person(TypedDict): (int, int), (str, str), (Address, Address), + ("Address", Address), (list, None), (List, None), (List[int], List[int]), (List[Person], List[Person]), (List["Person"], None), + (List["Address"], None), + (dict, None), (Dict, None), (Dict[str, int], Dict[str, int]), (Dict[str, Person], Dict[str, Person]), @@ -54,8 +57,6 @@ class Person(TypedDict): ] ) def test_normalize_abnormal_type_hint(type_hint, expected): - print(type_hint) - try: ret = normalize_type_hint(type_hint) except IllegalFormatException: From 699ae0bcd6100d0447347adb9916c1b3506c8738 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Mon, 29 Jun 2020 00:29:24 +0900 Subject: [PATCH 18/40] Apply new type converter * Some unitests remain failed * Revision handling is needed --- iconservice/iconscore/icon_score_engine.py | 37 +++++++++----- iconservice/iconscore/typing/conversion.py | 50 ++++++++++++++++--- iconservice/iconscore/typing/element.py | 6 +++ iconservice/iiss/engine.py | 6 +-- .../iiss/prevote/test_iiss_claim.py | 1 + .../iconscore/typing/test_convertion.py | 22 +++----- 6 files changed, 84 insertions(+), 38 deletions(-) diff --git a/iconservice/iconscore/icon_score_engine.py b/iconservice/iconscore/icon_score_engine.py index 0e92efa6b..35cbf65df 100644 --- a/iconservice/iconscore/icon_score_engine.py +++ b/iconservice/iconscore/icon_score_engine.py @@ -20,13 +20,18 @@ from typing import TYPE_CHECKING, Any from .icon_score_constant import STR_FALLBACK, ATTR_SCORE_GET_API, ATTR_SCORE_CALL, \ - ATTR_SCORE_VALIDATE_EXTERNAL_METHOD + ATTR_SCORE_VALIDATE_EXTERNAL_METHOD, CONST_CLASS_ELEMENTS from .icon_score_context import IconScoreContext from .icon_score_context_util import IconScoreContextUtil from ..base.address import Address, SYSTEM_SCORE_ADDRESS from ..base.exception import ScoreNotFoundException, InvalidParamsException from ..base.type_converter import TypeConverter from ..icon_constant import Revision +from .typing.conversion import convert_score_parameters, ConvertOption +from .typing.element import ( + ScoreElement, + get_score_element, +) if TYPE_CHECKING: from ..iconscore.icon_score_base import IconScoreBase @@ -90,8 +95,8 @@ def _validate_score_blacklist(context: 'IconScoreContext', icon_score_address: ' IconScoreContextUtil.validate_score_blacklist(context, icon_score_address) - @staticmethod - def _call(context: 'IconScoreContext', + @classmethod + def _call(cls, context: 'IconScoreContext', icon_score_address: 'Address', data: dict) -> Any: """Handle jsonrpc including both invoke and query @@ -103,9 +108,9 @@ def _call(context: 'IconScoreContext', func_name: str = data['method'] kw_params: dict = data.get('params', {}) - icon_score = IconScoreEngine._get_icon_score(context, icon_score_address) + icon_score = cls._get_icon_score(context, icon_score_address) - converted_params = IconScoreEngine._convert_score_params_by_annotations( + converted_params = cls._convert_score_params_by_annotations( context, icon_score, func_name, kw_params) context.set_func_type_by_icon_score(icon_score, func_name) context.current_address = icon_score_address @@ -121,20 +126,26 @@ def _convert_score_params_by_annotations(context: 'IconScoreContext', icon_score: 'IconScoreBase', func_name: str, kw_params: dict) -> dict: - tmp_params = deepcopy(kw_params) - validate_external_method = getattr(icon_score, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD) validate_external_method(func_name) - remove_invalid_params = False + # TODO: Implement type conversion considering TypedDict by goldworm + # remove_invalid_params = False + # if icon_score.address == SYSTEM_SCORE_ADDRESS and context.revision < Revision.SCORE_FUNC_PARAMS_CHECK.value: + # remove_invalid_params = True + # + # score_func = getattr(icon_score, func_name) + # tmp_params = deepcopy(kw_params) + # TypeConverter.adjust_params_to_method(score_func, tmp_params, remove_invalid_params) + + options = ConvertOption.NONE if icon_score.address == SYSTEM_SCORE_ADDRESS and context.revision < Revision.SCORE_FUNC_PARAMS_CHECK.value: - remove_invalid_params = True + options = ConvertOption.IGNORE_UNKNOWN_PARAMS - score_func = getattr(icon_score, func_name) - # TODO: Implement type conversion considering TypedDict by goldworm - TypeConverter.adjust_params_to_method(score_func, tmp_params, remove_invalid_params) + element: ScoreElement = get_score_element(icon_score, func_name) + params = convert_score_parameters(kw_params, element.signature, options) - return tmp_params + return params @staticmethod def _fallback(context: 'IconScoreContext', diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index d64b4a0b3..7fdb5e33f 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -13,12 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from inspect import Signature +from inspect import Signature, Parameter from typing import Optional, Dict, Union, Type, List, Any +from enum import Flag, auto from . import ( BaseObject, - BaseObjectType, is_base_type, is_struct, name_to_type, @@ -27,6 +27,7 @@ get_annotations, ) from ...base.address import Address +from ...base.exception import InvalidParamsException CommonObject = Union[bool, bytes, int, str, 'Address', Dict[str, BaseObject]] CommonType = Type[CommonObject] @@ -105,14 +106,47 @@ def is_hex(value: str) -> bool: return value.startswith("0x") or value.startswith("-0x") -def convert_score_parameters(params: Dict[str, Any], sig: Signature): - return { - name: str_to_object(param, sig.parameters[name].annotation) - for name, param in params.items() - } +class ConvertOption(Flag): + NONE = 0 + IGNORE_UNKNOWN_PARAMS = auto() + + +def convert_score_parameters( + params: Dict[str, Any], + sig: Signature, + options: ConvertOption = ConvertOption.NONE): + verify_arguments(params, sig) + + converted_params = {} + + for k, v in params.items(): + if not isinstance(k, str): + raise InvalidParamsException(f"Invalid key type: key={k}") + + try: + parameter: Parameter = sig.parameters[k] + converted_params[k] = str_to_object(v, parameter.annotation) + except KeyError: + if not (options & ConvertOption.IGNORE_UNKNOWN_PARAMS): + raise InvalidParamsException(f"Unknown param: key={k} value={v}") + + return converted_params + + +def verify_arguments(params: Dict[str, Any], sig: Signature): + for k in sig.parameters: + if k in ("self", "cls"): + continue + + parameter: Parameter = sig.parameters[k] + if parameter.default == Parameter.empty and k not in params: + raise InvalidParamsException(f"Parameter not found: {k}") def str_to_object(value: Union[str, list, dict], type_hint: type) -> Any: + if type(value) not in (str, list, dict): + raise InvalidParamsException(f"Invalid value type: {value}") + origin = get_origin(type_hint) if is_base_type(origin): @@ -130,3 +164,5 @@ def str_to_object(value: Union[str, list, dict], type_hint: type) -> Any: if origin is dict: type_hint = args[1] return {k: str_to_object(v, type_hint) for k, v in value.items()} + + raise InvalidParamsException(f"Failed to convert: value={value} type={type_hint}") diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 598411055..4ad0a2ae3 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -36,6 +36,7 @@ ScoreFlag, STR_FALLBACK, CONST_INDEXED_ARGS_COUNT, + CONST_CLASS_ELEMENTS, ) from ... import utils from ...base.exception import IllegalFormatException, InternalServiceErrorException @@ -282,3 +283,8 @@ def is_all_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: def is_any_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: return utils.is_any_flag_on(get_score_flag(obj), flag) + + +def get_score_element(score, func_name: str) -> ScoreElement: + elements = getattr(score, CONST_CLASS_ELEMENTS) + return elements[func_name] diff --git a/iconservice/iiss/engine.py b/iconservice/iiss/engine.py index bf9a22b22..3d5b0883f 100644 --- a/iconservice/iiss/engine.py +++ b/iconservice/iiss/engine.py @@ -31,8 +31,6 @@ InvalidParamsException, InvalidRequestException, OutOfBalanceException, FatalException, InternalServiceErrorException ) -from ..base.type_converter import TypeConverter -from ..base.type_converter_templates import ParamType from ..icon_constant import ISCORE_EXCHANGE_RATE, IISS_MAX_REWARD_RATE, \ IconScoreContextType, IISS_LOG_TAG, ROLLBACK_LOG_TAG, RCCalculateResult, INVALID_CLAIM_TX, Revision, \ RevisionChangedFlag @@ -461,12 +459,12 @@ def _convert_params_of_set_delegation(cls, cls._check_delegation_count(context, delegations) # TODO: Remove type conversion by goldworm - temp_delegations: list = TypeConverter.convert(delegations, ParamType.IISS_SET_DELEGATION) + # temp_delegations: list = TypeConverter.convert(delegations, ParamType.IISS_SET_DELEGATION) total_delegating: int = 0 converted_delegations: List[Tuple['Address', int]] = [] delegated_addresses = set() - for delegation in temp_delegations: + for delegation in delegations: address: 'Address' = delegation["address"] value: int = delegation["value"] assert isinstance(address, Address) diff --git a/tests/integrate_test/iiss/prevote/test_iiss_claim.py b/tests/integrate_test/iiss/prevote/test_iiss_claim.py index 31806ad85..27cb094c6 100644 --- a/tests/integrate_test/iiss/prevote/test_iiss_claim.py +++ b/tests/integrate_test/iiss/prevote/test_iiss_claim.py @@ -144,6 +144,7 @@ def _query_iscore_with_invalid_params(self): } # query iscore without an address + # with pytest.raises(InvalidParamsException): with pytest.raises(InvalidParamsException): self.icon_service_engine.query("icx_call", params) diff --git a/tests/unit_test/iconscore/typing/test_convertion.py b/tests/unit_test/iconscore/typing/test_convertion.py index 79dc8e803..300aec0c1 100644 --- a/tests/unit_test/iconscore/typing/test_convertion.py +++ b/tests/unit_test/iconscore/typing/test_convertion.py @@ -18,11 +18,10 @@ from typing import List, Dict import pytest - from typing_extensions import TypedDict -from iconservice.base.exception import IllegalFormatException from iconservice.base.address import Address, AddressPrefix +from iconservice.base.exception import InvalidParamsException from iconservice.iconscore.typing.conversion import ( convert_score_parameters, object_to_str, @@ -78,18 +77,13 @@ def func( assert params_in_object == params -def test_convert_score_parameters_without_no_type_hint(): - def func(_a, _b, _c, _d: List, _e: Dict): - pass +def test_convert_score_parameters_with_insufficient_parameters(): + class TestScore: + def func(self, address: Address): + pass - params = { - "_a": "0x1234", - "_b": "hello", - "_c": "0x1", - "_d": ["0", "1", "2"], - "_e": {"a": "a", "b": "b"}, - } + params = {} - with pytest.raises(IllegalFormatException): - function = Function(func) + with pytest.raises(InvalidParamsException): + function = Function(TestScore.func) convert_score_parameters(params, function.signature) From 10fe2570e09544ad77fbb6eed9f23a79a7297620 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Mon, 29 Jun 2020 18:31:57 +0900 Subject: [PATCH 19/40] Fix failures of unittest for params type conversion * New params type conversion caused some unittest failures. * One unittest still remains failed --- iconservice/iconscore/icon_score_engine.py | 6 ++- iconservice/iconscore/typing/conversion.py | 9 +++- iconservice/iiss/engine.py | 4 +- .../legacy_unittest/test_icon_score_engine.py | 16 +++---- tests/unit_test/iiss/test_iiss_engine.py | 42 ++++++------------- 5 files changed, 37 insertions(+), 40 deletions(-) diff --git a/iconservice/iconscore/icon_score_engine.py b/iconservice/iconscore/icon_score_engine.py index 35cbf65df..9d5965b09 100644 --- a/iconservice/iconscore/icon_score_engine.py +++ b/iconservice/iconscore/icon_score_engine.py @@ -139,7 +139,11 @@ def _convert_score_params_by_annotations(context: 'IconScoreContext', # TypeConverter.adjust_params_to_method(score_func, tmp_params, remove_invalid_params) options = ConvertOption.NONE - if icon_score.address == SYSTEM_SCORE_ADDRESS and context.revision < Revision.SCORE_FUNC_PARAMS_CHECK.value: + if ( + icon_score.address == SYSTEM_SCORE_ADDRESS + and func_name == "setPRep" + and context.revision < Revision.SCORE_FUNC_PARAMS_CHECK.value + ): options = ConvertOption.IGNORE_UNKNOWN_PARAMS element: ScoreElement = get_score_element(icon_score, func_name) diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index 7fdb5e33f..d3bd7ebb6 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -41,7 +41,7 @@ def base_object_to_str(value: Any) -> str: elif isinstance(value, bytes): return bytes_to_hex(value) elif isinstance(value, bool): - return "0x1" if value else "0x0" + return hex(value) elif isinstance(value, str): return value @@ -115,6 +115,13 @@ def convert_score_parameters( params: Dict[str, Any], sig: Signature, options: ConvertOption = ConvertOption.NONE): + """Convert string values in score parameters to object values + + :param params: + :param sig: + :param options: + :return: + """ verify_arguments(params, sig) converted_params = {} diff --git a/iconservice/iiss/engine.py b/iconservice/iiss/engine.py index 3d5b0883f..7aa2cc1a5 100644 --- a/iconservice/iiss/engine.py +++ b/iconservice/iiss/engine.py @@ -445,8 +445,8 @@ def _convert_params_of_set_delegation(cls, ) -> Tuple[int, List[Tuple['Address', int]]]: """Convert delegations format - [{"address": "hxe7af5fcfd8dfc67530a01a0e403882687528dfcb", "value", "0xde0b6b3a7640000"}, ...] -> - [(Address(hxe7af5fcfd8dfc67530a01a0e403882687528dfcb), 1000000000000000000), ...] + [{"address": Address(hxe7af5fcfd8dfc67530a01a0e403882687528dfcb), "value", 1234}, ...] -> + [(Address(hxe7af5fcfd8dfc67530a01a0e403882687528dfcb), 1234), ...] :param delegations: delegations of setDelegation JSON-RPC API request :return: total_delegating, (address, delegated) diff --git a/tests/legacy_unittest/test_icon_score_engine.py b/tests/legacy_unittest/test_icon_score_engine.py index d1d5c4455..fdfcdcfec 100644 --- a/tests/legacy_unittest/test_icon_score_engine.py +++ b/tests/legacy_unittest/test_icon_score_engine.py @@ -21,10 +21,10 @@ from unittest.mock import Mock, patch from iconservice import * -from iconservice.base.address import AddressPrefix, SYSTEM_SCORE_ADDRESS, Address +from iconservice.base.address import AddressPrefix, Address from iconservice.base.exception import ScoreNotFoundException, InvalidParamsException from iconservice.iconscore.icon_score_constant import ATTR_SCORE_GET_API, ATTR_SCORE_CALL, \ - ATTR_SCORE_VALIDATE_EXTERNAL_METHOD + ATTR_SCORE_VALIDATE_EXTERNAL_METHOD, from iconservice.iconscore.icon_score_context import IconScoreContext from iconservice.iconscore.icon_score_context import IconScoreContextType from iconservice.iconscore.icon_score_engine import IconScoreEngine @@ -193,6 +193,8 @@ def test_convert_score_params_by_annotations(self): def test_method(address: Address, integer: int): pass + func_name = test_method.__name__ + # success case: valid params and method primitive_params = {"address": str(create_address(AddressPrefix.EOA)), "integer": "0x10"} @@ -200,9 +202,9 @@ def test_method(address: Address, integer: int): score_object = Mock(spec=IconScoreBase) setattr(score_object, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD, Mock()) - setattr(score_object, 'test_method', test_method) - converted_params = \ - IconScoreEngine._convert_score_params_by_annotations(context, score_object, 'test_method', primitive_params) + setattr(score_object, func_name, test_method) + converted_params = IconScoreEngine._convert_score_params_by_annotations( + context, score_object, func_name, primitive_params) validate_external_method = getattr(score_object, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD) validate_external_method.assert_called() @@ -216,7 +218,7 @@ def test_method(address: Address, integer: int): self.assertRaises(InvalidParamsException, IconScoreEngine._convert_score_params_by_annotations, - context, score_object, 'test_method', not_matching_type_params) + context, score_object, func_name, not_matching_type_params) # not enough number of params inputted, validate_external_method = getattr(score_object, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD) @@ -225,7 +227,7 @@ def test_method(address: Address, integer: int): self.assertRaises(InvalidParamsException, IconScoreEngine._convert_score_params_by_annotations, - context, score_object, 'test_method', insufficient_params) + context, score_object, func_name, insufficient_params) def test_fallback(self): pass diff --git a/tests/unit_test/iiss/test_iiss_engine.py b/tests/unit_test/iiss/test_iiss_engine.py index 28db0eca2..e27c7e362 100644 --- a/tests/unit_test/iiss/test_iiss_engine.py +++ b/tests/unit_test/iiss/test_iiss_engine.py @@ -156,10 +156,7 @@ def create_delegations_param() -> Tuple[int, List]: address = Address.from_prefix_and_int(AddressPrefix.EOA, _id) value = _id - delegations.append({ - "address": str(address), - "value": hex(value) - }) + delegations.append({"address": address, "value": value}) total_delegating += value return total_delegating, delegations @@ -188,10 +185,7 @@ def test_convert_params_of_set_delegation_ok(self, context): address = Address.from_prefix_and_int(AddressPrefix.EOA, i + 1) value = random.randint(1, 10_000) - delegations.append({ - "address": str(address), - "value": hex(value) - }) + delegations.append({"address": address, "value": value}) total_delegating += value ret_total_delegating, ret_delegations = IISSEngine._convert_params_of_set_delegation(context, delegations) @@ -202,8 +196,8 @@ def test_convert_params_of_set_delegation_ok(self, context): address: 'Address' = item[0] value: int = item[1] - assert str(address) == delegations[i]["address"] - assert hex(value) == delegations[i]["value"] + assert address == delegations[i]["address"] + assert value == delegations[i]["value"] def test_convert_params_of_set_delegation_with_value_0(self, context): max_delegations: int = self._get_expected_max_delegations(context) @@ -215,10 +209,7 @@ def test_convert_params_of_set_delegation_with_value_0(self, context): address = Address.from_prefix_and_int(AddressPrefix.EOA, i + 1) value = 0 if i < zero_value_cnt else i + 1 - delegations.append({ - "address": str(address), - "value": hex(value) - }) + delegations.append({"address": address, "value": value}) total_delegating += value ret_total_delegating, ret_delegations = IISSEngine._convert_params_of_set_delegation(context, delegations) @@ -229,8 +220,8 @@ def test_convert_params_of_set_delegation_with_value_0(self, context): # 5 delegations including 0 value were dropped. for address, value in ret_delegations: delegation: Dict[str, Optional[str, int]] = delegations[i] - assert str(address) == delegation["address"] - assert hex(value) == delegation["value"] + assert address == delegation["address"] + assert value == delegation["value"] i += 1 def test_convert_params_of_set_delegation_with_value_less_than_0(self, context): @@ -242,10 +233,7 @@ def test_convert_params_of_set_delegation_with_value_less_than_0(self, context): address = Address.from_prefix_and_int(AddressPrefix.EOA, i + 1) value = values[i] - delegations.append({ - "address": str(address), - "value": hex(value) - }) + delegations.append({"address": address, "value": value}) with pytest.raises(InvalidParamsException): IISSEngine._convert_params_of_set_delegation(context, delegations) @@ -257,10 +245,7 @@ def test_convert_params_of_set_delegation_with_duplicate_address(self, context): address = Address.from_prefix_and_int(AddressPrefix.EOA, 1) value = random.randint(1, 100) - delegations.append({ - "address": str(address), - "value": hex(value) - }) + delegations.append({"address": address, "value": value}) with pytest.raises(InvalidParamsException): IISSEngine._convert_params_of_set_delegation(context, delegations) @@ -489,10 +474,9 @@ def test_handle_set_delegation_with_1_account(self): context.msg = Message(SENDER_ADDRESS, 0) context.storage.icx.get_account = Mock(side_effect=get_account) - params = {} new_delegations = [{ - "address": str(Address.from_prefix_and_int(AddressPrefix.EOA, 1)), - "value": hex(100) + "address": Address.from_prefix_and_int(AddressPrefix.EOA, 1), + "value": 100 }] class IISSEngineListenerImpl(IISSEngineListener): @@ -552,8 +536,8 @@ def test_internal_handle_set_delegation(self): address: 'Address' = item[0] value: int = item[1] - assert str(address) == delegations[i]["address"] - assert hex(value) == delegations[i]["value"] + assert address, value == delegations[i]["address"] + assert value == delegations[i]["value"] # IISSEngine._check_voting_power_is_enough() cached_accounts: Dict['Address', Tuple['Account', int]] = {} From 1b336237ad70e898ae42e4174e28b7a4c6660515 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Mon, 29 Jun 2020 21:08:05 +0900 Subject: [PATCH 20/40] Fix unittest failures * Fix unittest failures caused by IconScoreEngine._convert_score_params_by_annotations() --- iconservice/iconscore/icon_score_engine.py | 25 +++++-------------- iconservice/iconscore/typing/element.py | 14 ++++++++--- .../legacy_unittest/test_icon_score_engine.py | 13 ++++------ 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/iconservice/iconscore/icon_score_engine.py b/iconservice/iconscore/icon_score_engine.py index 9d5965b09..a57360706 100644 --- a/iconservice/iconscore/icon_score_engine.py +++ b/iconservice/iconscore/icon_score_engine.py @@ -19,19 +19,17 @@ from copy import deepcopy from typing import TYPE_CHECKING, Any -from .icon_score_constant import STR_FALLBACK, ATTR_SCORE_GET_API, ATTR_SCORE_CALL, \ - ATTR_SCORE_VALIDATE_EXTERNAL_METHOD, CONST_CLASS_ELEMENTS +from .icon_score_constant import STR_FALLBACK, ATTR_SCORE_GET_API, ATTR_SCORE_CALL from .icon_score_context import IconScoreContext from .icon_score_context_util import IconScoreContextUtil -from ..base.address import Address, SYSTEM_SCORE_ADDRESS -from ..base.exception import ScoreNotFoundException, InvalidParamsException -from ..base.type_converter import TypeConverter -from ..icon_constant import Revision from .typing.conversion import convert_score_parameters, ConvertOption from .typing.element import ( ScoreElement, get_score_element, ) +from ..base.address import Address, SYSTEM_SCORE_ADDRESS +from ..base.exception import ScoreNotFoundException, InvalidParamsException +from ..icon_constant import Revision if TYPE_CHECKING: from ..iconscore.icon_score_base import IconScoreBase @@ -53,8 +51,9 @@ def invoke(context: 'IconScoreContext', :param data_type: :param data: calldata """ + IconScoreEngine._validate_score_blacklist(context, icon_score_address) + if data_type == 'call': - IconScoreEngine._validate_score_blacklist(context, icon_score_address) IconScoreEngine._call(context, icon_score_address, data) else: IconScoreEngine._fallback(context, icon_score_address) @@ -126,18 +125,6 @@ def _convert_score_params_by_annotations(context: 'IconScoreContext', icon_score: 'IconScoreBase', func_name: str, kw_params: dict) -> dict: - validate_external_method = getattr(icon_score, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD) - validate_external_method(func_name) - - # TODO: Implement type conversion considering TypedDict by goldworm - # remove_invalid_params = False - # if icon_score.address == SYSTEM_SCORE_ADDRESS and context.revision < Revision.SCORE_FUNC_PARAMS_CHECK.value: - # remove_invalid_params = True - # - # score_func = getattr(icon_score, func_name) - # tmp_params = deepcopy(kw_params) - # TypeConverter.adjust_params_to_method(score_func, tmp_params, remove_invalid_params) - options = ConvertOption.NONE if ( icon_score.address == SYSTEM_SCORE_ADDRESS diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 4ad0a2ae3..a50eda592 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -39,7 +39,11 @@ CONST_CLASS_ELEMENTS, ) from ... import utils -from ...base.exception import IllegalFormatException, InternalServiceErrorException +from ...base.exception import ( + IllegalFormatException, + InternalServiceErrorException, + MethodNotFoundException +) def normalize_signature(sig: Signature) -> Signature: @@ -286,5 +290,9 @@ def is_any_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: def get_score_element(score, func_name: str) -> ScoreElement: - elements = getattr(score, CONST_CLASS_ELEMENTS) - return elements[func_name] + try: + elements = getattr(score, CONST_CLASS_ELEMENTS) + return elements[func_name] + except KeyError: + raise MethodNotFoundException( + f"Method not found: {type(score).__name__}.{func_name}") diff --git a/tests/legacy_unittest/test_icon_score_engine.py b/tests/legacy_unittest/test_icon_score_engine.py index fdfcdcfec..eb7e7a016 100644 --- a/tests/legacy_unittest/test_icon_score_engine.py +++ b/tests/legacy_unittest/test_icon_score_engine.py @@ -23,12 +23,12 @@ from iconservice import * from iconservice.base.address import AddressPrefix, Address from iconservice.base.exception import ScoreNotFoundException, InvalidParamsException -from iconservice.iconscore.icon_score_constant import ATTR_SCORE_GET_API, ATTR_SCORE_CALL, \ - ATTR_SCORE_VALIDATE_EXTERNAL_METHOD, +from iconservice.iconscore.icon_score_constant import ATTR_SCORE_GET_API, ATTR_SCORE_CALL, CONST_CLASS_ELEMENTS from iconservice.iconscore.icon_score_context import IconScoreContext from iconservice.iconscore.icon_score_context import IconScoreContextType from iconservice.iconscore.icon_score_engine import IconScoreEngine from iconservice.iconscore.icon_score_mapper import IconScoreMapper +from iconservice.iconscore.typing.element import Function from tests import create_address @@ -199,15 +199,14 @@ def test_method(address: Address, integer: int): primitive_params = {"address": str(create_address(AddressPrefix.EOA)), "integer": "0x10"} context = Mock(spec=IconScoreContext) - score_object = Mock(spec=IconScoreBase) + score_object = Mock(spec=[func_name, "__elements", "address"]) - setattr(score_object, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD, Mock()) setattr(score_object, func_name, test_method) + setattr(score_object, CONST_CLASS_ELEMENTS, {func_name: Function(test_method)}) + converted_params = IconScoreEngine._convert_score_params_by_annotations( context, score_object, func_name, primitive_params) - validate_external_method = getattr(score_object, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD) - validate_external_method.assert_called() # primitive_params must not be changed. self.assertEqual(type(primitive_params["address"]), str) self.assertEqual(type(converted_params["address"]), Address) @@ -221,8 +220,6 @@ def test_method(address: Address, integer: int): context, score_object, func_name, not_matching_type_params) # not enough number of params inputted, - validate_external_method = getattr(score_object, ATTR_SCORE_VALIDATE_EXTERNAL_METHOD) - validate_external_method.reset_mock() insufficient_params = {"address": str(create_address(AddressPrefix.EOA))} self.assertRaises(InvalidParamsException, From 2cb9fa33150a68b662bf2f1a8f13cd52c2aa9d9f Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Tue, 30 Jun 2020 02:06:50 +0900 Subject: [PATCH 21/40] Remove some comment-out codes --- iconservice/iconscore/icon_score_base.py | 27 ---------------------- iconservice/iconscore/typing/conversion.py | 7 +++--- iconservice/iconscore/typing/element.py | 16 ++++++------- iconservice/iiss/engine.py | 2 -- 4 files changed, 12 insertions(+), 40 deletions(-) diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index bb4662f50..c6a13b27d 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -340,33 +340,6 @@ def __new__(mcs, name, bases, namespace, **kwargs): elements: Mapping[str, ScoreElement] = create_score_elements(cls) setattr(cls, CONST_CLASS_ELEMENTS, elements) - # custom_funcs = [value for key, value in getmembers(cls, predicate=isfunction) - # if not key.startswith('__')] - # - # external_funcs = { - # func.__name__: signature(func) for func in custom_funcs - # if is_any_score_flag_on(func, ScoreFlag.EXTERNAL) - # } - # - # payable_funcs = [ - # func for func in custom_funcs - # if is_any_score_flag_on(func, ScoreFlag.PAYABLE) - # ] - # - # readonly_payables = [ - # func for func in payable_funcs - # if is_any_score_flag_on(func, ScoreFlag.READONLY) - # ] - # - # if bool(readonly_payables): - # raise IllegalFormatException(f"Payable method cannot be readonly") - # - # if external_funcs: - # setattr(cls, CONST_CLASS_EXTERNALS, external_funcs) - # if payable_funcs: - # payable_funcs = {func.__name__: signature(func) for func in payable_funcs} - # setattr(cls, CONST_CLASS_PAYABLES, payable_funcs) - # TODO: Replace it with a new list supporting struct and list # api_list = ScoreApiGenerator.generate(custom_funcs) api_list = get_score_api(elements.values()) diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index d3bd7ebb6..1f5eee00e 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -13,9 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +from enum import Flag, auto from inspect import Signature, Parameter from typing import Optional, Dict, Union, Type, List, Any -from enum import Flag, auto from . import ( BaseObject, @@ -85,7 +85,7 @@ def str_to_base_object(value: str, type_hint: type) -> BaseObject: if type_hint is Address: return Address.from_string(value) - raise TypeError(f"Unknown type: {type_hint}") + raise InvalidParamsException(f"Unknown type: {type_hint}") def bytes_to_hex(value: bytes, prefix: str = "0x") -> str: @@ -141,12 +141,13 @@ def convert_score_parameters( def verify_arguments(params: Dict[str, Any], sig: Signature): + for k in sig.parameters: if k in ("self", "cls"): continue parameter: Parameter = sig.parameters[k] - if parameter.default == Parameter.empty and k not in params: + if k not in params and parameter.default == Parameter.empty: raise InvalidParamsException(f"Parameter not found: {k}") diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index a50eda592..50dc06cad 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -124,21 +124,21 @@ def verify_score_flag(flag: ScoreFlag): class ScoreElement(object): - def __init__(self, element: callable): - self._element = element - self._signature: Signature = normalize_signature(signature(element)) + def __init__(self, origin: callable): + self._origin = origin + self._signature: Signature = normalize_signature(signature(origin)) @property - def element(self) -> callable: - return self._element + def origin(self) -> callable: + return self._origin @property def name(self) -> str: - return self._element.__name__ + return self._origin.__name__ @property def flag(self) -> ScoreFlag: - return get_score_flag(self._element) + return get_score_flag(self._origin) @property def signature(self) -> Signature: @@ -178,7 +178,7 @@ def __init__(self, eventlog: callable): @property def indexed_args_count(self) -> int: - return getattr(self.element, CONST_INDEXED_ARGS_COUNT, 0) + return getattr(self.origin, CONST_INDEXED_ARGS_COUNT, 0) class ScoreElementContainer(MutableMapping): diff --git a/iconservice/iiss/engine.py b/iconservice/iiss/engine.py index 7aa2cc1a5..7174f3308 100644 --- a/iconservice/iiss/engine.py +++ b/iconservice/iiss/engine.py @@ -458,8 +458,6 @@ def _convert_params_of_set_delegation(cls, cls._check_delegation_count(context, delegations) - # TODO: Remove type conversion by goldworm - # temp_delegations: list = TypeConverter.convert(delegations, ParamType.IISS_SET_DELEGATION) total_delegating: int = 0 converted_delegations: List[Tuple['Address', int]] = [] delegated_addresses = set() From 93244f04eed73732be5ee2465b05d5be934768be Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Tue, 30 Jun 2020 15:00:38 +0900 Subject: [PATCH 22/40] Update verify_score_flag() * Add ScoreFlag.FALLBACK * Add unittest --- iconservice/iconscore/icon_score_base.py | 8 ++++- iconservice/iconscore/icon_score_constant.py | 4 ++- iconservice/iconscore/typing/element.py | 30 ++++++++----------- .../iconscore/typing/test_element.py | 25 ++++++++++++++++ 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index c6a13b27d..e1fec81d5 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -300,7 +300,13 @@ def payable(func): if is_any_score_flag_on(func, ScoreFlag.PAYABLE): raise InvalidPayableException(FORMAT_DECORATOR_DUPLICATED.format('payable', func_name, cls_name)) - set_score_flag_on(func, ScoreFlag.PAYABLE) + flag = ScoreFlag.PAYABLE + if func_name == STR_FALLBACK: + # If a function has payable decorator and its name is "fallback", + # then it is a fallback function + flag |= ScoreFlag.FALLBACK + + set_score_flag_on(func, flag) @wraps(func) def __wrapper(calling_obj: Any, *args, **kwargs): diff --git a/iconservice/iconscore/icon_score_constant.py b/iconservice/iconscore/icon_score_constant.py index 409a22be4..75a3fe00d 100644 --- a/iconservice/iconscore/icon_score_constant.py +++ b/iconservice/iconscore/icon_score_constant.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from enum import Flag +from enum import Flag, unique from typing import TypeVar from ..base.address import Address @@ -42,6 +42,7 @@ ATTR_SCORE_VALIDATE_EXTERNAL_METHOD = "_IconScoreBase__validate_external_method" +@unique class ScoreFlag(Flag): NONE = 0 @@ -49,6 +50,7 @@ class ScoreFlag(Flag): READONLY = 0x01 EXTERNAL = 0x02 PAYABLE = 0x04 + FALLBACK = 0x08 FUNC = 0xFF # Used for eventlog declaration in score diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 50dc06cad..283a4bf96 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -34,7 +34,6 @@ from ..icon_score_constant import ( CONST_SCORE_FLAG, ScoreFlag, - STR_FALLBACK, CONST_INDEXED_ARGS_COUNT, CONST_CLASS_ELEMENTS, ) @@ -104,23 +103,19 @@ def normalize_type_hint(type_hint) -> type: def verify_score_flag(flag: ScoreFlag): """Check if score flag combination is valid - If the combination is not valid, raise an exception + If the flag combination is not valid, raise an exception """ - if flag & ScoreFlag.READONLY: - # READONLY cannot be combined with PAYABLE - if flag & ScoreFlag.PAYABLE: - raise IllegalFormatException(f"Payable method cannot be readonly") - # READONLY cannot be set alone without EXTERNAL - elif not (flag & ScoreFlag.EXTERNAL): - raise IllegalFormatException(f"Invalid score flag: {flag}") + valid = { + ScoreFlag.EXTERNAL, + ScoreFlag.EXTERNAL | ScoreFlag.PAYABLE, + ScoreFlag.EXTERNAL | ScoreFlag.READONLY, + ScoreFlag.FALLBACK | ScoreFlag.PAYABLE, + ScoreFlag.EVENTLOG, + ScoreFlag.INTERFACE, + } - # EVENTLOG cannot be combined with other flags - if flag & ScoreFlag.EVENTLOG and flag != ScoreFlag.EVENTLOG: - raise IllegalFormatException(f"Invalid score flag: {flag}") - - # INTERFACE cannot be combined with other flags - if flag & ScoreFlag.INTERFACE and flag != ScoreFlag.INTERFACE: - raise IllegalFormatException(f"Invalid score flag: {flag}") + if flag not in valid: + raise IllegalFormatException(f"Invalid score decorator: {flag}") class ScoreElement(object): @@ -166,7 +161,8 @@ def is_readonly(self) -> bool: @property def is_fallback(self) -> bool: - return self.name == STR_FALLBACK and self.is_payable + return utils.is_all_flag_on( + self.flag, ScoreFlag.FALLBACK | ScoreFlag.PAYABLE) class EventLog(ScoreElement): diff --git a/tests/unit_test/iconscore/typing/test_element.py b/tests/unit_test/iconscore/typing/test_element.py index e09d072a3..ba43b18ba 100644 --- a/tests/unit_test/iconscore/typing/test_element.py +++ b/tests/unit_test/iconscore/typing/test_element.py @@ -21,6 +21,8 @@ from iconservice.base.address import Address from iconservice.base.exception import IllegalFormatException from iconservice.iconscore.typing.element import normalize_type_hint +from iconservice.iconscore.icon_score_constant import ScoreFlag +from iconservice.iconscore.typing.element import verify_score_flag class Person(TypedDict): @@ -65,3 +67,26 @@ def test_normalize_abnormal_type_hint(type_hint, expected): ret = None assert ret == expected + + +@pytest.mark.parametrize( + "flag,success", + [ + (ScoreFlag.READONLY, False), + (ScoreFlag.PAYABLE, False), + (ScoreFlag.FALLBACK, False), + (ScoreFlag.READONLY | ScoreFlag.PAYABLE, False), + (ScoreFlag.READONLY | ScoreFlag.FALLBACK, False), + (ScoreFlag.EXTERNAL | ScoreFlag.FALLBACK, False), + (ScoreFlag.EXTERNAL | ScoreFlag.EVENTLOG, False), + (ScoreFlag.EXTERNAL | ScoreFlag.INTERFACE, False), + (ScoreFlag.EVENTLOG | ScoreFlag.READONLY, False), + (ScoreFlag.INTERFACE | ScoreFlag.PAYABLE, False) + ] +) +def test_verify_score_flag(flag, success): + if success: + verify_score_flag(flag) + else: + with pytest.raises(IllegalFormatException): + verify_score_flag(flag) From 83d0b4d505305c2cd53ba453cc812397bb3c3bd3 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Tue, 30 Jun 2020 21:12:44 +0900 Subject: [PATCH 23/40] Allow Optional type hint * Applying new type conversion to on_install and on_update is under development * verify_internal_call_arguments is under development --- iconservice/deploy/engine.py | 6 +++ iconservice/iconscore/internal_call.py | 4 ++ iconservice/iconscore/typing/conversion.py | 4 +- iconservice/iconscore/typing/element.py | 34 +++++++++++---- .../sample_score.py | 2 +- .../iconscore/typing/test_element.py | 42 +++++++++++++++---- 6 files changed, 74 insertions(+), 18 deletions(-) diff --git a/iconservice/deploy/engine.py b/iconservice/deploy/engine.py index 80fff5963..baad3062f 100644 --- a/iconservice/deploy/engine.py +++ b/iconservice/deploy/engine.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING from iconcommons import Logger + from .icon_score_deployer import IconScoreDeployer from .utils import remove_path, get_score_path from ..base.ComponentBase import EngineBase @@ -317,4 +318,9 @@ def _initialize_score(deploy_type: DeployType, score: 'IconScoreBase', params: d raise InvalidParamsException(f'Invalid deployType: {deploy_type}') TypeConverter.adjust_params_to_method(on_init, params) + + # TODO: Replace TypeConverter with convert_score_parameters by goldworm + # sig = normalize_signature(inspect.signature(on_init)) + # converted_params = convert_score_parameters(params, sig) + on_init(**params) diff --git a/iconservice/iconscore/internal_call.py b/iconservice/iconscore/internal_call.py index f866222d0..61fcd1a69 100644 --- a/iconservice/iconscore/internal_call.py +++ b/iconservice/iconscore/internal_call.py @@ -146,6 +146,10 @@ def _other_score_call(context: 'IconScoreContext', icon_score = IconScoreContextUtil.get_icon_score(context, addr_to) context.set_func_type_by_icon_score(icon_score, func_name) score_func = getattr(icon_score, ATTR_SCORE_CALL) + + # TODO: verify internal call arguments by goldworm + # verify_internal_call_arguments(icon_score, func_name, arg_params, kw_params) + return score_func(func_name=func_name, arg_params=arg_params, kw_params=kw_params) finally: context.func_type = prev_func_type diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index 1f5eee00e..27a546882 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -122,7 +122,7 @@ def convert_score_parameters( :param options: :return: """ - verify_arguments(params, sig) + _verify_arguments(params, sig) converted_params = {} @@ -140,7 +140,7 @@ def convert_score_parameters( return converted_params -def verify_arguments(params: Dict[str, Any], sig: Signature): +def _verify_arguments(params: Dict[str, Any], sig: Signature): for k in sig.parameters: if k in ("self", "cls"): diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 283a4bf96..84068ea6a 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect from collections import OrderedDict from collections.abc import MutableMapping from inspect import ( @@ -22,7 +23,7 @@ Signature, Parameter, ) -from typing import Union, Mapping, List, Dict +from typing import Union, Mapping, List, Dict, Optional, Tuple from . import ( is_base_type, @@ -41,7 +42,8 @@ from ...base.exception import ( IllegalFormatException, InternalServiceErrorException, - MethodNotFoundException + MethodNotFoundException, + InvalidParamsException, ) @@ -91,11 +93,17 @@ def normalize_type_hint(type_hint) -> type: args = get_args(type_hint) size = len(args) - if origin is list and size == 1: - return List[normalize_type_hint(args[0])] - - if origin is dict and size == 2 and args[0] is str: - return Dict[str, normalize_type_hint(args[1])] + if origin is list: + if size == 1: + return List[normalize_type_hint(args[0])] + elif origin is dict: + if size == 2 and args[0] is str: + return Dict[str, normalize_type_hint(args[1])] + elif origin is Union: + if size == 2 and type(None) in args: + arg = args[0] if args[1] is type(None) else args[1] + if arg is not None and arg: + return Union[normalize_type_hint(arg), None] raise IllegalFormatException(f"Unsupported type hint: {type_hint}") @@ -292,3 +300,15 @@ def get_score_element(score, func_name: str) -> ScoreElement: except KeyError: raise MethodNotFoundException( f"Method not found: {type(score).__name__}.{func_name}") + + +def verify_internal_call_arguments(score, func_name: str, args: Optional[Tuple], kwargs: Optional[Dict]): + element = get_score_element(score, func_name) + sig = element.signature + sig = inspect.signature(getattr(score, func_name)) + + try: + arguments = sig.bind(*args, **kwargs) + except TypeError: + raise InvalidParamsException( + f"Invalid internal call params: address={score.address} func={func_name}") diff --git a/tests/integrate_test/samples/sample_deploy_scores/install/sample_legacy_kwargs_params/sample_score.py b/tests/integrate_test/samples/sample_deploy_scores/install/sample_legacy_kwargs_params/sample_score.py index c7aa8b4d6..abc3c8e40 100644 --- a/tests/integrate_test/samples/sample_deploy_scores/install/sample_legacy_kwargs_params/sample_score.py +++ b/tests/integrate_test/samples/sample_deploy_scores/install/sample_legacy_kwargs_params/sample_score.py @@ -11,7 +11,7 @@ def __init__(self, db: IconScoreDatabase) -> None: super().__init__(db) self._value = VarDB('value', db, value_type=int) - def on_install(self, value: int=1000, **kwargs) -> None: + def on_install(self, value: int = 1000, **kwargs) -> None: super().on_install() self._value.set(value) diff --git a/tests/unit_test/iconscore/typing/test_element.py b/tests/unit_test/iconscore/typing/test_element.py index ba43b18ba..477c01280 100644 --- a/tests/unit_test/iconscore/typing/test_element.py +++ b/tests/unit_test/iconscore/typing/test_element.py @@ -13,15 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Dict, Union, Optional +from typing import List, Dict, Union, Optional, ForwardRef import pytest from typing_extensions import TypedDict from iconservice.base.address import Address from iconservice.base.exception import IllegalFormatException -from iconservice.iconscore.typing.element import normalize_type_hint from iconservice.iconscore.icon_score_constant import ScoreFlag +from iconservice.iconscore.typing.element import normalize_type_hint from iconservice.iconscore.typing.element import verify_score_flag @@ -38,24 +38,52 @@ class Person(TypedDict): (int, int), (str, str), (Address, Address), - ("Address", Address), (list, None), (List, None), + (List[bool], List[bool]), + (List[bytes], List[bytes]), (List[int], List[int]), + (List[str], List[str]), + (List[Address], List[Address]), (List[Person], List[Person]), (List["Person"], None), (List["Address"], None), (dict, None), (Dict, None), + (Dict[str, bool], Dict[str, bool]), + (Dict[str, bytes], Dict[str, bytes]), (Dict[str, int], Dict[str, int]), + (Dict[str, str], Dict[str, str]), + (Dict[str, Address], Dict[str, Address]), (Dict[str, Person], Dict[str, Person]), (Dict[int, str], None), - (Optional[str], None), - (Optional[List[str]], None), - (Optional[Dict[str, str]], None), + (Dict[str, "Address"], None), + (Optional[bool], Union[bool, None]), + (Optional[bytes], Union[bytes, None]), + (Optional[int], Union[int, None]), + (Optional[str], Union[str, None]), + (Optional[Address], Union[Address, None]), + (Optional[List[str]], Union[List[str], None]), + (Optional[Dict[str, str]], Union[Dict[str, str], None]), (Optional[Dict], None), (Union[str], str), (Union[str, int], None), + (Union[bool, None], Union[bool, None]), + (Union[bytes, None], Union[bytes, None]), + (Union[int, None], Union[int, None]), + (Union[str, None], Union[str, None]), + (Union[None, str], Union[str, None]), + (Union[Address, None], Union[Address, None]), + (Union[Person, None], Union[Person, None]), + (Union["Person", None], None), + (ForwardRef("bool"), None), + (ForwardRef("bytes"), None), + (ForwardRef("int"), None), + (ForwardRef("str"), None), + (ForwardRef("Address"), None), + (Optional[ForwardRef("Address")], None), + (Dict[str, ForwardRef("Address")], None), + (Union[ForwardRef("Person"), None], None), ] ) def test_normalize_abnormal_type_hint(type_hint, expected): @@ -63,8 +91,6 @@ def test_normalize_abnormal_type_hint(type_hint, expected): ret = normalize_type_hint(type_hint) except IllegalFormatException: ret = None - except TypeError: - ret = None assert ret == expected From 068b9ce50b2c0a24b55c5c4e50f68c7191f268c6 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Wed, 1 Jul 2020 20:22:15 +0900 Subject: [PATCH 24/40] Verify fallback() signature * Bugfix normalize_signature() * Add normalize_return_annotation() --- iconservice/iconscore/typing/element.py | 70 ++++++++++++++++++++++--- iconservice/utils/__init__.py | 2 + 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 84068ea6a..c0a83e7b8 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -47,20 +47,49 @@ ) -def normalize_signature(sig: Signature) -> Signature: +def normalize_signature(func: callable) -> Signature: + """Normalize signature of score methods + + 1. Normalize type hint: ex) no type hint -> str + 2. Remove "self" parameter + + :param func: function attribute from class + :return: + """ + sig = inspect.signature(func) params = sig.parameters new_params = [] normalized = False - for k in params: - new_param = normalize_parameter(params[k]) - new_params.append(new_param) - if params[k] != new_params: + # CAUTION: + # def A: + # def func(self): + # pass + # + # inspect.isfunction(A.func) == True + # inspect.isfunction(A().func) == False + # inspect.ismethod(A.func) == False + # inspect.isfunction(A().func) == True + is_regular_method: bool = inspect.isfunction(func) + + for i, k in enumerate(params): + # Remove "self" parameter from signature of regular method + if i == 0 and k == "self" and is_regular_method: + new_param = None + else: + new_param = normalize_parameter(params[k]) + new_params.append(new_param) + + if new_param is not params[k]: normalized = True + return_annotation = normalize_return_annotation(sig.return_annotation) + if return_annotation is not sig.return_annotation: + normalized = True + if normalized: - sig = sig.replace(parameters=new_params) + sig = sig.replace(parameters=new_params, return_annotation=return_annotation) return sig @@ -80,6 +109,13 @@ def normalize_parameter(param: Parameter) -> Parameter: return param.replace(annotation=type_hint) +def normalize_return_annotation(return_annotation: type) -> Union[type, Signature.empty]: + if return_annotation in (None, Signature.empty): + return Signature.empty + + return return_annotation + + def normalize_type_hint(type_hint) -> type: # If type hint is str, convert it to type hint if isinstance(type_hint, str): @@ -129,7 +165,7 @@ def verify_score_flag(flag: ScoreFlag): class ScoreElement(object): def __init__(self, origin: callable): self._origin = origin - self._signature: Signature = normalize_signature(signature(origin)) + self._signature: Signature = normalize_signature(origin) @property def origin(self) -> callable: @@ -154,6 +190,7 @@ class Function(ScoreElement): """ def __init__(self, func: callable): super().__init__(func) + self._verify() @property def is_external(self) -> bool: @@ -172,6 +209,23 @@ def is_fallback(self) -> bool: return utils.is_all_flag_on( self.flag, ScoreFlag.FALLBACK | ScoreFlag.PAYABLE) + def _verify(self): + if self.is_fallback: + self._verify_fallback_signature() + + def _verify_fallback_signature(self): + """Verify if the signature of fallback() is valid + + fallback function must have no parameters + """ + sig = self.signature + + if not ( + len(sig.parameters) == 0 + and sig.return_annotation in (None, Signature.empty) + ): + raise IllegalFormatException("Invalid fallback signature") + class EventLog(ScoreElement): """Represents an eventlog declared in a SCORE @@ -243,7 +297,7 @@ def freeze(self): self._readonly = True -def create_score_elements(cls) -> Mapping: +def create_score_elements(cls: type) -> Mapping: elements = ScoreElementContainer() for name, func in getmembers(cls, predicate=isfunction): diff --git a/iconservice/utils/__init__.py b/iconservice/utils/__init__.py index ba8e2f021..2849fb07d 100644 --- a/iconservice/utils/__init__.py +++ b/iconservice/utils/__init__.py @@ -25,8 +25,10 @@ from collections import namedtuple from enum import Flag from typing import Any, Union, Optional +import inspect from iconcommons import Logger + from ..icon_constant import BUILTIN_SCORE_ADDRESS_MAPPER, DATA_BYTE_ORDER, ICX_IN_LOOP From 381bdd0f12523bd8ece91071bba281b7c23b5607 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Wed, 1 Jul 2020 21:10:36 +0900 Subject: [PATCH 25/40] Rename ScoreElement to ScoreElementMetadata * All subclasses and functions associated with ScoreElement are also renamed --- iconservice/iconscore/icon_score_base.py | 38 ++++++------- iconservice/iconscore/icon_score_constant.py | 2 +- iconservice/iconscore/icon_score_engine.py | 4 +- iconservice/iconscore/typing/definition.py | 12 ++-- iconservice/iconscore/typing/element.py | 56 +++++++++---------- .../legacy_unittest/test_icon_score_engine.py | 6 +- .../iconscore/typing/test_convertion.py | 4 +- 7 files changed, 61 insertions(+), 61 deletions(-) diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index e1fec81d5..1b7edc4d8 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -30,7 +30,7 @@ FORMAT_IS_NOT_DERIVED_OF_OBJECT, STR_FALLBACK, CONST_CLASS_API, - CONST_CLASS_ELEMENTS, + CONST_CLASS_ELEMENT_METADATAS, BaseType, T, ) @@ -41,13 +41,13 @@ from .internal_call import InternalCall from .typing.definition import get_score_api from .typing.element import ( - ScoreElementContainer, - ScoreElement, - Function, + ScoreElementMetadataContainer, + ScoreElementMetadata, + FunctionMetadata, set_score_flag_on, is_any_score_flag_on, ) -from .typing.element import create_score_elements +from .typing.element import create_score_element_metadatas from ..base.address import Address from ..base.address import GOVERNANCE_SCORE_ADDRESS from ..base.exception import * @@ -343,8 +343,8 @@ def __new__(mcs, name, bases, namespace, **kwargs): raise InvalidParamsException('namespace is not dict!') # TODO: Normalize type hints of score parameters by goldworm - elements: Mapping[str, ScoreElement] = create_score_elements(cls) - setattr(cls, CONST_CLASS_ELEMENTS, elements) + elements: Mapping[str, ScoreElementMetadata] = create_score_element_metadatas(cls) + setattr(cls, CONST_CLASS_ELEMENT_METADATAS, elements) # TODO: Replace it with a new list supporting struct and list # api_list = ScoreApiGenerator.generate(custom_funcs) @@ -389,7 +389,7 @@ def __init__(self, db: 'IconScoreDatabase') -> None: self.__owner = IconScoreContextUtil.get_owner(self._context, self.__address) self.__icx = None - elements: ScoreElementContainer = self.__get_score_elements() + elements: ScoreElementMetadataContainer = self.__get_score_element_metadatas() if elements.externals == 0: raise InvalidExternalException('There is no external method in the SCORE') @@ -420,8 +420,8 @@ def __validate_external_method(self, func_name: str) -> None: f"Method not found: {type(self).__name__}.{func_name}") @classmethod - def __get_score_elements(cls) -> ScoreElementContainer: - return getattr(cls, CONST_CLASS_ELEMENTS) + def __get_score_element_metadatas(cls) -> ScoreElementMetadataContainer: + return getattr(cls, CONST_CLASS_ELEMENT_METADATAS) def __create_db_observer(self) -> 'DatabaseObserver': return DatabaseObserver( @@ -459,19 +459,19 @@ def __check_payable(self, func_name: str): f"Method not payable: {type(self).__name__}.{func_name}") def __is_external_method(self, func_name) -> bool: - elements = self.__get_score_elements() - func: Function = elements.get(func_name) - return isinstance(func, Function) and func.is_external + elements = self.__get_score_element_metadatas() + func: FunctionMetadata = elements.get(func_name) + return isinstance(func, FunctionMetadata) and func.is_external def __is_payable_method(self, func_name) -> bool: - elements = self.__get_score_elements() - func: Function = elements.get(func_name) - return isinstance(func, Function) and func.is_payable + elements = self.__get_score_element_metadatas() + func: FunctionMetadata = elements.get(func_name) + return isinstance(func, FunctionMetadata) and func.is_payable def __is_func_readonly(self, func_name: str) -> bool: - elements = self.__get_score_elements() - func: Function = elements.get(func_name) - return isinstance(func, Function) and func.is_readonly + elements = self.__get_score_element_metadatas() + func: FunctionMetadata = elements.get(func_name) + return isinstance(func, FunctionMetadata) and func.is_readonly # noinspection PyUnusedLocal @staticmethod diff --git a/iconservice/iconscore/icon_score_constant.py b/iconservice/iconscore/icon_score_constant.py index 75a3fe00d..13acfedba 100644 --- a/iconservice/iconscore/icon_score_constant.py +++ b/iconservice/iconscore/icon_score_constant.py @@ -24,7 +24,7 @@ BaseType = TypeVar("BaseType", bool, int, str, bytes, list, dict, Address) CONST_CLASS_API = '__api' -CONST_CLASS_ELEMENTS = '__elements' +CONST_CLASS_ELEMENT_METADATAS = '__element_metadatas' CONST_SCORE_FLAG = '__score_flag' CONST_INDEXED_ARGS_COUNT = '__indexed_args_count' diff --git a/iconservice/iconscore/icon_score_engine.py b/iconservice/iconscore/icon_score_engine.py index a57360706..64108f454 100644 --- a/iconservice/iconscore/icon_score_engine.py +++ b/iconservice/iconscore/icon_score_engine.py @@ -24,7 +24,7 @@ from .icon_score_context_util import IconScoreContextUtil from .typing.conversion import convert_score_parameters, ConvertOption from .typing.element import ( - ScoreElement, + ScoreElementMetadata, get_score_element, ) from ..base.address import Address, SYSTEM_SCORE_ADDRESS @@ -133,7 +133,7 @@ def _convert_score_params_by_annotations(context: 'IconScoreContext', ): options = ConvertOption.IGNORE_UNKNOWN_PARAMS - element: ScoreElement = get_score_element(icon_score, func_name) + element: ScoreElementMetadata = get_score_element(icon_score, func_name) params = convert_score_parameters(kw_params, element.signature, options) return params diff --git a/iconservice/iconscore/typing/definition.py b/iconservice/iconscore/typing/definition.py index a64ac1cd3..c68fbce88 100644 --- a/iconservice/iconscore/typing/definition.py +++ b/iconservice/iconscore/typing/definition.py @@ -20,7 +20,7 @@ from . import get_origin, get_args, is_struct from .conversion import is_base_type -from .element import ScoreElement, Function, EventLog +from .element import ScoreElementMetadata, FunctionMetadata, EventLogMetadata from ..icon_score_constant import STR_FALLBACK from ...base.exception import ( IllegalFormatException, @@ -32,7 +32,7 @@ """ -def get_score_api(elements: Iterable[ScoreElement]) -> List: +def get_score_api(elements: Iterable[ScoreElementMetadata]) -> List: """Returns score api used in icx_getScoreApi JSON-RPC method :param elements: @@ -42,11 +42,11 @@ def get_score_api(elements: Iterable[ScoreElement]) -> List: api = [] for element in elements: - if isinstance(element, Function): - func: Function = element + if isinstance(element, FunctionMetadata): + func: FunctionMetadata = element item = _get_function(func.name, func.signature, func.is_readonly, func.is_payable) - elif isinstance(element, EventLog): - eventlog: EventLog = element + elif isinstance(element, EventLogMetadata): + eventlog: EventLogMetadata = element item = _get_eventlog(eventlog.name, eventlog.signature, eventlog.indexed_args_count) else: raise InternalServiceErrorException(f"Invalid score element: {element} {type(element)}") diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index c0a83e7b8..c821b414d 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -36,7 +36,7 @@ CONST_SCORE_FLAG, ScoreFlag, CONST_INDEXED_ARGS_COUNT, - CONST_CLASS_ELEMENTS, + CONST_CLASS_ELEMENT_METADATAS, ) from ... import utils from ...base.exception import ( @@ -162,30 +162,30 @@ def verify_score_flag(flag: ScoreFlag): raise IllegalFormatException(f"Invalid score decorator: {flag}") -class ScoreElement(object): - def __init__(self, origin: callable): - self._origin = origin - self._signature: Signature = normalize_signature(origin) +class ScoreElementMetadata(object): + def __init__(self, element: callable): + self._signature: Signature = normalize_signature(element) + self._element = element @property - def origin(self) -> callable: - return self._origin + def element(self) -> callable: + return self._element @property def name(self) -> str: - return self._origin.__name__ + return self._element.__name__ @property def flag(self) -> ScoreFlag: - return get_score_flag(self._origin) + return get_score_flag(self._element) @property def signature(self) -> Signature: return self._signature -class Function(ScoreElement): - """Represents a exposed function of SCORE +class FunctionMetadata(ScoreElementMetadata): + """Represents metadata of an exposed function in a SCORE """ def __init__(self, func: callable): @@ -227,8 +227,8 @@ def _verify_fallback_signature(self): raise IllegalFormatException("Invalid fallback signature") -class EventLog(ScoreElement): - """Represents an eventlog declared in a SCORE +class EventLogMetadata(ScoreElementMetadata): + """Represents metadata of an eventlog declared in a SCORE """ def __init__(self, eventlog: callable): @@ -236,10 +236,10 @@ def __init__(self, eventlog: callable): @property def indexed_args_count(self) -> int: - return getattr(self.origin, CONST_INDEXED_ARGS_COUNT, 0) + return getattr(self.element, CONST_INDEXED_ARGS_COUNT, 0) -class ScoreElementContainer(MutableMapping): +class ScoreElementMetadataContainer(MutableMapping): """Container which has score elements like function and eventlog """ @@ -257,16 +257,16 @@ def externals(self) -> int: def eventlogs(self) -> int: return self._eventlogs - def __getitem__(self, k: str) -> ScoreElement: + def __getitem__(self, k: str) -> ScoreElementMetadata: return self._elements[k] - def __setitem__(self, k: str, v: ScoreElement) -> None: + def __setitem__(self, k: str, v: ScoreElementMetadata) -> None: self._check_writable() self._elements[k] = v - if isinstance(v, Function): + if isinstance(v, FunctionMetadata): self._externals += 1 - elif isinstance(v, EventLog): + elif isinstance(v, EventLogMetadata): self._eventlogs += 1 else: raise InternalServiceErrorException(f"Invalid element: {v}") @@ -291,14 +291,14 @@ def __delitem__(self, k: str) -> None: def _check_writable(self): if self._readonly: - raise InternalServiceErrorException("ScoreElementContainer not writable") + raise InternalServiceErrorException(f"{self.__class__.__name__} not writable") def freeze(self): self._readonly = True -def create_score_elements(cls: type) -> Mapping: - elements = ScoreElementContainer() +def create_score_element_metadatas(cls: type) -> Mapping: + elements = ScoreElementMetadataContainer() for name, func in getmembers(cls, predicate=isfunction): if name.startswith("__"): @@ -309,19 +309,19 @@ def create_score_elements(cls: type) -> Mapping: if utils.is_any_flag_on(flag, ScoreFlag.FUNC | ScoreFlag.EVENTLOG): verify_score_flag(flag) - elements[name] = create_score_element(func) + elements[name] = create_score_element_metadata(func) elements.freeze() return elements -def create_score_element(element: callable) -> Union[Function, EventLog]: +def create_score_element_metadata(element: callable) -> Union[FunctionMetadata, EventLogMetadata]: flags = get_score_flag(element) if flags & ScoreFlag.EVENTLOG: - return EventLog(element) + return EventLogMetadata(element) else: - return Function(element) + return FunctionMetadata(element) def get_score_flag(obj: callable, default: ScoreFlag = ScoreFlag.NONE) -> ScoreFlag: @@ -347,9 +347,9 @@ def is_any_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: return utils.is_any_flag_on(get_score_flag(obj), flag) -def get_score_element(score, func_name: str) -> ScoreElement: +def get_score_element(score, func_name: str) -> ScoreElementMetadata: try: - elements = getattr(score, CONST_CLASS_ELEMENTS) + elements = getattr(score, CONST_CLASS_ELEMENT_METADATAS) return elements[func_name] except KeyError: raise MethodNotFoundException( diff --git a/tests/legacy_unittest/test_icon_score_engine.py b/tests/legacy_unittest/test_icon_score_engine.py index eb7e7a016..19f3e0288 100644 --- a/tests/legacy_unittest/test_icon_score_engine.py +++ b/tests/legacy_unittest/test_icon_score_engine.py @@ -23,12 +23,12 @@ from iconservice import * from iconservice.base.address import AddressPrefix, Address from iconservice.base.exception import ScoreNotFoundException, InvalidParamsException -from iconservice.iconscore.icon_score_constant import ATTR_SCORE_GET_API, ATTR_SCORE_CALL, CONST_CLASS_ELEMENTS +from iconservice.iconscore.icon_score_constant import ATTR_SCORE_GET_API, ATTR_SCORE_CALL, CONST_CLASS_ELEMENT_METADATAS from iconservice.iconscore.icon_score_context import IconScoreContext from iconservice.iconscore.icon_score_context import IconScoreContextType from iconservice.iconscore.icon_score_engine import IconScoreEngine from iconservice.iconscore.icon_score_mapper import IconScoreMapper -from iconservice.iconscore.typing.element import Function +from iconservice.iconscore.typing.element import FunctionMetadata from tests import create_address @@ -202,7 +202,7 @@ def test_method(address: Address, integer: int): score_object = Mock(spec=[func_name, "__elements", "address"]) setattr(score_object, func_name, test_method) - setattr(score_object, CONST_CLASS_ELEMENTS, {func_name: Function(test_method)}) + setattr(score_object, CONST_CLASS_ELEMENT_METADATAS, {func_name: FunctionMetadata(test_method)}) converted_params = IconScoreEngine._convert_score_params_by_annotations( context, score_object, func_name, primitive_params) diff --git a/tests/unit_test/iconscore/typing/test_convertion.py b/tests/unit_test/iconscore/typing/test_convertion.py index 300aec0c1..b9d3d0db3 100644 --- a/tests/unit_test/iconscore/typing/test_convertion.py +++ b/tests/unit_test/iconscore/typing/test_convertion.py @@ -26,7 +26,7 @@ convert_score_parameters, object_to_str, ) -from iconservice.iconscore.typing.element import Function +from iconservice.iconscore.typing.element import FunctionMetadata class User(TypedDict): @@ -85,5 +85,5 @@ def func(self, address: Address): params = {} with pytest.raises(InvalidParamsException): - function = Function(TestScore.func) + function = FunctionMetadata(TestScore.func) convert_score_parameters(params, function.signature) From 856fdac07a65814676e6ffbdc1f66a83058c50d0 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Wed, 1 Jul 2020 22:13:56 +0900 Subject: [PATCH 26/40] Check parameter default type in normalize_parameter() --- iconservice/iconscore/typing/element.py | 26 +++++++++- .../iconscore/typing/test_element.py | 49 +++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index c821b414d..4f38dc342 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -19,11 +19,10 @@ from inspect import ( isfunction, getmembers, - signature, Signature, Parameter, ) -from typing import Union, Mapping, List, Dict, Optional, Tuple +from typing import Union, Mapping, List, Dict, Optional, Tuple, Any from . import ( is_base_type, @@ -102,6 +101,8 @@ def normalize_parameter(param: Parameter) -> Parameter: else: type_hint = normalize_type_hint(annotation) + check_parameter_default_type(type_hint, param.default) + if type_hint == annotation: # Nothing to update return param @@ -116,6 +117,27 @@ def normalize_return_annotation(return_annotation: type) -> Union[type, Signatur return return_annotation +def check_parameter_default_type(type_hint: type, default: Any): + # default value type check + if default in (Parameter.empty, None): + return + + origin = get_origin(type_hint) + + if origin is Union: + default_type = get_args(type_hint)[0] + else: + default_type = origin + + if not isinstance(default, default_type): + raise InvalidParamsException( + f'Default params type mismatch. value={default} type={type_hint}') + + if type(default) is bool and origin is not bool: + raise InvalidParamsException( + f'Default params type mismatch. value={default} type={type_hint}') + + def normalize_type_hint(type_hint) -> type: # If type hint is str, convert it to type hint if isinstance(type_hint, str): diff --git a/tests/unit_test/iconscore/typing/test_element.py b/tests/unit_test/iconscore/typing/test_element.py index 477c01280..3b403ecc2 100644 --- a/tests/unit_test/iconscore/typing/test_element.py +++ b/tests/unit_test/iconscore/typing/test_element.py @@ -18,11 +18,14 @@ import pytest from typing_extensions import TypedDict -from iconservice.base.address import Address -from iconservice.base.exception import IllegalFormatException +from iconservice.base.address import Address, AddressPrefix +from iconservice.base.exception import IllegalFormatException, InvalidParamsException from iconservice.iconscore.icon_score_constant import ScoreFlag -from iconservice.iconscore.typing.element import normalize_type_hint -from iconservice.iconscore.typing.element import verify_score_flag +from iconservice.iconscore.typing.element import ( + normalize_type_hint, + verify_score_flag, + check_parameter_default_type, +) class Person(TypedDict): @@ -116,3 +119,41 @@ def test_verify_score_flag(flag, success): else: with pytest.raises(IllegalFormatException): verify_score_flag(flag) + + +@pytest.mark.parametrize( + "type_hint,default,success", + [ + (bool, 0, False), + (bytes, "hello", False), + (int, "world", False), + (int, False, False), + (str, True, False), + (Address, 1, False), + (str, None, True), + (bool, False, True), + (bytes, b"hello", True), + (int, 1, True), + (str, "hello", True), + (Address, Address.from_prefix_and_int(AddressPrefix.EOA, 1), True), + (str, None, True), + (Union[int, None], None, True), + (Union[None, int], 0, False), + (Person, None, True), + (List[int], None, True), + (Union[List[Person], None], None, True), + (Dict[str, int], None, True), + (Union[Dict[str, int], None], None, True), + (Optional[bool], None, True), + (Optional[bytes], None, True), + (Optional[int], None, True), + (Optional[str], None, True), + (Optional[Address], None, True), + ] +) +def test_check_parameter_default_type(type_hint, default, success): + if success: + check_parameter_default_type(type_hint, default) + else: + with pytest.raises(InvalidParamsException): + check_parameter_default_type(type_hint, default) From 83ca2ea8bb5f6b017b53ecf8808c2cb572c8b77b Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Thu, 2 Jul 2020 14:42:28 +0900 Subject: [PATCH 27/40] Rename get_score_element() to get_score_element_metadata() * Parameter type check on internal call is under development --- iconservice/iconscore/icon_score_engine.py | 4 ++-- iconservice/iconscore/internal_call.py | 1 + iconservice/iconscore/typing/element.py | 18 +++++++++++------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/iconservice/iconscore/icon_score_engine.py b/iconservice/iconscore/icon_score_engine.py index 64108f454..310514ff3 100644 --- a/iconservice/iconscore/icon_score_engine.py +++ b/iconservice/iconscore/icon_score_engine.py @@ -25,7 +25,7 @@ from .typing.conversion import convert_score_parameters, ConvertOption from .typing.element import ( ScoreElementMetadata, - get_score_element, + get_score_element_metadata, ) from ..base.address import Address, SYSTEM_SCORE_ADDRESS from ..base.exception import ScoreNotFoundException, InvalidParamsException @@ -133,7 +133,7 @@ def _convert_score_params_by_annotations(context: 'IconScoreContext', ): options = ConvertOption.IGNORE_UNKNOWN_PARAMS - element: ScoreElementMetadata = get_score_element(icon_score, func_name) + element: ScoreElementMetadata = get_score_element_metadata(icon_score, func_name) params = convert_score_parameters(kw_params, element.signature, options) return params diff --git a/iconservice/iconscore/internal_call.py b/iconservice/iconscore/internal_call.py index 61fcd1a69..792236f29 100644 --- a/iconservice/iconscore/internal_call.py +++ b/iconservice/iconscore/internal_call.py @@ -21,6 +21,7 @@ from .icon_score_event_log import EventLogEmitter from .icon_score_step import StepType from .icon_score_trace import Trace, TraceType +from .typing.element import verify_internal_call_arguments from ..base.address import Address, SYSTEM_SCORE_ADDRESS, GOVERNANCE_SCORE_ADDRESS from ..base.exception import StackOverflowException, ScoreNotFoundException from ..base.message import Message diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 4f38dc342..3671c420e 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -13,12 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import inspect from collections import OrderedDict from collections.abc import MutableMapping from inspect import ( isfunction, getmembers, + signature, Signature, Parameter, ) @@ -55,7 +55,7 @@ def normalize_signature(func: callable) -> Signature: :param func: function attribute from class :return: """ - sig = inspect.signature(func) + sig = signature(func) params = sig.parameters new_params = [] @@ -70,7 +70,7 @@ def normalize_signature(func: callable) -> Signature: # inspect.isfunction(A().func) == False # inspect.ismethod(A.func) == False # inspect.isfunction(A().func) == True - is_regular_method: bool = inspect.isfunction(func) + is_regular_method: bool = isfunction(func) for i, k in enumerate(params): # Remove "self" parameter from signature of regular method @@ -369,7 +369,7 @@ def is_any_score_flag_on(obj: callable, flag: ScoreFlag) -> bool: return utils.is_any_flag_on(get_score_flag(obj), flag) -def get_score_element(score, func_name: str) -> ScoreElementMetadata: +def get_score_element_metadata(score, func_name: str) -> ScoreElementMetadata: try: elements = getattr(score, CONST_CLASS_ELEMENT_METADATAS) return elements[func_name] @@ -379,12 +379,16 @@ def get_score_element(score, func_name: str) -> ScoreElementMetadata: def verify_internal_call_arguments(score, func_name: str, args: Optional[Tuple], kwargs: Optional[Dict]): - element = get_score_element(score, func_name) + element = get_score_element_metadata(score, func_name) sig = element.signature - sig = inspect.signature(getattr(score, func_name)) try: - arguments = sig.bind(*args, **kwargs) + if args is None: + args = () + if kwargs is None: + kwargs = {} + + sig.bind(*args, **kwargs) except TypeError: raise InvalidParamsException( f"Invalid internal call params: address={score.address} func={func_name}") From 7e36d7e6446588381238066103ea19e60b30ccea Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Thu, 2 Jul 2020 14:48:23 +0900 Subject: [PATCH 28/40] Remove useless codes from score api definition part --- iconservice/iconscore/typing/definition.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/iconservice/iconscore/typing/definition.py b/iconservice/iconscore/typing/definition.py index c68fbce88..732f06977 100644 --- a/iconservice/iconscore/typing/definition.py +++ b/iconservice/iconscore/typing/definition.py @@ -105,9 +105,6 @@ def _get_inputs(params: Mapping[str, Parameter]) -> list: inputs = [] for name, param in params.items(): - if not _is_param_valid(param): - continue - annotation = param.annotation type_hint = str if annotation is Parameter.empty else annotation @@ -220,9 +217,6 @@ def _get_eventlog(func_name: str, sig: Signature, indexed_args_count: int) -> Di inputs = [] for name, param in params.items(): - if not _is_param_valid(param): - continue - annotation = param.annotation type_hint = str if annotation is Parameter.empty else annotation inp: Dict = _get_input(name, type_hint, param.default) @@ -235,7 +229,3 @@ def _get_eventlog(func_name: str, sig: Signature, indexed_args_count: int) -> Di "type": "eventlog", "inputs": inputs } - - -def _is_param_valid(param: Parameter) -> bool: - return param.name not in ("self", "cls") From 95a9067cc97a7a6c6bd59c74922715d4f97a9c63 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Thu, 2 Jul 2020 17:43:32 +0900 Subject: [PATCH 29/40] Remove unused comments --- iconservice/iconscore/icon_score_base.py | 3 --- iconservice/iconscore/typing/element.py | 10 +++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/iconservice/iconscore/icon_score_base.py b/iconservice/iconscore/icon_score_base.py index 1b7edc4d8..c7acea61e 100644 --- a/iconservice/iconscore/icon_score_base.py +++ b/iconservice/iconscore/icon_score_base.py @@ -342,12 +342,9 @@ def __new__(mcs, name, bases, namespace, **kwargs): if not isinstance(namespace, dict): raise InvalidParamsException('namespace is not dict!') - # TODO: Normalize type hints of score parameters by goldworm elements: Mapping[str, ScoreElementMetadata] = create_score_element_metadatas(cls) setattr(cls, CONST_CLASS_ELEMENT_METADATAS, elements) - # TODO: Replace it with a new list supporting struct and list - # api_list = ScoreApiGenerator.generate(custom_funcs) api_list = get_score_api(elements.values()) setattr(cls, CONST_CLASS_API, api_list) diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 3671c420e..2201ca1ce 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -66,10 +66,18 @@ def normalize_signature(func: callable) -> Signature: # def func(self): # pass # + # @classmethod + # def cfunc(self): + # pass + # # inspect.isfunction(A.func) == True # inspect.isfunction(A().func) == False # inspect.ismethod(A.func) == False - # inspect.isfunction(A().func) == True + # inspect.ismethod(A().func) == True + # inspect.isfunction(A.cfunc) == False + # inspect.ismethod(A.cfunc) == True + # inspect.isfunction(A().cfunc) == False + # inspect.ismethod(A().cfunc) == True is_regular_method: bool = isfunction(func) for i, k in enumerate(params): From 97837a16b4c87ec1b27de0157af036124738b2cc Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Thu, 2 Jul 2020 17:43:59 +0900 Subject: [PATCH 30/40] Make str_to_object() allow None as a argument --- iconservice/iconscore/typing/conversion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index 27a546882..0d3f0f938 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -151,7 +151,10 @@ def _verify_arguments(params: Dict[str, Any], sig: Signature): raise InvalidParamsException(f"Parameter not found: {k}") -def str_to_object(value: Union[str, list, dict], type_hint: type) -> Any: +def str_to_object(value: Union[str, list, dict, None], type_hint: type) -> Any: + if value is None: + return None + if type(value) not in (str, list, dict): raise InvalidParamsException(f"Invalid value type: {value}") From 8cd416a104b236474653485e14e19442fa6b7368 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Thu, 2 Jul 2020 18:06:16 +0900 Subject: [PATCH 31/40] verify_internal_call_arguments() is under development --- iconservice/iconscore/internal_call.py | 9 +++++-- iconservice/iconscore/typing/element.py | 5 +--- .../iconscore/typing/test_element.py | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/iconservice/iconscore/internal_call.py b/iconservice/iconscore/internal_call.py index 792236f29..2892d7066 100644 --- a/iconservice/iconscore/internal_call.py +++ b/iconservice/iconscore/internal_call.py @@ -21,7 +21,11 @@ from .icon_score_event_log import EventLogEmitter from .icon_score_step import StepType from .icon_score_trace import Trace, TraceType -from .typing.element import verify_internal_call_arguments +from .typing.element import ( + verify_internal_call_arguments, + get_score_element_metadata, + ScoreElementMetadata, +) from ..base.address import Address, SYSTEM_SCORE_ADDRESS, GOVERNANCE_SCORE_ADDRESS from ..base.exception import StackOverflowException, ScoreNotFoundException from ..base.message import Message @@ -149,7 +153,8 @@ def _other_score_call(context: 'IconScoreContext', score_func = getattr(icon_score, ATTR_SCORE_CALL) # TODO: verify internal call arguments by goldworm - # verify_internal_call_arguments(icon_score, func_name, arg_params, kw_params) + # metadata: ScoreElementMetadata = get_score_element_metadata(icon_score, func_name) + # verify_internal_call_arguments(metadata.signature, arg_params, kw_params) return score_func(func_name=func_name, arg_params=arg_params, kw_params=kw_params) finally: diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 2201ca1ce..0c7583739 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -386,10 +386,7 @@ def get_score_element_metadata(score, func_name: str) -> ScoreElementMetadata: f"Method not found: {type(score).__name__}.{func_name}") -def verify_internal_call_arguments(score, func_name: str, args: Optional[Tuple], kwargs: Optional[Dict]): - element = get_score_element_metadata(score, func_name) - sig = element.signature - +def verify_internal_call_arguments(sig: Signature, args: Optional[Tuple], kwargs: Optional[Dict]): try: if args is None: args = () diff --git a/tests/unit_test/iconscore/typing/test_element.py b/tests/unit_test/iconscore/typing/test_element.py index 3b403ecc2..9aa380bfe 100644 --- a/tests/unit_test/iconscore/typing/test_element.py +++ b/tests/unit_test/iconscore/typing/test_element.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect from typing import List, Dict, Union, Optional, ForwardRef import pytest @@ -25,6 +26,7 @@ normalize_type_hint, verify_score_flag, check_parameter_default_type, + verify_internal_call_arguments, ) @@ -157,3 +159,26 @@ def test_check_parameter_default_type(type_hint, default, success): else: with pytest.raises(InvalidParamsException): check_parameter_default_type(type_hint, default) + + +@pytest.mark.parametrize( + "args,kwargs,valid", + [ + ((0,), None, True), + ((0,), {}, True), + (None, None, False), + (("hello",), {}, False), + ] +) +def test_verify_internal_call_arguments(args, kwargs, valid): + def func(a: int): + a += 1 + pass + + sig = inspect.signature(func) + + if valid: + verify_internal_call_arguments(sig, args, kwargs) + else: + with pytest.raises(InvalidParamsException): + verify_internal_call_arguments(sig, args, kwargs) From 2e4ff3d79388027c92d8ecc7baa244b0bc24738b Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Fri, 3 Jul 2020 22:34:18 +0900 Subject: [PATCH 32/40] Check if argument types match to type hints in parameters on internal call --- iconservice/icon_constant.py | 1 + iconservice/iconscore/internal_call.py | 9 +- iconservice/iconscore/typing/__init__.py | 13 + iconservice/iconscore/typing/conversion.py | 14 +- iconservice/iconscore/typing/element.py | 13 - iconservice/iconscore/typing/verification.py | 145 ++++++++++ iconservice/icx/issue/regulator.py | 15 + tests/unit_test/iconscore/typing/__init__.py | 13 + .../iconscore/typing/test__init__.py | 17 ++ .../iconscore/typing/test_element.py | 25 -- .../iconscore/typing/test_verification.py | 108 +++++++ tests/unittest/icx/__init__.py | 0 tests/unittest/icx/test_coin_part.py | 265 ------------------ 13 files changed, 326 insertions(+), 312 deletions(-) create mode 100644 iconservice/iconscore/typing/verification.py create mode 100644 tests/unit_test/iconscore/typing/test_verification.py delete mode 100644 tests/unittest/icx/__init__.py delete mode 100644 tests/unittest/icx/test_coin_part.py diff --git a/iconservice/icon_constant.py b/iconservice/icon_constant.py index 0a46c68a9..56e5982e5 100644 --- a/iconservice/icon_constant.py +++ b/iconservice/icon_constant.py @@ -132,6 +132,7 @@ class Revision(Enum): SET_IREP_VIA_NETWORK_PROPOSAL = 9 MULTIPLE_UNSTAKE = 9 FIX_COIN_PART_BYTES_ENCODING = 9 + VERIFY_INTERNAL_CALL_ARGS = 9 LATEST = 9 diff --git a/iconservice/iconscore/internal_call.py b/iconservice/iconscore/internal_call.py index 2892d7066..7a464e2f4 100644 --- a/iconservice/iconscore/internal_call.py +++ b/iconservice/iconscore/internal_call.py @@ -22,10 +22,10 @@ from .icon_score_step import StepType from .icon_score_trace import Trace, TraceType from .typing.element import ( - verify_internal_call_arguments, get_score_element_metadata, ScoreElementMetadata, ) +from .typing.verification import verify_internal_call_arguments from ..base.address import Address, SYSTEM_SCORE_ADDRESS, GOVERNANCE_SCORE_ADDRESS from ..base.exception import StackOverflowException, ScoreNotFoundException from ..base.message import Message @@ -152,9 +152,10 @@ def _other_score_call(context: 'IconScoreContext', context.set_func_type_by_icon_score(icon_score, func_name) score_func = getattr(icon_score, ATTR_SCORE_CALL) - # TODO: verify internal call arguments by goldworm - # metadata: ScoreElementMetadata = get_score_element_metadata(icon_score, func_name) - # verify_internal_call_arguments(metadata.signature, arg_params, kw_params) + if context.revision >= Revision.VERIFY_INTERNAL_CALL_ARGS.value: + # TODO: verify internal call arguments by goldworm + metadata: ScoreElementMetadata = get_score_element_metadata(icon_score, func_name) + verify_internal_call_arguments(metadata.signature, arg_params, kw_params) return score_func(func_name=func_name, arg_params=arg_params, kw_params=kw_params) finally: diff --git a/iconservice/iconscore/typing/__init__.py b/iconservice/iconscore/typing/__init__.py index 47c299851..70b230af1 100644 --- a/iconservice/iconscore/typing/__init__.py +++ b/iconservice/iconscore/typing/__init__.py @@ -54,6 +54,9 @@ def get_origin(type_hint: type) -> Optional[type]: :param type_hint: :return: """ + if type_hint == "Address": + type_hint = Address + if isinstance(type_hint, type): return type_hint @@ -73,3 +76,13 @@ def is_struct(type_hint) -> bool: def get_annotations(obj: Any, default: Any) -> Dict[str, type]: return getattr(obj, "__annotations__", default) + + +def isinstance_ex(value: Any, _type: type) -> bool: + if not isinstance(value, _type): + return False + + if type(value) is bool and _type is not bool: + return False + + return True diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index 0d3f0f938..20eb38bb1 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -141,13 +141,17 @@ def convert_score_parameters( def _verify_arguments(params: Dict[str, Any], sig: Signature): + """ - for k in sig.parameters: - if k in ("self", "cls"): - continue + :param params: + :param sig: normalized signature + :return: + """ + parameters = sig.parameters - parameter: Parameter = sig.parameters[k] - if k not in params and parameter.default == Parameter.empty: + for k in parameters: + parameter: Parameter = parameters[k] + if k not in params and parameter.default is Parameter.empty: raise InvalidParamsException(f"Parameter not found: {k}") diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 0c7583739..0b2a70c46 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -384,16 +384,3 @@ def get_score_element_metadata(score, func_name: str) -> ScoreElementMetadata: except KeyError: raise MethodNotFoundException( f"Method not found: {type(score).__name__}.{func_name}") - - -def verify_internal_call_arguments(sig: Signature, args: Optional[Tuple], kwargs: Optional[Dict]): - try: - if args is None: - args = () - if kwargs is None: - kwargs = {} - - sig.bind(*args, **kwargs) - except TypeError: - raise InvalidParamsException( - f"Invalid internal call params: address={score.address} func={func_name}") diff --git a/iconservice/iconscore/typing/verification.py b/iconservice/iconscore/typing/verification.py new file mode 100644 index 000000000..e04d7b077 --- /dev/null +++ b/iconservice/iconscore/typing/verification.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from inspect import ( + Signature, + Parameter, +) +from typing import Optional, Tuple, Dict, Any, List, Mapping + +from typing_extensions import TypedDict + +from . import ( + get_origin, + get_args, + get_annotations, + is_base_type, + is_struct, + isinstance_ex, +) +from ...base.exception import InvalidParamsException + + +def verify_internal_call_arguments(sig: Signature, args: Optional[Tuple], kwargs: Optional[Dict]): + """Check if argument types matches to type hints in sig.parameters + + :param sig: normalized signature of method in SCORE + :param args: args in tuple + :param kwargs: + :return: + """ + if args is None: + args = () + if kwargs is None: + kwargs = {} + + params: Dict[str, Any] = {} + parameters = sig.parameters + + bind_arguments(params, parameters, args, kwargs) + add_default_value_to_params(params, parameters) + + for name, parameter in parameters.items(): + if name not in params: + raise InvalidParamsException(f"Argument not found: name={name}") + + type_hint = parameter.annotation + value = params[name] + + try: + verify_type_hint(value, type_hint) + except: + raise InvalidParamsException( + f"Type mismatch: name={name} type_hint={type_hint} value_type={type(value)}") + + +def bind_arguments( + params: Dict[str, Any], + parameters: Mapping[str, Parameter], + args: Optional[Tuple], kwargs: Optional[Dict]) -> Dict[str, Any]: + for arg, k in zip(args, parameters): + params[k] = arg + + for k in kwargs: + if k in params: + raise InvalidParamsException(f"Duplicated argument: name={k} value={kwargs[k]}") + + if k not in parameters: + raise InvalidParamsException(f"Invalid argument: name={k} value={kwargs[k]}") + + params[k] = kwargs[k] + + return params + + +def add_default_value_to_params(params: Dict[str, Any], parameters: Mapping[str, Parameter]): + if len(params) == len(parameters): + return + + # fill default values in params: + for k in parameters: + if k in params: + continue + + parameter = parameters[k] + if parameter is Parameter.empty: + raise InvalidParamsException(f"Argument not found: name={k}") + + params[k] = parameter.default + + +def verify_type_hint(value: Any, type_hint: type): + origin: type = get_origin(type_hint) + + if is_base_type(origin): + if not isinstance_ex(value, origin): + raise TypeError + elif is_struct(origin): + verify_struct_type_hint(value, type_hint) + elif origin is list: + verify_list_type_hint(value, type_hint) + elif origin is dict: + verify_dict_type_hint(value, type_hint) + else: + raise TypeError + + +def verify_struct_type_hint(value: TypedDict, type_hint: type): + annotations = get_annotations(type_hint, None) + assert annotations is not None + + for name, type_hint in annotations.items(): + verify_type_hint(value[name], type_hint) + + +def verify_list_type_hint(values: List[Any], type_hint: type): + args = get_args(type_hint) + + for value in values: + verify_type_hint(value, args[0]) + + +def verify_dict_type_hint(values: Dict[str, Any], type_hint: type): + args = get_args(type_hint) + key_type_hint = args[0] + value_type_hint = args[1] + + assert key_type_hint is str + + for k, v in values.items(): + if not isinstance_ex(k, key_type_hint): + raise TypeError + + verify_type_hint(v, value_type_hint) diff --git a/iconservice/icx/issue/regulator.py b/iconservice/icx/issue/regulator.py index d0b0164df..1efe69bfb 100644 --- a/iconservice/icx/issue/regulator.py +++ b/iconservice/icx/issue/regulator.py @@ -148,6 +148,21 @@ def _calculate_prev_block_cumulative_fee(cls, return covered_icx_by_fee, remain_over_issued_icx, corrected_issue_amount + @classmethod + def _calculate_prev_block_cumulative_fee(cls, + remain_over_issued_icx: int, + corrected_issue_amount: int, + prev_block_cumulative_fee: int) -> Tuple[int, int, int]: + corrected_issue_amount -= prev_block_cumulative_fee + if corrected_issue_amount >= 0: + covered_icx_by_fee = prev_block_cumulative_fee + else: + covered_icx_by_fee = prev_block_cumulative_fee + corrected_issue_amount + remain_over_issued_icx = remain_over_issued_icx + abs(corrected_issue_amount) + corrected_issue_amount = 0 + + return covered_icx_by_fee, remain_over_issued_icx, corrected_issue_amount + @classmethod def _separate_icx_and_iscore(cls, iscore: int) -> Tuple[int, int]: abs_iscore = abs(iscore) diff --git a/tests/unit_test/iconscore/typing/__init__.py b/tests/unit_test/iconscore/typing/__init__.py index 8291f7cea..2add19eb5 100644 --- a/tests/unit_test/iconscore/typing/__init__.py +++ b/tests/unit_test/iconscore/typing/__init__.py @@ -12,3 +12,16 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +from typing import List +from typing_extensions import TypedDict + +from iconservice.base.address import AddressPrefix, Address + + +class Person(TypedDict): + name: str + age: int + single: bool + data: bytes + wallets: List[Address] diff --git a/tests/unit_test/iconscore/typing/test__init__.py b/tests/unit_test/iconscore/typing/test__init__.py index 9fae92e80..acea5914d 100644 --- a/tests/unit_test/iconscore/typing/test__init__.py +++ b/tests/unit_test/iconscore/typing/test__init__.py @@ -10,6 +10,7 @@ from iconservice.iconscore.typing import ( get_origin, get_args, + isinstance_ex, ) @@ -27,6 +28,7 @@ class Person(TypedDict): (int, int), (str, str), (Address, Address), + ("Address", Address), (List[int], list), (List[List[str]], list), (Dict, dict), @@ -75,3 +77,18 @@ def test_get_args_with_struct(): for name, type_hint in annotations.items(): assert type_hint == expected[name] + + +@pytest.mark.parametrize( + "value,_type,expected", + [ + (True, int, False), + (False, int, False), + (0, bool, False), + (1, bool, False), + (True, bool, True), + (False, bool, True), + ] +) +def test_isinstance_ex(value, _type, expected): + assert isinstance_ex(value, _type) == expected diff --git a/tests/unit_test/iconscore/typing/test_element.py b/tests/unit_test/iconscore/typing/test_element.py index 9aa380bfe..3b403ecc2 100644 --- a/tests/unit_test/iconscore/typing/test_element.py +++ b/tests/unit_test/iconscore/typing/test_element.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import inspect from typing import List, Dict, Union, Optional, ForwardRef import pytest @@ -26,7 +25,6 @@ normalize_type_hint, verify_score_flag, check_parameter_default_type, - verify_internal_call_arguments, ) @@ -159,26 +157,3 @@ def test_check_parameter_default_type(type_hint, default, success): else: with pytest.raises(InvalidParamsException): check_parameter_default_type(type_hint, default) - - -@pytest.mark.parametrize( - "args,kwargs,valid", - [ - ((0,), None, True), - ((0,), {}, True), - (None, None, False), - (("hello",), {}, False), - ] -) -def test_verify_internal_call_arguments(args, kwargs, valid): - def func(a: int): - a += 1 - pass - - sig = inspect.signature(func) - - if valid: - verify_internal_call_arguments(sig, args, kwargs) - else: - with pytest.raises(InvalidParamsException): - verify_internal_call_arguments(sig, args, kwargs) diff --git a/tests/unit_test/iconscore/typing/test_verification.py b/tests/unit_test/iconscore/typing/test_verification.py new file mode 100644 index 000000000..c7ff28457 --- /dev/null +++ b/tests/unit_test/iconscore/typing/test_verification.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 ICON Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import os +from typing import List + +import pytest + +from iconservice.base.address import Address, AddressPrefix +from iconservice.base.exception import InvalidParamsException +from iconservice.iconscore.typing.verification import ( + verify_internal_call_arguments, + verify_type_hint, + bind_arguments, +) +from . import Person + + +@pytest.mark.parametrize( + "args,kwargs,valid", + [ + ((0,), None, True), + ((0,), {}, True), + (None, None, False), + (("hello",), {}, False), + ] +) +def test_verify_internal_call_arguments(args, kwargs, valid): + def func(a: int): + a += 1 + + sig = inspect.signature(func) + + if valid: + verify_internal_call_arguments(sig, args, kwargs) + else: + with pytest.raises(InvalidParamsException): + verify_internal_call_arguments(sig, args, kwargs) + + +@pytest.mark.parametrize( + "args,kwargs", + [ + ((True, b"hello", 0), {}), + ((), {"a": True, "b": b"bytes", "c": 100}), + ((), {"a": True, "b": b"bytes", "c": "hello"}) + ] +) +def test_bind_arguments(args, kwargs): + def func(a: bool, b: bytes, c: int): + pass + + params = {} + sig = inspect.signature(func) + + params = bind_arguments(params, sig.parameters, args, kwargs) + assert len(params) == len(args) + len(kwargs) + + +@pytest.mark.parametrize( + "value,type_hint,success", + [ + (True, bool, True), + (False, bool, True), + (b"bytes", bytes, True), + (True, int, False), + (False, int, False), + (0, int, True), + (10, int, True), + (-9, int, True), + ("hello", str, True), + (Address.from_prefix_and_int(AddressPrefix.EOA, 0), Address, True), + ("hxe08a1ded7635bc7b769f4893aef65cf00049377b", Address, False), + ([True, False], List[bool], True), + ([True, False], List[int], False), + ([0, 1, 2], List[int], True), + ([0, 1, 2], List[str], False), + (["a", "b", "c"], List[str], True), + (["a", "b", "c"], List[bool], False), + ( + { + "name": "hello", "age": 100, "single": True, "data": b"world", + "wallets": [Address(AddressPrefix.CONTRACT, os.urandom(20))], + }, + Person, + True + ), + ] +) +def test_verify_type_hint(value, type_hint, success): + if success: + verify_type_hint(value, type_hint) + else: + with pytest.raises(expected_exception=(TypeError, KeyError)): + verify_type_hint(value, type_hint) diff --git a/tests/unittest/icx/__init__.py b/tests/unittest/icx/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/unittest/icx/test_coin_part.py b/tests/unittest/icx/test_coin_part.py deleted file mode 100644 index 5b7e66d20..000000000 --- a/tests/unittest/icx/test_coin_part.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Copyright 2018 ICON Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import pytest - -from iconservice.base.address import ICON_EOA_ADDRESS_BYTES_SIZE, ICON_CONTRACT_ADDRESS_BYTES_SIZE -from iconservice.base.exception import InvalidParamsException, OutOfBalanceException -from iconservice.icon_constant import Revision, DEFAULT_BYTE_SIZE, DATA_BYTE_ORDER -from iconservice.icx.base_part import BasePartState -from iconservice.icx.coin_part import CoinPartType, CoinPartFlag, CoinPart, CoinPartVersion -from iconservice.utils import set_flag -from iconservice.utils.msgpack_for_db import MsgPackForDB -from tests import create_address - - -class TestAccountType: - def test_account_type(self): - assert 0 == CoinPartType.GENERAL - assert 1 == CoinPartType.GENESIS - assert 2 == CoinPartType.TREASURY - assert 'GENERAL' == str(CoinPartType.GENERAL) - assert 'GENESIS' == str(CoinPartType.GENESIS) - assert 'TREASURY' == str(CoinPartType.TREASURY) - - def test_from_int(self): - assert CoinPartType(0) == CoinPartType.GENERAL - assert CoinPartType(1) == CoinPartType.GENESIS - assert CoinPartType(2) == CoinPartType.TREASURY - pytest.raises(ValueError, CoinPartType, 3) - - -class TestCoinPart: - def test_coin_part_revision_3(self): - part1 = CoinPart() - assert part1 is not None - assert CoinPartFlag.NONE is part1.flags - assert part1.balance == 0 - - part1.deposit(100) - assert 100 == part1.balance - - part1.withdraw(100) - assert 0 == part1.balance - - # wrong value - with pytest.raises(InvalidParamsException): - part1.deposit(-10) - - # 0 transfer is possible - old = part1.balance - part1.deposit(0) - assert old == part1.balance - - with pytest.raises(InvalidParamsException): - part1.withdraw(-11234) - - with pytest.raises(OutOfBalanceException): - part1.withdraw(1) - - old = part1.balance - part1.withdraw(0) - assert old == part1.balance - - @pytest.mark.parametrize("revision", [Revision(3), Revision(4)]) - def test_coin_part_from_bytes_to_bytes_revision_3_to_4(self, revision): - # Todo: No need to tests after revision 4? - part1 = CoinPart() - data = part1.to_bytes(revision.value) - assert isinstance(data, bytes) is True - assert len(data) == 36 - - part2 = CoinPart.from_bytes(data) - assert part2.type == CoinPartType.GENERAL - assert part2.balance == 0 - - part1.type = CoinPartType.GENESIS - part1.deposit(1024) - - part3 = CoinPart.from_bytes(part1.to_bytes(revision.value)) - assert part3.type == CoinPartType.GENESIS - assert part3.balance == 1024 - - def test_coin_part_from_bytes_to_bytes_old_db_load_revision_4(self): - part1 = CoinPart() - - balance = 1024 - part1.type = CoinPartType.GENERAL - part1.deposit(balance) - - part2 = CoinPart.from_bytes(part1.to_bytes(Revision.THREE.value)) - assert part2 == part1 - - data: bytes = part1.to_bytes(Revision.FOUR.value) - part3 = CoinPart.from_bytes(data) - assert part3 == part1 - - def test_coin_part_flag(self): - part1 = CoinPart() - assert part1.states == BasePartState.NONE - - part1._flags = set_flag(part1.flags, CoinPartFlag.HAS_UNSTAKE, True) - assert CoinPartFlag.HAS_UNSTAKE in part1.flags - - part1._flags = set_flag(part1.flags, CoinPartFlag.HAS_UNSTAKE, False) - assert CoinPartFlag.NONE == part1.flags - assert CoinPartFlag.HAS_UNSTAKE not in part1.flags - - def test_coin_part_make_key(self): - key = CoinPart.make_key(create_address()) - assert ICON_EOA_ADDRESS_BYTES_SIZE == len(key) - - key = CoinPart.make_key(create_address(1)) - assert ICON_CONTRACT_ADDRESS_BYTES_SIZE == len(key) - - def test_coin_part_type_property(self): - part = CoinPart() - assert CoinPartType.GENERAL == part.type - - for coin_part_type in CoinPartType: - part = CoinPart(coin_part_type=coin_part_type) - assert coin_part_type == part.type - - for coin_part_type in CoinPartType: - part = CoinPart() - part.type = coin_part_type - assert coin_part_type == part.type - - with pytest.raises(ValueError) as e: - part.type = len(CoinPartType) + 1 - - assert "Invalid CoinPartType" == e.value.args[0] - - def test_coin_part_balance(self): - balance = 10000 - part = CoinPart(balance=balance) - assert balance == part.balance - - def test_coin_part_flags(self): - part = CoinPart(flags=CoinPartFlag.HAS_UNSTAKE) - assert CoinPartFlag.HAS_UNSTAKE == part.flags - - def test_coin_part_deposit(self): - balance = 100 - part = CoinPart() - - assert part.is_dirty() is False - part.deposit(balance) - assert part.is_dirty() is True - - assert balance == part.balance - - def test_coin_part_withdraw(self): - balance = 100 - part = CoinPart() - - assert part.is_dirty() is False - part.deposit(balance) - part.withdraw(balance) - assert part.is_dirty() is True - assert 0 == part.balance - - def test_coin_part_equal(self): - part1 = CoinPart() - part2 = CoinPart() - assert part1 == part2 - - balance = 100 - part1.deposit(balance) - part3 = CoinPart(balance=balance) - assert part1 == part3 - - @pytest.mark.parametrize("revision", [i for i in range(Revision.IISS.value)]) - def test_coin_part_to_bytes_before_rev_iiss(self, revision): - coin_type = CoinPartType.GENERAL - coin_flag = CoinPartFlag.NONE - value = 5 - - coin_part = CoinPart(coin_type, coin_flag, value) - actual_bytes = coin_part.to_bytes(revision) - expected_bytes = CoinPart._STRUCT_FORMAT.pack(CoinPartVersion.STRUCT, - CoinPartType.GENERAL.value, - CoinPartFlag.NONE.value, - value.to_bytes(DEFAULT_BYTE_SIZE, DATA_BYTE_ORDER)) - - assert actual_bytes == expected_bytes - - @pytest.mark.parametrize( - "revision", - [i for i in range(Revision.IISS.value, Revision.FIX_COIN_PART_BYTES_ENCODING.value)] - ) - def test_first_coin_part_to_bytes_from_rev_iiss_to_rev_9(self, revision): - is_first = True - coin_type = CoinPartType.GENERAL - coin_flag = CoinPartFlag.NONE - value = 5 - - coin_part = CoinPart(coin_type, coin_flag, value, is_first) - actual_bytes = coin_part.to_bytes(revision) - - # 94 means list in msgpack - # def _pack_array_header(self, n): - # if n <= 0x0F: - # return self._buffer.write(struct.pack("B", 0x90 + n)) - expected_bytes = ( - b'\x94' + - MsgPackForDB.dumps(CoinPartVersion.MSG_PACK) + - MsgPackForDB.dumps(CoinPartType.GENERAL) + - coin_flag.value.to_bytes(1, DATA_BYTE_ORDER) + - value.to_bytes(1, DATA_BYTE_ORDER) - ) - - assert actual_bytes == expected_bytes - - @pytest.mark.parametrize( - "revision", - [i for i in range(Revision.IISS.value, Revision.FIX_COIN_PART_BYTES_ENCODING.value)] - ) - def test_coin_part_to_bytes_from_rev_iiss_to_rev_9(self, revision): - is_first = False - coin_type = CoinPartType.GENERAL - coin_flag = CoinPartFlag.NONE - value = 5 - - coin_part = CoinPart(coin_type, coin_flag, value, is_first) - actual_bytes = coin_part.to_bytes(revision) - - expected_bytes = b'\x94' + \ - MsgPackForDB.dumps(CoinPartVersion.MSG_PACK) + \ - coin_type.value.to_bytes(1, DATA_BYTE_ORDER) + \ - coin_flag.value.to_bytes(1, DATA_BYTE_ORDER) + \ - value.to_bytes(1, DATA_BYTE_ORDER) - - assert actual_bytes == expected_bytes - - def test_coin_part_to_bytes_from_after_rev_9(self): - revision = Revision.FIX_COIN_PART_BYTES_ENCODING.value - coin_type = CoinPartType.GENERAL - coin_flag = CoinPartFlag.NONE - value = 5 - - coin_part = CoinPart(coin_type, coin_flag, value) - actual_bytes = coin_part.to_bytes(revision) - - expected_bytes = b'\x94' + \ - CoinPartVersion.MSG_PACK.value.to_bytes(1, DATA_BYTE_ORDER) + \ - coin_type.value.to_bytes(1, DATA_BYTE_ORDER) + \ - coin_flag.value.to_bytes(1, DATA_BYTE_ORDER) + \ - value.to_bytes(1, DATA_BYTE_ORDER) - - assert actual_bytes == expected_bytes From 47a55b315184ac185ba2ff80cac8b678d184b798 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Sat, 4 Jul 2020 03:45:30 +0900 Subject: [PATCH 33/40] Fix bugs in normalize functions * a: int = None -> a: Union[int, None] = None * Add is_struct_valid() and check_if_struct_is_valid() --- iconservice/iconscore/typing/element.py | 92 ++++++++++++++++--- .../iconscore/typing/test_element.py | 57 +++++++++++- 2 files changed, 136 insertions(+), 13 deletions(-) diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 0b2a70c46..c078c527e 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -22,13 +22,14 @@ Signature, Parameter, ) -from typing import Union, Mapping, List, Dict, Optional, Tuple, Any +from typing import Union, Mapping, List, Dict, Any, Set from . import ( is_base_type, is_struct, get_origin, get_args, + get_annotations, name_to_type, ) from ..icon_score_constant import ( @@ -109,6 +110,10 @@ def normalize_parameter(param: Parameter) -> Parameter: else: type_hint = normalize_type_hint(annotation) + # a: int = None -> a: Union[int, None] = None + if param.default is None and get_origin(type_hint) is not Union: + type_hint = Union[type_hint, None] + check_parameter_default_type(type_hint, param.default) if type_hint == annotation: @@ -153,27 +158,54 @@ def normalize_type_hint(type_hint) -> type: origin = get_origin(type_hint) - if is_base_type(origin) or is_struct(origin): + if is_base_type(origin): return type_hint - args = get_args(type_hint) - size = len(args) + if is_struct(origin): + # Check if type_hint cycling or invalid nested type hint are present + if not is_struct_valid(origin): + raise IllegalFormatException(f"Invalid type hint: {type_hint}") + return type_hint if origin is list: - if size == 1: - return List[normalize_type_hint(args[0])] + return normalize_list_type_hint(type_hint) elif origin is dict: - if size == 2 and args[0] is str: - return Dict[str, normalize_type_hint(args[1])] + return normalize_dict_type_hint(type_hint) elif origin is Union: - if size == 2 and type(None) in args: - arg = args[0] if args[1] is type(None) else args[1] - if arg is not None and arg: - return Union[normalize_type_hint(arg), None] + return normalize_union_type_hint(type_hint) raise IllegalFormatException(f"Unsupported type hint: {type_hint}") +def normalize_list_type_hint(type_hint: type) -> type: + args = get_args(type_hint) + + if len(args) == 1: + return List[normalize_type_hint(args[0])] + + raise IllegalFormatException(f"Invalid type hint: {type_hint}") + + +def normalize_dict_type_hint(type_hint: type) -> type: + args = get_args(type_hint) + + if len(args) == 2 and args[0] is str: + return Dict[str, normalize_type_hint(args[1])] + + raise IllegalFormatException(f"Invalid type hint: {type_hint}") + + +def normalize_union_type_hint(type_hint: type) -> type: + args = get_args(type_hint) + + if len(args) == 2 and type(None) in args: + arg = args[0] if args[1] is type(None) else args[1] + if arg is not None and arg: + return Union[normalize_type_hint(arg), None] + + raise IllegalFormatException(f"Invalid type hint: {type_hint}") + + def verify_score_flag(flag: ScoreFlag): """Check if score flag combination is valid @@ -384,3 +416,39 @@ def get_score_element_metadata(score, func_name: str) -> ScoreElementMetadata: except KeyError: raise MethodNotFoundException( f"Method not found: {type(score).__name__}.{func_name}") + + +def is_struct_valid(type_hint: type) -> bool: + try: + check_if_struct_is_valid(type_hint) + except: + return False + + return True + + +def check_if_struct_is_valid(type_hint: type, structs: Set[Any] = None): + if structs is None: + structs = set() + + if type_hint in structs: + raise IllegalFormatException(f"Circular type hint: {type_hint}") + + structs.add(type_hint) + annotations = get_annotations(type_hint, None) + + for type_hint in annotations.values(): + origin = get_origin(type_hint) + if is_base_type(origin): + continue + + if origin is list: + normalize_list_type_hint(type_hint) + elif origin is dict: + normalize_dict_type_hint(type_hint) + elif origin is Union: + normalize_union_type_hint(type_hint) + elif is_struct(origin): + check_if_struct_is_valid(type_hint, structs) + else: + raise IllegalFormatException(f"Invalid type hint: {type_hint}") diff --git a/tests/unit_test/iconscore/typing/test_element.py b/tests/unit_test/iconscore/typing/test_element.py index 3b403ecc2..8d033b2eb 100644 --- a/tests/unit_test/iconscore/typing/test_element.py +++ b/tests/unit_test/iconscore/typing/test_element.py @@ -13,6 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +from inspect import ( + Parameter +) from typing import List, Dict, Union, Optional, ForwardRef import pytest @@ -23,6 +26,7 @@ from iconservice.iconscore.icon_score_constant import ScoreFlag from iconservice.iconscore.typing.element import ( normalize_type_hint, + normalize_parameter, verify_score_flag, check_parameter_default_type, ) @@ -33,6 +37,29 @@ class Person(TypedDict): age: int +class InvalidListType(TypedDict): + name: str + wallets: List + + +class InvalidDictType(TypedDict): + persons: Dict[int, str] + + +class InvalidUnionType(TypedDict): + value: Union[bool, int, str] + + +class ValidNestedType(TypedDict): + value: int + nested: Person + + +class InvalidNestedType(TypedDict): + value: int + nested: InvalidListType + + @pytest.mark.parametrize( "type_hint,expected", [ @@ -41,6 +68,7 @@ class Person(TypedDict): (int, int), (str, str), (Address, Address), + (Person, Person), (list, None), (List, None), (List[bool], List[bool]), @@ -87,9 +115,14 @@ class Person(TypedDict): (Optional[ForwardRef("Address")], None), (Dict[str, ForwardRef("Address")], None), (Union[ForwardRef("Person"), None], None), + (InvalidListType, None), + (InvalidDictType, None), + (InvalidUnionType, None), + (InvalidNestedType, None), + (ValidNestedType, ValidNestedType), ] ) -def test_normalize_abnormal_type_hint(type_hint, expected): +def test_normalize_type_hint(type_hint, expected): try: ret = normalize_type_hint(type_hint) except IllegalFormatException: @@ -157,3 +190,25 @@ def test_check_parameter_default_type(type_hint, default, success): else: with pytest.raises(InvalidParamsException): check_parameter_default_type(type_hint, default) + + +@pytest.mark.parametrize( + "type_hint,default,expected", + [ + (int, 0, int), + (int, None, Optional[int]), + (int, None, Union[int, None]), + (Person, None, Optional[Person]), + (Person, Parameter.empty, Person), + (List[str], None, Union[List[str], None]), + (Dict[str, int], None, Union[Dict[str, int], None]), + (Union[str, None], None, Union[str, None]), + (Union[str, None], Parameter.empty, Union[str, None]), + (Optional[int], Parameter.empty, Union[int, None]), + ] +) +def test_normalize_parameter(type_hint, default, expected): + parameter = Parameter( + "a", Parameter.POSITIONAL_ONLY, default=default, annotation=type_hint) + new_parameter = normalize_parameter(parameter) + assert new_parameter.annotation == expected From e7180820fdb124ef51449ca3f4ddf8380e4bf7ae Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Sat, 4 Jul 2020 11:20:50 +0900 Subject: [PATCH 34/40] Consider Union type in type conversion and verification --- iconservice/iconscore/typing/__init__.py | 2 +- iconservice/iconscore/typing/conversion.py | 5 ++++- iconservice/iconscore/typing/verification.py | 9 ++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/iconservice/iconscore/typing/__init__.py b/iconservice/iconscore/typing/__init__.py index 70b230af1..60100fb44 100644 --- a/iconservice/iconscore/typing/__init__.py +++ b/iconservice/iconscore/typing/__init__.py @@ -63,7 +63,7 @@ def get_origin(type_hint: type) -> Optional[type]: return getattr(type_hint, "__origin__", None) -def get_args(type_hint: type) -> Tuple[type, ...]: +def get_args(type_hint: type) -> Tuple[type]: return getattr(type_hint, "__args__", ()) diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index 20eb38bb1..546f6eb9f 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -15,7 +15,7 @@ from enum import Flag, auto from inspect import Signature, Parameter -from typing import Optional, Dict, Union, Type, List, Any +from typing import Optional, Dict, Union, Type, List, Any, Tuple from . import ( BaseObject, @@ -180,4 +180,7 @@ def str_to_object(value: Union[str, list, dict, None], type_hint: type) -> Any: type_hint = args[1] return {k: str_to_object(v, type_hint) for k, v in value.items()} + if origin is Union: + return str_to_object(value, args[0]) + raise InvalidParamsException(f"Failed to convert: value={value} type={type_hint}") diff --git a/iconservice/iconscore/typing/verification.py b/iconservice/iconscore/typing/verification.py index e04d7b077..879cee1a2 100644 --- a/iconservice/iconscore/typing/verification.py +++ b/iconservice/iconscore/typing/verification.py @@ -17,7 +17,7 @@ Signature, Parameter, ) -from typing import Optional, Tuple, Dict, Any, List, Mapping +from typing import Optional, Tuple, Dict, Any, List, Mapping, Union from typing_extensions import TypedDict @@ -112,6 +112,8 @@ def verify_type_hint(value: Any, type_hint: type): verify_list_type_hint(value, type_hint) elif origin is dict: verify_dict_type_hint(value, type_hint) + elif origin is Union: + verify_union_type_hint(value, type_hint) else: raise TypeError @@ -143,3 +145,8 @@ def verify_dict_type_hint(values: Dict[str, Any], type_hint: type): raise TypeError verify_type_hint(v, value_type_hint) + + +def verify_union_type_hint(value: Union[Any, None], type_hint: type): + args = get_args(type_hint) + verify_type_hint(value, args[0]) From dbcbb0e1c2b9b3cfad125b675269b91133c1dbbe Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Sun, 5 Jul 2020 00:05:24 +0900 Subject: [PATCH 35/40] Fix minor bugs * Fix a bug in merge_arguments * Replace dict with OrderedDict in str_to_object() --- iconservice/iconscore/typing/conversion.py | 18 ++++++----- iconservice/iconscore/typing/element.py | 23 +++++++------- iconservice/iconscore/typing/verification.py | 21 +++++++++---- .../iconscore/typing/test_element.py | 2 +- .../iconscore/typing/test_verification.py | 30 ++++++++++++++----- 5 files changed, 63 insertions(+), 31 deletions(-) diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index 546f6eb9f..6b219ec49 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -13,9 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from collections import OrderedDict from enum import Flag, auto from inspect import Signature, Parameter -from typing import Optional, Dict, Union, Type, List, Any, Tuple +from typing import Optional, Dict, Union, Type, List, Any from . import ( BaseObject, @@ -141,7 +142,7 @@ def convert_score_parameters( def _verify_arguments(params: Dict[str, Any], sig: Signature): - """ + """Check if all required arguments are present :param params: :param sig: normalized signature @@ -152,14 +153,14 @@ def _verify_arguments(params: Dict[str, Any], sig: Signature): for k in parameters: parameter: Parameter = parameters[k] if k not in params and parameter.default is Parameter.empty: - raise InvalidParamsException(f"Parameter not found: {k}") + raise InvalidParamsException(f"Argument not found: {k}") def str_to_object(value: Union[str, list, dict, None], type_hint: type) -> Any: if value is None: return None - if type(value) not in (str, list, dict): + if not isinstance(value, (dict, list, str)): raise InvalidParamsException(f"Invalid value type: {value}") origin = get_origin(type_hint) @@ -169,7 +170,9 @@ def str_to_object(value: Union[str, list, dict, None], type_hint: type) -> Any: if is_struct(origin): annotations = get_annotations(origin, None) - return {k: str_to_object(v, annotations[k]) for k, v in value.items()} + return OrderedDict( + (k, str_to_object(v, annotations[k])) for k, v in value.items() + ) args = get_args(type_hint) @@ -177,8 +180,9 @@ def str_to_object(value: Union[str, list, dict, None], type_hint: type) -> Any: return [str_to_object(i, args[0]) for i in value] if origin is dict: - type_hint = args[1] - return {k: str_to_object(v, type_hint) for k, v in value.items()} + return OrderedDict( + (k, str_to_object(v, type_hint=args[1])) for k, v in value.items() + ) if origin is Union: return str_to_object(value, args[0]) diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index c078c527e..057d03384 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -57,7 +57,7 @@ def normalize_signature(func: callable) -> Signature: :return: """ sig = signature(func) - params = sig.parameters + parameters = sig.parameters new_params = [] normalized = False @@ -81,15 +81,15 @@ def normalize_signature(func: callable) -> Signature: # inspect.ismethod(A().cfunc) == True is_regular_method: bool = isfunction(func) - for i, k in enumerate(params): + for i, k in enumerate(parameters): # Remove "self" parameter from signature of regular method if i == 0 and k == "self" and is_regular_method: new_param = None else: - new_param = normalize_parameter(params[k]) + new_param = normalize_parameter(parameters[k]) new_params.append(new_param) - if new_param is not params[k]: + if new_param is not parameters[k]: normalized = True return_annotation = normalize_return_annotation(sig.return_annotation) @@ -102,8 +102,11 @@ def normalize_signature(func: callable) -> Signature: return sig -def normalize_parameter(param: Parameter) -> Parameter: - annotation = param.annotation +def normalize_parameter(parameter: Parameter) -> Parameter: + if parameter.kind != Parameter.POSITIONAL_OR_KEYWORD: + raise IllegalFormatException("Invalid signature") + + annotation = parameter.annotation if annotation == Parameter.empty: type_hint = str @@ -111,16 +114,16 @@ def normalize_parameter(param: Parameter) -> Parameter: type_hint = normalize_type_hint(annotation) # a: int = None -> a: Union[int, None] = None - if param.default is None and get_origin(type_hint) is not Union: + if parameter.default is None and get_origin(type_hint) is not Union: type_hint = Union[type_hint, None] - check_parameter_default_type(type_hint, param.default) + check_parameter_default_type(type_hint, parameter.default) if type_hint == annotation: # Nothing to update - return param + return parameter - return param.replace(annotation=type_hint) + return parameter.replace(annotation=type_hint) def normalize_return_annotation(return_annotation: type) -> Union[type, Signature.empty]: diff --git a/iconservice/iconscore/typing/verification.py b/iconservice/iconscore/typing/verification.py index 879cee1a2..5a1526b6c 100644 --- a/iconservice/iconscore/typing/verification.py +++ b/iconservice/iconscore/typing/verification.py @@ -48,7 +48,7 @@ def verify_internal_call_arguments(sig: Signature, args: Optional[Tuple], kwargs params: Dict[str, Any] = {} parameters = sig.parameters - bind_arguments(params, parameters, args, kwargs) + merge_arguments(params, parameters, args, kwargs) add_default_value_to_params(params, parameters) for name, parameter in parameters.items(): @@ -65,10 +65,23 @@ def verify_internal_call_arguments(sig: Signature, args: Optional[Tuple], kwargs f"Type mismatch: name={name} type_hint={type_hint} value_type={type(value)}") -def bind_arguments( +def merge_arguments( params: Dict[str, Any], parameters: Mapping[str, Parameter], - args: Optional[Tuple], kwargs: Optional[Dict]) -> Dict[str, Any]: + args: Optional[Tuple], kwargs: Optional[Dict]): + """Merge args and kwargs to a dictionary + + Type checking and parameter default value will be handled in the next phase + + :param params: dictionary which will contain merged arguments from args and kwargs + :param parameters: function signature + :param args: arguments in tuple + :param kwargs: keyword arguments in dict + :return: + """ + if len(args) > len(parameters): + raise InvalidParamsException(f"Too many arguments") + for arg, k in zip(args, parameters): params[k] = arg @@ -81,8 +94,6 @@ def bind_arguments( params[k] = kwargs[k] - return params - def add_default_value_to_params(params: Dict[str, Any], parameters: Mapping[str, Parameter]): if len(params) == len(parameters): diff --git a/tests/unit_test/iconscore/typing/test_element.py b/tests/unit_test/iconscore/typing/test_element.py index 8d033b2eb..06de9ac04 100644 --- a/tests/unit_test/iconscore/typing/test_element.py +++ b/tests/unit_test/iconscore/typing/test_element.py @@ -209,6 +209,6 @@ def test_check_parameter_default_type(type_hint, default, success): ) def test_normalize_parameter(type_hint, default, expected): parameter = Parameter( - "a", Parameter.POSITIONAL_ONLY, default=default, annotation=type_hint) + "a", Parameter.POSITIONAL_OR_KEYWORD, default=default, annotation=type_hint) new_parameter = normalize_parameter(parameter) assert new_parameter.annotation == expected diff --git a/tests/unit_test/iconscore/typing/test_verification.py b/tests/unit_test/iconscore/typing/test_verification.py index c7ff28457..27207e600 100644 --- a/tests/unit_test/iconscore/typing/test_verification.py +++ b/tests/unit_test/iconscore/typing/test_verification.py @@ -24,7 +24,7 @@ from iconservice.iconscore.typing.verification import ( verify_internal_call_arguments, verify_type_hint, - bind_arguments, + merge_arguments, ) from . import Person @@ -52,22 +52,36 @@ def func(a: int): @pytest.mark.parametrize( - "args,kwargs", + "args,kwargs,success", [ - ((True, b"hello", 0), {}), - ((), {"a": True, "b": b"bytes", "c": 100}), - ((), {"a": True, "b": b"bytes", "c": "hello"}) + ((True, b"bytes", 0), {}, True), + ((), {"a": True, "b": b"bytes", "c": 100}, True), + ((), {"a": True, "b": b"bytes", "c": "hello"}, True), + ((True,), {"b": b"bytes", "c": 0}, True), + ((True, b"bytes"), {"c": "hello"}, True), + ((True, b"bytes"), {"c": "hello", "d": 0}, False), + ((True, b"bytes", 0, "hello"), {}, False), + ((), {"a": True, "b": b"bytes", "c": "hello", "d": 1}, False), + ((True, b"bytes", 0), {"b": b"bytes2"}, False), + ((True, b"bytes"), {}, True), + ((), {"a": False}, True), + ((True,), {"a": False}, False), + ((), {}, True), ] ) -def test_bind_arguments(args, kwargs): +def test_merge_arguments(args, kwargs, success): def func(a: bool, b: bytes, c: int): pass params = {} sig = inspect.signature(func) - params = bind_arguments(params, sig.parameters, args, kwargs) - assert len(params) == len(args) + len(kwargs) + if success: + merge_arguments(params, sig.parameters, args, kwargs) + assert len(params) == len(args) + len(kwargs) + else: + with pytest.raises(InvalidParamsException): + merge_arguments(params, sig.parameters, args, kwargs) @pytest.mark.parametrize( From e3af39a32b04a5743ab5f55a6b0bc417b25012a8 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Sun, 5 Jul 2020 01:49:55 +0900 Subject: [PATCH 36/40] Apply convert_score_parameters to DeployEngine * 3 tests remain failed --- iconservice/deploy/engine.py | 9 ++++++--- iconservice/iconscore/typing/element.py | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/iconservice/deploy/engine.py b/iconservice/deploy/engine.py index baad3062f..a786f7563 100644 --- a/iconservice/deploy/engine.py +++ b/iconservice/deploy/engine.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect import os from typing import TYPE_CHECKING @@ -34,6 +35,8 @@ from ..iconscore.icon_score_context_util import IconScoreContextUtil from ..iconscore.icon_score_mapper_object import IconScoreInfo from ..iconscore.icon_score_step import StepType, get_deploy_content_size +from ..iconscore.typing.conversion import convert_score_parameters +from ..iconscore.typing.element import normalize_signature from ..iconscore.utils import get_score_deploy_path from ..utils import is_builtin_score @@ -317,10 +320,10 @@ def _initialize_score(deploy_type: DeployType, score: 'IconScoreBase', params: d else: raise InvalidParamsException(f'Invalid deployType: {deploy_type}') - TypeConverter.adjust_params_to_method(on_init, params) + # TypeConverter.adjust_params_to_method(on_init, params) # TODO: Replace TypeConverter with convert_score_parameters by goldworm - # sig = normalize_signature(inspect.signature(on_init)) - # converted_params = convert_score_parameters(params, sig) + sig = normalize_signature(on_init) + params = convert_score_parameters(params, sig) on_init(**params) diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 057d03384..2efed9d52 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -104,7 +104,8 @@ def normalize_signature(func: callable) -> Signature: def normalize_parameter(parameter: Parameter) -> Parameter: if parameter.kind != Parameter.POSITIONAL_OR_KEYWORD: - raise IllegalFormatException("Invalid signature") + raise IllegalFormatException( + f"Invalid signature: name={parameter.name} kind={parameter.kind}") annotation = parameter.annotation From 908328c10902a6d42a770c41e639cad84ccd5a6a Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Sun, 5 Jul 2020 10:50:55 +0900 Subject: [PATCH 37/40] Skip 2 unit-tests for DeployEngine --- tests/unit_test/deploy/test_icon_score_deploy_engine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit_test/deploy/test_icon_score_deploy_engine.py b/tests/unit_test/deploy/test_icon_score_deploy_engine.py index 76b5cf716..283d9f8fe 100644 --- a/tests/unit_test/deploy/test_icon_score_deploy_engine.py +++ b/tests/unit_test/deploy/test_icon_score_deploy_engine.py @@ -424,6 +424,7 @@ def set_test(self, mocker): self.mock_score.on_update = self.on_update = Mock() self.mock_score.on_invalid = self.on_invalid = Mock() + @pytest.mark.skip("TypeConverter is replaced with convert_score_parameters()") def test_initialize_score_on_install(self, mock_engine, mocker): """case on_install""" deploy_type = DeployType.INSTALL @@ -437,6 +438,7 @@ def test_initialize_score_on_install(self, mock_engine, mocker): self.on_invalid.assert_not_called() mocker.stopall() + @pytest.mark.skip("TypeConverter is replaced with convert_score_parameters()") def test_initialize_score_case_on_update(self, mock_engine, mocker): """case on_update""" deploy_type = DeployType.UPDATE From a00afc3664dfa6727456280445dab749a547bcbc Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Sun, 5 Jul 2020 13:57:53 +0900 Subject: [PATCH 38/40] Apply convert_score_parameters to DeployEngine * Revision handling * Fix unittest failures --- iconservice/deploy/engine.py | 23 ++++++++++++------- ...st_integrate_score_oninstall_parameters.py | 5 ++-- .../deploy/test_icon_score_deploy_engine.py | 7 ++++-- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/iconservice/deploy/engine.py b/iconservice/deploy/engine.py index a786f7563..a754869b0 100644 --- a/iconservice/deploy/engine.py +++ b/iconservice/deploy/engine.py @@ -210,13 +210,17 @@ def _on_deploy(self, # score_info.get_score() returns a cached or created score instance # according to context.revision. score: 'IconScoreBase' = score_info.get_score(context.revision) - ScoreApiGenerator.check_on_deploy(context, score) + + # check_on_deploy will be done by normalize_signature() + # since Revision.THREE + if context.revision < Revision.THREE.value: + ScoreApiGenerator.check_on_deploy(context, score) # owner is set in IconScoreBase.__init__() context.msg = Message(sender=score.owner) context.tx = None - self._initialize_score(tx_params.deploy_type, score, params) + self._initialize_score(context, tx_params.deploy_type, score, params) new_tx_score_mapper[score_address] = score_info except BaseException as e: Logger.warning(f'Failed to deploy a SCORE: {score_address}', ICON_DEPLOY_LOG_TAG) @@ -306,7 +310,10 @@ def _write_score_to_score_deploy_path(context: 'IconScoreContext', IconScoreDeployer.deploy_legacy(score_deploy_path, content) @staticmethod - def _initialize_score(deploy_type: DeployType, score: 'IconScoreBase', params: dict): + def _initialize_score( + context: 'Context', + deploy_type: DeployType, + score: 'IconScoreBase', params: dict): """Call on_install() or on_update() of a SCORE only once when installing or updating it :param deploy_type: DeployType.INSTALL or DeployType.UPDATE @@ -320,10 +327,10 @@ def _initialize_score(deploy_type: DeployType, score: 'IconScoreBase', params: d else: raise InvalidParamsException(f'Invalid deployType: {deploy_type}') - # TypeConverter.adjust_params_to_method(on_init, params) - - # TODO: Replace TypeConverter with convert_score_parameters by goldworm - sig = normalize_signature(on_init) - params = convert_score_parameters(params, sig) + if context.revision < Revision.THREE.value: + TypeConverter.adjust_params_to_method(on_init, params) + else: + sig = normalize_signature(on_init) + params = convert_score_parameters(params, sig) on_init(**params) diff --git a/tests/integrate_test/test_integrate_score_oninstall_parameters.py b/tests/integrate_test/test_integrate_score_oninstall_parameters.py index 88c2bf01d..3240fb98b 100644 --- a/tests/integrate_test/test_integrate_score_oninstall_parameters.py +++ b/tests/integrate_test/test_integrate_score_oninstall_parameters.py @@ -126,11 +126,11 @@ def test_invalid_kwargs_parameter_value_oninstall(self): score_name=f"install/sample_legacy_kwargs_params", from_=self._accounts[0], to_=SYSTEM_SCORE_ADDRESS) - score_addr = tx_results[0].score_address + score_address = tx_results[0].score_address query_request = { "from": self._admin, - "to": score_addr, + "to": score_address, "dataType": "call", "data": { "method": "hello", @@ -147,4 +147,3 @@ def test_invalid_kwargs_parameter_value_oninstall(self): to_=SYSTEM_SCORE_ADDRESS, expected_status=False) raise_exception_end_tag("sample_invalid_kwargs_parameter_value_oninstall") - diff --git a/tests/unit_test/deploy/test_icon_score_deploy_engine.py b/tests/unit_test/deploy/test_icon_score_deploy_engine.py index 283d9f8fe..6fc8ddff0 100644 --- a/tests/unit_test/deploy/test_icon_score_deploy_engine.py +++ b/tests/unit_test/deploy/test_icon_score_deploy_engine.py @@ -230,7 +230,7 @@ def test_on_deploy(mock_engine, context, mocker): next_tx_hash) mock_engine._create_score_info.assert_called_with(context, SCORE_ADDRESS, next_tx_hash) score_info.get_score.assert_called_with(context.revision) - mock_engine._initialize_score.assert_called_with(deploy_type, mock_score, deploy_params) + mock_engine._initialize_score.assert_called_with(context, deploy_type, mock_score, deploy_params) assert context.msg == backup_msg assert context.tx == backup_tx @@ -455,11 +455,14 @@ def test_initialize_score_case_on_update(self, mock_engine, mocker): def test_initialize_score_invalid(self, mock_engine, mocker): """case invalid method name""" deploy_type = 'invalid' + context = Mock(spec=["revision"]) + context.revision = 0 self.set_test(mocker) with pytest.raises(InvalidParamsException) as e: - mock_engine._initialize_score(deploy_type, self.mock_score, self.params) + mock_engine._initialize_score( + context, deploy_type, self.mock_score, self.params) assert e.value.code == ExceptionCode.INVALID_PARAMETER assert e.value.message == f"Invalid deployType: {deploy_type}" From 5b9f06820c661e88526249d8b08ccc6f706b0a39 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Mon, 6 Jul 2020 22:02:14 +0900 Subject: [PATCH 39/40] Fix some bugs in iconscore/typing package * Deny Dict type as SCORE parameter * Bugfix in set_default_value_to_params() * Add related unittests --- iconservice/iconscore/typing/conversion.py | 48 +++++++---- iconservice/iconscore/typing/element.py | 29 ++++--- iconservice/iconscore/typing/verification.py | 28 ++++-- tests/unit_test/iconscore/typing/__init__.py | 13 --- .../iconscore/typing/test_convertion.py | 78 +++++++++++++++-- .../iconscore/typing/test_element.py | 33 ++++--- .../iconscore/typing/test_verification.py | 86 +++++++++++++++++-- 7 files changed, 242 insertions(+), 73 deletions(-) diff --git a/iconservice/iconscore/typing/conversion.py b/iconservice/iconscore/typing/conversion.py index 6b219ec49..897ae1dd0 100644 --- a/iconservice/iconscore/typing/conversion.py +++ b/iconservice/iconscore/typing/conversion.py @@ -16,7 +16,7 @@ from collections import OrderedDict from enum import Flag, auto from inspect import Signature, Parameter -from typing import Optional, Dict, Union, Type, List, Any +from typing import Optional, Dict, Union, Any from . import ( BaseObject, @@ -27,12 +27,10 @@ get_args, get_annotations, ) +from .verification import set_default_value_to_params from ...base.address import Address from ...base.exception import InvalidParamsException -CommonObject = Union[bool, bytes, int, str, 'Address', Dict[str, BaseObject]] -CommonType = Type[CommonObject] - def base_object_to_str(value: Any) -> str: if isinstance(value, Address): @@ -49,7 +47,7 @@ def base_object_to_str(value: Any) -> str: raise TypeError(f"Unsupported type: {type(value)}") -def object_to_str(value: Any) -> Union[List, Dict, CommonObject]: +def object_to_str(value: Any) -> Union[Any]: if is_base_type(type(value)): return base_object_to_str(value) @@ -59,6 +57,9 @@ def object_to_str(value: Any) -> Union[List, Dict, CommonObject]: if isinstance(value, dict): return {k: object_to_str(value[k]) for k in value} + if value is None: + return None + raise TypeError(f"Unsupported type: {type(value)}") @@ -126,18 +127,21 @@ def convert_score_parameters( _verify_arguments(params, sig) converted_params = {} + parameters = sig.parameters for k, v in params.items(): if not isinstance(k, str): raise InvalidParamsException(f"Invalid key type: key={k}") try: - parameter: Parameter = sig.parameters[k] + parameter: Parameter = parameters[k] converted_params[k] = str_to_object(v, parameter.annotation) except KeyError: if not (options & ConvertOption.IGNORE_UNKNOWN_PARAMS): raise InvalidParamsException(f"Unknown param: key={k} value={v}") + set_default_value_to_params(params, parameters) + return converted_params @@ -157,10 +161,7 @@ def _verify_arguments(params: Dict[str, Any], sig: Signature): def str_to_object(value: Union[str, list, dict, None], type_hint: type) -> Any: - if value is None: - return None - - if not isinstance(value, (dict, list, str)): + if not isinstance(value, (dict, list, str, type(None))): raise InvalidParamsException(f"Invalid value type: {value}") origin = get_origin(type_hint) @@ -169,10 +170,7 @@ def str_to_object(value: Union[str, list, dict, None], type_hint: type) -> Any: return str_to_base_object(value, origin) if is_struct(origin): - annotations = get_annotations(origin, None) - return OrderedDict( - (k, str_to_object(v, annotations[k])) for k, v in value.items() - ) + return str_to_object_in_struct(value, type_hint) args = get_args(type_hint) @@ -185,6 +183,24 @@ def str_to_object(value: Union[str, list, dict, None], type_hint: type) -> Any: ) if origin is Union: - return str_to_object(value, args[0]) + # Assume that only the specific type of Union (= Optional) is allowed in iconservice + return None if value is None else str_to_object(value, args[0]) + + raise InvalidParamsException(f"Type mismatch: value={value} type_hint={type_hint}") + + +def str_to_object_in_struct(value: Dict[str, Optional[str]], type_hint: type) -> Dict[str, Any]: + annotations = get_annotations(type_hint, None) + + ret = OrderedDict() + + for k, v in value.items(): + if k not in annotations: + raise InvalidParamsException(f"Unknown field in struct: key={k}") + + ret[k] = str_to_object(v, annotations[k]) + + if len(ret) != len(annotations): + raise InvalidParamsException(f"Missing field in struct") - raise InvalidParamsException(f"Failed to convert: value={value} type={type_hint}") + return ret diff --git a/iconservice/iconscore/typing/element.py b/iconservice/iconscore/typing/element.py index 2efed9d52..2a5ccb678 100644 --- a/iconservice/iconscore/typing/element.py +++ b/iconservice/iconscore/typing/element.py @@ -22,7 +22,7 @@ Signature, Parameter, ) -from typing import Union, Mapping, List, Dict, Any, Set +from typing import Union, Mapping, List, Any, Set from . import ( is_base_type, @@ -32,6 +32,7 @@ get_annotations, name_to_type, ) +from . import isinstance_ex from ..icon_score_constant import ( CONST_SCORE_FLAG, ScoreFlag, @@ -146,19 +147,18 @@ def check_parameter_default_type(type_hint: type, default: Any): else: default_type = origin - if not isinstance(default, default_type): + if not isinstance_ex(default, default_type): raise InvalidParamsException( - f'Default params type mismatch. value={default} type={type_hint}') - - if type(default) is bool and origin is not bool: - raise InvalidParamsException( - f'Default params type mismatch. value={default} type={type_hint}') + f'Default value type mismatch. value={default} type={type_hint}') def normalize_type_hint(type_hint) -> type: # If type hint is str, convert it to type hint if isinstance(type_hint, str): - type_hint = name_to_type(type_hint) + if type_hint == "Address": + type_hint = name_to_type(type_hint) + else: + raise IllegalFormatException(f"Invalid type hint: {repr(type_hint)}") origin = get_origin(type_hint) @@ -191,12 +191,15 @@ def normalize_list_type_hint(type_hint: type) -> type: def normalize_dict_type_hint(type_hint: type) -> type: - args = get_args(type_hint) + raise IllegalFormatException(f"Dict not supported: {type_hint}") - if len(args) == 2 and args[0] is str: - return Dict[str, normalize_type_hint(args[1])] - - raise IllegalFormatException(f"Invalid type hint: {type_hint}") + # TODO: To support Dict type as SCORE parameter comment out the code below by goldworm + # args = get_args(type_hint) + # + # if len(args) == 2 and args[0] is str: + # return Dict[str, normalize_type_hint(args[1])] + # + # raise IllegalFormatException(f"Invalid type hint: {type_hint}") def normalize_union_type_hint(type_hint: type) -> type: diff --git a/iconservice/iconscore/typing/verification.py b/iconservice/iconscore/typing/verification.py index 5a1526b6c..99181516e 100644 --- a/iconservice/iconscore/typing/verification.py +++ b/iconservice/iconscore/typing/verification.py @@ -19,8 +19,6 @@ ) from typing import Optional, Tuple, Dict, Any, List, Mapping, Union -from typing_extensions import TypedDict - from . import ( get_origin, get_args, @@ -49,7 +47,7 @@ def verify_internal_call_arguments(sig: Signature, args: Optional[Tuple], kwargs parameters = sig.parameters merge_arguments(params, parameters, args, kwargs) - add_default_value_to_params(params, parameters) + set_default_value_to_params(params, parameters) for name, parameter in parameters.items(): if name not in params: @@ -60,7 +58,7 @@ def verify_internal_call_arguments(sig: Signature, args: Optional[Tuple], kwargs try: verify_type_hint(value, type_hint) - except: + except TypeError: raise InvalidParamsException( f"Type mismatch: name={name} type_hint={type_hint} value_type={type(value)}") @@ -95,18 +93,27 @@ def merge_arguments( params[k] = kwargs[k] -def add_default_value_to_params(params: Dict[str, Any], parameters: Mapping[str, Parameter]): +def set_default_value_to_params(params: Dict[str, Any], parameters: Mapping[str, Parameter]): + """Set default parameter value to missing parameter + + If No default parameter value is available for missing argument, an exception is raised + + :param params: + :param parameters: + :return: + """ if len(params) == len(parameters): return - # fill default values in params: + # Set default parameter values to missing arguments for k in parameters: if k in params: continue parameter = parameters[k] - if parameter is Parameter.empty: - raise InvalidParamsException(f"Argument not found: name={k}") + if parameter.default is Parameter.empty: + raise InvalidParamsException( + f"Missing argument: name={k} type={parameter.annotation}") params[k] = parameter.default @@ -129,11 +136,14 @@ def verify_type_hint(value: Any, type_hint: type): raise TypeError -def verify_struct_type_hint(value: TypedDict, type_hint: type): +def verify_struct_type_hint(value: Dict[str, Any], type_hint: type): annotations = get_annotations(type_hint, None) assert annotations is not None for name, type_hint in annotations.items(): + if name not in value: + raise InvalidParamsException(f"Missing field in struct: name={name}") + verify_type_hint(value[name], type_hint) diff --git a/tests/unit_test/iconscore/typing/__init__.py b/tests/unit_test/iconscore/typing/__init__.py index 2add19eb5..8291f7cea 100644 --- a/tests/unit_test/iconscore/typing/__init__.py +++ b/tests/unit_test/iconscore/typing/__init__.py @@ -12,16 +12,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from typing import List -from typing_extensions import TypedDict - -from iconservice.base.address import AddressPrefix, Address - - -class Person(TypedDict): - name: str - age: int - single: bool - data: bytes - wallets: List[Address] diff --git a/tests/unit_test/iconscore/typing/test_convertion.py b/tests/unit_test/iconscore/typing/test_convertion.py index b9d3d0db3..e213cd9c4 100644 --- a/tests/unit_test/iconscore/typing/test_convertion.py +++ b/tests/unit_test/iconscore/typing/test_convertion.py @@ -15,7 +15,7 @@ import inspect import os -from typing import List, Dict +from typing import List, Dict, Optional, Union import pytest from typing_extensions import TypedDict @@ -25,7 +25,9 @@ from iconservice.iconscore.typing.conversion import ( convert_score_parameters, object_to_str, + str_to_object_in_struct, ) +from iconservice.iconscore.typing.element import normalize_signature from iconservice.iconscore.typing.element import FunctionMetadata @@ -33,7 +35,12 @@ class User(TypedDict): name: str age: int single: bool - wallet: Address + wallet: Union[Address, None] + + +class Person(TypedDict): + name: str + age: Optional[int] def test_convert_score_parameters(): @@ -64,11 +71,11 @@ def func( "_struct": {"name": "hello", "age": 30, "single": True, "wallet": address}, "_list_of_struct": [ {"name": "hello", "age": 30, "single": True, "wallet": address}, - {"name": "world", "age": 40, "single": False}, + {"name": "world", "age": 40, "single": False, "wallet": address}, ], "_dict_of_str_and_struct": { "a": {"name": "hello", "age": 30, "single": True, "wallet": address}, - "b": {"age": 27}, + "b": {"name": "h", "age": 10, "single": False, "wallet": None}, }, } @@ -77,7 +84,7 @@ def func( assert params_in_object == params -def test_convert_score_parameters_with_insufficient_parameters(): +def test_convert_score_parameters_with_insufficient_params(): class TestScore: def func(self, address: Address): pass @@ -87,3 +94,64 @@ def func(self, address: Address): with pytest.raises(InvalidParamsException): function = FunctionMetadata(TestScore.func) convert_score_parameters(params, function.signature) + + +@pytest.mark.parametrize( + "skipped_field,success", + [ + (None, True), + ("name", False), + ("age", False), + ("single", False), + ("wallet", False), + ] +) +def test_str_to_object(skipped_field, success): + class TestScore: + def func(self, user: User): + pass + + address = Address(AddressPrefix.EOA, os.urandom(20)) + params = { + "user": { + "name": "hello", + "age": 30, + "single": True, + "wallet": address, + } + } + + if skipped_field: + del params["user"][skipped_field] + + str_params = object_to_str(params) + sig = normalize_signature(TestScore.func) + + if success: + ret = convert_score_parameters(str_params, sig) + assert ret == params + else: + with pytest.raises(InvalidParamsException): + convert_score_parameters(str_params, sig) + + +@pytest.mark.parametrize( + "age,success", + [ + (10, True), + (None, True), + (-1, False), + ] +) +def test_str_to_object_in_struct(age, success): + expected = {"name": "john"} + if age is None or age > 0: + expected["age"] = age + + params = object_to_str(expected) + + if success: + assert str_to_object_in_struct(params, Person) == expected + else: + with pytest.raises(InvalidParamsException): + str_to_object_in_struct(params, Person) diff --git a/tests/unit_test/iconscore/typing/test_element.py b/tests/unit_test/iconscore/typing/test_element.py index 06de9ac04..c15ebd1af 100644 --- a/tests/unit_test/iconscore/typing/test_element.py +++ b/tests/unit_test/iconscore/typing/test_element.py @@ -60,6 +60,11 @@ class InvalidNestedType(TypedDict): nested: InvalidListType +class InvalidNestedType2(TypedDict): + value: int + nested_dict: Dict[str, str] + + @pytest.mark.parametrize( "type_hint,expected", [ @@ -81,12 +86,12 @@ class InvalidNestedType(TypedDict): (List["Address"], None), (dict, None), (Dict, None), - (Dict[str, bool], Dict[str, bool]), - (Dict[str, bytes], Dict[str, bytes]), - (Dict[str, int], Dict[str, int]), - (Dict[str, str], Dict[str, str]), - (Dict[str, Address], Dict[str, Address]), - (Dict[str, Person], Dict[str, Person]), + (Dict[str, bool], None), + (Dict[str, bytes], None), + (Dict[str, int], None), + (Dict[str, str], None), + (Dict[str, Address], None), + (Dict[str, Person], None), (Dict[int, str], None), (Dict[str, "Address"], None), (Optional[bool], Union[bool, None]), @@ -95,7 +100,7 @@ class InvalidNestedType(TypedDict): (Optional[str], Union[str, None]), (Optional[Address], Union[Address, None]), (Optional[List[str]], Union[List[str], None]), - (Optional[Dict[str, str]], Union[Dict[str, str], None]), + (Optional[Dict[str, str]], None), (Optional[Dict], None), (Union[str], str), (Union[str, int], None), @@ -201,14 +206,22 @@ def test_check_parameter_default_type(type_hint, default, success): (Person, None, Optional[Person]), (Person, Parameter.empty, Person), (List[str], None, Union[List[str], None]), - (Dict[str, int], None, Union[Dict[str, int], None]), (Union[str, None], None, Union[str, None]), (Union[str, None], Parameter.empty, Union[str, None]), (Optional[int], Parameter.empty, Union[int, None]), + (Dict[str, int], None, None), + (List[Dict[str, int]], None, None), + (Optional[Dict[str, int]], None, None), + (Union[Dict[str, int], None], None, None), ] ) def test_normalize_parameter(type_hint, default, expected): parameter = Parameter( "a", Parameter.POSITIONAL_OR_KEYWORD, default=default, annotation=type_hint) - new_parameter = normalize_parameter(parameter) - assert new_parameter.annotation == expected + + if expected is None: + with pytest.raises(IllegalFormatException): + normalize_parameter(parameter) + else: + new_parameter = normalize_parameter(parameter) + assert new_parameter.annotation == expected diff --git a/tests/unit_test/iconscore/typing/test_verification.py b/tests/unit_test/iconscore/typing/test_verification.py index 27207e600..780b1a42a 100644 --- a/tests/unit_test/iconscore/typing/test_verification.py +++ b/tests/unit_test/iconscore/typing/test_verification.py @@ -18,6 +18,7 @@ from typing import List import pytest +from typing_extensions import TypedDict from iconservice.base.address import Address, AddressPrefix from iconservice.base.exception import InvalidParamsException @@ -25,8 +26,16 @@ verify_internal_call_arguments, verify_type_hint, merge_arguments, + set_default_value_to_params, ) -from . import Person + + +class Person(TypedDict): + name: str + age: int + single: bool + data: bytes + wallets: List[Address] @pytest.mark.parametrize( @@ -36,7 +45,7 @@ ((0,), {}, True), (None, None, False), (("hello",), {}, False), - ] + ], ) def test_verify_internal_call_arguments(args, kwargs, valid): def func(a: int): @@ -67,7 +76,7 @@ def func(a: int): ((), {"a": False}, True), ((True,), {"a": False}, False), ((), {}, True), - ] + ], ) def test_merge_arguments(args, kwargs, success): def func(a: bool, b: bytes, c: int): @@ -84,6 +93,45 @@ def func(a: bool, b: bytes, c: int): merge_arguments(params, sig.parameters, args, kwargs) +@pytest.mark.parametrize( + "_name,_age,success", + [ + ("john", 13, True), + ("", 10, True), + ("bob", None, True), + ("", None, True), + (None, 10, False), + (None, None, False), + ], +) +def test_set_default_value_to_params(_name, _age, success): + default = -1 + + def func(name: str, age: int = default): + pass + + sig = inspect.signature(func) + + params = {} + expected = {} + + if _name is not None: + expected["name"] = params["name"] = _name + + if _age is not None: + expected["age"] = params["age"] = _age + else: + expected["age"] = default + + if success: + set_default_value_to_params(params, sig.parameters) + assert params == expected + assert params["age"] == expected["age"] + else: + with pytest.raises(InvalidParamsException): + set_default_value_to_params(params, sig.parameters) + + @pytest.mark.parametrize( "value,type_hint,success", [ @@ -106,17 +154,41 @@ def func(a: bool, b: bytes, c: int): (["a", "b", "c"], List[bool], False), ( { - "name": "hello", "age": 100, "single": True, "data": b"world", + "name": "hello", + "age": 100, + "single": True, + "data": b"world", + "wallets": [Address(AddressPrefix.CONTRACT, os.urandom(20))], + }, + Person, + True, + ), + ( + { + "name": "hello", + "age": False, + "single": True, + "data": b"world", + "wallets": [Address(AddressPrefix.CONTRACT, os.urandom(20))], + }, + Person, + False, + ), + ( + { + "name": "hello", + "age": 50, + "single": True, "wallets": [Address(AddressPrefix.CONTRACT, os.urandom(20))], }, Person, - True + False, ), - ] + ], ) def test_verify_type_hint(value, type_hint, success): if success: verify_type_hint(value, type_hint) else: - with pytest.raises(expected_exception=(TypeError, KeyError)): + with pytest.raises(expected_exception=(TypeError, InvalidParamsException)): verify_type_hint(value, type_hint) From 86dbe3a89a8d1eddc3db46ba5dc7fb6a03397715 Mon Sep 17 00:00:00 2001 From: Chiwon Cho Date: Tue, 7 Jul 2020 00:51:21 +0900 Subject: [PATCH 40/40] Change supported python version: 3.7 --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 468916a51..275c05a8d 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ 'license': "Apache License 2.0", 'install_requires': requires, 'extras_require': extra_requires, - 'python_requires': '>=3.6.5, <3.8', + 'python_requires': '>=3.7, <3.8', 'entry_points': { 'console_scripts': [ 'iconservice=iconservice.icon_service_cli:main' @@ -50,7 +50,6 @@ 'Intended Audience :: System Administrators', 'Natural Language :: English', 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7' ] }