-
Notifications
You must be signed in to change notification settings - Fork 148
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e1a7222
commit 04be56e
Showing
7 changed files
with
240 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
227 changes: 169 additions & 58 deletions
227
vizro-core/tests/unit/vizro/managers/test_data_manager.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,62 +1,173 @@ | ||
"""Unit tests for vizro.managers.data_manager.""" | ||
from contextlib import suppress | ||
|
||
import time | ||
|
||
import numpy as np | ||
import pandas as pd | ||
import pytest | ||
from vizro.managers._data_manager import DataManager | ||
|
||
|
||
class TestDataManager: | ||
def setup_method(self): | ||
self.data_manager = DataManager() | ||
self.data = pd.DataFrame({"col1": [1, 2, 3], "col2": [4, 5, 6]}) | ||
|
||
def test_add_dataframe(self): | ||
dataset_name = "test_dataset" | ||
component_id = "component_id_a" | ||
self.data_manager[dataset_name] = self.data | ||
self.data_manager._add_component(component_id, dataset_name) | ||
assert self.data_manager._get_component_data(component_id).equals(self.data) | ||
|
||
def test_add_lazy_dataframe(self): | ||
dataset_name = "test_lazy_dataset" | ||
component_id = "component_id_b" | ||
|
||
def lazy_data(): | ||
return self.data | ||
|
||
self.data_manager[dataset_name] = lazy_data | ||
self.data_manager._add_component(component_id, dataset_name) | ||
assert self.data_manager._get_component_data(component_id).equals(lazy_data()) | ||
|
||
def test_add_existing_dataset(self): | ||
dataset_name = "existing_dataset" | ||
self.data_manager[dataset_name] = self.data | ||
with pytest.raises(ValueError): | ||
self.data_manager[dataset_name] = self.data | ||
|
||
def test_add_invalid_dataset(self): | ||
dataset_name = "invalid_dataset" | ||
invalid_data = "not_a_dataframe" | ||
with pytest.raises(TypeError): | ||
self.data_manager[dataset_name] = invalid_data | ||
|
||
def test_add_component_to_nonexistent_dataset(self): | ||
component_id = "test_component" | ||
dataset_name = "nonexistent_dataset" | ||
with pytest.raises(KeyError): | ||
self.data_manager._add_component(component_id, dataset_name) | ||
|
||
def test_add_existing_component(self): | ||
component_id = "existing_component" | ||
dataset_name = "test_dataset" | ||
self.data_manager[dataset_name] = self.data | ||
self.data_manager._add_component(component_id, dataset_name) | ||
with pytest.raises(ValueError): | ||
self.data_manager._add_component(component_id, dataset_name) | ||
|
||
def test_get_component_data_nonexistent(self): | ||
dataset_name = "test_dataset" | ||
nonexistent_component = "nonexistent_component" | ||
self.data_manager[dataset_name] = self.data | ||
with pytest.raises(KeyError): | ||
self.data_manager._get_component_data(nonexistent_component) | ||
from flask_caching import Cache | ||
|
||
from asserts import assert_frame_not_equal | ||
from vizro import Vizro | ||
from vizro.managers import data_manager | ||
from pandas.testing import assert_frame_equal | ||
|
||
|
||
@pytest.fixture(autouse=True) | ||
def clear_cache(): | ||
yield | ||
# Vizro._reset doesn't empty the cache, so any tests which have something other than NullCache must clear it | ||
# after running. Suppress AttributeError: 'Cache' object has no attribute 'app' that occurs when | ||
# data_manager._cache_has_app is False. | ||
with suppress(AttributeError): | ||
data_manager.cache.clear() | ||
|
||
|
||
class TestLoad: | ||
def test_static(self): | ||
data = pd.DataFrame([1, 2, 3]) | ||
data_manager["data"] = data | ||
loaded_data = data_manager["data"].load() | ||
assert_frame_equal(loaded_data, data) | ||
# Make sure loaded_data is a copy rather than the same object. | ||
assert loaded_data is not data | ||
|
||
def test_dynamic(self): | ||
data = lambda: pd.DataFrame([1, 2, 3]) | ||
data_manager["data"] = data | ||
loaded_data = data_manager["data"].load() | ||
assert_frame_equal(loaded_data, data()) | ||
# Make sure loaded_data is a copy rather than the same object. | ||
assert loaded_data is not data() | ||
|
||
|
||
class TestInvalid: | ||
def test_static_data_des_not_support_timeout(self): | ||
data = pd.DataFrame([1, 2, 3]) | ||
data_manager["data"] = data | ||
with pytest.raises( | ||
AttributeError, match="Static data that is a pandas.DataFrame itself does not support timeout" | ||
): | ||
data_manager["data"].timeout = 10 | ||
|
||
def test_setitem(self): | ||
with pytest.raises( | ||
TypeError, match="Data source data must be a pandas DataFrame or function that returns a pandas DataFrame." | ||
): | ||
data_manager["data"] = pd.Series([1, 2, 3]) | ||
|
||
def test_does_not_exist(self): | ||
with pytest.raises(KeyError, match="Data source data does not exist."): | ||
data_manager["data"] | ||
|
||
|
||
def make_random_data(): | ||
return pd.DataFrame(np.random.default_rng().random(3)) | ||
|
||
|
||
class TestCacheNotOperational: | ||
def test_null_cache_no_app(self): | ||
# No app at all, so memoize decorator is bypassed completely as data_manager._cache_has_app is False. | ||
data_manager["data"] = make_random_data | ||
loaded_data_1 = data_manager["data"].load() | ||
loaded_data_2 = data_manager["data"].load() | ||
assert_frame_not_equal(loaded_data_1, loaded_data_2) | ||
|
||
def test_null_cache_with_app(self): | ||
# App exists but cache is NullCache so does not do anything. | ||
data_manager["data"] = make_random_data | ||
Vizro() | ||
loaded_data_1 = data_manager["data"].load() | ||
loaded_data_2 = data_manager["data"].load() | ||
assert_frame_not_equal(loaded_data_1, loaded_data_2) | ||
|
||
def test_cache_no_app(self): | ||
# App exists and has a real cache but data_manager.cache is set too late so app is not attached to cache. | ||
data_manager["data"] = make_random_data | ||
Vizro() | ||
data_manager.cache = Cache(config={"CACHE_TYPE": "SimpleCache"}) | ||
|
||
with pytest.warns(UserWarning, match="Cache does not have Vizro app attached and so is not operational."): | ||
loaded_data_1 = data_manager["data"].load() | ||
loaded_data_2 = data_manager["data"].load() | ||
assert_frame_not_equal(loaded_data_1, loaded_data_2) | ||
|
||
|
||
@pytest.fixture | ||
def simple_cache(): | ||
# We don't need the Flask request context to run tests. (flask-caching tests for memoize use | ||
# app.test_request_context() but look like they don't actually need to, since only flask_caching.Cache.cached | ||
# requires the request context.) | ||
# We do need a Flask app to be attached for the cache to be operational though, hence the call Vizro(). | ||
data_manager.cache = Cache(config={"CACHE_TYPE": "SimpleCache"}) | ||
Vizro() | ||
yield | ||
|
||
|
||
# TODO: think if any more needed here | ||
# TODO: make sure no changes would have affected the manual testing | ||
class TestCache: | ||
def test_simple_cache(self, simple_cache): | ||
data_manager["data"] = make_random_data | ||
loaded_data_1 = data_manager["data"].load() | ||
loaded_data_2 = data_manager["data"].load() | ||
|
||
# Cache saves result. | ||
assert_frame_equal(loaded_data_1, loaded_data_2) | ||
|
||
def test_shared_dynamic_data_function(self, simple_cache): | ||
data_manager["data_x"] = make_random_data | ||
data_manager["data_y"] = make_random_data | ||
|
||
# Two data sources that shared the same function are independent. | ||
loaded_data_x_1 = data_manager["data_x"].load() | ||
loaded_data_y_1 = data_manager["data_y"].load() | ||
assert_frame_not_equal(loaded_data_x_1, loaded_data_y_1) | ||
|
||
loaded_data_x_2 = data_manager["data_x"].load() | ||
loaded_other_y_2 = data_manager["data_y"].load() | ||
|
||
# Cache saves result. | ||
assert_frame_equal(loaded_data_x_1, loaded_data_x_2) | ||
assert_frame_equal(loaded_data_y_1, loaded_other_y_2) | ||
|
||
def test_change_default_timeout(self): | ||
data_manager.cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 1}) | ||
Vizro() | ||
|
||
data_manager["data"] = make_random_data | ||
loaded_data_1 = data_manager["data"].load() | ||
time.sleep(1) | ||
loaded_data_2 = data_manager["data"].load() | ||
|
||
# Cache has expired in between two data loads. | ||
assert_frame_not_equal(loaded_data_1, loaded_data_2) | ||
|
||
def test_change_individual_timeout(self, simple_cache): | ||
data_manager["data"] = make_random_data | ||
data_manager["data"].timeout = 1 | ||
|
||
loaded_data_1 = data_manager["data"].load() | ||
time.sleep(1) | ||
loaded_data_2 = data_manager["data"].load() | ||
|
||
# Cache has expired in between two data loads. | ||
assert_frame_not_equal(loaded_data_1, loaded_data_2) | ||
|
||
def test_timeouts_do_not_interfere(self, simple_cache): | ||
# This test only passes thanks to the code in memoize that alters the wrapped.__func__.__qualname__, | ||
# as explained in the docstring there. If that bit of code is removed then this test correctly fails. | ||
data_manager["data_x"] = make_random_data | ||
data_manager["data_y"] = make_random_data | ||
data_manager["data_y"].timeout = 1 | ||
|
||
loaded_data_x_1 = data_manager["data_x"].load() | ||
loaded_data_y_1 = data_manager["data_y"].load() | ||
time.sleep(1) | ||
loaded_data_x_2 = data_manager["data_x"].load() | ||
loaded_data_y_2 = data_manager["data_y"].load() | ||
|
||
# Cache has expired for data_y but not data_x. | ||
assert_frame_equal(loaded_data_x_1, loaded_data_x_2) | ||
assert_frame_not_equal(loaded_data_y_1, loaded_data_y_2) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,12 @@ | ||
import plotly.express as px | ||
import vizro.plotly.express as hpx | ||
import vizro.plotly.express as vpx | ||
|
||
|
||
def test_non_chart_unchanged(): | ||
assert hpx.data is px.data | ||
assert vpx.data is px.data | ||
|
||
|
||
def test_chart_wrapped(): | ||
graph = hpx.scatter(px.data.iris(), x="petal_width", y="petal_length") | ||
graph = vpx.scatter(px.data.iris(), x="petal_width", y="petal_length") | ||
assert graph._captured_callable._function is px.scatter | ||
assert hpx.scatter is not px.scatter | ||
assert vpx.scatter is not px.scatter |