]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-143886: Ensure function annotations are returned in order of definition (#143888)
authorCarey Metcalfe <carey@cmetcalfe.ca>
Wed, 15 Apr 2026 13:24:28 +0000 (09:24 -0400)
committerGitHub <noreply@github.com>
Wed, 15 Apr 2026 13:24:28 +0000 (06:24 -0700)
Ensure function annotations are returned in order of definition

Previously, when getting type annotations of a function, normal
arguments were returned before positional-only ones in the dictionary.
Since `functools.singledispatch` relies on this ordering being correct
to dispatch based on the type of the first argument, this issue was
causing incorrect registrations for functions with positional-only
arguments.

This commit updates how annotations are generated so that
positional-only arguments are generated and added to the dictionary
before normal arguments.

Lib/test/test_functools.py
Lib/test/test_typing.py
Misc/NEWS.d/next/Core_and_Builtins/2026-01-15-13-37-21.gh-issue-143886.2gk5QC.rst [new file with mode: 0644]
Python/codegen.c

index efa85b564f7cdf806a1b5fccb29290881dd9f6ed..a8ee7d119e4bc6101aa29b4870cc513e1277408b 100644 (file)
@@ -3358,6 +3358,21 @@ class TestSingleDispatch(unittest.TestCase):
         with self.assertRaisesRegex(TypeError, msg):
             A().t(a=1)
 
+    def test_positional_only_argument(self):
+        @functools.singledispatch
+        def f(arg, /, extra):
+            return "base"
+        @f.register
+        def f_int(arg: int, /, extra: str):
+            return "int"
+        @f.register
+        def f_str(arg: str, /, extra: int):
+            return "str"
+
+        self.assertEqual(f(None, "extra"), "base")
+        self.assertEqual(f(1, "extra"), "int")
+        self.assertEqual(f("s", "extra"), "str")
+
     def test_union(self):
         @functools.singledispatch
         def f(arg):
index c6f08ff8a052abccc071382e88e0def3d37c7cab..9c0172f6ba7f23948343051113192cafb790c073 100644 (file)
@@ -7269,6 +7269,13 @@ class GetTypeHintsTests(BaseTestCase):
         self.assertEqual(TD.__annotations__, {'a': EqualToForwardRef('UniqueT', owner=TD, module=TD.__module__)})
         self.assertEqual(get_type_hints(TD), {'a': TD.__type_params__[0]})
 
+    def test_get_type_hints_order(self):
+        """Ensure that the order of function annotations matches the order they're defined"""
+        def f(positional: int, /, normal: str, *args: bytes, kwarg: list, **kwargs: bool) -> tuple:
+            pass
+
+        self.assertEqual(list(gth(f)), ["positional", "normal", "args", "kwarg", "kwargs", "return"])
+
 
 class GetUtilitiesTestCase(TestCase):
     def test_get_origin(self):
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-01-15-13-37-21.gh-issue-143886.2gk5QC.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-15-13-37-21.gh-issue-143886.2gk5QC.rst
new file mode 100644 (file)
index 0000000..fe4835e
--- /dev/null
@@ -0,0 +1,3 @@
+Reorder function annotations so positional-only arguments are returned
+before other arguments. This fixes how :func:`functools.singledispatch`
+registers functions with positional-only arguments.
index aca590d055f466037661bdda4fdafd073303b244..a0e5d9f659415eb0592e9d12805585a42462a060 100644 (file)
@@ -1130,10 +1130,10 @@ codegen_annotations_in_scope(compiler *c, location loc,
                              Py_ssize_t *annotations_len)
 {
     RETURN_IF_ERROR(
-        codegen_argannotations(c, args->args, annotations_len, loc));
+        codegen_argannotations(c, args->posonlyargs, annotations_len, loc));
 
     RETURN_IF_ERROR(
-        codegen_argannotations(c, args->posonlyargs, annotations_len, loc));
+        codegen_argannotations(c, args->args, annotations_len, loc));
 
     if (args->vararg && args->vararg->annotation) {
         RETURN_IF_ERROR(