Skip to content

Commit 06483bd

Browse files
committed
[libclang/python] Fix some type errors, add type annotations
1 parent 1bad702 commit 06483bd

File tree

3 files changed

+100
-63
lines changed

3 files changed

+100
-63
lines changed

clang/bindings/python/clang/cindex.py

Lines changed: 87 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
Most object information is exposed using properties, when the underlying API
4444
call is efficient.
4545
"""
46-
from __future__ import absolute_import, division, print_function
46+
from __future__ import annotations
4747

4848
# TODO
4949
# ====
@@ -66,46 +66,77 @@
6666

6767
import collections.abc
6868
import os
69+
import sys
6970
from enum import Enum
7071

72+
from typing import (
73+
Any,
74+
Callable,
75+
Generic,
76+
Optional,
77+
Type as TType,
78+
TypeVar,
79+
TYPE_CHECKING,
80+
Union as TUnion,
81+
)
82+
from typing_extensions import Protocol, TypeAlias
83+
84+
if TYPE_CHECKING:
85+
from ctypes import _Pointer
86+
87+
StrPath: TypeAlias = TUnion[str, os.PathLike[str]]
88+
LibFunc: TypeAlias = TUnion[
89+
"tuple[str, Optional[list[Any]]]",
90+
"tuple[str, Optional[list[Any]], Any]",
91+
"tuple[str, Optional[list[Any]], Any, Callable[..., Any]]",
92+
]
93+
CObjP: TypeAlias = _Pointer[Any]
94+
95+
TSeq = TypeVar("TSeq", covariant=True)
96+
97+
class NoSliceSequence(Protocol[TSeq]):
98+
def __len__(self) -> int: ...
99+
def __getitem__(self, key: int) -> TSeq: ...
100+
71101

72102
# Python 3 strings are unicode, translate them to/from utf8 for C-interop.
73103
class c_interop_string(c_char_p):
74-
def __init__(self, p=None):
104+
def __init__(self, p: str | bytes | None = None):
75105
if p is None:
76106
p = ""
77107
if isinstance(p, str):
78108
p = p.encode("utf8")
79109
super(c_char_p, self).__init__(p)
80110

81-
def __str__(self):
82-
return self.value
111+
def __str__(self) -> str:
112+
return self.value or ""
83113

84114
@property
85-
def value(self):
86-
if super(c_char_p, self).value is None:
115+
def value(self) -> str | None: # type: ignore [override]
116+
val = super(c_char_p, self).value
117+
if val is None:
87118
return None
88-
return super(c_char_p, self).value.decode("utf8")
119+
return val.decode("utf8")
89120

90121
@classmethod
91-
def from_param(cls, param):
122+
def from_param(cls, param: str | bytes | None) -> c_interop_string:
92123
if isinstance(param, str):
93124
return cls(param)
94125
if isinstance(param, bytes):
95126
return cls(param)
96127
if param is None:
97128
# Support passing null to C functions expecting char arrays
98-
return None
129+
return cls(param)
99130
raise TypeError(
100131
"Cannot convert '{}' to '{}'".format(type(param).__name__, cls.__name__)
101132
)
102133

103134
@staticmethod
104-
def to_python_string(x, *args):
135+
def to_python_string(x: c_interop_string, *args: Any) -> str | None:
105136
return x.value
106137

107138

108-
def b(x):
139+
def b(x: str | bytes) -> bytes:
109140
if isinstance(x, bytes):
110141
return x
111142
return x.encode("utf8")
@@ -115,9 +146,7 @@ def b(x):
115146
# object. This is a problem, because it means that from_parameter will see an
116147
# integer and pass the wrong value on platforms where int != void*. Work around
117148
# this by marshalling object arguments as void**.
118-
c_object_p = POINTER(c_void_p)
119-
120-
callbacks = {}
149+
c_object_p: TType[CObjP] = POINTER(c_void_p)
121150

122151
### Exception Classes ###
123152

@@ -169,25 +198,32 @@ def __init__(self, enumeration, message):
169198

170199
### Structures and Utility Classes ###
171200

201+
TInstance = TypeVar("TInstance")
202+
TResult = TypeVar("TResult")
203+
172204

173-
class CachedProperty:
205+
class CachedProperty(Generic[TInstance, TResult]):
174206
"""Decorator that lazy-loads the value of a property.
175207
176208
The first time the property is accessed, the original property function is
177209
executed. The value it returns is set as the new value of that instance's
178210
property, replacing the original method.
179211
"""
180212

181-
def __init__(self, wrapped):
213+
def __init__(self, wrapped: Callable[[TInstance], TResult]):
182214
self.wrapped = wrapped
183215
try:
184216
self.__doc__ = wrapped.__doc__
185217
except:
186218
pass
187219

188-
def __get__(self, instance, instance_type=None):
220+
def __get__(self, instance: TInstance, instance_type: Any = None) -> TResult:
189221
if instance is None:
190-
return self
222+
property_name = self.wrapped.__name__
223+
class_name = instance_type.__name__
224+
raise TypeError(
225+
f"'{property_name}' is not a static attribute of '{class_name}'"
226+
)
191227

192228
value = self.wrapped(instance)
193229
setattr(instance, self.wrapped.__name__, value)
@@ -200,13 +236,16 @@ class _CXString(Structure):
200236

201237
_fields_ = [("spelling", c_char_p), ("free", c_int)]
202238

203-
def __del__(self):
239+
def __del__(self) -> None:
204240
conf.lib.clang_disposeString(self)
205241

206242
@staticmethod
207-
def from_result(res, fn=None, args=None):
243+
def from_result(res: _CXString, fn: Any = None, args: Any = None) -> str:
208244
assert isinstance(res, _CXString)
209-
return conf.lib.clang_getCString(res)
245+
pystr: str | None = conf.lib.clang_getCString(res)
246+
if pystr is None:
247+
return ""
248+
return pystr
210249

211250

212251
class SourceLocation(Structure):
@@ -2030,8 +2069,8 @@ def visitor(child, parent, children):
20302069
children.append(child)
20312070
return 1 # continue
20322071

2033-
children = []
2034-
conf.lib.clang_visitChildren(self, callbacks["cursor_visit"](visitor), children)
2072+
children: list[Cursor] = []
2073+
conf.lib.clang_visitChildren(self, cursor_visit_callback(visitor), children)
20352074
return iter(children)
20362075

20372076
def walk_preorder(self):
@@ -2543,10 +2582,8 @@ def visitor(field, children):
25432582
fields.append(field)
25442583
return 1 # continue
25452584

2546-
fields = []
2547-
conf.lib.clang_Type_visitFields(
2548-
self, callbacks["fields_visit"](visitor), fields
2549-
)
2585+
fields: list[Cursor] = []
2586+
conf.lib.clang_Type_visitFields(self, fields_visit_callback(visitor), fields)
25502587
return iter(fields)
25512588

25522589
def get_exception_specification_kind(self):
@@ -3058,7 +3095,7 @@ def visitor(fobj, lptr, depth, includes):
30583095
# Automatically adapt CIndex/ctype pointers to python objects
30593096
includes = []
30603097
conf.lib.clang_getInclusions(
3061-
self, callbacks["translation_unit_includes"](visitor), includes
3098+
self, translation_unit_includes_callback(visitor), includes
30623099
)
30633100

30643101
return iter(includes)
@@ -3570,15 +3607,15 @@ def write_main_file_to_stdout(self):
35703607

35713608
# Now comes the plumbing to hook up the C library.
35723609

3573-
# Register callback types in common container.
3574-
callbacks["translation_unit_includes"] = CFUNCTYPE(
3610+
# Register callback types
3611+
translation_unit_includes_callback = CFUNCTYPE(
35753612
None, c_object_p, POINTER(SourceLocation), c_uint, py_object
35763613
)
3577-
callbacks["cursor_visit"] = CFUNCTYPE(c_int, Cursor, Cursor, py_object)
3578-
callbacks["fields_visit"] = CFUNCTYPE(c_int, Cursor, py_object)
3614+
cursor_visit_callback = CFUNCTYPE(c_int, Cursor, Cursor, py_object)
3615+
fields_visit_callback = CFUNCTYPE(c_int, Cursor, py_object)
35793616

35803617
# Functions strictly alphabetical order.
3581-
functionList = [
3618+
functionList: list[LibFunc] = [
35823619
(
35833620
"clang_annotateTokens",
35843621
[TranslationUnit, POINTER(Token), c_uint, POINTER(Cursor)],
@@ -3748,7 +3785,7 @@ def write_main_file_to_stdout(self):
37483785
("clang_getIncludedFile", [Cursor], c_object_p, File.from_result),
37493786
(
37503787
"clang_getInclusions",
3751-
[TranslationUnit, callbacks["translation_unit_includes"], py_object],
3788+
[TranslationUnit, translation_unit_includes_callback, py_object],
37523789
),
37533790
(
37543791
"clang_getInstantiationLocation",
@@ -3833,7 +3870,7 @@ def write_main_file_to_stdout(self):
38333870
"clang_tokenize",
38343871
[TranslationUnit, SourceRange, POINTER(POINTER(Token)), POINTER(c_uint)],
38353872
),
3836-
("clang_visitChildren", [Cursor, callbacks["cursor_visit"], py_object], c_uint),
3873+
("clang_visitChildren", [Cursor, cursor_visit_callback, py_object], c_uint),
38373874
("clang_Cursor_getNumArguments", [Cursor], c_int),
38383875
("clang_Cursor_getArgument", [Cursor, c_uint], Cursor, Cursor.from_result),
38393876
("clang_Cursor_getNumTemplateArguments", [Cursor], c_int),
@@ -3859,19 +3896,19 @@ def write_main_file_to_stdout(self):
38593896
("clang_Type_getSizeOf", [Type], c_longlong),
38603897
("clang_Type_getCXXRefQualifier", [Type], c_uint),
38613898
("clang_Type_getNamedType", [Type], Type, Type.from_result),
3862-
("clang_Type_visitFields", [Type, callbacks["fields_visit"], py_object], c_uint),
3899+
("clang_Type_visitFields", [Type, fields_visit_callback, py_object], c_uint),
38633900
]
38643901

38653902

38663903
class LibclangError(Exception):
3867-
def __init__(self, message):
3904+
def __init__(self, message: str):
38683905
self.m = message
38693906

3870-
def __str__(self):
3907+
def __str__(self) -> str:
38713908
return self.m
38723909

38733910

3874-
def register_function(lib, item, ignore_errors):
3911+
def register_function(lib: CDLL, item: LibFunc, ignore_errors: bool) -> None:
38753912
# A function may not exist, if these bindings are used with an older or
38763913
# incompatible version of libclang.so.
38773914
try:
@@ -3895,28 +3932,28 @@ def register_function(lib, item, ignore_errors):
38953932
func.errcheck = item[3]
38963933

38973934

3898-
def register_functions(lib, ignore_errors):
3935+
def register_functions(lib: CDLL, ignore_errors: bool) -> None:
38993936
"""Register function prototypes with a libclang library instance.
39003937
39013938
This must be called as part of library instantiation so Python knows how
39023939
to call out to the shared library.
39033940
"""
39043941

3905-
def register(item):
3906-
return register_function(lib, item, ignore_errors)
3942+
def register(item: LibFunc) -> None:
3943+
register_function(lib, item, ignore_errors)
39073944

39083945
for f in functionList:
39093946
register(f)
39103947

39113948

39123949
class Config:
39133950
library_path = None
3914-
library_file = None
3951+
library_file: str | None = None
39153952
compatibility_check = True
39163953
loaded = False
39173954

39183955
@staticmethod
3919-
def set_library_path(path):
3956+
def set_library_path(path: StrPath) -> None:
39203957
"""Set the path in which to search for libclang"""
39213958
if Config.loaded:
39223959
raise Exception(
@@ -3927,7 +3964,7 @@ def set_library_path(path):
39273964
Config.library_path = os.fspath(path)
39283965

39293966
@staticmethod
3930-
def set_library_file(filename):
3967+
def set_library_file(filename: StrPath) -> None:
39313968
"""Set the exact location of libclang"""
39323969
if Config.loaded:
39333970
raise Exception(
@@ -3938,7 +3975,7 @@ def set_library_file(filename):
39383975
Config.library_file = os.fspath(filename)
39393976

39403977
@staticmethod
3941-
def set_compatibility_check(check_status):
3978+
def set_compatibility_check(check_status: bool) -> None:
39423979
"""Perform compatibility check when loading libclang
39433980
39443981
The python bindings are only tested and evaluated with the version of
@@ -3964,13 +4001,13 @@ def set_compatibility_check(check_status):
39644001
Config.compatibility_check = check_status
39654002

