diff --git a/CHANGES b/CHANGES index 9a5d7ca..014550b 100644 --- a/CHANGES +++ b/CHANGES @@ -1,10 +1,17 @@ Change log ========== -1.9.99 (2012-10-25) +2.0.0 (2019-11-01) +------------------ + +- Documentation +- macOS rpath fix/show scripts +- Tables and structure parameters type check + +1.9.99 (2019-10-25) ------------------- -- Python 3.8 wheels added, 3.6 and 3.5 dropped +- Python 3.8 wheels added, 3,6, 3.5 dropped - Datatypes checks and unit test, fix #91, #120 - Connection info request when disconnected, does not open the connection any more - Connection and RFM call parameters check diff --git a/VERSION b/VERSION index 9b2dbda..9e11c3b 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.99 +2.0.0 diff --git a/tests/nwrfcsdk-version-darwin.sh b/ci/utils/nwrfcsdk-version-darwin.sh similarity index 100% rename from tests/nwrfcsdk-version-darwin.sh rename to ci/utils/nwrfcsdk-version-darwin.sh diff --git a/tests/nwrfcsdk-version-linux.sh b/ci/utils/nwrfcsdk-version-linux.sh similarity index 100% rename from tests/nwrfcsdk-version-linux.sh rename to ci/utils/nwrfcsdk-version-linux.sh diff --git a/tests/nwrfcsdk-version.bat b/ci/utils/nwrfcsdk-version.bat similarity index 100% rename from tests/nwrfcsdk-version.bat rename to ci/utils/nwrfcsdk-version.bat diff --git a/ci/utils/fix_paths.sh b/ci/utils/paths_fix.sh similarity index 100% rename from ci/utils/fix_paths.sh rename to ci/utils/paths_fix.sh diff --git a/ci/utils/show_paths.sh b/ci/utils/paths_show.sh similarity index 100% rename from ci/utils/show_paths.sh rename to ci/utils/paths_show.sh diff --git a/setup.py b/setup.py index 7217931..9418935 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def _read(name): if sys.platform.startswith("linux"): - subprocess.call("./tests/nwrfcsdk-version-linux.sh", shell=True) + subprocess.call("./ci/utils/nwrfcsdk-version-linux.sh", shell=True) LIBS = ["sapnwrfc", "sapucum"] MACROS = [ ("NDEBUG", None), @@ -59,7 +59,7 @@ def _read(name): ] LINK_ARGS = ["-L{}/lib".format(SAPNWRFC_HOME)] elif sys.platform.startswith("win"): - subprocess.call("tests\\nwrfcsdk-version.bat", shell=True) + subprocess.call("ci\\utils\\nwrfcsdk-version.bat", shell=True) LIBS = ["sapnwrfc", "libsapucum"] MACROS = [ ("_LARGEFILE_SOURCE", None), @@ -81,7 +81,7 @@ def _read(name): "-LIBPATH:{}\\PCbuild".format(PYTHONSOURCE), ] elif sys.platform.startswith("darwin"): - subprocess.call("./tests/nwrfcsdk-version-darwin.sh", shell=True) + subprocess.call("./ci/utils/nwrfcsdk-version-darwin.sh", shell=True) MACOS_VERSION_MIN = "10.10" # unicode paths fix # https://apple.stackexchange.com/questions/337940/why-is-usr-include-missing-i-have-xcode-and-command-line-tools-installed-moja diff --git a/src/pyrfc/__init__.py b/src/pyrfc/__init__.py index 5ee2083..0d24ab7 100755 --- a/src/pyrfc/__init__.py +++ b/src/pyrfc/__init__.py @@ -18,7 +18,8 @@ # Set DLL path, due to https://docs.python.org/3.8/whatsnew/3.8.html#bpo-36085-whatsnew import os -if os.name == 'nt': + +if os.name == "nt": try: os.add_dll_directory(os.path.join(os.environ["SAPNWRFC_HOME"], "lib")) except Exception: @@ -46,4 +47,4 @@ __author__ = """"Srdjan Boskovic""" __email__ = "srdjan.boskovic@sap.com" -__version__ = "1.9.99" +__version__ = "2.0.0" diff --git a/src/pyrfc/_pyrfc.pyx b/src/pyrfc/_pyrfc.pyx index 9753d68..86842ac 100755 --- a/src/pyrfc/_pyrfc.pyx +++ b/src/pyrfc/_pyrfc.pyx @@ -387,7 +387,8 @@ cdef class Connection: cdef unsigned paramCount cdef SAP_UC *cName if type(func_name) is not str: - raise RFCError("Remote function module name must be a string, received:", func_name) + if type(func_name) is not unicode: + raise RFCError("Remote function module name must be unicode string, received:", func_name, type(func_name)) cdef SAP_UC *funcName = fillString(func_name) if not self.alive: self._open() @@ -1686,14 +1687,19 @@ cdef fillVariable(RFCTYPE typ, RFC_FUNCTION_HANDLE container, SAP_UC* cName, val cdef RFC_TABLE_HANDLE table cdef SAP_UC* cValue cdef SAP_RAW* bValue + #print ("fill", wrapString(cName), value) try: if typ == RFCTYPE_STRUCTURE: + if type(value) is not dict: + raise TypeError('dictionary required for structure parameter, received', str(type(value))) rc = RfcGetStructure(container, cName, &struct, &errorInfo) if rc != RFC_OK: raise wrapError(&errorInfo) for name, value in value.iteritems(): fillStructureField(typeDesc, struct, name, value) elif typ == RFCTYPE_TABLE: + if type(value) is not list: + raise TypeError('list required for table parameter, received', str(type(value))) rc = RfcGetTable(container, cName, &table, &errorInfo) if rc != RFC_OK: raise wrapError(&errorInfo) @@ -1708,13 +1714,13 @@ cdef fillVariable(RFCTYPE typ, RFC_FUNCTION_HANDLE container, SAP_UC* cName, val free(bValue) elif typ == RFCTYPE_CHAR: if type(value) is not str and type(value) is not unicode: - raise TypeError('an string is required, received', value, 'of type:', str(type(value))) + raise TypeError('an string is required, received', value, 'of type', str(type(value))) cValue = fillString(value) rc = RfcSetChars(container, cName, cValue, strlenU(cValue), &errorInfo) free(cValue) elif typ == RFCTYPE_STRING: if type(value) is not str and type(value) is not unicode: - raise TypeError('an string is required, received', value, 'of type:', str(type(value))) + raise TypeError('an string is required, received', value, 'of type', str(type(value))) cValue = fillString(value) rc = RfcSetString(container, cName, cValue, strlenU(cValue), &errorInfo) free(cValue) @@ -1722,7 +1728,7 @@ cdef fillVariable(RFCTYPE typ, RFC_FUNCTION_HANDLE container, SAP_UC* cName, val try: Decimal(value) except: - raise TypeError('a decimal value is required, received', value) + raise TypeError('a decimal value is required, received', value, 'of type', str(type(value))) cValue = fillString(value) rc = RfcSetNum(container, cName, cValue, strlenU(cValue), &errorInfo) free(cValue) @@ -1731,49 +1737,61 @@ cdef fillVariable(RFCTYPE typ, RFC_FUNCTION_HANDLE container, SAP_UC* cName, val try: Decimal(value) except: - raise TypeError('a decimal value is required, received', value) + raise TypeError('a decimal value required, received', value, 'of type', str(type(value))) cValue = fillString(str(value)) rc = RfcSetString(container, cName, cValue, strlenU(cValue), &errorInfo) free(cValue) elif typ in (RFCTYPE_INT, RFCTYPE_INT1, RFCTYPE_INT2): if type(value) is not int: - raise TypeError('an integer is required, received', value) + raise TypeError('an integer required, received', value, 'of type', str(type(value))) rc = RfcSetInt(container, cName, value, &errorInfo) elif typ == RFCTYPE_INT8: if type(value) is not int: - raise TypeError('an integer is required, received', value) + raise TypeError('an integer required, received', value, 'of type', str(type(value))) rc = RfcSetInt8(container, cName, value, &errorInfo) elif typ == RFCTYPE_DATE: if (value): # not None or empty - try: - if type(value) is datetime.date: - cValue = fillString('{:04d}{:02d}{:02d}'.format(value.year, value.month, value.day)) - else: - # raise len error - if len(value) != 8: raise - # raise type or out of range error - datetime.date(int(value[:4]), int(value[4:6]), int(value[6:8])) - cValue = fillString(value) - except: - raise TypeError('a date value required, received', value) + format_ok = True + if type(value) is datetime.date: + cValue = fillString('{:04d}{:02d}{:02d}'.format(value.year, value.month, value.day)) + else: + try: + if len(value) != 8: + format_ok = False + else: + if len(value.rstrip()) > 0: + datetime.date(int(value[:4]), int(value[4:6]), int(value[6:8])) + cValue = fillString(value) + except: + format_ok = False + if not format_ok: + raise TypeError('date value required, received', value, 'of type', str(type(value))) rc = RfcSetDate(container, cName, cValue, &errorInfo) free(cValue) + else: + rc = RFC_OK elif typ == RFCTYPE_TIME: if (value): # not None or empty - try: - if type(value) is datetime.time: - cValue = fillString('{:02d}{:02d}{:02d}'.format(value.hour, value.minute, value.second)) - else: - # raise len error - if len(value) != 6: raise - # raise type or out of range error - datetime.time(int(value[:2]), int(value[2:4]), int(value[4:6])) - cValue = fillString(value) - except: - #raise RFCError('Invalid time value when filling %s: %s' % (wrapString(cName), str(value))) - raise TypeError('a time value required, received', value) + format_ok = True + if type(value) is datetime.time: + cValue = fillString('{:02d}{:02d}{:02d}'.format(value.hour, value.minute, value.second)) + else: + try: + if len(value) != 6: + format_ok = False + else: + if len(value.rstrip()) > 0: + datetime.time(int(value[:2]), int(value[2:4]), int(value[4:6])) + cValue = fillString(value) + except: + format_ok = False + + if not format_ok: + raise TypeError('time value required, received', value, 'of type', str(type(value))) rc = RfcSetTime(container, cName, cValue, &errorInfo) free(cValue) + else: + rc = RFC_OK else: raise RFCError('Unknown RFC type %d when filling %s' % (typ, wrapString(cName))) except TypeError as e: diff --git a/tests/test_connection.py b/tests/test_connection.py index 0837a0e..fe2546c 100755 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -134,10 +134,24 @@ def test_ping(self): assert error["key"] == "RFC_INVALID_HANDLE" assert error["message"][0] == "An invalid handle was passed to the API call" - def test_STFC_returns_unicode(self): + def test_RFM_name_string(self): result = self.conn.call("STFC_CONNECTION", REQUTEXT=UNICODETEST) assert result["ECHOTEXT"] == UNICODETEST + def test_RFM_name_unicode(self): + result = self.conn.call(u"STFC_CONNECTION", REQUTEXT=UNICODETEST) + assert result["ECHOTEXT"] == UNICODETEST + + def test_RFM_name_invalid_type(self): + try: + self.conn.call(123) + except Exception as ex: + assert ex.args == ( + "Remote function module name must be unicode string, received:", + 123, + int, + ) + def test_STFC_returns_structure_and_table(self): IMPORTSTRUCT = { "RFCFLOAT": 1.23456789, diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 33fd66a..57af745 100755 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -37,6 +37,33 @@ client = Connection(**CONNECTION_INFO) +def test_structure_rejects_non_dict(): + try: + IMPORTSTRUCT = {"RFCINT1": "1"} + + output = client.call("STFC_STRUCTURE", IMPORTSTRUCT=[IMPORTSTRUCT]) + except Exception as ex: + assert type(ex) is TypeError + assert ex.args[0] == 'dictionary required for structure parameter, received' + if sys.version > "3.0": + assert ex.args[1] == "" + else: + assert ex.args[1] == "" + assert ex.args[2] == 'IMPORTSTRUCT' + +def test_table_rejects_non_list(): + try: + IMPORTSTRUCT = {"RFCINT1": "1"} + + output = client.call("STFC_STRUCTURE", RFCTABLE=IMPORTSTRUCT) + except Exception as ex: + assert type(ex) is TypeError + assert ex.args[0] == 'list required for table parameter, received' + if sys.version > "3.0": + assert ex.args[1] == "" + else: + assert ex.args[1] == "" + assert ex.args[2] == 'RFCTABLE' def test_basic_datatypes(): INPUTS = [ @@ -316,13 +343,22 @@ def test_raw_types_accept_bytearray(): def test_date_time(): DATETIME_TEST = [ - {"RFCDATE": "20161231", "RFCTIME": "123456"}, # good + {"RFCDATE": "20161231", "RFCTIME": "123456"}, # good, correct date + {"RFCDATE": "", "RFCTIME": "123456"}, # good, empty date + {"RFCDATE": " ", "RFCTIME": "123456"}, # good, space date + {"RFCDATE": "20161231", "RFCTIME": ""}, # good, empty time + {"RFCDATE": "20161231", "RFCTIME": " "}, # good, space time + {"RFCDATE": "20161231", "RFCTIME": "000000"}, # good, zero time {"RFCDATE": "2016123", "RFCTIME": "123456"}, # shorter date + {"RFCDATE": " ", "RFCTIME": "123456"}, # shorter empty date {"RFCDATE": "201612311", "RFCTIME": "123456"}, # longer date + {"RFCDATE": " ", "RFCTIME": "123456"}, # longer empty date {"RFCDATE": "20161232", "RFCTIME": "123456"}, # out of range date {"RFCDATE": 20161231, "RFCTIME": "123456"}, # wrong date type {"RFCDATE": "20161231", "RFCTIME": "12345"}, # shorter time + {"RFCDATE": "20161231", "RFCTIME": " "}, # shorter empty time {"RFCDATE": "20161231", "RFCTIME": "1234566"}, # longer time + {"RFCDATE": "20161231", "RFCTIME": " "}, # longer empty time {"RFCDATE": "20161231", "RFCTIME": "123466"}, # out of range time {"RFCDATE": "20161231", "RFCTIME": 123456}, # wrong time type ] @@ -332,24 +368,24 @@ def test_date_time(): try: result = client.call("STFC_STRUCTURE", IMPORTSTRUCT=dt)["ECHOSTRUCT"] assert dt["RFCDATE"] == result["RFCDATE"] - assert dt["RFCTIME"] == result["RFCTIME"] - assert counter == 1 + if dt["RFCTIME"] == "": + assert "000000" == result["RFCTIME"] + else: + assert dt["RFCTIME"] == result["RFCTIME"] except Exception as e: assert type(e) is TypeError - if counter < 6: - assert e.args == ( - "a date value required, received", - dt["RFCDATE"], - "RFCDATE", - "IMPORTSTRUCT", - ) + + if counter < 13: + assert e.args[0] == "date value required, received" + assert e.args[1] == dt["RFCDATE"] + assert e.args[3] == str(type(dt["RFCDATE"])) + assert e.args[4] == "RFCDATE" else: - assert e.args == ( - "a time value required, received", - dt["RFCTIME"], - "RFCTIME", - "IMPORTSTRUCT", - ) + assert e.args[0] == "time value required, received" + assert e.args[1] == dt["RFCTIME"] + assert e.args[3] == str(type(dt["RFCTIME"])) + assert e.args[4] == "RFCTIME" + assert e.args[5] == "IMPORTSTRUCT" def test_date_accepts_string(): @@ -426,106 +462,133 @@ def test_time_accepts_time(): def test_error_int_rejects_string(): IMPORTSTRUCT = {"RFCINT1": "1"} - RFCTABLE = [IMPORTSTRUCT] try: - output = client.call( - "STFC_STRUCTURE", IMPORTSTRUCT=IMPORTSTRUCT, RFCTABLE=RFCTABLE - ) + output = client.call("STFC_STRUCTURE", IMPORTSTRUCT=IMPORTSTRUCT) except Exception as ex: assert type(ex) is TypeError + assert ex.args[0] =="an integer required, received" + assert ex.args[1] == IMPORTSTRUCT["RFCINT1"] if sys.version > "3.0": - assert ex.args == ( - "an integer is required, received", - "1", - "RFCINT1", - "IMPORTSTRUCT", - ) + assert ex.args[3] == "" else: - assert ex.args == ( - "an integer is required, received", - "1", - "RFCINT1", - "RFCTABLE", - ) - + assert ex.args[3] == "" + assert ex.args[4] =="RFCINT1" + assert ex.args[5] =="IMPORTSTRUCT" + try: + output = client.call("STFC_STRUCTURE", RFCTABLE=[IMPORTSTRUCT]) + except Exception as ex: + assert type(ex) is TypeError + assert type(ex) is TypeError + assert ex.args[0] =="an integer required, received" + assert ex.args[1] == IMPORTSTRUCT["RFCINT1"] + if sys.version > "3.0": + assert ex.args[3] == "" + else: + assert ex.args[3] == "" + assert ex.args[4] =="RFCINT1" + assert ex.args[5] =="RFCTABLE" def test_error_int_rejects_float(): IMPORTSTRUCT = {"RFCINT1": 1.0} - RFCTABLE = [IMPORTSTRUCT] try: - output = client.call( - "STFC_STRUCTURE", IMPORTSTRUCT=IMPORTSTRUCT, RFCTABLE=RFCTABLE - ) + output = client.call("STFC_STRUCTURE", IMPORTSTRUCT=IMPORTSTRUCT) + except Exception as ex: + assert type(ex) is TypeError + assert ex.args[0] =="an integer required, received" + assert ex.args[1] == IMPORTSTRUCT["RFCINT1"] + if sys.version > "3.0": + assert ex.args[3] == "" + else: + assert ex.args[3] == "" + assert ex.args[4] =="RFCINT1" + assert ex.args[5] =="IMPORTSTRUCT" + + try: + output = client.call("STFC_STRUCTURE", RFCTABLE=[IMPORTSTRUCT]) except Exception as ex: assert type(ex) is TypeError + assert ex.args[0] =="an integer required, received" + assert ex.args[1] == IMPORTSTRUCT["RFCINT1"] if sys.version > "3.0": - assert ex.args == ( - "an integer is required, received", - 1.0, - "RFCINT1", - "IMPORTSTRUCT", - ) + assert ex.args[3] == "" else: - assert ex.args == ( - "an integer is required, received", - 1.0, - "RFCINT1", - "RFCTABLE", - ) + assert ex.args[3] == "" + assert ex.args[4] =="RFCINT1" + assert ex.args[5] =="RFCTABLE" -def test_error_string_rejects_int(): +def test_error_string_rejects_None(): IMPORTSTRUCT = {"RFCCHAR4": None} - RFCTABLE = [IMPORTSTRUCT] try: - output = client.call( - "STFC_STRUCTURE", IMPORTSTRUCT=IMPORTSTRUCT, RFCTABLE=RFCTABLE - ) + output = client.call("STFC_STRUCTURE", IMPORTSTRUCT=IMPORTSTRUCT) + except Exception as ex: + assert type(ex) is TypeError + assert ex.args[0] =="an string is required, received" + assert ex.args[1] == IMPORTSTRUCT["RFCCHAR4"] + if sys.version > "3.0": + assert ex.args[3] == "" + else: + assert ex.args[3] == "" + assert ex.args[4] =="RFCCHAR4" + assert ex.args[5] =="IMPORTSTRUCT" + + + try: + output = client.call("STFC_STRUCTURE", RFCTABLE=[IMPORTSTRUCT]) + except Exception as ex: + assert type(ex) is TypeError + assert ex.args[0] =="an string is required, received" + assert ex.args[1] == IMPORTSTRUCT["RFCCHAR4"] + if sys.version > "3.0": + assert ex.args[3] == "" + else: + assert ex.args[3] == "" + assert ex.args[4] =="RFCCHAR4" + assert ex.args[5] =="RFCTABLE" + + +def test_error_string_rejects_int(): + IMPORTSTRUCT = {"RFCCHAR4": 1} + try: + output = client.call("STFC_STRUCTURE", IMPORTSTRUCT=IMPORTSTRUCT) except Exception as ex: assert type(ex) is TypeError + assert ex.args[0] =="an string is required, received" + assert ex.args[1] == IMPORTSTRUCT["RFCCHAR4"] if sys.version > "3.0": - assert ex.args == ( - "an string is required, received", - None, - "of type:", - "", - "RFCCHAR4", - "IMPORTSTRUCT", - ) + assert ex.args[3] == "" else: - assert ex.args == ( - "an string is required, received", - None, - "of type:", - "", - "RFCCHAR4", - "RFCTABLE", - ) + assert ex.args[3] == "" + assert ex.args[4] =="RFCCHAR4" + assert ex.args[5] =="IMPORTSTRUCT" + try: + output = client.call("STFC_STRUCTURE", RFCTABLE=[IMPORTSTRUCT]) + except Exception as ex: + assert type(ex) is TypeError + assert ex.args[0] =="an string is required, received" + assert ex.args[1] == IMPORTSTRUCT["RFCCHAR4"] + if sys.version > "3.0": + assert ex.args[3] == "" + else: + assert ex.args[3] == "" + assert ex.args[4] =="RFCCHAR4" + assert ex.args[5] =="RFCTABLE" def test_float_rejects_not_a_number_string(): IMPORTSTRUCT = {"RFCFLOAT": "A"} - RFCTABLE = [IMPORTSTRUCT] try: - output = client.call( - "STFC_STRUCTURE", IMPORTSTRUCT=IMPORTSTRUCT, RFCTABLE=RFCTABLE - ) + output = client.call("STFC_STRUCTURE", IMPORTSTRUCT=IMPORTSTRUCT) except Exception as ex: assert type(ex) is TypeError + assert ex.args[0] =="a decimal value required, received" + assert ex.args[1] == IMPORTSTRUCT["RFCFLOAT"] if sys.version > "3.0": - assert ex.args == ( - "a decimal value is required, received", - "A", - "RFCFLOAT", - "IMPORTSTRUCT", - ) + assert ex.args[3] == "" else: - assert ex.args == ( - "a decimal value is required, received", - "A", - "RFCFLOAT", - "RFCTABLE", - ) + assert ex.args[3] == "" + assert ex.args[4] =="RFCFLOAT" + assert ex.args[5] =="IMPORTSTRUCT" def test_bcd_rejects_not_a_number_string(): @@ -537,13 +600,14 @@ def test_bcd_rejects_not_a_number_string(): ] except Exception as ex: assert type(ex) is TypeError - assert ex.args == ( - "a decimal value is required, received", - "A", - "ZDEC", - "IS_INPUT", - ) - + assert ex.args[0] =="a decimal value required, received" + assert ex.args[1] == IS_INPUT["ZDEC"] + if sys.version > "3.0": + assert ex.args[3] == "" + else: + assert ex.args[3] == "" + assert ex.args[4] =="ZDEC" + assert ex.args[5] =="IS_INPUT" client.close() diff --git a/tests/test_errors.py b/tests/test_errors.py index 0ab4954..5786737 100755 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -76,8 +76,9 @@ def test_call_non_string_RFM_name(self): self.conn.call(1) except pyrfc.RFCError as ex: assert ex.args == ( - "Remote function module name must be a string, received:", + "Remote function module name must be unicode string, received:", 1, + int, ) def test_call_non_existing_RFM_parameter(self):