From 4d450f556f52c83d82ed134276f2e830cf0f1fd2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 15 Aug 2008 19:38:35 -0700 Subject: [PATCH] IGN:Complete migration to new config infrastructure --- setup.py | 2 +- src/calibre/__init__.py | 407 +------------------ src/calibre/constants.py | 30 ++ src/calibre/debug.py | 3 +- src/calibre/ebooks/lit/reader.py | 2 +- src/calibre/ebooks/lrf/__init__.py | 3 +- src/calibre/ebooks/lrf/html/convert_to.py | 3 +- src/calibre/ebooks/lrf/lrfparser.py | 3 +- src/calibre/ebooks/lrf/lrs/convert_from.py | 3 +- src/calibre/ebooks/lrf/meta.py | 4 +- src/calibre/ebooks/lrf/pdf/reflow.py | 3 +- src/calibre/ebooks/metadata/__init__.py | 5 +- src/calibre/ebooks/metadata/isbndb.py | 3 +- src/calibre/ebooks/metadata/library_thing.py | 3 +- src/calibre/ebooks/metadata/meta.py | 12 +- src/calibre/ebooks/mobi/reader.py | 2 +- src/calibre/gui2/__init__.py | 64 ++- src/calibre/gui2/cover_flow.py | 5 +- src/calibre/gui2/dialogs/config.py | 47 ++- src/calibre/gui2/dialogs/fetch_metadata.py | 6 +- src/calibre/gui2/dialogs/lrf_single.py | 10 +- src/calibre/gui2/dialogs/metadata_single.py | 5 +- src/calibre/gui2/dialogs/password.py | 20 +- src/calibre/gui2/library.py | 6 +- src/calibre/gui2/lrf_renderer/main.py | 15 +- src/calibre/gui2/main.py | 62 ++- src/calibre/gui2/main_window.py | 2 +- src/calibre/gui2/widgets.py | 21 +- src/calibre/library/cli.py | 8 +- src/calibre/linux_installer.py | 1 + src/calibre/startup.py | 149 +++++++ src/calibre/translations/pygettext.py | 6 +- src/calibre/utils/config.py | 272 ++++++++++++- src/calibre/utils/lock.py | 86 ++++ src/calibre/web/fetch/simple.py | 3 +- 35 files changed, 735 insertions(+), 541 deletions(-) create mode 100644 src/calibre/constants.py create mode 100644 src/calibre/startup.py create mode 100644 src/calibre/utils/lock.py diff --git a/setup.py b/setup.py index d3be969886..346c9059d5 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ sys.path.append('src') iswindows = re.search('win(32|64)', sys.platform) isosx = 'darwin' in sys.platform islinux = not isosx and not iswindows -src = open('src/calibre/__init__.py', 'rb').read() +src = open('src/calibre/constants.py', 'rb').read() VERSION = re.search(r'__version__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1) APPNAME = re.search(r'__appname__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1) print 'Setup', APPNAME, 'version:', VERSION diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index b34b760afc..6a5da2ac1e 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -1,123 +1,21 @@ ''' E-book management software''' __license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal ' -__version__ = '0.4.83' -__docformat__ = "epytext" -__author__ = "Kovid Goyal " -__appname__ = 'calibre' +__copyright__ = '2008, Kovid Goyal ' +__docformat__ = 'restructuredtext en' -import sys, os, logging, mechanize, locale, copy, cStringIO, re, subprocess, \ - textwrap, atexit, cPickle, codecs, time -from gettext import GNUTranslations +import sys, os, re, logging, time, subprocess, mechanize, atexit from htmlentitydefs import name2codepoint from math import floor -from optparse import OptionParser as _OptionParser -from optparse import IndentedHelpFormatter from logging import Formatter -from PyQt4.QtCore import QSettings, QVariant, QUrl, QByteArray, QString -from PyQt4.QtGui import QDesktopServices +from PyQt4.QtCore import QUrl +from PyQt4.QtGui import QDesktopServices +from calibre.startup import plugins, winutil, winutilerror +from calibre.constants import iswindows, isosx, islinux, isfrozen, \ + terminal_controller, preferred_encoding, \ + __appname__, __version__, __author__, \ + win32event, win32api, winerror, fcntl -from calibre.translations.msgfmt import make -from calibre.ebooks.chardet import detect -from calibre.utils.terminfo import TerminalController - -terminal_controller = TerminalController(sys.stdout) -iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower() -isosx = 'darwin' in sys.platform.lower() -islinux = not(iswindows or isosx) -isfrozen = hasattr(sys, 'frozen') - -try: - locale.setlocale(locale.LC_ALL, '') -except: - dl = locale.getdefaultlocale() - try: - if dl: - locale.setlocale(dl[0]) - except: - pass - -try: - preferred_encoding = locale.getpreferredencoding() - codecs.lookup(preferred_encoding) -except: - preferred_encoding = 'utf-8' - -if getattr(sys, 'frozen', False): - if iswindows: - plugin_path = os.path.join(os.path.dirname(sys.executable), 'plugins') - elif isosx: - plugin_path = os.path.join(getattr(sys, 'frameworks_dir'), 'plugins') - elif islinux: - plugin_path = os.path.join(getattr(sys, 'frozen_path'), 'plugins') - sys.path.insert(0, plugin_path) -else: - import pkg_resources - plugins = getattr(pkg_resources, 'resource_filename')(__appname__, 'plugins') - sys.path.insert(0, plugins) - -if iswindows and getattr(sys, 'frozen', False): - sys.path.insert(1, os.path.dirname(sys.executable)) - - -plugins = {} -for plugin in ['pictureflow', 'lzx', 'msdes'] + \ - (['winutil'] if iswindows else []) + \ - (['usbobserver'] if isosx else []): - try: - p, err = __import__(plugin), '' - except Exception, err: - p = None - err = str(err) - plugins[plugin] = (p, err) - -if iswindows: - winutil, winutilerror = plugins['winutil'] - if not winutil: - raise RuntimeError('Failed to load the winutil plugin: %s'%winutilerror) - if len(sys.argv) > 1: - sys.argv[1:] = winutil.argv()[1-len(sys.argv):] - win32event = __import__('win32event') - winerror = __import__('winerror') - win32api = __import__('win32api') -else: - import fcntl - -_abspath = os.path.abspath -def my_abspath(path, encoding=sys.getfilesystemencoding()): - ''' - Work around for buggy os.path.abspath. This function accepts either byte strings, - in which it calls os.path.abspath, or unicode string, in which case it first converts - to byte strings using `encoding`, calls abspath and then decodes back to unicode. - ''' - to_unicode = False - if isinstance(path, unicode): - path = path.encode(encoding) - to_unicode = True - res = _abspath(path) - if to_unicode: - res = res.decode(encoding) - return res - -os.path.abspath = my_abspath -_join = os.path.join -def my_join(a, *p): - encoding=sys.getfilesystemencoding() - p = [a] + list(p) - _unicode = False - for i in p: - if isinstance(i, unicode): - _unicode = True - break - p = [i.encode(encoding) if isinstance(i, unicode) else i for i in p] - - res = _join(*p) - if _unicode: - res = res.decode(encoding) - return res - -os.path.join = my_join def unicode_path(path, abs=False): if not isinstance(path, unicode): @@ -135,10 +33,6 @@ def osx_version(): return int(m.group(1)), int(m.group(2)), int(m.group(3)) -# Default translation is NOOP -import __builtin__ -__builtin__.__dict__['_'] = lambda s: s - class CommandLineError(Exception): pass @@ -180,122 +74,6 @@ def setup_cli_handlers(logger, level): logger.addHandler(handler) -class CustomHelpFormatter(IndentedHelpFormatter): - - def format_usage(self, usage): - return _("%sUsage%s: %s\n") % (terminal_controller.BLUE, terminal_controller.NORMAL, usage) - - def format_heading(self, heading): - return "%*s%s%s%s:\n" % (self.current_indent, terminal_controller.BLUE, - "", heading, terminal_controller.NORMAL) - - def format_option(self, option): - 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, "", - terminal_controller.GREEN+opts+terminal_controller.NORMAL) - indent_first = self.help_position - else: # start help on same line as opts - opts = "%*s%-*s " % (self.current_indent, "", opt_width + len(terminal_controller.GREEN + terminal_controller.NORMAL), - terminal_controller.GREEN + opts + terminal_controller.NORMAL) - 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(_OptionParser): - - def __init__(self, - usage='%prog [options] filename', - version='%%prog (%s %s)'%(__appname__, __version__), - epilog=_('Created by ')+terminal_controller.RED+__author__+terminal_controller.NORMAL, - gui_mode=False, - conflict_handler='resolve', - **kwds): - usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\ - '''enclose the arguments in quotation marks.''' - _OptionParser.__init__(self, usage=usage, version=version, epilog=epilog, - formatter=CustomHelpFormatter(), - conflict_handler=conflict_handler, **kwds) - self.gui_mode = gui_mode - - def error(self, msg): - if self.gui_mode: - raise Exception(msg) - _OptionParser.error(self, msg) - - def merge(self, parser): - ''' - Add options from parser to self. In case of conflicts, confilicting options from - parser are skipped. - ''' - opts = list(parser.option_list) - groups = list(parser.option_groups) - - def merge_options(options, container): - for opt in copy.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 str(opt).strip(): - yield opt - for gr in self.option_groups: - for opt in gr.option_list: - if str(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 overriden by - non default values in lower. - ''' - for dest in lower.__dict__.keys(): - if not upper.__dict__.has_key(dest): - 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 load_library(name, cdll): if iswindows: return cdll.LoadLibrary(name) @@ -392,40 +170,6 @@ def fit_image(width, height, pwidth, pheight): return scaled, int(width), int(height) -def get_lang(): - lang = locale.getdefaultlocale()[0] - if lang is None and os.environ.has_key('LANG'): # Needed for OS X - try: - lang = os.environ['LANG'] - except: - pass - if lang: - match = re.match('[a-z]{2,3}', lang) - if match: - lang = match.group() - return lang - -def set_translator(): - # To test different translations invoke as - # LC_ALL=de_DE.utf8 program - try: - from calibre.translations.compiled import translations - except: - return - lang = get_lang() - if lang: - buf = None - if os.access(lang+'.po', os.R_OK): - buf = cStringIO.StringIO() - make(lang+'.po', buf) - buf = cStringIO.StringIO(buf.getvalue()) - elif translations.has_key(lang): - buf = cStringIO.StringIO(translations[lang]) - if buf is not None: - t = GNUTranslations(buf) - t.install(unicode=True) - -set_translator() def sanitize_file_name(name): ''' @@ -510,120 +254,6 @@ def relpath(target, base=os.curdir): rel_list = [os.pardir] * (len(base_list)-i) + target_list[i:] return os.path.join(*rel_list) -def _clean_lock_file(file): - try: - file.close() - except: - pass - try: - os.remove(file.name) - except: - pass - -class LockError(Exception): - pass -class ExclusiveFile(object): - - def __init__(self, path, timeout=10): - self.path = path - self.timeout = timeout - - def __enter__(self): - self.file = open(self.path, 'a+b') - self.file.seek(0) - timeout = self.timeout - if iswindows: - name = ('Local\\'+(__appname__+self.file.name).replace('\\', '_'))[:201] - while self.timeout < 0 or timeout >= 0: - self.mutex = win32event.CreateMutex(None, False, name) - if win32api.GetLastError() != winerror.ERROR_ALREADY_EXISTS: break - time.sleep(1) - timeout -= 1 - else: - while self.timeout < 0 or timeout >= 0: - try: - fcntl.lockf(self.file.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB) - break - except IOError: - time.sleep(1) - timeout -= 1 - if timeout < 0 and self.timeout >= 0: - self.file.close() - raise LockError - return self.file - - def __exit__(self, type, value, traceback): - self.file.close() - if iswindows: - win32api.CloseHandle(self.mutex) - -def singleinstance(name): - ''' - Return True if no other instance of the application identified by name is running, - False otherwise. - @param name: The name to lock. - @type name: string - ''' - if iswindows: - mutexname = 'mutexforsingleinstanceof'+__appname__+name - mutex = win32event.CreateMutex(None, False, mutexname) - if mutex: - atexit.register(win32api.CloseHandle, mutex) - return not win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS - else: - global _lock_file - path = os.path.expanduser('~/.'+__appname__+'_'+name+'.lock') - try: - f = open(path, 'w') - fcntl.lockf(f.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB) - atexit.register(_clean_lock_file, f) - return True - except IOError: - return False - - return False - -class Settings(QSettings): - - def __init__(self, name='calibre2'): - QSettings.__init__(self, QSettings.IniFormat, QSettings.UserScope, - 'kovidgoyal.net', name) - - def get(self, key, default=None): - try: - key = str(key) - if not self.contains(key): - return default - val = str(self.value(key, QVariant()).toByteArray()) - if not val: - return None - return cPickle.loads(val) - except: - return default - - def set(self, key, val): - val = cPickle.dumps(val, -1) - self.setValue(str(key), QVariant(QByteArray(val))) - -_settings = Settings() - -if not _settings.get('rationalized'): - __settings = Settings(name='calibre') - dbpath = os.path.join(os.path.expanduser('~'), 'library1.db').decode(sys.getfilesystemencoding()) - dbpath = unicode(__settings.value('database path', - QVariant(QString.fromUtf8(dbpath.encode('utf-8')))).toString()) - cmdline = __settings.value('LRF conversion defaults', QVariant(QByteArray(''))).toByteArray().data() - - if cmdline: - cmdline = cPickle.loads(cmdline) - _settings.set('LRF conversion defaults', cmdline) - _settings.set('rationalized', True) - try: - os.unlink(unicode(__settings.fileName())) - except: - pass - _settings.set('database path', dbpath) - _spat = re.compile(r'^the\s+|^a\s+|^an\s+', re.IGNORECASE) def english_sort(x, y): ''' @@ -671,11 +301,8 @@ def strftime(fmt, t=time.localtime()): A version of strtime that returns unicode strings. ''' result = time.strftime(fmt, t) - try: - return unicode(result, locale.getpreferredencoding(), 'replace') - except: - return unicode(result, 'utf-8', 'replace') - + return unicode(result, preferred_encoding, 'replace') + def entity_to_unicode(match, exceptions=[], encoding='cp1252'): ''' @param match: A match object such that '&'+match.group(1)';' is the entity. @@ -708,7 +335,7 @@ def entity_to_unicode(match, exceptions=[], encoding='cp1252'): return unichr(name2codepoint[ent]) except KeyError: return '&'+ent+';' - + if isosx: fdir = os.path.expanduser('~/.fonts') if not os.path.exists(fdir): @@ -716,6 +343,10 @@ if isosx: if not os.path.exists(os.path.join(fdir, 'LiberationSans_Regular.ttf')): from calibre.ebooks.lrf.fonts.liberation import __all__ as fonts for font in fonts: - exec 'from calibre.ebooks.lrf.fonts.liberation.'+font+' import font_data' - open(os.path.join(fdir, font+'.ttf'), 'wb').write(font_data) + l = {} + exec 'from calibre.ebooks.lrf.fonts.liberation.'+font+' import font_data' in l + open(os.path.join(fdir, font+'.ttf'), 'wb').write(l['font_data']) +# Migrate from QSettings based config system +from calibre.utils.config import migrate +migrate() \ No newline at end of file diff --git a/src/calibre/constants.py b/src/calibre/constants.py new file mode 100644 index 0000000000..f9a2380b41 --- /dev/null +++ b/src/calibre/constants.py @@ -0,0 +1,30 @@ +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' +__appname__ = 'calibre' +__version__ = '0.4.83' +__author__ = "Kovid Goyal " +''' +Various run time constants. +''' + +import sys, locale, codecs +from calibre.utils.terminfo import TerminalController + +terminal_controller = TerminalController(sys.stdout) + +iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower() +isosx = 'darwin' in sys.platform.lower() +islinux = not(iswindows or isosx) +isfrozen = hasattr(sys, 'frozen') + +try: + preferred_encoding = locale.getpreferredencoding() + codecs.lookup(preferred_encoding) +except: + preferred_encoding = 'utf-8' + +win32event = __import__('win32event') if iswindows else None +winerror = __import__('winerror') if iswindows else None +win32api = __import__('win32api') if iswindows else None +fcntl = None if iswindows else __import__('fcntl') \ No newline at end of file diff --git a/src/calibre/debug.py b/src/calibre/debug.py index f6f869cdb8..12bc4f8063 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -7,7 +7,8 @@ Embedded console for debugging. ''' import sys, os, re -from calibre import OptionParser, iswindows, isosx +from calibre.utils.config import OptionParser +from calibre.constants import iswindows, isosx from calibre.libunzip import update def option_parser(): diff --git a/src/calibre/ebooks/lit/reader.py b/src/calibre/ebooks/lit/reader.py index b76f6fb4f0..1668d2bc5c 100644 --- a/src/calibre/ebooks/lit/reader.py +++ b/src/calibre/ebooks/lit/reader.py @@ -808,7 +808,7 @@ class LitReader(object): os.makedirs(dir) def option_parser(): - from calibre import OptionParser + from calibre.utils.config import OptionParser parser = OptionParser(usage=_('%prog [options] LITFILE')) parser.add_option( '-o', '--output-dir', default='.', diff --git a/src/calibre/ebooks/lrf/__init__.py b/src/calibre/ebooks/lrf/__init__.py index 6b724fd008..03d59d825d 100644 --- a/src/calibre/ebooks/lrf/__init__.py +++ b/src/calibre/ebooks/lrf/__init__.py @@ -14,7 +14,8 @@ from calibre.ebooks.lrf.pylrs.pylrs import TextBlock, Header, PutObj, \ Paragraph, TextStyle, BlockStyle from calibre.ebooks.lrf.fonts import FONT_FILE_MAP from calibre.ebooks import ConversionError -from calibre import __appname__, __version__, __author__, iswindows, OptionParser +from calibre import __appname__, __version__, __author__, iswindows +from calibre.utils.config import OptionParser __docformat__ = "epytext" diff --git a/src/calibre/ebooks/lrf/html/convert_to.py b/src/calibre/ebooks/lrf/html/convert_to.py index 2fff037abe..a86e4e072e 100644 --- a/src/calibre/ebooks/lrf/html/convert_to.py +++ b/src/calibre/ebooks/lrf/html/convert_to.py @@ -2,7 +2,8 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' import sys, logging, os -from calibre import setup_cli_handlers, OptionParser +from calibre import setup_cli_handlers +from calibre.utils.config import OptionParser from calibre.ebooks import ConversionError from calibre.ebooks.lrf.meta import get_metadata from calibre.ebooks.lrf.lrfparser import LRFDocument diff --git a/src/calibre/ebooks/lrf/lrfparser.py b/src/calibre/ebooks/lrf/lrfparser.py index 0398836788..21d970ab29 100644 --- a/src/calibre/ebooks/lrf/lrfparser.py +++ b/src/calibre/ebooks/lrf/lrfparser.py @@ -4,7 +4,8 @@ __copyright__ = '2008, Kovid Goyal ' import sys, array, os, re, codecs, logging -from calibre import OptionParser, setup_cli_handlers +from calibre import setup_cli_handlers +from calibre.utils.config import OptionParser from calibre.ebooks.lrf.meta import LRFMetaFile from calibre.ebooks.lrf.objects import get_object, PageTree, StyleObject, \ Font, Text, TOCObject, BookAttr, ruby_tags diff --git a/src/calibre/ebooks/lrf/lrs/convert_from.py b/src/calibre/ebooks/lrf/lrs/convert_from.py index 2a065c2fb5..686e8a0839 100644 --- a/src/calibre/ebooks/lrf/lrs/convert_from.py +++ b/src/calibre/ebooks/lrf/lrs/convert_from.py @@ -6,7 +6,8 @@ Compile a LRS file into a LRF file. import sys, os, logging -from calibre import OptionParser, setup_cli_handlers +from calibre import setup_cli_handlers +from calibre.utils.config import OptionParser from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, NavigableString, \ CData, Tag from calibre.ebooks.lrf.pylrs.pylrs import Book, PageStyle, TextStyle, \ diff --git a/src/calibre/ebooks/lrf/meta.py b/src/calibre/ebooks/lrf/meta.py index 80fc542c1c..9996b2be84 100644 --- a/src/calibre/ebooks/lrf/meta.py +++ b/src/calibre/ebooks/lrf/meta.py @@ -574,8 +574,8 @@ class LRFMetaFile(object): def option_parser(): - from optparse import OptionParser - from calibre import __appname__, __version__ + from calibre.utils.config import OptionParser + from calibre.constants import __appname__, __version__ parser = OptionParser(usage = \ _('''%prog [options] mybook.lrf diff --git a/src/calibre/ebooks/lrf/pdf/reflow.py b/src/calibre/ebooks/lrf/pdf/reflow.py index f553aa1b85..87e8837896 100644 --- a/src/calibre/ebooks/lrf/pdf/reflow.py +++ b/src/calibre/ebooks/lrf/pdf/reflow.py @@ -7,7 +7,8 @@ Convert PDF to a reflowable format using pdftoxml.exe as the PDF parsing backend import sys, os, re, tempfile, subprocess, atexit, shutil, logging, xml.parsers.expat from xml.etree.ElementTree import parse -from calibre import isosx, OptionParser, setup_cli_handlers, __appname__ +from calibre import isosx, setup_cli_handlers, __appname__ +from calibre.utils.config import OptionParser from calibre.ebooks import ConversionError PDFTOXML = 'pdftoxml.exe' diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 413e284794..180a07afa8 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -11,8 +11,9 @@ from urllib import unquote, quote from urlparse import urlparse -from calibre import __version__ as VERSION, relpath -from calibre import OptionParser +from calibre.constants import __version__ as VERSION +from calibre import relpath +from calibre.utils.config import OptionParser def get_parser(extension): ''' Return an option parser with the basic metadata options already setup''' diff --git a/src/calibre/ebooks/metadata/isbndb.py b/src/calibre/ebooks/metadata/isbndb.py index ec81684ef2..1f0dde3696 100644 --- a/src/calibre/ebooks/metadata/isbndb.py +++ b/src/calibre/ebooks/metadata/isbndb.py @@ -7,7 +7,8 @@ Interface to isbndb.com. My key HLLXQX2A. import sys, logging, re, socket from urllib import urlopen, quote -from calibre import setup_cli_handlers, OptionParser +from calibre import setup_cli_handlers +from calibre.utils.config import OptionParser from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup diff --git a/src/calibre/ebooks/metadata/library_thing.py b/src/calibre/ebooks/metadata/library_thing.py index 9a254b0613..f93ffafd66 100644 --- a/src/calibre/ebooks/metadata/library_thing.py +++ b/src/calibre/ebooks/metadata/library_thing.py @@ -6,7 +6,8 @@ Fetch cover from LibraryThing.com based on ISBN number. import sys, socket, os, re, mechanize -from calibre import browser as _browser, OptionParser +from calibre import browser as _browser +from calibre.utils.config import OptionParser from calibre.ebooks.BeautifulSoup import BeautifulSoup browser = None diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py index 1c84af8009..4fa0f180be 100644 --- a/src/calibre/ebooks/metadata/meta.py +++ b/src/calibre/ebooks/metadata/meta.py @@ -3,6 +3,7 @@ __copyright__ = '2008, Kovid Goyal ' import os, re, collections +from calibre.utils.config import prefs from calibre.ebooks.metadata.rtf import get_metadata as rtf_metadata from calibre.ebooks.metadata.fb2 import get_metadata as fb2_metadata from calibre.ebooks.lrf.meta import get_metadata as lrf_metadata @@ -94,20 +95,11 @@ def set_metadata(stream, mi, stream_type='lrf'): elif stream_type == 'rtf': set_rtf_metadata(stream, mi) -_filename_pat = re.compile(ur'(?P.+) - (?P<author>[^_]+)') - -def get_filename_pat(): - return _filename_pat.pattern - -def set_filename_pat(pat): - global _filename_pat - _filename_pat = re.compile(pat) - def metadata_from_filename(name, pat=None): name = os.path.splitext(name)[0] mi = MetaInformation(None, None) if pat is None: - pat = _filename_pat + pat = re.compile(prefs.get('filename_pattern')) match = pat.search(name) if match: try: diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 7053b7591f..fccb4f68b4 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -408,7 +408,7 @@ def get_metadata(stream): return mi def option_parser(): - from calibre import OptionParser + from calibre.utils.config import OptionParser parser = OptionParser(usage=_('%prog [options] myebook.mobi')) parser.add_option('-o', '--output-dir', default='.', help=_('Output directory. Defaults to current directory.')) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 8c2b8e51b4..df2596564b 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -2,19 +2,49 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' """ The GUI """ import sys, os, re, StringIO, traceback -from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ +from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \ QByteArray, QLocale, QUrl, QTranslator, QCoreApplication from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ QIcon, QTableView, QDialogButtonBox, QApplication ORG_NAME = 'KovidsBrain' APP_UID = 'libprs500' -from calibre import __author__, islinux, iswindows, Settings, isosx, get_lang +from calibre import __author__, islinux, iswindows, isosx +from calibre.startup import get_lang +from calibre.utils.config import Config, ConfigProxy, dynamic import calibre.resources as resources NONE = QVariant() #: Null value to return from the data function of item models - +def _config(): + c = Config('gui', 'preferences for the calibre GUI') + c.add_opt('frequently_used_directories', default=[], + help=_('Frequently used directories')) + c.add_opt('send_to_device_by_default', default=True, + help=_('Send downloaded periodical content to device automatically')) + c.add_opt('save_to_disk_single_format', default='lrf', + help=_('The format to use when saving single files to disk')) + c.add_opt('confirm_delete', default=False, + help=_('Confirm before deleting')) + c.add_opt('toolbar_icon_size', default=QSize(48, 48), + help=_('Toolbar icon size')) # value QVariant.toSize + c.add_opt('show_text_in_toolbar', default=True, + help=_('Show button labels in the toolbar')) + c.add_opt('main_window_geometry', default=None, + help=_('Main window geometry')) # value QVariant.toByteArray + c.add_opt('new_version_notification', default=True, + help=_('Notify when a new version is available')) + c.add_opt('use_roman_numerals_for_series_number', default=True, + help=_('Use Roman numerals for series number')) + c.add_opt('cover_flow_queue_length', default=6, + help=_('Number of covers to show in the cover browsing mode')) + c.add_opt('LRF_conversion_defaults', default=[], + help=_('Defaults for conversion to LRF')) + c.add_opt('LRF_ebook_viewer_options', default=None, + help=_('Options for the LRF ebook viewer')) + return ConfigProxy(c) + +config = _config() # Turn off DeprecationWarnings in windows GUI if iswindows: import warnings @@ -105,14 +135,12 @@ class TableView(QTableView): QTableView.__init__(self, parent) self.read_settings() - def read_settings(self): - self.cw = Settings().get(self.__class__.__name__ + ' column widths') + self.cw = dynamic[self.__class__.__name__+'column widths'] def write_settings(self): - settings = Settings() - settings.set(self.__class__.__name__ + ' column widths', - tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))])) + dynamic[self.__class__.__name__+'column widths'] = \ + tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))]) def restore_column_widths(self): if self.cw and len(self.cw): @@ -125,10 +153,11 @@ class TableView(QTableView): @param cols: A list of booleans or None. If an entry is False the corresponding column is hidden, if True it is shown. ''' + key = self.__class__.__name__+'visible columns' if cols: - Settings().set(self.__class__.__name__ + ' visible columns', cols) + dynamic[key] = cols else: - cols = Settings().get(self.__class__.__name__ + ' visible columns') + cols = dynamic[key] if not cols: cols = [True for i in range(self.model().columnCount(self))] @@ -232,7 +261,7 @@ _sidebar_directories = [] def set_sidebar_directories(dirs): global _sidebar_directories if dirs is None: - dirs = Settings().get('frequently used directories', []) + dirs = config['frequently_used_directories'] _sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs] class FileDialog(QObject): @@ -255,10 +284,10 @@ class FileDialog(QObject): if add_all_files_filter or not ftext: ftext += 'All files (*)' - settings = Settings() self.dialog_name = name if name else 'dialog_' + title self.selected_files = None self.fd = None + if islinux: self.fd = QFileDialog(parent) self.fd.setFileMode(mode) @@ -266,15 +295,15 @@ class FileDialog(QObject): self.fd.setModal(modal) self.fd.setFilter(ftext) self.fd.setWindowTitle(title) - state = settings.get(self.dialog_name, QByteArray()) - if not self.fd.restoreState(state): + state = dynamic[self.dialog_name] + if not state or not self.fd.restoreState(state): self.fd.setDirectory(os.path.expanduser('~')) osu = [i for i in self.fd.sidebarUrls()] self.fd.setSidebarUrls(osu + _sidebar_directories) QObject.connect(self.fd, SIGNAL('accepted()'), self.save_dir) self.accepted = self.fd.exec_() == QFileDialog.Accepted else: - dir = settings.get(self.dialog_name, os.path.expanduser('~')) + dir = dynamic.get(self.dialog_name, default=os.path.expanduser('~')) self.selected_files = [] if mode == QFileDialog.AnyFile: f = qstring_to_unicode( @@ -299,7 +328,7 @@ class FileDialog(QObject): self.selected_files.append(f) if self.selected_files: self.selected_files = [qstring_to_unicode(q) for q in self.selected_files] - settings.set(self.dialog_name, os.path.dirname(self.selected_files[0])) + dynamic[self.dialog_name] = os.path.dirname(self.selected_files[0]) self.accepted = bool(self.selected_files) @@ -313,8 +342,7 @@ class FileDialog(QObject): def save_dir(self): if self.fd: - settings = Settings() - settings.set(self.dialog_name, self.fd.saveState()) + dynamic[self.dialog_name] = self.fd.saveState() def choose_dir(window, name, title): diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index 3a40e03200..196e723be7 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -12,7 +12,8 @@ import sys, os from PyQt4.QtGui import QImage, QSizePolicy from PyQt4.QtCore import Qt, QSize, SIGNAL, QObject -from calibre import Settings, plugins +from calibre import plugins +from calibre.gui2 import config pictureflow, pictureflowerror = plugins['pictureflow'] if pictureflow is not None: @@ -70,7 +71,7 @@ if pictureflow is not None: def __init__(self, height=300, parent=None): pictureflow.PictureFlow.__init__(self, parent, - Settings().get('cover flow queue length', 6)+1) + config['cover_flow_queue_length']+1) self.setSlideSize(QSize(int(2/3. * height), height)) self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+25)) self.setFocusPolicy(Qt.WheelFocus) diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index e4379deb56..c84c3a3713 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -5,9 +5,10 @@ import os from PyQt4.QtGui import QDialog, QMessageBox, QListWidgetItem, QIcon from PyQt4.QtCore import SIGNAL, QTimer, Qt, QSize, QVariant -from calibre import islinux, Settings +from calibre import islinux from calibre.gui2.dialogs.config_ui import Ui_Dialog -from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog +from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config +from calibre.utils.config import prefs from calibre.gui2.widgets import FilenamePattern from calibre.ebooks import BOOK_EXTENSIONS @@ -24,18 +25,17 @@ class ConfigDialog(QDialog, Ui_Dialog): self.item2 = QListWidgetItem(QIcon(':/images/view.svg'), _('Advanced'), self.category_list) self.db = db self.current_cols = columns - settings = Settings() - path = settings.get('database path') + path = prefs['database_path'] self.location.setText(os.path.dirname(path)) self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse) self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact) - dirs = settings.get('frequently used directories', []) - rn = settings.get('use roman numerals for series number', True) - self.timeout.setValue(settings.get('network timeout', 5)) + dirs = config['frequently_used_directories'] + rn = config['use_roman_numerals_for_series_number'] + self.timeout.setValue(prefs['network_timeout']) self.roman_numerals.setChecked(rn) - self.new_version_notification.setChecked(settings.get('new version notification', True)) + self.new_version_notification.setChecked(config['new_version_notification']) self.directory_list.addItems(dirs) self.connect(self.add_button, SIGNAL('clicked(bool)'), self.add_dir) self.connect(self.remove_button, SIGNAL('clicked(bool)'), self.remove_dir) @@ -57,17 +57,17 @@ class ConfigDialog(QDialog, Ui_Dialog): self.filename_pattern = FilenamePattern(self) self.metadata_box.layout().insertWidget(0, self.filename_pattern) - icons = settings.get('toolbar icon size', self.ICON_SIZES[0]) + icons = config['toolbar_icon_size'] self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2) - self.show_toolbar_text.setChecked(settings.get('show text in toolbar', True)) + self.show_toolbar_text.setChecked(config['show_text_in_toolbar']) for ext in BOOK_EXTENSIONS: self.single_format.addItem(ext.upper(), QVariant(ext)) - single_format = settings.get('save to disk single format', 'lrf') + single_format = config['save_to_disk_single_format'] self.single_format.setCurrentIndex(BOOK_EXTENSIONS.index(single_format)) - self.cover_browse.setValue(settings.get('cover flow queue length', 6)) - self.confirm_delete.setChecked(settings.get('confirm delete', False)) + self.cover_browse.setValue(config['cover_flow_queue_length']) + self.confirm_delete.setChecked(config['confirm_delete']) def compact(self, toggled): d = Vacuum(self, self.db) @@ -89,19 +89,18 @@ class ConfigDialog(QDialog, Ui_Dialog): self.directory_list.takeItem(idx) def accept(self): - settings = Settings() - settings.set('use roman numerals for series number', bool(self.roman_numerals.isChecked())) - settings.set('new version notification', bool(self.new_version_notification.isChecked())) - settings.set('network timeout', int(self.timeout.value())) + config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked()) + config['new_version_notification'] = bool(self.new_version_notification.isChecked()) + prefs['network_timeout'] = int(self.timeout.value()) path = qstring_to_unicode(self.location.text()) self.final_columns = [self.columns.item(i).checkState() == Qt.Checked for i in range(self.columns.count())] - settings.set('toolbar icon size', self.ICON_SIZES[self.toolbar_button_size.currentIndex()]) - settings.set('show text in toolbar', bool(self.show_toolbar_text.isChecked())) - settings.set('confirm delete', bool(self.confirm_delete.isChecked())) + config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()] + config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked()) + config['confirm_delete'] = bool(self.confirm_delete.isChecked()) pattern = self.filename_pattern.commit() - settings.set('filename pattern', pattern) - settings.set('save to disk single format', BOOK_EXTENSIONS[self.single_format.currentIndex()]) - settings.set('cover flow queue length', self.cover_browse.value()) + config['filename_pattern'] = pattern + config['save_to_disk_single_format'] = BOOK_EXTENSIONS[self.single_format.currentIndex()] + config['cover_flow_queue_length'] = self.cover_browse.value() if not path or not os.path.exists(path) or not os.path.isdir(path): d = error_dialog(self, _('Invalid database location'), _('Invalid database location ')+path+_('<br>Must be a directory.')) @@ -112,7 +111,7 @@ class ConfigDialog(QDialog, Ui_Dialog): else: self.database_location = os.path.abspath(path) self.directories = [qstring_to_unicode(self.directory_list.item(i).text()) for i in range(self.directory_list.count())] - settings.set('frequently used directories', self.directories) + config['frequently_used_directories'] = self.directories QDialog.accept(self) class Vacuum(QMessageBox): diff --git a/src/calibre/gui2/dialogs/fetch_metadata.py b/src/calibre/gui2/dialogs/fetch_metadata.py index f905e72e81..76a5979f9e 100644 --- a/src/calibre/gui2/dialogs/fetch_metadata.py +++ b/src/calibre/gui2/dialogs/fetch_metadata.py @@ -13,7 +13,7 @@ from PyQt4.QtGui import QDialog, QItemSelectionModel from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata from calibre.gui2 import error_dialog, NONE, info_dialog from calibre.ebooks.metadata.isbndb import create_books, option_parser, ISBNDBError -from calibre import Settings +from calibre.utils.config import prefs class Matches(QAbstractTableModel): @@ -76,7 +76,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): self.timeout = timeout QObject.connect(self.fetch, SIGNAL('clicked()'), self.fetch_metadata) - self.key.setText(Settings().get('isbndb.com key', '')) + self.key.setText(prefs['isbndb_com_key']) self.setWindowTitle(title if title else 'Unknown') self.tlabel.setText(self.tlabel.text().arg(title if title else 'Unknown')) @@ -105,7 +105,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): _('You must specify a valid access key for isbndb.com')) return else: - Settings().set('isbndb.com key', key) + prefs['isbndb_com_key'] = key args = ['isbndb'] if self.isbn: diff --git a/src/calibre/gui2/dialogs/lrf_single.py b/src/calibre/gui2/dialogs/lrf_single.py index e8f3d61b7c..787df4d080 100644 --- a/src/calibre/gui2/dialogs/lrf_single.py +++ b/src/calibre/gui2/dialogs/lrf_single.py @@ -9,11 +9,11 @@ from PyQt4.QtGui import QAbstractSpinBox, QLineEdit, QCheckBox, QDialog, \ from calibre.gui2.dialogs.lrf_single_ui import Ui_LRFSingleDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2 import qstring_to_unicode, error_dialog, \ - pixmap_to_data, choose_images + pixmap_to_data, choose_images, config from calibre.gui2.widgets import FontFamilyModel from calibre.ebooks.lrf import option_parser from calibre.ptempfile import PersistentTemporaryFile -from calibre import __appname__, Settings +from calibre.constants import __appname__ font_family_model = None @@ -109,7 +109,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog): def load_saved_global_defaults(self): - cmdline = Settings().get('LRF conversion defaults') + cmdline = config['LRF_conversion_defaults'] if cmdline: self.set_options_from_cmdline(cmdline) @@ -163,7 +163,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog): def select_cover(self, checked): files = choose_images(self, 'change cover dialog', - u'Choose cover for ' + qstring_to_unicode(self.gui_title.text())) + _('Choose cover for ') + qstring_to_unicode(self.gui_title.text())) if not files: return _file = files[0] @@ -385,7 +385,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog): cmdline.extend([u'--cover', self.cover_file.name]) self.cmdline = [unicode(i) for i in cmdline] else: - Settings().set('LRF conversion defaults', cmdline) + config.set('LRF_conversion_defaults', cmdline) QDialog.accept(self) class LRFBulkDialog(LRFSingleDialog): diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 14baa0de91..fa6ea780d2 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -18,7 +18,8 @@ from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.dialogs.password import PasswordDialog from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError -from calibre import Settings, islinux +from calibre import islinux +from calibre.utils.config import prefs class Format(QListWidgetItem): def __init__(self, parent, ext, size, path=None): @@ -145,7 +146,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog): QObject.connect(self.remove_series_button, SIGNAL('clicked()'), self.remove_unused_series) self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author) - self.timeout = float(Settings().get('network timeout', 5)) + self.timeout = float(prefs['network_timeout']) self.title.setText(db.title(row)) isbn = db.isbn(self.id, index_is_id=True) if not isbn: diff --git a/src/calibre/gui2/dialogs/password.py b/src/calibre/gui2/dialogs/password.py index 9248f313d4..31e01a25cf 100644 --- a/src/calibre/gui2/dialogs/password.py +++ b/src/calibre/gui2/dialogs/password.py @@ -1,12 +1,11 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' - +import re from PyQt4.QtGui import QDialog, QLineEdit -from PyQt4.QtCore import QVariant, SIGNAL, Qt +from PyQt4.QtCore import SIGNAL, Qt from calibre.gui2.dialogs.password_ui import Ui_Dialog -from calibre.gui2 import qstring_to_unicode -from calibre import Settings +from calibre.gui2 import qstring_to_unicode, dynamic class PasswordDialog(QDialog, Ui_Dialog): @@ -14,10 +13,12 @@ class PasswordDialog(QDialog, Ui_Dialog): QDialog.__init__(self, window) Ui_Dialog.__init__(self) self.setupUi(self) + self.cfg_key = re.sub(r'[^0-9a-zA-Z]', '_', name) - settings = Settings() - un = settings.get(name+': un', u'') - pw = settings.get(name+': pw', u'') + un = dynamic[self.cfg_key+'__un'] + pw = dynamic[self.cfg_key+'__un'] + if not un: un = '' + if not pw: pw = '' self.gui_username.setText(un) self.gui_password.setText(pw) self.sname = name @@ -37,7 +38,6 @@ class PasswordDialog(QDialog, Ui_Dialog): return qstring_to_unicode(self.gui_password.text()) def accept(self): - settings = Settings() - settings.set(self.sname+': un', unicode(self.gui_username.text())) - settings.set(self.sname+': pw', unicode(self.gui_password.text())) + dynamic.set(self.cfg_key+'__un', unicode(self.gui_username.text())) + dynamic.set(self.cfg_key+'__pw', unicode(self.gui_password.text())) QDialog.accept(self) diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 76527c3034..24ccc46707 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -14,10 +14,10 @@ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \ QCoreApplication, SIGNAL, QObject, QSize, QModelIndex, \ QTimer -from calibre import Settings, preferred_encoding +from calibre import preferred_encoding from calibre.ptempfile import PersistentTemporaryFile from calibre.library.database import LibraryDatabase, text_to_tokens -from calibre.gui2 import NONE, TableView, qstring_to_unicode +from calibre.gui2 import NONE, TableView, qstring_to_unicode, config class LibraryDelegate(QItemDelegate): COLOR = QColor("blue") @@ -117,7 +117,7 @@ class BooksModel(QAbstractTableModel): self.load_queue = deque() def read_config(self): - self.use_roman_numbers = Settings().get('use roman numerals for series number', True) + self.use_roman_numbers = config['use_roman_numerals_for_series_number'] def set_database(self, db): diff --git a/src/calibre/gui2/lrf_renderer/main.py b/src/calibre/gui2/lrf_renderer/main.py index 2fc4222a8f..8e3b805f7a 100644 --- a/src/calibre/gui2/lrf_renderer/main.py +++ b/src/calibre/gui2/lrf_renderer/main.py @@ -6,10 +6,11 @@ import sys, logging, os, traceback, time from PyQt4.QtGui import QKeySequence, QPainter, QDialog, QSpinBox, QSlider, QIcon from PyQt4.QtCore import Qt, QObject, SIGNAL, QCoreApplication, QThread -from calibre import __appname__, setup_cli_handlers, islinux, Settings +from calibre import __appname__, setup_cli_handlers, islinux from calibre.ebooks.lrf.lrfparser import LRFDocument -from calibre.gui2 import ORG_NAME, APP_UID, error_dialog, choose_files, Application +from calibre.gui2 import ORG_NAME, APP_UID, error_dialog, \ + config, choose_files, Application from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog from calibre.gui2.lrf_renderer.main_ui import Ui_MainWindow from calibre.gui2.lrf_renderer.config_ui import Ui_ViewerConfig @@ -102,13 +103,15 @@ class Main(MainWindow, Ui_MainWindow): def configure(self, triggered): - opts = Settings().get('LRF ebook viewer options', self.opts) + opts = config['LRF_ebook_viewer_options'] + if not opts: + opts = self.opts d = Config(self, opts) d.exec_() if d.result() == QDialog.Accepted: opts.white_background = bool(d.white_background.isChecked()) opts.hyphenate = bool(d.hyphenate.isChecked()) - Settings().set('LRF ebook viewer options', opts) + config['LRF_ebook_viewer_options'] = opts def set_ebook(self, stream): self.progress_bar.setMinimum(0) @@ -281,7 +284,9 @@ Read the LRF ebook book.lrf return parser def normalize_settings(parser, opts): - saved_opts = Settings().get('LRF ebook viewer options', opts) + saved_opts = config['LRF_ebook_viewer_options'] + if not saved_opts: + saved_opts = opts for opt in parser.option_list: if not opt.dest: continue diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 576b4f9a98..96c8c8d9a4 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -3,23 +3,24 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' import os, sys, textwrap, collections, traceback, shutil, time from xml.parsers.expat import ExpatError from functools import partial -from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ - QVariant, QUrl, QSize +from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \ QToolButton, QDialog, QDesktopServices from PyQt4.QtSvg import QSvgRenderer from calibre import __version__, __appname__, islinux, sanitize_file_name, \ - Settings, iswindows, isosx, preferred_encoding + iswindows, isosx, preferred_encoding from calibre.ptempfile import PersistentTemporaryFile -from calibre.ebooks.metadata.meta import get_metadata, get_filename_pat, set_filename_pat +from calibre.ebooks.metadata.meta import get_metadata from calibre.devices.errors import FreeSpaceError from calibre.devices.interface import Device +from calibre.utils.config import prefs, dynamic from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \ initialize_file_icon_provider, question_dialog,\ pixmap_to_data, choose_dir, ORG_NAME, \ set_sidebar_directories, Dispatcher, \ - SingleApplication, Application, available_height, max_available_height + SingleApplication, Application, available_height, \ + max_available_height, config from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror from calibre.library.database import LibraryDatabase from calibre.gui2.update import CheckForUpdates @@ -120,12 +121,12 @@ class Main(MainWindow, Ui_MainWindow): sm.addAction(_('Send to storage card by default')) sm.actions()[-1].setCheckable(True) def default_sync(checked): - Settings().set('send to device by default', bool(checked)) + config.set('send_to_device_by_default', bool(checked)) QObject.disconnect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_main_memory) QObject.disconnect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_card) QObject.connect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_card if checked else self.sync_to_main_memory) QObject.connect(sm.actions()[-1], SIGNAL('toggled(bool)'), default_sync) - sm.actions()[-1].setChecked(Settings().get('send to device by default', False)) + sm.actions()[-1].setChecked(config.get('send_to_device_by_default')) default_sync(sm.actions()[-1].isChecked()) self.sync_menu = sm # Needed md = QMenu() @@ -152,7 +153,7 @@ class Main(MainWindow, Ui_MainWindow): self.save_menu = QMenu() self.save_menu.addAction(_('Save to disk')) self.save_menu.addAction(_('Save to disk in a single directory')) - self.save_menu.addAction(_('Save only %s format to disk')%Settings().get('save to disk single format', 'lrf').upper()) + self.save_menu.addAction(_('Save only %s format to disk')%config.get('save_to_disk_single_format').upper()) self.view_menu = QMenu() self.view_menu.addAction(_('View')) @@ -529,7 +530,7 @@ class Main(MainWindow, Ui_MainWindow): rows = view.selectionModel().selectedRows() if not rows or len(rows) == 0: return - if Settings().get('confirm delete', False): + if config['confirm_delete']: d = question_dialog(self, _('Confirm delete'), _('Are you sure you want to delete these %d books?')%len(rows)) if d.exec_() != QMessageBox.Yes: @@ -680,7 +681,7 @@ class Main(MainWindow, Ui_MainWindow): ############################## Save to disk ################################ def save_single_format_to_disk(self, checked): - self.save_to_disk(checked, True, Settings().get('save to disk single format', 'lrf')) + self.save_to_disk(checked, True, config['save_to_disk_single_format']) def save_to_single_dir(self, checked): self.save_to_disk(checked, True) @@ -1067,9 +1068,8 @@ class Main(MainWindow, Ui_MainWindow): d.exec_() if d.result() == d.Accepted: self.library_view.set_visible_columns(d.final_columns) - settings = Settings() - self.tool_bar.setIconSize(settings.value('toolbar icon size', QVariant(QSize(48, 48))).toSize()) - self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if settings.get('show text in toolbar', True) else Qt.ToolButtonIconOnly) + self.tool_bar.setIconSize(config['toolbar_icon_size']) + self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if config['show_text_in_toolbar'] else Qt.ToolButtonIconOnly) if os.path.dirname(self.database_path) != d.database_location: try: @@ -1100,8 +1100,7 @@ class Main(MainWindow, Ui_MainWindow): d.exec_() newloc = self.database_path self.database_path = newloc - settings = Settings() - settings.set('database path', self.database_path) + prefs().set('database_path', self.database_path) except Exception, err: traceback.print_exc() d = error_dialog(self, _('Could not move database'), unicode(err)) @@ -1200,12 +1199,10 @@ class Main(MainWindow, Ui_MainWindow): def read_settings(self): - settings = Settings() - settings.beginGroup('Main Window') - geometry = settings.value('main window geometry', QVariant()).toByteArray() - self.restoreGeometry(geometry) - settings.endGroup() - self.database_path = settings.get('database path') + geometry = config['main_window_geometry'] + if geometry is not None: + self.restoreGeometry(geometry) + self.database_path = prefs['database_path'] if not os.access(os.path.dirname(self.database_path), os.W_OK): error_dialog(self, _('Database does not exist'), _('The directory in which the database should be: %s no longer exists. Please choose a new database location.')%self.database_path).exec_() self.database_path = choose_dir(self, 'database path dialog', 'Choose new location for database') @@ -1214,24 +1211,18 @@ class Main(MainWindow, Ui_MainWindow): if not os.path.exists(self.database_path): os.makedirs(self.database_path) self.database_path = os.path.join(self.database_path, 'library1.db') - settings.set('database path', self.database_path) + prefs.set('database_path', self.database_path) set_sidebar_directories(None) - set_filename_pat(settings.get('filename pattern', get_filename_pat())) - self.tool_bar.setIconSize(settings.get('toolbar icon size', QSize(48, 48))) - self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if settings.get('show text in toolbar', True) else Qt.ToolButtonIconOnly) + self.tool_bar.setIconSize(config['toolbar_icon_size']) + self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if config['show_text_in_toolbar'] else Qt.ToolButtonIconOnly) def write_settings(self): - settings = Settings() - settings.beginGroup("Main Window") - settings.setValue("main window geometry", QVariant(self.saveGeometry())) - settings.endGroup() - settings.beginGroup('Book Views') + config.set('main_window_geometry', self.saveGeometry()) self.library_view.write_settings() if self.device_connected: self.memory_view.write_settings() - settings.endGroup() - + def closeEvent(self, e): msg = 'There are active jobs. Are you sure you want to quit?' if self.job_manager.has_device_jobs(): @@ -1261,17 +1252,16 @@ class Main(MainWindow, Ui_MainWindow): self.vanity.setText(self.vanity_template%(dict(version=self.latest_version, device=self.device_info))) self.vanity.update() - s = Settings() - if s.get('new version notification', True) and s.get('update to version %s'%version, True): + if config.get('new_version_notification') and dynamic.get('update to version %s'%version, True): d = question_dialog(self, _('Update available'), _('%s has been updated to version %s. See the <a href="http://calibre.kovidgoyal.net/wiki/Changelog">new features</a>. Visit the download page?')%(__appname__, version)) if d.exec_() == QMessageBox.Yes: url = 'http://calibre.kovidgoyal.net/download_'+('windows' if iswindows else 'osx' if isosx else 'linux') QDesktopServices.openUrl(QUrl(url)) - s.set('update to version %s'%version, False) + dynamic.set('update to version %s'%version, False) def main(args=sys.argv): - from calibre import singleinstance + from calibre.utils.lock import singleinstance pid = os.fork() if False and islinux else -1 if pid <= 0: diff --git a/src/calibre/gui2/main_window.py b/src/calibre/gui2/main_window.py index f9fbeaac3b..db9cdd389d 100644 --- a/src/calibre/gui2/main_window.py +++ b/src/calibre/gui2/main_window.py @@ -5,7 +5,7 @@ import StringIO, traceback, sys from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog -from calibre import OptionParser +from calibre.utils.config import OptionParser def option_parser(usage='''\ Usage: %prog [options] diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 7fe8a278e8..ad89a08cb6 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -9,15 +9,16 @@ from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \ QSyntaxHighlighter, QCursor, QColor, QWidget, \ QAbstractItemDelegate, QPixmap, QStyle, QFontMetrics from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \ - QObject, QRegExp, QString + QObject, QRegExp, QString, QSettings from calibre.gui2.jobs2 import DetailView -from calibre.gui2 import human_readable, NONE, TableView, qstring_to_unicode, error_dialog +from calibre.gui2 import human_readable, NONE, TableView, \ + qstring_to_unicode, error_dialog from calibre.gui2.filename_pattern_ui import Ui_Form -from calibre import fit_image, Settings +from calibre import fit_image from calibre.utils.fontconfig import find_font_families -from calibre.ebooks.metadata.meta import get_filename_pat, metadata_from_filename, \ - set_filename_pat +from calibre.ebooks.metadata.meta import metadata_from_filename +from calibre.utils.config import prefs @@ -29,7 +30,7 @@ class FilenamePattern(QWidget, Ui_Form): self.connect(self.test_button, SIGNAL('clicked()'), self.do_test) self.connect(self.re, SIGNAL('returnPressed()'), self.do_test) - self.re.setText(get_filename_pat()) + self.re.setText(prefs['filename_pattern']) def do_test(self): try: @@ -66,9 +67,9 @@ class FilenamePattern(QWidget, Ui_Form): return re.compile(pat) def commit(self): - pat = self.pattern() - set_filename_pat(pat) - return pat.pattern + pat = self.pattern().pattern + prefs['filename_pattern'] = pat + return pat @@ -367,13 +368,13 @@ class PythonHighlighter(QSyntaxHighlighter): @classmethod def loadConfig(cls): Config = cls.Config + settings = QSettings() def setDefaultString(name, default): value = settings.value(name).toString() if value.isEmpty(): value = default Config[name] = value - settings = Settings() for name in ("window", "shell"): Config["%swidth" % name] = settings.value("%swidth" % name, QVariant(QApplication.desktop() \ diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index fe1fe6580b..cd5f40a778 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -10,7 +10,8 @@ Command line interface to the calibre database. import sys, os from textwrap import TextWrapper -from calibre import OptionParser, Settings, terminal_controller, preferred_encoding +from calibre import terminal_controller, preferred_encoding +from calibre.utils.config import OptionParser, prefs try: from calibre.utils.single_qt_application import send_message except: @@ -24,7 +25,8 @@ FIELDS = set(['title', 'authors', 'publisher', 'rating', 'timestamp', 'size', 't def get_parser(usage): parser = OptionParser(usage) go = parser.add_option_group('GLOBAL OPTIONS') - go.add_option('--database', default=None, help=_('Path to the calibre database. Default is to use the path stored in the settings.')) + go.add_option('--database', default=None, + help=_('Path to the calibre database. Default is to use the path stored in the settings.')) return parser def get_db(dbpath, options): @@ -422,7 +424,7 @@ For help on an individual command: %%prog command --help return 1 command = eval('command_'+args[1]) - dbpath = Settings().get('database path') + dbpath = prefs.get('database_path') return command(args[2:], dbpath) diff --git a/src/calibre/linux_installer.py b/src/calibre/linux_installer.py index d2e19e913a..f494bd0b14 100644 --- a/src/calibre/linux_installer.py +++ b/src/calibre/linux_installer.py @@ -244,6 +244,7 @@ def do_postinstall(destdir): os.chdir(destdir) os.environ['LD_LIBRARY_PATH'] = destdir+':'+os.environ.get('LD_LIBRARY_PATH', '') os.environ['PYTHONPATH'] = destdir + os.environ['PYTHONSTARTUP'] = '' subprocess.call((os.path.join(destdir, 'calibre_postinstall'), '--save-manifest-to', t.name)) finally: os.chdir(cwd) diff --git a/src/calibre/startup.py b/src/calibre/startup.py new file mode 100644 index 0000000000..347a1fab93 --- /dev/null +++ b/src/calibre/startup.py @@ -0,0 +1,149 @@ +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +Perform various initialization tasks. +''' + +import locale, sys, os, re, cStringIO +from gettext import GNUTranslations + +# Default translation is NOOP +import __builtin__ +__builtin__.__dict__['_'] = lambda s: s + +from calibre.constants import iswindows, isosx, islinux, isfrozen +from calibre.translations.msgfmt import make + +_run_once = False +if not _run_once: + _run_once = True + ################################################################################ + # Setup translations + + def get_lang(): + lang = locale.getdefaultlocale()[0] + if lang is None and os.environ.has_key('LANG'): # Needed for OS X + try: + lang = os.environ['LANG'] + except: + pass + if lang: + match = re.match('[a-z]{2,3}', lang) + if match: + lang = match.group() + return lang + + def set_translator(): + # To test different translations invoke as + # LC_ALL=de_DE.utf8 program + try: + from calibre.translations.compiled import translations + except: + return + lang = get_lang() + if lang: + buf = None + if os.access(lang+'.po', os.R_OK): + buf = cStringIO.StringIO() + make(lang+'.po', buf) + buf = cStringIO.StringIO(buf.getvalue()) + elif translations.has_key(lang): + buf = cStringIO.StringIO(translations[lang]) + if buf is not None: + t = GNUTranslations(buf) + t.install(unicode=True) + + set_translator() + + ################################################################################ + # Initialize locale + try: + locale.setlocale(locale.LC_ALL, '') + except: + dl = locale.getdefaultlocale() + try: + if dl: + locale.setlocale(dl[0]) + except: + pass + + ################################################################################ + # Load plugins + if isfrozen: + if iswindows: + plugin_path = os.path.join(os.path.dirname(sys.executable), 'plugins') + sys.path.insert(1, os.path.dirname(sys.executable)) + elif isosx: + plugin_path = os.path.join(getattr(sys, 'frameworks_dir'), 'plugins') + elif islinux: + plugin_path = os.path.join(getattr(sys, 'frozen_path'), 'plugins') + sys.path.insert(0, plugin_path) + else: + import pkg_resources + plugins = getattr(pkg_resources, 'resource_filename')('calibre', 'plugins') + sys.path.insert(0, plugins) + + plugins = {} + for plugin in ['pictureflow', 'lzx', 'msdes'] + \ + (['winutil'] if iswindows else []) + \ + (['usbobserver'] if isosx else []): + try: + p, err = __import__(plugin), '' + except Exception, err: + p = None + err = str(err) + plugins[plugin] = (p, err) + + ################################################################################ + # Improve builtin path functions to handle unicode sensibly + + _abspath = os.path.abspath + def my_abspath(path, encoding=sys.getfilesystemencoding()): + ''' + Work around for buggy os.path.abspath. This function accepts either byte strings, + in which it calls os.path.abspath, or unicode string, in which case it first converts + to byte strings using `encoding`, calls abspath and then decodes back to unicode. + ''' + to_unicode = False + if isinstance(path, unicode): + path = path.encode(encoding) + to_unicode = True + res = _abspath(path) + if to_unicode: + res = res.decode(encoding) + return res + + os.path.abspath = my_abspath + _join = os.path.join + def my_join(a, *p): + encoding=sys.getfilesystemencoding() + p = [a] + list(p) + _unicode = False + for i in p: + if isinstance(i, unicode): + _unicode = True + break + p = [i.encode(encoding) if isinstance(i, unicode) else i for i in p] + + res = _join(*p) + if _unicode: + res = res.decode(encoding) + return res + + os.path.join = my_join + + + ################################################################################ + # Platform specific modules + winutil = winutilerror = None + if iswindows: + winutil, winutilerror = plugins['winutil'] + if not winutil: + raise RuntimeError('Failed to load the winutil plugin: %s'%winutilerror) + if len(sys.argv) > 1: + sys.argv[1:] = winutil.argv()[1-len(sys.argv):] + + ################################################################################ + \ No newline at end of file diff --git a/src/calibre/translations/pygettext.py b/src/calibre/translations/pygettext.py index 0c6fac8d9f..9578ef2d51 100644 --- a/src/calibre/translations/pygettext.py +++ b/src/calibre/translations/pygettext.py @@ -164,8 +164,8 @@ DEFAULTKEYWORDS = ', '.join(default_keywords) EMPTYSTRING = '' -from calibre import __appname__ -from calibre import __version__ as version +from calibre.constants import __appname__ +from calibre.constants import __version__ as version # The normal pot-file header. msgmerge and Emacs's po-mode work better if it's # there. @@ -641,4 +641,4 @@ def main(outfile, args=sys.argv[1:]): eater.write(outfile) if __name__ == '__main__': - main(sys.stdout) \ No newline at end of file + main(sys.stdout) diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 8191ac402a..997bfa230f 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -6,10 +6,14 @@ __docformat__ = 'restructuredtext en' ''' Manage application-wide preferences. ''' -import os, re, cPickle +import os, re, cPickle, textwrap from copy import deepcopy +from optparse import OptionParser as _OptionParser +from optparse import IndentedHelpFormatter from PyQt4.QtCore import QString -from calibre import iswindows, isosx, OptionParser, ExclusiveFile, LockError +from calibre.constants import terminal_controller, iswindows, isosx, \ + __appname__, __version__, __author__ +from calibre.utils.lock import LockError, ExclusiveFile from collections import defaultdict if iswindows: @@ -26,7 +30,125 @@ else: if not os.path.exists(config_dir): os.makedirs(config_dir, mode=448) # 0700 == 448 + +class CustomHelpFormatter(IndentedHelpFormatter): + def format_usage(self, usage): + return _("%sUsage%s: %s\n") % (terminal_controller.BLUE, terminal_controller.NORMAL, usage) + + def format_heading(self, heading): + return "%*s%s%s%s:\n" % (self.current_indent, terminal_controller.BLUE, + "", heading, terminal_controller.NORMAL) + + def format_option(self, option): + 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, "", + terminal_controller.GREEN+opts+terminal_controller.NORMAL) + indent_first = self.help_position + else: # start help on same line as opts + opts = "%*s%-*s " % (self.current_indent, "", opt_width + len(terminal_controller.GREEN + terminal_controller.NORMAL), + terminal_controller.GREEN + opts + terminal_controller.NORMAL) + 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(_OptionParser): + + def __init__(self, + usage='%prog [options] filename', + version='%%prog (%s %s)'%(__appname__, __version__), + epilog=_('Created by ')+terminal_controller.RED+__author__+terminal_controller.NORMAL, + gui_mode=False, + conflict_handler='resolve', + **kwds): + usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\ + '''enclose the arguments in quotation marks.''' + _OptionParser.__init__(self, usage=usage, version=version, epilog=epilog, + formatter=CustomHelpFormatter(), + conflict_handler=conflict_handler, **kwds) + self.gui_mode = gui_mode + + def error(self, msg): + if self.gui_mode: + raise Exception(msg) + _OptionParser.error(self, msg) + + def merge(self, parser): + ''' + Add options from parser to self. In case of conflicts, confilicting 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 str(opt).strip(): + yield opt + for gr in self.option_groups: + for opt in gr.option_list: + if str(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 not upper.__dict__.has_key(dest): + 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] + + + class Option(object): def __init__(self, name, switches=[], help='', type=None, choices=None, @@ -250,8 +372,154 @@ class StringConfig(object): setattr(opts, name, val) footer = self.option_set.get_override_section(self.src) self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' + +class ConfigProxy(object): + ''' + A Proxy to minimize file reads for widely used config settings + ''' + def __init__(self, config): + self.__config = config + self.__opts = None + + def refresh(self): + self.__opts = self.__config.parse() + def __getitem__(self, key): + return self.get(key) + + def __setitem__(self, key, val): + return self.set(key, val) + + def get(self, key): + if self.__opts is None: + self.refresh() + return getattr(self.__opts, key) + + def set(self, key, val): + if self.__opts is None: + self.refresh() + setattr(self.__opts, key, val) + return self.__config.set(key, val) + +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 non human readable pickle file, so only use this + class for preferences that you don't intend to have the users edit directly. + ''' + def __init__(self, name='dynamic'): + self.name = name + self.file_path = os.path.join(config_dir, name+'.pickle') + with ExclusiveFile(self.file_path) as f: + raw = f.read() + d = cPickle.loads(raw) if raw.strip() else {} + dict.__init__(self, d) + + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + return None + + 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 hasattr(self, 'file_path') and self.file_path: + with ExclusiveFile(self.file_path) as f: + raw = cPickle.dumps(self, -1) + f.seek(0) + f.truncate() + f.write(raw) + +dynamic = DynamicConfig() + +def _prefs(): + c = Config('global', 'calibre wide preferences') + c.add_opt('database_path', + default=os.path.expanduser('~/library1.db'), + help=_('Path to the database in which books are stored')) + c.add_opt('filename_pattern', default=ur'(?P<title>.+) - (?P<author>[^_]+)', + help=_('Pattern to guess metadata from filenames')) + c.add_opt('isbndb_com_key', default='', + help=_('Access key for isbndb.com')) + c.add_opt('network_timeout', default=5, + help=_('Default timeout for network operations (seconds)')) + + c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') + return c + +prefs = ConfigProxy(_prefs()) + +def migrate(): + p = prefs + if p.get('migrated'): + return + + from PyQt4.QtCore import QSettings, QVariant + class Settings(QSettings): + + def __init__(self, name='calibre2'): + QSettings.__init__(self, QSettings.IniFormat, QSettings.UserScope, + 'kovidgoyal.net', name) + + def get(self, key, default=None): + try: + key = str(key) + if not self.contains(key): + return default + val = str(self.value(key, QVariant()).toByteArray()) + if not val: + return None + return cPickle.loads(val) + except: + return default + + s, migrated = Settings(), set([]) + all_keys = set(map(unicode, s.allKeys())) + from calibre.gui2 import config, dynamic + def _migrate(key, safe=None, from_qvariant=None, p=config): + try: + if key not in all_keys: + return + if safe is None: + safe = re.sub(r'[^0-9a-zA-Z]', '_', key) + val = s.get(key) + if from_qvariant is not None: + val = getattr(s.value(key), from_qvariant)() + p.set(safe, val) + except: + pass + finally: + migrated.add(key) + + + _migrate('database path', p=prefs) + _migrate('filename pattern', p=prefs) + _migrate('network timeout', p=prefs) + _migrate('isbndb.com key', p=prefs) + + _migrate('frequently used directories') + _migrate('send to device by default') + _migrate('save to disk single format') + _migrate('confirm delete') + _migrate('show text in toolbar') + _migrate('new version notification') + _migrate('use roman numerals for series number') + _migrate('cover flow queue length') + _migrate('LRF conversion defaults') + _migrate('LRF ebook viewer options') + + for key in all_keys - migrated: + if key.endswith(': un') or key.endswith(': pw'): + _migrate(key, p=dynamic) + p.set('migrated', True) if __name__ == '__main__': diff --git a/src/calibre/utils/lock.py b/src/calibre/utils/lock.py new file mode 100644 index 0000000000..692ccb856a --- /dev/null +++ b/src/calibre/utils/lock.py @@ -0,0 +1,86 @@ +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +Secure access to locked files from multiple processes. +''' + +from calibre.constants import iswindows, __appname__, \ + win32api, win32event, winerror, fcntl +import time, atexit, os + +class LockError(Exception): + pass + +class ExclusiveFile(object): + + def __init__(self, path, timeout=10): + self.path = path + self.timeout = timeout + + def __enter__(self): + self.file = open(self.path, 'a+b') + self.file.seek(0) + timeout = self.timeout + if iswindows: + name = ('Local\\'+(__appname__+self.file.name).replace('\\', '_'))[:201] + while self.timeout < 0 or timeout >= 0: + self.mutex = win32event.CreateMutex(None, False, name) + if win32api.GetLastError() != winerror.ERROR_ALREADY_EXISTS: break + time.sleep(1) + timeout -= 1 + else: + while self.timeout < 0 or timeout >= 0: + try: + fcntl.lockf(self.file.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB) + break + except IOError: + time.sleep(1) + timeout -= 1 + if timeout < 0 and self.timeout >= 0: + self.file.close() + raise LockError + return self.file + + def __exit__(self, type, value, traceback): + self.file.close() + if iswindows: + win32api.CloseHandle(self.mutex) + + +def _clean_lock_file(file): + try: + file.close() + except: + pass + try: + os.remove(file.name) + except: + pass + + +def singleinstance(name): + ''' + Return True if no other instance of the application identified by name is running, + False otherwise. + @param name: The name to lock. + @type name: string + ''' + if iswindows: + mutexname = 'mutexforsingleinstanceof'+__appname__+name + mutex = win32event.CreateMutex(None, False, mutexname) + if mutex: + atexit.register(win32api.CloseHandle, mutex) + return not win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS + else: + path = os.path.expanduser('~/.'+__appname__+'_'+name+'.lock') + try: + f = open(path, 'w') + fcntl.lockf(f.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB) + atexit.register(_clean_lock_file, f) + return True + except IOError: + return False + + return False diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py index 82b09a321a..749c57bcde 100644 --- a/src/calibre/web/fetch/simple.py +++ b/src/calibre/web/fetch/simple.py @@ -12,9 +12,10 @@ from urllib import url2pathname from httplib import responses from calibre import setup_cli_handlers, browser, sanitize_file_name, \ - OptionParser, relpath, LoggingInterface + relpath, LoggingInterface from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag from calibre.ebooks.chardet import xml_to_unicode +from calibre.utils.config import OptionParser class FetchError(Exception): pass