Skip to content

Commit

Permalink
Disallow null values in input dict
Browse files Browse the repository at this point in the history
  • Loading branch information
apragsdale committed Nov 30, 2021
1 parent fb694c3 commit 7d39cff
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 0 deletions.
28 changes: 28 additions & 0 deletions demes/load_dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,31 @@ def _unstringify_infinities(data: MutableMapping[str, Any]) -> None:
data["defaults"][default]["start_time"] = float(start_time)


def _no_null_values(data: MutableMapping[str, Any]) -> None:
"""
Checks for any null values in the input data.
"""

def check_if_None(key, val):
if val is None:
raise ValueError(f"{key} must have a non-null value")

def assert_no_nulls(d):
for k, v in d.items():
if isinstance(v, dict):
assert_no_nulls(v)
elif isinstance(v, list):
for _ in v:
if isinstance(_, dict):
assert_no_nulls(_)
else:
check_if_None(k, v)
else:
check_if_None(k, v)

assert_no_nulls(data)


def loads_asdict(string, *, format="yaml") -> MutableMapping[str, Any]:
"""
Load a YAML or JSON string into a dictionary of nested objects.
Expand Down Expand Up @@ -149,6 +174,9 @@ def load_asdict(filename, *, format="yaml") -> MutableMapping[str, Any]:
data = _load_yaml_asdict(f)
else:
raise ValueError(f"unknown format: {format}")
# We forbid null values in the input data.
# See https://github.com/popsim-consortium/demes-spec/issues/76
_no_null_values(data)
# The string "Infinity" should only be present in JSON files.
# But YAML is a superset of JSON, so we want the YAML loader to also
# load JSON files without problem.
Expand Down
105 changes: 105 additions & 0 deletions tests/test_load_dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,111 @@ def test_json_infinities_get_stringified(self):
g2 = demes.loads(json_str, format="json")
g2.assert_close(g1)

def test_load_with_null_deme_start_time(self):
model = """
time_units: generations
demes:
- name: a
start_time: null
epochs:
- {end_time: 0, start_size: 1}
"""
with pytest.raises(ValueError, match="must have a non-null value"):
demes.loads(model)

def test_load_with_null_deme_name(self):
model = """
time_units: generations
demes:
- name: null
epochs:
- {end_time: 0, start_size: 1}
"""
with pytest.raises(ValueError, match="must have a non-null value"):
demes.loads(model)

def test_load_with_null_time_unit(self):
model = """
time_units: null
demes:
- name: a
epochs:
- {end_time: 0, start_size: 1}
"""
with pytest.raises(ValueError, match="must have a non-null value"):
demes.loads(model)

def test_load_with_null_epochs(self):
model = """
time_units: generations
demes:
- name: a
epochs:
"""
with pytest.raises(ValueError, match="must have a non-null value"):
demes.loads(model)

def test_load_with_null_epoch_end_time(self):
model = """
time_units: generations
demes:
- name: a
epochs:
- {end_time: null, start_size: 1}
"""
with pytest.raises(ValueError, match="must have a non-null value"):
demes.loads(model)

def test_load_with_null_migrations(self):
model = """
time_units: generations
demes:
- name: a
epochs:
- {start_size: 1}
- name: b
epochs:
- {start_size: 1}
migrations:
"""
with pytest.raises(ValueError, match="must have a non-null value"):
demes.loads(model)

def test_load_with_null_migration_demes(self):
model = """
time_units: generations
demes:
- name: a
epochs:
- {start_size: 1}
- name: b
epochs:
- {start_size: 1}
migrations:
- demes:
rate: 0.01
"""
with pytest.raises(ValueError, match="must have a non-null value"):
demes.loads(model)
model = """
time_units: generations
defaults:
migration:
demes: [a, b]
demes:
- name: a
epochs:
- {start_size: 1}
- name: b
epochs:
- {start_size: 1}
migrations:
- demes:
rate: 0.01
"""
with pytest.raises(ValueError, match="must have a non-null value"):
demes.loads(model)


class TestMultiDocument:
@pytest.mark.parametrize("yaml_file", tests.example_files())
Expand Down

0 comments on commit 7d39cff

Please sign in to comment.