Skip to content
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

#101784 part 1: introduce ctyped in an independent manner #101941

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

TsXor
Copy link

@TsXor TsXor commented Aug 5, 2024

This is part 1 as described in #101784.
I moved LibclangExports, LibclangError and modified Config class to a new file: binder.py. It can now be used by cindex.py with a simple from .binder import Config.
This PR doesn't modify any existing files, so it is safe to merge. However, @DeinAlptraum will need to manually delete functionList, Config class and LibclangError and add from .binder import Config to utilize its typing benefits.

Note: compared to #101784, I added a check that prevents ctyped from importing WINFUNCTYPE when not on windows, which should fix failing linux tests.

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

github-actions bot commented Aug 5, 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 5, 2024
Copy link

github-actions bot commented Aug 5, 2024

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

You can test this locally with the following command:
darker --check --diff -r 98e4413a38f286147b863a6ead9625228ab0ec7d...cbe82fc24aa94c919aaa1a01ae72e11ddcd71f19 clang/bindings/python/clang/binder.py clang/bindings/python/clang/ctyped.py clang/bindings/python/tests/ctyped/__init__.py clang/bindings/python/tests/ctyped/test_stub_conversion.py
View the diff from darker here.
--- clang/binder.py	2024-08-06 14:17:29.000000 +0000
+++ clang/binder.py	2024-08-08 15:03:52.918837 +0000
@@ -8,58 +8,79 @@
 from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
 
 from typing_extensions import Annotated
 
 from .ctyped import *
-from .ctyped import (ANNO_PARAMETER, ANNO_RESULT, ANNO_RESULT_CONVERTER,
-                     generate_metadata)
+from .ctyped import (
+    ANNO_PARAMETER,
+    ANNO_RESULT,
+    ANNO_RESULT_CONVERTER,
+    generate_metadata,
+)
 
 if TYPE_CHECKING:
     from ctypes import CDLL
     from types import EllipsisType
 
-    from .cindex import (CCRStructure, CodeCompletionResults,
-                         CompilationDatabase, CompileCommands, Cursor,
-                         CursorKind, Diagnostic, File, FileInclusion, Index,
-                         Rewriter, SourceLocation, SourceRange, StrPath,
-                         TemplateArgumentKind, Token, TranslationUnit)
+    from .cindex import (
+        CCRStructure,
+        CodeCompletionResults,
+        CompilationDatabase,
+        CompileCommands,
+        Cursor,
+        CursorKind,
+        Diagnostic,
+        File,
+        FileInclusion,
+        Index,
+        Rewriter,
+        SourceLocation,
+        SourceRange,
+        StrPath,
+        TemplateArgumentKind,
+        Token,
+        TranslationUnit,
+    )
     from .cindex import Type as ASTType
     from .cindex import _CXString, _CXUnsavedFile
 else:
     EllipsisType = type(Ellipsis)
 
 
 # delayed imports, a list of import name and their alias
 # if alias is same as name, use `...`
 CINDEX_DELAYED_IMPORTS: List[Tuple[str, Union[str, EllipsisType]]] = [
-    ('CCRStructure', ...),
-    ('CodeCompletionResults', ...),
-    ('CompilationDatabase', ...),
-    ('CompileCommands', ...),
-    ('Cursor', ...),
-    ('CursorKind', ...),
-    ('Diagnostic', ...),
-    ('File', ...),
-    ('FileInclusion', ...),
-    ('Index', ...),
-    ('Rewriter', ...),
-    ('SourceLocation', ...),
-    ('SourceRange', ...),
-    ('TemplateArgumentKind', ...),
-    ('Token', ...),
-    ('TranslationUnit', ...),
-    ('Type', 'ASTType'),
-    ('_CXString', ...),
-    ('_CXUnsavedFile', ...),
-    ('c_interop_string', ...),
+    ("CCRStructure", ...),
+    ("CodeCompletionResults", ...),
+    ("CompilationDatabase", ...),
+    ("CompileCommands", ...),
+    ("Cursor", ...),
+    ("CursorKind", ...),
+    ("Diagnostic", ...),
+    ("File", ...),
+    ("FileInclusion", ...),
+    ("Index", ...),
+    ("Rewriter", ...),
+    ("SourceLocation", ...),
+    ("SourceRange", ...),
+    ("TemplateArgumentKind", ...),
+    ("Token", ...),
+    ("TranslationUnit", ...),
+    ("Type", "ASTType"),
+    ("_CXString", ...),
+    ("_CXUnsavedFile", ...),
+    ("c_interop_string", ...),
 ]
+
 
 def load_cindex_types() -> None:
     cindex_imports: Dict[str, Any] = {}
     from . import cindex
+
     for name, alias in CINDEX_DELAYED_IMPORTS:
-        if isinstance(alias, EllipsisType): alias = name
+        if isinstance(alias, EllipsisType):
+            alias = name
         cindex_imports[alias] = getattr(cindex, name)
     globals().update(cindex_imports)
 
 
 # ctypes doesn't implicitly convert c_void_p to the appropriate wrapper
@@ -69,79 +90,111 @@
 CObjectP = CPointer[c_void_p]
 c_object_p: Type[CObjectP] = convert_annotation(CObjectP)
 
 
 # Register callback types
-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]
+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]
 
 # TODO: these lines should replace the definition in cindex.py
-#translation_unit_includes_callback: Type[CFuncPointer] = convert_annotation(TranslationUnitIncludesCallback, globals())
-#cursor_visit_callback: Type[CFuncPointer] = convert_annotation(CursorVisitCallback, globals())
-#fields_visit_callback: Type[CFuncPointer] = convert_annotation(FieldsVisitCallback, globals())
+# translation_unit_includes_callback: Type[CFuncPointer] = convert_annotation(TranslationUnitIncludesCallback, globals())
+# cursor_visit_callback: Type[CFuncPointer] = convert_annotation(CursorVisitCallback, globals())
+# fields_visit_callback: Type[CFuncPointer] = convert_annotation(FieldsVisitCallback, globals())
 
 
 # Misc object param/result types
 # A type may only have param type or result type, this is normal.
-ASTTypeResult = Annotated['ASTType', ANNO_RESULT, 'ASTType', 'ASTType.from_result']
-
-CInteropStringParam = Annotated[Union[str, bytes, None], ANNO_PARAMETER, 'c_interop_string']
-CInteropStringResult = Annotated[Optional[str], ANNO_RESULT, 'c_interop_string', 'c_interop_string.to_python_string']
-
-CXStringResult = Annotated[str, ANNO_RESULT, '_CXString', '_CXString.from_result']
-
-CompilationDatabaseParam = Annotated['CompilationDatabase', ANNO_PARAMETER, c_object_p]
-CompilationDatabaseResult = Annotated['CompilationDatabase', ANNO_RESULT, c_object_p, 'CompilationDatabase.from_result']
-
-CompileCommandsResult = Annotated['CompileCommands', ANNO_RESULT, c_object_p, 'CompileCommands.from_result']
-
-CursorResult = Annotated['Cursor', ANNO_RESULT, 'Cursor', 'Cursor.from_cursor_result']
-CursorNullableResult = Annotated[Optional['Cursor'], ANNO_RESULT, 'Cursor', 'Cursor.from_result']
-
-DiagnosticParam = Annotated['Diagnostic', ANNO_PARAMETER, c_object_p]
-
-FileResult = Annotated['File', ANNO_RESULT, c_object_p, 'File.from_result']
-
-TemplateArgumentKindResult = Annotated['TemplateArgumentKind', ANNO_RESULT_CONVERTER, 'TemplateArgumentKind.from_id']
-
-TranslationUnitParam = Annotated['TranslationUnit', ANNO_PARAMETER, c_object_p]
+ASTTypeResult = Annotated["ASTType", ANNO_RESULT, "ASTType", "ASTType.from_result"]
+
+CInteropStringParam = Annotated[
+    Union[str, bytes, None], ANNO_PARAMETER, "c_interop_string"
+]
+CInteropStringResult = Annotated[
+    Optional[str], ANNO_RESULT, "c_interop_string", "c_interop_string.to_python_string"
+]
+
+CXStringResult = Annotated[str, ANNO_RESULT, "_CXString", "_CXString.from_result"]
+
+CompilationDatabaseParam = Annotated["CompilationDatabase", ANNO_PARAMETER, c_object_p]
+CompilationDatabaseResult = Annotated[
+    "CompilationDatabase", ANNO_RESULT, c_object_p, "CompilationDatabase.from_result"
+]
+
+CompileCommandsResult = Annotated[
+    "CompileCommands", ANNO_RESULT, c_object_p, "CompileCommands.from_result"
+]
+
+CursorResult = Annotated["Cursor", ANNO_RESULT, "Cursor", "Cursor.from_cursor_result"]
+CursorNullableResult = Annotated[
+    Optional["Cursor"], ANNO_RESULT, "Cursor", "Cursor.from_result"
+]
+
+DiagnosticParam = Annotated["Diagnostic", ANNO_PARAMETER, c_object_p]
+
+FileResult = Annotated["File", ANNO_RESULT, c_object_p, "File.from_result"]
+
+TemplateArgumentKindResult = Annotated[
+    "TemplateArgumentKind", ANNO_RESULT_CONVERTER, "TemplateArgumentKind.from_id"
+]
+
+TranslationUnitParam = Annotated["TranslationUnit", ANNO_PARAMETER, c_object_p]
 
 
 # Functions strictly alphabetical order.
 # NOTE:
 #   - These functions are stubs, they are not implemented, and is replaced by C functions at runtime.
 #   - 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: CUlongParam, p4: CPointerParam[Cursor]) -> CLongResult:
