/
ctypes.py
229 lines (194 loc) · 10.4 KB
/
ctypes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
"""Plugin to provide accurate types for some parts of the ctypes module."""
from typing import List, Optional
# Fully qualified instead of "from mypy.plugin import ..." to avoid circular import problems.
import mypy.plugin
from mypy import nodes
from mypy.maptype import map_instance_to_supertype
from mypy.messages import format_type
from mypy.subtypes import is_subtype
from mypy.types import (
AnyType, CallableType, Instance, NoneType, Type, TypeOfAny, UnionType,
union_items, ProperType, get_proper_type
)
from mypy.typeops import make_simplified_union
def _get_bytes_type(api: 'mypy.plugin.CheckerPluginInterface') -> Instance:
"""Return the type corresponding to bytes on the current Python version.
This is bytes in Python 3, and str in Python 2.
"""
return api.named_generic_type(
'builtins.bytes' if api.options.python_version >= (3,) else 'builtins.str', [])
def _get_text_type(api: 'mypy.plugin.CheckerPluginInterface') -> Instance:
"""Return the type corresponding to Text on the current Python version.
This is str in Python 3, and unicode in Python 2.
"""
return api.named_generic_type(
'builtins.str' if api.options.python_version >= (3,) else 'builtins.unicode', [])
def _find_simplecdata_base_arg(tp: Instance, api: 'mypy.plugin.CheckerPluginInterface'
) -> Optional[ProperType]:
"""Try to find a parametrized _SimpleCData in tp's bases and return its single type argument.
None is returned if _SimpleCData appears nowhere in tp's (direct or indirect) bases.
"""
if tp.type.has_base('ctypes._SimpleCData'):
simplecdata_base = map_instance_to_supertype(tp,
api.named_generic_type('ctypes._SimpleCData', [AnyType(TypeOfAny.special_form)]).type)
assert len(simplecdata_base.args) == 1, '_SimpleCData takes exactly one type argument'
return get_proper_type(simplecdata_base.args[0])
return None
def _autoconvertible_to_cdata(tp: Type, api: 'mypy.plugin.CheckerPluginInterface') -> Type:
"""Get a type that is compatible with all types that can be implicitly converted to the given
CData type.
Examples:
* c_int -> Union[c_int, int]
* c_char_p -> Union[c_char_p, bytes, int, NoneType]
* MyStructure -> MyStructure
"""
allowed_types = []
# If tp is a union, we allow all types that are convertible to at least one of the union
# items. This is not quite correct - strictly speaking, only types convertible to *all* of the
# union items should be allowed. This may be worth changing in the future, but the more
# correct algorithm could be too strict to be useful.
for t in union_items(tp):
# Every type can be converted from itself (obviously).
allowed_types.append(t)
if isinstance(t, Instance):
unboxed = _find_simplecdata_base_arg(t, api)
if unboxed is not None:
# If _SimpleCData appears in tp's (direct or indirect) bases, its type argument
# specifies the type's "unboxed" version, which can always be converted back to
# the original "boxed" type.
allowed_types.append(unboxed)
if t.type.has_base('ctypes._PointerLike'):
# Pointer-like _SimpleCData subclasses can also be converted from
# an int or None.
allowed_types.append(api.named_generic_type('builtins.int', []))
allowed_types.append(NoneType())
return make_simplified_union(allowed_types)
def _autounboxed_cdata(tp: Type) -> ProperType:
"""Get the auto-unboxed version of a CData type, if applicable.
For *direct* _SimpleCData subclasses, the only type argument of _SimpleCData in the bases list
is returned.
For all other CData types, including indirect _SimpleCData subclasses, tp is returned as-is.
"""
tp = get_proper_type(tp)
if isinstance(tp, UnionType):
return make_simplified_union([_autounboxed_cdata(t) for t in tp.items])
elif isinstance(tp, Instance):
for base in tp.type.bases:
if base.type.fullname == 'ctypes._SimpleCData':
# If tp has _SimpleCData as a direct base class,
# the auto-unboxed type is the single type argument of the _SimpleCData type.
assert len(base.args) == 1
return get_proper_type(base.args[0])
# If tp is not a concrete type, or if there is no _SimpleCData in the bases,
# the type is not auto-unboxed.
return tp
def _get_array_element_type(tp: Type) -> Optional[ProperType]:
"""Get the element type of the Array type tp, or None if not specified."""
tp = get_proper_type(tp)
if isinstance(tp, Instance):
assert tp.type.fullname == 'ctypes.Array'
if len(tp.args) == 1:
return get_proper_type(tp.args[0])
return None
def array_constructor_callback(ctx: 'mypy.plugin.FunctionContext') -> Type:
"""Callback to provide an accurate signature for the ctypes.Array constructor."""
# Extract the element type from the constructor's return type, i. e. the type of the array
# being constructed.
et = _get_array_element_type(ctx.default_return_type)
if et is not None:
allowed = _autoconvertible_to_cdata(et, ctx.api)
assert len(ctx.arg_types) == 1, \
"The stub of the ctypes.Array constructor should have a single vararg parameter"
for arg_num, (arg_kind, arg_type) in enumerate(zip(ctx.arg_kinds[0], ctx.arg_types[0]), 1):
if arg_kind == nodes.ARG_POS and not is_subtype(arg_type, allowed):
ctx.api.msg.fail(
'Array constructor argument {} of type {}'
' is not convertible to the array element type {}'
.format(arg_num, format_type(arg_type), format_type(et)), ctx.context)
elif arg_kind == nodes.ARG_STAR:
ty = ctx.api.named_generic_type("typing.Iterable", [allowed])
if not is_subtype(arg_type, ty):
it = ctx.api.named_generic_type("typing.Iterable", [et])
ctx.api.msg.fail(
'Array constructor argument {} of type {}'
' is not convertible to the array element type {}'
.format(arg_num, format_type(arg_type), format_type(it)), ctx.context)
return ctx.default_return_type
def array_getitem_callback(ctx: 'mypy.plugin.MethodContext') -> Type:
"""Callback to provide an accurate return type for ctypes.Array.__getitem__."""
et = _get_array_element_type(ctx.type)
if et is not None:
unboxed = _autounboxed_cdata(et)
assert len(ctx.arg_types) == 1, \
'The stub of ctypes.Array.__getitem__ should have exactly one parameter'
assert len(ctx.arg_types[0]) == 1, \
"ctypes.Array.__getitem__'s parameter should not be variadic"
index_type = get_proper_type(ctx.arg_types[0][0])
if isinstance(index_type, Instance):
if index_type.type.has_base('builtins.int'):
return unboxed
elif index_type.type.has_base('builtins.slice'):
return ctx.api.named_generic_type('builtins.list', [unboxed])
return ctx.default_return_type
def array_setitem_callback(ctx: 'mypy.plugin.MethodSigContext') -> CallableType:
"""Callback to provide an accurate signature for ctypes.Array.__setitem__."""
et = _get_array_element_type(ctx.type)
if et is not None:
allowed = _autoconvertible_to_cdata(et, ctx.api)
assert len(ctx.default_signature.arg_types) == 2
index_type = get_proper_type(ctx.default_signature.arg_types[0])
if isinstance(index_type, Instance):
arg_type = None
if index_type.type.has_base('builtins.int'):
arg_type = allowed
elif index_type.type.has_base('builtins.slice'):
arg_type = ctx.api.named_generic_type('builtins.list', [allowed])
if arg_type is not None:
# Note: arg_type can only be None if index_type is invalid, in which case we use
# the default signature and let mypy report an error about it.
return ctx.default_signature.copy_modified(
arg_types=ctx.default_signature.arg_types[:1] + [arg_type],
)
return ctx.default_signature
def array_iter_callback(ctx: 'mypy.plugin.MethodContext') -> Type:
"""Callback to provide an accurate return type for ctypes.Array.__iter__."""
et = _get_array_element_type(ctx.type)
if et is not None:
unboxed = _autounboxed_cdata(et)
return ctx.api.named_generic_type('typing.Iterator', [unboxed])
return ctx.default_return_type
def array_value_callback(ctx: 'mypy.plugin.AttributeContext') -> Type:
"""Callback to provide an accurate type for ctypes.Array.value."""
et = _get_array_element_type(ctx.type)
if et is not None:
types = [] # type: List[Type]
for tp in union_items(et):
if isinstance(tp, AnyType):
types.append(AnyType(TypeOfAny.from_another_any, source_any=tp))
elif isinstance(tp, Instance) and tp.type.fullname == 'ctypes.c_char':
types.append(_get_bytes_type(ctx.api))
elif isinstance(tp, Instance) and tp.type.fullname == 'ctypes.c_wchar':
types.append(_get_text_type(ctx.api))
else:
ctx.api.msg.fail(
'Array attribute "value" is only available'
' with element type "c_char" or "c_wchar", not {}'
.format(format_type(et)), ctx.context)
return make_simplified_union(types)
return ctx.default_attr_type
def array_raw_callback(ctx: 'mypy.plugin.AttributeContext') -> Type:
"""Callback to provide an accurate type for ctypes.Array.raw."""
et = _get_array_element_type(ctx.type)
if et is not None:
types = [] # type: List[Type]
for tp in union_items(et):
if (isinstance(tp, AnyType)
or isinstance(tp, Instance) and tp.type.fullname == 'ctypes.c_char'):
types.append(_get_bytes_type(ctx.api))
else:
ctx.api.msg.fail(
'Array attribute "raw" is only available'
' with element type "c_char", not {}'
.format(format_type(et)), ctx.context)
return make_simplified_union(types)
return ctx.default_attr_type