Skip to content

Commit

Permalink
Merge 6fedb00 into 85a69e8
Browse files Browse the repository at this point in the history
  • Loading branch information
cdonovick committed Jul 25, 2019
2 parents 85a69e8 + 6fedb00 commit dec2cee
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 145 deletions.
181 changes: 108 additions & 73 deletions hwtypes/adt_meta.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import itertools as it
import typing as tp
from abc import ABCMeta, abstractmethod
from collections import OrderedDict

import weakref

from types import MappingProxyType
from collections.abc import Mapping, MutableMapping
from collections import OrderedDict
from .util import TypedProperty
from .util import OrderedFrozenDict
from .util import _issubclass

__all__ = ['BoundMeta', 'TupleMeta', 'ProductMeta', 'SumMeta', 'EnumMeta']

Expand All @@ -34,6 +35,25 @@ def is_adt_type(t):

class ReservedNameError(Exception): pass

RESERVED_NAMES = frozenset({
'enumerate',
'fields',
'field_dict',
'is_bound',
'is_cached',
'value',
'value_dict',
})

RESERVED_SUNDERS = frozenset({
'_cached_',
'_fields_',
'_field_table_',
'_unbound_base_',
})

RESERVED_ATTRS = frozenset(RESERVED_NAMES | RESERVED_SUNDERS)

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

def __new__(mcs, name, bases, namespace, fields=None, **kwargs):
if '_fields_' in namespace:
raise ReservedNameError('class attribute _fields_ is reserved by the type machinery')
if '_unbound_base_' in namespace:
raise ReservedNameError('class attribute _unbound_base_ is reserved by the type machinery')
for rname in RESERVED_SUNDERS:
if rname in namespace:
raise ReservedNameError(f'class attribute {rname} is reserved by the type machinery')

bound_types = fields
has_bound_base = False
Expand All @@ -82,9 +101,9 @@ def __new__(mcs, name, bases, namespace, fields=None, **kwargs):
if hasattr(t, '_fields_cb'):
bound_types = t._fields_cb(bound_types)

namespace['_fields_'] = bound_types
namespace['_unbound_base_'] = None
t = super().__new__(mcs, name, bases, namespace, **kwargs)
t._fields_ = bound_types
t._unbound_base_ = None

if bound_types is None:
# t is a unbound type
Expand All @@ -106,13 +125,14 @@ def _fields_cb(cls, idx):
return tuple(idx)

def __getitem__(cls, idx) -> 'BoundMeta':
mcs = type(cls)
if not isinstance(idx, tp.Iterable):
idx = idx,

idx = cls._fields_cb(idx)

try:
return BoundMeta._class_cache[cls, idx]
return mcs._class_cache[cls, idx]
except KeyError:
pass

Expand All @@ -123,9 +143,10 @@ def __getitem__(cls, idx) -> 'BoundMeta':
bases.extend(b[idx] for b in cls.__bases__ if isinstance(b, BoundMeta))
bases = tuple(bases)
class_name = '{}[{}]'.format(cls.__name__, ', '.join(map(lambda t : t.__name__, idx)))
t = type(cls)(class_name, bases, {}, fields=idx)
t.__module__ = cls.__module__
BoundMeta._class_cache[cls, idx] = t

t = mcs(class_name, bases, {'__module__' : cls.__module__}, fields=idx)
mcs._class_cache[cls, idx] = t
t._cached_ = True
return t

@property
Expand All @@ -152,17 +173,21 @@ def unbound_t(cls) -> 'BoundMeta':
def __repr__(cls):
return f"{cls.__name__}"

def rebind(cls, A : type, B : type):
def rebind(cls, A: type, B: type, rebind_sub_types: bool = False):
new_fields = []
for T in cls.fields:
if T == A:
if T == A or (rebind_sub_types and _issubclass(T,A)):
new_fields.append(B)
elif isinstance(T, BoundMeta):
new_fields.append(T.rebind(A, B))
new_fields.append(T.rebind(A, B, rebind_sub_types))
else:
new_fields.append(T)
return cls.unbound_t[new_fields]

