diff --git a/README.rst b/README.rst index 7739e198..a8e22de0 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/docs/formatting.md b/docs/formatting.md index 57779f4c..03d9f62a 100644 --- a/docs/formatting.md +++ b/docs/formatting.md @@ -381,15 +381,20 @@ Starting a format string with `\f ` allows to set a different format strin + + E + An arbitrary Python expression + \fE title.upper().replace(' ', '-') + F An f-string literal \fF '{title.strip()}' by {artist.capitalize()} - E - An arbitrary Python expression - \fE title.upper().replace(' ', '-') + J + A Jinja template + \fJ '{{title | trim}}' by {{artist | capitalize}} T @@ -401,6 +406,11 @@ Starting a format string with `\f ` allows to set a different format strin Path to a template file containing an f-string literal \fTF ~/.templates/fstr.txt + + TJ + Path to a template file containing a Jinja template + \fTF ~/.templates/jinja.txt + M Path or name of a Python module diff --git a/gallery_dl/formatter.py b/gallery_dl/formatter.py index 8e0dc3f1..1e4ba35b 100644 --- a/gallery_dl/formatter.py +++ b/gallery_dl/formatter.py @@ -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]),) diff --git a/gallery_dl/version.py b/gallery_dl/version.py index def021c6..1ffd2b55 100644 --- a/gallery_dl/version.py +++ b/gallery_dl/version.py @@ -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 diff --git a/test/test_formatter.py b/test/test_formatter.py index 9d59fda4..dc7c30a6 100644 --- a/test/test_formatter.py +++ b/test/test_formatter.py @@ -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")