Skip to content

Fix all mypy --strict errors in clang python binding #101784

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

TsXor
Copy link

@TsXor TsXor commented Aug 3, 2024

Related: #76664

I used metadata reflection so that we can import C library functions just by declaring annotated python functions. This makes C function types visible to type checker, then it's easy to fix most typing errors.

Copy link

github-actions bot commented Aug 3, 2024

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be
notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write
permissions for the repository. In which case you can instead tag reviewers by
name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review
by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate
is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot llvmbot added the clang Clang issues not falling into any other category label Aug 3, 2024
@llvmbot
Copy link
Member

llvmbot commented Aug 3, 2024

@llvm/pr-subscribers-clang

Author: None (TsXor)

Changes

Related: #76664

I used metadata reflection so that we can import C library functions just by declaring annotated python functions. This makes C function types visible to type checker, then it's easy to fix most typing errors.


Patch is 153.58 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/101784.diff

5 Files Affected:

  • (modified) clang/bindings/python/clang/cindex.py (+1145-751)
  • (added) clang/bindings/python/clang/ctyped.py (+334)
  • (modified) clang/bindings/python/tests/cindex/test_type.py (+1-1)
  • (added) clang/bindings/python/tests/ctyped/init.py ()
  • (added) clang/bindings/python/tests/ctyped/test_stub_conversion.py (+359)
diff --git a/clang/bindings/python/clang/cindex.py b/clang/bindings/python/clang/cindex.py
index 2038ef6045c7d..521dc2829ae41 100644
--- a/clang/bindings/python/clang/cindex.py
+++ b/clang/bindings/python/clang/cindex.py
@@ -62,36 +62,50 @@
 #
 # o implement additional SourceLocation, SourceRange, and File methods.
 
-from ctypes import *
+from ctypes import (c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, c_long,  # pyright: ignore[reportUnusedImport]
+                    c_ulong, c_longlong,c_ulonglong, c_size_t, c_ssize_t,  # pyright: ignore[reportUnusedImport]
+                    c_bool, c_char, c_wchar, c_float, c_double, c_longdouble,  # pyright: ignore[reportUnusedImport]
+                    c_char_p, c_wchar_p, c_void_p)  # pyright: ignore[reportUnusedImport]
+from ctypes import py_object, Structure, POINTER, byref, cast, cdll
+from .ctyped import *
+from .ctyped import ANNO_CONVERTIBLE, generate_metadata
 
 import os
 import sys
 from enum import Enum
 
 from typing import (
+    cast as tcast,
     Any,
     Callable,
+    Dict,
+    Generator,
     Generic,
+    Iterator,
+    List,
     Optional,
+    Tuple,
     Type as TType,
     TypeVar,
     TYPE_CHECKING,
     Union as TUnion,
 )
 
+from typing_extensions import Annotated
+
 if TYPE_CHECKING:
-    from ctypes import _Pointer
-    from typing_extensions import Protocol, TypeAlias
+    from typing_extensions import Protocol, Self, TypeAlias
+    from ctypes import CDLL
 
     StrPath: TypeAlias = TUnion[str, os.PathLike[str]]
-    LibFunc: TypeAlias = TUnion[
-        "tuple[str, Optional[list[Any]]]",
-        "tuple[str, Optional[list[Any]], Any]",
-        "tuple[str, Optional[list[Any]], Any, Callable[..., Any]]",
-    ]
-
+    StrOrBytes: TypeAlias = TUnion[str, bytes]
+    FsPath: TypeAlias = TUnion[StrOrBytes, os.PathLike[str]]
     TSeq = TypeVar("TSeq", covariant=True)
 
+    class SupportsReadStringData(Protocol):
+        def read(self) -> str | bytes:
+            ...
+
     class NoSliceSequence(Protocol[TSeq]):
         def __len__(self) -> int:
             ...
@@ -102,7 +116,7 @@ def __getitem__(self, key: int) -> TSeq:
 
 # Python 3 strings are unicode, translate them to/from utf8 for C-interop.
 class c_interop_string(c_char_p):
-    def __init__(self, p: str | bytes | None = None):
+    def __init__(self, p: 'CInteropString' = None):
         if p is None:
             p = ""
         if isinstance(p, str):
@@ -120,7 +134,7 @@ def value(self) -> str | None:  # type: ignore [override]
         return val.decode("utf8")
 
     @classmethod
-    def from_param(cls, param: str | bytes | None) -> c_interop_string:
+    def from_param(cls, param: 'CInteropString') -> c_interop_string:
         if isinstance(param, str):
             return cls(param)
         if isinstance(param, bytes):
@@ -136,6 +150,8 @@ def from_param(cls, param: str | bytes | None) -> c_interop_string:
     def to_python_string(x: c_interop_string, *args: Any) -> str | None:
         return x.value
 
+CInteropString = Annotated[TUnion[str, bytes, None], ANNO_CONVERTIBLE, c_interop_string]
+
 
 def b(x: str | bytes) -> bytes:
     if isinstance(x, bytes):
@@ -147,7 +163,8 @@ def b(x: str | bytes) -> bytes:
 # object. This is a problem, because it means that from_parameter will see an
 # integer and pass the wrong value on platforms where int != void*. Work around
 # this by marshalling object arguments as void**.
-c_object_p: TType[_Pointer[Any]] = POINTER(c_void_p)
+CObjectP = CPointer[c_void_p]
+c_object_p: TType[CObjectP] = convert_annotation(CObjectP)
 
 ### Exception Classes ###
 
@@ -183,7 +200,7 @@ class TranslationUnitSaveError(Exception):
     # Indicates that the translation unit was somehow invalid.
     ERROR_INVALID_TU = 3
 
-    def __init__(self, enumeration, message):
+    def __init__(self, enumeration: int, message: str):
         assert isinstance(enumeration, int)
 
         if enumeration < 1 or enumeration > 3:
@@ -241,7 +258,7 @@ def __del__(self) -> None:
         conf.lib.clang_disposeString(self)
 
     @staticmethod
-    def from_result(res: _CXString, fn: Any = None, args: Any = None) -> str:
+    def from_result(res: _CXString, fn: Optional[Callable[..., _CXString]] = None, args: Optional[Tuple[Any, ...]] = None) -> str:
         assert isinstance(res, _CXString)
         pystr: str | None = conf.lib.clang_getCString(res)
         if pystr is None:
@@ -255,71 +272,73 @@ class SourceLocation(Structure):
     """
 
     _fields_ = [("ptr_data", c_void_p * 2), ("int_data", c_uint)]
-    _data = None
+    _data: Optional[Tuple[Optional[File], int, int, int]] = None
 
-    def _get_instantiation(self):
+    def _get_instantiation(self) -> Tuple[Optional[File], int, int, int]:
         if self._data is None:
-            f, l, c, o = c_object_p(), c_uint(), c_uint(), c_uint()
+            fp, l, c, o = c_object_p(), c_uint(), c_uint(), c_uint()
             conf.lib.clang_getInstantiationLocation(
-                self, byref(f), byref(l), byref(c), byref(o)
+                self, byref(fp), byref(l), byref(c), byref(o)
             )
-            if f:
-                f = File(f)
+            if fp:
+                f = File(fp)
             else:
                 f = None
             self._data = (f, int(l.value), int(c.value), int(o.value))
         return self._data
 
     @staticmethod
-    def from_position(tu, file, line, column):
+    def from_position(tu: TranslationUnit, file: File, line: int, column: int) -> SourceLocation:
         """
         Retrieve the source location associated with a given file/line/column in
         a particular translation unit.
         """
-        return conf.lib.clang_getLocation(tu, file, line, column)  # type: ignore [no-any-return]
+        return conf.lib.clang_getLocation(tu, file, line, column)
 
     @staticmethod
-    def from_offset(tu, file, offset):
+    def from_offset(tu: TranslationUnit, file: File, offset: int) -> SourceLocation:
         """Retrieve a SourceLocation from a given character offset.
 
         tu -- TranslationUnit file belongs to
         file -- File instance to obtain offset from
         offset -- Integer character offset within file
         """
-        return conf.lib.clang_getLocationForOffset(tu, file, offset)  # type: ignore [no-any-return]
+        return conf.lib.clang_getLocationForOffset(tu, file, offset)
 
     @property
-    def file(self):
+    def file(self) -> Optional[File]:
         """Get the file represented by this source location."""
         return self._get_instantiation()[0]
 
     @property
-    def line(self):
+    def line(self) -> int:
         """Get the line represented by this source location."""
         return self._get_instantiation()[1]
 
     @property
-    def column(self):
+    def column(self) -> int:
         """Get the column represented by this source location."""
         return self._get_instantiation()[2]
 
     @property
-    def offset(self):
+    def offset(self) -> int:
         """Get the file offset represented by this source location."""
         return self._get_instantiation()[3]
 
     @property
-    def is_in_system_header(self):
+    def is_in_system_header(self) -> bool:
         """Returns true if the given source location is in a system header."""
-        return conf.lib.clang_Location_isInSystemHeader(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_Location_isInSystemHeader(self)
 
-    def __eq__(self, other):
-        return conf.lib.clang_equalLocations(self, other)  # type: ignore [no-any-return]
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, SourceLocation):
+            return NotImplemented
+        return conf.lib.clang_equalLocations(self, other)
 
-    def __ne__(self, other):
+    def __ne__(self, other: object) -> bool:
         return not self.__eq__(other)
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         if self.file:
             filename = self.file.name
         else:
@@ -346,40 +365,43 @@ class SourceRange(Structure):
     # FIXME: Eliminate this and make normal constructor? Requires hiding ctypes
     # object.
     @staticmethod
-    def from_locations(start, end):
-        return conf.lib.clang_getRange(start, end)  # type: ignore [no-any-return]
+    def from_locations(start: SourceLocation, end: SourceLocation) -> SourceRange:
+        return conf.lib.clang_getRange(start, end)
 
     @property
-    def start(self):
+    def start(self) -> SourceLocation:
         """
         Return a SourceLocation representing the first character within a
         source range.
         """
-        return conf.lib.clang_getRangeStart(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_getRangeStart(self)
 
     @property
-    def end(self):
+    def end(self) -> SourceLocation:
         """
         Return a SourceLocation representing the last character within a
         source range.
         """
-        return conf.lib.clang_getRangeEnd(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_getRangeEnd(self)
 
-    def __eq__(self, other):
-        return conf.lib.clang_equalRanges(self, other)  # type: ignore [no-any-return]
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, SourceRange):
+            return NotImplemented
+        return conf.lib.clang_equalRanges(self, other)
 
-    def __ne__(self, other):
+    def __ne__(self, other: object) -> bool:
         return not self.__eq__(other)
 
-    def __contains__(self, other):
+    def __contains__(self, other: object) -> bool:
         """Useful to detect the Token/Lexer bug"""
         if not isinstance(other, SourceLocation):
             return False
-        if other.file is None and self.start.file is None:
-            pass
-        elif (
-            self.start.file.name != other.file.name
-            or other.file.name != self.end.file.name
+        if (
+            other.file is not None
+            and self.start.file is not None
+            and self.end.file is not None
+            and (other.file.name != self.start.file.name
+                 or other.file.name != self.end.file.name)
         ):
             # same file name
             return False
@@ -396,7 +418,7 @@ def __contains__(self, other):
                 return True
         return False
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return "<SourceRange start %r, end %r>" % (self.start, self.end)
 
 
@@ -421,23 +443,25 @@ class Diagnostic:
     DisplayCategoryName = 0x20
     _FormatOptionsMask = 0x3F
 
-    def __init__(self, ptr):
+    ptr: CObjectP
+
+    def __init__(self, ptr: CObjectP):
         self.ptr = ptr
 
-    def __del__(self):
+    def __del__(self) -> None:
         conf.lib.clang_disposeDiagnostic(self)
 
     @property
-    def severity(self):
-        return conf.lib.clang_getDiagnosticSeverity(self)  # type: ignore [no-any-return]
+    def severity(self) -> int:
+        return conf.lib.clang_getDiagnosticSeverity(self)
 
     @property
-    def location(self):
-        return conf.lib.clang_getDiagnosticLocation(self)  # type: ignore [no-any-return]
+    def location(self) -> SourceLocation:
+        return conf.lib.clang_getDiagnosticLocation(self)
 
     @property
-    def spelling(self):
-        return conf.lib.clang_getDiagnosticSpelling(self)  # type: ignore [no-any-return]
+    def spelling(self) -> str:
+        return conf.lib.clang_getDiagnosticSpelling(self)
 
     @property
     def ranges(self) -> NoSliceSequence[SourceRange]:
@@ -451,7 +475,7 @@ def __len__(self) -> int:
             def __getitem__(self, key: int) -> SourceRange:
                 if key >= len(self):
                     raise IndexError
-                return conf.lib.clang_getDiagnosticRange(self.diag, key)  # type: ignore [no-any-return]
+                return conf.lib.clang_getDiagnosticRange(self.diag, key)
 
         return RangeIterator(self)
 
@@ -492,28 +516,28 @@ def __getitem__(self, key: int) -> Diagnostic:
         return ChildDiagnosticsIterator(self)
 
     @property
-    def category_number(self):
+    def category_number(self) -> int:
         """The category number for this diagnostic or 0 if unavailable."""
-        return conf.lib.clang_getDiagnosticCategory(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_getDiagnosticCategory(self)
 
     @property
-    def category_name(self):
+    def category_name(self) -> str:
         """The string name of the category for this diagnostic."""
-        return conf.lib.clang_getDiagnosticCategoryText(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_getDiagnosticCategoryText(self)
 
     @property
-    def option(self):
+    def option(self) -> str:
         """The command-line option that enables this diagnostic."""
-        return conf.lib.clang_getDiagnosticOption(self, None)  # type: ignore [no-any-return]
+        return conf.lib.clang_getDiagnosticOption(self, None)
 
     @property
-    def disable_option(self):
+    def disable_option(self) -> str:
         """The command-line option that disables this diagnostic."""
         disable = _CXString()
         conf.lib.clang_getDiagnosticOption(self, byref(disable))
         return _CXString.from_result(disable)
 
-    def format(self, options=None):
+    def format(self, options: Optional[int] = None) -> str:
         """
         Format this diagnostic for display. The options argument takes
         Diagnostic.Display* flags, which can be combined using bitwise OR. If
