Skip to content

interface

Interface for plugin groups.

PluginGroupMeta

Bases: ABCMeta

Metaclass to initialize some things on creation.

Source code in src/metador_core/plugin/interface.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class PluginGroupMeta(ABCMeta):
    """Metaclass to initialize some things on creation."""

    def __init__(self, name, bases, dct):
        assert is_pluginlike(self, check_group=False)

        # attach generated subclass that auto-fills the group for plugin infos
        self.PluginRef: Type[AnyPluginRef] = AnyPluginRef._subclass_for(
            self.Plugin.name
        )

        if pgi_cls := self.Plugin.__dict__.get("plugin_info_class"):
            # attach group name to provided plugin info class
            pgi_cls.group = self.Plugin.name
        else:
            # derive generic plugin info class with the group defined
            class PGInfo(PluginBase):
                group = self.Plugin.name

            self.Plugin.plugin_info_class = PGInfo

        # sanity checks... this magic should not mess with the PluginBase
        assert self.Plugin.plugin_info_class is not PluginBase
        assert PluginBase.group == ""

PluginGroup

Bases: Generic[T]

All pluggable entities in metador are subclasses of this class.

The type parameter is the (parent) class of all loaded plugins.

They must implement the check method and be listed as plugin group. The name of their entrypoint defines the name of the plugin group.

Source code in src/metador_core/plugin/interface.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
class PluginGroup(Generic[T], metaclass=PluginGroupMeta):
    """All pluggable entities in metador are subclasses of this class.

    The type parameter is the (parent) class of all loaded plugins.

    They must implement the check method and be listed as plugin group.
    The name of their entrypoint defines the name of the plugin group.
    """

    PluginRef: TypeAlias = AnyPluginRef
    """Plugin reference class for this plugin group."""

    _PKG_META: ClassVar[Dict[str, PluginPkgMeta]] = pkg_meta
    """Package name -> package metadata."""

    class Plugin:
        """This is the plugin group plugin group, the first loaded group."""

        name = PG_GROUP_NAME
        version = (0, 1, 0)
        plugin_info_class = PGPlugin
        plugin_class: Type
        # plugin_class = PluginGroup  # can't set that -> check manually

    _ENTRY_POINTS: Dict[EPName, EntryPoint]
    """Dict of entry points of versioned plugins (not loaded)."""

    _VERSIONS: Dict[str, List[AnyPluginRef]]
    """Mapping from plugin name to pluginrefs of available versions."""

    _LOADED_PLUGINS: Dict[AnyPluginRef, Type[T]]
    """Dict from entry points to loaded plugins of that pluggable type."""

    def _add_ep(self, epname_str: str, ep_obj: EntryPoint):
        """Add an entrypoint loaded from importlib_metadata."""
        try:
            ep_name = EPName(epname_str)
        except TypeError:
            msg = f"{epname_str}: Invalid entrypoint name, must match {EP_NAME_REGEX}"
            raise ValueError(msg)
        if type(self) is not PluginGroup and not ep_name_has_namespace(ep_name):
            msg = f"{epname_str}: Plugin name has no qualifying namespace!"
            raise ValueError(msg)

        name, version = from_ep_name(ep_name)
        p_ref = AnyPluginRef(group=self.name, name=name, version=version)

        if ep_name in self._ENTRY_POINTS:
            self._LOADED_PLUGINS.pop(p_ref, None)  # unload, if loaded
            pkg = ep_obj.dist
            msg = f"WARNING: {ep_name} is probably provided by multiple packages!\n"
            msg += f"The plugin will now be provided by: {pkg.name} {pkg.version}"
            eprint(msg)
        self._ENTRY_POINTS[ep_name] = ep_obj

        if ep_name not in self._VERSIONS:
            self._VERSIONS[name] = []
        self._VERSIONS[name].append(p_ref)
        self._VERSIONS[name].sort()  # should be cheap

    def __init__(self, entrypoints: Dict[str, EntryPoint]):
        self._ENTRY_POINTS = {}
        self._VERSIONS = {}

        for k, v in entrypoints.items():
            self._add_ep(k, v)

        self._LOADED_PLUGINS = {}
        self.__post_init__()

    def __post_init__(self):
        if type(self) is PluginGroup:
            # make the magic plugingroup plugin add itself for consistency
            ep_name = util.to_ep_name(self.Plugin.name, self.Plugin.version)
            ep_path = f"{type(self).__module__}:{type(self).__name__}"
            ep = EntryPoint(ep_name, ep_path, self.name)
            self._add_ep(ep_name, ep)

            self_ref = AnyPluginRef(
                group=self.name, name=self.name, version=self.Plugin.version
            )
            self._LOADED_PLUGINS[self_ref] = self
            self.provider(self_ref).plugins[self.name].append(self_ref)

    @property
    def name(self) -> str:
        """Return name of the plugin group."""
        return self.Plugin.name

    @property
    def packages(self) -> Dict[str, PluginPkgMeta]:
        """Return metadata of all packages providing metador plugins."""
        return dict(self._PKG_META)

    def versions(
        self, p_name: str, version: Optional[SemVerTuple] = None
    ) -> List[AnyPluginRef]:
        """Return installed versions of a plugin (compatible with given version)."""
        refs = list(self._VERSIONS.get(p_name) or [])
        if version is None:
            return refs
        requested = self.PluginRef(name=p_name, version=version)
        return [ref for ref in refs if ref.supports(requested)]

    def resolve(
        self, p_name: str, version: Optional[SemVerTuple] = None
    ) -> Optional[AnyPluginRef]:
        """Return most recent compatible version of a plugin."""
        if refs := self.versions(p_name, version):
            return refs[-1]  # latest (compatible) version
        return None

    def provider(self, ref: AnyPluginRef) -> PluginPkgMeta:
        """Return package metadata of Python package providing this plugin."""
        if type(self) is PluginGroup and ref.name == PG_GROUP_NAME:
            # special case - the mother plugingroup plugin is not an EP,
            # so we cheat a bit (schema is in same package, but is an EP)
            return self.provider(self.resolve("schema"))

        ep_name = util.to_ep_name(ref.name, ref.version)
        ep = self._ENTRY_POINTS[ep_name]
        return self._PKG_META[cast(Any, ep).dist.name]

    def is_plugin(self, p_cls):
        """Return whether this class is a (possibly marked) installed plugin."""
        if not isinstance(p_cls, type) or not issubclass(
            p_cls, self.Plugin.plugin_class
        ):
            return False

        c = UndefVersion._unwrap(p_cls) or p_cls  # get real underlying class
        # check its exactly a registered plugin, if it has a Plugin section
        if info := c.__dict__.get("Plugin"):
            if not isinstance(info, PluginBase):
                return False
            loaded_p = self._get_unsafe(info.name, info.version)
            return loaded_p is c
        else:
            return False

    # ----

    def __repr__(self):
        return f"<PluginGroup '{self.name}' {list(self.keys())}>"

    def __str__(self):
        def pg_line(name_refs):
            name, refs = name_refs
            vs = list(map(lambda x: to_semver_str(x.version), refs))
            # p = self.provider(pg_ref.name)
            # pkg = f"{p.name} {semver_str(p.version)}"
            return f"\t'{name}' ({', '.join(vs)})"

        pgs = "\n".join(map(pg_line, self._VERSIONS.items()))
        return f"Available '{self.name}' plugins:\n{pgs}"

    # ----
    # dict-like interface will provide latest versions of plugins by default

    def __contains__(self, key) -> bool:
        name, version = plugin_args(key)
        if pg_versions := self._VERSIONS.get(name):
            if not version:
                return True
            pg = self.PluginRef(name=name, version=version)
            return pg in pg_versions
        return False

    def __getitem__(self, key) -> Type[T]:
        if key not in self:
            raise KeyError(f"{self.name} not found: {key}")
        return self.get(key)

    def keys(self) -> Iterator[AnyPluginRef]:
        """Return all names of all plugins."""
        for pgs in self._VERSIONS.values():
            yield from pgs

    def values(self) -> Iterator[Type[T]]:
        """Return latest versions of all plugins (THIS LOADS ALL PLUGINS!)."""
        return map(self.__getitem__, self.keys())

    def items(self) -> Iterator[Tuple[AnyPluginRef, Type[T]]]:
        """Return pairs of plugin name and latest installed version (THIS LOADS ALL PLUGINS!)."""
        return map(lambda k: (k, self[k]), self.keys())

    # ----

    def _get_unsafe(self, p_name: str, version: Optional[SemVerTuple] = None):
        """Return most recent compatible version of given plugin name, without safety rails.

        Raises KeyError if no (compatible) schema found.

        For internal use only!
        """
        if ref := self.resolve(p_name, version):
            self._ensure_is_loaded(ref)
            return self._LOADED_PLUGINS[ref]
        else:  # error
            msg = f"{p_name}"
            if version:
                msg += f": no installed version is compatible with {version}"
            raise KeyError(msg)

    # inspired by this nice trick: https://stackoverflow.com/a/60362860
    PRX = TypeVar("PRX", bound="Type[T]")  # type: ignore

    @overload
    def get(self, key: str, version: Optional[SemVerTuple] = None) -> Optional[Type[T]]:
        ...  # pragma: no cover

    @overload
    def get(self, key: PRX, version: Optional[SemVerTuple] = None) -> Optional[PRX]:
        ...  # pragma: no cover

    def get(
        self, key: Union[str, PRX], version: Optional[SemVerTuple] = None
    ) -> Union[Type[T], PRX, None]:
        key_, version = plugin_args(key, version)

        # retrieve compatible plugin
        try:
            ret = self._get_unsafe(key_, version)
        except KeyError:
            return None

        if version is None:
            # no version constraint was passed or inferred -> mark it
            ret = UndefVersion._mark_class(ret)

        if isinstance(key, str):
            return cast(Type[T], ret)
        else:
            return ret

    # ----

    def _ensure_is_loaded(self, ref: AnyPluginRef):
        """Load plugin from entrypoint, if it is not loaded yet."""
        assert ref.group == self.name
        if ref in self._LOADED_PLUGINS:
            return  # already loaded, all good

        ep_name = util.to_ep_name(ref.name, ref.version)
        ret = self._ENTRY_POINTS[ep_name].load()
        self._LOADED_PLUGINS[ref] = ret

        self._load_plugin(ep_name, ret)

    def _explicit_plugin_deps(self, plugin) -> Set[AnyPluginRef]:
        """Return all plugin dependencies that must be taken into account."""
        def_deps = set(plugin.Plugin.requires)
        extra_deps = set(self.plugin_deps(plugin) or set())
        return def_deps.union(extra_deps)

    def plugin_deps(self, plugin) -> Set[AnyPluginRef]:
        """Return additional automatically inferred dependencies for a plugin."""

    def _load_plugin(self, ep_name: EPName, plugin):
        """Run checks and finalize loaded plugin."""
        from ..plugins import plugingroups

        # run inner Plugin class checks (with possibly new Fields cls)
        if not plugin.__dict__.get("Plugin"):
            raise TypeError(f"{ep_name}: {plugin} is missing Plugin inner class!")
        # pass ep_name to check that it agrees with the plugin info
        plugin.Plugin = self.Plugin.plugin_info_class.parse_info(
            plugin.Plugin, ep_name=ep_name
        )

        # do general checks first, if they fail no need to continue
        self._check_common(ep_name, plugin)
        self.check_plugin(ep_name, plugin)

        for dep_ref in self._explicit_plugin_deps(plugin):
            dep_grp = plugingroups[dep_ref.group]
            dep_grp._ensure_is_loaded(dep_ref)

        self.init_plugin(plugin)

    def _check_common(self, ep_name: EPName, plugin):
        """Perform both the common and specific checks a registered plugin.

        Raises a TypeError with message in case of failure.
        """
        # check correct base class of plugin, if stated
        if self.Plugin.plugin_class:
            util.check_is_subclass(ep_name, plugin, self.Plugin.plugin_class)

    def check_plugin(self, ep_name: EPName, plugin: Type[T]):
        """Perform plugin group specific checks on a registered plugin.

        Raises a TypeError with message in case of failure.

        To be overridden in subclasses for plugin group specific checks.

        Args:
            ep_name: Declared entrypoint name.
            plugin: Object the entrypoint is pointing to.
        """
        # NOTE: following cannot happen as long as we enforce
        # overriding check_plugin.
        # keep that here for now, in case we loosen this
        # if type(self) is not PluginGroup:
        #    return  # is not the "plugingroup" group itself

        # these are the checks done for other plugin group plugins:
        util.check_is_subclass(ep_name, plugin, PluginGroup)
        util.check_is_subclass(ep_name, self.Plugin.plugin_info_class, PluginBase)
        if plugin != PluginGroup:  # exclude itself. this IS its check_plugin
            util.check_implements_method(ep_name, plugin, PluginGroup.check_plugin)

        # NOTE: following cannot happen as long as we set the group
        # automatically using the metaclass.
        # keep that in case we decide to change that / get rid of the metaclass
        # ---
        # make sure that the declared plugin_info_class for the group sets 'group'
        # and it is also equal to the plugin group 'name'.
        # this is the safest way to make sure that Plugin.ref() works correctly.
        # ppgi_cls = plugin.Plugin.plugin_info_class
        # if not ppgi_cls.group:
        #    raise TypeError(f"{ep_name}: {ppgi_cls} is missing 'group' attribute!")
        # if not ppgi_cls.group == plugin.Plugin.name:
        #    msg = f"{ep_name}: {ppgi_cls.__name__}.group != {plugin.__name__}.Plugin.name!"
        #    raise TypeError(msg)

    def init_plugin(self, plugin: Type[T]):
        """Override this to do something after the plugin has been checked."""
        if type(self) is not PluginGroup:
            return  # is not the "plugingroup" group itself
        create_pg(plugin)  # create plugin group if it does not exist

