Skip to content

metaclass

This module defines a metaclass that should be used for all plugin types.

MarkerMixin

Base class for Metador-internal marker mixins.

It can be used to hand out copies of classes with markings, without actually modifying the original class.

Source code in src/metador_core/plugin/metaclass.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class MarkerMixin:
    """Base class for Metador-internal marker mixins.

    It can be used to hand out copies of classes with markings,
    without actually modifying the original class.
    """

    @classmethod
    def _fieldname(cls):
        return f"__{cls.__name__}_unwrapped__"

    @classmethod
    def _is_marked(cls, c) -> bool:
        """Return whether `c` is proper subclass of this marker."""
        return c is not cls and issubclass(c, cls)

    @classmethod
    def _mark_class(cls, c):
        """Mark a class with this marker mixin."""
        if cls._is_marked(c):
            raise TypeError(f"{c} already marked by {cls}!")

        ret = c.__class__(c.__name__, (cls, c), {})
        # ret.__module__ = c.__module__
        setattr(ret, cls._fieldname(), c)

        # NOTE: discouraged!
        # https://docs.python.org/3/howto/annotations.html#annotations-howto
        # ----
        # anns = getattr(ret, "__annotations__", {})
        # anns[unw_field] = ClassVar[Type]
        # ret.__annotations__ = anns

        return ret

    @classmethod
    def _unwrap(cls, c):
        """Return the original class, or None if given argument is not marked."""
        if issubclass(c, cls):
            return getattr(c, cls._fieldname())
        else:
            return None

UndefVersion

Bases: MarkerMixin

Marker for a plugin class retrieved with no specified version.

We have to do this crazy thing, because wrapt.ObjectProxy-wrapped classes can be transparently derived, and what is even worse, the derived class is not wrapped anymore.

The mixin subclass approach therefore makes more sense here, as the metaclass then can check for its presence.

Source code in src/metador_core/plugin/metaclass.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class UndefVersion(MarkerMixin):
    """Marker for a plugin class retrieved with no specified version.

    We have to do this crazy thing, because wrapt.ObjectProxy-wrapped
    classes can be transparently derived, and what is even worse,
    the derived class is not wrapped anymore.

    The mixin subclass approach therefore makes more sense here, as
    the metaclass then can check for its presence.
    """

    @classmethod
    def _mark_class(cls, c):
        # NOTE: we also want to mark nested non-plugins to prevent subclassing
        # so we do not assume that cls.Plugin is defined
        ret = super()._mark_class(c)
        # make sure that the Plugin section *is* actually inherited,
        # (normally this is prevented by the plugin metaclass)
        # that way we can use the marked class as if it was the real one
        if not ret.__dict__.get("Plugin"):
            ret.Plugin = c.Plugin

        return ret

PluginMetaclassMixin

Bases: type

Metaclass mixin to be used with plugins of any group.

It provides an is_plugin property to classes to quickly check if they seem to be valid registered plugins.

It ensures that: * the Plugin inner class is not automatically inherited * registered Plugin classes cannot be subclassed if loaded without a fixed version

For the second part, this works together with the PluginGroup implementation, which makes sure that schemas requested without versions are not actually handed out, but instead users get a subclass with the UndefVersion mixin we can detect here.

Source code in src/metador_core/plugin/metaclass.py
 75
 76
 77
 78
 79
 80
 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
class PluginMetaclassMixin(type):
    """Metaclass mixin to be used with plugins of any group.

    It provides an is_plugin property to classes to quickly check if they
    seem to be valid registered plugins.

    It ensures that:
    * the `Plugin` inner class is not automatically inherited
    * registered Plugin classes cannot be subclassed if loaded without a fixed version

    For the second part, this works together with the PluginGroup implementation,
    which makes sure that schemas requested without versions are not actually handed out,
    but instead users get a subclass with the `UndefVersion` mixin we can detect here.
    """

    def __repr__(self):
        # add plugin name and version to default class repr
        if c := UndefVersion._unwrap(self):
            # indicate the attached UndefVersion
            return f"{repr(c)} (version unspecified)"
        else:
            # indicate loaded plugin name and version
            pg_str = ""
            if pgi := self.__dict__.get("Plugin"):
                pgi = self.Plugin
                pg_str = f" ({pgi.name} {to_semver_str(pgi.version)})"

            return f"{super().__repr__()}{pg_str}"

    def __new__(cls, name, bases, dct):
        # prevent inheriting from a plugin accessed without stated version
        for b in bases:
            if UndefVersion._is_marked(b):
                if pgi := b.__dict__.get("Plugin"):
                    ref = f"plugin '{pgi.name}'"
                else:
                    ref = f"{UndefVersion._unwrap(b)} originating from a plugin"
                msg = f"{name}: Cannot inherit from {ref} of unspecified version!"
                raise TypeError(msg)

        # prevent inheriting inner Plugin class by setting it to None
        if "Plugin" not in dct:
            dct["Plugin"] = None

        # hide special marker base class from parent metaclass (if present)
        # so it does not have to know about any of this happening
        # (otherwise it could interfere with other checks)
        # NOTE: needed e.g. for schemas to work properly
        filt_bases = tuple(b for b in bases if b is not UndefVersion)
        ret = super().__new__(cls, name, filt_bases, dct)

        # add marker back, as if it was present all along
        if len(filt_bases) < len(bases):
            ret.__bases__ = (UndefVersion, *ret.__bases__)

        return ret