Dont store tweaks in a .py file

Use a .json file instead. As with the previosu migrations of pickle and
.py config files this causes the settings to become disjoint if an
upgrage/downgrade is done.
This commit is contained in:
Kovid Goyal 2019-03-26 11:25:15 +05:30
parent 9b0ed4204d
commit 05ef1457e8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 110 additions and 74 deletions

View File

@ -1,14 +1,11 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3' # License: GPLv3 Copyright: 2010, Kovid Goyal <kovid at kovidgoyal.net>
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' from __future__ import unicode_literals
__docformat__ = 'restructuredtext en'
''' # Contains various tweaks that affect calibre behavior. Only edit this file if
Contains various tweaks that affect calibre behavior. Only edit this file if # you know what you are doing. If you delete this file, it will be recreated from
you know what you are doing. If you delete this file, it will be recreated from # defaults.
defaults.
'''
#: Auto increment series index #: Auto increment series index
# The algorithm used to assign a book added to an existing series a series number. # The algorithm used to assign a book added to an existing series a series number.
@ -220,7 +217,7 @@ per_language_title_sort_articles = {
'eng' : (r'A\s+', r'The\s+', r'An\s+'), 'eng' : (r'A\s+', r'The\s+', r'An\s+'),
# Esperanto # Esperanto
'epo': (r'La\s+', r"L'", 'L\xb4'), 'epo': (r'La\s+', r"L'", 'L´'),
# Spanish # Spanish
'spa' : (r'El\s+', r'La\s+', r'Lo\s+', r'Los\s+', r'Las\s+', r'Un\s+', 'spa' : (r'El\s+', r'La\s+', r'Lo\s+', r'Los\s+', r'Las\s+', r'Un\s+',
@ -230,10 +227,10 @@ per_language_title_sort_articles = {
r'Des\s+', r'De\s+La\s+', r'De\s+', r"D'", u'D´', u'L'), r'Des\s+', r'De\s+La\s+', r'De\s+', r"D'", u'D´', u'L'),
# Italian # Italian
'ita': ('Lo\\s+', 'Il\\s+', "L'", 'L\xb4', 'La\\s+', 'Gli\\s+', 'ita': ('Lo\\s+', 'Il\\s+', "L'", 'L´', 'La\\s+', 'Gli\\s+',
'I\\s+', 'Le\\s+', 'Uno\\s+', 'Un\\s+', 'Una\\s+', "Un'", 'I\\s+', 'Le\\s+', 'Uno\\s+', 'Un\\s+', 'Una\\s+', "Un'",
'Un\xb4', 'Dei\\s+', 'Degli\\s+', 'Delle\\s+', 'Del\\s+', 'Un´', 'Dei\\s+', 'Degli\\s+', 'Delle\\s+', 'Del\\s+',
'Della\\s+', 'Dello\\s+', "Dell'", 'Dell\xb4'), 'Della\\s+', 'Dello\\s+', "Dell'", 'Dell´'),
# Portuguese # Portuguese
'por' : (r'A\s+', r'O\s+', r'Os\s+', r'As\s+', r'Um\s+', r'Uns\s+', 'por' : (r'A\s+', r'O\s+', r'Os\s+', r'As\s+', r'Um\s+', r'Uns\s+',

View File

@ -13,13 +13,13 @@ from collections import OrderedDict
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit from calibre.gui2.preferences import ConfigWidgetBase, test_widget, AbortCommit
from calibre.gui2.search_box import SearchBox2 from calibre.gui2.search_box import SearchBox2
from calibre.gui2 import error_dialog, info_dialog from calibre.gui2 import error_dialog, info_dialog
from calibre.utils.config import read_raw_tweaks, write_tweaks from calibre.utils.config_base import read_custom_tweaks, write_custom_tweaks, exec_tweaks, default_tweaks_raw
from calibre.gui2.widgets import PythonHighlighter from calibre.gui2.widgets import PythonHighlighter
from calibre import isbytestring from calibre import isbytestring
from calibre.utils.icu import lower from calibre.utils.icu import lower
from calibre.utils.search_query_parser import (ParseException, from calibre.utils.search_query_parser import (ParseException,
SearchQueryParser) SearchQueryParser)
from polyglot.builtins import iteritems, unicode_type, range from polyglot.builtins import iteritems, unicode_type, range, map
from PyQt5.Qt import ( from PyQt5.Qt import (
QAbstractListModel, Qt, QStyledItemDelegate, QStyle, QStyleOptionViewItem, QAbstractListModel, Qt, QStyledItemDelegate, QStyle, QStyleOptionViewItem,
@ -30,6 +30,14 @@ from PyQt5.Qt import (
ROOT = QModelIndex() ROOT = QModelIndex()
def normalize(val):
if isinstance(val, (list, tuple)):
return tuple(map(normalize, val))
if isinstance(val, dict):
return {k: normalize(v) for k, v in iteritems(val)}
return val
def format_doc(doc): def format_doc(doc):
current_indent = default_indent = None current_indent = default_indent = None
lines = [''] lines = ['']
@ -98,7 +106,7 @@ class Tweak(object): # {{{
ans.append('# ' + line) ans.append('# ' + line)
for key, val in iteritems(self.default_values): for key, val in iteritems(self.default_values):
val = self.custom_values.get(key, val) val = self.custom_values.get(key, val)
ans.append('%s = %r'%(key, val)) ans.append(u'%s = %r'%(key, val))
ans = '\n'.join(ans) ans = '\n'.join(ans)
if isinstance(ans, unicode_type): if isinstance(ans, unicode_type):
ans = ans.encode('utf-8') ans = ans.encode('utf-8')
@ -111,16 +119,21 @@ class Tweak(object): # {{{
@property @property
def is_customized(self): def is_customized(self):
for x, val in iteritems(self.default_values): for x, val in iteritems(self.default_values):
if self.custom_values.get(x, val) != val: cval = self.custom_values.get(x, val)
if normalize(cval) != normalize(val):
return True return True
return False return False
@property @property
def edit_text(self): def edit_text(self):
from pprint import pformat
ans = ['# %s'%self.name] ans = ['# %s'%self.name]
for x, val in iteritems(self.default_values): for x, val in iteritems(self.default_values):
val = self.custom_values.get(x, val) val = self.custom_values.get(x, val)
ans.append('%s = %r'%(x, val)) if isinstance(val, (list, tuple, dict, set, frozenset)):
ans.append(u'%s = %s' % (x, pformat(val)))
else:
ans.append(u'%s = %r'%(x, val))
return '\n\n'.join(ans) return '\n\n'.join(ans)
def restore_to_default(self): def restore_to_default(self):
@ -137,9 +150,7 @@ class Tweaks(QAbstractListModel, AdaptSQP): # {{{
def __init__(self, parent=None): def __init__(self, parent=None):
QAbstractListModel.__init__(self, parent) QAbstractListModel.__init__(self, parent)
SearchQueryParser.__init__(self, ['all']) SearchQueryParser.__init__(self, ['all'])
raw_defaults, raw_custom = read_raw_tweaks() self.parse_tweaks()
self.parse_tweaks(raw_defaults, raw_custom)
def rowCount(self, *args): def rowCount(self, *args):
return len(self.tweaks) return len(self.tweaks)
@ -168,32 +179,32 @@ class Tweaks(QAbstractListModel, AdaptSQP): # {{{
return tweak return tweak
return None return None
def parse_tweaks(self, defaults, custom): def parse_tweaks(self):
l, g = {}, {}
try: try:
exec(custom, g, l) custom_tweaks = read_custom_tweaks()
except: except:
print('Failed to load custom tweaks file') print('Failed to load custom tweaks file')
import traceback import traceback
traceback.print_exc() traceback.print_exc()
dl, dg = {}, {} custom_tweaks = {}
exec(defaults, dg, dl) default_tweaks = exec_tweaks(default_tweaks_raw())
defaults = default_tweaks_raw().decode('utf-8')
lines = defaults.splitlines() lines = defaults.splitlines()
pos = 0 pos = 0
self.tweaks = [] self.tweaks = []
while pos < len(lines): while pos < len(lines):
line = lines[pos] line = lines[pos]
if line.startswith('#:'): if line.startswith('#:'):
pos = self.read_tweak(lines, pos, dl, l) pos = self.read_tweak(lines, pos, default_tweaks, custom_tweaks)
pos += 1 pos += 1
self.tweaks.sort() self.tweaks.sort()
default_keys = set(dl) default_keys = set(default_tweaks)
custom_keys = set(l) custom_keys = set(custom_tweaks)
self.plugin_tweaks = {} self.plugin_tweaks = {}
for key in custom_keys - default_keys: for key in custom_keys - default_keys:
self.plugin_tweaks[key] = l[key] self.plugin_tweaks[key] = custom_tweaks[key]
def read_tweak(self, lines, pos, defaults, custom): def read_tweak(self, lines, pos, defaults, custom):
name = lines[pos][2:].strip() name = lines[pos][2:].strip()
@ -259,20 +270,20 @@ class Tweaks(QAbstractListModel, AdaptSQP): # {{{
' edit it unless you know what you are doing.', '', ' edit it unless you know what you are doing.', '',
] ]
for tweak in self.tweaks: for tweak in self.tweaks:
ans.extend(['', str(tweak), '']) ans.extend(['', unicode_type(tweak), ''])
if self.plugin_tweaks: if self.plugin_tweaks:
ans.extend(['', '', ans.extend(['', '',
'# The following are tweaks for installed plugins', '']) '# The following are tweaks for installed plugins', ''])
for key, val in iteritems(self.plugin_tweaks): for key, val in iteritems(self.plugin_tweaks):
ans.extend(['%s = %r'%(key, val), '', '']) ans.extend([u'%s = %r'%(key, val), '', ''])
return '\n'.join(ans) return '\n'.join(ans)
@property @property
def plugin_tweaks_string(self): def plugin_tweaks_string(self):
ans = [] ans = []
for key, val in iteritems(self.plugin_tweaks): for key, val in iteritems(self.plugin_tweaks):
ans.extend(['%s = %r'%(key, val), '', '']) ans.extend([u'%s = %r'%(key, val), '', ''])
ans = '\n'.join(ans) ans = '\n'.join(ans)
if isbytestring(ans): if isbytestring(ans):
ans = ans.decode('utf-8') ans = ans.decode('utf-8')
@ -369,7 +380,6 @@ class TweaksView(QListView):
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setSpacing(5) self.setSpacing(5)
self.setUniformItemSizes(True)
self.setVerticalScrollMode(self.ScrollPerPixel) self.setVerticalScrollMode(self.ScrollPerPixel)
def currentChanged(self, cur, prev): def currentChanged(self, cur, prev):
@ -544,8 +554,10 @@ class ConfigWidget(ConfigWidgetBase):
def commit(self): def commit(self):
raw = self.tweaks.to_string() raw = self.tweaks.to_string()
if not isinstance(raw, bytes):
raw = raw.encode('utf-8')
try: try:
exec(raw) custom_tweaks = exec_tweaks(raw)
except: except:
import traceback import traceback
error_dialog(self, _('Invalid tweaks'), error_dialog(self, _('Invalid tweaks'),
@ -554,7 +566,7 @@ class ConfigWidget(ConfigWidgetBase):
' you find the invalid setting.'), ' you find the invalid setting.'),
det_msg=traceback.format_exc(), show=True) det_msg=traceback.format_exc(), show=True)
raise AbortCommit('abort') raise AbortCommit('abort')
write_tweaks(raw) write_custom_tweaks(custom_tweaks)
ConfigWidgetBase.commit(self) ConfigWidgetBase.commit(self)
return True return True

View File

@ -19,7 +19,7 @@ from calibre.constants import (
from calibre.utils.config_base import ( from calibre.utils.config_base import (
Config, ConfigInterface, ConfigProxy, Option, OptionSet, OptionValues, Config, ConfigInterface, ConfigProxy, Option, OptionSet, OptionValues,
StringConfig, json_dumps, json_loads, make_config_dir, plugin_dir, prefs, StringConfig, json_dumps, json_loads, make_config_dir, plugin_dir, prefs,
read_raw_tweaks, read_tweaks, tweaks, write_tweaks, from_json, to_json tweaks, from_json, to_json
) )
from calibre.utils.lock import ExclusiveFile from calibre.utils.lock import ExclusiveFile
@ -30,9 +30,8 @@ optparse._ = _
if False: if False:
# Make pyflakes happy # Make pyflakes happy
Config, ConfigProxy, Option, OptionValues, StringConfig Config, ConfigProxy, Option, OptionValues, StringConfig, OptionSet,
OptionSet, ConfigInterface, read_tweaks, write_tweaks ConfigInterface, tweaks, plugin_dir, prefs, from_json, to_json
read_raw_tweaks, tweaks, plugin_dir, prefs, from_json, to_json
def check_config_write_access(): def check_config_write_access():

View File

@ -12,8 +12,8 @@ from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from calibre.utils.lock import ExclusiveFile from calibre.utils.lock import ExclusiveFile
from calibre.constants import config_dir, CONFIG_DIR_MODE, ispy3 from calibre.constants import config_dir, CONFIG_DIR_MODE, ispy3, preferred_encoding
from polyglot.builtins import unicode_type from polyglot.builtins import unicode_type, iteritems, map
plugin_dir = os.path.join(config_dir, 'plugins') plugin_dir = os.path.join(config_dir, 'plugins')
@ -522,50 +522,78 @@ if prefs['installation_uuid'] is None:
# Read tweaks # Read tweaks
def read_raw_tweaks(): def tweaks_file():
return os.path.join(config_dir, u'tweaks.json')
def make_unicode(obj):
if isinstance(obj, bytes):
try:
return obj.decode('utf-8')
except UnicodeDecodeError:
return obj.decode(preferred_encoding, errors='replace')
if isinstance(obj, (list, tuple)):
return list(map(make_unicode, obj))
if isinstance(obj, dict):
return {make_unicode(k): make_unicode(v) for k, v in iteritems(obj)}
return obj
def write_custom_tweaks(tweaks_dict):
make_config_dir() make_config_dir()
default_tweaks = P('default_tweaks.py', data=True, raw = json_dumps(make_unicode(tweaks_dict))
allow_user_override=False) with open(tweaks_file(), 'wb') as f:
tweaks_file = os.path.join(config_dir, 'tweaks.py') f.write(raw)
if not os.path.exists(tweaks_file):
with open(tweaks_file, 'wb') as f:
f.write(default_tweaks) def exec_tweaks(path):
with open(tweaks_file, 'rb') as f: if isinstance(path, bytes):
return default_tweaks, f.read() raw = path
fname = '<string>'
else:
with open(path, 'rb') as f:
raw = f.read()
fname = f.name
code = compile(raw, fname, 'exec')
l = {}
g = {'__file__': fname}
exec(code, g, l)
return l
def read_custom_tweaks():
make_config_dir()
tf = tweaks_file()
if os.path.exists(tf):
with open(tf, 'rb') as f:
raw = f.read()
return json_loads(raw)
old_tweaks_file = tf.rpartition(u'.')[0] + u'.py'
ans = {}
if os.path.exists(old_tweaks_file):
ans = exec_tweaks(old_tweaks_file)
ans = make_unicode(ans)
write_custom_tweaks(ans)
return ans
def default_tweaks_raw():
return P('default_tweaks.py', data=True, allow_user_override=False)
def read_tweaks(): def read_tweaks():
default_tweaks, tweaks = read_raw_tweaks() default_tweaks = exec_tweaks(default_tweaks_raw())
l, g = {}, {} default_tweaks.update(read_custom_tweaks())
try: return default_tweaks
exec(tweaks, g, l)
except:
import traceback
print('Failed to load custom tweaks file')
traceback.print_exc()
dl, dg = {}, {}
exec(default_tweaks, dg, dl)
dl.update(l)
return dl
def write_tweaks(raw):
make_config_dir()
tweaks_file = os.path.join(config_dir, 'tweaks.py')
with open(tweaks_file, 'wb') as f:
f.write(raw)
tweaks = read_tweaks() tweaks = read_tweaks()
def reset_tweaks_to_default(): def reset_tweaks_to_default():
default_tweaks = P('default_tweaks.py', data=True, default_tweaks = exec_tweaks(default_tweaks_raw())
allow_user_override=False)
dl, dg = {}, {}
exec(default_tweaks, dg, dl)
tweaks.clear() tweaks.clear()
tweaks.update(dl) tweaks.update(default_tweaks)
class Tweak(object): class Tweak(object):