Repository: optuna/optuna

Issue:
Add `optuna.study.delete_study()`.
Thanks to https://github.com/pfnet/optuna/pull/524, users can delete studies using command-line interface as follows:

```bash
optuna delete-study --study-name my-study --storage sqlite:///example.db
```

If we implement `optuna.study.delete_study(study_name, storage)` like `optuna.study.create_study()`, it will be useful for users who embed Optuna into their software.


Discussion:

Resolving patch:
diff --git a/optuna/__init__.py b/optuna/__init__.py
--- a/optuna/__init__.py
+++ b/optuna/__init__.py
@@ -13,6 +13,7 @@
 from optuna import visualization  # NOQA
 
 from optuna.study import create_study  # NOQA
+from optuna.study import delete_study  # NOQA
 from optuna.study import get_all_study_summaries  # NOQA
 from optuna.study import load_study  # NOQA
 from optuna.study import Study  # NOQA
diff --git a/optuna/study.py b/optuna/study.py
--- a/optuna/study.py
+++ b/optuna/study.py
@@ -677,6 +677,29 @@ def load_study(
     return Study(study_name=study_name, storage=storage, sampler=sampler, pruner=pruner)
 
 
+def delete_study(
+        study_name,  # type: str
+        storage,  # type: Union[str, storages.BaseStorage]
+):
+    # type: (...) -> None
+    """Delete a :class:`~optuna.study.Study` object.
+
+    Args:
+        study_name:
+            Study's name.
+        storage:
+            Database URL such as ``sqlite:///example.db``. Optuna internally uses `SQLAlchemy
+            <https://www.sqlalchemy.org/>`_ to handle databases. Please refer to `SQLAlchemy's
+            document <https://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls>`_ for
+            further details.
+
+    """
+
+    storage = storages.get_storage(storage)
+    study_id = storage.get_study_id_from_name(study_name)
+    storage.delete_study(study_id)
+
+
 def get_all_study_summaries(storage):
     # type: (Union[str, storages.BaseStorage]) -> List[structs.StudySummary]
     """Get all history of studies stored in a specified storage.


Test patch:
diff --git a/tests/test_study.py b/tests/test_study.py
--- a/tests/test_study.py
+++ b/tests/test_study.py
@@ -539,6 +539,31 @@ def test_load_study(storage_mode, cache_mode):
         assert created_study.study_id == loaded_study.study_id
 
 
+@pytest.mark.parametrize('storage_mode', STORAGE_MODES)
+@pytest.mark.parametrize('cache_mode', CACHE_MODES)
+def test_delete_study(storage_mode, cache_mode):
+    # type: (str, bool) -> None
+
+    with StorageSupplier(storage_mode, cache_mode) as storage:
+        # Get storage object because delete_study does not accept None.
+        storage = optuna.storages.get_storage(storage=storage)
+        assert storage is not None
+
+        # Test deleting a non-existing study.
+        with pytest.raises(ValueError):
+            optuna.delete_study("invalid-study-name", storage)
+
+        # Test deleting an existing study.
+        study = optuna.create_study(storage=storage, load_if_exists=False)
+        optuna.delete_study(study.study_name, storage)
+
+        # Test failed to delete the study which is already deleted.
+        if not isinstance(study._storage, optuna.storages.InMemoryStorage):
+            # Skip `InMemoryStorage` because it just internally initializes trials and so on.
+            with pytest.raises(ValueError):
+                optuna.delete_study(study.study_name, storage)
+
+
 def test_nested_optimization():
     # type: () -> None