Repository: canonical/operator
Pull request #101 (2020-01-10 16:53:39)

Add convience properties for accessing model, meta, and charm_dir
Always having to write `self.framework.model` gets repetitive and has to be done frequently enough that having a short-cut of `self.model` is worth doing.


Patch:
diff --git a/.gitignore b/.gitignore
index 36b14f6..cf0f371 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 __pycache__
 /sandbox
+.idea
diff --git a/README.md b/README.md
index 375a962..f289ebc 100644
--- a/README.md
+++ b/README.md
@@ -5,26 +5,43 @@ for early testing.
 
 ## Getting Started
 
-Start by creating a charm directory with at least the following files:
+Your charm directory should have the following overall structure:
 
-* `src/charm.py` (must be executable and use Python 3.6+)
-* `hooks/install` (or `hooks/start` for K8s charms) sym-linked to `../src/charm.py`
-* `metadata.yaml`
+```
+.
++-- metadata.yaml
++-- mod/
++-- lib/
+|   +-- ops -> ../mod/operator/ops
++-- src/
+|   +-- charm.py
++-- hooks/
+    +-- install -> ../src/charm.py
+```
 
-Then install the framework into the `lib/` directory using:
+The `mod/` directory will contain the operator framework dependency as a git
+submodule:
 
 ```
-mkdir lib/
-pip install -t lib/ https://github.com/canonical/operator
+git submodule add https://github.com/canonical/operator mod/operator
 ```
 
-Your `src/charm.py` is the entry point for your charm logic. At a minimum, it
-needs to define a subclass of `CharmBase` and pass that into the framework's
-`main` function:
+Other dependencies included as git submodules can be added there as well.
+
+The `lib/` directory will then contain symlinks to subdirectories of your
+submodule dependencies to enable them to be imported into the charm:
+
+```
+ln -s ../mod/operator/ops lib/ops
+```
+
+Your `src/charm.py` is the entry point for your charm logic. It should be set
+to executable and use Python 3.6 or greater. At a minimum, it needs to define a
+subclass of `CharmBase` and pass that into the framework's `main` function:
 
 ```python
 import sys
-sys.path.insert.append('lib')
+sys.path.append('lib')
 
 from ops.charm import CharmBase
 from ops.main import main
@@ -53,11 +70,20 @@ class MyCharm(CharmBase):
 Every standard event in Juju may be observed that way, and you can also easily
 define your own events in your custom types.
 
+The `hooks/` directory will then contain symlinks to your `src/charm.py` entry
+point so that Juju can call it. You only need to set up the `hooks/install` link
+(`hooks/start` for K8s charms, until [lp#1854635](https://bugs.launchpad.net/juju/+bug/1854635)
+is resolved), and the framework will fill out all others at runtime.
+
 Once your charm is ready, deploy it as normal with:
 
 ```
 juju deploy .
 ```
 
-You can sync subsequent changes from the framework by running the `pip`
-command again with `--upgrade`.
+You can sync subsequent changes from the framework and other submodule
+dependencies by running:
+
+```
+git submodule update
+```
diff --git a/ops/framework.py b/ops/framework.py
index 9c27bf1..3181aa1 100755
--- a/ops/framework.py
+++ b/ops/framework.py
@@ -194,6 +194,9 @@ class Object:
         kind = self.handle_kind
         if isinstance(parent, Framework):
             self.framework = parent
+            # Avoid Framework instances having a circular reference to themselves.
+            if self.framework is self:
+                self.framework = weakref.proxy(self.framework)
             self.handle = Handle(None, kind, key)
         else:
             self.framework = parent.framework
@@ -212,6 +215,18 @@ class Object:
 
         # TODO Detect conflicting handles here.
 
+    @property
+    def model(self):
+        return self.framework.model
+
+    @property
+    def meta(self):
+        return self.framework.meta
+
+    @property
+    def charm_dir(self):
+        return self.framework.charm_dir
+
 
 class EventsBase(Object):
     """Convenience type to allow defining .on attributes at class level."""
@@ -376,6 +391,11 @@ class Framework(Object):
 
     on = FrameworkEvents()
 
+    # Override properties from Object so that we can set them in __init__.
+    model = None
+    meta = None
+    charm_dir = None
+
     def __init__(self, data_path, charm_dir, meta, model):
 
         super().__init__(self, None)


Test patch:
diff --git a/test/test_framework.py b/test/test_framework.py
index 4fa38aa..7551d9d 100755
--- a/test/test_framework.py
+++ b/test/test_framework.py
@@ -700,6 +700,16 @@ class TestFramework(unittest.TestCase):
         # (The event key goes up by 2 due to the pre-commit and commit events.)
         self.assertEqual(obs2.seen, [('4', 'second'), ('1', 'first')])
 
+    def test_helper_properties(self):
+        framework = self.create_framework()
+        framework.model = 'test-model'
+        framework.meta = 'test-meta'
+
+        my_obj = Object(framework, 'my_obj')
+        self.assertEqual(my_obj.model, framework.model)
+        self.assertEqual(my_obj.meta, framework.meta)
+        self.assertEqual(my_obj.charm_dir, framework.charm_dir)
+
 
 class TestStoredState(unittest.TestCase):