Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhancement: JSON variable file support #4542

Merged
merged 4 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
70 changes: 70 additions & 0 deletions atest/robot/variables/json_variable_file.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
*** Settings ***
Suite Setup Run Tests --variablefile ${VARDIR}/cli.json -V ${VARDIR}/cli2.json --pythonpath ${VARDIR}
... variables/json_variable_file.robot
Force Tags require-json
sunday2 marked this conversation as resolved.
Show resolved Hide resolved
Resource atest_resource.robot

*** Variables ***
${VARDIR} ${DATADIR}/../testresources/res_and_var_files

*** Test Cases ***
Valid JSON file
Check Test Case ${TESTNAME}

Non-ASCII strings
Check Test Case ${TESTNAME}

Dictionary is dot-accessible
Check Test Case ${TESTNAME}

Nested dictionary is dot-accessible
Check Test Case ${TESTNAME}

Dictionary inside list is dot-accessible
Check Test Case ${TESTNAME}

JSON file in PYTHONPATH
Check Test Case ${TESTNAME}

Import Variables keyword
Check Test Case ${TESTNAME}

JSON file from CLI
Check Test Case ${TESTNAME}

Invalid JSON file
Processing should have failed 0 4 invalid.json
... ${EMPTY}
... JSONDecodeError*

Non-mapping JSON file
Processing should have failed 1 5 non_dict.json
... ${EMPTY}
... JSON variable file must be a mapping, got list.

JSON files do not accept arguments
Processing should have failed 2 6 valid.json
... with arguments ? arguments | not | accepted ?${SPACE}
... JSON variable files do not accept arguments.

Non-existing JSON file
Importing should have failed 3 7
... Variable file 'non_existing.Json' does not exist.

JSON with invalid encoding
Processing should have failed 4 8 invalid_encoding.json
... ${EMPTY}
... UnicodeDecodeError*

*** Keywords ***
Processing should have failed
[Arguments] ${index} ${lineno} ${file} ${arguments} ${error}
${path} = Normalize Path ${DATADIR}/variables/${file}
Importing should have failed ${index} ${lineno}
... Processing variable file '${path}' ${arguments}failed:
... ${error}

Importing should have failed
[Arguments] ${index} ${lineno} @{error}
Error In File ${index} variables/json_variable_file.robot ${lineno}
... @{error}
1 change: 1 addition & 0 deletions atest/testdata/variables/invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name: "jack"
sunday2 marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions atest/testdata/variables/invalid_encoding.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"encoding": "latin-1",
"expected": "utf-8",
"non-ascii": "hyv�� y�t�!"
}
53 changes: 53 additions & 0 deletions atest/testdata/variables/json_variable_file.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
*** Settings ***
Variables valid.json
Variables pythonpath.json
Variables ./invalid.json
Variables ..${/}variables${/}non_dict.json
Variables valid.json arguments not accepted
Variables non_existing.Json
Variables invalid_encoding.json
Test Template Should Be Equal

*** Variables ***
@{EXPECTED LIST} one ${2}
&{EXPECTED DICT} a=1 b=${2} 3=${EXPECTED LIST} key with spaces=value with spaces


*** Test Cases ***
Valid JSON file
${STRING} Hello, YAML!
${INTEGER} ${42}
${FLOAT} ${3.14}
${LIST} ${EXPECTED LIST}
${DICT} ${EXPECTED DICT}
${BOOL} ${TRUE}
${NULL} ${NULL}
sunday2 marked this conversation as resolved.
Show resolved Hide resolved

Non-ASCII strings
${NON} äscii
${NÖN} äscii

Dictionary is dot-accessible
${DICT.a} 1
${DICT.b} ${2}

Nested dictionary is dot-accessible
${NESTED DICT.dict} ${EXPECTED DICT}
${NESTED DICT.dict.a} 1
${NESTED DICT.dict.b} ${2}

Dictionary inside list is dot-accessible
${LIST WITH DICT[1].key} value
${LIST WITH DICT[2].dict} ${EXPECTED DICT}
${LIST WITH DICT[2].nested[0].leaf} value

JSON file in PYTHONPATH
${JSON FILE IN PYTHONPATH} ${TRUE}