-        raise NotImplementedError
-
-    def clang_CompilationDatabase_dispose(self, p1: CompilationDatabaseParam) -> CLongResult:
-        raise NotImplementedError
-
-    def clang_CompilationDatabase_fromDirectory(self, p1: CInteropStringParam, p2: CPointerParam[c_ulong]) -> CompilationDatabaseResult:
-        raise NotImplementedError
-
-    def clang_CompilationDatabase_getAllCompileCommands(self, p1: CompilationDatabaseParam) -> CompileCommandsResult:
-        raise NotImplementedError
-
-    def clang_CompilationDatabase_getCompileCommands(self, p1: CompilationDatabaseParam, p2: CInteropStringParam) -> CompileCommandsResult:
+    def clang_annotateTokens(
+        self,
+        p1: TranslationUnit,
+        p2: CPointerParam[Token],
+        p3: CUlongParam,
+        p4: CPointerParam[Cursor],
+    ) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CompilationDatabase_dispose(
+        self, p1: CompilationDatabaseParam
+    ) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CompilationDatabase_fromDirectory(
+        self, p1: CInteropStringParam, p2: CPointerParam[c_ulong]
+    ) -> CompilationDatabaseResult:
+        raise NotImplementedError
+
+    def clang_CompilationDatabase_getAllCompileCommands(
+        self, p1: CompilationDatabaseParam
+    ) -> CompileCommandsResult:
+        raise NotImplementedError
+
+    def clang_CompilationDatabase_getCompileCommands(
+        self, p1: CompilationDatabaseParam, p2: CInteropStringParam
+    ) -> CompileCommandsResult:
         raise NotImplementedError
 
     def clang_CompileCommands_dispose(self, p1: CObjectP) -> CLongResult:
         raise NotImplementedError
 
-    def clang_CompileCommands_getCommand(self, p1: CObjectP, p2: CUlongParam) -> CObjectP:
+    def clang_CompileCommands_getCommand(
+        self, p1: CObjectP, p2: CUlongParam
+    ) -> CObjectP:
         raise NotImplementedError
 
     def clang_CompileCommands_getSize(self, p1: CObjectP) -> CUlongResult:
         raise NotImplementedError
 
-    def clang_CompileCommand_getArg(self, p1: CObjectP, p2: CUlongParam) -> CXStringResult:
+    def clang_CompileCommand_getArg(
+        self, p1: CObjectP, p2: CUlongParam
+    ) -> CXStringResult:
         raise NotImplementedError
 
     def clang_CompileCommand_getDirectory(self, p1: CObjectP) -> CXStringResult:
         raise NotImplementedError
 
@@ -149,41 +202,60 @@
         raise NotImplementedError
 
     def clang_CompileCommand_getNumArgs(self, p1: CObjectP) -> CUlongResult:
         raise NotImplementedError
 
-    def clang_codeCompleteAt(self, p1: TranslationUnit, p2: CInteropStringParam, p3: CLongParam, p4: CLongParam, p5: CPointerParam[_CXUnsavedFile], p6: CLongParam, p7: CLongParam) -> CPointer[CCRStructure]:
-        raise NotImplementedError
-
-    def clang_codeCompleteGetDiagnostic(self, p1: CodeCompletionResults, p2: CLongParam) -> Diagnostic:
-        raise NotImplementedError
-
-    def clang_codeCompleteGetNumDiagnostics(self, p1: CodeCompletionResults) -> CLongResult:
+    def clang_codeCompleteAt(
+        self,
+        p1: TranslationUnit,
+        p2: CInteropStringParam,
+        p3: CLongParam,
+        p4: CLongParam,
+        p5: CPointerParam[_CXUnsavedFile],
+        p6: CLongParam,
+        p7: CLongParam,
+    ) -> CPointer[CCRStructure]:
+        raise NotImplementedError
+
+    def clang_codeCompleteGetDiagnostic(
+        self, p1: CodeCompletionResults, p2: CLongParam
+    ) -> Diagnostic:
+        raise NotImplementedError
+
+    def clang_codeCompleteGetNumDiagnostics(
+        self, p1: CodeCompletionResults
+    ) -> CLongResult:
         raise NotImplementedError
 
     def clang_createIndex(self, p1: CLongParam, p2: CLongParam) -> CObjectP:
         raise NotImplementedError
 
-    def clang_createTranslationUnit(self, p1: Index, p2: CInteropStringParam) -> CObjectP:
+    def clang_createTranslationUnit(
+        self, p1: Index, p2: CInteropStringParam
+    ) -> CObjectP:
         raise NotImplementedError
 
     def clang_CXRewriter_create(self, p1: TranslationUnit) -> CObjectP:
         raise NotImplementedError
 
     def clang_CXRewriter_dispose(self, p1: Rewriter) -> CLongResult:
         raise NotImplementedError
 
-    def clang_CXRewriter_insertTextBefore(self, p1: Rewriter, p2: SourceLocation, p3: CInteropStringParam) -> CLongResult:
+    def clang_CXRewriter_insertTextBefore(
+        self, p1: Rewriter, p2: SourceLocation, p3: CInteropStringParam
+    ) -> CLongResult:
         raise NotImplementedError
 
     def clang_CXRewriter_overwriteChangedFiles(self, p1: Rewriter) -> CLongResult:
         raise NotImplementedError
 
     def clang_CXRewriter_removeText(self, p1: Rewriter, p2: SourceRange) -> CLongResult:
         raise NotImplementedError
 
-    def clang_CXRewriter_replaceText(self, p1: Rewriter, p2: SourceRange, p3: CInteropStringParam) -> CLongResult:
+    def clang_CXRewriter_replaceText(
+        self, p1: Rewriter, p2: SourceRange, p3: CInteropStringParam
+    ) -> CLongResult:
         raise NotImplementedError
 
     def clang_CXRewriter_writeMainFileToStdOut(self, p1: Rewriter) -> CLongResult:
         raise NotImplementedError
 
@@ -242,11 +314,13 @@
         raise NotImplementedError
 
     def clang_defaultSaveOptions(self, p1: TranslationUnit) -> CUlongResult:
         raise NotImplementedError
 
-    def clang_disposeCodeCompleteResults(self, p1: CodeCompletionResults) -> CLongResult:
+    def clang_disposeCodeCompleteResults(
+        self, p1: CodeCompletionResults
+    ) -> CLongResult:
         raise NotImplementedError
 
     def clang_disposeDiagnostic(self, p1: Diagnostic) -> CLongResult:
         raise NotImplementedError
 
@@ -254,11 +328,13 @@
         raise NotImplementedError
 
     def clang_disposeString(self, p1: _CXString) -> CLongResult:
         raise NotImplementedError
 
-    def clang_disposeTokens(self, p1: TranslationUnit, p2: CPointer[Token], p3: CUintParam) -> CLongResult:
+    def clang_disposeTokens(
+        self, p1: TranslationUnit, p2: CPointer[Token], p3: CUintParam
+    ) -> CLongResult:
         raise NotImplementedError
 
     def clang_disposeTranslationUnit(self, p1: TranslationUnit) -> CLongResult:
         raise NotImplementedError
 
@@ -305,17 +381,21 @@
         raise NotImplementedError
 
     def clang_getCompletionBriefComment(self, p1: CObjectP) -> CXStringResult:
         raise NotImplementedError
 
-    def clang_getCompletionChunkCompletionString(self, p1: CObjectP, p2: CLongParam) -> CObjectP:
+    def clang_getCompletionChunkCompletionString(
+        self, p1: CObjectP, p2: CLongParam
+    ) -> CObjectP:
         raise NotImplementedError
 
     def clang_getCompletionChunkKind(self, p1: CObjectP, p2: CLongParam) -> CLongResult:
         raise NotImplementedError
 
-    def clang_getCompletionChunkText(self, p1: CObjectP, p2: CLongParam) -> CXStringResult:
+    def clang_getCompletionChunkText(
+        self, p1: CObjectP, p2: CLongParam
+    ) -> CXStringResult:
         raise NotImplementedError
 
     def clang_getCompletionPriority(self, p1: CObjectP) -> CLongResult:
         raise NotImplementedError
 
@@ -350,11 +430,13 @@
         raise NotImplementedError
 
     def clang_getCursorReferenced(self, p1: Cursor) -> CursorNullableResult:
         raise NotImplementedError
 