PluginRef class-attribute instance-attribute

PluginRef: TypeAlias = AnyPluginRef

Plugin reference class for this plugin group.

name property

name: str

Return name of the plugin group.

packages property

packages: Dict[str, PluginPkgMeta]

Return metadata of all packages providing metador plugins.

Plugin

This is the plugin group plugin group, the first loaded group.

Source code in src/metador_core/plugin/interface.py
 96
 97
 98
 99
100
101
102
class Plugin:
    """This is the plugin group plugin group, the first loaded group."""

    name = PG_GROUP_NAME
    version = (0, 1, 0)
    plugin_info_class = PGPlugin
    plugin_class: Type

versions

versions(
    p_name: str, version: Optional[SemVerTuple] = None
) -> List[AnyPluginRef]

Return installed versions of a plugin (compatible with given version).

Source code in src/metador_core/plugin/interface.py
175
176
177
178
179
180
181
182
183
def versions(
    self, p_name: str, version: Optional[SemVerTuple] = None
) -> List[AnyPluginRef]:
    """Return installed versions of a plugin (compatible with given version)."""
    refs = list(self._VERSIONS.get(p_name) or [])
    if version is None:
        return refs
    requested = self.PluginRef(name=p_name, version=version)
    return [ref for ref in refs if ref.supports(requested)]

