diff --git a/resources/recipes/financial_times_uk.recipe b/resources/recipes/financial_times_uk.recipe index 152e6a9f59..cf219cfda1 100644 --- a/resources/recipes/financial_times_uk.recipe +++ b/resources/recipes/financial_times_uk.recipe @@ -1,5 +1,5 @@ __license__ = 'GPL v3' -__copyright__ = '2010, Darko Miletic ' +__copyright__ = '2010-2011, Darko Miletic ' ''' ft.com ''' @@ -52,22 +52,38 @@ class FinancialTimes(BasicNewsRecipe): .copyright{font-size: x-small} """ - def parse_index(self): + def get_artlinks(self, elem): articles = [] + for item in elem.findAll('a',href=True): + url = self.PREFIX + item['href'] + title = self.tag_to_string(item) + date = strftime(self.timefmt) + articles.append({ + 'title' :title + ,'date' :date + ,'url' :url + ,'description':'' + }) + return articles + + def parse_index(self): + feeds = [] soup = self.index_to_soup(self.INDEX) wide = soup.find('div',attrs={'class':'wide'}) - if wide: - for item in wide.findAll('a',href=True): - url = self.PREFIX + item['href'] - title = self.tag_to_string(item) - date = strftime(self.timefmt) - articles.append({ - 'title' :title - ,'date' :date - ,'url' :url - ,'description':'' - }) - return [('FT UK edition',articles)] + if not wide: + return feeds + strest = wide.findAll('h3', attrs={'class':'section'}) + if not strest: + return feeds + st = wide.find('h4',attrs={'class':'section-no-arrow'}) + if st: + strest.insert(0,st) + for item in strest: + ftitle = self.tag_to_string(item) + self.report_progress(0, _('Fetching feed')+' %s...'%(ftitle)) + feedarts = self.get_artlinks(item.parent.ul) + feeds.append((ftitle,feedarts)) + return feeds def preprocess_html(self, soup): return self.adeify_images(soup) diff --git a/src/calibre/gui2/preferences/social.py b/src/calibre/gui2/preferences/social.py index 5f66f12326..a22bcce091 100644 --- a/src/calibre/gui2/preferences/social.py +++ b/src/calibre/gui2/preferences/social.py @@ -62,6 +62,7 @@ class SocialMetadata(QDialog): return if not self.worker.is_alive(): self.accept() + return QTimer.singleShot(50, self.update) def accept(self): diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py index efcf9e6379..2e16b0f4c3 100644 --- a/src/calibre/gui2/preferences/template_functions.py +++ b/src/calibre/gui2/preferences/template_functions.py @@ -25,37 +25,49 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): template function is written in python. It takes information from the book, processes it in some way, then returns a string result. Functions defined here are usable in templates in the same way that builtin - functions are usable. The function must be named evaluate, and must - have the signature shown below.

-

evaluate(self, formatter, kwargs, mi, locals, your_arguments) + functions are usable. The function must be named evaluate, and + must have the signature shown below.

+

evaluate(self, formatter, kwargs, mi, locals, your parameters) → returning a unicode string

-

The arguments to evaluate are: +

The parameters of the evaluate function are:

    -
  • formatter: the instance of the formatter being used to +
  • formatter: the instance of the formatter being used to evaluate the current template. You can use this to do recursive template evaluation.
  • -
  • kwargs: a dictionary of metadata. Field values are in this - dictionary. mi: a Metadata instance. Used to get field information. +
  • kwargs: a dictionary of metadata. Field values are in this + dictionary. +
  • mi: a Metadata instance. Used to get field information. This parameter can be None in some cases, such as when evaluating non-book templates.
  • -
  • locals: the local variables assigned to by the current +
  • locals: the local variables assigned to by the current template program.
  • -
  • Your_arguments must be one or more parameter (number - matching the arg count box), or the value *args for a variable number - of arguments. These are values passed into the function. One argument - is required, and is usually the value of the field being operated upon. - Note that when writing in basic template mode, the user does not - provide this first argument. Instead it is the value of the field the - function is operating upon.
  • +
  • your parameters: You must supply one or more formal + parameters. The number must match the arg count box, unless arg count is + -1 (variable number or arguments), in which case the last argument must + be *args. At least one argument is required, and is usually the value of + the field being operated upon. Note that when writing in basic template + mode, the user does not provide this first argument. Instead it is + supplied by the formatter.

- The following example function looks for various values in the tags - metadata field, returning those values that appear in tags. + The following example function checks the value of the field. If the + field is not empty, the field's value is returned, otherwise the value + EMPTY is returned.

+        name: my_ifempty
+        arg count: 1
+        doc: my_ifempty(val) -- return val if it is not empty, otherwise the string 'EMPTY'
+        program code:
         def evaluate(self, formatter, kwargs, mi, locals, val):
-            awards=['allbooks', 'PBook', 'ggff']
-            return ', '.join([t for t in kwargs.get('tags') if t in awards])
-        
+ if val: + return val + else: + return 'EMPTY' + This function can be called in any of the three template program modes: +
    +
  • single-function mode: {tags:my_ifempty()}
  • +
  • template program mode: {tags:'my_ifempty($)'}
  • +
  • general program mode: program: my_ifempty(field('tags'))
  • ''') self.textBrowser.setHtml(help_text) @@ -67,14 +79,22 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.build_function_names_box() self.function_name.currentIndexChanged[str].connect(self.function_index_changed) self.function_name.editTextChanged.connect(self.function_name_edited) + self.argument_count.valueChanged.connect(self.enable_replace_button) + self.documentation.textChanged.connect(self.enable_replace_button) + self.program.textChanged.connect(self.enable_replace_button) self.create_button.clicked.connect(self.create_button_clicked) self.delete_button.clicked.connect(self.delete_button_clicked) self.create_button.setEnabled(False) self.delete_button.setEnabled(False) + self.replace_button.setEnabled(False) self.clear_button.clicked.connect(self.clear_button_clicked) + self.replace_button.clicked.connect(self.replace_button_clicked) self.program.setTabStopWidth(20) self.highlighter = PythonHighlighter(self.program.document()) + def enable_replace_button(self): + self.replace_button.setEnabled(self.delete_button.isEnabled()) + def clear_button_clicked(self): self.build_function_names_box() self.program.clear() @@ -112,6 +132,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.create_button.setEnabled(True) self.delete_button.setEnabled(False) self.build_function_names_box(set_to=name) + self.program.setReadOnly(False) else: error_dialog(self.gui, _('Template functions'), _('Function not defined'), show=True) @@ -143,6 +164,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.documentation.setReadOnly(False) self.argument_count.setReadOnly(False) self.create_button.setEnabled(True) + self.replace_button.setEnabled(False) + self.program.setReadOnly(False) def function_index_changed(self, txt): txt = unicode(txt) @@ -156,15 +179,21 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): func = self.funcs[txt] self.argument_count.setValue(func.arg_count) self.documentation.setText(func.doc) + self.program.setPlainText(func.program_text) if txt in self.builtins: self.documentation.setReadOnly(True) self.argument_count.setReadOnly(True) - self.program.clear() + self.program.setReadOnly(True) self.delete_button.setEnabled(False) else: self.program.setPlainText(func.program_text) self.delete_button.setEnabled(True) + self.program.setReadOnly(False) + self.replace_button.setEnabled(False) + def replace_button_clicked(self): + self.delete_button_clicked() + self.create_button_clicked() def refresh_gui(self, gui): pass diff --git a/src/calibre/gui2/preferences/template_functions.ui b/src/calibre/gui2/preferences/template_functions.ui index d323a8dc7e..0e56f18025 100644 --- a/src/calibre/gui2/preferences/template_functions.ui +++ b/src/calibre/gui2/preferences/template_functions.ui @@ -38,7 +38,7 @@ - Enter the name of the function to create + Enter the name of the function to create. true @@ -48,7 +48,7 @@ - + Arg &count: @@ -100,6 +100,13 @@ + + + + &Replace + + + @@ -144,8 +151,7 @@ - - + diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index f7be9b4b89..eb74b7319b 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -4979,9 +4979,11 @@ then rebuild the catalog. if opts_dict['ids']: build_log.append(" book count: %d" % len(opts_dict['ids'])) - sections_list = [] + sections_list = ['Authors'] + ''' if opts.generate_authors: sections_list.append('Authors') + ''' if opts.generate_titles: sections_list.append('Titles') if opts.generate_genres: diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index abcb3021be..49b807ff1c 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -96,7 +96,7 @@ class _Parser(object): # classic assignment statement self.consume() cls = funcs['assign'] - return cls.eval(self.parent, self.parent.kwargs, + return cls.eval_(self.parent, self.parent.kwargs, self.parent.book, self.parent.locals, id, self.expr()) return self.parent.locals.get(id, _('unknown id ') + id) # We have a function. @@ -130,7 +130,7 @@ class _Parser(object): cls = funcs[id] if cls.arg_count != -1 and len(args) != cls.arg_count: self.error('incorrect number of arguments for function {}'.format(id)) - return cls.eval(self.parent, self.parent.kwargs, + return cls.eval_(self.parent, self.parent.kwargs, self.parent.book, self.parent.locals, *args) else: f = self.parent.functions[id] @@ -284,14 +284,13 @@ class TemplateFormatter(string.Formatter): else: args = self.arg_parser.scan(fmt[p+1:])[0] args = [self.backslash_comma_to_comma.sub(',', a) for a in args] - if (func.arg_count == 1 and (len(args) != 0)) or \ + if (func.arg_count == 1 and (len(args) != 1 or args[0])) or \ (func.arg_count > 1 and func.arg_count != len(args)+1): - print args raise ValueError('Incorrect number of arguments for function '+ fmt[0:p]) if func.arg_count == 1: - val = func.eval(self, self.kwargs, self.book, self.locals, val).strip() + val = func.eval_(self, self.kwargs, self.book, self.locals, val).strip() else: - val = func.eval(self, self.kwargs, self.book, self.locals, + val = func.eval_(self, self.kwargs, self.book, self.locals, val, *args).strip() if val: val = self._do_format(val, dispfmt) diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index b0895ce1b3..7237f227e2 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -8,7 +8,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re, traceback +import inspect, re, traceback, sys from calibre.utils.titlecase import titlecase from calibre.utils.icu import capitalize, strcmp @@ -58,13 +58,10 @@ class FormatterFunction(object): name = 'no name provided' arg_count = 0 - def __init__(self): - formatter_functions.register_builtin(self) - def evaluate(self, formatter, kwargs, mi, locals, *args): raise NotImplementedError() - def eval(self, formatter, kwargs, mi, locals, *args): + def eval_(self, formatter, kwargs, mi, locals, *args): try: ret = self.evaluate(formatter, kwargs, mi, locals, *args) if isinstance(ret, (str, unicode)): @@ -75,9 +72,21 @@ class FormatterFunction(object): return ','.join(list) except: traceback.print_exc() - return _('Function threw exception' + traceback.format_exc()) + exc_type, exc_value, exc_traceback = sys.exc_info() + info = ': '.join(traceback.format_exception(exc_type, exc_value, + exc_traceback)[-2:]).replace('\n', '') + return _('Exception ' + info) -class BuiltinStrcmp(FormatterFunction): + +class BuiltinFormatterFunction(FormatterFunction): + def __init__(self): + formatter_functions.register_builtin(self) + eval_func = inspect.getmembers(self.__class__, + lambda x: inspect.ismethod(x) and x.__name__ == 'evaluate') + lines = [l[4:] for l in inspect.getsourcelines(eval_func[0][1])[0]] + self.program_text = ''.join(lines) + +class BuiltinStrcmp(BuiltinFormatterFunction): name = 'strcmp' arg_count = 5 doc = _('strcmp(x, y, lt, eq, gt) -- does a case-insensitive comparison of x ' @@ -92,7 +101,7 @@ class BuiltinStrcmp(FormatterFunction): return eq return gt -class BuiltinCmp(FormatterFunction): +class BuiltinCmp(BuiltinFormatterFunction): name = 'cmp' arg_count = 5 doc = _('cmp(x, y, lt, eq, gt) -- compares x and y after converting both to ' @@ -107,7 +116,7 @@ class BuiltinCmp(FormatterFunction): return eq return gt -class BuiltinStrcat(FormatterFunction): +class BuiltinStrcat(BuiltinFormatterFunction): name = 'strcat' arg_count = -1 doc = _('strcat(a, b, ...) -- can take any number of arguments. Returns a ' @@ -120,7 +129,7 @@ class BuiltinStrcat(FormatterFunction): res += args[i] return res -class BuiltinAdd(FormatterFunction): +class BuiltinAdd(BuiltinFormatterFunction): name = 'add' arg_count = 2 doc = _('add(x, y) -- returns x + y. Throws an exception if either x or y are not numbers.') @@ -130,7 +139,7 @@ class BuiltinAdd(FormatterFunction): y = float(y if y else 0) return unicode(x + y) -class BuiltinSubtract(FormatterFunction): +class BuiltinSubtract(BuiltinFormatterFunction): name = 'subtract' arg_count = 2 doc = _('subtract(x, y) -- returns x - y. Throws an exception if either x or y are not numbers.') @@ -140,7 +149,7 @@ class BuiltinSubtract(FormatterFunction): y = float(y if y else 0) return unicode(x - y) -class BuiltinMultiply(FormatterFunction): +class BuiltinMultiply(BuiltinFormatterFunction): name = 'multiply' arg_count = 2 doc = _('multiply(x, y) -- returns x * y. Throws an exception if either x or y are not numbers.') @@ -150,7 +159,7 @@ class BuiltinMultiply(FormatterFunction): y = float(y if y else 0) return unicode(x * y) -class BuiltinDivide(FormatterFunction): +class BuiltinDivide(BuiltinFormatterFunction): name = 'divide' arg_count = 2 doc = _('divide(x, y) -- returns x / y. Throws an exception if either x or y are not numbers.') @@ -160,7 +169,7 @@ class BuiltinDivide(FormatterFunction): y = float(y if y else 0) return unicode(x / y) -class BuiltinTemplate(FormatterFunction): +class BuiltinTemplate(BuiltinFormatterFunction): name = 'template' arg_count = 1 doc = _('template(x) -- evaluates x as a template. The evaluation is done ' @@ -168,17 +177,17 @@ class BuiltinTemplate(FormatterFunction): 'the caller and the template evaluation. Because the { and } ' 'characters are special, you must use [[ for the { character and ' ']] for the } character; they are converted automatically. ' - 'For example, ``template(\'[[title_sort]]\') will evaluate the ' + 'For example, template(\'[[title_sort]]\') will evaluate the ' 'template {title_sort} and return its value.') def evaluate(self, formatter, kwargs, mi, locals, template): template = template.replace('[[', '{').replace(']]', '}') return formatter.safe_format(template, kwargs, 'TEMPLATE', mi) -class BuiltinEval(FormatterFunction): +class BuiltinEval(BuiltinFormatterFunction): name = 'eval' arg_count = 1 - doc = _('eval(template)`` -- evaluates the template, passing the local ' + doc = _('eval(template) -- evaluates the template, passing the local ' 'variables (those \'assign\'ed to) instead of the book metadata. ' ' This permits using the template processor to construct complex ' 'results from local variables.') @@ -188,7 +197,7 @@ class BuiltinEval(FormatterFunction): template = template.replace('[[', '{').replace(']]', '}') return eval_formatter.safe_format(template, locals, 'EVAL', None) -class BuiltinAssign(FormatterFunction): +class BuiltinAssign(BuiltinFormatterFunction): name = 'assign' arg_count = 2 doc = _('assign(id, val) -- assigns val to id, then returns val. ' @@ -198,7 +207,7 @@ class BuiltinAssign(FormatterFunction): locals[target] = value return value -class BuiltinPrint(FormatterFunction): +class BuiltinPrint(BuiltinFormatterFunction): name = 'print' arg_count = -1 doc = _('print(a, b, ...) -- prints the arguments to standard output. ' @@ -209,7 +218,7 @@ class BuiltinPrint(FormatterFunction): print args return None -class BuiltinField(FormatterFunction): +class BuiltinField(BuiltinFormatterFunction): name = 'field' arg_count = 1 doc = _('field(name) -- returns the metadata field named by name') @@ -217,7 +226,7 @@ class BuiltinField(FormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, name): return formatter.get_value(name, [], kwargs) -class BuiltinSubstr(FormatterFunction): +class BuiltinSubstr(BuiltinFormatterFunction): name = 'substr' arg_count = 3 doc = _('substr(str, start, end) -- returns the start\'th through the end\'th ' @@ -230,7 +239,7 @@ class BuiltinSubstr(FormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_): return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)] -class BuiltinLookup(FormatterFunction): +class BuiltinLookup(BuiltinFormatterFunction): name = 'lookup' arg_count = -1 doc = _('lookup(val, pattern, field, pattern, field, ..., else_field) -- ' @@ -257,7 +266,7 @@ class BuiltinLookup(FormatterFunction): return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs) i += 2 -class BuiltinTest(FormatterFunction): +class BuiltinTest(BuiltinFormatterFunction): name = 'test' arg_count = 3 doc = _('test(val, text if not empty, text if empty) -- return `text if not ' @@ -269,7 +278,7 @@ class BuiltinTest(FormatterFunction): else: return value_not_set -class BuiltinContains(FormatterFunction): +class BuiltinContains(BuiltinFormatterFunction): name = 'contains' arg_count = 4 doc = _('contains(val, pattern, text if match, text if not match) -- checks ' @@ -284,13 +293,13 @@ class BuiltinContains(FormatterFunction): else: return value_if_not -class BuiltinSwitch(FormatterFunction): +class BuiltinSwitch(BuiltinFormatterFunction): name = 'switch' arg_count = -1 doc = _('switch(val, pattern, value, pattern, value, ..., else_value) -- ' - 'for each ``pattern, value`` pair, checks if the field matches ' - 'the regular expression ``pattern`` and if so, returns that ' - 'value. If no pattern matches, then else_value is returned. ' + 'for each `pattern, value` pair, checks if the field matches ' + 'the regular expression `pattern` and if so, returns that ' + '`value`. If no pattern matches, then else_value is returned. ' 'You can have as many `pattern, value` pairs as you want') def evaluate(self, formatter, kwargs, mi, locals, val, *args): @@ -304,7 +313,7 @@ class BuiltinSwitch(FormatterFunction): return args[i+1] i += 2 -class BuiltinRe(FormatterFunction): +class BuiltinRe(BuiltinFormatterFunction): name = 're' arg_count = 3 doc = _('re(val, pattern, replacement) -- return the field after applying ' @@ -315,10 +324,10 @@ class BuiltinRe(FormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement): return re.sub(pattern, replacement, val) -class BuiltinEvaluate(FormatterFunction): - name = 'evaluate' +class BuiltinIfempty(BuiltinFormatterFunction): + name = 'ifempty' arg_count = 2 - doc = _('evaluate(val, text if empty) -- return val if val is not empty, ' + doc = _('ifempty(val, text if empty) -- return val if val is not empty, ' 'otherwise return `text if empty`') def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty): @@ -327,7 +336,7 @@ class BuiltinEvaluate(FormatterFunction): else: return value_if_empty -class BuiltinShorten(FormatterFunction): +class BuiltinShorten(BuiltinFormatterFunction): name = 'shorten' arg_count = 4 doc = _('shorten(val, left chars, middle text, right chars) -- Return a ' @@ -352,7 +361,7 @@ class BuiltinShorten(FormatterFunction): else: return val -class BuiltinCount(FormatterFunction): +class BuiltinCount(BuiltinFormatterFunction): name = 'count' arg_count = 2 doc = _('count(val, separator) -- interprets the value as a list of items ' @@ -363,7 +372,7 @@ class BuiltinCount(FormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val, sep): return unicode(len(val.split(sep))) -class BuiltinListitem(FormatterFunction): +class BuiltinListitem(BuiltinFormatterFunction): name = 'list_item' arg_count = 3 doc = _('list_item(val, index, separator) -- interpret the value as a list of ' @@ -383,7 +392,7 @@ class BuiltinListitem(FormatterFunction): except: return '' -class BuiltinUppercase(FormatterFunction): +class BuiltinUppercase(BuiltinFormatterFunction): name = 'uppercase' arg_count = 1 doc = _('uppercase(val) -- return value of the field in upper case') @@ -391,7 +400,7 @@ class BuiltinUppercase(FormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return val.upper() -class BuiltinLowercase(FormatterFunction): +class BuiltinLowercase(BuiltinFormatterFunction): name = 'lowercase' arg_count = 1 doc = _('lowercase(val) -- return value of the field in lower case') @@ -399,7 +408,7 @@ class BuiltinLowercase(FormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return val.lower() -class BuiltinTitlecase(FormatterFunction): +class BuiltinTitlecase(BuiltinFormatterFunction): name = 'titlecase' arg_count = 1 doc = _('titlecase(val) -- return value of the field in title case') @@ -407,7 +416,7 @@ class BuiltinTitlecase(FormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return titlecase(val) -class BuiltinCapitalize(FormatterFunction): +class BuiltinCapitalize(BuiltinFormatterFunction): name = 'capitalize' arg_count = 1 doc = _('capitalize(val) -- return value of the field capitalized') @@ -423,7 +432,7 @@ builtin_contains = BuiltinContains() builtin_count = BuiltinCount() builtin_divide = BuiltinDivide() builtin_eval = BuiltinEval() -builtin_evaluate = BuiltinEvaluate() +builtin_ifempty = BuiltinIfempty() builtin_field = BuiltinField() builtin_list_item = BuiltinListitem() builtin_lookup = BuiltinLookup()