Skip to content

Commit

Permalink
SubClass memorization
Browse files Browse the repository at this point in the history
* SubClass memorization
* Mask could be calculated from size (all 1) and size from mask (bit_size)

Signed-off-by: Alexey Stepanov <penguinolog@gmail.com>
  • Loading branch information
penguinolog committed Dec 9, 2016
1 parent 697ee19 commit 656d904
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 59 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.rst
@@ -0,0 +1,12 @@
CHANGELOG
=========
Version 0.1.1
-------------
* Class memorization

* Mask could be calculated from size (all 1) and size from mask (bitlength)


Version 0.1.0
-------------
* Initial release: Minimally tested, API stabilization started
1 change: 0 additions & 1 deletion TODO.md
@@ -1,2 +1 @@
* Memorize classes?
* Documentation
2 changes: 1 addition & 1 deletion binfield/__init__.py
Expand Up @@ -15,5 +15,5 @@

from .binfield import BinField

__version__ = '0.1.0'
__version__ = '0.1.1'
__author__ = "Alexey Stepanov <penguinolog@gmail.com>"
148 changes: 93 additions & 55 deletions binfield/binfield.py
Expand Up @@ -55,7 +55,7 @@ def _is_valid_slice(obj):
valid_precondition = isinstance(obj, slice) and obj.step is None
if not valid_precondition:
return False
if obj.start is not None:
if obj.start is not None and obj.stop is not None:
return valid_precondition and obj.start < obj.stop
return valid_precondition

Expand Down Expand Up @@ -207,19 +207,24 @@ def __new__(mcs, name, bases, classdict):
classdict[m_key] = _make_mapping_property(m_key)

size = classdict.get('_size_', None)
mask = classdict.get('_mask_', None)
mask_from_size = None

if size is not None and not isinstance(size, int):
raise TypeError(
'Pre-defined size has invalid type: {!r}'.format(size)
)
if mask is not None and not isinstance(mask, int):
raise TypeError(
'Pre-defined mask has invalid type: {!r}'.format(mask)
)
if size is not None:
if not isinstance(size, int):
raise TypeError(
'Pre-defined size has invalid type: {!r}'.format(size)
)
mask_from_size = (1 << size) - 1

mask = classdict.get('_mask_', mask_from_size)

if size is None and mask is not None:
size = mask.bit_length()
if mask is not None:
if not isinstance(mask, int):
raise TypeError(
'Pre-defined mask has invalid type: {!r}'.format(mask)
)
if size is None:
size = mask.bit_length()

classdict['_size_'] = property(
fget=lambda _: size,
Expand Down Expand Up @@ -258,10 +263,12 @@ def __new__(mcs, name, bases, classdict):
doc="""Read-only mapping structure"""
)

classdict['_cache_'] = {} # Use for subclasses memorize

return super(BinFieldMeta, mcs).__new__(mcs, name, bases, classdict)

@classmethod
def makecls(mcs, name, mapping=None, mask=None, length=None):
def makecls(mcs, name, mapping=None, mask=None, size=None):
"""Create new BinField subclass
:param name: Class name
Expand All @@ -270,17 +277,17 @@ def makecls(mcs, name, mapping=None, mask=None, length=None):
:type mapping: dict
:param mask: Data mask for new class
:type mask: int
:param length: BinField bit length
:type length: int
:param size: BinField bit length
:type size: int
:returns: BinField subclass
"""
if mapping is not None:
classdict = mapping
classdict['_size_'] = length
classdict['_size_'] = size
classdict['_mask_'] = mask
classdict['__slots__'] = ()
else:
classdict = {'_size_': length, '_mask_': mask, '__slots__': ()}
classdict = {'_size_': size, '_mask_': mask, '__slots__': ()}
return mcs.__new__(mcs, name, (BinField, ), classdict)


Expand All @@ -295,6 +302,8 @@ class BinField(BaseBinFieldMeta): # noqa # redefinition of unused 'BinField'
"""BinField representation"""
__slots__ = ['__value', '__parent_link', '__dict__']

_cache_ = {} # Will be replaced by the same by metaclass, but helps lint

