Skip to content

Commit fc9bb71

Browse files
committed
Fix image utils optional deps errors
Signed-off-by: Shizoqua <hr.lanreshittu@yahoo.com>
1 parent b16fa09 commit fc9bb71

File tree

4 files changed

+179
-107
lines changed

4 files changed

+179
-107
lines changed

sdk/python/feast/image_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ def validate_image_format(image_bytes: bytes) -> bool:
241241
Returns:
242242
True if valid image, False otherwise
243243
"""
244+
_check_image_dependencies()
244245
try:
245246
with Image.open(io.BytesIO(image_bytes)) as img:
246247
img.verify()
@@ -259,6 +260,7 @@ def get_image_metadata(image_bytes: bytes) -> dict:
259260
Raises:
260261
ValueError: If image cannot be processed
261262
"""
263+
_check_image_dependencies()
262264
try:
263265
with Image.open(io.BytesIO(image_bytes)) as img:
264266
return {

sdk/python/pytest.ini

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,8 @@ markers =
66
universal_online_stores: mark a test as using all online stores.
77
rbac_remote_integration_test: mark a integration test related to rbac and remote functionality.
88

9-
env =
10-
IS_TEST=True
11-
129
filterwarnings =
1310
error::_pytest.warning_types.PytestConfigWarning
14-
error::_pytest.warning_types.PytestUnhandledCoroutineWarning
1511
ignore::DeprecationWarning:pyspark.sql.pandas.*:
1612
ignore::DeprecationWarning:pyspark.sql.connect.*:
1713
ignore::DeprecationWarning:httpx.*:

sdk/python/tests/conftest.py

Lines changed: 144 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,67 @@
3636
create_document_dataset,
3737
create_image_dataset,
3838
)
39-
from tests.integration.feature_repos.integration_test_repo_config import ( # noqa: E402
40-
IntegrationTestRepoConfig,
41-
)
42-
from tests.integration.feature_repos.repo_configuration import ( # noqa: E402
43-
AVAILABLE_OFFLINE_STORES,
44-
AVAILABLE_ONLINE_STORES,
45-
OFFLINE_STORE_TO_PROVIDER_CONFIG,
46-
Environment,
47-
TestData,
48-
construct_test_environment,
49-
construct_universal_feature_views,
50-
construct_universal_test_data,
51-
)
52-
from tests.integration.feature_repos.universal.data_sources.file import ( # noqa: E402
53-
FileDataSourceCreator,
54-
)
55-
from tests.integration.feature_repos.universal.entities import ( # noqa: E402
56-
customer,
57-
driver,
58-
location,
59-
)
60-
from tests.utils.auth_permissions_util import default_store
39+
try:
40+
from tests.integration.feature_repos.integration_test_repo_config import ( # noqa: E402
41+
IntegrationTestRepoConfig,
42+
)
43+
from tests.integration.feature_repos.repo_configuration import ( # noqa: E402
44+
AVAILABLE_OFFLINE_STORES,
45+
AVAILABLE_ONLINE_STORES,
46+
OFFLINE_STORE_TO_PROVIDER_CONFIG,
47+
Environment,
48+
TestData,
49+
construct_test_environment,
50+
construct_universal_feature_views,
51+
construct_universal_test_data,
52+
)
53+
from tests.integration.feature_repos.universal.data_sources.file import ( # noqa: E402
54+
FileDataSourceCreator,
55+
)
56+
from tests.integration.feature_repos.universal.entities import ( # noqa: E402
57+
customer,
58+
driver,
59+
location,
60+
)
61+
62+
_integration_test_deps_available = True
63+
except ModuleNotFoundError:
64+
_integration_test_deps_available = False
65+
66+
IntegrationTestRepoConfig = None # type: ignore[assignment]
67+
AVAILABLE_OFFLINE_STORES = [] # type: ignore[assignment]
68+
AVAILABLE_ONLINE_STORES = {} # type: ignore[assignment]
69+
OFFLINE_STORE_TO_PROVIDER_CONFIG = {} # type: ignore[assignment]
70+
Environment = Any # type: ignore[assignment]
71+
TestData = Any # type: ignore[assignment]
72+
73+
def construct_test_environment(*args, **kwargs): # type: ignore[no-redef]
74+
raise RuntimeError("Integration test dependencies are not available")
75+
76+
def construct_universal_feature_views(*args, **kwargs): # type: ignore[no-redef]
77+
raise RuntimeError("Integration test dependencies are not available")
78+
79+
def construct_universal_test_data(*args, **kwargs): # type: ignore[no-redef]
80+
raise RuntimeError("Integration test dependencies are not available")
81+
82+
class FileDataSourceCreator: # type: ignore[no-redef]
83+
pass
84+
85+
def customer(*args, **kwargs): # type: ignore[no-redef]
86+
raise RuntimeError("Integration test dependencies are not available")
87+
88+
def driver(*args, **kwargs): # type: ignore[no-redef]
89+
raise RuntimeError("Integration test dependencies are not available")
90+
91+
def location(*args, **kwargs): # type: ignore[no-redef]
92+
raise RuntimeError("Integration test dependencies are not available")
93+
94+
try:
95+
from tests.utils.auth_permissions_util import default_store
96+
except ModuleNotFoundError:
97+
98+
def default_store(*args, **kwargs): # type: ignore[no-redef]
99+
raise RuntimeError("Auth test dependencies are not available")
61100
from tests.utils.http_server import check_port_open, free_port # noqa: E402
62101
from tests.utils.ssl_certifcates_util import (
63102
combine_trust_stores,
@@ -67,6 +106,8 @@
67106

68107
logger = logging.getLogger(__name__)
69108

109+
os.environ.setdefault("IS_TEST", "True")
110+
70111
level = logging.INFO
71112
logging.basicConfig(
72113
format="%(asctime)s %(name)s %(levelname)s: %(message)s",
@@ -85,7 +126,7 @@
85126

86127

87128
def pytest_configure(config):
88-
if platform in ["darwin", "windows"]:
129+
if platform == "darwin" or platform.startswith("win"):
89130
multiprocessing.set_start_method("spawn", force=True)
90131
else:
91132
multiprocessing.set_start_method("fork")
@@ -239,92 +280,92 @@ def pytest_generate_tests(metafunc: pytest.Metafunc):
239280
240281
See more examples at https://docs.pytest.org/en/6.2.x/example/parametrize.html#paramexamples
241282
242-
We also utilize indirect parametrization here. Since `environment` is a fixture,
243-
when we call metafunc.parametrize("environment", ..., indirect=True) we actually
244-
parametrizing this "environment" fixture and not the test itself.
245-
Moreover, by utilizing `_config_cache` we are able to share `environment` fixture between different tests.
246-
In order for pytest to group tests together (and share environment fixture)
247-
parameter should point to the same Python object (hence, we use _config_cache dict to store those objects).
248283
"""
249-
if "environment" in metafunc.fixturenames:
250-
markers = {m.name: m for m in metafunc.definition.own_markers}
251-
offline_stores = None
252-
if "universal_offline_stores" in markers:
253-
# Offline stores can be explicitly requested
254-
if "only" in markers["universal_offline_stores"].kwargs:
255-
offline_stores = [
256-
OFFLINE_STORE_TO_PROVIDER_CONFIG.get(store_name)
257-
for store_name in markers["universal_offline_stores"].kwargs["only"]
258-
if store_name in OFFLINE_STORE_TO_PROVIDER_CONFIG
259-
]
260-
else:
261-
offline_stores = AVAILABLE_OFFLINE_STORES
284+
if "environment" not in metafunc.fixturenames:
285+
return
286+
287+
if not _integration_test_deps_available:
288+
pytest.skip("Integration test dependencies are not available")
289+
290+
markers = {m.name: m for m in metafunc.definition.iter_markers()}
291+
292+
offline_stores = None
293+
if "universal_offline_stores" in markers:
294+
# Offline stores can be explicitly requested
295+
if "only" in markers["universal_offline_stores"].kwargs:
296+
offline_stores = [
297+
OFFLINE_STORE_TO_PROVIDER_CONFIG.get(store_name)
298+
for store_name in markers["universal_offline_stores"].kwargs["only"]
299+
if store_name in OFFLINE_STORE_TO_PROVIDER_CONFIG
300+
]
262301
else:
263-
# default offline store for testing online store dimension
264-
offline_stores = [("local", FileDataSourceCreator)]
265-
266-
online_stores = None
267-
if "universal_online_stores" in markers:
268-
# Online stores can be explicitly requested
269-
if "only" in markers["universal_online_stores"].kwargs:
270-
online_stores = [
271-
AVAILABLE_ONLINE_STORES.get(store_name)
272-
for store_name in markers["universal_online_stores"].kwargs["only"]
273-
if store_name in AVAILABLE_ONLINE_STORES
274-
]
275-
else:
276-
online_stores = AVAILABLE_ONLINE_STORES.values()
277-
278-
if online_stores is None:
279-
# No online stores requested -> setting the default or first available
302+
offline_stores = AVAILABLE_OFFLINE_STORES
303+
else:
304+
# default offline store for testing online store dimension
305+
offline_stores = [("local", FileDataSourceCreator)]
306+
307+
online_stores = None
308+
if "universal_online_stores" in markers:
309+
# Online stores can be explicitly requested
310+
if "only" in markers["universal_online_stores"].kwargs:
280311
online_stores = [
281-
AVAILABLE_ONLINE_STORES.get(
282-
"redis",
283-
AVAILABLE_ONLINE_STORES.get(
284-
"sqlite", next(iter(AVAILABLE_ONLINE_STORES.values()))
285-
),
286-
)
312+
AVAILABLE_ONLINE_STORES.get(store_name)
313+
for store_name in markers["universal_online_stores"].kwargs["only"]
314+
if store_name in AVAILABLE_ONLINE_STORES
287315
]
288-
289-
extra_dimensions: List[Dict[str, Any]] = [{}]
290-
291-
if "python_server" in metafunc.fixturenames:
292-
extra_dimensions.extend([{"python_feature_server": True}])
293-
294-
configs = []
295-
if offline_stores:
296-
for provider, offline_store_creator in offline_stores:
297-
for online_store, online_store_creator in online_stores:
298-
for dim in extra_dimensions:
299-
config = {
300-
"provider": provider,
301-
"offline_store_creator": offline_store_creator,
302-
"online_store": online_store,
303-
"online_store_creator": online_store_creator,
304-
**dim,
305-
}
306-
307-
c = IntegrationTestRepoConfig(**config)
308-
309-
if c not in _config_cache:
310-
marks = [
311-
pytest.mark.xdist_group(name=m)
312-
for m in c.offline_store_creator.xdist_groups()
313-
]
314-
# Check if there are any test markers associated with the creator and add them.
315-
if c.offline_store_creator.test_markers():
316-
marks.extend(c.offline_store_creator.test_markers())
317-
318-
_config_cache[c] = pytest.param(c, marks=marks)
319-
320-
configs.append(_config_cache[c])
321316
else:
322-
# No offline stores requested -> setting the default or first available
323-
offline_stores = [("local", FileDataSourceCreator)]
317+
online_stores = AVAILABLE_ONLINE_STORES.values()
324318

325-
metafunc.parametrize(
326-
"environment", configs, indirect=True, ids=[str(c) for c in configs]
327-
)
319+
if online_stores is None:
320+
# No online stores requested -> setting the default or first available
321+
online_stores = [
322+
AVAILABLE_ONLINE_STORES.get(
323+
"redis",
324+
AVAILABLE_ONLINE_STORES.get(
325+
"sqlite", next(iter(AVAILABLE_ONLINE_STORES.values()))
326+
),
327+
)
328+
]
329+
330+
extra_dimensions: List[Dict[str, Any]] = [{}]
331+
332+
if "python_server" in metafunc.fixturenames:
333+
extra_dimensions.extend([{"python_feature_server": True}])
334+
335+
configs = []
336+
if offline_stores:
337+
for provider, offline_store_creator in offline_stores:
338+
for online_store, online_store_creator in online_stores:
339+
for dim in extra_dimensions:
340+
config = {
341+
"provider": provider,
342+
"offline_store_creator": offline_store_creator,
343+
"online_store": online_store,
344+
"online_store_creator": online_store_creator,
345+
**dim,
346+
}
347+
348+
c = IntegrationTestRepoConfig(**config)
349+
350+
if c not in _config_cache:
351+
marks = [
352+
pytest.mark.xdist_group(name=m)
353+
for m in c.offline_store_creator.xdist_groups()
354+
]
355+
# Check if there are any test markers associated with the creator and add them.
356+
if c.offline_store_creator.test_markers():
357+
marks.extend(c.offline_store_creator.test_markers())
358+
359+
_config_cache[c] = pytest.param(c, marks=marks)
360+
361+
configs.append(_config_cache[c])
362+
else:
363+
# No offline stores requested -> setting the default or first available
364+
offline_stores = [("local", FileDataSourceCreator)]
365+
366+
metafunc.parametrize(
367+
"environment", configs, indirect=True, ids=[str(c) for c in configs]
368+
)
328369

329370

330371
@pytest.fixture
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2024 The Feast Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest
16+
17+
18+
def test_validate_image_format_raises_when_deps_missing(monkeypatch):
19+
from feast import image_utils
20+
21+
monkeypatch.setattr(image_utils, "_image_dependencies_available", False)
22+
23+
with pytest.raises(ImportError, match="Image processing dependencies are not installed"):
24+
image_utils.validate_image_format(b"anything")
25+
26+
27+
def test_get_image_metadata_raises_when_deps_missing(monkeypatch):
28+
from feast import image_utils
29+
30+
monkeypatch.setattr(image_utils, "_image_dependencies_available", False)
31+
32+
with pytest.raises(ImportError, match="Image processing dependencies are not installed"):
33+
image_utils.get_image_metadata(b"anything")

0 commit comments

Comments
 (0)