chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:34 +08:00
commit 4b22cfda96
9037 changed files with 2363717 additions and 0 deletions
+400
View File
@@ -0,0 +1,400 @@
import datetime
import json
import os
import sys
from unittest import mock
import numpy as np
import pandas as pd
import pytest
import scipy.sparse
import mlflow
from mlflow.exceptions import MlflowException
from mlflow.models.python_api import (
_CONTENT_TYPE_CSV,
_CONTENT_TYPE_JSON,
_serialize_input_data,
)
from mlflow.tracing.constant import TraceMetadataKey
from mlflow.utils.env_manager import CONDA, LOCAL, UV, VIRTUALENV
from tests.tracing.helper import get_traces
@pytest.mark.parametrize(
("input_data", "expected_data", "content_type"),
[
(
"x,y\n1,3\n2,4",
pd.DataFrame({"x": [1, 2], "y": [3, 4]}),
_CONTENT_TYPE_CSV,
),
(
{"a": [1]},
{"a": np.array([1])},
_CONTENT_TYPE_JSON,
),
(
1,
np.array(1),
_CONTENT_TYPE_JSON,
),
(
np.array([1, 2, 3]),
np.array([1, 2, 3]),
_CONTENT_TYPE_JSON,
),
(
scipy.sparse.csc_matrix([[1, 2], [3, 4]]),
np.array([[1, 2], [3, 4]]),
_CONTENT_TYPE_JSON,
),
(
# uLLM input, no change
{"input": "some_data"},
{"input": "some_data"},
_CONTENT_TYPE_JSON,
),
],
)
@pytest.mark.parametrize(
"env_manager",
[VIRTUALENV, UV],
)
def test_predict(input_data, expected_data, content_type, env_manager):
class TestModel(mlflow.pyfunc.PythonModel):
def predict(self, context, model_input):
if isinstance(model_input, pd.DataFrame):
assert model_input.equals(expected_data)
elif isinstance(model_input, np.ndarray):
assert np.array_equal(model_input, expected_data)
else:
assert model_input == expected_data
return {}
with mlflow.start_run():
model_info = mlflow.pyfunc.log_model(
name="model",
python_model=TestModel(),
extra_pip_requirements=["pytest"],
)
mlflow.models.predict(
model_uri=model_info.model_uri,
input_data=input_data,
content_type=content_type,
env_manager=env_manager,
)
@pytest.mark.parametrize(
"env_manager",
[VIRTUALENV, CONDA, UV],
)
def test_predict_with_pip_requirements_override(env_manager):
if env_manager == CONDA:
if sys.platform == "win32":
pytest.skip("Skipping conda tests on Windows")
class TestModel(mlflow.pyfunc.PythonModel):
def predict(self, context, model_input):
# XGBoost should be installed by pip_requirements_override
import xgboost
assert xgboost.__version__ == "1.7.3"
# Scikit-learn version should be overridden to 1.3.0 by pip_requirements_override
import sklearn
assert sklearn.__version__ == "1.3.0"
with mlflow.start_run():
model_info = mlflow.pyfunc.log_model(
name="model",
python_model=TestModel(),
extra_pip_requirements=["scikit-learn==1.3.2", "pytest"],
)
requirements_override = ["xgboost==1.7.3", "scikit-learn==1.3.0"]
if env_manager == CONDA:
# Install charset-normalizer with conda-forge to work around pip-vs-conda issue during
# CI tests. At the beginning of the CI test, it installs MLflow dependencies via pip,
# which includes charset-normalizer. Then when it runs this test case, the conda env
# is created but charset-normalizer is installed via the default channel, which is one
# major version behind the version installed via pip (as of 2024 Jan). As a result,
# Python env confuses pip and conda versions and cause errors like "ImportError: cannot
# import name 'COMMON_SAFE_ASCII_CHARACTERS' from 'charset_normalizer.constant'".
# To work around this, we install the latest cversion from the conda-forge.
# TODO: Implement better isolation approach for pip and conda environments during testing.
requirements_override.append("conda-forge::charset-normalizer")
mlflow.models.predict(
model_uri=model_info.model_uri,
input_data={"inputs": [1, 2, 3]},
content_type=_CONTENT_TYPE_JSON,
pip_requirements_override=requirements_override,
env_manager=env_manager,
)
@pytest.mark.parametrize("env_manager", [VIRTUALENV, CONDA, UV])
def test_predict_with_model_alias(env_manager):
class TestModel(mlflow.pyfunc.PythonModel):
def predict(self, context, model_input):
assert os.environ["TEST"] == "test"
return model_input
with mlflow.start_run():
mlflow.pyfunc.log_model(
name="model",
python_model=TestModel(),
registered_model_name="model_name",
)
client = mlflow.MlflowClient()
client.set_registered_model_alias("model_name", "test_alias", 1)
mlflow.models.predict(
model_uri="models:/model_name@test_alias",
input_data="abc",
env_manager=env_manager,
extra_envs={"TEST": "test"},
)
@pytest.mark.parametrize("env_manager", [VIRTUALENV, CONDA, UV])
def test_predict_with_extra_envs(env_manager):
class TestModel(mlflow.pyfunc.PythonModel):
def predict(self, context, model_input):
assert os.environ["TEST"] == "test"
return model_input
with mlflow.start_run():
model_info = mlflow.pyfunc.log_model(
name="model",
python_model=TestModel(),
)
mlflow.models.predict(
model_uri=model_info.model_uri,
input_data="abc",
content_type=_CONTENT_TYPE_JSON,
env_manager=env_manager,
extra_envs={"TEST": "test"},
)
def test_predict_with_extra_envs_errors():
class TestModel(mlflow.pyfunc.PythonModel):
def predict(self, context, model_input):
assert os.environ["TEST"] == "test"
return model_input
with mlflow.start_run():
model_info = mlflow.pyfunc.log_model(
name="model",
python_model=TestModel(),
)
with pytest.raises(
MlflowException,
match=r"Extra environment variables are only "
r"supported when env_manager is set to 'virtualenv', 'conda' or 'uv'",
):
mlflow.models.predict(
model_uri=model_info.model_uri,
input_data="abc",
content_type=_CONTENT_TYPE_JSON,
env_manager=LOCAL,
extra_envs={"TEST": "test"},
)
with pytest.raises(
MlflowException, match=r"An exception occurred while running model prediction"
):
mlflow.models.predict(
model_uri=model_info.model_uri,
input_data="abc",
content_type=_CONTENT_TYPE_JSON,
)
@pytest.fixture
def mock_backend():
mock_backend = mock.MagicMock()
with mock.patch("mlflow.models.python_api.get_flavor_backend", return_value=mock_backend):
yield mock_backend
def test_predict_with_both_input_data_and_path_raise(mock_backend):
with pytest.raises(MlflowException, match=r"Both input_data and input_path are provided"):
mlflow.models.predict(
model_uri="runs:/test/Model",
input_data={"inputs": [1, 2, 3]},
input_path="input.csv",
content_type=_CONTENT_TYPE_CSV,
)
def test_predict_invalid_content_type(mock_backend):
with pytest.raises(MlflowException, match=r"Content type must be one of"):
mlflow.models.predict(
model_uri="runs:/test/Model",
input_data={"inputs": [1, 2, 3]},
content_type="any",
)
def test_predict_with_input_none(mock_backend):
mlflow.models.predict(
model_uri="runs:/test/Model",
content_type=_CONTENT_TYPE_CSV,
)
mock_backend.predict.assert_called_once_with(
model_uri="runs:/test/Model",
input_path=None,
output_path=None,
content_type=_CONTENT_TYPE_CSV,
pip_requirements_override=None,
extra_envs=None,
)
@pytest.mark.parametrize(
("input_data", "content_type", "expected"),
[
# String (convert to serving input)
("[1, 2, 3]", _CONTENT_TYPE_JSON, '{"inputs": "[1, 2, 3]"}'),
# uLLM String (no change)
({"input": "data"}, _CONTENT_TYPE_JSON, '{"input": "data"}'),
("x,y,z\n1,2,3\n4,5,6", _CONTENT_TYPE_CSV, "x,y,z\n1,2,3\n4,5,6"),
# Bool
(True, _CONTENT_TYPE_JSON, '{"inputs": true}'),
# Int
(1, _CONTENT_TYPE_JSON, '{"inputs": 1}'),
# Float
(1.0, _CONTENT_TYPE_JSON, '{"inputs": 1.0}'),
# Datetime
(
datetime.datetime(2021, 1, 1, 0, 0, 0),
_CONTENT_TYPE_JSON,
'{"inputs": "2021-01-01T00:00:00"}',
),
# List
([1, 2, 3], _CONTENT_TYPE_CSV, "0\n1\n2\n3\n"), # a header '0' is added by pandas
([[1, 2, 3], [4, 5, 6]], _CONTENT_TYPE_CSV, "0,1,2\n1,2,3\n4,5,6\n"),
# Dict (pandas)
(
{
"x": [
1,
2,
],
"y": [3, 4],
},
_CONTENT_TYPE_CSV,
"x,y\n1,3\n2,4\n",
),
# Dict (json)
({"a": [1, 2, 3]}, _CONTENT_TYPE_JSON, '{"inputs": {"a": [1, 2, 3]}}'),
# Pandas DataFrame (csv)
(pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]}), _CONTENT_TYPE_CSV, "x,y\n1,4\n2,5\n3,6\n"),
# Pandas DataFrame (json)
(
pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]}),
_CONTENT_TYPE_JSON,
'{"dataframe_split": {"columns": ["x", "y"], "data": [[1, 4], [2, 5], [3, 6]]}}',
),
# Numpy Array
(np.array([1, 2, 3]), _CONTENT_TYPE_JSON, '{"inputs": [1, 2, 3]}'),
# CSC Matrix
(
scipy.sparse.csc_matrix([[1, 2], [3, 4]]),
_CONTENT_TYPE_JSON,
'{"inputs": [[1, 2], [3, 4]]}',
),
# CSR Matrix
(
scipy.sparse.csr_matrix([[1, 2], [3, 4]]),
_CONTENT_TYPE_JSON,
'{"inputs": [[1, 2], [3, 4]]}',
),
],
)
def test_serialize_input_data(input_data, content_type, expected):
if content_type == _CONTENT_TYPE_JSON:
assert json.loads(_serialize_input_data(input_data, content_type)) == json.loads(expected)
else:
assert _serialize_input_data(input_data, content_type) == expected
@pytest.mark.parametrize(
("input_data", "content_type"),
[
# Invalid input datatype for the content type
(1, _CONTENT_TYPE_CSV),
({1, 2, 3}, _CONTENT_TYPE_CSV),
# Invalid string
("x,y\n1,2\n3,4,5\n", _CONTENT_TYPE_CSV),
# Invalid list
([[1, 2], [3, 4], 5], _CONTENT_TYPE_CSV),
# Invalid dict (unserealizable)
({"x": 1, "y": {1, 2, 3}}, _CONTENT_TYPE_JSON),
],
)
def test_serialize_input_data_invalid_format(input_data, content_type):
with pytest.raises(MlflowException): # noqa: PT011
_serialize_input_data(input_data, content_type)
def test_predict_use_current_experiment():
class TestModel(mlflow.pyfunc.PythonModel):
@mlflow.trace
def predict(self, context, model_input: list[str]):
return model_input
exp_id = mlflow.set_experiment("test_experiment").experiment_id
client = mlflow.MlflowClient()
with mlflow.start_run():
model_info = mlflow.pyfunc.log_model(
name="model",
python_model=TestModel(),
)
assert len(client.search_traces(locations=[exp_id])) == 0
mlflow.models.predict(
model_uri=model_info.model_uri,
input_data=["a", "b", "c"],
env_manager=VIRTUALENV,
)
traces = client.search_traces(locations=[exp_id])
assert len(traces) == 1
assert json.loads(traces[0].data.request)["model_input"] == ["a", "b", "c"]
def test_predict_traces_link_to_active_model():
model = mlflow.set_active_model(name="test_model")
class TestModel(mlflow.pyfunc.PythonModel):
@mlflow.trace
def predict(self, context, model_input: list[str]):
return model_input
with mlflow.start_run():
model_info = mlflow.pyfunc.log_model(
name="model",
python_model=TestModel(),
)
traces = get_traces()
assert len(traces) == 0
mlflow.models.predict(
model_uri=model_info.model_uri,
input_data=["a", "b", "c"],
env_manager=VIRTUALENV,
)
traces = get_traces()
assert len(traces) == 1
assert traces[0].info.request_metadata[TraceMetadataKey.MODEL_ID] == model.model_id