#!/usr/bin/env python
# -*- coding: utf-8 -*-
# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised))
# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia
# Copyright (c) 2022, 2023, 2024 QuatroPe
# All rights reserved.
# =============================================================================
# DOCS
# =============================================================================
"""Functionalities for the user's extension of scikit-criteria.
This module introduces decorators that enable the creation of aggregation and
transformation models using only functions.
It is important to note that models created with these decorators are much less
flexible than those created using inheritance and lack certain properties of
real objects.
"""
# =============================================================================
# IMPORTS
# =============================================================================รง
from .utils import hidden
with hidden():
import inspect
import warnings
from .agg import RankResult, SKCDecisionMakerABC
from .preprocessing import SKCTransformerABC
from .utils import doc_inherit
# =============================================================================
# CONSTANTS
# =============================================================================
_MODEL_PARAMETERS = (
"matrix",
"objectives",
"weights",
"dtypes",
"alternatives",
"criteria",
"hparams",
)
# =============================================================================
# WARNINGS
# =============================================================================
[docs]
class NonStandardNameWarning(UserWarning):
"""Custom warning class to indicate that a name does not follow a \
specific standard.
This warning is raised when a given name does not adhere to the specified
naming convention.
"""
warnings.simplefilter("always", NonStandardNameWarning)
# =============================================================================
# LOGIC
# =============================================================================
def _create_init_signature(kwargs):
"""Create an initialization signature for a class based on the provided \
keyword arguments."""
params = [
inspect.Parameter(
name, default=default, kind=inspect.Parameter.KEYWORD_ONLY
)
for name, default in kwargs.items()
]
signature = inspect.Signature(params)
return signature
def _check_model_CapWords_convention_name(model_name):
"""Check if a model name follows the 'CapWords' convention and issue a \
warning if not."""
if model_name[0].islower():
msg = (
"Models names should normally use the 'CapWords' convention."
f"try change {model_name!r} to "
f"{model_name.title()!r} or {model_name.upper()!r}"
)
warnings.warn(msg, category=NonStandardNameWarning, stacklevel=3)
def _check_function_parameters(func):
"""Raise a type error if a function has invalid parameters according to a \
predefined set."""
has_kwars = False
not_used = set(_MODEL_PARAMETERS)
for pname, param in inspect.signature(func).parameters.items():
if param.kind is param.VAR_KEYWORD:
has_kwars = True
continue
elif pname not in _MODEL_PARAMETERS:
raise TypeError(
f"{func.__qualname__}() has an invalid parameter {pname!r}"
)
not_used.remove(pname)
if not_used and not has_kwars:
raise TypeError(
f"{func.__qualname__}() is missing the parameter(s) {not_used}"
)
[docs]
def mkagg(maybe_func=None, **hparams):
"""Decorator factory function for creating aggregation classes.
Parameters
----------
maybe_func : callable, optional
Optional aggregation function to be wrapped into a class. If provided,
the decorator is applied immediately.
The decorated function should receive the parameters 'matrix',
'objectives', 'weights', 'dtypes', 'alternatives', 'criteria',
'hparams', or kwargs.
Additionally, it should return an array with rankings for each
alternative and an optional dictionary with calculations that you wish
to store in the 'extra' attribute of the ranking."
**hparams : keyword arguments
Hyperparameters specific to the aggregation function.
Returns
-------
Agg : class or decorator
Aggregation class decorator or Aggregatio model with added
functionality.
Notes
-----
This decorator is designed for creating aggregation model from aggregation
functions. It provides an interface for creating aggregated
decision-making models.
Examples
--------
>>> @mkagg
>>> def MyAgg(**kwargs):
>>> # Implementation of the aggregation function
The above example will create an aggregation class with the name 'MyAgg'
based on the provided aggregation function.
>>> @mkagg(foo=1)
>>> def MyAgg(**kwargs):
>>> # Implementation of the aggregation function
The above example will create an aggregation class with the specified
hyperparameter 'foo' and the name 'MyAgg'.
"""
def _agg_maker(agg_func):
agg_name = agg_func.__name__
_check_model_CapWords_convention_name(agg_name)
_check_function_parameters(agg_func)
class _AutoAGG(SKCDecisionMakerABC):
__doc__ = agg_func.__doc__
_skcriteria_parameters = tuple(hparams)
_skcriteria_init_signature = _create_init_signature(hparams)
def __init__(self, **kwargs):
try:
bound = self._skcriteria_init_signature.bind(**kwargs)
except TypeError as err:
raise TypeError(f"{agg_name}.__init__() {err}")
bound.apply_defaults()
self.__dict__.update(bound.kwargs)
__init__.__signature__ = _skcriteria_init_signature
@doc_inherit(SKCDecisionMakerABC.get_method_name)
def get_method_name(self):
return agg_name
@doc_inherit(SKCDecisionMakerABC._evaluate_data)
def _evaluate_data(self, **kwargs):
rank, extra = agg_func(hparams=self, **kwargs)
return rank, extra
@doc_inherit(SKCDecisionMakerABC._make_result)
def _make_result(self, alternatives, values, extra):
return RankResult(
agg_name,
alternatives=alternatives,
values=values,
extra=extra,
)
return type(agg_name, (_AutoAGG,), {"__module__": agg_func.__module__})
return _agg_maker if maybe_func is None else _agg_maker(maybe_func)