Skip to content

Conversation

@johnslavik
Copy link
Member

@johnslavik johnslavik commented Jan 6, 2026

A very practical but more general approach than GH-140255 to fixing annotation parsing in functools.singledispatch and functools.singledispatchmethod.

Aims to fix issues GH-84644, GH-130827, and GH-143886.

It can be broken if one uses a user-defined alternative implementation of staticmethod or something analogous.
Will break incorrect but working registrees. I haven't investigated stripping Annotated typeforms yet.

Consulting a test which fails with this fix at https://github.com/python/cpython/pull/130309/changes#r2663516538 -- I think that the test is wrong.

@johnslavik

This comment was marked as resolved.

@johnslavik
Copy link
Member Author

johnslavik commented Jan 8, 2026

I was asked to filter out tests that were helpful at the time of writing this code with detecting backward compatibility problems, but already pass on main. If we're interested in increasing coverage, they'll be recoverable from a commit that removes them.

I'll do it in the next few days.

In the meantime, the implementation on its own can be reviewed.

Copy link
Member

@JelleZijlstra JelleZijlstra left a comment

Choose a reason for hiding this comment

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

Looks reasonable to me!

@johnslavik
Copy link
Member Author

I'll do it in the next few days.

Done now.

Ready for a proper review.

The incorrect test was fixed separately; the merge resolves the conflict
and reduces the diff size.
return registry.get(match)

def _get_singledispatch_annotated_param(func, *, _inside_dispatchmethod=False):
"""Finds the first positional and user-specified parameter in a callable
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: I'd use the imperative form of the verb instead of the descriptive form:

Suggested change
"""Finds the first positional and user-specified parameter in a callable
"""Find the first positional and user-specified parameter in a callable

Copy link
Member Author

@johnslavik johnslavik Jan 16, 2026

Choose a reason for hiding this comment

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

Thanks. I had considered it and picked what was more consistent with the rest of the docstrings in this module. I'm open to changing to imperative form if it's preferred for new functionality.

all(isinstance(arg, type) for arg in cls.__args__))

def register(cls, func=None):
def register(cls, func=None, _inside_dispatchmethod=False):
Copy link
Member

Choose a reason for hiding this comment

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

Consider changing this from a boolean to a scope parameter, where scope is one of function or method. Such a form would be more descriptive and flexible for the possibility of additional scopes.

Copy link
Member Author

@johnslavik johnslavik Jan 16, 2026

Choose a reason for hiding this comment

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

I like this semantics.

scope sounds quite good as a name, but I reckon that I'd call it role or purpose, because any callable can be a standalone function or a method simply depending on its purpose in the code.

Copy link
Member Author

@johnslavik johnslavik Jan 16, 2026

Choose a reason for hiding this comment

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

OTOH, maybe we could change the parameter name to _skip_first_arg. That's what it's essentially doing.

I don't think this is likely there would exist any other scope / role / purpose than function (0) and method (1).

Comment on lines +3196 to +3208
with self.assertRaises(TypeError) as exc:
@i.register
def _() -> None:
return "My function doesn't take arguments"
self.assertStartsWith(str(exc.exception), msg_prefix)
self.assertEndsWith(str(exc.exception), "does not accept positional arguments.")

with self.assertRaises(TypeError) as exc:
@i.register
def _(*, foo: str) -> None:
return "My function takes keyword-only arguments"
self.assertStartsWith(str(exc.exception), msg_prefix)
self.assertEndsWith(str(exc.exception), "does not accept positional arguments.")
Copy link
Member

Choose a reason for hiding this comment

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

I appreciate the tests, but I feel like this change possible deserves its own test cases and not simply expansions of the existing test cases. I haven't looked deeply, so my instincts may be wrong here, but do consider creating independent tests where feasible and not too intrusive on the existing tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I'll keep this open. I'll appreciate additional feedback about the preferred way forward.


argname = _get_singledispatch_annotated_param(
func, _inside_dispatchmethod=_inside_dispatchmethod)
if argname is None:
Copy link
Member

Choose a reason for hiding this comment

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

Consider raising this exception in the call above, reducing the return signature of the method and keeping concerns together.

argname = _get_singledispatch_annotated_param(
func, _inside_dispatchmethod=_inside_dispatchmethod)
if argname is None:
raise TypeError(
Copy link
Member

Choose a reason for hiding this comment

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

What was the behavior of singledispatch before this change? Would it wrap the function and then fail at runtime when called? If so, that shift in behavior from import-time to run-time could cause problems for functions that were improperly wrapped but never called. Is that the intention of this change?

Copy link
Member

@jaraco jaraco left a comment

Choose a reason for hiding this comment

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

Overall, this looks good to me. I don't have enough ownership stake in the original implementation to say whether this change aligns with the goals of singledispatch, so I'd appreciate an additional approval from @rhettinger .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting merge needs backport to 3.13 bugs and security fixes needs backport to 3.14 bugs and security fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants