Files
wehub-resource-sync dc6079821b
Docs Tests / Check for file changes (push) Has been cancelled
Docs Tests / Test Documentation (push) Has been cancelled
Docs Tests / Documentation Linting Checks (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.9) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.9) (push) Has been cancelled
Continuous Integration / Check for file changes (push) Has been cancelled
Continuous Integration / Wait for docs tests (push) Has been cancelled
Continuous Integration / Code Quality (push) Has been cancelled
Continuous Integration / Check for changelog (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Upload coverage reports to codeclimate (push) Has been cancelled
Continuous Integration / Run Non-Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Run Broker Integration Tests (push) Has been cancelled
Continuous Integration / Run Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Build Docker base images and setup environment (push) Has been cancelled
Continuous Integration / Build Docker (default) (push) Has been cancelled
Continuous Integration / Build Docker (full) (push) Has been cancelled
Continuous Integration / Build Docker (mitie-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-de) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-it) (push) Has been cancelled
Continuous Integration / Deploy to PyPI (push) Has been cancelled
Continuous Integration / Notify Slack & Publish Release Notes (push) Has been cancelled
Publish Documentation / Evaluate release tag (push) Has been cancelled
Publish Documentation / Prebuild Docs (push) Has been cancelled
Publish Documentation / Preview Docs (push) Has been cancelled
Publish Documentation / Check for file changes (push) Has been cancelled
Publish Documentation / Publish Docs (push) Has been cancelled
Automatic PR Merger / mergepal (push) Has been cancelled
CI Github Actions / Run Tests (push) Has been cancelled
Semgrep / Semgrep Workflow Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:47 +08:00

685 lines
24 KiB
Python

import itertools
import os
import tempfile
from pathlib import Path
from typing import Optional, Text, List, Dict, Tuple, Any
import numpy as np
import pytest
import scipy.sparse
from rasa.shared.nlu.constants import (
FEATURE_TYPE_SENTENCE,
FEATURE_TYPE_SEQUENCE,
TEXT,
INTENT,
)
from rasa.shared.nlu.training_data.features import (
Features,
FeatureMetadata,
save_features,
load_features,
)
@pytest.fixture
def safe_tensors_tmp_file() -> str:
with tempfile.NamedTemporaryFile(delete=False, suffix=".safetensors") as f:
yield f.name
os.unlink(f.name)
@pytest.fixture
def dense_features() -> Features:
features_matrix = np.array([[1, 2, 3], [4, 5, 6]])
return Features(
features=features_matrix,
feature_type="dense",
attribute="test",
origin="test_origin",
)
@pytest.fixture
def sparse_features() -> Features:
features_matrix = scipy.sparse.csr_matrix(
([1, 2, 3], ([0, 1, 1], [0, 1, 2])), shape=(2, 3)
)
return Features(
features=features_matrix,
feature_type="sparse",
attribute="test",
origin="test_origin",
)
@pytest.mark.parametrize(
"type,is_sparse,",
itertools.product([FEATURE_TYPE_SENTENCE, FEATURE_TYPE_SEQUENCE], [True, False]),
)
def test_print(type: Text, is_sparse: bool):
first_dim = 1 if type == FEATURE_TYPE_SEQUENCE else 3
matrix = np.full(shape=(first_dim, 2), fill_value=1)
if is_sparse:
matrix = scipy.sparse.coo_matrix(matrix)
feat = Features(
features=matrix,
attribute="fixed-attribute",
feature_type=type,
origin="origin--doesn't-matter-here",
)
assert repr(feat)
assert str(feat)
def test_combine_with_existing_dense_features():
existing_features = Features(
np.array([[1, 0, 2, 3], [2, 0, 0, 1]]), FEATURE_TYPE_SEQUENCE, TEXT, "test"
)
fingerprint = existing_features.fingerprint()
new_features = Features(
np.array([[1, 0], [0, 1]]), FEATURE_TYPE_SEQUENCE, TEXT, "origin"
)
expected_features = np.array([[1, 0, 2, 3, 1, 0], [2, 0, 0, 1, 0, 1]])
existing_features.combine_with_features(new_features)
assert np.all(expected_features == existing_features.features)
# check that combining features changes fingerprint
assert fingerprint != existing_features.fingerprint()
def test_combine_with_existing_dense_features_shape_mismatch():
existing_features = Features(
np.array([[1, 0, 2, 3], [2, 0, 0, 1]]), FEATURE_TYPE_SEQUENCE, TEXT, "test"
)
new_features = Features(np.array([[0, 1]]), FEATURE_TYPE_SEQUENCE, TEXT, "origin")
with pytest.raises(ValueError):
existing_features.combine_with_features(new_features)
def test_combine_with_existing_sparse_features():
existing_features = Features(
scipy.sparse.csr_matrix([[1, 0, 2, 3], [2, 0, 0, 1]]),
FEATURE_TYPE_SEQUENCE,
TEXT,
"test",
)
fingerprint = existing_features.fingerprint()
new_features = Features(
scipy.sparse.csr_matrix([[1, 0], [0, 1]]), FEATURE_TYPE_SEQUENCE, TEXT, "origin"
)
expected_features = [[1, 0, 2, 3, 1, 0], [2, 0, 0, 1, 0, 1]]
existing_features.combine_with_features(new_features)
actual_features = existing_features.features.toarray()
assert np.all(expected_features == actual_features)
# check that combining features changes fingerprint
assert fingerprint != existing_features.fingerprint()
def test_combine_with_existing_sparse_features_shape_mismatch():
existing_features = Features(
scipy.sparse.csr_matrix([[1, 0, 2, 3], [2, 0, 0, 1]]),
FEATURE_TYPE_SEQUENCE,
TEXT,
"test",
)
new_features = Features(
scipy.sparse.csr_matrix([[0, 1]]), FEATURE_TYPE_SEQUENCE, TEXT, "origin"
)
with pytest.raises(ValueError):
existing_features.combine_with_features(new_features)
def test_for_features_fingerprinting_collisions():
"""Tests that features fingerprints are unique."""
m1 = np.asarray([[0.5, 3.1, 3.0], [1.1, 1.2, 1.3], [4.7, 0.3, 2.7]])
m2 = np.asarray([[0, 0, 0], [1, 2, 3], [0, 0, 1]])
dense_features = [
Features(m1, FEATURE_TYPE_SENTENCE, TEXT, "CountVectorsFeaturizer"),
Features(m2, FEATURE_TYPE_SENTENCE, TEXT, "CountVectorsFeaturizer"),
Features(m1, FEATURE_TYPE_SEQUENCE, TEXT, "CountVectorsFeaturizer"),
Features(m1, FEATURE_TYPE_SEQUENCE, TEXT, "RegexFeaturizer"),
Features(m1, FEATURE_TYPE_SENTENCE, INTENT, "CountVectorsFeaturizer"),
]
dense_fingerprints = {f.fingerprint() for f in dense_features}
assert len(dense_fingerprints) == len(dense_features)
sparse_features = [
Features(
scipy.sparse.coo_matrix(m1),
FEATURE_TYPE_SENTENCE,
TEXT,
"CountVectorsFeaturizer",
),
Features(
scipy.sparse.coo_matrix(m2),
FEATURE_TYPE_SENTENCE,
TEXT,
"CountVectorsFeaturizer",
),
Features(
scipy.sparse.coo_matrix(m1),
FEATURE_TYPE_SEQUENCE,
TEXT,
"CountVectorsFeaturizer",
),
Features(
scipy.sparse.coo_matrix(m1), FEATURE_TYPE_SEQUENCE, TEXT, "RegexFeaturizer"
),
Features(
scipy.sparse.coo_matrix(m1),
FEATURE_TYPE_SENTENCE,
INTENT,
"CountVectorsFeaturizer",
),
]
sparse_fingerprints = {f.fingerprint() for f in sparse_features}
assert len(sparse_fingerprints) == len(sparse_features)
def test_feature_fingerprints_take_into_account_full_array():
"""Tests that fingerprint isn't using summary/abbreviated array info."""
big_array = np.random.random((128, 128))
f1 = Features(big_array, FEATURE_TYPE_SENTENCE, TEXT, "RegexFeaturizer")
big_array_with_zero = np.copy(big_array)
big_array_with_zero[64, 64] = 0.0
f2 = Features(big_array_with_zero, FEATURE_TYPE_SENTENCE, TEXT, "RegexFeaturizer")
assert f1.fingerprint() != f2.fingerprint()
f1_sparse = Features(
scipy.sparse.coo_matrix(big_array),
FEATURE_TYPE_SENTENCE,
TEXT,
"RegexFeaturizer",
)
f2_sparse = Features(
scipy.sparse.coo_matrix(big_array_with_zero),
FEATURE_TYPE_SENTENCE,
TEXT,
"RegexFeaturizer",
)
assert f1_sparse.fingerprint() != f2_sparse.fingerprint()
def _generate_feature_list_and_modifications(
is_sparse: bool, type: Text, number: int
) -> Tuple[List[Features], List[Dict[Text, Any]]]:
"""Creates a list of features with the required properties and some modifications.
The modifications are given by a list of kwargs dictionaries that can be used to
instantiate `Features` that differ from the aforementioned list of features in
exactly one property (i.e. type, sequence length (if the given `type` is
sequence type only), attribute, origin)
Args:
is_sparse: whether all features should be sparse
type: the type to be used for all features
number: the number of features to generate
Returns:
a tuple containing a list of features with the requested attributes and
a list of kwargs dictionaries that can be used to instantiate `Features` that
differ from the aforementioned list of features in exactly one property
"""
seq_len = 3
first_dim = 1 if type == FEATURE_TYPE_SENTENCE else 3
# create list of features whose properties match - except the shapes and
# feature values which are chosen in a specific way
features_list = []
for idx in range(number):
matrix = np.full(shape=(first_dim, idx + 1), fill_value=idx + 1)
if is_sparse:
matrix = scipy.sparse.coo_matrix(matrix)
config = dict(
features=matrix,
attribute="fixed-attribute",
feature_type=type,
origin=f"origin-{idx}",
)
feat = Features(**config)
features_list.append(feat)
# prepare some Features that differ from the features above in certain ways
modifications = []
# - if we modify one attribute
modifications.append({**config, **{"attribute": "OTHER"}})
# - if we modify one attribute
other_type = (
FEATURE_TYPE_SENTENCE
if type == FEATURE_TYPE_SEQUENCE
else FEATURE_TYPE_SEQUENCE
)
other_seq_len = 1 if other_type == FEATURE_TYPE_SENTENCE else seq_len
other_matrix = np.full(shape=(other_seq_len, number - 1), fill_value=number)
if is_sparse:
other_matrix = scipy.sparse.coo_matrix(other_matrix)
modifications.append(
{**config, **{"feature_type": other_type, "features": other_matrix}}
)
# - if we modify one origin
modifications.append({**config, **{"origin": "Other"}})
# - if we modify one sequence length
if type == FEATURE_TYPE_SEQUENCE:
matrix = np.full(shape=(seq_len + 1, idx + 1), fill_value=idx)
if is_sparse:
matrix = scipy.sparse.coo_matrix(matrix)
modifications.append({**config, **{"features": matrix}})
return features_list, modifications
@pytest.mark.parametrize(
"is_sparse,type,number,use_expected_origin",
itertools.product(
[True, False],
[FEATURE_TYPE_SENTENCE, FEATURE_TYPE_SEQUENCE],
[1, 2, 5],
[True, False],
),
)
def test_combine(is_sparse: bool, type: Text, number: int, use_expected_origin: bool):
features_list, modifications = _generate_feature_list_and_modifications(
is_sparse=is_sparse, type=type, number=number
)
modified_features = [Features(**config) for config in modifications]
first_dim = features_list[0].features.shape[0]
origins = [f"origin-{idx}" for idx in range(len(features_list))]
if number == 1:
# in this case the origin will be same str as before, not a list
origins = origins[0]
expected_origin = origins if use_expected_origin else None
# works as expected
combination = Features.combine(features_list, expected_origins=expected_origin)
assert combination.features.shape[1] == int(number * (number + 1) / 2)
assert combination.features.shape[0] == first_dim
assert combination.origin == origins
assert combination.is_sparse() == is_sparse
matrix = combination.features
if is_sparse:
matrix = combination.features.todense()
for idx in range(number):
offset = int(idx * (idx + 1) / 2)
assert np.all(matrix[:, offset : (offset + idx + 1)] == idx + 1)
# fails as expected in these cases
if use_expected_origin and number > 1:
for modified_feature in modified_features:
features_list_copy = features_list.copy()
features_list_copy[-1] = modified_feature
with pytest.raises(ValueError):
Features.combine(features_list_copy, expected_origins=expected_origin)
@pytest.mark.parametrize(
"is_sparse,type,number",
itertools.product(
[True, False], [FEATURE_TYPE_SENTENCE, FEATURE_TYPE_SEQUENCE], [1, 2, 5]
),
)
def test_filter(is_sparse: bool, type: Text, number: int):
features_list, modifications = _generate_feature_list_and_modifications(
is_sparse=is_sparse, type=type, number=number
)
# fix the filter configuration first (note: we ignore origin on purpose for now)
filter_config = dict(attributes=["fixed-attribute"], type=type, is_sparse=is_sparse)
# we get all features back if all features map...
result = Features.filter(features_list, **filter_config)
assert len(result) == number
# ... and less matches if we change the (relevant) properties of some features
modified_features = [
Features(**config)
for config in modifications
if set(config.keys()).intersection(filter_config.keys())
]
if number > 1:
for modified_feature in modified_features:
features_list_copy = features_list.copy()
features_list_copy[-1] = modified_feature
result = Features.filter(features_list_copy, **filter_config)
assert len(result) == number - 1
if number > 2:
for feat_a, feat_b in itertools.combinations(modified_features, 2):
features_list_copy = features_list.copy()
features_list_copy[-1] = feat_a
features_list_copy[-2] = feat_b
result = Features.filter(features_list_copy, **filter_config)
assert len(result) == number - 2
# don't forget to check the origin
filter_config = dict(
attributes=["fixed-attribute"],
type=type,
origin=["origin-0"],
is_sparse=is_sparse,
)
result = Features.filter(features_list, **filter_config)
assert len(result) == 1
@pytest.mark.parametrize(
"num_features_per_attribute,specified_attributes",
itertools.product(
[{"a": 3, "b": 1, "c": 0}],
[None, ["a", "b", "c", "doesnt-appear"], ["doesnt-appear"]],
),
)
def test_groupby(
num_features_per_attribute: Dict[Text, int],
specified_attributes: Optional[List[Text]],
):
features_list = []
for attribute, number in num_features_per_attribute.items():
for idx in range(number):
matrix = np.full(shape=(1, idx + 1), fill_value=idx + 1)
config = dict(
features=matrix,
attribute=attribute,
feature_type=FEATURE_TYPE_SEQUENCE, # doesn't matter
origin=f"origin-{idx}", # doens't matter
)
feat = Features(**config)
features_list.append(feat)
result = Features.groupby_attribute(features_list, attributes=specified_attributes)
if specified_attributes is None:
for attribute, number in num_features_per_attribute.items():
if number > 0:
assert attribute in result
assert len(result[attribute]) == number
else:
assert attribute not in result
else:
assert set(result.keys()) == set(specified_attributes)
for attribute in specified_attributes:
assert attribute in result
number = num_features_per_attribute.get(attribute, 0)
assert len(result[attribute]) == number
@pytest.mark.parametrize(
"shuffle_mode,num_features_per_combination",
itertools.product(
["reversed", "random"], [[1, 0, 0, 0], [1, 1, 1, 1], [2, 3, 4, 5], [0, 1, 2, 2]]
),
)
def test_reduce(
shuffle_mode: Text, num_features_per_combination: Tuple[int, int, int, int]
):
# all combinations - in the expected order
# (i.e. all sparse before all dense and sequence before sentence)
all_combinations = [
(FEATURE_TYPE_SEQUENCE, True),
(FEATURE_TYPE_SENTENCE, True),
(FEATURE_TYPE_SEQUENCE, False),
(FEATURE_TYPE_SENTENCE, False),
]
# multiply accordingly and mess up the order
chosen_combinations = [
spec
for spec, num in zip(all_combinations, num_features_per_combination)
for _ in range(num)
]
if shuffle_mode == "reversed":
messed_up_order = reversed(chosen_combinations)
else:
# Note: rng.permutation would mess up the types
rng = np.random.default_rng(23452345)
permutation = rng.permutation(len(chosen_combinations))
messed_up_order = [chosen_combinations[idx] for idx in permutation]
# create features accordingly
features_list = []
for idx, (type, is_sparse) in enumerate(messed_up_order):
first_dim = 1 if type == FEATURE_TYPE_SEQUENCE else 3
matrix = np.full(shape=(first_dim, 1), fill_value=1)
if is_sparse:
matrix = scipy.sparse.coo_matrix(matrix)
config = dict(
features=matrix,
attribute="fixed-attribute", # must be the same
feature_type=type,
origin="origin-does-matter-here", # must be the same
)
feat = Features(**config)
features_list.append(feat)
# reduce!
reduced_list = Features.reduce(features_list)
assert len(reduced_list) == sum(num > 0 for num in num_features_per_combination)
idx = 0
for num, (type, is_sparse) in zip(num_features_per_combination, all_combinations):
if num == 0:
# nothing to check here - because we already checked the length above
# and check the types and shape of all existing features in this loop
pass
else:
feature = reduced_list[idx]
assert feature.is_sparse() == is_sparse
assert feature.type == type
assert feature.features.shape[-1] == num
idx += 1
@pytest.mark.parametrize("differ", ["attribute", "origin"])
def test_reduce_raises_if_combining_different_origins_or_attributes(differ: Text):
# create features accordingly
arbitrary_fixed_type = FEATURE_TYPE_SENTENCE
features_list = []
for idx in range(2):
first_dim = 1
arbitrary_matrix_matching_type = np.full(shape=(first_dim, 1), fill_value=1)
config = dict(
features=arbitrary_matrix_matching_type,
attribute="fixed-attribute" if differ != "attribute" else f"attr-{idx}",
feature_type=arbitrary_fixed_type,
origin="fixed-origin" if differ != "origin" else f"origin-{idx}",
)
feat = Features(**config)
features_list.append(feat)
# reduce!
if differ == "attribute":
message = "Expected all Features to describe the same attribute"
expected_origin = ["origin"]
else:
message = "Expected 'origin-1' to be the origin of the 0-th"
expected_origin = ["origin-1"]
with pytest.raises(ValueError, match=message):
Features.reduce(features_list, expected_origins=expected_origin)
def test_feature_metadata():
metadata = FeatureMetadata(
data_type="dense",
attribute="text",
origin="test",
is_sparse=False,
shape=(10, 5),
safetensors_key="key_0",
)
assert metadata.data_type == "dense"
assert metadata.attribute == "text"
assert metadata.origin == "test"
assert not metadata.is_sparse
assert metadata.shape == (10, 5)
assert metadata.safetensors_key == "key_0"
def test_save_dense_features(safe_tensors_tmp_file: str, dense_features: Features):
features_dict = {"test_key": [dense_features]}
metadata = save_features(features_dict, safe_tensors_tmp_file)
assert "test_key" in metadata
assert len(metadata["test_key"]) == 1
assert metadata["test_key"][0]["data_type"] == "dense"
assert metadata["test_key"][0]["shape"] == (2, 3)
assert not metadata["test_key"][0]["is_sparse"]
assert Path(safe_tensors_tmp_file).exists()
def test_save_sparse_features(safe_tensors_tmp_file: str, sparse_features: Features):
features_dict = {"test_key": [sparse_features]}
metadata = save_features(features_dict, safe_tensors_tmp_file)
assert "test_key" in metadata
assert len(metadata["test_key"]) == 1
assert metadata["test_key"][0]["data_type"] == "sparse"
assert metadata["test_key"][0]["shape"] == (2, 3)
assert metadata["test_key"][0]["is_sparse"]
assert Path(safe_tensors_tmp_file).exists()
def test_save_mixed_features(
safe_tensors_tmp_file: str, dense_features: Features, sparse_features: Features
):
features_dict = {"test_key": [dense_features, sparse_features]}
metadata = save_features(features_dict, safe_tensors_tmp_file)
assert "test_key" in metadata
assert len(metadata["test_key"]) == 2
assert metadata["test_key"][0]["data_type"] == "dense"
assert metadata["test_key"][1]["data_type"] == "sparse"
assert Path(safe_tensors_tmp_file).exists()
def test_save_multiple_keys(
safe_tensors_tmp_file: str, dense_features: Features, sparse_features: Features
):
features_dict = {"dense_key": [dense_features], "sparse_key": [sparse_features]}
metadata = save_features(features_dict, safe_tensors_tmp_file)
assert "dense_key" in metadata
assert "sparse_key" in metadata
assert metadata["dense_key"][0]["data_type"] == "dense"
assert metadata["sparse_key"][0]["data_type"] == "sparse"
assert Path(safe_tensors_tmp_file).exists()
@pytest.fixture
def setup_save_load(
safe_tensors_tmp_file: str, dense_features: Features, sparse_features: Features
) -> Tuple[str, Dict[str, Any], Dict[str, List[Features]]]:
features_dict = {"dense_key": [dense_features], "sparse_key": [sparse_features]}
metadata = save_features(features_dict, safe_tensors_tmp_file)
return safe_tensors_tmp_file, metadata, features_dict
def test_load_dense_features(
setup_save_load: Tuple[str, Dict[str, Any], Dict[str, List[Features]]],
):
temp_file, metadata, original_dict = setup_save_load
loaded_dict = load_features(temp_file, metadata)
assert "dense_key" in loaded_dict
assert len(loaded_dict["dense_key"]) == 1
assert not loaded_dict["dense_key"][0].is_sparse()
np.testing.assert_array_equal(
loaded_dict["dense_key"][0].features, original_dict["dense_key"][0].features
)
def test_load_sparse_features(
setup_save_load: Tuple[str, Dict[str, Any], Dict[str, List[Features]]],
):
temp_file, metadata, original_dict = setup_save_load
loaded_dict = load_features(temp_file, metadata)
assert "sparse_key" in loaded_dict
assert len(loaded_dict["sparse_key"]) == 1
assert loaded_dict["sparse_key"][0].is_sparse()
assert (
loaded_dict["sparse_key"][0].features != original_dict["sparse_key"][0].features
).nnz == 0
def test_load_preserves_metadata(
setup_save_load: Tuple[str, Dict[str, Any], Dict[str, List[Features]]],
):
temp_file, metadata, original_dict = setup_save_load
loaded_dict = load_features(temp_file, metadata)
for key in original_dict:
for orig_feat, loaded_feat in zip(original_dict[key], loaded_dict[key]):
assert orig_feat.type == loaded_feat.type
assert orig_feat.attribute == loaded_feat.attribute
assert orig_feat.origin == loaded_feat.origin
def test_load_nonexistent_file():
with pytest.raises(Exception):
load_features("nonexistent.safetensors", {})
def test_load_invalid_metadata(safe_tensors_tmp_file: str, dense_features: Features):
features_dict = {"test_key": [dense_features]}
metadata = save_features(features_dict, safe_tensors_tmp_file)
# Corrupt the metadata
metadata["test_key"][0]["safetensors_key"] = "invalid_key"
with pytest.raises(Exception):
load_features(safe_tensors_tmp_file, metadata)
def test_end_to_end(safe_tensors_tmp_file: str):
# Create test data
dense_matrix = np.array([[1, 2], [3, 4]])
sparse_matrix = scipy.sparse.csr_matrix(([1, 2], ([0, 1], [0, 1])), shape=(2, 2))
features_dict = {
"group1": [
Features(dense_matrix, "dense", "test1", "origin1"),
Features(sparse_matrix, "sparse", "test2", "origin2"),
],
"group2": [
Features(dense_matrix * 2, "dense", "test3", ["origin3", "origin4"])
],
}
# Save features
metadata = save_features(features_dict, safe_tensors_tmp_file)
# Load features
loaded_dict = load_features(safe_tensors_tmp_file, metadata)
# Verify structure
assert set(loaded_dict.keys()) == set(features_dict.keys())
assert len(loaded_dict["group1"]) == 2
assert len(loaded_dict["group2"]) == 1
# Verify dense features
np.testing.assert_array_equal(
loaded_dict["group1"][0].features, features_dict["group1"][0].features
)
# Verify sparse features
assert (
loaded_dict["group1"][1].features != features_dict["group1"][1].features
).nnz == 0
# Verify metadata
assert loaded_dict["group1"][0].type == "dense"
assert loaded_dict["group1"][1].type == "sparse"
assert loaded_dict["group2"][0].origin == ["origin3", "origin4"]