Repository: mlflow/mlflow

Issue:
[BUG] mlflow-skinny not recognized as mlflow requirement when saving model
### Issues Policy acknowledgement

- [X] I have read and agree to submit bug reports in accordance with the [issues policy](https://www.github.com/mlflow/mlflow/blob/master/ISSUE_POLICY.md)

### Willingness to contribute

No. I cannot contribute a bug fix at this time.

### MLflow version

mlflow-skinny==1.27.0

### System information

- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Windows 10 Enterprise
- **Python version**:  3.9.14
- **yarn version, if running the dev UI**: 


### Describe the problem

When saving a model and passing a pip_requirements list with mlflow-skinny but not mlflow, the resulting requirements.txt has a first line with a mlflow (without version). An example illustrates this better. 

When running something like the code below:
```python
packages = ["mlflow-skinny==1.27.0", "other-package==1.0.0"]
mlflow.statsmodels.log_model(
        model,
        "model",
        conda_env=None,
        signature=model_signature,
        pip_requirements=packages,
    )
```

The saved requirements.txt will have the following contents:

```
mlflow
mlflow-skinny==1.27.0
other-package==1.0.0
```

This is undesired when the original environment does not have mlflow, but only mlflow-skinny for example.

I was able to track down the issue to the function [mlflow.utils.environment._contains_mlflow_requirement](https://github.com/mlflow/mlflow/blob/2e4bb67d0eb9e25b4de480bee2301274ebc96882/mlflow/utils/environment.py#L441). 

This function will return False for mlflow-skinny, which triggers the code below ([from here](https://github.com/mlflow/mlflow/blob/2e4bb67d0eb9e25b4de480bee2301274ebc96882/mlflow/utils/environment.py#L464)):

```python
    if not _contains_mlflow_requirement(pip_reqs):
        pip_reqs.insert(0, "mlflow")
```


### Tracking information

_No response_

### Code to reproduce issue

In my case I'm using the statsmodels API, but it doesn't really matter since it's calling the code below.

```python
import mlflow

default_pip_reqs = ["mlflow-skinny==1.27.0", "other-package==1.0.0"]

_, pip_reqs, _ = mlflow.utils.environment._process_pip_requirements(default_pip_reqs)
print(pip_reqs)
```

This prints:
```
['mlflow', 'mlflow-skinny==1.27.0', 'other-package==1.0.0']
```

### Stack trace

No errors in this case, so not sure what to print here. This is the output from the code above.

```python
['mlflow', 'mlflow-skinny==1.27.0', 'other-package==1.0.0']
```

### Other info / logs

_No response_

### What component(s) does this bug affect?

- [ ] `area/artifacts`: Artifact stores and artifact logging
- [ ] `area/build`: Build and test infrastructure for MLflow
- [ ] `area/docs`: MLflow documentation pages
- [ ] `area/examples`: Example code
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for Model Registry
- [X] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [ ] `area/pipelines`: Pipelines, Pipeline APIs, Pipeline configs, Pipeline Templates
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: MLflow Model server, model deployment tools, Spark UDFs
- [ ] `area/server-infra`: MLflow Tracking server backend
- [ ] `area/tracking`: Tracking Service, tracking client APIs, autologging

### What interface(s) does this bug affect?

- [ ] `area/uiux`: Front-end, user experience, plotting, JavaScript, JavaScript dev server
- [ ] `area/docker`: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
- [ ] `area/sqlalchemy`: Use of SQLAlchemy in the Tracking Service or Model Registry
- [ ] `area/windows`: Windows support

### What language(s) does this bug affect?

- [ ] `language/r`: R APIs and clients
- [ ] `language/java`: Java APIs and clients
- [ ] `language/new`: Proposals for new client languages

### What integration(s) does this bug affect?

- [ ] `integrations/azure`: Azure and Azure ML integrations
- [ ] `integrations/sagemaker`: SageMaker integrations
- [ ] `integrations/databricks`: Databricks integrations

Discussion:

Patch:
diff --git a/mlflow/utils/environment.py b/mlflow/utils/environment.py
index 4d6b455ed..fe70d0b5b 100644
--- a/mlflow/utils/environment.py
+++ b/mlflow/utils/environment.py
@@ -418,7 +418,7 @@ def _is_mlflow_requirement(requirement_string):
     try:
         # `Requirement` throws an `InvalidRequirement` exception if `requirement_string` doesn't
         # conform to PEP 508 (https://www.python.org/dev/peps/pep-0508).
-        return Requirement(requirement_string).name.lower() == "mlflow"
+        return Requirement(requirement_string).name.lower() in ["mlflow", "mlflow-skinny"]
     except InvalidRequirement:
         # A local file path or URL falls into this branch.
 


Test patch:
diff --git a/tests/utils/test_environment.py b/tests/utils/test_environment.py
index 4cb922ad2..9c407a30f 100644
--- a/tests/utils/test_environment.py
+++ b/tests/utils/test_environment.py
@@ -209,6 +209,7 @@ def test_is_mlflow_requirement():
     assert _is_mlflow_requirement("mlflow; python_version < '3.8'")
     assert _is_mlflow_requirement("mlflow @ https://github.com/mlflow/mlflow.git")
     assert _is_mlflow_requirement("mlflow @ file:///path/to/mlflow")
+    assert _is_mlflow_requirement("mlflow-skinny==1.2.3")
     assert not _is_mlflow_requirement("foo")
     # Ensure packages that look like mlflow are NOT considered as mlflow.
     assert not _is_mlflow_requirement("mlflow-foo")
@@ -219,6 +220,7 @@ def test_contains_mlflow_requirement():
     assert _contains_mlflow_requirement(["mlflow"])
     assert _contains_mlflow_requirement(["mlflow==1.2.3"])
     assert _contains_mlflow_requirement(["mlflow", "foo"])
+    assert _contains_mlflow_requirement(["mlflow-skinny"])
     assert not _contains_mlflow_requirement([])
     assert not _contains_mlflow_requirement(["foo"])
 
@@ -232,6 +234,7 @@ def test_get_pip_requirement_specifier():
     assert _get_pip_requirement_specifier("  -r reqs.txt") == " "
     assert _get_pip_requirement_specifier("mlflow==1.2.3 --hash=foo") == "mlflow==1.2.3"
     assert _get_pip_requirement_specifier("mlflow==1.2.3       --hash=foo") == "mlflow==1.2.3      "
+    assert _get_pip_requirement_specifier("mlflow-skinny==1.2 --foo=bar") == "mlflow-skinny==1.2"
 
 
 def test_process_pip_requirements(tmpdir):
@@ -310,5 +313,10 @@ def test_process_conda_env(tmpdir):
     assert reqs == ["mlflow", "a", "-c constraints.txt"]
     assert cons == ["c"]
 
+    conda_env, reqs, cons = _process_conda_env(make_conda_env(["mlflow-skinny", "a", "b"]))
+    assert _get_pip_deps(conda_env) == ["mlflow-skinny", "a", "b"]
+    assert reqs == ["mlflow-skinny", "a", "b"]
+    assert cons == []
+
     with pytest.raises(TypeError, match=r"Expected .+, but got `int`"):
         _process_conda_env(0)