Repository: ray-project/ray

Issue:
[tune] ExperimentAnalysis doesn't expand user as docs imply
### What is the problem?

The Ray docs give an example where a path with the "~" operator is included:
https://docs.ray.io/en/master/tune/api_docs/analysis.html#experimentanalysis-tune-experimentanalysis

```
>>> tune.run(my_trainable, name="my_exp", local_dir="~/tune_results")
>>> analysis = ExperimentAnalysis(
>>>     experiment_checkpoint_path="~/tune_results/my_exp/state.json")
```

This throws an error for me:
```
from ray.tune import ExperimentAnalysis
analysis = ExperimentAnalysis("~/rayexp/experiment_state-2020-06-06_20-22-19.json")

    203     def __init__(self, experiment_checkpoint_path, trials=None):
--> 204         with open(experiment_checkpoint_path) as f:
    205             _experiment_state = json.load(f)
    206             self._experiment_state = _experiment_state

FileNotFoundError: [Errno 2] No such file or directory: '~/rayexp/experiment_state-2020-06-06_20-22-19.json'
```

Whereas the corresponding expanded version does not:

```
from ray.tune import ExperimentAnalysis
from os.path import expanduser
analysis = ExperimentAnalysis(expanduser("~/rayexp/experiment_state-2020-06-06_20-22-19.json"))
```

*Ray version and other system information (Python version, TensorFlow version, OS):*

Ray 0.8.5
Python 3.8
Ubuntu 18

### Reproduction (REQUIRED)

See description above.



Discussion:

Resolving patch:
diff --git a/python/ray/tune/analysis/experiment_analysis.py b/python/ray/tune/analysis/experiment_analysis.py
--- a/python/ray/tune/analysis/experiment_analysis.py
+++ b/python/ray/tune/analysis/experiment_analysis.py
@@ -201,6 +201,11 @@ class ExperimentAnalysis(Analysis):
     """
 
     def __init__(self, experiment_checkpoint_path, trials=None):
+        experiment_checkpoint_path = os.path.expanduser(
+            experiment_checkpoint_path)
+        if not os.path.isfile(experiment_checkpoint_path):
+            raise ValueError(
+                "{} is not a valid file.".format(experiment_checkpoint_path))
         with open(experiment_checkpoint_path) as f:
             _experiment_state = json.load(f)
             self._experiment_state = _experiment_state


Test patch:
diff --git a/python/ray/tune/tests/test_experiment_analysis_mem.py b/python/ray/tune/tests/test_experiment_analysis_mem.py
--- a/python/ray/tune/tests/test_experiment_analysis_mem.py
+++ b/python/ray/tune/tests/test_experiment_analysis_mem.py
@@ -1,12 +1,16 @@
+import json
 import unittest
 import shutil
 import tempfile
+import os
 import random
 import pandas as pd
+import pytest
 import numpy as np
 
 import ray
-from ray.tune import run, Trainable, sample_from, Analysis, grid_search
+from ray.tune import (run, Trainable, sample_from, Analysis,
+                      ExperimentAnalysis, grid_search)
 from ray.tune.examples.async_hyperband_example import MyTrainableClass
 
 
@@ -37,14 +41,36 @@ def load_checkpoint(self, checkpoint_path):
                 pass
 
         self.MockTrainable = MockTrainable
+        self.test_dir = tempfile.mkdtemp()
         ray.init(local_mode=False, num_cpus=1)
 
     def tearDown(self):
         shutil.rmtree(self.test_dir, ignore_errors=True)
         ray.shutdown()
 
+    def testInit(self):
+        experiment_checkpoint_path = os.path.join(self.test_dir,
+                                                  "experiment_state.json")
+        checkpoint_data = {
+            "checkpoints": [{
+                "trainable_name": "MockTrainable",
+                "logdir": "/mock/test/MockTrainable_0_id=3_2020-07-12"
+            }]
+        }
+
+        with open(experiment_checkpoint_path, "w") as f:
+            f.write(json.dumps(checkpoint_data))
+
+        experiment_analysis = ExperimentAnalysis(experiment_checkpoint_path)
+        self.assertEqual(len(experiment_analysis._checkpoints), 1)
+        self.assertTrue(experiment_analysis.trials is None)
+
+    def testInitException(self):
+        experiment_checkpoint_path = os.path.join(self.test_dir, "mock.json")
+        with pytest.raises(ValueError):
+            ExperimentAnalysis(experiment_checkpoint_path)
+
     def testCompareTrials(self):
-        self.test_dir = tempfile.mkdtemp()
         scores = np.asarray(list(self.MockTrainable.scores_dict.values()))
         scores_all = scores.flatten("F")
         scores_last = scores_all[5:]
@@ -126,7 +152,7 @@ def testDataframe(self):
         analysis = Analysis(self.test_dir)
         df = analysis.dataframe()
         self.assertTrue(isinstance(df, pd.DataFrame))
-        self.assertEquals(df.shape[0], self.num_samples * 2)
+        self.assertEqual(df.shape[0], self.num_samples * 2)
 
     def testBestLogdir(self):
         analysis = Analysis(self.test_dir)
@@ -134,17 +160,16 @@ def testBestLogdir(self):
         self.assertTrue(logdir.startswith(self.test_dir))
         logdir2 = analysis.get_best_logdir(self.metric, mode="min")
         self.assertTrue(logdir2.startswith(self.test_dir))
-        self.assertNotEquals(logdir, logdir2)
+        self.assertNotEqual(logdir, logdir2)
 
     def testBestConfigIsLogdir(self):
         analysis = Analysis(self.test_dir)
         for metric, mode in [(self.metric, "min"), (self.metric, "max")]:
             logdir = analysis.get_best_logdir(metric, mode=mode)
             best_config = analysis.get_best_config(metric, mode=mode)
-            self.assertEquals(analysis.get_all_configs()[logdir], best_config)
+            self.assertEqual(analysis.get_all_configs()[logdir], best_config)
 
 
 if __name__ == "__main__":
-    import pytest
     import sys
     sys.exit(pytest.main(["-v", __file__]))