# gitlab - A GitLab client and webhook receiver for maubot
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
import os.path
from pathlib import Path
from typing import Any, Callable, Dict, List, Tuple, Union
from jinja2 import BaseLoader
from jinja2 import Environment as JinjaEnvironment
from jinja2 import Template, TemplateNotFound
from ops_bot import get_project_root
from ops_bot.util import markdown
def sync_read_file(path: Union[Path, str]) -> str:
with open(path) as file:
return file.read()
def sync_list_files(directory: Union[Path, str]) -> list[str]:
return os.listdir(directory)
class TemplateUtil:
@staticmethod
def bold_scope(label: str) -> str:
try:
scope, label = label.rsplit("::", 1)
return f"{scope}::{label}"
except ValueError:
return label
@staticmethod
def pluralize(val: int, unit: str) -> str:
if val == 1:
return f"{val} {unit}"
return f"{val} {unit}s"
@classmethod
def format_time(cls, seconds: Union[int, float], enable_days: bool = False) -> str:
seconds = abs(seconds)
frac_seconds = round(seconds - int(seconds), 1)
minutes, seconds = divmod(int(seconds), 60)
hours, minutes = divmod(minutes, 60)
if enable_days:
days, hours = divmod(hours, 24)
else:
days = 0
parts = []
if days > 0:
parts.append(cls.pluralize(days, "day"))
if hours > 0:
parts.append(cls.pluralize(hours, "hour"))
if minutes > 0:
parts.append(cls.pluralize(minutes, "minute"))
if seconds > 0 or len(parts) == 0:
parts.append(cls.pluralize(int(seconds + frac_seconds), "second"))
if len(parts) == 1:
return parts[0]
return ", ".join(parts[:-1]) + f" and {parts[-1]}"
@staticmethod
def join_human_list(
data: List[str],
*,
joiner: str = ", ",
final_joiner: str = " and ",
mutate: Callable[[str], str] = lambda val: val,
) -> str:
if not data:
return ""
elif len(data) == 1:
return mutate(data[0])
return (
joiner.join(mutate(val) for val in data[:-1])
+ final_joiner
+ mutate(data[-1])
)
class TemplateProxy:
_env: JinjaEnvironment
_args: Dict[str, Any]
def __init__(self, env: JinjaEnvironment, args: Dict[str, Any]) -> None:
self._env = env
self._args = args
def __getattr__(self, item: str) -> str:
try:
tpl = self._env.get_template(item)
except TemplateNotFound:
raise AttributeError(item)
return tpl.render(**self._args)
class PluginTemplateLoader(BaseLoader):
directory: Path
macros: str
def __init__(self, base: str, directory: str) -> None:
base_path = get_project_root() / ".." / "templates" / base
self.directory = base_path / directory
self.macros = sync_read_file(base_path / "macros.html")
def get_source(
self, environment: Any, name: str
) -> Tuple[str, str, Callable[[], bool]]:
path = self.directory / f"{name}.html"
try:
tpl = sync_read_file(path)
except KeyError:
raise TemplateNotFound(name)
return self.macros + tpl, name, lambda: True
def list_templates(self) -> List[str]:
return [
os.path.splitext(os.path.basename(path))[0]
for path in sync_list_files(self.directory)
if path.endswith(".html")
]
class TemplateManager:
_env: JinjaEnvironment
_loader: PluginTemplateLoader
def __init__(self, base: str, directory: str) -> None:
self._loader = PluginTemplateLoader(base, directory)
self._env = JinjaEnvironment( # nosec B701
loader=self._loader,
lstrip_blocks=True,
trim_blocks=True,
autoescape=False,
extensions=["jinja2.ext.do"],
)
self._env.filters["markdown"] = lambda message: markdown.render(
message, allow_html=True
)
def __getitem__(self, item: str) -> Template:
return self._env.get_template(item)
def proxy(self, args: Dict[str, Any]) -> TemplateProxy:
return TemplateProxy(self._env, args)