# pylint: disable=super-init-not-called
def __init__(self, x=0, base=10, _parent=None):
"""Creates new BinField object from integer value
Expand Down Expand Up @@ -502,6 +511,26 @@ def __getnewargs__(self): # PYPY requires this
def __setstate__(self, state):
self.__init__(**state) # getstate returns enough data for __init__

def _get_child_cls_(self, mask, name, clsmask, size, mapping=None):
"""
:type mask:int
:type name: str
:type mapping: dict
:param clsmask: int
:param size: int
"""
# Memorize
if (mask, name) not in self.__class__._cache_:
cls = BinFieldMeta.makecls(
name=name,
mapping=mapping,
mask=clsmask,
size=size
)
self.__class__._cache_[(mask, name)] = cls
return self.__class__._cache_[(mask, name)]

# Access as dict
def _getslice_(self, item, mapping=None, name=None):
"""Get slice from self
Expand All @@ -520,8 +549,8 @@ def _getslice_(self, item, mapping=None, name=None):

stop = (
item.stop
if (not self._size_ or item.stop < self._size_)
else self._size_
if item.stop and (not self._size_ or item.stop < self._size_)
else self._bit_size_
)

mask = (1 << stop) - 1
Expand All @@ -530,25 +559,48 @@ def _getslice_(self, item, mapping=None, name=None):
mask = mask & self._mask_

if item.start:
mask = mask >> item.start << item.start
cls = BinFieldMeta.makecls(
name=name,
mapping=mapping,
mask=mask >> item.start,
length=stop - item.start
)
return cls(
(int(self) & mask) >> item.start,
_parent=(self, item.start)
)
clsmask = mask >> item.start
mask = clsmask << item.start
start = item.start
else:
clsmask = mask
start = 0

cls = BinFieldMeta.makecls(
# Memorize
cls = self._get_child_cls_(
mask=mask,
name=name,
clsmask=clsmask,
size=stop - start,
mapping=mapping,
)
return cls((int(self) & mask) >> start, _parent=(self, start))

def _getindex_(self, item, name=None):
if self._size_ and item > self._size_:
raise IndexError(
'Index {} is out of data length {}'
''.format(item, self._size_))

if name is None:
name = '{cls}_index_{index}'.format(
cls=self.__class__.__name__,
index=item
)

mask = 1 << item

cls = self._get_child_cls_(
mask=mask,
length=stop
name=name,
clsmask=0b1, # Single bit mask is always 0b1
size=1 # Single bit
)

return cls(
(int(self) & mask) >> item,
_parent=(self, item)
)
return cls(int(self) & mask, _parent=(self, 0))

def __getitem__(self, item):
"""Extract bits
Expand All @@ -558,26 +610,7 @@ def __getitem__(self, item):
:raises: IndexError
"""
if isinstance(item, int):
# Single bit return as integer
if self._size_ and item > self._size_:
raise IndexError(
'Index {} is out of data length {}'
''.format(item, self._size_))

name = '{cls}_index_{index}'.format(
cls=self.__class__.__name__,
index=item
)

cls = BinFieldMeta.makecls(
name=name,
mask=0b1,
length=1
)
return cls(
(int(self) & (1 << item)) >> item,
_parent=(self, item)
)
return self._getindex_(item)

if _is_valid_slice(item):
return self._getslice_(item)
Expand All @@ -592,8 +625,13 @@ def __getitem__(self, item):
raise IndexError("Mapping is not available")

idx = self._mapping_.get(item)
if isinstance(idx, (int, slice, tuple, list)):
return self.__getitem__(idx)
if isinstance(idx, int):
return self._getindex_(idx, name=item)
if isinstance(idx, slice):
return self._getslice_(idx, name=item)
if isinstance(idx, (tuple, list)):
return self._getslice_(slice(*idx), name=item)

if isinstance(idx, dict): # Nested _mapping_
# Extract slice
slc = slice(*idx['_index_'])
Expand Down
6 changes: 4 additions & 2 deletions test/test_baseFunc.py
Expand Up @@ -237,10 +237,12 @@ class NestedMappedBinField(BinField):

nested_copy = copy.copy(nbf.nested_block)
self.assertFalse(nested_copy is nbf.nested_block)
# Hash will be different due to different base classes between calls
self.assertNotEqual(hash(nested_copy), hash(nbf.nested_block))
# Hash will be the same due to class memorize.
self.assertEqual(hash(nested_copy), hash(nbf.nested_block))
nested_copy.single_bit = 1
self.assertNotEqual(nbf.nested_block, nested_copy)
# Data changed -> hash changed
self.assertNotEqual(hash(nested_copy), hash(nbf.nested_block))
self.assertEqual(nbf, 0b11000001) # Original
self.assertEqual(
str(nbf),
Expand Down

0 comments on commit 656d904

Please sign in to comment.