@property
def is_cached(cls):
return getattr(cls, '_cached_', False)

class TupleMeta(BoundMeta):
def __getitem__(cls, idx):
if cls.is_bound:
Expand All @@ -185,9 +210,9 @@ def enumerate(cls):
def field_dict(cls):
return MappingProxyType({idx : field for idx, field in enumerate(cls.fields)})

_RESERVED_NAMES = {'enumerate', 'fields', 'field_dict', 'is_bound', 'value', 'value_dict'}

class ProductMeta(TupleMeta):
def __new__(mcs, name, bases, namespace, **kwargs):
def __new__(mcs, name, bases, namespace, cache=False, **kwargs):
fields = {}
ns = {}
for base in bases:
Expand All @@ -197,9 +222,14 @@ def __new__(mcs, name, bases, namespace, **kwargs):
raise TypeError(f'Conflicting definitions of field {k}')
else:
fields[k] = v

for k, v in namespace.items():
if _is_sunder(k) or _is_dunder(k) or _is_descriptor(v):
if k in RESERVED_SUNDERS:
raise ReservedNameError(f'class attribute {k} is reserved by the type machinery')
elif _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, type):
if k in fields:
raise TypeError(f'Conflicting definitions of field {k}')
Expand All @@ -209,22 +239,36 @@ def __new__(mcs, name, bases, namespace, **kwargs):
ns[k] = v

if fields:
return mcs._from_fields(fields, name, bases, ns, **kwargs)
return mcs._from_fields(fields, name, bases, ns, cache, **kwargs)
else:
return super().__new__(mcs, name, bases, ns, **kwargs)

def __init__(cls, name, bases, namespace, cache=False, **kwargs):
return super().__init__(name, bases, namespace, **kwargs)

@classmethod
def _from_fields(mcs, fields, name, bases, ns, **kwargs):
def _from_fields(mcs, fields, name, bases, ns, cache, **kwargs):
if cache:
ns_idx = set()
for k,v in ns.items():
if not _is_dunder(k):
ns_idx.add((k,v))

fields_idx = tuple(map(tuple, fields.items()))
cache_idx = (fields_idx,
bases,
name,
frozenset(ns_idx),
frozenset(kwargs.items()),)

try:
return mcs._class_cache[cache_idx]
except KeyError:
pass

# not strictly necessary could iterative over class dict finding
# TypedProperty to reconstruct _field_table_ but that seems bad
if '_field_table_' in ns:
raise ReservedNameError('class attribute _field_table_ is reserved by the type machinery')
else:
ns['_field_table_'] = OrderedDict()

for field in fields:
if field in _RESERVED_NAMES:
raise ReservedNameError(f'Field name {field} is reserved by the type machinery')
field_table = dict()

def _get_tuple_base(bases):
for base in bases:
Expand Down Expand Up @@ -257,13 +301,16 @@ def prop(self, value):
for field_name, field_type in fields.items():
assert field_name not in ns
idx = idx_table[field_name]
ns['_field_table_'][field_name] = field_type
field_table[field_name] = field_type
ns[field_name] = _make_prop(field_type, idx)


# this is all really gross but I don't know how to do this cleanly
# need to build t so I can call super() in new and init
# need to exec to get proper signatures
t = super().__new__(mcs, name, bases, ns, **kwargs)
t._field_table_ = OrderedFrozenDict(field_table)
t._cahced_ = cache
gs = {name : t, 'ProductMeta' : ProductMeta}
ls = {}

Expand All @@ -286,16 +333,8 @@ def __init__(self, {type_sig}):
exec(__init__, gs, ls)
t.__init__ = ls['__init__']

product_base = None
for base in bases:
if isinstance(base, mcs):
if product_base is None:
product_base = base
else:
raise TypeError('Can only inherit from one product type')

if product_base is not None and not product_base.is_bound:
t._unbound_base_ = product_base
if cache:
mcs._class_cache[cache_idx] = t

return t

Expand All @@ -306,51 +345,46 @@ def __getitem__(cls, idx):
else:
raise TypeError("Cannot bind product types with getitem")

