Skip to content

oversampleqa.plugin_system

oversampleqa.plugin_system

Simple plugin management for metrics and validators.

PluginManager

Runtime registry for pluggable components.

Source code in src/oversampleqa/plugin_system.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 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
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
class PluginManager:
    """Runtime registry for pluggable components."""

    def __init__(self) -> None:
        self._metric_plugins: dict[str, type[DistanceMetricProtocol]] = {}
        self._validator_plugins: dict[str, type[ValidatorProtocol]] = {}

    def register_metric(
        self,
        name: str,
        metric_cls: type[DistanceMetricProtocol],
        *,
        domain: MetricDomain = "real",
        check_axioms: bool = True,
        metric_kwargs: dict[str, Any] | None = None,
    ) -> None:
        """Register a metric class by name, after checking it behaves like one.

        Registration used to be a bare dictionary assignment: any object could
        be registered under any name, silently replacing a built-in, and a
        metric that violated the axioms would be discovered only by whoever
        eventually distrusted its numbers.

        Three checks now run, each raising :class:`~oversampleqa.PluginError`:

        1. **Name collision.** A name already taken by a built-in or another
           plugin is refused rather than overwritten.
        2. **Signature.** The callable must accept two positional arguments.
        3. **Axioms.** ``d(x, x) == 0``, ``d(x, y) > 0`` for distinct points,
           symmetry, non-negativity and finiteness on random input.

        The third is not hypothetical. The built-in ``hassanat`` shipped for
        this project's entire history scoring ``[-5]`` and ``[5]`` as distance
        zero, because it compared absolute values -- it was not a metric, and
        nothing checked. This is that check.

        Args:
            name: Metric identifier.
            metric_cls: Metric callable or class implementing ``__call__``.
            domain: Input the metric is defined on. See
                :data:`~oversampleqa.plugin_contract.METRIC_DOMAINS`.
            check_axioms: Run the axiom smoke check. Disable only for a metric
                you have verified another way, and say why.
            metric_kwargs: Extra arguments the metric needs, such as
                ``cov_inv`` for Mahalanobis.

        Raises:
            PluginError: On collision, bad signature, or axiom failure.
        """
        from .distance import _METRICS
        from .exceptions import PluginError
        from .plugin_contract import check_metric_axioms, validate_metric_signature

        if name in _METRICS:
            raise PluginError(
                f"metric {name!r} is already a built-in. Registering over it "
                "would silently change results for every caller using that "
                "name; choose a different one."
            )
        if name in self._metric_plugins:
            raise PluginError(
                f"metric {name!r} is already registered by another plugin. "
                "Unregister it first if replacement is intended."
            )

        candidate = metric_cls() if isinstance(metric_cls, type) else metric_cls
        validate_metric_signature(candidate, name)

        if check_axioms:
            report = check_metric_axioms(
                candidate, name, domain=domain, **(metric_kwargs or {})
            )
            if not report.ok:
                detail = "\n  ".join(report.failures)
                raise PluginError(
                    f"metric {name!r} does not satisfy the distance axioms:\n"
                    f"  {detail}\n"
                    "A metric that violates these produces numbers that look "
                    "plausible and mean nothing."
                )

        self._metric_plugins[name] = metric_cls

    def unregister_metric(self, name: str) -> None:
        """Remove a registered metric plugin.

        Args:
            name: Metric identifier.

        Raises:
            PluginError: If no plugin is registered under that name.
        """
        from .exceptions import PluginError

        if name not in self._metric_plugins:
            raise PluginError(f"no metric plugin registered as {name!r}")
        del self._metric_plugins[name]

    def register_validator(
        self, name: str, validator_cls: type[ValidatorProtocol]
    ) -> None:
        """Register a validator class by name, refusing collisions.

        This used to be a bare dictionary assignment, so two plugins claiming
        the same name left whichever loaded last in place and said nothing --
        and with entry-point discovery the load order is not something either
        author controls. The metric path already refused collisions; this makes
        the two consistent.

        Args:
            name: Validator identifier.
            validator_cls: Validator class implementing ``validate``.

        Raises:
            PluginError: If the name is taken, or the class has no callable
                ``validate``.
        """
        from .exceptions import PluginError

        if name in self._validator_plugins:
            raise PluginError(
                f"validator {name!r} is already registered by another plugin. "
                "Unregister it first if replacement is intended."
            )
        if not callable(getattr(validator_cls, "validate", None)):
            raise PluginError(
                f"validator {name!r} has no callable 'validate'. The protocol "
                "is validate(X, y, minority_label, oversampler, **kwargs)."
            )
        self._validator_plugins[name] = validator_cls

    def unregister_validator(self, name: str) -> None:
        """Remove a registered validator plugin.

        Args:
            name: Validator identifier.

        Raises:
            PluginError: If no plugin is registered under that name.
        """
        from .exceptions import PluginError

        if name not in self._validator_plugins:
            raise PluginError(f"no validator plugin registered as {name!r}")
        del self._validator_plugins[name]

    def get_metric(self, name: str) -> type[DistanceMetricProtocol]:
        """Retrieve a registered metric class by name.

        Args:
            name: Metric identifier.

        Returns:
            Registered metric class.
        """
        if name not in self._metric_plugins:
            raise KeyError(f"Metric '{name}' is not registered")
        return self._metric_plugins[name]

    def get_validator(self, name: str) -> type[ValidatorProtocol]:
        """Retrieve a registered validator class by name.

        Args:
            name: Validator identifier.

        Returns:
            Registered validator class.
        """
        if name not in self._validator_plugins:
            raise KeyError(f"Validator '{name}' is not registered")
        return self._validator_plugins[name]

    def discover_plugins(self, package: str = "oversampleqa_plugins") -> None:
        """Discover plugins within a namespace package.

        Args:
            package: Namespace package to scan.
        """

        try:
            module = importlib.import_module(package)
        except ImportError:
            return
        if not hasattr(module, "__path__"):
            return
        for _, name, _ in pkgutil.iter_modules(module.__path__):
            discovered = importlib.import_module(f"{package}.{name}")
            self._register_module(discovered)

    def discover_entry_points(
        self,
        *,
        metric_group: str = METRIC_ENTRY_POINT_GROUP,
        validator_group: str = VALIDATOR_ENTRY_POINT_GROUP,
        strict: bool = False,
    ) -> list[str]:
        """Register metrics and validators advertised by installed packages.

        This is the discovery mechanism a third-party package should use: it
        needs no import of a magic namespace package and no scan of the
        filesystem, only an entry point in its own metadata. See
        ``examples/plugins/`` for a worked example.

        A plugin that fails to import, or that fails the axiom check, does not
        prevent the others from loading. Each failure raises a warning naming
        the entry point and the reason, because a plugin that quietly fails to
        register looks exactly like a plugin that was never installed, and the
        difference is an afternoon of confusion.

        Args:
            metric_group: Entry-point group scanned for metrics.
            validator_group: Entry-point group scanned for validators.
            strict: Raise on the first failure instead of warning and
                continuing. Use in tests, where a plugin that silently fails to
                load would make the suite pass for the wrong reason.

        Returns:
            Names successfully registered, metrics and validators together.

        Raises:
            PluginError: If ``strict`` and any entry point fails.
        """
        from .exceptions import PluginError

        registered: list[str] = []
        for group, register in (
            (metric_group, self.register_metric),
            (validator_group, self.register_validator),
        ):
            for entry_point in metadata.entry_points(group=group):
                try:
                    loaded = entry_point.load()
                    kwargs: dict[str, Any] = {}
                    if group == metric_group:
                        # Without this every plugin metric was axiom-checked on
                        # real-valued input, so a correct metric defined only on
                        # non-negative or binary input -- hellinger and jaccard
                        # are two the package itself ships -- failed
                        # registration with "does not satisfy the distance
                        # axioms". It satisfies them; it was being checked
                        # somewhere it is not defined.
                        kwargs["domain"] = _declared_domain(loaded, entry_point.name)
                    register(entry_point.name, loaded, **kwargs)
                except Exception as exc:
                    detail = (
                        f"plugin {entry_point.name!r} from group {group!r} "
                        f"could not be registered: {exc}"
                    )
                    if strict:
                        raise PluginError(detail) from exc
                    warnings.warn(detail, RuntimeWarning, stacklevel=2)
                    continue
                registered.append(entry_point.name)
        return registered

    def _register_module(self, module: Any) -> None:
        """Register all compatible classes in a module.

        Args:
            module: Imported module object.
        """
        # getmembers yields `type[object]`. The casts record that the guard
        # immediately above is what establishes the protocol -- it is a runtime
        # duck-type check on an arbitrary user module, which no annotation can
        # express. Registration re-validates signature and axioms regardless.
        for _, cls in inspect.getmembers(module, inspect.isclass):
            if self._implements_metric(cls):
                self.register_metric(
                    cls.__name__.lower(), cast("type[DistanceMetricProtocol]", cls)
                )
            elif self._implements_validator(cls):
                self.register_validator(
                    cls.__name__.lower(), cast("type[ValidatorProtocol]", cls)
                )

    @staticmethod
    def _implements_metric(cls: type) -> bool:
        """Return True if instances of ``cls`` are callable.

        Args:
            cls: Class object.

        Returns:
            ``True`` if the class, or one of its bases, defines ``__call__``.

        Notes:
            This used to be ``callable(getattr(cls, "__call__", None))``, which
            is ``True`` for **every** class: ``SomeClass.__call__`` resolves to
            ``type.__call__``, the metaclass hook used to instantiate it, and
            that is callable. Module discovery therefore classified every class
            as a metric, and the validator branch below was unreachable -- no
            validator has ever been discovered from a module.

            Walking ``__mro__`` and looking in each ``__dict__`` asks the right
            question: does the class itself define ``__call__``? ``object`` does
            not, and the metaclass is not in the MRO.
        """
        return any("__call__" in klass.__dict__ for klass in cls.__mro__)

    @staticmethod
    def _implements_validator(cls: type) -> bool:
        """Return True if class looks like a validator.

        Args:
            cls: Class object.

        Returns:
            ``True`` if the class defines ``validate``.
        """
        return callable(getattr(cls, "validate", None))

