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
21 changes: 16 additions & 5 deletions hathor/nanocontracts/faux_immutable.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

from typing_extensions import ParamSpec

# special attrs:
SKIP_VALIDATION_ATTR: str = '__skip_faux_immutability_validation__'
ALLOW_INHERITANCE_ATTR: str = '__allow_faux_inheritance__'


def _validate_faux_immutable_meta(name: str, bases: tuple[type, ...], attrs: dict[str, object]) -> None:
"""Run validations during faux-immutable class creation."""
Expand All @@ -37,18 +41,25 @@ def _validate_faux_immutable_meta(name: str, bases: tuple[type, ...], attrs: dic
'__call__',
})

# pop the attribute so the created class doesn't have it and it isn't inherited
allow_inheritance = attrs.pop(ALLOW_INHERITANCE_ATTR, False)

# Prohibit all other dunder attributes/methods.
for attr in attrs:
if '__' in attr and attr not in required_attrs | allowed_dunder:
raise TypeError(f'faux-immutable class `{name}` must not define `{attr}`')

# Prohibit inheritance on faux-immutable classes, this may be less strict in the future,
# but we may only allow bases where `type(base) is _FauxImmutableMeta`.
if len(bases) != 1 or not bases[0] is FauxImmutable:
# but we may only allow bases where `type(base) is FauxImmutableMeta`.
if len(bases) != 1:
raise TypeError('faux-immutable only allows one base')

base, = bases
if base is not FauxImmutable and not allow_inheritance:
raise TypeError(f'faux-immutable class `{name}` must inherit from `FauxImmutable` only')


class _FauxImmutableMeta(type):
class FauxImmutableMeta(type):
"""
A metaclass for faux-immutable classes.
This means the class objects themselves are immutable, that is, `__setattr__` always raises AttributeError.
Expand All @@ -60,15 +71,15 @@ def __new__(cls, name, bases, attrs, **kwargs):
# validations are just a sanity check to make sure we only apply this metaclass to classes
# that will actually become immutable, for example, using this metaclass doesn't provide
# complete faux-immutability if the class doesn't define `__slots__`.
if not attrs.get('__skip_faux_immutability_validation__', False):
if not attrs.get(SKIP_VALIDATION_ATTR, False):
_validate_faux_immutable_meta(name, bases, attrs)
return super().__new__(cls, name, bases, attrs, **kwargs)

def __setattr__(cls, name: str, value: object) -> None:
raise AttributeError(f'cannot set attribute `{name}` on faux-immutable class')


class FauxImmutable(metaclass=_FauxImmutableMeta):
class FauxImmutable(metaclass=FauxImmutableMeta):
"""
Utility superclass for creating faux-immutable classes.
Simply inherit from it to define a faux-immutable class.
Expand Down
2 changes: 1 addition & 1 deletion hathor/nanocontracts/nc_types/bytes_nc_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _from_type(cls, type_: type[B], /, *, type_map: NCType.TypeMap) -> Self:
@override
def _check_value(self, value: bytes, /, *, deep: bool) -> None:
if isclass(self._actual_type):
if not isinstance(value, self._actual_type):
if not isinstance(value, (bytes, self._actual_type)):
raise TypeError('expected {self._actual_type} instance')
else:
if not isinstance(value, bytes):
Expand Down
50 changes: 41 additions & 9 deletions hathor/nanocontracts/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import inspect
from dataclasses import dataclass
from enum import Enum, unique
from typing import Any, Callable, Generic, NewType, TypeAlias, TypeVar
from typing import Any, Callable, Generic, TypeAlias, TypeVar

from typing_extensions import override

Expand All @@ -28,28 +28,60 @@
validate_method_types,
)
from hathor.nanocontracts.exception import BlueprintSyntaxError, NCSerializationError
from hathor.nanocontracts.faux_immutable import FauxImmutableMeta
from hathor.transaction.util import bytes_to_int, int_to_bytes
from hathor.utils.typing import InnerTypeMixin

