Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions hathor/nanocontracts/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,21 @@

from __future__ import annotations

from collections.abc import Callable
from functools import wraps
from typing import TYPE_CHECKING, Any, final

from hathor.nanocontracts.blueprint_env import BlueprintEnvironment
from hathor.nanocontracts.exception import BlueprintSyntaxError
from hathor.nanocontracts.fields.container import Container
from hathor.nanocontracts.nc_types.utils import pretty_type
from hathor.nanocontracts.types import NC_FALLBACK_METHOD, NC_INITIALIZE_METHOD, NC_METHOD_TYPE_ATTR, NCMethodType
from hathor.nanocontracts.types import (
NC_ALLOWED_ACTIONS_ATTR,
NC_FALLBACK_METHOD,
NC_INITIALIZE_METHOD,
NC_METHOD_TYPE_ATTR,
NCMethodType,
)

if TYPE_CHECKING:
from hathor.nanocontracts.nc_exec_logs import NCLogger
Expand All @@ -38,7 +47,14 @@ class _BlueprintBase(type):
This metaclass will modify the attributes and set Fields to them according to their types.
"""

def __new__(cls, name, bases, attrs, **kwargs):
def __new__(
cls: type[_BlueprintBase],
name: str,
bases: tuple[type, ...],
attrs: dict[str, Any],
/,
**kwargs: Any
) -> _BlueprintBase:
from hathor.nanocontracts.fields import make_field_for_type

# Initialize only subclasses of Blueprint.
Expand Down Expand Up @@ -68,6 +84,8 @@ def __new__(cls, name, bases, attrs, **kwargs):
# Finally, create class!
new_class = super().__new__(cls, name, bases, attrs, **kwargs)

container_fields: list[str] = []

# Create the Field instance according to each type.
for field_name, field_type in attrs[NC_FIELDS_ATTR].items():
value = getattr(new_class, field_name, None)
Expand All @@ -83,6 +101,8 @@ def __new__(cls, name, bases, attrs, **kwargs):
f'unsupported field type: `{field_name}: {pretty_type(field_type)}`'
)
setattr(new_class, field_name, field)
if field.is_container:
container_fields.append(field_name)
else:
# This is the case when a value is specified.
# Example:
Expand All @@ -91,6 +111,27 @@ def __new__(cls, name, bases, attrs, **kwargs):
# This was not implemented yet and will be extended later.
raise BlueprintSyntaxError(f'fields with default values are currently not supported: `{field_name}`')

# validation makes sure we already have it
original_init_fn = getattr(new_class, NC_INITIALIZE_METHOD)
init_containers_fn = _make_initialize_uninitialized_container_fields_fn(container_fields)

# patch initialize method so it initializes containers fields implicitly
@wraps(original_init_fn)
def patched_init_fn(self: Blueprint, *args: Any, **kwargs: Any) -> Any:
ret = original_init_fn(self, *args, **kwargs)
init_containers_fn(self)
return ret

# copy important attributes
important_attrs = [NC_METHOD_TYPE_ATTR, NC_ALLOWED_ACTIONS_ATTR, '__annotations__']
for attr in important_attrs:
setattr(patched_init_fn, attr, getattr(original_init_fn, attr))
# XXX: this attribute is important for resolving the original method's signature
setattr(patched_init_fn, '__wrapped__', original_init_fn)

# replace the original init method
setattr(new_class, NC_INITIALIZE_METHOD, patched_init_fn)

return new_class

@staticmethod
Expand Down Expand Up @@ -142,3 +183,12 @@ def syscall(self) -> BlueprintEnvironment:
def log(self) -> NCLogger:
"""Return the logger for the current contract."""
return self.syscall.__log__


def _make_initialize_uninitialized_container_fields_fn(container_fields: list[str]) -> Callable[['Blueprint'], None]:
def _initialize_uninitialized_container_fields(self: Blueprint) -> None:
for field in container_fields:
container: Container = getattr(self, field)
assert isinstance(container, Container)
container.__try_init_storage__()
return _initialize_uninitialized_container_fields
9 changes: 6 additions & 3 deletions hathor/nanocontracts/blueprint_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Collection, Optional, Sequence, final
from typing import TYPE_CHECKING, Any, Collection, Optional, Sequence, TypeAlias, final

from hathor.nanocontracts.storage import NCContractStorage
from hathor.nanocontracts.types import Amount, BlueprintId, ContractId, NCAction, TokenUid
Expand All @@ -27,6 +27,9 @@
from hathor.nanocontracts.types import NCArgs


NCAttrCache: TypeAlias = dict[bytes, Any] | None


class BlueprintEnvironment:
"""A class that holds all possible interactions a blueprint may have with the system."""

Expand All @@ -43,8 +46,8 @@ def __init__(
self.__log__ = nc_logger
self.__runner = runner
self.__storage__ = storage
# XXX: we could replace dict|None with a Cache that can be disabled, cleared, limited, etc
self.__cache__: dict[str, Any] | None = None if disable_cache else {}
# XXX: we could replace dict|None with a cache class that can be disabled, cleared, limited, etc
self.__cache__: NCAttrCache = None if disable_cache else {}

@final
@property
Expand Down
38 changes: 19 additions & 19 deletions hathor/nanocontracts/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,35 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import deque
from collections import OrderedDict, deque
from typing import TypeVar

from hathor.nanocontracts.fields.deque_field import DequeField
from hathor.nanocontracts.fields.dict_field import DictField
from hathor.nanocontracts.fields.container import TypeToContainerMap
from hathor.nanocontracts.fields.deque_container import DequeContainer
from hathor.nanocontracts.fields.dict_container import DictContainer
from hathor.nanocontracts.fields.field import Field
from hathor.nanocontracts.fields.set_field import SetField
from hathor.nanocontracts.fields.utils import TypeToFieldMap
from hathor.nanocontracts.nc_types import DEFAULT_TYPE_ALIAS_MAP, FIELD_TYPE_TO_NC_TYPE_MAP
from hathor.nanocontracts.fields.set_container import SetContainer
from hathor.nanocontracts.nc_types import ESSENTIAL_TYPE_ALIAS_MAP, FIELD_TYPE_TO_NC_TYPE_MAP
from hathor.nanocontracts.nc_types.utils import TypeAliasMap, TypeToNCTypeMap

__all__ = [
'TYPE_TO_FIELD_MAP',
'DequeField',
'DictField',
'TYPE_TO_CONTAINER_MAP',
'DequeContainer',
'DictContainer',
'Field',
'SetField',
'SetContainer',
'TypeToFieldMap',
'make_field_for_type',
]

T = TypeVar('T')

TYPE_TO_FIELD_MAP: TypeToFieldMap = {
dict: DictField,
list: DequeField, # XXX: we should really make a ListField, a deque is different from a list
set: SetField,
deque: DequeField,
# XXX: other types fallback to FIELD_TYPE_TO_NC_TYPE_MAP
TYPE_TO_CONTAINER_MAP: TypeToContainerMap = {
deque: DequeContainer,
dict: DictContainer,
OrderedDict: DictContainer,
list: DequeContainer, # XXX: we should really make a ListField, a deque is different from a list
set: SetContainer,
}


Expand All @@ -49,13 +49,13 @@ def make_field_for_type(
type_: type[T],
/,
*,
type_field_map: TypeToFieldMap = TYPE_TO_FIELD_MAP,
type_alias_map: TypeAliasMap = ESSENTIAL_TYPE_ALIAS_MAP,
type_nc_type_map: TypeToNCTypeMap = FIELD_TYPE_TO_NC_TYPE_MAP,
type_alias_map: TypeAliasMap = DEFAULT_TYPE_ALIAS_MAP,
type_container_map: TypeToContainerMap = TYPE_TO_CONTAINER_MAP,
) -> Field[T]:
""" Like Field.from_name_and_type, but with default maps.

Default arguments can't be easily added to NCType.from_type signature because of recursion.
"""
type_map = Field.TypeMap(type_alias_map, type_nc_type_map, type_field_map)
type_map = Field.TypeMap(type_alias_map, type_nc_type_map, type_container_map)
return Field.from_name_and_type(name, type_, type_map=type_map)
Loading
Loading