register_metric(name, metric_cls, *, domain='real', check_axioms=True, metric_kwargs=None)

Register a metric class by name, after checking it behaves like one.

Registration used to be a bare dictionary assignment: any object could be registered under any name, silently replacing a built-in, and a metric that violated the axioms would be discovered only by whoever eventually distrusted its numbers.

Three checks now run, each raising :class:~oversampleqa.PluginError:

  1. Name collision. A name already taken by a built-in or another plugin is refused rather than overwritten.
  2. Signature. The callable must accept two positional arguments.
  3. Axioms. d(x, x) == 0, d(x, y) > 0 for distinct points, symmetry, non-negativity and finiteness on random input.

The third is not hypothetical. The built-in hassanat shipped for this project's entire history scoring [-5] and [5] as distance zero, because it compared absolute values -- it was not a metric, and nothing checked. This is that check.

Parameters:

Name Type Description Default
name str

Metric identifier.

required
metric_cls type[DistanceMetricProtocol]

Metric callable or class implementing __call__.

required
domain MetricDomain

Input the metric is defined on. See :data:~oversampleqa.plugin_contract.METRIC_DOMAINS.

'real'
check_axioms bool

Run the axiom smoke check. Disable only for a metric you have verified another way, and say why.

True
metric_kwargs dict[str, Any] | None

Extra arguments the metric needs, such as cov_inv for Mahalanobis.

None

Raises:

Type Description
PluginError

On collision, bad signature, or axiom failure.

Source code in src/oversampleqa/plugin_system.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 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
def register_metric(
    self,
    name: str,
    metric_cls: type[DistanceMetricProtocol],
    *,
    domain: MetricDomain = "real",
    check_axioms: bool = True,
    metric_kwargs: dict[str, Any] | None = None,
) -> None:
    """Register a metric class by name, after checking it behaves like one.

    Registration used to be a bare dictionary assignment: any object could
    be registered under any name, silently replacing a built-in, and a
    metric that violated the axioms would be discovered only by whoever
    eventually distrusted its numbers.

    Three checks now run, each raising :class:`~oversampleqa.PluginError`:

    1. **Name collision.** A name already taken by a built-in or another
       plugin is refused rather than overwritten.
    2. **Signature.** The callable must accept two positional arguments.
    3. **Axioms.** ``d(x, x) == 0``, ``d(x, y) > 0`` for distinct points,
       symmetry, non-negativity and finiteness on random input.

    The third is not hypothetical. The built-in ``hassanat`` shipped for
    this project's entire history scoring ``[-5]`` and ``[5]`` as distance
    zero, because it compared absolute values -- it was not a metric, and
    nothing checked. This is that check.

    Args:
        name: Metric identifier.
        metric_cls: Metric callable or class implementing ``__call__``.
        domain: Input the metric is defined on. See
            :data:`~oversampleqa.plugin_contract.METRIC_DOMAINS`.
        check_axioms: Run the axiom smoke check. Disable only for a metric
            you have verified another way, and say why.
        metric_kwargs: Extra arguments the metric needs, such as
            ``cov_inv`` for Mahalanobis.

    Raises:
        PluginError: On collision, bad signature, or axiom failure.
    """
    from .distance import _METRICS
    from .exceptions import PluginError
    from .plugin_contract import check_metric_axioms, validate_metric_signature

    if name in _METRICS:
        raise PluginError(
            f"metric {name!r} is already a built-in. Registering over it "
            "would silently change results for every caller using that "
            "name; choose a different one."
        )
    if name in self._metric_plugins:
        raise PluginError(
            f"metric {name!r} is already registered by another plugin. "
            "Unregister it first if replacement is intended."
        )

    candidate = metric_cls() if isinstance(metric_cls, type) else metric_cls
    validate_metric_signature(candidate, name)

    if check_axioms:
        report = check_metric_axioms(
            candidate, name, domain=domain, **(metric_kwargs or {})
        )
        if not report.ok:
            detail = "\n  ".join(report.failures)
            raise PluginError(
                f"metric {name!r} does not satisfy the distance axioms:\n"
                f"  {detail}\n"
                "A metric that violates these produces numbers that look "
                "plausible and mean nothing."
            )

    self._metric_plugins[name] = metric_cls

unregister_metric(name)

Remove a registered metric plugin.

Parameters:

Name Type Description Default
name str

Metric identifier.

required

Raises:

Type Description
PluginError

If no plugin is registered under that name.

Source code in src/oversampleqa/plugin_system.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def unregister_metric(self, name: str) -> None:
    """Remove a registered metric plugin.

    Args:
        name: Metric identifier.

    Raises:
        PluginError: If no plugin is registered under that name.
    """
    from .exceptions import PluginError

    if name not in self._metric_plugins:
        raise PluginError(f"no metric plugin registered as {name!r}")
    del self._metric_plugins[name]

register_validator(name, validator_cls)

Register a validator class by name, refusing collisions.

This used to be a bare dictionary assignment, so two plugins claiming the same name left whichever loaded last in place and said nothing -- and with entry-point discovery the load order is not something either author controls. The metric path already refused collisions; this makes the two consistent.

Parameters:

Name Type Description Default
name str

Validator identifier.

required
validator_cls type[ValidatorProtocol]

Validator class implementing validate.

required

Raises:

Type Description
PluginError

If the name is taken, or the class has no callable validate.

Source code in src/oversampleqa/plugin_system.py
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
def register_validator(
    self, name: str, validator_cls: type[ValidatorProtocol]
) -> None:
    """Register a validator class by name, refusing collisions.

    This used to be a bare dictionary assignment, so two plugins claiming
    the same name left whichever loaded last in place and said nothing --
    and with entry-point discovery the load order is not something either
    author controls. The metric path already refused collisions; this makes
    the two consistent.

    Args:
        name: Validator identifier.
        validator_cls: Validator class implementing ``validate``.

    Raises:
        PluginError: If the name is taken, or the class has no callable
            ``validate``.
    """
    from .exceptions import PluginError

    if name in self._validator_plugins:
        raise PluginError(
            f"validator {name!r} is already registered by another plugin. "
            "Unregister it first if replacement is intended."
        )
    if not callable(getattr(validator_cls, "validate", None)):
        raise PluginError(
            f"validator {name!r} has no callable 'validate'. The protocol "
            "is validate(X, y, minority_label, oversampler, **kwargs)."
        )
    self._validator_plugins[name] = validator_cls

