diff --git a/JsonPreprocessor/CJsonPreprocessor.py b/JsonPreprocessor/CJsonPreprocessor.py index 66cdd427..c37b7daf 100644 --- a/JsonPreprocessor/CJsonPreprocessor.py +++ b/JsonPreprocessor/CJsonPreprocessor.py @@ -202,7 +202,7 @@ def __init__(self, syntax: CSyntaxType = CSyntaxType.python , currentCfg : dict self.lNestedParams = [] self.lDotInParamName = [] - def __reset(self) -> None: + def __reset(self, bCleanGlobalVars : bool = False) -> None: ''' Reset initial variables which are set in constructor method after master Json file is loaded. ''' @@ -212,6 +212,21 @@ def __reset(self) -> None: self.dUpdatedParams = {} self.lNestedParams = [] self.lDotInParamName = [] + if bCleanGlobalVars: + lGlobalVars = [] + for name, value in globals().items(): + if re.match("^__.+$", name): + continue + else: + if isinstance(value, str) or isinstance(value, int) or \ + isinstance(value, float) or isinstance(value, dict) or \ + isinstance(value, list) or isinstance(value, type(None)): + lGlobalVars.append(name) + for var in lGlobalVars: + try: + del globals()[var] + except: + pass def __processImportFiles(self, input_data : dict) -> dict: ''' @@ -244,6 +259,7 @@ def __processImportFiles(self, input_data : dict) -> dict: # length of lImportedFiles should equal to recursive_level self.lImportedFiles = self.lImportedFiles[:self.recursive_level] if abs_path_file in self.lImportedFiles: + self.__reset(bCleanGlobalVars=True) raise Exception(f"Cyclic imported json file '{abs_path_file}'!") oJsonImport = self.jsonLoad(abs_path_file, masterFile=False) @@ -361,12 +377,13 @@ def __referVarHandle(referVar : str, sInputStr : str) -> str: dotdictVariable = re.sub('\$\${\s*(.*?)\s*}', '\\1', referVar) lDotdictVariable = dotdictVariable.split(".") lParams = self.__handleDotdictFormat(lDotdictVariable, []) - sParam = '$${' + lParams[0] + '}' + rootElement = lParams[0] + sParam = '$${' + rootElement + '}' lParams.pop(0) for item in lParams: sParam = sParam + "[" + item + "]" if re.match("^\d+$", item) else sParam + "['" + item + "']" - sInputStr = re.sub('\$\${\s*([^\}]*)\s*}', sParam, sInputStr) - referVar = re.findall('(\$\${\s*.*?\s*})', sInputStr)[0] + sInputStr = re.sub(referVar.replace("$", "\$"), sParam, sInputStr) + referVar = '$${' + rootElement + '}' tmpReferVar = re.sub("\$", "\\$", referVar) pattern = '(' + tmpReferVar + '\s*(\[+\s*.*?\s*\]+)*)' variable = re.search(pattern, sInputStr) @@ -376,6 +393,8 @@ def __referVarHandle(referVar : str, sInputStr : str) -> str: referVars = re.findall("(" + pattern + ")", sInputStr) while sInputStr.count("$$") > len(referVars): for var in referVars: + if var not in sInputStr: + continue if "." in var: ddVar = re.sub('\$\${\s*(.*?)\s*}', '\\1', var) lddVar = ddVar.split(".") @@ -398,7 +417,12 @@ def __referVarHandle(referVar : str, sInputStr : str) -> str: exec(sExec, globals(), ldict) tmpValue = ldict['value'] except: + self.__reset(bCleanGlobalVars=True) raise Exception(f"The variable '{var.replace('$$', '$')}' is not available!") + if bKey and (isinstance(tmpValue, list) or isinstance(tmpValue, dict)): + self.__reset(bCleanGlobalVars=True) + raise Exception(f"Overwrite the element '{sInputStr.replace('$$', '$')}' failed! \ +Due to the datatype of '{sVar.replace('$$', '$')}' is '{type(tmpValue)}'. Only simple data types are allowed to be substituted inside.") subPattern = "(" + tmpVar + "(\[\s*'[^\$\[\]\(\)]+'\s*\]|\[\s*\d+\s*\])*)" var = re.sub("\$", "\\$", re.search(subPattern, sInputStr).group(1)) if re.search("\[.+\]", var): @@ -419,8 +443,7 @@ def __referVarHandle(referVar : str, sInputStr : str) -> str: tmpPattern = re.sub("\$", "\\$", sUpdateVar) sInputStr = re.sub(tmpPattern, '', sInputStr, count=1) for var in referVars[::-1]: - tmpVar = re.sub("\$", "\\$", var) - pattern = '(' + tmpVar + '\s*\[\s*.*?\s*\])' + pattern = '(' + var.replace('$', '\$') + '\s*\[\s*.*?\s*\])' variable = re.findall(pattern, sInputStr) if variable == []: sExec = "value = " + re.search('^\s*\$\${(\s*.*?)}', var).group(1) @@ -429,9 +452,12 @@ def __referVarHandle(referVar : str, sInputStr : str) -> str: exec(sExec, globals(), ldict) tmpValue = ldict['value'] except: + self.__reset(bCleanGlobalVars=True) raise Exception(f"The variable '{var}' is not available!") - sInputStr = re.sub(tmpVar, tmpValue, sInputStr) if isinstance(tmpValue, str) else \ - re.sub(tmpVar, str(tmpValue), sInputStr) + if re.search("\[\s*"+ var.replace('$', '\$') +"\s*\]", sInputStr) and isinstance(tmpValue, str): + sInputStr = sInputStr.replace(var, "'" + var + "'") + sInputStr = re.sub(var.replace('$', '\$'), tmpValue, sInputStr) if isinstance(tmpValue, str) else \ + re.sub(var.replace('$', '\$'), str(tmpValue), sInputStr) continue while variable != []: fullVariable = variable[0] @@ -445,6 +471,7 @@ def __referVarHandle(referVar : str, sInputStr : str) -> str: exec(sExec, globals(), ldict) tmpValue = ldict['value'] except: + self.__reset(bCleanGlobalVars=True) raise Exception(f"The variable '{fullVariable}' is not available!") pattern = re.sub('\[', '\\[', fullVariable) pattern = re.sub('\]', '\\]', pattern) @@ -497,31 +524,35 @@ def __handleDotdictFormat(self, lInputListParams : list, lParams: list = []) -> else: return self.__handleDotdictFormat(lInputListParams, lParams) - def __checkAndCreateNewElement(self, sKey: str, value): + def __checkAndCreateNewElement(self, sKey: str, value, bCheck=False): ''' This method check and create new elements if they are not exist. ''' rootKey = re.sub("\[.*\]", "", sKey) subElements = re.findall("\[\s*'([0-9A-Za-z_]+[0-9A-Za-z\.\-_]*)'\s*\]", sKey) - if len(subElements) <= 1: - return + if len(subElements) < 1: + return True else: for index, element in enumerate(subElements): - if index < len(subElements) - 1: + if index < len(subElements): rootKey = rootKey + "['" + element + "']" sExec = "dumpData = " + rootKey try: exec(sExec) except: - sExec = rootKey + " = {}" - try: - exec(sExec, globals()) - except Exception as error: - self.__reset() - errorMsg = f"Could not set variable '{sKey}' with value '{value}'! Reason: {error}" - raise Exception(errorMsg) + if bCheck==True: + return False # Return 'False' when detected implicit creation of data structures based on nested parameters. + else: + sExec = rootKey + " = {}" + try: + exec(sExec, globals()) + except Exception as error: + self.__reset(bCleanGlobalVars=True) + errorMsg = f"Could not set variable '{sKey}' with value '{value}'! Reason: {error}" + raise Exception(errorMsg) else: continue + return True def __updateAndReplaceNestedParam(self, oJson : dict, bNested : bool = False, recursive : bool = False): ''' @@ -550,7 +581,7 @@ def __jsonUpdated(k, v, oJson, bNested, keyNested = ''): try: exec(sExec, globals()) except Exception as error: - self.__reset() + self.__reset(bCleanGlobalVars=True) errorMsg = f"Could not set variable '{k}' with value '{v}'! Reason: {error}" raise Exception(errorMsg) @@ -594,14 +625,16 @@ def __loadNestedValue(initValue: str, sInputStr: str, bKey=False, key=''): elif re.match("^\s*" + pattern + "\s*$", sInputStr): sInputStr = re.sub("\$", "$$", sInputStr) else: + self.__reset(bCleanGlobalVars=True) while "str(" in initValue: initValue = re.sub("str\(([\${}0-9A-Za-z\.\-_\[\]]+)\)", "\\1", initValue, count=1) raise Exception(f"Invalid syntax! One or more than one opened or closed curly bracket is missing in expression '{initValue}'.\n \ Please check the configuration file of the executed test!") sInputStr = self.__checkParamName(sInputStr) - valueAfterProcessed = self.__nestedParamHandler(sInputStr) + valueAfterProcessed = self.__nestedParamHandler(sInputStr) if not bValueConvertString else \ + self.__nestedParamHandler(sInputStr, bKey=bKey) for valueProcessed in valueAfterProcessed: - tmpValueAfterProcessed = re.sub('\\${\s*(.*?)\s*}', '\\1', valueProcessed) + tmpValueAfterProcessed = re.sub("'*\${\s*(.*?)\s*}'*", '\\1', valueProcessed) sExec = "value = " + tmpValueAfterProcessed if isinstance(tmpValueAfterProcessed, str) else \ "value = " + str(tmpValueAfterProcessed) try: @@ -614,18 +647,19 @@ def __loadNestedValue(initValue: str, sInputStr: str, bKey=False, key=''): sInputStr = re.sub("\$\$", "$", sInputStr) sInputStr = str(ldict['value']) if bValueConvertString else ldict['value'] except: - self.__reset() + self.__reset(bCleanGlobalVars=True) if CNameMangling.DUPLICATEDKEY_00.value in valueProcessed: valueProcessed = valueProcessed.replace(CNameMangling.DUPLICATEDKEY_00.value, '') elif CNameMangling.DUPLICATEDKEY_02.value in valueProcessed: valueProcessed = valueProcessed.replace(CNameMangling.DUPLICATEDKEY_02.value, '') raise Exception(f"The variable '{valueProcessed}' is not available!") if bKey and type(ldict['value']) in [list, dict]: - self.__reset() + self.__reset(bCleanGlobalVars=True) while 'str(' in key: key = re.sub("str\(([0-9A-Za-z\._\${}'\[\]]+)\)", "\\1", key) - errorMsg = f"Could not substitute parameter '{key}'! Composite data types are not allowed. \ -The value of parameter '{valueProcessed}' is {ldict['value']}" + errorMsg = f"Found expression '{key}' with at least one parameter of composite data type \ +('{valueProcessed}' is of type {type(ldict['value'])}). Because of this expression is the name of a parameter, \ +only simple data types are allowed to be substituted inside." raise Exception(errorMsg) if "${" not in str(sInputStr): break @@ -638,10 +672,11 @@ def __loadNestedValue(initValue: str, sInputStr: str, bKey=False, key=''): globals().update({k:v}) tmpJson = copy.deepcopy(oJson) - pattern = "\${\s*[0-9A-Za-z_]+[0-9A-Za-z\.\$\{\}\-_]*\s*}(\[+\s*'.+'\s*\]+|\[+\s*\d+\s*\]+)*" + pattern = "\${\s*[0-9A-Za-z_]+[0-9A-Za-z\.\$\{\}\-_]*\s*}(\[+\s*'.+'\s*\]+|\[+\s*\d+\s*\]+|\[+\s*\${.+\s*\]+)*" for k, v in tmpJson.items(): keyNested = '' bStrConvert = False + bImplicitCreation = False if CNameMangling.DUPLICATEDKEY_00.value in k: del oJson[k] k = k.replace(CNameMangling.DUPLICATEDKEY_00.value, '') @@ -655,20 +690,29 @@ def __loadNestedValue(initValue: str, sInputStr: str, bKey=False, key=''): elif CNameMangling.STRINGCONVERT.value in k: bStrConvert = True del oJson[k] - k = k.replace(CNameMangling.STRINGCONVERT.value, '') - oJson[k] = v - keyNested = k + keyNested = k.replace(CNameMangling.STRINGCONVERT.value, '') + oJson[keyNested] = v bNested = True while "${" in k: - k = __loadNestedValue(keyNested, k) + k = __loadNestedValue(keyNested, k, bKey=True, key=keyNested) elif re.match("^\s*" + pattern + "\s*$", k.lower()): keyNested = k + if re.search("\[\s*'*" + pattern + "'*\s*\]", keyNested) or \ + re.search("\." + pattern + "[\.}]+", keyNested): + bImplicitCreation = True k = re.sub("\$", "$$", k) k = self.__checkParamName(k) keyAfterProcessed = self.__nestedParamHandler(k, bKey=True) k = re.sub("\$\$", "$", k) k = re.sub('^\s*\${\s*(.*?)\s*}', '\\1', keyAfterProcessed[0]) + # Temporary disable implicit creation of data structures based on nested parameters. + # In case check sub-element returns False -> reset() and raise an exception. + if bImplicitCreation and not self.__checkAndCreateNewElement(k, v, bCheck=True): + self.__reset(bCleanGlobalVars=True) + raise Exception(f"The implicit creation of data structures based on nested parameter is not supported. \ +New parameter '{k}' could not be created by the expression '{keyNested}'") elif k.count('{') != k.count('}'): + self.__reset(bCleanGlobalVars=True) raise Exception(f"Could not overwrite parameter {k} due to wrong format.\n \ Please check key '{k}' in config file!!!") @@ -757,6 +801,9 @@ def __recursiveNestedHandling(sInputStr: str, lNestedParam: list) -> str: valueNumberPattern = "[0-9\.]+" if "${" in sInputStr: + if re.search("\[[0-9\s]*[A-Za-z_]+[0-9\s]*\]", sInputStr): + self.__reset(bCleanGlobalVars=True) + raise Exception(f"Invalid syntax! A sub-element in {sInputStr.strip()} has to enclosed in quotes.") if re.match("^\s*" + nestedPattern + "\s*,*\]*}*\s*$", sInputStr.lower()): sInputStr = re.sub("(" + nestedPattern + ")", "\"\\1\"", sInputStr) nestedParam = re.sub("^\s*\"(.+)\"\s*.*$", "\\1", sInputStr) @@ -818,6 +865,7 @@ def __recursiveNestedHandling(sInputStr: str, lNestedParam: list) -> str: sInputStr = __recursiveNestedHandling(sInputStr, tmpList) elif "," in sInputStr: if not re.match("^\s*\".+\"\s*$", sInputStr): + self.__reset(bCleanGlobalVars=True) raise Exception(f"Invalid nested parameter format: {sInputStr} - The double quotes are missing!!!") listPattern = "^\s*(\"*" + nestedPattern + "\"*\s*,+\s*|" + valueStrPattern + "\s*,+\s*|" + valueNumberPattern + "\s*,+\s*)+" + \ "(\"*" + nestedPattern + "\"*\s*,*\s*|" + valueStrPattern + "\s*,*\s*|" + valueNumberPattern + "\s*,*\s*)*\]*}*\s*$" @@ -831,6 +879,7 @@ def __recursiveNestedHandling(sInputStr: str, lNestedParam: list) -> str: tmpItem = item if "${" in item: if not re.match("^\s*\"*" + nestedPattern + "\"*\]*}*\s*$", item): + self.__reset(bCleanGlobalVars=True) raise Exception(f"Invalid nested parameter format: {item}") elif re.match("^\s*\".*" + nestedPattern + ".*\"\s*$", item): item = re.sub("(" + nestedPattern + ")", "str(\\1)", item) @@ -846,10 +895,12 @@ def __recursiveNestedHandling(sInputStr: str, lNestedParam: list) -> str: sInputStr = newInputStr elif re.search("\${\s*}", sInputStr) or re.search("\${.+}\.", sInputStr) \ or (nestedKey and (sInputStr.count("{") != sInputStr.count("}") or sInputStr.count("[") != sInputStr.count("]"))): + self.__reset(bCleanGlobalVars=True) raise Exception(f"Invalid parameter format: {sInputStr}") elif nestedKey and re.match("^\s*\${[^\(\)\!@#%\^\&\-\+\/\\\=`~\?]+[}\[\]]+\s*$", sInputStr): sInputStr = re.sub("^\s*(\${[^\(\)\!@#%\^\&\-\+\/\\\=`~\?]+[}\[\]]+)\s*$", "\"\\1\"", sInputStr) else: + self.__reset(bCleanGlobalVars=True) raise Exception(f"Invalid nested parameter format: {sInputStr} - The double quotes are missing!!!") sOutput = sInputStr @@ -974,6 +1025,7 @@ def __removeDuplicatedKey(dInput : dict) -> dict: jFile = CString.NormalizePath(jFile, sReferencePathAbs=os.path.dirname(os.path.abspath(sys.argv[0]))) if not(os.path.isfile(jFile)): + self.__reset(bCleanGlobalVars=True) raise Exception(f"File '{jFile}' is not existing!") self.lImportedFiles.append(jFile) @@ -981,8 +1033,7 @@ def __removeDuplicatedKey(dInput : dict) -> dict: try: sJsonData= self.__load_and_removeComments(jFile) except Exception as reason: - if masterFile: - self.__reset() + self.__reset(bCleanGlobalVars=True) raise Exception(f"Could not read json file '{jFile}' due to: '{reason}'!") pattern = "\${\s*[0-9A-Za-z_]+[0-9A-Za-z\.\-_]*\s*}(\[+\s*'.+'\s*\]+|\[+\s*\d+\s*\]+)*" @@ -993,12 +1044,14 @@ def __removeDuplicatedKey(dInput : dict) -> dict: try: listDummy = shlex.split(line) except Exception as error: + self.__reset(bCleanGlobalVars=True) raise Exception(f"\n{str(error)} in line: '{line}'") if re.search(pattern, line): lNestedVar = re.findall("\${\s*([0-9A-Za-z_]+[0-9A-Za-z\.\-_]*)\s*}", line) for nestedVar in lNestedVar: if nestedVar[0].isdigit(): + self.__reset(bCleanGlobalVars=True) raise Exception(f"Invalid parameter format in line: {line.strip()}") tmpList = re.findall("(\"[^\"]+\")", line) line = re.sub("(\"[^\"]+\")", CNameMangling.COLONS.value, line) @@ -1056,12 +1109,13 @@ def __removeDuplicatedKey(dInput : dict) -> dict: tmpNestedParam = nestedParam.replace("$", "\$") tmpNestedParam = tmpNestedParam.replace("[", "\[") tmpNestedParam = tmpNestedParam.replace("]", "\]") - if re.search("(\s*\"str\(" + tmpNestedParam + "\)\"\s*:)", newLine) \ - or re.search("(\s*\"" + tmpNestedParam + "\"\s*:)", newLine): + if re.search("(\s*\"str\(" + tmpNestedParam + "\)\"\s*:)", newLine.replace(CNameMangling.STRINGCONVERT.value, '')) \ + or re.search("(\s*\"" + tmpNestedParam + "\"\s*:)", newLine.replace(CNameMangling.STRINGCONVERT.value, '')): self.lNestedParams.remove(nestedParam) sJsonDataUpdated = sJsonDataUpdated + newLine + "\n" else: if "${" in line: + self.__reset(bCleanGlobalVars=True) invalidPattern1 = "\${\s*[0-9A-Za-z\._]*\[.+\][0-9A-Za-z\._]*\s*}" if re.search(invalidPattern1, line): raise Exception(f"Invalid syntax: Found index inside curly brackets in line '{line.strip()}'. \ @@ -1075,6 +1129,7 @@ def __removeDuplicatedKey(dInput : dict) -> dict: if self.syntax == CSyntaxType.python: CJSONDecoder = CPythonJSONDecoder else: + self.__reset(bCleanGlobalVars=True) raise Exception(f"Provided syntax '{self.syntax}' is not supported.") try: @@ -1082,8 +1137,7 @@ def __removeDuplicatedKey(dInput : dict) -> dict: cls=CJSONDecoder, object_pairs_hook=self.__processImportFiles) except Exception as error: - if masterFile: - self.__reset() + self.__reset(bCleanGlobalVars=True) raise Exception(f"JSON file: {jFile}\n{error}") self.__checkDotInParamName(oJson) @@ -1124,14 +1178,14 @@ def __removeDuplicatedKey(dInput : dict) -> dict: param = self.__checkParamName(param) param = re.sub("\${", "$${", param) parseNestedParam = self.__nestedParamHandler(param) - tmpParseNestedParam = re.sub('\${\s*(.*?)\s*}', '\\1', parseNestedParam[0]) + tmpParseNestedParam = re.sub("'*\${\s*(.*?)\s*}'*", '\\1', parseNestedParam[0]) sExec = "value = " + tmpParseNestedParam if isinstance(tmpParseNestedParam, str) else \ "value = " + str(tmpParseNestedParam) try: ldict = {} exec(sExec, globals(), ldict) except: - self.__reset() + self.__reset(bCleanGlobalVars=True) raise Exception(f"The variable '{parseNestedParam[0]}' is not available!") self.__reset() diff --git a/JsonPreprocessor/JsonPreprocessor.pdf b/JsonPreprocessor/JsonPreprocessor.pdf index e273db91..3c318f47 100644 Binary files a/JsonPreprocessor/JsonPreprocessor.pdf and b/JsonPreprocessor/JsonPreprocessor.pdf differ diff --git a/config/robotframework_aio/release_items_JsonPreprocessor.json b/config/robotframework_aio/release_items_JsonPreprocessor.json index 9f848fa2..6640d9e0 100644 --- a/config/robotframework_aio/release_items_JsonPreprocessor.json +++ b/config/robotframework_aio/release_items_JsonPreprocessor.json @@ -45,20 +45,22 @@ # ------------------------------------------ "RELEASES" : { - "0.9." : [ - "JsonPreprocessor change 0.9 Implicit creation of data structures", - "JsonPreprocessor change 0.9 Dotdict feature bug fixing", - "JsonPreprocessor change 0.9 Update nested parameters handling in key name and value", - "JsonPreprocessor change 0.9 Nested parameter feature bug fixing", - "JsonPreprocessor change 0.9 Nested parameters substitution and overwriting improvement", - "JsonPreprocessor change 0.9 Jsonp file path computation improvement" - ], "0.10." : [ - "JsonPreprocessor change 0.10 Add jsonDump method to write a file in JSON format", - "JsonPreprocessor change 0.10 Improve nested parameter format", - "JsonPreprocessor change 0.10 Improve error message log", - "JsonPreprocessor change 0.10 Fix bugs of data structures implicitly", - "JsonPreprocessor change 0.10 Improve index handling together with nested parameters" +" +* Added a ``jsonDump`` method to write the content of a Python dictionary to a file in JSON format + + **Example:** + + .. code:: + + dictTest = {\"A\" 1, \"B\" 2} + json_preprocessor = CJsonPreprocessor() + json_preprocessor.jsonDump(dictTest, \"./OutputFile.json\") + +* Improved format of nested parameters; improved error messages +* Some bugs fixed in implicitly created data structures +* Improved index handling together with nested parameters +" ] } -} \ No newline at end of file +} diff --git a/packagedoc/additional_docs/Description.tex b/packagedoc/additional_docs/Description.tex index 91cd16c0..7c51c836 100644 --- a/packagedoc/additional_docs/Description.tex +++ b/packagedoc/additional_docs/Description.tex @@ -32,16 +32,13 @@ \section{How to execute} print(f"'{reason}'") \end{pythoncode} -\textbf{!!! Caution:} relative path bug: -\href{https://github.com/test-fullautomation/python-jsonpreprocessor/issues/83}{issues/83} \textbf{!!!} - The main method of the \pkg\ is: \pcode{jsonLoad}. Input is the path and the name of a JSON file. Output is a dictionary containing all values parsed from this JSON file. In case of any errors while computing the JSON file, the \pkg\ throws an exception. Therefore it is required to call the method \pcode{jsonLoad} inside a \pcode{try/except} block. -\pcode{pprint} is used in this example to give the output a better readibility in console. +\pcode{pprint} is used in this example to give the output a better readability in console. \vspace{2ex} diff --git a/packagedoc/additional_docs/The JSONP format.tex b/packagedoc/additional_docs/The JSONP format.tex index a24ece51..b92ccb1d 100644 --- a/packagedoc/additional_docs/The JSONP format.tex +++ b/packagedoc/additional_docs/The JSONP format.tex @@ -125,6 +125,28 @@ \section{Comments} All lines starting with \pcode{//}, are ignored by the \pkg. The output of this example is the same than in the previous example. +\vspace{2ex} + +Also block comments and inline comments are possible, realized by a pair of \pcode{/* */}: + +\begin{pythoncode} +{ + /* + "param1" : 1, + "param2" : "A", + */ + + "testlist" : ["A1", /*"B2", "C3",*/ "D4"] +} +\end{pythoncode} + +\vspace{2ex} + +\textbf{Outcome:} + +\begin{pythonlog} +{'testlist': ['A1', 'D4']} +\end{pythonlog} % -------------------------------------------------------------------------------------------------------------- @@ -300,7 +322,7 @@ \section{Overwrite parameters} "common_param_2" : "common componentB value 2" \end{pythoncode} -in \plog{componentB.jsonp} the initial definition +in \plog{componentB.jsonp}, the initial definition \begin{pythoncode} "common_param_2" : "common value 2" @@ -521,7 +543,7 @@ \section{Overwrite parameters} "int_val_b" : "${int_val}", \end{pythoncode} -the parameter \pcode{int_val_b} would be of type \pcode{string}. +the parameter \pcode{int_val_b} is of type \pcode{string}. \vspace{2ex} @@ -624,10 +646,76 @@ \section{Overwrite parameters} \vspace{2ex} -\textbf{Use of a common dictionary} +\textbf{Dictionary keys and indices as parameter} + +In all code examples above the indices of lists and the key names of dictionaries have been hard coded strings. It is also possible to use parameters: + +\begin{pythoncode} +{ + "index1" : 0, + "index2" : 1, + "key1" : "keyA", + "key2" : "keyB", + "testlist" : ["A", "B"], + "testdict" : {"keyA" : "A", "keyB" : "B"}, + "tmp1" : ${testlist}[${index1}], + "tmp2" : ${testdict}[${key1}], + ${testlist}[${index1}] : ${testlist}[${index2}], + ${testdict}[${key1}] : ${testdict}[${key2}], + ${testlist}[${index2}] : ${tmp1}, + ${testdict}[${key2}] : ${tmp2} +} +\end{pythoncode} + +\textbf{Outcome:} + +\vspace{2ex} + +\begin{pythonlog} +{'index1': 0, + 'index2': 1, + 'key1': 'keyA', + 'key2': 'keyB', + 'testdict': {'keyA': 'B', 'keyB': 'A'}, + 'testlist': ['B', 'A'], + 'tmp1': 'A', + 'tmp2': 'A'} +\end{pythonlog} + +\newpage + +\textbf{Meaning of single quotes in square brackets} + +Single quotes are used to convert the content inside to a string. + +\begin{itemize} + \item In case of the parameter \pcode{param} is of type string, the expressions \pcode{[$\{param\}]} and \pcode{['$\{param\}']} have the same outcome: + The content inside the square brackets is a string. The single quotes have no meaning in this case (because the parameter is already of type string). + \item In case of the parameter \pcode{param} is of type integer, the quotes in \pcode{['$\{param\}']} convert the integer value to a string. + Without the quotes (\pcode{[$\{param\}]}), the content inside the square brackets is an integer. +\end{itemize} + +In the context of \pkg\ JSON files, only strings and integers are expected to be inside square brackets (except the brackets are used to define a list). +Other data types are not supported here. + +Whether a string or an integer is expected, depends on the data type of the parameter, the square bracket expression belongs to. Dictionaries require a string (a key name), lists require an integer (an index). +Deviations will cause an error. + +Summarized the following combinations are valid (on both the left hand side of the colon and the right hand side of the colon): + +\begin{pythoncode} +${listparam}[${intparam}] +${listparam}[1] +${dictparam}['${intparam}'] +${dictparam}[${stringparam}] +${dictparam}['${stringparam}'] +${dictparam}['keyname'] +\end{pythoncode} \vspace{2ex} +\textbf{Use of a common dictionary} + The last example in this section covers the following use case: \begin{itemize} @@ -639,6 +727,8 @@ \section{Overwrite parameters} To realize this, it is necessary to separate the initialization of the dictionary from all positions where keys are added to this dictionary. +\newpage + These are the JSON files: \vspace{2ex} @@ -661,7 +751,7 @@ \section{Overwrite parameters} } \end{pythoncode} -\newpage +\vspace{2ex} \textbullet \plog{featureA.jsonp} @@ -700,7 +790,7 @@ \section{Overwrite parameters} } \end{pythoncode} -\vspace{2ex} +\newpage \textbf{Explanation:} @@ -884,9 +974,6 @@ \section{Substitution of dollar operator expressions} } \end{pythoncode} -\textbf{!!! Caution:} substitution bug: -\href{https://github.com/test-fullautomation/python-jsonpreprocessor/issues/92}{issues/92} \textbf{!!!} - \vspace{2ex} \textbf{Outcome:} @@ -911,29 +998,35 @@ \section{Substitution of dollar operator expressions} } \end{pythoncode} -\vspace{2ex} - -\textbf{Outcome:} +If this would be allowed, the last line would cause a parameter with this name: \begin{pythonlog} -!!! Error message expected; still under development !!! +"test_parameter_['AB', 12, True, None, {'kA': 'kAval', 'kB': 'kBval'}]" \end{pythonlog} -\textbf{!!! Caution:} -\href{https://github.com/test-fullautomation/python-jsonpreprocessor/issues/69#issuecomment-1589581903}{issues/69 issuecomment-1589581903} \textbf{!!!} +And this is absolutely not a valid name! \vspace{2ex} -\textbf{Overwriting vs. substitution} +To prevent the users from generating invalid parameter names, the \pkg\ throws an error: -\vspace{2ex} +\begin{pythonlog} +"Error: 'Found expression '...' with at least one parameter of composite data type ... +Because of this expression is the name of a parameter, only simple data types are allowed to be substituted inside.'!" +\end{pythonlog} + +\newpage + +\section{Overwriting vs. substitution} -With the last two examples in this section we take a deeper look at the syntax difference between overwriting and substitution. +In this section we take a deeper look at the syntax difference between overwriting and substitution explained above. \vspace{2ex} \textbf{1. Overwriting:} +\vspace{2ex} + With \pcode{$\{param2\} : $\{param1\}} the initial value \pcode{"XYZ"} of parameter \pcode{param2} is overwritten with the value \pcode{"ABC"} of parameter \pcode{param1}. @@ -959,6 +1052,8 @@ \section{Substitution of dollar operator expressions} \textbf{2. Substitution:} +\vspace{2ex} + With \pcode{"$\{param2\}" : $\{param1\}} a new parameter with name \pcode{XYZ} (the value of \pcode{param2}) and value \pcode{"ABC"} is created. @@ -1060,11 +1155,41 @@ \section{Implicite creation of dictionaries} } \end{pythoncode} -you will \textit{not} get an error message! The entire data structure will be created implicitely. The impact is that this method +you will \textit{not} get an error message! The entire data structure will be created implicitly. The impact is that this method is very susceptible to typing mistakes. The implicite creation of data structures does not work with lists! In case you use a list index out of range, you will get a corresponding error message. +\newpage + +\textbf{Key names} + +The implicit creation of data structures is only possible with \textit{hard coded} key names. Parameters are not supported. + +\vspace{2ex} + +\textbf{Example:} + +\begin{pythoncode} +{ + "paramA" : "ABC", + "subKey" : "ABC", + ${testdict.subKey.subKey.paramA} : "DEF" +} +\end{pythoncode} + +All sub key levels within the expression \pcode{$\{testdict.subKey.subKey.paramA\}} are interpreted as hard coded strings, even in case of parameters with the same name do exist. + +For example: The name of the implicitly created key at bottom level is \pcode{"paramA"}, and not the value \pcode{"ABC"} of the parameter with the same name (\pcode{"paramA"}). + +\vspace{2ex} + +\textbf{Therefore the outcome is:} + +\begin{pythonlog} +{'paramA': 'ABC', 'subKey': 'ABC', 'testdict': {'subKey': {'subKey': {'paramA': 'DEF'}}}} +\end{pythonlog} + % -------------------------------------------------------------------------------------------------------------- diff --git a/test/JPP_TestUsecases.csv b/test/JPP_TestUsecases.csv index 0e4d7cd3..7cf077ad 100644 --- a/test/JPP_TestUsecases.csv +++ b/test/JPP_TestUsecases.csv @@ -69,6 +69,7 @@ JPP_0507|COMPOSITE_EXPRESSIONS|GOODCASE|JSON file containing several string conc JPP_0508|COMPOSITE_EXPRESSIONS|GOODCASE|JSON file containing several string concatenations in separate lines (2) JPP_0509|COMPOSITE_EXPRESSIONS|GOODCASE|JSON file containing several parameter assignments in separate lines (different syntax) JPP_0510|COMPOSITE_EXPRESSIONS|GOODCASE|JSON file containing several parameter assignments in separate lines (extended string concatenation) +JPP_0511|COMPOSITE_EXPRESSIONS|GOODCASE|JSON file containing a list; list index is defined by a parameter JPP_0550|COMPOSITE_EXPRESSIONS|BADCASE|JSON file with composite data structure (nested lists and dictionaries / some key names with dots inside) JPP_0600|CODE_COMMENTS|GOODCASE|JSON file with several combinations of code comments JPP_0950|COMMON_SYNTAX_VIOLATIONS|BADCASE|JSON file with syntax error (1) @@ -78,6 +79,15 @@ JPP_0953|COMMON_SYNTAX_VIOLATIONS|BADCASE|JSON file with syntax error (4): file JPP_0954|COMMON_SYNTAX_VIOLATIONS|BADCASE|JSON file with syntax error (5): file is empty (multiple pairs of brackets only) JPP_1000|IMPLICIT_CREATION|GOODCASE|JSON file with dictionary keys to be created implicitly JPP_1001|IMPLICIT_CREATION|GOODCASE|JSON file with dictionary keys to be created implicitly (same key names at all levels) +JPP_1050|IMPLICIT_CREATION|BADCASE|JSON file with implicit creation of data structures based on parameters (1) +JPP_1051|IMPLICIT_CREATION|BADCASE|JSON file with implicit creation of data structures based on parameters (2) +JPP_1052|IMPLICIT_CREATION|BADCASE|JSON file with implicit creation of data structures based on parameters (3) +JPP_1053|IMPLICIT_CREATION|BADCASE|JSON file with implicit creation of data structures based on parameters (4) +JPP_1054|IMPLICIT_CREATION|BADCASE|JSON file with implicit creation of data structures based on parameters (5) +JPP_1055|IMPLICIT_CREATION|BADCASE|JSON file with implicit creation of data structures based on parameters (5) +JPP_1056|IMPLICIT_CREATION|BADCASE|JSON file with implicit creation of data structures based on parameters (6) +JPP_1057|IMPLICIT_CREATION|BADCASE|JSON file with implicit creation of data structures based on parameters (7) +JPP_1058|IMPLICIT_CREATION|BADCASE|JSON file with implicit creation of data structures based on parameters (8) JPP_1150|CYCLIC_IMPORTS|BADCASE|JSON file with cyclic imports (JSON file imports itself) JPP_1151|CYCLIC_IMPORTS|BADCASE|JSON file with cyclic imports (JSON file imports another file, that is already imported) JPP_1200|PATH_FORMATS|GOODCASE|Relative path to JSON file diff --git a/test/JPP_TestUsecases.html b/test/JPP_TestUsecases.html index e37e35da..e844199b 100644 --- a/test/JPP_TestUsecases.html +++ b/test/JPP_TestUsecases.html @@ -2340,6 +2340,39 @@ 70 +