@@ -524,19 +548,19 @@ def format(self, options=None):
             options = conf.lib.clang_defaultDiagnosticDisplayOptions()
         if options & ~Diagnostic._FormatOptionsMask:
             raise ValueError("Invalid format options")
-        return conf.lib.clang_formatDiagnostic(self, options)  # type: ignore [no-any-return]
+        return conf.lib.clang_formatDiagnostic(self, options)
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return "<Diagnostic severity %r, location %r, spelling %r>" % (
             self.severity,
             self.location,
             self.spelling,
         )
 
-    def __str__(self):
+    def __str__(self) -> str:
         return self.format()
 
-    def from_param(self):
+    def from_param(self) -> CObjectP:
         return self.ptr
 
 
@@ -547,11 +571,14 @@ class FixIt:
     with the given value.
     """
 
-    def __init__(self, range, value):
+    range: SourceRange
+    value: str
+
+    def __init__(self, range: SourceRange, value: str):
         self.range = range
         self.value = value
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return "<FixIt range %r, value %r>" % (self.range, self.value)
 
 
@@ -570,16 +597,20 @@ class TokenGroup:
     You should not instantiate this class outside of this module.
     """
 
-    def __init__(self, tu, memory, count):
+    _tu: TranslationUnit
+    _memory: CPointer[Token]
+    _count: c_uint
+
+    def __init__(self, tu: TranslationUnit, memory: CPointer[Token], count: c_uint):
         self._tu = tu
         self._memory = memory
         self._count = count
 
-    def __del__(self):
+    def __del__(self) -> None:
         conf.lib.clang_disposeTokens(self._tu, self._memory, self._count)
 
     @staticmethod
-    def get_tokens(tu, extent):
+    def get_tokens(tu: TranslationUnit, extent: SourceRange) -> Generator[Token, None, None]:
         """Helper method to return all tokens in an extent.
 
         This functionality is needed multiple places in this module. We define
@@ -616,16 +647,16 @@ class BaseEnumeration(Enum):
     """
     Common base class for named enumerations held in sync with Index.h values.
     """
+    value: int # pyright: ignore[reportIncompatibleMethodOverride]
 
-
-    def from_param(self):
+    def from_param(self) -> int:
         return self.value
 
     @classmethod
-    def from_id(cls, id):
+    def from_id(cls, id: int) -> Self:
         return cls(id)
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return "%s.%s" % (
             self.__class__.__name__,
             self.name,
@@ -636,7 +667,7 @@ class TokenKind(BaseEnumeration):
     """Describes a specific type of a Token."""
 
     @classmethod
-    def from_value(cls, value):
+    def from_value(cls, value: int) -> Self:
         """Obtain a registered TokenKind instance from its value."""
         return cls.from_id(value)
 
@@ -653,45 +684,44 @@ class CursorKind(BaseEnumeration):
     """
 
     @staticmethod
-    def get_all_kinds():
+    def get_all_kinds() -> List[CursorKind]:
         """Return all CursorKind enumeration instances."""
         return list(CursorKind)
 
-    def is_declaration(self):
+    def is_declaration(self) -> bool:
         """Test if this is a declaration kind."""
