Repository: glotzerlab/signac
Pull request #113 (2019-02-04 22:22:53)

Feature/h5store context manager reentry
Enable reentry of the H5Store context manager.

## Description

This essentially enables:
```python
with job.data:
    with job.data:
```
## Motivation and Context

This is especially useful if you use the store in different functions and just want to make sure that the file is open within that specific context without caring about whether the file is already opened in the outer context.

## Types of Changes
<!-- Please select all items that apply either now or after creating the pull request: -->
- [ ] Documentation update
- [ ] Bug fix
- [x] New feature
- [x] Breaking change<sup>1</sup>

This pull request is based on
- [ ] *master*, because it is a bug fix or an update to the documentation.
- [x] *develop*, because it introduces a new feature.

<sup>1</sup>The change breaks (or has the potential to break) existing functionality.

## Checklist:
<!-- Please select all items that apply either now or after creating the pull request. -->
<!-- If you are unsure about any of these items, do not hesitate to ask! -->
- [x] I am familiar with the [**Contributing Guidelines**](https://github.com/glotzerlab/signac/blob/master/CONTRIBUTING.md).
- [x] I agree with the terms of the [**Contributor Agreement**](https://github.com/glotzerlab/signac/blob/master/ContributorAgreement.md).
- [x] My name is on the [list of contributors](https://github.com/glotzerlab/signac/blob/master/contributors.txt).
- [x] My code follows the [code style guideline](https://github.com/glotzerlab/signac/blob/misc/contributor-flow/CONTRIBUTING.md#code-style) of this project.
- [x] The changes introduced by this pull request are covered by existing or newly introduced tests.

If necessary:
- [ ] I have updated the API documentation as part of the package doc-strings.
- [ ] I have created a separate pull request to update the [framework documentation](https://docs.signac.io/) on [signac-docs](https://github.com/glotzerlab/signac-docs) and linked it here.
- [ ] I have updated the [changelog](https://github.com/glotzerlab/signac/blob/master/changelog.txt).


csadorf: I just noticed the tests failing on PyPy2 and 3. I'll fix that now.
csadorf: @bdice I've addressed all comments and fixed all issues.

Patch:
diff --git a/signac/core/h5store.py b/signac/core/h5store.py
index ba51b329..d7fb80a3 100644
--- a/signac/core/h5store.py
+++ b/signac/core/h5store.py
@@ -55,9 +55,12 @@ def _requires_tables():
 logger = logging.getLogger(__name__)
 
 
-class ClosedH5StoreError(RuntimeError):
-    "Raised when trying to access a closed group."
-    pass
+class H5StoreClosedError(RuntimeError):
+    "Raised when trying to access a closed store."
+
+
+class H5StoreAlreadyOpenError(OSError):
+    """Indicates that the underlying HDF5-file is already openend."""
 
 
 def _h5set(file, grp, key, value, path=None):
@@ -266,7 +269,10 @@ class H5Store(MutableMapping):
         self.close()
 
     def __enter__(self):
-        self.open()
+        try:
+            self.open()
+        except H5StoreAlreadyOpenError:
+            pass
         return self
 
     def __exit__(self, exception_type, exception_value, exception_traceback):
@@ -275,7 +281,7 @@ class H5Store(MutableMapping):
     def open(self):
         """Opens the datastore file."""
         if self._file is not None:
-            raise OSError("File '{}' is already open.".format(self))
+            raise H5StoreAlreadyOpenError(self)
         import h5py
         self._thread_lock.acquire()
         try:
@@ -300,7 +306,7 @@ class H5Store(MutableMapping):
     @property
     def file(self):
         if self._file is None:
-            raise ClosedH5StoreError(self._filename)
+            raise H5StoreClosedError(self._filename)
         else:
             return self._file
 


Test patch:
diff --git a/tests/test_h5store.py b/tests/test_h5store.py
index 622e8991..54b6790f 100644
--- a/tests/test_h5store.py
+++ b/tests/test_h5store.py
@@ -15,7 +15,7 @@ from platform import python_implementation
 from multiprocessing.pool import ThreadPool
 from contextlib import closing
 
-from signac.core.h5store import H5Store, ClosedH5StoreError
+from signac.core.h5store import H5Store, H5StoreClosedError, H5StoreAlreadyOpenError
 from signac.common import six
 from signac.warnings import SignacDeprecationWarning
 
@@ -243,6 +243,20 @@ class H5StoreTest(BaseH5StoreTest):
             self.assertEqual(len(h5s), 1)
             self.assertEqual(h5s[key], d)
 
+    def test_open_twice(self):
+        h5s = self.get_h5store()
+        h5s.open()
+        try:
+            with self.assertRaises(H5StoreAlreadyOpenError):
+                h5s.open()
+        finally:
+            h5s.close()
+
+    def test_open_reentry(self):
+        with self.open_h5store() as h5s:
+            with h5s:
+                pass
+
     def test_reopen_explicit_open_close(self):
         h5s = self.get_h5store().open()
         key = 'reopen'
@@ -282,7 +296,7 @@ class H5StoreTest(BaseH5StoreTest):
                     self.assertEqual(h5s[k], v)
                     try:
                         same_h5s[k] = h5s[k]
-                    except ClosedH5StoreError:
+                    except H5StoreClosedError:
                         pass
                     self.assertEqual(h5s[k], v)
                     self.assertEqual(same_h5s[k], v)