Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Fixing format issues
Signed-off-by: Nick Quinn <nicholas_quinn@apple.com>
  • Loading branch information
nickquinn408 committed Dec 13, 2025
commit afe646061b46231fde7ce5f41283fda2c3b0aa27
23 changes: 15 additions & 8 deletions sdk/python/feast/type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@
FloatList,
Int32List,
Int64List,
StringList,
Map,
MapList,
StringList,
)
from feast.protos.feast.types.Value_pb2 import Value as ProtoValue
from feast.value_type import ListType, ValueType
Expand Down Expand Up @@ -75,13 +75,13 @@ def feast_value_type_to_python_type(field_value_proto: ProtoValue) -> Any:
if val_attr is None:
return None
val = getattr(field_value_proto, val_attr)

# Handle Map and MapList types FIRST (before generic list processing)
if val_attr == "map_val":
return _handle_map_value(val)
elif val_attr == "map_list_val":
return _handle_map_list_value(val)

# If it's a _LIST type extract the list.
if hasattr(val, "val"):
val = list(val.val)
Expand All @@ -105,6 +105,7 @@ def feast_value_type_to_python_type(field_value_proto: ProtoValue) -> Any:

return val


def _handle_map_value(map_message) -> Dict[str, Any]:
"""Handle Map proto message containing map<string, Value> val."""
result = {}
Expand All @@ -127,6 +128,7 @@ def _handle_map_list_value(map_list_message) -> List[Dict[str, Any]]:

return result


def feast_value_type_to_pandas_type(value_type: ValueType) -> Any:
value_type_to_pandas_type: Dict[ValueType, str] = {
ValueType.FLOAT: "float",
Expand Down Expand Up @@ -414,19 +416,23 @@ def _python_value_to_proto_value(
# Handle Map and MapList types first
if feast_value_type == ValueType.MAP:
return [
ProtoValue(map_val=_python_dict_to_map_proto(value)) if value is not None else ProtoValue()
ProtoValue(map_val=_python_dict_to_map_proto(value))
if value is not None
else ProtoValue()
for value in values
]

if feast_value_type == ValueType.MAP_LIST:
return [
ProtoValue(map_list_val=_python_list_to_map_list_proto(value)) if value is not None else ProtoValue()
ProtoValue(map_list_val=_python_list_to_map_list_proto(value))
if value is not None
else ProtoValue()
for value in values
]

# ToDo: make a better sample for type checks (more than one element)
sample = next(filter(_non_empty_value, values), None) # first not empty value

# Detect list type and handle separately
if "list" in feast_value_type.name.lower():
# Feature can be list but None is still valid
Expand Down Expand Up @@ -570,7 +576,9 @@ def _python_dict_to_map_proto(python_dict: Dict[str, Any]) -> Map:
for key, value in python_dict.items():
# Handle None values explicitly
if value is None:
map_proto.val[key].CopyFrom(ProtoValue()) # Empty ProtoValue represents None
map_proto.val[key].CopyFrom(
ProtoValue()
) # Empty ProtoValue represents None
continue

if isinstance(value, dict):
Expand Down Expand Up @@ -1270,4 +1278,3 @@ def convert_array_item(item) -> Union[np.ndarray, Any]:
return item

return series.apply(convert_array_item)

1 change: 0 additions & 1 deletion sdk/python/feast/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,3 @@ def from_feast_type(
]

raise ValueError(f"Could not convert feast type {feast_type} to ValueType.")

2 changes: 1 addition & 1 deletion sdk/python/feast/value_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class ValueType(enum.Enum):
PDF_BYTES = 22
IMAGE_BYTES = 23


ListType = Union[
Type[BoolList],
Type[BytesList],
Expand All @@ -62,4 +63,3 @@ class ValueType(enum.Enum):
Type[Int64List],
Type[StringList],
]

75 changes: 29 additions & 46 deletions sdk/python/tests/unit/test_type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
import pandas as pd
import pytest

from feast.protos.feast.types.Value_pb2 import Map, MapList
from feast.type_map import (
feast_value_type_to_python_type,
python_values_to_proto_values,
python_type_to_feast_value_type,
_python_dict_to_map_proto,
_python_list_to_map_list_proto,
feast_value_type_to_python_type,
python_type_to_feast_value_type,
python_values_to_proto_values,
)
from feast.value_type import ValueType
from feast.protos.feast.types.Value_pb2 import Value as ProtoValue, Map, MapList


def test_null_unix_timestamp():
Expand Down Expand Up @@ -119,13 +119,10 @@ def test_nested_map_conversion(self):
"""Test nested MAP type conversion."""
test_dict = {
"level1": {
"level2": {
"key": "deep_value",
"number": 42
},
"simple": "value"
"level2": {"key": "deep_value", "number": 42},
"simple": "value",
},
"top_level": "top_value"
"top_level": "top_value",
}

protos = python_values_to_proto_values([test_dict], ValueType.MAP)
Expand All @@ -145,7 +142,7 @@ def test_map_with_different_value_types(self):
"float_val": 3.14,
"bool_val": True,
"list_val": [1, 2, 3],
"string_list_val": ["a", "b", "c"]
"string_list_val": ["a", "b", "c"],
}

protos = python_values_to_proto_values([test_dict], ValueType.MAP)
Expand Down Expand Up @@ -191,7 +188,7 @@ def test_map_list_conversion(self):
test_list = [
{"name": "John", "age": 30},
{"name": "Jane", "age": 25},
{"name": "Bob", "score": 85.5}
{"name": "Bob", "score": 85.5},
]

protos = python_values_to_proto_values([test_list], ValueType.MAP_LIST)
Expand All @@ -209,14 +206,8 @@ def test_map_list_conversion(self):
def test_map_list_with_nested_maps(self):
"""Test MAP_LIST with nested maps."""
test_list = [
{
"user": {"name": "John", "details": {"city": "NYC"}},
"score": 100
},
{
"user": {"name": "Jane", "details": {"city": "SF"}},
"score": 95
}
{"user": {"name": "John", "details": {"city": "NYC"}}, "score": 100},
{"user": {"name": "Jane", "details": {"city": "SF"}}, "score": 95},
]

protos = python_values_to_proto_values([test_list], ValueType.MAP_LIST)
Expand All @@ -232,7 +223,7 @@ def test_map_list_with_lists_in_maps(self):
"""Test MAP_LIST where maps contain lists."""
test_list = [
{"name": "John", "hobbies": ["reading", "swimming"]},
{"name": "Jane", "scores": [95, 87, 92]}
{"name": "Jane", "scores": [95, 87, 92]},
]

protos = python_values_to_proto_values([test_list], ValueType.MAP_LIST)
Expand Down Expand Up @@ -293,7 +284,7 @@ def test_multiple_map_values(self):
test_dicts = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "city": "NYC"}
{"name": "Charlie", "city": "NYC"},
]

