Skip to content

Commit

Permalink
Merge branch 'qt-viewer' into 'master'
Browse files Browse the repository at this point in the history
  • Loading branch information
remram44 committed Oct 11, 2013
2 parents 9cbd7f0 + f71c890 commit 4cfe3e6
Show file tree
Hide file tree
Showing 6 changed files with 407 additions and 3 deletions.
12 changes: 11 additions & 1 deletion file_archive/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,12 +275,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,
}


Expand All @@ -295,7 +304,8 @@ def main(args):
" or: {bin} <store> print [-m] [-t] [key1=value1] [...]\n"
" or: {bin} <store> remove [-f] <filehash>\n"
" or: {bin} <store> remove [-f] <key1=value1> [...]\n"
" or: {bin} <store> verify\n".format(
" or: {bin} <store> verify\n"
" or: {bin} <store> view\n".format(
bin='file_archive'))

if len(args) < 2:
Expand Down
99 changes: 99 additions & 0 deletions file_archive/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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)
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 = '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}


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)
conditions = {}
while not isinstance(parser.current_token, EndToken):
expr = parser.expression()
if isinstance(expr, Token):
raise ParserError("Found unexpected token %s in query" % expr)
_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
222 changes: 222 additions & 0 deletions file_archive/viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import platform
import subprocess
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)


try:
import tdparser
except ImportError:
sys.stderr.write("tdparser is required by 'file_archive view'\n")
sys.exit(3)


from .compat import int_types
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)
return QtCore.QCoreApplication.translate(
'file_archive.viewer',
s,
disambiguation,
QtCore.QCoreApplication.UnicodeUTF8)


class SearchError(Exception):
"""Error while querying the file store.
"""


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)

self.store = 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(('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)

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()

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:
error = _(u"{h} not found", h=h)

else:
try:
conditions = parse_expression(query)
except ValueError as e:
error = e.args[0]
else:
entries = self.store.query(conditions)

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:
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
file_item.addChild(MetadataItem(entry['hash'], k, v))
self._result_tree.expandAll()

def _selection_changed(self):
items = self._result_tree.selectedItems()
for t, button in self._buttons:
if t == 'single':
button.setEnabled(
len(items) == 1 and
isinstance(items[0], FileItem))
else:
button.setEnabled(bool(items) and
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([])

window = StoreViewerWindow(store)
window.show()

application.exec_()
sys.exit(0)
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)'])
Loading

0 comments on commit 4cfe3e6

Please sign in to comment.