Import Variables keyword
[Setup] Import Variables ${CURDIR}/valid2.json
${VALID 2} imported successfully

JSON file from CLI
${JSON FILE FROM CLI} woot!
${JSON FILE FROM CLI2} kewl!
6 changes: 6 additions & 0 deletions atest/testdata/variables/non_dict.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
"Not dictionary",
{
"true": "top-level"
}
]
55 changes: 55 additions & 0 deletions atest/testdata/variables/valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"string": "Hello, YAML!",
"non": "äscii",
"nön": "äscii",
"integer": 42,
"float": 3.14,
"bool": true,
"null": null,
"list": [
"one",
2
],
"dict": {
"a": "1",
"b": 2,
"3": [
"one",
2
],
"key with spaces": "value with spaces"
},
"nested dict": {
"dict": {
"a": "1",
"b": 2,
"3": [
"one",
2
],
"key with spaces": "value with spaces"
}
},
"list with dict": [
"scalar",
{
"key": "value"
},
{
"dict": {
"a": "1",
"b": 2,
"3": [
"one",
2
],
"key with spaces": "value with spaces"
},
"nested": [
{
"leaf": "value"
}
]
}
]
}
3 changes: 3 additions & 0 deletions atest/testdata/variables/valid2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"valid 2": "imported successfully"
}
3 changes: 3 additions & 0 deletions atest/testresources/res_and_var_files/cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"json file from cli": "woot!"
}
3 changes: 3 additions & 0 deletions atest/testresources/res_and_var_files/cli2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"json file from cli2": "kewl!"
}
3 changes: 3 additions & 0 deletions atest/testresources/res_and_var_files/pythonpath.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"json file in pythonpath": true
}
2 changes: 1 addition & 1 deletion src/robot/running/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
class Namespace:
_default_libraries = ('BuiltIn', 'Reserved', 'Easter')
_library_import_by_path_ends = ('.py', '/', os.sep)
_variables_import_by_path_ends = _library_import_by_path_ends + ('.yaml', '.yml')
_variables_import_by_path_ends = _library_import_by_path_ends + ('.yaml', '.yml') + ('.json',)

def __init__(self, variables, suite, resource, languages):
LOGGER.info(f"Initializing namespace for suite '{suite.longname}'.")
Expand Down
27 changes: 27 additions & 0 deletions src/robot/variables/filesetter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import inspect
import io
import json
try:
import yaml
except ImportError:
Expand Down Expand Up @@ -43,6 +44,8 @@ def _import_if_needed(self, path_or_variables, args=None):
% (path_or_variables, args))
if path_or_variables.lower().endswith(('.yaml', '.yml')):
importer = YamlImporter()
elif path_or_variables.endswith(('json',)):
sunday2 marked this conversation as resolved.
Show resolved Hide resolved
importer = JsonImporter()
else:
importer = PythonImporter()
try:
Expand Down Expand Up @@ -147,3 +150,27 @@ def _validate(self, name, value):
if name[0] == '&' and not is_dict_like(value):
raise DataError("Invalid variable '%s': Expected dict-like value, "
"got %s." % (name, type_name(value)))


class JsonImporter:
def import_variables(self, path, args=None):
if args:
raise DataError('JSON variable files do not accept arguments.')
variables = self._import(path)
return [('${%s}' % name, self._dot_dict(value))
for name, value in variables]

def _import(self, path):
with io.open(path, encoding='UTF-8') as stream:
sunday2 marked this conversation as resolved.
Show resolved Hide resolved
variables = json.load(stream)
if not is_dict_like(variables):
raise DataError('JSON variable file must be a mapping, got %s.'
% type_name(variables))
return variables.items()

def _dot_dict(self, value):
if is_dict_like(value):
return DotDict((k, self._dot_dict(v)) for k, v in value.items())
if is_list_like(value):
return [self._dot_dict(v) for v in value]
return value
2 changes: 1 addition & 1 deletion src/robot/variables/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def as_dict(self, decoration=True):


class GlobalVariables(Variables):
_import_by_path_ends = ('.py', '/', os.sep, '.yaml', '.yml')
_import_by_path_ends = ('.py', '/', os.sep, '.yaml', '.yml', '.json')

def __init__(self, settings):
super().__init__()
Expand Down