Source code for tmep.functions

# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""This file originates from the file `beets/library.py
<https://github.com/beetbox/beets/blob/58afaf07a52df2b53bb2f8990cd06005cd063d9e/beets/library.py#L1341>`_
of the `beets project <http://beets.io>`_.
"""

from __future__ import annotations

import re
import textwrap
import time
import typing
from typing import Optional

from unidecode import unidecode

from tmep.types import FunctionCollection, Values


def _int_arg(s: str) -> int:
    """Convert a string argument to an integer for use in a template
    function. May raise a ValueError.
    """
    return int(s.strip())


[docs] class DefaultTemplateFunctions: """A container class for the default functions provided to path templates. These functions are contained in an object to provide additional context to the functions -- specifically, the Item being evaluated. """ prefix = "fn_" values: Optional[Values] func_names: typing.List[str] def __init__(self, values: Optional[Values] = None): """Parametrize the functions.""" self.values = values
[docs] def get(self) -> FunctionCollection: """Returns a dictionary containing the functions defined in this object. The keys are function names (as exposed in templates) and the values are Python functions. """ out: FunctionCollection = {} for key in self.func_names: out[key[len(self.prefix) :]] = getattr(self, key) return out
[docs] def fn_alpha(self, text: str) -> str: """ * synopsis: ``%alpha{text}`` * description: This function first ASCIIfies the given text, then all \ non alphabet characters are replaced with whitespaces. * example: ``%alpha{a1b23c}`` → ``a b c`` """ text = self.fn_asciify(text) text = re.sub(r"[^a-zA-Z]+", " ", text) return re.sub(r"\s+", " ", text)
[docs] def fn_alphanum(self, text: str) -> str: """ * synopsis: ``%alphanum{text}`` * description: This function first ASCIIfies the given text, then all \ non alpanumeric characters are replaced with whitespaces. * example: ``%alphanum{après-évêque1}`` → ``apres eveque1`` """ text = self.fn_asciify(text) text = re.sub(r"[^a-zA-Z0-9]+", " ", text) return re.sub(r"\s+", " ", text)
[docs] @staticmethod def fn_asciify(text: str) -> str: """ * synopsis: ``%asciify{text}`` * description: Translate non-ASCII characters to their ASCII \ equivalents. For example, “café” becomes “cafe”. Uses the mapping \ provided by the unidecode module. * example: ``%asciify{äÄöÖüÜ}`` → ``aeAeoeOeueUe`` """ ger_umlaute = {"ae": "ä", "oe": "ö", "ue": "ü", "Ae": "Ä", "Oe": "Ö", "Ue": "Ü"} for replace, search in ger_umlaute.items(): text = text.replace(search, replace) return str(unidecode(text).replace("[?]", ""))
[docs] @staticmethod def fn_delchars(text: str, chars: str) -> str: """ * synopsis: ``%delchars{text,chars}`` * description: Delete every single character of “chars“ in “text”. * example: ``%delchars{Schubert, ue}`` → ``Schbrt`` """ for char in chars: text = text.replace(char, "") return text
[docs] @staticmethod def fn_deldupchars(text: str, chars: str = r"-_\.") -> str: """ * synopsis: ``%deldupchars{text,chars}`` * description: Search for duplicate characters and replace with only \ one occurrance of this characters. * example: ``%deldupchars{a---b___c...d}`` → ``a-b_c.d``; \ ``%deldupchars{a---b___c, -}`` → ``a-b___c`` """ import re return re.sub(r"([" + chars + r"])\1*", r"\1", text)
[docs] @staticmethod def fn_first( text: str, count: int = 1, skip: int = 0, sep: str = "; ", join_str: str = "; " ) -> str: """ * synopsis: ``%first{text}`` or ``%first{text,count,skip}`` or \ ``%first{text,count,skip,sep,join}`` * description: Returns the first item, separated by ``;``. You can use \ ``%first{text,count,skip}``, where count is the number of items \ (default 1) and skip is number to skip (default 0). You can also \ use ``%first{text,count,skip,sep,join}`` where ``sep`` is the separator, \ like ``;`` or ``/`` and join is the text to concatenate the items. * example: ``%first{Alice / Bob / Eve,2,0, / , & }`` → ``Alice & Bob`` :param text: the string :param count: The number of items included :param skip: The number of items skipped :param sep: the separator. Usually is '; ' (default) or '/ ' :param join_str: the string which will join the items, default '; '. """ skip = int(skip) count = skip + int(count) return join_str.join(text.split(sep)[skip:count])
[docs] @staticmethod def fn_if(condition: str, trueval: str, falseval: str = "") -> str: """If ``condition`` is nonempty and nonzero, emit ``trueval``; otherwise, emit ``falseval`` (if provided). * synopsis: ``%if{condition,trueval}`` or \ ``%if{condition,trueval,falseval}`` * description: If condition is nonempty (or nonzero, if it’s a \ number), then returns the second argument. Otherwise, returns the \ third argument if specified (or nothing if ``falseval`` is left off). * example: ``x%if{false,foo}`` → ``x`` """ c: typing.Union[str, int] c = condition try: int_condition = _int_arg(condition) except ValueError: if condition.lower() == "false": return falseval else: c = int_condition if c: return trueval else: return falseval
[docs] def fn_ifdef(self, field: str, trueval: str = "", falseval: str = "") -> str: """If field exists return trueval or the field (default) otherwise, emit return falseval (if provided). * synopsis: ``%ifdef{field}``, ``%ifdef{field,trueval}`` or \ ``%ifdef{field,trueval,falseval}`` * description: If field exists, then return ``trueval`` or field \ (default). Otherwise, returns ``falseval``. The field should be \ entered without ``$``. * example: ``%ifdef{compilation,Compilation}`` :param field: The name of the field :param trueval: The string if the condition is true :param falseval: The string if the condition is false :return: The string, based on condition """ if self.values and field in self.values: return trueval else: return falseval
[docs] def fn_ifdefempty(self, field: str, trueval: str = "", falseval: str = ""): """If field exists and is emtpy return trueval otherwise, emit return falseval (if provided). * synopsis: ``%ifdefempty{field,text}`` or \ ``%ifdefempty{field,text,falsetext}`` * description: If field exists and is empty, then return ``truetext``. \ Otherwise, returns ``falsetext``. The field should be \ entered without ``$``. * example: ``%ifdefempty{compilation,Album,Compilation}`` :param field: The name of the field :param trueval: The string if the condition is true :param falseval: The string if the condition is false :return: The string, based on condition """ if not self.values: return falseval if ( field not in self.values or (field in self.values and not self.values[field]) or re.search(r"^\s*$", self.values[field]) ): return trueval else: return falseval
[docs] def fn_ifdefnotempty( self, field: str, trueval: str = "", falseval: str = "" ) -> str: """If field is not emtpy return trueval or the field (default) otherwise, emit return falseval (if provided). * synopsis: ``%ifdefnotempty{field,text}`` or \ ``%ifdefnotempty{field,text,falsetext}`` * description: If field is not empty, then return ``truetext``. \ Otherwise, returns ``falsetext``. The field should be \ entered without ``$``. * example: ``%ifdefnotempty{compilation,Compilation,Album}`` :param field: The name of the field :param trueval: The string if the condition is true :param falseval: The string if the condition is false :return: The string, based on condition """ if not self.values: return trueval if ( field not in self.values or (field in self.values and not self.values[field]) or re.search(r"^\s*$", self.values[field]) ): return falseval else: return trueval
[docs] @staticmethod def fn_initial(text: str) -> str: """ * synopsis: ``%initial{text}`` * description: Get the first character of a text in lowercase. The \ text is converted to ASCII. All non word characters are erased. Only letters and numbers are preserved. If the first character is a number, then the result is '0'. * example: ``%initial{Schubert}`` → ``s`` :param string text: Input text to build initial from. :return: A single character """ text = unidecode(text) text = re.sub(r"[^a-zA-Z0-9]+", "", text) text = text[0:1] text = text.lower() if not text: return "_" try: int(text) text = "0" except Exception: pass return text
[docs] @staticmethod def fn_left(text: str, n: str) -> str: """Get the leftmost characters of a string. * synopsis: ``%left{text,n}`` * description: Return the first “n” characters of “text”. * example: ``%left{Schubert, 3}`` → ``Sch`` """ return text[0 : _int_arg(n)]
[docs] @staticmethod def fn_lower(text: str) -> str: """Convert a string to lower case * synopsis: ``%lower{text}`` * description: Convert “text” to lowercase. * example: ``%lower{SCHUBERT}`` → ``schubert`` """ return text.lower()
[docs] @staticmethod def fn_nowhitespace(text: str, replace: str = "-") -> str: """ * synopsis: ``%nowhitespace{text,replace}`` * description: Replace all whitespace characters with ``replace``. \ By default: a dash (``-``) * example: ``%nowhitespace{a b}`` → ``a-b``; ``%nowhitespace{a b, _}`` → ``a_b`` """ return re.sub(r"\s+", replace, text)
[docs] @staticmethod def fn_num(number: int, count: int = 2) -> str: """Pad decimal number with leading zeros * synopsis: ``%num{number,count}`` * description: Pad decimal number with leading zeros. * example: ``%num{7,3}`` → ``007`` """ return str(number).zfill(int(count))
[docs] @staticmethod def fn_replchars(text: str, replace: str, chars: str) -> str: """ * synopsis: ``%replchars{text,chars,replace}`` * description: Replace the characters “chars” in “text” with \ “replace”. * example: ``%replchars{Schubert,-,ue}`` → ``Sch-b-rt`` """ for char in chars: text = text.replace(char, replace) return text
[docs] @staticmethod def fn_right(text: str, n: str) -> str: """Get the rightmost characters of a string. * synopsis: ``%right{text,n}`` * description: Return the last “n” characters of “text”. * example: ``%right{Schubert,3}`` → ``ert`` """ return text[-_int_arg(n) :]
[docs] @staticmethod def fn_sanitize(text: str) -> str: """ * synopsis: ``%sanitize{text}`` * description: Delete characters that are not allowed in most file systems. * example: ``%sanitize{x:*?<>|/~&x}`` → ``xx`` """ for char in ':*?"<>|\/~&{}': # noqa: W605 text = text.replace(char, "") return text
[docs] @staticmethod def fn_shorten(text: str, max_size: int = 32) -> str: """Shorten the given text to ``max_size`` * synopsis: ``%shorten{text}`` or ``%shorten{text,max_size}`` * description: Shorten “text” on word boundarys. * example: ``%shorten{Lorem ipsum dolor sit, 10}`` → ``Lorem`` """ max_size = int(max_size) if len(text) <= max_size: return text text = textwrap.wrap(text, max_size)[0] import re text = re.sub(r"\W+$", "", text) return text.strip()
[docs] @staticmethod def fn_time(text: str, fmt: str, cur_fmt: str) -> str: """Format a time value using `strftime`. * synopsis: ``%time{date_time,format,curformat}`` * description: Return the date and time in any format accepted by \ ``strftime``. For example, to get the year, use ``%time{$added,%Y}``. * example: ``%time{30 Nov 2024,%Y,%d %b %Y}`` → ``2024`` """ return time.strftime(fmt, time.strptime(text, cur_fmt))
[docs] @staticmethod def fn_title(text: str) -> str: """Convert a string to title case * synopsis: ``%title{text}`` * description: Convert “text” to Title Case. * example: ``%title{franz schubert}`` → ``Franz Schubert`` """ return text.title()
[docs] @staticmethod def fn_upper(text: str) -> str: """Covert a string to upper case * synopsis: ``%upper{text}`` * description: Convert “text” to UPPERCASE. * example: ``%upper{foo}`` → ``FOO`` """ return text.upper()
# Get the name of fn_* functions in the above class. DefaultTemplateFunctions.func_names = [ s for s in dir(DefaultTemplateFunctions) if s.startswith(DefaultTemplateFunctions.prefix) ]