diff options
Diffstat (limited to 'gallery_dl/formatter.py')
| -rw-r--r-- | gallery_dl/formatter.py | 128 |
1 files changed, 102 insertions, 26 deletions
diff --git a/gallery_dl/formatter.py b/gallery_dl/formatter.py index 6affc3e..7a49049 100644 --- a/gallery_dl/formatter.py +++ b/gallery_dl/formatter.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright 2021-2023 Mike Fährmann +# Copyright 2021-2025 Mike Fährmann # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as @@ -28,21 +28,17 @@ def parse(format_string, default=NONE, fmt=format): except KeyError: 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:] - - if kind == "T": - cls = TemplateFormatter - elif kind == "TF": - cls = TemplateFStringFormatter - elif kind == "E": - cls = ExpressionFormatter - elif kind == "M": - cls = ModuleFormatter - elif kind == "F": - cls = FStringFormatter + try: + cls = _FORMATTERS[kind[1:]] + except KeyError: + import logging + logging.getLogger("formatter").error( + "Invalid formatter type '%s'", kind[1:]) + cls = StringFormatter + else: + cls = StringFormatter formatter = _CACHE[key] = cls(format_string, default, fmt) return formatter @@ -208,6 +204,48 @@ class ExpressionFormatter(): self.format_map = util.compile_expression(expression) +class FStringFormatter(): + """Generate text by evaluating an f-string literal""" + + def __init__(self, fstring, default=NONE, fmt=None): + self.format_map = util.compile_expression(f'f"""{fstring}"""') + + +def _init_jinja(): + import jinja2 + from . import config + + if opts := config.get((), "jinja"): + JinjaFormatter.env = env = jinja2.Environment( + **opts.get("environment") or {}) + else: + JinjaFormatter.env = jinja2.Environment() + return + + if policies := opts.get("policies"): + env.policies.update(policies) + + if path := opts.get("filters"): + module = util.import_file(path).__dict__ + env.filters.update( + module["__filters__"] if "__filters__" in module else module) + + if path := opts.get("tests"): + module = util.import_file(path).__dict__ + env.tests.update( + module["__tests__"] if "__tests__" in module else module) + + +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: + _init_jinja() + self.format_map = self.env.from_string(source).render + + class ModuleFormatter(): """Generate text by calling an external function""" @@ -217,13 +255,6 @@ class ModuleFormatter(): self.format_map = getattr(module, function_name) -class FStringFormatter(): - """Generate text by evaluating an f-string literal""" - - def __init__(self, fstring, default=NONE, fmt=None): - self.format_map = util.compile_expression('f"""' + fstring + '"""') - - class TemplateFormatter(StringFormatter): """Read format_string from file""" @@ -242,6 +273,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]),) @@ -302,7 +342,7 @@ def _parse_optional(format_spec, default): fmt = _build_format_func(format_spec, default) def optional(obj): - return before + fmt(obj) + after if obj else "" + return f"{before}{fmt(obj)}{after}" if obj else "" return optional @@ -385,6 +425,27 @@ def _parse_join(format_spec, default): return apply_join +def _parse_map(format_spec, default): + key, _, format_spec = format_spec.partition(_SEPARATOR) + key = key[1:] + fmt = _build_format_func(format_spec, default) + + def map_(obj): + if not obj or isinstance(obj, str): + return fmt(obj) + + results = [] + for item in obj: + if isinstance(item, dict): + value = item.get(key, ...) + results.append(default if value is ... else value) + else: + results.append(item) + return fmt(results) + + return map_ + + def _parse_replace(format_spec, default): old, new, format_spec = format_spec.split(_SEPARATOR, 2) old = old[1:] @@ -463,8 +524,7 @@ class Literal(): # __getattr__, __getattribute__, and __class_getitem__ # are all slower than regular __getitem__ - @staticmethod - def __getitem__(key): + def __getitem__(self, key): return key @@ -472,6 +532,18 @@ _literal = Literal() _CACHE = {} _SEPARATOR = "/" +_FORMATTERS = { + "E" : ExpressionFormatter, + "F" : FStringFormatter, + "J" : JinjaFormatter, + "M" : ModuleFormatter, + "S" : StringFormatter, + "T" : TemplateFormatter, + "TF": TemplateFStringFormatter, + "FT": TemplateFStringFormatter, + "TJ": TemplateJinjaFormatter, + "JT": TemplateJinjaFormatter, +} _GLOBALS = { "_env": lambda: os.environ, "_lit": lambda: _literal, @@ -485,12 +557,15 @@ _CONVERSIONS = { "C": string.capwords, "j": util.json_dumps, "t": str.strip, - "L": len, + "n": len, + "L": util.code_to_language, "T": util.datetime_to_timestamp_string, "d": text.parse_timestamp, + "D": util.to_datetime, "U": text.unescape, "H": lambda s: text.unescape(text.remove_html(s)), "g": text.slugify, + "W": text.sanitize_whitespace, "S": util.to_string, "s": str, "r": repr, @@ -506,6 +581,7 @@ _FORMAT_SPECIFIERS = { "D": _parse_datetime, "J": _parse_join, "L": _parse_maxlen, + "M": _parse_map, "O": _parse_offset, "R": _parse_replace, "S": _parse_sort, |