-    def clang_getCursorReferenceNameRange(self, p1: Cursor, p2: CUlongParam, p3: CUlongParam) -> SourceRange:
+    def clang_getCursorReferenceNameRange(
+        self, p1: Cursor, p2: CUlongParam, p3: CUlongParam
+    ) -> SourceRange:
         raise NotImplementedError
 
     def clang_getCursorResultType(self, p1: Cursor) -> ASTTypeResult:
         raise NotImplementedError
 
@@ -380,20 +462,24 @@
         raise NotImplementedError
 
     def clang_getDeclObjCTypeEncoding(self, p1: Cursor) -> CXStringResult:
         raise NotImplementedError
 
-    def clang_getDiagnostic(self, p1: TranslationUnitParam, p2: CUlongParam) -> CObjectP:
+    def clang_getDiagnostic(
+        self, p1: TranslationUnitParam, p2: CUlongParam
+    ) -> CObjectP:
         raise NotImplementedError
 
     def clang_getDiagnosticCategory(self, p1: Diagnostic) -> CUlongResult:
         raise NotImplementedError
 
     def clang_getDiagnosticCategoryText(self, p1: Diagnostic) -> CXStringResult:
         raise NotImplementedError
 
-    def clang_getDiagnosticFixIt(self, p1: Diagnostic, p2: CUlongParam, p3: CPointerParam[SourceRange]) -> CXStringResult:
+    def clang_getDiagnosticFixIt(
+        self, p1: Diagnostic, p2: CUlongParam, p3: CPointerParam[SourceRange]
+    ) -> CXStringResult:
         raise NotImplementedError
 
     def clang_getDiagnosticInSet(self, p1: CObjectP, p2: CUlongParam) -> CObjectP:
         raise NotImplementedError
 
@@ -404,11 +490,13 @@
         raise NotImplementedError
 
     def clang_getDiagnosticNumRanges(self, p1: Diagnostic) -> CUlongResult:
         raise NotImplementedError
 
-    def clang_getDiagnosticOption(self, p1: Diagnostic, p2: CPointerParam[_CXString]) -> CXStringResult:
+    def clang_getDiagnosticOption(
+        self, p1: Diagnostic, p2: CPointerParam[_CXString]
+    ) -> CXStringResult:
         raise NotImplementedError
 
     def clang_getDiagnosticRange(self, p1: Diagnostic, p2: CUlongParam) -> SourceRange:
         raise NotImplementedError
 
@@ -446,20 +534,36 @@
         raise NotImplementedError
 
     def clang_getIncludedFile(self, p1: Cursor) -> FileResult:
         raise NotImplementedError
 
-    def clang_getInclusions(self, p1: TranslationUnit, p2: TranslationUnitIncludesCallback, p3: CPyObject[List[FileInclusion]]) -> CLongResult:
-        raise NotImplementedError
-
-    def clang_getInstantiationLocation(self, p1: SourceLocation, p2: CPointerParam[CObjectP], p3: CPointerParam[c_ulong], p4: CPointerParam[c_ulong], p5: CPointerParam[c_ulong]) -> CLongResult:
-        raise NotImplementedError
-
-    def clang_getLocation(self, p1: TranslationUnit, p2: File, p3: CUlongParam, p4: CUlongParam) -> SourceLocation:
-        raise NotImplementedError
-
-    def clang_getLocationForOffset(self, p1: TranslationUnit, p2: File, p3: CUlongParam) -> SourceLocation:
+    def clang_getInclusions(
+        self,
+        p1: TranslationUnit,
+        p2: TranslationUnitIncludesCallback,
+        p3: CPyObject[List[FileInclusion]],
+    ) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getInstantiationLocation(
+        self,
+        p1: SourceLocation,
+        p2: CPointerParam[CObjectP],
+        p3: CPointerParam[c_ulong],
+        p4: CPointerParam[c_ulong],
+        p5: CPointerParam[c_ulong],
+    ) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getLocation(
+        self, p1: TranslationUnit, p2: File, p3: CUlongParam, p4: CUlongParam
+    ) -> SourceLocation:
+        raise NotImplementedError
+
+    def clang_getLocationForOffset(
+        self, p1: TranslationUnit, p2: File, p3: CUlongParam
+    ) -> SourceLocation:
         raise NotImplementedError
 
     def clang_getNullCursor(self) -> Cursor:
         raise NotImplementedError
 
@@ -515,11 +619,13 @@
         raise NotImplementedError
 
     def clang_getTokenSpelling(self, p1: TranslationUnit, p2: Token) -> CXStringResult:
         raise NotImplementedError
 
-    def clang_getTranslationUnitCursor(self, p1: TranslationUnit) -> CursorNullableResult:
+    def clang_getTranslationUnitCursor(
+        self, p1: TranslationUnit
+    ) -> CursorNullableResult:
         raise NotImplementedError
 
     def clang_getTranslationUnitSpelling(self, p1: TranslationUnit) -> CXStringResult:
         raise NotImplementedError
 
@@ -593,44 +699,79 @@
         raise NotImplementedError
 
     def clang_isVolatileQualifiedType(self, p1: ASTType) -> bool:
         raise NotImplementedError
 
-    def clang_parseTranslationUnit(self, p1: Index, p2: CInteropStringParam, p3: CPointerParam[c_char_p], p4: CLongParam, p5: CPointerParam[_CXUnsavedFile], p6: CLongParam, p7: CLongParam) -> CObjectP:
-        raise NotImplementedError
-
-    def clang_reparseTranslationUnit(self, p1: TranslationUnit, p2: CLongParam, p3: CPointerParam[_CXUnsavedFile], p4: CLongParam) -> CLongResult:
-        raise NotImplementedError
-
-    def clang_saveTranslationUnit(self, p1: TranslationUnit, p2: CInteropStringParam, p3: CUlongParam) -> CLongResult:
-        raise NotImplementedError
-
-    def clang_tokenize(self, p1: TranslationUnit, p2: SourceRange, p3: CPointerParam[CPointer[Token]], p4: CPointerParam[c_ulong]) -> CLongResult:
-        raise NotImplementedError
-
-    def clang_visitChildren(self, p1: Cursor, p2: CursorVisitCallback, p3: CPyObject[List[Cursor]]) -> CUlongResult:
+    def clang_parseTranslationUnit(
+        self,
+        p1: Index,
+        p2: CInteropStringParam,
+        p3: CPointerParam[c_char_p],
+        p4: CLongParam,
+        p5: CPointerParam[_CXUnsavedFile],
+        p6: CLongParam,
+        p7: CLongParam,
+    ) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_reparseTranslationUnit(
+        self,
+        p1: TranslationUnit,
+        p2: CLongParam,
+        p3: CPointerParam[_CXUnsavedFile],
+        p4: CLongParam,
+    ) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_saveTranslationUnit(
+        self, p1: TranslationUnit, p2: CInteropStringParam, p3: CUlongParam
+    ) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_tokenize(
+        self,
+        p1: TranslationUnit,
+        p2: SourceRange,
+        p3: CPointerParam[CPointer[Token]],
+        p4: CPointerParam[c_ulong],
+    ) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_visitChildren(
+        self, p1: Cursor, p2: CursorVisitCallback, p3: CPyObject[List[Cursor]]
+    ) -> CUlongResult:
         raise NotImplementedError
 
     def clang_Cursor_getNumArguments(self, p1: Cursor) -> CLongResult:
         raise NotImplementedError
 
-    def clang_Cursor_getArgument(self, p1: Cursor, p2: CUlongParam) -> CursorNullableResult:
+    def clang_Cursor_getArgument(
+        self, p1: Cursor, p2: CUlongParam
+    ) -> CursorNullableResult:
         raise NotImplementedError
 
     def clang_Cursor_getNumTemplateArguments(self, p1: Cursor) -> CLongResult:
         raise NotImplementedError
 
-    def clang_Cursor_getTemplateArgumentKind(self, p1: Cursor, p2: CUlongParam) -> TemplateArgumentKindResult:
-        raise NotImplementedError
-
-    def clang_Cursor_getTemplateArgumentType(self, p1: Cursor, p2: CUlongParam) -> ASTTypeResult:
-        raise NotImplementedError
-
-    def clang_Cursor_getTemplateArgumentValue(self, p1: Cursor, p2: CUlongParam) -> CLonglongResult:
-        raise NotImplementedError
-
-    def clang_Cursor_getTemplateArgumentUnsignedValue(self, p1: Cursor, p2: CUlongParam) -> CUlonglongResult:
+    def clang_Cursor_getTemplateArgumentKind(
+        self, p1: Cursor, p2: CUlongParam
+    ) -> TemplateArgumentKindResult:
+        raise NotImplementedError
+
+    def clang_Cursor_getTemplateArgumentType(
+        self, p1: Cursor, p2: CUlongParam
+    ) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_Cursor_getTemplateArgumentValue(
+        self, p1: Cursor, p2: CUlongParam
+    ) -> CLonglongResult:
+        raise NotImplementedError
+
+    def clang_Cursor_getTemplateArgumentUnsignedValue(
+        self, p1: Cursor, p2: CUlongParam
+    ) -> CUlonglongResult:
         raise NotImplementedError
 
     def clang_Cursor_isAnonymous(self, p1: Cursor) -> bool:
         raise NotImplementedError
 
