Skip to content

Commit

Permalink
Merge pull request #7976 from ckan/remove-pyutillib
Browse files Browse the repository at this point in the history
[proposal] Implement simplified version of pyutilib
  • Loading branch information
amercader committed May 14, 2024
2 parents ff9afc5 + 2d7733a commit 15f32e5
Show file tree
Hide file tree
Showing 17 changed files with 377 additions and 264 deletions.
12 changes: 12 additions & 0 deletions changes/7976.removal
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- PyUtilib dependency removed. All the primitives for the plugin system are now defined in CKAN.
- The deprecated methods with the form ``after_<action>`` and ``before_<action>`` of the :py:class:`~ckan.plugins.interfaces.IPackageController` and :py:class:`~ckan.plugins.interfaces.IResourceController` interfaces have been removed. The form ``after_<type>_<action>`` must be used from now on. E.g. ``after_create()`` -> ``after_dataset_create()`` or ``after_resource_create()``.
- It is now possible to extend interface classes directly when implementing plugins, which provides better integration with development tools, e.g.::

class Plugin(p.SingletonPlugin, IClick):
pass

This is equivalent to::

class Plugin(p.SingletonPlugin):
p.implements(p.IClick, inherit=True)

3 changes: 1 addition & 2 deletions ckan/lib/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,7 @@ def set_default_group_plugin() -> None:
global _group_controllers
# Setup the fallback behaviour if one hasn't been defined.
if _default_group_plugin is None:
_default_group_plugin = cast(
plugins.IDatasetForm, DefaultGroupForm())
_default_group_plugin = cast(plugins.IGroupForm, DefaultGroupForm())
if _default_organization_plugin is None:
_default_organization_plugin = cast(
plugins.IGroupForm, DefaultOrganizationForm())
Expand Down
2 changes: 1 addition & 1 deletion ckan/logic/action/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ def _group_or_org_create(context: Context,
group_type = data_dict.get('type', 'organization' if is_org else 'group')
group_plugin = lib_plugins.lookup_group_plugin(group_type)
try:
schema: Schema = group_plugin.form_to_db_schema_options({
schema: Schema = getattr(group_plugin, "form_to_db_schema_options")({
'type': 'create', 'api': 'api_version' in context,
'context': context})
except AttributeError:
Expand Down
2 changes: 1 addition & 1 deletion ckan/logic/action/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,7 @@ def _group_or_org_show(

group_plugin = lib_plugins.lookup_group_plugin(group_dict['type'])
try:
schema: Schema = group_plugin.db_to_form_schema_options({
schema: Schema = getattr(group_plugin, "db_to_form_schema_options")({
'type': 'show',
'api': 'api_version' in context,
'context': context})
Expand Down
8 changes: 5 additions & 3 deletions ckan/logic/action/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,9 +663,11 @@ def _group_or_org_update(
# get the schema
group_plugin = lib_plugins.lookup_group_plugin(group.type)
try:
schema = group_plugin.form_to_db_schema_options({'type': 'update',
'api': 'api_version' in context,
'context': context})
schema = getattr(group_plugin, "form_to_db_schema_options")({
'type': 'update',
'api': 'api_version' in context,
'context': context
})
except AttributeError:
schema = group_plugin.form_to_db_schema()

Expand Down
4 changes: 2 additions & 2 deletions ckan/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# encoding: utf-8

from ckan.plugins.core import * # noqa
from ckan.plugins.interfaces import * # noqa
from ckan.plugins.interfaces import * # noqa: F401, F403
from ckan.plugins.core import * # noqa: F401, F403


def __getattr__(name: str):
Expand Down
198 changes: 198 additions & 0 deletions ckan/plugins/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"""Base code units used by plugin system.
This module contains adapted and simplified version of pyutilib plugin system
that was used historically by CKAN.
"""

from __future__ import annotations

import sys

from typing import Any
from typing_extensions import ClassVar, TypeVar

TSingleton = TypeVar("TSingleton", bound="SingletonPlugin")


class PluginException(Exception):
"""Exception base class for plugin errors."""


class ExistingInterfaceException(PluginException):
"""Interface with the same name already exists."""

def __init__(self, name: str):
self.name = name

def __str__(self):
return f"Interface {self.name} has already been defined"


class PluginNotFoundException(PluginException):
"""Requested plugin cannot be found."""

def __init__(self, name: str):
self.name = name

def __str__(self):
return f"Interface {self.name} does not exist"


class Interface:
"""Base class for custom interfaces.
Marker base class for extension point interfaces. This class is not
intended to be instantiated. Instead, the declaration of subclasses of
Interface are recorded, and these classes are used to define extension
points.
Example:
>>> class IExample(Interface):
>>> def example_method(self):
>>> pass
"""

# force PluginImplementations to iterate over interface in reverse order
_reverse_iteration_order: ClassVar[bool] = False

# collection of interface-classes extending base Interface. This is used to
# guarantee unique names of interfaces.
_interfaces: ClassVar[set[type[Interface]]] = set()

# there is no practical use of `name` attribute in interface, because
# interfaces are never instantiated. But declaring this attribute
# simplifies typing when iterating over interface implementations.
name: str

def __init_subclass__(cls, **kwargs: Any):
"""Prevent interface name duplication when interfaces are created."""

# `implements(..., inherit=True)` adds interface to the list of
# plugin's bases. There is no reason to disallow identical plugin-class
# names, so this scenario stops execution early.
if isinstance(cls, Plugin):
return

if cls in Interface._interfaces:
raise ExistingInterfaceException(cls.__name__)

Interface._interfaces.add(cls)

@classmethod
def provided_by(cls, instance: Plugin) -> bool:
"""Check that the object is an instance of the class that implements
the interface.
Example:
>>> activity = get_plugin("activity")
>>> assert IConfigurer.provided_by(activity)
"""
return cls.implemented_by(type(instance))

@classmethod
def implemented_by(cls, other: type[Plugin]) -> bool:
"""Check whether the class implements the current interface.
Example:
>>> assert IConfigurer.implemented_by(ActivityPlugin)
"""
try:
return issubclass(other, cls) or cls in other._implements
except AttributeError:
return False


class PluginMeta(type):
"""Metaclass for plugins that initializes supplementary attributes required
by interface implementations.
"""

def __new__(cls, name: str, bases: tuple[type, ...], data: dict[str, Any]):
data.setdefault("_implements", set())
data.setdefault("_inherited_interfaces", set())

# add all interfaces with `inherit=True` to the bases of plugin
# class. It adds default implementation of methods from interface to
# the plugin's class
bases += tuple(data["_inherited_interfaces"] - set(bases))

# copy interfaces implemented by the parent classes into a new one to
# correctly identify if interface is provided_by/implemented_by the new
# class.
for base in bases:
data["_implements"].update(getattr(base, "_implements", set()))

return super().__new__(cls, name, tuple(bases), data)


class Plugin(metaclass=PluginMeta):
"""Base class for plugins which require multiple instances.
Unless you need multiple instances of your plugin object you should
probably use SingletonPlugin.
"""

# collection of all interfaces implemented by the plugin. Used by
# `Interface.implemented_by` check
_implements: ClassVar[set[type[Interface]]]

# collection of interfaces implemented with `inherit=True`. These
# interfaces are added as parent classes to the plugin
_inherited_interfaces: ClassVar[set[type[Interface]]]

# name of the plugin instance. All known plugins are instances of
# `SingletonPlugin`, so it may be converted to ClassVar in future. Right
# now it's kept as instance variable for compatibility with original
# implementation from pyutilib
name: str

def __init__(self, *args: Any, **kwargs: Any):
name = kwargs.pop("name", None)
if not name:
name = self.__class__.__name__
self.name = name

def __str__(self):
return f"<Plugin {self.name}>"


class SingletonPlugin(Plugin):
"""Base class for plugins which are singletons (ie most of them)
One singleton instance of this class will be created when the plugin is
loaded. Subsequent calls to the class constructor will always return the
same singleton instance.
"""

def __new__(cls, *args: Any, **kwargs: Any):
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)

return cls._instance


def implements(interface: type[Interface], inherit: bool = False):
"""Can be used in the class definition of `Plugin` subclasses to
declare the extension points that are implemented by this
interface class.
Example:
>>> class MyPlugin(Plugin):
>>> implements(IConfigurer, inherit=True)
If compatibility with CKAN pre-v2.11 is not required, plugin class should
extend interface class.
Example:
>>> class MyPlugin(Plugin, IConfigurer):
>>> pass
"""
frame = sys._getframe(1)
locals_ = frame.f_locals
locals_.setdefault("_implements", set()).add(interface)
if inherit:
locals_.setdefault("_inherited_interfaces", set()).add(interface)

0 comments on commit 15f32e5

Please sign in to comment.