[formatter] add basic 'Jinja2' template support (#1390)

This commit is contained in:
Mike Fährmann
2025-07-16 23:06:35 +02:00
parent cbf930c99c
commit f25c5f4b0c
5 changed files with 77 additions and 7 deletions

View File

@@ -36,6 +36,7 @@ Optional
- SecretStorage_: GNOME keyring passwords for ``--cookies-from-browser``
- Psycopg_: PostgreSQL archive support
- truststore_: Native system certificate support
- Jinja_: Jinja template support
Installation
@@ -476,6 +477,7 @@ To authenticate with a ``mastodon`` instance, run *gallery-dl* with
.. _SecretStorage: https://pypi.org/project/SecretStorage/
.. _Psycopg: https://www.psycopg.org/
.. _truststore: https://truststore.readthedocs.io/en/latest/
.. _Jinja: https://jinja.palletsprojects.com/
.. _Snapd: https://docs.snapcraft.io/installing-snapd
.. _OAuth: https://en.wikipedia.org/wiki/OAuth
.. _Chocolatey: https://chocolatey.org/install

View File

@@ -381,15 +381,20 @@ Starting a format string with `\f<Type> ` allows to set a different format strin
</tr>
</thead>
<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>
<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><code>\fF '{title.strip()}' by {artist.capitalize()}</code></td>
</tr>
<tr>
<td align="center"><code>E</code></td>
<td>An arbitrary Python expression</td>
<td><code>\fE title.upper().replace(' ', '-')</code></td>
<td align="center"><code>J</code></td>
<td>A <a href="https://jinja.palletsprojects.com/">Jinja</a> template</td>
<td><code>\fJ '{{title | trim}}' by {{artist | capitalize}}</code></td>
</tr>
<tr>
<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><code>\fTF ~/.templates/fstr.txt</code></td>
</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>
<td align="center"><code>M</code></td>
<td>Path or name of a Python module

View File

@@ -29,7 +29,7 @@ def parse(format_string, default=NONE, fmt=format):
pass
cls = StringFormatter
if format_string.startswith("\f"):
if format_string and format_string[0] == "\f":
kind, _, format_string = format_string.partition(" ")
kind = kind[1:]
@@ -37,12 +37,16 @@ def parse(format_string, default=NONE, fmt=format):
cls = TemplateFormatter
elif kind == "TF":
cls = TemplateFStringFormatter
elif kind == "TJ":
cls = TemplateJinjaFormatter
elif kind == "E":
cls = ExpressionFormatter
elif kind == "M":
cls = ModuleFormatter
elif kind == "J":
cls = JinjaFormatter
elif kind == "F":
cls = FStringFormatter
elif kind == "M":
cls = ModuleFormatter
formatter = _CACHE[key] = cls(format_string, default, fmt)
return formatter
@@ -224,6 +228,17 @@ class FStringFormatter():
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):
"""Read format_string from file"""
@@ -242,6 +257,15 @@ class TemplateFStringFormatter(FStringFormatter):
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):
if field_name[0] == "'":
return "_lit", (operator.itemgetter(field_name[1:-1]),)

View File

@@ -6,5 +6,5 @@
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
__version__ = "1.30.0"
__version__ = "1.30.1-dev"
__variant__ = None

View File

@@ -17,6 +17,11 @@ import tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from gallery_dl import formatter, text, util # noqa E402
try:
import jinja2
except ImportError:
jinja2 = None
class TestFormatter(unittest.TestCase):
@@ -476,6 +481,35 @@ class TestFormatter(unittest.TestCase):
with self.assertRaises(OSError):
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):
with tempfile.TemporaryDirectory() as tmpdirname:
path = os.path.join(tmpdirname, "testmod.py")