CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_metadata_requests.py1629 linesDownload Raw Back to utils
1"""
2Metadata Routing Utility
3
4In order to better understand the components implemented in this file, one
5needs to understand their relationship to one another.
6
7The only relevant public API for end users are the ``set_{method}_request`` methods,
8e.g. ``estimator.set_fit_request(sample_weight=True)``. However, third-party
9developers and users who implement custom meta-estimators, need to deal with
10the objects implemented in this file.
11
12The routing is coordinated by building ``MetadataRequest`` objects
13for objects that consume metadata, and ``MetadataRouter`` objects for objects that
14can route metadata, which are then aligned during a call to `process_routing()`. This
15function returns a Bunch object (dictionary-like) with all the information on the
16consumers and which metadata they had requested and the actual metadata values. A
17routing method (such as `fit` in a meta-estimator) can now provide the metadata to the
18relevant consuming method (such as `fit` in a sub-estimator).
19
20The ``MetadataRequest`` and ``MetadataRouter`` objects are constructed via a
21``get_metadata_routing`` method, which all scikit-learn estimators provide.
22This method is automatically implemented via ``BaseEstimator`` for all simple
23estimators, but needs a custom implementation for meta-estimators.
24
25MetadataRequest
26~~~~~~~~~~~~~~~
27
28In non-routing consumers, the simplest case, e.g. ``SVM``, ``get_metadata_routing``
29returns a ``MetadataRequest`` object  which is assigned to the consumer's
30`_metadata_request` attribute. It stores which metadata is required by each method of
31the consumer by including one ``MethodMetadataRequest`` per method in ``METHODS``
32(e. g. ``fit``, ``score``, etc).
33
34Users and developers almost never need to directly add a new ``MethodMetadataRequest``,
35to the consumer's `_metadata_request` attribute, since these are generated
36automatically. This attribute is modified while running `set_{method}_request` methods
37(such as `set_fit_request()`), which adds the request via
38`method_metadata_request.add_request(param=prop, alias=alias)`.
39
40The ``alias`` in the ``add_request`` method has to be either a string (an alias),
41or one of ``[True (requested), False (unrequested), None (error if passed)]``. There
42are some other special values such as ``UNUSED`` and ``WARN`` which are used
43for purposes such as warning of removing a metadata in a child class, but not
44used by the end users.
45
46MetadataRouter
47~~~~~~~~~~~~~~
48
49In routers (such as meta-estimators or multi metric scorers), ``get_metadata_routing``
50returns a ``MetadataRouter`` object. It provides information about which method, from
51the router object, calls which method in a consumer's object, and also, which metadata
52had been requested by the consumer's methods, thus specifying how metadata is to be
53passed. If a sub-estimator is a router as well, their routing information is also stored
54in the meta-estimators router.
55
56Conceptually, this information looks like:
57
58```
59{
60    "sub_estimator1": (
61        mapping=[(caller="fit", callee="transform"), ...],
62        router=MetadataRequest(...),  # or another MetadataRouter
63    ),
64    ...
65}
66```
67
68The `MetadataRouter` objects are never stored and are always recreated anew whenever
69the object's `get_metadata_routing` method is called.
70
71An object that is both a router and a consumer, e.g. a meta-estimator which
72consumes ``sample_weight`` and routes ``sample_weight`` to its sub-estimators
73also returns a ``MetadataRouter`` object. Its routing information includes both
74information about what metadata is required by the object itself (added via
75``MetadataRouter.add_self_request``), as well as the routing information for its
76sub-estimators (added via ``MetadataRouter.add``).
77
78Implementation Details
79~~~~~~~~~~~~~~~~~~~~~~
80
81To give the above representation some structure, we use the following objects:
82
83- ``(caller=..., callee=...)`` is a namedtuple called ``MethodPair``.
84
85- The list of ``MethodPair`` stored in the ``mapping`` field of a `RouterMappingPair` is
86  a ``MethodMapping`` object.
87
88- ``(mapping=..., router=...)`` is a namedtuple called ``RouterMappingPair``.
89
90The ``set_{method}_request`` methods are dynamically generated for estimators
91which inherit from ``BaseEstimator``. This is done by attaching instances
92of the ``RequestMethod`` descriptor to classes, which is done in the
93``_MetadataRequester`` class, and ``BaseEstimator`` inherits from this mixin.
94This mixin also implements the ``get_metadata_routing``, which meta-estimators
95need to override, but it works for simple consumers as is.
96"""
97
98# Authors: The scikit-learn developers
99# SPDX-License-Identifier: BSD-3-Clause
100
101import inspect
102from collections import namedtuple
103from copy import deepcopy
104from typing import TYPE_CHECKING, Optional, Union
105from warnings import warn
106
107from .. import get_config
108from ..exceptions import UnsetMetadataPassedError
109from ._bunch import Bunch
110
111# Only the following methods are supported in the routing mechanism. Adding new
112# methods at the moment involves monkeypatching this list.
113# Note that if this list is changed or monkeypatched, the corresponding method
114# needs to be added under a TYPE_CHECKING condition like the one done here in
115# _MetadataRequester
116SIMPLE_METHODS = [
117    "fit",
118    "partial_fit",
119    "predict",
120    "predict_proba",
121    "predict_log_proba",
122    "decision_function",
123    "score",
124    "split",
125    "transform",
126    "inverse_transform",
127]
128
129# These methods are a composite of other methods and one cannot set their
130# requests directly. Instead they should be set by setting the requests of the
131# simple methods which make the composite ones.
132COMPOSITE_METHODS = {
133    "fit_transform": ["fit", "transform"],
134    "fit_predict": ["fit", "predict"],
135}
136
137METHODS = SIMPLE_METHODS + list(COMPOSITE_METHODS.keys())
138
139
140def _routing_enabled():
141    """Return whether metadata routing is enabled.
142
143    .. versionadded:: 1.3
144
145    Returns
146    -------
147    enabled : bool
148        Whether metadata routing is enabled. If the config is not set, it
149        defaults to False.
150    """
151    return get_config().get("enable_metadata_routing", False)
152
153
154def _raise_for_params(params, owner, method, allow=None):
155    """Raise an error if metadata routing is not enabled and params are passed.
156
157    .. versionadded:: 1.4
158
159    Parameters
160    ----------
161    params : dict
162        The metadata passed to a method.
163
164    owner : object
165        The object to which the method belongs.
166
167    method : str
168        The name of the method, e.g. "fit".
169
170    allow : list of str, default=None
171        A list of parameters which are allowed to be passed even if metadata
172        routing is not enabled.
173
174    Raises
175    ------
176    ValueError
177        If metadata routing is not enabled and params are passed.
178    """
179    caller = (
180        f"{owner.__class__.__name__}.{method}" if method else owner.__class__.__name__
181    )
182
183    allow = allow if allow is not None else {}
184
185    if not _routing_enabled() and (params.keys() - allow):
186        raise ValueError(
187            f"Passing extra keyword arguments to {caller} is only supported if"
188            " enable_metadata_routing=True, which you can set using"
189            " `sklearn.set_config`. See the User Guide"
190            " <https://scikit-learn.org/stable/metadata_routing.html> for more"
191            f" details. Extra parameters passed are: {set(params)}"
192        )
193
194
195def _raise_for_unsupported_routing(obj, method, **kwargs):
196    """Raise when metadata routing is enabled and metadata is passed.
197
198    This is used in meta-estimators which have not implemented metadata routing
199    to prevent silent bugs. There is no need to use this function if the
200    meta-estimator is not accepting any metadata, especially in `fit`, since
201    if a meta-estimator accepts any metadata, they would do that in `fit` as
202    well.
203
204    Parameters
205    ----------
206    obj : estimator
207        The estimator for which we're raising the error.
208
209    method : str
210        The method where the error is raised.
211
212    **kwargs : dict
213        The metadata passed to the method.
214    """
215    kwargs = {key: value for key, value in kwargs.items() if value is not None}
216    if _routing_enabled() and kwargs:
217        cls_name = obj.__class__.__name__
218        raise NotImplementedError(
219            f"{cls_name}.{method} cannot accept given metadata ({set(kwargs.keys())})"
220            f" since metadata routing is not yet implemented for {cls_name}."
221        )
222
223
224class _RoutingNotSupportedMixin:
225    """A mixin to be used to remove the default `get_metadata_routing`.
226
227    This is used in meta-estimators where metadata routing is not yet
228    implemented.
229
230    This also makes it clear in our rendered documentation that this method
231    cannot be used.
232    """
233
234    def get_metadata_routing(self):
235        """Raise `NotImplementedError`.
236
237        This estimator does not support metadata routing yet."""
238        raise NotImplementedError(
239            f"{self.__class__.__name__} has not implemented metadata routing yet."
240        )
241
242
243# Request values
244# ==============
245# Each request value needs to be one of the following values, or an alias.
246
247# this is used in `__metadata_request__*` attributes to indicate that a
248# metadata is not present even though it may be present in the
249# corresponding method's signature.
250UNUSED = "$UNUSED$"
251
252# this is used whenever a default value is changed, and therefore the user
253# should explicitly set the value, otherwise a warning is shown. An example
254# is when a meta-estimator is only a router, but then becomes also a
255# consumer in a new release.
256WARN = "$WARN$"
257
258# this is the default used in `set_{method}_request` methods to indicate no
259# change requested by the user.
260UNCHANGED = "$UNCHANGED$"
261
262VALID_REQUEST_VALUES = [False, True, None, UNUSED, WARN]
263
264
265def request_is_alias(item):
266    """Check if an item is a valid string alias for a metadata.
267
268    Values in ``VALID_REQUEST_VALUES`` are not considered aliases in this
269    context. Only a string which is a valid identifier is.
270
271    Parameters
272    ----------
273    item : object
274        The given item to be checked if it can be an alias for the metadata.
275
276    Returns
277    -------
278    result : bool
279        Whether the given item is a valid alias.
280    """
281    if item in VALID_REQUEST_VALUES:
282        return False
283
284    # item is only an alias if it's a valid identifier
285    return isinstance(item, str) and item.isidentifier()
286
287
288def request_is_valid(item):
289    """Check if an item is a valid request value (and not an alias).
290
291    Parameters
292    ----------
293    item : object
294        The given item to be checked.
295
296    Returns
297    -------
298    result : bool
299        Whether the given item is valid.
300    """
301    return item in VALID_REQUEST_VALUES
302
303
304# Metadata Request for Simple Consumers
305# =====================================
306# This section includes MethodMetadataRequest and MetadataRequest which are
307# used in simple consumers.
308
309
310class MethodMetadataRequest:
311    """Container for metadata requests associated with a single method.
312
313    Instances of this class get used within a :class:`MetadataRequest` - one per each
314    public method (`fit`, `transform`, ...) that its owning consumer has.
315
316    .. versionadded:: 1.3
317
318    Parameters
319    ----------
320    owner : str
321        A display name for the object owning these requests.
322
323    method : str
324        The name of the method to which these requests belong.
325
326    requests : dict of {str: bool, None or str}, default=None
327        The initial requests for this method.
328    """
329
330    def __init__(self, owner, method, requests=None):
331        self._requests = requests or dict()
332        self.owner = owner
333        self.method = method
334
335    @property
336    def requests(self):
337        """Dictionary of the form: ``{key: alias}``."""
338        return self._requests
339
340    def add_request(
341        self,
342        *,
343        param,
344        alias,
345    ):
346        """Add request info for a metadata.
347
348        Parameters
349        ----------
350        param : str
351            The metadata for which a request is set.
352
353        alias : str, or {True, False, None}
354            Specifies which metadata should be routed to the method that owns this
355            `MethodMetadataRequest`.
356
357            - str: the name (or alias) of metadata given to a meta-estimator that
358              should be routed to the method that owns this `MethodMetadataRequest`.
359
360            - True: requested
361
362            - False: not requested
363
364            - None: error if passed
365        """
366        if not request_is_alias(alias) and not request_is_valid(alias):
367            raise ValueError(
368                f"The alias you're setting for `{param}` should be either a "
369                "valid identifier or one of {None, True, False}, but given "
370                f"value is: `{alias}`"
371            )
372
373        if alias == param:
374            alias = True
375
376        if alias == UNUSED:
377            if param in self._requests:
378                del self._requests[param]
379            else:
380                raise ValueError(
381                    f"Trying to remove parameter {param} with UNUSED which doesn't"
382                    " exist."
383                )
384        else:
385            self._requests[param] = alias
386
387        return self
388
389    def _get_param_names(self, return_alias):
390        """Get names of all metadata that can be consumed or routed by this method.
391
392        This method returns the names of all metadata, even the ``False``
393        ones.
394
395        Parameters
396        ----------
397        return_alias : bool
398            Controls whether original or aliased names should be returned. If
399            ``False``, aliases are ignored and original names are returned.
400
401        Returns
402        -------
403        names : set of str
404            A set of strings with the names of all metadata.
405        """
406        return set(
407            alias if return_alias and not request_is_valid(alias) else prop
408            for prop, alias in self._requests.items()
409            if not request_is_valid(alias) or alias is not False
410        )
411
412    def _check_warnings(self, *, params):
413        """Check whether metadata is passed which is marked as WARN.
414
415        If any metadata is passed which is marked as WARN, a warning is raised.
416
417        Parameters
418        ----------
419        params : dict
420            The metadata passed to a method.
421        """
422        params = {} if params is None else params
423        warn_params = {
424            prop
425            for prop, alias in self._requests.items()
426            if alias == WARN and prop in params
427        }
428        for param in warn_params:
429            warn(
430                f"Support for {param} has recently been added to this class. "
431                "To maintain backward compatibility, it is ignored now. "
432                f"Using `set_{self.method}_request({param}={{True, False}})` "
433                "on this method of the class, you can set the request value "
434                "to False to silence this warning, or to True to consume and "
435                "use the metadata."
436            )
437
438    def _route_params(self, params, parent, caller):
439        """Prepare the given metadata to be passed to the method.
440
441        The output of this method can be used directly as the input to the
442        corresponding method as **kwargs.
443
444        Parameters
445        ----------
446        params : dict
447            A dictionary of provided metadata.
448
449        parent : object
450            Parent class object, that routes the metadata.
451
452        caller : str
453            Method from the parent class object, where the metadata is routed from.
454
455        Returns
456        -------
457        params : Bunch
458            A :class:`~sklearn.utils.Bunch` of {metadata: value} which can be
459            passed to the corresponding method.
460        """
461        self._check_warnings(params=params)
462        unrequested = dict()
463        args = {arg: value for arg, value in params.items() if value is not None}
464        res = Bunch()
465        for prop, alias in self._requests.items():
466            if alias is False or alias == WARN:
467                continue
468            elif alias is True and prop in args:
469                res[prop] = args[prop]
470            elif alias is None and prop in args:
471                unrequested[prop] = args[prop]
472            elif alias in args:
473                res[prop] = args[alias]
474        if unrequested:
475            if self.method in COMPOSITE_METHODS:
476                callee_methods = COMPOSITE_METHODS[self.method]
477            else:
478                callee_methods = [self.method]
479            set_requests_on = "".join(
480                [
481                    f".set_{method}_request({{metadata}}=True/False)"
482                    for method in callee_methods
483                ]
484            )
485            message = (
486                f"[{', '.join([key for key in unrequested])}] are passed but are not"
487                " explicitly set as requested or not requested for"
488                f" {self.owner}.{self.method}, which is used within"
489                f" {parent}.{caller}. Call `{self.owner}"
490                + set_requests_on
491                + "` for each metadata you want to request/ignore. See the"
492                " Metadata Routing User guide"
493                " <https://scikit-learn.org/stable/metadata_routing.html> for more"
494                " information."
495            )
496            raise UnsetMetadataPassedError(
497                message=message,
498                unrequested_params=unrequested,
499                routed_params=res,
500            )
501        return res
502
503    def _consumes(self, params):
504        """Check whether the given metadata are consumed by this method.
505
506        Parameters
507        ----------
508        params : iterable of str
509            An iterable of parameters to check.
510
511        Returns
512        -------
513        consumed : set of str
514            A set of parameters which are consumed by this method.
515        """
516        params = set(params)
517        res = set()
518        for prop, alias in self._requests.items():
519            if alias is True and prop in params:
520                res.add(prop)
521            elif isinstance(alias, str) and alias in params:
522                res.add(alias)
523        return res
524
525    def _serialize(self):
526        """Serialize the object.
527
528        Returns
529        -------
530        obj : dict
531            A serialized version of the instance in the form of a dictionary.
532        """
533        return self._requests
534
535    def __repr__(self):
536        return str(self._serialize())
537
538    def __str__(self):
539        return str(repr(self))
540
541
542class MetadataRequest:
543    """Contains the metadata request info of a consumer.
544
545    Instances of `MethodMetadataRequest` are used in this class for each
546    available method under `metadatarequest.{method}`.
547
548    Consumer-only classes such as simple estimators return a serialized
549    version of this class as the output of `get_metadata_routing()`.
550
551    .. versionadded:: 1.3
552
553    Parameters
554    ----------
555    owner : str
556        The name of the object to which these requests belong.
557    """
558
559    # this is here for us to use this attribute's value instead of doing
560    # `isinstance` in our checks, so that we avoid issues when people vendor
561    # this file instead of using it directly from scikit-learn.
562    _type = "metadata_request"
563
564    def __init__(self, owner):
565        self.owner = owner
566        for method in SIMPLE_METHODS:
567            setattr(
568                self,
569                method,
570                MethodMetadataRequest(owner=owner, method=method),
571            )
572
573    def consumes(self, method, params):
574        """Check whether the given metadata are consumed by the given method.
575
576        .. versionadded:: 1.4
577
578        Parameters
579        ----------
580        method : str
581            The name of the method to check.
582
583        params : iterable of str
584            An iterable of parameters to check.
585
586        Returns
587        -------
588        consumed : set of str
589            A set of parameters which are consumed by the given method.
590        """
591        return getattr(self, method)._consumes(params=params)
592
593    def __getattr__(self, name):
594        # Called when the default attribute access fails with an AttributeError
595        # (either __getattribute__() raises an AttributeError because name is
596        # not an instance attribute or an attribute in the class tree for self;
597        # or __get__() of a name property raises AttributeError). This method
598        # should either return the (computed) attribute value or raise an
599        # AttributeError exception.
600        # https://docs.python.org/3/reference/datamodel.html#object.__getattr__
601        if name not in COMPOSITE_METHODS:
602            raise AttributeError(
603                f"'{self.__class__.__name__}' object has no attribute '{name}'"
604            )
605
606        requests = {}
607        for method in COMPOSITE_METHODS[name]:
608            mmr = getattr(self, method)
609            existing = set(requests.keys())
610            upcoming = set(mmr.requests.keys())
611            common = existing & upcoming
612            conflicts = [key for key in common if requests[key] != mmr._requests[key]]
613            if conflicts:
614                raise ValueError(
615                    f"Conflicting metadata requests for {', '.join(conflicts)} while"
616                    f" composing the requests for {name}. Metadata with the same name"
617                    f" for methods {', '.join(COMPOSITE_METHODS[name])} should have the"
618                    " same request value."
619                )
620            requests.update(mmr._requests)
621        return MethodMetadataRequest(owner=self.owner, method=name, requests=requests)
622
623    def _get_param_names(self, method, return_alias, ignore_self_request=None):
624        """Get names of all metadata that can be consumed or routed by specified \
625            method.
626
627        This method returns the names of all metadata, even the ``False``
628        ones.
629
630        Parameters
631        ----------
632        method : str
633            The name of the method for which metadata names are requested.
634
635        return_alias : bool
636            Controls whether original or aliased names should be returned. If
637            ``False``, aliases are ignored and original names are returned.
638
639        ignore_self_request : bool
640            Ignored. Present for API compatibility.
641
642        Returns
643        -------
644        names : set of str
645            A set of strings with the names of all metadata.
646        """
647        return getattr(self, method)._get_param_names(return_alias=return_alias)
648
649    def _route_params(self, *, params, method, parent, caller):
650        """Prepare the given parameters to be passed to the method.
651
652        The output of this method can be used directly as the input to the
653        corresponding method as extra keyword arguments to pass metadata.
654
655        Parameters
656        ----------
657        params : dict
658            A dictionary of provided metadata.
659
660        method : str
661            The name of the method for which the parameters are requested and
662            routed.
663
664        parent : object
665            Parent class object, that routes the metadata.
666
667        caller : str
668            Method from the parent class object, where the metadata is routed from.
669
670        Returns
671        -------
672        params : Bunch
673            A :class:`~sklearn.utils.Bunch` of {metadata: value} which can be given to
674            the corresponding method.
675        """
676        return getattr(self, method)._route_params(
677            params=params, parent=parent, caller=caller
678        )
679
680    def _check_warnings(self, *, method, params):
681        """Check whether metadata is passed which is marked as WARN.
682
683        If any metadata is passed which is marked as WARN, a warning is raised.
684
685        Parameters
686        ----------
687        method : str
688            The name of the method for which the warnings should be checked.
689
690        params : dict
691            The metadata passed to a method.
692        """
693        getattr(self, method)._check_warnings(params=params)
694
695    def _serialize(self):
696        """Serialize the object.
697
698        Returns
699        -------
700        obj : dict
701            A serialized version of the instance in the form of a dictionary.
702        """
703        output = dict()
704        for method in SIMPLE_METHODS:
705            mmr = getattr(self, method)
706            if len(mmr.requests):
707                output[method] = mmr._serialize()
708        return output
709
710    def __repr__(self):
711        return str(self._serialize())
712
713    def __str__(self):
714        return str(repr(self))
715
716
717# Metadata Request for Routers
718# ============================
719# This section includes all objects required for MetadataRouter which is used
720# in routers, returned by their ``get_metadata_routing``.
721
722# `RouterMappingPair` is used to store a (mapping, router) tuple where `mapping` is a
723# `MethodMapping` object and `router` is the output of `get_metadata_routing`.
724# `MetadataRouter` stores a collection of `RouterMappingPair` objects in its
725# `_route_mappings` attribute.
726RouterMappingPair = namedtuple("RouterMappingPair", ["mapping", "router"])
727
728# `MethodPair` is used to store a single method routing. `MethodMapping` stores a list
729# of `MethodPair` objects in its `_routes` attribute.
730MethodPair = namedtuple("MethodPair", ["caller", "callee"])
731
732
733class MethodMapping:
734    """Stores the mapping between caller and callee methods for a :term:`router`.
735
736    This class is primarily used in a ``get_metadata_routing()`` of a router
737    object when defining the mapping between the router's methods and a sub-object (a
738    sub-estimator or a scorer).
739
740    Iterating through an instance of this class yields
741    ``MethodPair(caller, callee)`` instances.
742
743    .. versionadded:: 1.3
744    """
745
746    def __init__(self):
747        self._routes = []
748
749    def __iter__(self):
750        return iter(self._routes)
751
752    def add(self, *, caller, callee):
753        """Add a method mapping.
754
755        Parameters
756        ----------
757
758        caller : str
759            Parent estimator's method name in which the ``callee`` is called.
760
761        callee : str
762            Child object's method name. This method is called in ``caller``.
763
764        Returns
765        -------
766        self : MethodMapping
767            Returns self.
768        """
769        if caller not in METHODS:
770            raise ValueError(
771                f"Given caller:{caller} is not a valid method. Valid methods are:"
772                f" {METHODS}"
773            )
774        if callee not in METHODS:
775            raise ValueError(
776                f"Given callee:{callee} is not a valid method. Valid methods are:"
777                f" {METHODS}"
778            )
779        self._routes.append(MethodPair(caller=caller, callee=callee))
780        return self
781
782    def _serialize(self):
783        """Serialize the object.
784
785        Returns
786        -------
787        obj : list
788            A serialized version of the instance in the form of a list.
789        """
790        result = list()
791        for route in self._routes:
792            result.append({"caller": route.caller, "callee": route.callee})
793        return result
794
795    def __repr__(self):
796        return str(self._serialize())
797
798    def __str__(self):
799        return str(repr(self))
800
801
802class MetadataRouter:
803    """Coordinates metadata routing for a :term:`router` object.
804
805    This class is used by :term:`meta-estimators` or functions that can route metadata,
806    to handle their metadata routing. Routing information is stored in a
807    dictionary-like structure of the form ``{"object_name":
808    RouterMappingPair(mapping, router)}``, where ``mapping``
809    is an instance of :class:`~sklearn.utils.metadata_routing.MethodMapping` and
810    ``router`` is either a
811    :class:`~sklearn.utils.metadata_routing.MetadataRequest` or another
812    :class:`~sklearn.utils.metadata_routing.MetadataRouter` instance.
813
814    .. versionadded:: 1.3
815
816    Parameters
817    ----------
818    owner : str
819        The name of the object to which these requests belong.
820    """
821
822    # this is here for us to use this attribute's value instead of doing
823    # `isinstance`` in our checks, so that we avoid issues when people vendor
824    # this file instead of using it directly from scikit-learn.
825    _type = "metadata_router"
826
827    def __init__(self, owner):
828        self._route_mappings = dict()
829        # `_self_request` is used if the router is also a consumer.
830        # _self_request, (added using `add_self_request()`) is treated
831        # differently from the other consumer objects which are stored in
832        # _route_mappings.
833        self._self_request = None
834        self.owner = owner
835
836    def add_self_request(self, obj):
837        """Add `self` (as a :term:`consumer`) to the `MetadataRouter`.
838
839        This method is used if the :term:`router` is also a :term:`consumer`, and hence
840        the router itself needs to be included in the routing. The passed object
841        can be an estimator or a
842        :class:`~sklearn.utils.metadata_routing.MetadataRequest`.
843
844        A router should add itself using this method instead of `add` since it
845        should be treated differently than the other consumer objects to which metadata
846        is routed by the router.
847
848        Parameters
849        ----------
850        obj : object
851            This is typically the router instance, i.e. `self` in a
852            ``get_metadata_routing()`` implementation. It can also be a
853            ``MetadataRequest`` instance.
854
855        Returns
856        -------
857        self : MetadataRouter
858            Returns `self`.
859        """
860        if getattr(obj, "_type", None) == "metadata_request":
861            self._self_request = deepcopy(obj)
862        elif hasattr(obj, "_get_metadata_request"):
863            self._self_request = deepcopy(obj._get_metadata_request())
864        else:
865            raise ValueError(
866                "Given `obj` is neither a `MetadataRequest` nor does it implement the"
867                " required API. Inheriting from `BaseEstimator` implements the required"
868                " API."
869            )
870        return self
871
872    def add(self, *, method_mapping, **objs):
873        """Add :term:`consumers <consumer>` to the `MetadataRouter`.
874
875        The estimators that consume metadata are passed as named objects along with a
876        method mapping, that defines how their methods relate to those of the
877        :term:`router`.
878
879        Parameters
880        ----------
881        method_mapping : MethodMapping
882            The mapping between the child (:term:`consumer`) and the parent's
883            (:term:`router`'s) methods.
884
885        **objs : dict
886            A dictionary of objects, whose requests are extracted by calling
887            :func:`~sklearn.utils.metadata_routing.get_routing_for_object` on them.
888
889        Returns
890        -------
891        self : MetadataRouter
892            Returns `self`.
893        """
894        method_mapping = deepcopy(method_mapping)
895
896        for name, obj in objs.items():
897            self._route_mappings[name] = RouterMappingPair(
898                mapping=method_mapping, router=get_routing_for_object(obj)
899            )
900        return self
901
902    def consumes(self, method, params):
903        """Check whether the given metadata is consumed by the given method.
904
905        .. versionadded:: 1.4
906
907        Parameters
908        ----------
909        method : str
910            The name of the method to check.
911
912        params : iterable of str
913            An iterable of parameters to check.
914
915        Returns
916        -------
917        consumed : set of str
918            A set of parameters which are consumed by the given method.
919        """
920        res = set()
921        if self._self_request:
922            res = res | self._self_request.consumes(method=method, params=params)
923
924        for _, route_mapping in self._route_mappings.items():
925            for caller, callee in route_mapping.mapping:
926                if caller == method:
927                    res = res | route_mapping.router.consumes(
928                        method=callee, params=params
929                    )
930
931        return res
932
933    def _get_param_names(self, *, method, return_alias, ignore_self_request):
934        """Get names of all metadata that can be consumed or routed by specified \
935            method.
936
937        This method returns the names of all metadata, even the ``False``
938        ones.
939
940        Parameters
941        ----------
942        method : str
943            The name of the method for which metadata names are requested.
944
945        return_alias : bool
946            Controls whether original or aliased names should be returned,
947            which only applies to the stored `self`. If no `self` routing
948            object is stored, this parameter has no effect.
949
950        ignore_self_request : bool
951            If `self._self_request` should be ignored. This is used in `_route_params`.
952            If ``True``, ``return_alias`` has no effect.
953
954        Returns
955        -------
956        names : set of str
957            A set of strings with the names of all metadata.
958        """
959        res = set()
960        if self._self_request and not ignore_self_request:
961            res = res.union(
962                self._self_request._get_param_names(
963                    method=method, return_alias=return_alias
964                )
965            )
966
967        for name, route_mapping in self._route_mappings.items():
968            for caller, callee in route_mapping.mapping:
969                if caller == method:
970                    res = res.union(
971                        route_mapping.router._get_param_names(
972                            method=callee, return_alias=True, ignore_self_request=False
973                        )
974                    )
975        return res
976
977    def _route_params(self, *, params, method, parent, caller):
978        """Prepare the given metadata to be passed to the method.
979
980        This is used when a router is used as a child object of another router.
981        The parent router then passes all parameters understood by the child
982        object to it and delegates their validation to the child.
983
984        The output of this method can be used directly as the input to the
985        corresponding method as **kwargs.
986
987        Parameters
988        ----------
989        params : dict
990            A dictionary of provided metadata.
991
992        method : str
993            The name of the method for which the metadata is requested and routed.
994
995        parent : object
996            Parent class object, that routes the metadata.
997
998        caller : str
999            Method from the parent class object, where the metadata is routed from.
1000
1001        Returns
1002        -------
1003        params : Bunch
1004            A :class:`~sklearn.utils.Bunch` of {metadata: value} which can be given to
1005            the corresponding method.
1006        """
1007        res = Bunch()
1008        if self._self_request:
1009            res.update(
1010                self._self_request._route_params(
1011                    params=params,
1012                    method=method,
1013                    parent=parent,
1014                    caller=caller,
1015                )
1016            )
1017
1018        param_names = self._get_param_names(
1019            method=method, return_alias=True, ignore_self_request=True
1020        )
1021        child_params = {
1022            key: value for key, value in params.items() if key in param_names
1023        }
1024        for key in set(res.keys()).intersection(child_params.keys()):
1025            # conflicts are okay if the passed objects are the same, but it's
1026            # an issue if they're different objects.
1027            if child_params[key] is not res[key]:
1028                raise ValueError(
1029                    f"In {self.owner}, there is a conflict on {key} between what is"
1030                    " requested for this estimator and what is requested by its"
1031                    " children. You can resolve this conflict by using an alias for"
1032                    " the child estimators' requested metadata."
1033                )
1034
1035        res.update(child_params)
1036        return res
1037
1038    def route_params(self, *, caller, params):
1039        """Get the values of metadata requested by :term:`consumers <consumer>`.
1040
1041        Returns a :class:`~sklearn.utils.Bunch` containing the metadata that this
1042        :term:`router`'s `caller` method needs to route, organized by each
1043        :term:`consumer` and their corresponding methods.
1044
1045        This can be used to pass the required metadata to corresponding methods in
1046        consumers.
1047
1048        Parameters
1049        ----------
1050        caller : str
1051            The name of the :term:`router`'s method through which the metadata is
1052            routed. For example, if called inside the :term:`fit` method of a router,
1053            this would be `"fit"`.
1054
1055        params : dict
1056            A dictionary of provided metadata.
1057
1058        Returns
1059        -------
1060        params : Bunch
1061            A :class:`~sklearn.utils.Bunch` of the form
1062            ``{"object_name": {"method_name": {metadata: value}}}``.
1063        """
1064        if self._self_request:
1065            self._self_request._check_warnings(params=params, method=caller)
1066
1067        res = Bunch()
1068        for name, route_mapping in self._route_mappings.items():
1069            router, mapping = route_mapping.router, route_mapping.mapping
1070
1071            res[name] = Bunch()
1072            for _caller, _callee in mapping:
1073                if _caller == caller:
1074                    res[name][_callee] = router._route_params(
1075                        params=params,
1076                        method=_callee,
1077                        parent=self.owner,
1078                        caller=caller,
1079                    )
1080        return res
1081
1082    def validate_metadata(self, *, method, params):
1083        """Validate given metadata for a method.
1084
1085        This raises a ``TypeError`` if some of the passed metadata are not
1086        understood by child objects.
1087
1088        Parameters
1089        ----------
1090        method : str
1091            The name of the :term:`router`'s method through which the metadata is
1092            routed. For example, if called inside the :term:`fit` method of a router,
1093            this would be `"fit"`.
1094
1095        params : dict
1096            A dictionary of provided metadata.
1097        """
1098        param_names = self._get_param_names(
1099            method=method, return_alias=False, ignore_self_request=False
1100        )
1101        if self._self_request:
1102            self_params = self._self_request._get_param_names(
1103                method=method, return_alias=False
1104            )
1105        else:
1106            self_params = set()
1107        extra_keys = set(params.keys()) - param_names - self_params
1108        if extra_keys:
1109            raise TypeError(
1110                f"{self.owner}.{method} got unexpected argument(s) {extra_keys}, which"
1111                " are not routed to any object."
1112            )
1113
1114    def _serialize(self):
1115        """Serialize the object.
1116
1117        Returns
1118        -------
1119        obj : dict
1120            A serialized version of the instance in the form of a dictionary.
1121        """
1122        res = dict()
1123        if self._self_request:
1124            res["$self_request"] = self._self_request._serialize()
1125        for name, route_mapping in self._route_mappings.items():
1126            res[name] = dict()
1127            res[name]["mapping"] = route_mapping.mapping._serialize()
1128            res[name]["router"] = route_mapping.router._serialize()
1129
1130        return res
1131
1132    def __iter__(self):
1133        if self._self_request:
1134            method_mapping = MethodMapping()
1135            for method in METHODS:
1136                method_mapping.add(caller=method, callee=method)
1137            yield (
1138                "$self_request",
1139                RouterMappingPair(mapping=method_mapping, router=self._self_request),
1140            )
1141        for name, route_mapping in self._route_mappings.items():
1142            yield (name, route_mapping)
1143
1144    def __repr__(self):
1145        return str(self._serialize())
1146
1147    def __str__(self):
1148        return str(repr(self))
1149
1150
1151def get_routing_for_object(obj=None):
1152    """Get a ``Metadata{Router, Request}`` instance from the given object.
1153
1154    This function returns a
1155    :class:`~sklearn.utils.metadata_routing.MetadataRouter` or a
1156    :class:`~sklearn.utils.metadata_routing.MetadataRequest` from the given input.
1157
1158    This function always returns a copy or an instance constructed from the
1159    input, such that changing the output of this function will not change the
1160    original object.
1161
1162    .. versionadded:: 1.3
1163
1164    Parameters
1165    ----------
1166    obj : object
1167        - If the object provides a `get_metadata_routing` method, return a copy
1168            of the output of that method.
1169        - If the object is already a
1170            :class:`~sklearn.utils.metadata_routing.MetadataRequest` or a
1171            :class:`~sklearn.utils.metadata_routing.MetadataRouter`, return a copy
1172            of that.
1173        - Returns an empty :class:`~sklearn.utils.metadata_routing.MetadataRequest`
1174            otherwise.
1175
1176    Returns
1177    -------
1178    obj : MetadataRequest or MetadataRouter
1179        A ``MetadataRequest`` or a ``MetadataRouter`` taken or created from
1180        the given object.
1181    """
1182    # doing this instead of a try/except since an AttributeError could be raised
1183    # for other reasons.
1184    if hasattr(obj, "get_metadata_routing"):
1185        return deepcopy(obj.get_metadata_routing())
1186
1187    elif getattr(obj, "_type", None) in ["metadata_request", "metadata_router"]:
1188        return deepcopy(obj)
1189
1190    return MetadataRequest(owner=None)
1191
1192
1193# Request method
1194# ==============
1195# This section includes what's needed for the `RequestMethod` descriptor and
1196# the dynamic generation of `set_{method}_request` methods in the `_MetadataRequester`
1197# mixin class.
1198
1199# These strings are used to dynamically generate the docstrings for the methods.
1200REQUESTER_DOC = """        Configure whether metadata should be requested to be \

Showing the first 1,200 of 1629 lines. Download the file for the rest.

Aluode/PerceptionLabPortable · CoolFace