chiark / gitweb /
documentation/python: adopt PEP585 naming for pybind11 typing hints.
authorVladimír Vondruš <mosra@centrum.cz>
Mon, 16 Sep 2024 16:33:20 +0000 (18:33 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Mon, 16 Sep 2024 16:40:55 +0000 (18:40 +0200)
This is what pybind11 2.12+ does, but as I have to parse the docstrings
and prepend the `typing.` namespace, I can retrospectively make this
change for older pybind11 versions as well. In particular, the following
is changed in type annotations of generated documentation, independently
of pybind11 version:

 - typing.Tuple is now tuple
 - typing.List is now list
 - typing.Dict is now dict
 - typing.Set is now set

If the python standard library inventory file is used, the type
annotations in signatures will now link to the builtin types instead of
the typing module.

Note that if your documentation uses external docstrings and matches
overloads by the signature, you may need to adapt the rename to make
them match again.

documentation/python.py
documentation/test_python/pybind_external_overload_docs/docs.rst
documentation/test_python/pybind_external_overload_docs/pybind_external_overload_docs.html
documentation/test_python/pybind_signatures/pybind_signatures.MyClass.html
documentation/test_python/pybind_signatures/pybind_signatures.html
documentation/test_python/pybind_type_links/pybind_type_links.cpp
documentation/test_python/pybind_type_links/pybind_type_links.html
documentation/test_python/test_pybind.py
documentation/test_python/test_search.py
package/ci/circleci.yml

index 176e0fd21a44ab578d5756bef414f254246b9a71..6c8274c0f937151ffec2d61ccf7130606e75a020 100755 (executable)
@@ -908,7 +908,14 @@ def _pybind11_extract_default_argument(string):
     raise SyntaxError("Unexpected end of `{}`".format(string))
 
 def _pybind_map_name_prefix_or_add_typing_suffix(state: State, input_type: str):
-    if input_type in ['Callable', 'Dict', 'Iterator', 'Iterable', 'List', 'Optional', 'Set', 'Tuple', 'Union']:
+    # As of pybind11 2.12, the names match https://peps.python.org/pep-0585/
+    # which replaces the original typing.List, Dict etc. with actual builtin
+    # types to avoid duplication. To make testing simpler, this tool makes them
+    # follow PEP585 with older pybind11 as well.
+    input_type_lowercase = input_type.lower()
+    if input_type_lowercase in ['dict', 'list', 'set', 'tuple']:
+        return input_type_lowercase
+    if input_type in ['Callable', 'Iterator', 'Iterable', 'Optional', 'Union']:
         return 'typing.' + input_type
     else:
         return map_name_prefix(state, input_type)
index 2499e017bff82b50f01b2dbfef58c2d6737e62ac..9a99c092de9e6f02d3c7a68a22f0fcea07348f5a 100644 (file)
@@ -1,10 +1,10 @@
-.. py:function:: pybind_external_overload_docs.foo(a: int, b: typing.Tuple[int, str])
+.. py:function:: pybind_external_overload_docs.foo(a: int, b: tuple[int, str])
     :param a: First parameter
     :param b: Second parameter
 
     Details for the first overload.
 
-.. py:function:: pybind_external_overload_docs.foo(arg0: typing.Callable[[float, typing.List[float]], int])
+.. py:function:: pybind_external_overload_docs.foo(arg0: typing.Callable[[float, list[float]], int])
     :param arg0: The caller
 
     Complex signatures in the second overload should be matched properly, too.
index f0f5411005908c2779b21009107294e3c5586e4e..44841302fe05c28c639327e148a9c6818395dbb5 100644 (file)
           <h2><a href="#functions">Functions</a></h2>
           <dl class="m-doc">
             <dt>
-              <span class="m-doc-wrap-bumper">def <a href="#foo-0a6d7" class="m-doc">foo</a>(</span><span class="m-doc-wrap">a: int,
-              b: typing.Tuple[int, str]) -&gt; None</span>
+              <span class="m-doc-wrap-bumper">def <a href="#foo-e6197" class="m-doc">foo</a>(</span><span class="m-doc-wrap">a: int,
+              b: tuple[int, str]) -&gt; None</span>
             </dt>
             <dd>First overload</dd>
             <dt>
-              <span class="m-doc-wrap-bumper">def <a href="#foo-515df" class="m-doc">foo</a>(</span><span class="m-doc-wrap">arg0: typing.Callable[[float, typing.List[float]], int]<span class="m-text m-dim">, /</span>) -&gt; None</span>
+              <span class="m-doc-wrap-bumper">def <a href="#foo-1a204" class="m-doc">foo</a>(</span><span class="m-doc-wrap">arg0: typing.Callable[[float, list[float]], int]<span class="m-text m-dim">, /</span>) -&gt; None</span>
             </dt>
             <dd>Second overload</dd>
             <dt>
         </section>
         <section>
           <h2>Function documentation</h2>
-          <section class="m-doc-details" id="foo-0a6d7"><div>
+          <section class="m-doc-details" id="foo-e6197"><div>
             <h3>
-              <span class="m-doc-wrap-bumper">def pybind_external_overload_docs.<wbr /></span><span class="m-doc-wrap"><span class="m-doc-wrap-bumper"><a href="#foo-0a6d7" class="m-doc-self">foo</a>(</span><span class="m-doc-wrap">a: int,
-              b: typing.Tuple[int, str]) -&gt; None</span></span>
+              <span class="m-doc-wrap-bumper">def pybind_external_overload_docs.<wbr /></span><span class="m-doc-wrap"><span class="m-doc-wrap-bumper"><a href="#foo-e6197" class="m-doc-self">foo</a>(</span><span class="m-doc-wrap">a: int,
+              b: tuple[int, str]) -&gt; None</span></span>
             </h3>
             <p>First overload</p>
             <table class="m-table m-fullwidth m-flat">
@@ -95,9 +95,9 @@
             </table>
 <p>Details for the first overload.</p>
           </div></section>
-          <section class="m-doc-details" id="foo-515df"><div>
+          <section class="m-doc-details" id="foo-1a204"><div>
             <h3>
-              <span class="m-doc-wrap-bumper">def pybind_external_overload_docs.<wbr /></span><span class="m-doc-wrap"><span class="m-doc-wrap-bumper"><a href="#foo-515df" class="m-doc-self">foo</a>(</span><span class="m-doc-wrap">arg0: typing.Callable[[float, typing.List[float]], int]<span class="m-text m-dim">, /</span>) -&gt; None</span></span>
+              <span class="m-doc-wrap-bumper">def pybind_external_overload_docs.<wbr /></span><span class="m-doc-wrap"><span class="m-doc-wrap-bumper"><a href="#foo-1a204" class="m-doc-self">foo</a>(</span><span class="m-doc-wrap">arg0: typing.Callable[[float, list[float]], int]<span class="m-text m-dim">, /</span>) -&gt; None</span></span>
             </h3>
             <p>Second overload</p>
             <table class="m-table m-fullwidth m-flat">
index f0664f3661884e7b071a9f85ea612352a7c4b8c5..bb4942ecbd9c60dbf151287c1136233ac92cf2ed 100644 (file)
             <dt id="instance_function">
               <span class="m-doc-wrap-bumper">def <a href="#instance_function" class="m-doc-self">instance_function</a>(</span><span class="m-doc-wrap">self,
               arg0: int,
-              arg1: str<span class="m-text m-dim">, /</span>) -&gt; typing.Tuple[float, int]</span>
+              arg1: str<span class="m-text m-dim">, /</span>) -&gt; tuple[float, int]</span>
             </dt>
             <dd>Instance method with positional-only args</dd>
             <dt id="instance_function_kwargs">
               <span class="m-doc-wrap-bumper">def <a href="#instance_function_kwargs" class="m-doc-self">instance_function_kwargs</a>(</span><span class="m-doc-wrap">self,
               hey: int,
-              what: str = &#x27;&lt;eh?&gt;&#x27;) -&gt; typing.Tuple[float, int]</span>
+              what: str = &#x27;&lt;eh?&gt;&#x27;) -&gt; tuple[float, int]</span>
             </dt>
             <dd>Instance method with position or keyword args</dd>
           </dl>
index 43ef942b50abf4ea20a659a8b640305e60f545dc..83ef7d4a5fe2dad23607dee7329df95901334f26 100644 (file)
@@ -98,7 +98,7 @@
             </dt>
             <dd>Scale an integer, kwargs</dd>
             <dt id="takes_a_function">
-              <span class="m-doc-wrap-bumper">def <a href="#takes_a_function" class="m-doc-self">takes_a_function</a>(</span><span class="m-doc-wrap">arg0: typing.Callable[[float, typing.List[float]], int]<span class="m-text m-dim">, /</span>) -&gt; None</span>
+              <span class="m-doc-wrap-bumper">def <a href="#takes_a_function" class="m-doc-self">takes_a_function</a>(</span><span class="m-doc-wrap">arg0: typing.Callable[[float, list[float]], int]<span class="m-text m-dim">, /</span>) -&gt; None</span>
             </dt>
             <dd>A function taking a Callable</dd>
             <dt id="takes_a_function_returning_none">
             </dt>
             <dd>A function taking a Callable that returns None</dd>
             <dt id="taking_a_list_returning_a_tuple">
-              <span class="m-doc-wrap-bumper">def <a href="#taking_a_list_returning_a_tuple" class="m-doc-self">taking_a_list_returning_a_tuple</a>(</span><span class="m-doc-wrap">arg0: typing.List[float]<span class="m-text m-dim">, /</span>) -&gt; typing.Tuple[int, int, int]</span>
+              <span class="m-doc-wrap-bumper">def <a href="#taking_a_list_returning_a_tuple" class="m-doc-self">taking_a_list_returning_a_tuple</a>(</span><span class="m-doc-wrap">arg0: list[float]<span class="m-text m-dim">, /</span>) -&gt; tuple[int, int, int]</span>
             </dt>
             <dd>Takes a list, returns a tuple</dd>
             <dt id="tenOverloads-fe11a">
index eed8fb5a3ace229b947b3b97b06e71f7ad4b513e..0d6e93408e86c0f5c298b5cc80cd7512da8e9155 100644 (file)
@@ -17,7 +17,7 @@ struct Foo {
 
 Foo typeReturn() { return {}; }
 
-void typeNested(const std::pair<Foo, std::vector<Enum>>&) {}
+void typeNested(const std::pair<Foo, std::vector<Enum>>&, const std::set<Enum>&, const std::map<int, Foo>&) {}
 
 void typeNestedEnumAndDefault(std::pair<int, Enum>) {}
 
index 09fc4d505230b4d293a9211ba6162249bee7185c..bb9afa3fd13015f2bf91eab8f0a09f95a43d1fb6 100644 (file)
             </dt>
             <dd>A function taking an enum with a default</dd>
             <dt id="type_nested">
-              <span class="m-doc-wrap-bumper">def <a href="#type_nested" class="m-doc-self">type_nested</a>(</span><span class="m-doc-wrap">arg0: <a href="https://docs.python.org/3/library/typing.html#typing.Tuple" class="m-doc-external">typing.Tuple</a>[<a href="pybind_type_links.Foo.html" class="m-doc">Foo</a>, <a href="https://docs.python.org/3/library/typing.html#typing.List" class="m-doc-external">typing.List</a>[<a href="pybind_type_links.html#Enum" class="m-doc">Enum</a>]]<span class="m-text m-dim">, /</span>) -&gt; <a href="https://docs.python.org/3/library/constants.html#None" class="m-doc-external">None</a></span>
+              <span class="m-doc-wrap-bumper">def <a href="#type_nested" class="m-doc-self">type_nested</a>(</span><span class="m-doc-wrap">arg0: <a href="https://docs.python.org/3/library/stdtypes.html#tuple" class="m-doc-external">tuple</a>[<a href="pybind_type_links.Foo.html" class="m-doc">Foo</a>, <a href="https://docs.python.org/3/library/stdtypes.html#list" class="m-doc-external">list</a>[<a href="pybind_type_links.html#Enum" class="m-doc">Enum</a>]],
+              arg1: <a href="https://docs.python.org/3/library/stdtypes.html#set" class="m-doc-external">set</a>[<a href="pybind_type_links.html#Enum" class="m-doc">Enum</a>],
+              arg2: <a href="https://docs.python.org/3/library/stdtypes.html#dict" class="m-doc-external">dict</a>[<a href="https://docs.python.org/3/library/functions.html#int" class="m-doc-external">int</a>, <a href="pybind_type_links.Foo.html" class="m-doc">Foo</a>]<span class="m-text m-dim">, /</span>) -&gt; <a href="https://docs.python.org/3/library/constants.html#None" class="m-doc-external">None</a></span>
             </dt>
             <dd>A function with nested type annotation</dd>
             <dt id="type_nested_enum_and_default">
-              <span class="m-doc-wrap-bumper">def <a href="#type_nested_enum_and_default" class="m-doc-self">type_nested_enum_and_default</a>(</span><span class="m-doc-wrap">value: <a href="https://docs.python.org/3/library/typing.html#typing.Tuple" class="m-doc-external">typing.Tuple</a>[<a href="https://docs.python.org/3/library/functions.html#int" class="m-doc-external">int</a>, <a href="pybind_type_links.html#Enum" class="m-doc">Enum</a>] = (3, Enum.FIRST)) -&gt; <a href="https://docs.python.org/3/library/constants.html#None" class="m-doc-external">None</a></span>
+              <span class="m-doc-wrap-bumper">def <a href="#type_nested_enum_and_default" class="m-doc-self">type_nested_enum_and_default</a>(</span><span class="m-doc-wrap">value: <a href="https://docs.python.org/3/library/stdtypes.html#tuple" class="m-doc-external">tuple</a>[<a href="https://docs.python.org/3/library/functions.html#int" class="m-doc-external">int</a>, <a href="pybind_type_links.html#Enum" class="m-doc">Enum</a>] = (3, Enum.FIRST)) -&gt; <a href="https://docs.python.org/3/library/constants.html#None" class="m-doc-external">None</a></span>
             </dt>
             <dd>A function taking a nested enum with a default. This won&#x27;t have a link.</dd>
             <dt id="type_return">
index 97e16da6956c1d17028317f4b8fc744d37ba1e2e..73db6017b6880b5d04e029340ec94320d0d2e4ed 100644 (file)
@@ -97,28 +97,43 @@ class Signature(unittest.TestCase):
             ], None, None))
 
     def test_square_brackets(self):
