Repository: mlflow/mlflow

Issue:
Two models one code name 
### Willingness to contribute

Yes. I can contribute a fix for this bug independently.

### System information

- **Have I written custom code (as opposed to using a stock example script provided in MLflow)**: custom code
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: 20.04
- **MLflow installed from (source or binary)**: source
- **MLflow version (run ``mlflow --version``)**: 1.23.1
- **Python version**: 3.9
- **yarn version, if running the dev UI**: N/A


### Describe the problem

When two mlflow models have the same name for the code packaged with them, for example: a preprocessing code name `prep_features` was packaged with two different mlflow models. If the first mlflow model is loaded, then the second one is also loaded, python will only load the `prep_features` module once. This leads to problems if `prep_features` for the second model is different from the first model.



### Tracking information

_No response_

### Code to reproduce issue

prep_features. py change 1 to 2 when creating custom_model2 with the 2m1cn.py script
```python
def preprocess_features():
    return 1 # 2 for custom_model2
```
2m1cn.py use this file to create custom_model1 and custom_model2 
```python
import mlflow.pyfunc

class CM(mlflow.pyfunc.PythonModel):
    def __init__(self):
        pass
    def predict(self, context, model_input):
        from prep_features import preprocess_features
        return preprocess_features()

# Construct and save the model
model_path = "custom_model1" # "custom_model2"
code_path = ["prep_features.py"]
custom_model = CM()
mlflow.pyfunc.save_model(path=model_path, python_model=custom_model, code_path=code_path)
```

test.py now use this file to test your created models
```python
# Evaluate the models
import mlflow.pyfunc
import pandas as pd
model_input = pd.DataFrame([range(10)])
loaded_model1 = mlflow.pyfunc.load_model("custom_model1")
print(f"Model1: {loaded_model1.predict(model_input)}")
loaded_model2 = mlflow.pyfunc.load_model("custom_model2")
print(f"Model2: {loaded_model2.predict(model_input)}")
```

The output should be 1 for custom_model1 and 2 for custom_model 2. But because of the bug the output is 1 for both.

### 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/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:

Resolving patch:
diff --git a/mlflow/utils/model_utils.py b/mlflow/utils/model_utils.py
--- a/mlflow/utils/model_utils.py
+++ b/mlflow/utils/model_utils.py
@@ -1,5 +1,6 @@
 import os
 import sys
+from pathlib import Path
 
 from mlflow.exceptions import MlflowException
 from mlflow.models import Model
@@ -118,6 +119,13 @@ def _validate_and_copy_code_paths(code_paths, path, default_subpath="code"):
 
 def _add_code_to_system_path(code_path):
     sys.path = [code_path] + _get_code_dirs(code_path) + sys.path
+    # Delete cached modules so they will get reloaded anew from the correct code path
+    # Otherwise python will use the cached modules
+    modules = [
+        p.stem for p in Path(code_path).rglob("*.py") if p.is_file() and p.name != "__init__.py"
+    ]
+    for module in modules:
+        sys.modules.pop(module, None)
 
 
 def _validate_and_prepare_target_save_path(path):


Test patch:
diff --git a/tests/pyfunc/test_model_export_with_class_and_artifacts.py b/tests/pyfunc/test_model_export_with_class_and_artifacts.py
--- a/tests/pyfunc/test_model_export_with_class_and_artifacts.py
+++ b/tests/pyfunc/test_model_export_with_class_and_artifacts.py
@@ -1069,3 +1069,53 @@ def test_predict(sk_model, model_input):
         loaded_pyfunc_model.predict(iris_data[0]),
         test_predict(sk_model=sklearn_knn_model, model_input=iris_data[0]),
     )
+
+
+class CustomModel(mlflow.pyfunc.PythonModel):
+    def __init__(self):
+        pass
+
+    def predict(self, context, model_input):
+        import custom_module
+
+        return custom_module.predict()
+
+
+def test_model_with_code_path_does_not_use_cached_module(tmp_path):
+    dir1 = tmp_path.joinpath("1")
+    dir2 = tmp_path.joinpath("2")
+    dir1.mkdir()
+    dir2.mkdir()
+    mod1 = dir1.joinpath("custom_module.py")
+    mod2 = dir2.joinpath("custom_module.py")
+    mod1.write_text(
+        """
+def predict():
+    return 1
+"""
+    )
+    mod2.write_text(
+        """
+def predict():
+    return 2
+"""
+    )
+
+    custom_model = CustomModel()
+    with mlflow.start_run():
+        model_info1 = mlflow.pyfunc.log_model(
+            artifact_path="model1",
+            python_model=custom_model,
+            code_path=[str(mod1)],
+        )
+        model_info2 = mlflow.pyfunc.log_model(
+            artifact_path="model2",
+            python_model=custom_model,
+            code_path=[str(mod2)],
+        )
+
+    model_input = pd.DataFrame([[1, 2, 3]])
+    loaded_model1 = mlflow.pyfunc.load_model(model_info1.model_uri)
+    assert loaded_model1.predict(model_input) == 1
+    loaded_model2 = mlflow.pyfunc.load_model(model_info2.model_uri)
+    assert loaded_model2.predict(model_input) == 2