Skip to content

Commit

Permalink
Ensure field names aren't used by the type machinery
Browse files Browse the repository at this point in the history
  • Loading branch information
cdonovick committed Jul 9, 2019
1 parent 7ea0aaf commit 4cb1391
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 8 deletions.
11 changes: 11 additions & 0 deletions hwtypes/adt.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,16 @@ def __ne__(self, other):
def __hash__(self):
return hash(self.value)

def __getattribute__(self, attr):
# prevent:
# class E(Enum):
# a = 0
# b = 1
# E.a.b == E.b
if attr in type(self).field_dict:
raise AttributeError('Cannot access enum members from enum instances')
else:
return super().__getattribute__(attr)

def new_instruction():
return EnumMeta.Auto()
30 changes: 22 additions & 8 deletions hwtypes/adt_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ def _is_dunder(name):
and name[2] != '_' and name[-3] != '_')


def _is_sunder(name):
return (len(name) > 2
and name[0] == name[-1] == '_'
and name[1] != '_' and name[-2] != '_')


def _is_descriptor(obj):
return hasattr(obj, '__get__') or hasattr(obj, '__set__') or hasattr(obj, '__delete__')

Expand All @@ -32,6 +38,8 @@ def is_adt_type(t):
return isinstance(t, BoundMeta)


class ReservedNameError(Exception): pass

# Can't have abstract metaclass https://bugs.python.org/issue36881
class BoundMeta(type): #, metaclass=ABCMeta):
# (UnboundType, (types...)) : BoundType
Expand All @@ -54,7 +62,7 @@ def __call__(cls, *args, **kwargs):

def __new__(mcs, name, bases, namespace, fields=None, **kwargs):
if '_fields_' in namespace:
raise TypeError('class attribute _fields_ is reversed by the type machinery')
raise ReservedNameError('class attribute _fields_ is reserved by the type machinery')

bound_types = fields
for base in bases:
Expand Down Expand Up @@ -145,7 +153,7 @@ def enumerate(cls):
def field_dict(cls):
return MappingProxyType({idx : field for idx, field in enumerate(cls.fields)})


_RESERVED_NAMES = {'fields', 'field_dict', 'is_bound', 'value', 'value_dict'}
class ProductMeta(TupleMeta):
def __new__(mcs, name, bases, namespace, **kwargs):
fields = {}
Expand All @@ -158,7 +166,11 @@ def __new__(mcs, name, bases, namespace, **kwargs):
else:
fields[k] = v
for k, v in namespace.items():
if isinstance(v, type):
if _is_sunder(k) or _is_dunder(k) or _is_descriptor(v):
ns[k] = v
elif k in _RESERVED_NAMES:
raise ReservedNameError(f'Field name {k} is reserved by the type machinery')
elif isinstance(v, type):
if k in fields:
raise TypeError(f'Conflicting definitions of field {k}')
else:
Expand All @@ -176,7 +188,7 @@ def from_fields(mcs, fields, name, bases, ns, **kwargs):
# not strictly necessary could iterative over class dict finding
# TypedProperty to reconstruct _field_table_ but that seems bad
if '_field_table_' in ns:
raise TypeError('class attribute _field_table_ is reversed by the type machinery')
raise ReservedNameError('class attribute _field_table_ is reserved by the type machinery')
else:
ns['_field_table_'] = dict()

Expand Down Expand Up @@ -286,16 +298,18 @@ def __repr__(self):

def __new__(mcs, cls_name, bases, namespace, **kwargs):
if '_field_table_' in namespace:
raise TypeError('class attribute _field_table_ is reversed by the type machinery')
raise ReservedNameError('class attribute _field_table_ is reserved by the type machinery')

elems = {}
ns = {}

for k, v in namespace.items():
if isinstance(v, (int, mcs.Auto)):
elems[k] = v
elif _is_dunder(k) or _is_descriptor(v):
if _is_dunder(k) or _is_sunder(k) or _is_descriptor(v):
ns[k] = v
elif k in _RESERVED_NAMES:
raise ReservedNameError(f'Field name {k} is resevsed by the type machinery')
elif isinstance(v, (int, mcs.Auto)):
elems[k] = v
else:
raise TypeError(f'Enum value should be int not {type(v)}')

Expand Down
18 changes: 18 additions & 0 deletions tests/test_adt.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
from hwtypes.adt import Product, Sum, Enum, Tuple
from hwtypes.adt_meta import _RESERVED_NAMES, ReservedNameError
from hwtypes.modifiers import new

class En1(Enum):
Expand Down Expand Up @@ -31,6 +32,9 @@ def test_enum():
assert isinstance(En1.a, Enum)
assert isinstance(En1.a, En1)

with pytest.raises(AttributeError):
En1.a.b

def test_tuple():
assert set(Tu.enumerate()) == {
Tu(En1.a, En2.c),
Expand Down Expand Up @@ -153,3 +157,17 @@ def test_repr(T):
assert isinstance(s, str)
assert s != ''


@pytest.mark.parametrize("T_field", [(Enum, '0'), (Product, 'int')])
@pytest.mark.parametrize("field_name", list(_RESERVED_NAMES))
def test_reserved(T_field, field_name):
T, field = T_field
l_dict = {'T' : T}
cls_str = f'''
class _(T):
{field_name} = {field}
'''
with pytest.raises(ReservedNameError):
exec(cls_str, l_dict)


0 comments on commit 4cb1391

Please sign in to comment.