-        return conf.lib.clang_isDeclaration(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_isDeclaration(self)
 
-    def is_reference(self):
+    def is_reference(self) -> bool:
         """Test if this is a reference kind."""
-        return conf.lib.clang_isReference(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_isReference(self)
 
-    def is_expression(self):
+    def is_expression(self) -> bool:
         """Test if this is an expression kind."""
-        return conf.lib.clang_isExpression(self)  # type: ignore [no-any-return]
-
-    def is_statement(self):
+        return conf.lib.clang_isExpression(self)
+    def is_statement(self) -> bool:
         """Test if this is a statement kind."""
-        return conf.lib.clang_isStatement(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_isStatement(self)
 
-    def is_attribute(self):
+    def is_attribute(self) -> bool:
         """Test if this is an attribute kind."""
-        return conf.lib.clang_isAttribute(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_isAttribute(self)
 
-    def is_invalid(self):
+    def is_invalid(self) -> bool:
         """Test if this is an invalid kind."""
-        return conf.lib.clang_isInvalid(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_isInvalid(self)
 
-    def is_translation_unit(self):
+    def is_translation_unit(self) -> bool:
         """Test if this is a translation unit kind."""
-        return conf.lib.clang_isTranslationUnit(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_isTranslationUnit(self)
 
-    def is_preprocessing(self):
+    def is_preprocessing(self) -> bool:
         """Test if this is a preprocessing kind."""
-        return conf.lib.clang_isPreprocessing(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_isPreprocessing(self)
 
-    def is_unexposed(self):
+    def is_unexposed(self) -> bool:
         """Test if this is an unexposed kind."""
-        return conf.lib.clang_isUnexposed(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_isUnexposed(self)
 
 
     ###
@@ -1555,7 +1585,7 @@ class Cursor(Structure):
     _fields_ = [("_kind_id", c_int), ("xdata", c_int), ("data", c_void_p * 3)]
 
     @staticmethod
-    def from_location(tu, location):
+    def from_location(tu: TranslationUnit, location: SourceLocation) -> Cursor:
         # We store a reference to the TU in the instance so the TU won't get
         # collected before the cursor.
         cursor = conf.lib.clang_getCursor(tu, location)
@@ -1563,54 +1593,56 @@ def from_location(tu, location):
 
         return cursor
 
-    def __eq__(self, other):
-        return conf.lib.clang_equalCursors(self, other)  # type: ignore [no-any-return]
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, Cursor):
+            return NotImplemented
+        return conf.lib.clang_equalCursors(self, other)
 
-    def __ne__(self, other):
+    def __ne__(self, other: object) -> bool:
         return not self.__eq__(other)
 
-    def is_definition(self):
+    def is_definition(self) -> bool:
         """
         Returns true if the declaration pointed at by the cursor is also a
         definition of that entity.
         """
-        return conf.lib.clang_isCursorDefinition(self)  # type: ignore [no-any-return]
+        return conf.lib.clang_isCursorDefinition(self)
 
-    def is_const_method(self):
+    def is_const_method(self) -> bool:
         """Returns True if the cursor ...
[truncated]

TsXor added 3 commits August 3, 2024 22:40
- fixed error handling of TranslationUnit.reparse
- fixed silly mistake in Config.function_exists
I tried many methods to work it around, but all end up breaking other tests. Seems that the best way is to modify the test.
@TsXor TsXor requested a review from DeinAlptraum as a code owner August 3, 2024 18:41
@DeinAlptraum
Copy link
Contributor

@TsXor thank you for your work!
I am generally all in favor of typing annotations, and I like that ctyped adds a stronger connection between the Python function definitions and the C library functions in terms of types.

That said, this PR is pretty massive and grows the Python bindings by about 800 lines of code (ignoring tests), which imo is quite a lot just to pass the strict type check. There are also a lot of refactoring and other changes in this PR that, while generally welcome, seem unrelated and should be factored out. In general, multiple smaller PRs are preferred over something of this size, to have clearer boundaries and ease reviewing.

For reference, I'd like to point out that I've also opened a PR for strict typing in #78114, which has been under review for a while.
With that out of the way: I'm still a very new contributor and since I've opened a PR for essentially the exact same thing, don't feel comfortable deciding on how to proceed with this. I'm summoning @Endilll: what are your thoughts on this?

@DeinAlptraum DeinAlptraum added the clang:as-a-library libclang and C++ API label Aug 3, 2024
Copy link

github-actions bot commented Aug 3, 2024

⚠️ Python code formatter, darker found issues in your code. ⚠️

You can test this locally with the following command:
darker --check --diff -r 98e4413a38f286147b863a6ead9625228ab0ec7d...c3ebad6a3447101cb307d5ca118d28d1b78b4dbe clang/bindings/python/clang/ctyped.py clang/bindings/python/tests/ctyped/__init__.py clang/bindings/python/tests/ctyped/test_stub_conversion.py clang/bindings/python/clang/cindex.py clang/bindings/python/tests/cindex/test_translation_unit.py clang/bindings/python/tests/cindex/test_type.py
View the diff from darker here.
--- clang/cindex.py	2024-08-03 14:40:40.000000 +0000
+++ clang/cindex.py	2024-08-03 23:15:21.593600 +0000
@@ -60,14 +60,33 @@
 # o cleanup ctypes wrapping, would be nice to separate the ctypes details more
 #   clearly, and hide from the external interface (i.e., help(cindex)).
 #
 # o implement additional SourceLocation, SourceRange, and File methods.
 
-from ctypes import (c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, c_long,  # pyright: ignore[reportUnusedImport]
-                    c_ulong, c_longlong,c_ulonglong, c_size_t, c_ssize_t,  # pyright: ignore[reportUnusedImport]
-                    c_bool, c_char, c_wchar, c_float, c_double, c_longdouble,  # pyright: ignore[reportUnusedImport]
-                    c_char_p, c_wchar_p, c_void_p)  # pyright: ignore[reportUnusedImport]
+from ctypes import (
+    c_byte,
+    c_ubyte,
+    c_short,
+    c_ushort,
+    c_int,
+    c_uint,
+    c_long,  # pyright: ignore[reportUnusedImport]
+    c_ulong,
+    c_longlong,
+    c_ulonglong,
+    c_size_t,
+    c_ssize_t,  # pyright: ignore[reportUnusedImport]
+    c_bool,
+    c_char,
+    c_wchar,
+    c_float,
+    c_double,
+    c_longdouble,  # pyright: ignore[reportUnusedImport]
+    c_char_p,
+    c_wchar_p,
+    c_void_p,
+)  # pyright: ignore[reportUnusedImport]
 from ctypes import py_object, Structure, POINTER, byref, cast, cdll
 from .ctyped import *
 from .ctyped import ANNO_CONVERTIBLE, generate_metadata
 
 import os
@@ -114,11 +133,11 @@
             ...
 
 
 # Python 3 strings are unicode, translate them to/from utf8 for C-interop.
 class c_interop_string(c_char_p):
-    def __init__(self, p: 'CInteropString' = None):
+    def __init__(self, p: "CInteropString" = None):
         if p is None:
             p = ""
         if isinstance(p, str):
             p = p.encode("utf8")
         super(c_char_p, self).__init__(p)
@@ -132,11 +151,11 @@
         if val is None:
             return None
         return val.decode("utf8")
 
     @classmethod
-    def from_param(cls, param: 'CInteropString') -> c_interop_string:
+    def from_param(cls, param: "CInteropString") -> c_interop_string:
         if isinstance(param, str):
             return cls(param)
         if isinstance(param, bytes):
             return cls(param)
         if param is None:
@@ -168,11 +187,11 @@
 
 ### Exception Classes ###
 
 
 class CXError(Exception):
-    '''Represents C error of type enum CXErrorCode.'''
+    """Represents C error of type enum CXErrorCode."""
 
     # A generic error code, no further details are available.
     #
     # Errors of this kind can get their own specific error codes in future
     # libclang versions.
@@ -297,11 +316,15 @@
 
     def __del__(self) -> None:
         conf.lib.clang_disposeString(self)
 
     @staticmethod
-    def from_result(res: _CXString, fn: Optional[Callable[..., _CXString]] = None, args: Optional[Tuple[Any, ...]] = None) -> str:
+    def from_result(
+        res: _CXString,
+        fn: Optional[Callable[..., _CXString]] = None,
+        args: Optional[Tuple[Any, ...]] = None,
+    ) -> str:
         assert isinstance(res, _CXString)
         pystr: str | None = conf.lib.clang_getCString(res)
         if pystr is None:
             return ""
         return pystr
@@ -329,11 +352,13 @@
                 f = None
             self._data = (f, int(l.value), int(c.value), int(o.value))
         return self._data
 
     @staticmethod
-    def from_position(tu: TranslationUnit, file: File, line: int, column: int) -> SourceLocation:
+    def from_position(
+        tu: TranslationUnit, file: File, line: int, column: int
+    ) -> SourceLocation:
         """
         Retrieve the source location associated with a given file/line/column in
         a particular translation unit.
         """
         return conf.lib.clang_getLocation(tu, file, line, column)
@@ -445,12 +470,14 @@
             return False
         if (
             other.file is not None
             and self.start.file is not None
             and self.end.file is not None
-            and (other.file.name != self.start.file.name
-                 or other.file.name != self.end.file.name)
+            and (
+                other.file.name != self.start.file.name
+                or other.file.name != self.end.file.name
+            )
         ):
             # same file name
             return False
         # same file, in between lines
         if self.start.line < other.line < self.end.line:
@@ -655,11 +682,13 @@
 
     def __del__(self) -> None:
         conf.lib.clang_disposeTokens(self._tu, self._memory, self._count)
 
     @staticmethod
-    def get_tokens(tu: TranslationUnit, extent: SourceRange) -> Generator[Token, None, None]:
+    def get_tokens(
+        tu: TranslationUnit, extent: SourceRange
+    ) -> Generator[Token, None, None]:
         """Helper method to return all tokens in an extent.
 
         This functionality is needed multiple places in this module. We define
         it here because it seems like a logical place.
         """
@@ -692,11 +721,12 @@
 ### Cursor Kinds ###
 class BaseEnumeration(Enum):
     """
     Common base class for named enumerations held in sync with Index.h values.
     """
-    value: int # pyright: ignore[reportIncompatibleMethodOverride]
+
+    value: int  # pyright: ignore[reportIncompatibleMethodOverride]
 
     def from_param(self) -> int:
         return self.value
 
     @classmethod
@@ -744,10 +774,11 @@
         return conf.lib.clang_isReference(self)
 
     def is_expression(self) -> bool:
         """Test if this is an expression kind."""
         return conf.lib.clang_isExpression(self)
+
     def is_statement(self) -> bool:
         """Test if this is a statement kind."""
         return conf.lib.clang_isStatement(self)
 
     def is_attribute(self) -> bool:
@@ -2214,11 +2245,13 @@
         Retrieve the width of a bitfield.
         """
         return conf.lib.clang_getFieldDeclBitWidth(self)
 
     @staticmethod
-    def from_result(res: Cursor, fn: Callable[..., Cursor], args: Tuple[Any, ...]) -> Optional[Cursor]:
+    def from_result(
+        res: Cursor, fn: Callable[..., Cursor], args: Tuple[Any, ...]
+    ) -> Optional[Cursor]:
         assert isinstance(res, Cursor)
         # FIXME: There should just be an isNull method.
         if res == conf.lib.clang_getNullCursor():
             return None
 
@@ -2238,11 +2271,13 @@
 
         res._tu = tu
         return res
 
     @staticmethod
-    def from_cursor_result(res: Cursor, fn: Callable[..., Cursor], args: Tuple[Any, ...]) -> Optional[Cursor]:
+    def from_cursor_result(
+        res: Cursor, fn: Callable[..., Cursor], args: Tuple[Any, ...]
+    ) -> Optional[Cursor]:
         assert isinstance(res, Cursor)
         if res == conf.lib.clang_getNullCursor():
             return None
 
         res._tu = args[0]._tu
@@ -2527,11 +2562,13 @@
 
                 return self.length
 
             def __getitem__(self, key: int) -> Type:
                 # FIXME Support slice objects.
-                if not isinstance(key, int): # pyright: ignore[reportUnnecessaryIsInstance]
+                if not isinstance(
+                    key, int
+                ):  # pyright: ignore[reportUnnecessaryIsInstance]
                     raise TypeError("Must supply a non-negative int.")
 
                 if key < 0:
                     raise IndexError("Only non-negative indexes are accepted.")
 
@@ -2580,11 +2617,11 @@
     @property
     def translation_unit(self) -> TranslationUnit:
         """The TranslationUnit to which this Type is associated."""
         # If this triggers an AttributeError, the instance was not properly
         # instantiated.
-        return self._tu # type: ignore[no-any-return]
+        return self._tu  # type: ignore[no-any-return]
 
     @staticmethod
     def from_result(res: Type, fn: Callable[..., Type], args: Tuple[Any, ...]) -> Type:
         assert isinstance(res, Type)
 
@@ -2728,11 +2765,11 @@
 
         def visitor(field: Cursor, children: List[Cursor]) -> int:
             assert field != conf.lib.clang_getNullCursor()
 
             # Create reference to TU so it isn't GC'd before Cursor.
-            field._tu = self._tu # pyright: ignore[reportPrivateUsage]
+            field._tu = self._tu  # pyright: ignore[reportPrivateUsage]
             fields.append(field)
             return 1  # continue
 
         fields: List[Cursor] = []
         conf.lib.clang_Type_visitFields(self, fields_visit_callback(visitor), fields)
@@ -2751,11 +2788,11 @@
     def spelling(self) -> str:
         """Retrieve the spelling of this Type."""
         return conf.lib.clang_getTypeSpelling(self)
 
     def __eq__(self, other: object) -> bool:
-        if other is None: # in case user write `x.type == None`
+        if other is None:  # in case user write `x.type == None`
             return False
         elif not isinstance(other, Type):
             return NotImplemented
         else:
             return conf.lib.clang_equalTypes(self, other)
@@ -2774,21 +2811,25 @@
 class ClangObject:
     """
     A helper for Clang objects. This class helps act as an intermediary for
     the ctypes library and the Clang CIndex library.
     """
+
     obj: CObjectP
     _as_parameter_: CObjectP
 
     def __init__(self, obj: CObjectP):
         assert isinstance(obj, c_object_p) and obj
         self.obj = self._as_parameter_ = obj
 
     def from_param(self) -> CObjectP:
         return self._as_parameter_
 
-ClangObjectParam = Annotated[TUnion[CObjectP, ClangObject], ANNO_CONVERTIBLE, c_object_p]
+
+ClangObjectParam = Annotated[
+    TUnion[CObjectP, ClangObject], ANNO_CONVERTIBLE, c_object_p
+]
 
 
 class _CXUnsavedFile(Structure):
     """Helper for passing unsaved file arguments."""
 
@@ -2796,11 +2837,14 @@
 
     name: r_char_p
     contents: r_char_p
     length: r_ulong
 
-UnsavedFileInfo: TypeAlias = Tuple['FsPath', TUnion['StrOrBytes', 'SupportsReadStringData']]
+
+UnsavedFileInfo: TypeAlias = Tuple[
+    "FsPath", TUnion["StrOrBytes", "SupportsReadStringData"]
+]
 
 # Functions calls through the python interface are rather slow. Fortunately,
 # for most symboles, we do not need to perform a function call. Their spelling
 # never changes and is consequently provided by this spelling cache.
 SpellingCache = {
@@ -2955,15 +2999,15 @@
 
     @property
     def brief_comment(self) -> str:
         if conf.function_exists("clang_getCompletionBriefComment"):
             return conf.lib.clang_getCompletionBriefComment(self.obj)
-        return ''
+        return ""
 
     def __repr__(self) -> str:
         return (
-            " | ".join([str(a) for a in self]) # type: ignore[attr-defined]
+            " | ".join([str(a) for a in self])  # type: ignore[attr-defined]
             + " || Priority: "
             + str(self.priority)
             + " || Availability: "
             + str(self.availability)
             + " || Brief comment: "
@@ -3010,20 +3054,22 @@
         if len(self) <= key:
             raise IndexError
         # FIXME: Current type stub of ctypes does not provide signature of
         #        __getitem__ in class _Pointer. Remove this ignore when they
         #        fixed that.
-        return self.results[key] # type: ignore[no-any-return]
+        return self.results[key]  # type: ignore[no-any-return]
 
 
 class CodeCompletionResults(ClangObject):
-    obj: CPointer[CCRStructure] # type: ignore[assignment]
-    _as_parameter_: CPointer[CCRStructure] # type: ignore[assignment]
+    obj: CPointer[CCRStructure]  # type: ignore[assignment]
+    _as_parameter_: CPointer[CCRStructure]  # type: ignore[assignment]
 
     def __init__(self, ptr: CPointer[CCRStructure]):
         assert isinstance(ptr, POINTER(CCRStructure)) and ptr
-        self.obj = self._as_parameter_ = ptr # pyright: ignore[reportIncompatibleVariableOverride]
+        self.obj = (
+            self._as_parameter_
+        ) = ptr  # pyright: ignore[reportIncompatibleVariableOverride]
 
     def __del__(self) -> None:
         conf.lib.clang_disposeCodeCompleteResults(self)
 
     @property
@@ -3069,11 +3115,12 @@
     def read(self, path: FsPath) -> TranslationUnit:
         """Load a TranslationUnit from the given AST file."""
         return TranslationUnit.from_ast_file(path, self)
 
     def parse(
-        self, path: Optional[FsPath],
+        self,
+        path: Optional[FsPath],
         args: Optional[List[str]] = None,
         unsaved_files: Optional[List[UnsavedFileInfo]] = None,
         options: int = 0,
     ) -> TranslationUnit:
         """Load the translation unit from the given source code file by running
@@ -3131,30 +3178,33 @@
     PARSE_INCLUDE_BRIEF_COMMENTS_IN_CODE_COMPLETION = 128
 
     index: Index
 
     @staticmethod
-    def process_unsaved_files(unsaved_files: List[UnsavedFileInfo]) -> Optional[CArray[_CXUnsavedFile]]:
+    def process_unsaved_files(
+        unsaved_files: List[UnsavedFileInfo],
+    ) -> Optional[CArray[_CXUnsavedFile]]:
         unsaved_array = None
         if len(unsaved_files):
             unsaved_array = (_CXUnsavedFile * len(unsaved_files))()
             for i, (name, contents) in enumerate(unsaved_files):
                 if hasattr(contents, "read"):
-                    contents = tcast('SupportsReadStringData', contents).read()
-                binary_contents = b(tcast('StrOrBytes', contents))
+                    contents = tcast("SupportsReadStringData", contents).read()
+                binary_contents = b(tcast("StrOrBytes", contents))
                 unsaved_array[i].name = b(os.fspath(name))
                 unsaved_array[i].contents = binary_contents
                 unsaved_array[i].length = len(binary_contents)
         return unsaved_array
 
     @classmethod
     def from_source(
-        cls, filename: Optional[FsPath],
+        cls,
+        filename: Optional[FsPath],
         args: Optional[List[str]] = None,
         unsaved_files: Optional[List[UnsavedFileInfo]] = None,
         options: int = 0,
-        index: Optional[Index] = None
+        index: Optional[Index] = None,
     ) -> Self:
         """Create a TranslationUnit by parsing source.
 
         This is capable of processing source code both from files on the
         filesystem as well as in-memory contents.
@@ -3277,11 +3327,16 @@
         this sequence is always the input file. Note that this method will not
         recursively iterate over header files included through precompiled
         headers.
         """
 
-        def visitor(fobj: CObjectP, lptr: CPointer[SourceLocation], depth: int, includes: List[FileInclusion]) -> None:
+        def visitor(
+            fobj: CObjectP,
+            lptr: CPointer[SourceLocation],
+            depth: int,
+            includes: List[FileInclusion],
+        ) -> None:
             if depth > 0:
                 loc = lptr.contents
                 includes.append(FileInclusion(loc.file, File(fobj), loc, depth))
 
         # Automatically adapt CIndex/ctype pointers to python objects
@@ -3295,11 +3350,13 @@
     def get_file(self, filename: FsPath) -> File:
         """Obtain a File from this translation unit."""
 
         return File.from_name(self, filename)
 
-    def get_location(self, filename: FsPath, position: Tuple[int, int]) -> SourceLocation:
+    def get_location(
+        self, filename: FsPath, position: Tuple[int, int]
+    ) -> SourceLocation:
         """Obtain a SourceLocation for a file in this translation unit.
 
         The position can be specified by passing:
 
           - Integer file offset. Initial file offset is 0.
@@ -3312,11 +3369,14 @@
             return SourceLocation.from_offset(self, f, position)
 
         return SourceLocation.from_position(self, f, position[0], position[1])
 
     _Location = TUnion[int, Tuple[int, int], SourceLocation]
-    def get_extent(self, filename: FsPath, locations: Tuple[_Location, _Location]) -> SourceRange:
+
+    def get_extent(
+        self, filename: FsPath, locations: Tuple[_Location, _Location]
+    ) -> SourceRange:
         """Obtain a SourceRange from this translation unit.
 
         The bounds of the SourceRange must ultimately be defined by a start and
         end SourceLocation. For the locations argument, you can pass:
 
@@ -3378,11 +3438,13 @@
                     raise IndexError
                 return Diagnostic(diag)
 
         return DiagIterator(self)
 
-    def reparse(self, unsaved_files: Optional[List[UnsavedFileInfo]] = None, options: int = 0) -> None:
+    def reparse(
+        self, unsaved_files: Optional[List[UnsavedFileInfo]] = None, options: int = 0
+    ) -> None:
         """
         Reparse an already parsed translation unit.
 
         In-memory contents for files can be provided by passing a list of pairs
         as unsaved_files, the first items should be the filenames to be mapped
@@ -3395,11 +3457,11 @@
         unsaved_files_array = self.process_unsaved_files(unsaved_files)
         result = conf.lib.clang_reparseTranslationUnit(
             self, len(unsaved_files), unsaved_files_array, options
         )
         if result != 0:
-            raise CXError(result, 'Error reparsing TranslationUnit.')
+            raise CXError(result, "Error reparsing TranslationUnit.")
 
     def save(self, filename: FsPath) -> None:
         """Saves the TranslationUnit to a file.
 
         This is equivalent to passing -emit-ast to the clang frontend. The
@@ -3413,13 +3475,11 @@
         TranslationUnit.diagnostics().
 
         filename -- The path to save the translation unit to (str or PathLike).
         """
         options = conf.lib.clang_defaultSaveOptions(self)
-        result = conf.lib.clang_saveTranslationUnit(
-            self, os.fspath(filename), options
-        )
+        result = conf.lib.clang_saveTranslationUnit(self, os.fspath(filename), options)
         if result != 0:
             raise TranslationUnitSaveError(result, "Error saving TranslationUnit.")
 
     def codeComplete(
         self,
@@ -3465,25 +3525,31 @@
         )
         if ptr:
             return CodeCompletionResults(ptr)
         return None
 
-    def get_tokens(self, locations: Optional[Tuple[SourceLocation, SourceLocation]] = None, extent: Optional[SourceRange] = None) -> Generator[Token, None, None]:
+    def get_tokens(
+        self,
+        locations: Optional[Tuple[SourceLocation, SourceLocation]] = None,
+        extent: Optional[SourceRange] = None,
+    ) -> Generator[Token, None, None]:
         """Obtain tokens in this translation unit.
 
         This is a generator for Token instances. The caller specifies a range
         of source code to obtain tokens for. The range can be specified as a
         2-tuple of SourceLocation or as a SourceRange. If both are defined,
         behavior is undefined.
         """
-        
+
         if locations is not None:
-            final_extent = SourceRange.from_locations(start=locations[0], end=locations[1])
+            final_extent = SourceRange.from_locations(
+                start=locations[0], end=locations[1]
+            )
         elif extent is not None:
             final_extent = extent
         else:
-            raise ValueError('no extent given')
+            raise ValueError("no extent given")
 
         return TokenGroup.get_tokens(self, final_extent)
 
 
 class File(ClangObject):
@@ -3516,13 +3582,15 @@
 
     def __repr__(self) -> str:
         return "<File: %s>" % (self.name)
 
     @staticmethod
-    def from_result(res: CObjectP, fn: Callable[..., CObjectP], args: Tuple[Any, ...]) -> File:
+    def from_result(
+        res: CObjectP, fn: Callable[..., CObjectP], args: Tuple[Any, ...]
+    ) -> File:
         assert isinstance(res, c_object_p)
-        resobj = File(res) # pyright: ignore
+        resobj = File(res)  # pyright: ignore
 
         # Copy a reference to the TranslationUnit to prevent premature GC.
         resobj._tu = args[0]._tu
         return resobj
 
@@ -3640,11 +3708,13 @@
         if not cc:
             raise IndexError
         return CompileCommand(cc, self)
 
     @staticmethod
-    def from_result(res: CObjectP, fn: Callable[..., CObjectP], args: Tuple[Any, ...]) -> Optional[CompileCommands]:
+    def from_result(
+        res: CObjectP, fn: Callable[..., CObjectP], args: Tuple[Any, ...]
+    ) -> Optional[CompileCommands]:
         if not res:
             return None
         return CompileCommands(res)
 
 
@@ -3658,11 +3728,13 @@
 
     def __del__(self) -> None:
         conf.lib.clang_CompilationDatabase_dispose(self)
 
     @staticmethod
-    def from_result(res: CObjectP, fn: Callable[..., CObjectP], args: Tuple[Any, ...]) -> CompilationDatabase:
+    def from_result(
+        res: CObjectP, fn: Callable[..., CObjectP], args: Tuple[Any, ...]
+    ) -> CompilationDatabase:
         if not res:
             raise CompilationDatabaseError(0, "CompilationDatabase loading failed")
         return CompilationDatabase(res)
 
     @staticmethod
@@ -3737,11 +3809,11 @@
 
     @property
     def cursor(self) -> Cursor:
         """The Cursor this Token corresponds to."""
         cursor = Cursor()
-        cursor._tu = self._tu # pyright: ignore[reportPrivateUsage]
+        cursor._tu = self._tu  # pyright: ignore[reportPrivateUsage]
 
         conf.lib.clang_annotateTokens(self._tu, byref(self), 1, byref(cursor))
 
         return cursor
 
@@ -3806,15 +3878,19 @@
 
 
 # Now comes the plumbing to hook up the C library.
 
 # Register callback types
-TranslationUnitIncludesCallback = Annotated[CFuncPointer, None, c_object_p, CPointer[SourceLocation], c_uint, py_object]
+TranslationUnitIncludesCallback = Annotated[
+    CFuncPointer, None, c_object_p, CPointer[SourceLocation], c_uint, py_object
+]
 CursorVisitCallback = Annotated[CFuncPointer, c_int, Cursor, Cursor, py_object]
 FieldsVisitCallback = Annotated[CFuncPointer, c_int, Cursor, py_object]
 
-translation_unit_includes_callback: TType[CFuncPointer] = convert_annotation(TranslationUnitIncludesCallback)
+translation_unit_includes_callback: TType[CFuncPointer] = convert_annotation(
+    TranslationUnitIncludesCallback
+)
 cursor_visit_callback: TType[CFuncPointer] = convert_annotation(CursorVisitCallback)
 fields_visit_callback: TType[CFuncPointer] = convert_annotation(FieldsVisitCallback)
 
 
 # Functions strictly alphabetical order.
@@ -3823,26 +3899,38 @@
 #   - If Config.compatibility_check is set to `False`, then a function is allowed to be missing.
 #   - If a function is missing in C library, it will not be replaced, thus causing NotImplementedError when called.
 #   - Missing functions are given a `_missing_` attribute, you can check it with `hasattr(conf.lib.xxx, '_missing_')`.
 #   - These stub functions are generated with a script from old data and manually corrected, so parameter names are missing.
 class LibclangExports:
-    def clang_annotateTokens(self, p1: TranslationUnit, p2: CPointerParam[Token], p3: p_ulong, p4: CPointerParam[Cursor]) -> r_long:
+    def clang_annotateTokens(
+        self,
+        p1: TranslationUnit,
+        p2: CPointerParam[Token],
+        p3: p_ulong,
+        p4: CPointerParam[Cursor],
+    ) -> r_long:
         raise NotImplementedError
 
     def clang_CompilationDatabase_dispose(self, p1: ClangObjectParam) -> r_long:
         raise NotImplementedError
 
     @with_errcheck(CompilationDatabase.from_result)
-    def clang_CompilationDatabase_fromDirectory(self, p1: CInteropString, p2: CPointerParam[c_ulong]) -> CObjectP:
+    def clang_CompilationDatabase_fromDirectory(
+        self, p1: CInteropString, p2: CPointerParam[c_ulong]
+    ) -> CObjectP:
         raise NotImplementedError
 
     @with_errcheck(CompileCommands.from_result)
-    def clang_CompilationDatabase_getAllCompileCommands(self, p1: ClangObjectParam) -> CObjectP:
+    def clang_CompilationDatabase_getAllCompileCommands(
+        self, p1: ClangObjectParam
+    ) -> CObjectP:
         raise NotImplementedError
 
     @with_errcheck(CompileCommands.from_result)
-    def clang_CompilationDatabase_getCompileCommands(self, p1: ClangObjectParam, p2: CInteropString) -> CObjectP:
+    def clang_CompilationDatabase_getCompileCommands(
+        self, p1: ClangObjectParam, p2: CInteropString
+    ) -> CObjectP:
         raise NotImplementedError
 
     def clang_CompileCommands_dispose(self, p1: CObjectP) -> r_long:
         raise NotImplementedError
 
@@ -3865,14 +3953,25 @@
         raise NotImplementedError
 
     def clang_CompileCommand_getNumArgs(self, p1: CObjectP) -> r_ulong:
         raise NotImplementedError
 
-    def clang_codeCompleteAt(self, p1: TranslationUnit, p2: CInteropString, p3: p_long, p4: p_long, p5: CPointerParam[_CXUnsavedFile], p6: p_long, p7: p_long) -> CPointer[CCRStructure]:
-        raise NotImplementedError
-
-    def clang_codeCompleteGetDiagnostic(self, p1: CodeCompletionResults, p2: p_long) -> Diagnostic:
+    def clang_codeCompleteAt(
+        self,
+        p1: TranslationUnit,
+        p2: CInteropString,
+        p3: p_long,
+        p4: p_long,
+        p5: CPointerParam[_CXUnsavedFile],
+        p6: p_long,
+        p7: p_long,
+    ) -> CPointer[CCRStructure]:
+        raise NotImplementedError
+
+    def clang_codeCompleteGetDiagnostic(
+        self, p1: CodeCompletionResults, p2: p_long
+    ) -> Diagnostic:
         raise NotImplementedError
 
     def clang_codeCompleteGetNumDiagnostics(self, p1: CodeCompletionResults) -> r_long:
         raise NotImplementedError
 
@@ -3886,20 +3985,24 @@
         raise NotImplementedError
 
     def clang_CXRewriter_dispose(self, p1: Rewriter) -> r_long:
         raise NotImplementedError
 
-    def clang_CXRewriter_insertTextBefore(self, p1: Rewriter, p2: SourceLocation, p3: CInteropString) -> r_long:
+    def clang_CXRewriter_insertTextBefore(
+        self, p1: Rewriter, p2: SourceLocation, p3: CInteropString
+    ) -> r_long:
         raise NotImplementedError
 
     def clang_CXRewriter_overwriteChangedFiles(self, p1: Rewriter) -> r_long:
         raise NotImplementedError
 
     def clang_CXRewriter_removeText(self, p1: Rewriter, p2: SourceRange) -> r_long:
         raise NotImplementedError
 
-    def clang_CXRewriter_replaceText(self, p1: Rewriter, p2: SourceRange, p3: CInteropString) -> r_long:
+    def clang_CXRewriter_replaceText(
+        self, p1: Rewriter, p2: SourceRange, p3: CInteropString
+    ) -> r_long:
         raise NotImplementedError
 
     def clang_CXRewriter_writeMainFileToStdOut(self, p1: Rewriter) -> r_long:
         raise NotImplementedError
 
@@ -3970,11 +4073,13 @@
         raise NotImplementedError
 
     def clang_disposeString(self, p1: _CXString) -> r_long:
         raise NotImplementedError
 
-    def clang_disposeTokens(self, p1: TranslationUnit, p2: CPointer[Token], p3: p_uint) -> r_long:
+    def clang_disposeTokens(
+        self, p1: TranslationUnit, p2: CPointer[Token], p3: p_uint
+    ) -> r_long:
         raise NotImplementedError
 
     def clang_disposeTranslationUnit(self, p1: TranslationUnit) -> r_long:
         raise NotImplementedError
 
@@ -4027,11 +4132,13 @@
 
     @with_errcheck(_CXString.from_result)
     def clang_getCompletionBriefComment(self, p1: CObjectP) -> _CXString:
         raise NotImplementedError
 
-    def clang_getCompletionChunkCompletionString(self, p1: CObjectP, p2: p_long) -> CObjectP:
+    def clang_getCompletionChunkCompletionString(
+        self, p1: CObjectP, p2: p_long
+    ) -> CObjectP:
         raise NotImplementedError
 
     def clang_getCompletionChunkKind(self, p1: CObjectP, p2: p_long) -> r_long:
         raise NotImplementedError
 
@@ -4078,11 +4185,13 @@
 
     @with_errcheck(Cursor.from_result)
     def clang_getCursorReferenced(self, p1: Cursor) -> Cursor:
         raise NotImplementedError
 
-    def clang_getCursorReferenceNameRange(self, p1: Cursor, p2: p_ulong, p3: p_ulong) -> SourceRange:
+    def clang_getCursorReferenceNameRange(
+        self, p1: Cursor, p2: p_ulong, p3: p_ulong
+    ) -> SourceRange:
         raise NotImplementedError
 
     @with_errcheck(Type.from_result)
     def clang_getCursorResultType(self, p1: Cursor) -> Type:
         raise NotImplementedError
@@ -4126,11 +4235,13 @@
     @with_errcheck(_CXString.from_result)
     def clang_getDiagnosticCategoryText(self, p1: Diagnostic) -> _CXString:
         raise NotImplementedError
 
     @with_errcheck(_CXString.from_result)
-    def clang_getDiagnosticFixIt(self, p1: Diagnostic, p2: p_ulong, p3: CPointerParam[SourceRange]) -> _CXString:
+    def clang_getDiagnosticFixIt(
+        self, p1: Diagnostic, p2: p_ulong, p3: CPointerParam[SourceRange]
+    ) -> _CXString:
         raise NotImplementedError
 
     def clang_getDiagnosticInSet(self, p1: CObjectP, p2: p_ulong) -> CObjectP:
         raise NotImplementedError
 
@@ -4142,11 +4253,13 @@
 
     def clang_getDiagnosticNumRanges(self, p1: Diagnostic) -> r_ulong:
         raise NotImplementedError
 
     @with_errcheck(_CXString.from_result)
-    def clang_getDiagnosticOption(self, p1: Diagnostic, p2: CPointerParam[_CXString]) -> _CXString:
+    def clang_getDiagnosticOption(
+        self, p1: Diagnostic, p2: CPointerParam[_CXString]
+    ) -> _CXString:
         raise NotImplementedError
 
     def clang_getDiagnosticRange(self, p1: Diagnostic, p2: p_ulong) -> SourceRange:
         raise NotImplementedError
 
@@ -4190,20 +4303,36 @@
 
     @with_errcheck(File.from_result)
     def clang_getIncludedFile(self, p1: Cursor) -> CObjectP:
         raise NotImplementedError
 
-    def clang_getInclusions(self, p1: TranslationUnit, p2: TranslationUnitIncludesCallback, p3: CPyObject[List[FileInclusion]]) -> r_long:
-        raise NotImplementedError
-
-    def clang_getInstantiationLocation(self, p1: SourceLocation, p2: CPointerParam[CObjectP], p3: CPointerParam[c_ulong], p4: CPointerParam[c_ulong], p5: CPointerParam[c_ulong]) -> r_long:
-        raise NotImplementedError
-
-    def clang_getLocation(self, p1: TranslationUnit, p2: File, p3: p_ulong, p4: p_ulong) -> SourceLocation:
-        raise NotImplementedError
-
-    def clang_getLocationForOffset(self, p1: TranslationUnit, p2: File, p3: p_ulong) -> SourceLocation:
+    def clang_getInclusions(
+        self,
+        p1: TranslationUnit,
+        p2: TranslationUnitIncludesCallback,
+        p3: CPyObject[List[FileInclusion]],
+    ) -> r_long:
+        raise NotImplementedError
+
+    def clang_getInstantiationLocation(
+        self,
+        p1: SourceLocation,
+        p2: CPointerParam[CObjectP],
+        p3: CPointerParam[c_ulong],
+        p4: CPointerParam[c_ulong],
+        p5: CPointerParam[c_ulong],
+    ) -> r_long:
+        raise NotImplementedError
+
+    def clang_getLocation(
+        self, p1: TranslationUnit, p2: File, p3: p_ulong, p4: p_ulong
+    ) -> SourceLocation:
+        raise NotImplementedError
+
+    def clang_getLocationForOffset(
+        self, p1: TranslationUnit, p2: File, p3: p_ulong
+    ) -> SourceLocation:
         raise NotImplementedError
 
     def clang_getNullCursor(self) -> Cursor:
         raise NotImplementedError
 
@@ -4350,23 +4479,48 @@
         raise NotImplementedError
 
     def clang_isVolatileQualifiedType(self, p1: Type) -> bool:
         raise NotImplementedError
 
-    def clang_parseTranslationUnit(self, p1: Index, p2: CInteropString, p3: CPointerParam[c_char_p], p4: p_long, p5: CPointerParam[_CXUnsavedFile], p6: p_long, p7: p_long) -> CObjectP:
-        raise NotImplementedError
-
-    def clang_reparseTranslationUnit(self, p1: TranslationUnit, p2: p_long, p3: CPointerParam[_CXUnsavedFile], p4: p_long) -> r_long:
-        raise NotImplementedError
-
-    def clang_saveTranslationUnit(self, p1: TranslationUnit, p2: CInteropString, p3: p_ulong) -> r_long:
-        raise NotImplementedError
-
-    def clang_tokenize(self, p1: TranslationUnit, p2: SourceRange, p3: CPointerParam[CPointer[Token]], p4: CPointerParam[c_ulong]) -> r_long:
-        raise NotImplementedError
-
-    def clang_visitChildren(self, p1: Cursor, p2: CursorVisitCallback, p3: CPyObject[List[Cursor]]) -> r_ulong:
+    def clang_parseTranslationUnit(
+        self,
+        p1: Index,
+        p2: CInteropString,
+        p3: CPointerParam[c_char_p],
+        p4: p_long,
+        p5: CPointerParam[_CXUnsavedFile],
+        p6: p_long,
+        p7: p_long,
+    ) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_reparseTranslationUnit(
+        self,
+        p1: TranslationUnit,
+        p2: p_long,
+        p3: CPointerParam[_CXUnsavedFile],
+        p4: p_long,
+    ) -> r_long:
+        raise NotImplementedError
+
+    def clang_saveTranslationUnit(
+        self, p1: TranslationUnit, p2: CInteropString, p3: p_ulong
+    ) -> r_long:
+        raise NotImplementedError
+
+    def clang_tokenize(
+        self,
+        p1: TranslationUnit,
+        p2: SourceRange,
+        p3: CPointerParam[CPointer[Token]],
+        p4: CPointerParam[c_ulong],
+    ) -> r_long:
+        raise NotImplementedError
+
+    def clang_visitChildren(
+        self, p1: Cursor, p2: CursorVisitCallback, p3: CPyObject[List[Cursor]]
+    ) -> r_ulong:
         raise NotImplementedError
 
     def clang_Cursor_getNumArguments(self, p1: Cursor) -> r_long:
         raise NotImplementedError
 
@@ -4383,14 +4537,18 @@
 
     @with_errcheck(Type.from_result)
     def clang_Cursor_getTemplateArgumentType(self, p1: Cursor, p2: p_ulong) -> Type:
         raise NotImplementedError
 
-    def clang_Cursor_getTemplateArgumentValue(self, p1: Cursor, p2: p_ulong) -> r_longlong:
-        raise NotImplementedError
-
-    def clang_Cursor_getTemplateArgumentUnsignedValue(self, p1: Cursor, p2: p_ulong) -> r_ulonglong:
+    def clang_Cursor_getTemplateArgumentValue(
+        self, p1: Cursor, p2: p_ulong
+    ) -> r_longlong:
+        raise NotImplementedError
+
+    def clang_Cursor_getTemplateArgumentUnsignedValue(
+        self, p1: Cursor, p2: p_ulong
+    ) -> r_ulonglong:
         raise NotImplementedError
 
     def clang_Cursor_isAnonymous(self, p1: Cursor) -> bool:
         raise NotImplementedError
 
@@ -4439,11 +4597,13 @@
 
     @with_errcheck(Type.from_result)
     def clang_Type_getNamedType(self, p1: Type) -> Type:
         raise NotImplementedError
 
-    def clang_Type_visitFields(self, p1: Type, p2: FieldsVisitCallback, p3: CPyObject[List[Cursor]]) -> r_ulong:
+    def clang_Type_visitFields(
+        self, p1: Type, p2: FieldsVisitCallback, p3: CPyObject[List[Cursor]]
+    ) -> r_ulong:
         raise NotImplementedError
 
 
 class LibclangError(Exception):
     m: str
@@ -4553,15 +4713,15 @@
             raise LibclangError(msg)
 
         return library
 
     def function_exists(self, name: str) -> bool:
-        return not hasattr(getattr(self.lib, name), '_missing_')
+        return not hasattr(getattr(self.lib, name), "_missing_")
 
 
 def generate_metadata_debug() -> Dict[str, Dict[str, Any]]:
-    ''' Generate ctypes metadata for debugging purpose. '''
+    """Generate ctypes metadata for debugging purpose."""
     return {name: info for name, info in generate_metadata(LibclangExports, globals())}
 
 
 conf = Config()
 
--- clang/ctyped.py	2024-08-03 14:41:42.000000 +0000
+++ clang/ctyped.py	2024-08-03 23:15:21.802497 +0000
@@ -1,48 +1,94 @@
-from ctypes import (CFUNCTYPE, POINTER, WINFUNCTYPE, c_bool, c_byte, c_char,
-                    c_char_p, c_double, c_float, c_int, c_long, c_longdouble,
-                    c_longlong, c_short, c_size_t, c_ssize_t, c_ubyte, c_uint,
-                    c_ulong, c_ulonglong, c_ushort, c_void_p, c_wchar,
-                    c_wchar_p, py_object)
+from ctypes import (
+    CFUNCTYPE,
+    POINTER,
+    WINFUNCTYPE,
+    c_bool,
+    c_byte,
+    c_char,
+    c_char_p,
+    c_double,
+    c_float,
+    c_int,
+    c_long,
+    c_longdouble,
+    c_longlong,
+    c_short,
+    c_size_t,
+    c_ssize_t,
+    c_ubyte,
+    c_uint,
+    c_ulong,
+    c_ulonglong,
+    c_ushort,
+    c_void_p,
+    c_wchar,
+    c_wchar_p,
+    py_object,
+)
 from inspect import Parameter, signature
-from typing import (TYPE_CHECKING, Any, Callable, Dict, Generator, Generic,
-                    List, Optional, Tuple, Type, TypeVar, Union, cast)
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Callable,
+    Dict,
+    Generator,
+    Generic,
+    List,
+    Optional,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+    cast,
+)
 
 from typing_extensions import Annotated, ParamSpec, TypeAlias
 
-_T = TypeVar('_T')
+_T = TypeVar("_T")
 
 if TYPE_CHECKING:
     from ctypes import _CArgObject  # pyright: ignore[reportPrivateUsage]
     from ctypes import _CData  # pyright: ignore[reportPrivateUsage]
 
-AnyCData = TypeVar('AnyCData', bound='_CData')
+AnyCData = TypeVar("AnyCData", bound="_CData")
 
 if TYPE_CHECKING:
     from ctypes import Array as _Array  # pyright: ignore[reportPrivateUsage]
-    from ctypes import \
-        _FuncPointer as _FuncPointer  # pyright: ignore[reportPrivateUsage]
-    from ctypes import \
-        _Pointer as _Pointer  # pyright: ignore[reportPrivateUsage]
+    from ctypes import (
+        _FuncPointer as _FuncPointer,
+    )  # pyright: ignore[reportPrivateUsage]
+    from ctypes import _Pointer as _Pointer  # pyright: ignore[reportPrivateUsage]
 
     # ctypes documentation noted implicit conversion for pointers:
     # "For example, you can pass compatible array instances instead of pointer
     #  types. So, for POINTER(c_int), ctypes accepts an array of c_int:"
     # "In addition, if a function argument is explicitly declared to be a
     #  pointer type (such as POINTER(c_int)) in argtypes, an object of the
     #  pointed type (c_int in this case) can be passed to the function. ctypes
     #  will apply the required byref() conversion in this case automatically."
     # also, current ctype typeshed thinks byref returns _CArgObject
-    _PointerCompatible: TypeAlias = Union['_CArgObject', _Pointer[AnyCData], None, _Array[AnyCData], AnyCData]
-    _PyObject: TypeAlias = Union['py_object[_T]', _T]
+    _PointerCompatible: TypeAlias = Union[
+        "_CArgObject", _Pointer[AnyCData], None, _Array[AnyCData], AnyCData
+    ]
+    _PyObject: TypeAlias = Union["py_object[_T]", _T]
 else:
     # at runtime we don't really import those symbols
-    class _Array(Generic[AnyCData]): ...
-    class _Pointer(Generic[AnyCData]): ...
-    class _PointerCompatible(Generic[AnyCData]): ...
-    class _FuncPointer: ...
-    class _PyObject(Generic[AnyCData]): ...
+    class _Array(Generic[AnyCData]):
+        ...
+
+    class _Pointer(Generic[AnyCData]):
+        ...
+
+    class _PointerCompatible(Generic[AnyCData]):
+        ...
+
+    class _FuncPointer:
+        ...
+
+    class _PyObject(Generic[AnyCData]):
+        ...
 
 
 # ANNO_CONVETIBLE can be used to declare that a class have a `from_param`
 # method which can convert other types when used as `argtypes`.
 # For example: `CClass = Annotated[bytes, ANNO_CONVERTIBLE, c_class]` means
@@ -100,13 +146,17 @@
 p_size_t = Annotated[Union[c_size_t, int], ANNO_BASIC, c_size_t]
 p_ssize_t = Annotated[Union[c_ssize_t, int], ANNO_BASIC, c_ssize_t]
 p_float = Annotated[Union[c_float, float], ANNO_BASIC, c_float]
 p_double = Annotated[Union[c_double, float], ANNO_BASIC, c_double]
 p_longdouble = Annotated[Union[c_longdouble, float], ANNO_BASIC, c_longdouble]
-p_char_p = Annotated[Union[c_char_p, _Array[c_wchar], bytes, None], ANNO_BASIC, c_char_p]
-p_wchar_p = Annotated[Union[c_wchar_p, _Array[c_wchar], str, None], ANNO_BASIC, c_wchar_p]
-p_void_p = Annotated[Union['_CArgObject', c_void_p, int, None], ANNO_BASIC, c_void_p]
+p_char_p = Annotated[
+    Union[c_char_p, _Array[c_wchar], bytes, None], ANNO_BASIC, c_char_p
+]
+p_wchar_p = Annotated[
+    Union[c_wchar_p, _Array[c_wchar], str, None], ANNO_BASIC, c_wchar_p
+]
+p_void_p = Annotated[Union["_CArgObject", c_void_p, int, None], ANNO_BASIC, c_void_p]
 
 # export Pointer, PointerCompatible, Array and FuncPointer annotation
 
 CArray = Annotated[_Array[AnyCData], ANNO_ARRAY]
 CPointer = Annotated[_Pointer[AnyCData], ANNO_POINTER]
@@ -114,163 +164,196 @@
 CFuncPointer = Annotated[_FuncPointer, ANNO_CFUNC]
 WinFuncPointer = Annotated[_FuncPointer, ANNO_WINFUNC]
 CPyObject = Annotated[_PyObject[_T], ANNO_PYOBJ]
 
 
-_Params = ParamSpec('_Params')
-_OrigRet = TypeVar('_OrigRet')
-_NewRet = TypeVar('_NewRet')
-
-def with_errcheck(checker: Callable[[_OrigRet, Callable[..., _OrigRet], Tuple[Any, ...]], _NewRet]) -> Callable[[Callable[_Params, _OrigRet]], Callable[_Params, _NewRet]]:
-    ''' Decorates a stub function with an error checker. '''
+_Params = ParamSpec("_Params")
+_OrigRet = TypeVar("_OrigRet")
+_NewRet = TypeVar("_NewRet")
+
+
+def with_errcheck(
+    checker: Callable[[_OrigRet, Callable[..., _OrigRet], Tuple[Any, ...]], _NewRet]
+) -> Callable[[Callable[_Params, _OrigRet]], Callable[_Params, _NewRet]]:
+    """Decorates a stub function with an error checker."""
+
     def decorator(wrapped: Callable[_Params, _OrigRet]) -> Callable[_Params, _NewRet]:
         def wrapper(*args: _Params.args, **kwargs: _Params.kwargs) -> _NewRet:
             raise NotImplementedError
 
         # attach original declaration and error checker to wrapper
-        setattr(wrapper, '_decl_errcheck_', (wrapped, checker))
+        setattr(wrapper, "_decl_errcheck_", (wrapped, checker))
         return wrapper
 
     return decorator
+
 
 # NOTE: Actually, converter is a deprecated form of `restype`.
 # According to ctypes documentation:
 # "It is possible to assign a callable Python object that is not a ctypes
 #  type, in this case the function is assumed to return a C int, and the
 #  callable will be called with this integer, allowing further processing
 #  or error checking. Using this is deprecated, for more flexible post
 #  processing or error checking use a ctypes data type as restype and
 #  assign a callable to the errcheck attribute."
 
-def with_converter(converter: Callable[[int], _NewRet]) -> Callable[[Callable[_Params, r_int]], Callable[_Params, _NewRet]]:
-    ''' Decorates a stub function with a converter, its return type MUST be `r_int`. '''
+
+def with_converter(
+    converter: Callable[[int], _NewRet]
+) -> Callable[[Callable[_Params, r_int]], Callable[_Params, _NewRet]]:
+    """Decorates a stub function with a converter, its return type MUST be `r_int`."""
+
     def decorator(wrapped: Callable[_Params, r_int]) -> Callable[_Params, _NewRet]:
         def wrapper(*args: _Params.args, **kwargs: _Params.kwargs) -> _NewRet:
             raise NotImplementedError
 
         # attach original declaration and converter to wrapper
-        setattr(wrapper, '_decl_converter_', (wrapped, converter))
+        setattr(wrapper, "_decl_converter_", (wrapped, converter))
         return wrapper
 
     return decorator
 
 
-def convert_annotation(typ: Any, global_ns: Optional[Dict[str, Any]] = None) -> Type[Any]:
-    ''' Convert an annotation to effective runtime type. '''
+def convert_annotation(
+    typ: Any, global_ns: Optional[Dict[str, Any]] = None
+) -> Type[Any]:
+    """Convert an annotation to effective runtime type."""
     if global_ns is None:
         global_ns = globals()
 
     if isinstance(typ, str):
-        try: typ = eval(typ, global_ns)
+        try:
+            typ = eval(typ, global_ns)
         except Exception as exc:
-            raise ValueError('Evaluation of delayed annotation failed!') from exc
-
-    if not hasattr(typ, '__metadata__'):
+            raise ValueError("Evaluation of delayed annotation failed!") from exc
+
+    if not hasattr(typ, "__metadata__"):
         return cast(Type[Any], typ)
 
     # type is Annotated
     ident, *detail = typ.__metadata__
     if ident is ANNO_CONVERTIBLE:
-        ctyp, = detail
+        (ctyp,) = detail
         return cast(Type[Any], ctyp)
     elif ident is ANNO_ARRAY:
-        try: count, = detail
+        try:
+            (count,) = detail
         except ValueError:
-            raise ValueError('CArray needs to be annotated with its size')
-        ctyp, = typ.__args__[0].__args__
+            raise ValueError("CArray needs to be annotated with its size")
+        (ctyp,) = typ.__args__[0].__args__
         return cast(Type[Any], convert_annotation(ctyp, global_ns=global_ns) * count)
     elif ident is ANNO_POINTER:
         assert not detail
-        ctyp, = typ.__args__[0].__args__
-        return POINTER(convert_annotation(ctyp, global_ns=global_ns)) # pyright: ignore
+        (ctyp,) = typ.__args__[0].__args__
+        return POINTER(convert_annotation(ctyp, global_ns=global_ns))  # pyright: ignore
     elif ident is ANNO_CFUNC:
         if not detail:
-            raise ValueError('CFuncPointer needs to be annotated with its signature')
+            raise ValueError("CFuncPointer needs to be annotated with its signature")
         return CFUNCTYPE(*(convert_annotation(t, global_ns=global_ns) for t in detail))
     elif ident is ANNO_WINFUNC:
         if not detail:
-            raise ValueError('WinFuncPointer needs to be annotated with its signature')
-        return WINFUNCTYPE(*(convert_annotation(t, global_ns=global_ns) for t in detail))
+            raise ValueError("WinFuncPointer needs to be annotated with its signature")
+        return WINFUNCTYPE(
+            *(convert_annotation(t, global_ns=global_ns) for t in detail)
+        )
     elif ident is ANNO_PYOBJ:
         assert not detail
         return py_object
     else:
-        raise ValueError(f'Unexpected annotated type {typ}')
-
-
-def convert_func_decl(decl: Callable[..., Any], global_ns: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
-    ''' Converts a stub function to ctypes metadata. '''
+        raise ValueError(f"Unexpected annotated type {typ}")
+
+
+def convert_func_decl(
+    decl: Callable[..., Any], global_ns: Optional[Dict[str, Any]] = None
+) -> Dict[str, Any]:
+    """Converts a stub function to ctypes metadata."""
     if global_ns is None:
         global_ns = globals()
 
     result: Dict[str, Any] = {}
 
     errcheck = None
     converter = None
 
     while True:
-        if hasattr(decl, '_decl_errcheck_'):
+        if hasattr(decl, "_decl_errcheck_"):
             if errcheck is not None:
-                raise ValueError('duplicate errcheck in stub function')
-            decl, errcheck = getattr(decl, '_decl_errcheck_')
+                raise ValueError("duplicate errcheck in stub function")
+            decl, errcheck = getattr(decl, "_decl_errcheck_")
             continue
 
-        if hasattr(decl, '_decl_converter_'):
+        if hasattr(decl, "_decl_converter_"):
             if converter is not None:
-                raise ValueError('duplicate converter in stub function')
-            decl, converter = getattr(decl, '_decl_converter_')
+                raise ValueError("duplicate converter in stub function")
+            decl, converter = getattr(decl, "_decl_converter_")
             continue
 
         break
 
     sig = signature(decl)
 
-    param_annos = [p.annotation for p in sig.parameters.values() if p.name != 'self']
+    param_annos = [p.annotation for p in sig.parameters.values() if p.name != "self"]
     if all(anno is not Parameter.empty for anno in param_annos):
-        result['argtypes'] = [convert_annotation(anno, global_ns=global_ns) for anno in param_annos] or None
+        result["argtypes"] = [
+            convert_annotation(anno, global_ns=global_ns) for anno in param_annos
+        ] or None
 
     if sig.return_annotation is not Parameter.empty:
-        result['restype'] = convert_annotation(sig.return_annotation, global_ns=global_ns)
-
-    if errcheck is not None: result['errcheck'] = errcheck
-    if converter is not None: result['restype'] = converter
+        result["restype"] = convert_annotation(
+            sig.return_annotation, global_ns=global_ns
+        )
+
+    if errcheck is not None:
+        result["errcheck"] = errcheck
+    if converter is not None:
+        result["restype"] = converter
 
     return result
 
 
 if TYPE_CHECKING:
     from ctypes import CDLL, WinDLL
-    _DLLT = TypeVar('_DLLT', bound=CDLL)
-_LibDecl = TypeVar('_LibDecl')
-
-
-def generate_metadata(decl_cls: Type[_LibDecl], global_ns: Optional[Dict[str, Any]] = None) -> Generator[Tuple[str, Dict[str, Any]], None, None]:
-    ''' Generate ctypes metadata for a stub class. '''
+
+    _DLLT = TypeVar("_DLLT", bound=CDLL)
+_LibDecl = TypeVar("_LibDecl")
+
+
+def generate_metadata(
+    decl_cls: Type[_LibDecl], global_ns: Optional[Dict[str, Any]] = None
+) -> Generator[Tuple[str, Dict[str, Any]], None, None]:
+    """Generate ctypes metadata for a stub class."""
     if global_ns is None:
         global_ns = globals()
 
     for name in dir(decl_cls):
-        if name.startswith('_'): continue
+        if name.startswith("_"):
+            continue
         value = getattr(decl_cls, name)
-        if not callable(value): continue
+        if not callable(value):
+            continue
 
         yield name, convert_func_decl(value, global_ns=global_ns)
 
 
-def load_annotated_library(loader: 'Union[CDLL, WinDLL]', decl_cls: Type[_LibDecl], global_ns: Optional[Dict[str, Any]] = None) -> Tuple[_LibDecl, List[str]]:
-    ''' Load a library and set signature metadata according to python type hints.
-        `decl_cls` is a class which should only contain method declarations.
-        Note: you should only name `self` as `self`, the converter depends on this.
-    '''
+def load_annotated_library(
+    loader: "Union[CDLL, WinDLL]",
+    decl_cls: Type[_LibDecl],
+    global_ns: Optional[Dict[str, Any]] = None,
+) -> Tuple[_LibDecl, List[str]]:
+    """Load a library and set signature metadata according to python type hints.
+    `decl_cls` is a class which should only contain method declarations.
+    Note: you should only name `self` as `self`, the converter depends on this.
+    """
     if global_ns is None:
         global_ns = globals()
 
     result = decl_cls()
     missing: List[str] = []
 
     for name, info in generate_metadata(decl_cls, global_ns=global_ns):
-        try: func = getattr(loader, name)
+        try:
+            func = getattr(loader, name)
         except AttributeError:
             stub = getattr(result, name)
             stub._missing_ = True
             missing.append(name)
             continue
@@ -282,64 +365,60 @@
 
     return result, missing
 
 
 __all__ = [
-    'ANNO_CONVERTIBLE',
-    'AnyCData',
-
-    'p_bool',
-    'p_char',
-    'p_wchar',
-    'p_byte',
-    'p_ubyte',
-    'p_short',
-    'p_ushort',
-    'p_int',
-    'p_uint',
-    'p_long',
-    'p_ulong',
-    'p_longlong',
-    'p_ulonglong',
-    'p_size_t',
-    'p_ssize_t',
-    'p_float',
-    'p_double',
-    'p_longdouble',
-    'p_char_p',
-    'p_wchar_p',
-    'p_void_p',
-
-    'r_bool',
-    'r_char',
-    'r_wchar',
-    'r_byte',
-    'r_ubyte',
-    'r_short',
-    'r_ushort',
-    'r_int',
-    'r_uint',
-    'r_long',
-    'r_ulong',
-    'r_longlong',
-    'r_ulonglong',
-    'r_size_t',
-    'r_ssize_t',
-    'r_float',
-    'r_double',
-    'r_longdouble',
-    'r_char_p',
-    'r_wchar_p',
-    'r_void_p',
-
-    'CArray',
-    'CPointer',
-    'CPointerParam',
-    'CFuncPointer',
-    'WinFuncPointer',
-    'CPyObject',
-
-    'convert_annotation',
-    'with_errcheck',
-    'with_converter',
-    'load_annotated_library',
+    "ANNO_CONVERTIBLE",
+    "AnyCData",
+    "p_bool",
+    "p_char",
+    "p_wchar",
+    "p_byte",
+    "p_ubyte",
+    "p_short",
+    "p_ushort",
+    "p_int",
+    "p_uint",
+    "p_long",
+    "p_ulong",
+    "p_longlong",
+    "p_ulonglong",
+    "p_size_t",
+    "p_ssize_t",
+    "p_float",
+    "p_double",
+    "p_longdouble",
+    "p_char_p",
+    "p_wchar_p",
+    "p_void_p",
+    "r_bool",
+    "r_char",
+    "r_wchar",
+    "r_byte",
+    "r_ubyte",
+    "r_short",
+    "r_ushort",
+    "r_int",
+    "r_uint",
+    "r_long",
+    "r_ulong",
+    "r_longlong",
+    "r_ulonglong",
+    "r_size_t",
+    "r_ssize_t",
+    "r_float",
+    "r_double",
+    "r_longdouble",
+    "r_char_p",
+    "r_wchar_p",
+    "r_void_p",
+    "CArray",
+    "CPointer",
+    "CPointerParam",
+    "CFuncPointer",
+    "WinFuncPointer",
+    "CPyObject",
+    "convert_annotation",
+    "with_errcheck",
+    "with_converter",
+    "load_annotated_library",
 ]
--- tests/ctyped/test_stub_conversion.py	2024-08-03 03:21:33.000000 +0000
+++ tests/ctyped/test_stub_conversion.py	2024-08-03 23:15:22.143008 +0000
@@ -7,14 +7,20 @@
 
 from dictdiffer import diff as dictdiff  # type: ignore
 
 from clang.cindex import *
 from clang.cindex import _CXString  # pyright: ignore[reportPrivateUsage]
-from clang.cindex import (CCRStructure, Rewriter, c_interop_string, c_object_p,
-                          cursor_visit_callback, fields_visit_callback,
-                          generate_metadata_debug,
-                          translation_unit_includes_callback)
+from clang.cindex import (
+    CCRStructure,
+    Rewriter,
+    c_interop_string,
+    c_object_p,
+    cursor_visit_callback,
+    fields_visit_callback,
+    generate_metadata_debug,
+    translation_unit_includes_callback,
+)
 
 
 # Functions strictly alphabetical order.
 # This is previous version of ctypes metadata, we check equality to this so
 # that we can ensure `ctyped` doesn't break anything in its conversion.
@@ -304,56 +310,73 @@
 ]
 
 
 # Sadly, ctypes provides no API to check if type is pointer or array.
 # Here we use regex to check type name.
-arr_regex = re.compile(r'(?P<typ>[A-Za-z0-9_]+)_Array_(?P<count>[0-9]+)')
-ptr_regex = re.compile(r'LP_(?P<typ>[A-Za-z0-9_]+)')
+arr_regex = re.compile(r"(?P<typ>[A-Za-z0-9_]+)_Array_(?P<count>[0-9]+)")
+ptr_regex = re.compile(r"LP_(?P<typ>[A-Za-z0-9_]+)")
+
 
 def is_ptr_type(typ: Any):
-    return typ in (c_void_p, c_char_p, c_wchar_p) or ptr_regex.fullmatch(typ.__name__) is not None
+    return (
+        typ in (c_void_p, c_char_p, c_wchar_p)
+        or ptr_regex.fullmatch(typ.__name__) is not None
+    )
+
 
 def is_arr_type(typ: Any):
     return arr_regex.fullmatch(typ.__name__) is not None
+
 
 # If we change a c_void_p parameter to a more exact pointer types, it
 # should still be working.
 def is_void_specialization(old_type: Any, new_type: Any):
     return old_type == c_void_p and is_ptr_type(new_type)
 
 
 def old_data_to_dict(data: List[Any]):
     result: Dict[str, Any] = {}
-    result['argtypes'], *data = data
-    if not result['argtypes']: result['argtypes'] = None
-    if data: result['restype'], *data = data
-    else: result['restype'] = c_int
-    if data: result['errcheck'], *data = data
+    result["argtypes"], *data = data
+    if not result["argtypes"]:
+        result["argtypes"] = None
+    if data:
+        result["restype"], *data = data
+    else:
+        result["restype"] = c_int
+    if data:
+        result["errcheck"], *data = data
     return result
 
 
 def is_incompatible_diff(diff: Any):
-    kind, path, detail = diff # pyright: ignore[reportUnusedVariable]
-    if kind == 'add': return True
+    kind, path, detail = diff  # pyright: ignore[reportUnusedVariable]
+    if kind == "add":
+        return True
     old_type, new_type = detail
-    if is_void_specialization(old_type, new_type): return False
+    if is_void_specialization(old_type, new_type):
+        return False
     return True
 
 
 class TestStubConversion(unittest.TestCase):
     def test_equality(self):
         """Ensure that ctyped does not break anything."""
-        old_function_dict: Dict[str, Dict[str, Any]] = {name: old_data_to_dict(val) for name, *val in FUNCTION_LIST}
+        old_function_dict: Dict[str, Dict[str, Any]] = {
+            name: old_data_to_dict(val) for name, *val in FUNCTION_LIST
+        }
         new_function_dict = generate_metadata_debug()
 
         missing_functions = set(old_function_dict.keys())
         stable_functions: Set[str] = set()
         for new_func in new_function_dict:
             if new_func in missing_functions:
                 missing_functions.remove(new_func)
                 stable_functions.add(new_func)
 
-        type_diff = [list(dictdiff(old_function_dict[name], new_function_dict[name])) for name in stable_functions] # type: ignore
-        type_break = [diffset for diffset in type_diff if diffset and any(is_incompatible_diff(diff) for diff in diffset)] # type: ignore
-
-        self.assertTrue(not missing_functions, f'Functions {missing_functions} are missing after stub conversion!')
-        self.assertTrue(not type_break, f'Type break happens after stub conversion!')
+        type_diff = [list(dictdiff(old_function_dict[name], new_function_dict[name])) for name in stable_functions]  # type: ignore
+        type_break = [diffset for diffset in type_diff if diffset and any(is_incompatible_diff(diff) for diff in diffset)]  # type: ignore
+
+        self.assertTrue(
+            not missing_functions,
+            f"Functions {missing_functions} are missing after stub conversion!",
+        )
+        self.assertTrue(not type_break, f"Type break happens after stub conversion!")

@Endilll
Copy link
Contributor

Endilll commented Aug 3, 2024

Thank you for your contribution! This is somewhat tricky situation, now that we have multiple opened PR in the same area of the codebase. But I think there is a way forward:

  1. [libclang/python] Add strict typing to clang Python bindings (#76664) #78114 and related work has been underway for several weeks, and I don't think anything should change there from the procedural standpoint. I encourage @TsXor to review it, and if you think something can be done better, feel free to leave a review. I also encourage @DeinAlptraum to take a look at this PR as it stands, and if you think this PR does something better than your work that is not yet merged, we can go with the approach here instead.

  2. This PR is massive, which makes it hard to both review and, potentially, revert. As with [libclang/python] Add strict typing to clang Python bindings (#76664) #78114, I suspect that one can compile a list of fixes by topic that this PR covers. Items in such list are a good approximation of individual PRs that you should submit instead of this PR.

  3. I see that >1k changed lines are tested by just 360 lines of code. This is sure a very rough estimate (sorry, I don't have time to review something this big right away), but it makes me wonder whether there's a room for more testing.

@TsXor
Copy link
Author

TsXor commented Aug 4, 2024

@DeinAlptraum @Endilll
I made this change mostly as a proof of concept: most type checking errors here originates from type information of C library functions invisible in type checker's eye. To really fix all type checking errors in a sane way instead of just putting a #type: ignore after every conf.lib.* calls, we must introduce ctyped or something alike.
This "huge" PR can be splitted to the following independent parts:

  1. Introduce ctyped and replace functionList with stub class LibclangExports. Implementation of Config.lib and Config.function_exists need to be adjusted accordingly. I will split LibclangImports to a independent file so that it is more readable. That 360-line test can ensure that ctyped and LibclangExports is a drop-in and equivalent replacement of functionList and register_function.
  2. Fix error handling of TranslationUnit.reparse.
  3. Rename those camelCase methods and fields to snake_case.
  4. Add type check to __eq__ methods.
  5. Add return type annotations to methods and remove previous no-return-any ignores. This is very easy, you just need to copy what is inferred in vscode.
  6. Add type annotations to classes. Also, auto generate field metadata for annotated Stucture. (Annotation conversion for structures is not implemented in this PR.)

I'll later collect part 1 and submit a PR to @DeinAlptraum 's fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:as-a-library libclang and C++ API clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants