Repository: wandb/wandb

Issue:
[CLI]: wand.save() does not accept pathlib.Path
### Describe the bug

This is contrary to how most python libraries work. For instance, it is perfectly fine to pass a `pathlib.Path` to
* `open()`
* `torch.save()`
* `numpy.save()`
* `xarray.to_netcdf()`
To give just a few examples. Explicitly requiring a string is unexpected and counter-intuitive.


### Additional Files

_No response_

### Environment

WandB version:

OS:

Python version:

Versions of relevant libraries:


### Additional Context

_No response_


Discussion:
Hi @sehoffmann, thanks for reporting this! I'll create a new feature request to allow passing a `pathlib.Path` to `wandb.save()`, thanks for the feedback!
Hey @luisbergua,
I would like to work on this, can I pick this up ?
Hey @techytushar, great to hear it! [Here](https://github.com/wandb/wandb/blob/main/CONTRIBUTING.md) you have all the steps to contribute, thanks a lot! 

Resolving patch:
diff --git a/wandb/sdk/wandb_run.py b/wandb/sdk/wandb_run.py
--- a/wandb/sdk/wandb_run.py
+++ b/wandb/sdk/wandb_run.py
@@ -6,6 +6,7 @@
 import logging
 import numbers
 import os
+import pathlib
 import re
 import sys
 import threading
@@ -1714,16 +1715,16 @@ def log(
     @_run_decorator._attach
     def save(
         self,
-        glob_str: Optional[str] = None,
-        base_path: Optional[str] = None,
+        glob_str: Optional[Union[str, pathlib.Path]] = None,
+        base_path: Optional[Union[str, pathlib.Path]] = None,
         policy: "PolicyName" = "live",
     ) -> Union[bool, List[str]]:
         """Ensure all files matching `glob_str` are synced to wandb with the policy specified.
 
         Arguments:
-            glob_str: (string) a relative or absolute path to a unix glob or regular
-                path.  If this isn't specified the method is a noop.
-            base_path: (string) the base path to run the glob relative to
+            glob_str: (string, pathlib.Path) a relative or absolute path to a unix glob or regular
+                path. If this isn't specified the method is a noop.
+            base_path: (string, pathlib.Path) the base path to run the glob relative to
             policy: (string) on of `live`, `now`, or `end`
                 - live: upload the file as it changes, overwriting the previous version
                 - now: upload the file once now
@@ -1744,8 +1745,8 @@ def save(
 
     def _save(
         self,
-        glob_str: Optional[str] = None,
-        base_path: Optional[str] = None,
+        glob_str: Optional[Union[str, pathlib.Path]] = None,
+        base_path: Optional[Union[str, pathlib.Path]] = None,
         policy: "PolicyName" = "live",
     ) -> Union[bool, List[str]]:
         if policy not in ("live", "end", "now"):
@@ -1754,8 +1755,12 @@ def _save(
             )
         if isinstance(glob_str, bytes):
             glob_str = glob_str.decode("utf-8")
+        elif isinstance(glob_str, pathlib.Path):
+            glob_str = str(glob_str)
         if not isinstance(glob_str, str):
-            raise ValueError("Must call wandb.save(glob_str) with glob_str a str")
+            raise ValueError(
+                "Must call wandb.save(glob_str) with glob_str as str or pathlib.Path object"
+            )
 
         if base_path is None:
             if os.path.isabs(glob_str):


Test patch:
diff --git a/tests/pytest_tests/unit_tests/test_wandb_save.py b/tests/pytest_tests/unit_tests/test_wandb_save.py
--- a/tests/pytest_tests/unit_tests/test_wandb_save.py
+++ b/tests/pytest_tests/unit_tests/test_wandb_save.py
@@ -1,4 +1,5 @@
 import os
+import pathlib
 import tempfile
 
 import pytest
@@ -77,3 +78,18 @@ def test_save_relative_path(mock_run, parse_records, record_q):
     file_record = parsed.files[0].files[0]
     assert file_record.path == os.path.relpath(test_path, root)
     assert file_record.policy == 0
+
+
+@pytest.mark.xfail(reason="This test is flaky")
+def test_save_path_object(mock_run, parse_records, record_q):
+    run = mock_run()
+
+    with open("test.rad", "w") as f:
+        f.write("something")
+    path = pathlib.Path("test.rad")
+    run.save(path)
+    assert os.path.exists(os.path.join(run.dir, "test.rad"))
+    parsed = parse_records(record_q)
+    file_record = parsed.files[0].files[0]
+    assert file_record.path == "test.rad"
+    assert file_record.policy == 2