Skip to content

Commit

Permalink
Added root validator that warns on missing expected fields, plus asso…
Browse files Browse the repository at this point in the history
…ciated tests
  • Loading branch information
ml-evs committed Nov 4, 2020
1 parent 111bffa commit 1ec531d
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 2 deletions.
28 changes: 28 additions & 0 deletions optimade/models/structures.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# pylint: disable=no-self-argument,line-too-long,no-name-in-module
import re
import warnings
from enum import IntEnum, Enum
from sys import float_info
from typing import List, Optional, Union
Expand All @@ -16,6 +17,7 @@
ANONYMOUS_ELEMENTS,
CHEMICAL_FORMULA_REGEXP,
)
from optimade.server.warnings import MissingExpectedField

EXTENDED_CHEMICAL_SYMBOLS = CHEMICAL_SYMBOLS + EXTRA_SYMBOLS

Expand Down Expand Up @@ -242,6 +244,14 @@ def check_self_consistency(cls, v, values):
return v


CORRELATED_STRUCTURE_FIELDS = (
{"dimension_types", "nperiodic_dimensions"},
{"cartesian_site_positions", "species_at_sites"},
{"nsites", "cartesian_site_positions"},
{"species_at_sites", "species"},
)


class StructureResourceAttributes(EntryResourceAttributes):
"""This class contains the Field for the attributes used to represent a structure, e.g. unit cell, atoms, positions."""

Expand Down Expand Up @@ -790,6 +800,24 @@ def schema_extra(schema, model):
for prop in nullable_props:
schema["properties"][prop]["nullable"] = True

@root_validator(pre=True)
def warn_on_missing_correlated_fields(cls, values):
"""Emit warnings if a field takes a null value when a value
was expected based on the value/nullity of another field.
"""
accumulated_warnings = []
for field_set in CORRELATED_STRUCTURE_FIELDS:
missing_fields = {f for f in field_set if values.get(f) is None}
if missing_fields and len(missing_fields) != len(field_set):
accumulated_warnings += [
f"Structure with values {values} is missing fields {missing_fields} which are required if {field_set - missing_fields} are present."
]

for warn in accumulated_warnings:
warnings.warn(warn, MissingExpectedField)

return values

@validator("chemical_formula_reduced", "chemical_formula_hill")
def check_ordered_formula(cls, v, field):
if v is None:
Expand Down
5 changes: 5 additions & 0 deletions optimade/server/warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@ class TooManyValues(OptimadeWarning):

class QueryParamNotUsed(OptimadeWarning):
"""A query parameter is not used in this request."""


class MissingExpectedField(OptimadeWarning):
"""A field was provided with a null value when a related field was provided
with a value."""
24 changes: 22 additions & 2 deletions tests/models/test_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

from pydantic import ValidationError

from optimade.models.structures import StructureResource
from optimade.models.structures import StructureResource, CORRELATED_STRUCTURE_FIELDS
from optimade.server.warnings import MissingExpectedField


MAPPER = "StructureMapper"
Expand All @@ -20,6 +21,7 @@ def test_good_structures(mapper):
StructureResource(**mapper(MAPPER).map_back(structure))


@pytest.mark.filterwarnings("ignore", category=MissingExpectedField)
def test_good_structure_with_missing_data(mapper, good_structure):
"""Check deserialization of well-formed structure used
as example data with all combinations of null values
Expand Down Expand Up @@ -168,7 +170,7 @@ def test_bad_structures(bad_structures, mapper):


@pytest.mark.parametrize("deformity", deformities)
def test_structure_deformities(good_structure, deformity):
def test_structure_fatal_deformities(good_structure, deformity):
"""Make specific checks upon performing single invalidating deformations
of the data of a good structure.
Expand All @@ -182,3 +184,21 @@ def test_structure_deformities(good_structure, deformity):
good_structure["attributes"].update(deformity)
with pytest.raises(ValidationError, match=fr".*{re.escape(message)}.*"):
StructureResource(**good_structure)


minor_deformities = (
{f: None} for f in set(f for _ in CORRELATED_STRUCTURE_FIELDS for f in _)
)


@pytest.mark.parametrize("deformity", minor_deformities)
def test_structure_minor_deformities(good_structure, deformity):
"""Make specific checks upon performing single minor invalidations
of the data of a good structure that should emit warnings.
"""
if deformity is None:
StructureResource(**good_structure)
else:
good_structure["attributes"].update(deformity)
with pytest.warns(MissingExpectedField):
StructureResource(**good_structure)

0 comments on commit 1ec531d

Please sign in to comment.