Source code for plone.api.addon
"""API to handle add-on management."""
from dataclasses import dataclass
from functools import lru_cache
from plone.api import portal
from plone.api.exc import InvalidParameterError
from plone.api.validation import required_parameters
from Products.CMFPlone.controlpanel.browser.quickinstaller import InstallerView
from Products.CMFPlone.interfaces import INonInstallable
from Products.CMFPlone.utils import get_installer
from Products.GenericSetup import EXTENSION
from typing import Dict
from typing import List
from typing import Tuple
from zope.component import getAllUtilitiesRegisteredFor
from zope.globalrequest import getRequest
import logging
import pkg_resources
logger = logging.getLogger("plone.api.addon")
__all__ = [
"AddonInformation",
"NonInstallableAddons",
"get_addons",
"get_addon_ids",
"get_version",
"get",
"install",
"uninstall",
]
[docs]
@dataclass
class NonInstallableAddons:
"""Set of add-ons not available for installation."""
profiles: List[str]
products: List[str]
def _get_installer() -> InstallerView:
"""Return the InstallerView."""
portal_obj = portal.get()
return get_installer(portal_obj, getRequest())
@lru_cache(maxsize=1)
def _get_non_installable_addons() -> NonInstallableAddons:
"""Return information about non installable add-ons.
We cache this on first use, as those utilities are registered
during the application startup
:returns: NonInstallableAddons instance.
"""
ignore_profiles = []
ignore_products = []
utils = getAllUtilitiesRegisteredFor(INonInstallable)
for util in utils:
ni_profiles = getattr(util, "getNonInstallableProfiles", None)
if ni_profiles is not None:
ignore_profiles.extend(ni_profiles())
ni_products = getattr(util, "getNonInstallableProducts", None)
if ni_products is not None:
ignore_products.extend(ni_products())
return NonInstallableAddons(
profiles=ignore_profiles,
products=ignore_products,
)
@lru_cache(maxsize=1)
def _cached_addons() -> Tuple[Tuple[str, AddonInformation]]:
"""Return information about add-ons in this installation.
:returns: Tuple of tuples with add-on id and AddonInformation.
:rtype: Tuple
"""
installer = _get_installer()
setup_tool = installer.ps
addons = {}
non_installable = _get_non_installable_addons()
# Known profiles:
profiles = setup_tool.listProfileInfo()
for profile in profiles:
if profile["type"] != EXTENSION:
continue
pid = profile["id"]
if pid in non_installable.profiles:
continue
pid_parts = pid.split(":")
if len(pid_parts) != 2:
logger.error(f"Profile with id '{pid}' is invalid.")
# Which package (product) is this from?
product_id = profile["product"]
flags = []
is_broken = not installer.is_product_installable(product_id, allow_hidden=True)
is_non_installable = product_id in non_installable.products
valid = not (is_broken or is_non_installable)
if is_broken:
flags.append("broken")
if is_non_installable:
flags.append("non_installable")
profile_type = pid_parts[-1]
if product_id not in addons:
# get some basic information on the product
product = {
"id": product_id,
"version": get_version(product_id),
"title": product_id,
"description": "",
"upgrade_profiles": {},
"other_profiles": [],
"install_profile": {},
"uninstall_profile": {},
"upgrade_info": {},
"profile_type": profile_type,
"valid": valid,
"flags": flags,
}
install_profile = installer.get_install_profile(product_id)
if install_profile is not None:
product["title"] = install_profile["title"]
product["description"] = install_profile["description"]
product["install_profile"] = install_profile
product["profile_type"] = "default"
uninstall_profile = installer.get_uninstall_profile(product_id)
if uninstall_profile is not None:
product["uninstall_profile"] = uninstall_profile
# Do not override profile_type.
if not product["profile_type"]:
product["profile_type"] = "uninstall"
if "version" in profile:
product["upgrade_profiles"][profile["version"]] = profile
else:
product["other_profiles"].append(profile)
addons[product_id] = AddonInformation(**product)
return tuple(addons.items())
def _update_addon_info(
addon: AddonInformation, installer: InstallerView
) -> AddonInformation:
"""Update information about an add-on.
:param addon: [required] Add-on object to be updated
:type addon: AddonInformation object
:param installer: InstallerView object to check for add-on info
:type installer: InstallerView object
:returns: Updated AddonInformation object
:rtype: AddonInformation object
"""
addon_id = addon.id
if addon.valid:
flags = []
# Update only what could be changed
is_installed = installer.is_product_installed(addon_id)
if is_installed:
addon.upgrade_info = installer.upgrade_info(addon_id) or {}
if addon.upgrade_info.get("available"):
flags.append("upgradable")
else:
flags.append("installed")
else:
flags.append("available")
addon.flags = flags
return addon
def _get_addons() -> List[AddonInformation]:
"""Return an updated list of add-on information.
:returns: List of AddonInformation.
:rtype: List
"""
installer = _get_installer()
addons = dict(_cached_addons())
result = []
for addon in addons.values():
result.append(_update_addon_info(addon, installer))
return result
[docs]
def get_addons(limit: str = "") -> List[AddonInformation]:
"""List add-ons in this Plone site.
:param limit: Limit list of add-ons.
'installed': only products that are installed and not hidden
'upgradable': only products with upgrades
'available': products that are not installed but could be
'non_installable': Non installable products
'broken': uninstallable products with broken dependencies
:type limit: string
:returns: List of AddonInformation.
:raises:
InvalidParameterError
:Example: :ref:`addons-get-addons`
"""
addons = _get_addons()
if limit in ("non_installable", "broken"):
return [addon for addon in addons if limit in addon.flags]
addons = [addon for addon in addons if addon.valid]
if limit in ("installed", "upgradable", "available"):
addons = [addon for addon in addons if limit in addon.flags]
elif limit != "":
raise InvalidParameterError(f"Parameter limit='{limit}' is not valid.")
return addons
[docs]
def get_addon_ids(limit: str = "") -> List[str]:
"""List add-ons ids in this Plone site.
:param limit: Limit list of add-ons.
'installed': only products that are installed and not hidden
'upgradable': only products with upgrades
'available': products that are not installed but could be
'non_installable': Non installable products
'broken': uninstallable products with broken dependencies
:type limit: string
:returns: List of add-on ids.
"""
addons = get_addons(limit=limit)
return [addon.id for addon in addons]
[docs]
@required_parameters("addon")
def get_version(addon: str) -> str:
"""Return the version of the product (package)."""
try:
dist = pkg_resources.get_distribution(addon)
return dist.version
except pkg_resources.DistributionNotFound:
if "." in addon:
return ""
return get_version(f"Products.{addon}")
[docs]
@required_parameters("addon")
def get(addon: str) -> AddonInformation:
"""Information about an Add-on.
:param addon: ID of the add-on to be retrieved.
:returns: Add-on information.
:rtype: string
"""
addons = dict(_cached_addons())
if addon not in addons:
raise InvalidParameterError(f"No add-on {addon} found.")
return _update_addon_info(addons.get(addon), _get_installer())
[docs]
@required_parameters("addon")
def install(addon: str) -> bool:
"""Install an add-on.
:param addon: ID of the add-on to be installed.
:returns: Status of the installation.
"""
installer = _get_installer()
return installer.install_product(addon)
[docs]
@required_parameters("addon")
def uninstall(addon: str) -> bool:
"""Uninstall an add-on.
:param addon: ID of the add-on to be uninstalled.
:returns: Status of the uninstallation.
:rtype: Boolean value representing the status of the uninstallation.
"""
installer = _get_installer()
return installer.uninstall_product(addon)