diff --git a/find_tests.py b/find_tests.py index 6fb03ca..524a883 100644 --- a/find_tests.py +++ b/find_tests.py @@ -17,6 +17,18 @@ def test_hyphenated(self): def test_equal_int(self): self.compare('a == 1', {'a': 1}) + def test_plus_operator(self): + self.compare('a == +1', {'a': 1}) + + def test_minus(self): + self.compare('a == -1', {'a': -1}) + + def test_more_than_minus(self): + self.compare('a > -1', {'a': {'$gt': -1}}) + + def test_less_than_minus(self): + self.compare('a < -1', {'a': {'$lt': -1}}) + def test_not_equal_string(self): self.compare('a != "foo"', {'a': {'$ne': 'foo'}}) @@ -173,6 +185,10 @@ def test_polygon_and_box(self): {'$geoWithin': {'$' + shape: [[1, 2], [3, 4], [5, 6]]}}}) + def test_symver(self): + self.compare('version == symver("1.0.0 (0)")', {'version': 1000000000.0}) + + class PqlSchemaAwareTestCase(BasePqlTestCase): def compare(self, string, expected): @@ -184,6 +200,9 @@ def compare(self, string, expected): def test_sanity(self): self.compare('a == 3', {'a': 3}) + def test_minus(self): + self.compare('a == -1', {'a': -1}) + def test_invalid_field(self): with self.assertRaises(pql.ParseError) as context: self.compare('b == 3', None) diff --git a/pql/matching.py b/pql/matching.py index 22625b2..a9e6e4c 100644 --- a/pql/matching.py +++ b/pql/matching.py @@ -22,14 +22,15 @@ import bson import datetime import dateutil.parser +import re from calendar import timegm def parse_date(node): - if hasattr(node, 'n'): # it's a number! + if type(node.value) in (int, float): # it's a number! return datetime.datetime.fromtimestamp(node.n) try: - return dateutil.parser.parse(node.s) + return dateutil.parser.parse(node.value) except Exception as e: raise ParseError('Error parsing date: ' + str(e), col_offset=node.col_offset) @@ -111,6 +112,8 @@ def __init__(self, *a, **k): super(SchemaAwareParser, self).__init__(SchemaAwareOperatorMap(*a, **k)) class FieldName(AstHandler): + def handle_Constant(self, node): + return node.value def handle_Str(self, node): return node.s def handle_Name(self, name): @@ -284,8 +287,12 @@ def handle_geoIntersects(self, node): def handle_geoWithin(self, node): return {'$geoWithin': GeoShapeParser().handle(self.get_arg(node, 0))} +class SymverFunc(Func): + def handle_symver(self, node): + return self.parse_arg(node, 0, SymverField()) + class GenericFunc(StringFunc, IntFunc, ListFunc, DateTimeFunc, - IdFunc, EpochFunc, EpochUTCFunc, GeoFunc): + IdFunc, EpochFunc, EpochUTCFunc, GeoFunc, SymverFunc): pass #---Operators---# @@ -338,7 +345,7 @@ class Field(AstHandler): SPECIAL_VALUES = {'None': None, 'null': None} - def handle_NameConstant(self,node): + def handle_Constant(self, node): try: return self.SPECIAL_VALUES[str(node.value)] except KeyError: @@ -365,10 +372,21 @@ def handle_Call(self, node): return StringFunc().handle(node) def handle_Str(self, node): return node.s + def handle_Constant(self, node): + return node.value class IntField(AlgebricField): + def handle_Constant(self, node): + return node.value def handle_Num(self, node): return node.n + def handle_UnaryOp(self, node): + op_type = type(node.op) + if (op_type == ast.USub): + return - node.operand.value + else: + return node.operand.value + def handle_Call(self, node): return IntFunc().handle(node) @@ -395,6 +413,8 @@ def handle_Dict(self, node): for key, value in zip(node.keys, node.values)) class DateTimeField(AlgebricField): + def handle_Constant(self, node): + return parse_date(node.value) def handle_Str(self, node): return parse_date(node) def handle_Num(self, node): @@ -403,6 +423,8 @@ def handle_Call(self, node): return DateTimeFunc().handle(node) class EpochField(AlgebricField): + def handle_Constant(self, node): + return float(parse_date(node).strftime('%s.%f')) def handle_Str(self, node): return float(parse_date(node).strftime('%s.%f')) def handle_Num(self, node): @@ -411,6 +433,8 @@ def handle_Call(self, node): return EpochFunc().handle(node) class EpochUTCField(AlgebricField): + def handle_Constant(self, node): + return timegm(parse_date(node).timetuple()) def handle_Str(self, node): return timegm(parse_date(node).timetuple()) def handle_Num(self, node): @@ -419,11 +443,27 @@ def handle_Call(self, node): return EpochUTCFunc().handle(node) class IdField(AlgebricField): + def handle_Constant(self, node): + return bson.ObjectId(node.value) def handle_Str(self, node): return bson.ObjectId(node.s) def handle_Call(self, node): return IdFunc().handle(node) +class SymverField(AlgebricField): + re_version = r'(\d+)\.(\d+)\.(\d+)\s*\((\d+)\)' + + def handle_Constant(self, node): + d = re.findall(self.re_version, node.value)[0] + return (int(d[0]) * 1e9) + (int(d[1]) * 1e6) + (int(d[2]) * 1e3) + int(d[3]) + + def handle_Num(self, node): + d = re.findall(self.re_version, node.value)[0] + return (int(d[0]) * 1e9) + (int(d[1]) * 1e6) + (int(d[2]) * 1e3) + int(d[3]) + + def handle_Call(self, node): + return SymverFunc().handle(node) + class GenericField(IntField, BoolField, StringField, ListField, DictField, GeoField): def handle_Call(self, node): return GenericFunc().handle(node) diff --git a/setup.py b/setup.py index 43d78e6..91696fd 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -__version__ = '0.5.0' +__version__ = '0.5.1' setup(name='pql', version=__version__, @@ -12,6 +12,9 @@ "Intended Audience :: Developers", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X"], license='BSD', @@ -19,4 +22,4 @@ # require the bson.ObjectId type, It's safe to assume it won't change (famous last words) install_requires=['pymongo', 'python-dateutil'], - packages=['pql']) \ No newline at end of file + packages=['pql']) diff --git a/tox.ini b/tox.ini index 21caa3a..8dcd39c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,7 @@ [tox] -envlist = py33,py35 +envlist = py{py3,27,35,36,37,38} +skip_missing_interpreters = true + [testenv] deps=nose -commands=nosetests \ No newline at end of file +commands=nosetests