-        self.assertEqual(parse_pybind_signature(self.state, [],
-            'foo(a: Tuple[int, str], no_really: str) -> List[str]'),
-            ('foo', '', [
-                ('a', 'typing.Tuple[int, str]', 'typing.Tuple[int, str]', None),
-                ('no_really', 'str', 'str', None),
-            ], 'typing.List[str]', 'typing.List[str]'))
+        for i in [
+            # pybind11 2.11 and older
+            'foo(a: Tuple[int, str], no_really: str) -> List[str]',
+            # pybind11 2.12+
+            'foo(a: tuple[int, str], no_really: str) -> list[str]'
+        ]:
+            self.assertEqual(parse_pybind_signature(self.state, [], i),
+                ('foo', '', [
+                    ('a', 'tuple[int, str]', 'tuple[int, str]', None),
+                    ('no_really', 'str', 'str', None),
+                ], 'list[str]', 'list[str]'))
 
     def test_nested_square_brackets(self):
-        self.assertEqual(parse_pybind_signature(self.state, [],
-            'foo(a: Tuple[int, List[Tuple[int, int]]], another: float) -> Union[str, None]'),
-            ('foo', '', [
-                ('a', 'typing.Tuple[int, typing.List[typing.Tuple[int, int]]]', 'typing.Tuple[int, typing.List[typing.Tuple[int, int]]]', None),
-                ('another', 'float', 'float', None),
-            ], 'typing.Union[str, None]', 'typing.Union[str, None]'))
+        for i in [
+            # pybind11 2.11 and older
+            'foo(a: Tuple[int, Set[Tuple[int, int]]], another: float) -> Union[str, None]',
+            # pybind11 2.12+
+            'foo(a: tuple[int, set[tuple[int, int]]], another: float) -> Union[str, None]'
+        ]:
+            self.assertEqual(parse_pybind_signature(self.state, [], i),
+                ('foo', '', [
+                    ('a', 'tuple[int, set[tuple[int, int]]]', 'tuple[int, set[tuple[int, int]]]', None),
+                    ('another', 'float', 'float', None),
+                ], 'typing.Union[str, None]', 'typing.Union[str, None]'))
 
     def test_callable(self):