39664003
@CachedProperty
3967-
def lib(self):
4004+
def lib(self) -> CDLL:
39684005
lib = self.get_cindex_library()
39694006
register_functions(lib, not Config.compatibility_check)
39704007
Config.loaded = True
39714008
return lib
39724009

3973-
def get_filename(self):
4010+
def get_filename(self) -> str:
39744011
if Config.library_file:
39754012
return Config.library_file
39764013

@@ -3990,7 +4027,7 @@ def get_filename(self):
39904027

39914028
return file
39924029

3993-
def get_cindex_library(self):
4030+
def get_cindex_library(self) -> CDLL:
39944031
try:
39954032
library = cdll.LoadLibrary(self.get_filename())
39964033
except OSError as e:
@@ -4003,7 +4040,7 @@ def get_cindex_library(self):
40034040

40044041
return library
40054042

4006-
def function_exists(self, name):
4043+
def function_exists(self, name: str) -> bool:
40074044
try:
40084045
getattr(self.lib, name)
40094046
except AttributeError:

clang/bindings/python/tests/cindex/test_code_completion.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def test_code_complete(self):
5353
expected = [
5454
"{'int', ResultType} | {'test1', TypedText} || Priority: 50 || Availability: Available || Brief comment: Aaa.",
5555
"{'void', ResultType} | {'test2', TypedText} | {'(', LeftParen} | {')', RightParen} || Priority: 50 || Availability: Available || Brief comment: Bbb.",
56-
"{'return', TypedText} | {';', SemiColon} || Priority: 40 || Availability: Available || Brief comment: None",
56+
"{'return', TypedText} | {';', SemiColon} || Priority: 40 || Availability: Available || Brief comment: ",
5757
]
5858
self.check_completion_results(cr, expected)
5959

