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:
2026-02-18 17:10:34 +01:00
parent a1c1d2b975
commit 00a5f08171
12 changed files with 297 additions and 37 deletions
+12
View File
@@ -75,3 +75,15 @@ 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'}}`
## 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
View File
@@ -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."
+17 -4
View File
@@ -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'
]
View File
+5 -9
View File
@@ -1,13 +1,14 @@
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.
@@ -49,13 +50,8 @@ class INIConfiguration(Configuration):
: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):
+15
View File
@@ -0,0 +1,15 @@
'''
The parsers
Modules:
- ini: INI parser.
- json: JSON parser.
'''
# from . import json
# from . import ini
__all__ = [
'json',
'ini'
]
+15 -15
View File
@@ -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 -2
View File
@@ -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}')
+15
View File
@@ -0,0 +1,15 @@
'''
The serializers
Modules:
- ini: INI serializer.
- json: JSON serializer.
'''
# from . import json
# from . import ini
__all__ = [
'json',
'ini'
]
+23
View File
@@ -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()
+135
View File
@@ -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)
+51
View File
@@ -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