Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove non-string key support #719

Merged
merged 3 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Changed
+++++++

- Functions raising ``AssertionError`` now raise ``RuntimeError`` (#612).
- State points and documents require keys to be of type ``str`` (#719).
- The keyword ``_id`` of the Job constructor has been renamed to ``id_`` (#681).

Removed
Expand Down
73 changes: 3 additions & 70 deletions signac/synced_collections/backends/collection_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import json
import os
import uuid
import warnings
from collections.abc import Mapping, Sequence
from typing import Callable, FrozenSet
from typing import Sequence as Sequence_t
Expand All @@ -26,7 +25,7 @@
_numpy_cache_blocklist,
)
from ..utils import AbstractTypeResolver, SyncedCollectionJSONEncoder
from ..validators import json_format_validator, no_dot_in_key
from ..validators import json_format_validator, no_dot_in_key, require_string_key

"""
There are many classes defined in this file. Most of the definitions are
Expand All @@ -37,54 +36,6 @@
"""


# TODO: This method should be removed in signac 2.0.
def _str_key(key):
VALID_KEY_TYPES = (str, int, bool, type(None))

if not isinstance(key, VALID_KEY_TYPES):
raise KeyTypeError(
f"Mapping keys must be str, int, bool or None, not {type(key).__name__}"
)
elif not isinstance(key, str):
warnings.warn(
f"Use of {type(key).__name__} as key is deprecated "
"and will be removed in version 2.0",
FutureWarning,
)
key = str(key)
return key


# TODO: This method should be removed in signac 2.0.
def _convert_key_to_str(data):
"""Recursively convert non-string keys to strings in dicts.

This method supports :class:`collections.abc.Sequence` or
:class:`collections.abc.Mapping` types as inputs, and recursively
searches for any entries in :class:`collections.abc.Mapping` types where
the key is not a string. This functionality is added for backwards
compatibility with legacy behavior in signac, which allowed integer keys
for dicts. These inputs were silently converted to string keys and stored
since JSON does not support integer keys. This behavior is deprecated and
will become an error in signac 2.0.

Note for developers: this method is designed for use as a validator in the
synced collections framework, but due to the backwards compatibility requirement
it violates the general behavior of validators by modifying the data in place.
This behavior can be removed in signac 2.0 once non-str keys become an error.
"""
if isinstance(data, dict):
# Explicitly call `list(keys)` to get a fixed list of keys to avoid
# running into issues with iterating over a DictKeys view while
# modifying the dict at the same time.
for key in list(data):
_convert_key_to_str(data[key])
data[_str_key(key)] = data.pop(key)
elif isinstance(data, list):
for i, value in enumerate(data):
_convert_key_to_str(value)


_json_attr_dict_validator_type_resolver = AbstractTypeResolver(
{
# We identify >0d numpy arrays as sequences for validation purposes.
Expand All @@ -107,8 +58,6 @@ def json_attr_dict_validator(data):
This validator combines the following logic:
- JSON format validation
- Ensuring no dots are present in string keys
- Converting non-str keys to strings. This is a backwards compatibility
layer that will be removed in signac 2.0.

Parameters
----------
Expand Down Expand Up @@ -141,17 +90,9 @@ def json_attr_dict_validator(data):
raise InvalidKeyError(
f"Mapping keys may not contain dots ('.'): {key}."
)
elif isinstance(key, (int, bool, type(None))):
# TODO: Remove this branch in signac 2.0.
warnings.warn(
f"Use of {type(key).__name__} as key is deprecated "
"and will be removed in version 2.0.",
FutureWarning,
)
data[str(key)] = data.pop(key)
else:
raise KeyTypeError(
f"Mapping keys must be str, int, bool or None, not {type(key).__name__}."
f"Mapping keys must be str, not {type(key).__name__}."
)
elif switch_type == "SEQUENCE":
for value in data:
Expand Down Expand Up @@ -209,15 +150,7 @@ class JSONCollection(SyncedCollection):

_backend = __name__ # type: ignore
_supports_threading = True

# The order in which these validators are added is important, because
# validators are called in sequence and _convert_key_to_str will ensure that
# valid non-str keys are converted to strings before json_format_validator is
# called. This ordering is an implementation detail that we should not rely on
# in the future, however, the _convert_key_to_str validator will be removed in
# signac 2.0 so this is OK (that validator is modifying the data in place,
# which is unsupported behavior that will be removed in signac 2.0 as well).
_validators: Sequence_t[Callable] = (_convert_key_to_str, json_format_validator)
_validators: Sequence_t[Callable] = (require_string_key, json_format_validator)

def __init__(self, filename=None, write_concern=False, *args, **kwargs):
# The `_filename` attribute _must_ be defined prior to calling the
Expand Down
25 changes: 4 additions & 21 deletions signac/synced_collections/data_types/synced_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,9 @@ def _update(self, data=None, _validate=False):
)

def __setitem__(self, key, value):
# TODO: Remove in signac 2.0, currently we're constructing a dict to
# allow in-place modification by _convert_key_to_str, but validators
# should not have side effects once that backwards compatibility layer
# is removed, so we can validate a temporary dict {key: value} and
# directly set using those rather than looping over data.

data = {key: value}
self._validate(data)
self._validate({key: value})
with self._load_and_save, self._suspend_sync:
for key, value in data.items():
self._data[key] = self._from_base(value, parent=self)
self._data[key] = self._from_base(value, parent=self)

def reset(self, data):
"""Update the instance with new data.
Expand Down Expand Up @@ -257,16 +249,7 @@ def setdefault(self, key, default=None): # noqa: D102
if key in self._data:
ret = self._data[key]
else:
ret = self._from_base(default, parent=self)
# TODO: Remove in signac 2.0, currently we're constructing a dict
# to allow in-place modification by _convert_key_to_str, but
# validators should not have side effects once that backwards
# compatibility layer is removed, so we can validate a temporary
# dict {key: value} and directly set using those rather than
# looping over data.
data = {key: ret}
self._validate(data)
self._validate({key: default})
with self._suspend_sync:
for key, value in data.items():
self._data[key] = value
ret = self._data[key] = self._from_base(default, parent=self)
return ret
7 changes: 2 additions & 5 deletions signac/synced_collections/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ def no_dot_in_key(data):
If the key contains invalid characters or is otherwise malformed.

"""
VALID_KEY_TYPES = (str, int, bool, type(None))

switch_type = _no_dot_in_key_type_resolver.get_type(data)

if switch_type == "MAPPING":
Expand All @@ -56,10 +54,9 @@ def no_dot_in_key(data):
raise InvalidKeyError(
f"Mapping keys may not contain dots ('.'): {key}"
)
# TODO: Make it an error to have a non-str key here in signac 2.0.
elif not isinstance(key, VALID_KEY_TYPES):
else:
raise KeyTypeError(
f"Mapping keys must be str, int, bool or None, not {type(key).__name__}"
f"Mapping keys must be str, not {type(key).__name__}"
)
no_dot_in_key(value)
elif switch_type == "SEQUENCE":
Expand Down
26 changes: 8 additions & 18 deletions tests/test_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,16 +432,11 @@ def test_interface_multiple_changes(self):
assert job.sp == job2.sp
assert job.id == job2.id

@pytest.mark.filterwarnings("ignore:Use of int as key is deprecated")
@pytest.mark.filterwarnings("ignore:Use of NoneType as key is deprecated")
@pytest.mark.filterwarnings("ignore:Use of bool as key is deprecated")
def test_valid_sp_key_types(self):
job = self.open_job(dict(invalid_key=True)).init()

class A:
pass
job = self.open_job(dict(valid_key=True)).init()

for key in ("0", 0, True, False, None):
# Only strings are permitted as keys
for key in ("0", "1"):
job.sp[key] = "test"
assert str(key) in job.sp

Expand All @@ -454,7 +449,7 @@ class A:

job = self.open_job(dict(invalid_key=True)).init()

for key in (0.0, A(), (1, 2, 3)):
for key in (1, True, False, None, 0.0, A(), (1, 2, 3)):
with pytest.raises(KeyTypeError):
job.sp[key] = "test"
with pytest.raises(KeyTypeError):
Expand All @@ -465,16 +460,11 @@ class A:
with pytest.raises(TypeError):
job.sp = {key: "test"}

@pytest.mark.filterwarnings("ignore:Use of int as key is deprecated")
@pytest.mark.filterwarnings("ignore:Use of NoneType as key is deprecated")
@pytest.mark.filterwarnings("ignore:Use of bool as key is deprecated")
def test_valid_doc_key_types(self):
job = self.open_job(dict(invalid_key=True)).init()

class A:
pass
job = self.open_job(dict(valid_key=True)).init()

for key in ("0", 0, True, False, None):
# Only strings are permitted as keys
for key in ("0", "1"):
job.doc[key] = "test"
assert str(key) in job.doc

Expand All @@ -484,7 +474,7 @@ def test_invalid_doc_key_types(self):
class A:
pass

for key in (0.0, A(), (1, 2, 3)):
for key in (1, True, False, None, 0.0, A(), (1, 2, 3)):
with pytest.raises(KeyTypeError):
job.doc[key] = "test"
with pytest.raises(KeyTypeError):
Expand Down
8 changes: 3 additions & 5 deletions tests/test_jsondict.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,16 +219,14 @@ def test_keys_with_dots(self):
with pytest.raises(InvalidKeyError):
jsd["a.b"] = None

@pytest.mark.filterwarnings("ignore:Use of int as key is deprecated")
@pytest.mark.filterwarnings("ignore:Use of NoneType as key is deprecated")
@pytest.mark.filterwarnings("ignore:Use of bool as key is deprecated")
def test_keys_valid_type(self):
jsd = self.get_json_dict()

class MyStr(str):
pass

for key in ("key", MyStr("key"), 0, None, True):
# Only strings are permitted as keys
for key in ("key", MyStr("key")):
d = jsd[key] = self.get_testdata()
assert str(key) in jsd
assert jsd[str(key)] == d
Expand All @@ -239,7 +237,7 @@ def test_keys_invalid_type(self):
class A:
pass

for key in (0.0, A(), (1, 2, 3)):
for key in (1, True, False, None, 0.0, A(), (1, 2, 3)):
with pytest.raises(KeyTypeError):
jsd[key] = self.get_testdata()
for key in ([], {}):
Expand Down
10 changes: 0 additions & 10 deletions tests/test_synced_collections/test_json_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,6 @@ class TestJSONDict(JSONCollectionTest, SyncedDictTest):

_collection_type = JSONDict

# The following test tests the support for non-str keys
# for JSON backend which will be removed in version 2.0.
# See issue: https://github.com/glotzerlab/signac/issues/316.
def test_keys_non_str_valid_type(self, synced_collection, testdata):
for key in (0, None, True):
with pytest.warns(FutureWarning, match="Use of.+as key is deprecated"):
synced_collection[key] = testdata
assert str(key) in synced_collection
assert synced_collection[str(key)] == testdata


class TestJSONList(JSONCollectionTest, SyncedListTest):

Expand Down
2 changes: 1 addition & 1 deletion tests/test_synced_collections/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class TestNoDotInKey:
def test_valid_data(self, testdata):
test_dict = {}
# valid data
for key in ("valid_str", 1, False, None):
for key in ("valid_str", "another_valid_str"):
test_dict[key] = testdata
no_dot_in_key(test_dict)
assert key in test_dict
Expand Down