[formatter] add basic 'Jinja2' template support (#1390)
This commit is contained in:
@@ -36,6 +36,7 @@ Optional
|
|||||||
- SecretStorage_: GNOME keyring passwords for ``--cookies-from-browser``
|
- SecretStorage_: GNOME keyring passwords for ``--cookies-from-browser``
|
||||||
- Psycopg_: PostgreSQL archive support
|
- Psycopg_: PostgreSQL archive support
|
||||||
- truststore_: Native system certificate support
|
- truststore_: Native system certificate support
|
||||||
|
- Jinja_: Jinja template support
|
||||||
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
@@ -476,6 +477,7 @@ To authenticate with a ``mastodon`` instance, run *gallery-dl* with
|
|||||||
.. _SecretStorage: https://pypi.org/project/SecretStorage/
|
.. _SecretStorage: https://pypi.org/project/SecretStorage/
|
||||||
.. _Psycopg: https://www.psycopg.org/
|
.. _Psycopg: https://www.psycopg.org/
|
||||||
.. _truststore: https://truststore.readthedocs.io/en/latest/
|
.. _truststore: https://truststore.readthedocs.io/en/latest/
|
||||||
|
.. _Jinja: https://jinja.palletsprojects.com/
|
||||||
.. _Snapd: https://docs.snapcraft.io/installing-snapd
|
.. _Snapd: https://docs.snapcraft.io/installing-snapd
|
||||||
.. _OAuth: https://en.wikipedia.org/wiki/OAuth
|
.. _OAuth: https://en.wikipedia.org/wiki/OAuth
|
||||||
.. _Chocolatey: https://chocolatey.org/install
|
.. _Chocolatey: https://chocolatey.org/install
|
||||||
|
|||||||
@@ -381,15 +381,20 @@ Starting a format string with `\f<Type> ` allows to set a different format strin
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><code>E</code></td>
|
||||||
|
<td>An arbitrary Python expression</td>
|
||||||
|
<td><code>\fE title.upper().replace(' ', '-')</code></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><code>F</code></td>
|
<td align="center"><code>F</code></td>
|
||||||
<td>An <a href="https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals">f-string</a> literal</td>
|
<td>An <a href="https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals">f-string</a> literal</td>
|
||||||
<td><code>\fF '{title.strip()}' by {artist.capitalize()}</code></td>
|
<td><code>\fF '{title.strip()}' by {artist.capitalize()}</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><code>E</code></td>
|
<td align="center"><code>J</code></td>
|
||||||
<td>An arbitrary Python expression</td>
|
<td>A <a href="https://jinja.palletsprojects.com/">Jinja</a> template</td>
|
||||||
<td><code>\fE title.upper().replace(' ', '-')</code></td>
|
<td><code>\fJ '{{title | trim}}' by {{artist | capitalize}}</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><code>T</code></td>
|
<td align="center"><code>T</code></td>
|
||||||
@@ -401,6 +406,11 @@ Starting a format string with `\f<Type> ` allows to set a different format strin
|
|||||||
<td>Path to a template file containing an <a href="https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals">f-string</a> literal</td>
|
<td>Path to a template file containing an <a href="https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals">f-string</a> literal</td>
|
||||||
<td><code>\fTF ~/.templates/fstr.txt</code></td>
|
<td><code>\fTF ~/.templates/fstr.txt</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><code>TJ</code></td>
|
||||||
|
<td>Path to a template file containing a <a href="https://jinja.palletsprojects.com/">Jinja</a> template</td>
|
||||||
|
<td><code>\fTF ~/.templates/jinja.txt</code></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><code>M</code></td>
|
<td align="center"><code>M</code></td>
|
||||||
<td>Path or name of a Python module
|
<td>Path or name of a Python module
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def parse(format_string, default=NONE, fmt=format):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
cls = StringFormatter
|
cls = StringFormatter
|
||||||
if format_string.startswith("\f"):
|
if format_string and format_string[0] == "\f":
|
||||||
kind, _, format_string = format_string.partition(" ")
|
kind, _, format_string = format_string.partition(" ")
|
||||||
kind = kind[1:]
|
kind = kind[1:]
|
||||||
|
|
||||||
@@ -37,12 +37,16 @@ def parse(format_string, default=NONE, fmt=format):
|
|||||||
cls = TemplateFormatter
|
cls = TemplateFormatter
|
||||||
elif kind == "TF":
|
elif kind == "TF":
|
||||||
cls = TemplateFStringFormatter
|
cls = TemplateFStringFormatter
|
||||||
|
elif kind == "TJ":
|
||||||
|
cls = TemplateJinjaFormatter
|
||||||
elif kind == "E":
|
elif kind == "E":
|
||||||
cls = ExpressionFormatter
|
cls = ExpressionFormatter
|
||||||
elif kind == "M":
|
elif kind == "J":
|
||||||
cls = ModuleFormatter
|
cls = JinjaFormatter
|
||||||
elif kind == "F":
|
elif kind == "F":
|
||||||
cls = FStringFormatter
|
cls = FStringFormatter
|
||||||
|
elif kind == "M":
|
||||||
|
cls = ModuleFormatter
|
||||||
|
|
||||||
formatter = _CACHE[key] = cls(format_string, default, fmt)
|
formatter = _CACHE[key] = cls(format_string, default, fmt)
|
||||||
return formatter
|
return formatter
|
||||||
@@ -224,6 +228,17 @@ class FStringFormatter():
|
|||||||
self.format_map = util.compile_expression(f'f"""{fstring}"""')
|
self.format_map = util.compile_expression(f'f"""{fstring}"""')
|
||||||
|
|
||||||
|
|
||||||
|
class JinjaFormatter():
|
||||||
|
"""Generate text by evaluating a Jinja template string"""
|
||||||
|
env = None
|
||||||
|
|
||||||
|
def __init__(self, source, default=NONE, fmt=None):
|
||||||
|
if self.env is None:
|
||||||
|
import jinja2
|
||||||
|
JinjaFormatter.env = jinja2.Environment()
|
||||||
|
self.format_map = self.env.from_string(source).render
|
||||||
|
|
||||||
|
|
||||||
class TemplateFormatter(StringFormatter):
|
class TemplateFormatter(StringFormatter):
|
||||||
"""Read format_string from file"""
|
"""Read format_string from file"""
|
||||||
|
|
||||||
@@ -242,6 +257,15 @@ class TemplateFStringFormatter(FStringFormatter):
|
|||||||
FStringFormatter.__init__(self, fstring, default, fmt)
|
FStringFormatter.__init__(self, fstring, default, fmt)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateJinjaFormatter(JinjaFormatter):
|
||||||
|
"""Generate text by evaluating a Jinja template"""
|
||||||
|
|
||||||
|
def __init__(self, path, default=NONE, fmt=None):
|
||||||
|
with open(util.expand_path(path)) as fp:
|
||||||
|
source = fp.read()
|
||||||
|
JinjaFormatter.__init__(self, source, default, fmt)
|
||||||
|
|
||||||
|
|
||||||
def parse_field_name(field_name):
|
def parse_field_name(field_name):
|
||||||
if field_name[0] == "'":
|
if field_name[0] == "'":
|
||||||
return "_lit", (operator.itemgetter(field_name[1:-1]),)
|
return "_lit", (operator.itemgetter(field_name[1:-1]),)
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
# it under the terms of the GNU General Public License version 2 as
|
# it under the terms of the GNU General Public License version 2 as
|
||||||
# published by the Free Software Foundation.
|
# published by the Free Software Foundation.
|
||||||
|
|
||||||
__version__ = "1.30.0"
|
__version__ = "1.30.1-dev"
|
||||||
__variant__ = None
|
__variant__ = None
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import tempfile
|
|||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from gallery_dl import formatter, text, util # noqa E402
|
from gallery_dl import formatter, text, util # noqa E402
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jinja2
|
||||||
|
except ImportError:
|
||||||
|
jinja2 = None
|
||||||
|
|
||||||
|
|
||||||
class TestFormatter(unittest.TestCase):
|
class TestFormatter(unittest.TestCase):
|
||||||
|
|
||||||
@@ -476,6 +481,35 @@ class TestFormatter(unittest.TestCase):
|
|||||||
with self.assertRaises(OSError):
|
with self.assertRaises(OSError):
|
||||||
formatter.parse("\fTF /")
|
formatter.parse("\fTF /")
|
||||||
|
|
||||||
|
@unittest.skipIf(jinja2 is None, "no jinja2")
|
||||||
|
def test_jinja(self):
|
||||||
|
self._run_test("\fJ {{a}}", self.kwdict["a"])
|
||||||
|
self._run_test("\fJ {{name}}{{name}} {{a}}", "{}{} {}".format(
|
||||||
|
self.kwdict["name"], self.kwdict["name"], self.kwdict["a"]))
|
||||||
|
self._run_test("\fJ foo-'\"{{a | upper}}\"'-bar",
|
||||||
|
"""foo-'"{}"'-bar""".format(self.kwdict["a"].upper()))
|
||||||
|
|
||||||
|
@unittest.skipIf(jinja2 is None, "no jinja2")
|
||||||
|
def test_template_jinja(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||||
|
path1 = os.path.join(tmpdirname, "tpl1")
|
||||||
|
path2 = os.path.join(tmpdirname, "tpl2")
|
||||||
|
|
||||||
|
with open(path1, "w") as fp:
|
||||||
|
fp.write("{{a}}")
|
||||||
|
fmt1 = formatter.parse("\fTJ " + path1)
|
||||||
|
|
||||||
|
with open(path2, "w") as fp:
|
||||||
|
fp.write("foo-'\"{{a | upper}}\"'-bar")
|
||||||
|
fmt2 = formatter.parse("\fTJ " + path2)
|
||||||
|
|
||||||
|
self.assertEqual(fmt1.format_map(self.kwdict), self.kwdict["a"])
|
||||||
|
self.assertEqual(fmt2.format_map(self.kwdict),
|
||||||
|
"""foo-'"{}"'-bar""".format(self.kwdict["a"].upper()))
|
||||||
|
|
||||||
|
with self.assertRaises(OSError):
|
||||||
|
formatter.parse("\fTJ /")
|
||||||
|
|
||||||
def test_module(self):
|
def test_module(self):
|
||||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||||
path = os.path.join(tmpdirname, "testmod.py")
|
path = os.path.join(tmpdirname, "testmod.py")
|
||||||
|
|||||||
Reference in New Issue
Block a user