-
-
Notifications
You must be signed in to change notification settings - Fork 26k
MNT refactoring in routing _MetadataRequester #31534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def __repr__(self): | ||
return "..." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since repr is now used in error messages, putting ...
here for simplicity. These lists are otherwise pretty ugly and large to print.
with pytest.raises( | ||
AttributeError, match="'MetadataRequest' object has no attribute 'other_method'" | ||
): | ||
TestDefaultsBadMethodName().get_metadata_routing() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking whether __metadata_request__method_name
actually has a valid method_name
doesn't exist anymore now. I don't think it's crucial to have. As a result, the test for the check is also removed.
@@ -1385,28 +1385,74 @@ def __init_subclass__(cls, **kwargs): | |||
.. [1] https://www.python.org/dev/peps/pep-0487 | |||
""" | |||
try: | |||
requests = cls._get_default_requests() | |||
for method in SIMPLE_METHODS: | |||
method_metadata_args = cls._get_metadata_args_and_aliases(method) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of building the whole default request object, we now only check for parameters which are to be included here in __init_subclass__
, which is what we actually need. The actual MetadataRequest
object is now only created when needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Co-authored-by: Omar Salman <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for walking me through these changes, @adrinjalali!
I have now looked it all through and have some comments that I hope will help with internal documentation of the new functionality.
My thought on design is: Could the value for owner
become a string early on when owner=self
gets passed to the MetadataRequest
and MethodMetadataRequest
object? We could check right there if we deal with a scorer and in this case manually create a string that serves the purpose of the visualisation in #31535. This would be simpler, I believe.
This PR is adding a lot of complexity to allow owner
to be either a string or an instance in order to access the instances' _routing_repr
and if there is another way, than this would be pretty cool.
Since the `owner` object used to be the type name (str), we return that string if | ||
the given `obj` is a string, otherwise we return the object's type name. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the `owner` object used to be the type name (str), we return that string if | |
the given `obj` is a string, otherwise we return the object's type name. | |
Since the `owner` object can be the type name (str), we return that string if | |
the given `obj` is a string, otherwise we return the object's type name. |
I think in documentation, there should not be any references to prior logic, only the current one.
params = list(inspect.signature(getattr(cls, method)).parameters.items())[1:] | ||
params = defaultdict( | ||
str, | ||
{ | ||
pname: None | ||
for pname, param in params | ||
if pname not in {"X", "y", "Y", "Xt", "yt"} | ||
and param.kind not in {param.VAR_POSITIONAL, param.VAR_KEYWORD} | ||
}, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion to rename some variables for better readability:
params = list(inspect.signature(getattr(cls, method)).parameters.items())[1:] | |
params = defaultdict( | |
str, | |
{ | |
pname: None | |
for pname, param in params | |
if pname not in {"X", "y", "Y", "Xt", "yt"} | |
and param.kind not in {param.VAR_POSITIONAL, param.VAR_KEYWORD} | |
}, | |
) | |
signature_items = list( | |
inspect.signature(getattr(cls, method)).parameters.items() | |
)[1:] | |
params = defaultdict( | |
str, | |
{ | |
param_name: None | |
for param_name, param_info in signature_items | |
if param_name not in {"X", "y", "Y", "Xt", "yt"} | |
and param_info.kind | |
not in {param_info.VAR_POSITIONAL, param_info.VAR_KEYWORD} | |
}, | |
) |
Or shorter sign_items
so there is no line break triggered by black.
super().__init_subclass__(**kwargs) | ||
|
||
@classmethod | ||
def _build_request_for_signature(cls, router, method): | ||
def _get_metadata_args_and_aliases(cls, method: str): | ||
"""Get the metadata arguments for a method.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"""Get the metadata arguments for a method.""" | |
"""Get the metadata request values for a method. | |
Returns a dict with the names of the metadata to be requested for `method` | |
and the corresponding request values (which can be among {True, False, | |
None} or a str for an alias). This method first populates the dict with a | |
`None` request value for every metadata present in `method`s signature and | |
may then overwrite these request values with request value from a | |
`__metadata_request__{method}` class attribute. | |
""" |
Maybe like this?
(Clarify what "metadata arguments" is and explain hierarchy of request values for the signatures versus the __metadata_request__{method}
.)
super().__init_subclass__(**kwargs) | ||
|
||
@classmethod | ||
def _build_request_for_signature(cls, router, method): | ||
def _get_metadata_args_and_aliases(cls, method: str): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it somehow unclear, what a "metadata arg" is. Would it make sense to rename this method into something like _get_metadata_request_values_and_aliases()
or, since the alias can be considered as a request value as well, _get_metadata_request_values()
maybe? In the docs for MethodMetadataRequest
, we call that same information "request info", which would also be an option.
# __metadata_request__* attributes take precedence over signature | ||
# sniffing. | ||
|
||
# need to go through the MRO since this is a class attribute and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# need to go through the MRO since this is a class attribute and | |
# need to go through the MRO since this is a classmethod and |
That makes it clearer that we are referring to this method with "this".
# Here we add request values specified via those class attributes | ||
# to the `MetadataRequest` object. Adding a request which already |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# Here we add request values specified via those class attributes
# to the `MetadataRequest` object.
I don't think we're really doing that... since we're only building a dict with information here. I am thinking, if anything, we're preparing to add something to a MethodMetadataRequest
, but not here directly, only after this method has returned.
@@ -1409,28 +1430,74 @@ def __init_subclass__(cls, **kwargs): | |||
.. [1] https://www.python.org/dev/peps/pep-0487 | |||
""" | |||
try: | |||
requests = cls._get_default_requests() | |||
for method in SIMPLE_METHODS: | |||
method_metadata_args = cls._get_metadata_args_and_aliases(method) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
method_metadata_args = cls._get_metadata_args_and_aliases(method)
Here too, I would suggest to rename method_metadata_args
into maybe method_metadata_request_values
or method_metadata_request_info
to give anyone reading this a hand in understanding what this is about.
for base_class in reversed(inspect.getmro(cls)): | ||
for attr, value in vars(base_class).items(): | ||
# we don't check for equivalence since python prefixes attrs | ||
# starting with __ with the `_ClassName`. | ||
if substr not in attr: | ||
continue | ||
for prop, alias in value.items(): | ||
# Here we add request values specified via those class attributes | ||
# to the `MetadataRequest` object. Adding a request which already | ||
# exists will override the previous one. Since we go through the | ||
# MRO in reverse order, the one specified by the lowest most classes | ||
# in the inheritance tree are the ones which take effect. | ||
if prop not in params and alias == UNUSED: | ||
raise ValueError( | ||
f"Trying to remove parameter {prop} with UNUSED which" | ||
" doesn't exist." | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, I wonder if it is required to loop over the whole mro, because as I understand we only need to check if the attribute is existing on the class that is the closest to the current cls
.
So, getattr(cls, f"__metadata_request__{method}")
(or similar) and then compare if substr
is a part of it isn't enough? Isn't getattr()
doing all the work for us by going through the mro while prioritising child classes?
Edit: I have tried this out in this comment.
|
||
return {param: alias for param, alias in params.items() if alias is not UNUSED} | ||
|
||
def _build_request_for_signature(self, method): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method doesn't take a router
argument anymore. The info about it should also be removed from the docstring, currently li.1509-1510. (I cannot comment there.)
"""Collect default request values. | ||
|
||
This method combines the information present in ``__metadata_request__*`` | ||
class attributes, as well as determining request keys from method | ||
signatures. | ||
""" | ||
requests = MetadataRequest(owner=cls.__name__) | ||
requests = MetadataRequest(owner=self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also add object
as a passable type to the owner
param in the docstring of MetadataRequest
and MethodMetadataRequest
too. It currently is owner : str
.
# need to go through the MRO since this is a class attribute and | ||
# ``vars`` doesn't report the parent class attributes. We go through | ||
# the reverse of the MRO so that child classes have precedence over | ||
# their parents. | ||
substr = f"__metadata_request__{method}" | ||
for base_class in reversed(inspect.getmro(cls)): | ||
for attr, value in vars(base_class).items(): | ||
# we don't check for equivalence since python prefixes attrs | ||
# starting with __ with the `_ClassName`. | ||
if substr not in attr: | ||
continue | ||
for prop, alias in value.items(): | ||
# Here we add request values specified via those class attributes | ||
# to the `MetadataRequest` object. Adding a request which already | ||
# exists will override the previous one. Since we go through the | ||
# MRO in reverse order, the one specified by the lowest most classes | ||
# in the inheritance tree are the ones which take effect. | ||
if prop not in params and alias == UNUSED: | ||
raise ValueError( | ||
f"Trying to remove parameter {prop} with UNUSED which" | ||
" doesn't exist." | ||
) | ||
|
||
params[prop] = alias |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tried this below and it fails in test_splitter_get_metadata_routing
, and in test_metadata_routed_to_group_splitter
(both for the same reason) because while getattr()
can access the parent's attributes, the name mangeling makes it a bit hard to know what the attributes' name is and then it is not found. That is not unsolvable though, please let me know what you think, @adrinjalali:
# need to go through the MRO since this is a class attribute and | |
# ``vars`` doesn't report the parent class attributes. We go through | |
# the reverse of the MRO so that child classes have precedence over | |
# their parents. | |
substr = f"__metadata_request__{method}" | |
for base_class in reversed(inspect.getmro(cls)): | |
for attr, value in vars(base_class).items(): | |
# we don't check for equivalence since python prefixes attrs | |
# starting with __ with the `_ClassName`. | |
if substr not in attr: | |
continue | |
for prop, alias in value.items(): | |
# Here we add request values specified via those class attributes | |
# to the `MetadataRequest` object. Adding a request which already | |
# exists will override the previous one. Since we go through the | |
# MRO in reverse order, the one specified by the lowest most classes | |
# in the inheritance tree are the ones which take effect. | |
if prop not in params and alias == UNUSED: | |
raise ValueError( | |
f"Trying to remove parameter {prop} with UNUSED which" | |
" doesn't exist." | |
) | |
params[prop] = alias | |
# update dict of metadata request values with info from | |
# `__metadata_request__{method}` class attributes, if these exist | |
try: | |
values = getattr(cls, f"_{cls.__name__}__metadata_request__{method}") | |
for prop, alias in values.items(): | |
if prop not in params and alias == UNUSED: | |
raise ValueError( | |
f"Trying to remove parameter {prop} with UNUSED which" | |
" doesn't exist." | |
) | |
params[prop] = alias | |
except AttributeError: | |
pass |
I am thinking of two possible solutions:
- Here in this method we could have a good guess of what the attribute's name is by using the mro without walking down it.
- Or, since we don't have so many splitters that accept groups, and we could also define the class attribute (
__metadata_request__split = {"groups": True}
) on them directly instead of viaGroupsConsumerMixin
.
That would (hopefully) make all the tests pass and also simplify the code?
The goal of this refactoring is to have the actual instance as the
owner
inMetadataRequest
object, which is needed for the work in visualising the routing (PR coming).As a consequence, the
repr
of the owners is used now in error messages instead, so the tests are fixed.