@@ -94,7 +94,7 @@ def test_code_complete_pathlike(self):
9494
expected = [
9595
"{'int', ResultType} | {'test1', TypedText} || Priority: 50 || Availability: Available || Brief comment: Aaa.",
9696
"{'void', ResultType} | {'test2', TypedText} | {'(', LeftParen} | {')', RightParen} || Priority: 50 || Availability: Available || Brief comment: Bbb.",
97-
"{'return', TypedText} | {';', SemiColon} || Priority: 40 || Availability: Available || Brief comment: None",
97+
"{'return', TypedText} | {';', SemiColon} || Priority: 40 || Availability: Available || Brief comment: ",
9898
]
9999
self.check_completion_results(cr, expected)
100100

@@ -128,19 +128,19 @@ class Q : public P {
128128
cr = tu.codeComplete("fake.cpp", 12, 5, unsaved_files=files)
129129

130130
expected = [
131-
"{'const', TypedText} || Priority: 50 || Availability: Available || Brief comment: None",
132-
"{'volatile', TypedText} || Priority: 50 || Availability: Available || Brief comment: None",
133-
"{'operator', TypedText} || Priority: 40 || Availability: Available || Brief comment: None",
134-
"{'P', TypedText} || Priority: 50 || Availability: Available || Brief comment: None",
135-
"{'Q', TypedText} || Priority: 50 || Availability: Available || Brief comment: None",
131+
"{'const', TypedText} || Priority: 50 || Availability: Available || Brief comment: ",
132+
"{'volatile', TypedText} || Priority: 50 || Availability: Available || Brief comment: ",
133+
"{'operator', TypedText} || Priority: 40 || Availability: Available || Brief comment: ",
134+
"{'P', TypedText} || Priority: 50 || Availability: Available || Brief comment: ",
135+
"{'Q', TypedText} || Priority: 50 || Availability: Available || Brief comment: ",
136136
]
137137
self.check_completion_results(cr, expected)
138138

139139
cr = tu.codeComplete("fake.cpp", 13, 5, unsaved_files=files)
140140
expected = [
141-
"{'P', TypedText} | {'::', Text} || Priority: 75 || Availability: Available || Brief comment: None",
142-
"{'P &', ResultType} | {'operator=', TypedText} | {'(', LeftParen} | {'const P &', Placeholder} | {')', RightParen} || Priority: 79 || Availability: Available || Brief comment: None",
143-
"{'int', ResultType} | {'member', TypedText} || Priority: 35 || Availability: NotAccessible || Brief comment: None",
144-
"{'void', ResultType} | {'~P', TypedText} | {'(', LeftParen} | {')', RightParen} || Priority: 79 || Availability: Available || Brief comment: None",
141+
"{'P', TypedText} | {'::', Text} || Priority: 75 || Availability: Available || Brief comment: ",
142+
"{'P &', ResultType} | {'operator=', TypedText} | {'(', LeftParen} | {'const P &', Placeholder} | {')', RightParen} || Priority: 79 || Availability: Available || Brief comment: ",
143+
"{'int', ResultType} | {'member', TypedText} || Priority: 35 || Availability: NotAccessible || Brief comment: ",
144+
"{'void', ResultType} | {'~P', TypedText} | {'(', LeftParen} | {')', RightParen} || Priority: 79 || Availability: Available || Brief comment: ",
145145
]
146146
self.check_completion_results(cr, expected)

0 commit comments

Comments
 (0)