unregister_validator(name)

Remove a registered validator plugin.

Parameters:

Name Type Description Default
name str

Validator identifier.

required

Raises:

Type Description
PluginError

If no plugin is registered under that name.

Source code in src/oversampleqa/plugin_system.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def unregister_validator(self, name: str) -> None:
    """Remove a registered validator plugin.

    Args:
        name: Validator identifier.

    Raises:
        PluginError: If no plugin is registered under that name.
    """
    from .exceptions import PluginError

    if name not in self._validator_plugins:
        raise PluginError(f"no validator plugin registered as {name!r}")
    del self._validator_plugins[name]

get_metric(name)

Retrieve a registered metric class by name.

Parameters:

Name Type Description Default
name str

Metric identifier.

required

Returns:

Type Description
type[DistanceMetricProtocol]

Registered metric class.

Source code in src/oversampleqa/plugin_system.py
174
175
176
177
178
179
180
181
182
183
184
185
def get_metric(self, name: str) -> type[DistanceMetricProtocol]:
    """Retrieve a registered metric class by name.

    Args:
        name: Metric identifier.

    Returns:
        Registered metric class.
    """
    if name not in self._metric_plugins:
        raise KeyError(f"Metric '{name}' is not registered")
    return self._metric_plugins[name]

get_validator(name)

Retrieve a registered validator class by name.

Parameters:

Name Type Description Default
name str

Validator identifier.

required

Returns:

Type Description
type[ValidatorProtocol]

Registered validator class.

Source code in src/oversampleqa/plugin_system.py
187
188
189
190
191
192
193
194
195
196
197
198
def get_validator(self, name: str) -> type[ValidatorProtocol]:
    """Retrieve a registered validator class by name.

    Args:
        name: Validator identifier.

    Returns:
        Registered validator class.
    """
    if name not in self._validator_plugins:
        raise KeyError(f"Validator '{name}' is not registered")
    return self._validator_plugins[name]

discover_plugins(package='oversampleqa_plugins')

Discover plugins within a namespace package.

Parameters:

Name Type Description Default
package str

Namespace package to scan.

'oversampleqa_plugins'
Source code in src/oversampleqa/plugin_system.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
def discover_plugins(self, package: str = "oversampleqa_plugins") -> None:
    """Discover plugins within a namespace package.

    Args:
        package: Namespace package to scan.
    """

    try:
        module = importlib.import_module(package)
    except ImportError:
        return
    if not hasattr(module, "__path__"):
        return
    for _, name, _ in pkgutil.iter_modules(module.__path__):
        discovered = importlib.import_module(f"{package}.{name}")
        self._register_module(discovered)

discover_entry_points(*, metric_group=METRIC_ENTRY_POINT_GROUP, validator_group=VALIDATOR_ENTRY_POINT_GROUP, strict=False)

Register metrics and validators advertised by installed packages.

This is the discovery mechanism a third-party package should use: it needs no import of a magic namespace package and no scan of the filesystem, only an entry point in its own metadata. See examples/plugins/ for a worked example.

A plugin that fails to import, or that fails the axiom check, does not prevent the others from loading. Each failure raises a warning naming the entry point and the reason, because a plugin that quietly fails to register looks exactly like a plugin that was never installed, and the difference is an afternoon of confusion.

Parameters:

Name Type Description Default
metric_group str

Entry-point group scanned for metrics.

METRIC_ENTRY_POINT_GROUP
validator_group str

Entry-point group scanned for validators.

VALIDATOR_ENTRY_POINT_GROUP
strict bool

Raise on the first failure instead of warning and continuing. Use in tests, where a plugin that silently fails to load would make the suite pass for the wrong reason.

False

Returns:

Type Description
list[str]

Names successfully registered, metrics and validators together.

Raises:

Type Description
PluginError

If strict and any entry point fails.

Source code in src/oversampleqa/plugin_system.py
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
def discover_entry_points(
    self,
    *,
    metric_group: str = METRIC_ENTRY_POINT_GROUP,
    validator_group: str = VALIDATOR_ENTRY_POINT_GROUP,
    strict: bool = False,
) -> list[str]:
    """Register metrics and validators advertised by installed packages.

    This is the discovery mechanism a third-party package should use: it
    needs no import of a magic namespace package and no scan of the
    filesystem, only an entry point in its own metadata. See
    ``examples/plugins/`` for a worked example.

    A plugin that fails to import, or that fails the axiom check, does not
    prevent the others from loading. Each failure raises a warning naming
    the entry point and the reason, because a plugin that quietly fails to
    register looks exactly like a plugin that was never installed, and the
    difference is an afternoon of confusion.

    Args:
        metric_group: Entry-point group scanned for metrics.
        validator_group: Entry-point group scanned for validators.
        strict: Raise on the first failure instead of warning and
            continuing. Use in tests, where a plugin that silently fails to
            load would make the suite pass for the wrong reason.

    Returns:
        Names successfully registered, metrics and validators together.

    Raises:
        PluginError: If ``strict`` and any entry point fails.
    """
    from .exceptions import PluginError

    registered: list[str] = []
    for group, register in (
        (metric_group, self.register_metric),
        (validator_group, self.register_validator),
    ):
        for entry_point in metadata.entry_points(group=group):
            try:
                loaded = entry_point.load()
                kwargs: dict[str, Any] = {}
                if group == metric_group:
                    # Without this every plugin metric was axiom-checked on
                    # real-valued input, so a correct metric defined only on
                    # non-negative or binary input -- hellinger and jaccard
                    # are two the package itself ships -- failed
                    # registration with "does not satisfy the distance
                    # axioms". It satisfies them; it was being checked
                    # somewhere it is not defined.
                    kwargs["domain"] = _declared_domain(loaded, entry_point.name)
                register(entry_point.name, loaded, **kwargs)
            except Exception as exc:
                detail = (
                    f"plugin {entry_point.name!r} from group {group!r} "
                    f"could not be registered: {exc}"
                )
                if strict:
                    raise PluginError(detail) from exc
                warnings.warn(detail, RuntimeWarning, stacklevel=2)
                continue
            registered.append(entry_point.name)
    return registered

CustomEuclideanMetric

Example metric plugin.

Source code in src/oversampleqa/plugin_system.py
401
402
403
404
405
406
@register_metric("custom_euclidean")
class CustomEuclideanMetric:
    """Example metric plugin."""

    def __call__(self, x1: FloatArray, x2: FloatArray, **_: Any) -> float:
        return float(np.linalg.norm(x1 - x2))

register_metric(name)

Decorator to register a metric plugin by name.

Parameters:

Name Type Description Default
name str

Metric identifier.

required
Source code in src/oversampleqa/plugin_system.py
373
374
375
376
377
378
379
380
381
382
383
384
def register_metric(name: str) -> Callable[[Any], Any]:
    """Decorator to register a metric plugin by name.

    Args:
        name: Metric identifier.
    """

    def decorator(cls: type[DistanceMetricProtocol]) -> type[DistanceMetricProtocol]:
        plugin_manager.register_metric(name, cls)
        return cls

    return decorator

register_validator(name)

Decorator to register a validator plugin by name.

Parameters:

Name Type Description Default
name str

Validator identifier.

required
Source code in src/oversampleqa/plugin_system.py
387
388
389
390
391
392
393
394
395
396
397
398
def register_validator(name: str) -> Callable[[Any], Any]:
    """Decorator to register a validator plugin by name.

    Args:
        name: Validator identifier.
    """

    def decorator(cls: type[ValidatorProtocol]) -> type[ValidatorProtocol]:
        plugin_manager.register_validator(name, cls)
        return cls

    return decorator