json-object-mapper 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- json_object_mapper-2.0.0/LICENSE +3 -0
- json_object_mapper-2.0.0/PKG-INFO +111 -0
- json_object_mapper-2.0.0/README.md +96 -0
- json_object_mapper-2.0.0/pyproject.toml +48 -0
- json_object_mapper-2.0.0/setup.cfg +4 -0
- json_object_mapper-2.0.0/setup.py +3 -0
- json_object_mapper-2.0.0/src/json_object_mapper/__init__.py +6 -0
- json_object_mapper-2.0.0/src/json_object_mapper/core.py +220 -0
- json_object_mapper-2.0.0/src/json_object_mapper/exceptions.py +2 -0
- json_object_mapper-2.0.0/src/json_object_mapper/helpers.py +143 -0
- json_object_mapper-2.0.0/src/json_object_mapper.egg-info/PKG-INFO +111 -0
- json_object_mapper-2.0.0/src/json_object_mapper.egg-info/SOURCES.txt +13 -0
- json_object_mapper-2.0.0/src/json_object_mapper.egg-info/dependency_links.txt +1 -0
- json_object_mapper-2.0.0/src/json_object_mapper.egg-info/top_level.txt +1 -0
- json_object_mapper-2.0.0/tests/test_json_object_mapper.py +128 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: json-object-mapper
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Attribute-style access wrapper for JSON-like data with dot-path set/delete and safe defaults
|
|
5
|
+
Author-email: Kobby Owen <dev@kobbyowen.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kobbyowen/json2obj
|
|
8
|
+
Project-URL: Repository, https://github.com/kobbyowen/json2obj
|
|
9
|
+
Project-URL: Issues, https://github.com/kobbyowen/json2obj/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# json-object-mapper
|
|
17
|
+
|
|
18
|
+
**`json-object-mapper`** is a lightweight Python library that lets you interact with JSON data using **attribute-style access** instead of dictionary keys.
|
|
19
|
+
|
|
20
|
+
The PyPI distribution is published as `json-object-mapper`.
|
|
21
|
+
|
|
22
|
+
It turns JSON objects into `JSONObjectMapper` instances that feel natural to use in Python code, while preserving full JSON compatibility.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from json_object_mapper import JSONObjectMapper
|
|
26
|
+
|
|
27
|
+
data = {
|
|
28
|
+
"user": {"name": "Kobby", "age": 29, "skills": ["python", "aws", "forex"]}
|
|
29
|
+
}
|
|
30
|
+
obj = JSONObjectMapper(data)
|
|
31
|
+
print(obj.user.name) # "Kobby"
|
|
32
|
+
print(obj.user.skills[0]) # "python"
|
|
33
|
+
print(obj.to_json(indent=2))
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## ✨ Features
|
|
37
|
+
|
|
38
|
+
- Attribute-style access (`obj.key`) for dict keys
|
|
39
|
+
- Recursive wrapping for nested dicts and lists
|
|
40
|
+
- Read-only mode (immutability enforced)
|
|
41
|
+
- Dot/bracket path lookups (`obj.get_path("a.b[0].c")`)
|
|
42
|
+
- **New:** `set_path()` / `del_path()` for dot paths
|
|
43
|
+
- **New:** `default_factory` + `autocreate_missing` for safe defaults
|
|
44
|
+
- Utility methods: `to_dict`, `to_json`, `from_json`, `merge`
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install json-object-mapper
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from json_object_mapper import JSONObjectMapper, JSONAccessError
|
|
56
|
+
|
|
57
|
+
# 1) Wrap & read (basic)
|
|
58
|
+
obj = JSONObjectMapper({"user": {"name": "Kobby", "age": 29}})
|
|
59
|
+
assert obj.user.name == "Kobby"
|
|
60
|
+
assert obj.user.age == 29
|
|
61
|
+
|
|
62
|
+
# 2) Write with dot notation
|
|
63
|
+
obj.user.country = "GH"
|
|
64
|
+
assert obj.user.country == "GH"
|
|
65
|
+
|
|
66
|
+
# 3) Lists of dicts (read & write via attribute access)
|
|
67
|
+
obj.services = [{}] # start with a list containing one dict
|
|
68
|
+
obj.services[0].name = "auth" # item is wrapped → dot works
|
|
69
|
+
obj.services[0].enabled = True
|
|
70
|
+
assert obj.services[0].name == "auth"
|
|
71
|
+
assert obj.services[0].enabled is True
|
|
72
|
+
|
|
73
|
+
# 4) Non-identifier keys (use mapping-style access)
|
|
74
|
+
# Keys like "first-name" can't be attributes; use get()/[] instead
|
|
75
|
+
obj.meta = {"first-name": "Kobby"}
|
|
76
|
+
assert obj.meta.get("first-name") == "Kobby"
|
|
77
|
+
assert obj.meta["first-name"] == "Kobby"
|
|
78
|
+
# getattr(obj.meta, "first-name") would raise JSONAccessError
|
|
79
|
+
|
|
80
|
+
# 5) Safe defaults + auto-create (no extra helpers required)
|
|
81
|
+
# Missing attributes produce defaults; with autocreate they persist.
|
|
82
|
+
cfg = JSONObjectMapper({}, default_factory=dict, autocreate_missing=True)
|
|
83
|
+
cfg.profile.settings.theme = "dark" # on-demand creation of nested dicts
|
|
84
|
+
assert cfg.profile.settings.theme == "dark"
|
|
85
|
+
|
|
86
|
+
# 6) Merge convenience (shallow merge into root dict)
|
|
87
|
+
cfg.merge({"features": {"beta": True}})
|
|
88
|
+
assert cfg.features.beta is True
|
|
89
|
+
|
|
90
|
+
# 7) Read-only wrappers (safe reads; writes raise)
|
|
91
|
+
ro = JSONObjectMapper({"debug": True}, readonly=True)
|
|
92
|
+
assert ro.debug is True
|
|
93
|
+
try:
|
|
94
|
+
ro.debug = False
|
|
95
|
+
raise AssertionError("should not be able to write in readonly mode")
|
|
96
|
+
except AttributeError:
|
|
97
|
+
pass
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Tests
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python -m pip install -e .
|
|
104
|
+
python -m unittest discover -s tests -v
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Publishing
|
|
108
|
+
|
|
109
|
+
Publishing is automated with GitHub Actions using PyPI trusted publishing. Create a GitHub release after configuring the `pypi` environment in the repository settings.
|
|
110
|
+
|
|
111
|
+
MIT License.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# json-object-mapper
|
|
2
|
+
|
|
3
|
+
**`json-object-mapper`** is a lightweight Python library that lets you interact with JSON data using **attribute-style access** instead of dictionary keys.
|
|
4
|
+
|
|
5
|
+
The PyPI distribution is published as `json-object-mapper`.
|
|
6
|
+
|
|
7
|
+
It turns JSON objects into `JSONObjectMapper` instances that feel natural to use in Python code, while preserving full JSON compatibility.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from json_object_mapper import JSONObjectMapper
|
|
11
|
+
|
|
12
|
+
data = {
|
|
13
|
+
"user": {"name": "Kobby", "age": 29, "skills": ["python", "aws", "forex"]}
|
|
14
|
+
}
|
|
15
|
+
obj = JSONObjectMapper(data)
|
|
16
|
+
print(obj.user.name) # "Kobby"
|
|
17
|
+
print(obj.user.skills[0]) # "python"
|
|
18
|
+
print(obj.to_json(indent=2))
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## ✨ Features
|
|
22
|
+
|
|
23
|
+
- Attribute-style access (`obj.key`) for dict keys
|
|
24
|
+
- Recursive wrapping for nested dicts and lists
|
|
25
|
+
- Read-only mode (immutability enforced)
|
|
26
|
+
- Dot/bracket path lookups (`obj.get_path("a.b[0].c")`)
|
|
27
|
+
- **New:** `set_path()` / `del_path()` for dot paths
|
|
28
|
+
- **New:** `default_factory` + `autocreate_missing` for safe defaults
|
|
29
|
+
- Utility methods: `to_dict`, `to_json`, `from_json`, `merge`
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install json-object-mapper
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from json_object_mapper import JSONObjectMapper, JSONAccessError
|
|
41
|
+
|
|
42
|
+
# 1) Wrap & read (basic)
|
|
43
|
+
obj = JSONObjectMapper({"user": {"name": "Kobby", "age": 29}})
|
|
44
|
+
assert obj.user.name == "Kobby"
|
|
45
|
+
assert obj.user.age == 29
|
|
46
|
+
|
|
47
|
+
# 2) Write with dot notation
|
|
48
|
+
obj.user.country = "GH"
|
|
49
|
+
assert obj.user.country == "GH"
|
|
50
|
+
|
|
51
|
+
# 3) Lists of dicts (read & write via attribute access)
|
|
52
|
+
obj.services = [{}] # start with a list containing one dict
|
|
53
|
+
obj.services[0].name = "auth" # item is wrapped → dot works
|
|
54
|
+
obj.services[0].enabled = True
|
|
55
|
+
assert obj.services[0].name == "auth"
|
|
56
|
+
assert obj.services[0].enabled is True
|
|
57
|
+
|
|
58
|
+
# 4) Non-identifier keys (use mapping-style access)
|
|
59
|
+
# Keys like "first-name" can't be attributes; use get()/[] instead
|
|
60
|
+
obj.meta = {"first-name": "Kobby"}
|
|
61
|
+
assert obj.meta.get("first-name") == "Kobby"
|
|
62
|
+
assert obj.meta["first-name"] == "Kobby"
|
|
63
|
+
# getattr(obj.meta, "first-name") would raise JSONAccessError
|
|
64
|
+
|
|
65
|
+
# 5) Safe defaults + auto-create (no extra helpers required)
|
|
66
|
+
# Missing attributes produce defaults; with autocreate they persist.
|
|
67
|
+
cfg = JSONObjectMapper({}, default_factory=dict, autocreate_missing=True)
|
|
68
|
+
cfg.profile.settings.theme = "dark" # on-demand creation of nested dicts
|
|
69
|
+
assert cfg.profile.settings.theme == "dark"
|
|
70
|
+
|
|
71
|
+
# 6) Merge convenience (shallow merge into root dict)
|
|
72
|
+
cfg.merge({"features": {"beta": True}})
|
|
73
|
+
assert cfg.features.beta is True
|
|
74
|
+
|
|
75
|
+
# 7) Read-only wrappers (safe reads; writes raise)
|
|
76
|
+
ro = JSONObjectMapper({"debug": True}, readonly=True)
|
|
77
|
+
assert ro.debug is True
|
|
78
|
+
try:
|
|
79
|
+
ro.debug = False
|
|
80
|
+
raise AssertionError("should not be able to write in readonly mode")
|
|
81
|
+
except AttributeError:
|
|
82
|
+
pass
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Tests
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python -m pip install -e .
|
|
89
|
+
python -m unittest discover -s tests -v
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Publishing
|
|
93
|
+
|
|
94
|
+
Publishing is automated with GitHub Actions using PyPI trusted publishing. Create a GitHub release after configuring the `pypi` environment in the repository settings.
|
|
95
|
+
|
|
96
|
+
MIT License.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "json-object-mapper"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Attribute-style access wrapper for JSON-like data with dot-path set/delete and safe defaults"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
authors = [{name="Kobby Owen", email="dev@kobbyowen.com"}]
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ["LICENSE"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/kobbyowen/json2obj"
|
|
20
|
+
Repository = "https://github.com/kobbyowen/json2obj"
|
|
21
|
+
Issues = "https://github.com/kobbyowen/json2obj/issues"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
package-dir = {"" = "src"}
|
|
25
|
+
include-package-data = true
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
addopts = "-q"
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
line-length = 100
|
|
35
|
+
target-version = "py38" # change to py39/py310/py311 as needed
|
|
36
|
+
extend-exclude = ["build", "dist", ".venv", ".git", ".mypy_cache", ".pytest_cache"]
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint]
|
|
39
|
+
# Rule sets: E (pycodestyle), F (pyflakes), I (isort), UP (pyupgrade), B (bugbear),
|
|
40
|
+
# SIM (simplify), PL (pylint-lite), RUF (ruff-specific)
|
|
41
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "PL", "RUF"]
|
|
42
|
+
ignore = ["E501"] # ignore line-length; if you use Ruff as formatter, you can remove this
|
|
43
|
+
per-file-ignores = { "tests/*" = ["PLR2004"] } # allow magic numbers in tests
|
|
44
|
+
|
|
45
|
+
[tool.ruff.format] # only used if you run "ruff format"
|
|
46
|
+
quote-style = "double"
|
|
47
|
+
indent-style = "space"
|
|
48
|
+
skip-magic-trailing-comma = false
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Callable,
|
|
6
|
+
Dict,
|
|
7
|
+
Iterable,
|
|
8
|
+
Iterator,
|
|
9
|
+
List,
|
|
10
|
+
Mapping,
|
|
11
|
+
MutableMapping,
|
|
12
|
+
Optional,
|
|
13
|
+
Tuple,
|
|
14
|
+
Union,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .exceptions import JSONAccessError
|
|
18
|
+
from .helpers import (
|
|
19
|
+
assign_at,
|
|
20
|
+
delete_on_parent,
|
|
21
|
+
ensure_next,
|
|
22
|
+
is_identifier,
|
|
23
|
+
is_last,
|
|
24
|
+
parse_path_tokens,
|
|
25
|
+
peek_is_index,
|
|
26
|
+
traverse_parent,
|
|
27
|
+
wrap_value,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
JSONScalar = Union[str, int, float, bool, None]
|
|
31
|
+
JSONType = Union[JSONScalar, "JSONObjectMapper", List["JSONObjectMapper"], Dict[str, Any]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class JSONObjectMapper:
|
|
35
|
+
__slots__ = ("__autocreate_missing", "__default_factory", "__json", "__readonly")
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
data: Union[str, bytes, bytearray, Mapping[str, Any], MutableMapping[str, Any], List[Any]],
|
|
40
|
+
*,
|
|
41
|
+
readonly: bool = False,
|
|
42
|
+
default_factory: Optional[Callable[[], Any]] = None,
|
|
43
|
+
autocreate_missing: bool = False,
|
|
44
|
+
_no_copy: bool = False,
|
|
45
|
+
) -> None:
|
|
46
|
+
self.__json = (
|
|
47
|
+
json.loads(data)
|
|
48
|
+
if isinstance(data, (str, bytes, bytearray))
|
|
49
|
+
else (data if _no_copy else copy.deepcopy(data))
|
|
50
|
+
)
|
|
51
|
+
if not isinstance(self.__json, (dict, list)):
|
|
52
|
+
raise TypeError("JSONObjectMapper must wrap a dict or list (or JSON string thereof).")
|
|
53
|
+
self.__readonly = bool(readonly)
|
|
54
|
+
self.__default_factory = default_factory
|
|
55
|
+
self.__autocreate_missing = bool(autocreate_missing)
|
|
56
|
+
|
|
57
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
58
|
+
return f"JSONObjectMapper(readonly={self.__readonly}, data={self.__json!r})"
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> Any:
|
|
61
|
+
return copy.deepcopy(self.__json)
|
|
62
|
+
|
|
63
|
+
def to_json(self, *, indent: Optional[int] = None, sort_keys: bool = False) -> str:
|
|
64
|
+
return json.dumps(self.__json, indent=indent, sort_keys=sort_keys)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_json(
|
|
68
|
+
cls, json_string: Union[str, bytes, bytearray], *, readonly: bool = False
|
|
69
|
+
) -> "JSONObjectMapper":
|
|
70
|
+
return cls(json_string, readonly=readonly)
|
|
71
|
+
|
|
72
|
+
def readonly(self) -> bool:
|
|
73
|
+
return self.__readonly
|
|
74
|
+
|
|
75
|
+
# ---------------------------- attribute access ----------------------------
|
|
76
|
+
|
|
77
|
+
def __getattr__(self, attribute_name: str) -> Any:
|
|
78
|
+
data = object.__getattribute__(self, "_JSONObjectMapper__json")
|
|
79
|
+
if isinstance(data, list):
|
|
80
|
+
raise JSONAccessError(f"Cannot access attribute '{attribute_name}' on list")
|
|
81
|
+
if attribute_name in type(self).__dict__:
|
|
82
|
+
return object.__getattribute__(self, attribute_name)
|
|
83
|
+
if not is_identifier(attribute_name):
|
|
84
|
+
raise JSONAccessError(f"'{attribute_name}' is not a valid attribute identifier")
|
|
85
|
+
try:
|
|
86
|
+
value = data[attribute_name]
|
|
87
|
+
except KeyError as error:
|
|
88
|
+
value = self._on_missing_attr(data, attribute_name, error)
|
|
89
|
+
return self._wrap(value)
|
|
90
|
+
|
|
91
|
+
def _on_missing_attr(self, data: dict, attribute_name: str, error: Exception) -> Any:
|
|
92
|
+
factory = object.__getattribute__(self, "_JSONObjectMapper__default_factory")
|
|
93
|
+
readonly_flag = object.__getattribute__(self, "_JSONObjectMapper__readonly")
|
|
94
|
+
autocreate_flag = object.__getattribute__(self, "_JSONObjectMapper__autocreate_missing")
|
|
95
|
+
if factory is None:
|
|
96
|
+
raise JSONAccessError(f"'{attribute_name}' not found") from error
|
|
97
|
+
produced = factory()
|
|
98
|
+
if autocreate_flag and not readonly_flag:
|
|
99
|
+
data[attribute_name] = produced
|
|
100
|
+
return produced
|
|
101
|
+
|
|
102
|
+
def _wrap(self, value: Any) -> Any:
|
|
103
|
+
readonly_flag = object.__getattribute__(self, "_JSONObjectMapper__readonly")
|
|
104
|
+
default_factory = object.__getattribute__(self, "_JSONObjectMapper__default_factory")
|
|
105
|
+
autocreate_flag = object.__getattribute__(self, "_JSONObjectMapper__autocreate_missing")
|
|
106
|
+
return wrap_value(
|
|
107
|
+
value, readonly_flag, default_factory, autocreate_flag, factory_object=self.__class__
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def __setattr__(self, attribute_name: str, value: Any) -> None:
|
|
111
|
+
if attribute_name in {
|
|
112
|
+
"_JSONObjectMapper__json",
|
|
113
|
+
"_JSONObjectMapper__readonly",
|
|
114
|
+
"_JSONObjectMapper__default_factory",
|
|
115
|
+
"_JSONObjectMapper__autocreate_missing",
|
|
116
|
+
}:
|
|
117
|
+
return object.__setattr__(self, attribute_name, value)
|
|
118
|
+
if self.__readonly:
|
|
119
|
+
raise AttributeError("Mapper is read-only")
|
|
120
|
+
if isinstance(self.__json, list):
|
|
121
|
+
raise AttributeError("Cannot set attributes on a list")
|
|
122
|
+
if not is_identifier(attribute_name):
|
|
123
|
+
raise AttributeError(f"'{attribute_name}' is not a valid attribute name")
|
|
124
|
+
self.__json[attribute_name] = value
|
|
125
|
+
|
|
126
|
+
# ----------------------------- mapping access -----------------------------
|
|
127
|
+
|
|
128
|
+
def __getitem__(self, key: Union[int, str]) -> Any:
|
|
129
|
+
try:
|
|
130
|
+
value = self.__json[key] # type: ignore[index]
|
|
131
|
+
except Exception as error:
|
|
132
|
+
raise KeyError(key) from error
|
|
133
|
+
return self._wrap(value)
|
|
134
|
+
|
|
135
|
+
def __setitem__(self, key: Union[int, str], value: Any) -> None:
|
|
136
|
+
if self.__readonly:
|
|
137
|
+
raise AttributeError("Mapper is read-only")
|
|
138
|
+
self.__json[key] = value # type: ignore[index]
|
|
139
|
+
|
|
140
|
+
def __iter__(self) -> Iterator:
|
|
141
|
+
return iter(self.__json)
|
|
142
|
+
|
|
143
|
+
def __len__(self) -> int:
|
|
144
|
+
return len(self.__json)
|
|
145
|
+
|
|
146
|
+
def get(self, key: Union[str, int], default: Any = None) -> Any:
|
|
147
|
+
try:
|
|
148
|
+
value = self.__json[key] # type: ignore[index]
|
|
149
|
+
except Exception:
|
|
150
|
+
return default
|
|
151
|
+
return self._wrap(value)
|
|
152
|
+
|
|
153
|
+
def keys(self) -> Iterable:
|
|
154
|
+
if isinstance(self.__json, dict):
|
|
155
|
+
return self.__json.keys()
|
|
156
|
+
raise TypeError("keys() only valid for dict roots")
|
|
157
|
+
|
|
158
|
+
def items(self) -> Iterable[Tuple[Any, Any]]:
|
|
159
|
+
if isinstance(self.__json, dict):
|
|
160
|
+
return ((key, self._wrap(value)) for key, value in self.__json.items())
|
|
161
|
+
raise TypeError("items() only valid for dict roots")
|
|
162
|
+
|
|
163
|
+
def values(self) -> Iterable[Any]:
|
|
164
|
+
if isinstance(self.__json, dict):
|
|
165
|
+
return (self._wrap(value) for value in self.__json.values())
|
|
166
|
+
raise TypeError("values() only valid for dict roots")
|
|
167
|
+
|
|
168
|
+
# -------------------------------- path ops --------------------------------
|
|
169
|
+
|
|
170
|
+
def get_path(self, path: str, default: Any = None) -> Any:
|
|
171
|
+
current: Any = self
|
|
172
|
+
for token in parse_path_tokens(path):
|
|
173
|
+
try:
|
|
174
|
+
current = (
|
|
175
|
+
getattr(current, token.group("name"))
|
|
176
|
+
if token.group("name")
|
|
177
|
+
else current[int(token.group("index"))]
|
|
178
|
+
)
|
|
179
|
+
except Exception:
|
|
180
|
+
return default
|
|
181
|
+
return current
|
|
182
|
+
|
|
183
|
+
def set_path(self, path: str, value: Any, *, create_parents: bool = True) -> "JSONObjectMapper":
|
|
184
|
+
if self.__readonly:
|
|
185
|
+
raise AttributeError("Mapper is read-only")
|
|
186
|
+
tokens = parse_path_tokens(path)
|
|
187
|
+
if not tokens:
|
|
188
|
+
raise ValueError("Empty path")
|
|
189
|
+
current = self.__json
|
|
190
|
+
for index, token in enumerate(tokens):
|
|
191
|
+
if is_last(index, tokens):
|
|
192
|
+
assign_at(current, token, value, create_parents, path)
|
|
193
|
+
return self
|
|
194
|
+
current = ensure_next(
|
|
195
|
+
current, token, peek_is_index(tokens, index), create_parents, path
|
|
196
|
+
)
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
def del_path(self, path: str, *, raise_on_missing: bool = False) -> "JSONObjectMapper":
|
|
200
|
+
if self.__readonly:
|
|
201
|
+
raise AttributeError("Mapper is read-only")
|
|
202
|
+
tokens = parse_path_tokens(path)
|
|
203
|
+
if not tokens:
|
|
204
|
+
raise ValueError("Empty path")
|
|
205
|
+
parent = traverse_parent(self.__json, tokens, path, raise_on_missing)
|
|
206
|
+
if parent is None:
|
|
207
|
+
return self
|
|
208
|
+
delete_on_parent(parent, tokens[-1], raise_on_missing)
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
# --------------------------------- merge ----------------------------------
|
|
212
|
+
|
|
213
|
+
def merge(self, other: Mapping[str, Any]) -> "JSONObjectMapper":
|
|
214
|
+
if self.__readonly:
|
|
215
|
+
raise AttributeError("Mapper is read-only")
|
|
216
|
+
if not isinstance(self.__json, dict):
|
|
217
|
+
raise TypeError("merge() requires a dict root")
|
|
218
|
+
for key, value in other.items():
|
|
219
|
+
self.__json[key] = value
|
|
220
|
+
return self
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Callable, List, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def is_identifier(key: str) -> bool:
|
|
6
|
+
return bool(re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", key))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PATH_TOKEN = re.compile(
|
|
10
|
+
r"""
|
|
11
|
+
(?P<name>[A-Za-z_][A-Za-z0-9_]*) # identifier
|
|
12
|
+
| \[ (?P<index>\d+) \] # [index]
|
|
13
|
+
""",
|
|
14
|
+
re.VERBOSE,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ensure_list_length(lst: List[Any], index: int) -> None:
|
|
19
|
+
if index >= len(lst):
|
|
20
|
+
lst.extend([None] * (index + 1 - len(lst)))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_path_tokens(path: str):
|
|
24
|
+
return list(PATH_TOKEN.finditer(path.replace(".", " ")))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_last(current_index: int, tokens: List[Any]) -> bool:
|
|
28
|
+
return current_index == len(tokens) - 1
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def peek_is_index(tokens: List[Any], current_index: int) -> bool:
|
|
32
|
+
return tokens[current_index + 1].group("index") is not None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def expect_dict(obj: Any, context: str):
|
|
36
|
+
if not isinstance(obj, dict):
|
|
37
|
+
raise TypeError(f"Expected dict {context}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def expect_list(obj: Any, context: str):
|
|
41
|
+
if not isinstance(obj, list):
|
|
42
|
+
raise TypeError(f"Expected list {context}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_token_value(container: Any, token):
|
|
46
|
+
name, index = token.group("name"), token.group("index")
|
|
47
|
+
if name is not None:
|
|
48
|
+
expect_dict(container, f"before '{name}'")
|
|
49
|
+
return container[name]
|
|
50
|
+
expect_list(container, f"before index [{index}]")
|
|
51
|
+
return container[int(index)]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def assign_at(container: Any, token, value: Any, create_parents: bool, path: str):
|
|
55
|
+
name, index = token.group("name"), token.group("index")
|
|
56
|
+
if name is not None:
|
|
57
|
+
expect_dict(container, f"in '{path}'")
|
|
58
|
+
container[name] = value
|
|
59
|
+
return
|
|
60
|
+
expect_list(container, f"in '{path}'")
|
|
61
|
+
index_int = int(index)
|
|
62
|
+
if index_int >= len(container) and not create_parents:
|
|
63
|
+
raise IndexError(f"Index {index_int} out of range in '{path}'")
|
|
64
|
+
ensure_list_length(container, index_int)
|
|
65
|
+
container[index_int] = value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ensure_next(container: Any, token, next_is_index: bool, create_parents: bool, path: str):
|
|
69
|
+
name, index = token.group("name"), token.group("index")
|
|
70
|
+
if name is not None:
|
|
71
|
+
expect_dict(container, f"in '{path}'")
|
|
72
|
+
next_value = container.get(name)
|
|
73
|
+
if next_value is None:
|
|
74
|
+
if not create_parents:
|
|
75
|
+
raise KeyError(f"Missing key '{name}' in '{path}'")
|
|
76
|
+
container[name] = [] if next_is_index else {}
|
|
77
|
+
next_value = container[name]
|
|
78
|
+
return next_value
|
|
79
|
+
expect_list(container, f"in '{path}'")
|
|
80
|
+
index_int = int(index)
|
|
81
|
+
if index_int >= len(container):
|
|
82
|
+
if not create_parents:
|
|
83
|
+
raise IndexError(f"Index {index_int} out of range in '{path}'")
|
|
84
|
+
ensure_list_length(container, index_int)
|
|
85
|
+
container[index_int] = [] if next_is_index else {}
|
|
86
|
+
return container[index_int]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def traverse_parent(root: Any, tokens: List[Any], path: str, strict: bool):
|
|
90
|
+
current = root
|
|
91
|
+
for _, token in enumerate(tokens[:-1]):
|
|
92
|
+
try:
|
|
93
|
+
current = get_token_value(current, token)
|
|
94
|
+
except Exception:
|
|
95
|
+
if strict:
|
|
96
|
+
segment = token.group("name") or f"[{token.group('index')}]"
|
|
97
|
+
raise KeyError(f"Missing segment before end at '{segment}' in '{path}'") from None
|
|
98
|
+
return None
|
|
99
|
+
return current
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def delete_on_parent(parent: Any, last_token, strict: bool):
|
|
103
|
+
name, index = last_token.group("name"), last_token.group("index")
|
|
104
|
+
if name is not None:
|
|
105
|
+
expect_dict(parent, "for deletion")
|
|
106
|
+
if name in parent:
|
|
107
|
+
del parent[name]
|
|
108
|
+
elif strict:
|
|
109
|
+
raise KeyError(f"Missing key '{name}'")
|
|
110
|
+
return
|
|
111
|
+
expect_list(parent, "for deletion")
|
|
112
|
+
index_int = int(index)
|
|
113
|
+
if 0 <= index_int < len(parent):
|
|
114
|
+
parent.pop(index_int)
|
|
115
|
+
elif strict:
|
|
116
|
+
raise IndexError(f"Index {index_int} out of range")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def wrap_value(
|
|
120
|
+
value: Any,
|
|
121
|
+
readonly: bool,
|
|
122
|
+
default_factory: Optional[Callable[[], Any]],
|
|
123
|
+
autocreate_missing: bool,
|
|
124
|
+
factory_object: type,
|
|
125
|
+
) -> Any:
|
|
126
|
+
if isinstance(value, dict):
|
|
127
|
+
return factory_object(
|
|
128
|
+
value,
|
|
129
|
+
readonly=readonly,
|
|
130
|
+
default_factory=default_factory,
|
|
131
|
+
autocreate_missing=autocreate_missing,
|
|
132
|
+
_no_copy=True,
|
|
133
|
+
)
|
|
134
|
+
if isinstance(value, list):
|
|
135
|
+
return [
|
|
136
|
+
(
|
|
137
|
+
wrap_value(item, readonly, default_factory, autocreate_missing, factory_object)
|
|
138
|
+
if isinstance(item, (dict, list))
|
|
139
|
+
else item
|
|
140
|
+
)
|
|
141
|
+
for item in value
|
|
142
|
+
]
|
|
143
|
+
return value
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: json-object-mapper
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Attribute-style access wrapper for JSON-like data with dot-path set/delete and safe defaults
|
|
5
|
+
Author-email: Kobby Owen <dev@kobbyowen.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kobbyowen/json2obj
|
|
8
|
+
Project-URL: Repository, https://github.com/kobbyowen/json2obj
|
|
9
|
+
Project-URL: Issues, https://github.com/kobbyowen/json2obj/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# json-object-mapper
|
|
17
|
+
|
|
18
|
+
**`json-object-mapper`** is a lightweight Python library that lets you interact with JSON data using **attribute-style access** instead of dictionary keys.
|
|
19
|
+
|
|
20
|
+
The PyPI distribution is published as `json-object-mapper`.
|
|
21
|
+
|
|
22
|
+
It turns JSON objects into `JSONObjectMapper` instances that feel natural to use in Python code, while preserving full JSON compatibility.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from json_object_mapper import JSONObjectMapper
|
|
26
|
+
|
|
27
|
+
data = {
|
|
28
|
+
"user": {"name": "Kobby", "age": 29, "skills": ["python", "aws", "forex"]}
|
|
29
|
+
}
|
|
30
|
+
obj = JSONObjectMapper(data)
|
|
31
|
+
print(obj.user.name) # "Kobby"
|
|
32
|
+
print(obj.user.skills[0]) # "python"
|
|
33
|
+
print(obj.to_json(indent=2))
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## ✨ Features
|
|
37
|
+
|
|
38
|
+
- Attribute-style access (`obj.key`) for dict keys
|
|
39
|
+
- Recursive wrapping for nested dicts and lists
|
|
40
|
+
- Read-only mode (immutability enforced)
|
|
41
|
+
- Dot/bracket path lookups (`obj.get_path("a.b[0].c")`)
|
|
42
|
+
- **New:** `set_path()` / `del_path()` for dot paths
|
|
43
|
+
- **New:** `default_factory` + `autocreate_missing` for safe defaults
|
|
44
|
+
- Utility methods: `to_dict`, `to_json`, `from_json`, `merge`
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install json-object-mapper
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from json_object_mapper import JSONObjectMapper, JSONAccessError
|
|
56
|
+
|
|
57
|
+
# 1) Wrap & read (basic)
|
|
58
|
+
obj = JSONObjectMapper({"user": {"name": "Kobby", "age": 29}})
|
|
59
|
+
assert obj.user.name == "Kobby"
|
|
60
|
+
assert obj.user.age == 29
|
|
61
|
+
|
|
62
|
+
# 2) Write with dot notation
|
|
63
|
+
obj.user.country = "GH"
|
|
64
|
+
assert obj.user.country == "GH"
|
|
65
|
+
|
|
66
|
+
# 3) Lists of dicts (read & write via attribute access)
|
|
67
|
+
obj.services = [{}] # start with a list containing one dict
|
|
68
|
+
obj.services[0].name = "auth" # item is wrapped → dot works
|
|
69
|
+
obj.services[0].enabled = True
|
|
70
|
+
assert obj.services[0].name == "auth"
|
|
71
|
+
assert obj.services[0].enabled is True
|
|
72
|
+
|
|
73
|
+
# 4) Non-identifier keys (use mapping-style access)
|
|
74
|
+
# Keys like "first-name" can't be attributes; use get()/[] instead
|
|
75
|
+
obj.meta = {"first-name": "Kobby"}
|
|
76
|
+
assert obj.meta.get("first-name") == "Kobby"
|
|
77
|
+
assert obj.meta["first-name"] == "Kobby"
|
|
78
|
+
# getattr(obj.meta, "first-name") would raise JSONAccessError
|
|
79
|
+
|
|
80
|
+
# 5) Safe defaults + auto-create (no extra helpers required)
|
|
81
|
+
# Missing attributes produce defaults; with autocreate they persist.
|
|
82
|
+
cfg = JSONObjectMapper({}, default_factory=dict, autocreate_missing=True)
|
|
83
|
+
cfg.profile.settings.theme = "dark" # on-demand creation of nested dicts
|
|
84
|
+
assert cfg.profile.settings.theme == "dark"
|
|
85
|
+
|
|
86
|
+
# 6) Merge convenience (shallow merge into root dict)
|
|
87
|
+
cfg.merge({"features": {"beta": True}})
|
|
88
|
+
assert cfg.features.beta is True
|
|
89
|
+
|
|
90
|
+
# 7) Read-only wrappers (safe reads; writes raise)
|
|
91
|
+
ro = JSONObjectMapper({"debug": True}, readonly=True)
|
|
92
|
+
assert ro.debug is True
|
|
93
|
+
try:
|
|
94
|
+
ro.debug = False
|
|
95
|
+
raise AssertionError("should not be able to write in readonly mode")
|
|
96
|
+
except AttributeError:
|
|
97
|
+
pass
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Tests
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python -m pip install -e .
|
|
104
|
+
python -m unittest discover -s tests -v
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Publishing
|
|
108
|
+
|
|
109
|
+
Publishing is automated with GitHub Actions using PyPI trusted publishing. Create a GitHub release after configuring the `pypi` environment in the repository settings.
|
|
110
|
+
|
|
111
|
+
MIT License.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
src/json_object_mapper/__init__.py
|
|
6
|
+
src/json_object_mapper/core.py
|
|
7
|
+
src/json_object_mapper/exceptions.py
|
|
8
|
+
src/json_object_mapper/helpers.py
|
|
9
|
+
src/json_object_mapper.egg-info/PKG-INFO
|
|
10
|
+
src/json_object_mapper.egg-info/SOURCES.txt
|
|
11
|
+
src/json_object_mapper.egg-info/dependency_links.txt
|
|
12
|
+
src/json_object_mapper.egg-info/top_level.txt
|
|
13
|
+
tests/test_json_object_mapper.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
json_object_mapper
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from json_object_mapper import JSONAccessError, JSONObjectMapper
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestJSONObjectMapper(unittest.TestCase):
|
|
7
|
+
def test_attr_access(self):
|
|
8
|
+
m = JSONObjectMapper({"a": {"b": 1}})
|
|
9
|
+
self.assertEqual(m.a.b, 1)
|
|
10
|
+
|
|
11
|
+
def test_list_indexing(self):
|
|
12
|
+
m = JSONObjectMapper({"xs": [{"v": 1}, {"v": 2}]})
|
|
13
|
+
self.assertEqual(m.xs[1].v, 2)
|
|
14
|
+
|
|
15
|
+
def test_invalid_attr(self):
|
|
16
|
+
m = JSONObjectMapper({"a": 1})
|
|
17
|
+
with self.assertRaises(JSONAccessError):
|
|
18
|
+
_ = m.not_here
|
|
19
|
+
|
|
20
|
+
def test_non_identifier_key(self):
|
|
21
|
+
m = JSONObjectMapper({"not-valid!": 3})
|
|
22
|
+
self.assertEqual(m.get("not-valid!"), 3)
|
|
23
|
+
with self.assertRaises(JSONAccessError):
|
|
24
|
+
_ = getattr(m, "not-valid!")
|
|
25
|
+
|
|
26
|
+
def test_set_readonly(self):
|
|
27
|
+
m = JSONObjectMapper({"a": 1}, readonly=True)
|
|
28
|
+
with self.assertRaises(AttributeError):
|
|
29
|
+
m.a = 2
|
|
30
|
+
with self.assertRaises(AttributeError):
|
|
31
|
+
m["a"] = 2
|
|
32
|
+
|
|
33
|
+
def test_merge(self):
|
|
34
|
+
m = JSONObjectMapper({"a": 1})
|
|
35
|
+
m.merge({"b": 2})
|
|
36
|
+
self.assertEqual(m.b, 2)
|
|
37
|
+
|
|
38
|
+
def test_to_from_json(self):
|
|
39
|
+
m = JSONObjectMapper.from_json('{"x": 7, "y": {"z": 9}}')
|
|
40
|
+
self.assertEqual(m.y.z, 9)
|
|
41
|
+
s = m.to_json()
|
|
42
|
+
self.assertIn('"x": 7', s)
|
|
43
|
+
|
|
44
|
+
def test_get_path(self):
|
|
45
|
+
m = JSONObjectMapper({"u": {"n": {"a": [{"me": "k"}]}}})
|
|
46
|
+
self.assertEqual(m.get_path("u.n.a[0].me"), "k")
|
|
47
|
+
self.assertEqual(m.get_path("u.missing", default=None), None)
|
|
48
|
+
|
|
49
|
+
def test_default_factory_no_autocreate(self):
|
|
50
|
+
m = JSONObjectMapper({}, default_factory=dict)
|
|
51
|
+
self.assertEqual(m.profile.to_dict(), {})
|
|
52
|
+
self.assertNotIn("profile", m.to_dict())
|
|
53
|
+
|
|
54
|
+
def test_default_factory_with_autocreate(self):
|
|
55
|
+
m = JSONObjectMapper({}, default_factory=dict, autocreate_missing=True)
|
|
56
|
+
_ = m.profile
|
|
57
|
+
self.assertIn("profile", m.to_dict())
|
|
58
|
+
m.profile.settings = {"theme": "dark"}
|
|
59
|
+
self.assertEqual(m.profile.settings.theme, "dark")
|
|
60
|
+
|
|
61
|
+
def test_default_factory_readonly(self):
|
|
62
|
+
m = JSONObjectMapper({}, default_factory=dict, autocreate_missing=True, readonly=True)
|
|
63
|
+
_ = m.profile
|
|
64
|
+
self.assertNotIn("profile", m.to_dict())
|
|
65
|
+
|
|
66
|
+
def test_set_path_simple_create_parents(self):
|
|
67
|
+
m = JSONObjectMapper({})
|
|
68
|
+
m.set_path("a.b[0].c", 10)
|
|
69
|
+
self.assertEqual(m.get_path("a.b[0].c"), 10)
|
|
70
|
+
self.assertEqual(m.to_dict(), {"a": {"b": [{"c": 10}]}})
|
|
71
|
+
|
|
72
|
+
def test_set_path_replace_value(self):
|
|
73
|
+
m = JSONObjectMapper({"a": {"b": [{"c": 1}]}})
|
|
74
|
+
m.set_path("a.b[0].c", 2)
|
|
75
|
+
self.assertEqual(m.get_path("a.b[0].c"), 2)
|
|
76
|
+
|
|
77
|
+
def test_set_path_no_create_parents_errors(self):
|
|
78
|
+
m = JSONObjectMapper({})
|
|
79
|
+
with self.assertRaises(KeyError):
|
|
80
|
+
m.set_path("a.b.c", 1, create_parents=False)
|
|
81
|
+
m = JSONObjectMapper({"a": {}})
|
|
82
|
+
with self.assertRaises(TypeError):
|
|
83
|
+
m.set_path("a[0].c", 1, create_parents=False)
|
|
84
|
+
m = JSONObjectMapper({"a": {"b": []}})
|
|
85
|
+
with self.assertRaises(IndexError):
|
|
86
|
+
m.set_path("a.b[2].c", 1, create_parents=False)
|
|
87
|
+
|
|
88
|
+
def test_set_path_list_growth(self):
|
|
89
|
+
m = JSONObjectMapper({"xs": []})
|
|
90
|
+
m.set_path("xs[2]", 99)
|
|
91
|
+
self.assertEqual(m.to_dict(), {"xs": [None, None, 99]})
|
|
92
|
+
|
|
93
|
+
def test_del_path_dict_key(self):
|
|
94
|
+
m = JSONObjectMapper({"a": {"b": 1, "c": 2}})
|
|
95
|
+
m.del_path("a.b")
|
|
96
|
+
self.assertEqual(m.to_dict(), {"a": {"c": 2}})
|
|
97
|
+
|
|
98
|
+
def test_del_path_list_index(self):
|
|
99
|
+
m = JSONObjectMapper({"xs": [10, 20, 30]})
|
|
100
|
+
m.del_path("xs[1]")
|
|
101
|
+
self.assertEqual(m.to_dict(), {"xs": [10, 30]})
|
|
102
|
+
|
|
103
|
+
def test_del_path_missing_silent(self):
|
|
104
|
+
m = JSONObjectMapper({"a": {"b": 1}})
|
|
105
|
+
m.del_path("a.c")
|
|
106
|
+
self.assertEqual(m.to_dict(), {"a": {"b": 1}})
|
|
107
|
+
|
|
108
|
+
def test_del_path_missing_raise(self):
|
|
109
|
+
m = JSONObjectMapper({"a": {"b": 1}})
|
|
110
|
+
with self.assertRaises(KeyError):
|
|
111
|
+
m.del_path("a.c", raise_on_missing=True)
|
|
112
|
+
m2 = JSONObjectMapper({"xs": [1]})
|
|
113
|
+
with self.assertRaises(IndexError):
|
|
114
|
+
m2.del_path("xs[5]", raise_on_missing=True)
|
|
115
|
+
|
|
116
|
+
def test_set_path_readonly_raises(self):
|
|
117
|
+
m = JSONObjectMapper({}, readonly=True)
|
|
118
|
+
with self.assertRaises(AttributeError):
|
|
119
|
+
m.set_path("a.b", 1)
|
|
120
|
+
|
|
121
|
+
def test_del_path_readonly_raises(self):
|
|
122
|
+
m = JSONObjectMapper({"a": {"b": 1}}, readonly=True)
|
|
123
|
+
with self.assertRaises(AttributeError):
|
|
124
|
+
m.del_path("a.b")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
unittest.main()
|