# XXX: mypy gives the following errors on all subclasses of `bytes` that use FauxImmutableMeta:
#
# Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
# bases
#
# However `bytes` metaclass appears to be `type` (just like `int`'s) and `FauxImmutableMeta` correctly inherits from
# `type`, so it seems like it's a mypy error.


# Types to be used by blueprints.
class Address(bytes):
class Address(bytes, metaclass=FauxImmutableMeta): # type: ignore[misc]
__allow_faux_inheritance__ = True
__slots__ = ()


class VertexId(bytes, metaclass=FauxImmutableMeta): # type: ignore[misc]
__slots__ = ()
__allow_faux_inheritance__ = True


class BlueprintId(VertexId): # type: ignore[misc]
__slots__ = ()
__allow_faux_inheritance__ = True


class ContractId(VertexId): # type: ignore[misc]
__slots__ = ()
__allow_faux_inheritance__ = True


class TokenUid(bytes, metaclass=FauxImmutableMeta): # type: ignore[misc]
__slots__ = ()
__allow_faux_inheritance__ = True


class TxOutputScript(bytes, metaclass=FauxImmutableMeta): # type: ignore[misc]
__slots__ = ()
__allow_faux_inheritance__ = True


class VertexId(bytes):
class Amount(int, metaclass=FauxImmutableMeta):
__slots__ = ()
__allow_faux_inheritance__ = True


class ContractId(VertexId):
class Timestamp(int, metaclass=FauxImmutableMeta):
__slots__ = ()
__allow_faux_inheritance__ = True


Amount = NewType('Amount', int)
Timestamp = NewType('Timestamp', int)
TokenUid = NewType('TokenUid', bytes)
TxOutputScript = NewType('TxOutputScript', bytes)
BlueprintId = NewType('BlueprintId', VertexId)
CallerId: TypeAlias = Address | ContractId

