geändert: README.md
geändert: pyproject.toml geändert: src/config_parser/__init__.py gelöscht: src/config_parser/__main__.py geändert: src/config_parser/ini.py neue Datei: src/config_parser/parse/__init__.py geändert: src/config_parser/parse/ini.py geändert: src/config_parser/parse/json.py neue Datei: src/config_parser/serialize/__init__.py neue Datei: src/config_parser/serialize/ini.py neue Datei: src/config_parser/serialize/json.py neue Datei: tests/json/test_serializer.py
This commit is contained in:
@@ -74,4 +74,16 @@ If the configuration file content is:
|
||||
}
|
||||
```
|
||||
|
||||
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'}}`
|
||||
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'}}`
|
||||
|
||||
## Changelog
|
||||
### Version 1.1.0
|
||||
- JSON and INI serializer
|
||||
|
||||
### Version 1.0.0
|
||||
- JSON parser
|
||||
- better API
|
||||
|
||||
### Version 0.1.0
|
||||
- First release
|
||||
- INI parser
|
||||
+1
-1
@@ -4,5 +4,5 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "config-parser"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
description = "A configuration file parser."
|
||||
@@ -2,13 +2,26 @@
|
||||
A library for parsing configuration files in various formats.
|
||||
|
||||
Modules:
|
||||
- configuration: Base classes.
|
||||
- ini: INI file parser and serializer.
|
||||
- exceptions: Custom exceptions for configuration parsing.
|
||||
- _configuration: Base classes.
|
||||
- ini: INI configuration.
|
||||
- json: JSON configuration.
|
||||
- exceptions: Exceptions for configuration parsing.
|
||||
- parse: A package including the parsers.
|
||||
- serialize: A package including the serializers.
|
||||
'''
|
||||
|
||||
from ._configuration import Configuration
|
||||
# from . import exceptions
|
||||
# from . import json
|
||||
# from . import ini
|
||||
# from . import parse
|
||||
# from . import serialize
|
||||
|
||||
__all__ = [
|
||||
'Configuration'
|
||||
'Configuration',
|
||||
'exceptions',
|
||||
'json',
|
||||
'ini',
|
||||
'parse',
|
||||
'serialize'
|
||||
]
|
||||
+10
-14
@@ -1,16 +1,17 @@
|
||||
import typing
|
||||
from ._configuration import Configuration
|
||||
from .parse.ini import COMMENT_PREFIXES, QUOTATION_MARKS, parse_ini
|
||||
from .parse.ini import _COMMENT_PREFIXES, _QUOTATION_MARKS, parse_ini
|
||||
from .serialize.ini import serialize as serialize_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):
|
||||
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.
|
||||
@@ -42,26 +43,21 @@ class INIConfiguration(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()
|
||||
|
||||
return serialize_ini(self._config, separator)
|
||||
|
||||
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
|
||||
'''
|
||||
@@ -71,11 +67,11 @@ class INIConfigurationGroup(INIConfiguration):
|
||||
@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
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'''
|
||||
The parsers
|
||||
|
||||
Modules:
|
||||
- ini: INI parser.
|
||||
- json: JSON parser.
|
||||
'''
|
||||
|
||||
# from . import json
|
||||
# from . import ini
|
||||
|
||||
__all__ = [
|
||||
'json',
|
||||
'ini'
|
||||
]
|
||||
@@ -1,11 +1,11 @@
|
||||
import typing
|
||||
from ..exceptions import INIInvalidGroupHeader, INIInvalidKeyValueLine, INISyntaxError
|
||||
|
||||
QUOTATION_MARKS = ['"', "'"]
|
||||
COMMENT_PREFIXES = ['#', ';']
|
||||
KEY_VALUE_LINE = 0
|
||||
GROUP_HEADER_LINE = 1
|
||||
COMMENT_LINE = 2
|
||||
_QUOTATION_MARKS = ['"', "'"]
|
||||
_COMMENT_PREFIXES = ['#', ';']
|
||||
_KEY_VALUE_LINE = 0
|
||||
_GROUP_HEADER_LINE = 1
|
||||
_COMMENT_LINE = 2
|
||||
|
||||
def reverse_dict(d: typing.Dict) -> typing.Dict:
|
||||
'''
|
||||
@@ -19,7 +19,7 @@ def reverse_dict(d: typing.Dict) -> 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]:
|
||||
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.
|
||||
|
||||
@@ -93,7 +93,7 @@ def parse_group_header(line: str, ignore_errors: bool = False) -> str:
|
||||
raise INIInvalidGroupHeader('the header is invalid')
|
||||
|
||||
|
||||
def parse_line_type(line: str, comment_prefixes: typing.Collection[str] = COMMENT_PREFIXES, ignore_errors: bool = False) -> int:
|
||||
def parse_line_type(line: str, comment_prefixes: typing.Collection[str] = _COMMENT_PREFIXES, ignore_errors: bool = False) -> int:
|
||||
'''
|
||||
Returns the type of a line.
|
||||
|
||||
@@ -111,14 +111,14 @@ def parse_line_type(line: str, comment_prefixes: typing.Collection[str] = COMMEN
|
||||
'''
|
||||
line = line.strip()
|
||||
if any(line.startswith(prefix) for prefix in comment_prefixes) or not line.strip():
|
||||
return COMMENT_LINE
|
||||
return _COMMENT_LINE
|
||||
elif line.startswith('[') and line.endswith(']') and len(line) > 2:
|
||||
return GROUP_HEADER_LINE
|
||||
return _GROUP_HEADER_LINE
|
||||
elif '=' in line or ':' in line:
|
||||
return KEY_VALUE_LINE
|
||||
return _KEY_VALUE_LINE
|
||||
else:
|
||||
if ignore_errors:
|
||||
return COMMENT_LINE
|
||||
return _COMMENT_LINE
|
||||
raise INISyntaxError('the line is invalid')
|
||||
|
||||
def compress_conf(conf):
|
||||
@@ -126,7 +126,7 @@ def compress_conf(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]]:
|
||||
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.
|
||||
@@ -147,11 +147,11 @@ def parse_ini(conf, comment_prefixes=COMMENT_PREFIXES, quotation_marks=QUOTATION
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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)
|
||||
@@ -2,7 +2,7 @@ from ..exceptions import EscapeSequenceSyntaxError, JSONValueSyntaxError, JSONNu
|
||||
import typing
|
||||
import re
|
||||
|
||||
JSON_ESCAPE_SEQUENCES = {
|
||||
_JSON_ESCAPE_SEQUENCES = {
|
||||
'"': '"',
|
||||
'\\': '\\',
|
||||
'/': '/',
|
||||
@@ -62,7 +62,7 @@ def parse_escape_sequences(string: str) -> bool:
|
||||
if c == 'u':
|
||||
unicode_escape_sequence = ''
|
||||
else:
|
||||
result += JSON_ESCAPE_SEQUENCES[c]
|
||||
result += _JSON_ESCAPE_SEQUENCES[c]
|
||||
|
||||
if unicode_escape_sequence:
|
||||
raise EscapeSequenceSyntaxError(f'Invalid unicode escape sequence in JSON string: \\u{unicode_escape_sequence}')
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'''
|
||||
The serializers
|
||||
|
||||
Modules:
|
||||
- ini: INI serializer.
|
||||
- json: JSON serializer.
|
||||
'''
|
||||
|
||||
# from . import json
|
||||
# from . import ini
|
||||
|
||||
__all__ = [
|
||||
'json',
|
||||
'ini'
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
from ..exceptions import INISyntaxError
|
||||
|
||||
def serialize(data: dict, separator: str) -> str:
|
||||
'''
|
||||
Serializes the data to an INI formatted string.
|
||||
|
||||
:param data: The INI configuration.
|
||||
:type data: dict
|
||||
|
||||
:param separator: The separator.
|
||||
:type separator: str
|
||||
|
||||
:return: The INI formatted string.
|
||||
:rtype: str
|
||||
'''
|
||||
|
||||
result = ''
|
||||
for key, value in data.items():
|
||||
if hasattr(value, '_name'):
|
||||
result += value.to_string(separator=separator)
|
||||
else:
|
||||
result += f'{key}{separator}{value}\n'
|
||||
return result.strip()
|
||||
@@ -0,0 +1,135 @@
|
||||
import copy
|
||||
from typing import *
|
||||
import collections.abc
|
||||
|
||||
_ESCAPE_SEQUENCES = {
|
||||
'\"': r'\"',
|
||||
'\\': r'\\',
|
||||
'\b': r'\b',
|
||||
'\f': r'\f',
|
||||
'\n': r'\n',
|
||||
'\r': r'\r',
|
||||
'\t': r'\t',
|
||||
}
|
||||
|
||||
class _Mapper:
|
||||
def __init__(self, mapping = {}):
|
||||
self._mapping = copy.deepcopy(mapping)
|
||||
|
||||
def _set(self, key, value):
|
||||
self._mapping[key] = value
|
||||
|
||||
def set(self, key, value):
|
||||
self._set(key, value)
|
||||
return self
|
||||
|
||||
def map(self, value):
|
||||
return self._mapping[value]
|
||||
|
||||
_MAPPER = _Mapper().set(True, 'true').set(False, 'false').set(None, 'null')
|
||||
|
||||
def _serialize_object(data: dict, level: int, indent: int, indent_char: str, separators: Tuple[str, str]) -> str:
|
||||
result = '{'
|
||||
_count = 0
|
||||
for _key, value in data.items():
|
||||
_count += 1
|
||||
key = _serialize(_key, 0, 0, '', separators)
|
||||
value = _serialize(value, level + 1, indent, indent_char, separators)
|
||||
if not isinstance(_key, str):
|
||||
key = '"' + key + '"'
|
||||
if indent:
|
||||
result += '\n'
|
||||
result += (level + 1) * indent * indent_char
|
||||
result += key
|
||||
result += separators[1]
|
||||
result += value
|
||||
if _count != len(data.keys()):
|
||||
if indent:
|
||||
result += separators[0].strip()
|
||||
else:
|
||||
result += separators[0]
|
||||
if indent:
|
||||
result += '\n'
|
||||
result += level * indent * indent_char + '}'
|
||||
return result
|
||||
|
||||
def _serialize_array(data: Sequence, level: int, indent: int, indent_char: str, separators: Tuple[str, str]) -> str:
|
||||
result = '['
|
||||
_count = 0
|
||||
for value in data:
|
||||
_count += 1
|
||||
value = _serialize(value, level + 1, indent, indent_char, separators)
|
||||
if indent:
|
||||
result += '\n'
|
||||
result += (level + 1) * indent * indent_char
|
||||
result += value
|
||||
if _count != len(data):
|
||||
if indent:
|
||||
result += separators[0].strip()
|
||||
else:
|
||||
result += separators[0]
|
||||
if indent:
|
||||
result += '\n'
|
||||
result += level * indent * indent_char + ']'
|
||||
return result
|
||||
|
||||
def _serialize_string(data: str) -> str:
|
||||
_data = data
|
||||
data = ''
|
||||
for c in _data:
|
||||
data += _ESCAPE_SEQUENCES.get(c, c)
|
||||
return '"' + data + '"'
|
||||
|
||||
|
||||
def _serialize_number(data: int | float) -> str:
|
||||
return str(data)
|
||||
|
||||
def _serialize(data: dict | list | str | int | float | bool | None, level: int, indent: int, indent_char: str, separators: Tuple[str, str]) -> str:
|
||||
if isinstance(data, (bool, type(None))):
|
||||
return _MAPPER.map(data)
|
||||
elif isinstance(data, dict):
|
||||
return _serialize_object(data, level, indent, indent_char, separators)
|
||||
elif isinstance(data, str):
|
||||
return _serialize_string(data)
|
||||
elif isinstance(data, (int, float)):
|
||||
return _serialize_number(data)
|
||||
elif isinstance(data, collections.abc.Sequence):
|
||||
return _serialize_array(data, level, indent, indent_char, separators)
|
||||
else:
|
||||
raise TypeError(f'Object of type {type(data).__name__} is not JSON serializable')
|
||||
|
||||
def _char_whitespaces(whitespaces: Tuple[int | str, int | str] | int | str) -> Tuple[str, str]:
|
||||
if isinstance(whitespaces, collections.abc.Sequence) and not isinstance(whitespaces, str):
|
||||
if len(whitespaces) != 2:
|
||||
raise ValueError(f'expected exactly two elements in whitespaces, got {len(whitespaces)}')
|
||||
if isinstance(whitespaces, (str, int)):
|
||||
whitespaces = whitespaces, whitespaces
|
||||
if isinstance(whitespaces[0], int):
|
||||
whitespaces = whitespaces[0] * ' ', whitespaces[1]
|
||||
if isinstance(whitespaces[1], int):
|
||||
whitespaces = whitespaces[0], whitespaces[1] * ' '
|
||||
return whitespaces
|
||||
|
||||
def serialize(data: dict | list | str | int | float | bool | None, indent: int = 0, indent_char: str = ' ', separators: Tuple[str, str] = (', ', ': ')) -> str:
|
||||
'''
|
||||
Serializes the data to a JSON formatted string.
|
||||
|
||||
:param data: The data.
|
||||
:type data: dict | list | str | int | float | bool | None
|
||||
:param indent: The indentation
|
||||
:type indent: int
|
||||
:param indent_char: The character the indentation will be filled with
|
||||
:type indent_char: str
|
||||
:param separators: A tuple with the separators. The first string is the element separator and the second string is the key value separator.
|
||||
:type separators: tuple
|
||||
|
||||
:raises TypeError: If the type is not JSON serializable.
|
||||
|
||||
:return: The JSON formatted string.
|
||||
:rtype: str
|
||||
'''
|
||||
|
||||
if len(separators) != 2:
|
||||
raise ValueError(f'expected exactly two elements in separators, got {len(separators)}.')
|
||||
|
||||
return _serialize(data, 0, indent, indent_char, separators)
|
||||
@@ -0,0 +1,51 @@
|
||||
from src.config_parser.serialize.json import serialize as serialize_json
|
||||
|
||||
def test_serialize_json():
|
||||
assert serialize_json(True) == 'true'
|
||||
assert serialize_json(False) == 'false'
|
||||
assert serialize_json(True, indent = 4) == 'true'
|
||||
|
||||
assert serialize_json(None) == 'null'
|
||||
|
||||
assert serialize_json({1: {None: True}}, indent = 4) == '''{
|
||||
"1": {
|
||||
"null": true
|
||||
}
|
||||
}'''
|
||||
assert serialize_json({1: {None: True}}) == '{"1": {"null": true}}'
|
||||
assert serialize_json({1: {None: True}, 2: 'Hello, World!'}, separators = (',', ':')) == '{"1":{"null":true},"2":"Hello, World!"}'
|
||||
|
||||
array = [True, False, 1, None, 'Hello, World!']
|
||||
assert serialize_json(array) == '[true, false, 1, null, "Hello, World!"]'
|
||||
assert serialize_json(array, 4, 'X', (';', ':')) == '''[
|
||||
XXXXtrue;
|
||||
XXXXfalse;
|
||||
XXXX1;
|
||||
XXXXnull;
|
||||
XXXX"Hello, World!"
|
||||
]'''
|
||||
assert serialize_json((1, 2, 3)) == '[1, 2, 3]'
|
||||
assert serialize_json(range(3)) == '[0, 1, 2]'
|
||||
assert serialize_json(b'\x0142') == '[1, 52, 50]'
|
||||
|
||||
assert serialize_json('Hello, World!') == '"Hello, World!"'
|
||||
assert serialize_json('Hello,\nWorld!') == '"Hello,\\nWorld!"'
|
||||
assert serialize_json('Hello,"World!') == '"Hello,\\"World!"'
|
||||
assert serialize_json('Hello,\\World!') == '"Hello,\\\\World!"'
|
||||
assert serialize_json('Hello,\bWorld!') == '"Hello,\\bWorld!"'
|
||||
assert serialize_json('Hello,\tWorld!') == '"Hello,\\tWorld!"'
|
||||
assert serialize_json('Hello,\rWorld!') == '"Hello,\\rWorld!"'
|
||||
assert serialize_json('Hello,\fWorld!') == '"Hello,\\fWorld!"'
|
||||
|
||||
assert serialize_json(0) == '0'
|
||||
assert serialize_json(1) == '1'
|
||||
assert serialize_json(-1) == '-1'
|
||||
assert serialize_json(0.0) == '0.0'
|
||||
assert serialize_json(-0.0) == '-0.0'
|
||||
assert serialize_json(-42.0) == '-42.0'
|
||||
|
||||
try:
|
||||
serialize_json({1, 2, 3})
|
||||
assert False, 'Expected TypeError'
|
||||
except TypeError:
|
||||
pass
|
||||
Reference in New Issue
Block a user