Support for a default configuration for INI files
This commit is contained in:
@@ -77,6 +77,11 @@ 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
|
## Changelog
|
||||||
|
### Version 1.2.0
|
||||||
|
- Support for a default configuration for INI files
|
||||||
|
- Consistent `__all__` lists in all modules
|
||||||
|
- Support for `in` at configuration classes
|
||||||
|
|
||||||
### Version 1.1.1
|
### Version 1.1.1
|
||||||
- Updated the `__init__.py` files in the packages. Now you do not need to run e. g. `import config_parser.parse`, you can simply run `import config_parser` to access `config_parser.parse`
|
- Updated the `__init__.py` files in the packages. Now you do not need to run e. g. `import config_parser.parse`, you can simply run `import config_parser` to access `config_parser.parse`
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,5 +4,5 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "config-parser"
|
name = "config-parser"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
description = "A configuration file parser."
|
description = "A configuration file parser."
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Configuration'
|
||||||
|
]
|
||||||
|
|
||||||
class Configuration:
|
class Configuration:
|
||||||
def __init__(self, _config: dict = {}):
|
def __init__(self, _config: dict = {}):
|
||||||
'''A base class for configurations.'''
|
'''A base class for configurations.'''
|
||||||
@@ -38,3 +42,6 @@ class Configuration:
|
|||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self._config.items())
|
return iter(self._config.items())
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return item in self._config
|
||||||
@@ -1,3 +1,21 @@
|
|||||||
|
__all__ = [
|
||||||
|
'ConfigurationSyntaxError',
|
||||||
|
'EscapeSequenceSyntaxError',
|
||||||
|
'INISyntaxError',
|
||||||
|
'INIInvalidGroupHeader',
|
||||||
|
'INIInvalidKeyValueLine',
|
||||||
|
'JSONSyntaxError',
|
||||||
|
'JSONValueSyntaxError',
|
||||||
|
'JSONNumberSyntaxError',
|
||||||
|
'JSONStringSyntaxError',
|
||||||
|
'JSONBooleanSyntaxError',
|
||||||
|
'JSONNullSyntaxError',
|
||||||
|
'JSONArraySyntaxError',
|
||||||
|
'JSONObjectSyntaxError',
|
||||||
|
'ConfigurationTypeError',
|
||||||
|
'JSONTypeError'
|
||||||
|
]
|
||||||
|
|
||||||
class ConfigurationSyntaxError(SyntaxError): ...
|
class ConfigurationSyntaxError(SyntaxError): ...
|
||||||
|
|
||||||
class EscapeSequenceSyntaxError(ConfigurationSyntaxError): ...
|
class EscapeSequenceSyntaxError(ConfigurationSyntaxError): ...
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
|
from __future__ import annotations
|
||||||
import typing
|
import typing
|
||||||
from ._configuration import Configuration
|
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
|
from .serialize.ini import serialize as serialize_ini
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'INIConfiguration',
|
||||||
|
'INIConfigurationSection'
|
||||||
|
]
|
||||||
|
|
||||||
class INIConfiguration(Configuration):
|
class INIConfiguration(Configuration):
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter({k: dict(v) if isinstance(v, INIConfigurationGroup) else v for k, v in self._config.items()}.items())
|
return iter({k: dict(v) if isinstance(v, INIConfigurationSection) else v for k, v in self._config.items()}.items())
|
||||||
|
|
||||||
@classmethod
|
@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, default: INIConfiguration = None):
|
||||||
'''
|
'''
|
||||||
Parses INI configuration from a string and returns an instance of INIConfiguration.
|
Parses INI configuration from a string and returns an instance of INIConfiguration.
|
||||||
|
|
||||||
@@ -22,6 +28,8 @@ class INIConfiguration(Configuration):
|
|||||||
:type quotation_marks: typing.Collection[str]
|
:type quotation_marks: typing.Collection[str]
|
||||||
:param ignore_errors: If True, parsing errors will be ignored.
|
:param ignore_errors: If True, parsing errors will be ignored.
|
||||||
:type ignore_errors: bool
|
:type ignore_errors: bool
|
||||||
|
:param default: The default configuration.
|
||||||
|
:type default: INIConfiguration
|
||||||
|
|
||||||
:raises INISyntaxError: If there is a syntax error in the INI configuration and ``ignore_errors`` is ``False``.
|
:raises INISyntaxError: If there is a syntax error in the INI configuration and ``ignore_errors`` is ``False``.
|
||||||
|
|
||||||
@@ -32,11 +40,19 @@ class INIConfiguration(Configuration):
|
|||||||
data = data.decode()
|
data = data.decode()
|
||||||
|
|
||||||
configuration = cls()
|
configuration = cls()
|
||||||
for group_name, content in parse_ini(data, comment_prefixes=comment_prefixes, quotation_marks=quotation_marks, ignore_errors=ignore_errors).items():
|
for section_name, content in parse_ini(data, comment_prefixes=comment_prefixes, quotation_marks=quotation_marks, ignore_errors=ignore_errors).items():
|
||||||
group = INIConfigurationGroup(group_name)
|
group = INIConfigurationSection(section_name)
|
||||||
for key, value in content.items():
|
for key, value in content.items():
|
||||||
group[key] = value
|
group[key] = value
|
||||||
configuration[group_name] = group
|
configuration[section_name] = group
|
||||||
|
|
||||||
|
if default is not None:
|
||||||
|
for section in default._config:
|
||||||
|
if section not in configuration:
|
||||||
|
configuration[section] = INIConfigurationSection(section)
|
||||||
|
for propk, propv in default[section]._config.items():
|
||||||
|
if propk not in dict(configuration[section]).keys():
|
||||||
|
configuration[section][propk] = propv
|
||||||
|
|
||||||
return configuration
|
return configuration
|
||||||
|
|
||||||
@@ -53,7 +69,7 @@ class INIConfiguration(Configuration):
|
|||||||
|
|
||||||
return serialize_ini(self._config, separator)
|
return serialize_ini(self._config, separator)
|
||||||
|
|
||||||
class INIConfigurationGroup(INIConfiguration):
|
class INIConfigurationSection(INIConfiguration):
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
'''
|
'''
|
||||||
A class representing a group in an INI configuration.
|
A class representing a group in an INI configuration.
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ from ._configuration import Configuration
|
|||||||
from .parse.json import parse_type, JSONObject, JSONString, JSONNumber, JSONNull, JSONBoolean, JSONArray
|
from .parse.json import parse_type, JSONObject, JSONString, JSONNumber, JSONNull, JSONBoolean, JSONArray
|
||||||
from .exceptions import JSONTypeError
|
from .exceptions import JSONTypeError
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'JSONConfiguration'
|
||||||
|
]
|
||||||
|
|
||||||
def _configuration(data):
|
def _configuration(data):
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
return JSONString(data)
|
return JSONString(data)
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import typing
|
import typing
|
||||||
from ..exceptions import INIInvalidGroupHeader, INIInvalidKeyValueLine, INISyntaxError
|
from ..exceptions import INIInvalidGroupHeader, INIInvalidKeyValueLine, INISyntaxError
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'parse_ini'
|
||||||
|
]
|
||||||
|
|
||||||
_QUOTATION_MARKS = ['"', "'"]
|
_QUOTATION_MARKS = ['"', "'"]
|
||||||
_COMMENT_PREFIXES = ['#', ';']
|
_COMMENT_PREFIXES = ['#', ';']
|
||||||
_KEY_VALUE_LINE = 0
|
_KEY_VALUE_LINE = 0
|
||||||
_GROUP_HEADER_LINE = 1
|
_GROUP_HEADER_LINE = 1
|
||||||
_COMMENT_LINE = 2
|
_COMMENT_LINE = 2
|
||||||
|
|
||||||
def reverse_dict(d: typing.Dict) -> typing.Dict:
|
def _reverse_dict(d: typing.Dict) -> typing.Dict:
|
||||||
'''
|
'''
|
||||||
Reverses the order of items in a dictionary.
|
Reverses the order of items in a dictionary.
|
||||||
|
|
||||||
@@ -19,7 +23,7 @@ def reverse_dict(d: typing.Dict) -> typing.Dict:
|
|||||||
'''
|
'''
|
||||||
return {k: v for k, v in list(reversed(d.items()))}
|
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.
|
Parses a key-value line.
|
||||||
|
|
||||||
@@ -70,8 +74,7 @@ def parse_key_value_line(line: str, comment_prefixes: typing.Collection[str] = _
|
|||||||
|
|
||||||
return {key: value}
|
return {key: value}
|
||||||
|
|
||||||
|
def _parse_group_header(line: str, ignore_errors: bool = False) -> str:
|
||||||
def parse_group_header(line: str, ignore_errors: bool = False) -> str:
|
|
||||||
'''
|
'''
|
||||||
Parses a group header line.
|
Parses a group header line.
|
||||||
|
|
||||||
@@ -92,8 +95,7 @@ def parse_group_header(line: str, ignore_errors: bool = False) -> str:
|
|||||||
return ''
|
return ''
|
||||||
raise INIInvalidGroupHeader('the header is invalid')
|
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.
|
Returns the type of a line.
|
||||||
|
|
||||||
@@ -121,7 +123,7 @@ def parse_line_type(line: str, comment_prefixes: typing.Collection[str] = _COMME
|
|||||||
return _COMMENT_LINE
|
return _COMMENT_LINE
|
||||||
raise INISyntaxError('the line is invalid')
|
raise INISyntaxError('the line is invalid')
|
||||||
|
|
||||||
def compress_conf(conf):
|
def _compress_conf(conf):
|
||||||
while '\n\n' in conf:
|
while '\n\n' in conf:
|
||||||
conf = conf.replace('\n\n', '\n')
|
conf = conf.replace('\n\n', '\n')
|
||||||
return conf.strip()
|
return conf.strip()
|
||||||
@@ -142,16 +144,16 @@ def parse_ini(conf, comment_prefixes=_COMMENT_PREFIXES, quotation_marks=_QUOTATI
|
|||||||
:rtype: typing.Dict[str, typing.Dict[str, str]]
|
:rtype: typing.Dict[str, typing.Dict[str, str]]
|
||||||
'''
|
'''
|
||||||
|
|
||||||
conf = compress_conf(conf)
|
conf = _compress_conf(conf)
|
||||||
result = {}
|
result = {}
|
||||||
current_group = {}
|
current_group = {}
|
||||||
for line in list(reversed(conf.split('\n'))):
|
for line in list(reversed(conf.split('\n'))):
|
||||||
line = line.strip()
|
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
|
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)
|
result[_parse_group_header(line, ignore_errors=ignore_errors)] = _reverse_dict(current_group)
|
||||||
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)
|
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)
|
return _reverse_dict(result)
|
||||||
@@ -2,6 +2,19 @@ from ..exceptions import EscapeSequenceSyntaxError, JSONValueSyntaxError, JSONNu
|
|||||||
import typing
|
import typing
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'JSONType',
|
||||||
|
'JSONNumber',
|
||||||
|
'JSONString',
|
||||||
|
'JSONBoolean',
|
||||||
|
'JSONNull',
|
||||||
|
'JSONArray',
|
||||||
|
'JSONObject',
|
||||||
|
'parse_escape_sequences',
|
||||||
|
'parse_type',
|
||||||
|
'parse_json'
|
||||||
|
]
|
||||||
|
|
||||||
_JSON_ESCAPE_SEQUENCES = {
|
_JSON_ESCAPE_SEQUENCES = {
|
||||||
'"': '"',
|
'"': '"',
|
||||||
'\\': '\\',
|
'\\': '\\',
|
||||||
@@ -23,51 +36,6 @@ _JSON_NUMBER_RE = re.compile(
|
|||||||
re.VERBOSE,
|
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:
|
class JSONType:
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -362,6 +330,51 @@ class JSONObject(JSONType):
|
|||||||
|
|
||||||
return cls(json_object)
|
return cls(json_object)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
def parse_type(value: str) -> typing.Type[JSONType]:
|
def parse_type(value: str) -> typing.Type[JSONType]:
|
||||||
'''
|
'''
|
||||||
Parses the type of the value.
|
Parses the type of the value.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from ..exceptions import INISyntaxError
|
__all__ = [
|
||||||
|
'serialize'
|
||||||
|
]
|
||||||
|
|
||||||
def serialize(data: dict, separator: str) -> str:
|
def serialize(data: dict, separator: str) -> str:
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import copy
|
|||||||
from typing import *
|
from typing import *
|
||||||
import collections.abc
|
import collections.abc
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'serialize'
|
||||||
|
]
|
||||||
|
|
||||||
_ESCAPE_SEQUENCES = {
|
_ESCAPE_SEQUENCES = {
|
||||||
'\"': r'\"',
|
'\"': r'\"',
|
||||||
'\\': r'\\',
|
'\\': r'\\',
|
||||||
@@ -80,7 +84,6 @@ def _serialize_string(data: str) -> str:
|
|||||||
data += _ESCAPE_SEQUENCES.get(c, c)
|
data += _ESCAPE_SEQUENCES.get(c, c)
|
||||||
return '"' + data + '"'
|
return '"' + data + '"'
|
||||||
|
|
||||||
|
|
||||||
def _serialize_number(data: int | float) -> str:
|
def _serialize_number(data: int | float) -> str:
|
||||||
return str(data)
|
return str(data)
|
||||||
|
|
||||||
|
|||||||
+16
-2
@@ -1,9 +1,9 @@
|
|||||||
from src.config_parser.ini import INIConfiguration, INIConfigurationGroup
|
from src.config_parser.ini import INIConfiguration, INIConfigurationSection
|
||||||
from src.config_parser.exceptions import INISyntaxError
|
from src.config_parser.exceptions import INISyntaxError
|
||||||
|
|
||||||
def test_generating():
|
def test_generating():
|
||||||
config = INIConfiguration()
|
config = INIConfiguration()
|
||||||
config.test_group = INIConfigurationGroup('test_group')
|
config.test_group = INIConfigurationSection('test_group')
|
||||||
config.test_group.key1 = 'value1'
|
config.test_group.key1 = 'value1'
|
||||||
config.test_group.key2 = 'value2'
|
config.test_group.key2 = 'value2'
|
||||||
assert config.to_string() == '''[test_group]
|
assert config.to_string() == '''[test_group]
|
||||||
@@ -44,3 +44,17 @@ invalid_line2
|
|||||||
assert False, 'Expected INISyntaxError for unterminated literal'
|
assert False, 'Expected INISyntaxError for unterminated literal'
|
||||||
except INISyntaxError:
|
except INISyntaxError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def test_default():
|
||||||
|
default_configuration = INIConfiguration.from_string('''[section1]
|
||||||
|
k1=v1
|
||||||
|
k2=v2
|
||||||
|
k3=v3
|
||||||
|
|
||||||
|
[section2]
|
||||||
|
l1=w1
|
||||||
|
l2=w2''')
|
||||||
|
assert dict(INIConfiguration.from_string('''[section1]
|
||||||
|
k1=v1
|
||||||
|
k2=v2
|
||||||
|
k4=v4''', default = default_configuration)) == {'section1': {'k1': 'v1', 'k2': 'v2', 'k3': 'v3', 'k4': 'v4'}, 'section2': {'l1': 'w1', 'l2': 'w2'}}
|
||||||
Reference in New Issue
Block a user