resolve

resolve(
    p_name: str, version: Optional[SemVerTuple] = None
) -> Optional[AnyPluginRef]

Return most recent compatible version of a plugin.

Source code in src/metador_core/plugin/interface.py
185
186
187
188
189
190
191
def resolve(
    self, p_name: str, version: Optional[SemVerTuple] = None
) -> Optional[AnyPluginRef]:
    """Return most recent compatible version of a plugin."""
    if refs := self.versions(p_name, version):
        return refs[-1]  # latest (compatible) version
    return None

provider

provider(ref: AnyPluginRef) -> PluginPkgMeta

Return package metadata of Python package providing this plugin.

Source code in src/metador_core/plugin/interface.py
193
194
195
196
197
198
199
200
201
202
def provider(self, ref: AnyPluginRef) -> PluginPkgMeta:
    """Return package metadata of Python package providing this plugin."""
    if type(self) is PluginGroup and ref.name == PG_GROUP_NAME:
        # special case - the mother plugingroup plugin is not an EP,
        # so we cheat a bit (schema is in same package, but is an EP)
        return self.provider(self.resolve("schema"))

    ep_name = util.to_ep_name(ref.name, ref.version)
    ep = self._ENTRY_POINTS[ep_name]
    return self._PKG_META[cast(Any, ep).dist.name]

is_plugin

is_plugin(p_cls)

Return whether this class is a (possibly marked) installed plugin.

Source code in src/metador_core/plugin/interface.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def is_plugin(self, p_cls):
    """Return whether this class is a (possibly marked) installed plugin."""
    if not isinstance(p_cls, type) or not issubclass(
        p_cls, self.Plugin.plugin_class
    ):
        return False

    c = UndefVersion._unwrap(p_cls) or p_cls  # get real underlying class
    # check its exactly a registered plugin, if it has a Plugin section
    if info := c.__dict__.get("Plugin"):
        if not isinstance(info, PluginBase):
            return False
        loaded_p = self._get_unsafe(info.name, info.version)
        return loaded_p is c
    else:
        return False

