mirror of
https://github.com/gryf/ebook-converter.git
synced 2026-03-13 21:25:54 +01:00
Initial import
This commit is contained in:
464
ebook_converter/utils/config.py
Normal file
464
ebook_converter/utils/config.py
Normal file
@@ -0,0 +1,464 @@
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
Manage application-wide preferences.
|
||||
'''
|
||||
|
||||
import optparse
|
||||
import os
|
||||
from copy import deepcopy
|
||||
|
||||
from calibre.constants import (
|
||||
CONFIG_DIR_MODE, __appname__, __author__, config_dir, get_version, iswindows
|
||||
)
|
||||
from calibre.utils.config_base import (
|
||||
Config, ConfigInterface, ConfigProxy, Option, OptionSet, OptionValues,
|
||||
StringConfig, json_dumps, json_loads, make_config_dir, plugin_dir, prefs,
|
||||
tweaks, from_json, to_json
|
||||
)
|
||||
from calibre.utils.lock import ExclusiveFile
|
||||
from polyglot.builtins import string_or_bytes, native_string_type
|
||||
|
||||
|
||||
# optparse uses gettext.gettext instead of _ from builtins, so we
|
||||
# monkey patch it.
|
||||
optparse._ = _
|
||||
|
||||
if False:
|
||||
# Make pyflakes happy
|
||||
Config, ConfigProxy, Option, OptionValues, StringConfig, OptionSet,
|
||||
ConfigInterface, tweaks, plugin_dir, prefs, from_json, to_json
|
||||
|
||||
|
||||
def check_config_write_access():
|
||||
return os.access(config_dir, os.W_OK) and os.access(config_dir, os.X_OK)
|
||||
|
||||
|
||||
class CustomHelpFormatter(optparse.IndentedHelpFormatter):
|
||||
|
||||
def format_usage(self, usage):
|
||||
from calibre.utils.terminal import colored
|
||||
parts = usage.split(' ')
|
||||
if parts:
|
||||
parts[0] = colored(parts[0], fg='yellow', bold=True)
|
||||
usage = ' '.join(parts)
|
||||
return colored(_('Usage'), fg='blue', bold=True) + ': ' + usage
|
||||
|
||||
def format_heading(self, heading):
|
||||
from calibre.utils.terminal import colored
|
||||
return "%*s%s:\n" % (self.current_indent, '',
|
||||
colored(heading, fg='blue', bold=True))
|
||||
|
||||
def format_option(self, option):
|
||||
import textwrap
|
||||
from calibre.utils.terminal import colored
|
||||
|
||||
result = []
|
||||
opts = self.option_strings[option]
|
||||
opt_width = self.help_position - self.current_indent - 2
|
||||
if len(opts) > opt_width:
|
||||
opts = "%*s%s\n" % (self.current_indent, "",
|
||||
colored(opts, fg='green'))
|
||||
indent_first = self.help_position
|
||||
else: # start help on same line as opts
|
||||
opts = "%*s%-*s " % (self.current_indent, "", opt_width +
|
||||
len(colored('', fg='green')), colored(opts, fg='green'))
|
||||
indent_first = 0
|
||||
result.append(opts)
|
||||
if option.help:
|
||||
help_text = self.expand_default(option).split('\n')
|
||||
help_lines = []
|
||||
|
||||
for line in help_text:
|
||||
help_lines.extend(textwrap.wrap(line, self.help_width))
|
||||
result.append("%*s%s\n" % (indent_first, "", help_lines[0]))
|
||||
result.extend(["%*s%s\n" % (self.help_position, "", line)
|
||||
for line in help_lines[1:]])
|
||||
elif opts[-1] != "\n":
|
||||
result.append("\n")
|
||||
return "".join(result)+'\n'
|
||||
|
||||
|
||||
class OptionParser(optparse.OptionParser):
|
||||
|
||||
def __init__(self,
|
||||
usage='%prog [options] filename',
|
||||
version=None,
|
||||
epilog=None,
|
||||
gui_mode=False,
|
||||
conflict_handler='resolve',
|
||||
**kwds):
|
||||
import textwrap
|
||||
from calibre.utils.terminal import colored
|
||||
|
||||
usage = textwrap.dedent(usage)
|
||||
if epilog is None:
|
||||
epilog = _('Created by ')+colored(__author__, fg='cyan')
|
||||
usage += '\n\n'+_('''Whenever you pass arguments to %prog that have spaces in them, '''
|
||||
'''enclose the arguments in quotation marks. For example: "{}"''').format(
|
||||
"C:\\some path with spaces" if iswindows else '/some path/with spaces') +'\n'
|
||||
if version is None:
|
||||
version = '%%prog (%s %s)'%(__appname__, get_version())
|
||||
optparse.OptionParser.__init__(self, usage=usage, version=version, epilog=epilog,
|
||||
formatter=CustomHelpFormatter(),
|
||||
conflict_handler=conflict_handler, **kwds)
|
||||
self.gui_mode = gui_mode
|
||||
if False:
|
||||
# Translatable string from optparse
|
||||
_("Options")
|
||||
_("show this help message and exit")
|
||||
_("show program's version number and exit")
|
||||
|
||||
def print_usage(self, file=None):
|
||||
from calibre.utils.terminal import ANSIStream
|
||||
s = ANSIStream(file)
|
||||
optparse.OptionParser.print_usage(self, file=s)
|
||||
|
||||
def print_help(self, file=None):
|
||||
from calibre.utils.terminal import ANSIStream
|
||||
s = ANSIStream(file)
|
||||
optparse.OptionParser.print_help(self, file=s)
|
||||
|
||||
def print_version(self, file=None):
|
||||
from calibre.utils.terminal import ANSIStream
|
||||
s = ANSIStream(file)
|
||||
optparse.OptionParser.print_version(self, file=s)
|
||||
|
||||
def error(self, msg):
|
||||
if self.gui_mode:
|
||||
raise Exception(msg)
|
||||
optparse.OptionParser.error(self, msg)
|
||||
|
||||
def merge(self, parser):
|
||||
'''
|
||||
Add options from parser to self. In case of conflicts, conflicting options from
|
||||
parser are skipped.
|
||||
'''
|
||||
opts = list(parser.option_list)
|
||||
groups = list(parser.option_groups)
|
||||
|
||||
def merge_options(options, container):
|
||||
for opt in deepcopy(options):
|
||||
if not self.has_option(opt.get_opt_string()):
|
||||
container.add_option(opt)
|
||||
|
||||
merge_options(opts, self)
|
||||
|
||||
for group in groups:
|
||||
g = self.add_option_group(group.title)
|
||||
merge_options(group.option_list, g)
|
||||
|
||||
def subsume(self, group_name, msg=''):
|
||||
'''
|
||||
Move all existing options into a subgroup named
|
||||
C{group_name} with description C{msg}.
|
||||
'''
|
||||
opts = [opt for opt in self.options_iter() if opt.get_opt_string() not in ('--version', '--help')]
|
||||
self.option_groups = []
|
||||
subgroup = self.add_option_group(group_name, msg)
|
||||
for opt in opts:
|
||||
self.remove_option(opt.get_opt_string())
|
||||
subgroup.add_option(opt)
|
||||
|
||||
def options_iter(self):
|
||||
for opt in self.option_list:
|
||||
if native_string_type(opt).strip():
|
||||
yield opt
|
||||
for gr in self.option_groups:
|
||||
for opt in gr.option_list:
|
||||
if native_string_type(opt).strip():
|
||||
yield opt
|
||||
|
||||
def option_by_dest(self, dest):
|
||||
for opt in self.options_iter():
|
||||
if opt.dest == dest:
|
||||
return opt
|
||||
|
||||
def merge_options(self, lower, upper):
|
||||
'''
|
||||
Merge options in lower and upper option lists into upper.
|
||||
Default values in upper are overridden by
|
||||
non default values in lower.
|
||||
'''
|
||||
for dest in lower.__dict__.keys():
|
||||
if dest not in upper.__dict__:
|
||||
continue
|
||||
opt = self.option_by_dest(dest)
|
||||
if lower.__dict__[dest] != opt.default and \
|
||||
upper.__dict__[dest] == opt.default:
|
||||
upper.__dict__[dest] = lower.__dict__[dest]
|
||||
|
||||
def add_option_group(self, *args, **kwargs):
|
||||
if isinstance(args[0], string_or_bytes):
|
||||
args = list(args)
|
||||
args[0] = native_string_type(args[0])
|
||||
return optparse.OptionParser.add_option_group(self, *args, **kwargs)
|
||||
|
||||
|
||||
class DynamicConfig(dict):
|
||||
'''
|
||||
A replacement for QSettings that supports dynamic config keys.
|
||||
Returns `None` if a config key is not found. Note that the config
|
||||
data is stored in a JSON file.
|
||||
'''
|
||||
|
||||
def __init__(self, name='dynamic'):
|
||||
dict.__init__(self, {})
|
||||
self.name = name
|
||||
self.defaults = {}
|
||||
self.refresh()
|
||||
|
||||
@property
|
||||
def file_path(self):
|
||||
return os.path.join(config_dir, self.name+'.pickle.json')
|
||||
|
||||
def decouple(self, prefix):
|
||||
self.name = prefix + self.name
|
||||
self.refresh()
|
||||
|
||||
def read_old_serialized_representation(self):
|
||||
from calibre.utils.shared_file import share_open
|
||||
from calibre.utils.serialize import pickle_loads
|
||||
path = self.file_path.rpartition('.')[0]
|
||||
try:
|
||||
with share_open(path, 'rb') as f:
|
||||
raw = f.read()
|
||||
except EnvironmentError:
|
||||
raw = b''
|
||||
try:
|
||||
d = pickle_loads(raw).copy()
|
||||
except Exception:
|
||||
d = {}
|
||||
return d
|
||||
|
||||
def refresh(self, clear_current=True):
|
||||
d = {}
|
||||
migrate = False
|
||||
if clear_current:
|
||||
self.clear()
|
||||
if os.path.exists(self.file_path):
|
||||
with ExclusiveFile(self.file_path) as f:
|
||||
raw = f.read()
|
||||
if raw:
|
||||
try:
|
||||
d = json_loads(raw)
|
||||
except Exception as err:
|
||||
print('Failed to de-serialize JSON representation of stored dynamic data for {} with error: {}'.format(
|
||||
self.name, err))
|
||||
else:
|
||||
d = self.read_old_serialized_representation()
|
||||
migrate = bool(d)
|
||||
else:
|
||||
d = self.read_old_serialized_representation()
|
||||
migrate = bool(d)
|
||||
if migrate and d:
|
||||
raw = json_dumps(d, ignore_unserializable=True)
|
||||
with ExclusiveFile(self.file_path) as f:
|
||||
f.seek(0), f.truncate()
|
||||
f.write(raw)
|
||||
|
||||
self.update(d)
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
return self.defaults.get(key, None)
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
return self.defaults.get(key, default)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
dict.__setitem__(self, key, val)
|
||||
self.commit()
|
||||
|
||||
def set(self, key, val):
|
||||
self.__setitem__(key, val)
|
||||
|
||||
def commit(self):
|
||||
if not getattr(self, 'name', None):
|
||||
return
|
||||
if not os.path.exists(self.file_path):
|
||||
make_config_dir()
|
||||
raw = json_dumps(self)
|
||||
with ExclusiveFile(self.file_path) as f:
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
f.write(raw)
|
||||
|
||||
|
||||
dynamic = DynamicConfig()
|
||||
|
||||
|
||||
class XMLConfig(dict):
|
||||
|
||||
'''
|
||||
Similar to :class:`DynamicConfig`, except that it uses an XML storage
|
||||
backend instead of a pickle file.
|
||||
|
||||
See `https://docs.python.org/dev/library/plistlib.html`_ for the supported
|
||||
data types.
|
||||
'''
|
||||
|
||||
EXTENSION = '.plist'
|
||||
|
||||
def __init__(self, rel_path_to_cf_file, base_path=config_dir):
|
||||
dict.__init__(self)
|
||||
self.no_commit = False
|
||||
self.defaults = {}
|
||||
self.file_path = os.path.join(base_path,
|
||||
*(rel_path_to_cf_file.split('/')))
|
||||
self.file_path = os.path.abspath(self.file_path)
|
||||
if not self.file_path.endswith(self.EXTENSION):
|
||||
self.file_path += self.EXTENSION
|
||||
|
||||
self.refresh()
|
||||
|
||||
def mtime(self):
|
||||
try:
|
||||
return os.path.getmtime(self.file_path)
|
||||
except EnvironmentError:
|
||||
return 0
|
||||
|
||||
def touch(self):
|
||||
try:
|
||||
os.utime(self.file_path, None)
|
||||
except EnvironmentError:
|
||||
pass
|
||||
|
||||
def raw_to_object(self, raw):
|
||||
from polyglot.plistlib import loads
|
||||
return loads(raw)
|
||||
|
||||
def to_raw(self):
|
||||
from polyglot.plistlib import dumps
|
||||
return dumps(self)
|
||||
|
||||
def decouple(self, prefix):
|
||||
self.file_path = os.path.join(os.path.dirname(self.file_path), prefix + os.path.basename(self.file_path))
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, clear_current=True):
|
||||
d = {}
|
||||
if os.path.exists(self.file_path):
|
||||
with ExclusiveFile(self.file_path) as f:
|
||||
raw = f.read()
|
||||
try:
|
||||
d = self.raw_to_object(raw) if raw.strip() else {}
|
||||
except SystemError:
|
||||
pass
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
d = {}
|
||||
if clear_current:
|
||||
self.clear()
|
||||
self.update(d)
|
||||
|
||||
def __getitem__(self, key):
|
||||
from polyglot.plistlib import Data
|
||||
try:
|
||||
ans = dict.__getitem__(self, key)
|
||||
if isinstance(ans, Data):
|
||||
ans = ans.data
|
||||
return ans
|
||||
except KeyError:
|
||||
return self.defaults.get(key, None)
|
||||
|
||||
def get(self, key, default=None):
|
||||
from polyglot.plistlib import Data
|
||||
try:
|
||||
ans = dict.__getitem__(self, key)
|
||||
if isinstance(ans, Data):
|
||||
ans = ans.data
|
||||
return ans
|
||||
except KeyError:
|
||||
return self.defaults.get(key, default)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
from polyglot.plistlib import Data
|
||||
if isinstance(val, bytes):
|
||||
val = Data(val)
|
||||
dict.__setitem__(self, key, val)
|
||||
self.commit()
|
||||
|
||||
def set(self, key, val):
|
||||
self.__setitem__(key, val)
|
||||
|
||||
def __delitem__(self, key):
|
||||
try:
|
||||
dict.__delitem__(self, key)
|
||||
except KeyError:
|
||||
pass # ignore missing keys
|
||||
else:
|
||||
self.commit()
|
||||
|
||||
def commit(self):
|
||||
if self.no_commit:
|
||||
return
|
||||
if hasattr(self, 'file_path') and self.file_path:
|
||||
dpath = os.path.dirname(self.file_path)
|
||||
if not os.path.exists(dpath):
|
||||
os.makedirs(dpath, mode=CONFIG_DIR_MODE)
|
||||
with ExclusiveFile(self.file_path) as f:
|
||||
raw = self.to_raw()
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
f.write(raw)
|
||||
|
||||
def __enter__(self):
|
||||
self.no_commit = True
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.no_commit = False
|
||||
self.commit()
|
||||
|
||||
|
||||
class JSONConfig(XMLConfig):
|
||||
|
||||
EXTENSION = '.json'
|
||||
|
||||
def raw_to_object(self, raw):
|
||||
return json_loads(raw)
|
||||
|
||||
def to_raw(self):
|
||||
return json_dumps(self)
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
return self.defaults[key]
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
return self.defaults.get(key, default)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
dict.__setitem__(self, key, val)
|
||||
self.commit()
|
||||
|
||||
|
||||
class DevicePrefs:
|
||||
|
||||
def __init__(self, global_prefs):
|
||||
self.global_prefs = global_prefs
|
||||
self.overrides = {}
|
||||
|
||||
def set_overrides(self, **kwargs):
|
||||
self.overrides = kwargs.copy()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.overrides.get(key, self.global_prefs[key])
|
||||
|
||||
|
||||
device_prefs = DevicePrefs(prefs)
|
||||
Reference in New Issue
Block a user