@@ -659,14 +800,18 @@
         raise NotImplementedError
 
     def clang_Type_getNumTemplateArguments(self, p1: ASTType) -> CLongResult:
         raise NotImplementedError
 
-    def clang_Type_getTemplateArgumentAsType(self, p1: ASTType, p2: CUlongParam) -> ASTTypeResult:
-        raise NotImplementedError
-
-    def clang_Type_getOffsetOf(self, p1: ASTType, p2: CInteropStringParam) -> CLonglongResult:
+    def clang_Type_getTemplateArgumentAsType(
+        self, p1: ASTType, p2: CUlongParam
+    ) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_Type_getOffsetOf(
+        self, p1: ASTType, p2: CInteropStringParam
+    ) -> CLonglongResult:
         raise NotImplementedError
 
     def clang_Type_getSizeOf(self, p1: ASTType) -> CLonglongResult:
         raise NotImplementedError
 
@@ -674,11 +819,13 @@
         raise NotImplementedError
 
     def clang_Type_getNamedType(self, p1: ASTType) -> ASTTypeResult:
         raise NotImplementedError
 
-    def clang_Type_visitFields(self, p1: ASTType, p2: FieldsVisitCallback, p3: CPyObject[List[Cursor]]) -> CUlongResult:
+    def clang_Type_visitFields(
+        self, p1: ASTType, p2: FieldsVisitCallback, p3: CPyObject[List[Cursor]]
+    ) -> CUlongResult:
         raise NotImplementedError
 
 
 class LibclangError(Exception):
     m: str
@@ -761,13 +908,15 @@
             self._lib = exports
         return self._lib
 
     @staticmethod
     def cfunc_metadata() -> Dict[str, Dict[str, Any]]:
-        ''' Generate ctypes metadata for debugging purpose. '''
+        """Generate ctypes metadata for debugging purpose."""
         load_cindex_types()
-        return {name: info for name, info in generate_metadata(LibclangExports, globals())}
+        return {
+            name: info for name, info in generate_metadata(LibclangExports, globals())
+        }
 
     def get_filename(self) -> str:
         if Config.library_file:
             return Config.library_file
 
@@ -799,6 +948,6 @@
             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_")
--- clang/ctyped.py	2024-08-06 14:17:29.000000 +0000
+++ clang/ctyped.py	2024-08-08 15:03:53.067877 +0000
@@ -1,25 +1,60 @@
 # pyright: reportPrivateUsage=false
 
 import sys
-from ctypes import (CFUNCTYPE, POINTER, 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,
+    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, ForwardRef, Generator, Generic,
-                    List, Optional, Tuple, Type, TypeVar, Union, cast)
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Callable,
+    Dict,
+    ForwardRef,
+    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, _CData
 
-AnyCData = TypeVar('AnyCData', bound='_CData')
+AnyCData = TypeVar("AnyCData", bound="_CData")
 
 
 if TYPE_CHECKING:
     from ctypes import Array as _Array
     from ctypes import _FuncPointer as _FuncPointer
@@ -31,29 +66,41 @@
     # "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]
+    _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]):
+        ...
 
 
 if sys.platform == "win32":
     from ctypes import WINFUNCTYPE
 else:
+
     def WINFUNCTYPE(
-        restype: Type['_CData'] | None,
-        *argtypes: Type['_CData'],
+        restype: Type["_CData"] | None,
+        *argtypes: Type["_CData"],
         use_errno: bool = False,
-        use_last_error: bool = False
+        use_last_error: bool = False,
     ) -> Type[_FuncPointer]:
         raise NotImplementedError
 
 
 # ANNO_CONVETIBLE can be used to declare that a class have a `from_param`
@@ -63,12 +110,12 @@
 # `CClass` as parameter type in stub function declaration and you will get what
 # you want.
 
 ANNO_BASIC = object()
 ANNO_PARAMETER = ANNO_BASIC
-ANNO_RESULT = object() # ANNO_RESULT_ERRCHECK
-ANNO_RESULT_CONVERTER = object() # deprecated by ctypes
+ANNO_RESULT = object()  # ANNO_RESULT_ERRCHECK
+ANNO_RESULT_CONVERTER = object()  # deprecated by ctypes
 ANNO_ARRAY = object()
 ANNO_POINTER = object()
 ANNO_CFUNC = object()
 ANNO_WINFUNC = object()
 ANNO_PYOBJ = object()
@@ -115,13 +162,21 @@
 CSizeTParam = Annotated[Union[c_size_t, int], ANNO_BASIC, c_size_t]
 CSsizeTParam = Annotated[Union[c_ssize_t, int], ANNO_BASIC, c_ssize_t]
 CFloatParam = Annotated[Union[c_float, float], ANNO_BASIC, c_float]
 CDoubleParam = Annotated[Union[c_double, float], ANNO_BASIC, c_double]
 CLongDoubleParam = Annotated[Union[c_longdouble, float], ANNO_BASIC, c_longdouble]
-CCharPParam = Annotated[Union[_Array[c_wchar], c_char_p, bytes, None], ANNO_BASIC, c_char_p]
-CWcharPParam = Annotated[Union[_Array[c_wchar], c_wchar_p, str, None], ANNO_BASIC, c_wchar_p]
-CVoidPParam = Annotated[Union['_CArgObject', _Pointer[Any], _Array[Any], c_void_p, int, None], ANNO_BASIC, c_void_p]
+CCharPParam = Annotated[
+    Union[_Array[c_wchar], c_char_p, bytes, None], ANNO_BASIC, c_char_p
+]
+CWcharPParam = Annotated[
+    Union[_Array[c_wchar], c_wchar_p, str, None], ANNO_BASIC, c_wchar_p
+]
+CVoidPParam = Annotated[
+    Union["_CArgObject", _Pointer[Any], _Array[Any], 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]
@@ -132,210 +187,253 @@
 
 
 # using decorators to declare errcheck and converter is convenient
 # but you will need to use ANNO_RESULT instead if you need delayed evaluation
 
-_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, CIntResult]], 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, CIntResult]], Callable[_Params, _NewRet]]:
+    """Decorates a stub function with a converter, its return type MUST be `r_int`."""
+
     def decorator(wrapped: Callable[_Params, CIntResult]) -> 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, ForwardRef):
         typ = typ.__forward_arg__
 
     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_BASIC:
-        ctyp, = detail
+        (ctyp,) = detail
         return convert_annotation(ctyp, global_ns=global_ns)
 
     elif ident is ANNO_RESULT:
         ctyp, _ = detail
         return convert_annotation(ctyp, global_ns=global_ns)
 
     elif ident is ANNO_RESULT_CONVERTER:
         return c_int
 
     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 get_resconv_info(typ: Any, global_ns: Optional[Dict[str, Any]] = None) -> Optional[Tuple[Any, Any]]:
+        raise ValueError(f"Unexpected annotated type {typ}")
+
+
+def get_resconv_info(
+    typ: Any, global_ns: Optional[Dict[str, Any]] = None
+) -> Optional[Tuple[Any, Any]]:
     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 None
     # type is Annotated
     ident, *detail = typ.__metadata__
     if ident not in (ANNO_RESULT, ANNO_RESULT_CONVERTER):
         return None
 
     if ident is ANNO_RESULT:
         _, conv = detail
     else:
-        conv, = detail
+        (conv,) = detail
     if isinstance(conv, str):
         conv = eval(conv, global_ns)
     if ident is ANNO_RESULT:
         return (conv, None)
     else:
         return (None, conv)
 
 
