Aluode/PerceptionLabPortable
0
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 \
