Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
457f100
Added inline Python code self test "JPP_2100"
HolQue Jul 18, 2025
91d8c71
Documentation extended: Python inline code
HolQue Jul 25, 2025
e55320c
Merge pull request #458 from test-fullautomation/HolQue/task/selftest…
test-fullautomation Aug 1, 2025
c6cdf33
Merge pull request #460 from test-fullautomation/HolQue/task/document…
test-fullautomation Aug 1, 2025
049e537
GOODCASE tests of section INLINE_CODE reworked
HolQue Aug 5, 2025
dd2d62e
Reworked tests commented out (JsonPreprocessor adaptions required)
HolQue Aug 5, 2025
fc40c1e
Merge pull request #461 from test-fullautomation/HolQue/task/test_and…
test-fullautomation Aug 6, 2025
ee8451f
BADCASE tests of section INLINE_CODE added
HolQue Aug 6, 2025
4fcda1d
tiny typo
HolQue Aug 6, 2025
f9e7545
Further BADCASE tests of section INLINE_CODE added
HolQue Aug 7, 2025
6f1a2f8
Merge pull request #462 from test-fullautomation/HolQue/task/test_and…
namsonx Aug 8, 2025
af47971
Ticket 454 - Python inline code handling
namsonx Aug 8, 2025
652392a
Some INLINE_CODE testcases fixed
HolQue Aug 11, 2025
9f220d2
Merge branch 'namsonx/task/stabi_branch' into HolQue/task/test_and_docu
HolQue Aug 11, 2025
dad1d19
Several INLINE_CODE testcases activated
HolQue Aug 11, 2025
e7194ca
Merge pull request #463 from test-fullautomation/HolQue/task/test_and…
namsonx Aug 12, 2025
87d6e83
Extensions of selftest and documentation
HolQue Aug 13, 2025
36b2368
Merge pull request #465 from test-fullautomation/HolQue/task/test_and…
test-fullautomation Aug 13, 2025
b7fa49f
Tiny typo update in an error message log
namsonx Aug 14, 2025
99bfd8b
Fixed tiny typo in sefltest
namsonx Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 74 additions & 25 deletions JsonPreprocessor/CJsonPreprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class CNameMangling(Enum):
STRINGVALUE = "__StringValueMake-up__"
HANDLEIMPORTED = "__CheckImportedHandling__"
DYNAMICIMPORTED = "__DynamicImportedHandling__"
PYTHONBUILTIN = "__PythonBuiltInFunction__"
PYBUILTINSTR = "__StrInPythonInlineCode__"

class CPythonJSONDecoder(json.JSONDecoder):
"""
Expand Down Expand Up @@ -156,7 +158,7 @@ def __init__(self, keyPattern):

def keyNameChecker(self, sKeyName: str):
if sKeyName=='' or regex.match(r'^\s+$', sKeyName):
self.errorMsg = f"Empty key name detected. Please enter a valid name."
self.errorMsg = "Empty key name detected. Please enter a valid name."
return False
if regex.match(self.keyPattern, sKeyName):
return True
Expand Down Expand Up @@ -386,7 +388,7 @@ def __init__(self, syntax: CSyntaxType = CSyntaxType.python , currentCfg : dict
self.keyPattern = keyPattern
self.lDataTypes = [name for name, value in vars(builtins).items() if isinstance(value, type)]
self.specialCharacters = r"!#$%^&()=[]{}|;',?`~"
self.pyCallPattern = r'<<\s*eval(?:(?!<<\s*eval|>>).)*>>' # The pattern of call Python builtin function in JSONP
self.pyCallPattern = r'<<\s*(?:(?!<<\s*|>>).)*>>' # The pattern of call Python builtin function in JSONP
self.lDataTypes.append(keyword.kwlist)
self.jsonPath = None
self.importTree = None
Expand Down Expand Up @@ -649,7 +651,7 @@ def __checkParamName(self, sInput: str) -> str:

/ *Type*: str /
"""
pattern = rf'\${{\s*([^\[]+)\s*}}'
pattern = r'\${\s*([^\[]+)\s*}'
lParams = regex.findall(pattern, sInput, regex.UNICODE)
for param in lParams:
if "." not in param and param in self.lDataTypes:
Expand Down Expand Up @@ -750,9 +752,12 @@ def __getNestedValue(sNestedParam : str):
oTmpObj = oTmpObj[int(element)]
i+=1
try:
ldict = {}
exec(sExec, locals(), ldict)
tmpValue = ldict['value']
if bPyBuiltIn:
tmpValue = sExec.replace('value = ', '')
else:
ldict = {}
exec(sExec, locals(), ldict)
tmpValue = ldict['value']
except Exception as error:
if self.bJSONPreCheck:
sNestedParam = self.__removeTokenStr(sNestedParam)
Expand All @@ -769,7 +774,7 @@ def __getNestedValue(sNestedParam : str):
errorMsg = f"{errorMsg} Reason: {error}" if ' or slices' not in str(error) else \
f"{errorMsg} Reason: {str(error).replace(' or slices', '')}"
else:
if isinstance(error, KeyError) and regex.search(r"\[\s*" + str(error) + "\s*\]", sNestedParam):
if isinstance(error, KeyError) and regex.search(r"\[\s*" + str(error) + r"\s*\]", sNestedParam):
errorMsg = f"Could not resolve expression '{sNestedParam.replace('$${', '${')}'. \
Reason: Key error {error}"
else:
Expand All @@ -778,7 +783,7 @@ def __getNestedValue(sNestedParam : str):
return tmpValue

bPyBuiltIn = False
if regex.match(self.pyCallPattern, sInputStr):
if regex.search(self.pyCallPattern, sInputStr):
bPyBuiltIn = True
specialCharacters = r'[]{}'
pattern = rf'\$\${{\s*[^{regex.escape(specialCharacters)}]+\s*}}'
Expand Down Expand Up @@ -1286,7 +1291,7 @@ def __handleList(lInput : list, bNested : bool, parentParams : str = '') -> list
oJson = self.currentCfg | oJson

tmpJson = copy.deepcopy(oJson)
pattern = rf"\${{\s*[^\[]+\s*}}"
pattern = r"\${\s*[^\[]+\s*}"
pattern = rf"{pattern}(\[+\s*'.+'\s*\]+|\[+\s*\d+\s*\]+|\[+\s*\${{.+\s*\]+)*"
for k, v in tmpJson.items():
if "${" not in k and CNameMangling.DUPLICATEDKEY_01.value not in k:
Expand Down Expand Up @@ -1408,13 +1413,11 @@ def __handleList(lInput : list, bNested : bool, parentParams : str = '') -> list
elif isinstance(v, list):
v = __handleList(v, bNested, parentParams)
elif isinstance(v, str) and self.__checkNestedParam(v):
bPyBuiltIn = False
# Check and handle the Python builtIn in JSONP
if regex.match(self.pyCallPattern, v):
if regex.match(r'^\s*<<\s*eval\s*>>\s*$', v):
errorMsg = f"The Python builtIn must not be empty. Please check '{self.__removeTokenStr(v)}'"
self.__reset()
raise Exception(errorMsg)
elif "${" not in v:
if regex.search(self.pyCallPattern, v):
bPyBuiltIn = True
if '${' not in v:
try:
v = self.__pyBuiltInHandle(v)
except Exception as error:
Expand Down Expand Up @@ -1445,7 +1448,8 @@ def __handleList(lInput : list, bNested : bool, parentParams : str = '') -> list
raise Exception(errorMsg)
v = __loadNestedValue(initValue, v, key=k)
# Check and handle the Python builtIn in JSONP
if isinstance(v, str) and regex.match(self.pyCallPattern, v):
if isinstance(v, str) and regex.search(self.pyCallPattern, v):
bPyBuiltIn = True
try:
v = self.__pyBuiltInHandle(v)
except Exception as error:
Expand All @@ -1466,7 +1470,7 @@ def __handleList(lInput : list, bNested : bool, parentParams : str = '') -> list
raise Exception(f"Invalid expression found: '{self.__removeTokenStr(initValue)}'.")
else:
break
if isinstance(v, str) and regex.search(r'\[[^\]]+\]', v):
if isinstance(v, str) and regex.search(r'\[[^\]]+\]', v) and not bPyBuiltIn:
sExec = 'value = ' + v
try:
ldict = {}
Expand Down Expand Up @@ -1511,7 +1515,7 @@ def __handleList(lInput : list, bNested : bool, parentParams : str = '') -> list
parentParams = ''
__jsonUpdated(k, v, oJson, parentParams, keyNested, paramInValue, bDuplicatedHandle, recursive)
if keyNested is not None and not bStrConvert:
transTable = str.maketrans({"[":"\[", "]":"\]" })
transTable = str.maketrans({"[":r"\[", "]":r"\]" })
tmpList = []
for key in self.dUpdatedParams:
if regex.match(r"^" + k.translate(transTable) + r"\['.+$", key, regex.UNICODE):
Expand Down Expand Up @@ -1560,7 +1564,7 @@ def __checkNestedParam(self, sInput : str, bKey=False) -> bool:
*raise exception if nested parameter format invalid*
"""
pattern = rf"^\${{\s*[^{regex.escape(self.specialCharacters)}]+\s*}}(\[.*\])+$"
pattern1 = rf"\${{[^\${{]+}}(\[[^\[]+\])*[^\[]*\${{"
pattern1 = r"\${[^\${]+}(\[[^\[]+\])*[^\[]*\${"
pattern2 = r"\[[\p{Nd}\.\-\+'\s]*:[\p{Nd}\.\-\+'\s]*\]|\[[\s\p{Nd}\+\-]*\${.+[}\]][\s\p{Nd}\+\-]*:[\s\p{Nd}\+\-]*\${.+[}\]][\s\p{Nd}\+\-]*\]|" # Slicing pattern
pattern2 = pattern2 + r"\[[\s\p{Nd}\+\-]*\${.+[}\]][\s\p{Nd}\+\-]*:[\p{Nd}\.\-\+'\s]*\]|\[[\p{Nd}\.\-\+'\s]*:[\s\p{Nd}\+\-]*\${.+[}\]][\s\p{Nd}\+\-]*\]" # Slicing pattern
if not bKey and regex.match(self.pyCallPattern, sInput):
Expand Down Expand Up @@ -1823,7 +1827,7 @@ def hashContent(sInput : str) -> str:
self.__reset()
raise Exception(jsonException)
self.JPGlobals = self.jsonCheck
importPattern = rf'([\'|"]\s*\[\s*import\s*\](_\d+)*\s*[\'|"]\s*:\s*[\'|"][^\'"]+[\'|"])'
importPattern = r'([\'|"]\s*\[\s*import\s*\](_\d+)*\s*[\'|"]\s*:\s*[\'|"][^\'"]+[\'|"])'
sJson = json.dumps(self.jsonCheck)
# Check cyclic import by comparing the content of the whole JSONP configuration object.
if len(self.importCheck)>1:
Expand All @@ -1850,15 +1854,59 @@ def __pyBuiltInHandle(self, sInput : str):
"""
Handles Python builtIn function.
"""
sExec = regex.sub(r'<<\s*eval(.*)>>', "evalValue = \\1", sInput)
if CNameMangling.PYBUILTINSTR.value in sInput:
sInput = sInput.replace(CNameMangling.PYBUILTINSTR.value, '"')
if CNameMangling.PYTHONBUILTIN.value in sInput:
sInput = regex.sub(rf'(self\.JPGlobals(?:(?!self\.JPGlobals).)+){CNameMangling.PYTHONBUILTIN.value}', '"\\1"', sInput)
pyInlineCode = regex.findall(self.pyCallPattern, sInput)[0]
sExec = regex.sub(r'<<\s*(.*)>>', "evalValue = \\1", pyInlineCode)
try:
ldict = {}
exec(sExec, locals(), ldict)
evalValue = ldict['evalValue']
except Exception as error:
raise Exception(error)

if not isinstance(evalValue, (str, int, float, bool, type(None), list, dict)):
errorMsg = f"The Python builtIn '{self.__removeTokenStr(sInput)}' return the value with \
the datatype '{type(evalValue)}' is not suitable for JSON."
raise Exception(errorMsg)
if CNameMangling.DYNAMICIMPORTED.value in sInput:
sInput = regex.sub(f'{CNameMangling.DYNAMICIMPORTED.value}', '/', sInput)
sInput = regex.sub(f'{self.pyCallPattern}', f'{evalValue}', sInput)
evalValue = sInput

return evalValue

def __pyInlineCodeSyntaxCheck(self, sInput):
"""
Checks the syntax of Python inline code.
"""
if regex.match(r'^\s*<<\s*>>\s*$', sInput):
errorMsg = f"The Python builtIn must not be empty. Please check '{self.__removeTokenStr(v)}'"
self.__reset()
raise Exception(errorMsg)
elif regex.search(rf'["\s]*{self.pyCallPattern}[^:]*["\s]*:', sInput):
errorMsg = f"Python inline code is not allowed as key! Please check the line {sInput}"
self.__reset()
raise Exception(errorMsg)
elif regex.search(rf':\s*".*{self.pyCallPattern}[^"]*"', sInput):
errorMsg = f"Python inline code must not be embedded part of a string! Please check the line {sInput}"
self.__reset()
raise Exception(errorMsg)
else:
pyInlineCode = regex.search(self.pyCallPattern, sInput)
if len(pyInlineCode) > 0:
pyInlineCode = pyInlineCode[0]
if pyInlineCode.count('"') % 2 == 1:
errorMsg = f"Invalid syntax in the Python inline code '{pyInlineCode}'."
self.__reset()
raise Exception(errorMsg)
elif regex.search(r'"\s*\${[^"]+"', pyInlineCode):
pyInlineCode = regex.sub(r'"\s*(\${[^"]+)\s*"', f'\\1{CNameMangling.PYTHONBUILTIN.value}', pyInlineCode)
pyInlineCode = regex.sub(r'"(\s*(?:(?!\${)[^"])*)"', \
f'{CNameMangling.PYBUILTINSTR.value}\\1{CNameMangling.PYBUILTINSTR.value}', pyInlineCode)
sInput = regex.sub(rf'({self.pyCallPattern})', f'"{pyInlineCode}"', sInput)
return sInput

def jsonLoad(self, jFile : str):
"""
Expand Down Expand Up @@ -2083,7 +2131,7 @@ def __handleLastElement(sInput : str) -> str:
raise Exception(f"{error} in line: '{line}'")
line = line.rstrip()
if regex.search(self.pyCallPattern, line):
line = regex.sub(rf'({self.pyCallPattern})', '"\\1"', line)
line = self.__pyInlineCodeSyntaxCheck(line)
if "${" in line:
line = regex.sub(r'\${\s*([^\s][^}]+[^\s])\s*}', '${\\1}', line)
curLine = line
Expand Down Expand Up @@ -2131,6 +2179,7 @@ def __handleLastElement(sInput : str) -> str:
item = item.replace(CNameMangling.NESTEDPARAM.value, tmpList03.pop(0))
curItem = item
if "${" in item:
tmpList = []
bHandle = False
if '"' in item and item.count('"')%2==0:
tmpList = regex.findall(r'"[^"]+"', item)
Expand Down Expand Up @@ -2178,9 +2227,9 @@ def __handleLastElement(sInput : str) -> str:
item = __handleLastElement(item)
elif not regex.match(r'^[\s{]*"[^"]*"\s*$', item):
if CNameMangling.STRINGVALUE.value in item:
item = regex.sub('(^[\s{]*)([^\s].+[^\s])\s*$', '\\1\'\\2\' ', item)
item = regex.sub(r'(^[\s{]*)([^\s].+[^\s])\s*$', '\\1\'\\2\' ', item)
else:
item = regex.sub('(^[\s{]*)([^\s].+[^\s])\s*$', '\\1"\\2" ', item)
item = regex.sub(r'(^[\s{]*)([^\s].+[^\s])\s*$', '\\1"\\2" ', item)
while CNameMangling.STRINGVALUE.value in item:
if "${" in tmpList[0]:
sValue = tmpList.pop(0)
Expand Down
Binary file modified JsonPreprocessor/JsonPreprocessor.pdf
Binary file not shown.
4 changes: 2 additions & 2 deletions JsonPreprocessor/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
#
# Version and date of JsonPreprocessor
#
VERSION = "0.9.3"
VERSION_DATE = "3.7.2025"
VERSION = "0.10.0"
VERSION_DATE = "25.07.2025"
21 changes: 19 additions & 2 deletions config/robotframework_aio/release_items_JsonPreprocessor.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# **************************************************************************************************************
#
# Copyright 2020-2023 Robert Bosch GmbH
# Copyright 2020-2025 Robert Bosch GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -202,6 +202,23 @@ This feature is an option. If the user does not define the parameter ``keyPatter
* Improved and aligned error messages
* Added and updated the self test according to the changed features
"
]
],
"0.14.2.;0.15.0." : [
"
**Python inline code**

**JsonPreprocessor** enables to use Python inline code inside JSONP files.

*Example*

| ``\"A\" : [1,2,3],``
| ``\"B\" : [4,5,6],``
| ``\"C\" : <<${A} + ${B}>>``

*Result*

| ``DotDict({'A': [1, 2, 3], 'B': [4, 5, 6], 'C': [1, 2, 3, 4, 5, 6]})``
"
]
}
}
3 changes: 3 additions & 0 deletions packagedoc/additional_docs/History.tex
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,7 @@
\historychange{- Allowed unicode digits to get a list element\newline
- Updated error messages}

\historyversiondate{0.10.0}{07/2025}
\historychange{Added possibility to use Python inline code inside JSONP files}

\end{packagehistory}
Loading
Loading