Files
2026-07-13 13:22:34 +08:00

1376 lines
53 KiB
Python

import filecmp
import io
import json
import os
import pathlib
import posixpath
import random
import re
from datetime import datetime, timezone
from typing import NamedTuple
from unittest import mock
import pytest
import yaml
import mlflow
from mlflow import MlflowClient, tracking
from mlflow.entities import LifecycleStage, Metric, Param, RunStatus, RunTag, ViewType
from mlflow.environment_variables import (
MLFLOW_ASYNC_LOGGING_THREADPOOL_SIZE,
MLFLOW_RUN_ID,
)
from mlflow.exceptions import MlflowException
from mlflow.protos.databricks_pb2 import (
INVALID_PARAMETER_VALUE,
RESOURCE_DOES_NOT_EXIST,
ErrorCode,
)
from mlflow.store.tracking.file_store import FileStore
from mlflow.tracking._tracking_service.client import TrackingServiceClient
from mlflow.tracking.fluent import start_run
from mlflow.utils.file_utils import local_file_uri_to_path
from mlflow.utils.mlflow_tags import (
MLFLOW_PARENT_RUN_ID,
MLFLOW_RUN_NAME,
MLFLOW_SOURCE_NAME,
MLFLOW_SOURCE_TYPE,
MLFLOW_USER,
)
from mlflow.utils.os import is_windows
from mlflow.utils.time import get_current_time_millis
from mlflow.utils.validation import (
MAX_METRICS_PER_BATCH,
MAX_PARAMS_TAGS_PER_BATCH,
)
class MockExperiment(NamedTuple):
experiment_id: str
lifecycle_stage: str
tags: dict[str, str] = {}
def test_create_experiment():
with pytest.raises(MlflowException, match="Invalid experiment name"):
mlflow.create_experiment(None)
with pytest.raises(MlflowException, match="Invalid experiment name"):
mlflow.create_experiment("")
exp_id = mlflow.create_experiment(f"Some random experiment name {random.randint(1, int(1e6))}")
assert exp_id is not None
def test_create_experiment_with_duplicate_name():
name = "popular_name"
exp_id = mlflow.create_experiment(name)
with pytest.raises(MlflowException, match=re.escape(f"Experiment(name={name}) already exists")):
mlflow.create_experiment(name)
tracking.MlflowClient().delete_experiment(exp_id)
with pytest.raises(MlflowException, match=re.escape(f"Experiment(name={name}) already exists")):
mlflow.create_experiment(name)
def test_create_experiments_with_bad_names():
# None for name
with pytest.raises(MlflowException, match="Invalid experiment name: 'None'"):
mlflow.create_experiment(None)
# empty string name
with pytest.raises(MlflowException, match="Invalid experiment name: ''"):
mlflow.create_experiment("")
@pytest.mark.parametrize("name", [123, 0, -1.2, [], ["A"], {1: 2}])
def test_create_experiments_with_bad_name_types(name):
with pytest.raises(
MlflowException,
match=re.escape(f"Invalid experiment name: {name}. Expects a string."),
):
mlflow.create_experiment(name)
@pytest.mark.usefixtures("reset_active_experiment")
def test_set_experiment_by_name():
name = "random_exp"
exp_id = mlflow.create_experiment(name)
exp1 = mlflow.set_experiment(name)
assert exp1.experiment_id == exp_id
with start_run() as run:
assert run.info.experiment_id == exp_id
another_name = "another_experiment"
exp2 = mlflow.set_experiment(another_name)
with start_run() as another_run:
assert another_run.info.experiment_id == exp2.experiment_id
@pytest.mark.usefixtures("reset_active_experiment")
def test_set_experiment_by_id():
name = "random_exp"
exp_id = mlflow.create_experiment(name)
active_exp = mlflow.set_experiment(experiment_id=exp_id)
assert active_exp.experiment_id == exp_id
with start_run() as run:
assert run.info.experiment_id == exp_id
nonexistent_id = "-1337"
with pytest.raises(MlflowException, match="No Experiment with id=-1337 exists") as exc:
mlflow.set_experiment(experiment_id=nonexistent_id)
assert exc.value.error_code == ErrorCode.Name(RESOURCE_DOES_NOT_EXIST)
with start_run() as run:
assert run.info.experiment_id == exp_id
def test_set_experiment_parameter_validation():
with pytest.raises(MlflowException, match="Must specify exactly one") as exc:
mlflow.set_experiment()
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
with pytest.raises(MlflowException, match="Must specify exactly one") as exc:
mlflow.set_experiment(None)
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
with pytest.raises(MlflowException, match="Must specify exactly one") as exc:
mlflow.set_experiment(None, None)
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
with pytest.raises(MlflowException, match="Must specify exactly one") as exc:
mlflow.set_experiment("name", "id")
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
def test_set_experiment_with_deleted_experiment():
name = "dead_exp"
mlflow.set_experiment(name)
with start_run() as run:
exp_id = run.info.experiment_id
tracking.MlflowClient().delete_experiment(exp_id)
with pytest.raises(MlflowException, match="Cannot set a deleted experiment") as exc:
mlflow.set_experiment(name)
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
with pytest.raises(MlflowException, match="Cannot set a deleted experiment") as exc:
mlflow.set_experiment(experiment_id=exp_id)
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
@pytest.mark.usefixtures("reset_active_experiment")
def test_set_experiment_with_zero_id():
mock_experiment = MockExperiment(experiment_id=0, lifecycle_stage=LifecycleStage.ACTIVE)
with (
mock.patch.object(
TrackingServiceClient,
"get_experiment_by_name",
mock.Mock(return_value=mock_experiment),
) as get_experiment_by_name_mock,
mock.patch.object(TrackingServiceClient, "create_experiment") as create_experiment_mock,
):
mlflow.set_experiment("my_exp")
get_experiment_by_name_mock.assert_called_once()
create_experiment_mock.assert_not_called()
def test_start_run_context_manager():
with start_run() as first_run:
first_uuid = first_run.info.run_id
# Check that start_run() causes the run information to be persisted in the store
persisted_run = tracking.MlflowClient().get_run(first_uuid)
assert persisted_run is not None
assert persisted_run.info == first_run.info
finished_run = tracking.MlflowClient().get_run(first_uuid)
assert finished_run.info.status == RunStatus.to_string(RunStatus.FINISHED)
# Launch a separate run that fails, verify the run status is FAILED and the run UUID is
# different
with pytest.raises(Exception, match="Failing run!"):
with start_run() as second_run:
raise Exception("Failing run!")
second_run_id = second_run.info.run_id
assert second_run_id != first_uuid
finished_run2 = tracking.MlflowClient().get_run(second_run_id)
assert finished_run2.info.status == RunStatus.to_string(RunStatus.FAILED)
def test_start_and_end_run():
# Use the start_run() and end_run() APIs without a `with` block, verify they work.
with start_run() as active_run:
mlflow.log_metric("name_1", 25)
finished_run = tracking.MlflowClient().get_run(active_run.info.run_id)
# Validate metrics
assert len(finished_run.data.metrics) == 1
assert finished_run.data.metrics["name_1"] == 25
def test_metric_timestamp():
with mlflow.start_run() as active_run:
mlflow.log_metric("name_1", 25)
mlflow.log_metric("name_1", 30)
run_id = active_run.info.run_id
# Check that metric timestamps are between run start and finish
client = MlflowClient()
history = client.get_metric_history(run_id, "name_1")
finished_run = client.get_run(run_id)
assert len(history) == 2
assert all(
m.timestamp >= finished_run.info.start_time and m.timestamp <= finished_run.info.end_time
for m in history
)
def test_log_batch():
expected_metrics = {"metric-key0": 1.0, "metric-key1": 4.0}
expected_params = {
"param-key0": "param-val0",
"param-key1": 123,
"param-key2": None,
}
exact_expected_tags = {"tag-key0": "tag-val0", "tag-key1": "tag-val1"}
approx_expected_tags = {
MLFLOW_USER,
MLFLOW_SOURCE_NAME,
MLFLOW_SOURCE_TYPE,
MLFLOW_RUN_NAME,
}
t = get_current_time_millis()
sorted_expected_metrics = sorted(expected_metrics.items(), key=lambda kv: kv[0])
metrics = [
Metric(key=key, value=value, timestamp=t, step=i)
for i, (key, value) in enumerate(sorted_expected_metrics)
]
params = [Param(key=key, value=value) for key, value in expected_params.items()]
tags = [RunTag(key=key, value=value) for key, value in exact_expected_tags.items()]
with start_run() as active_run:
run_id = active_run.info.run_id
MlflowClient().log_batch(run_id=run_id, metrics=metrics, params=params, tags=tags)
client = tracking.MlflowClient()
finished_run = client.get_run(run_id)
# Validate metrics
assert len(finished_run.data.metrics) == 2
for key, value in finished_run.data.metrics.items():
assert expected_metrics[key] == value
metric_history0 = client.get_metric_history(run_id, "metric-key0")
assert {(m.value, m.timestamp, m.step) for m in metric_history0} == {(1.0, t, 0)}
metric_history1 = client.get_metric_history(run_id, "metric-key1")
assert {(m.value, m.timestamp, m.step) for m in metric_history1} == {(4.0, t, 1)}
# Validate tags (for automatically-set tags)
assert len(finished_run.data.tags) == len(exact_expected_tags) + len(approx_expected_tags)
for tag_key, tag_value in finished_run.data.tags.items():
if tag_key in approx_expected_tags:
pass
else:
assert exact_expected_tags[tag_key] == tag_value
# Validate params
assert finished_run.data.params == {k: str(v) for k, v in expected_params.items()}
# test that log_batch works with fewer params
new_tags = {"1": "2", "3": "4", "5": "6"}
tags = [RunTag(key=key, value=value) for key, value in new_tags.items()]
client.log_batch(run_id=run_id, tags=tags)
finished_run_2 = client.get_run(run_id)
# Validate tags (for automatically-set tags)
assert len(finished_run_2.data.tags) == len(finished_run.data.tags) + 3
for tag_key, tag_value in finished_run_2.data.tags.items():
if tag_key in new_tags:
assert new_tags[tag_key] == tag_value
def test_log_batch_with_many_elements():
num_metrics = MAX_METRICS_PER_BATCH * 2
num_params = num_tags = MAX_PARAMS_TAGS_PER_BATCH * 2
expected_metrics = {f"metric-key{i}": float(i) for i in range(num_metrics)}
expected_params = {f"param-key{i}": f"param-val{i}" for i in range(num_params)}
exact_expected_tags = {f"tag-key{i}": f"tag-val{i}" for i in range(num_tags)}
t = get_current_time_millis()
sorted_expected_metrics = sorted(expected_metrics.items(), key=lambda kv: kv[1])
metrics = [
Metric(key=key, value=value, timestamp=t, step=i)
for i, (key, value) in enumerate(sorted_expected_metrics)
]
params = [Param(key=key, value=value) for key, value in expected_params.items()]
tags = [RunTag(key=key, value=value) for key, value in exact_expected_tags.items()]
with start_run() as active_run:
run_id = active_run.info.run_id
mlflow.tracking.MlflowClient().log_batch(
run_id=run_id, metrics=metrics, params=params, tags=tags
)
client = tracking.MlflowClient()
finished_run = client.get_run(run_id)
# Validate metrics
assert expected_metrics == finished_run.data.metrics
for i in range(num_metrics):
metric_history = client.get_metric_history(run_id, f"metric-key{i}")
assert {(m.value, m.timestamp, m.step) for m in metric_history} == {(float(i), t, i)}
# Validate tags
logged_tags = finished_run.data.tags
for tag_key, tag_value in exact_expected_tags.items():
assert logged_tags[tag_key] == tag_value
# Validate params
assert finished_run.data.params == expected_params
def test_log_metric():
with start_run() as active_run, mock.patch("time.time", return_value=123):
run_id = active_run.info.run_id
mlflow.log_metric("name_1", 25)
mlflow.log_metric("name_2", -3)
mlflow.log_metric("name_1", 30, 5)
mlflow.log_metric("name_1", 40, -2)
mlflow.log_metric("nested/nested/name", 40)
finished_run = tracking.MlflowClient().get_run(run_id)
# Validate metrics
assert len(finished_run.data.metrics) == 3
expected_pairs = {"name_1": 30, "name_2": -3, "nested/nested/name": 40}
for key, value in finished_run.data.metrics.items():
assert expected_pairs[key] == value
client = tracking.MlflowClient()
metric_history_name1 = client.get_metric_history(run_id, "name_1")
assert {(m.value, m.timestamp, m.step) for m in metric_history_name1} == {
(25, 123 * 1000, 0),
(30, 123 * 1000, 5),
(40, 123 * 1000, -2),
}
metric_history_name2 = client.get_metric_history(run_id, "name_2")
assert {(m.value, m.timestamp, m.step) for m in metric_history_name2} == {(-3, 123 * 1000, 0)}
def test_log_metrics_uses_millisecond_timestamp_resolution_fluent():
with start_run() as active_run, mock.patch("time.time") as time_mock:
time_mock.side_effect = lambda: 123
mlflow.log_metrics({"name_1": 25, "name_2": -3})
mlflow.log_metrics({"name_1": 30})
mlflow.log_metrics({"name_1": 40})
run_id = active_run.info.run_id
client = tracking.MlflowClient()
metric_history_name1 = client.get_metric_history(run_id, "name_1")
assert {(m.value, m.timestamp) for m in metric_history_name1} == {
(25, 123 * 1000),
(30, 123 * 1000),
(40, 123 * 1000),
}
metric_history_name2 = client.get_metric_history(run_id, "name_2")
assert {(m.value, m.timestamp) for m in metric_history_name2} == {(-3, 123 * 1000)}
def test_log_metrics_uses_millisecond_timestamp_resolution_client():
with start_run() as active_run, mock.patch("time.time") as time_mock:
time_mock.side_effect = lambda: 123
mlflow_client = tracking.MlflowClient()
run_id = active_run.info.run_id
mlflow_client.log_metric(run_id=run_id, key="name_1", value=25)
mlflow_client.log_metric(run_id=run_id, key="name_2", value=-3)
mlflow_client.log_metric(run_id=run_id, key="name_1", value=30)
mlflow_client.log_metric(run_id=run_id, key="name_1", value=40)
metric_history_name1 = mlflow_client.get_metric_history(run_id, "name_1")
assert {(m.value, m.timestamp) for m in metric_history_name1} == {
(25, 123 * 1000),
(30, 123 * 1000),
(40, 123 * 1000),
}
metric_history_name2 = mlflow_client.get_metric_history(run_id, "name_2")
assert {(m.value, m.timestamp) for m in metric_history_name2} == {(-3, 123 * 1000)}
@pytest.mark.parametrize("step_kwarg", [None, -10, 5])
def test_log_metrics_uses_common_timestamp_and_step_per_invocation(step_kwarg):
expected_metrics = {"name_1": 30, "name_2": -3, "nested/nested/name": 40}
with start_run() as active_run:
run_id = active_run.info.run_id
mlflow.log_metrics(expected_metrics, step=step_kwarg)
finished_run = tracking.MlflowClient().get_run(run_id)
# Validate metric key/values match what we expect, and that all metrics have the same timestamp
assert len(finished_run.data.metrics) == len(expected_metrics)
for key, value in finished_run.data.metrics.items():
assert expected_metrics[key] == value
common_timestamp = finished_run.data._metric_objs[0].timestamp
expected_step = step_kwarg if step_kwarg is not None else 0
for metric_obj in finished_run.data._metric_objs:
assert metric_obj.timestamp == common_timestamp
assert metric_obj.step == expected_step
@pytest.fixture
def get_store_mock():
with mock.patch("mlflow.store.file_store.FileStore.log_batch") as _get_store_mock:
yield _get_store_mock
def test_set_tags():
exact_expected_tags = {"name_1": "c", "name_2": "b", "nested/nested/name": 5}
approx_expected_tags = {
MLFLOW_USER,
MLFLOW_SOURCE_NAME,
MLFLOW_SOURCE_TYPE,
MLFLOW_RUN_NAME,
}
with start_run() as active_run:
run_id = active_run.info.run_id
mlflow.set_tags(exact_expected_tags)
finished_run = tracking.MlflowClient().get_run(run_id)
# Validate tags
assert len(finished_run.data.tags) == len(exact_expected_tags) + len(approx_expected_tags)
for tag_key, tag_val in finished_run.data.tags.items():
if tag_key in approx_expected_tags:
pass
else:
assert str(exact_expected_tags[tag_key]) == tag_val
def test_log_metric_validation():
with start_run() as active_run:
run_id = active_run.info.run_id
with pytest.raises(
MlflowException,
match="Invalid value \"apple\" for parameter 'value' supplied",
) as e:
mlflow.log_metric("name_1", "apple")
assert e.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
finished_run = tracking.MlflowClient().get_run(run_id)
assert len(finished_run.data.metrics) == 0
def test_log_param():
with start_run() as active_run:
run_id = active_run.info.run_id
assert mlflow.log_param("name_1", "a") == "a"
assert mlflow.log_param("name_2", "b") == "b"
assert mlflow.log_param("nested/nested/name", 5) == 5
finished_run = tracking.MlflowClient().get_run(run_id)
# Validate params
assert finished_run.data.params == {
"name_1": "a",
"name_2": "b",
"nested/nested/name": "5",
}
def test_log_params():
expected_params = {"name_1": "c", "name_2": "b", "nested/nested/name": 5}
with start_run() as active_run:
run_id = active_run.info.run_id
mlflow.log_params(expected_params)
finished_run = tracking.MlflowClient().get_run(run_id)
# Validate params
assert finished_run.data.params == {
"name_1": "c",
"name_2": "b",
"nested/nested/name": "5",
}
def test_log_params_duplicate_keys_raises():
params = {"a": "1", "b": "2"}
with start_run() as active_run:
run_id = active_run.info.run_id
mlflow.log_params(params)
with pytest.raises(
expected_exception=MlflowException,
match=r"Changing param values is not allowed. Param with key=",
) as e:
mlflow.log_param("a", "3")
assert e.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
finished_run = tracking.MlflowClient().get_run(run_id)
assert finished_run.data.params == params
@pytest.mark.skipif(is_windows(), reason="Windows do not support colon in params and metrics")
def test_param_metric_with_colon():
with start_run() as active_run:
run_id = active_run.info.run_id
mlflow.log_param("a:b", 3)
mlflow.log_metric("c:d", 4)
finished_run = tracking.MlflowClient().get_run(run_id)
# Validate param
assert len(finished_run.data.params) == 1
assert finished_run.data.params == {"a:b": "3"}
# Validate metric
assert len(finished_run.data.metrics) == 1
assert finished_run.data.metrics["c:d"] == 4
def test_log_batch_duplicate_entries_raises():
with start_run() as active_run:
run_id = active_run.info.run_id
with pytest.raises(
MlflowException, match=r"Duplicate parameter keys have been submitted."
) as e:
tracking.MlflowClient().log_batch(
run_id=run_id, params=[Param("a", "1"), Param("a", "2")]
)
assert e.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
def test_log_batch_validates_entity_names_and_values():
with start_run() as active_run:
run_id = active_run.info.run_id
metrics = [Metric(key="../bad/metric/name", value=0.3, timestamp=3, step=0)]
with pytest.raises(
MlflowException,
match=r"Invalid value \"../bad/metric/name\" for parameter \'metrics\[0\].name\'",
) as e:
tracking.MlflowClient().log_batch(run_id, metrics=metrics)
assert e.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
metrics = [Metric(key="ok-name", value="non-numerical-value", timestamp=3, step=0)]
with pytest.raises(
MlflowException,
match=r"Invalid value \"non-numerical-value\" "
+ r"for parameter \'metrics\[0\].value\' supplied",
) as e:
tracking.MlflowClient().log_batch(run_id, metrics=metrics)
assert e.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
metrics = [Metric(key="ok-name", value=0.3, timestamp="non-numerical-timestamp", step=0)]
with pytest.raises(
MlflowException,
match=r"Invalid value \"non-numerical-timestamp\" for "
+ r"parameter \'metrics\[0\].timestamp\' supplied",
) as e:
tracking.MlflowClient().log_batch(run_id, metrics=metrics)
assert e.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
params = [Param(key="../bad/param/name", value="my-val")]
with pytest.raises(
MlflowException,
match=r"Invalid value \"../bad/param/name\" for parameter \'params\[0\].key\' supplied",
) as e:
tracking.MlflowClient().log_batch(run_id, params=params)
assert e.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
tags = [Param(key="../bad/tag/name", value="my-val")]
with pytest.raises(
MlflowException,
match=r"Invalid value \"../bad/tag/name\" for parameter \'tags\[0\].key\' supplied",
) as e:
tracking.MlflowClient().log_batch(run_id, tags=tags)
assert e.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
metrics = [Metric(key=None, value=42.0, timestamp=4, step=1)]
with pytest.raises(
MlflowException,
match="Metric name cannot be None. A key name must be provided.",
) as e:
tracking.MlflowClient().log_batch(run_id, metrics=metrics)
assert e.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
def test_log_artifact_with_dirs(tmp_path):
# Test log artifact with a directory
art_dir = tmp_path / "parent"
art_dir.mkdir()
file0 = art_dir.joinpath("file0")
file0.write_text("something")
file1 = art_dir.joinpath("file1")
file1.write_text("something")
sub_dir = art_dir / "child"
sub_dir.mkdir()
with start_run():
artifact_uri = mlflow.get_artifact_uri()
run_artifact_dir = local_file_uri_to_path(artifact_uri)
mlflow.log_artifact(str(art_dir))
base = os.path.basename(str(art_dir))
assert os.listdir(run_artifact_dir) == [base]
assert set(os.listdir(os.path.join(run_artifact_dir, base))) == {
"child",
"file0",
"file1",
}
with open(os.path.join(run_artifact_dir, base, "file0")) as f:
assert f.read() == "something"
# Test log artifact with directory and specified parent folder
art_dir = tmp_path / "dir"
art_dir.mkdir()
with start_run():
artifact_uri = mlflow.get_artifact_uri()
run_artifact_dir = local_file_uri_to_path(artifact_uri)
mlflow.log_artifact(str(art_dir), "some_parent")
assert os.listdir(run_artifact_dir) == [os.path.basename("some_parent")]
assert os.listdir(os.path.join(run_artifact_dir, "some_parent")) == [
os.path.basename(str(art_dir))
]
sub_dir = art_dir.joinpath("another_dir")
sub_dir.mkdir()
with start_run():
artifact_uri = mlflow.get_artifact_uri()
run_artifact_dir = local_file_uri_to_path(artifact_uri)
mlflow.log_artifact(str(art_dir), "parent/and_child")
assert os.listdir(os.path.join(run_artifact_dir, "parent", "and_child")) == [
os.path.basename(str(art_dir))
]
assert set(
os.listdir(
os.path.join(
run_artifact_dir,
"parent",
"and_child",
os.path.basename(str(art_dir)),
)
)
) == {os.path.basename(str(sub_dir))}
def test_log_artifact(tmp_path):
# Create artifacts
artifact_dir = tmp_path.joinpath("artifact_dir")
artifact_dir.mkdir()
path0 = artifact_dir.joinpath("file0")
path1 = artifact_dir.joinpath("file1")
path0.write_text("0")
path1.write_text("1")
# Log an artifact, verify it exists in the directory returned by get_artifact_uri
# after the run finishes
artifact_parent_dirs = ["some_parent_dir", None]
for parent_dir in artifact_parent_dirs:
with start_run():
artifact_uri = mlflow.get_artifact_uri()
run_artifact_dir = local_file_uri_to_path(artifact_uri)
mlflow.log_artifact(path0, parent_dir)
expected_dir = (
os.path.join(run_artifact_dir, parent_dir)
if parent_dir is not None
else run_artifact_dir
)
assert os.listdir(expected_dir) == [os.path.basename(path0)]
logged_artifact_path = os.path.join(expected_dir, path0)
assert filecmp.cmp(logged_artifact_path, path0, shallow=False)
# Log multiple artifacts, verify they exist in the directory returned by get_artifact_uri
for parent_dir in artifact_parent_dirs:
with start_run():
artifact_uri = mlflow.get_artifact_uri()
run_artifact_dir = local_file_uri_to_path(artifact_uri)
mlflow.log_artifacts(artifact_dir, parent_dir)
# Check that the logged artifacts match
expected_artifact_output_dir = (
os.path.join(run_artifact_dir, parent_dir)
if parent_dir is not None
else run_artifact_dir
)
dir_comparison = filecmp.dircmp(artifact_dir, expected_artifact_output_dir)
assert len(dir_comparison.left_only) == 0
assert len(dir_comparison.right_only) == 0
assert len(dir_comparison.diff_files) == 0
assert len(dir_comparison.funny_files) == 0
@pytest.mark.parametrize("subdir", [None, ".", "dir", "dir1/dir2", "dir/.."])
def test_log_text(subdir):
filename = "file.txt"
text = "a"
artifact_file = filename if subdir is None else posixpath.join(subdir, filename)
with mlflow.start_run():
mlflow.log_text(text, artifact_file)
artifact_path = None if subdir is None else posixpath.normpath(subdir)
artifact_uri = mlflow.get_artifact_uri(artifact_path)
run_artifact_dir = local_file_uri_to_path(artifact_uri)
assert os.listdir(run_artifact_dir) == [filename]
filepath = os.path.join(run_artifact_dir, filename)
with open(filepath) as f:
assert f.read() == text
@pytest.mark.parametrize("subdir", [None, ".", "dir", "dir1/dir2", "dir/.."])
@pytest.mark.parametrize("extension", [".json", ".yml", ".yaml", ".txt", ""])
def test_log_dict(subdir, extension):
dictionary = {"k": "v"}
filename = "data" + extension
artifact_file = filename if subdir is None else posixpath.join(subdir, filename)
with mlflow.start_run():
mlflow.log_dict(dictionary, artifact_file)
artifact_path = None if subdir is None else posixpath.normpath(subdir)
artifact_uri = mlflow.get_artifact_uri(artifact_path)
run_artifact_dir = local_file_uri_to_path(artifact_uri)
assert os.listdir(run_artifact_dir) == [filename]
filepath = os.path.join(run_artifact_dir, filename)
extension = os.path.splitext(filename)[1]
with open(filepath) as f:
loaded = (
# Specify `Loader` to suppress the following deprecation warning:
# https://github.com/yaml/pyyaml/wiki/PyYAML-yaml.load(input)-Deprecation
yaml.load(f, Loader=yaml.SafeLoader)
if (extension in [".yml", ".yaml"])
else json.load(f)
)
assert loaded == dictionary
@pytest.mark.parametrize("subdir", [None, ".", "dir", "dir1/dir2", "dir/.."])
def test_log_stream_bytes(subdir):
filename = "file.bin"
content = b"binary content"
artifact_file = filename if subdir is None else posixpath.join(subdir, filename)
with mlflow.start_run():
stream = io.BytesIO(content)
mlflow.log_stream(stream, artifact_file)
artifact_path = None if subdir is None else posixpath.normpath(subdir)
artifact_uri = mlflow.get_artifact_uri(artifact_path)
run_artifact_dir = pathlib.Path(local_file_uri_to_path(artifact_uri))
assert list(run_artifact_dir.iterdir()) == [run_artifact_dir / filename]
assert (run_artifact_dir / filename).read_bytes() == content
def test_log_stream_empty():
with mlflow.start_run():
artifact_uri = mlflow.get_artifact_uri()
run_artifact_dir = pathlib.Path(local_file_uri_to_path(artifact_uri))
stream = io.BytesIO(b"")
mlflow.log_stream(stream, "empty.bin")
assert (run_artifact_dir / "empty.bin").read_bytes() == b""
def test_log_stream_large_content():
with mlflow.start_run():
# Large binary content (larger than chunk size of 8192)
large_content = b"x" * 100000
stream = io.BytesIO(large_content)
mlflow.log_stream(stream, "large.bin")
artifact_uri = mlflow.get_artifact_uri()
run_artifact_dir = pathlib.Path(local_file_uri_to_path(artifact_uri))
assert (run_artifact_dir / "large.bin").read_bytes() == large_content
def test_with_startrun():
run_id = None
t0 = get_current_time_millis()
with mlflow.start_run() as active_run:
assert mlflow.active_run() == active_run
run_id = active_run.info.run_id
t1 = get_current_time_millis()
run_info = mlflow.tracking._get_store().get_run(run_id).info
assert run_info.status == "FINISHED"
assert t0 <= run_info.end_time
assert run_info.end_time <= t1
assert mlflow.active_run() is None
def test_parent_create_run(monkeypatch):
with mlflow.start_run() as parent_run:
parent_run_id = parent_run.info.run_id
monkeypatch.setenv(MLFLOW_RUN_ID.name, parent_run_id)
with mlflow.start_run() as parent_run:
assert parent_run.info.run_id == parent_run_id
with pytest.raises(Exception, match="To start a nested run"):
mlflow.start_run()
with mlflow.start_run(nested=True) as child_run:
assert child_run.info.run_id != parent_run_id
with mlflow.start_run(nested=True) as grand_child_run:
pass
def verify_has_parent_id_tag(child_id, expected_parent_id):
tags = tracking.MlflowClient().get_run(child_id).data.tags
assert tags[MLFLOW_PARENT_RUN_ID] == expected_parent_id
verify_has_parent_id_tag(child_run.info.run_id, parent_run.info.run_id)
verify_has_parent_id_tag(grand_child_run.info.run_id, child_run.info.run_id)
assert mlflow.active_run() is None
def test_start_deleted_run():
run_id = None
with mlflow.start_run() as active_run:
run_id = active_run.info.run_id
tracking.MlflowClient().delete_run(run_id)
with pytest.raises(MlflowException, match="because it is in the deleted state."):
with mlflow.start_run(run_id=run_id):
pass
assert mlflow.active_run() is None
@pytest.mark.usefixtures("reset_active_experiment")
def test_start_run_exp_id_0():
mlflow.set_experiment("some-experiment")
# Create a run and verify that the current active experiment is the one we just set
with mlflow.start_run() as active_run:
exp_id = active_run.info.experiment_id
assert exp_id != FileStore.DEFAULT_EXPERIMENT_ID
assert MlflowClient().get_experiment(exp_id).name == "some-experiment"
# Set experiment ID to 0 when creating a run, verify that the specified experiment ID is honored
with mlflow.start_run(experiment_id=0) as active_run:
assert active_run.info.experiment_id == FileStore.DEFAULT_EXPERIMENT_ID
def test_get_artifact_uri_with_artifact_path_unspecified_returns_artifact_root_dir():
with mlflow.start_run() as active_run:
assert mlflow.get_artifact_uri(artifact_path=None) == active_run.info.artifact_uri
def test_get_artifact_uri_uses_currently_active_run_id():
artifact_path = "artifact"
with mlflow.start_run() as active_run:
assert mlflow.get_artifact_uri(
artifact_path=artifact_path
) == tracking.artifact_utils.get_artifact_uri(
run_id=active_run.info.run_id, artifact_path=artifact_path
)
def _assert_get_artifact_uri_appends_to_uri_path_component_correctly(
artifact_location, expected_uri_format
):
client = MlflowClient()
client.create_experiment("get-artifact-uri-test", artifact_location=artifact_location)
mlflow.set_experiment("get-artifact-uri-test")
with mlflow.start_run():
run_id = mlflow.active_run().info.run_id
for artifact_path in ["path/to/artifact", "/artifact/path", "arty.txt"]:
artifact_uri = mlflow.get_artifact_uri(artifact_path)
assert artifact_uri == tracking.artifact_utils.get_artifact_uri(run_id, artifact_path)
assert artifact_uri == expected_uri_format.format(
run_id=run_id,
path=artifact_path.lstrip("/"),
drive=pathlib.Path.cwd().drive,
)
@pytest.mark.parametrize(
("artifact_location", "expected_uri_format"),
[
(
"mysql://user:password@host:port/dbname?driver=mydriver",
"mysql://user:password@host:port/dbname/{run_id}/artifacts/{path}?driver=mydriver",
),
(
"mysql+driver://user:pass@host:port/dbname/subpath/#fragment",
"mysql+driver://user:pass@host:port/dbname/subpath/{run_id}/artifacts/{path}#fragment",
),
(
"s3://bucketname/rootpath",
"s3://bucketname/rootpath/{run_id}/artifacts/{path}",
),
],
)
def test_get_artifact_uri_appends_to_uri_path_component_correctly(
artifact_location, expected_uri_format
):
_assert_get_artifact_uri_appends_to_uri_path_component_correctly(
artifact_location, expected_uri_format
)
@pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
def test_get_artifact_uri_appends_to_local_path_component_correctly_on_windows():
_assert_get_artifact_uri_appends_to_uri_path_component_correctly(
"/dirname/rootpa#th?",
"file:///{drive}/dirname/rootpa/{run_id}/artifacts/{path}#th?",
)
@pytest.mark.skipif(is_windows(), reason="This test fails on Windows")
def test_get_artifact_uri_appends_to_local_path_component_correctly():
_assert_get_artifact_uri_appends_to_uri_path_component_correctly(
"/dirname/rootpa#th?", "{drive}/dirname/rootpa#th?/{run_id}/artifacts/{path}"
)
@pytest.mark.usefixtures("reset_active_experiment")
def test_search_runs():
mlflow.set_experiment("exp-for-search")
# Create a run and verify that the current active experiment is the one we just set
logged_runs = {}
with mlflow.start_run() as active_run:
logged_runs["first"] = active_run.info.run_id
mlflow.log_metric("m1", 0.001)
mlflow.log_metric("m2", 0.002)
mlflow.log_metric("m1", 0.002)
mlflow.log_param("p1", "a")
mlflow.set_tag("t1", "first-tag-val")
with mlflow.start_run() as active_run:
logged_runs["second"] = active_run.info.run_id
mlflow.log_metric("m1", 0.008)
mlflow.log_param("p2", "aa")
mlflow.set_tag("t2", "second-tag-val")
def verify_runs(runs, expected_set):
assert {r.info.run_id for r in runs} == {logged_runs[r] for r in expected_set}
experiment_id = MlflowClient().get_experiment_by_name("exp-for-search").experiment_id
# 2 runs in this experiment
assert len(MlflowClient().search_runs([experiment_id], run_view_type=ViewType.ACTIVE_ONLY)) == 2
# 2 runs that have metric "m1" > 0.001
runs = MlflowClient().search_runs([experiment_id], "metrics.m1 > 0.0001")
verify_runs(runs, ["first", "second"])
# 1 run with has metric "m1" > 0.002
runs = MlflowClient().search_runs([experiment_id], "metrics.m1 > 0.002")
verify_runs(runs, ["second"])
# no runs with metric "m1" > 0.1
runs = MlflowClient().search_runs([experiment_id], "metrics.m1 > 0.1")
verify_runs(runs, [])
# 1 run with metric "m2" > 0
runs = MlflowClient().search_runs([experiment_id], "metrics.m2 > 0")
verify_runs(runs, ["first"])
# 1 run each with param "p1" and "p2"
runs = MlflowClient().search_runs([experiment_id], "params.p1 = 'a'", ViewType.ALL)
verify_runs(runs, ["first"])
runs = MlflowClient().search_runs([experiment_id], "params.p2 != 'a'", ViewType.ALL)
verify_runs(runs, ["second"])
runs = MlflowClient().search_runs([experiment_id], "params.p2 = 'aa'", ViewType.ALL)
verify_runs(runs, ["second"])
# 1 run each with tag "t1" and "t2"
runs = MlflowClient().search_runs([experiment_id], "tags.t1 = 'first-tag-val'", ViewType.ALL)
verify_runs(runs, ["first"])
runs = MlflowClient().search_runs([experiment_id], "tags.t2 != 'qwerty'", ViewType.ALL)
verify_runs(runs, ["second"])
runs = MlflowClient().search_runs([experiment_id], "tags.t2 = 'second-tag-val'", ViewType.ALL)
verify_runs(runs, ["second"])
# delete "first" run
MlflowClient().delete_run(logged_runs["first"])
runs = MlflowClient().search_runs([experiment_id], "params.p1 = 'a'", ViewType.ALL)
verify_runs(runs, ["first"])
runs = MlflowClient().search_runs([experiment_id], "params.p1 = 'a'", ViewType.DELETED_ONLY)
verify_runs(runs, ["first"])
runs = MlflowClient().search_runs([experiment_id], "params.p1 = 'a'", ViewType.ACTIVE_ONLY)
verify_runs(runs, [])
@pytest.mark.usefixtures("reset_active_experiment")
def test_search_runs_multiple_experiments():
experiment_ids = [mlflow.create_experiment(f"exp__{exp_id}") for exp_id in range(1, 4)]
for eid in experiment_ids:
with mlflow.start_run(experiment_id=eid):
mlflow.log_metric("m0", 1)
mlflow.log_metric(f"m_{eid}", 2)
assert len(MlflowClient().search_runs(experiment_ids, "metrics.m0 > 0", ViewType.ALL)) == 3
assert len(MlflowClient().search_runs(experiment_ids, "metrics.m_1 > 0", ViewType.ALL)) == 1
assert len(MlflowClient().search_runs(experiment_ids, "metrics.m_2 = 2", ViewType.ALL)) == 1
assert len(MlflowClient().search_runs(experiment_ids, "metrics.m_3 < 4", ViewType.ALL)) == 1
def read_data(artifact_path):
import pandas as pd
if artifact_path.endswith(".json"):
return pd.read_json(artifact_path, orient="split")
if artifact_path.endswith(".parquet"):
return pd.read_parquet(artifact_path)
raise ValueError(f"Unsupported file type in {artifact_path}. Expected .json or .parquet")
@pytest.mark.skipif(
"MLFLOW_SKINNY" in os.environ,
reason="Skinny client does not support the np or pandas dependencies",
)
@pytest.mark.parametrize("file_type", ["json", "parquet"])
def test_log_table(file_type):
import pandas as pd
table_dict = {
"inputs": ["What is MLflow?", "What is Databricks?"],
"outputs": ["MLflow is ...", "Databricks is ..."],
"toxicity": [0.0, 0.0],
}
artifact_file = f"qabot_eval_results.{file_type}"
TAG_NAME = "mlflow.loggedArtifacts"
run_id = None
with pytest.raises(
MlflowException, match="data must be a pandas.DataFrame or a dictionary"
) as e:
with mlflow.start_run() as run:
# Log the incorrect data format as a table
mlflow.log_table(data="incorrect-data-format", artifact_file=artifact_file)
assert e.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
with mlflow.start_run() as run:
# Log the dictionary as a table
mlflow.log_table(data=table_dict, artifact_file=artifact_file)
run_id = run.info.run_id
run = mlflow.get_run(run_id)
artifact_path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path=artifact_file)
table_data = read_data(artifact_path)
assert table_data.shape[0] == 2
assert table_data.shape[1] == 3
# Get the current value of the tag
current_tag_value = json.loads(run.data.tags.get(TAG_NAME, "[]"))
assert {"path": artifact_file, "type": "table"} in current_tag_value
assert len(current_tag_value) == 1
table_df = pd.DataFrame.from_dict(table_dict)
with mlflow.start_run(run_id=run_id):
# Log the dataframe as a table
mlflow.log_table(data=table_df, artifact_file=artifact_file)
run = mlflow.get_run(run_id)
artifact_path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path=artifact_file)
table_data = read_data(artifact_path)
assert table_data.shape[0] == 4
assert table_data.shape[1] == 3
# Get the current value of the tag
current_tag_value = json.loads(run.data.tags.get(TAG_NAME, "[]"))
assert {"path": artifact_file, "type": "table"} in current_tag_value
assert len(current_tag_value) == 1
artifact_file_new = f"qabot_eval_results_new.{file_type}"
with mlflow.start_run(run_id=run_id):
# Log the dataframe as a table to new artifact file
mlflow.log_table(data=table_df, artifact_file=artifact_file_new)
run = mlflow.get_run(run_id)
artifact_path = mlflow.artifacts.download_artifacts(
run_id=run_id, artifact_path=artifact_file_new
)
table_data = read_data(artifact_path)
assert table_data.shape[0] == 2
assert table_data.shape[1] == 3
# Get the current value of the tag
current_tag_value = json.loads(run.data.tags.get(TAG_NAME, "[]"))
assert {"path": artifact_file_new, "type": "table"} in current_tag_value
assert len(current_tag_value) == 2
@pytest.mark.skipif(
"MLFLOW_SKINNY" in os.environ,
reason="Skinny client does not support the np or pandas dependencies",
)
@pytest.mark.parametrize("file_type", ["json", "parquet"])
def test_log_table_with_subdirectory(file_type):
import pandas as pd
table_dict = {
"inputs": ["What is MLflow?", "What is Databricks?"],
"outputs": ["MLflow is ...", "Databricks is ..."],
"toxicity": [0.0, 0.0],
}
artifact_file = f"dir/foo.{file_type}"
TAG_NAME = "mlflow.loggedArtifacts"
run_id = None
with mlflow.start_run() as run:
# Log the dictionary as a table
mlflow.log_table(data=table_dict, artifact_file=artifact_file)
run_id = run.info.run_id
run = mlflow.get_run(run_id)
artifact_path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path=artifact_file)
table_data = read_data(artifact_path)
assert table_data.shape[0] == 2
assert table_data.shape[1] == 3
# Get the current value of the tag
current_tag_value = json.loads(run.data.tags.get(TAG_NAME, "[]"))
assert {"path": artifact_file, "type": "table"} in current_tag_value
assert len(current_tag_value) == 1
table_df = pd.DataFrame.from_dict(table_dict)
with mlflow.start_run(run_id=run_id):
# Log the dataframe as a table
mlflow.log_table(data=table_df, artifact_file=artifact_file)
run = mlflow.get_run(run_id)
artifact_path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path=artifact_file)
table_data = read_data(artifact_path)
assert table_data.shape[0] == 4
assert table_data.shape[1] == 3
# Get the current value of the tag
current_tag_value = json.loads(run.data.tags.get(TAG_NAME, "[]"))
assert {"path": artifact_file, "type": "table"} in current_tag_value
assert len(current_tag_value) == 1
@pytest.mark.skipif(
"MLFLOW_SKINNY" in os.environ,
reason="Skinny client does not support the np or pandas dependencies",
)
@pytest.mark.parametrize("file_type", ["json", "parquet"])
def test_load_table(file_type):
table_dict = {
"inputs": ["What is MLflow?", "What is Databricks?"],
"outputs": ["MLflow is ...", "Databricks is ..."],
"toxicity": [0.0, 0.0],
}
artifact_file = f"qabot_eval_results.{file_type}"
artifact_file_2 = f"qabot_eval_results_2.{file_type}"
run_id_2 = None
with mlflow.start_run() as run:
# Log the dictionary as a table
mlflow.log_table(data=table_dict, artifact_file=artifact_file)
mlflow.log_table(data=table_dict, artifact_file=artifact_file_2)
with mlflow.start_run() as run:
# Log the dictionary as a table
mlflow.log_table(data=table_dict, artifact_file=artifact_file)
run_id_2 = run.info.run_id
with mlflow.start_run() as run:
# Log the dictionary as a table
mlflow.log_table(data=table_dict, artifact_file=artifact_file)
run_id_3 = run.info.run_id
extra_columns = ["run_id", "tags.mlflow.loggedArtifacts"]
# test 1: load table with extra columns
output_df = mlflow.load_table(artifact_file=artifact_file, extra_columns=extra_columns)
assert output_df.shape[0] == 6
assert output_df.shape[1] == 5
assert output_df["run_id"].nunique() == 3
assert output_df["tags.mlflow.loggedArtifacts"].nunique() == 2
# test 2: load table with extra columns and single run_id
output_df = mlflow.load_table(
artifact_file=artifact_file, run_ids=[run_id_2], extra_columns=extra_columns
)
assert output_df.shape[0] == 2
assert output_df.shape[1] == 5
assert output_df["run_id"].nunique() == 1
assert output_df["tags.mlflow.loggedArtifacts"].nunique() == 1
# test 3: load table with extra columns and multiple run_ids
output_df = mlflow.load_table(
artifact_file=artifact_file,
run_ids=[run_id_2, run_id_3],
extra_columns=extra_columns,
)
assert output_df.shape[0] == 4
assert output_df.shape[1] == 5
assert output_df["run_id"].nunique() == 2
assert output_df["tags.mlflow.loggedArtifacts"].nunique() == 1
# test 4: load table with no extra columns and run_ids specified but different artifact file
output_df = mlflow.load_table(artifact_file=artifact_file_2)
import pandas as pd
pd.testing.assert_frame_equal(output_df, pd.DataFrame(table_dict), check_dtype=False)
# test 5: load table with no extra columns and run_ids specified
output_df = mlflow.load_table(artifact_file=artifact_file)
assert output_df.shape[0] == 6
assert output_df.shape[1] == 3
# test 6: load table with no matching results found. Error case
with pytest.raises(
MlflowException, match="No runs found with the corresponding table artifact"
):
mlflow.load_table(artifact_file=f"error_case.{file_type}")
# test 7: load table with no matching extra_column found. Error case
with pytest.raises(KeyError, match="error_column"):
mlflow.load_table(artifact_file=artifact_file, extra_columns=["error_column"])
@pytest.mark.skipif(
"MLFLOW_SKINNY" in os.environ,
reason="Skinny client does not support the np or pandas dependencies",
)
@pytest.mark.parametrize("file_type", ["json", "parquet"])
def test_log_table_with_datetime_columns(file_type):
import pandas as pd
start_time = str(datetime.now(timezone.utc))
table_dict = {
"inputs": ["What is MLflow?", "What is Databricks?"],
"outputs": ["MLflow is ...", "Databricks is ..."],
"start_time": [start_time, start_time],
}
artifact_file = f"test_time.{file_type}"
with mlflow.start_run() as run:
# Log the dictionary as a table
mlflow.log_table(data=table_dict, artifact_file=artifact_file)
run_id = run.info.run_id
artifact_path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path=artifact_file)
if file_type == "parquet":
table_data = pd.read_parquet(artifact_path)
else:
table_data = pd.read_json(artifact_path, orient="split", convert_dates=False)
assert table_data["start_time"][0] == start_time
# append the same table to the same artifact file
mlflow.log_table(data=table_dict, artifact_file=artifact_file, run_id=run_id)
artifact_path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path=artifact_file)
if file_type == "parquet":
df = pd.read_parquet(artifact_path)
else:
df = pd.read_json(artifact_path, orient="split", convert_dates=False)
assert df["start_time"][2] == start_time
@pytest.mark.skipif(
"MLFLOW_SKINNY" in os.environ,
reason="Skinny client does not support the np or pandas dependencies",
)
@pytest.mark.parametrize("file_type", ["json", "parquet"])
def test_log_table_with_image_columns(file_type):
import numpy as np
from PIL import Image
image = mlflow.Image([[1, 2, 3]])
table_dict = {
"inputs": ["What is MLflow?", "What is Databricks?"],
"outputs": ["MLflow is ...", "Databricks is ..."],
"image": [image, image],
}
artifact_file = f"test_time.{file_type}"
with mlflow.start_run() as run:
# Log the dictionary as a table
mlflow.log_table(data=table_dict, artifact_file=artifact_file)
run_id = run.info.run_id
artifact_path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path=artifact_file)
table_data = read_data(artifact_path)
assert table_data["image"][0]["type"] == "image"
image_path = mlflow.artifacts.download_artifacts(
run_id=run_id, artifact_path=table_data["image"][0]["filepath"]
)
image2 = Image.open(image_path)
assert np.abs(image.to_array() - np.array(image2)).sum() == 0
# append the same table to the same artifact file
mlflow.log_table(data=table_dict, artifact_file=artifact_file, run_id=run_id)
artifact_path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path=artifact_file)
df = read_data(artifact_path)
assert df["image"][2]["type"] == "image"
@pytest.mark.skipif(
"MLFLOW_SKINNY" in os.environ,
reason="Skinny client does not support the np or pandas dependencies",
)
@pytest.mark.parametrize("file_type", ["json", "parquet"])
def test_log_table_with_pil_image_columns(file_type):
import numpy as np
from PIL import Image
image = Image.fromarray(np.array([[1.0, 2.0, 3.0]]))
image = image.convert("RGB")
table_dict = {
"inputs": ["What is MLflow?", "What is Databricks?"],
"outputs": ["MLflow is ...", "Databricks is ..."],
"image": [image, image],
}
artifact_file = f"test_time.{file_type}"
with mlflow.start_run() as run:
# Log the dictionary as a table
mlflow.log_table(data=table_dict, artifact_file=artifact_file)
run_id = run.info.run_id
artifact_path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path=artifact_file)
table_data = read_data(artifact_path)
assert table_data["image"][0]["type"] == "image"
image_path = mlflow.artifacts.download_artifacts(
run_id=run_id, artifact_path=table_data["image"][0]["filepath"]
)
image2 = Image.open(image_path)
assert np.abs(np.array(image) - np.array(image2)).sum() == 0
# append the same table to the same artifact file
mlflow.log_table(data=table_dict, artifact_file=artifact_file, run_id=run_id)
artifact_path = mlflow.artifacts.download_artifacts(run_id=run_id, artifact_path=artifact_file)
df = read_data(artifact_path)
assert df["image"][2]["type"] == "image"
@pytest.mark.skipif(
"MLFLOW_SKINNY" in os.environ,
reason="Skinny client does not support the np or pandas dependencies",
)
@pytest.mark.parametrize("file_type", ["json", "parquet"])
def test_log_table_with_invalid_image_columns(file_type):
image = mlflow.Image([[1, 2, 3]])
table_dict = {
"inputs": ["What is MLflow?", "What is Databricks?"],
"outputs": ["MLflow is ...", "Databricks is ..."],
"image": [image, "text"],
}
artifact_file = f"test_time.{file_type}"
with pytest.raises(ValueError, match="Column `image` contains a mix of images and non-images"):
with mlflow.start_run():
# Log the dictionary as a table
mlflow.log_table(data=table_dict, artifact_file=artifact_file)
@pytest.mark.skipif(
"MLFLOW_SKINNY" in os.environ,
reason="Skinny client does not support the np or pandas dependencies",
)
@pytest.mark.parametrize("file_type", ["json", "parquet"])
def test_log_table_with_valid_image_columns(file_type):
class ImageObj:
def __init__(self):
self.size = (1, 1)
def resize(self, size):
return self
def save(self, path):
with open(path, "w+") as f:
f.write("dummy data")
image_obj = ImageObj()
image = mlflow.Image([[1, 2, 3]])
table_dict = {
"inputs": ["What is MLflow?", "What is Databricks?"],
"outputs": ["MLflow is ...", "Databricks is ..."],
"image": [image, image_obj],
}
# No error should be raised
artifact_file = f"test_time.{file_type}"
with mlflow.start_run():
# Log the dictionary as a table
mlflow.log_table(data=table_dict, artifact_file=artifact_file)
def test_set_async_logging_threadpool_size():
MLFLOW_ASYNC_LOGGING_THREADPOOL_SIZE.set(6)
assert MLFLOW_ASYNC_LOGGING_THREADPOOL_SIZE.get() == 6
with mlflow.start_run():
mlflow.log_param("key", "val", synchronous=False)
store = mlflow.tracking._get_store()
async_queue = store._async_logging_queue
assert async_queue._batch_logging_worker_threadpool._max_workers == 6
mlflow.flush_async_logging()
MLFLOW_ASYNC_LOGGING_THREADPOOL_SIZE.unset()