diff --git a/.travis.yml b/.travis.yml index 19726e0..e43342a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,11 @@ language: python +python: + - "2.7" + - "3.6" + +os: + - linux + # - osx install: - pip install -r requirements.txt diff --git a/cwrap/__init__.py b/cwrap/__init__.py index 602e0c2..5d5e4d5 100644 --- a/cwrap/__init__.py +++ b/cwrap/__init__.py @@ -31,23 +31,23 @@ try: from .version import version as __version__ except ImportError: __version__ = '0.0.0' -__author__ = 'Jean-Paul Balabanian, Joakim Hove, and PG Drange' +__author__ = 'Statoil ASA' __copyright__ = 'Copyright 2016, Statoil ASA' __credits__ = __author__ __license__ = 'GPL' __maintainer__ = __author__ -__email__ = __author__ +__email__ = 'fg_gpl@statoil.com' __status__ = 'Prototype' from .basecclass import BaseCClass from .basecenum import BaseCEnum from .basecvalue import BaseCValue -from .cfile import CFILE +from .cfile import CFILE, copen as open from .clib import load, lib_name from .metacwrap import MetaCWrap from .prototype import REGISTERED_TYPES, Prototype, PrototypeError -__all__ = ['BaseCClass', 'BaseCEnum', 'BaseCValue', 'CFILE', +__all__ = ['BaseCClass', 'BaseCEnum', 'BaseCValue', 'CFILE', 'open', 'MetaCWrap', 'Prototype', 'load', 'lib_name'] diff --git a/cwrap/cfile.py b/cwrap/cfile.py index 4b46d42..22f01fd 100644 --- a/cwrap/cfile.py +++ b/cwrap/cfile.py @@ -14,57 +14,159 @@ # See the GNU General Public License at # for more details. -import ctypes import six -from .prototype import Prototype, PrototypeError +import sys +from .prototype import Prototype from .basecclass import BaseCClass -class CFILE(BaseCClass): - """ - Utility class to map a Python file handle <-> FILE* in C - """ - TYPE_NAME = "FILE" - _as_file = Prototype(ctypes.pythonapi, "void* PyFile_AsFile(py_object)") +if six.PY2: + import ctypes - def __init__(self, py_file): + def copen(filename, mode='r'): """ - Takes a python file handle and looks up the underlying FILE * + This is a compatibility layer for functions taking FILE* pointers, and + should not be used unless absolutely needed. - The purpose of the CFILE class is to be able to use python - file handles when calling C functions which expect a FILE - pointer. A CFILE instance should be created based on the - Python file handle, and that should be passed to the function - expecting a FILE pointer. + In Python 2 this function is simply an alias for open. In Python 3, + however, it returns an instance of CWrapFile, a very light weight + wrapper around a FILE* instance. + """ + return open(filename, mode) + + class CFILE(BaseCClass): + """ + Utility class to map a Python file handle <-> FILE* in C + """ + TYPE_NAME = "FILE" + + _as_file = Prototype(ctypes.pythonapi, "void* PyFile_AsFile(py_object)") + + def __init__(self, py_file): + """ + Takes a python file handle and looks up the underlying FILE * + + The purpose of the CFILE class is to be able to use python + file handles when calling C functions which expect a FILE + pointer. A CFILE instance should be created based on the + Python file handle, and that should be passed to the function + expecting a FILE pointer. + + The implementation is based on the ctypes object + pythonapi which is ctypes wrapping of the CPython api. + + C-function: + void fprintf_hello(FILE * stream , const char * msg); + + Python wrapper: + lib = clib.load( "lib.so" ) + fprintf_hello = Prototype(lib, "void fprintf_hello( FILE , char* )") + + Python use: + py_fileH = open("file.txt" , "w") + fprintf_hello( CFILE( py_fileH ) , "Message ...") + py_fileH.close() + + If the supplied argument is not of type py_file the function + will raise a TypeException. + + Examples: ecl.ecl.ecl_kw.EclKW.fprintf_grdecl() + """ + c_ptr = self._as_file(py_file) + try: + super(CFILE, self).__init__(c_ptr) + except ValueError: + raise TypeError("Sorry - the supplied argument is not a valid " + " Python file handle!") + + self.py_file = py_file - The implementation is based on the ctypes object - pythonapi which is ctypes wrapping of the CPython api. + def __del__(self): + pass - C-function: - void fprintf_hello(FILE * stream , const char * msg); - Python wrapper: - lib = clib.load( "lib.so" ) - fprintf_hello = Prototype(lib, "void fprintf_hello( FILE , char* )") +if six.PY3: + from .clib import load as cwrapload - Python use: - py_fileH = open("file.txt" , "w") - fprintf_hello( CFILE( py_fileH ) , "Message ...") - py_fileH.close() + class LibcPrototype(Prototype): + lib = cwrapload(None) - If the supplied argument is not of type py_file the function - will raise a TypeException. + def __init__(self, prototype, bind=False, allow_attribute_error=False): + super(LibcPrototype, self).__init__( + LibcPrototype.lib, + prototype, + bind=bind, + allow_attribute_error=allow_attribute_error) - Examples: ecl.ecl.ecl_kw.EclKW.fprintf_grdecl() + def copen(filename, mode='r'): """ - c_ptr = self._as_file(py_file) - try: - super(CFILE, self).__init__(c_ptr) - except ValueError as e: - raise TypeError("Sorry - the supplied argument is not a valid Python file handle!") + This is a compatibility layer for functions taking FILE* pointers, and + should not be used unless absolutely needed. + + In Python 2 this function is simply an alias for open. In Python 3, + however, it returns an instance of CWrapFile, a very lightweight + wrapper around a FILE* instance. + """ + return CWrapFile(filename, mode) + + class CWrapFile(BaseCClass): + """ + This is a compatibility layer for functions taking FILE* pointers, and + should not be used unless absolutely needed. + + CWrapFile is a very lightweight wrapper around FILE* instances. It is + meant be used inplace of python file objects that are to be passed to + foreign function calls under python 3. + + Example: + with cwrap.open('filename', 'mode') as f: + foreign_function_call(f) + """ + + TYPE_NAME = "FILE" + + _fopen = LibcPrototype("void* fopen (char*, char*)") + _fclose = LibcPrototype("int fclose (FILE)", bind=True) + _fflush = LibcPrototype("int fflush (FILE)", bind=True) + + def __init__(self, fname, mode): + c_ptr = self._fopen(fname, mode) + self._mode = mode + self._fname = fname + self._closed = False + + try: + super(CWrapFile, self).__init__(c_ptr) + except ValueError: + self._closed = True + raise IOError('Could not open file "{}" in mode {}' + .format(fname, mode)) + + def close(self): + if not self._closed: + self._fflush() + cs = self._fclose() + if (cs != 0): + raise IOError("Failed to close file") + self._closed = True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return exc_type is None - self.py_file = py_file + def free(self): + self.close() + def __del__(self): + self.close() - def __del__(self): - pass + def CFILE(f): + if not isinstance(f, CWrapFile): + raise TypeError("This function requires the use of CWrapFile, " + "not {} when running Python 3. See " + "help(cwrap.open) for more info" + .format(type(f).__name__)) + return f diff --git a/cwrap/metacwrap.py b/cwrap/metacwrap.py index ea36538..289c47f 100644 --- a/cwrap/metacwrap.py +++ b/cwrap/metacwrap.py @@ -56,8 +56,3 @@ def __init__(cls, name, bases, attrs): if isinstance(attr, Prototype): attr.resolve() attr.__name__ = key - - if attr.shouldBeBound(): - method = MethodType(attr, None, cls) - #method = six.create_bound_method(attr, cls) - setattr(cls, key, method) diff --git a/cwrap/prototype.py b/cwrap/prototype.py index e82743c..e0255cb 100644 --- a/cwrap/prototype.py +++ b/cwrap/prototype.py @@ -17,27 +17,73 @@ import ctypes import inspect import re - +import six import sys +from types import MethodType + class TypeDefinition(object): - def __init__(self, type_class_or_function, is_return_type, storage_type): + def __init__(self, + type_class_or_function, + is_return_type, + storage_type, + errcheck): self.storage_type = storage_type self.is_return_type = is_return_type self.type_class_or_function = type_class_or_function + # Note that errcheck (python name) can do more than error checking. See + # toStr in the CStringHelper class. + self.errcheck = errcheck + + +if six.PY3: + class CStringHelper(object): + @classmethod + def from_param(cls, value): + if value is None: + return None + elif isinstance(value, bytes): + return value + elif isinstance(value, ctypes.Array): + return value + else: + e = value.encode() + return e + + @staticmethod + def toStr(result, func, arguments): + """ + Transform a foreign char* type to str (if python 3). + + ctypes functions have an attribute called errcheck that can not + only be used for error checking but also to alter the result. If + errcheck is defined, the value returned from that function is what + is returned from the foreign function call. In this case, C returns + strings in the form of zero-terminated char* strings. To use these + as python strings (str) they must be decoded. + """ + if result is None or result == 0: + return None + return result.decode() REGISTERED_TYPES = {} """:type: dict[str,TypeDefinition]""" -def _registerType(type_name, type_class_or_function, is_return_type=True, storage_type=None): +def _registerType(type_name, + type_class_or_function, + is_return_type=True, + storage_type=None, + errcheck=None): if type_name in REGISTERED_TYPES: raise PrototypeError("Type: '%s' already registered!" % type_name) - REGISTERED_TYPES[type_name] = TypeDefinition(type_class_or_function, is_return_type, storage_type) + REGISTERED_TYPES[type_name] = TypeDefinition(type_class_or_function, + is_return_type, + storage_type, + errcheck) - # print("Registered: %s for class: %s" % (type_name, repr(type_class_or_function))) _registerType("void", None) _registerType("void*", ctypes.c_void_p) @@ -54,15 +100,25 @@ def _registerType(type_name, type_class_or_function, is_return_type=True, storag _registerType("long", ctypes.c_long) _registerType("long*", ctypes.POINTER(ctypes.c_long)) _registerType("char", ctypes.c_char) -_registerType("char*", ctypes.c_char_p) -_registerType("char**", ctypes.POINTER(ctypes.c_char_p)) +if six.PY2: + _registerType("char*", ctypes.c_char_p) + _registerType("char**", ctypes.POINTER(ctypes.c_char_p)) +if six.PY3: + _registerType( + "char*", + CStringHelper, + storage_type=ctypes.c_char_p, + errcheck=CStringHelper.toStr) _registerType("float", ctypes.c_float) _registerType("float*", ctypes.POINTER(ctypes.c_float)) _registerType("double", ctypes.c_double) _registerType("double*", ctypes.POINTER(ctypes.c_double)) _registerType("py_object", ctypes.py_object) -PROTOTYPE_PATTERN = "(?P[a-zA-Z][a-zA-Z0-9_*]*) +(?P[a-zA-Z]\w*) *[(](?P[a-zA-Z0-9_*, ]*)[)]" +PROTOTYPE_PATTERN = ("(?P[a-zA-Z][a-zA-Z0-9_*]*)" + " +(?P[a-zA-Z]\w*)" + " *[(](?P[a-zA-Z0-9_*, ]*)[)]") + class PrototypeError(Exception): pass @@ -87,7 +143,9 @@ def _parseType(self, type_name): if type_name in REGISTERED_TYPES: type_definition = REGISTERED_TYPES[type_name] - return type_definition.type_class_or_function, type_definition.storage_type + return (type_definition.type_class_or_function, + type_definition.storage_type, + type_definition.errcheck) raise ValueError("Unknown type: %s" % type_name) @@ -115,14 +173,14 @@ def resolve(self): if not restype in REGISTERED_TYPES or not REGISTERED_TYPES[restype].is_return_type: sys.stderr.write("The type used as return type: %s is not registered as a return type.\n" % restype) - return_type = self._parseType(restype) + return_type, storage_type, errcheck = self._parseType(restype) if inspect.isclass(return_type): sys.stderr.write(" Correct type may be: %s_ref or %s_obj.\n" % (restype, restype)) return None - return_type, storage_type = self._parseType(restype) + return_type, storage_type, errcheck = self._parseType(restype) func.restype = return_type @@ -134,6 +192,9 @@ def returnFunction(result, func, arguments): func.errcheck = returnFunction + if errcheck is not None: + func.errcheck = errcheck + if len(arguments) == 1 and arguments[0].strip() == "": func.argtypes = [] else: @@ -144,20 +205,29 @@ def returnFunction(result, func, arguments): self._func = func - def __call__(self, *args): if not self._resolved: self.resolve() self._resolved = True - if self._func is None: if self._allow_attribute_error: raise NotImplementedError("Function:%s has not been properly resolved" % self.__name__) else: raise PrototypeError("Prototype has not been properly resolved") - return self._func(*args) + def __get__(self, instance, owner): + if not self._resolved: + self.resolve() + self._resolved = True + if self.shouldBeBound(): + if six.PY2: + return MethodType(self, instance, owner) + if six.PY3: + return MethodType(self, instance) + else: + return self + def __repr__(self): bound = "" if self.shouldBeBound(): diff --git a/tests/test_basecclass.py b/tests/test_basecclass.py index e2430f0..3190946 100644 --- a/tests/test_basecclass.py +++ b/tests/test_basecclass.py @@ -5,9 +5,6 @@ class BaseCClassTest(unittest.TestCase): - def test_none_assertion(self): - self.assertFalse(None > 0) - def test_creation(self): with self.assertRaises(ValueError): obj = BaseCClass(0) diff --git a/tests/test_cfile.py b/tests/test_cfile.py index ffd9c71..f3d871e 100644 --- a/tests/test_cfile.py +++ b/tests/test_cfile.py @@ -4,7 +4,7 @@ import unittest -from cwrap import Prototype, CFILE, load +from cwrap import Prototype, CFILE, load, open as copen # Local copies so that the real ones don't get changed class TestUtilPrototype(Prototype): @@ -25,15 +25,15 @@ def test_cfile(self): with open("test", "w") as f: f.write("some content") - with open("test", "r") as f: + with copen("test", "r") as f: cfile = CFILE(f) - self.assertEqual(fileno(cfile), f.fileno()) + self.assertTrue(fileno(cfile)) os.chdir(cwd) shutil.rmtree( d ) - + def test_cfile_error(self): with self.assertRaises(TypeError): cfile = CFILE("some text") diff --git a/tests/test_libc.py b/tests/test_libc.py index 356070b..2ae1edc 100644 --- a/tests/test_libc.py +++ b/tests/test_libc.py @@ -1,25 +1,30 @@ -import ctypes import unittest from cwrap import BaseCClass, Prototype, load class LibCPrototype(Prototype): - lib = load( None ) + lib = load(None) - def __init__(self , prototype , bind = False, allow_attribute_error = False): - super(LibCPrototype , self).__init__( LibCPrototype.lib , prototype , bind = bind, allow_attribute_error = allow_attribute_error) + def __init__(self, prototype, bind=False, allow_attribute_error=False): + super(LibCPrototype, self).__init__( + LibCPrototype.lib, + prototype, + bind=bind, + allow_attribute_error=allow_attribute_error) class LibC(BaseCClass): TYPE_NAME = "void_libc_none" _malloc = LibCPrototype("void* malloc(void*)") - _abs = LibCPrototype("int abs(int)") - _atoi = LibCPrototype("int atoi(char*)") - _free = LibCPrototype("void free(void*)") - _missing_function = LibCPrototype("void missing_function(int*)", allow_attribute_error = True) + _abs = LibCPrototype("int abs(int)") + _atoi = LibCPrototype("int atoi(char*)") + _strchr = LibCPrototype("char* strchr(char*, int)") + _free = LibCPrototype("void free(void*)") + _missing_function = LibCPrototype("void missing_function(int*)", + allow_attribute_error=True) def __init__(self): - c_ptr = 1#c_ptr = self._malloc(4) + c_ptr = 1 # c_ptr = self._malloc(4) super(LibC, self).__init__(c_ptr) def abs(self, x): @@ -28,18 +33,24 @@ def abs(self, x): def atoi(self, s): return self._atoi(s) + def strchr(self, s, t): + return self._strchr(s, t) + def free(self): - pass#self._free(self.__c_pointer) + pass # self._free(self.__c_pointer) + class LibCTest(unittest.TestCase): def test_libc(self): - lib = LibC( ) - self.assertEqual( lib.abs(-3) , 3 ) - self.assertEqual( lib.abs(0) , 0 ) - self.assertEqual( lib.abs(42) , 42 ) - self.assertEqual( lib.atoi("12") , 12) - self.assertEqual( lib.atoi("-100") , -100) + lib = LibC() + self.assertEqual(lib.abs(-3), 3) + self.assertEqual(lib.abs(0), 0) + self.assertEqual(lib.abs(42), 42) + self.assertEqual(lib.atoi("12"), 12) + self.assertEqual(lib.atoi("-100"), -100) + self.assertEqual(lib.strchr("a,b", ord(",")), ",b") + self.assertEqual(lib.strchr("a,b", ord("x")), None) with self.assertRaises(NotImplementedError): - lib._missing_function( 100 ) + lib._missing_function(100)