Split non-GUI part of the coloring code into a non GUI module

This commit is contained in:
Kovid Goyal 2011-06-01 15:24:49 -06:00
parent cff2fcb6eb
commit 04c5fcc2ee
2 changed files with 180 additions and 169 deletions

View File

@ -2,186 +2,19 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
from future_builtins import map
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import json, binascii, re
from textwrap import dedent
from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox,
QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon,
QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox)
from calibre.utils.icu import sort_key
from calibre.gui2 import error_dialog
class Rule(object): # {{{
SIGNATURE = '# BasicColorRule():'
def __init__(self, fm):
self.color = None
self.fm = fm
self.conditions = []
def add_condition(self, col, action, val):
if col not in self.fm:
raise ValueError('%r is not a valid column name'%col)
v = self.validate_condition(col, action, val)
if v:
raise ValueError(v)
self.conditions.append((col, action, val))
def validate_condition(self, col, action, val):
m = self.fm[col]
dt = m['datatype']
if (dt in ('int', 'float', 'rating') and action in ('lt', 'eq', 'gt')):
try:
int(val) if dt == 'int' else float(val)
except:
return '%r is not a valid numerical value'%val
if (dt in ('comments', 'series', 'text', 'enumeration') and 'pattern'
in action):
try:
re.compile(val)
except:
return '%r is not a valid regular expression'%val
@property
def signature(self):
args = (self.color, self.conditions)
sig = json.dumps(args, ensure_ascii=False)
return self.SIGNATURE + binascii.hexlify(sig.encode('utf-8'))
@property
def template(self):
if not self.color or not self.conditions:
return None
conditions = map(self.apply_condition, self.conditions)
conditions = (',\n' + ' '*9).join(conditions)
return dedent('''\
program:
{sig}
test(and(
{conditions}
), {color}, '');
''').format(sig=self.signature, conditions=conditions,
color=self.color)
def apply_condition(self, condition):
col, action, val = condition
m = self.fm[col]
dt = m['datatype']
if dt == 'bool':
return self.bool_condition(col, action, val)
if dt in ('int', 'float', 'rating'):
return self.number_condition(col, action, val)
if dt == 'datetime':
return self.date_condition(col, action, val)
if dt in ('comments', 'series', 'text', 'enumeration'):
ism = m.get('is_multiple', False)
if ism:
return self.multiple_condition(col, action, val, ism)
return self.text_condition(col, action, val)
def bool_condition(self, col, action, val):
test = {'is true': 'True',
'is false': 'False',
'is undefined': 'None'}[action]
return "strcmp('%s', raw_field('%s'), '', '1', '')"%(test, col)
def number_condition(self, col, action, val):
lt, eq, gt = {
'eq': ('', '1', ''),
'lt': ('1', '', ''),
'gt': ('', '', '1')
}[action]
lt, eq, gt = '', '1', ''
return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt)
def date_condition(self, col, action, val):
lt, eq, gt = {
'eq': ('', '1', ''),
'lt': ('1', '', ''),
'gt': ('', '', '1')
}[action]
return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col,
val, lt, eq, gt)
def multiple_condition(self, col, action, val, sep):
if action == 'is set':
return "test('%s', '1', '')"%col
if action == 'is not set':
return "test('%s', '', '1')"%col
if action == 'has':
return "str_in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
if action == 'does not have':
return "str_in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
if action == 'has pattern':
return "in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
if action == 'does not have pattern':
return "in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
def text_condition(self, col, action, val):
if action == 'is set':
return "test('%s', '1', '')"%col
if action == 'is not set':
return "test('%s', '', '1')"%col
if action == 'is':
return "strcmp(field('%s'), \"%s\", '', '1', '')"%(col, val)
if action == 'is not':
return "strcmp(field('%s'), \"%s\", '1', '', '1')"%(col, val)
if action == 'matches pattern':
return "contains(field('%s'), \"%s\", '1', '')"%(col, val)
if action == 'does not match pattern':
return "contains(field('%s'), \"%s\", '', '1')"%(col, val)
# }}}
def rule_from_template(fm, template):
ok_lines = []
for line in template.splitlines():
if line.startswith(Rule.SIGNATURE):
raw = line[len(Rule.SIGNATURE):].strip()
try:
color, conditions = json.loads(binascii.unhexlify(raw).decode('utf-8'))
except:
continue
r = Rule(fm)
r.color = color
for c in conditions:
try:
r.add_condition(*c)
except:
continue
if r.color and r.conditions:
return r
else:
ok_lines.append(line)
return '\n'.join(ok_lines)
def conditionable_columns(fm):
for key in fm:
m = fm[key]
dt = m['datatype']
if m.get('name', False) and dt in ('bool', 'int', 'float', 'rating', 'series',
'comments', 'text', 'enumeration', 'datetime'):
yield key
def displayable_columns(fm):
for key in fm.displayable_field_keys():
if key not in ('sort', 'author_sort', 'comments', 'formats',
'identifiers', 'path'):
yield key
from calibre.library.coloring import (Rule, conditionable_columns,
displayable_columns)
class ConditionEditor(QWidget):

