There are 3 different people/codebases involved here:
- project A defines an extensible dispatch function called
f in module a.py.
- project B defines a new type
B and adds a dispatch rule for f(B(...)) in b.py.
- project C wants to be able to call the function
f and also wants to work with objects of type B.
The way I think that it should look is like:
# a.py
# f here will be the callable. The decorator extends it with
# dispatch rules but the object bound to `f` here is what any
# downstream code should import if it wants to call the
# function and have it dispatch to different types.
@dispatch
def f(arg: Any):
raise NotImplementedError
@f.register
def _(arg: int):
print("int")
Then B does:
# b.py
from a import f
class B:
...
@f.register
def _(arg: B):
print("B")
Now project C can do
# c.py
from a import f
from b import B
f(B())
Which imports would you propose to remove here?
If you mean that b.py should not need to have from a import f then I disagree. It sounds like you are proposing that we could instead have
# b.py
class B:
...
# The dispatch decorator collects this into a global registry based
# on the function name f so it is added to the same dispatch rules
# with the function f from a.py even though that function is not
# imported here
@dispatch
def f(arg: B):
print("B")
I don’t agree with using the function name globally like this. It is obvious that it will go wrong: two different projects could define a function with the same name and the dispatcher would mix them up. There has to be some way to associate the dispatch rule in b.py explicitly with the dispatch callable in a.py rather than just putting together all functions with the same name. I don’t see any way to do that without importing something somewhere.
It might be intentional that project B should be allowed to extend f by defining dispatch rules for the new type B that it provides. However we still need to ensure that importing f and B always loads all of the dispatch rules that are needed at runtime which means any that are defined in a.py as well as the extended rules defined in b.py. To make sure that this happens b.py needs to import something from a.py.
You could make a custom dispatcher but b.py would still have to import it:
# b.py
from a import f_dispatcher
class B:
...
@f_dispatcher
def _(arg: B):
print("B")
This is not any better than just doing from a import f and using @f.register.
Another point is that for project C that actually wants to call the function f it should always be the case that they import it from its original defined location in module a. They should not do from b import f and then call f(...). While the dispatch rules might be added by many different modules there should still be a single place from which the actual callable is imported for use.
Note that everything I have said above already applies to the functools.singledispatch decorator and is exactly how it already works.