-        self.assertEqual(parse_pybind_signature(self.state, [],
-            'foo(a: Callable[[int, Tuple[int, int]], float], another: float)'),
-            ('foo', '', [
-                ('a', 'typing.Callable[[int, typing.Tuple[int, int]], float]', 'typing.Callable[[int, typing.Tuple[int, int]], float]', None),
-                ('another', 'float', 'float', None),
-            ], None, None))
+        for i in [
+            # pybind11 2.11 and older
+            'foo(a: Callable[[int, Dict[int, int]], float], another: float)',
+            # pybind11 2.12+
+            'foo(a: Callable[[int, dict[int, int]], float], another: float)'
+        ]:
+            self.assertEqual(parse_pybind_signature(self.state, [], i),
+                ('foo', '', [
+                    ('a', 'typing.Callable[[int, dict[int, int]], float]', 'typing.Callable[[int, dict[int, int]], float]', None),
+                    ('another', 'float', 'float', None),
+                ], None, None))
 
     def test_kwargs(self):
         self.assertEqual(parse_pybind_signature(self.state, [],
@@ -154,13 +169,17 @@ class Signature(unittest.TestCase):
                 ('b', None, None, '3'),
             ], None, None))
 
-        self.assertEqual(parse_pybind_signature(self.state, [],
-            'foo(a: Tuple[int, ...] = (1,("hello", \'world\'),3,4))'),
-            ('foo', '', [
-                ('a', 'typing.Tuple[int, ...]',
-                      'typing.Tuple[int, ...]',
-                 '(1,("hello", \'world\'),3,4)')
-            ], None, None))
+        for i in [
+            # pybind11 2.11 and older
+            'foo(a: Tuple[int, ...] = (1,("hello", \'world\'),3,4))',
+            # pybind11 2.12+
+            'foo(a: tuple[int, ...] = (1,("hello", \'world\'),3,4))',
+        ]:
+            self.assertEqual(parse_pybind_signature(self.state, [], i),
+                ('foo', '', [
+                    ('a', 'tuple[int, ...]', 'tuple[int, ...]',
+                        '(1,("hello", \'world\'),3,4)')
+                ], None, None))
 
         self.assertEqual(parse_pybind_signature(self.state, [],
             'foo(a: str = [dict(key="A", value=\'B\')["key"][0], None][0])'),
@@ -233,9 +252,18 @@ class Signature(unittest.TestCase):
 
     def test_bad_return_type(self):
         bad_signature = ('foo', '', [('…', None, None, None)], None, None)
-        self.assertEqual(parse_pybind_signature(self.state, [], 'foo() -> List[[]'), bad_signature)
-        self.assertEqual(parse_pybind_signature(self.state, [], 'foo() -> List]'), bad_signature)
-        self.assertEqual(parse_pybind_signature(self.state, [], 'foo() -> ::std::vector<int>'), bad_signature)
+        for i in [
+            # pybind11 2.11 and older
+            'foo() -> List[[]',
+            'foo() -> List]',
+            # pybind11 2.12+
+            'foo() -> list[[]',
+            'foo() -> list]',
+            # C++ leaked into the signature
+            'foo() -> ::std::vector<int>'
+        ]:
+            self.assertEqual(parse_pybind_signature(self.state, [], i),
+                bad_signature)
 
     def test_crazy_stuff(self):
         self.assertEqual(parse_pybind_signature(self.state, [],
@@ -243,9 +271,14 @@ class Signature(unittest.TestCase):
             ('foo', '', [('…', None, None, None)], None, None))
 
     def test_crazy_stuff_nested(self):
-        self.assertEqual(parse_pybind_signature(self.state, [],
-            'foo(a: int, b: List[Math::Vector<4, UnsignedInt>])'),
-            ('foo', '', [('…', None, None, None)], None, None))
+        for i in [
+            # pybind11 2.11 and older
+            'foo(a: int, b: List[Math::Vector<4, UnsignedInt>])',
+            # pybind11 2.12+
+            'foo(a: int, b: list[Math::Vector<4, UnsignedInt>])'
+        ]:
+            self.assertEqual(parse_pybind_signature(self.state, [], i),
+                ('foo', '', [('…', None, None, None)], None, None))
 
     def test_crazy_stuff_docs(self):
         self.assertEqual(parse_pybind_signature(self.state, [],
@@ -258,9 +291,14 @@ class Signature(unittest.TestCase):
             ('foo', '', [('…', None, None, None)], None, None))
 
     def test_crazy_return_nested(self):
-        self.assertEqual(parse_pybind_signature(self.state, [],
-            'foo(a: int) -> List[Math::Vector<4, UnsignedInt>]'),
-            ('foo', '', [('…', None, None, None)], None, None))
+        for i in [
+            # pybind11 2.11 and older
+            'foo(a: int) -> List[Math::Vector<4, UnsignedInt>]',
+            # pybind11 2.12+
+            'foo(a: int) -> list[Math::Vector<4, UnsignedInt>]'
+        ]:
+            self.assertEqual(parse_pybind_signature(self.state, [], i),
+                ('foo', '', [('…', None, None, None)], None, None))
 
     def test_crazy_return_docs(self):
         self.assertEqual(parse_pybind_signature(self.state, [],
@@ -276,10 +314,17 @@ class Signature(unittest.TestCase):
         state = copy.deepcopy(self.state)
         state.name_mapping['module._module'] = 'module'
 
-        self.assertEqual(parse_pybind_signature(state, [],
-            'foo(a: module._module.Foo, b: typing.Tuple[int, module._module.Bar]) -> module._module.Baz'),
-            ('foo', '', [('a', 'module.Foo', 'module.Foo', None),
-                         ('b', 'typing.Tuple[int, module.Bar]', 'typing.Tuple[int, module.Bar]', None)], 'module.Baz', 'module.Baz'))
+        for i in [
+                # pybind11 2.11 and older
+            'foo(a: module._module.Foo, b: Tuple[int, module._module.Bar]) -> module._module.Baz',
+                # pybind11 2.12+
+            'foo(a: module._module.Foo, b: tuple[int, module._module.Bar]) -> module._module.Baz'
+        ]:
+            self.assertEqual(parse_pybind_signature(state, [], i),
+                ('foo', '', [
+                    ('a', 'module.Foo', 'module.Foo', None),
+                    ('b', 'tuple[int, module.Bar]', 'tuple[int, module.Bar]', None)
+                ], 'module.Baz', 'module.Baz'))
 
 class Signatures(BaseInspectTestCase):
     def test_positional_args(self):
index 1263db8c2c516c70946349db4eafe86f9e4fdf87..f8a19fec6ce7c4b3240054e4b64a73b41a1853c7 100644 (file)
@@ -229,9 +229,9 @@ search_long_suffix_length [4]
 many_parameters [0, 2]
 |              ($
 |               ) [1, 3]
-0: .many_parameters(arg0: typing.Tuple[float, int, str, typing.List[…) [prefix=4[:30], suffix_length=53, type=FUNCTION] -> #many_parameters-06151
+0: .many_parameters(arg0: tuple[float, int, str, list[tuple[int, int…) [prefix=4[:30], suffix_length=53, type=FUNCTION] -> #many_parameters-a4d3e
 1:  [prefix=0[:52], suffix_length=51, type=FUNCTION] ->
-2: .many_parameters(arg0: typing.Tuple[int, float, str, typing.List[…) [prefix=4[:30], suffix_length=53, type=FUNCTION] -> #many_parameters-31300
+2: .many_parameters(arg0: tuple[int, float, str, list[tuple[int, int…) [prefix=4[:30], suffix_length=53, type=FUNCTION] -> #many_parameters-99883
 3:  [prefix=2[:52], suffix_length=51, type=FUNCTION] ->
 4: search_long_suffix_length [type=MODULE] -> search_long_suffix_length.html
 (EntryType.PAGE, CssClass.SUCCESS, 'page'),
index c406dee15fc910ef26fbaf69bdb7be7ab0b0b892..520910b77e6cee8a3631b4820690ab204dc31bc4 100644 (file)
@@ -314,7 +314,7 @@ jobs:
     - install-base:
         extra: graphviz cmake ninja-build wget
     - install-python-deps:
-        # NumPy 2.0 doesn't work with pybind 2.11, see below
+        # NumPy 2.0 doesn't work with pybind < 2.12
         numpy-version: ==1.26.4
         # Ubuntu 22.04 has pygments 2.11
         pygments-version: ==2.11.0
@@ -323,10 +323,8 @@ jobs:
     - test-plugins
     - test-documentation-themes:
         python-version: "3.10"
-        # 2.12.0 is the first that works with NumPy 2.0, unfortunately it has
-        # different docstring typing annotations to which I need to adapt tests
-        # first. Pinning to a version before together with NumPy 1.
-        pybind-version: "2.11.1"
+        # 2.9 series seems to be the first to work with Python 3.10
+        pybind-version: "2.9.2"
         # 1.9.1 is in Ubuntu 22.04 repos, HOWEVER both 1.9.1 and 1.9.2 link
         # against libclang-9.so.1, which is stupid. I think I even asked if the
         # builds could be fixed back then, but of course they didn't even
@@ -340,7 +338,7 @@ jobs:
     - install-base:
         extra: graphviz cmake ninja-build wget
     - install-python-deps:
-        # NumPy 2.0 doesn't work with pybind 2.11, see below
+        # NumPy 2.0 doesn't work with pybind < 2.12
         numpy-version: ==1.26.4
         # Ubuntu 24.04 has pygments 2.17
         pygments-version: ==2.17.0
@@ -349,9 +347,8 @@ jobs:
     - test-plugins
     - test-documentation-themes:
         python-version: "3.11"
-        # 2.12.0 is the first that works with NumPy 2.0, unfortunately it has
-        # different docstring typing annotations to which I need to adapt tests
-        # first. Pinning to a version before together with NumPy 1.
+        # 2.11.1 is the last that uses pre-PEP585 typing annotations, make sure
+        # it's explicitly tested
         pybind-version: "2.11.1"
         # 1.9.8 is in Ubuntu 24.04 repos
         doxygen-version: "1.9.8"
@@ -362,18 +359,15 @@ jobs:
     steps:
     - install-base:
         extra: graphviz cmake ninja-build wget
-    - install-python-deps:
-        # NumPy 2.0 doesn't work with pybind 2.11, see below
-        numpy-version: ==1.26.4
+    - install-python-deps
     - checkout
     - test-theme
     - test-plugins
     - test-documentation-themes:
         python-version: "3.12"
-        # 2.12.0 is the first that works with NumPy 2.0, unfortunately it has
-        # different docstring typing annotations to which I need to adapt tests
-        # first. Pinning to a version before together with NumPy 1.
-        pybind-version: "2.11.1"
+        # 2.12 series is the first that uses PEP585 and first that works with
+        # NumPy 2.0
+        pybind-version: "2.12.1"
         # 1.11 is the latest that passes all tests, 1.12 first needs
         # https://github.com/doxygen/doxygen/pull/11141 merged to be usable.
         doxygen-version: "1.11.0"