Skip to content

Commit

Permalink
Redesign lambdas and projections handling
Browse files Browse the repository at this point in the history
Fixes exxeleron#7: validate lambda expression upon construction
  • Loading branch information
maciejlach committed Oct 1, 2014
1 parent ddc224d commit 27e6bc3
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 106 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
------------------------------------------------------------------------------
qPython 1.0 Beta 5 [TBA]
qPython 1.0 Beta 5 [2014.10.01]
------------------------------------------------------------------------------

- Redesign lambdas and projections handling
- Rework collections API
- Extend QConnection class with context manager API
- Fix: reading of generic null (::) embedded in lists
Expand All @@ -11,7 +12,8 @@
qPython 1.0 Beta 4 [2014.07.04]
------------------------------------------------------------------------------

- qtemporallist: force numpy.array type conversion in case of mismatch between meta.qtype and dtype of raw list
- qtemporallist: force numpy.array type conversion in case of mismatch between
meta.qtype and dtype of raw list
- Enable Travis CI integration
- Update project meta-information

Expand Down
26 changes: 19 additions & 7 deletions doc/source/type-conversion.rst
Original file line number Diff line number Diff line change
Expand Up @@ -261,16 +261,28 @@ For example::
qlist(numpy.array([366, 121, qnull(QDATE)]), qtype = QDATE_LIST)]))


Lambdas
*******
Functions, lambdas and projections
**********************************

The q lambda is mapped to custom Python class :class:`.qtype.QLambda`::
IPC protocol type codes 100+ are used to represent functions, lambdas and
projections. These types are represented as instances of base class
:class:`.qtype.QFunction` or descendent classes:

# {x+y}
QLambda('{x+y}')
- :class:`.qtype.QLambda` - represents q lambda expression, note the expression
is required to be either:

# {x+y} [3]
QLambda('{x+y}', numpy.int64(3))
- q expression enclosed in {}, e.g.: ``{x + y}``
- k expression, e.g.: ``k){x + y}``

- :class:`.qtype.QProjection` - represents function projection with parameters::
# { x + y}[3]
QProjection([QLambda('{x+y}'), numpy.int64(3)])

.. note:: Only :class:`.qtype.QLambda` and :class:`.qtype.QProjection` are
serializable. qPython doesn't provide means to serialize other
function types.


Errors
Expand Down
43 changes: 31 additions & 12 deletions qpython/qreader.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,6 @@ def _read_object(self):
raise QReaderException('Unable to deserialize q type: %s' % hex(qtype))


@parse(QNULL)
def _read_null(self, qtype = QNULL):
self._buffer.get_byte() # ignore
return None


@parse(QERROR)
def _read_error(self, qtype = QERROR):
raise QException(self._read_symbol())
Expand Down Expand Up @@ -315,19 +309,44 @@ def _read_general_list(self, qtype = QGENERAL_LIST):
return [self._read_object() for x in xrange(length)]


@parse(QNULL)
@parse(QUNARY_FUNC)
@parse(QBINARY_FUNC)
@parse(QTERNARY_FUNC)
def _read_function(self, qtype = QNULL):
code = self._buffer.get_byte()
return None if qtype == QNULL and code == 0 else QFunction(qtype)


@parse(QLAMBDA)
def _read_lambda(self, qtype = QLAMBDA):
self._buffer.get_symbol() # skip
expression = self._read_object()
return QLambda(expression)


@parse(QLAMBDA_PART)
def _read_lambda_part(self, qtype = QLAMBDA):
length = self._buffer.get_int() - 1
qlambda = self._read_lambda(qtype)
qlambda.parameters = [ self._read_object() for x in range(length) ]
return qlambda
@parse(QCOMPOSITION_FUNC)
def _read_function_composition(self, qtype = QCOMPOSITION_FUNC):
self._read_projection(qtype) # skip
return QFunction(qtype)


@parse(QADVERB_FUNC_106)
@parse(QADVERB_FUNC_107)
@parse(QADVERB_FUNC_108)
@parse(QADVERB_FUNC_109)
@parse(QADVERB_FUNC_110)
@parse(QADVERB_FUNC_111)
def _read_adverb_function(self, qtype = QADVERB_FUNC_106):
self._read_object() # skip
return QFunction(qtype)


@parse(QPROJECTION)
def _read_projection(self, qtype = QPROJECTION):
length = self._buffer.get_int()
parameters = [ self._read_object() for x in range(length) ]
return QProjection(parameters)


def _read_bytes(self, length):
Expand Down
193 changes: 132 additions & 61 deletions qpython/qtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,63 +70,85 @@
QKEYED_TABLE 0x63
QTABLE 0x62
QLAMBDA 0x64
QLAMBDA_PART 0x68
QUNARY_FUNC 0x65
QBINARY_FUNC 0x66
QTERNARY_FUNC 0x67
QCOMPOSITION_FUNC 0x69
QADVERB_FUNC_106 0x6a
QADVERB_FUNC_107 0x6b
QADVERB_FUNC_108 0x6c
QADVERB_FUNC_109 0x6d
QADVERB_FUNC_110 0x6e
QADVERB_FUNC_111 0x6f
QPROJECTION 0x68
QERROR -0x80
================== =============
'''

import numpy
import re
import uuid


# qtype constants:
QNULL = 0x65
QGENERAL_LIST = 0x00
QBOOL = -0x01
QBOOL_LIST = 0x01
QGUID = -0x02
QGUID_LIST = 0x02
QBYTE = -0x04
QBYTE_LIST = 0x04
QSHORT = -0x05
QSHORT_LIST = 0x05
QINT = -0x06
QINT_LIST = 0x06
QLONG = -0x07
QLONG_LIST = 0x07
QFLOAT = -0x08
QFLOAT_LIST = 0x08
QDOUBLE = -0x09
QDOUBLE_LIST = 0x09
QCHAR = -0x0a
QSTRING = 0x0a
QSTRING_LIST = 0x00
QSYMBOL = -0x0b
QSYMBOL_LIST = 0x0b

QTIMESTAMP = -0x0c
QTIMESTAMP_LIST = 0x0c
QMONTH = -0x0d
QMONTH_LIST = 0x0d
QDATE = -0x0e
QDATE_LIST = 0x0e
QDATETIME = -0x0f
QDATETIME_LIST = 0x0f
QTIMESPAN = -0x10
QTIMESPAN_LIST = 0x10
QMINUTE = -0x11
QMINUTE_LIST = 0x11
QSECOND = -0x12
QSECOND_LIST = 0x12
QTIME = -0x13
QTIME_LIST = 0x13

QDICTIONARY = 0x63
QKEYED_TABLE = 0x63
QTABLE = 0x62
QLAMBDA = 0x64
QLAMBDA_PART = 0x68
QERROR = -0x80
QNULL = 0x65
QGENERAL_LIST = 0x00
QBOOL = -0x01
QBOOL_LIST = 0x01
QGUID = -0x02
QGUID_LIST = 0x02
QBYTE = -0x04
QBYTE_LIST = 0x04
QSHORT = -0x05
QSHORT_LIST = 0x05
QINT = -0x06
QINT_LIST = 0x06
QLONG = -0x07
QLONG_LIST = 0x07
QFLOAT = -0x08
QFLOAT_LIST = 0x08
QDOUBLE = -0x09
QDOUBLE_LIST = 0x09
QCHAR = -0x0a
QSTRING = 0x0a
QSTRING_LIST = 0x00
QSYMBOL = -0x0b
QSYMBOL_LIST = 0x0b

QTIMESTAMP = -0x0c
QTIMESTAMP_LIST = 0x0c
QMONTH = -0x0d
QMONTH_LIST = 0x0d
QDATE = -0x0e
QDATE_LIST = 0x0e
QDATETIME = -0x0f
QDATETIME_LIST = 0x0f
QTIMESPAN = -0x10
QTIMESPAN_LIST = 0x10
QMINUTE = -0x11
QMINUTE_LIST = 0x11
QSECOND = -0x12
QSECOND_LIST = 0x12
QTIME = -0x13
QTIME_LIST = 0x13

QDICTIONARY = 0x63
QKEYED_TABLE = 0x63
QTABLE = 0x62
QLAMBDA = 0x64
QUNARY_FUNC = 0x65
QBINARY_FUNC = 0x66
QTERNARY_FUNC = 0x67
QCOMPOSITION_FUNC = 0x69
QADVERB_FUNC_106 = 0x6a
QADVERB_FUNC_107 = 0x6b
QADVERB_FUNC_108 = 0x6c
QADVERB_FUNC_109 = 0x6d
QADVERB_FUNC_110 = 0x6e
QADVERB_FUNC_111 = 0x6f
QPROJECTION = 0x68

QERROR = -0x80



Expand Down Expand Up @@ -269,34 +291,82 @@ class QException(Exception):



class QLambda(object):
class QFunction(object):
'''Represents a q function.'''

def __init__(self, qtype):
self.qtype = qtype


def __str__(self):
return '%s#%s' % (self.__class__.__name__, self.qtype)



class QLambda(QFunction):

'''Represents a q lambda expression.
.. note:: `expression` is trimmed and required to be valid q function
(``{..}``) or k function (``k){..}``).
:Parameters:
- `expression` (`string`) - lambda expression
- `parameters` (`list`) - list of parameters for lambda expression
:raises: `ValueError`
'''
def __init__(self, expression, *parameters):
def __init__(self, expression):
QFunction.__init__(self, QLAMBDA)

if not expression:
raise ValueError('Lambda expression cannot be None or empty')

expression = expression.strip()

if not QLambda._EXPRESSION_REGEX.match(expression):
raise ValueError('Invalid lambda expression: %s' % expression)

self.expression = expression


_EXPRESSION_REGEX = re.compile(r'\s*(k\))?\s*\{.*\}')


def __str__(self):
return '%s(\'%s\')' % (self.__class__.__name__, self.expression)


def __eq__(self, other):
return (isinstance(other, self.__class__) and self.expression == other.expression)



class QProjection(QFunction):

'''Represents a q projection.
:Parameters:
- `parameters` (`list`) - list of parameters for lambda expression
'''
def __init__(self, parameters):
QFunction.__init__(self, QPROJECTION)

self.parameters = parameters


def __str__(self):
params = []
parameters_str = []
for arg in self.parameters:
params.append('%s' % arg)

return '%s(\'%s\'%s)' % (self.__class__.__name__, self.expression, ', '.join(params) if params else '')
parameters_str.append('%s' % arg)

return '%s(%s)' % (self.__class__.__name__, ', '.join(parameters_str))


def __eq__(self, other):
result = (isinstance(other, self.__class__) and self.expression == other.expression)

if not result:
return False

return (not self.parameters and not other.parameters) or \
reduce(lambda v1,v2: v1 or v2, map(lambda v: v in self.parameters, other.parameters))



def __ne__(self, other):
return not self.__eq__(other)

Expand All @@ -311,6 +381,7 @@ class Mapper(object):
def __init__(self, call_map):
self.call_map = call_map


def __call__(self, *args):
def wrap(func):
for arg in args:
Expand Down
Loading

0 comments on commit 27e6bc3

Please sign in to comment.