Source code for

Utility functions/classes to help create modules in the **** package.
from __future__ import annotations

import ctypes
import re

BYTE = ctypes.c_byte
WORD = ctypes.c_ushort
DWORD = ctypes.c_ulong
HINSTANCE = ctypes.c_void_p

    'void': None,
    'void*': 'c_void_p',
    'BYTE': 'BYTE',
    'WORD': 'WORD',
    'DWORD': 'DWORD',
    'byte': 'c_byte',
    '_Bool': 'c_bool',
    'bool': 'c_bool',
    'char': 'c_char',
    'char*': 'c_char_p',
    'char const*': 'c_char_p',
    'char *': 'c_char_p',
    'signed char*': 'c_char_p',
    'unsigned char*': 'POINTER(c_ubyte)',
    'wchar_t': 'c_wchar',
    'wchar_t*': 'c_wchar_p',
    'short': 'c_short',
    'int': 'c_int',
    'long': 'c_long',
    'long long': 'c_longlong',
    'float': 'c_float',
    'double': 'c_double',
    'long double': 'c_longdouble',
    'size_t': 'c_size_t',
    'ssize_t': 'c_ssize_t',
    '__int8': 'c_int8',
    '__int16': 'c_int16',
    '__int': 'c_int',
    '__int32': 'c_int32',
    '__int64': 'c_int64',
    'signed short': 'c_short',
    'signed int': 'c_int',
    'signed __int8': 'c_int8',
    'signed __int16': 'c_int16',
    'signed __int': 'c_int',
    'signed __int32': 'c_int32',
    'signed __int64': 'c_int64',
    'signed long': 'c_long',
    'signed long long': 'c_longlong',
    'unsigned char': 'c_ubyte',
    'unsigned short': 'c_ushort',
    'unsigned int': 'c_uint',
    'unsigned __int8': 'c_uint8',
    'unsigned __int16': 'c_uint16',
    'unsigned __int': 'c_uint',
    'unsigned __int32': 'c_uint32',
    'unsigned __int64': 'c_uint64',
    'unsigned long': 'c_ulong',
    'unsigned long long': 'c_ulonglong',
    'int8_t': 'c_int8',
    'uint8_t': 'c_uint8',
    'int16_t': 'c_int16',
    'uint16_t': 'c_uint16',
    'int32_t': 'c_int32',
    'uint32_t': 'c_uint32',
    'int64_t': 'c_int64',
    'uint64_t': 'c_uint64',

