From b64a04023e43a71b5d789b8b5a439f6d6033a95e Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Mon, 7 Oct 2013 13:53:31 -0400 Subject: [PATCH 01/11] Starts a 'view' command --- file_archive/main.py | 12 +++++++++++- file_archive/viewer.py | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 file_archive/viewer.py diff --git a/file_archive/main.py b/file_archive/main.py index a90d097..e322ff3 100644 --- a/file_archive/main.py +++ b/file_archive/main.py @@ -243,12 +243,21 @@ def cmd_verify(store, args): store.verify() +def cmd_view(store, args): + if args: + sys.stderr.write("view command accepts no argument\n") + sys.exit(1) + from .viewer import run_viewer + run_viewer(store) + + commands = { 'add': cmd_add, 'query': cmd_query, 'print': cmd_print, 'remove': cmd_remove, 'verify': cmd_verify, + 'view': cmd_view, } @@ -265,7 +274,8 @@ def main(): " or: {bin} print [-m] [-t] [key1=value1] [...]\n" " or: {bin} remove [-f] \n" " or: {bin} remove [-f] [...]\n" - " or: {bin} verify\n".format( + " or: {bin} verify\n" + " or: {bin} view\n".format( bin='file_archive')) if len(sys.argv) < 3: diff --git a/file_archive/viewer.py b/file_archive/viewer.py new file mode 100644 index 0000000..cdcadd2 --- /dev/null +++ b/file_archive/viewer.py @@ -0,0 +1,22 @@ +import sys + +try: + from PyQt4 import QtCore, QtGui +except ImportError: + sys.stderr.write("PyQt4 is required by 'file_archive view'\n") + sys.exit(3) + + +class StoreViewerWindow(QtGui.QMainWindow): + def __init__(self): + QtGui.QMainWindow.__init__(self) + + +def run_viewer(store): + application = QtGui.QApplication([]) + + window = StoreViewerWindow() + window.show() + + application.exec_() + sys.exit(0) From f02782e932c67a1ce556fb81b18108272936d2bd Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Mon, 7 Oct 2013 14:47:51 -0400 Subject: [PATCH 02/11] UI --- file_archive/viewer.py | 94 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/file_archive/viewer.py b/file_archive/viewer.py index cdcadd2..f27d1b6 100644 --- a/file_archive/viewer.py +++ b/file_archive/viewer.py @@ -1,21 +1,111 @@ import sys try: + import sip + + api2_classes = [ + 'QData', 'QDateTime', 'QString', 'QTextStream', + 'QTime', 'QUrl', 'QVariant', + ] + for cl in api2_classes: + sip.setapi(cl, 2) + from PyQt4 import QtCore, QtGui except ImportError: sys.stderr.write("PyQt4 is required by 'file_archive view'\n") sys.exit(3) +def _(s, disambiguation=None, **kwargs): + if kwargs: + s = s.format(**kwargs) + return QtCore.QCoreApplication.translate( + 'file_archive.viewer', + s, + disambiguation, + QtCore.QCoreApplication.UnicodeUTF8) + + +class SearchError(Exception): + """Error while querying the file store. + """ + + class StoreViewerWindow(QtGui.QMainWindow): - def __init__(self): + def __init__(self, store): QtGui.QMainWindow.__init__(self) + self.store = store + + searchbar = QtGui.QHBoxLayout() + + self._input = QtGui.QLineEdit() + self._input.setPlaceholderText(_(u"Enter query here")) + self._input.returnPressed.connect(self._search) + searchbar.addWidget(self._input) + + searchbutton = QtGui.QPushButton(_(u"Search")) + searchbutton.clicked.connect(self._search) + searchbar.addWidget(searchbutton) + + results = QtGui.QHBoxLayout() + + self._result_tree = QtGui.QTreeWidget() + results.addWidget(self._result_tree) + + buttons = QtGui.QVBoxLayout() + but1 = QtGui.QPushButton(_(u"Buttons")) + but1.setEnabled(False) + buttons.addWidget(but1) + but2 = QtGui.QPushButton(_(u"go here")) + buttons.addWidget(but2) + but2.setEnabled(False) + results.addLayout(buttons) + + layout = QtGui.QVBoxLayout() + layout.addLayout(searchbar) + layout.addLayout(results) + + widget = QtGui.QWidget() + widget.setLayout(layout) + self.setCentralWidget(widget) + + def _search(self): + error = None + + query = self._input.text() + query = query.split() + + if len(query) == 1 and all(o not in query[0] for o in '=<>'): + h = query[0] + try: + entries = [self.store.get(h)] + except KeyError: + error = _(u"{h} not found", h=h) + + else: + try: + entries = self._search_conditions(query) + except SearchError, e: + error = e.message + + self._result_tree.clear() + + if error is not None: + w = QtGui.QTreeWidgetItem([error]) + w.setForeground(0, QtGui.QColor(255, 0, 0)) + self._result_tree.addTopLevelItem(w) + else: + pass # TODO : display results + + def _search_condition(self, query): + pass # TODO : search from a string + def run_viewer(store): application = QtGui.QApplication([]) - window = StoreViewerWindow() + window = StoreViewerWindow(store) window.show() application.exec_() From da2f3a47356513b2d764cbbd15179753eddd6c1a Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Mon, 7 Oct 2013 15:26:30 -0400 Subject: [PATCH 03/11] Adds a query parser (based on tdparser) --- file_archive/parser.py | 81 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 file_archive/parser.py diff --git a/file_archive/parser.py b/file_archive/parser.py new file mode 100644 index 0000000..d14f851 --- /dev/null +++ b/file_archive/parser.py @@ -0,0 +1,81 @@ +from tdparser import Lexer, Parser, Token, ParserError +from tdparser.topdown import EndToken + +lexer = Lexer() + + +class Key(Token): + regexp = r'[A-Za-z_][A-Za-z0-9_]*' + + def nud(self, context): + return self + + +class Number(Token): + regexp = r'-?\d+' + type = 'int' + + def __init__(self, text): + Token.__init__(self, text) + self.value = int(text) + + def nud(self, context): + return self + + +class String(Token): + regexp = r'"(?:[^\\"]|\\\\|\\")*"' + type = 'str' + + def __init__(self, text): + Token.__init__(self, text) + self.value = text[1:-1].replace('\\"', '"').replace('\\\\', '\\') + + def nud(self, context): + return self + + +class Operator(Token): + regexp = r'[=<>]' + lbp = 10 + + def led(self, left, context): + right = context.expression(self.lbp) + if not isinstance(left, Key) and isinstance(right, Key): + left, right = right, left + elif not isinstance(left, Key): + raise ParserError("Condition does not involve a key") + if self.text == '=': + cond = 'equal' + elif self.text == '<': + cond = 'lt' + elif self.text == '>': + cond = 'gt' + return left.text, {'type': right.type, cond: right.value} + + +lexer.register_tokens(Key, Number, String, Operator) + + +def parse_expression(expression): + tokens = lexer.lex(expression) + parser = Parser(tokens) + conditions = {} + while not isinstance(parser.current_token, EndToken): + expr = parser.expression() + if isinstance(expr, Token): + raise ValueError("Found unexpected token %s in query" % expr) + key, cond = expr + prec = conditions.get(key) + if prec is not None: + if prec['type'] != cond['type']: + raise ValueError("Differing types for conditions on key %s: " + "%s, %s" % (key, prec['type'], cond['type'])) + for k in cond.keys(): + if k != 'type' and k in prec: + raise ValueError("Multiple conditions %s on key %s" % ( + k, key)) + prec.update(cond) + else: + conditions[key] = cond + return conditions From e94d01f9c47c5759f9ea9d7c9f5327d19b4e8361 Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Mon, 7 Oct 2013 15:33:11 -0400 Subject: [PATCH 04/11] Use the parser in the viewer --- file_archive/viewer.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/file_archive/viewer.py b/file_archive/viewer.py index f27d1b6..446d770 100644 --- a/file_archive/viewer.py +++ b/file_archive/viewer.py @@ -16,6 +16,16 @@ sys.exit(3) +try: + import tdparser +except ImportError: + sys.stderr.write("tdparser is required by 'file_archive view'\n") + sys.exit(3) + + +from .parser import parse_expression + + def _(s, disambiguation=None, **kwargs): if kwargs: s = s.format(**kwargs) @@ -74,10 +84,10 @@ def _search(self): error = None query = self._input.text() - query = query.split() - if len(query) == 1 and all(o not in query[0] for o in '=<>'): - h = query[0] + if len(query.split()) == 1 and all(o not in query.strip() + for o in '=<>'): + h = query.strip() try: entries = [self.store.get(h)] except KeyError: @@ -85,9 +95,11 @@ def _search(self): else: try: - entries = self._search_conditions(query) - except SearchError, e: - error = e.message + conditions = parse_expression(query) + except ValueError as e: + error = e.args[0] + else: + entries = self.store.query(conditions) self._result_tree.clear() @@ -98,9 +110,6 @@ def _search(self): else: pass # TODO : display results - def _search_condition(self, query): - pass # TODO : search from a string - def run_viewer(store): application = QtGui.QApplication([]) From aaeaf4a986459eb370b85adfa56e6317a12643fa Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Mon, 7 Oct 2013 16:11:12 -0400 Subject: [PATCH 05/11] Displays results --- file_archive/viewer.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/file_archive/viewer.py b/file_archive/viewer.py index 446d770..4f233e2 100644 --- a/file_archive/viewer.py +++ b/file_archive/viewer.py @@ -23,6 +23,7 @@ sys.exit(3) +from .compat import int_types from .parser import parse_expression @@ -61,6 +62,8 @@ def __init__(self, store): results = QtGui.QHBoxLayout() self._result_tree = QtGui.QTreeWidget() + self._result_tree.setColumnCount(3) + self._result_tree.setHeaderLabels([_(u"Key"), _(u"Value"), _(u"Type")]) results.addWidget(self._result_tree) buttons = QtGui.QVBoxLayout() @@ -108,7 +111,20 @@ def _search(self): w.setForeground(0, QtGui.QColor(255, 0, 0)) self._result_tree.addTopLevelItem(w) else: - pass # TODO : display results + for entry in entries: + file_item = QtGui.QTreeWidgetItem([entry['hash']]) + self._result_tree.addTopLevelItem(file_item) + for k, v in entry.metadata.items(): + if k == 'hash': + continue + if isinstance(v, int_types): + t = 'int' + v = '%d' % v + else: # isinstance(v, string_types): + t = 'str' + i = QtGui.QTreeWidgetItem([k, v, t]) + file_item.addChild(i) + self._result_tree.expandAll() def run_viewer(store): From bc77b433afff7ff3a1211761aabacd56bbd7bd36 Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Mon, 7 Oct 2013 17:06:02 -0400 Subject: [PATCH 06/11] Adds an 'open' button --- file_archive/viewer.py | 71 +++++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/file_archive/viewer.py b/file_archive/viewer.py index 4f233e2..187dacf 100644 --- a/file_archive/viewer.py +++ b/file_archive/viewer.py @@ -1,3 +1,5 @@ +import platform +import subprocess import sys try: @@ -27,6 +29,20 @@ from .parser import parse_expression +system = platform.system().lower() +if system == 'windows': + def openfile(filename): + subprocess.call(['cmd', '/c', 'start', filename]) +elif system == 'darwin': + def openfile(filename): + subprocess.call(['open', filename]) +elif system.startswith('linux'): + def openfile(filename): + subprocess.call(['xdg-open', filename]) +else: + openfile = None + + def _(s, disambiguation=None, **kwargs): if kwargs: s = s.format(**kwargs) @@ -42,6 +58,23 @@ class SearchError(Exception): """ +class FileItem(QtGui.QTreeWidgetItem): + def __init__(self, filehash): + QtGui.QTreeWidgetItem.__init__(self, [filehash]) + self.hash = filehash + + +class MetadataItem(FileItem): + def __init__(self, filehash, key, value): + if isinstance(value, int_types): + t = 'int' + value = '%d' % value + else: # isinstance(v, string_types): + t = 'str' + QtGui.QTreeWidgetItem.__init__(self, [key, value, t]) + self.hash = filehash + + class StoreViewerWindow(QtGui.QMainWindow): def __init__(self, store): QtGui.QMainWindow.__init__(self) @@ -64,15 +97,19 @@ def __init__(self, store): self._result_tree = QtGui.QTreeWidget() self._result_tree.setColumnCount(3) self._result_tree.setHeaderLabels([_(u"Key"), _(u"Value"), _(u"Type")]) + self._result_tree.itemSelectionChanged.connect(self._selection_changed) results.addWidget(self._result_tree) buttons = QtGui.QVBoxLayout() - but1 = QtGui.QPushButton(_(u"Buttons")) - but1.setEnabled(False) - buttons.addWidget(but1) - but2 = QtGui.QPushButton(_(u"go here")) - buttons.addWidget(but2) - but2.setEnabled(False) + self._buttons = [] + open_button = QtGui.QPushButton(_(u"Open")) + if openfile is not None: + open_button.clicked.connect(self._openfile) + self._buttons.append(open_button) + else: + open_button.setEnabled(False) + buttons.addWidget(open_button) + self._selection_changed() results.addLayout(buttons) layout = QtGui.QVBoxLayout() @@ -112,20 +149,26 @@ def _search(self): self._result_tree.addTopLevelItem(w) else: for entry in entries: - file_item = QtGui.QTreeWidgetItem([entry['hash']]) + file_item = FileItem(entry['hash']) self._result_tree.addTopLevelItem(file_item) for k, v in entry.metadata.items(): if k == 'hash': continue - if isinstance(v, int_types): - t = 'int' - v = '%d' % v - else: # isinstance(v, string_types): - t = 'str' - i = QtGui.QTreeWidgetItem([k, v, t]) - file_item.addChild(i) + file_item.addChild(MetadataItem(entry['hash'], k, v)) self._result_tree.expandAll() + def _selection_changed(self): + items = self._result_tree.selectedItems() + for button in self._buttons: + button.setEnabled( + len(items) == 1 and + isinstance(items[0], FileItem)) + + def _openfile(self): + item = self._result_tree.currentItem() + if item is not None: + openfile(self.store.get_filename(item.hash)) + def run_viewer(store): application = QtGui.QApplication([]) From d84146930f0665af5450d18aa29479e075af4fb4 Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Mon, 7 Oct 2013 17:25:53 -0400 Subject: [PATCH 07/11] Adds a 'delete' button --- file_archive/viewer.py | 50 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/file_archive/viewer.py b/file_archive/viewer.py index 187dacf..7f93ad6 100644 --- a/file_archive/viewer.py +++ b/file_archive/viewer.py @@ -83,32 +83,47 @@ def __init__(self, store): searchbar = QtGui.QHBoxLayout() + # Input line for the query self._input = QtGui.QLineEdit() self._input.setPlaceholderText(_(u"Enter query here")) self._input.returnPressed.connect(self._search) searchbar.addWidget(self._input) + # Search button searchbutton = QtGui.QPushButton(_(u"Search")) searchbutton.clicked.connect(self._search) searchbar.addWidget(searchbutton) results = QtGui.QHBoxLayout() + # Result view, as a tree with metadata self._result_tree = QtGui.QTreeWidget() self._result_tree.setColumnCount(3) self._result_tree.setHeaderLabels([_(u"Key"), _(u"Value"), _(u"Type")]) self._result_tree.itemSelectionChanged.connect(self._selection_changed) results.addWidget(self._result_tree) + # Buttons, enabled/disabled when the selection changes buttons = QtGui.QVBoxLayout() self._buttons = [] + + # Open button; uses the system to choose the program to open with + # (on Windows, might ask you what to use every time because of filename + # scheme) open_button = QtGui.QPushButton(_(u"Open")) if openfile is not None: open_button.clicked.connect(self._openfile) - self._buttons.append(open_button) + self._buttons.append(('single', open_button)) else: open_button.setEnabled(False) buttons.addWidget(open_button) + + # Delete button, removes what's selected (with confirmation) + remove_button = QtGui.QPushButton(_(u"Delete")) + remove_button.clicked.connect(self._delete) + self._buttons.append(('multi', remove_button)) + buttons.addWidget(remove_button) + self._selection_changed() results.addLayout(buttons) @@ -159,16 +174,41 @@ def _search(self): def _selection_changed(self): items = self._result_tree.selectedItems() - for button in self._buttons: - button.setEnabled( - len(items) == 1 and - isinstance(items[0], FileItem)) + for t, button in self._buttons: + if t == 'single': + button.setEnabled( + len(items) == 1 and + isinstance(items[0], FileItem)) + else: + button.setEnabled(all(isinstance(i, FileItem) for i in items)) def _openfile(self): item = self._result_tree.currentItem() if item is not None: openfile(self.store.get_filename(item.hash)) + def _delete(self): + items = self._result_tree.selectedItems() + if not items: + return + confirm = QtGui.QMessageBox.question( + self, + _(u"Are you sure?"), + _(u"You are about to delete {num} entries from the store. " + "Please confirm.", num=len(items)), + QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel, + QtGui.QMessageBox.Cancel) + if confirm == QtGui.QMessageBox.Ok: + hashes = set([i.hash for i in items]) + i = 0 + while i < self._result_tree.topLevelItemCount(): + h = self._result_tree.topLevelItem(i).hash + if h in hashes: + self.store.remove(h) + self._result_tree.takeTopLevelItem(i) + else: + i += 1 + def run_viewer(store): application = QtGui.QApplication([]) From 0c795001d75f28db9210344ad8303749f629e185 Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Tue, 8 Oct 2013 12:10:24 -0400 Subject: [PATCH 08/11] Adds tdparser to dependencies --- setup.py | 3 ++- tox.ini | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 77c8fdb..d64db8b 100644 --- a/setup.py +++ b/setup.py @@ -36,4 +36,5 @@ 'Operating System :: OS Independent', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', - 'Topic :: System :: Archiving']) + 'Topic :: System :: Archiving'], + requires=['tdparser (>=1.1.4)']) diff --git a/tox.ini b/tox.ini index 9e92d49..25587aa 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,12 @@ envlist=py26,py27,py32,py33 [testenv] commands=nosetests tests -deps=nose +deps= + nose + tdparser [testenv:py26] deps= nose unittest2 + tdparser From 2bf9e0a460af1a90af03a558f854f6e2fa807913 Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Tue, 8 Oct 2013 12:06:04 -0400 Subject: [PATCH 09/11] Adds tests for parser.py --- file_archive/parser.py | 16 ++++++++----- tests/test_parser.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 tests/test_parser.py diff --git a/file_archive/parser.py b/file_archive/parser.py index d14f851..c51c784 100644 --- a/file_archive/parser.py +++ b/file_archive/parser.py @@ -41,16 +41,20 @@ class Operator(Token): def led(self, left, context): right = context.expression(self.lbp) + inverted = False if not isinstance(left, Key) and isinstance(right, Key): left, right = right, left + inverted = True elif not isinstance(left, Key): raise ParserError("Condition does not involve a key") + elif isinstance(right, Key): + raise ParserError("Condition involves two keys") if self.text == '=': cond = 'equal' elif self.text == '<': - cond = 'lt' - elif self.text == '>': - cond = 'gt' + cond = 'gt' if inverted else 'lt' + elif self.text == '>': # pragma: no branch + cond = 'lt' if inverted else 'gt' return left.text, {'type': right.type, cond: right.value} @@ -64,16 +68,16 @@ def parse_expression(expression): while not isinstance(parser.current_token, EndToken): expr = parser.expression() if isinstance(expr, Token): - raise ValueError("Found unexpected token %s in query" % expr) + raise ParserError("Found unexpected token %s in query" % expr) key, cond = expr prec = conditions.get(key) if prec is not None: if prec['type'] != cond['type']: - raise ValueError("Differing types for conditions on key %s: " + raise ParserError("Differing types for conditions on key %s: " "%s, %s" % (key, prec['type'], cond['type'])) for k in cond.keys(): if k != 'type' and k in prec: - raise ValueError("Multiple conditions %s on key %s" % ( + raise ParserError("Multiple conditions %s on key %s" % ( k, key)) prec.update(cond) else: diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..498c87f --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,53 @@ +import tdparser +try: + import unittest2 as unittest +except ImportError: + import unittest + +from file_archive.parser import parse_expression + + +class TestParser(unittest.TestCase): + def test_single(self): + self.assertEqual(parse_expression('key1=-12'), + {'key1': {'type': 'int', 'equal': -12}}) + self.assertEqual(parse_expression('key2 = "41"'), + {'key2': {'type': 'str', 'equal': '41'}}) + self.assertEqual(parse_expression('41 < key3'), + {'key3': {'type': 'int', 'gt': 41}}) + with self.assertRaises(tdparser.MissingTokensError): + parse_expression('key4=') + self.assertEqual(parse_expression('key5 >0'), + {'key5': {'type': 'int', 'gt': 0}}) + with self.assertRaises(tdparser.LexerError): + parse_expression('key6!"somethg') + with self.assertRaises(tdparser.ParserError): + parse_expression('"foo" = "bar"') + with self.assertRaises(tdparser.ParserError): + parse_expression('key7 = key8') + with self.assertRaises(tdparser.ParserError): + parse_expression('"value"') + + def test_strings(self): + self.assertEqual(parse_expression(r'key1="some string"'), + {'key1': {'type': 'str', 'equal': "some string"}}) + self.assertEqual(parse_expression(r'key1="some \"string\""'), + {'key1': {'type': 'str', 'equal': 'some "string"'}}) + with self.assertRaises(tdparser.LexerError): + parse_expression(r'key1="some \"string') + with self.assertRaises(tdparser.LexerError): + parse_expression(r'key1="some \"string\"') + with self.assertRaises(tdparser.LexerError): + self.assertEqual(parse_expression(r'key1="'), None) + + def test_multi(self): + self.assertEqual(parse_expression('key1=-12 key2 = "4 2" 41< key3'), + {'key1': {'type': 'int', 'equal': -12}, + 'key2': {'type': 'str', 'equal': '4 2'}, + 'key3': {'type': 'int', 'gt': 41}}) + self.assertEqual(parse_expression('key1>2 key1<4'), + {'key1': {'type': 'int', 'gt': 2, 'lt': 4}}) + with self.assertRaises(tdparser.ParserError): + parse_expression('key1="str" key1<2') + with self.assertRaises(tdparser.ParserError): + parse_expression('key1="str" key1="otherstr"') From 0ffafd64109aa4a9f7a176ec45f87ceae507c5fe Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Tue, 8 Oct 2013 12:20:32 -0400 Subject: [PATCH 10/11] Minor look & feel changes to the viewer --- file_archive/viewer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/file_archive/viewer.py b/file_archive/viewer.py index 7f93ad6..2b230fe 100644 --- a/file_archive/viewer.py +++ b/file_archive/viewer.py @@ -166,6 +166,7 @@ def _search(self): for entry in entries: file_item = FileItem(entry['hash']) self._result_tree.addTopLevelItem(file_item) + self._result_tree.setFirstItemColumnSpanned(file_item, True) for k, v in entry.metadata.items(): if k == 'hash': continue @@ -180,7 +181,8 @@ def _selection_changed(self): len(items) == 1 and isinstance(items[0], FileItem)) else: - button.setEnabled(all(isinstance(i, FileItem) for i in items)) + button.setEnabled(bool(items) and + all(isinstance(i, FileItem) for i in items)) def _openfile(self): item = self._result_tree.currentItem() From f71c890ccfd30a93a457c883b5d41a9e5457c93c Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Tue, 8 Oct 2013 12:29:53 -0400 Subject: [PATCH 11/11] Adds parse_expressions() for lists of conditions --- file_archive/parser.py | 40 +++++++++++++++++++++++++++------------- tests/test_parser.py | 20 ++++++++++++++++++-- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/file_archive/parser.py b/file_archive/parser.py index c51c784..c6eccbf 100644 --- a/file_archive/parser.py +++ b/file_archive/parser.py @@ -61,6 +61,22 @@ def led(self, left, context): lexer.register_tokens(Key, Number, String, Operator) +def _update_conditions(conditions, expr): + key, cond = expr + prec = conditions.get(key) + if prec is not None: + if prec['type'] != cond['type']: + raise ParserError("Differing types for conditions on key %s: " + "%s, %s" % (key, prec['type'], cond['type'])) + for k in cond.keys(): + if k != 'type' and k in prec: + raise ParserError("Multiple conditions %s on key %s" % ( + k, key)) + prec.update(cond) + else: + conditions[key] = cond + + def parse_expression(expression): tokens = lexer.lex(expression) parser = Parser(tokens) @@ -69,17 +85,15 @@ def parse_expression(expression): expr = parser.expression() if isinstance(expr, Token): raise ParserError("Found unexpected token %s in query" % expr) - key, cond = expr - prec = conditions.get(key) - if prec is not None: - if prec['type'] != cond['type']: - raise ParserError("Differing types for conditions on key %s: " - "%s, %s" % (key, prec['type'], cond['type'])) - for k in cond.keys(): - if k != 'type' and k in prec: - raise ParserError("Multiple conditions %s on key %s" % ( - k, key)) - prec.update(cond) - else: - conditions[key] = cond + _update_conditions(conditions, expr) + return conditions + + +def parse_expressions(expression_list): + conditions = {} + for expression in expression_list: + expr = lexer.parse(expression) + if isinstance(expr, Token): + raise ParserError("Found unexpected token %s in query" % expr) + _update_conditions(conditions, expr) return conditions diff --git a/tests/test_parser.py b/tests/test_parser.py index 498c87f..d923b88 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -4,7 +4,7 @@ except ImportError: import unittest -from file_archive.parser import parse_expression +from file_archive.parser import parse_expression, parse_expressions class TestParser(unittest.TestCase): @@ -45,9 +45,25 @@ def test_multi(self): {'key1': {'type': 'int', 'equal': -12}, 'key2': {'type': 'str', 'equal': '4 2'}, 'key3': {'type': 'int', 'gt': 41}}) - self.assertEqual(parse_expression('key1>2 key1<4'), + self.assertEqual(parse_expression('key1>2 key1<4 '), {'key1': {'type': 'int', 'gt': 2, 'lt': 4}}) with self.assertRaises(tdparser.ParserError): parse_expression('key1="str" key1<2') with self.assertRaises(tdparser.ParserError): parse_expression('key1="str" key1="otherstr"') + + def test_multi_splitted(self): + self.assertEqual(parse_expressions(['key1=-12', 'key2 = "4 2"', '41< key3']), + {'key1': {'type': 'int', 'equal': -12}, + 'key2': {'type': 'str', 'equal': '4 2'}, + 'key3': {'type': 'int', 'gt': 41}}) + self.assertEqual(parse_expressions(['key1>2 ', ' key1<4 ']), + {'key1': {'type': 'int', 'gt': 2, 'lt': 4}}) + with self.assertRaises(tdparser.ParserError): + parse_expressions(['key1="str"', 'key1<2']) + with self.assertRaises(tdparser.ParserError): + parse_expressions(['key1="str"', 'key1="otherstr"']) + with self.assertRaises(tdparser.ParserError): + parse_expressions(['key1="str" key1<2']) + with self.assertRaises(tdparser.ParserError): + parse_expressions(['"value"'])