Skip to content

Commit

Permalink
Merge branch 'master' of github.com:materialsvirtuallab/monty
Browse files Browse the repository at this point in the history
  • Loading branch information
shyuep committed Jan 3, 2025
2 parents 28be785 + b5dcadf commit 52258f1
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 18 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ jobs:
fail-fast: false
max-parallel: 20
matrix:
os: [ubuntu-latest, macos-14] #, windows-latest]
python-version: ["3.10", "3.12"]
os: [ubuntu-latest, macos-14, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]

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

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ maintainers = [
]
description = "Monty is the missing complement to Python."
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.10,<3.14"
classifiers = [
"Programming Language :: Python :: 3",
"Development Status :: 4 - Beta",
Expand Down
6 changes: 4 additions & 2 deletions src/monty/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ def __setitem__(self, key, value) -> None:

def update(self, *args, **kwargs) -> None:
"""Forbid adding or updating keys based on _allow_add and _allow_update."""
for key in dict(*args, **kwargs):

updates = dict(*args, **kwargs)
for key in updates:
if key not in self.data and not self._allow_add:
raise TypeError(
f"Cannot add new key {key!r} using update, because add is disabled."
Expand All @@ -99,7 +101,7 @@ def update(self, *args, **kwargs) -> None:
f"Cannot update key {key!r} using update, because update is disabled."
)

super().update(*args, **kwargs)
super().update(updates)

def setdefault(self, key, default=None) -> Any:
"""Forbid adding or updating keys based on _allow_add and _allow_update.
Expand Down
2 changes: 1 addition & 1 deletion src/monty/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def zopen(
# TODO: remove default value of `mode` to force user to give one after deadline
if mode is None:
warnings.warn(
"We strongly discourage using a default `mode`, it would be"
"We strongly discourage using a default `mode`, it would be "
f"set to `r` now but would not be allowed after {_deadline}",
FutureWarning,
stacklevel=2,
Expand Down
24 changes: 15 additions & 9 deletions src/monty/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,8 @@ def jsanitize(
jsanitize will try to get the as_dict() attribute of the object. If
no such attribute is found, an attribute error will be thrown. If
strict is False, jsanitize will simply call str(object) to convert
the object to a string representation.
the object to a string representation. If "skip" is provided,
jsanitize will skip and return the original object without modification.
allow_bson (bool): This parameter sets the behavior when jsanitize
encounters a bson supported type such as objectid and datetime. If
True, such bson types will be ignored, allowing for proper
Expand Down Expand Up @@ -1009,7 +1010,7 @@ def jsanitize(
except AttributeError:
pass

if not strict:
if strict is False:
return str(obj)

if isinstance(obj, str):
Expand All @@ -1024,13 +1025,18 @@ def jsanitize(
recursive_msonable=recursive_msonable,
)

return jsanitize(
obj.as_dict(),
strict=strict,
allow_bson=allow_bson,
enum_values=enum_values,
recursive_msonable=recursive_msonable,
)
try:
return jsanitize(
obj.as_dict(),
strict=strict,
allow_bson=allow_bson,
enum_values=enum_values,
recursive_msonable=recursive_msonable,
)
except Exception as exc_:
if strict == "skip":
return obj
raise exc_


def _serialize_callable(o):
Expand Down
15 changes: 14 additions & 1 deletion tests/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ def test_update_allowed(self):
dct.update({"a": 3})
assert dct["a"] == 3

# Test Iterator handling
dct.update(zip(["c", "d"], [11, 12]))
assert dct["c"] == 11

dct.setdefault("a", 4) # existing key
assert dct["a"] == 3

Expand Down Expand Up @@ -122,6 +126,11 @@ def test_frozen_like(self):
assert not dct._allow_add
assert not dct._allow_update

def test_iterator_handling(self):
"""Make sure iterators are handling correctly."""
c_dict = ControlledDict(zip(["c", "d"], [11, 12]))
assert c_dict["c"] == 11


def test_frozendict():
dct = frozendict({"hello": "world"})
Expand Down Expand Up @@ -157,7 +166,11 @@ def test_namespace_dict():
dct["hello"] = "world"
assert dct["key"] == "val"

# Test update (not allowed)
# Test use `update` to add new values
dct.update({"new_key": "new_value"})
assert dct["new_key"] == "new_value"

# Test add (not allowed)
with pytest.raises(TypeError, match="update is disabled"):
dct["key"] = "val"

Expand Down
1 change: 1 addition & 0 deletions tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ def test_lzw_files(self):

# Cannot decompress a real LZW file
with (
pytest.warns(FutureWarning, match="compress LZW-compressed files"),
pytest.raises(gzip.BadGzipFile, match="Not a gzipped file"),
zopen(f"{TEST_DIR}/real_lzw_file.txt.Z", "rt", encoding="utf-8") as f,
):
Expand Down
44 changes: 44 additions & 0 deletions tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,50 @@ def test_jsanitize(self):
clean = jsanitize(d, strict=True)
assert "@class" in clean["c"]

def test_unserializable_composite(self):
class Unserializable:
def __init__(self, a):
self._a = a

def __str__(self):
return "Unserializable"

class Composite(MSONable):
def __init__(self, name, unserializable, msonable):
self.name = name
self.unserializable = unserializable
self.msonable = msonable

composite_dictionary = {
"name": "test",
"unserializable": Unserializable(1),
"msonable": GoodMSONClass(1, 2, 3),
}

with pytest.raises(AttributeError):
jsanitize(composite_dictionary, strict=True)

composite_obj = Composite.from_dict(composite_dictionary)

with pytest.raises(AttributeError):
jsanitize(composite_obj, strict=True)

# Test that skip mode preserves unserializable objects
skipped_dict = jsanitize(composite_obj, strict="skip", recursive_msonable=True)
assert skipped_dict["name"] == "test", "String values should remain unchanged"
assert (
skipped_dict["unserializable"]._a == 1
), "Unserializable object should be preserved in skip mode"
assert (
skipped_dict["msonable"]["a"] == 1
), "MSONable object should be properly serialized"

# Test non-strict mode converts unserializable to string
dict_with_str = jsanitize(composite_obj, strict=False, recursive_msonable=True)
assert isinstance(
dict_with_str["unserializable"], str
), "Unserializable object should be converted to string in non-strict mode"

@pytest.mark.skipif(pd is None, reason="pandas not present")
def test_jsanitize_pandas(self):
s = pd.Series({"a": [1, 2, 3], "b": [4, 5, 6]})
Expand Down

0 comments on commit 52258f1

Please sign in to comment.