Repository: mlflow/mlflow

Issue:
[FR] Allow passing pathlib.Path to set_tracking_uri
### Willingness to contribute

Yes. I can contribute this feature independently.

### Proposal Summary

It would be intuitive that we could pass the standard library's paths object to `set_tracking_uri`.

It's also easy to implement, we just have to call `path.as_uri()`.

I assume there is other places where the user can pass an uri, would be nice if the behavior could be uniform across the API.

### Motivation

#### What is the use case for this feature?

```python
from pathlib import Path
path = Path("foo/bar")
mlflow.set_tracking_uri(path / "baz")
mlflow.start_run()
```

currently outputs:

```
AttributeError: 'PosixPath' object has no attribute 'decode'
```

#### Why is this use case valuable to support for MLflow users in general?

It's intuitive that this would work.

#### Why is this use case valuable to support for your project(s) or organization?

Our team has some standard path stored in `Path` instead of strings (e.g a standard path to store mlflow results).

#### Why is it currently difficult to achieve this use case?

It's not, but it is confusing that it isn't supported

### Details

_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
- [ ] `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
- [X] `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:
Hi @cacharle , thank you for raising this feature request. We'd be happy to review a PR that implements it. Thank you in advance for your contribution!
Why are we using `pathlib.Path` to represent a URI? `pathlib.Path("http://localhost:5000").as_uri()` doesn't even appear to work
@jhallard I use it to represent paths, not URI.

```python
>>> p =  Path("foo/bar")
>>> p.resolve().as_uri()
'file:///Users/me/foo/bar'
```

Resolving patch:
diff --git a/docs/source/conf.py b/docs/source/conf.py
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -343,6 +343,7 @@
     ("py:class", "scipy.sparse.csc.csc_matrix"),
     ("py:class", "scipy.sparse._csr.csr_matrix"),
     ("py:class", "scipy.sparse._csc.csc_matrix"),
+    ("py:class", "pathlib.Path"),
 ]
 
 
diff --git a/mlflow/tracking/_tracking_service/utils.py b/mlflow/tracking/_tracking_service/utils.py
--- a/mlflow/tracking/_tracking_service/utils.py
+++ b/mlflow/tracking/_tracking_service/utils.py
@@ -1,6 +1,8 @@
 import os
 from functools import partial
 import logging
+from pathlib import Path
+from typing import Union
 
 from mlflow.store.tracking import DEFAULT_LOCAL_FILE_AND_ARTIFACT_PATH
 from mlflow.store.db.db_types import DATABASE_ENGINES
@@ -39,7 +41,7 @@ def is_tracking_uri_set():
     return False
 
 
-def set_tracking_uri(uri: str) -> None:
+def set_tracking_uri(uri: Union[str, Path]) -> None:
     """
     Set the tracking server URI. This does not affect the
     currently active run (if one exists), but takes effect for successive runs.
@@ -53,6 +55,7 @@ def set_tracking_uri(uri: str) -> None:
                   Databricks CLI
                   `profile <https://github.com/databricks/databricks-cli#installation>`_,
                   "databricks://<profileName>".
+                - A :py:class:`pathlib.Path` instance
 
     .. code-block:: python
         :caption: Example
@@ -68,6 +71,12 @@ def set_tracking_uri(uri: str) -> None:
 
         Current tracking uri: file:///tmp/my_tracking
     """
+    if isinstance(uri, Path):
+        # On Windows with Python3.7 (https://bugs.python.org/issue38671)
+        # .resolve() doesn't return the absolute path if the directory doesn't exist
+        # so we're calling .absolute() first to get the absolute path on Windows,
+        # then .resolve() to clean the path
+        uri = uri.absolute().resolve().as_uri()
     global _tracking_uri
     _tracking_uri = uri
 


Test patch:
diff --git a/tests/tracking/_tracking_service/test_utils.py b/tests/tracking/_tracking_service/test_utils.py
--- a/tests/tracking/_tracking_service/test_utils.py
+++ b/tests/tracking/_tracking_service/test_utils.py
@@ -5,6 +5,7 @@
 import pickle
 import os
 import pytest
+from pathlib import Path
 
 import mlflow
 from mlflow.exceptions import MlflowException
@@ -15,6 +16,8 @@
 from mlflow.tracking.registry import UnsupportedModelRegistryStoreURIException
 from mlflow.tracking._tracking_service.registry import TrackingStoreRegistry
 from mlflow.tracking._tracking_service.utils import (
+    set_tracking_uri,
+    get_tracking_uri,
     _get_store,
     _resolve_tracking_uri,
     _TRACKING_INSECURE_TLS_ENV_VAR,
@@ -358,3 +361,14 @@ def test_store_object_can_be_serialized_by_pickle(tmpdir):
     pickle.dump(_get_store("https://example.com"), io.BytesIO())
     # pickle.dump(_get_store(f"sqlite:///{tmpdir.strpath}/mlflow.db"), io.BytesIO())
     # This throws `AttributeError: Can't pickle local object 'create_engine.<locals>.connect'`
+
+
+@pytest.mark.parametrize("absolute", [True, False], ids=["absolute", "relative"])
+def test_set_tracking_uri_with_path(tmp_path, monkeypatch, absolute):
+    monkeypatch.chdir(tmp_path)
+    path = Path("foo/bar")
+    if absolute:
+        path = tmp_path / path
+    with mock.patch("mlflow.tracking._tracking_service.utils._tracking_uri", None):
+        set_tracking_uri(path)
+        assert get_tracking_uri() == path.absolute().resolve().as_uri()