T = TypeVar('T')
Expand Down
134 changes: 0 additions & 134 deletions tests/nanocontracts/test_exposed_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,94 +44,6 @@
'hathor.nanocontracts.exception.NCFail.args',
'hathor.nanocontracts.exception.NCFail.some_new_attribute',
'hathor.nanocontracts.exception.NCFail.with_traceback',
'hathor.nanocontracts.types.Address.capitalize',
'hathor.nanocontracts.types.Address.center',
'hathor.nanocontracts.types.Address.count',
'hathor.nanocontracts.types.Address.decode',
'hathor.nanocontracts.types.Address.endswith',
'hathor.nanocontracts.types.Address.expandtabs',
'hathor.nanocontracts.types.Address.find',
'hathor.nanocontracts.types.Address.fromhex',
'hathor.nanocontracts.types.Address.hex',
'hathor.nanocontracts.types.Address.index',
'hathor.nanocontracts.types.Address.isalnum',
'hathor.nanocontracts.types.Address.isalpha',
'hathor.nanocontracts.types.Address.isascii',
'hathor.nanocontracts.types.Address.isdigit',
'hathor.nanocontracts.types.Address.islower',
'hathor.nanocontracts.types.Address.isspace',
'hathor.nanocontracts.types.Address.istitle',
'hathor.nanocontracts.types.Address.isupper',
'hathor.nanocontracts.types.Address.join',
'hathor.nanocontracts.types.Address.ljust',
'hathor.nanocontracts.types.Address.lower',
'hathor.nanocontracts.types.Address.lstrip',
'hathor.nanocontracts.types.Address.maketrans',
'hathor.nanocontracts.types.Address.partition',
'hathor.nanocontracts.types.Address.removeprefix',
'hathor.nanocontracts.types.Address.removesuffix',
'hathor.nanocontracts.types.Address.replace',
'hathor.nanocontracts.types.Address.rfind',
'hathor.nanocontracts.types.Address.rindex',
'hathor.nanocontracts.types.Address.rjust',
'hathor.nanocontracts.types.Address.rpartition',
'hathor.nanocontracts.types.Address.rsplit',
'hathor.nanocontracts.types.Address.rstrip',
'hathor.nanocontracts.types.Address.some_new_attribute',
'hathor.nanocontracts.types.Address.split',
'hathor.nanocontracts.types.Address.splitlines',
'hathor.nanocontracts.types.Address.startswith',
'hathor.nanocontracts.types.Address.strip',
'hathor.nanocontracts.types.Address.swapcase',
'hathor.nanocontracts.types.Address.title',
'hathor.nanocontracts.types.Address.translate',
'hathor.nanocontracts.types.Address.upper',
'hathor.nanocontracts.types.Address.zfill',
'hathor.nanocontracts.types.Amount.some_new_attribute',
'hathor.nanocontracts.types.BlueprintId.some_new_attribute',
'hathor.nanocontracts.types.ContractId.capitalize',
'hathor.nanocontracts.types.ContractId.center',
'hathor.nanocontracts.types.ContractId.count',
'hathor.nanocontracts.types.ContractId.decode',
'hathor.nanocontracts.types.ContractId.endswith',
'hathor.nanocontracts.types.ContractId.expandtabs',
'hathor.nanocontracts.types.ContractId.find',
'hathor.nanocontracts.types.ContractId.fromhex',
'hathor.nanocontracts.types.ContractId.hex',
'hathor.nanocontracts.types.ContractId.index',
'hathor.nanocontracts.types.ContractId.isalnum',
'hathor.nanocontracts.types.ContractId.isalpha',
'hathor.nanocontracts.types.ContractId.isascii',
'hathor.nanocontracts.types.ContractId.isdigit',
'hathor.nanocontracts.types.ContractId.islower',
'hathor.nanocontracts.types.ContractId.isspace',
'hathor.nanocontracts.types.ContractId.istitle',
'hathor.nanocontracts.types.ContractId.isupper',
'hathor.nanocontracts.types.ContractId.join',
'hathor.nanocontracts.types.ContractId.ljust',
'hathor.nanocontracts.types.ContractId.lower',
'hathor.nanocontracts.types.ContractId.lstrip',
'hathor.nanocontracts.types.ContractId.maketrans',
'hathor.nanocontracts.types.ContractId.partition',
'hathor.nanocontracts.types.ContractId.removeprefix',
'hathor.nanocontracts.types.ContractId.removesuffix',
'hathor.nanocontracts.types.ContractId.replace',
'hathor.nanocontracts.types.ContractId.rfind',
'hathor.nanocontracts.types.ContractId.rindex',
'hathor.nanocontracts.types.ContractId.rjust',
'hathor.nanocontracts.types.ContractId.rpartition',
'hathor.nanocontracts.types.ContractId.rsplit',
'hathor.nanocontracts.types.ContractId.rstrip',
'hathor.nanocontracts.types.ContractId.some_new_attribute',
'hathor.nanocontracts.types.ContractId.split',
'hathor.nanocontracts.types.ContractId.splitlines',
'hathor.nanocontracts.types.ContractId.startswith',
'hathor.nanocontracts.types.ContractId.strip',
'hathor.nanocontracts.types.ContractId.swapcase',
'hathor.nanocontracts.types.ContractId.title',
'hathor.nanocontracts.types.ContractId.translate',
'hathor.nanocontracts.types.ContractId.upper',
'hathor.nanocontracts.types.ContractId.zfill',
'hathor.nanocontracts.types.NCAcquireAuthorityAction.melt',
'hathor.nanocontracts.types.NCAcquireAuthorityAction.mint',
'hathor.nanocontracts.types.NCAcquireAuthorityAction.name',
Expand Down Expand Up @@ -204,52 +116,6 @@
'hathor.nanocontracts.types.SignedData.checksig',
'hathor.nanocontracts.types.SignedData.get_data_bytes',
'hathor.nanocontracts.types.SignedData.some_new_attribute',
'hathor.nanocontracts.types.Timestamp.some_new_attribute',
'hathor.nanocontracts.types.TokenUid.some_new_attribute',
'hathor.nanocontracts.types.TxOutputScript.some_new_attribute',
'hathor.nanocontracts.types.VertexId.capitalize',
'hathor.nanocontracts.types.VertexId.center',
'hathor.nanocontracts.types.VertexId.count',
'hathor.nanocontracts.types.VertexId.decode',
'hathor.nanocontracts.types.VertexId.endswith',
'hathor.nanocontracts.types.VertexId.expandtabs',
'hathor.nanocontracts.types.VertexId.find',
'hathor.nanocontracts.types.VertexId.fromhex',
'hathor.nanocontracts.types.VertexId.hex',
'hathor.nanocontracts.types.VertexId.index',
'hathor.nanocontracts.types.VertexId.isalnum',
'hathor.nanocontracts.types.VertexId.isalpha',
'hathor.nanocontracts.types.VertexId.isascii',
'hathor.nanocontracts.types.VertexId.isdigit',
'hathor.nanocontracts.types.VertexId.islower',
'hathor.nanocontracts.types.VertexId.isspace',
'hathor.nanocontracts.types.VertexId.istitle',
'hathor.nanocontracts.types.VertexId.isupper',
'hathor.nanocontracts.types.VertexId.join',
'hathor.nanocontracts.types.VertexId.ljust',
'hathor.nanocontracts.types.VertexId.lower',
'hathor.nanocontracts.types.VertexId.lstrip',
'hathor.nanocontracts.types.VertexId.maketrans',
'hathor.nanocontracts.types.VertexId.partition',
'hathor.nanocontracts.types.VertexId.removeprefix',
'hathor.nanocontracts.types.VertexId.removesuffix',
'hathor.nanocontracts.types.VertexId.replace',
'hathor.nanocontracts.types.VertexId.rfind',
'hathor.nanocontracts.types.VertexId.rindex',
'hathor.nanocontracts.types.VertexId.rjust',
'hathor.nanocontracts.types.VertexId.rpartition',
'hathor.nanocontracts.types.VertexId.rsplit',
'hathor.nanocontracts.types.VertexId.rstrip',
'hathor.nanocontracts.types.VertexId.some_new_attribute',
'hathor.nanocontracts.types.VertexId.split',
'hathor.nanocontracts.types.VertexId.splitlines',
'hathor.nanocontracts.types.VertexId.startswith',
'hathor.nanocontracts.types.VertexId.strip',
'hathor.nanocontracts.types.VertexId.swapcase',
'hathor.nanocontracts.types.VertexId.title',
'hathor.nanocontracts.types.VertexId.translate',
'hathor.nanocontracts.types.VertexId.upper',
'hathor.nanocontracts.types.VertexId.zfill',
'hathor.nanocontracts.types.fallback.some_new_attribute',
'hathor.nanocontracts.types.public.some_new_attribute',
'hathor.nanocontracts.types.view.some_new_attribute',
Expand Down
32 changes: 16 additions & 16 deletions tests/nanocontracts/test_faux_immutability.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_invalid_inheritance() -> None:
class Super:
pass

