Skip to content

Commit

Permalink
feat: convert between Row and dict (#206)
Browse files Browse the repository at this point in the history
Closes #204.

### Summary of Changes

Add two methods to `Row`:
* `from_dict` to create a `Row` from a `dict[str, Any]`
* `to_dict` to convert a `Row` to a `dict[str, Any]`

---------

Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
  • Loading branch information
lars-reimann and megalinter-bot committed Apr 18, 2023
1 parent 2930a09 commit e98b653
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 39 deletions.
46 changes: 44 additions & 2 deletions src/safeds/data/tabular/containers/_row.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections.abc import Iterable, Iterator
from __future__ import annotations

from hashlib import md5
from typing import Any
from typing import TYPE_CHECKING, Any

import pandas as pd
from IPython.core.display_functions import DisplayHandle, display
Expand All @@ -9,6 +10,9 @@
from safeds.data.tabular.exceptions import UnknownColumnNameError
from safeds.data.tabular.typing import ColumnType, Schema

if TYPE_CHECKING:
from collections.abc import Iterable, Iterator


class Row:
"""
Expand All @@ -22,6 +26,29 @@ class Row:
The schema of the row.
"""

# ------------------------------------------------------------------------------------------------------------------
# Creation
# ------------------------------------------------------------------------------------------------------------------

@staticmethod
def from_dict(data: dict[str, Any]) -> Row:
"""
Create a row from a dictionary that maps column names to column values.
Parameters
----------
data : dict[str, Any]
The data.
Returns
-------
row : Row
The generated row.
"""
row_frame = pd.DataFrame([data.values()], columns=list(data.keys()))
# noinspection PyProtectedMember
return Row(data.values(), Schema._from_dataframe(row_frame))

# ------------------------------------------------------------------------------------------------------------------
# Dunder methods
# ------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -180,6 +207,21 @@ def count(self) -> int:
"""
return len(self._data)

# ------------------------------------------------------------------------------------------------------------------
# Conversion
# ------------------------------------------------------------------------------------------------------------------

def to_dict(self) -> dict[str, Any]:
"""
Return a dictionary that maps column names to column values.
Returns
-------
data : dict[str, Any]
Dictionary representation of the row.
"""
return {column_name: self.get_value(column_name) for column_name in self.get_column_names()}

# ------------------------------------------------------------------------------------------------------------------
# IPython integration
# ------------------------------------------------------------------------------------------------------------------
Expand Down
112 changes: 77 additions & 35 deletions tests/safeds/data/tabular/containers/test_row.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@
from safeds.data.tabular.typing import ColumnType, Integer, Schema, String


class TestFromDict:
@pytest.mark.parametrize(
("data", "expected"),
[
(
{},
Row([]),
),
(
{
"a": 1,
"b": 2,
},
Row([1, 2], schema=Schema({"a": Integer(), "b": Integer()})),
),
],
)
def test_should_create_row_from_dict(self, data: dict[str, Any], expected: Row) -> None:
assert Row.from_dict(data) == expected


class TestInit:
@pytest.mark.parametrize(
("row", "expected"),
Expand Down Expand Up @@ -36,11 +57,11 @@ class TestEq:
@pytest.mark.parametrize(
("row1", "row2", "expected"),
[
(Row([]), Row([]), True),
(Row([0], Schema({"col1": Integer()})), Row([0], Schema({"col1": Integer()})), True),
(Row([0], Schema({"col1": Integer()})), Row([1], Schema({"col1": Integer()})), False),
(Row([0], Schema({"col1": Integer()})), Row([0], Schema({"col2": Integer()})), False),
(Row([0], Schema({"col1": Integer()})), Row(["a"], Schema({"col1": String()})), False),
(Row.from_dict({}), Row.from_dict({}), True),
(Row.from_dict({"col1": 0}), Row.from_dict({"col1": 0}), True),
(Row.from_dict({"col1": 0}), Row.from_dict({"col1": 1}), False),
(Row.from_dict({"col1": 0}), Row.from_dict({"col2": 0}), False),
(Row.from_dict({"col1": 0}), Row.from_dict({"col1": "a"}), False),
],
)
def test_should_return_whether_two_rows_are_equal(self, row1: Row, row2: Row, expected: bool) -> None:
Expand All @@ -49,8 +70,8 @@ def test_should_return_whether_two_rows_are_equal(self, row1: Row, row2: Row, ex
@pytest.mark.parametrize(
("row", "other"),
[
(Row([0], Schema({"col1": Integer()})), None),
(Row([0], Schema({"col1": Integer()})), Table([])),
(Row.from_dict({"col1": 0}), None),
(Row.from_dict({"col1": 0}), Table([])),
],
)
def test_should_return_not_implemented_if_other_is_not_row(self, row: Row, other: Any) -> None:
Expand All @@ -61,8 +82,8 @@ class TestGetitem:
@pytest.mark.parametrize(
("row", "column_name", "expected"),
[
(Row([0], Schema({"col1": Integer()})), "col1", 0),
(Row([0, "a"], Schema({"col1": Integer(), "col2": String()})), "col2", "a"),
(Row.from_dict({"col1": 0}), "col1", 0),
(Row.from_dict({"col1": 0, "col2": "a"}), "col2", "a"),
],
)
def test_should_return_the_value_in_the_column(self, row: Row, column_name: str, expected: Any) -> None:
Expand All @@ -71,8 +92,8 @@ def test_should_return_the_value_in_the_column(self, row: Row, column_name: str,
@pytest.mark.parametrize(
("row", "column_name"),
[
(Row([], Schema({})), "col1"),
(Row([0], Schema({"col1": Integer()})), "col2"),
(Row.from_dict({}), "col1"),
(Row.from_dict({"col1": 0}), "col2"),
],
)
def test_should_raise_if_column_does_not_exist(self, row: Row, column_name: str) -> None:
Expand All @@ -85,8 +106,8 @@ class TestHash:
@pytest.mark.parametrize(
("row1", "row2"),
[
(Row([]), Row([])),
(Row([0], Schema({"col1": Integer()})), Row([0], Schema({"col1": Integer()}))),
(Row.from_dict({}), Row.from_dict({})),
(Row.from_dict({"col1": 0}), Row.from_dict({"col1": 0})),
],
)
def test_should_return_same_hash_for_equal_rows(self, row1: Row, row2: Row) -> None:
Expand All @@ -95,9 +116,9 @@ def test_should_return_same_hash_for_equal_rows(self, row1: Row, row2: Row) -> N
@pytest.mark.parametrize(
("row1", "row2"),
[
(Row([0], Schema({"col1": Integer()})), Row([1], Schema({"col1": Integer()}))),
(Row([0], Schema({"col1": Integer()})), Row([0], Schema({"col2": Integer()}))),
(Row([0], Schema({"col1": Integer()})), Row(["a"], Schema({"col1": String()}))),
(Row.from_dict({"col1": 0}), Row.from_dict({"col1": 1})),
(Row.from_dict({"col1": 0}), Row.from_dict({"col2": 0})),
(Row.from_dict({"col1": 0}), Row.from_dict({"col1": "a"})),
],
)
def test_should_return_different_hash_for_unequal_rows(self, row1: Row, row2: Row) -> None:
Expand All @@ -108,8 +129,8 @@ class TestIter:
@pytest.mark.parametrize(
("row", "expected"),
[
(Row([], Schema({})), []),
(Row([0], Schema({"col1": Integer()})), ["col1"]),
(Row.from_dict({}), []),
(Row.from_dict({"col1": 0}), ["col1"]),
],
)
def test_should_return_an_iterator_for_the_column_names(self, row: Row, expected: list[str]) -> None:
Expand All @@ -120,8 +141,8 @@ class TestLen:
@pytest.mark.parametrize(
("row", "expected"),
[
(Row([]), 0),
(Row([0, "a"]), 2),
(Row.from_dict({}), 0),
(Row.from_dict({"col1": 0, "col2": "a"}), 2),
],
)
def test_should_return_the_number_of_columns(self, row: Row, expected: int) -> None:
Expand All @@ -132,8 +153,8 @@ class TestGetValue:
@pytest.mark.parametrize(
("row", "column_name", "expected"),
[
(Row([0], Schema({"col1": Integer()})), "col1", 0),
(Row([0, "a"], Schema({"col1": Integer(), "col2": String()})), "col2", "a"),
(Row.from_dict({"col1": 0}), "col1", 0),
(Row.from_dict({"col1": 0, "col2": "a"}), "col2", "a"),
],
)
def test_should_return_the_value_in_the_column(self, row: Row, column_name: str, expected: Any) -> None:
Expand All @@ -142,8 +163,8 @@ def test_should_return_the_value_in_the_column(self, row: Row, column_name: str,
@pytest.mark.parametrize(
("row", "column_name"),
[
(Row([], Schema({})), "col1"),
(Row([0], Schema({"col1": Integer()})), "col2"),
(Row.from_dict({}), "col1"),
(Row.from_dict({"col1": 0}), "col2"),
],
)
def test_should_raise_if_column_does_not_exist(self, row: Row, column_name: str) -> None:
Expand All @@ -155,9 +176,9 @@ class TestHasColumn:
@pytest.mark.parametrize(
("row", "column_name", "expected"),
[
(Row([], Schema({})), "col1", False),
(Row([0], Schema({"col1": Integer()})), "col1", True),
(Row([0], Schema({"col1": Integer()})), "col2", False),
(Row.from_dict({}), "col1", False),
(Row.from_dict({"col1": 0}), "col1", True),
(Row.from_dict({"col1": 0}), "col2", False),
],
)
def test_should_return_whether_the_row_has_the_column(self, row: Row, column_name: str, expected: bool) -> None:
Expand All @@ -168,8 +189,8 @@ class TestGetColumnNames:
@pytest.mark.parametrize(
("row", "expected"),
[
(Row([], Schema({})), []),
(Row([0], Schema({"col1": Integer()})), ["col1"]),
(Row.from_dict({}), []),
(Row.from_dict({"col1": 0}), ["col1"]),
],
)
def test_should_return_the_column_names(self, row: Row, expected: list[str]) -> None:
Expand All @@ -180,8 +201,8 @@ class TestGetTypeOfColumn:
@pytest.mark.parametrize(
("row", "column_name", "expected"),
[
(Row([0], Schema({"col1": Integer()})), "col1", Integer()),
(Row([0, "a"], Schema({"col1": Integer(), "col2": String()})), "col2", String()),
(Row.from_dict({"col1": 0}), "col1", Integer()),
(Row.from_dict({"col1": 0, "col2": "a"}), "col2", String()),
],
)
def test_should_return_the_type_of_the_column(self, row: Row, column_name: str, expected: ColumnType) -> None:
Expand All @@ -190,8 +211,8 @@ def test_should_return_the_type_of_the_column(self, row: Row, column_name: str,
@pytest.mark.parametrize(
("row", "column_name"),
[
(Row([], Schema({})), "col1"),
(Row([0], Schema({"col1": Integer()})), "col2"),
(Row.from_dict({}), "col1"),
(Row.from_dict({"col1": 0}), "col2"),
],
)
def test_should_raise_if_column_does_not_exist(self, row: Row, column_name: str) -> None:
Expand All @@ -203,9 +224,30 @@ class TestCount:
@pytest.mark.parametrize(
("row", "expected"),
[
(Row([]), 0),
(Row([0, "a"]), 2),
(Row.from_dict({}), 0),
(Row.from_dict({"col1": 0, "col2": "a"}), 2),
],
)
def test_should_return_the_number_of_columns(self, row: Row, expected: int) -> None:
assert row.count() == expected


class TestToDict:
@pytest.mark.parametrize(
("row", "expected"),
[
(
Row([]),
{},
),
(
Row([1, 2], schema=Schema({"a": Integer(), "b": Integer()})),
{
"a": 1,
"b": 2,
},
),
],
)
def test_should_return_dict_for_table(self, row: Row, expected: dict[str, Any]) -> None:
assert row.to_dict() == expected
4 changes: 2 additions & 2 deletions tests/safeds/data/tabular/containers/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class TestFromDict:
),
],
)
def test_should_create_table_from_dict(self, data: dict[str, Any], expected: Table) -> None:
def test_should_create_table_from_dict(self, data: dict[str, list[Any]], expected: Table) -> None:
assert Table.from_dict(data) == expected

def test_should_raise_if_columns_have_different_lengths(self) -> None:
Expand All @@ -49,7 +49,7 @@ class TestToDict:
),
],
)
def test_should_return_dict_for_table(self, table: Table, expected: dict[str, Any]) -> None:
def test_should_return_dict_for_table(self, table: Table, expected: dict[str, list[Any]]) -> None:
assert table.to_dict() == expected


Expand Down

0 comments on commit e98b653

Please sign in to comment.