ctyped 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ctyped/__init__.py +1 -0
- ctyped/exceptions.py +20 -0
- ctyped/library.py +472 -0
- ctyped/sniffer.py +128 -0
- ctyped/toolbox.py +3 -0
- ctyped/types.py +200 -0
- ctyped/utils.py +172 -0
- ctyped-0.8.1.dist-info/METADATA +66 -0
- ctyped-0.8.1.dist-info/RECORD +11 -0
- ctyped-0.8.1.dist-info/WHEEL +4 -0
- ctyped-0.8.1.dist-info/licenses/LICENSE +28 -0
ctyped/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = '0.8.1'
|
ctyped/exceptions.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class CtypedException(Exception):
|
|
4
|
+
""""""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FunctionRedeclared(CtypedException):
|
|
8
|
+
""""""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UnsupportedTypeError(CtypedException):
|
|
12
|
+
""""""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TypehintError(CtypedException):
|
|
16
|
+
""""""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SniffingError(CtypedException):
|
|
20
|
+
""""""
|
ctyped/library.py
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from ctypes.util import find_library
|
|
6
|
+
from functools import partial, partialmethod, reduce
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable, Union, List, Dict, Type, ContextManager
|
|
9
|
+
|
|
10
|
+
from .exceptions import UnsupportedTypeError, TypehintError, CtypedException
|
|
11
|
+
from .sniffer import NmSymbolSniffer, SniffResult
|
|
12
|
+
from .types import CChars, CastedTypeBase, CStruct
|
|
13
|
+
from .utils import cast_type, extract_func_info, FuncInfo
|
|
14
|
+
|
|
15
|
+
LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Scopes:
|
|
19
|
+
|
|
20
|
+
def __init__(self, params: dict):
|
|
21
|
+
self._scopes: list[dict] = []
|
|
22
|
+
self._keys = ['prefix', 'str_type', 'int_bits', 'int_sign']
|
|
23
|
+
self.push(params)
|
|
24
|
+
|
|
25
|
+
def __call__(
|
|
26
|
+
this,
|
|
27
|
+
prefix: str | None = None,
|
|
28
|
+
str_type: type[CastedTypeBase] = CChars,
|
|
29
|
+
int_bits: int | None = None,
|
|
30
|
+
int_sign: bool | None = None,
|
|
31
|
+
**kwargs) -> ContextManager['Scopes']:
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
:param prefix: Function name prefix to apply to functions under the manager.
|
|
35
|
+
|
|
36
|
+
:param str_type: Type to represent strings.
|
|
37
|
+
|
|
38
|
+
* ``CChars`` - strings as chars (ANSI) **default**
|
|
39
|
+
* ``CCharsW`` - strings as wide chars (UTF)
|
|
40
|
+
|
|
41
|
+
:param int_bits: int length to be used in function.
|
|
42
|
+
|
|
43
|
+
Possible values: 8, 16, 32, 64
|
|
44
|
+
|
|
45
|
+
:param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints.
|
|
46
|
+
|
|
47
|
+
:param kwargs:
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
return this.context(locals())
|
|
51
|
+
|
|
52
|
+
def push(self, params: dict):
|
|
53
|
+
|
|
54
|
+
scope = {key: params.get(key) for key in self._keys}
|
|
55
|
+
self._scopes.append(scope)
|
|
56
|
+
|
|
57
|
+
def pop(self):
|
|
58
|
+
self._scopes.pop()
|
|
59
|
+
|
|
60
|
+
def flatten(self):
|
|
61
|
+
|
|
62
|
+
scopes = self._scopes
|
|
63
|
+
keys_bool = {'int_sign'}
|
|
64
|
+
keys_concat = {'prefix'}
|
|
65
|
+
result = {}
|
|
66
|
+
|
|
67
|
+
def choose(current, prev):
|
|
68
|
+
return current or prev
|
|
69
|
+
|
|
70
|
+
def pick_bool(current, prev):
|
|
71
|
+
return current if current is not None else prev
|
|
72
|
+
|
|
73
|
+
def concat(current, prev):
|
|
74
|
+
return (prev or '') + (current or '')
|
|
75
|
+
|
|
76
|
+
for key in self._keys:
|
|
77
|
+
|
|
78
|
+
if key in keys_concat:
|
|
79
|
+
reducer = concat
|
|
80
|
+
|
|
81
|
+
elif key in keys_bool:
|
|
82
|
+
reducer = pick_bool
|
|
83
|
+
|
|
84
|
+
else:
|
|
85
|
+
reducer = choose
|
|
86
|
+
|
|
87
|
+
result[key] = reduce(reducer, (scope[key] for scope in scopes[::-1]))
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
@contextmanager
|
|
92
|
+
def context(self, params: dict):
|
|
93
|
+
self.push(params)
|
|
94
|
+
yield self
|
|
95
|
+
self.pop()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Library:
|
|
99
|
+
"""Main entry point to describe C library interface.
|
|
100
|
+
|
|
101
|
+
Basic usage:
|
|
102
|
+
|
|
103
|
+
.. code-block:: python
|
|
104
|
+
|
|
105
|
+
lib = Library('mylib')
|
|
106
|
+
|
|
107
|
+
with lib.scope(prefix='mylib_'):
|
|
108
|
+
|
|
109
|
+
@lib.function()
|
|
110
|
+
def my_func():
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
lib.bind_types()
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self, name: Union[str, Path], *, autoload: bool = True,
|
|
119
|
+
prefix: str | None = None,
|
|
120
|
+
str_type: type[CastedTypeBase] = CChars,
|
|
121
|
+
int_bits: int | None = None,
|
|
122
|
+
int_sign: bool | None = None
|
|
123
|
+
):
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
:param name: Shared library name or filepath.
|
|
127
|
+
|
|
128
|
+
:param autoload: Load library just on Library object initialization.
|
|
129
|
+
|
|
130
|
+
:param prefix: Function name prefix to apply to functions in the library.
|
|
131
|
+
|
|
132
|
+
Useful when C functions have common prefixes.
|
|
133
|
+
|
|
134
|
+
:param str_type: Type to represent strings.
|
|
135
|
+
|
|
136
|
+
* ``CChars`` - strings as chars (ANSI) **default**
|
|
137
|
+
* ``CCharsW`` - strings as wide chars (UTF)
|
|
138
|
+
|
|
139
|
+
.. note:: This setting is global to library. Can be changed on function definition level.
|
|
140
|
+
|
|
141
|
+
:param int_bits: int length to use by default.
|
|
142
|
+
|
|
143
|
+
Possible values: 8, 16, 32, 64
|
|
144
|
+
|
|
145
|
+
.. note:: This setting is global to library. Can be changed on function definition level.
|
|
146
|
+
|
|
147
|
+
:param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints.
|
|
148
|
+
|
|
149
|
+
.. note:: This setting is global to library. Can be changed on function definition level.
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
self.scope = Scopes(locals())
|
|
153
|
+
self.s = self.scope
|
|
154
|
+
|
|
155
|
+
self.name = str(name)
|
|
156
|
+
self.lib = None
|
|
157
|
+
self.funcs: dict[str, Union[Callable, partialmethod[Any]]] = {}
|
|
158
|
+
|
|
159
|
+
autoload and self.load()
|
|
160
|
+
|
|
161
|
+
def load(self):
|
|
162
|
+
"""Loads shared library."""
|
|
163
|
+
name = self.name
|
|
164
|
+
|
|
165
|
+
if not Path(name).exists():
|
|
166
|
+
name = name.replace('lib', '', 1)
|
|
167
|
+
name = find_library(name)
|
|
168
|
+
|
|
169
|
+
lib = ctypes.CDLL(name, use_errno=True)
|
|
170
|
+
|
|
171
|
+
if lib._name is None:
|
|
172
|
+
lib = None
|
|
173
|
+
|
|
174
|
+
if lib is None:
|
|
175
|
+
raise CtypedException(f'Unable to find library: {name or self.name}')
|
|
176
|
+
|
|
177
|
+
self.lib = lib
|
|
178
|
+
|
|
179
|
+
def structure(
|
|
180
|
+
self, *,
|
|
181
|
+
pack: int | None = None,
|
|
182
|
+
str_type: CastedTypeBase | None = None,
|
|
183
|
+
int_bits: int | None = None,
|
|
184
|
+
int_sign: bool | None = None,
|
|
185
|
+
):
|
|
186
|
+
"""Class decorator for C structures definition.
|
|
187
|
+
|
|
188
|
+
.. code-block:: python
|
|
189
|
+
|
|
190
|
+
@lib.structure
|
|
191
|
+
class MyStruct:
|
|
192
|
+
|
|
193
|
+
first: int
|
|
194
|
+
second: str
|
|
195
|
+
third: 'MyStruct'
|
|
196
|
+
|
|
197
|
+
:param pack: Allows custom maximum alignment for the fields (as #pragma pack(n)).
|
|
198
|
+
|
|
199
|
+
:param str_type: Type to represent strings.
|
|
200
|
+
|
|
201
|
+
:param int_bits: int length to be used in function.
|
|
202
|
+
|
|
203
|
+
:param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints.
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
params = locals()
|
|
207
|
+
|
|
208
|
+
def wrapper(cls_):
|
|
209
|
+
|
|
210
|
+
annotations = {
|
|
211
|
+
attrname: attrhint for attrname, attrhint in cls_.__annotations__.items()
|
|
212
|
+
if not attrname.startswith('_')}
|
|
213
|
+
|
|
214
|
+
with self.scope(**params):
|
|
215
|
+
|
|
216
|
+
cls_name = cls_.__name__
|
|
217
|
+
|
|
218
|
+
info = FuncInfo(
|
|
219
|
+
name_py=cls_name, name_c=None,
|
|
220
|
+
annotations=annotations, options=self.scope.flatten())
|
|
221
|
+
|
|
222
|
+
# todo maybe support big/little byte order
|
|
223
|
+
struct = type(cls_name, (CStruct, cls_), {})
|
|
224
|
+
|
|
225
|
+
ct_fields = {}
|
|
226
|
+
fields = []
|
|
227
|
+
|
|
228
|
+
for attrname, attrhint in annotations.items():
|
|
229
|
+
|
|
230
|
+
if attrhint == cls_name:
|
|
231
|
+
casted = ctypes.POINTER(struct)
|
|
232
|
+
ct_fields[attrname] = struct
|
|
233
|
+
|
|
234
|
+
else:
|
|
235
|
+
casted = cast_type(info, attrname, attrhint)
|
|
236
|
+
|
|
237
|
+
if issubclass(casted, CastedTypeBase):
|
|
238
|
+
ct_fields[attrname] = casted
|
|
239
|
+
|
|
240
|
+
fields.append((attrname, casted))
|
|
241
|
+
|
|
242
|
+
LOGGER.debug('Structure %s fields: %s', cls_name, fields)
|
|
243
|
+
|
|
244
|
+
if pack:
|
|
245
|
+
struct._pack_ = pack
|
|
246
|
+
|
|
247
|
+
struct._ct_fields = ct_fields
|
|
248
|
+
struct._fields_ = fields
|
|
249
|
+
|
|
250
|
+
return struct
|
|
251
|
+
|
|
252
|
+
return wrapper
|
|
253
|
+
|
|
254
|
+
def cls(
|
|
255
|
+
self, *,
|
|
256
|
+
prefix: str | None = None,
|
|
257
|
+
str_type: CastedTypeBase | None = None,
|
|
258
|
+
int_bits: int | None = None,
|
|
259
|
+
int_sign: bool | None = None,
|
|
260
|
+
):
|
|
261
|
+
"""Class decorator. Allows common parameters application for class methods.
|
|
262
|
+
|
|
263
|
+
.. code-block:: python
|
|
264
|
+
|
|
265
|
+
@lib.cls(prefix='common_', str_type=CCharsW)
|
|
266
|
+
class Wide:
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
@lib.function()
|
|
270
|
+
def get_utf(some: str) -> str:
|
|
271
|
+
...
|
|
272
|
+
|
|
273
|
+
:param prefix: Function name prefix to apply to functions under the manager.
|
|
274
|
+
|
|
275
|
+
:param str_type: Type to represent strings.
|
|
276
|
+
|
|
277
|
+
:param int_bits: int length to be used in function.
|
|
278
|
+
|
|
279
|
+
:param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints.
|
|
280
|
+
|
|
281
|
+
"""
|
|
282
|
+
self.scope.push(locals())
|
|
283
|
+
|
|
284
|
+
def wrapper(cls_):
|
|
285
|
+
# Class body construction is done, unwind scope.
|
|
286
|
+
self.scope.pop()
|
|
287
|
+
return cls_
|
|
288
|
+
|
|
289
|
+
return wrapper
|
|
290
|
+
|
|
291
|
+
def function(
|
|
292
|
+
self, name_c: Union[str, Callable] | None = None, *, wrap: bool = False,
|
|
293
|
+
str_type: CastedTypeBase | None = None,
|
|
294
|
+
int_bits: int | None = None,
|
|
295
|
+
int_sign: bool | None = None,
|
|
296
|
+
|
|
297
|
+
) -> Callable:
|
|
298
|
+
"""Decorator to mark functions which exported from the library.
|
|
299
|
+
|
|
300
|
+
:param name_c: C function name with or without prefix (see ``.scope(prefix=)``).
|
|
301
|
+
If not set, Python function name is used.
|
|
302
|
+
|
|
303
|
+
:param wrap: Do not replace decorated function with ctypes function,
|
|
304
|
+
but with wrapper, allowing pre- or post-process ctypes function call.
|
|
305
|
+
|
|
306
|
+
Useful to organize functions to classes (to automatically pass ``self``)
|
|
307
|
+
to ctypes function to C function.
|
|
308
|
+
|
|
309
|
+
.. code-block:: python
|
|
310
|
+
|
|
311
|
+
class Thing(CObject):
|
|
312
|
+
|
|
313
|
+
@lib.function(wrap=True)
|
|
314
|
+
def one(self, some: int) -> int:
|
|
315
|
+
# Implicitly pass Thing instance alongside
|
|
316
|
+
# with explicitly passed `some` arg.
|
|
317
|
+
...
|
|
318
|
+
|
|
319
|
+
@lib.function(wrap=True)
|
|
320
|
+
def two(self, some:int, cfunc: Callable) -> int:
|
|
321
|
+
# `cfunc` is a wrapper, calling an actual ctypes function.
|
|
322
|
+
# If no arguments provided the wrapper will try detect them
|
|
323
|
+
# automatically.
|
|
324
|
+
result = cfunc()
|
|
325
|
+
return result + 1
|
|
326
|
+
|
|
327
|
+
:param str_type: Type to represent strings.
|
|
328
|
+
|
|
329
|
+
.. note:: Overrides the same named param from library level (see ``__init__`` description).
|
|
330
|
+
|
|
331
|
+
:param int_bits: int length to be used in function.
|
|
332
|
+
|
|
333
|
+
.. note:: Overrides the same named param from library level (see ``__init__`` description).
|
|
334
|
+
|
|
335
|
+
:param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints.
|
|
336
|
+
|
|
337
|
+
.. note:: Overrides the same named param from library level (see ``__init__`` description).
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
def cfunc_wrapped(*args, f: Callable, **kwargs):
|
|
341
|
+
|
|
342
|
+
if not args:
|
|
343
|
+
argvals = inspect.getargvalues(inspect.currentframe().f_back)
|
|
344
|
+
loc = argvals.locals
|
|
345
|
+
args = tuple(loc[argname] for argname in argvals.args if argname != 'cfunc')
|
|
346
|
+
|
|
347
|
+
return f(*args)
|
|
348
|
+
|
|
349
|
+
def function_(func_py: Callable, *, name_c: str | None, scope: dict):
|
|
350
|
+
|
|
351
|
+
info = extract_func_info(func_py, name_c=name_c, scope=scope, registry=self.funcs)
|
|
352
|
+
name = info.name_c
|
|
353
|
+
|
|
354
|
+
func_c = getattr(self.lib, name)
|
|
355
|
+
|
|
356
|
+
# Prepare for late binding in .bind_types().
|
|
357
|
+
func_c.ctyped = info
|
|
358
|
+
|
|
359
|
+
if wrap:
|
|
360
|
+
func_args = inspect.getfullargspec(func_py).args
|
|
361
|
+
|
|
362
|
+
if 'cfunc' in func_args:
|
|
363
|
+
# Use existing function, pass `cfunc`.
|
|
364
|
+
|
|
365
|
+
LOGGER.debug('Func [ %s -> %s ] uses wrapped manual call.', name, info.name_py)
|
|
366
|
+
|
|
367
|
+
func_swapped = partialmethod(func_py, cfunc=partial(cfunc_wrapped, f=func_c))
|
|
368
|
+
|
|
369
|
+
else:
|
|
370
|
+
# Automatically bind first param (self, cls)
|
|
371
|
+
|
|
372
|
+
LOGGER.debug('Func [ %s -> %s ] uses wrapped auto call.', name, info.name_py)
|
|
373
|
+
|
|
374
|
+
func_swapped = partialmethod(func_c)
|
|
375
|
+
|
|
376
|
+
func_swapped.cfunc = func_c
|
|
377
|
+
func_out = func_swapped
|
|
378
|
+
|
|
379
|
+
else:
|
|
380
|
+
|
|
381
|
+
LOGGER.debug('Func [ %s -> %s ] uses direct call.', name, info.name_py)
|
|
382
|
+
|
|
383
|
+
func_out = func_c
|
|
384
|
+
|
|
385
|
+
self.funcs[name] = func_out
|
|
386
|
+
|
|
387
|
+
return func_out
|
|
388
|
+
|
|
389
|
+
if callable(name_c):
|
|
390
|
+
# Decorator without params.
|
|
391
|
+
scope = self.scope.flatten()
|
|
392
|
+
py_func, name_c = name_c, None
|
|
393
|
+
return function_(py_func, name_c=name_c, scope=scope)
|
|
394
|
+
|
|
395
|
+
# Decorator with parameters.
|
|
396
|
+
with self.scope(**locals()) as scope:
|
|
397
|
+
scope = scope.flatten()
|
|
398
|
+
|
|
399
|
+
return partial(function_, name_c=name_c, scope=scope)
|
|
400
|
+
|
|
401
|
+
def method(self, name_c: str | None = None, **kwargs):
|
|
402
|
+
"""Decorator. The same as ``.function()`` with ``wrap=True``."""
|
|
403
|
+
return self.function(name_c=name_c, wrap=True, **kwargs)
|
|
404
|
+
|
|
405
|
+
def sniff(self) -> SniffResult:
|
|
406
|
+
"""Sniffs the library for symbols.
|
|
407
|
+
|
|
408
|
+
Sniffing result can be used as 'ctyped' code generator.
|
|
409
|
+
|
|
410
|
+
"""
|
|
411
|
+
sniffer = NmSymbolSniffer(self.lib._name)
|
|
412
|
+
result = sniffer.sniff()
|
|
413
|
+
return result
|
|
414
|
+
|
|
415
|
+
def bind_types(self):
|
|
416
|
+
"""Deduces ctypes argument and result types from Python type hints,
|
|
417
|
+
binding those types to ctypes functions.
|
|
418
|
+
|
|
419
|
+
"""
|
|
420
|
+
LOGGER.debug('Binding signature types to ctypes functions ...')
|
|
421
|
+
|
|
422
|
+
for name_c, func_out in self.funcs.items():
|
|
423
|
+
|
|
424
|
+
func_c = getattr(func_out, 'cfunc', func_out)
|
|
425
|
+
func_info: FuncInfo = func_c.ctyped
|
|
426
|
+
|
|
427
|
+
name_py = func_info.name_py
|
|
428
|
+
annotations = func_info.annotations
|
|
429
|
+
errcheck = None
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
return_is_annotated = 'return' in annotations
|
|
433
|
+
restype = cast_type(func_info, 'return', annotations.pop('return', None))
|
|
434
|
+
|
|
435
|
+
if restype and issubclass(restype, CastedTypeBase):
|
|
436
|
+
errcheck = restype._ct_res
|
|
437
|
+
|
|
438
|
+
argtypes = [cast_type(func_info, argname, argtype) for argname, argtype in annotations.items()]
|
|
439
|
+
|
|
440
|
+
except TypehintError:
|
|
441
|
+
# Reset annotations to allow subsequent .bind_types() calls w/o exceptions.
|
|
442
|
+
func_info.annotations.clear()
|
|
443
|
+
raise
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
if argtypes:
|
|
447
|
+
func_c.argtypes = argtypes
|
|
448
|
+
|
|
449
|
+
if restype or return_is_annotated:
|
|
450
|
+
func_c.restype = restype
|
|
451
|
+
|
|
452
|
+
if errcheck:
|
|
453
|
+
func_c.errcheck = errcheck
|
|
454
|
+
|
|
455
|
+
except TypeError as e:
|
|
456
|
+
|
|
457
|
+
raise UnsupportedTypeError(
|
|
458
|
+
f'Unsupported types declared for {name_py} ({name_c}). '
|
|
459
|
+
f'Args: {argtypes}. Result: {restype}. Errcheck: {errcheck}.'
|
|
460
|
+
) from e
|
|
461
|
+
|
|
462
|
+
#####################################################################################
|
|
463
|
+
# Shortcuts
|
|
464
|
+
|
|
465
|
+
f = function
|
|
466
|
+
"""Shortcut for ``.function()``."""
|
|
467
|
+
|
|
468
|
+
m = method
|
|
469
|
+
"""Shortcut for ``.method()``."""
|
|
470
|
+
|
|
471
|
+
s = None # type: ignore
|
|
472
|
+
"""Shortcut for ``.scope()``."""
|
ctyped/sniffer.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from textwrap import dedent
|
|
6
|
+
from typing import List, Union
|
|
7
|
+
|
|
8
|
+
from .exceptions import SniffingError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SniffedSymbol(NamedTuple):
|
|
12
|
+
name: str
|
|
13
|
+
address: str
|
|
14
|
+
line: str
|
|
15
|
+
"""Represents a symbol sniffed from a library."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SniffResult:
|
|
19
|
+
"""Represents a library sniffing results."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *, libpath: str):
|
|
22
|
+
self.symbols: list[SniffedSymbol] = []
|
|
23
|
+
self.libpath = libpath
|
|
24
|
+
|
|
25
|
+
def add_symbol(self, symbol: SniffedSymbol):
|
|
26
|
+
"""Added a symbol to the result."""
|
|
27
|
+
self.symbols.append(symbol)
|
|
28
|
+
|
|
29
|
+
def to_ctyped(self):
|
|
30
|
+
"""Generates ctyped code from sniff result."""
|
|
31
|
+
|
|
32
|
+
dumped = [
|
|
33
|
+
'###',
|
|
34
|
+
f'# Code below was automatically generated {datetime.utcnow()} UTC',
|
|
35
|
+
f'# Total functions: {len(self.symbols)}',
|
|
36
|
+
'###',
|
|
37
|
+
f"lib = Library('{self.libpath}')",
|
|
38
|
+
''
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
dumped.extend(dedent(
|
|
42
|
+
f'''
|
|
43
|
+
@lib.f
|
|
44
|
+
def {symbol.name}():
|
|
45
|
+
"""{symbol.line}"""
|
|
46
|
+
'''
|
|
47
|
+
) for symbol in self.symbols)
|
|
48
|
+
|
|
49
|
+
dumped.append('\nlib.bind_types()')
|
|
50
|
+
|
|
51
|
+
return '\n'.join(dumped)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NmSymbolSniffer:
|
|
55
|
+
"""Uses 'nm' command from 'binutils' package to sniff a library for exported symbols."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, libpath: Union[str, Path]):
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
:param libpath: Library path to sniff for symbols.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
self.libpath = str(libpath)
|
|
64
|
+
|
|
65
|
+
def _run(self) -> list[str]:
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
['nm', '-DCl', self.libpath],
|
|
70
|
+
capture_output=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
except FileNotFoundError: # pragma: nocover
|
|
74
|
+
|
|
75
|
+
raise SniffingError(
|
|
76
|
+
"Command 'nm' execution failed. "
|
|
77
|
+
"Make sure 'nm' command from 'binutils' package is available.") from None
|
|
78
|
+
|
|
79
|
+
if result.returncode: # pragma: nocover
|
|
80
|
+
raise SniffingError(f"Command 'nm' execution failed: {result.stderr.decode()}")
|
|
81
|
+
|
|
82
|
+
return result.stdout.decode().splitlines()
|
|
83
|
+
|
|
84
|
+
def _get_symbols(self, lines: list[str]) -> list[SniffedSymbol]:
|
|
85
|
+
|
|
86
|
+
symbols = []
|
|
87
|
+
|
|
88
|
+
for line in lines:
|
|
89
|
+
|
|
90
|
+
if line.startswith(' '):
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
chunks = line.split(' ')
|
|
94
|
+
chunks_len = len(chunks)
|
|
95
|
+
|
|
96
|
+
if chunks_len < 2 or chunks[1] != 'T':
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
if len(chunks) != 3: # pragma: nocover
|
|
100
|
+
raise SniffingError(
|
|
101
|
+
f"Command 'nm' execution failed: 3 chunks line expected, but given {chunks}")
|
|
102
|
+
|
|
103
|
+
address, symtype, name = chunks
|
|
104
|
+
|
|
105
|
+
if symtype != 'T' or name.startswith('_'):
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
name, _, srcline = name.partition('\t')
|
|
109
|
+
|
|
110
|
+
symbols.append(
|
|
111
|
+
SniffedSymbol(
|
|
112
|
+
name=name,
|
|
113
|
+
address=address,
|
|
114
|
+
line=srcline,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return symbols
|
|
119
|
+
|
|
120
|
+
def sniff(self) -> SniffResult:
|
|
121
|
+
"""Runs symbols sniffing for library."""
|
|
122
|
+
|
|
123
|
+
result = SniffResult(libpath=self.libpath)
|
|
124
|
+
|
|
125
|
+
for symbol in self._get_symbols(self._run()):
|
|
126
|
+
result.add_symbol(symbol)
|
|
127
|
+
|
|
128
|
+
return result
|
ctyped/toolbox.py
ADDED
ctyped/types.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
from typing import Any, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CastedTypeBase:
|
|
6
|
+
|
|
7
|
+
@classmethod
|
|
8
|
+
def _ct_prep(cls, val: Any) -> Union[bytes, int]:
|
|
9
|
+
# Prepare value. Used for structure fields.
|
|
10
|
+
return val
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def _ct_res(cls, cobj: Any, *args, **kwargs) -> Any: # pragma: nocover
|
|
14
|
+
# Function result caster.
|
|
15
|
+
raise NotImplementedError(f'{cls.__name__}._ct_res() is not implemented')
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_param(cls, val: Any) -> Any:
|
|
19
|
+
# Function parameter caster.
|
|
20
|
+
return val
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# getattr to cheat type hints
|
|
24
|
+
CShort: int = ctypes.c_short
|
|
25
|
+
CShortU: int = ctypes.c_ushort
|
|
26
|
+
CLong: int = ctypes.c_long
|
|
27
|
+
CLongU: int = ctypes.c_ulong
|
|
28
|
+
CLongLong: int = ctypes.c_longlong
|
|
29
|
+
CLongLongU: int = ctypes.c_ulonglong
|
|
30
|
+
|
|
31
|
+
CInt: int = ctypes.c_int
|
|
32
|
+
CIntU: int = ctypes.c_uint
|
|
33
|
+
CInt8: int = ctypes.c_int8
|
|
34
|
+
CInt8U: int = ctypes.c_uint8
|
|
35
|
+
CInt16: int = ctypes.c_int16
|
|
36
|
+
CInt16U: int = ctypes.c_uint16
|
|
37
|
+
CInt32: int = ctypes.c_int32
|
|
38
|
+
CInt32U: int = ctypes.c_uint32
|
|
39
|
+
CInt64: int = ctypes.c_int64
|
|
40
|
+
CInt64U: int = ctypes.c_uint64
|
|
41
|
+
|
|
42
|
+
CPointer: Any = ctypes.c_void_p
|
|
43
|
+
CObject = CPointer # Mere alias for those who prefer ``class My(CObject): ...`` better.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CStruct(CastedTypeBase, ctypes.Structure):
|
|
47
|
+
"""Helper to represent a structure using native byte order."""
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def _ct_prep(cls, val):
|
|
51
|
+
return ctypes.pointer(val)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def _ct_res(cls, cobj: Any, *args, **kwargs) -> Any:
|
|
55
|
+
|
|
56
|
+
if isinstance(cobj, cls):
|
|
57
|
+
# Structure as a function result.
|
|
58
|
+
return cobj
|
|
59
|
+
|
|
60
|
+
# Substructure handling.
|
|
61
|
+
return cobj.contents
|
|
62
|
+
|
|
63
|
+
def __setattr__(self, key, value):
|
|
64
|
+
# Allows structure field value casting on assignments.
|
|
65
|
+
|
|
66
|
+
casted = self._ct_fields.get(key)
|
|
67
|
+
|
|
68
|
+
if casted:
|
|
69
|
+
value = casted._ct_prep(value)
|
|
70
|
+
|
|
71
|
+
super().__setattr__(key, value)
|
|
72
|
+
|
|
73
|
+
def __getattribute__(self, key):
|
|
74
|
+
# Allows customizes structure members types handling.
|
|
75
|
+
|
|
76
|
+
get = super().__getattribute__
|
|
77
|
+
|
|
78
|
+
value = get(key)
|
|
79
|
+
|
|
80
|
+
casted = get('_ct_fields').get(key)
|
|
81
|
+
|
|
82
|
+
if casted:
|
|
83
|
+
return casted._ct_res(value)
|
|
84
|
+
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class CRef(CastedTypeBase):
|
|
89
|
+
"""Reference helper."""
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def carray(cls, typecls: Any, *, size: int = 1) -> 'CRef':
|
|
93
|
+
"""Alternative constructor. Creates a reference to array."""
|
|
94
|
+
|
|
95
|
+
typecls = {
|
|
96
|
+
|
|
97
|
+
int: ctypes.c_int,
|
|
98
|
+
str: ctypes.c_char,
|
|
99
|
+
bool: ctypes.c_bool,
|
|
100
|
+
float: ctypes.c_float,
|
|
101
|
+
|
|
102
|
+
}.get(typecls, typecls)
|
|
103
|
+
|
|
104
|
+
val = (typecls * (size or 1))()
|
|
105
|
+
|
|
106
|
+
return cls(val)
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def cbool(cls, value: bool = False) -> 'CRef':
|
|
110
|
+
"""Alternative constructor. Creates a reference to boolean."""
|
|
111
|
+
return cls(ctypes.c_bool(value))
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def cint(cls, value: int = 0) -> 'CRef':
|
|
115
|
+
"""Alternative constructor. Creates a reference to integer."""
|
|
116
|
+
return cls(ctypes.c_int(value))
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def cfloat(cls, value: float = 0.0) -> 'CRef':
|
|
120
|
+
"""Alternative constructor. Creates a reference to float."""
|
|
121
|
+
return cls(ctypes.c_float(value))
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_param(cls, obj: 'CRef'):
|
|
125
|
+
return ctypes.byref(obj._ct_val)
|
|
126
|
+
|
|
127
|
+
def __init__(self, cval: Any):
|
|
128
|
+
self._ct_val = cval
|
|
129
|
+
|
|
130
|
+
def __iter__(self):
|
|
131
|
+
# Allows iteration for arrays.
|
|
132
|
+
return iter(self._ct_val)
|
|
133
|
+
|
|
134
|
+
def __str__(self):
|
|
135
|
+
val = self._ct_val.value
|
|
136
|
+
|
|
137
|
+
if isinstance(val, bytes):
|
|
138
|
+
val = val.decode('utf-8')
|
|
139
|
+
|
|
140
|
+
return str(val)
|
|
141
|
+
|
|
142
|
+
def __int__(self):
|
|
143
|
+
return int(self._ct_val.value)
|
|
144
|
+
|
|
145
|
+
def __float__(self):
|
|
146
|
+
return float(self._ct_val.value)
|
|
147
|
+
|
|
148
|
+
def __bool__(self):
|
|
149
|
+
return bool(self._ct_val.value)
|
|
150
|
+
|
|
151
|
+
def __eq__(self, other):
|
|
152
|
+
return self._ct_val.value == other
|
|
153
|
+
|
|
154
|
+
def __ne__(self, other):
|
|
155
|
+
return self._ct_val.value != other
|
|
156
|
+
|
|
157
|
+
def __lt__(self, other):
|
|
158
|
+
return self._ct_val.value < other
|
|
159
|
+
|
|
160
|
+
def __gt__(self, other):
|
|
161
|
+
return self._ct_val.value > other
|
|
162
|
+
|
|
163
|
+
def __le__(self, other):
|
|
164
|
+
return self._ct_val.value <= other
|
|
165
|
+
|
|
166
|
+
def __ge__(self, other):
|
|
167
|
+
return self._ct_val.value >= other
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class CChars(CastedTypeBase, ctypes.c_char_p):
|
|
171
|
+
"""Represents a Python string as a C chars pointer."""
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def _ct_prep(cls, val):
|
|
175
|
+
return val.encode('utf-8')
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def _ct_res(cls, cobj: 'CChars', *args, **kwargs) -> str | None:
|
|
179
|
+
value = cobj.value
|
|
180
|
+
|
|
181
|
+
if not value:
|
|
182
|
+
return ''
|
|
183
|
+
|
|
184
|
+
return value.decode('utf-8')
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def from_param(cls, val: str):
|
|
188
|
+
return ctypes.c_char_p(val.encode('utf-8'))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class CCharsW(CastedTypeBase, ctypes.c_wchar_p):
|
|
192
|
+
"""Represents a Python string as a C wide chars pointer."""
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def _ct_res(cls, cobj: 'CCharsW', *args, **kwargs) -> str | None:
|
|
196
|
+
return cobj.value or ''
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def from_param(cls, val: str):
|
|
200
|
+
return ctypes.c_wchar_p(val)
|
ctyped/utils.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
from ctypes import get_errno, CFUNCTYPE
|
|
4
|
+
from errno import errorcode
|
|
5
|
+
from os import strerror
|
|
6
|
+
from typing import Callable, Optional
|
|
7
|
+
|
|
8
|
+
from .exceptions import TypehintError, FunctionRedeclared
|
|
9
|
+
from .types import *
|
|
10
|
+
|
|
11
|
+
class FuncInfo(NamedTuple):
|
|
12
|
+
name_py: str
|
|
13
|
+
name_c: str
|
|
14
|
+
annotations: dict
|
|
15
|
+
options: dict
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ErrorInfo(NamedTuple):
|
|
19
|
+
num: int
|
|
20
|
+
code: int
|
|
21
|
+
msg: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _Missing(NamedTuple):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_MISSING = _Missing()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extract_func_info(func: Callable, *, name_c: str | None, scope: dict, registry: dict) -> FuncInfo:
|
|
32
|
+
|
|
33
|
+
name_py = func.__name__
|
|
34
|
+
name = scope.get('prefix', '') + (name_c or name_py)
|
|
35
|
+
|
|
36
|
+
if name in registry:
|
|
37
|
+
raise FunctionRedeclared(f'Unable to redeclare: {name} ({name_py})')
|
|
38
|
+
|
|
39
|
+
annotated_args = {}
|
|
40
|
+
annotations = func.__annotations__
|
|
41
|
+
|
|
42
|
+
# Gather all args and annotations for them.
|
|
43
|
+
for argname in inspect.getfullargspec(func).args:
|
|
44
|
+
|
|
45
|
+
if argname == 'cfunc':
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
annotation = annotations.get(argname, _MISSING)
|
|
49
|
+
|
|
50
|
+
if argname == 'self' and annotation is _MISSING:
|
|
51
|
+
# Pick class name from qualname.
|
|
52
|
+
annotation = func.__qualname__.split('.')[-2]
|
|
53
|
+
|
|
54
|
+
annotated_args[argname] = annotation
|
|
55
|
+
|
|
56
|
+
annotated_args['return'] = annotations.get('return')
|
|
57
|
+
|
|
58
|
+
return FuncInfo(name_py=name_py, name_c=name, annotations=annotated_args, options=scope)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def thint_str_to_obj(thint: str):
|
|
62
|
+
|
|
63
|
+
fback = inspect.currentframe().f_back
|
|
64
|
+
|
|
65
|
+
while fback:
|
|
66
|
+
target = fback.f_globals.get(thint)
|
|
67
|
+
|
|
68
|
+
if target:
|
|
69
|
+
return target
|
|
70
|
+
|
|
71
|
+
fback = fback.f_back
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def cast_type(func_info, argname: str, thint: Any):
|
|
75
|
+
|
|
76
|
+
if thint is None:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
if isinstance(thint, str):
|
|
80
|
+
thint_orig = thint
|
|
81
|
+
thint = thint_str_to_obj(thint)
|
|
82
|
+
|
|
83
|
+
if thint is None:
|
|
84
|
+
raise TypehintError(
|
|
85
|
+
f'Unable to resolve type hint. '
|
|
86
|
+
f'Function: {func_info.name_py}. Arg: {argname}. Type: {thint_orig}.')
|
|
87
|
+
|
|
88
|
+
if thint is bool:
|
|
89
|
+
thint = ctypes.c_bool
|
|
90
|
+
|
|
91
|
+
elif thint is float:
|
|
92
|
+
thint = ctypes.c_float
|
|
93
|
+
|
|
94
|
+
elif thint is str:
|
|
95
|
+
thint = func_info.options.get('str_type', CChars)
|
|
96
|
+
|
|
97
|
+
elif thint is int:
|
|
98
|
+
int_bits = func_info.options.get('int_bits')
|
|
99
|
+
int_sign = func_info.options.get('int_sign', False)
|
|
100
|
+
|
|
101
|
+
thint_map = {
|
|
102
|
+
8: (CInt8, CInt8U),
|
|
103
|
+
16: (CInt16, CInt16U),
|
|
104
|
+
32: (CInt32, CInt32U),
|
|
105
|
+
64: (CInt64, CInt64U),
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if int_bits:
|
|
110
|
+
assert int_bits in thint_map.keys(), 'Wrong value passed for int_bits.'
|
|
111
|
+
|
|
112
|
+
else:
|
|
113
|
+
int_bits = 64 # todo maybe try to guess
|
|
114
|
+
|
|
115
|
+
type_idx = 1 if int_sign is False else 0
|
|
116
|
+
|
|
117
|
+
thint = thint_map[int_bits][type_idx] or thint
|
|
118
|
+
|
|
119
|
+
return thint
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_last_error() -> ErrorInfo:
|
|
123
|
+
"""Returns last error (``errno``) information named tuple:
|
|
124
|
+
|
|
125
|
+
.. code-block:: python
|
|
126
|
+
|
|
127
|
+
(err_no, err_code, err_message)
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
num = get_errno()
|
|
131
|
+
code = errorcode[num]
|
|
132
|
+
msg = strerror(num)
|
|
133
|
+
|
|
134
|
+
return ErrorInfo(num=num, code=code, msg=msg)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def c_callback(use_errno: bool = False) -> Callable:
|
|
138
|
+
"""Decorator to turn a Python function into a C callback function.
|
|
139
|
+
|
|
140
|
+
.. code-block:: python
|
|
141
|
+
|
|
142
|
+
@lib.f
|
|
143
|
+
def c_func_using_callback(hook: CPointer) -> int:
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
@c_callback
|
|
147
|
+
def hook(num: int) -> int:
|
|
148
|
+
return num + 10
|
|
149
|
+
|
|
150
|
+
c_func_using_callback(hook)
|
|
151
|
+
|
|
152
|
+
:param use_errno:
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
def cfunction_(func: Callable) -> Callable:
|
|
156
|
+
|
|
157
|
+
func_info = extract_func_info(func, name_c=None, scope={}, registry={})
|
|
158
|
+
annotations = func_info.annotations
|
|
159
|
+
|
|
160
|
+
restype = cast_type(func_info, 'return', annotations.pop('return', None))
|
|
161
|
+
argtypes = [cast_type(func_info, argname, argtype) for argname, argtype in annotations.items()]
|
|
162
|
+
|
|
163
|
+
functype = CFUNCTYPE(restype, *argtypes, use_errno=use_errno)
|
|
164
|
+
cfunc = functype(func)
|
|
165
|
+
|
|
166
|
+
return cfunc
|
|
167
|
+
|
|
168
|
+
if callable(use_errno):
|
|
169
|
+
# Decorator without params.
|
|
170
|
+
return cfunction_(use_errno)
|
|
171
|
+
|
|
172
|
+
return cfunction_
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ctyped
|
|
3
|
+
Version: 0.8.1
|
|
4
|
+
Summary: Build ctypes interfaces for shared libraries with type hinting
|
|
5
|
+
Project-URL: Homepage, https://github.com/idlesign/ctyped
|
|
6
|
+
Author-email: Igor Starikov <idlesign@yandex.ru>
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: cffi,ctypes,shared library
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# ctyped
|
|
25
|
+
|
|
26
|
+
<https://github.com/idlesign/ctyped>
|
|
27
|
+
|
|
28
|
+
[](https://pypi.python.org/pypi/ctyped)
|
|
29
|
+
[](https://pypi.python.org/pypi/ctyped)
|
|
30
|
+
[](https://coveralls.io/r/idlesign/ctyped)
|
|
31
|
+
|
|
32
|
+
## Description
|
|
33
|
+
|
|
34
|
+
*Build ctypes interfaces for shared libraries with type hinting*
|
|
35
|
+
|
|
36
|
+
**Requires Python 3.10+**
|
|
37
|
+
|
|
38
|
+
* Less boilerplate;
|
|
39
|
+
* Logical structuring;
|
|
40
|
+
* Basic code generator (.so function -> ctyped function);
|
|
41
|
+
* Useful helpers.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from ctyped.toolbox import Library
|
|
45
|
+
|
|
46
|
+
# Define library.
|
|
47
|
+
lib = Library('mylib.so')
|
|
48
|
+
|
|
49
|
+
@lib.structure
|
|
50
|
+
class Box:
|
|
51
|
+
|
|
52
|
+
one: int
|
|
53
|
+
two: str
|
|
54
|
+
|
|
55
|
+
# Type less with function names prefixes.
|
|
56
|
+
with lib.scope(prefix='mylib_'):
|
|
57
|
+
|
|
58
|
+
# Describe function available in the library.
|
|
59
|
+
@lib.function
|
|
60
|
+
def some_func(title: str, year: int, box: Box) -> str:
|
|
61
|
+
...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
<https://ctyped.readthedocs.io/>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ctyped/__init__.py,sha256=WH2l3vFfy8w0Du6NRYJJK1jNK0WjKXJ202C4JjWcUWo,18
|
|
2
|
+
ctyped/exceptions.py,sha256=_GJHxHRPevkL7DldeUvBvOTXXNVhSpMuIfp7z-UYvco,263
|
|
3
|
+
ctyped/library.py,sha256=ptDGhOS7Pg6CKMEJn1Co43EWgAW6gwvvp2-FHHEM8kg,14418
|
|
4
|
+
ctyped/sniffer.py,sha256=pteQKKsEX4BL7S4tdPqTwZ2cQHztLEkjN4pQtfKfMcg,3361
|
|
5
|
+
ctyped/toolbox.py,sha256=6Yg1tM3HTudlP9ziH-Aogdj29AJdiHZ2O1iKgAjuTlY,108
|
|
6
|
+
ctyped/types.py,sha256=NKJ5l_53k-EWuejOGMp6xiEKzPV3LB7_xoFUJaK3m8Q,5011
|
|
7
|
+
ctyped/utils.py,sha256=mXAm7YW4axakHxsaMMejRM6Qvn3zfm3nPhefAzlMnjE,4135
|
|
8
|
+
ctyped-0.8.1.dist-info/METADATA,sha256=4vUo0QRi8pz1iBh576G0ygKcNoJXpJFog6TXUNA7ixY,1919
|
|
9
|
+
ctyped-0.8.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
10
|
+
ctyped-0.8.1.dist-info/licenses/LICENSE,sha256=vfvP40vyus-Y2rmKhWDSIGSBpLgTBuLj8YN-SU7Boyw,1499
|
|
11
|
+
ctyped-0.8.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Copyright (c) 2019-2026, Igor `idle sign` Starikov
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
* Neither the name of the ctyped nor the names of its
|
|
15
|
+
contributors may be used to endorse or promote products derived from
|
|
16
|
+
this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
28
|
+
|