Initial commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
dist/
|
||||||
|
src/*.egg-info/
|
||||||
|
.vscode/
|
||||||
|
.pytest_cache/
|
||||||
|
__pycache__/
|
||||||
@@ -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.
|
||||||
@@ -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`
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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__
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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): ...
|
||||||
@@ -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()
|
||||||
@@ -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'
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
@@ -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'
|
||||||
|
]
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
} } }
|
||||||
|
}
|
||||||
|
} }}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user