keys

keys() -> Iterator[AnyPluginRef]

Return all names of all plugins.

Source code in src/metador_core/plugin/interface.py
254
255
256
257
def keys(self) -> Iterator[AnyPluginRef]:
    """Return all names of all plugins."""
    for pgs in self._VERSIONS.values():
        yield from pgs

values

values() -> Iterator[Type[T]]

Return latest versions of all plugins (THIS LOADS ALL PLUGINS!).

Source code in src/metador_core/plugin/interface.py
259
260
261
def values(self) -> Iterator[Type[T]]:
    """Return latest versions of all plugins (THIS LOADS ALL PLUGINS!)."""
    return map(self.__getitem__, self.keys())

items

items() -> Iterator[Tuple[AnyPluginRef, Type[T]]]

Return pairs of plugin name and latest installed version (THIS LOADS ALL PLUGINS!).

Source code in src/metador_core/plugin/interface.py
263
264
265
def items(self) -> Iterator[Tuple[AnyPluginRef, Type[T]]]:
    """Return pairs of plugin name and latest installed version (THIS LOADS ALL PLUGINS!)."""
    return map(lambda k: (k, self[k]), self.keys())

plugin_deps

plugin_deps(plugin) -> Set[AnyPluginRef]

Return additional automatically inferred dependencies for a plugin.

Source code in src/metador_core/plugin/interface.py
336
337
def plugin_deps(self, plugin) -> Set[AnyPluginRef]:
    """Return additional automatically inferred dependencies for a plugin."""

check_plugin

check_plugin(ep_name: EPName, plugin: Type[T])

Perform plugin group specific checks on a registered plugin.

Raises a TypeError with message in case of failure.

To be overridden in subclasses for plugin group specific checks.

Parameters:

Name Type Description Default
ep_name EPName

Declared entrypoint name.

required
plugin Type[T]

Object the entrypoint is pointing to.

required
Source code in src/metador_core/plugin/interface.py
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
def check_plugin(self, ep_name: EPName, plugin: Type[T]):
    """Perform plugin group specific checks on a registered plugin.

    Raises a TypeError with message in case of failure.

    To be overridden in subclasses for plugin group specific checks.

    Args:
        ep_name: Declared entrypoint name.
        plugin: Object the entrypoint is pointing to.
    """
    # NOTE: following cannot happen as long as we enforce
    # overriding check_plugin.
    # keep that here for now, in case we loosen this
    # if type(self) is not PluginGroup:
    #    return  # is not the "plugingroup" group itself

    # these are the checks done for other plugin group plugins:
    util.check_is_subclass(ep_name, plugin, PluginGroup)
    util.check_is_subclass(ep_name, self.Plugin.plugin_info_class, PluginBase)
    if plugin != PluginGroup:  # exclude itself. this IS its check_plugin
        util.check_implements_method(ep_name, plugin, PluginGroup.check_plugin)

init_plugin

init_plugin(plugin: Type[T])

Override this to do something after the plugin has been checked.

Source code in src/metador_core/plugin/interface.py
407
408
409
410
411
def init_plugin(self, plugin: Type[T]):
    """Override this to do something after the plugin has been checked."""
    if type(self) is not PluginGroup:
        return  # is not the "plugingroup" group itself
    create_pg(plugin)  # create plugin group if it does not exist

create_pg

create_pg(pg_cls)

Create plugin group instance if it does not exist.

Source code in src/metador_core/plugin/interface.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
def create_pg(pg_cls):
    """Create plugin group instance if it does not exist."""
    pg_ref = AnyPluginRef(
        group=PG_GROUP_NAME, name=pg_cls.Plugin.name, version=pg_cls.Plugin.version
    )
    if pg_ref in _plugin_groups:
        return _plugin_groups[pg_ref]

    if not isinstance(pg_cls.Plugin, PluginBase):
        # magic - substitute Plugin class with parsed plugin object
        pg_cls.Plugin = PGPlugin.parse_info(pg_cls.Plugin)

    # TODO: currently we cannot distinguish entrypoints
    # for different versions of the plugin group.
    # should not be problematic for now,
    # as the groups should not change much
    pg = pg_cls(get_group(pg_ref.name))
    _plugin_groups[pg_ref] = pg