Skip to content
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

Refactoring: Clarifying the responsabilities of each component #102

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

gabrieldemarmiesse
Copy link
Contributor

@gabrieldemarmiesse gabrieldemarmiesse commented Sep 4, 2023

  • The cache is maintained by the resolver now
  • The resolver is immutable, making it simpler to reason about. Only the cache can change inside the resolver.
  • A method registry is created. Functions can be added to it, but can never be removed, making it simpler to reason about.

Thus we simplify the whole logic of list swapping, cache invalidation and such. The resolver is fully in charge of doing whatever it sees fit with the inputs, without having other concepts getting in the way.

If one is brave enough, by changing only the Signature class + the resolver, it should be enough to support keyword arguments.

Closes #92

Copy link
Member

@wesselb wesselb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @gabrieldemarmiesse! Thanks for this PR!! Your changes make a lot of sense. I've left a few comments and suggestions. :)

self,
method: Optional[Callable[..., Any]] = None,
precedence: int = 0,
) -> Union[Function, Callable[[Callable[..., Any]], Function]]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gabrieldemarmiesse I wonder if this is the more accurate way to type what @dispatch does. What do you think of something like the following?

from typing import TypeVar, Protocol

F = TypeVar("F")

class FunctionProtocol(Function, Protocol[F]):
    __call__: F


    ...

    @overload
    def __call__(self, method: None, precedence: int = 0) -> Union[
        Callable[[F], FunctionProtocol[F]],
        Callable[[F, int], FunctionProtocol[F]],
    ]:
        ...

    def __call__(self, method: F, precedence: int = 0) -> FunctionProtocol[F]:
        ...

I'm not sure if that actually works, but it might better capture the spirit of @dispatch: retain what the function does originally, but add Function-functionality to it.

Comment on lines -128 to -142
try:
self._resolve_pending_registrations()
except NameError: # pragma: specific no cover 3.7 3.8 3.9
# When `staticmethod` is combined with
# `from __future__ import annotations`, in Python 3.10 and higher
# `staticmethod` will attempt to inherit `__doc__` (see
# https://docs.python.org/3/library/functions.html#staticmethod). Since
# we are still in class construction, forward references are not yet
# defined, so attempting to resolve all pending methods might fail with a
# `NameError`. This is fine, because later calling `__doc__` on the
# `staticmethod` will again call this `__doc__`, at which point all methods
# will resolve properly. For now, we just ignore the error and undo the
# partially completed :meth:`Function._resolve_pending_registrations` by
# clearing the cache.
self.clear_cache(reregister=False)
Copy link
Member

@wesselb wesselb Sep 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain why you've removed this? Do you think it's not at all necessary to resolve pending registration when getting the owner? I should've documented why that line was there in the first place... I think removing it might make sense, but I'm worried about subtly breaking something that's currently not tested for, so I just want to double check with you.

Comment on lines +287 to +288
assert len(g.methods) == 1
assert g.methods[0].implementation == f
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert len(g.methods) == 1
assert g.methods[0].implementation == f
assert len(g.methods) == 1
assert g.methods[0].implementation == f
assert g.methods[0].precedence == 0

assert len(f._pending) == 0
assert len(f._cache) == 0
f._resolver
assert f._methods_registry._resolver._cache == {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert f._methods_registry._resolver._cache == {}
assert not f._methods_registry._resolver._cache

(slightly more Pythonic)

self.signatures: List[Signature] = []
self.is_faithful: bool = True
def __init__(self, signatures: Iterable[Signature]):
signatures_dict = {hash(s): s for s in signatures}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh, I now understand that this is used to remove duplicate signatures. What I'm worried about is specifically which duplicate is kept. Specifically, if signatures implements the same method twice, then the later one should be kept. Right now, which implementation is kept is up to the implementation of the dictionary. I think it's safest to implement this in way that very explicitly retains the later implementations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also have this tested by a unit test!


def resolve(self, target: Union[Tuple[object, ...], Signature]) -> Signature:
def _resolve(self, target: Union[Tuple[object, ...], Signature]) -> Signature:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why make this a private method?

with pytest.raises(NotFoundLookupError):
# E: pyright(argument of type "float" cannot be assigned to parameter "x")
# E: mypy(no overload variant of "f" matches argument type "float")
f(1.0)
f(1.0, 2.0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f(1.0, 2.0)
f(1.0, 2.0)
def test_methods() -> None:
assert f.methods

How about adding in a unit test like this? That would test whether the typing for Dispatcher is right.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Would you be open to a bit of refactoring?
2 participants