with pytest.raises(TypeError, match='faux-immutable class `Foo` must inherit from `FauxImmutable` only'):
with pytest.raises(TypeError, match='faux-immutable only allows one base'):
class Foo(FauxImmutable, Super):
__slots__ = ()

Expand Down Expand Up @@ -145,80 +145,80 @@ def class_method(cls) -> None:
# Existing attribute on class
#

# protected by _FauxImmutableMeta.__setattr__
# protected by FauxImmutableMeta.__setattr__
with pytest.raises(AttributeError, match='cannot set attribute `attr` on faux-immutable class'):
Foo.attr = 'bar'

# protected by _FauxImmutableMeta.__setattr__
# protected by FauxImmutableMeta.__setattr__
with pytest.raises(AttributeError, match='cannot set attribute `attr` on faux-immutable class'):
setattr(Foo, 'attr', 'bar')

# protected by Python itself
with pytest.raises(TypeError, match="can't apply this __setattr__ to _FauxImmutableMeta object"):
with pytest.raises(TypeError, match="can't apply this __setattr__ to FauxImmutableMeta object"):
object.__setattr__(Foo, 'attr', 'bar')

#
# Existing class attribute on class
#

# protected by _FauxImmutableMeta.__setattr__
# protected by FauxImmutableMeta.__setattr__
with pytest.raises(AttributeError, match='cannot set attribute `class_attr` on faux-immutable class'):
Foo.class_attr = 'bar'