protos = python_values_to_proto_values(test_dicts, ValueType.MAP)
Expand All @@ -306,11 +297,7 @@ def test_multiple_map_values(self):

def test_multiple_map_list_values(self):
"""Test conversion of multiple MAP_LIST values."""
test_lists = [
[{"id": 1}, {"id": 2}],
[{"id": 3}, {"id": 4}],
[]
]
test_lists = [[{"id": 1}, {"id": 2}], [{"id": 3}, {"id": 4}], []]

protos = python_values_to_proto_values(test_lists, ValueType.MAP_LIST)
converted_values = [feast_value_type_to_python_type(proto) for proto in protos]
Expand All @@ -324,10 +311,7 @@ def test_map_with_map_list_value(self):
"""Test MAP containing MAP_LIST as a value."""
test_dict = {
"metadata": {"version": "1.0"},
"items": [
{"name": "item1", "count": 5},
{"name": "item2", "count": 3}
]
"items": [{"name": "item1", "count": 5}, {"name": "item2", "count": 3}],
}

protos = python_values_to_proto_values([test_dict], ValueType.MAP)
Expand All @@ -338,11 +322,14 @@ def test_map_with_map_list_value(self):
assert converted["items"][0]["name"] == "item1"
assert converted["items"][1]["count"] == 3

@pytest.mark.parametrize("invalid_value", [
[{"key": "value"}, "not_a_dict", {"another": "dict"}],
["string1", "string2"],
[1, 2, 3]
])
@pytest.mark.parametrize(
"invalid_value",
[
[{"key": "value"}, "not_a_dict", {"another": "dict"}],
["string1", "string2"],
[1, 2, 3],
],
)
def test_map_list_with_invalid_items(self, invalid_value):
"""Test that MAP_LIST with non-dict items raises appropriate errors."""
with pytest.raises((ValueError, TypeError)):
Expand Down Expand Up @@ -371,14 +358,8 @@ def test_roundtrip_conversion_consistency(self):
"integer": 42,
"float": 3.14159,
"boolean": True,
"nested": {
"inner_string": "world",
"inner_list": [1, 2, 3]
},
"list_of_maps": [
{"item": "first"},
{"item": "second"}
]
"nested": {"inner_string": "world", "inner_list": [1, 2, 3]},
"list_of_maps": [{"item": "first"}, {"item": "second"}],
}

# Convert to proto and back
Expand All @@ -390,9 +371,11 @@ def test_roundtrip_conversion_consistency(self):
assert converted["integer"] == original_map["integer"]
assert converted["float"] == original_map["float"]
assert converted["boolean"] == original_map["boolean"]
assert converted["nested"]["inner_string"] == original_map["nested"]["inner_string"]
assert (
converted["nested"]["inner_string"]
== original_map["nested"]["inner_string"]
)
assert converted["nested"]["inner_list"] == original_map["nested"]["inner_list"]
assert len(converted["list_of_maps"]) == len(original_map["list_of_maps"])
assert converted["list_of_maps"][0]["item"] == "first"
assert converted["list_of_maps"][1]["item"] == "second"

Loading