geändert: .gitignore

geändert:       README.md
	geändert:       pyproject.toml
	geändert:       src/config_parser/__init__.py
	neue Datei:     src/config_parser/_configuration.py
	neue Datei:     src/config_parser/exceptions.py
	neue Datei:     src/config_parser/ini.py
	neue Datei:     src/config_parser/json.py
	neue Datei:     src/config_parser/parse/ini.py
	neue Datei:     src/config_parser/parse/json.py
	neue Datei:     tests/ini/test_ini.py
	neue Datei:     tests/json/test.json
	neue Datei:     tests/json/test_json.py
	neue Datei:     tests/json/test_parser.py
	neue Datei:     tests/test_configuration_class.py
This commit is contained in:
2026-02-07 15:28:28 +01:00
parent d34c40e52a
commit 467d0418ff
15 changed files with 1937 additions and 54 deletions
+4 -1
View File
@@ -1,2 +1,5 @@
dist/ dist/
src/*.egg-info/ src/*.egg-info/
.vscode/
.pytest_cache/
__pycache__/
+45 -6
View File
@@ -1,6 +1,6 @@
# config-parser # config-parser
A library to parse configuration files. Currently it supports only INI format, but support for other formats is planned. A library to parse configuration files.
## Installation ## Installation
You can install the library using pip: You can install the library using pip:
@@ -10,20 +10,24 @@ pip install config-parser --index-url https://jcloud-services.ddns.net/simple/
``` ```
## Usage ## Usage
Here's a simple example of how to use the `config-parser` library to read an INI configuration file: Here are a few simple examples of how to use the `config-parser` library to read an configuration files:
### INI Configuration
```python ```python
from config_parser import parse_ini from config_parser.ini import INIConfiguration
with open('config.conf', 'r') as file: with open('config.conf', 'r') as file:
config_content = file.read() config_content = file.read()
parsed = parse_ini(config_content) parsed = INIConfiguration.from_string(config_content)
print(parsed) print(dict(parsed))
``` ```
If the configuration file content is: If the configuration file content is:
```ini ```ini
global1=global value1
global2=global value2
[section1] [section1]
key1=value1 key1=value1
key2=value2 key2=value2
@@ -35,4 +39,39 @@ hello=world
key=value key=value
``` ```
the result will be `{'section1': {'key1': 'value1', 'key2': 'value2'}, 'section2': {'hello': 'world'}, 'section3': {'key': 'value'}}` the result will be `{'section1': {'key1': 'value1', 'key2': 'value2'}, 'section2': {'hello': 'world'}, 'section3': {'key': 'value'}}`
### JSON Configuration
```python
from config_parser.json import JSONConfiguration
with open('config.json', 'r') as file:
config_content = file.read()
parsed = JSONConfiguration.from_string(config_content)
print(dict(parsed))
```
If the configuration file content is:
```json
{
"section1": {
"key1": "value1",
"key2": "value2",
"number": 42,
"number2": 3.14,
"number3": -1,
"boolean": true,
"boolean2": false,
"null": null
},
"section2": {
"hello": "world"
},
"section3": {
"key": "value"
}
}
```
the result will be `{'section1': {'key1': 'value1', 'key2': 'value2', 'number': 42, 'number2': 3.14, 'number3': -1, 'boolean': True, 'boolean2': False, 'null': None}, 'section2': {'hello': 'world'}, 'section3': {'key': 'value'}}`
+1 -1
View File
@@ -4,5 +4,5 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "config-parser" name = "config-parser"
version = "0.1.0" version = "1.0.0"
description = "A configuration file parser." description = "A configuration file parser."
+11 -46
View File
@@ -1,49 +1,14 @@
QUOTATION_MARKS = {'"', '\''} '''
KEY_VALUE_LINE = 0 A library for parsing configuration files in various formats.
GROUP_HEADER_LINE = 1
COMMENT_LINE = 2
def reverse_dict(d): Modules:
return {k: v for k, v in list(reversed(d.items()))} - configuration: Base classes.
- ini: INI file parser and serializer.
- exceptions: Custom exceptions for configuration parsing.
'''
def parse_key_value_line(line): from ._configuration import Configuration
if '=' not in line:
return {'': ''}
return {line.split('=')[0].strip(): '='.join(line.split('=')[1:]).strip()}
__all__ = [
def parse_group_header(line): 'Configuration'
if line.startswith('[') and line.endswith(']'): ]
return line[1:-1]
def parse_line_type(line):
line = line.strip()
if line.startswith('#'):
return COMMENT_LINE
if line.startswith('[') and line.endswith(']'):
return GROUP_HEADER_LINE
if '=' in line:
return KEY_VALUE_LINE
def compress_conf(conf):
while '\n\n' in conf:
conf = conf.replace('\n\n', '\n')
return conf.strip()
def parse_ini(conf):
'''Parses INI configuration from a string and returns a nested dictionary.'''
conf = compress_conf(conf)
result = {}
current_group = {}
for line in list(reversed(conf.split('\n'))):
line = line.strip()
if parse_line_type(line) == COMMENT_LINE:
continue
if parse_line_type(line) == GROUP_HEADER_LINE:
result[parse_group_header(line)] = reverse_dict(current_group)
current_group = {}
if parse_line_type(line) == KEY_VALUE_LINE:
current_group = current_group | parse_key_value_line(line)
return reverse_dict(result)
+40
View File
@@ -0,0 +1,40 @@
import copy
class Configuration:
def __init__(self, _config: dict = {}):
'''A base class for configurations.'''
self._config = copy.deepcopy(_config)
def __getattribute__(self, name):
_config = super().__getattribute__('_config')
if name.startswith('_') or name in self.__dir__():
return super().__getattribute__(name)
else:
if name not in _config:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
return _config[name]
def __setattr__(self, name, value):
if name.startswith('_') or name in self.__dir__():
super().__setattr__(name, value)
else:
self._config[name] = value
def __delattr__(self, name):
if name.startswith('_') or name in self.__dir__():
super().__delattr__(name)
else:
del self._config[name]
def __setitem__(self, key, value):
self._config[key] = value
def __getitem__(self, key):
return self._config[key]
def __delitem__(self, key):
del self._config[key]
def __iter__(self):
return iter(self._config.items())
+25
View File
@@ -0,0 +1,25 @@
class ConfigurationSyntaxError(SyntaxError): ...
class EscapeSequenceSyntaxError(ConfigurationSyntaxError): ...
class INISyntaxError(ConfigurationSyntaxError): ...
class INIInvalidGroupHeader(INISyntaxError): ...
class INIInvalidKeyValueLine(INISyntaxError): ...
class JSONSyntaxError(ConfigurationSyntaxError): ...
class JSONValueSyntaxError(JSONSyntaxError): ...
class JSONNumberSyntaxError(JSONSyntaxError): ...
class JSONStringSyntaxError(JSONSyntaxError): ...
class JSONBooleanSyntaxError(JSONSyntaxError): ...
class JSONNullSyntaxError(JSONSyntaxError): ...
class JSONArraySyntaxError(JSONSyntaxError): ...
class JSONObjectSyntaxError(JSONSyntaxError): ...
class ConfigurationTypeError(TypeError): ...
class JSONTypeError(ConfigurationTypeError): ...
+87
View File
@@ -0,0 +1,87 @@
import sys, os; sys.path = [os.path.dirname(__file__)] + sys.path
import typing
from ._configuration import Configuration
from .parse.ini import COMMENT_PREFIXES, QUOTATION_MARKS, parse_ini
class INIConfiguration(Configuration):
def __iter__(self):
return iter({k: dict(v) if isinstance(v, INIConfigurationGroup) else v for k, v in self._config.items()}.items())
@classmethod
def from_string(cls, data: str | bytes, comment_prefixes: typing.Collection[str] = COMMENT_PREFIXES, quotation_marks: typing.Collection[str] = QUOTATION_MARKS, ignore_errors: bool = False):
'''
Parses INI configuration from a string and returns an instance of INIConfiguration.
:param cls: The class method is called on.
:type cls: typing.Type[INIConfiguration]
:param data: The INI configuration data to parse.
:type data: str | bytes
:param comment_prefixes: A collection of prefixes that denote comments in the INI file.
:type comment_prefixes: typing.Collection[str]
:param quotation_marks: A collection of characters used for quoting values in the INI file.
:type quotation_marks: typing.Collection[str]
:param ignore_errors: If True, parsing errors will be ignored.
:type ignore_errors: bool
:raises INISyntaxError: If there is a syntax error in the INI configuration and ``ignore_errors`` is ``False``.
:return: An instance of INIConfiguration representing the parsed INI configuration.
:rtype: INIConfiguration
'''
if isinstance(data, bytes):
data = data.decode()
configuration = cls()
for group_name, content in parse_ini(data, comment_prefixes=comment_prefixes, quotation_marks=quotation_marks, ignore_errors=ignore_errors).items():
group = INIConfigurationGroup(group_name)
for key, value in content.items():
group[key] = value
configuration[group_name] = group
return configuration
def to_string(self, separator: str = '='):
'''
Serializes the INIConfiguration instance to an INI formatted string.
:param separator: The separator to use between keys and values in the output string.
:type separator: str
:return: An INI formatted string representing the INIConfiguration instance.
:rtype: str
'''
result = ''
for key, value in self._config.items():
if isinstance(value, INIConfigurationGroup):
result += value.to_string(separator=separator)
else:
result += f'{key}{separator}{value}\n'
return result.strip()
class INIConfigurationGroup(INIConfiguration):
def __init__(self, name: str):
'''
A class representing a group in an INI configuration.
:param name: The name of the group.
:type name: str
'''
super().__init__()
self._name = name
@property
def name(self):
return self._name
def to_string(self, separator: str = '='):
'''
Serializes the INIConfigurationGroup instance to an INI formatted string.
:param separator: The separator to use between keys and values in the output string.
:type separator: str
:return: An INI formatted string representing the INIConfigurationGroup instance.
:rtype: str
'''
return ('[' + self._name + ']\n' + super().to_string(separator=separator)).strip()
+58
View File
@@ -0,0 +1,58 @@
import sys, os; sys.path = [os.path.dirname(__file__)] + sys.path
import typing
from ._configuration import Configuration
from .parse.json import parse_json, parse_type, JSONObject, JSONString, JSONNumber, JSONNull, JSONBoolean, JSONArray
from .exceptions import JSONTypeError
def _configuration(data):
if isinstance(data, str):
return JSONString(data)
if isinstance(data, (int, float)):
return JSONNumber(data)
if data is None:
return JSONNull(None)
if isinstance(data, bool):
return JSONBoolean(data)
if isinstance(data, list):
return JSONArray(data)
if isinstance(data, dict):
return JSONObject(data)
class JSONConfiguration(Configuration):
def __iter__(self) -> iter:
return iter({k: v.value for k, v in self._config.items()}.items())
@classmethod
def from_string(cls, data: str | bytes, ignore_errors: bool = False):
'''
Parses JSON configuration from a string and returns an instance of JSONConfiguration.
:param data: The JSON data
:type data: str
:param ignore_errors: If True, errors will be ignored.
:type ignore_errors: bool
:raises JSONValueSyntaxError: If a value is invalid and ``ignore_errors`` is ``False``.
:raises JSONObjectSyntaxError: If an object is invalid and ``ignore_errors`` is ``False``.
:raises JSONArraySyntaxError: If an array is invalid and ``ignore_errors`` is ``False``.
:raises JSONStringSyntaxError: If a string is invalid and ``ignore_errors`` is ``False``.
:raises JSONNullSyntaxError: If a null value is invalid and ``ignore_errors`` is ``False``.
:raises JSONBooleanSyntaxError: If a null value is invalid and ``ignore_errors`` is ``False``.
:raises JSONNumberSyntaxError: If a number is invalid and ``ignore_errors`` is ``False``.
:raises EscapeSequenceSyntaxError: If an escape sequence is invalid and ``ignore_errors`` is ``False``.
:return: An instance of JSONConfiguration representing the parsed JSON configuration
:rtype: JSONConfiguration
'''
try:
data = parse_type(data).parse(data)
if isinstance(data, JSONObject):
return cls({k: _configuration(v) for k, v in data.value.items()})
elif not ignore_errors:
raise JSONTypeError(f'expected object, got {data._type}. Use config_parser.parse.json.parse_json to parse JSONs.')
except:
if ignore_errors:
return cls()
else:
raise
+157
View File
@@ -0,0 +1,157 @@
import typing
from ..exceptions import INIInvalidGroupHeader, INIInvalidKeyValueLine, INISyntaxError
QUOTATION_MARKS = ['"', "'"]
COMMENT_PREFIXES = ['#', ';']
KEY_VALUE_LINE = 0
GROUP_HEADER_LINE = 1
COMMENT_LINE = 2
def reverse_dict(d: typing.Dict) -> typing.Dict:
'''
Reverses the order of items in a dictionary.
:param d: The dictionary to reverse.
:type d: typing.Dict
:return: A new dictionary with the items in reversed order.
:rtype: typing.Dict
'''
return {k: v for k, v in list(reversed(d.items()))}
def parse_key_value_line(line: str, comment_prefixes: typing.Collection[str] = COMMENT_PREFIXES, quotation_marks: typing.Collection[str] = QUOTATION_MARKS, ignore_errors: bool = False) -> typing.Dict[str, str]:
'''
Parses a key-value line.
:param line: The key-value-line
:type line: str
:param comment_prefixes: A collection of prefixes that denote comments in the INI file.
:type comment_prefixes: typing.Collection[str]
:param quotation_marks: A collection of characters used for quoting values in the INI file.
:type quotation_marks: typing.Collection[str]
:param ignore_errors: If True, errors are ignored.
:type ignore_errors: bool
:raises INIInvalidKeyValueLine: If the key-value line is invalid and ignore_errors is False.
:return: A dictionary with the key and the value (`{'key': 'value'}`) or an empty dictionary if an error has occured and ignore_errors is True.
:rtype: Dict[str, str]
'''
separator = None
if ':' in line and '=' not in line:
separator = ':'
if '=' in line and ':' not in line:
separator = '='
if ':' in line and '=' in line:
separator = '=' if line.index('=') < line.index(':') else ':'
if separator is None:
if ignore_errors:
return {}
raise INIInvalidKeyValueLine('the key-value line is invalid')
key = line.split(separator)[0].strip()
value = separator.join(line.split(separator)[1:]).strip()
if any(value.endswith(mark) and not value.startswith(mark) for mark in quotation_marks):
if ignore_errors:
return {}
raise INISyntaxError('unterminated literal')
if any(value.startswith(mark) for mark in quotation_marks):
if value[0] == value[-1]:
value = value[1:-1]
else:
if ignore_errors:
return {}
raise INISyntaxError('unterminated literal')
else:
for prefix in comment_prefixes:
if prefix in value:
value = value.split(prefix)[0].strip()
return {key: value}
def parse_group_header(line: str, ignore_errors: bool = False) -> str:
'''
Parses a group header line.
:param line: The group header line to parse.
:type line: str
:param ignore_errors: If True, errors are ignored.
:type ignore_errors: bool
:raises INIInvalidGroupHeader: If the group header is invalid and ignore_errors is False.
:return: The name of the group.
:rtype: str
'''
if line.startswith('[') and line.endswith(']') and len(line) > 2:
return line[1:-1]
else:
if ignore_errors:
return ''
raise INIInvalidGroupHeader('the header is invalid')
def parse_line_type(line: str, comment_prefixes: typing.Collection[str] = COMMENT_PREFIXES, ignore_errors: bool = False) -> int:
'''
Returns the type of a line.
:param line: The line to check.
:type line: str
:param comment_prefixes: A collection of prefixes that denote comments in the INI file.
:type comment_prefixes: typing.Collection[str]
:param ignore_errors: If True, errors are ignored.
:type ignore_errors: bool
:raises INISyntaxError: If the line is invalid and ignore_errors is False.
:return: The type of the line (COMMENT_LINE, GROUP_HEADER_LINE, or KEY_VALUE_LINE).
:rtype: int
'''
line = line.strip()
if any(line.startswith(prefix) for prefix in comment_prefixes) or not line.strip():
return COMMENT_LINE
elif line.startswith('[') and line.endswith(']') and len(line) > 2:
return GROUP_HEADER_LINE
elif '=' in line or ':' in line:
return KEY_VALUE_LINE
else:
if ignore_errors:
return COMMENT_LINE
raise INISyntaxError('the line is invalid')
def compress_conf(conf):
while '\n\n' in conf:
conf = conf.replace('\n\n', '\n')
return conf.strip()
def parse_ini(conf, comment_prefixes=COMMENT_PREFIXES, quotation_marks=QUOTATION_MARKS, ignore_errors: bool = False, global_group: bool = True) -> typing.Dict[str, typing.Dict[str, str]]:
'''Parses INI configuration from a string and returns a nested dictionary.
:param conf: The INI configuration string to parse.
:param comment_prefixes: A collection of prefixes that denote comments in the INI file.
:param quotation_marks: A collection of characters used for quoting values in the INI file.
:param ignore_errors: If True, parsing errors will be ignored.
:param global_group: If True, key-value pairs outside of any group will be included. Otherwise, an exception will be raised.
:return: A nested dictionary representing the parsed INI configuration.
:raises INISyntaxError: If there is a syntax error in the INI configuration and ignore_errors is False.
:rtype: typing.Dict[str, typing.Dict[str, str]]
'''
conf = compress_conf(conf)
result = {}
current_group = {}
for line in list(reversed(conf.split('\n'))):
line = line.strip()
if parse_line_type(line, comment_prefixes=comment_prefixes, ignore_errors=ignore_errors) == COMMENT_LINE:
continue
if parse_line_type(line, comment_prefixes=comment_prefixes, ignore_errors=ignore_errors) == GROUP_HEADER_LINE:
result[parse_group_header(line, ignore_errors=ignore_errors)] = reverse_dict(current_group)
current_group = {}
if parse_line_type(line, comment_prefixes=comment_prefixes, ignore_errors=ignore_errors) == KEY_VALUE_LINE:
current_group = current_group | parse_key_value_line(line, comment_prefixes=comment_prefixes, quotation_marks=quotation_marks, ignore_errors=ignore_errors)
return reverse_dict(result)
+589
View File
@@ -0,0 +1,589 @@
from ..exceptions import EscapeSequenceSyntaxError, JSONValueSyntaxError, JSONNumberSyntaxError, JSONStringSyntaxError, JSONBooleanSyntaxError, JSONNullSyntaxError, JSONArraySyntaxError, JSONObjectSyntaxError, JSONTypeError
import typing
import re
JSON_ESCAPE_SEQUENCES = {
'"': '"',
'\\': '\\',
'/': '/',
'b': '\b',
'f': '\f',
'n': '\n',
'r': '\r',
't': '\t',
}
_JSON_NUMBER_RE = re.compile(
r"""
-? # optional minus
(?:0|[1-9][0-9]*) # integer part
(?:\.[0-9]+)? # optional fraction
(?:[eE][+-]?[0-9]+)? # optional exponent
\Z
""",
re.VERBOSE,
)
def parse_escape_sequences(string: str) -> bool:
'''
Returns the unescaped string.
:param string: The string to validate
:type string: str
:return: The unescaped string
:rtype: str
'''
result = ''
unicode_escape_sequence = None
escape_sequence = False
for c in string:
if c == '\\' and not escape_sequence:
escape_sequence = True
continue
if unicode_escape_sequence is not None:
if len(unicode_escape_sequence) == 4:
result += chr(int(unicode_escape_sequence, 16))
unicode_escape_sequence = None
else:
if c.lower() not in '0123456789abcdef':
raise EscapeSequenceSyntaxError(f'Invalid unicode escape sequence in JSON string: \\u{unicode_escape_sequence}{c}')
unicode_escape_sequence += c
if not escape_sequence and not unicode_escape_sequence:
result += c
if escape_sequence:
escape_sequence = False
if c not in ('"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u'):
raise EscapeSequenceSyntaxError(f'Invalid escape sequence in JSON string: \\{c}')
if c == 'u':
unicode_escape_sequence = ''
else:
result += JSON_ESCAPE_SEQUENCES[c]
if unicode_escape_sequence:
raise EscapeSequenceSyntaxError(f'Invalid unicode escape sequence in JSON string: \\u{unicode_escape_sequence}')
return result
class JSONType:
def __init__(self, value):
self.value = value
def __repr__(self):
return self.value.__repr__()
class JSONNumber(JSONType):
@property
def _type(self):
return 'number'
@classmethod
def parse(cls, value: str) -> typing.Union[int, float]:
'''
Parses a JSON number value.
:param value: The value
:type value: str
:raises JSONNumberSyntaxError: If the number is not a valid JSON number.
:raises JSONTypeError: If the value is not a number.
:return: The number in ``value``
:rtype: JSONNumber
'''
value = value.strip()
if not isinstance(value, str):
raise TypeError('JSON numbers must be provided as strings')
if not _JSON_NUMBER_RE.match(value):
raise JSONNumberSyntaxError(f'Invalid JSON number: {value!r}')
if '.' not in value and 'e' not in value and 'E' not in value:
return cls(int(value))
return cls(float(value))
class JSONString(JSONType):
@property
def _type(self):
return 'string'
@classmethod
def parse(cls, value: str):
'''
Parses a JSON string value.
:param value: The value
:type value: str
:raises JSONStringSyntaxError: If the string is not a valid JSON string.
:return: The string in ``value``
:rtype: JSONString
'''
value = value.strip()
if not (value[0] == '"' and value[-1] == '"' and value.count('"') - value.count('\\"') == 2):
raise JSONStringSyntaxError(f'Invalid JSON string (JSON string have to start and end with quotation marks): {value}')
else:
return cls(parse_escape_sequences(value[1:-1]))
class JSONBoolean(JSONType):
@property
def _type(self):
return 'boolean'
@classmethod
def parse(cls, value: str):
'''
Parses a JSON boolean value.
:param value: The value
:type value: str
:raises JSONBooleanSyntaxError: If the boolean is neither ``true`` nor ``false``.
:return: The boolean in ``value``
:rtype: JSONBoolean
'''
value = value.strip()
if value in ('true', 'false'):
return cls(True if value == 'true' else False)
else:
raise JSONBooleanSyntaxError(f'Invalid JSON boolean: {value}')
class JSONNull(JSONType):
@property
def _type(self):
return 'null'
@classmethod
def parse(cls, value: str):
'''
Parses a JSON null value.
:param value: The value
:type value: str
:raises JSONNullSyntaxError: If the null value is not ``null``.
:return: The null value
:rtype: JSONNull
'''
value = value.strip()
if value == 'null':
return cls(None)
else:
raise JSONNullSyntaxError(f'Invalid JSON null value: {value}')
class JSONArray(JSONType):
@property
def _type(self):
return 'array'
@classmethod
def parse(cls, value: str):
'''
Parses a JSON array.
:param value: The array
:type value: str
:raises JSONArraySyntaxError: If the array is invalid.
:raises JSONValueSyntaxError: If a value is invalid
:raises JSONObjectSyntaxError: If an object is invalid.
:raises JSONStringSyntaxError: If a string is invalid.
:raises JSONNullSyntaxError: If a null value is invalid.
:raises JSONBooleanSyntaxError: If a null value is invalid.
:raises JSONNumberSyntaxError: If a number is invalid.
:raises EscapeSequenceSyntaxError: If an escape sequence is invalid.
:return: The array as a dictionary
:rtype: dict
'''
value = value.strip()
value_type = parse_type(value)
if value_type != JSONArray:
raise JSONValueSyntaxError('expected an array, got', value_type._type)
string = False
arrays = 0
objects = 0
current_value = ''
escape_sequence = False
_array = []
for c in value[1:-1]:
if c == '"' and not escape_sequence:
string = not string
if c == '[' and not string:
arrays += 1
if c == ']' and not string:
arrays -= 1
if c == '{' and not string:
objects += 1
if c == '}' and not string:
objects -= 1
current_value += c
if string and c == '\\':
if not escape_sequence:
escape_sequence = True
else:
escape_sequence = False
else:
escape_sequence = False
if c == ',' and not string and not arrays and not objects:
_array.append(current_value[:-1])
current_value = ''
if current_value:
_array.append(current_value)
array = []
for e in _array:
array.append(parse_type(e).parse(e).value)
return cls(array)
class JSONObject(JSONType):
@property
def _type(self):
return 'object'
@classmethod
def parse(cls, value: str) -> str:
'''
Parses a JSON object.
:param value: The value
:type value: str
:raises JSONObjectSyntaxError: If the object is invalid.
:raises JSONValueSyntaxError: If a value is invalid
:raises JSONArraySyntaxError: If an array is invalid.
:raises JSONStringSyntaxError: If a string is invalid.
:raises JSONNullSyntaxError: If a null value is invalid.
:raises JSONBooleanSyntaxError: If a null value is invalid.
:raises JSONNumberSyntaxError: If a number is invalid.
:raises EscapeSequenceSyntaxError: If an escape sequence is invalid.
:return: The object as a dictionary
:rtype: dict
'''
value = value.strip()
value_type = parse_type(value)
if value_type != JSONObject:
raise JSONValueSyntaxError('expected an object, got', value._type)
string = False
current_key = ''
current_value = None
arrays = 0
objects = 0
escape_sequence = False
_object = {}
for c in value[1:-1]:
if c == '"' and not escape_sequence:
string = not string
if c == '[' and not string:
arrays += 1
if c == ']' and not string:
arrays -= 1
if c == '{' and not string:
objects += 1
if c == '}' and not string:
objects -= 1
if current_value is None:
current_key += c
else:
current_value += c
if string and c == '\\':
if not escape_sequence:
escape_sequence = True
else:
escape_sequence = False
else:
escape_sequence = False
if not string and c == ':' and not arrays and not objects:
current_key = current_key[:-1]
current_value = ''
if not string and c == ',' and not arrays and not objects:
if current_value is not None:
_object[current_key] = current_value[:-1]
current_key = ''
current_value = None
else:
raise JSONObjectSyntaxError(f'expected \':\'')
if current_key:
if current_value is not None:
_object[current_key] = current_value
current_key = ''
current_value = None
else:
raise JSONObjectSyntaxError(f'expected value')
json_object = {}
for k, v in _object.items():
json_object[parse_type(k).parse(k).value] = parse_type(v).parse(v).value
return cls(json_object)
def parse_type(value: str) -> typing.Type[JSONType]:
'''
Parses the type of the value.
:param value: The value
:type value: str
:raises JSONValueSyntaxError: If a value is invalid
:raises JSONObjectSyntaxError: If an object is invalid.
:raises JSONArraySyntaxError: If an array is invalid.
:raises JSONStringSyntaxError: If a string is invalid.
:raises JSONNullSyntaxError: If a null value is invalid.
:raises JSONBooleanSyntaxError: If a null value is invalid.
:raises JSONNumberSyntaxError: If a number is invalid.
:raises EscapeSequenceSyntaxError: If an escape sequence is invalid.
:return: The JSON type
:rtype: type
'''
value = value.strip()
if not value:
raise JSONValueSyntaxError('the value cannot be empty')
if value[0] == '{':
if value[-1] != '}':
raise JSONArraySyntaxError(f'an object has to be closed with \'}}\', but found \'{value[-1]}\'')
string = False
current_key = ''
current_value = None
arrays = 0
objects = 0
escape_sequence = False
for c in value[1:-1]:
terminating_quotation_mark = False
if c == '"' and not escape_sequence:
if string:
terminating_quotation_mark = True
string = not string
if c == '[' and not string:
arrays += 1
if c == ']' and not string:
arrays -= 1
if arrays < 0:
raise JSONObjectSyntaxError('unexpected char: \']\'')
if c == '{' and not string:
objects += 1
if c == '}' and not string:
objects -= 1
if objects < 0:
raise JSONObjectSyntaxError('unexpected char: \'}\'')
if not string and not terminating_quotation_mark and not arrays and not objects:
if c.strip().lower() not in '0123456789abcdefghijklmnopqrstuvwxyz,:[]{}.-' + ('"' if escape_sequence else ''):
raise JSONObjectSyntaxError(f'unexpected char: \'{c}\'')
if current_value is None:
current_key += c
else:
current_value += c
if string and c == '\\':
if not escape_sequence:
escape_sequence = True
else:
escape_sequence = False
else:
escape_sequence = False
if not string and c == ':' and not arrays and not objects:
current_key = current_key[:-1]
if current_value is None:
current_value = ''
else:
raise JSONObjectSyntaxError(f'expected \',\'')
if not string and c == ',' and not arrays and not objects:
if current_value is not None:
if parse_type(current_key) != JSONString:
raise JSONObjectSyntaxError(f'keys have to be strings')
parse_type(current_value[:-1])
current_key = ''
current_value = None
else:
raise JSONObjectSyntaxError(f'expected \':\'')
if current_key:
if current_value is not None:
if parse_type(current_key) != JSONString:
raise JSONObjectSyntaxError(f'keys have to be strings')
parse_type(current_value)
current_key = ''
current_value = None
else:
raise JSONObjectSyntaxError(f'expected value')
if string:
raise JSONStringSyntaxError('unterminated string literal')
if value[-2] == ',':
raise JSONObjectSyntaxError('expected value after \',\'')
return JSONObject
elif value[0] == '[':
if value[-1] != ']':
raise JSONArraySyntaxError(f'an array has to be closed with \']\', but found \'{value[-1]}\'')
string = False
arrays = 0
objects = 0
current_value = ''
escape_sequence = False
for c in value[1:-1]:
terminating_quotation_mark = False
if c == '"' and not escape_sequence:
if string:
terminating_quotation_mark = True
string = not string
if c == '[' and not string:
arrays += 1
if c == ']' and not string:
arrays -= 1
if arrays < 0:
raise JSONArraySyntaxError('unexpected char: \']\'')
if c == '{' and not string:
objects += 1
if c == '}' and not string:
objects -= 1
if objects < 0:
raise JSONArraySyntaxError('unexpected char: \'}\'')
if (not string) and (not terminating_quotation_mark) and (not arrays) and (not objects):
if c.strip().lower() not in '0123456789abcdefghijklmnopqrstuvwxyz,[]{}.-' + ('"' if escape_sequence else ''):
raise JSONArraySyntaxError(f'unexpected char: \'{c}\'')
current_value += c
if string and c == '\\':
if not escape_sequence:
escape_sequence = True
else:
escape_sequence = False
else:
escape_sequence = False
if c == ',' and not string and not arrays and not objects:
parse_type(current_value[:-1])
current_value = ''
if string:
raise JSONStringSyntaxError('unterminated string literal')
if value[-2] == ',':
raise JSONArraySyntaxError('expected value after \',\'')
if current_value:
parse_type(current_value)
return JSONArray
elif value[0] == '"': # string
if value[0] != '"':
raise JSONStringSyntaxError(f'expected \'"\' as first char, got \'{value[0]}\'')
if '\n' in value:
raise JSONStringSyntaxError(f'line feeds are not allowed inside of strings. Use escape sequences.')
if value.count('"') < 2:
raise JSONStringSyntaxError('unterminated string literal')
if value.count('"') - value.count('\\"') > 2 or value[-1] != '"':
raise JSONStringSyntaxError(f'unexpected token: \'{"\"".join(value.split("\"")[2:])}\'')
parse_escape_sequences(value)
return JSONString
elif value in ('null', 'false', 'true'): # null or boolean
if value == 'null': # null
return JSONNull
else: # boolean
return JSONBoolean
elif _JSON_NUMBER_RE.match(value): # number
try:
float(value)
return JSONNumber
except:
raise JSONNumberSyntaxError(f'invalid number: {value}')
else:
raise JSONValueSyntaxError(f'unexpected token: \'{value}\'')
def parse_json(json: str) -> dict | list | str | int | float | None | bool:
'''
Parses the JSON.
:param json: The JSON data
:type json: str
:raises JSONValueSyntaxError: If a value is invalid
:raises JSONObjectSyntaxError: If an object is invalid.
:raises JSONArraySyntaxError: If an array is invalid.
:raises JSONStringSyntaxError: If a string is invalid.
:raises JSONNullSyntaxError: If a null value is invalid.
:raises JSONBooleanSyntaxError: If a null value is invalid.
:raises JSONNumberSyntaxError: If a number is invalid.
:raises EscapeSequenceSyntaxError: If an escape sequence is invalid.
:return: The JSON as a Python object (``dict``, ``list``, ``str``, ``int``/``float``, ``boolean``, ``None``)
:rtype: dict | list | str | int | float | boolean | None
'''
json_type = parse_type(json)
return json_type.parse(json).value
+46
View File
@@ -0,0 +1,46 @@
from src.config_parser.ini import INIConfiguration, INIConfigurationGroup
from src.config_parser.exceptions import INISyntaxError
def test_generating():
config = INIConfiguration()
config.test_group = INIConfigurationGroup('test_group')
config.test_group.key1 = 'value1'
config.test_group.key2 = 'value2'
assert config.to_string() == '''[test_group]
key1=value1
key2=value2'''
def test_parsing():
# Test deserializer
config_string = '''
property1 = 42
property2=123
[configuration_group1]
hello=world
key1=value1
[group2]
world=hel#lo
key2 = "val#ue2"
[]
hello2=world2
invalid_line1
invalid_line2
# comment'''
assert dict(INIConfiguration.from_string(config_string, ignore_errors=True)) == {'configuration_group1': {'hello': 'world', 'key1': 'value1'}, 'group2': {'world': 'hel', 'key2': 'val#ue2', 'hello2': 'world2'}}
try:
INIConfiguration.from_string('key="value')
assert False, 'Expected INISyntaxError for unterminated literal'
except INISyntaxError:
pass
try:
INIConfiguration.from_string('key=value"')
assert False, 'Expected INISyntaxError for unterminated literal'
except INISyntaxError:
pass
+168
View File
@@ -0,0 +1,168 @@
{
"level_1": {
"meta": {
"version": "1.0",
"generated": true,
"tags": ["test", "deep", "nested", "json"]
},
"level_2": {
"array": [
{
"id": 1,
"level_3": {
"level_4": {
"level_5": {
"config": {
"enabled": true,
"thresholds": {
"low": 0.1,
"medium": 0.5,
"high": 0.9
},
"modes": [
{
"name": "alpha",
"params": {
"retry": 3,
"timeout": {
"connect": 1000,
"read": 5000,
"deep": {
"even_deeper": {
"flag": false,
"notes": [
"still",
"going",
{
"deeper": {
"than": {
"most": {
"humans": {
"expect": {
"value": 42
}
}
}
}
}
}
]
}
}
}
}
}
]
}
}
}
}
}
],
"level_2_object": {
"a": {
"b": {
"c": {
"d": {
"e": {
"f": {
"g": {
"h": {
"i": {
"j": {
"k": "bottom"
}
}
}
}
}
}
}
}
}
}
}
}
},
"level3": {
"root": [
[
[
[
{
"a": [
{
"b": [
{
"c": [
{
"d": [
{ "e": [
{ "f": "bottom"
}
]
}
]
}
]
}
]
}
]
}
]
]
]
]
}
, "4": {
"level": {
"level": {
"level": {
"level": {
"level": {
"items": [
{"x": {"y": {"z": [1,2,3,4,5]}}},
{"x": {"y": {"z": [1,2,3,4,5]}}},
{"x": {"y": {"z": [1,2,3,4,5]}}},
{"x": {"y": {"z": [1,2,3,4,5]}}},
{"x": {"y": {"z": [1,2,3,4,5]}}}
]
}
}
}
}
}
}
,
"0":{
"n0": {
"n1": {
"n2": {
"n3": {
"n4": {
"n5": {
"n6": { "n7": { "n8": { "n9": { "n10": { "n11": { "n12": {
"n13": {
"n14": {
"n15": {
"n16": {
"n17": {
"n18": { "n19": { "n20": "bottom"
} } }
}
} }}
}
}
}
} }
}
}
}
}
}}
}
}
}
}
+209
View File
@@ -0,0 +1,209 @@
from src.config_parser.json import JSONConfiguration
from src.config_parser.exceptions import JSONObjectSyntaxError, JSONValueSyntaxError, JSONStringSyntaxError, JSONArraySyntaxError, EscapeSequenceSyntaxError
def test_json_configuration():
# Test valid JSON configuration parsing
assert dict(JSONConfiguration.from_string('{"key1": "value1", "key2": 42, "key3": true, "key4": {"k1": 42, "k2": [1, null, true]}}')) == {"key1": "value1", "key2": 42, "key3": True, "key4": {"k1": 42, "k2": [1, None, True]}}
assert dict(JSONConfiguration.from_string('''{
"name": "John Doe",
"active": true,
"score": 99.5,
"roles": ["admin", "developer"],
"meta": null
}''')) == {'name': 'John Doe', 'active': True, 'score': 99.5, 'roles': ['admin', 'developer'], 'meta': None}
assert dict(JSONConfiguration.from_string('''{
"items": [],
"config": {},
"enabled": false
}''')) == {'items': [], 'config': {}, 'enabled': False}
assert dict(JSONConfiguration.from_string('''{
"int": 1,
"float": 1.0,
"scientific": 1e3,
"negative": -42
}''')) == {
'int': 1,
'float': 1.0,
'scientific': 1000.0,
'negative': -42
}
assert dict(JSONConfiguration.from_string('''{
"text": "🌍",
"escaped": "Line1\\nLine2\\tTabbed",
"unicode_escape": "\u263A"
}''')) == {
'text': '🌍',
'escaped': 'Line1\nLine2\tTabbed',
'unicode_escape': ''
}
assert dict(JSONConfiguration.from_string('''{"m": [
{ "id": 1, "value": "a" },
{ "id": 2, "value": "b" },
{ "id": 3, "value": null }
]}
''')) == {
'm': [
{'id': 1, 'value': 'a'},
{'id': 2, 'value': 'b'},
{'id': 3, 'value': None},
]
}
assert dict(JSONConfiguration.from_string('''{
"1": "one",
"true": "yes",
"null": "nothing"
}
''')) == {
'1': 'one',
'true': 'yes',
'null': 'nothing'
}
assert dict(JSONConfiguration.from_string('''{
"a": {
"b": {
"c": {
"d": [1, 2, {"e": false}]
}
}
}
}
''')) == {
'a': {
'b': {
'c': {
'd': [1, 2, {'e': False}]
}
}
}
}
assert dict(JSONConfiguration.from_string('''{
"x": 1,
"x": 2,
"x": 3
}
''')) == {'x': 3}
assert dict(JSONConfiguration.from_string('''{"nested": [[[[[[[[[[[[[[[[[[[[0]]]]]]]]]]]]]]]]]]]]}''')) == {'nested': [[[[[[[[[[[[[[[[[[[[0]]]]]]]]]]]]]]]]]]]]}
assert dict(JSONConfiguration.from_string('''{
"id": 9007199254740993
}
''')) == {'id': 9007199254740993}
assert dict(JSONConfiguration.from_string('''{
"value": 0.1
}''')) == {'value': 0.1}
assert dict(JSONConfiguration.from_string('{ "x": null }')) == {'x': None}
assert dict(JSONConfiguration.from_string('{}')) == {}
assert dict(JSONConfiguration.from_string('''{
"a": "",
"b": null
}
''')) == {'a': '', 'b': None}
assert dict(JSONConfiguration.from_string('''{
"char1": "é",
"char2": "e\u0301"
}''')) == {
'char1': 'é',
'char2': ''
}
assert dict(JSONConfiguration.from_string('''{
"text": "hello\u0000world"
}''')) == {'text': 'hello\x00world'}
assert dict(JSONConfiguration.from_string('''{
"enabled": "false"
}
''')) == {'enabled': 'false'}
assert dict(JSONConfiguration.from_string('''{"array": [1, "1", true, null, {}, []]
}''')) == {'array': [1, '1', True, None, {}, []]}
assert dict(JSONConfiguration.from_string('''{ "a" :
[ 1 ,2
,3 ] }
''')) == {'a': [1, 2, 3]}
# Test exceptions
try:
JSONConfiguration.from_string('{"a": 1,}')
assert False, 'Excepted JSONObjectSyntaxError'
except JSONObjectSyntaxError:
pass
try:
JSONConfiguration.from_string('''{
// Comment
"a": 1
}''')
assert False, 'Excepted JSONObjectSyntaxError'
except JSONObjectSyntaxError:
pass
try:
JSONConfiguration.from_string('{"a": 1 /* Comment */}')
assert False, 'Excepted JSONObjectSyntaxError'
except JSONObjectSyntaxError:
pass
try:
JSONConfiguration.from_string('{a: 1}')
assert False, 'Excepted JSONValueSyntaxError'
except JSONValueSyntaxError:
pass
try:
JSONConfiguration.from_string('{\'a\': \'b\'}')
assert False, 'Excepted JSONObjectSyntaxError'
except JSONObjectSyntaxError:
pass
try:
JSONConfiguration.from_string('{"x": 012}')
assert False, 'Excepted JSONValueSyntaxError'
except JSONValueSyntaxError:
pass
try:
JSONConfiguration.from_string('{"x": NaN, "y": Infinity}')
assert False, 'Excepted JSONValueSyntaxError'
except JSONValueSyntaxError:
pass
try:
JSONConfiguration.from_string('{"text": "hello\nworld"}') # This is not a JSON escape sequence!
assert False, 'Excepted JSONStringSyntaxError'
except JSONStringSyntaxError:
pass
try:
JSONConfiguration.from_string('{"x": "\q"}')
assert False, 'Excepted EscapeSequenceSyntaxError'
except EscapeSequenceSyntaxError:
pass
try:
JSONConfiguration.from_string('{"x": "\\u123"}')
assert False, 'Excepted EscapeSequenceSyntaxError'
except EscapeSequenceSyntaxError:
pass
try:
JSONConfiguration.from_string('{ "a": 1 } { "b": 2 }')
assert False, 'Excepted JSONObjectSyntaxError'
except JSONObjectSyntaxError:
pass
try:
JSONConfiguration.from_string('{"a": 1 "b": 2}')
assert False, 'Excepted JSONObjectSyntaxError'
except JSONObjectSyntaxError:
pass
try:
JSONConfiguration.from_string('{"a": [1, 2, 3}')
assert False, 'Excepted JSONArraySyntaxError'
except JSONArraySyntaxError:
pass
try:
JSONConfiguration.from_string('')
assert False, 'Excepted JSONValueSyntaxError'
except JSONValueSyntaxError:
pass
+429
View File
@@ -0,0 +1,429 @@
from src.config_parser.parse.json import parse_escape_sequences, JSONNumber, JSONString, JSONBoolean, JSONNull, JSONArray, JSONObject, parse_type, parse_json
from src.config_parser.exceptions import EscapeSequenceSyntaxError, JSONNumberSyntaxError, JSONStringSyntaxError, JSONBooleanSyntaxError, JSONNullSyntaxError, JSONArraySyntaxError, JSONObjectSyntaxError, JSONValueSyntaxError
import os
def test_parse_escape_sequences():
# Test valid escape sequences
assert parse_escape_sequences(r'Hello\nWorld') == 'Hello\nWorld'
assert parse_escape_sequences(r'Hello\tWorld') == 'Hello\tWorld'
assert parse_escape_sequences(r'Hello\\World') == 'Hello\\World'
assert parse_escape_sequences(r'Hello\"World\"') == 'Hello"World"'
assert parse_escape_sequences(r'Hello\/World') == 'Hello/World'
assert parse_escape_sequences(r'Hello\bWorld') == 'Hello\bWorld'
assert parse_escape_sequences(r'Hello\fWorld') == 'Hello\fWorld'
assert parse_escape_sequences(r'Hello\rWorld') == 'Hello\rWorld'
assert parse_escape_sequences(r'Hello\u0041World') == 'HelloAWorld'
# Test invalid escape sequences
try:
parse_escape_sequences(r'Hello\qWorld')
assert False, 'Expected EscapeSequenceSyntaxError'
except EscapeSequenceSyntaxError:
pass
try:
parse_escape_sequences(r'Hello\u00G1World')
assert False, 'Expected EscapeSequenceSyntaxError'
except EscapeSequenceSyntaxError:
pass
try:
parse_escape_sequences(r'\u123')
assert False, 'Expected EscapeSequenceSyntaxError'
except EscapeSequenceSyntaxError:
pass
def test_parse_number():
# Test parsing integers
assert JSONNumber.parse('0').value == 0
assert JSONNumber.parse('42').value == 42
assert JSONNumber.parse('-42').value == -42
# Test parsing floats
assert JSONNumber.parse('0.0').value == 0.0
assert JSONNumber.parse('3.14').value == 3.14
assert JSONNumber.parse('-3.14').value == -3.14
# Test exceptions
try:
JSONNumber.parse('abc')
assert False, 'Expected JSONNumberSyntaxError'
except JSONNumberSyntaxError:
pass
try:
JSONNumber.parse('01')
assert False, 'Expected JSONNumberSyntaxError'
except JSONNumberSyntaxError:
pass
def test_parse_str():
# Test parsing strings
assert JSONString.parse('"Hello, World!"').value == 'Hello, World!'
assert JSONString.parse(r'"Hello,\nWorld!"').value == 'Hello,\nWorld!'
assert JSONString.parse(r'"Hello,\\World!"').value == 'Hello,\\World!'
# Test exceptions
try:
JSONString.parse('\'Hello, World!\'')
assert False, 'Excepted JSONStringSyntaxError'
except JSONStringSyntaxError:
pass
try:
JSONString.parse('"Hello, World!')
assert False, 'Excepted JSONStringSyntaxError'
except JSONStringSyntaxError:
pass
try:
JSONString.parse('Hello, World!"')
assert False, 'Excepted JSONStringSyntaxError'
except JSONStringSyntaxError:
pass
try:
JSONString.parse('Hello, World!')
assert False, 'Excepted JSONStringSyntaxError'
except JSONStringSyntaxError:
pass
try:
JSONString.parse('""Hello, World!"')
assert False, 'Excepted JSONStringSyntaxError'
except JSONStringSyntaxError:
pass
def test_parse_boolean():
# Test parsing booleans
assert JSONBoolean.parse('true').value == True
assert JSONBoolean.parse('false').value == False
# Test exceptions
try:
JSONBoolean.parse('True')
assert False, 'Expected JSONBooleanSyntaxError'
except JSONBooleanSyntaxError:
pass
try:
JSONBoolean.parse('False')
assert False, 'Expected JSONBooleanSyntaxError'
except JSONBooleanSyntaxError:
pass
try:
JSONBoolean.parse('')
assert False, 'Expected JSONBooleanSyntaxError'
except JSONBooleanSyntaxError:
pass
def test_parse_null():
# Test parsing null
assert JSONNull.parse('null').value is None
# Test exceptions
try:
JSONNull.parse('NULL')
assert False, 'Expected JSONNullSyntaxError'
except JSONNullSyntaxError:
pass
try:
JSONNull.parse('Null')
assert False, 'Expected JSONNullSyntaxError'
except JSONNullSyntaxError:
pass
try:
JSONNull.parse('')
assert False, 'Expected JSONNullSyntaxError'
except JSONNullSyntaxError:
pass
def test_parse_array():
# Test parsing arrays
assert JSONArray.parse('[]').value == []
assert JSONArray.parse('[1, 2, 3]').value == [1, 2, 3]
assert JSONArray.parse('[1, "2", true, null]').value == [1, '2', True, None]
assert JSONArray.parse(r'[1, "2", true, null]').value == [1, '2', True, None]
# Test exceptions
try:
JSONArray.parse('[')
assert False, 'Expected JSONArraySyntaxError'
except JSONArraySyntaxError:
pass
try:
JSONArray.parse('[1, 2')
assert False, 'Expected JSONArraySyntaxError'
except JSONArraySyntaxError:
pass
def test_parse_object():
# Test parsing objects
assert JSONObject.parse('{}').value == {}
assert JSONObject.parse('{"key": "value"}').value == {'key': 'value'}
assert JSONObject.parse('{"key1": 1, "key2": "value2", "key3": true, "key4": null}').value == {'key1': 1, 'key2': 'value2', 'key3': True, 'key4': None}
assert JSONObject.parse(r'{"key1": 1, "key2": "value2", "key3": true, "key4": null}').value == {'key1': 1, 'key2': 'value2', 'key3': True, 'key4': None}
with open(os.path.dirname(__file__) + '/test.json') as jsonf:
assert JSONObject.parse(jsonf.read()).value == {
"level_1": {
"meta": {
"version": "1.0",
"generated": True,
"tags": ["test", "deep", "nested", "json"],
},
"level_2": {
"array": [
{
"id": 1,
"level_3": {
"level_4": {
"level_5": {
"config": {
"enabled": True,
"thresholds": {
"low": 0.1,
"medium": 0.5,
"high": 0.9,
},
"modes": [
{
"name": "alpha",
"params": {
"retry": 3,
"timeout": {
"connect": 1000,
"read": 5000,
"deep": {
"even_deeper": {
"flag": False,
"notes": [
"still",
"going",
{
"deeper": {
"than": {
"most": {
"humans": {
"expect": {
"value": 42
}
}
}
}
}
},
],
}
},
},
},
}
],
}
}
}
},
}
],
"level_2_object": {
"a": {
"b": {
"c": {
"d": {
"e": {
"f": {
"g": {
"h": {
"i": {
"j": {
"k": "bottom"
}
}
}
}
}
}
}
}
}
}
},
},
},
"level3": {
"root": [
[
[
[
{
"a": [
{
"b": [
{
"c": [
{
"d": [
{
"e": [
{
"f": "bottom"
}
]
}
]
}
]
}
]
}
]
}
]
]
]
]
},
"4": {
"level": {
"level": {
"level": {
"level": {
"level": {
"items": [
{"x": {"y": {"z": [1, 2, 3, 4, 5]}}},
{"x": {"y": {"z": [1, 2, 3, 4, 5]}}},
{"x": {"y": {"z": [1, 2, 3, 4, 5]}}},
{"x": {"y": {"z": [1, 2, 3, 4, 5]}}},
{"x": {"y": {"z": [1, 2, 3, 4, 5]}}},
]
}
}
}
}
}
},
"0": {
"n0": {
"n1": {
"n2": {
"n3": {
"n4": {
"n5": {
"n6": {
"n7": {
"n8": {
"n9": {
"n10": {
"n11": {
"n12": {
"n13": {
"n14": {
"n15": {
"n16": {
"n17": {
"n18": {
"n19": {
"n20": "bottom"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
},
}
jsonf.close()
# Test exceptions
def test_parse_type():
# Test detecting types
assert parse_type('42') == JSONNumber
assert parse_type('"abc"') == JSONString
assert parse_type('true') == JSONBoolean
assert parse_type('false') == JSONBoolean
assert parse_type('null') == JSONNull
assert parse_type('[1, 2, 3, "4", "5", "6", true, false, null]') == JSONArray
assert parse_type('{"1": 2}') == JSONObject
assert parse_type('{"1": \n\t 2 }') == JSONObject # test whitespaces
# Test very large data
with open(os.path.dirname(__file__) + '/test.json', 'r') as jsonf:
assert parse_type('[' + jsonf.read() + ']') == JSONArray
jsonf.close()
# Test exceptions
try:
parse_type('abc')
assert False, 'Expected JSONValueSyntaxError'
except JSONValueSyntaxError:
pass
try:
parse_type('1s')
assert False, 'Expected JSONValueSyntaxError'
except JSONValueSyntaxError:
pass
try:
parse_type('[')
assert False, 'Expected JSONArraySyntaxError'
except JSONArraySyntaxError:
pass
try:
parse_type('[a b]')
assert False, 'Expected JSONValueSyntaxError'
except JSONValueSyntaxError:
pass
try:
parse_type('"abc"abc')
assert False, 'Expected JSONStringSyntaxError'
except JSONStringSyntaxError:
pass
try:
parse_type('\'abc\'abc')
assert False, 'Expected JSONValueSyntaxError'
except JSONValueSyntaxError:
pass
try:
parse_type('{1: 2}')
assert False, 'Expected JSONObjectSyntaxError'
except JSONObjectSyntaxError:
pass
try:
parse_type('{"1"}')
assert False, 'Expected JSONObjectSyntaxError'
except JSONObjectSyntaxError:
pass
def test_parse_json():
# Test parsing JSON
assert parse_json('{"key": "value"}') == {'key': 'value'}
assert parse_json('[1, 2, 3]') == [1, 2, 3]
assert parse_json('{"key1": 1, "key2": "value2", "key3": true, "key4": null}') == {'key1': 1, 'key2': 'value2', 'key3': True, 'key4': None}
assert parse_json(r'{"key1": 1, "key2": "value2", "key3": true, "key4": null}') == {'key1': 1, 'key2': 'value2', 'key3': True, 'key4': None}
+68
View File
@@ -0,0 +1,68 @@
import sys, os; sys.path.append(os.getcwd())
from src.config_parser import Configuration
def test_crud_configuration_attrs():
config = Configuration()
# Test setting an attribute
config.abc = 42
# Test getting an attribute
assert config.abc == 42
# Test getting a non-existing attribute
try:
config.non_existing
assert False, "AttributeError was not raised"
except AttributeError:
pass
# Test updating an attribute
config.abc = 100
# Test getting the updated attribute
assert config.abc == 100
# Test getting dictionary
assert dict(config) == {'abc': 100}
# Test deleting an attribute
del config.abc
try:
config.abc
assert False, "AttributeError was not raised"
except AttributeError:
pass
def test_crud_configuration_config_items():
config = Configuration()
# Test setting an item
config['key1'] = 'value1'
# Test getting an item
assert config['key1'] == 'value1'
assert config.key1 == 'value1'
# Test updating an item
config['key1'] = 'value2'
# Test getting the updated item
assert config['key1'] == 'value2'
assert config.key1 == 'value2'
# Test deleting an item and exceptions
del config['key1']
try:
config['key1']
assert False, "KeyError was not raised"
except KeyError:
pass
try:
config.key1
assert False, "AttributeError was not raised"
except AttributeError:
pass