# protected by _FauxImmutableMeta.__setattr__
# protected by FauxImmutableMeta.__setattr__
with pytest.raises(AttributeError, match='cannot set attribute `class_attr` on faux-immutable class'):
setattr(Foo, 'class_attr', 'bar')

# protected by Python itself
with pytest.raises(TypeError, match="can't apply this __setattr__ to _FauxImmutableMeta object"):
with pytest.raises(TypeError, match="can't apply this __setattr__ to FauxImmutableMeta object"):
object.__setattr__(Foo, 'class_attr', 'bar')

#
# Existing method on class
#

# protected by _FauxImmutableMeta.__setattr__
# protected by FauxImmutableMeta.__setattr__
with pytest.raises(AttributeError, match='cannot set attribute `method` on faux-immutable class'):
Foo.method = lambda self: None # type: ignore[method-assign]

# protected by _FauxImmutableMeta.__setattr__
# protected by FauxImmutableMeta.__setattr__
with pytest.raises(AttributeError, match='cannot set attribute `method` on faux-immutable class'):
setattr(Foo, 'method', lambda self: None)

# protected by Python itself
with pytest.raises(TypeError, match="can't apply this __setattr__ to _FauxImmutableMeta object"):
with pytest.raises(TypeError, match="can't apply this __setattr__ to FauxImmutableMeta object"):
object.__setattr__(Foo, 'method', lambda self: None)

#
# Existing class method on class
#

# protected by _FauxImmutableMeta.__setattr__
# protected by FauxImmutableMeta.__setattr__
with pytest.raises(AttributeError, match='cannot set attribute `class_method` on faux-immutable class'):
Foo.class_method = lambda: None # type: ignore[method-assign]

# protected by _FauxImmutableMeta.__setattr__
# protected by FauxImmutableMeta.__setattr__
with pytest.raises(AttributeError, match='cannot set attribute `class_method` on faux-immutable class'):
setattr(Foo, 'class_method', lambda self: None)

# protected by Python itself
with pytest.raises(TypeError, match="can't apply this __setattr__ to _FauxImmutableMeta object"):
with pytest.raises(TypeError, match="can't apply this __setattr__ to FauxImmutableMeta object"):
object.__setattr__(Foo, 'class_method', lambda self: None)

#
# New attribute on class
#

# protected by _FauxImmutableMeta.__setattr__
# protected by FauxImmutableMeta.__setattr__
with pytest.raises(AttributeError, match='cannot set attribute `new_class_attr` on faux-immutable class'):
Foo.new_class_attr = 'bar'

# protected by _FauxImmutableMeta.__setattr__
# protected by FauxImmutableMeta.__setattr__
with pytest.raises(AttributeError, match='cannot set attribute `new_class_attr` on faux-immutable class'):
setattr(Foo, 'new_class_attr', 'bar')

# protected by Python itself
with pytest.raises(TypeError, match="can't apply this __setattr__ to _FauxImmutableMeta object"):
with pytest.raises(TypeError, match="can't apply this __setattr__ to FauxImmutableMeta object"):
object.__setattr__(Foo, 'new_class_attr', 'bar')


Expand Down
Loading