Skip to content

Commit

Permalink
feat: Metric file accepts missing columns if they have defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
nh13 committed Aug 4, 2023
1 parent 3f56238 commit f1f9635
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 6 deletions.
5 changes: 5 additions & 0 deletions fgpyo/util/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,8 @@ def attribute_is_optional(attribute: attr.Attribute) -> bool:
return types.get_origin_type(attribute.type) is Union and isinstance(
None, types.get_arg_types(attribute.type)
)


def attribute_has_default(attribute: attr.Attribute) -> bool:
"""Returns True if the attribute has a default value, False otherwise"""
return attribute.default != attr.NOTHING or attribute_is_optional(attribute)
13 changes: 7 additions & 6 deletions fgpyo/util/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,17 +169,18 @@ def read(cls, path: Path, ignore_extra_fields: bool = True) -> Iterator[Any]:
missing_from_class = file_fields.difference(class_fields)
missing_from_file = class_fields.difference(file_fields)

# ignore optional class fields that are missing from the file (via header), they'll get
# a default of None
field_name_to_attribute = attr.fields_dict(cls)

# ignore class fields that are missing from the file (via header) if they're optional
# or have a default
if len(missing_from_file) > 0:
field_name_to_attribute = attr.fields_dict(cls)
missing_from_file_optionals = [
fields_with_defaults = [
field
for field in missing_from_file
if inspect.attribute_is_optional(field_name_to_attribute[field])
if inspect.attribute_has_default(field_name_to_attribute[field])
]
# remove optional class fields from the fields
missing_from_file = missing_from_file.difference(missing_from_file_optionals)
missing_from_file = missing_from_file.difference(fields_with_defaults)

# raise an exception if there are non-optional class fields missing from the file
if len(missing_from_file) > 0:
Expand Down
11 changes: 11 additions & 0 deletions fgpyo/util/tests/test_inspect.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fgpyo.util.inspect import attribute_is_optional
from fgpyo.util.inspect import attr_from
from fgpyo.util.inspect import attribute_has_default

import attr
from typing import Optional
Expand Down Expand Up @@ -30,6 +31,16 @@ def test_attr_from() -> None:
def test_attribute_is_optional() -> None:
fields_dict = attr.fields_dict(Name)
assert not attribute_is_optional(fields_dict["required"])
assert not attribute_is_optional(fields_dict["custom_parser"])
assert attribute_is_optional(fields_dict["optional_no_default"])
assert attribute_is_optional(fields_dict["optional_with_default_none"])
assert attribute_is_optional(fields_dict["optional_with_default_some"])


def test_attribute_has_default() -> None:
fields_dict = attr.fields_dict(Name)
assert not attribute_has_default(fields_dict["required"])
assert not attribute_has_default(fields_dict["custom_parser"])
assert attribute_has_default(fields_dict["optional_no_default"])
assert attribute_has_default(fields_dict["optional_with_default_none"])
assert attribute_has_default(fields_dict["optional_with_default_some"])
27 changes: 27 additions & 0 deletions fgpyo/util/tests/test_metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ class PersonMaybeAge(Metric["PersonMaybeAge"]):
age: Optional[int]


@attr.s(auto_attribs=True, frozen=True)
class PersonDefault(Metric["PersonDefault"]):
name: str
age: int = 0


@pytest.mark.parametrize("metric", DUMMY_METRICS)
def test_metric_roundtrip(tmpdir: TmpDir, metric: DummyMetric) -> None:
path: Path = Path(tmpdir) / "metrics.txt"
Expand Down Expand Up @@ -183,6 +189,27 @@ def test_metrics_read_missing_optional_columns(tmpdir: TmpDir) -> None:
list(PersonMaybeAge.read(path=path))


def test_metric_read_missing_column_with_default(tmpdir: TmpDir) -> None:
person = PersonDefault(name="Max")
path = Path(tmpdir) / "metrics.txt"

# The "age" column hs a default, and not in the file, but that's ok
with path.open("w") as writer:
writer.write("name\nMax\n")
assert list(PersonDefault.read(path=path)) == [person]

# All fields specified
with path.open("w") as writer:
writer.write("name\tage\nMax\t42\n")
assert list(PersonDefault.read(path=path)) == [PersonDefault(name="Max", age=42)]

# Just age specified, but not the required name column!
with path.open("w") as writer:
writer.write("age\n42\n")
with pytest.raises(ValueError):
list(PersonDefault.read(path=path))


def test_metric_header() -> None:
assert DummyMetric.header() == [
"int_value",
Expand Down

0 comments on commit f1f9635

Please sign in to comment.