Initial commit

This commit is contained in:
2026-03-12 19:34:25 +01:00
commit b00e5d9829
22 changed files with 2916 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
dist/
src/*.egg-info/
.vscode/
.pytest_cache/
__pycache__/
+215
View File
@@ -0,0 +1,215 @@
Copyright 2026 jCloud Services GbR
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+88
View File
@@ -0,0 +1,88 @@
# jcloud-config-parser
A library to parse configuration files.
## Installation
You can install the library using pip:
```bash
pip install jcloud-config-parser --index-url https://repo.jcloud-services.ddns.net/simple/
```
## Usage
Here are a few simple examples of how to use the `jcloud-config-parser` library to read an configuration files:
### INI Configuration
```python
from jcloud_config_parser.ini import INIConfiguration
with open('config.conf', 'r') as file:
config_content = file.read()
parsed = INIConfiguration.from_string(config_content)
print(dict(parsed))
```
If the configuration file content is:
```ini
global1=global value1
global2=global value2
[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 jcloud_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'}}`
## Full documentation
For the full documentation, see the Python docstrings.
## Changelog
### Version 0.1.0
- Changed the name from `config-parser` to `jcloud-config-parser`
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/bash
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
python3 -m build
scp dist/* jcloud@jcloud-services.ddns.net:/srv/data/wwwstatic/repo/simple/jcloud-config-parser
+9
View File
@@ -0,0 +1,9 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "jcloud-config-parser"
version = "0.1.0"
description = "A configuration file parser."
license = "Apache-2.0"
+42
View File
@@ -0,0 +1,42 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''
A library for parsing configuration files in various formats.
Modules:
- _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 *
from ._configuration import __all__ as _configuration__all__
from . import exceptions
from . import json
from . import ini
from . import parse
from . import serialize
__all__ = [
'exceptions',
'json',
'ini',
'parse',
'serialize',
*_configuration__all__
]
+129
View File
@@ -0,0 +1,129 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
from types import FunctionType
from typing import Any
__all__ = [
'Configuration',
'set_mutability',
'is_mutable',
]
class Configuration:
def __init__(self, _config: dict = {}, mutable: bool = True) -> None:
'''A base class for configurations.'''
self._config = copy.deepcopy(_config)
self._mutable = mutable
def _check_mutability(self) -> None:
return self._mutable
def _mutate(self, func: FunctionType) -> FunctionType:
def wrapper(*args, **kwargs) -> Any:
if self._check_mutability():
return func(*args, **kwargs)
else:
raise TypeError('configuration is immutable')
return wrapper
def _set(self, name: str, value: str) -> None:
@self._mutate
def _set(name: str, value: str) -> None:
self._config[name] = value
_set(name, value)
def _delete(self, name: str) -> None:
@self._mutate
def _delete(name: str) -> None:
del self._config[name]
_delete(name)
def _get(self, name: str) -> str:
return self._config[name]
def __getattribute__(self, name: str):
if name.startswith('_') or name in self.__dir__():
return super().__getattribute__(name)
else:
if name not in self._config:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
return self._get(name)
def __setattr__(self, name, value):
if name.startswith('_') or name in self.__dir__():
super().__setattr__(name, value)
else:
self._set(name, value)
def __delattr__(self, name):
if name.startswith('_') or name in self.__dir__():
super().__delattr__(name)
else:
self._delete(name)
def __setitem__(self, key, value):
self._set(key, value)
def __getitem__(self, key):
return self._config[key]
def __delitem__(self, key):
self._delete(key)
def __iter__(self):
return iter(self._config.items())
def __contains__(self, item):
return item in self._config
def set_mutability(configuration: Configuration, mutable: bool, *, operate_on_original_object: bool = True) -> Configuration:
'''
Sets the mutability of a configuration.
:param configuration: The configuration object.
:type configuration: Configuration
:param mutable: The new mutability.
:type mutable: bool
:param operate_on_original_object: If ``True``, the mutability of the
original object will be set. If ``False``, the original object will be
unchanged. The new configuration will always be returned.
:type operate_on_original_object: bool
:return: The configuration object with the new mutability
:rtype: Configuration
'''
if not operate_on_original_object:
configuration = copy.deepcopy(configuration)
configuration._mutable = mutable
return configuration
def is_mutable(configuration: Configuration) -> Configuration:
'''
Checks whether a configuration is mutable.
:param configuration: The configuration.
:type configuration: Configuration
:return: The mutability
:rtype: bool
'''
return configuration._mutable
+57
View File
@@ -0,0 +1,57 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__all__ = [
'ConfigurationSyntaxError',
'EscapeSequenceSyntaxError',
'INISyntaxError',
'INIInvalidGroupHeader',
'INIInvalidKeyValueLine',
'JSONSyntaxError',
'JSONValueSyntaxError',
'JSONNumberSyntaxError',
'JSONStringSyntaxError',
'JSONBooleanSyntaxError',
'JSONNullSyntaxError',
'JSONArraySyntaxError',
'JSONObjectSyntaxError',
'ConfigurationTypeError',
'JSONTypeError'
]
class ConfigurationSyntaxError(Exception): ...
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): ...
+111
View File
@@ -0,0 +1,111 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import typing
from ._configuration import Configuration
from .parse.ini import _COMMENT_PREFIXES, _QUOTATION_MARKS, parse_ini
from .serialize.ini import serialize as serialize_ini
__all__ = [
'INIConfiguration',
'INIConfigurationSection'
]
class INIConfiguration(Configuration):
def __iter__(self):
return iter({k: dict(v) if isinstance(v, INIConfigurationSection) 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, default: INIConfiguration = None):
'''
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
: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``.
:return: An instance of INIConfiguration representing the parsed INI configuration.
:rtype: INIConfiguration
'''
if isinstance(data, bytes):
data = data.decode()
configuration = cls()
for section_name, content in parse_ini(data, comment_prefixes=comment_prefixes, quotation_marks=quotation_marks, ignore_errors=ignore_errors).items():
group = INIConfigurationSection(section_name)
for key, value in content.items():
group[key] = value
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
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
'''
return serialize_ini(self._config, separator)
class INIConfigurationSection(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()
+73
View File
@@ -0,0 +1,73 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ._configuration import Configuration
from .parse.json import parse_type, JSONObject, JSONString, JSONNumber, JSONNull, JSONBoolean, JSONArray
from .exceptions import JSONTypeError
__all__ = [
'JSONConfiguration'
]
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 jcloud_config_parser.parse.json.parse_json to parse JSONs.')
except:
if ignore_errors:
return cls()
else:
raise
@@ -0,0 +1,29 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''
The parsers
Modules:
- ini: INI parser.
- json: JSON parser.
'''
from . import json
from . import ini
__all__ = [
'json',
'ini'
]
+173
View File
@@ -0,0 +1,173 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import typing
from ..exceptions import INIInvalidGroupHeader, INIInvalidKeyValueLine, INISyntaxError
__all__ = [
'parse_ini'
]
_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)
+616
View File
@@ -0,0 +1,616 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ..exceptions import EscapeSequenceSyntaxError, JSONValueSyntaxError, JSONNumberSyntaxError, JSONStringSyntaxError, JSONBooleanSyntaxError, JSONNullSyntaxError, JSONArraySyntaxError, JSONObjectSyntaxError
import typing
import re
__all__ = [
'JSONType',
'JSONNumber',
'JSONString',
'JSONBoolean',
'JSONNull',
'JSONArray',
'JSONObject',
'parse_escape_sequences',
'parse_type',
'parse_json'
]
_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,
)
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_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]:
'''
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
@@ -0,0 +1,29 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''
The serializers
Modules:
- ini: INI serializer.
- json: JSON serializer.
'''
from . import json
from . import ini
__all__ = [
'json',
'ini'
]
+39
View File
@@ -0,0 +1,39 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__all__ = [
'serialize'
]
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()
+152
View File
@@ -0,0 +1,152 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
from typing import *
import collections.abc
__all__ = [
'serialize'
]
_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)
+97
View File
@@ -0,0 +1,97 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_config_parser.ini import INIConfiguration, INIConfigurationSection
from src.jcloud_config_parser.exceptions import INISyntaxError
from src.jcloud_config_parser import set_mutability
def test_generating():
config = INIConfiguration()
config.test_group = INIConfigurationSection('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
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'}}
def test_mutability():
config = INIConfiguration(mutable=False)
try:
config.key = 'value'
assert False, 'Expected TypeError'
except TypeError:
pass
set_mutability(config, True)
assert config._mutable == True
config.key = 'value'
set_mutability(config, False)
assert config._mutable == False
try:
del config.key
assert False, 'Expected TypeError'
except TypeError:
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"
} } }
}
} }}
}
}
}
} }
}
}
}
}
}}
}
}
}
}
+246
View File
@@ -0,0 +1,246 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_config_parser.json import JSONConfiguration
from src.jcloud_config_parser.exceptions import JSONObjectSyntaxError, JSONValueSyntaxError, JSONStringSyntaxError, JSONArraySyntaxError, EscapeSequenceSyntaxError
from src.jcloud_config_parser import set_mutability
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
def test_mutability():
config = JSONConfiguration(mutable=False)
try:
config.key = 'value'
assert False, 'Expected TypeError'
except TypeError:
pass
set_mutability(config, True)
assert config._mutable == True
config.key = 'value'
set_mutability(config, False)
assert config._mutable == False
try:
del config.key
assert False, 'Expected TypeError'
except TypeError:
pass
+443
View File
@@ -0,0 +1,443 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_config_parser.parse.json import parse_escape_sequences, JSONNumber, JSONString, JSONBoolean, JSONNull, JSONArray, JSONObject, parse_type, parse_json
from src.jcloud_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}
+65
View File
@@ -0,0 +1,65 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_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
+112
View File
@@ -0,0 +1,112 @@
# Copyright 2026 jCloud Services GbR
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from src.jcloud_config_parser import Configuration, set_mutability, is_mutable
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
def test_mutability():
config = Configuration(mutable=False)
try:
config.key = 'value'
assert False, 'Expected TypeError'
except TypeError:
pass
set_mutability(config, True)
assert config._mutable == True
config.key = 'value'
set_mutability(config, False)
assert config._mutable == False
assert is_mutable(config) == False
try:
del config.key
assert False, 'Expected TypeError'
except TypeError:
pass
# Test operate_on_original_object parameter of set_mutability
config = Configuration(mutable = True)
set_mutability(config, False, operate_on_original_object = True)
assert is_mutable(config) == False
new_config = set_mutability(config, True, operate_on_original_object = False)
assert is_mutable(new_config) == True
assert is_mutable(config) == False