(|%+YKHsAm@INL
zvj7T%WA?lpeNc%&m*o!^G0Hgg4>^-92f*!D5YgB1YjF0*lQ_07)bCm66|hhec#kd6
z_>TwqJWepcz(Q!6GP0Cn0-P|cD~RZZMY1l@#id;=M|RDVz3$74j`j=T2rD7?YO0T<
z8bl+UtD@oCG7y&h=^$o~*|zifMG3)Om<`H&{q%o_{ps$|?!A7O5zjc{cD(U0Jwcnvhy1LMiFRa%e2drGhKdr6)*U2qVNQ$}#+7g7-UO7lNLe1hu{sV1K4{M%;mFU3=6Y=P!_6G7dU
z4g*F0Vuebpqdwv41E*Iq#4OX91D_X?|9b5HnsR4Q-TeqEI%FZ5YeAoHpK9G>Nr4TI
z%hoK5+D_LbH?cC0x~#GI?<@0;E~IXmQ$0=*_0{4+7S;k@JeoyW1Pw*;hp64mAJxcq
zy;}qd!`01oX>UWh-<9jQrvO7|6p?@hH}#z=k0aUf@xs2?WEOERrre@4#EOIE(h30_
zW%{B7xoUX%4DV&tVNNoKxzVw`?&G<;qFcXEms8;#kW)>bd3B`4j{Au5g1
z@kVJis3K37WGaTk>S#Xa-*{f?@$tI7DE&z;)n4s;xGWY;TKQ3za%nD&s0A5SH9XP?;43}FuMMEr=S?AI6cYueD@AGAXso$T@F<4BWq)u7*W5%c85
zc2Zu>s`arB&qroO*Wkae*QzK^j6}1j6L2As3+MRu!(26rLq`!|Yx&-nS*F+$3>zer
zsvNTC>W9^Wos|+8a`Uk#>ZbXJ=xbT63h9?sUse~Fysiw*=U%eP-2(dd-o`c&@`blf
zesHsh-@TS}?wfjL78ti@zL=FFg|&6)`yy&mY%+|PFM@h7I5tvt|2Z0*pNA9Kj7;`0
z&nvC8FtiaqpM(FtPAj{3=3{^DIVZleY3C`T8rM>jW%vO5>`yDobXH7c3MV}5J*NRO
zU5aSCZw2&+%@t}v&f{4}YD)8S(Ff8Ol1)^9DR&caL@T#4{l`DpdtMivc&}gL)Zpo;
zEs*PJ$tnC7;lZ5?jGy+yb&B0FIGfOZHfn3@?|Jy_pil`nRB#=$ON&NISh#z2x$*u<3op*uyDKQXf?
zrygC|p@prHW-y;XYgD~7`vsOml07#YtH3p%&v^XZC#2%AKp5Z1V=X;EuNF$(yHt+N
za89gHKX0a`GjJhfXTC(t`iwb14W(ZPUt7Z^8Y0Tn4DzCB`Z
zte=r{x5O2q8#<`!!LZm2NThV`ziA>`^K)n}FQ=}V;=Sp@30W*ekciI8f${0Z22%%l
zHNVEo&TRA$vjk3|tUGmKUViaHVm8qJTha+~P9sXdVu%eu*YMchDgh=^{18*_3`r)S
zh6LzX+@f9pe$1f4B<~1^68SSRAdwYShx1|;&;VdE`3&tyeR%F7JI|C=-=1{Gc}}1H
zP-DlM%a?Of@IjE+Ps!HG%K4|Q-6Cr_R06J>Qls`99&Zc+5r+NC0=U!A^J)dNun_6(
zb!eKxAxUFD+eY|5Ve*@@O20ke{#0^2hTFqPABWg(J#?pDB{F2{63;?E#nxOypl10g
zDoO$nG4MI;XGv4EG16k>A%vAq{I%*Sty*YrI1|~|sK`E1MiJk53Yl;9$#8I1^E+6@
zIkW~Jn&i{bY#GTXXndlTfj5W6Pk@tFB%o0FeEEtm8jhcoA+Fe8BM+mn3WJrMahYn_
z$2kUZ7FmbmrfGD?`OWd|*)>Rs>zn$tAWT!-o+81j+KT%)@4j#O=MW<4O4VdeI9o<^
z86fYpv}dM0Jyuq=7VItaH+Z<=a+H1cnBT~%mXC+T{7l{3j_xDPH(8ODr`k-kZc<7{j7?H{=n!{k^s!IjBp2z7~1lA021<*7q
zQHce1VKD!Ib**J?&0+Z5OzNnec^bzq-^7A5^ncnCqRWzl*uTHbO>iYZi)Th5NS{gQ
zqLC+!#}?(>rlFG*6}89{%agN`OD_qI+O62^zgzs)V8R#jX_J$)ngI|wI8}|JaRiDQ
z?3O)yZR}y~L&_#;diB9B>fXc|3M(J82ZV2JTkTjvCVEO}2OTOP0j=&>W@+W}0up$V
zMF(|m^mzJ!_}>27OH9R_&aVG_u7JUJ&b3oo9z|i>eU62L^LudAMDtL3{%gn6#1=aZ
zz|>9bPjbL6mipaKdX0mp&!87~l`62A18QW7*@?yWaL&-nrGXJiM84GD
z7Lva$jZ#O2G@QKplU>q?C4^$iP^gC#RHQOG!w?C30&fzBV16no0
zpN|eO%&MPakAM3k?!SFedFjHkfCF=@!pix=l;zuY!VP~*PivxFG|jqY%kV=<9q1am
zsau1J-{Hd>5L2X9xMXpMEf4XHw3G__5Aw>^tKmYbUbI|7x{cKi*H+FWM)Cl`F$cEA
zo86S?Sx7@OmIS=Ty5sKa66N0`rmwlCx#~Ii61D*^A54ZkS2E?%|2o12Bk7ljV2vKV
zIm<}yd&pmUQ(JMjbgWmMPLj0UKXDp(*Y{e_^Zp{>y2luyXei~<7tx-}T&b^qA%MPi3`Eey`_=0tp7QoqSCTb#jH
zo}s0E1ikr0XD5W+{?*7Kal{%JQ=#Bh{N48R!@fJ~v{|7WEOL&DwQ^<|x2A31s>
z-5~4Uvh-bbWm4S=d#92BR!c`-YjtUmu#L~bF@ftIkiJlPm~OI#fLTc6%KAz!-z-Q5
zOH}1@@50A0l9j-NDcJ{r%nTjgLk%2;L=&IsDOw^k1l>z^@OOrLlQh)#yHODLd&V~_YO
zFAFw2Qp0!}8tTyxbu1PL^DW)%Chsj0^Y~zsh@p9|;NusdY?`WqjaaC=1zuRBiiYN7
zfU`~mA^H4eNI*+b9mM2e`JdQ@a
zVNydrqSA4KoX?G5Eh@x$$kPn?Wfu1$Nl_y{QmgmmvMg;J`LZqqfhNBb$inQdehPY}
zX2xPeYFGt_%Atz}^J(j&Ovb|2vQp7w6PXT45w0UuxNG7a_H>%Az(U{qOoty?hK1vf
zE=qLM5ts%F>}zeYJ~8Nv^$7Be{H4KAJ-*!1Wt{o-p>_%MR)MdvW@_J=b=36`bgM<+Ear
zg`sN>l-(EDsLDg>dqTM{KqTwjn7Sy;zEVWFXgFe*9ysen6772#Q4Prx!-uLCqLw``^=InbZW+O@mnN}rAs51cLQ3AQIE
zi)VCTjF}f8*XDlrA>E^US%%0P3|O}@ky`HTRrD*S{ZyUX=wpoS)*4Hn=I0SjW}G)M
z49x7C3+nR4A%*8Sc_{24Yx*y>kKL&*
z4OkZxov=uxS@OQLB|&mi?_truRO>dh>`yp42HS%Zw?1;7iYt#Ts0-V7O+;AV5ah9y
zhJD<24lP%wNrgjuqve$zb;8t@gz@UW({1>kTo~IJv)M#_C5Qt}Chg9aC^KvrF=GhJ
zZEI`l=#S`#8>8&!z1xUsdI-gY
z7$E!6)um3*e;o1CwnLxfjOMma`xb7fUqO2^$2G-WNqFazG23MQ5Pd~i+kFz{5dK0f
z2NdQtZA4UMa%Fsw{gN+>$CH`IS%S*}$ES`9V5>bTf+IPLn<_%!IG#wNmTo!Qss&%)9Qt(T`1tMp^wx&JT{@3)rO?li`y}DDZ)NBFBpdSUi
zO-;}9jg;Z^`NDu}-COv<2?!f6SdqAok?U=IXZr>hQ
zVq(iSz!YM)HjpYXMd;cu7o78ccoKUj*!6=DzWrr@AI3@qGMJoM(eii-{@{1ltvXAF
z?aV{d=9h4&0*?ox2+3DJ3)cL0x>Y+u&49dUGzA9eXU75SX!r-euc*XSe+Oe`$o?De
z(m(!5XjQ2Y@$}y3DL;ew00T_|u1tS|6RW=pCz4UWObnqcW1)lz{bogl9RsF(n$ff6
zl4C*sn#aQ-fBzS2AmLIQ+)5B{niA(=^s-zo`-1+{z~{^?zQ*B<+zbmt;u_cnQk~Df
z#oF^g>%05l`>RH-&5}n#@h}bvrYxrPTlInWfq#ls-}anyI&iGf(w~s>lSMVQZ=aM_
zHmUfvUO=3(pCLZ#G~DrSWnB!G7>#V2ZSs=$nYGW~UW6eDf6z@+9MVW_^%}VB|aB=dAB$<(36HgXj!aXB#ga
zJQ?5m^8b@6(?h65Do8wAevzYpLSWD@HYgwYt`*J!+d>tQP~p*cN7ru()1xLSGAQ7*
zw|YJE3W}X{t9)~rPxX*ueE(bZc@fq3LG0;ot1s4E@r-i4o4xyR%D;@#>fg|q>o4C`
zO;s3{B%J|QICUX_Hr~#ER3(;eoX=+an7trX>L6R_x_zd~vE_-sJ}8ht9OYF*%JLZ_
zrLoosVpaT1xx!UWnl;28Z9YaU+_g(Vn@XmA{-h`YnLhmtVgLL>>ne1RYkE9mn$PKa`OXXhuhTHC~cb=Zo{PdD?iOllaIw;P8w_)W=
z=zXh;K3}wHyVnHE^xr_=EooEb%D`z1QYR=k76jQmaXzq~@YoCg_Ul0kftjMs`0TX7
zt|);;AqiI&(ZK7VaCTfJSv&DCH~W??r7(`{a~HOqTA>n__tG|eS#ifhTeefG*`u3(
zc$D=%i$s*{z=ek}r!J3q(;JOM(aX&(;%Whg73!#Z>|zn4+=0_^IN_)#*-oMIz3;u2
zY_WlZz?Vh#1fx__xd=gQ(~&L*8esVGqr6T;u|eHGF|zK)?|+qt>U>`bK5ld!(Y;YS&e8i8$gkHbiS%c3Sum1FDnRb}(7
zn|VIKF)pI!R_@N(Hu0B+K1#M{=z#E!kc^lYN>V<(qOq5xWwCtkRQ5n<@h#hP+T&H*
z%Nb`cilOKOeD57El``4hyWyLs5D_D2^FX5Sv!yjUI-PjLvhvV(SZ<};JdB`JD
z?76`}1teof#)4&YggibhkLE^NSPY1@uhn&%d6b3mBu|2+g`5M@&5S1OV+rim!(%8~
z5B4paO5_XeWb5)Q=5$_=cp8CgB;|r5r;z+-i?3DNLfQ}IRy)1cw}IHGYKARBS1?9p
zy0e~JNXQm7JtWxl9dan~+NZo(ya0}QfK4#_;~6&2ot2E>2av{yC%3}c6B>hGK7OYX
zyxfIHo7)!
zpdG{YwT486Y?98i-H95}<-lM`#oqS1SX}
z3x4qr7b|qN9pqG53G`YN{-P+__<;+zfWjlMQN8@`_wPCP9gY1?W(KKn;>>(0UvBV8
z|8?x5gJ)3cUn12uErfpzszNw|y%z<6LX&bd21FRN`ON8&^#tEZmDOgHo`c_g&&eea
zUofSo{jH=~l^9;t$|thAmeW+q_
zoxZx7FZ00jgtGZ~r8sZBlQ?ah$ZXpIL%N;T5W)|}O?=IG4yzZf$~4K5
zIm~x`g-@5polm!R6)jIc-?Y~kz~MG|R7?OI&J_zF8{Vi+19G(2LL6LTL3GLxRO@et
z8(WT1KZYY?32fJa_FU~YwcGzTpEFOSPO?icFQHEGJBkR8ak$&lTJVY8cF6$)@}i5a
zwYzui?|}=~TbG5DQNPruLQ8zASRDoHwcMrQGPylupM_CboCL`Ss4q8E)965++}t3g
zKu=P3nF>GACRbna{M}f&P8vtta&V!Zphly>D2{z}sX#%aBv)t}2jNg`0M$#~$|Xhb
z4`+fccg%g?5gOMVe4h;~KX0y_KBP=#D2_=cl$O5$)aUOe(1J2=OSP1*uP}aCPF1rX
zPM)NsRdilO1F=saN0sAF^jdwA(N*o=dX&Q3wdaeCKTsTt!^`;QdG+vXu83~2LYw6gl};D)SZ4M}^JiYR>J&?t`#X!J
zq+X`H_xOoJ{~~@u`1Q;YP4JH|fWmv$+>d@9%Q#z$+znt{MLe7Dl$0`qD#c
n!0pJC#X)i#^rr?RK2vc8p|WWr#BBo{1f(FNB3&VA68OIWy&hn>
literal 0
HcmV?d00001
diff --git a/manual/template_lang.rst b/manual/template_lang.rst
index c677cba8d1..343f5861a9 100644
--- a/manual/template_lang.rst
+++ b/manual/template_lang.rst
@@ -630,7 +630,7 @@ the value of a custom field #genre. You cannot do this in the :ref:`Single Funct
The example shows several things:
-* `TPM` is used if the expression begins with ``:'`` and ends with ``'``. Anything else is assumed to be in :ref:`Single Function Mode `.
+* `TPM` is used if the expression begins with ``:'`` and ends with ``'}``. Anything else is assumed to be in :ref:`Single Function Mode `.
* the variable ``$`` stands for the field named in the template: the expression is operating upon, ``#series`` in this case.
* functions must be given all their arguments. There is no default value. For example, the standard built-in functions must be given an additional initial parameter indicating the source field.
* white space is ignored and can be used anywhere within the expression.
@@ -642,16 +642,56 @@ In `TPM`, using ``{`` and ``}`` characters in string literals can lead to errors
As with `General Program Mode`, for functions documented under :ref:`Single Function Mode ` you must supply the value the function is to act upon as the first parameter in addition to the documented parameters. In `TPM` you can use ``$`` to access the value specified by the ``lookup name`` for the template expression.
-Stored general program mode templates
+.. _python_mode:
+
+Python Template Mode
+-----------------------------------
+
+Python Template Mode (PTM) lets you write templates using native python and the `calibre API `_. The database API will be of most use; further discussion is beyond the scope of this manual. PTM templates are faster and can do more complicated operations but you must know how to write code in python using the calibre API.
+
+A PTM template begins with::
+
+ python:
+ def evaluate(book, db, globals, arguments, **kwargs):
+ # book is a calibre metadata object
+ # db is a calibre legacy database object
+ # globals is the template global variable dictionary
+ # arguments is a list of arguments if the template is called by a GPM template, otherwise None
+ # kwargs is a dictionary provided for future use
+
+ # Python code goes here
+ return 'a string'
+
+You can add the above text to your template using the context menu, usually accessed with a right click. The comments are not significant and can be removed. You must use python indenting.
+
+Here is an example of a PTM template that produces a list of all the authors for a series. The list is stored in a `Column built from other columns, behaves like tags`. It shows in :guilabel:`Book details` and has the `on separate lines` checked (in :guilabel:`Preferences->Look & feel->Book details`). That option requires the list to be comma-separated. To satisfy that requirement the template converts commas in author names to semicolons then builds a comma-separated list of authors. The authors are then sorted, which is why the template uses author_sort.::
+
+ python:
+ def evaluate(book, db, globals, arguments, **kwargs):
+ if book.series is None:
+ return ''
+ ans = set()
+ for id_ in db.search_getting_ids(f'series:"={book.series}"', ''):
+ ans.update([v.strip() for v in db.new_api.field_for('author_sort', id_).split('&')])
+ return ', '.join(v.replace(',', ';') for v in sorted(ans))
+
+The output in :guilabel:`Book details` looks like this:
+
+.. image:: images/python_template_example.png
+ :align: center
+ :alt: E-book conversion dialog
+ :class: half-width-img
+
+Stored templates
----------------------------------------
-:ref:`General Program Mode ` supports saving templates and calling those templates from another template, much like calling stored functions. You save templates using :guilabel:`Preferences->Advanced->Template functions`. More information is provided in that dialog. You call a template the same way you call a function, passing positional arguments if desired. An argument can be any expression. Examples of calling a template, assuming the stored template is named ``foo``:
+Both :ref:`General Program Mode ` and :ref:`Python Template Mode ` support saving templates and calling those templates from another template, much like calling stored functions. You save templates using :guilabel:`Preferences->Advanced->Template functions`. More information is provided in that dialog. You call a template the same way you call a function, passing positional arguments if desired. An argument can be any expression. Examples of calling a template, assuming the stored template is named ``foo``:
* ``foo()`` -- call the template passing no arguments.
* ``foo(a, b)`` call the template passing the values of the two variables ``a`` and ``b``.
* ``foo(if field('series') then field('series_index') else 0 fi)`` -- if the book has a ``series`` then pass the ``series_index``, otherwise pass the value ``0``.
-You retrieve the arguments passed in the call to the stored template using the ``arguments`` function. It both declares and initializes local variables, effectively parameters. The variables are positional; they get the value of the parameter given in the call in the same position. If the corresponding parameter is not provided in the call then ``arguments`` assigns that variable the provided default value. If there is no default value then the variable is set to the empty string. For example, the following ``arguments`` function declares 2 variables, ``key``, ``alternate``::
+In GPM you retrieve the arguments passed in the call to the stored template using the ``arguments`` function. It both declares and initializes local variables, effectively parameters. The variables are positional; they get the value of the parameter given in the call in the same position. If the corresponding parameter is not provided in the call then ``arguments`` assigns that variable the provided default value. If there is no default value then the variable is set to the empty string. For example, the following ``arguments`` function declares 2 variables, ``key``, ``alternate``::
arguments(key, alternate='series')
@@ -661,6 +701,8 @@ Examples, again assuming the stored template is named ``foo``:
* ``foo('series', '#genre')`` the variable ``key`` is assigned the value ``'series'`` and the variable ``alternate`` is assigned the value ``'#genre'``.
* ``foo()`` -- the variable ``key`` is assigned the empty string and the variable ``alternate`` is assigned the value ``'series'``.
+In PTM the arguments are passed in the ``arguments`` parameter, which is a list of strings. There isn't any way to specify default values. You must check the length of the ``arguments`` list to be sure that the number of arguments is what you expect.
+
An easy way to test stored templates is using the ``Template tester`` dialog. For ease of access give it a keyboard shortcut in :guilabel:`Preferences->Advanced->Keyboard shortcuts->Template tester`. Giving the ``Stored templates`` dialog a shortcut will help switching more rapidly between the tester and editing the stored template's source code.
Providing additional information to templates
diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py
index d8c5ed7068..36f3eac3ea 100644
--- a/src/calibre/gui2/dialogs/template_dialog.py
+++ b/src/calibre/gui2/dialogs/template_dialog.py
@@ -23,7 +23,7 @@ from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog
from calibre.library.coloring import (displayable_columns, color_row_key)
from calibre.utils.config_base import tweaks
from calibre.utils.date import DEFAULT_DATE
-from calibre.utils.formatter_functions import formatter_functions
+from calibre.utils.formatter_functions import formatter_functions, StoredObjectType
from calibre.utils.formatter import StopException
from calibre.utils.icu import sort_key
from calibre.utils.localization import localize_user_manual_link
@@ -42,49 +42,89 @@ class ParenPosition:
class TemplateHighlighter(QSyntaxHighlighter):
+ # Code in this class is liberally borrowed from gui2.widgets.PythonHighlighter
BN_FACTOR = 1000
- KEYWORDS = ["program", 'if', 'then', 'else', 'elif', 'fi', 'for', 'rof',
- 'separator', 'break', 'continue', 'return', 'in', 'inlist',
- 'def', 'fed', 'limit']
+ KEYWORDS_GPM = ["program", 'if', 'then', 'else', 'elif', 'fi', 'for', 'rof',
+ 'separator', 'break', 'continue', 'return', 'in', 'inlist',
+ 'def', 'fed', 'limit']
+
+ KEYWORDS_PYTHON = ["and", "as", "assert", "break", "class", "continue", "def",
+ "del", "elif", "else", "except", "exec", "finally", "for", "from",
+ "global", "if", "import", "in", "is", "lambda", "not", "or",
+ "pass", "print", "raise", "return", "try", "while", "with",
+ "yield"]
+
+ BUILTINS_PYTHON = ["abs", "all", "any", "basestring", "bool", "callable", "chr",
+ "classmethod", "cmp", "compile", "complex", "delattr", "dict",
+ "dir", "divmod", "enumerate", "eval", "execfile", "exit", "file",
+ "filter", "float", "frozenset", "getattr", "globals", "hasattr",
+ "hex", "id", "int", "isinstance", "issubclass", "iter", "len",
+ "list", "locals", "long", "map", "max", "min", "object", "oct",
+ "open", "ord", "pow", "property", "range", "reduce", "repr",
+ "reversed", "round", "set", "setattr", "slice", "sorted",
+ "staticmethod", "str", "sum", "super", "tuple", "type", "unichr",
+ "unicode", "vars", "xrange", "zip"]
+
+ CONSTANTS_PYTHON = ["False", "True", "None", "NotImplemented", "Ellipsis"]
def __init__(self, parent=None, builtin_functions=None):
super().__init__(parent)
self.initialize_formats()
- self.initialize_rules(builtin_functions)
+ self.initialize_rules(builtin_functions, for_python=False)
self.regenerate_paren_positions()
self.highlighted_paren = False
- def initialize_rules(self, builtin_functions):
+ def initialize_rules(self, builtin_functions, for_python=False):
+ self.for_python = for_python
r = []
def a(a, b):
r.append((re.compile(a), b))
- a(
- r"\b[a-zA-Z]\w*\b(?!\(|\s+\()"
- r"|\$+#?[a-zA-Z]\w*",
- "identifier")
+ if not for_python:
+ a(
+ r"\b[a-zA-Z]\w*\b(?!\(|\s+\()"
+ r"|\$+#?[a-zA-Z]\w*",
+ "identifier")
- a(
- "|".join([r"\b%s\b" % keyword for keyword in self.KEYWORDS]),
- "keyword")
+ a(
+ "|".join([r"\b%s\b" % keyword for keyword in self.KEYWORDS_GPM]),
+ "keyword")
- a(
- "|".join([r"\b%s\b" % builtin for builtin in
- (builtin_functions if builtin_functions else
- formatter_functions().get_builtins())]),
- "builtin")
+ a(
+ "|".join([r"\b%s\b" % builtin for builtin in
+ (builtin_functions if builtin_functions else
+ formatter_functions().get_builtins())]),
+ "builtin")
+ a(r"""(? -1:
+ self.setCurrentBlockState(state)
+ self.setFormat(i, len(text), self.Formats["string"])
+
if self.generate_paren_positions:
t = str(text)
i = 0
@@ -327,6 +399,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.set_mi(mi, fm)
self.last_text = ''
+ self.highlighting_gpm = True
self.highlighter = TemplateHighlighter(self.textbox.document(), builtin_functions=self.builtins)
self.textbox.cursorPositionChanged.connect(self.text_cursor_changed)
self.textbox.textChanged.connect(self.textbox_changed)
@@ -494,9 +567,12 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
ca.setIcon(QIcon.ic('ok.png'))
ca.triggered.connect(partial(self.set_word_wrap, not word_wrapping))
m.addSeparator()
- ca = m.addAction(_('Load template from the Template tester'))
- ca.triggered.connect(self.load_last_template_text)
+ ca = m.addAction(_('Add python template definition text'))
+ ca.triggered.connect(self.add_python_template_header_text)
m.addSeparator()
+ ca = m.addAction(_('Load template from the Template tester'))
+ m.addSeparator()
+ ca.triggered.connect(self.load_last_template_text)
ca = m.addAction(_('Load template from file'))
ca.setIcon(QIcon.ic('document_open.png'))
ca.triggered.connect(self.load_template_from_file)
@@ -505,6 +581,19 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
ca.triggered.connect(self.save_template)
m.exec(self.textbox.mapToGlobal(point))
+ def add_python_template_header_text(self):
+ self.textbox.setPlainText('python:\n'
+ 'def evaluate(book, db, globals, arguments, **kwargs):\n'
+ '\t# book is a calibre metadata object\n'
+ '\t# db is a calibre legacy database object\n'
+ '\t# globals is the template global variable dictionary\n'
+ '\t# arguments is a list of arguments if the template is '
+ 'called by a GPM template, otherwise None\n'
+ '\t# kwargs is a dictionary provided for future use'
+ '\n\n\t# Python code goes here\n'
+ "\treturn 'a string'" +
+ self.textbox.toPlainText())
+
def set_word_wrap(self, to_what):
gprefs['gpm_template_editor_word_wrap_mode'] = to_what
self.textbox.setWordWrapMode(QTextOption.WrapMode.WordWrap if to_what else QTextOption.WrapMode.NoWrap)
@@ -673,6 +762,17 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
def textbox_changed(self):
cur_text = str(self.textbox.toPlainText())
+ if cur_text.startswith('python:'):
+ if self.highlighting_gpm == True:
+ self.highlighter.initialize_rules(self.builtins, True)
+ self.highlighting_gpm = False
+ self.break_box.setChecked(False)
+ self.break_box.setEnabled(False)
+ print(self.break_box.isEnabled())
+ elif not self.highlighting_gpm:
+ self.highlighter.initialize_rules(self.builtins, False)
+ self.highlighting_gpm = True
+ self.break_box.setEnabled(True)
if self.last_text != cur_text:
self.last_text = cur_text
self.highlighter.regenerate_paren_positions()
@@ -707,14 +807,15 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
pos_in_block)
def function_type_string(self, name, longform=True):
- if self.all_functions[name].is_python:
+ if self.all_functions[name].object_type is StoredObjectType.PythonFunction:
if name in self.builtins:
return (_('Built-in template function') if longform else
_('Built-in function'))
return (_('User defined Python template function') if longform else
_('User function'))
- else:
- return (_('Stored user defined template') if longform else _('Stored template'))
+ elif self.all_functions[name].object_type is StoredObjectType.StoredPythonTemplate:
+ return (_('Stored user defined python template') if longform else _('Stored template'))
+ return (_('Stored user defined GPM template') if longform else _('Stored template'))
def function_changed(self, toWhat):
name = str(self.function.itemData(toWhat))
diff --git a/src/calibre/gui2/dialogs/template_dialog.ui b/src/calibre/gui2/dialogs/template_dialog.ui
index e97d602e26..e384531ad4 100644
--- a/src/calibre/gui2/dialogs/template_dialog.ui
+++ b/src/calibre/gui2/dialogs/template_dialog.ui
@@ -344,8 +344,10 @@ you the value as well as all the local variables</p>
<p>The text of the template program goes in this box.
- Don't forget that a General Program Mode template must begin with
- the word "program:".</p>
+ A General Program Mode template must begin with the word "program:".
+ A python template must begin with the word "python:" followed by a
+ function definition line. There is a context menu item you can use
+ to enter the first lines of a python template.</p>
diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py
index 59b80a76e8..29982a5d45 100644
--- a/src/calibre/gui2/preferences/template_functions.py
+++ b/src/calibre/gui2/preferences/template_functions.py
@@ -13,7 +13,8 @@ from calibre.gui2.preferences.template_functions_ui import Ui_Form
from calibre.gui2.widgets import PythonHighlighter
from calibre.utils.formatter_functions import (
compile_user_function, compile_user_template_functions, formatter_functions,
- function_pref_is_python, function_pref_name, load_user_template_functions
+ function_object_type, function_pref_name, load_user_template_functions,
+ StoredObjectType
)
from polyglot.builtins import iteritems
@@ -48,9 +49,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
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
+ be *args. Note that when a function is called in basic template
+ mode at least one argument is always passed. It is
supplied by the formatter.
@@ -87,10 +87,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
in template processing. You use a stored template in another template as
if it were a template function, for example 'some_name(arg1, arg2...)'.
- Stored templates must use General Program Mode -- they must begin with
- the text '{0}'. You retrieve arguments passed to a stored template using
- the '{1}()' template function, as in '{1}(var1, var2, ...)'. The passed
- arguments are copied to the named variables.
+ Stored templates must use either General Program Mode -- they must
+ either begin with the text '{0}' or be {1}. You retrieve arguments
+ passed to a GPM stored template using the '{2}()' template function, as
+ in '{2}(var1, var2, ...)'. The passed arguments are copied to the named
+ variables. Arguments passed to a python template are in the '{2}'
+ parameter. Arguments are always strings.
For example, this stored template checks if any items are in a
list, returning '1' if any are found and '' if not.
@@ -112,7 +114,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
See the template language tutorial for more information.