[docs] def camelcase_to_underscore(text): """Converts ``CamelCaseText`` to ``camel_case_text``. Parameters ---------- text : :class:`str` The camel-case text to be converted. Returns ------- :class:`str` The `text` converted to lowercase and separated by underscores. """ s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', text) return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
[docs] def get_lines(path, remove_comments=True): """Returns the lines in a C/C++ header file that are not empty. Also strips the whitespace from each line and can optionally remove the comments. Parameters ---------- path : :class:`str` The path to a C/C++ header file. remove_comments : :class:`bool`, optional Whether to remove the comments. Returns ------- :class:`list` of :class:`str` The list of lines in the header file. """ lines = [] with open(path, mode='rt') as fp: for line in fp.readlines(): line_strip = line.strip() if not line_strip: continue if remove_comments: if line_strip.startswith('/') or line_strip.startswith('*'): continue line_strip = line_strip.split('/')[0].strip() lines.append(line_strip) return lines
[docs] class CHeader(object): _defines_regex = re.compile(r'^#define\s+(\w+)\s+([^/]+)') _enum_regex = re.compile(r'^(typedef\s+)?enum\s+(\w+)(\s*:\s*\w+\s*\w+)?') _enum_alias_regex = re.compile(r'}\s*(\w+)\s*;') _struct_regex = re.compile(r'^(typedef\s+)?struct\s+(\w+)') _callback_regex = re.compile(r'typedef\s+void\s*\(\s*(\w+)?\s*\*(\w+)\s*\)') def __init__(self, path, remove_comments=True): """Parses a C/C++ header file to determine the constants, enums, structs, callbacks and the function signatures. Parameters ---------- path : :class:`str` The path to the header file. remove_comments : :class:`bool`, optional Whether to remove the comments. """ self._struct_imports = [] # the structs that must be imported for the C functions self._lines = get_lines(path, remove_comments) self._get_enums() self._get_structs() self._get_callbacks()
[docs] def constants(self, ignore_ifdef=True): """Finds the ``#define`` statements that are in the C/C++ header file. Parameters ---------- ignore_ifdef : :class:`bool`, optional Whether to ignore the ``#define`` statements in between the ``#ifdef`` and ``#endif`` statements. Returns ------- :class:`dict` A dictionary of all the ``#define`` constants (as strings). """ _constants = {} count = 0 for line in self._lines: if ignore_ifdef and line.startswith('#ifdef'): count += 1 elif ignore_ifdef and line.startswith('#endif'): count -= 1 if count > 0: continue m = if m is not None: name = value = if value.endswith('f'): value = value[:-1] elif value.endswith('L'): value = value[:-1] _constants[name] = value return _constants
[docs] def enums(self): """ Returns ------- :class:`dict` The ``enums`` that are defined in the C/C++ header file. The value for each dictionary key is a tuple of ``(the enum name, the enum data type, a dict of name-value pairs)``. """ return self._enums
[docs] def structs(self): """ Returns ------- :class:`dict` The ``structs`` that are defined in the C/C++ header file. """ return self._structs
[docs] def callbacks(self): """ Returns ------- :class:`dict` The ``callbacks`` that are defined in the C/C++ header file. """ return self._callbacks
[docs] def functions(self, regex): r"""Returns the function signatures. Parameters ---------- regex : :class:`str` The regex must contain 2 groups, ``(return type)(function name)``, and it must match the function declaration up until, but excluding, the ``(`` which begins the argument declarations. For example, If the function declarations are similar to ``FILTERFLIPPERDLL_API unsigned int __cdecl FF_GetTransitTime(const char * serialNo);`` then the value of `regex` could be ``r'_API\s+([\w\s]+?)__cdecl\s+(\w+)'`` If the function declarations are similar to ``PREF0 PREF1 PICO_STATUS PREF2 PREF3 (ps6000OpenUnit)(int16_t *handle, int8_t *serial);`` then the value of `regex` could be ``r'PREF0\s+PREF1\s+(\w+)\s+PREF2\s+PREF3\s+\((\w+)\)'`` Returns ------- :class:`dict` The function signature. The key is the function name and the value is a list of ``[return type, [(argument data type, argument name), ... ] ]``. """ _fcn_regex = re.compile(regex) lines = list(self._lines) # create a copy, since it will be modified self._struct_imports = [] fcns = {} i, n = 0, len(lines) while i < n: m =[i]) if m is None: i += 1 continue # in case the function name is contained within brackets, e.g. (ps6000OpenUnit) lines[i] = _fcn_regex.split(lines[i])[-1] text, i = CHeader.get_text_between_brackets(lines, i, '(', ')') fcns[] = [self._convert_ctype(, self._split_datatype_name(text, ',')] i += 1 return fcns
[docs] def get_lines(self): """ Returns ------- :class:`list` of :class:`str` The lines in the C/C++ header file. """ return self._lines
[docs] def get_struct_imports(self): """ Returns ------- :class:`list` of :class:`str` The list of ``structs`` that must be imported for the C/C++ functions. Note ---- Must call :meth:`.functions` first. """ return self._struct_imports
[docs] @staticmethod def get_text_between_brackets(lines, index, bracket1, bracket2): """Get all the text (excluding comments) between two brackets. Parameters ---------- lines : :class:`list` of :class:`str` A list of lines, see :meth:`.get_lines`. index : :class:`int` The current index in `lines`. bracket1 : :class:`str` One of ``{``, ``(``, or ``[`` bracket2 : :class:`str` One of ``}``, ``)`` or ``]`` Returns ------- :class:`str` The text between `bracket1` and `bracket2`. :class:`int` The current index in `lines`. """ assert bracket1 in ('(', '{', '['), 'Invalid bracket1 "{}"'.format(bracket1) assert bracket2 in (')', '}', ']'), 'Invalid bracket2 "{}"'.format(bracket2) def _remove_comment(_line): # remove any comments that are on this line return _line.split('/')[0].strip() while bracket1 not in lines[index]: index += 1 text = _remove_comment(lines[index].split(bracket1)[1]) while True: if bracket2 in lines[index]: text = text.split(bracket2)[0].strip() break else: index += 1 text += _remove_comment(lines[index]) return text, index
def _get_enums(self): """Find the ``enums``""" self._enums = {} i, n = 0, len(self._lines) while i < n: m =[i]) if m is None: i += 1 continue _, enum_name, data_type = m.groups() if data_type is not None: data_type = data_type.replace(':', '').strip() data_type = CTYPES_MAP[data_type] else: data_type = 'c_int' # get all the text between { } text, i = CHeader.get_text_between_brackets(self._lines, i, '{', '}') if text.endswith(','): text = text[:-1] # determine if there is a naming convention m =[i]) alias = enum_name if m is None else # we now have a string with a "," separating the values members = {} is_hex = False auto_increment = 0 for item in text.split(','): item_split = item.split('=') name = item_split[0].strip() if name == 'None': name = 'NONE' if len(item_split) == 1: # then no value if is_hex: value = str(hex(auto_increment)) else: value = str(auto_increment) auto_increment += 1 else: value = item_split[1].strip() if value.lower().startswith('0x'): is_hex = True auto_increment = int(value, 16) + 1 else: try: auto_increment = int(value) + 1 except ValueError: pass # the value equals the name of a pre-defined value(s) members[name] = value self._enums[enum_name] = (alias, data_type, members) i += 1 def _get_structs(self): """Find the ``structs``""" self._structs = {} i, n = 0, len(self._lines) while i < n: m =[i]) if m is None: i += 1 continue text, i = CHeader.get_text_between_brackets(self._lines, i, '{', '}') self._structs[] = self._split_datatype_name(text, ';') i += 1 def _get_callbacks(self): """Find the ``callbacks``""" lines = list(self._lines) # create a copy, since it will be modified self._callbacks = {} i, n = 0, len(lines) while i < n: m =[i]) if m is None: i += 1 continue lines[i] = self._callback_regex.split(lines[i])[-1] text, i = CHeader.get_text_between_brackets(lines, i, '(', ')') self._callbacks[] = self._split_datatype_name(text, ',') i += 1 def _split_datatype_name(self, text, delimiter): """Splits a list of arguments into a (argument data type, argument name) Parameters ---------- text : :class:`str` The text from :meth:`.get_text_between_brackets`. delimiter : :class:`str` The delimiter to use to split the data type and argument name. Returns ------- :class:`list` of :class:`tup` A list of tuples [(argument data type, argument name), ... ]. """ if text.endswith(delimiter): text = text[:-1] args = text.split(delimiter) _special = ('**', '*', '&') fields = [] for item in args: item_split = item.split() if not item_split: fields.append(()) continue if len(item_split) == 1: field_type = item_split[0] field_name = 'UNKNOWN' else: field_type = ' '.join(item_split[:-1]) field_name = item_split[-1] for c in _special: if c in field_name: field_type += c field_name = field_name.replace(c, '') break field_type = field_type.replace(' *', '*') field_type = self._convert_ctype(field_type) if ('[' in field_name) and (']' in field_name): field_name, end = field_name.split('[') field_type += ' * ' + end.split(']')[0] fields.append((field_type, field_name)) return fields def _convert_ctype(self, c_type): """Convert a C data type to the appropriate ctype. Parameters ---------- c_type : :class:`str` The C/C++ data type. Returns ------- :class:`str` The appropriate Python representation of the data type. """ def _get_enum_dtype(tup): return 'c_int' if tup[1] is None else tup[1] dtype = c_type.replace('const', '').strip() ptr = None if dtype.endswith('**'): ptr = 'POINTER(POINTER({}))' dtype = dtype[:-2].strip() elif dtype.endswith('*') and ('void' not in dtype) and ('char' not in dtype): ptr = 'POINTER({})' dtype = dtype[:-1].strip() elif dtype.endswith('&'): ptr = 'POINTER({})' dtype = dtype[:-1].strip() if dtype in self._structs: self._struct_imports.append(dtype) if dtype in CTYPES_MAP: dtype = CTYPES_MAP[dtype] else: if dtype in self._enums: dtype = _get_enum_dtype(self._enums[dtype]) else: for item in self._enums.values(): if dtype in item[0]: dtype = _get_enum_dtype(item) break if ptr is not None: return ptr.format(dtype) else: return dtype