def __repr__(cls):
if cls.is_bound:
field_spec = ', '.join(map('{0[0]}={0[1].__name__}'.format, cls.field_dict.items()))
return f"{cls.__bases__[0].__name__}('{cls.__name__}', {field_spec})"
else:
return super().__repr__()

@property
def field_dict(cls):
return MappingProxyType(cls._field_table_)

def from_fields(cls,
class_name: str,
name: str,
fields: tp.Mapping[str, type],
module: tp.Optional[str] = None,
qualname: tp.Optional[str] = None):
cache: tp.Optional[bool] = None):

if cls.is_bound:
raise TypeError('Type must not be bound')

for field in fields:
if field in RESERVED_ATTRS:
raise ReservedNameError(f'Field name {field} is reserved by the type machinery')

ns = {}

if module is None:
module = cls.__module__
if cache is None:
cache = True

if qualname is None:
qualname = class_name

ns['__module__'] = module
ns['__qualname__'] = qualname
ns['__module__'] = cls.__module__
ns['__qualname__'] = name

return cls._from_fields(fields, class_name, (cls,), ns)
return cls._from_fields(fields, name, (cls,), ns, cache)


def rebind(cls, A : type, B : type):
def rebind(cls, A: type, B: type, rebind_sub_types: bool = False):
new_fields = OrderedDict()
for field, T in cls.field_dict.items():
if T == A:
if T == A or (rebind_sub_types and _issubclass(T,A)):
new_fields[field] = B
elif isinstance(T, BoundMeta):
new_fields[field] = T.rebind(A, B)
new_fields[field] = T.rebind(A, B, rebind_sub_types)
else:
new_fields[field] = T

if new_fields != cls.field_dict:
return cls.unbound_t.from_fields(cls.__name__, new_fields, cls.__module__, cls.__qualname__)
return cls.unbound_t.from_fields(cls.__name__, new_fields, cache=cls.is_cached)
else:
return cls

Expand All @@ -369,11 +403,12 @@ def prop(self, value):

if fields is not None:
for field in fields:
if field.__name__ in _RESERVED_NAMES:
raise ReservedNameError(f'Field name {field.__name__} is reserved by the type machinery')
elif field.__name__ in namespace:
raise TypeError(f'Field name {field.__name__} cannot be used as a class attribute')
namespace[field.__name__] = _make_prop(field)
fname = field.__name__
if fname in RESERVED_ATTRS:
raise ReservedNameError(f'Field name {fname} is reserved by the type machinery')
elif fname in namespace:
raise TypeError(f'Field name {fname} cannot be used as a class attribute')
namespace[fname] = _make_prop(field)

return super().__new__(mcs, name, bases, namespace, fields, **kwargs)

Expand All @@ -398,36 +433,36 @@ def __repr__(self):
return 'Auto()'

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

elems = {}
ns = {}

for k, v in namespace.items():
if _is_dunder(k) or _is_sunder(k) or _is_descriptor(v):
if k in RESERVED_SUNDERS:
raise ReservedNameError(f'class attribute {k} is reserved by the type machinery')
elif _is_dunder(k) or _is_sunder(k) or _is_descriptor(v):
ns[k] = v
elif k in _RESERVED_NAMES:
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)}')

ns['_field_table_'] = name_table = dict()
t = super().__new__(mcs, cls_name, bases, ns, **kwargs)
name_table = dict()

if not elems:
return t

for name, value in elems.items():
elem = t.__new__(t)
elem.__init__(value)
t.__init__(elem, value)
setattr(elem, '_name_', name)
name_table[name] = elem
setattr(t, name, elem)

t._fields_ = tuple(name_table.values())
t._field_table_ = name_table

enum_base = None
for base in bases:
Expand All @@ -454,7 +489,7 @@ def field_dict(cls):
def enumerate(cls):
yield from cls.fields

def rebind(cls, A : type, B : type):
def rebind(cls, A: type, B: type, rebind_sub_types: bool = False):
# Enums aren't bound to types
# could potentialy rebind values but that seems annoying
return cls
Loading

0 comments on commit dec2cee

Please sign in to comment.