-def convert_func_decl(decl: Callable[..., Any], global_ns: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
-    ''' Converts a stub function to ctypes metadata. '''
+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 or converter is not None:
-                raise ValueError('duplicate return conversion specifications, burst your legs')
-            decl, errcheck = getattr(decl, '_decl_errcheck_')
+                raise ValueError(
+                    "duplicate return conversion specifications, burst your legs"
+                )
+            decl, errcheck = getattr(decl, "_decl_errcheck_")
             continue
 
-        if hasattr(decl, '_decl_converter_'):
+        if hasattr(decl, "_decl_converter_"):
             if errcheck is not None or converter is not None:
-                raise ValueError('duplicate return conversion specifications, burst your legs')
-            decl, converter = getattr(decl, '_decl_converter_')
+                raise ValueError(
+                    "duplicate return conversion specifications, burst your legs"
+                )
+            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:
         resconv = get_resconv_info(sig.return_annotation, global_ns=global_ns)
         if resconv is not None:
             if errcheck is not None or converter is not None:
-                ValueError('duplicate return conversion specifications, burst your legs')
+                ValueError(
+                    "duplicate return conversion specifications, burst your legs"
+                )
             errcheck, converter = resconv
-        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
-_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. '''
+_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
@@ -347,87 +445,82 @@
 
     return result, missing
 
 
 __all__ = [
-    'ANNO_PARAMETER',
-    'AnyCData',
-
-    'c_bool',
-    'c_char',
-    'c_wchar',
-    'c_byte',
-    'c_ubyte',
-    'c_short',
-    'c_ushort',
-    'c_int',
-    'c_uint',
-    'c_long',
-    'c_ulong',
-    'c_longlong',
-    'c_ulonglong',
-    'c_size_t',
-    'c_ssize_t',
-    'c_float',
-    'c_double',
-    'c_longdouble',
-    'c_char_p',
-    'c_wchar_p',
-    'c_void_p',
-    'py_object',
-
-    'CBoolParam',
-    'CCharParam',
-    'CWcharParam',
-    'CByteParam',
-    'CUbyteParam',
-    'CShortParam',
-    'CUshortParam',
-    'CIntParam',
-    'CUintParam',
-    'CLongParam',
-    'CUlongParam',
-    'CLonglongParam',
-    'CUlonglongParam',
-    'CSizeTParam',
-    'CSsizeTParam',
-    'CFloatParam',
-    'CDoubleParam',
-    'CLongDoubleParam',
-    'CCharPParam',
-    'CWcharPParam',
-    'CVoidPParam',
-
-    'CBoolResult',
-    'CCharResult',
-    'CWcharResult',
-    'CByteResult',
-    'CUbyteResult',
-    'CShortResult',
-    'CUshortResult',
-    'CIntResult',
-    'CUintResult',
-    'CLongResult',
-    'CUlongResult',
-    'CLonglongResult',
-    'CUlonglongResult',
-    'CSizeTResult',
-    'CSsizeTResult',
-    'CFloatResult',
-    'CDoubleResult',
-    'CLongdoubleResult',
-    'CCharPResult',
-    'CWcharPResult',
-    'CVoidPResult',
-
-    'CArray',
-    'CPointer',
-    'CPointerParam',
-    'CFuncPointer',
-    'WinFuncPointer',
-    'CPyObject',
-
-    'convert_annotation',
-    'with_errcheck',
-    'with_converter',
-    'load_annotated_library',
+    "ANNO_PARAMETER",
+    "AnyCData",
+    "c_bool",
+    "c_char",
+    "c_wchar",
+    "c_byte",
+    "c_ubyte",
+    "c_short",
+    "c_ushort",
+    "c_int",
+    "c_uint",
+    "c_long",
+    "c_ulong",
+    "c_longlong",
+    "c_ulonglong",
+    "c_size_t",
+    "c_ssize_t",
+    "c_float",
+    "c_double",
+    "c_longdouble",
+    "c_char_p",
+    "c_wchar_p",
+    "c_void_p",
+    "py_object",
+    "CBoolParam",
+    "CCharParam",
+    "CWcharParam",
+    "CByteParam",
+    "CUbyteParam",
+    "CShortParam",
+    "CUshortParam",
+    "CIntParam",
+    "CUintParam",
+    "CLongParam",
+    "CUlongParam",
+    "CLonglongParam",
+    "CUlonglongParam",
+    "CSizeTParam",
+    "CSsizeTParam",
+    "CFloatParam",
+    "CDoubleParam",
+    "CLongDoubleParam",
+    "CCharPParam",
+    "CWcharPParam",
+    "CVoidPParam",
+    "CBoolResult",
+    "CCharResult",
+    "CWcharResult",
+    "CByteResult",
+    "CUbyteResult",
+    "CShortResult",
+    "CUshortResult",
+    "CIntResult",
+    "CUintResult",
+    "CLongResult",
+    "CUlongResult",
+    "CLonglongResult",
+    "CUlonglongResult",
+    "CSizeTResult",
+    "CSsizeTResult",
+    "CFloatResult",
+    "CDoubleResult",
+    "CLongdoubleResult",
+    "CCharPResult",
+    "CWcharPResult",
+    "CVoidPResult",
+    "CArray",
+    "CPointer",
+    "CPointerParam",
+    "CFuncPointer",
+    "WinFuncPointer",
+    "CPyObject",
+    "convert_annotation",
+    "with_errcheck",
+    "with_converter",
+    "load_annotated_library",
 ]
--- tests/ctyped/test_stub_conversion.py	2024-08-06 14:17:29.000000 +0000
+++ tests/ctyped/test_stub_conversion.py	2024-08-08 15:03:53.238072 +0000
@@ -6,13 +6,19 @@
 from dictdiffer import diff as dictdiff
 
 from clang.binder import Config as BinderConfig
 from clang.binder import c_object_p
 from clang.cindex import *
-from clang.cindex import (CCRStructure, Rewriter, _CXString, c_interop_string,
-                          cursor_visit_callback, fields_visit_callback,
-                          translation_unit_includes_callback)
+from clang.cindex import (
+    CCRStructure,
+    Rewriter,
+    _CXString,
+    c_interop_string,
+    cursor_visit_callback,
+    fields_visit_callback,
+    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.
@@ -302,56 +308,82 @@
 ]
 
 
 # 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
-    if kind == 'add': return True
+    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 = BinderConfig.cfunc_metadata()
 
         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_break = [diffset for diffset in type_diff if diffset and any(is_incompatible_diff(diff) for diff in diffset)]
-
-        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_break}!')
+        type_diff = [
+            list(dictdiff(old_function_dict[name], new_function_dict[name]))
+            for name in stable_functions
+        ]
+        type_break = [
+            diffset
+            for diffset in type_diff
+            if diffset and any(is_incompatible_diff(diff) for diff in diffset)
+        ]
+
+        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_break}!"
+        )

@llvmbot
Copy link
Collaborator

llvmbot commented Aug 5, 2024

@llvm/pr-subscribers-clang

Author: None (TsXor)

Changes

This is part 1 as described in #101784.
I moved LibclangExports, LibclangError and modified Config class to a new file: binder.py. It can now be used by cindex.py with a simple from .binder import Config.
This PR doesn't modify any existing files, so it is safe to merge. However, @DeinAlptraum will need to manually delete functionList, Config class and LibclangError and add from .binder import Config to utilize its typing benefits.

Note: compared to #101784, I added a check that prevents ctyped from importing WINFUNCTYPE when not on windows, which should fix failing linux tests.


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

4 Files Affected:

  • (added) clang/bindings/python/clang/binder.py (+804)
  • (added) clang/bindings/python/clang/ctyped.py (+433)
  • (added) clang/bindings/python/tests/ctyped/init.py ()
  • (added) clang/bindings/python/tests/ctyped/test_stub_conversion.py (+357)
diff --git a/clang/bindings/python/clang/binder.py b/clang/bindings/python/clang/binder.py
new file mode 100644
index 0000000000000..8cc661a097cb2
--- /dev/null
+++ b/clang/bindings/python/clang/binder.py
@@ -0,0 +1,804 @@
+# pyright: reportPrivateUsage=false
+
+# Enable delayed evaluation of function annotations.
+from __future__ import annotations
+
+import os
+from ctypes import cdll
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
+
+from typing_extensions import Annotated
+
+from .ctyped import *
+from .ctyped import (ANNO_PARAMETER, ANNO_RESULT, ANNO_RESULT_CONVERTER,
+                     generate_metadata)
+
+if TYPE_CHECKING:
+    from ctypes import CDLL
+    from types import EllipsisType
+
+    from .cindex import (CCRStructure, CodeCompletionResults,
+                         CompilationDatabase, CompileCommands, Cursor,
+                         CursorKind, Diagnostic, File, FileInclusion, Index,
+                         Rewriter, SourceLocation, SourceRange, StrPath,
+                         TemplateArgumentKind, Token, TranslationUnit)
+    from .cindex import Type as ASTType
+    from .cindex import _CXString, _CXUnsavedFile
+else:
+    EllipsisType = type(Ellipsis)
+
+
+# delayed imports, a list of import name and their alias
+# if alias is same as name, use `...`
+CINDEX_DELAYED_IMPORTS: List[Tuple[str, Union[str, EllipsisType]]] = [
+    ('CCRStructure', ...),
+    ('CodeCompletionResults', ...),
+    ('CompilationDatabase', ...),
+    ('CompileCommands', ...),
+    ('Cursor', ...),
+    ('CursorKind', ...),
+    ('Diagnostic', ...),
+    ('File', ...),
+    ('FileInclusion', ...),
+    ('Index', ...),
+    ('Rewriter', ...),
+    ('SourceLocation', ...),
+    ('SourceRange', ...),
+    ('TemplateArgumentKind', ...),
+    ('Token', ...),
+    ('TranslationUnit', ...),
+    ('Type', 'ASTType'),
+    ('_CXString', ...),
+    ('_CXUnsavedFile', ...),
+    ('c_interop_string', ...),
+]
+
+def load_cindex_types() -> None:
+    cindex_imports: Dict[str, Any] = {}
+    from . import cindex
+    for name, alias in CINDEX_DELAYED_IMPORTS:
+        if isinstance(alias, EllipsisType): alias = name
+        cindex_imports[alias] = getattr(cindex, name)
+    globals().update(cindex_imports)
+
+
+# ctypes doesn't implicitly convert c_void_p to the appropriate wrapper
+# 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**.
+CObjectP = CPointer[c_void_p]
+c_object_p: Type[CObjectP] = convert_annotation(CObjectP)
+
+
+# Register callback types
+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]
+
+# TODO: these lines should replace the definition in cindex.py
+#translation_unit_includes_callback: Type[CFuncPointer] = convert_annotation(TranslationUnitIncludesCallback, globals())
+#cursor_visit_callback: Type[CFuncPointer] = convert_annotation(CursorVisitCallback, globals())
+#fields_visit_callback: Type[CFuncPointer] = convert_annotation(FieldsVisitCallback, globals())
+
+
+# Misc object param/result types
+# A type may only have param type or result type, this is normal.
+ASTTypeResult = Annotated['ASTType', ANNO_RESULT, 'ASTType', 'ASTType.from_result']
+
+CInteropStringParam = Annotated[Union[str, bytes, None], ANNO_PARAMETER, 'c_interop_string']
+CInteropStringResult = Annotated[Optional[str], ANNO_RESULT, 'c_interop_string', 'c_interop_string.to_python_string']
+
+CXStringResult = Annotated[str, ANNO_RESULT, '_CXString', '_CXString.from_result']
+
+CompilationDatabaseParam = Annotated['CompilationDatabase', ANNO_PARAMETER, c_object_p]
+CompilationDatabaseResult = Annotated['CompilationDatabase', ANNO_RESULT, c_object_p, 'CompilationDatabase.from_result']
+
+CompileCommandsResult = Annotated['CompileCommands', ANNO_RESULT, c_object_p, 'CompileCommands.from_result']
+
+CursorResult = Annotated['Cursor', ANNO_RESULT, 'Cursor', 'Cursor.from_cursor_result']
+CursorNullableResult = Annotated[Optional['Cursor'], ANNO_RESULT, 'Cursor', 'Cursor.from_result']
+
+DiagnosticParam = Annotated['Diagnostic', ANNO_PARAMETER, c_object_p]
+
+FileResult = Annotated['File', ANNO_RESULT, c_object_p, 'File.from_result']
+
+TemplateArgumentKindResult = Annotated['TemplateArgumentKind', ANNO_RESULT_CONVERTER, 'TemplateArgumentKind.from_id']
+
+TranslationUnitParam = Annotated['TranslationUnit', ANNO_PARAMETER, c_object_p]
+
+
+# Functions strictly alphabetical order.
+# NOTE:
+#   - These functions are stubs, they are not implemented, and is replaced by C functions at runtime.
+#   - 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: CUlongParam, p4: CPointerParam[Cursor]) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CompilationDatabase_dispose(self, p1: CompilationDatabaseParam) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CompilationDatabase_fromDirectory(self, p1: CInteropStringParam, p2: CPointerParam[c_ulong]) -> CompilationDatabaseResult:
+        raise NotImplementedError
+
+    def clang_CompilationDatabase_getAllCompileCommands(self, p1: CompilationDatabaseParam) -> CompileCommandsResult:
+        raise NotImplementedError
+
+    def clang_CompilationDatabase_getCompileCommands(self, p1: CompilationDatabaseParam, p2: CInteropStringParam) -> CompileCommandsResult:
+        raise NotImplementedError
+
+    def clang_CompileCommands_dispose(self, p1: CObjectP) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CompileCommands_getCommand(self, p1: CObjectP, p2: CUlongParam) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_CompileCommands_getSize(self, p1: CObjectP) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_CompileCommand_getArg(self, p1: CObjectP, p2: CUlongParam) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_CompileCommand_getDirectory(self, p1: CObjectP) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_CompileCommand_getFilename(self, p1: CObjectP) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_CompileCommand_getNumArgs(self, p1: CObjectP) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_codeCompleteAt(self, p1: TranslationUnit, p2: CInteropStringParam, p3: CLongParam, p4: CLongParam, p5: CPointerParam[_CXUnsavedFile], p6: CLongParam, p7: CLongParam) -> CPointer[CCRStructure]:
+        raise NotImplementedError
+
+    def clang_codeCompleteGetDiagnostic(self, p1: CodeCompletionResults, p2: CLongParam) -> Diagnostic:
+        raise NotImplementedError
+
+    def clang_codeCompleteGetNumDiagnostics(self, p1: CodeCompletionResults) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_createIndex(self, p1: CLongParam, p2: CLongParam) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_createTranslationUnit(self, p1: Index, p2: CInteropStringParam) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_CXRewriter_create(self, p1: TranslationUnit) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_CXRewriter_dispose(self, p1: Rewriter) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CXRewriter_insertTextBefore(self, p1: Rewriter, p2: SourceLocation, p3: CInteropStringParam) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CXRewriter_overwriteChangedFiles(self, p1: Rewriter) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CXRewriter_removeText(self, p1: Rewriter, p2: SourceRange) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CXRewriter_replaceText(self, p1: Rewriter, p2: SourceRange, p3: CInteropStringParam) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CXRewriter_writeMainFileToStdOut(self, p1: Rewriter) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_CXXConstructor_isConvertingConstructor(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXConstructor_isCopyConstructor(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXConstructor_isDefaultConstructor(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXConstructor_isMoveConstructor(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXField_isMutable(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXMethod_isConst(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXMethod_isDefaulted(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXMethod_isDeleted(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXMethod_isCopyAssignmentOperator(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXMethod_isMoveAssignmentOperator(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXMethod_isExplicit(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXMethod_isPureVirtual(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXMethod_isStatic(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXMethod_isVirtual(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_CXXRecord_isAbstract(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_Cursor_getStorageClass(self, p1: Cursor) -> CIntResult:
+        raise NotImplementedError
+
+    def clang_EnumDecl_isScoped(self, p1: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_defaultDiagnosticDisplayOptions(self) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_defaultSaveOptions(self, p1: TranslationUnit) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_disposeCodeCompleteResults(self, p1: CodeCompletionResults) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_disposeDiagnostic(self, p1: Diagnostic) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_disposeIndex(self, p1: Index) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_disposeString(self, p1: _CXString) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_disposeTokens(self, p1: TranslationUnit, p2: CPointer[Token], p3: CUintParam) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_disposeTranslationUnit(self, p1: TranslationUnit) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_equalCursors(self, p1: Cursor, p2: Cursor) -> bool:
+        raise NotImplementedError
+
+    def clang_equalLocations(self, p1: SourceLocation, p2: SourceLocation) -> bool:
+        raise NotImplementedError
+
+    def clang_equalRanges(self, p1: SourceRange, p2: SourceRange) -> bool:
+        raise NotImplementedError
+
+    def clang_equalTypes(self, p1: ASTType, p2: ASTType) -> bool:
+        raise NotImplementedError
+
+    def clang_formatDiagnostic(self, p1: Diagnostic, p2: CUlongParam) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getAddressSpace(self, p1: ASTType) -> CIntResult:
+        raise NotImplementedError
+
+    def clang_getArgType(self, p1: ASTType, p2: CUlongParam) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_getArrayElementType(self, p1: ASTType) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_getArraySize(self, p1: ASTType) -> CLonglongResult:
+        raise NotImplementedError
+
+    def clang_getFieldDeclBitWidth(self, p1: Cursor) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getCanonicalCursor(self, p1: Cursor) -> CursorResult:
+        raise NotImplementedError
+
+    def clang_getCanonicalType(self, p1: ASTType) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_getChildDiagnostics(self, p1: Diagnostic) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_getCompletionAvailability(self, p1: CObjectP) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getCompletionBriefComment(self, p1: CObjectP) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getCompletionChunkCompletionString(self, p1: CObjectP, p2: CLongParam) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_getCompletionChunkKind(self, p1: CObjectP, p2: CLongParam) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getCompletionChunkText(self, p1: CObjectP, p2: CLongParam) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getCompletionPriority(self, p1: CObjectP) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getCString(self, p1: _CXString) -> CInteropStringResult:
+        raise NotImplementedError
+
+    def clang_getCursor(self, p1: TranslationUnit, p2: SourceLocation) -> Cursor:
+        raise NotImplementedError
+
+    def clang_getCursorAvailability(self, p1: Cursor) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getCursorDefinition(self, p1: Cursor) -> CursorNullableResult:
+        raise NotImplementedError
+
+    def clang_getCursorDisplayName(self, p1: Cursor) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getCursorExceptionSpecificationType(self, p1: Cursor) -> CIntResult:
+        raise NotImplementedError
+
+    def clang_getCursorExtent(self, p1: Cursor) -> SourceRange:
+        raise NotImplementedError
+
+    def clang_getCursorLexicalParent(self, p1: Cursor) -> CursorResult:
+        raise NotImplementedError
+
+    def clang_getCursorLinkage(self, p1: Cursor) -> CIntResult:
+        raise NotImplementedError
+
+    def clang_getCursorLocation(self, p1: Cursor) -> SourceLocation:
+        raise NotImplementedError
+
+    def clang_getCursorReferenced(self, p1: Cursor) -> CursorNullableResult:
+        raise NotImplementedError
+
+    def clang_getCursorReferenceNameRange(self, p1: Cursor, p2: CUlongParam, p3: CUlongParam) -> SourceRange:
+        raise NotImplementedError
+
+    def clang_getCursorResultType(self, p1: Cursor) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_getCursorSemanticParent(self, p1: Cursor) -> CursorResult:
+        raise NotImplementedError
+
+    def clang_getCursorSpelling(self, p1: Cursor) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getCursorTLSKind(self, p1: Cursor) -> CIntResult:
+        raise NotImplementedError
+
+    def clang_getCursorType(self, p1: Cursor) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_getCursorUSR(self, p1: Cursor) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_Cursor_getMangling(self, p1: Cursor) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getCXXAccessSpecifier(self, p1: Cursor) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_getDeclObjCTypeEncoding(self, p1: Cursor) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getDiagnostic(self, p1: TranslationUnitParam, p2: CUlongParam) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_getDiagnosticCategory(self, p1: Diagnostic) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_getDiagnosticCategoryText(self, p1: Diagnostic) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getDiagnosticFixIt(self, p1: Diagnostic, p2: CUlongParam, p3: CPointerParam[SourceRange]) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getDiagnosticInSet(self, p1: CObjectP, p2: CUlongParam) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_getDiagnosticLocation(self, p1: Diagnostic) -> SourceLocation:
+        raise NotImplementedError
+
+    def clang_getDiagnosticNumFixIts(self, p1: Diagnostic) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_getDiagnosticNumRanges(self, p1: Diagnostic) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_getDiagnosticOption(self, p1: Diagnostic, p2: CPointerParam[_CXString]) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getDiagnosticRange(self, p1: Diagnostic, p2: CUlongParam) -> SourceRange:
+        raise NotImplementedError
+
+    def clang_getDiagnosticSeverity(self, p1: Diagnostic) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getDiagnosticSpelling(self, p1: Diagnostic) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getElementType(self, p1: ASTType) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_getEnumConstantDeclUnsignedValue(self, p1: Cursor) -> CUlonglongResult:
+        raise NotImplementedError
+
+    def clang_getEnumConstantDeclValue(self, p1: Cursor) -> CLonglongResult:
+        raise NotImplementedError
+
+    def clang_getEnumDeclIntegerType(self, p1: Cursor) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_getExceptionSpecificationType(self, p1: ASTType) -> CIntResult:
+        raise NotImplementedError
+
+    def clang_getFile(self, p1: TranslationUnit, p2: CInteropStringParam) -> CObjectP:
+        raise NotImplementedError
+
+    def clang_getFileName(self, p1: File) -> CXStringResult:
+        raise NotImplementedError
+
+    def clang_getFileTime(self, p1: File) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_getIBOutletCollectionType(self, p1: Cursor) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_getIncludedFile(self, p1: Cursor) -> FileResult:
+        raise NotImplementedError
+
+    def clang_getInclusions(self, p1: TranslationUnit, p2: TranslationUnitIncludesCallback, p3: CPyObject[List[FileInclusion]]) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getInstantiationLocation(self, p1: SourceLocation, p2: CPointerParam[CObjectP], p3: CPointerParam[c_ulong], p4: CPointerParam[c_ulong], p5: CPointerParam[c_ulong]) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getLocation(self, p1: TranslationUnit, p2: File, p3: CUlongParam, p4: CUlongParam) -> SourceLocation:
+        raise NotImplementedError
+
+    def clang_getLocationForOffset(self, p1: TranslationUnit, p2: File, p3: CUlongParam) -> SourceLocation:
+        raise NotImplementedError
+
+    def clang_getNullCursor(self) -> Cursor:
+        raise NotImplementedError
+
+    def clang_getNumArgTypes(self, p1: ASTType) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_getNumCompletionChunks(self, p1: CObjectP) -> CLongResult:
+        raise NotImplementedError
+
+    def clang_getNumDiagnostics(self, p1: TranslationUnitParam) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_getNumDiagnosticsInSet(self, p1: CObjectP) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_getNumElements(self, p1: ASTType) -> CLonglongResult:
+        raise NotImplementedError
+
+    def clang_getNumOverloadedDecls(self, p1: Cursor) -> CUlongResult:
+        raise NotImplementedError
+
+    def clang_getOverloadedDecl(self, p1: Cursor, p2: CUlongParam) -> CursorResult:
+        raise NotImplementedError
+
+    def clang_getPointeeType(self, p1: ASTType) -> ASTTypeResult:
+        raise NotImplementedError
+
+    def clang_getRange(self, p1: SourceLocation, p2: SourceLocation) -> SourceRange:
+        raise NotImplementedError
+
+    def clang_getRangeEnd(self, p1: SourceRange) -> SourceLocation:
+        raise NotImplementedError
+
+    def clang_getRangeStart(self, p1: SourceRange) -> SourceLocation:
+        raise NotImplementedError
+
+    d...
[truncated]

Copy link
Contributor

@Endilll Endilll left a comment

Choose a reason for hiding this comment

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

Thank you for splitting that large PR into something smaller. That said, 1.6k lines of changes is no small change either.
This PR needs more high-level explanation of what you try to achieve and how. At the moment I'm not sold we want to go in this direction.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see value in annotating exported C functions themselves (binder.py), because they are wrapped by cindex.py. From maintenance perspective I'd rather leave # type: ignore at the call sites of those functions, than accept a whole new pile of stubs. Users of cindex.py won't notice either way, because I don't think they need to interact with C functions directly.

Copy link
Author

@TsXor TsXor Aug 6, 2024

Choose a reason for hiding this comment

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

What I think is just contradictory: I don't see value in leaving # type: ignore at the call sites of those functions everywhere. If you are satisfied with this, why don't we just make a cindex.pyi stub independent from cindex.py? Users of cindex.py won't notice either way.

I chose this metadata reflection way like pydantic just because it is beneficial to this binding. Having typed C functions can let us make less errors and easier to review.

The ultimate goal of introducing inline typing is to benefit both the project developers and users. I'm not being sarcastic. If we are really satisfied with current situation, we should really consider using separated pyi stubs instead of ignoring every call. Simply run stubgen on #101784 and do some little fixes, a stub is done.

Copy link
Author

Choose a reason for hiding this comment

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

In fact there are even some C functions used implicitly here: they are not declared in functionList, and relies on default restype as c_int to work. (Including clang_getCursorTLSKind, which causes segfault here, but I'm not sure if absense of signature is the cause.)

Copy link
Contributor

Choose a reason for hiding this comment

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

Regarding the segfault: this was a purely local problem for me, the function runs just fine for me now, and has always run fine in tests in the CI.
I only left that comment to indicate that I couldn't test that specific part.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the information, but I want to point out the bug #101548 fixed is actually avoidable by annotating exported C functions.

Copy link
Contributor

Choose a reason for hiding this comment

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

This file seems to implement a whole abstraction layer for annotations. I'd like to understand what the benefits are, because I see a non-trivial amount of complexity here.

Copy link
Author

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

The complexity here is because conversion rules in ctypes is just complex itself. You have 2 ways to do 1 thing there.

Copy link
Contributor

@DeinAlptraum DeinAlptraum left a comment

Choose a reason for hiding this comment

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

I agree with what Endill said: this is a rather big change and I'm not sure I see it adding enough value to justify this.
It also adds quite some complexity on top of the otherwise relatively light-weight Python bindings. I generally like seeing changes towards better typing annotations, but this is not something we need to push through no-matter-what and e.g. the type: ignores, while not exactly pretty, are "good enough" for our case.

Another reason I'm hesitant to support this, is that the bindings currently depend only on the Python standard library, and adding e.g. ctyped would break this. Adding outside dependencies is not strictly a problem, but again any change to this would imo need a good justification that I don't see at the moment.

Edit: sorry, I got confused during review, thought the relative .ctyped import was an outside module import. Dismiss the "outside dependencies" comment

@TsXor
Copy link
Author

TsXor commented Aug 6, 2024

I made a stub by running stubgen on #101784 and fixing some errors. See it at gist.

@Endilll
Copy link
Contributor

Endilll commented Aug 7, 2024

I've been staring at cindex.py for the better part of yesterday and today. I'll try to describe the status quo as I understand it first, to make sure all three of us are on the same page, then I'll describe the direction I think we should take.

Status quo

  1. I believe we're looking at three principal levels here: (1) libclang shared library, which exports functions; (2) function signatures of those exported functions described via ctypes (functionList), and (3) user-facing cindex API implemented in terms of said functions.
  2. libclang itself is considered a bedrock for cindex.py. Not much to discuss here.
  3. ctypes function signatures are required to describe how to call exported functions. Typical entry in functionList looks like the following: ("clang_isConstQualifiedType", [Type], bool), where function name is followed by a list of parameter types, which in turn is followed by return type. This information is used to fill in argtypes and restype attributes of the imported function object respectively. (@DeinAlptraum so functionList is there to stay one way or another.)
  4. cindex API is typically a rather thin layer on top of ctypes function signatures, which provides convenient classes and methods.
  5. There are two quirks to how values are returned from libclang into Python realm. The first one is about structural types. When exported function returns such a type, on ctypes side we need to declare a Python class that derives from ctypes.Structure, and fill in _fields_ member describing the layout. Then this type can be used to initialize restype attribute of imported function object. When this function is called, under the hood ctypes constructs the Python object according to layout and returns it.
class SourceLocation(Structure):
    _fields_ = [("ptr_data", c_void_p * 2), ("int_data", c_uint)]
  1. The second quirk is about conversions of returned objects. If we need to return something else than what is specified in restype, e.g. a string, we need to perform a conversion. Conveniently, imported function objects has errcheck attribute that accepts a function. It is expected to perform checks (and raise an exception if they don't pass), but more importantly for us, what this function returns is what the call to imported function object returns, so it can be used as a conversion function as well (such usage is supported by the official documentation). This is what the 4th member of entries in functionList used for: ("clang_getFileName", [File], _CXString, _CXString.from_result).

Recent developments

  1. I believe our type annotation efforts up until now (mostly @DeinAlptraum patches) have been focused on cindex API.
  2. One of the problems we've encountered while doing that is that imported function we rely on are not typed. This is not an issue when passing arguments (typed -> untyped is fine), but is a problem for the values we return (untyped -> typed make type checkers complain). This is the reason why we have so many # type: ignore [no-any-return] comments on return statements in cindex.py.
  3. It seems that ctypes does not care about type hints, despite the fact that function signatures are specified. This is quite unfortunate, and leads to lack of type annotations for the layer of imported functions. I wasn't able to find any widely deployed solution for this.
  4. It seems that the approach @TsXor is pursuing is to fill in the gap between ctypes and type hinting by introducing machinery that generates type hints for imported functions from their signatures (remember, we need to specify them anyway for ctypes to understand how to call them). This is a cleaner approach to resolve the issue with untyped returns than silencing type checkers with # type: ignore [no-any-return].

Proposed direction

  1. I think adding type hints has two primary goals: enable users to do type checking, and provide more information to autocompletion engines. Specifically, cindex API is what needs to be annotated for users to get those benefits. I'm not sure why anyone would use "raw" imported function when there's much more convenient API in the same module.
  2. I think our "current" approach supported by @DeinAlptraum work allows us to achieve both those primary goals.
  3. I'm sympathetic to the "new" approach @TsXor is taking, but when I look at it for the standpoint of the primary goals, I don't see significant improvements that would justify the significant complexity it has over "current" approach.
  4. ctyped is a useful work, but not for Clang, because we don't have enough maintainers experienced in this particular corner of Python. The problem it solves should be solved upstream, in ctypes or typing modules of the Python standard library. Once there is an upstream solution, we can reconsider the implementation approach of our type hints.
  5. We shouldn't rely errcheck when we can help it. I find this to be a bit too much magic when one looks at the implementation of cindex API trying to understand what happens between libclang and cindex API.
  6. Relying less on errcheck should help with contentious # type: ignore [no-any-return] comments sprinkled all over the place.
# bad
def get_template_argument_kind(self, num: int) -> TemplateArgumentKind:
    return conf.lib.clang_Cursor_getTemplateArgumentKind(self, num)  # type: ignore [no-any-return]

# good
def get_template_argument_kind(self, num: int) -> TemplateArgumentKind:
    return TemplateArgumentKind.from_id(conf.lib.clang_Cursor_getTemplateArgumentKind(self, num))
  1. Hopefully the similar transformation can help us when structural types are returned. In this case, it's not errcheck that works, but _fields_ and internal ctypes machinery. I'm a bit surprised that ctypes doesn't do as much for us, because this seems rather trivial to implement.
# bad
def start(self) -> SourceLocation:
    return conf.lib.clang_getRangeStart(self)  # type: ignore [no-any-return]

# good
def start(self) -> SourceLocation:
    return SourceLocation(conf.lib.clang_getRangeStart(self))
  1. In Fix all mypy --strict errors in clang python binding #101784 I saw more fixes than ctyped and things around that. You're encouraged to submit them separately.

I hope this long write-up help us to get on the same page, and find a way forward.
CC @AaronBallman hopefully this is accessible enough for you to wrap your head around if you ever wanted to know how our Python binding work. Maybe we can turn this into documentation.

@TsXor
Copy link
Author

TsXor commented Aug 8, 2024

I still think we can still do something to stop #101548 from happening again. Here is a script that can locate all conf.lib.* calls and find if any of them use an undeclared symbol in functionList. Maybe we can add it to test.

import ast, sys
from typing import List, Optional, Set
from clang import cindex

if sys.version_info < (3, 9):
    from astunparse import unparse
    ast.unparse = unparse
    del unparse


class CCallVisitor(ast.NodeVisitor):
    lib_node: str
    attr_set: Set[str]
    error_nodes: List[ast.expr]

    def __init__(self, lib_node: str, attr_set: Set[str]):
        self.lib_node = lib_node
        self.attr_set = attr_set
        self.error_nodes = []

    def check_target_call(self, node: ast.expr) -> Optional[str]:
        node_text = ast.unparse(node).strip()
        if not node_text.startswith(self.lib_node):
            return None
        return node_text

    def visit_Call(self, node: ast.Call) -> None:
        func = node.func
        node_text = self.check_target_call(func)
        if node_text is not None:
            if not (isinstance(func, ast.Attribute) and func.attr in self.attr_set):
                self.error_nodes.append(node)


with open(cindex.__file__, 'rt') as modfp:
    modtxt = modfp.read()
modast = ast.parse(modtxt, mode='exec')

func_checker = CCallVisitor(
    'conf.lib',
    {fn[0] for fn in cindex.functionList},
)
func_checker.visit(modast)
for node in func_checker.error_nodes:
    print(f'lineno={node.lineno}, col_offset={node.col_offset}, {ast.unparse(node)}')

@Endilll
Copy link
Contributor

Endilll commented Aug 8, 2024

I think this can be prevented by additional testing. Feel free to submit your suggestion as an additional test.
I also saw declared, but unused symbols in functionList, e.g. clang_getTemplateCursorKind. That might be worth to look at, too

@DeinAlptraum
Copy link
Contributor

DeinAlptraum commented Aug 8, 2024

@Endilll thanks a lot for the summary! Turning this into documentation also sounds like a good idea.

15. & 16. fully agree it would be nicer to go without using this errcheck functionality, and just call the conversion functions directly. Even if the documentation approves of our use case, imo our current usage is really just abuse of the functionality, and makes the code harder to understand. And as you said, calling it directly would allow us to remove some of the type: ignores.

17. this does not work out of the box, at least not without further changes. E.g. the exact example you gave there results in an error for me.

Regarding lib function calls for functions not declared in functionList: agreed, we should cover this with tests.

@TsXor
Copy link
Author

TsXor commented Aug 8, 2024

  1. this does not work out of the box, at least not without further changes. E.g. the exact example you gave there results in an error for me.

If you mean conf.lib.clang_Cursor_getTemplateArgumentKind, then it just happened to be that exception. This function uses a deprecated ctypes feature: if you assign a callable to restype, then the return type of the C function will be treated as int, and this int will be passed to the callable, and the result is returned.

@DeinAlptraum
Copy link
Contributor

DeinAlptraum commented Aug 8, 2024

If you mean conf.lib.clang_Cursor_getTemplateArgumentKind, then it just happened to be that exception.

That one worked fine for me (need the args passed as well, but that's it), I meant the SourceLocation example with clang_getRangeStart below that.

Good point about the restype for clang_Cursor_getTemplateArgumentKind though, I didn't notice and we should probably change that

@TsXor
Copy link
Author

TsXor commented Aug 8, 2024

I don't quite get what 17 meant. As far as I know about ctypes, conf.lib.clang_getRangeStart should be guaranteed to return an instance of SourceLocation. If it is to extinguish type errors, typing.cast(SourceLocation, conf.lib.clang_getRangeStart(self)) can do this.

@Endilll
Copy link
Contributor

Endilll commented Aug 8, 2024

Good point about the restype for clang_Cursor_getTemplateArgumentKind though, I didn't notice and we should probably change that

Yes, I'd like us to stop relying on that for the same reason I don't like errcheck. The fact that it's deprecated further adds to it.

I don't quite get what 17 meant. As far as I know about ctypes, conf.lib.clang_getRangeStart should be guaranteed to return an instance of SourceLocation. If it is to extinguish type errors, typing.cast(SourceLocation, conf.lib.clang_getRangeStart(self)) can do this.

Yes, 17 is about type errors when a structural type is returned. Good point about typing.cast(). It's been long enough I forgot that it exists. It might be the best way to address type errors in this case.

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