"""Text functions."""
import re
import textwrap
from dataclasses import dataclass
from typing import Optional
from generate_changelog.actions import register_builtin
from generate_changelog.configuration import IntOrCallable, StrOrCallable
from generate_changelog.utilities import eval_if_callable
[docs]
@register_builtin
@dataclass(frozen=True)
class SetDefault:
"""Return a default value when called with an empty value."""
default: StrOrCallable = ""
[docs]
def __call__(self, input_text: StrOrCallable) -> str:
"""Return a default value when called with an empty value."""
default = eval_if_callable(self.default)
text = eval_if_callable(input_text)
return text or default
[docs]
@register_builtin
@dataclass(frozen=True)
class PrefixString:
"""Prefix a string to the input when called."""
prefix: StrOrCallable = ""
"""The string to prefix"""
[docs]
def __call__(self, input_text: StrOrCallable) -> str:
"""Prefix input_text."""
text = eval_if_callable(input_text) or ""
prefix = eval_if_callable(self.prefix) or ""
return f"{prefix}{text}"
[docs]
@register_builtin
@dataclass(frozen=True)
class AppendString:
"""Create a callable that can append a string to the input."""
postfix: StrOrCallable = ""
"""The string to append."""
[docs]
def __call__(self, input_text: StrOrCallable) -> str:
"""Append a string to the input_text."""
text = eval_if_callable(input_text) or ""
postfix = eval_if_callable(self.postfix) or ""
return f"{text}{postfix}"
[docs]
@register_builtin
@dataclass(frozen=True)
class Strip:
"""Create a callable that will strip a string from the ends of an input."""
chars: StrOrCallable = " "
[docs]
def __call__(self, input_text: StrOrCallable) -> str:
"""Strip characters from the ends of the input."""
text = eval_if_callable(input_text) or ""
chars = eval_if_callable(self.chars) or " "
return text.strip(chars)
[docs]
@register_builtin
@dataclass(frozen=True)
class RegExCommand:
"""A base class to hold regular expression information."""
pattern: StrOrCallable
"""The regular expression to match against."""
ascii_flag: bool = False
ignorecase_flag: bool = False
locale_flag: bool = False
multiline_flag: bool = False
dotall_flag: bool = False
verbose_flag: bool = False
@property
def flags(self) -> re.RegexFlag:
"""The combined RegexFlags."""
from functools import reduce
flags = [
(self.ascii_flag, re.ASCII),
(self.ignorecase_flag, re.IGNORECASE),
(self.locale_flag, re.LOCALE),
(self.multiline_flag, re.MULTILINE),
(self.dotall_flag, re.DOTALL),
(self.verbose_flag, re.VERBOSE),
]
return reduce(lambda x, y: x | y, [value for use, value in flags if use], re.RegexFlag(0))
[docs]
@register_builtin
@dataclass(frozen=True)
class FirstRegExMatch(RegExCommand):
"""When called, returns the first match in a string using a predefined regex."""
named_subgroup: Optional[str] = None
"""The named subgroup defined in the pattern to return."""
default_value: StrOrCallable = ""
"""The value to return if no match is found."""
[docs]
def __call__(self, input_text: StrOrCallable) -> str:
"""Search the input_text for the predefined pattern and return it."""
text = eval_if_callable(input_text)
pattern = eval_if_callable(self.pattern)
match = re.search(pattern, text, self.flags)
if match is None:
return eval_if_callable(self.default_value)
group_dict = match.groupdict()
if self.named_subgroup and self.named_subgroup in group_dict:
return group_dict[self.named_subgroup] or eval_if_callable(self.default_value)
return match.group(0)
[docs]
@register_builtin
@dataclass(frozen=True)
class FirstRegExMatchPosition(RegExCommand):
"""When called, returns the position of the first match in a string using a predefined regex."""
[docs]
def __call__(self, input_text: StrOrCallable) -> int:
"""Search the input_text for the predefined pattern and return its position."""
text = eval_if_callable(input_text)
pattern = eval_if_callable(self.pattern)
match = re.search(pattern, text, self.flags)
return match.start() if match else 0
[docs]
@register_builtin
@dataclass(frozen=True)
class RegexSub(RegExCommand):
"""Create a callable that will make substitutions using regular expressions."""
replacement: StrOrCallable = ""
"""The replacement string for matches."""
[docs]
def __call__(self, input_text: StrOrCallable) -> str:
"""Do the substitution on the input_text."""
text = eval_if_callable(input_text)
pattern = eval_if_callable(self.pattern)
replacement = eval_if_callable(self.replacement)
replacement = re.sub(r"\\([\d+])", r"\\g<\1>", replacement) # Replace back-references of type '\1' to '\g<1>'
return re.sub(pattern, replacement, text, flags=self.flags)
[docs]
@register_builtin
@dataclass(frozen=True)
class PrefixLines:
"""Creates a callable to prefix lines to input text."""
prefix: StrOrCallable
"""The characters to put in front of each line."""
first_line: Optional[StrOrCallable] = None
"""Prefix the first line with these characters."""
[docs]
def __call__(self, input_text: StrOrCallable) -> str:
"""Prepend characters to the lines in input text."""
text = eval_if_callable(input_text) or ""
prefix = eval_if_callable(self.prefix) or ""
first_line_prefix = eval_if_callable(self.first_line) or prefix
lines = text.splitlines()
if not lines:
return ""
first_line = f"{first_line_prefix}{lines[0]}".rstrip(" ")
prefixed_lines = [f"{prefix}{line}".rstrip(" ") for line in lines[1:]]
prefixed_lines.insert(0, first_line)
return "\n".join(prefixed_lines) + "\n"
[docs]
@register_builtin
@dataclass(frozen=True)
class WrapParagraphs:
"""Create a callable to wrap the paragraphs of a string."""
paragraph_pattern: StrOrCallable = "\n\n"
"""Pattern to detect paragraphs."""
paragraph_join: StrOrCallable = "\n\n"
"""Join the wrapped paragraphs with this string."""
width: int = 88
"""The maximum width of each line of the paragraph."""
[docs]
def __call__(self, input_text: StrOrCallable) -> str:
"""Wrap each paragraph of the input text."""
paragraph_pattern = eval_if_callable(self.paragraph_pattern)
pattern = re.compile(paragraph_pattern, re.MULTILINE)
text = eval_if_callable(input_text)
paragraph_join = eval_if_callable(self.paragraph_join)
paragraphs = pattern.split(text)
wrapped_paragraphs = [textwrap.fill(p, width=self.width) for p in paragraphs]
return paragraph_join.join(wrapped_paragraphs)
register_builtin("prefix_caret")(PrefixString("^"))
register_builtin("append_dot")(AppendString("."))
register_builtin("noop")(lambda txt: txt)
register_builtin("strip_spaces")(Strip())
[docs]
@register_builtin
def capitalize(msg: str) -> str:
"""
Capitalize the first character for a string.
Args:
msg: The string to capitalize
Returns:
The capitalized string
"""
return msg[0].upper() + msg[1:]
[docs]
@register_builtin
@dataclass
class Slice:
"""When called, return a slice of the sequence."""
start: Optional[IntOrCallable] = None
"""The start of the slice. None means the beginning of the sequence."""
stop: Optional[IntOrCallable] = None
"""The end of the slice. None means the end of the sequence."""
step: Optional[IntOrCallable] = None
"""Slice using this step betweeen indices. None means don't use the step."""
[docs]
def __call__(self, input_text: StrOrCallable) -> str:
"""Slice the sequence."""
text = eval_if_callable(input_text)
start = eval_if_callable(self.start)
stop = eval_if_callable(self.stop)
step = eval_if_callable(self.step)
return text[start:stop:step]