# Copyright (C) 2016-2023 Louis Paternault
#
# 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 <http://www.gnu.org/licenses/>.
"""Generic definitions to implement cell plugins."""
import contextlib
import importlib
import logging
import os
import pkgutil
import sys
import jinja2
LOGGER = logging.getLogger("pypimonitor")
def load_cell_plugins():
"""Iterator over the cell plugins."""
for _, name, _ in pkgutil.walk_packages(
path=sys.modules[__name__].__path__, prefix=f"{__name__}."
):
module = importlib.import_module(name)
for attr in dir(module):
obj = getattr(module, attr)
if (
isinstance(obj, type)
and issubclass(obj, Cell)
and obj.keyword is not None
):
yield obj
@contextlib.contextmanager
def temp_update(source, update):
"""Temporary update of a dictionary."""
bak = {}
new = set()
for key in update:
if key not in source:
new.add(key)
source[key] = update[key]
for key in source:
if key in update:
bak[key] = source[key]
source[key] = update[key]
yield source
for key, value in bak.items():
source[key] = value
for key in new:
del source[key]
[docs]
class Cell:
"""Render some piece of information about a package as HTML code."""
#: Keyword referencing the plugin, used in the :ref:`yaml` file to enable
#: this plugin. If ``None``, the class is an abstract class that cannot be
#: used directly.
keyword = None
#: Title of the column.
title = ""
#: Default values for package arguments. See :ref:`defaultrequired` for more details.
default = {}
#: List of names of required package arguments. See :ref:`defaultrequired` for more details.
required = []
def __init__(self, renderer):
self.renderer = renderer
self.config = renderer.config
@jinja2.pass_context
def __call__(self, context, cell, package):
filled = self.default.copy()
filled.update(self.config["packages"][package].get(cell, {}))
# Detecting missing parameters
missing = set()
for key in self.required:
if key not in filled:
missing.add(key)
if missing:
return self.render_error(
context,
self.keyword,
package,
"Missing argument(s) {}.".format( # pylint: disable=consider-using-f-string
", ".join([f"'{key}'" for key in missing])
),
)
return self.render(context, package, filled)
[docs]
def render(self, context, package, cell):
"""Return the HTML code corresponding to this cell.
:param context: Current `Jinja2 context
<http://jinja.pocoo.org/docs/dev/api/#jinja2.runtime.Context>`_.
:param str package: Package name.
:param dict cell: Package arguments for this cell.
:rtype: str
:return: The HTML code to display in the given cell.
"""
raise NotImplementedError()
[docs]
@staticmethod
def render_error(context, cell, package, message):
"""Return the HTML code corresponding to an error.
:param context: Current `Jinja2 context
<http://jinja.pocoo.org/docs/dev/api/#jinja2.runtime.Context>`_.
:param str cell: Cell name (plugin keyword).
:param str package: Package name.
:param str message: Human readable error message.
:rtype: str
:return: The HTML code to display in the given cell.
"""
full_message = f"Package '{package}', cell '{cell}': {message}"
LOGGER.error(full_message)
return context.environment.get_template(
os.path.join("cells", "cellerror.html")
).render(message=full_message)
[docs]
class Jinja2(Cell):
"""Generic class for cells that are barely more than a template.
When this class is used to render a cell, it renders template
``self.keyword``. When doing so, the template has access to the following
variables:
- `package`: the name of the package being processed, as a string;
- `pypi`: the information about this package got from pypi, as a dictionary
(for instance https://pypi.org/pypi/pypimonitor/json);
- `cell`: the cell options (as defined in the :ref:`yaml` configuration,
maybe completed with :ref:`default values <defaultrequired>`) as a
dictionary.
"""
def render(self, context, package, cell):
"""Return the HTML code corresponding to this cell."""
with temp_update(
dict(context),
{"package": package, "pypi": self.renderer[package], "cell": cell},
) as arguments:
return context.environment.get_template(self.template).render(**arguments)
@property
def template(self):
"""Return template path.
By default, this is ``cells/KEYWORD.html``. One can redefine this class
to provide alternative template path.
"""
return os.path.join("cells", f"{self.keyword}.html")