View File

@ -0,0 +1,178 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
from future_builtins import map
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import binascii, re, json
from textwrap import dedent
class Rule(object): # {{{
SIGNATURE = '# BasicColorRule():'
def __init__(self, fm, color=None):
self.color = color
self.fm = fm
self.conditions = []
def add_condition(self, col, action, val):
if col not in self.fm:
raise ValueError('%r is not a valid column name'%col)
v = self.validate_condition(col, action, val)
if v:
raise ValueError(v)
self.conditions.append((col, action, val))
def validate_condition(self, col, action, val):
m = self.fm[col]
dt = m['datatype']
if (dt in ('int', 'float', 'rating') and action in ('lt', 'eq', 'gt')):
try:
int(val) if dt == 'int' else float(val)
except:
return '%r is not a valid numerical value'%val
if (dt in ('comments', 'series', 'text', 'enumeration') and 'pattern'
in action):
try:
re.compile(val)
except:
return '%r is not a valid regular expression'%val
@property
def signature(self):
args = (self.color, self.conditions)
sig = json.dumps(args, ensure_ascii=False)
return self.SIGNATURE + binascii.hexlify(sig.encode('utf-8'))
@property
def template(self):
if not self.color or not self.conditions:
return None
conditions = map(self.apply_condition, self.conditions)
conditions = (',\n' + ' '*9).join(conditions)
return dedent('''\
program:
{sig}
test(and(
{conditions}
), {color}, '');
''').format(sig=self.signature, conditions=conditions,
color=self.color)
def apply_condition(self, condition):
col, action, val = condition
m = self.fm[col]
dt = m['datatype']
if dt == 'bool':
return self.bool_condition(col, action, val)
if dt in ('int', 'float', 'rating'):
return self.number_condition(col, action, val)
if dt == 'datetime':
return self.date_condition(col, action, val)
if dt in ('comments', 'series', 'text', 'enumeration'):
ism = m.get('is_multiple', False)
if ism:
return self.multiple_condition(col, action, val, ism)
return self.text_condition(col, action, val)
def bool_condition(self, col, action, val):
test = {'is true': 'True',
'is false': 'False',
'is undefined': 'None'}[action]
return "strcmp('%s', raw_field('%s'), '', '1', '')"%(test, col)
def number_condition(self, col, action, val):
lt, eq, gt = {
'eq': ('', '1', ''),
'lt': ('1', '', ''),
'gt': ('', '', '1')
}[action]
lt, eq, gt = '', '1', ''
return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt)
def date_condition(self, col, action, val):
lt, eq, gt = {
'eq': ('', '1', ''),
'lt': ('1', '', ''),
'gt': ('', '', '1')
}[action]
return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col,
val, lt, eq, gt)
def multiple_condition(self, col, action, val, sep):
if action == 'is set':
return "test('%s', '1', '')"%col
if action == 'is not set':
return "test('%s', '', '1')"%col
if action == 'has':
return "str_in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
if action == 'does not have':
return "str_in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
if action == 'has pattern':
return "in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
if action == 'does not have pattern':
return "in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
def text_condition(self, col, action, val):
if action == 'is set':
return "test('%s', '1', '')"%col
if action == 'is not set':
return "test('%s', '', '1')"%col
if action == 'is':
return "strcmp(field('%s'), \"%s\", '', '1', '')"%(col, val)
if action == 'is not':
return "strcmp(field('%s'), \"%s\", '1', '', '1')"%(col, val)
if action == 'matches pattern':
return "contains(field('%s'), \"%s\", '1', '')"%(col, val)
if action == 'does not match pattern':
return "contains(field('%s'), \"%s\", '', '1')"%(col, val)
# }}}
def rule_from_template(fm, template):
ok_lines = []
for line in template.splitlines():
if line.startswith(Rule.SIGNATURE):
raw = line[len(Rule.SIGNATURE):].strip()
try:
color, conditions = json.loads(binascii.unhexlify(raw).decode('utf-8'))
except:
continue
r = Rule(fm)
r.color = color
for c in conditions:
try:
r.add_condition(*c)
except:
continue
if r.color and r.conditions:
return r
else:
ok_lines.append(line)
return '\n'.join(ok_lines)
def conditionable_columns(fm):
for key in fm:
m = fm[key]
dt = m['datatype']
if m.get('name', False) and dt in ('bool', 'int', 'float', 'rating', 'series',
'comments', 'text', 'enumeration', 'datetime'):
yield key
def displayable_columns(fm):
for key in fm.displayable_field_keys():
if key not in ('sort', 'author_sort', 'comments', 'formats',
'identifiers', 'path'):
yield key