Repository: canonical/operator
Pull request #709 (2022-03-01 21:16:09)

Add Container.isdir and Container.exists helpers
These are common file io patterns that previously required a fair bit of
boilerplate for users to accomplish.

Before:

```python
try:
    f = container.list_files(path, itself=True)[0]
    isdir = f.type == pebble.FileType.DIRECTORY
except pebble.APIError as err:
    if err.code == 404:
        isdir = False
    raise err
```

After:

```python
container.isdir(path)
```

Similar pattern for `exists`.

@mmanciop 

jnsgruk: A comment for consideration, how do you feel about `file_exists` and `is_dir` as names here? Feels a bit more consistent with other names in the api?
rwcarlsen: I opted for these names because that's what they are named in the python stdlib.  I'm not opposed to other names, but considering that "push" and "pull" are pretty non-specific already, it seemed like fully descriptive names were not necessarily a goal for this API and so I settled on stdlib matching as a decent approach.
jnsgruk: Fair enough, I like that logic! I’ll get this merged, can you update the
pebble file API section in the docs? 😃

On Thu, 3 Mar 2022 at 17:33, Robert Carlsen ***@***.***>
wrote:

> I opted for these names because that's what they are named in the python
> stdlib. I'm not opposed to other names, but considering that "push" and
> "pull" are pretty non-specific already, it seemed like fully descriptive
> names were not necessarily a goal for this API and so I settled on stdlib
> matching as a decent approach.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/canonical/operator/pull/709#issuecomment-1058237745>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAFDGWPEVV642WDSJU3UDYTU6DSW3ANCNFSM5PVFYSKQ>
> .
> You are receiving this because you commented.Message ID:
> ***@***.***>
>


Patch:
diff --git a/ops/model.py b/ops/model.py
index e627951..5da9efe 100644
--- a/ops/model.py
+++ b/ops/model.py
@@ -1270,6 +1270,26 @@ class Container:
         """
         return self._pebble.list_files(path, pattern=pattern, itself=itself)
 
+    def exists(self, path: str) -> bool:
+        """Return true if the path exists on the container filesystem."""
+        try:
+            self._pebble.list_files(path, itself=True)
+        except pebble.APIError as err:
+            if err.code == 404:
+                return False
+            raise err
+        return True
+
+    def isdir(self, path: str) -> bool:
+        """Return true if a directory exists at the given path on the container filesystem."""
+        try:
+            files = self._pebble.list_files(path, itself=True)
+        except pebble.APIError as err:
+            if err.code == 404:
+                return False
+            raise err
+        return files[0].type == pebble.FileType.DIRECTORY
+
     def make_dir(
             self, path: str, *, make_parents: bool = False, permissions: int = None,
             user_id: int = None, user: str = None, group_id: int = None, group: str = None):


Test patch:
diff --git a/test/test_testing.py b/test/test_testing.py
index 1f8cfd2..0a1b322 100644
--- a/test/test_testing.py
+++ b/test/test_testing.py
@@ -1646,6 +1646,33 @@ class TestHarness(unittest.TestCase):
         self.assertFalse(path.parent.exists())
         self.assertFalse(path.parent.parent.exists())
 
+    def test_container_isdir_and_exists(self):
+        harness = Harness(CharmBase, meta='''
+            name: test-app
+            containers:
+              foo:
+                resource: foo-image
+            ''')
+        self.addCleanup(harness.cleanup)
+        harness.begin()
+        c = harness.model.unit.containers['foo']
+
+        dir_path = '/tmp/foo/dir'
+        file_path = '/tmp/foo/file'
+
+        self.assertFalse(c.isdir(dir_path))
+        self.assertFalse(c.exists(dir_path))
+        self.assertFalse(c.isdir(file_path))
+        self.assertFalse(c.exists(file_path))
+
+        c.make_dir(dir_path, make_parents=True)
+        c.push(file_path, 'data')
+
+        self.assertTrue(c.isdir(dir_path))
+        self.assertTrue(c.exists(dir_path))
+        self.assertFalse(c.isdir(file_path))
+        self.assertTrue(c.exists(file_path))
+
     def test_add_oci_resource_custom(self):
         harness = Harness(CharmBase, meta='''
             name: test-app