Skip to content

Commit 52258f1

Browse files
committed
Merge branch 'master' of github.com:materialsvirtuallab/monty
2 parents 28be785 + b5dcadf commit 52258f1

File tree

8 files changed

+84
-18
lines changed

8 files changed

+84
-18
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ jobs:
88
fail-fast: false
99
max-parallel: 20
1010
matrix:
11-
os: [ubuntu-latest, macos-14] #, windows-latest]
12-
python-version: ["3.10", "3.12"]
11+
os: [ubuntu-latest, macos-14, windows-latest]
12+
python-version: ["3.10", "3.11", "3.12", "3.13"]
1313

1414
runs-on: ${{ matrix.os }}
1515

1616
steps:
1717
- uses: actions/checkout@v4
1818

19-
- name: Set up Python ${{ matrix.python }}
19+
- name: Set up Python ${{ matrix.python-version }}
2020
uses: actions/setup-python@v5
2121
with:
22-
python-version: ${{ matrix.python }}
22+
python-version: ${{ matrix.python-version }}
2323

2424
- name: Install dependencies
2525
run: |

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ maintainers = [
99
]
1010
description = "Monty is the missing complement to Python."
1111
readme = "README.md"
12-
requires-python = ">=3.10"
12+
requires-python = ">=3.10,<3.14"
1313
classifiers = [
1414
"Programming Language :: Python :: 3",
1515
"Development Status :: 4 - Beta",

src/monty/collections.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ def __setitem__(self, key, value) -> None:
8989

9090
def update(self, *args, **kwargs) -> None:
9191
"""Forbid adding or updating keys based on _allow_add and _allow_update."""
92-
for key in dict(*args, **kwargs):
92+
93+
updates = dict(*args, **kwargs)
94+
for key in updates:
9395
if key not in self.data and not self._allow_add:
9496
raise TypeError(
9597
f"Cannot add new key {key!r} using update, because add is disabled."
@@ -99,7 +101,7 @@ def update(self, *args, **kwargs) -> None:
99101
f"Cannot update key {key!r} using update, because update is disabled."
100102
)
101103

102-
super().update(*args, **kwargs)
104+
super().update(updates)
103105

104106
def setdefault(self, key, default=None) -> Any:
105107
"""Forbid adding or updating keys based on _allow_add and _allow_update.

src/monty/io.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def zopen(
6161
# TODO: remove default value of `mode` to force user to give one after deadline
6262
if mode is None:
6363
warnings.warn(
64-
"We strongly discourage using a default `mode`, it would be"
64+
"We strongly discourage using a default `mode`, it would be "
6565
f"set to `r` now but would not be allowed after {_deadline}",
6666
FutureWarning,
6767
stacklevel=2,

src/monty/json.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,8 @@ def jsanitize(
905905
jsanitize will try to get the as_dict() attribute of the object. If
906906
no such attribute is found, an attribute error will be thrown. If
907907
strict is False, jsanitize will simply call str(object) to convert
908-
the object to a string representation.
908+
the object to a string representation. If "skip" is provided,
909+
jsanitize will skip and return the original object without modification.
909910
allow_bson (bool): This parameter sets the behavior when jsanitize
910911
encounters a bson supported type such as objectid and datetime. If
911912
True, such bson types will be ignored, allowing for proper
@@ -1009,7 +1010,7 @@ def jsanitize(
10091010
except AttributeError:
10101011
pass
10111012

1012-
if not strict:
1013+
if strict is False:
10131014
return str(obj)
10141015

10151016
if isinstance(obj, str):
@@ -1024,13 +1025,18 @@ def jsanitize(
10241025
recursive_msonable=recursive_msonable,
10251026
)
10261027

1027-
return jsanitize(
1028-
obj.as_dict(),
1029-
strict=strict,
1030-
allow_bson=allow_bson,
1031-
enum_values=enum_values,
1032-
recursive_msonable=recursive_msonable,
1033-
)
1028+
try:
1029+
return jsanitize(
1030+
obj.as_dict(),
1031+
strict=strict,
1032+
allow_bson=allow_bson,
1033+
enum_values=enum_values,
1034+
recursive_msonable=recursive_msonable,
1035+
)
1036+
except Exception as exc_:
1037+
if strict == "skip":
1038+
return obj
1039+
raise exc_
10341040

10351041

10361042
def _serialize_callable(o):

tests/test_collections.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ def test_update_allowed(self):
6262
dct.update({"a": 3})
6363
assert dct["a"] == 3
6464

65+
# Test Iterator handling
66+
dct.update(zip(["c", "d"], [11, 12]))
67+
assert dct["c"] == 11
68+
6569
dct.setdefault("a", 4) # existing key
6670
assert dct["a"] == 3
6771

@@ -122,6 +126,11 @@ def test_frozen_like(self):
122126
assert not dct._allow_add
123127
assert not dct._allow_update
124128

129+
def test_iterator_handling(self):
130+
"""Make sure iterators are handling correctly."""
131+
c_dict = ControlledDict(zip(["c", "d"], [11, 12]))
132+
assert c_dict["c"] == 11
133+
125134

126135
def test_frozendict():
127136
dct = frozendict({"hello": "world"})
@@ -157,7 +166,11 @@ def test_namespace_dict():
157166
dct["hello"] = "world"
158167
assert dct["key"] == "val"
159168

160-
# Test update (not allowed)
169+
# Test use `update` to add new values
170+
dct.update({"new_key": "new_value"})
171+
assert dct["new_key"] == "new_value"
172+
173+
# Test add (not allowed)
161174
with pytest.raises(TypeError, match="update is disabled"):
162175
dct["key"] = "val"
163176

tests/test_io.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ def test_lzw_files(self):
426426

427427
# Cannot decompress a real LZW file
428428
with (
429+
pytest.warns(FutureWarning, match="compress LZW-compressed files"),
429430
pytest.raises(gzip.BadGzipFile, match="Not a gzipped file"),
430431
zopen(f"{TEST_DIR}/real_lzw_file.txt.Z", "rt", encoding="utf-8") as f,
431432
):

tests/test_json.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,50 @@ def test_jsanitize(self):
875875
clean = jsanitize(d, strict=True)
876876
assert "@class" in clean["c"]
877877

878+
def test_unserializable_composite(self):
879+
class Unserializable:
880+
def __init__(self, a):
881+
self._a = a
882+
883+
def __str__(self):
884+
return "Unserializable"
885+
886+
class Composite(MSONable):
887+
def __init__(self, name, unserializable, msonable):
888+
self.name = name
889+
self.unserializable = unserializable
890+
self.msonable = msonable
891+
892+
composite_dictionary = {
893+
"name": "test",
894+
"unserializable": Unserializable(1),
895+
"msonable": GoodMSONClass(1, 2, 3),
896+
}
897+
898+
with pytest.raises(AttributeError):
899+
jsanitize(composite_dictionary, strict=True)
900+
901+
composite_obj = Composite.from_dict(composite_dictionary)
902+
903+
with pytest.raises(AttributeError):
904+
jsanitize(composite_obj, strict=True)
905+
906+
# Test that skip mode preserves unserializable objects
907+
skipped_dict = jsanitize(composite_obj, strict="skip", recursive_msonable=True)
908+
assert skipped_dict["name"] == "test", "String values should remain unchanged"
909+
assert (
910+
skipped_dict["unserializable"]._a == 1
911+
), "Unserializable object should be preserved in skip mode"
912+
assert (
913+
skipped_dict["msonable"]["a"] == 1
914+
), "MSONable object should be properly serialized"
915+
916+
# Test non-strict mode converts unserializable to string
917+
dict_with_str = jsanitize(composite_obj, strict=False, recursive_msonable=True)
918+
assert isinstance(
919+
dict_with_str["unserializable"], str
920+
), "Unserializable object should be converted to string in non-strict mode"
921+
878922
@pytest.mark.skipif(pd is None, reason="pandas not present")
879923
def test_jsanitize_pandas(self):
880924
s = pd.Series({"a": [1, 2, 3], "b": [4, 5, 6]})

0 commit comments

Comments
 (0)