├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG ├── CONTRIBUTING ├── INSTALL ├── LICENSE ├── MANIFEST.in ├── README.rst ├── ctyped ├── __init__.py ├── exceptions.py ├── library.py ├── sniffer.py ├── toolbox.py ├── types.py └── utils.py ├── docs ├── Makefile ├── build │ └── empty └── source │ ├── _static │ └── empty │ ├── _templates │ └── empty │ ├── conf.py │ ├── index.rst │ ├── library.rst │ ├── quickstart.rst │ ├── rst_guide.rst │ └── utils.rst ├── setup.cfg ├── setup.py ├── tests ├── mylib │ ├── compile.sh │ ├── mylib.c │ └── mylib.so └── test_basic.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = ctyped/ 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .pydevproject 3 | .idea 4 | .tox 5 | __pycache__ 6 | *.pyc 7 | *.pyo 8 | *.egg-info 9 | docs/_build/ 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: python 4 | 5 | sudo: false 6 | 7 | python: 8 | - 3.7 9 | - 3.6 10 | 11 | install: 12 | - pip install coverage coveralls 13 | 14 | script: 15 | - coverage run --source=ctyped setup.py test 16 | 17 | after_success: 18 | - coveralls 19 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | ctyped authors 2 | ============== 3 | 4 | Created by Igor `idle sign` Starikov. 5 | 6 | 7 | Contributors 8 | ------------ 9 | 10 | Here could be your name. 11 | 12 | 13 | 14 | Translators 15 | ----------- 16 | 17 | Here could be your name. 18 | 19 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | ctyped changelog 2 | ================ 3 | 4 | 5 | v0.8.0 [2019-11-21] 6 | ------------------- 7 | + Added .so sniffing with ctyped code generation support. 8 | + Library now can handle Path objects. 9 | 10 | 11 | v0.7.1 [2019-10-14] 12 | ------------------- 13 | * Fixed 'Library.load()' LD search. 14 | * Fixed 'Unable to find library' message. 15 | 16 | 17 | v0.7.0 18 | ------ 19 | + Added basic structures support. 20 | + CRef.carray() constructor now allows shortcuts for bool and float. 21 | * CObject simplified. 22 | 23 | 24 | v0.6.0 25 | ------ 26 | + 'get_last_error()' now returns ErrorInfo named tuple. 27 | + Basic support for callback functions. 28 | * Class methods performance improved (less wrappers). 29 | 30 | 31 | v0.5.0 32 | ------ 33 | + Added 'CRef' helper to pass by reference in arguments. 34 | + Now '-> None' annotation can be used for 'void' result. 35 | + Proper 'bool' handling. 36 | 37 | 38 | v0.4.0 39 | ------ 40 | + 'function/f' and 'method/m' decorators now allow no params (less brackets). 41 | + Added 'Library.cls' decorator for classes. 42 | 43 | 44 | v0.3.0 45 | ------ 46 | + 'functions_prefix' context manager is replaced by advanced 'scope'. 47 | 48 | 49 | v0.2.0 50 | ------ 51 | + Added 'int_bits' shortcut option. 52 | + Added 'int_sign' shortcut option. 53 | + Added CObject convenience. 54 | + Added support for Python strings as wide chars. 55 | * Result is now casted with 'errcheck'. 56 | 57 | 58 | v0.1.0 59 | ------ 60 | + Basic functionality. -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | ctyped contributing 2 | =================== 3 | 4 | 5 | Submit issues 6 | ------------- 7 | 8 | If you spotted something weird in application behavior or want to propose a feature you are welcome. 9 | 10 | 11 | Write code 12 | ---------- 13 | If you are eager to participate in application development and to work on an existing issue (whether it should 14 | be a bugfix or a feature implementation), fork, write code, and make a pull request right from the forked project page. 15 | 16 | 17 | Spread the word 18 | --------------- 19 | 20 | If you have some tips and tricks or any other words that you think might be of interest for the others — publish it 21 | wherever you find convenient. 22 | 23 | 24 | See also: https://github.com/idlesign/ctyped 25 | 26 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | ctyped installation 2 | =================== 3 | 4 | 5 | Python ``pip`` package is required to install ``ctyped``. 6 | 7 | 8 | From sources 9 | ------------ 10 | 11 | Use the following command line to install ``ctyped`` from sources directory (containing setup.py): 12 | 13 | pip install . 14 | 15 | or 16 | 17 | python setup.py install 18 | 19 | 20 | From PyPI 21 | --------- 22 | 23 | Alternatively you can install ``ctyped`` from PyPI: 24 | 25 | pip install ctyped 26 | 27 | 28 | Use `-U` flag for upgrade: 29 | 30 | pip install -U ctyped 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, 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 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include CHANGELOG 3 | include INSTALL 4 | include LICENSE 5 | include README.rst 6 | 7 | include docs/Makefile 8 | recursive-include docs *.rst 9 | recursive-include docs *.py 10 | recursive-include tests * 11 | 12 | recursive-exclude * __pycache__ 13 | recursive-exclude * *.py[co] 14 | recursive-exclude * empty 15 | 16 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ctyped 2 | ====== 3 | https://github.com/idlesign/ctyped 4 | 5 | |release| |lic| |ci| |coverage| 6 | 7 | .. |release| image:: https://img.shields.io/pypi/v/ctyped.svg 8 | :target: https://pypi.python.org/pypi/ctyped 9 | 10 | .. |lic| image:: https://img.shields.io/pypi/l/ctyped.svg 11 | :target: https://pypi.python.org/pypi/ctyped 12 | 13 | .. |ci| image:: https://img.shields.io/travis/idlesign/ctyped/master.svg 14 | :target: https://travis-ci.org/idlesign/ctyped 15 | 16 | .. |coverage| image:: https://img.shields.io/coveralls/idlesign/ctyped/master.svg 17 | :target: https://coveralls.io/r/idlesign/ctyped 18 | 19 | 20 | Description 21 | ----------- 22 | 23 | *Build ctypes interfaces for shared libraries with type hinting* 24 | 25 | **Requires Python 3.6+** 26 | 27 | * Less boilerplate; 28 | * Logical structuring; 29 | * Basic code generator (.so function -> ctyped function); 30 | * Useful helpers. 31 | 32 | .. code-block:: python 33 | 34 | from ctyped.toolbox import Library 35 | 36 | # Define library. 37 | lib = Library('mylib.so') 38 | 39 | @lib.structure 40 | class Box: 41 | 42 | one: int 43 | two: str 44 | 45 | # Type less with function names prefixes. 46 | with lib.scope(prefix='mylib_'): 47 | 48 | # Describe function available in the library. 49 | @lib.function 50 | def some_func(title: str, year: int, box: Box) -> str: 51 | ... 52 | 53 | # Bind ctype types to functions available in the library. 54 | lib.bind_types() 55 | 56 | # Call library function. 57 | result_string = some_func('Hello!', 2019, Box(one=35, two='dummy')) 58 | 59 | 60 | Read the documentation for more information. 61 | 62 | 63 | Documentation 64 | ------------- 65 | 66 | http://ctyped.readthedocs.org/ 67 | -------------------------------------------------------------------------------- /ctyped/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | VERSION = (0, 8, 0) 4 | """Application version number tuple.""" 5 | 6 | VERSION_STR = '.'.join(map(str, VERSION)) 7 | """Application version number string.""" -------------------------------------------------------------------------------- /ctyped/exceptions.py: -------------------------------------------------------------------------------- 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 | """""" 21 | -------------------------------------------------------------------------------- /ctyped/library.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import inspect 3 | import logging 4 | import os 5 | from contextlib import contextmanager 6 | from ctypes.util import find_library 7 | from functools import partial, partialmethod, reduce 8 | from pathlib import Path 9 | from typing import Any, Optional, Callable, Union, List, Dict, Type, ContextManager 10 | 11 | from .exceptions import UnsupportedTypeError, TypehintError, CtypedException 12 | from .sniffer import NmSymbolSniffer, SniffResult 13 | from .types import CChars, CastedTypeBase, CStruct 14 | from .utils import cast_type, extract_func_info, FuncInfo 15 | 16 | LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | class Scopes: 20 | 21 | def __init__(self, params: dict): 22 | self._scopes: List[Dict] = [] 23 | self._keys = ['prefix', 'str_type', 'int_bits', 'int_sign'] 24 | self.push(params) 25 | 26 | def __call__( 27 | this, 28 | prefix: Optional[str] = None, 29 | str_type: Type[CastedTypeBase] = CChars, 30 | int_bits: Optional[int] = None, 31 | int_sign: Optional[bool] = None, 32 | **kwargs) -> ContextManager['Scopes']: 33 | """ 34 | 35 | :param prefix: Function name prefix to apply to functions under the manager. 36 | 37 | :param str_type: Type to represent strings. 38 | 39 | * ``CChars`` - strings as chars (ANSI) **default** 40 | * ``CCharsW`` - strings as wide chars (UTF) 41 | 42 | :param int_bits: int length to be used in function. 43 | 44 | Possible values: 8, 16, 32, 64 45 | 46 | :param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints. 47 | 48 | :param kwargs: 49 | 50 | """ 51 | return this.context(locals()) 52 | 53 | def push(self, params: dict): 54 | 55 | scope = {key: params.get(key) for key in self._keys} 56 | self._scopes.append(scope) 57 | 58 | def pop(self): 59 | self._scopes.pop() 60 | 61 | def flatten(self): 62 | 63 | scopes = self._scopes 64 | keys_bool = {'int_sign'} 65 | keys_concat = {'prefix'} 66 | result = {} 67 | 68 | def choose(current, prev): 69 | return current or prev 70 | 71 | def pick_bool(current, prev): 72 | return current if current is not None else prev 73 | 74 | def concat(current, prev): 75 | return (prev or '') + (current or '') 76 | 77 | for key in self._keys: 78 | 79 | if key in keys_concat: 80 | reducer = concat 81 | 82 | elif key in keys_bool: 83 | reducer = pick_bool 84 | 85 | else: 86 | reducer = choose 87 | 88 | result[key] = reduce(reducer, (scope[key] for scope in scopes[::-1])) 89 | 90 | return result 91 | 92 | @contextmanager 93 | def context(self, params: dict): 94 | self.push(params) 95 | yield self 96 | self.pop() 97 | 98 | 99 | class Library: 100 | """Main entry point to describe C library interface. 101 | 102 | Basic usage: 103 | 104 | .. code-block:: python 105 | 106 | lib = Library('mylib') 107 | 108 | with lib.scope(prefix='mylib_'): 109 | 110 | @lib.function() 111 | def my_func(): 112 | ... 113 | 114 | lib.bind_types() 115 | 116 | """ 117 | 118 | def __init__( 119 | self, name: Union[str, Path], *, autoload: bool = True, 120 | prefix: Optional[str] = None, 121 | str_type: Type[CastedTypeBase] = CChars, 122 | int_bits: Optional[int] = None, 123 | int_sign: Optional[bool] = None 124 | ): 125 | """ 126 | 127 | :param name: Shared library name or filepath. 128 | 129 | :param autoload: Load library just on Library object initialization. 130 | 131 | :param prefix: Function name prefix to apply to functions in the library. 132 | 133 | Useful when C functions have common prefixes. 134 | 135 | :param str_type: Type to represent strings. 136 | 137 | * ``CChars`` - strings as chars (ANSI) **default** 138 | * ``CCharsW`` - strings as wide chars (UTF) 139 | 140 | .. note:: This setting is global to library. Can be changed on function definition level. 141 | 142 | :param int_bits: int length to use by default. 143 | 144 | Possible values: 8, 16, 32, 64 145 | 146 | .. note:: This setting is global to library. Can be changed on function definition level. 147 | 148 | :param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints. 149 | 150 | .. note:: This setting is global to library. Can be changed on function definition level. 151 | 152 | """ 153 | self.scope = Scopes(locals()) 154 | self.s = self.scope 155 | 156 | self.name = str(name) 157 | self.lib = None 158 | self.funcs: Dict[str, Union[Callable, partialmethod[Any]]] = {} 159 | 160 | autoload and self.load() 161 | 162 | def load(self): 163 | """Loads shared library.""" 164 | name = self.name 165 | 166 | if not os.path.exists(name): 167 | name = name.replace('lib', '', 1) 168 | name = find_library(name) 169 | 170 | lib = ctypes.CDLL(name, use_errno=True) 171 | 172 | if lib._name is None: 173 | lib = None 174 | 175 | if lib is None: 176 | raise CtypedException(f'Unable to find library: {name or self.name}') 177 | 178 | self.lib = lib 179 | 180 | def structure( 181 | self, *, 182 | pack: Optional[int] = None, 183 | str_type: Optional[CastedTypeBase] = None, 184 | int_bits: Optional[int] = None, 185 | int_sign: Optional[bool] = None, 186 | ): 187 | """Class decorator for C structures definition. 188 | 189 | .. code-block:: python 190 | 191 | @lib.structure 192 | class MyStruct: 193 | 194 | first: int 195 | second: str 196 | third: 'MyStruct' 197 | 198 | :param pack: Allows custom maximum alignment for the fields (as #pragma pack(n)). 199 | 200 | :param str_type: Type to represent strings. 201 | 202 | :param int_bits: int length to be used in function. 203 | 204 | :param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints. 205 | 206 | """ 207 | params = locals() 208 | 209 | def wrapper(cls_): 210 | 211 | annotations = { 212 | attrname: attrhint for attrname, attrhint in cls_.__annotations__.items() 213 | if not attrname.startswith('_')} 214 | 215 | with self.scope(**params): 216 | 217 | cls_name = cls_.__name__ 218 | 219 | info = FuncInfo( 220 | name_py=cls_name, name_c=None, 221 | annotations=annotations, options=self.scope.flatten()) 222 | 223 | # todo maybe support big/little byte order 224 | struct = type(cls_name, (CStruct, cls_), {}) 225 | 226 | ct_fields = {} 227 | fields = [] 228 | 229 | for attrname, attrhint in annotations.items(): 230 | 231 | if attrhint == cls_name: 232 | casted = ctypes.POINTER(struct) 233 | ct_fields[attrname] = struct 234 | 235 | else: 236 | casted = cast_type(info, attrname, attrhint) 237 | 238 | if issubclass(casted, CastedTypeBase): 239 | ct_fields[attrname] = casted 240 | 241 | fields.append((attrname, casted)) 242 | 243 | LOGGER.debug(f'Structure {cls_name} fields: {fields}') 244 | 245 | if pack: 246 | struct._pack_ = pack 247 | 248 | struct._ct_fields = ct_fields 249 | struct._fields_ = fields 250 | 251 | return struct 252 | 253 | return wrapper 254 | 255 | def cls( 256 | self, *, 257 | prefix: Optional[str] = None, 258 | str_type: Optional[CastedTypeBase] = None, 259 | int_bits: Optional[int] = None, 260 | int_sign: Optional[bool] = None, 261 | ): 262 | """Class decorator. Allows common parameters application for class methods. 263 | 264 | .. code-block:: python 265 | 266 | @lib.cls(prefix='common_', str_type=CCharsW) 267 | class Wide: 268 | 269 | @staticmethod 270 | @lib.function() 271 | def get_utf(some: str) -> str: 272 | ... 273 | 274 | :param prefix: Function name prefix to apply to functions under the manager. 275 | 276 | :param str_type: Type to represent strings. 277 | 278 | :param int_bits: int length to be used in function. 279 | 280 | :param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints. 281 | 282 | """ 283 | self.scope.push(locals()) 284 | 285 | def wrapper(cls_): 286 | # Class body construction is done, unwind scope. 287 | self.scope.pop() 288 | return cls_ 289 | 290 | return wrapper 291 | 292 | def function( 293 | self, name_c: Optional[Union[str, Callable]] = None, *, wrap: bool = False, 294 | str_type: Optional[CastedTypeBase] = None, 295 | int_bits: Optional[int] = None, 296 | int_sign: Optional[bool] = None, 297 | 298 | ) -> Callable: 299 | """Decorator to mark functions which exported from the library. 300 | 301 | :param name_c: C function name with or without prefix (see ``.scope(prefix=)``). 302 | If not set, Python function name is used. 303 | 304 | :param wrap: Do not replace decorated function with ctypes function, 305 | but with wrapper, allowing pre- or post-process ctypes function call. 306 | 307 | Useful to organize functions to classes (to automatically pass ``self``) 308 | to ctypes function to C function. 309 | 310 | .. code-block:: python 311 | 312 | class Thing(CObject): 313 | 314 | @lib.function(wrap=True) 315 | def one(self, some: int) -> int: 316 | # Implicitly pass Thing instance alongside 317 | # with explicitly passed `some` arg. 318 | ... 319 | 320 | @lib.function(wrap=True) 321 | def two(self, some:int, cfunc: Callable) -> int: 322 | # `cfunc` is a wrapper, calling an actual ctypes function. 323 | # If no arguments provided the wrapper will try detect them 324 | # automatically. 325 | result = cfunc() 326 | return result + 1 327 | 328 | :param str_type: Type to represent strings. 329 | 330 | .. note:: Overrides the same named param from library level (see ``__init__`` description). 331 | 332 | :param int_bits: int length to be used in function. 333 | 334 | .. note:: Overrides the same named param from library level (see ``__init__`` description). 335 | 336 | :param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints. 337 | 338 | .. note:: Overrides the same named param from library level (see ``__init__`` description). 339 | 340 | """ 341 | def cfunc_wrapped(*args, f: Callable, **kwargs): 342 | 343 | if not args: 344 | argvals = inspect.getargvalues(getattr(inspect.currentframe(), 'f_back')) 345 | loc = argvals.locals 346 | args = tuple(loc[argname] for argname in argvals.args if argname != 'cfunc') 347 | 348 | return f(*args) 349 | 350 | def function_(func_py: Callable, *, name_c: Optional[str], scope: dict): 351 | 352 | info = extract_func_info(func_py, name_c=name_c, scope=scope, registry=self.funcs) 353 | name = info.name_c 354 | 355 | func_c = getattr(self.lib, name) 356 | 357 | # Prepare for late binding in .bind_types(). 358 | func_c.ctyped = info 359 | 360 | if wrap: 361 | func_args = inspect.getfullargspec(func_py).args 362 | 363 | if 'cfunc' in func_args: 364 | # Use existing function, pass `cfunc`. 365 | 366 | LOGGER.debug(f'Func [ {name} -> {info.name_py} ] uses wrapped manual call.') 367 | 368 | func_swapped = partialmethod(func_py, cfunc=partial(cfunc_wrapped, f=func_c)) 369 | 370 | else: 371 | # Automatically bind first param (self, cls) 372 | 373 | LOGGER.debug(f'Func [ {name} -> {info.name_py} ] uses wrapped auto call.') 374 | 375 | func_swapped = partialmethod(func_c) 376 | 377 | setattr(func_swapped, 'cfunc', func_c) 378 | func_out = func_swapped 379 | 380 | else: 381 | 382 | LOGGER.debug(f'Func [ {name} -> {info.name_py} ] uses direct call.') 383 | 384 | func_out = func_c 385 | 386 | self.funcs[name] = func_out 387 | 388 | return func_out 389 | 390 | if callable(name_c): 391 | # Decorator without params. 392 | scope = self.scope.flatten() 393 | py_func, name_c = name_c, None 394 | return function_(py_func, name_c=name_c, scope=scope) 395 | 396 | # Decorator with parameters. 397 | with self.scope(**locals()) as scope: 398 | scope = scope.flatten() 399 | 400 | return partial(function_, name_c=name_c, scope=scope) 401 | 402 | def method(self, name_c: Optional[str] = None, **kwargs): 403 | """Decorator. The same as ``.function()`` with ``wrap=True``.""" 404 | return self.function(name_c=name_c, wrap=True, **kwargs) 405 | 406 | def sniff(self) -> SniffResult: 407 | """Sniffs the library for symbols. 408 | 409 | Sniffing result can be used as 'ctyped' code generator. 410 | 411 | """ 412 | sniffer = NmSymbolSniffer(self.lib._name) 413 | result = sniffer.sniff() 414 | return result 415 | 416 | def bind_types(self): 417 | """Deduces ctypes argument and result types from Python type hints, 418 | binding those types to ctypes functions. 419 | 420 | """ 421 | LOGGER.debug('Binding signature types to ctypes functions ...') 422 | 423 | for name_c, func_out in self.funcs.items(): 424 | 425 | func_c = getattr(func_out, 'cfunc', func_out) 426 | func_info: FuncInfo = func_c.ctyped 427 | 428 | name_py = func_info.name_py 429 | annotations = func_info.annotations 430 | errcheck = None 431 | 432 | try: 433 | return_is_annotated = 'return' in annotations 434 | restype = cast_type(func_info, 'return', annotations.pop('return', None)) 435 | 436 | if restype and issubclass(restype, CastedTypeBase): 437 | errcheck = restype._ct_res 438 | 439 | argtypes = [cast_type(func_info, argname, argtype) for argname, argtype in annotations.items()] 440 | 441 | except TypehintError: 442 | # Reset annotations to allow subsequent .bind_types() calls w/o exceptions. 443 | func_info.annotations.clear() 444 | raise 445 | 446 | try: 447 | if argtypes: 448 | func_c.argtypes = argtypes 449 | 450 | if restype or return_is_annotated: 451 | func_c.restype = restype 452 | 453 | if errcheck: 454 | func_c.errcheck = errcheck 455 | 456 | except TypeError as e: 457 | 458 | raise UnsupportedTypeError( 459 | f'Unsupported types declared for {name_py} ({name_c}). ' 460 | f'Args: {argtypes}. Result: {restype}. Errcheck: {errcheck}.' 461 | ) from e 462 | 463 | ##################################################################################### 464 | # Shortcuts 465 | 466 | f = function 467 | """Shortcut for ``.function()``.""" 468 | 469 | m = method 470 | """Shortcut for ``.method()``.""" 471 | 472 | s = None # type: ignore 473 | """Shortcut for ``.scope()``.""" 474 | -------------------------------------------------------------------------------- /ctyped/sniffer.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from collections 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 | SniffedSymbol = namedtuple('SniffedSymbol', ['name', 'address', 'line']) 12 | """Represents a symbol sniffed from a library.""" 13 | 14 | 15 | class SniffResult: 16 | """Represents a library sniffing results.""" 17 | 18 | def __init__(self, *, libpath: str): 19 | self.symbols: List[SniffedSymbol] = [] 20 | self.libpath = libpath 21 | 22 | def add_symbol(self, symbol: SniffedSymbol): 23 | """Added a symbol to the result.""" 24 | self.symbols.append(symbol) 25 | 26 | def to_ctyped(self): 27 | """Generates ctyped code from sniff result.""" 28 | 29 | dumped = [ 30 | '###', 31 | f'# Code below was automatically generated {datetime.utcnow()} UTC', 32 | f'# Total functions: {len(self.symbols)}', 33 | '###', 34 | f"lib = Library('{self.libpath}')", 35 | '' 36 | ] 37 | 38 | for symbol in self.symbols: 39 | dumped.append(dedent( 40 | f''' 41 | @lib.f 42 | def {symbol.name}(): 43 | """{symbol.line}""" 44 | ''' 45 | )) 46 | 47 | dumped.append('\nlib.bind_types()') 48 | 49 | return '\n'.join(dumped) 50 | 51 | 52 | class NmSymbolSniffer: 53 | """Uses 'nm' command from 'binutils' package to sniff a library for exported symbols.""" 54 | 55 | def __init__(self, libpath: Union[str, Path]): 56 | """ 57 | 58 | :param libpath: Library path to sniff for symbols. 59 | 60 | """ 61 | self.libpath = str(libpath) 62 | 63 | def _run(self) -> List[str]: 64 | 65 | try: 66 | result = subprocess.run( 67 | ['nm', '-DCl', self.libpath], 68 | stdout=subprocess.PIPE, 69 | stderr=subprocess.PIPE, 70 | ) 71 | 72 | except FileNotFoundError: # pragma: nocover 73 | 74 | raise SniffingError( 75 | "Command 'nm' execution failed. " 76 | "Make sure 'nm' command from 'binutils' package is available.") 77 | 78 | if result.returncode: # pragma: nocover 79 | raise SniffingError(f"Command 'nm' execution failed: {result.stderr.decode()}") 80 | 81 | return result.stdout.decode().splitlines() 82 | 83 | def _get_symbols(self, lines: List[str]) -> List[SniffedSymbol]: 84 | 85 | symbols = [] 86 | 87 | for line in lines: 88 | 89 | if line.startswith(' '): 90 | continue 91 | 92 | chunks = line.split(' ') 93 | chunks_len = len(chunks) 94 | 95 | if chunks_len < 2 or chunks[1] != 'T': 96 | continue 97 | 98 | if len(chunks) != 3: # pragma: nocover 99 | raise SniffingError( 100 | f"Command 'nm' execution failed: 3 chunks line expected, but given {chunks}") 101 | 102 | address, symtype, name = chunks 103 | 104 | if symtype != 'T' or name.startswith('_'): 105 | continue 106 | 107 | name, _, srcline = name.partition('\t') 108 | 109 | symbols.append( 110 | SniffedSymbol( 111 | name=name, 112 | address=address, 113 | line=srcline, 114 | ) 115 | ) 116 | 117 | return symbols 118 | 119 | def sniff(self) -> SniffResult: 120 | """Runs symbols sniffing for library.""" 121 | 122 | result = SniffResult(libpath=self.libpath) 123 | 124 | for symbol in self._get_symbols(self._run()): 125 | result.add_symbol(symbol) 126 | 127 | return result 128 | -------------------------------------------------------------------------------- /ctyped/toolbox.py: -------------------------------------------------------------------------------- 1 | from .library import Library 2 | from .types import CObject, CRef 3 | from .utils import get_last_error, c_callback 4 | -------------------------------------------------------------------------------- /ctyped/types.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | from typing import Any, Optional, 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 = getattr(ctypes, 'c_short') 25 | CShortU: int = getattr(ctypes, 'c_ushort') 26 | CLong: int = getattr(ctypes, 'c_long') 27 | CLongU: int = getattr(ctypes, 'c_ulong') 28 | CLongLong: int = getattr(ctypes, 'c_longlong') 29 | CLongLongU: int = getattr(ctypes, 'c_ulonglong') 30 | 31 | CInt: int = getattr(ctypes, 'c_int') 32 | CIntU: int = getattr(ctypes, 'c_uint') 33 | CInt8: int = getattr(ctypes, 'c_int8') 34 | CInt8U: int = getattr(ctypes, 'c_uint8') 35 | CInt16: int = getattr(ctypes, 'c_int16') 36 | CInt16U: int = getattr(ctypes, 'c_uint16') 37 | CInt32: int = getattr(ctypes, 'c_int32') 38 | CInt32U: int = getattr(ctypes, 'c_uint32') 39 | CInt64: int = getattr(ctypes, 'c_int64') 40 | CInt64U: int = getattr(ctypes, 'c_uint64') 41 | 42 | CPointer: Any = getattr(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) -> Optional[str]: 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) -> Optional[str]: 196 | return cobj.value or '' 197 | 198 | @classmethod 199 | def from_param(cls, val: str): 200 | return ctypes.c_wchar_p(val) 201 | -------------------------------------------------------------------------------- /ctyped/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from collections import namedtuple 3 | from ctypes import get_errno, CFUNCTYPE 4 | from errno import errorcode 5 | from os import strerror 6 | from typing import Callable 7 | 8 | from .exceptions import TypehintError, FunctionRedeclared 9 | from .types import * 10 | 11 | FuncInfo = namedtuple('FuncInfo', ['name_py', 'name_c', 'annotations', 'options']) 12 | ErrorInfo = namedtuple('ErrorInfo', ['num', 'code', 'msg']) 13 | 14 | _MISSING = namedtuple('MissingType', []) 15 | 16 | 17 | def extract_func_info(func: Callable, *, name_c: Optional[str], scope: dict, registry: dict) -> FuncInfo: 18 | 19 | name_py = func.__name__ 20 | name = scope.get('prefix', '') + (name_c or name_py) 21 | 22 | if name in registry: 23 | raise FunctionRedeclared(f'Unable to redeclare: {name} ({name_py})') 24 | 25 | annotated_args = {} 26 | annotations = func.__annotations__ 27 | 28 | # Gather all args and annotations for them. 29 | for argname in inspect.getfullargspec(func).args: 30 | 31 | if argname == 'cfunc': 32 | continue 33 | 34 | annotation = annotations.get(argname, _MISSING) 35 | 36 | if argname == 'self' and annotation is _MISSING: 37 | # Pick class name from qualname. 38 | annotation = func.__qualname__.split('.')[-2] 39 | 40 | annotated_args[argname] = annotation 41 | 42 | annotated_args['return'] = annotations.get('return') 43 | 44 | return FuncInfo(name_py=name_py, name_c=name, annotations=annotated_args, options=scope) 45 | 46 | 47 | def thint_str_to_obj(thint: str): 48 | 49 | fback = getattr(inspect.currentframe(), 'f_back') 50 | 51 | while fback: 52 | target = fback.f_globals.get(thint) 53 | 54 | if target: 55 | return target 56 | 57 | fback = fback.f_back 58 | 59 | 60 | def cast_type(func_info, argname: str, thint: Any): 61 | 62 | if thint is None: 63 | return None 64 | 65 | if isinstance(thint, str): 66 | thint_orig = thint 67 | thint = thint_str_to_obj(thint) 68 | 69 | if thint is None: 70 | raise TypehintError( 71 | f'Unable to resolve type hint. ' 72 | f'Function: {func_info.name_py}. Arg: {argname}. Type: {thint_orig}.') 73 | 74 | if thint is bool: 75 | thint = ctypes.c_bool 76 | 77 | elif thint is float: 78 | thint = ctypes.c_float 79 | 80 | elif thint is str: 81 | thint = func_info.options.get('str_type', CChars) 82 | 83 | elif thint is int: 84 | int_bits = func_info.options.get('int_bits') 85 | int_sign = func_info.options.get('int_sign', False) 86 | 87 | thint_map = { 88 | 8: (CInt8, CInt8U), 89 | 16: (CInt16, CInt16U), 90 | 32: (CInt32, CInt32U), 91 | 64: (CInt64, CInt64U), 92 | 93 | } 94 | 95 | if int_bits: 96 | assert int_bits in thint_map.keys(), 'Wrong value passed for int_bits.' 97 | 98 | else: 99 | int_bits = 64 # todo maybe try to guess 100 | 101 | type_idx = 1 if int_sign is False else 0 102 | 103 | thint = thint_map[int_bits][type_idx] or thint 104 | 105 | return thint 106 | 107 | 108 | def get_last_error() -> ErrorInfo: 109 | """Returns last error (``errno``) information named tuple: 110 | 111 | .. code-block:: python 112 | 113 | (err_no, err_code, err_message) 114 | 115 | """ 116 | num = get_errno() 117 | code = errorcode[num] 118 | msg = strerror(num) 119 | 120 | return ErrorInfo(num=num, code=code, msg=msg) 121 | 122 | 123 | def c_callback(use_errno: bool = False) -> Callable: 124 | """Decorator to turn a Python function into a C callback function. 125 | 126 | .. code-block:: python 127 | 128 | @lib.f 129 | def c_func_using_callback(hook: CPointer) -> int: 130 | ... 131 | 132 | @c_callback 133 | def hook(num: int) -> int: 134 | return num + 10 135 | 136 | c_func_using_callback(hook) 137 | 138 | :param use_errno: 139 | 140 | """ 141 | def cfunction_(func: Callable) -> Callable: 142 | 143 | func_info = extract_func_info(func, name_c=None, scope={}, registry={}) 144 | annotations = func_info.annotations 145 | 146 | restype = cast_type(func_info, 'return', annotations.pop('return', None)) 147 | argtypes = [cast_type(func_info, argname, argtype) for argname, argtype in annotations.items()] 148 | 149 | functype = CFUNCTYPE(restype, *argtypes, use_errno=use_errno) 150 | cfunc = functype(func) 151 | 152 | return cfunc 153 | 154 | if callable(use_errno): 155 | # Decorator without params. 156 | return cfunction_(use_errno) 157 | 158 | return cfunction_ 159 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ctyped.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ctyped.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/ctyped" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ctyped" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | 132 | -------------------------------------------------------------------------------- /docs/build/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/ctyped/b24eb31f312bcecd10eb96838b5ed13b9f6f4b49/docs/build/empty -------------------------------------------------------------------------------- /docs/source/_static/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/ctyped/b24eb31f312bcecd10eb96838b5ed13b9f6f4b49/docs/source/_static/empty -------------------------------------------------------------------------------- /docs/source/_templates/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/ctyped/b24eb31f312bcecd10eb96838b5ed13b9f6f4b49/docs/source/_templates/empty -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ctyped documentation build configuration file. 4 | # 5 | # This file is execfile()d with the current directory set to its containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | 13 | import sys, os 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | sys.path.insert(0, os.path.abspath('../../')) 19 | from ctyped import VERSION 20 | 21 | # -- Mocking ------------------------------------------------------------------ 22 | 23 | # This is used to mock certain modules. 24 | # It helps to build docs in environments where those modules are not available. 25 | # E.g. it could be useful for http://readthedocs.org/ 26 | MODULES_TO_MOCK = [] 27 | 28 | 29 | if MODULES_TO_MOCK: 30 | 31 | class ModuleMock(object): 32 | 33 | __all__ = [] 34 | 35 | def __init__(self, *args, **kwargs): 36 | pass 37 | 38 | def __call__(self, *args, **kwargs): 39 | return ModuleMock() 40 | 41 | def __iter__(self): 42 | return iter([]) 43 | 44 | @classmethod 45 | def __getattr__(cls, name): 46 | if name in ('__file__', '__path__'): 47 | return '/dev/null' 48 | elif name.upper() != name and name[0] == name[0].upper(): 49 | # Mock classes. 50 | MockType = type(name, (ModuleMock,), {}) 51 | MockType.__module__ = __name__ 52 | return MockType 53 | return ModuleMock() 54 | 55 | for mod_name in MODULES_TO_MOCK: 56 | sys.modules[mod_name] = ModuleMock() 57 | 58 | 59 | # -- General configuration ----------------------------------------------------- 60 | 61 | # If your documentation needs a minimal Sphinx version, state it here. 62 | #needs_sphinx = '1.0' 63 | 64 | # Add any Sphinx extension module names here, as strings. They can be extensions 65 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 66 | extensions = ['sphinx.ext.autodoc'] 67 | 68 | # Instruct autoclass directive to document both class and __init__ docstrings. 69 | autoclass_content = 'both' 70 | 71 | # Add any paths that contain templates here, relative to this directory. 72 | templates_path = ['_templates'] 73 | 74 | # The suffix of source filenames. 75 | source_suffix = '.rst' 76 | 77 | # The encoding of source files. 78 | #source_encoding = 'utf-8-sig' 79 | 80 | # The master toctree document. 81 | master_doc = 'index' 82 | 83 | # General information about the project. 84 | project = u'ctyped' 85 | copyright = u'2019, Igor `idle sign` Starikov' 86 | 87 | # The version info for the project you're documenting, acts as replacement for 88 | # |version| and |release|, also used in various other places throughout the 89 | # built documents. 90 | # 91 | # The short X.Y version. 92 | version = '.'.join(map(str, VERSION)) 93 | # The full version, including alpha/beta/rc tags. 94 | release = '.'.join(map(str, VERSION)) 95 | 96 | # The language for content autogenerated by Sphinx. Refer to documentation 97 | # for a list of supported languages. 98 | #language = None 99 | 100 | # There are two options for replacing |today|: either, you set today to some 101 | # non-false value, then it is used: 102 | #today = '' 103 | # Else, today_fmt is used as the format for a strftime call. 104 | #today_fmt = '%B %d, %Y' 105 | 106 | # List of patterns, relative to source directory, that match files and 107 | # directories to ignore when looking for source files. 108 | exclude_patterns = ['rst_guide.rst'] 109 | 110 | # The reST default role (used for this markup: `text`) to use for all documents. 111 | #default_role = None 112 | 113 | # If true, '()' will be appended to :func: etc. cross-reference text. 114 | #add_function_parentheses = True 115 | 116 | # If true, the current module name will be prepended to all description 117 | # unit titles (such as .. function::). 118 | #add_module_names = True 119 | 120 | # If true, sectionauthor and moduleauthor directives will be shown in the 121 | # output. They are ignored by default. 122 | #show_authors = False 123 | 124 | # The name of the Pygments (syntax highlighting) style to use. 125 | pygments_style = 'sphinx' 126 | 127 | # A list of ignored prefixes for module index sorting. 128 | #modindex_common_prefix = [] 129 | 130 | 131 | # -- Options for HTML output --------------------------------------------------- 132 | 133 | # The theme to use for HTML and HTML Help pages. See the documentation for 134 | # a list of builtin themes. 135 | html_theme = 'default' 136 | 137 | # Theme options are theme-specific and customize the look and feel of a theme 138 | # further. For a list of options available for each theme, see the 139 | # documentation. 140 | #html_theme_options = {} 141 | 142 | # Add any paths that contain custom themes here, relative to this directory. 143 | #html_theme_path = [] 144 | 145 | # The name for this set of Sphinx documents. If None, it defaults to 146 | # " v documentation". 147 | #html_title = None 148 | 149 | # A shorter title for the navigation bar. Default is the same as html_title. 150 | #html_short_title = None 151 | 152 | # The name of an image file (relative to this directory) to place at the top 153 | # of the sidebar. 154 | #html_logo = None 155 | 156 | # The name of an image file (within the static path) to use as favicon of the 157 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 158 | # pixels large. 159 | #html_favicon = None 160 | 161 | # Add any paths that contain custom static files (such as style sheets) here, 162 | # relative to this directory. They are copied after the builtin static files, 163 | # so a file named "default.css" will overwrite the builtin "default.css". 164 | html_static_path = ['_static'] 165 | 166 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 167 | # using the given strftime format. 168 | #html_last_updated_fmt = '%b %d, %Y' 169 | 170 | # If true, SmartyPants will be used to convert quotes and dashes to 171 | # typographically correct entities. 172 | #html_use_smartypants = True 173 | 174 | # Custom sidebar templates, maps document names to template names. 175 | #html_sidebars = {} 176 | 177 | # Additional templates that should be rendered to pages, maps page names to 178 | # template names. 179 | #html_additional_pages = {} 180 | 181 | # If false, no module index is generated. 182 | #html_domain_indices = True 183 | 184 | # If false, no index is generated. 185 | #html_use_index = True 186 | 187 | # If true, the index is split into individual pages for each letter. 188 | #html_split_index = False 189 | 190 | # If true, links to the reST sources are added to the pages. 191 | #html_show_sourcelink = True 192 | 193 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 194 | #html_show_sphinx = True 195 | 196 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 197 | #html_show_copyright = True 198 | 199 | # If true, an OpenSearch description file will be output, and all pages will 200 | # contain a tag referring to it. The value of this option must be the 201 | # base URL from which the finished HTML is served. 202 | #html_use_opensearch = '' 203 | 204 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 205 | #html_file_suffix = None 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = 'ctypeddoc' 209 | 210 | 211 | # -- Options for LaTeX output -------------------------------------------------- 212 | 213 | # The paper size ('letter' or 'a4'). 214 | #latex_paper_size = 'letter' 215 | 216 | # The font size ('10pt', '11pt' or '12pt'). 217 | #latex_font_size = '10pt' 218 | 219 | # Grouping the document tree into LaTeX files. List of tuples 220 | # (source start file, target name, title, author, documentclass [howto/manual]). 221 | latex_documents = [ 222 | ('index', 'ctyped.tex', u'ctyped Documentation', 223 | u'Igor `idle sign` Starikov', 'manual'), 224 | ] 225 | 226 | # The name of an image file (relative to this directory) to place at the top of 227 | # the title page. 228 | #latex_logo = None 229 | 230 | # For "manual" documents, if this is true, then toplevel headings are parts, 231 | # not chapters. 232 | #latex_use_parts = False 233 | 234 | # If true, show page references after internal links. 235 | #latex_show_pagerefs = False 236 | 237 | # If true, show URL addresses after external links. 238 | #latex_show_urls = False 239 | 240 | # Additional stuff for the LaTeX preamble. 241 | #latex_preamble = '' 242 | 243 | # Documents to append as an appendix to all manuals. 244 | #latex_appendices = [] 245 | 246 | # If false, no module index is generated. 247 | #latex_domain_indices = True 248 | 249 | 250 | # -- Options for manual page output -------------------------------------------- 251 | 252 | # One entry per manual page. List of tuples 253 | # (source start file, name, description, authors, manual section). 254 | man_pages = [ 255 | ('index', 'ctyped', u'ctyped Documentation', 256 | [u'Igor `idle sign` Starikov'], 1) 257 | ] 258 | 259 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ctyped documentation 2 | ==================== 3 | https://github.com/idlesign/ctyped 4 | 5 | 6 | 7 | Description 8 | ----------- 9 | 10 | *Build ctypes interfaces for shared libraries with type hinting* 11 | 12 | **Requires Python 3.6+** 13 | 14 | * Less boilerplate; 15 | * Logical structuring; 16 | * Basic code generator (.so function -> ctyped function); 17 | * Useful helpers. 18 | 19 | 20 | Requirements 21 | ------------ 22 | 23 | 1. Python 3.6+ 24 | 25 | 26 | 27 | Table of Contents 28 | ----------------- 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | 33 | quickstart 34 | library 35 | utils 36 | -------------------------------------------------------------------------------- /docs/source/library.rst: -------------------------------------------------------------------------------- 1 | Library 2 | ======= 3 | 4 | 5 | .. automodule:: ctyped.library 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | 5 | .. code-block:: python 6 | 7 | from typing import Callable 8 | from ctyped.toolbox import Library 9 | from ctyped.types import CInt 10 | 11 | # Define a library. 12 | lib = Library('mylib.so') 13 | 14 | # Structures are defined with the help of `structure` decorator 15 | @lib.structure 16 | class Box: 17 | 18 | one: int 19 | two: str 20 | innerbox: 'Box' # That'll be a pointer. 21 | 22 | # Type less with function names prefixes. 23 | with lib.scope(prefix='mylib_'): 24 | 25 | # Describe function available in the library. 26 | @lib.function(name='otherfunc') 27 | def some_func(title: str, year: int) -> str: 28 | ... 29 | 30 | @lib.f # `f` is a shortcut for function. 31 | def struct_func(src: Box) -> Box: 32 | ... 33 | 34 | with lib.s(prefix='mylib_grouped_', int_bits=64, int_sign=False): # `s` is a shortcut for scope. 35 | 36 | class Thing(CInt): 37 | 38 | @lib.method(int_sign=True) # Override `int_sign` from scope. 39 | def one(self, some: int) -> int: 40 | # Implicitly pass Thing instance alongside 41 | # with explicitly passed `some` arg. 42 | ... 43 | 44 | @lib.m # `m` is a shortcut for method. 45 | def two(self, some:int, cfunc: Callable) -> int: 46 | # `cfunc` is a wrapper, calling an actual ctypes function. 47 | result = cfunc() 48 | # If no arguments, the wrapper will try to detect them automatically. 49 | return result + 1 50 | 51 | @lib.function 52 | def get_thing() -> Thing: 53 | ... 54 | 55 | # Or you may use classes as namespaces. 56 | @lib.cls(prefix='common_', str_type=CCharsW) 57 | class Wide: 58 | 59 | @staticmethod 60 | @lib.function 61 | def get_utf(some: str) -> str: 62 | ... 63 | 64 | # Bind ctype types to functions available in the library. 65 | lib.bind_types() 66 | 67 | # Call function from the library. Call ``mylib_otherfunc`` 68 | result_string = some_func('Hello!', 2019) 69 | result_wide = Wide.get_utf('some') # Call ``common_get_utf`` 70 | 71 | # Now to structures. Call ``mylib_struct_func`` 72 | mybox = struct_func(Box(one=35, two='dummy', innerbox=Box(one=100))) 73 | # Let's pretend our function returns a box inside a box (similar to what's in the params). 74 | mybox.one # Access box field value. 75 | mybox.innerbox.one # Access values from nested objects. 76 | 77 | thing = get_thing() 78 | 79 | thing.one(12) # Call ``mylib_mylib_grouped_one``. 80 | thing.two(13) # Call ``mylib_mylib_grouped_two`` 81 | 82 | 83 | Sniffing 84 | ======== 85 | 86 | To save some time on function definition you can use ``ctyped`` automatic code generator. 87 | 88 | It won't give you fully functional code, but is able to lower typing chore. 89 | 90 | .. code-block:: python 91 | 92 | from ctyped.sniffer import NmSymbolSniffer 93 | 94 | # We sniff library first. 95 | sniffer = NmSymbolSniffer('/here/is/my/libsome.so') 96 | sniffed = sniffer.sniff() 97 | 98 | # Now let's generate ctyped code. 99 | dumped = sniffed.to_ctyped() 100 | 101 | # At last we save autogenerated code into a file. 102 | with open('library.py', 'w') as f: 103 | f.write(dumped) 104 | 105 | 106 | There's also a shortcut to sniff an already defined library: 107 | 108 | .. code-block:: python 109 | 110 | ... 111 | sniffed = lib.sniff() 112 | dumped = result.to_ctyped() 113 | 114 | -------------------------------------------------------------------------------- /docs/source/rst_guide.rst: -------------------------------------------------------------------------------- 1 | RST Quick guide 2 | =============== 3 | 4 | Online reStructuredText editor - http://rst.ninjs.org/ 5 | 6 | 7 | Main heading 8 | ============ 9 | 10 | 11 | Secondary heading 12 | ----------------- 13 | 14 | Minor heading 15 | ~~~~~~~~~~~~~ 16 | 17 | 18 | Typography 19 | ---------- 20 | 21 | **Bold** 22 | 23 | `Italic` 24 | 25 | ``Accent`` 26 | 27 | 28 | 29 | Blocks 30 | ------ 31 | 32 | Double colon to consider the following paragraphs preformatted:: 33 | 34 | This text is preformated. Can be used for code samples. 35 | 36 | 37 | .. code-block:: python 38 | 39 | # code-block accepts language name to highlight code 40 | # E.g.: python, html 41 | import this 42 | 43 | 44 | .. note:: 45 | 46 | This text will be rendered as a note block (usually green). 47 | 48 | 49 | .. warning:: 50 | 51 | This text will be rendered as a warning block (usually red). 52 | 53 | 54 | 55 | Lists 56 | ----- 57 | 58 | 1. Ordered item 1. 59 | 60 | Indent paragraph to make in belong to the above list item. 61 | 62 | 2. Ordered item 2. 63 | 64 | 65 | + Unordered item 1. 66 | + Unordered item . 67 | 68 | 69 | 70 | Links 71 | ----- 72 | 73 | :ref:`Documentation inner link label ` 74 | 75 | .. _some-marker: 76 | 77 | 78 | `Outer link label `_ 79 | 80 | Inline URLs are converted to links automatically: http://github.com/idlesign/makeapp/ 81 | 82 | 83 | Images 84 | ------ 85 | 86 | .. image:: path_to_image/image.png 87 | 88 | 89 | Automation 90 | ---------- 91 | 92 | http://sphinx-doc.org/ext/autodoc.html 93 | 94 | .. automodule:: my_module 95 | :members: 96 | 97 | .. autoclass:: my_module.MyClass 98 | :members: do_this, do_that 99 | :inherited-members: 100 | :undoc-members: 101 | :private-members: 102 | :special-members: 103 | :show-inheritance: 104 | 105 | -------------------------------------------------------------------------------- /docs/source/utils.rst: -------------------------------------------------------------------------------- 1 | Utils 2 | ===== 3 | 4 | 5 | .. automodule:: ctyped.utils 6 | :members: 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | release = clean --all sdist bdist_wheel upload 3 | test = pytest -v 4 | 5 | [wheel] 6 | universal = 1 7 | 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | 5 | from setuptools import setup, find_packages 6 | 7 | import sys 8 | 9 | PATH_BASE = os.path.dirname(__file__) 10 | 11 | 12 | def read_file(fpath): 13 | """Reads a file within package directories.""" 14 | with io.open(os.path.join(PATH_BASE, fpath)) as f: 15 | return f.read() 16 | 17 | 18 | def get_version(): 19 | """Returns version number, without module import (which can lead to ImportError 20 | if some dependencies are unavailable before install.""" 21 | contents = read_file(os.path.join('ctyped', '__init__.py')) 22 | version = re.search('VERSION = \(([^)]+)\)', contents) 23 | version = version.group(1).replace(', ', '.').strip() 24 | return version 25 | 26 | 27 | setup( 28 | name='ctyped', 29 | version=get_version(), 30 | url='https://github.com/idlesign/ctyped', 31 | 32 | description='Build ctypes interfaces for shared libraries with type hinting', 33 | long_description=read_file('README.rst'), 34 | license='BSD 3-Clause License', 35 | 36 | author='Igor `idle sign` Starikov', 37 | author_email='idlesign@yandex.ru', 38 | 39 | packages=find_packages(), 40 | include_package_data=True, 41 | zip_safe=False, 42 | 43 | install_requires=[], 44 | setup_requires=[] + (['pytest-runner'] if 'test' in sys.argv else []) + [], 45 | 46 | test_suite='tests', 47 | 48 | tests_require=[ 49 | 'pytest', 50 | ], 51 | 52 | classifiers=[ 53 | # As in https://pypi.python.org/pypi?:action=list_classifiers 54 | 'Development Status :: 4 - Beta', 55 | 'Operating System :: OS Independent', 56 | 'Programming Language :: Python', 57 | 'Programming Language :: Python :: 3', 58 | 'Programming Language :: Python :: 3.6', 59 | 'Programming Language :: Python :: 3.7', 60 | 'License :: OSI Approved :: BSD License' 61 | ], 62 | ) 63 | -------------------------------------------------------------------------------- /tests/mylib/compile.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | gcc -Wall -g -shared -o mylib.so -fPIC mylib.c 3 | -------------------------------------------------------------------------------- /tests/mylib/mylib.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | int buggy1() { 12 | return 777; 13 | } 14 | 15 | 16 | int buggy2() { 17 | return 888; 18 | } 19 | 20 | 21 | int with_errno() { 22 | errno = ENOENT; 23 | return 333; 24 | } 25 | 26 | 27 | int f_noprefix_1() { 28 | return -10; 29 | } 30 | 31 | 32 | int f_prefix_one_func_1() { 33 | return 1; 34 | } 35 | 36 | 37 | int f_prefix_one_func_2() { 38 | return 2; 39 | } 40 | 41 | 42 | int f_prefix_one_prefix_two_func_3() { 43 | return 3; 44 | } 45 | 46 | 47 | int f_prefix_one_get_prober() { 48 | srand(time(NULL)); 49 | return rand(); 50 | } 51 | 52 | 53 | int f_prefix_one_probe_add_one(int val) { 54 | return val + 1; 55 | } 56 | 57 | 58 | int f_prefix_one_probe_add_two(int val) { 59 | return val + 2; 60 | } 61 | 62 | 63 | void f_prefix_one_byref_int(int * val) { 64 | *val = 33; 65 | } 66 | 67 | 68 | bool f_prefix_one_bool_to_bool(bool val) { 69 | return !val; 70 | } 71 | 72 | 73 | float f_prefix_one_float_to_float(float val) { 74 | return val; 75 | } 76 | 77 | 78 | typedef int (*callback) (int num); 79 | 80 | 81 | int f_prefix_one_backcaller(callback hook) { 82 | return hook(33); 83 | } 84 | 85 | 86 | uint8_t f_prefix_one_uint8_add(uint8_t val) { 87 | return val + 1; 88 | } 89 | 90 | const char * f_prefix_one_char_p(char* val) { 91 | char prefix[] = "hereyouare: "; 92 | char *out = malloc(sizeof(char) * (strlen(prefix) + strlen(val) + 1 )); 93 | strcpy(out, prefix); 94 | strcat(out, val); 95 | return out; 96 | } 97 | 98 | 99 | const wchar_t * f_prefix_one_wchar_p(wchar_t* val) { 100 | setlocale(LC_ALL, "en_US.utf8"); 101 | 102 | wchar_t prefix[] = L"вот: "; 103 | wchar_t *out = malloc(sizeof(wchar_t) * (wcslen(prefix) + wcslen(val) + 1 )); 104 | 105 | wcscpy(out, prefix); 106 | wcscat(out, val); 107 | 108 | return out; 109 | } 110 | 111 | 112 | typedef struct MyStruct { 113 | 114 | uint8_t one; 115 | char * two; 116 | struct MyStruct * next; 117 | 118 | } mystruct_t; 119 | 120 | 121 | mystruct_t f_prefix_one_handle_mystruct(mystruct_t val) { 122 | val.one += 2; 123 | val.next->one += 5; 124 | 125 | char prefix[] = "thing"; 126 | 127 | char *out = malloc(sizeof(char) * (strlen(prefix) + strlen(val.two) + 1 )); 128 | strcpy(out, val.two); 129 | strcat(out, prefix); 130 | 131 | val.two = out; 132 | 133 | return val; 134 | } 135 | -------------------------------------------------------------------------------- /tests/mylib/mylib.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idlesign/ctyped/b24eb31f312bcecd10eb96838b5ed13b9f6f4b49/tests/mylib/mylib.so -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | import faulthandler 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from ctyped.exceptions import FunctionRedeclared, TypehintError, UnsupportedTypeError 7 | from ctyped.toolbox import Library, get_last_error, c_callback 8 | from ctyped.types import CInt, CCharsW, CRef, CPointer 9 | 10 | ############################################################ 11 | # Library interface 12 | 13 | # Watch for crashes. 14 | faulthandler.enable() 15 | 16 | MYLIB_PATH = Path(__file__).parent / 'mylib' / 'mylib.so' 17 | 18 | mylib = Library(MYLIB_PATH, int_bits=32) 19 | 20 | 21 | @mylib.structure(int_bits=8, pack=16) 22 | class MyStruct: 23 | 24 | _hidden: 'dummy' 25 | first: int 26 | second: str 27 | third: 'MyStruct' 28 | 29 | def get_additional(self): 30 | return 10 31 | 32 | 33 | @mylib.f() 34 | def f_noprefix_1() -> int: 35 | ... 36 | 37 | 38 | @mylib.f() 39 | def with_errno() -> int: 40 | ... 41 | 42 | 43 | with mylib.scope('f_prefix_one_'): 44 | 45 | @mylib.function('func_1') 46 | def function_one() -> int: 47 | ... 48 | 49 | @mylib.function() 50 | def func_2() -> int: 51 | ... 52 | 53 | @mylib.f 54 | def backcaller(val: CPointer) -> int: 55 | ... 56 | 57 | @mylib.f 58 | def handle_mystruct(val: MyStruct) -> MyStruct: 59 | ... 60 | 61 | @mylib.f 62 | def byref_int(val: CRef) -> None: 63 | ... 64 | 65 | @mylib.f 66 | def bool_to_bool(val: bool) -> bool: 67 | ... 68 | 69 | @mylib.f 70 | def float_to_float(val: float) -> float: 71 | ... 72 | 73 | @mylib.function(int_bits=8, int_sign=False) 74 | def uint8_add(val: int) -> int: 75 | ... 76 | 77 | @mylib.f('char_p') 78 | def func_str(some: str) -> str: 79 | ... 80 | 81 | @mylib.cls(prefix='wchar_', str_type=CCharsW) 82 | class Wide: 83 | 84 | @staticmethod 85 | @mylib.f('p') 86 | def func_str_utf(some: str) -> str: 87 | ... 88 | 89 | with mylib.s('prefix_two_'): 90 | 91 | @mylib.f('func_3') 92 | def func_prefix_two_3() -> int: 93 | ... 94 | 95 | class Prober(CInt): 96 | 97 | @mylib.m 98 | def probe_add_one(self) -> int: 99 | ... 100 | 101 | @mylib.m('probe_add_two') 102 | def probe_add_three(self, cfunc) -> int: 103 | result = cfunc() 104 | return result + 1 105 | 106 | @mylib.f 107 | def get_prober() -> Prober: 108 | ... 109 | 110 | 111 | mylib.bind_types() 112 | 113 | ############################################################ 114 | # Tests 115 | 116 | 117 | def test_sniff(): 118 | result = mylib.sniff() 119 | dumped = result.to_ctyped() 120 | assert "mylib.so')" in dumped 121 | assert 'def buggy1():' in dumped 122 | assert 'bind_types()' in dumped 123 | 124 | 125 | def test_basic(): 126 | assert f_noprefix_1() == -10 127 | assert function_one() == 1 128 | assert func_2() == 2 129 | assert func_prefix_two_3() == 3 130 | assert uint8_add(4) == 5 131 | assert not bool_to_bool(True) 132 | assert bool_to_bool(False) 133 | 134 | float_ = 1.3 135 | assert float_to_float(float_) == pytest.approx(float_) 136 | 137 | prober = get_prober() 138 | assert isinstance(prober, Prober) 139 | 140 | prober_val = prober.value 141 | 142 | assert prober.probe_add_one() == prober_val + 1 143 | assert prober.probe_add_three() == prober_val + 3 144 | 145 | byref_val = CRef.cint() 146 | assert byref_int(byref_val) is None 147 | assert int(byref_val) == 33 148 | assert byref_val == 33 149 | assert byref_val != 34 150 | assert 32 < byref_val < 34 151 | assert 32 <= byref_val <= 34 152 | assert byref_val 153 | assert float(byref_val) == pytest.approx(float(33)) 154 | assert str(byref_val) == '33' 155 | 156 | 157 | def test_cref_instantiation(): 158 | assert isinstance(CRef.carray(bool, size=10), CRef) 159 | assert isinstance(CRef.cbool(True), CRef) 160 | assert isinstance(CRef.cfloat(10.25), CRef) 161 | 162 | 163 | def test_with_errno(): 164 | assert with_errno() == 333 165 | err = get_last_error() 166 | assert err.num == 2 167 | assert err.code == 'ENOENT' 168 | assert 'such file' in err.msg 169 | 170 | 171 | def test_strings(): 172 | 173 | assert func_str('mind') == 'hereyouare: mind' 174 | assert Wide.func_str_utf('пример') == 'вот: пример' 175 | 176 | 177 | def test_no_redeclare(): 178 | 179 | with pytest.raises(FunctionRedeclared): 180 | 181 | @mylib.f() 182 | def f_noprefix_1() -> int: 183 | ... 184 | 185 | 186 | def test_unresolved_typehint(): 187 | 188 | @mylib.f() 189 | def buggy1(one: 'SomeDummyType') -> int: 190 | ... 191 | 192 | with pytest.raises(TypehintError) as e: 193 | mylib.bind_types() 194 | 195 | assert 'SomeDummyType' in str(e.value) 196 | 197 | 198 | def test_unsupported_type(): 199 | 200 | class SomeType: pass 201 | 202 | @mylib.f() 203 | def buggy2(one: SomeType) -> int: 204 | ... 205 | 206 | with pytest.raises(UnsupportedTypeError) as e: 207 | mylib.bind_types() 208 | 209 | assert 'buggy2 (buggy2)' in str(e.value) 210 | 211 | 212 | def test_callback(): 213 | 214 | @c_callback 215 | def hook(num: int) -> int: 216 | return num + 10 217 | 218 | assert backcaller(hook) == 43 219 | 220 | 221 | def test_struct(): 222 | 223 | struct = MyStruct(first=2, second='any', third=MyStruct(first=10)) 224 | 225 | assert struct.get_additional() == 10 # verify method is copied 226 | 227 | result = handle_mystruct(struct) 228 | assert result.first == 4 229 | assert result.second == 'anything' 230 | assert '%s' % result.second == 'anything' 231 | assert result.second 232 | 233 | result.second = '' 234 | assert not result.second 235 | assert result.third.first == 15 236 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # See http://tox.readthedocs.org/en/latest/examples.html for samples. 2 | [tox] 3 | envlist = 4 | py{36,37} 5 | 6 | skip_missing_interpreters = True 7 | 8 | install_command = pip install {opts} {packages} 9 | 10 | [testenv] 11 | commands = 12 | python setup.py test 13 | 14 | deps = 15 | 16 | --------------------------------------------------------------------------------