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

Disallow null values in input YAML #387

Merged
merged 1 commit into from
Nov 30, 2021
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
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