Repository: canonical/operator (language: python)

Issue:
Annoyance: Figuring out which module has my object
When writing charms, I am often able to remember the name of object that I want (`EventBase`, `StoredState`, etc.), but often forget which module it comes from. There's logic to the modules, of course -- `CharmBase` is in `charm`, the status objects live in `model`, and the things that I want when I'm maybe being a little bit hacky live in `framework`. I can make guesses, and those guesses are easy to check with the introspection tools in an IDE or a good old fashioned `grep` on the command line.

It's a little mental speed bump, however, of the sort that I think we want to eliminate.

Note: there's a warning about circular imports in `ops/__init__.py`, which means that we'd need to be thoughtful if we imported more objects into the top level module. It might be worth a spike, however. Lines like `self.model.unit.status = MaintenanceStatus("Frobbling the bitzors")` would be easier to write if I didn't first have to remember that statuses live in `ops.model` rather than `ops.charm` or `ops.framework`.



Discussion / hints:
I approve of your annoyance here.
PyCharm is pretty clever in auto-importing stuff from the right place, so I can't say I feel your pain here. However.
For the sake of brainstorming, I can think of a few more options other than adding to the toplevel imports:

1) Do something which I've seen a lot in C++ binding libraries: adding stuff such as statuses as enum-likes inside some 'self'-reachable namespace.
One would do:
`self.unit.status = self.unit.ActiveStatus('msg')`

If we are determined enough, we could make it so that the user only has to import CharmBase, the rest (of ops) is reachable from within the charm.

2) A mitigation strategy, which however has some value on its own imho: object-oriented is fun, but it's also frustrating when you have to import objects that do very little and could be replaced by primitives with little to no loss of semantic expressiveness. 
What if instead of `self.unit.status = ActiveStatus(msg)` one could do: `self.unit.set_status('active', msg='foo')`?
Seems way more idiomatic, allows us to skip an import, simplifies the code (all the Status classes are boilerplaty). Any downsides? Nope. We can even add type hinting with `typing.Literal['active', 'blocked', '...']` to have runtime code checking (and mypy) whine about bugs...

3) Auto-imports. I.e. add `from ops.model import *;from ops.framework import *` at runtime. Could be even implemented as an interpreter instruction (`#!ops`), or simply as 'inject all these modules in the charm.py global namespace before executing it'.
Risk of variable collision, sure, but nothing that can't be solved with some good ol' AST parse and check.
I agree it'd probably be better if `ops` was designed so -- from the perspective of the user, anyway -- more things lived at the top level, and you could just `import ops` to get `ops.Foo`. However, I agree with @PietroPasotti that IDEs (PyCharm, VSCode) help with this a ton.

It's definitely too much churn to restructure this outright. However, it might be worth doing a "simplification" for ops 2.1 as part of the splitting up of big files that we're planning to do anyway, along the lines of putting this in `ops/__init__.py`:

```python
from .charm import CharmBase, RelationChangedEvent, SecretEvent, ...  # and lots more
from .framework import EventBase
from .main import main
from .model import *  # or if not *, a lot of things
```

Then for most charms you could just include `import ops` alone, and use `ops.Foo` directly.

What do you think, @jameinel?
On the topic of status setting, there was definitely an appetite for
reworking it. Setting an attribute to a specific set of classes was
definitely seen as awkward. Especially around Blocked status. (there may be
one or more reasons why you could be blocked, and having one of them
resolve shouldn't cause the entire blocked status to be resolved, so
something along the lines of a named blocker, and then being able to clear
out those named blockers individually could easily be a better system.)
Having it be a function rather than an attribute is good, you then run into
'but I'd rather use an Enum rather than strings' and then you're back into
having to figure out how to import the right enum at the right time. (Enums
help with typos and spell checking, but hurt around coupling and imports.)

For the rest of the structure. I do think we currently expose a little bit
too much implementation detail (`from ops.charm import CharmBase`).
I think it would be a good opportunity to do a set of 'these are the things
you *should* be touching from ops'. When I look at your list above, I see
things like "RelationChangedEvent" which I actually think you *shouldn't*
be importing into your code (unless you need them for typing, or possibly
custom events derived from them).

I do think having a richer 'ops' package (eg `class
MyCharm(ops.CharmBase):`), would probably serve us well, and could be a
great way to help target what are the objects you really should be creating
in your charms.
We already have some things which are "Don't instantiate this directly"
which feels like the sort of thing that should then not get imported into
the top level 'ops' package, leaving us with a relatively clean set of
things that you *are* expected to touch directly and often as a charm
author.

So broadly I'm keen on getting a nice level of top level imports, but I
think we should approach it with "don't just curry everything, but give a
curated ops.* set of things". And where we find it awkward to do a good
job, take a real look at it and see if there is a better way to express
what we want.


On Wed, Jan 11, 2023 at 9:48 PM Ben Hoyt ***@***.***> wrote:

> I agree it'd probably be better if ops was designed so -- from the
> perspective of the user, anyway -- more things lived at the top level, and
> you could just import ops to get ops.Foo. However, I agree with
> @PietroPasotti <https://github.com/PietroPasotti> that IDEs (PyCharm,
> VSCode) help with this a ton.
>
> It's definitely too much churn to restructure this outright. However, it
> might be worth doing a "simplification" for ops 2.1 as part of the
> splitting up of big files that we're planning to do anyway, along the lines
> of putting this in ops/__init__.py:
>
> from .charm import CharmBase, RelationChangedEvent, SecretEvent, ...  # and lots morefrom .framework import EventBasefrom .model import *  # or if not *, a lot of things
>
> Then for most charms you could just include import ops alone, and use
> ops.Foo directly.
>
> What do you think, @jameinel <https://github.com/jameinel>?
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/canonical/operator/issues/731#issuecomment-1379743134>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AABRQ7IRYWB2XCHWJTM3TMTWR5WGBANCNFSM5RR3V2FA>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

Good thoughts @jameinel. I agree with your suggested approach, though I think there might be debate about the details of what goes at the top level and what doesn't when it comes down to it. For example, the `FooEvent` classes -- we're encouraging the use of type annotations, so that's why those are needed. Anyway, I'll keep this in mind for the restructuring we're doing anyway as part of 2.1 (https://github.com/canonical/operator/issues/884).
Indeed, I had mentioned typing as something that could be an exception.
(though should we have a separate 'ops.typing.X' like the stdlib has you
"from typing import List".
Anyway, I would try to avoid importing everything, because I think guiding
users towards the useful subset is a very helpful thing, but I do agree
that having them have to understand the internal structure of ops is not
ideal.

John
=:->


On Thu, Jan 12, 2023 at 8:38 PM Ben Hoyt ***@***.***> wrote:

> Good thoughts @jameinel <https://github.com/jameinel>. I agree with your
> suggested approach, though I think there might be debate about the details
> of what goes at the top level and what doesn't when it comes down to it.
> For example, the FooEvent classes -- we're encouraging the use of type
> annotations, so that's why those are needed. Anyway, I'll keep this in mind
> for the restructuring we're doing anyway as part of 2.1 (#884
> <https://github.com/canonical/operator/issues/884>).
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/canonical/operator/issues/731#issuecomment-1381200563>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AABRQ7JEA6ESCBE26BFRUTTWSCW2NANCNFSM5RR3V2FA>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

I've taken a look at this, and I have a slightly radical (is that an oxymoron?) proposal: I'm more and more thinking we should import basically all the names -- or almost all the names -- from each `ops.foo` submodule into the top level (probably via an `__all__` list in each submodule and then `from .charm import *` and so on in `ops/__init__.py`).

There are a lot of names in some submodules (for example all the event classes), but the names are descriptive and unique (and we could easily have a test guaranteeing the `__all__` lists are kept unique), and in some ways the submodule that they're in is an implementation detail: users shouldn't have to know that an ops feature was implemented in `ops.myfile`.

The exception would be `pebble.py`, which has a lot of names that most users won't need (and they're not nearly as unique -- it was designed more as a namespaced package). The Pebble functionality should almost always be accessed via `model.Container`.

Doing this would allow charmers to only have one import at the top of their `charm.py` or charm lib, either `import ops` and then `ops.Foo`, or `from ops import Foo` and then `Foo` (depending on taste). Rather than the multiplicity of imports they need now, and figuring out which submodule a name is from.

Writing code would be easier: people's IDEs would easily be able to autocomplete (for example) `ops.PebbleReadyEvent` (from `charm.py`) or `ops.Secret` (from `model.py`).

I don't think the approach of having a separate `ops.typing` is great. These are real classes, not just "typing" things. (In fact, the Python type annotations have gone more towards using built-in names -- "real classes" -- where possible, like `list[T]` instead of `typing.List[T]` and `Foo | Bar` instead of `typing.Union[Foo, Bar]`.)

Thoughts @PietroPasotti, @jameinel, @sed-i?
Totally on board with the radical option @benhoyt, my proposal however is to have an `ops.core` package collecting everything that's in `ops` right now, minus the `testing` stuff.

This would allow us to make `ops` pure namespace and grow with time a package of native-feeling extensions one could import from ops

We'd need a grace period with dummy `ops.charm` files that import all from `ops.core.charm` and spam away deprecationwarning, but I think this could all be done in a backwards-compatible way.

Recap of my proposal:

- /ops # no names in here! pure namespace.
  - /ops/core # all 'builtin' functionality in ops (minus pebble and testing) in here.
    -  /ops/core/charm.py, main.py, etc... # all modules currently in /ops now live in /ops/core.
  - /ops/pebble # all k8s-specific names (this opens the doors to moving pebble stuff altogether in a separate package by the way, so you don't install it unless you really need it: `pip install ops[k8s]`. That'd be cool!)
  - /ops/develop # development/testing utilities (again, we could move this out to a separate package. `pip install ops[dev]`?)
    - /ops/develop/testing.py # Harness.
    
  - /ops/charm.py # temporary dummy file redirecting to /ops/core/charm.py Deprecated!
  - ... and so on for main.py, and all files currently in ops.
I'm hoping to consider the idea of extra / namespace package soon, but I don't really like the idea of making `ops` empty and putting *everything* in `ops[subpackage]`. I want it to be as simple as `pip install ops` and `import ops` for most people, and it seems like your suggested approach would just make it more difficult for charmers.

I'm not actually too worried about a few dozen KB (for `pebble.py` or whatever) in charms that don't need it. Source code is not huge, and it seems a small price to pay for keeping things flat and simple to use.

Fix patch:
diff --git a/ops/__init__.py b/ops/__init__.py
index 5e01059fc..e8675162d 100644
--- a/ops/__init__.py
+++ b/ops/__init__.py
@@ -39,6 +39,142 @@
 Full developer documentation is available at https://juju.is/docs/sdk.
 """
 
-# Import here the bare minimum to break the circular import between modules
-from . import charm  # type: ignore # noqa
-from .version import version as __version__  # type: ignore # noqa
+# The isort command wants to rearrange the nicely-formatted imports below;
+# just skip it for this file.
+# isort:skip_file
+
+# Similarly, Pyright complains that all of these things are unused imports,
+# so disable it:
+# pyright: reportUnusedImport=false
+
+# Import pebble explicitly. It's the one module we don't import names from below.
+from . import pebble  # type: ignore # noqa: F401
+
+# Also import charm explicitly. This is not strictly necessary as the
+# "from .charm" import automatically does that, but be explicit since this
+# import was here previously
+from . import charm  # type: ignore # noqa: F401
+
+# Import the main module, which we've overriden in main.py to be callable.
+# This allows "import ops; ops.main(Charm)" to work as expected.
+from . import main  # type: ignore # noqa: F401
+
+# Explicitly import names from sub-modules so users can just "import ops" and
+# then use them as "ops.X".
+from .charm import (  # noqa: F401
+    ActionEvent,
+    ActionMeta,
+    CharmBase,
+    CharmEvents,
+    CharmMeta,
+    CollectMetricsEvent,
+    ConfigChangedEvent,
+    ContainerMeta,
+    ContainerStorageMeta,
+    HookEvent,
+    InstallEvent,
+    LeaderElectedEvent,
+    LeaderSettingsChangedEvent,
+    PayloadMeta,
+    PebbleReadyEvent,
+    PostSeriesUpgradeEvent,
+    PreSeriesUpgradeEvent,
+    RelationBrokenEvent,
+    RelationChangedEvent,
+    RelationCreatedEvent,
+    RelationDepartedEvent,
+    RelationEvent,
+    RelationJoinedEvent,
+    RelationMeta,
+    RelationRole,
+    RemoveEvent,
+    ResourceMeta,
+    SecretChangedEvent,
+    SecretEvent,
+    SecretExpiredEvent,
+    SecretRemoveEvent,
+    SecretRotateEvent,
+    StartEvent,
+    StopEvent,
+    StorageAttachedEvent,
+    StorageDetachingEvent,
+    StorageEvent,
+    StorageMeta,
+    UpdateStatusEvent,
+    UpgradeCharmEvent,
+    WorkloadEvent,
+)
+
+from .framework import (  # noqa: F401
+    BoundEvent,
+    BoundStoredState,
+    CommitEvent,
+    EventBase,
+    EventSource,
+    Framework,
+    FrameworkEvents,
+    Handle,
+    HandleKind,
+    LifecycleEvent,
+    NoTypeError,
+    Object,
+    ObjectEvents,
+    PreCommitEvent,
+    PrefixedEvents,
+    StoredDict,
+    StoredList,
+    StoredSet,
+    StoredState,
+    StoredStateData,
+)
+
+from .jujuversion import JujuVersion  # noqa: F401
+
+from .model import (  # noqa: F401 E402
+    ActiveStatus,
+    Application,
+    Binding,
+    BindingMapping,
+    BlockedStatus,
+    CheckInfoMapping,
+    ConfigData,
+    Container,
+    ContainerMapping,
+    ErrorStatus,
+    InvalidStatusError,
+    LazyMapping,
+    MaintenanceStatus,
+    Model,
+    ModelError,
+    MultiPushPullError,
+    Network,
+    NetworkInterface,
+    OpenedPort,
+    Pod,
+    Relation,
+    RelationData,
+    RelationDataAccessError,
+    RelationDataContent,
+    RelationDataError,
+    RelationDataTypeError,
+    RelationMapping,
+    RelationNotFoundError,
+    Resources,
+    Secret,
+    SecretInfo,
+    SecretNotFoundError,
+    SecretRotate,
+    ServiceInfoMapping,
+    StatusBase,
+    Storage,
+    StorageMapping,
+    TooManyRelatedAppsError,
+    Unit,
+    UnknownStatus,
+    WaitingStatus,
+)
+
+# NOTE: don't import testing or Harness here, as that's a test-time concern
+# rather than a runtime concern.
+
+from .version import version as __version__  # noqa: F401 E402
diff --git a/ops/main.py b/ops/main.py
index 0dbb31557..88681fb04 100755
--- a/ops/main.py
+++ b/ops/main.py
@@ -12,7 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Main entry point to the Operator Framework."""
+"""Main entry point to the Operator Framework.
+
+Note that this module is callable, and calls the :func:`ops.main.main` function.
+This is so that :code:`import ops` followed by :code:`ops.main(MyCharm)` works
+as expected.
+"""
 
 import logging
 import os
@@ -438,3 +443,15 @@ def main(charm_class: Type[ops.charm.CharmBase],
         framework.commit()
     finally:
         framework.close()
+
+
+# Make this module callable and call main(), so that "import ops" and then
+# "ops.main(Charm)" works as expected now that everything is imported in
+# ops/__init__.py. Idea from https://stackoverflow.com/a/48100440/68707
+class _CallableModule(sys.modules[__name__].__class__):
+    def __call__(self, charm_class: Type[ops.charm.CharmBase],
+                 use_juju_for_storage: Optional[bool] = None):
+        return main(charm_class, use_juju_for_storage=use_juju_for_storage)
+
+
+sys.modules[__name__].__class__ = _CallableModule


Test patch:
diff --git a/test/test_charm.py b/test/test_charm.py
index 1b7613fff..9b7ba83e1 100755
--- a/test/test_charm.py
+++ b/test/test_charm.py
@@ -18,16 +18,8 @@
 import unittest
 from pathlib import Path
 
-from ops.charm import (
-    CharmBase,
-    CharmEvents,
-    CharmMeta,
-    ContainerMeta,
-    ContainerStorageMeta,
-    StartEvent,
-)
-from ops.framework import EventBase, EventSource, Framework
-from ops.model import Model, Storage, _ModelBackend
+import ops
+from ops.model import _ModelBackend
 from ops.storage import SQLiteStorage
 
 from .test_helpers import fake_script, fake_script_calls
@@ -48,33 +40,33 @@ def restore_env(env):
 
         self.tmpdir = Path(tempfile.mkdtemp())
         self.addCleanup(shutil.rmtree, str(self.tmpdir))
-        self.meta = CharmMeta()
+        self.meta = ops.CharmMeta()
 
-        class CustomEvent(EventBase):
+        class CustomEvent(ops.EventBase):
             pass
 
-        class TestCharmEvents(CharmEvents):
-            custom = EventSource(CustomEvent)
+        class TestCharmEvents(ops.CharmEvents):
+            custom = ops.EventSource(CustomEvent)
 
         # Relations events are defined dynamically and modify the class attributes.
         # We use a subclass temporarily to prevent these side effects from leaking.
-        CharmBase.on = TestCharmEvents()
+        ops.CharmBase.on = TestCharmEvents()
 
         def cleanup():
-            CharmBase.on = CharmEvents()
+            ops.CharmBase.on = ops.CharmEvents()
         self.addCleanup(cleanup)
 
     def create_framework(self):
-        model = Model(self.meta, _ModelBackend('local/0'))
+        model = ops.Model(self.meta, _ModelBackend('local/0'))
         # we can pass foo_event as event_name because we're not actually testing dispatch
-        framework = Framework(SQLiteStorage(':memory:'), self.tmpdir, self.meta,
-                              model)
+        framework = ops.Framework(SQLiteStorage(':memory:'), self.tmpdir, self.meta,
+                                  model)
         self.addCleanup(framework.close)
         return framework
 
     def test_basic(self):
 
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
 
             def __init__(self, *args):
                 super().__init__(*args)
@@ -116,7 +108,7 @@ def wrapper(charm, evt):
                 fn(charm, evt)
             return wrapper
 
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             def __init__(self, *args):
                 super().__init__(*args)
                 framework.observe(self.on.start, self._on_start)
@@ -132,16 +124,16 @@ def _on_start(self, event):
         # check that the event has been seen by the decorator
         self.assertEqual(1, len(events))
         # check that the event has been seen by the observer
-        self.assertIsInstance(charm.seen, StartEvent)
+        self.assertIsInstance(charm.seen, ops.StartEvent)
 
     def test_empty_action(self):
-        meta = CharmMeta.from_yaml('name: my-charm', '')
+        meta = ops.CharmMeta.from_yaml('name: my-charm', '')
         self.assertEqual(meta.actions, {})
 
     def test_helper_properties(self):
         framework = self.create_framework()
 
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             pass
 
         charm = MyCharm(framework)
@@ -153,7 +145,7 @@ class MyCharm(CharmBase):
 
     def test_relation_events(self):
 
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             def __init__(self, *args):
                 super().__init__(*args)
                 self.seen = []
@@ -170,7 +162,7 @@ def on_any_relation(self, event):
                 self.seen.append(type(event).__name__)
 
         # language=YAML
-        self.meta = CharmMeta.from_yaml(metadata='''
+        self.meta = ops.CharmMeta.from_yaml(metadata='''
 name: my-charm
 requires:
  req1:
@@ -216,7 +208,7 @@ def on_any_relation(self, event):
     def test_storage_events(self):
         this = self
 
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             def __init__(self, *args):
                 super().__init__(*args)
                 self.seen = []
@@ -239,7 +231,7 @@ def _on_stor4_attach(self, event):
                 self.seen.append(type(event).__name__)
 
         # language=YAML
-        self.meta = CharmMeta.from_yaml('''
+        self.meta = ops.CharmMeta.from_yaml('''
 name: my-charm
 storage:
   stor-4:
@@ -310,12 +302,12 @@ def _on_stor4_attach(self, event):
 
         charm = MyCharm(self.create_framework())
 
-        charm.on['stor1'].storage_attached.emit(Storage("stor1", 0, charm.model._backend))
-        charm.on['stor2'].storage_detaching.emit(Storage("stor2", 0, charm.model._backend))
-        charm.on['stor3'].storage_attached.emit(Storage("stor3", 0, charm.model._backend))
-        charm.on['stor-4'].storage_attached.emit(Storage("stor-4", 0, charm.model._backend))
+        charm.on['stor1'].storage_attached.emit(ops.Storage("stor1", 0, charm.model._backend))
+        charm.on['stor2'].storage_detaching.emit(ops.Storage("stor2", 0, charm.model._backend))
+        charm.on['stor3'].storage_attached.emit(ops.Storage("stor3", 0, charm.model._backend))
+        charm.on['stor-4'].storage_attached.emit(ops.Storage("stor-4", 0, charm.model._backend))
         charm.on['stor-multiple-dashes'].storage_attached.emit(
-            Storage("stor-multiple-dashes", 0, charm.model._backend))
+            ops.Storage("stor-multiple-dashes", 0, charm.model._backend))
 
         self.assertEqual(charm.seen, [
             'StorageAttachedEvent',
@@ -326,7 +318,7 @@ def _on_stor4_attach(self, event):
 
     def test_workload_events(self):
 
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             def __init__(self, *args):
                 super().__init__(*args)
                 self.seen = []
@@ -342,7 +334,7 @@ def on_any_pebble_ready(self, event):
                 self.count += 1
 
         # language=YAML
-        self.meta = CharmMeta.from_yaml(metadata='''
+        self.meta = ops.CharmMeta.from_yaml(metadata='''
 name: my-charm
 containers:
   container-a:
@@ -368,7 +360,7 @@ def on_any_pebble_ready(self, event):
     def test_relations_meta(self):
 
         # language=YAML
-        self.meta = CharmMeta.from_yaml('''
+        self.meta = ops.CharmMeta.from_yaml('''
 name: my-charm
 requires:
   database:
@@ -390,7 +382,7 @@ def test_relations_meta(self):
     def test_relations_meta_limit_type_validation(self):
         with self.assertRaisesRegex(TypeError, "limit should be an int, not <class 'str'>"):
             # language=YAML
-            self.meta = CharmMeta.from_yaml('''
+            self.meta = ops.CharmMeta.from_yaml('''
 name: my-charm
 requires:
   database:
@@ -402,7 +394,7 @@ def test_relations_meta_scope_type_validation(self):
         with self.assertRaisesRegex(TypeError,
                                     "scope should be one of 'global', 'container'; not 'foobar'"):
             # language=YAML
-            self.meta = CharmMeta.from_yaml('''
+            self.meta = ops.CharmMeta.from_yaml('''
 name: my-charm
 requires:
   database:
@@ -413,7 +405,7 @@ def test_relations_meta_scope_type_validation(self):
     @classmethod
     def _get_action_test_meta(cls):
         # language=YAML
-        return CharmMeta.from_yaml(metadata='''
+        return ops.CharmMeta.from_yaml(metadata='''
 name: my-charm
 ''', actions='''
 foo-bar:
@@ -442,7 +434,7 @@ def _setup_test_action(self):
 
     def test_action_events(self):
 
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
 
             def __init__(self, *args):
                 super().__init__(*args)
@@ -482,7 +474,7 @@ def _on_start_action(self, event):
 
     def test_invalid_action_results(self):
 
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
 
             def __init__(self, *args):
                 super().__init__(*args)
@@ -509,7 +501,7 @@ def _on_foo_bar_action(self, event):
 
     def _test_action_event_defer_fails(self, cmd_type):
 
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
 
             def __init__(self, *args):
                 super().__init__(*args)
@@ -532,7 +524,7 @@ def test_action_event_defer_fails(self):
         self._test_action_event_defer_fails('action')
 
     def test_containers(self):
-        meta = CharmMeta.from_yaml("""
+        meta = ops.CharmMeta.from_yaml("""
 name: k8s-charm
 containers:
   test1:
@@ -540,13 +532,13 @@ def test_containers(self):
   test2:
     k: v
 """)
-        self.assertIsInstance(meta.containers['test1'], ContainerMeta)
-        self.assertIsInstance(meta.containers['test2'], ContainerMeta)
+        self.assertIsInstance(meta.containers['test1'], ops.ContainerMeta)
+        self.assertIsInstance(meta.containers['test2'], ops.ContainerMeta)
         self.assertEqual(meta.containers['test1'].name, 'test1')
         self.assertEqual(meta.containers['test2'].name, 'test2')
 
     def test_containers_storage(self):
-        meta = CharmMeta.from_yaml("""
+        meta = ops.CharmMeta.from_yaml("""
 name: k8s-charm
 storage:
   data:
@@ -563,13 +555,13 @@ def test_containers_storage(self):
       - storage: other
         location: /test/otherdata
 """)
-        self.assertIsInstance(meta.containers['test1'], ContainerMeta)
-        self.assertIsInstance(meta.containers['test1'].mounts["data"], ContainerStorageMeta)
+        self.assertIsInstance(meta.containers['test1'], ops.ContainerMeta)
+        self.assertIsInstance(meta.containers['test1'].mounts["data"], ops.ContainerStorageMeta)
         self.assertEqual(meta.containers['test1'].mounts["data"].location, '/test/storagemount')
         self.assertEqual(meta.containers['test1'].mounts["other"].location, '/test/otherdata')
 
     def test_containers_storage_multiple_mounts(self):
-        meta = CharmMeta.from_yaml("""
+        meta = ops.CharmMeta.from_yaml("""
 name: k8s-charm
 storage:
   data:
@@ -583,8 +575,8 @@ def test_containers_storage_multiple_mounts(self):
       - storage: data
         location: /test/otherdata
 """)
-        self.assertIsInstance(meta.containers['test1'], ContainerMeta)
-        self.assertIsInstance(meta.containers['test1'].mounts["data"], ContainerStorageMeta)
+        self.assertIsInstance(meta.containers['test1'], ops.ContainerMeta)
+        self.assertIsInstance(meta.containers['test1'].mounts["data"], ops.ContainerStorageMeta)
         self.assertEqual(
             meta.containers['test1'].mounts["data"].locations[0],
             '/test/storagemount')
@@ -594,7 +586,7 @@ def test_containers_storage_multiple_mounts(self):
             meta.containers["test1"].mounts["data"].location
 
     def test_secret_events(self):
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             def __init__(self, *args):
                 super().__init__(*args)
                 self.seen = []
@@ -625,7 +617,7 @@ def on_secret_expired(self, event):
                 assert event.revision == 42
                 self.seen.append(type(event).__name__)
 
-        self.meta = CharmMeta.from_yaml(metadata='name: my-charm')
+        self.meta = ops.CharmMeta.from_yaml(metadata='name: my-charm')
         charm = MyCharm(self.create_framework())
 
         charm.on.secret_changed.emit('secret:changed', None)
diff --git a/test/test_framework.py b/test/test_framework.py
index 56a46a496..950f5e4db 100755
--- a/test/test_framework.py
+++ b/test/test_framework.py
@@ -27,25 +27,8 @@
 
 import logassert
 
-from ops import charm
-from ops.framework import (
-    _BREAKPOINT_WELCOME_MESSAGE,
-    BoundStoredState,
-    CommitEvent,
-    EventBase,
-    EventSource,
-    Framework,
-    Handle,
-    Object,
-    ObjectEvents,
-    PreCommitEvent,
-    StoredDict,
-    StoredList,
-    StoredSet,
-    StoredState,
-    StoredStateData,
-    _event_regex,
-)
+import ops
+from ops.framework import _BREAKPOINT_WELCOME_MESSAGE, _event_regex
 from ops.storage import NoSnapshotError, SQLiteStorage
 
 
@@ -62,24 +45,24 @@ def setUp(self):
 
     def test_deprecated_init(self):
         # For 0.7, this still works, but it is deprecated.
-        framework = Framework(':memory:', None, None, None)
+        framework = ops.Framework(':memory:', None, None, None)
         self.assertLoggedWarning(
             "deprecated: Framework now takes a Storage not a path")
         self.assertIsInstance(framework._storage, SQLiteStorage)
 
     def test_handle_path(self):
         cases = [
-            (Handle(None, "root", None), "root"),
-            (Handle(None, "root", "1"), "root[1]"),
-            (Handle(Handle(None, "root", None), "child", None), "root/child"),
-            (Handle(Handle(None, "root", "1"), "child", "2"), "root[1]/child[2]"),
+            (ops.Handle(None, "root", None), "root"),
+            (ops.Handle(None, "root", "1"), "root[1]"),
+            (ops.Handle(ops.Handle(None, "root", None), "child", None), "root/child"),
+            (ops.Handle(ops.Handle(None, "root", "1"), "child", "2"), "root[1]/child[2]"),
         ]
         for handle, path in cases:
             self.assertEqual(str(handle), path)
-            self.assertEqual(Handle.from_path(path), handle)
+            self.assertEqual(ops.Handle.from_path(path), handle)
 
     def test_handle_attrs_readonly(self):
-        handle = Handle(None, 'kind', 'key')
+        handle = ops.Handle(None, 'kind', 'key')
         with self.assertRaises(AttributeError):
             handle.parent = 'foo'
         with self.assertRaises(AttributeError):
@@ -92,10 +75,10 @@ def test_handle_attrs_readonly(self):
     def test_restore_unknown(self):
         framework = self.create_framework()
 
-        class Foo(Object):
+        class Foo(ops.Object):
             pass
 
-        handle = Handle(None, "a_foo", "some_key")
+        handle = ops.Handle(None, "a_foo", "some_key")
 
         framework.register_type(Foo, None, handle.kind)
 
@@ -119,7 +102,7 @@ def snapshot(self):
             def restore(self, snapshot):
                 self.my_n = snapshot["My N!"] + 1
 
-        handle = Handle(None, "a_foo", "some_key")
+        handle = ops.Handle(None, "a_foo", "some_key")
         event = Foo(handle, 1)
 
         framework1 = self.create_framework(tmpdir=self.tmpdir)
@@ -151,15 +134,15 @@ def restore(self, snapshot):
     def test_simple_event_observer(self):
         framework = self.create_framework()
 
-        class MyEvent(EventBase):
+        class MyEvent(ops.EventBase):
             pass
 
-        class MyNotifier(Object):
-            foo = EventSource(MyEvent)
-            bar = EventSource(MyEvent)
-            baz = EventSource(MyEvent)
+        class MyNotifier(ops.Object):
+            foo = ops.EventSource(MyEvent)
+            bar = ops.EventSource(MyEvent)
+            baz = ops.EventSource(MyEvent)
 
-        class MyObserver(Object):
+        class MyObserver(ops.Object):
             def __init__(self, parent, key):
                 super().__init__(parent, key)
                 self.seen = []
@@ -193,16 +176,16 @@ def on_foo(self, event):
 
     def test_bad_sig_observer(self):
 
-        class MyEvent(EventBase):
+        class MyEvent(ops.EventBase):
             pass
 
-        class MyNotifier(Object):
-            foo = EventSource(MyEvent)
-            bar = EventSource(MyEvent)
-            baz = EventSource(MyEvent)
-            qux = EventSource(MyEvent)
+        class MyNotifier(ops.Object):
+            foo = ops.EventSource(MyEvent)
+            bar = ops.EventSource(MyEvent)
+            baz = ops.EventSource(MyEvent)
+            qux = ops.EventSource(MyEvent)
 
-        class MyObserver(Object):
+        class MyObserver(ops.Object):
             def _on_foo(self):
                 assert False, 'should not be reached'
 
@@ -230,9 +213,9 @@ def _on_qux(self, event, extra=None):
     def test_on_pre_commit_emitted(self):
         framework = self.create_framework(tmpdir=self.tmpdir)
 
-        class PreCommitObserver(Object):
+        class PreCommitObserver(ops.Object):
 
-            _stored = StoredState()
+            _stored = ops.StoredState()
 
             def __init__(self, parent, key):
                 super().__init__(parent, key)
@@ -259,7 +242,7 @@ def on_commit(self, event):
 
         self.assertEqual(obs._stored.myinitdata, 41)
         self.assertEqual(obs._stored.mydata, 42)
-        self.assertTrue(obs.seen, [PreCommitEvent, CommitEvent])
+        self.assertTrue(obs.seen, [ops.PreCommitEvent, ops.CommitEvent])
         framework.close()
 
         other_framework = self.create_framework(tmpdir=self.tmpdir)
@@ -275,17 +258,17 @@ def on_commit(self, event):
     def test_defer_and_reemit(self):
         framework = self.create_framework()
 
-        class MyEvent(EventBase):
+        class MyEvent(ops.EventBase):
             pass
 
-        class MyNotifier1(Object):
-            a = EventSource(MyEvent)
-            b = EventSource(MyEvent)
+        class MyNotifier1(ops.Object):
+            a = ops.EventSource(MyEvent)
+            b = ops.EventSource(MyEvent)
 
-        class MyNotifier2(Object):
-            c = EventSource(MyEvent)
+        class MyNotifier2(ops.Object):
+            c = ops.EventSource(MyEvent)
 
-        class MyObserver(Object):
+        class MyObserver(ops.Object):
             def __init__(self, parent, key):
                 super().__init__(parent, key)
                 self.seen = []
@@ -312,11 +295,11 @@ def on_any(self, event):
         pub2.c.emit()
 
         # Events remain stored because they were deferred.
-        ev_a_handle = Handle(pub1, "a", "1")
+        ev_a_handle = ops.Handle(pub1, "a", "1")
         framework.load_snapshot(ev_a_handle)
-        ev_b_handle = Handle(pub1, "b", "2")
+        ev_b_handle = ops.Handle(pub1, "b", "2")
         framework.load_snapshot(ev_b_handle)
-        ev_c_handle = Handle(pub2, "c", "3")
+        ev_c_handle = ops.Handle(pub2, "c", "3")
         framework.load_snapshot(ev_c_handle)
         # make sure the objects are gone before we reemit them
         gc.collect()
@@ -345,7 +328,7 @@ def on_any(self, event):
     def test_custom_event_data(self):
         framework = self.create_framework()
 
-        class MyEvent(EventBase):
+        class MyEvent(ops.EventBase):
             def __init__(self, handle, n):
                 super().__init__(handle)
                 self.my_n = n
@@ -357,10 +340,10 @@ def restore(self, snapshot):
                 super().restore(snapshot)
                 self.my_n = snapshot["My N!"] + 1
 
-        class MyNotifier(Object):
-            foo = EventSource(MyEvent)
+        class MyNotifier(ops.Object):
+            foo = ops.EventSource(MyEvent)
 
-        class MyObserver(Object):
+        class MyObserver(ops.Object):
             def __init__(self, parent, key):
                 super().__init__(parent, key)
                 self.seen = []
@@ -399,16 +382,16 @@ def test_weak_observer(self):
 
         observed_events = []
 
-        class MyEvent(EventBase):
+        class MyEvent(ops.EventBase):
             pass
 
-        class MyEvents(ObjectEvents):
-            foo = EventSource(MyEvent)
+        class MyEvents(ops.ObjectEvents):
+            foo = ops.EventSource(MyEvent)
 
-        class MyNotifier(Object):
+        class MyNotifier(ops.Object):
             on = MyEvents()
 
-        class MyObserver(Object):
+        class MyObserver(ops.Object):
             def _on_foo(self, event):
                 observed_events.append("foo")
 
@@ -428,7 +411,7 @@ def _on_foo(self, event):
     def test_forget_and_multiple_objects(self):
         framework = self.create_framework()
 
-        class MyObject(Object):
+        class MyObject(ops.Object):
             pass
 
         o1 = MyObject(framework, "path")
@@ -453,7 +436,7 @@ class MyObject(Object):
     def test_forget_and_multiple_objects_with_load_snapshot(self):
         framework = self.create_framework(tmpdir=self.tmpdir)
 
-        class MyObject(Object):
+        class MyObject(ops.Object):
             def __init__(self, parent, name):
                 super().__init__(parent, name)
                 self.value = name
@@ -496,17 +479,17 @@ def restore(self, value):
     def test_events_base(self):
         framework = self.create_framework()
 
-        class MyEvent(EventBase):
+        class MyEvent(ops.EventBase):
             pass
 
-        class MyEvents(ObjectEvents):
-            foo = EventSource(MyEvent)
-            bar = EventSource(MyEvent)
+        class MyEvents(ops.ObjectEvents):
+            foo = ops.EventSource(MyEvent)
+            bar = ops.EventSource(MyEvent)
 
-        class MyNotifier(Object):
+        class MyNotifier(ops.Object):
             on = MyEvents()
 
-        class MyObserver(Object):
+        class MyObserver(ops.Object):
             def __init__(self, parent, key):
                 super().__init__(parent, key)
                 self.seen = []
@@ -534,23 +517,23 @@ def _on_bar(self, event):
         self.assertEqual(repr(pub.on), f"<{fqn}: bar, foo>")
 
     def test_conflicting_event_attributes(self):
-        class MyEvent(EventBase):
+        class MyEvent(ops.EventBase):
             pass
 
-        event = EventSource(MyEvent)
+        event = ops.EventSource(MyEvent)
 
-        class MyEvents(ObjectEvents):
+        class MyEvents(ops.ObjectEvents):
             foo = event
 
         with self.assertRaises(RuntimeError) as cm:
-            class OtherEvents(ObjectEvents):
+            class OtherEvents(ops.ObjectEvents):
                 foo = event
         self.assertEqual(
             str(cm.exception.__cause__),
             "EventSource(MyEvent) reused as MyEvents.foo and OtherEvents.foo")
 
         with self.assertRaises(RuntimeError) as cm:
-            class MyNotifier(Object):
+            class MyNotifier(ops.Object):
                 on = MyEvents()
                 bar = event
         self.assertEqual(
@@ -563,13 +546,13 @@ def test_reemit_ignores_unknown_event_type(self):
 
         framework = self.create_framework()
 
-        class MyEvent(EventBase):
+        class MyEvent(ops.EventBase):
             pass
 
-        class MyNotifier(Object):
-            foo = EventSource(MyEvent)
+        class MyNotifier(ops.Object):
+            foo = ops.EventSource(MyEvent)
 
-        class MyObserver(Object):
+        class MyObserver(ops.Object):
             def __init__(self, parent, key):
                 super().__init__(parent, key)
                 self.seen = []
@@ -602,20 +585,20 @@ def _on_foo(self, event):
     def test_auto_register_event_types(self):
         framework = self.create_framework()
 
-        class MyFoo(EventBase):
+        class MyFoo(ops.EventBase):
             pass
 
-        class MyBar(EventBase):
+        class MyBar(ops.EventBase):
             pass
 
-        class MyEvents(ObjectEvents):
-            foo = EventSource(MyFoo)
+        class MyEvents(ops.ObjectEvents):
+            foo = ops.EventSource(MyFoo)
 
-        class MyNotifier(Object):
+        class MyNotifier(ops.Object):
             on = MyEvents()
-            bar = EventSource(MyBar)
+            bar = ops.EventSource(MyBar)
 
-        class MyObserver(Object):
+        class MyObserver(ops.Object):
             def __init__(self, parent, key):
                 super().__init__(parent, key)
                 self.seen = []
@@ -645,17 +628,17 @@ def _on_bar(self, event):
     def test_dynamic_event_types(self):
         framework = self.create_framework()
 
-        class MyEventsA(ObjectEvents):
+        class MyEventsA(ops.ObjectEvents):
             handle_kind = 'on_a'
 
-        class MyEventsB(ObjectEvents):
+        class MyEventsB(ops.ObjectEvents):
             handle_kind = 'on_b'
 
-        class MyNotifier(Object):
+        class MyNotifier(ops.Object):
             on_a = MyEventsA()
             on_b = MyEventsB()
 
-        class MyObserver(Object):
+        class MyObserver(ops.Object):
             def __init__(self, parent, key):
                 super().__init__(parent, key)
                 self.seen = []
@@ -671,16 +654,16 @@ def _on_bar(self, event):
         pub = MyNotifier(framework, "1")
         obs = MyObserver(framework, "1")
 
-        class MyFoo(EventBase):
+        class MyFoo(ops.EventBase):
             pass
 
-        class MyBar(EventBase):
+        class MyBar(ops.EventBase):
             pass
 
-        class DeadBeefEvent(EventBase):
+        class DeadBeefEvent(ops.EventBase):
             pass
 
-        class NoneEvent(EventBase):
+        class NoneEvent(ops.EventBase):
             pass
 
         pub.on_a.define_event("foo", MyFoo)
@@ -711,7 +694,7 @@ class NoneEvent(EventBase):
             pub.on_a.define_event("foo", MyFoo)
 
     def test_event_key_roundtrip(self):
-        class MyEvent(EventBase):
+        class MyEvent(ops.EventBase):
             def __init__(self, handle, value):
                 super().__init__(handle)
                 self.value = value
@@ -722,10 +705,10 @@ def snapshot(self):
             def restore(self, value):
                 self.value = value
 
-        class MyNotifier(Object):
-            foo = EventSource(MyEvent)
+        class MyNotifier(ops.Object):
+            foo = ops.EventSource(MyEvent)
 
-        class MyObserver(Object):
+        class MyObserver(ops.Object):
             has_deferred = False
 
             def __init__(self, parent, key):
@@ -768,7 +751,7 @@ def test_helper_properties(self):
         framework.model = 'test-model'
         framework.meta = 'test-meta'
 
-        my_obj = Object(framework, 'my_obj')
+        my_obj = ops.Object(framework, 'my_obj')
         self.assertEqual(my_obj.model, framework.model)
 
     def test_ban_concurrent_frameworks(self):
@@ -782,11 +765,11 @@ def test_snapshot_saving_restricted_to_simple_types(self):
         # this can not be saved, as it has not simple types!
         to_be_saved = {"bar": TestFramework}
 
-        class FooEvent(EventBase):
+        class FooEvent(ops.EventBase):
             def snapshot(self):
                 return to_be_saved
 
-        handle = Handle(None, "a_foo", "some_key")
+        handle = ops.Handle(None, "a_foo", "some_key")
         event = FooEvent(handle)
 
         framework = self.create_framework()
@@ -799,20 +782,20 @@ def snapshot(self):
         self.assertEqual(str(cm.exception), expected)
 
     def test_unobserved_events_dont_leave_cruft(self):
-        class FooEvent(EventBase):
+        class FooEvent(ops.EventBase):
             def snapshot(self):
                 return {'content': 1}
 
-        class Events(ObjectEvents):
-            foo = EventSource(FooEvent)
+        class Events(ops.ObjectEvents):
+            foo = ops.EventSource(FooEvent)
 
-        class Emitter(Object):
+        class Emitter(ops.Object):
             on = Events()
 
         framework = self.create_framework()
         e = Emitter(framework, 'key')
         e.on.foo.emit()
-        ev_1_handle = Handle(e.on, "foo", "1")
+        ev_1_handle = ops.Handle(e.on, "foo", "1")
         with self.assertRaises(NoSnapshotError):
             framework.load_snapshot(ev_1_handle)
         # Committing will save the framework's state, but no other snapshots should be saved
@@ -839,14 +822,14 @@ def test_event_regex(self):
     def test_remove_unreferenced_events(self):
         framework = self.create_framework()
 
-        class Evt(EventBase):
+        class Evt(ops.EventBase):
             pass
 
-        class Events(ObjectEvents):
-            event = EventSource(Evt)
+        class Events(ops.ObjectEvents):
+            event = ops.EventSource(Evt)
 
-        class ObjectWithStorage(Object):
-            _stored = StoredState()
+        class ObjectWithStorage(ops.Object):
+            _stored = ops.StoredState()
             on = Events()
 
             def __init__(self, framework, key):
@@ -859,7 +842,7 @@ def _on_event(self, event):
 
         # This is an event that 'happened in the past' that doesn't have an associated notice.
         o = ObjectWithStorage(framework, 'obj')
-        handle = Handle(o.on, 'event', '100')
+        handle = ops.Handle(o.on, 'event', '100')
         event = Evt(handle)
         framework.save_snapshot(event)
         self.assertEqual(list(framework._storage.list_snapshots()), [handle.path])
@@ -890,26 +873,34 @@ def setUp(self):
         self.addCleanup(shutil.rmtree, str(self.tmpdir))
 
     def test_stored_dict_repr(self):
-        self.assertEqual(repr(StoredDict(None, {})), "ops.framework.StoredDict()")
-        self.assertEqual(repr(StoredDict(None, {"a": 1})), "ops.framework.StoredDict({'a': 1})")
+        self.assertEqual(repr(ops.StoredDict(None, {})), "ops.framework.StoredDict()")
+        self.assertEqual(
+            repr(
+                ops.StoredDict(
+                    None, {
+                        "a": 1})), "ops.framework.StoredDict({'a': 1})")
 
     def test_stored_list_repr(self):
-        self.assertEqual(repr(StoredList(None, [])), "ops.framework.StoredList()")
-        self.assertEqual(repr(StoredList(None, [1, 2, 3])), 'ops.framework.StoredList([1, 2, 3])')
+        self.assertEqual(repr(ops.StoredList(None, [])), "ops.framework.StoredList()")
+        self.assertEqual(
+            repr(
+                ops.StoredList(
+                    None, [
+                        1, 2, 3])), 'ops.framework.StoredList([1, 2, 3])')
 
     def test_stored_set_repr(self):
-        self.assertEqual(repr(StoredSet(None, set())), 'ops.framework.StoredSet()')
-        self.assertEqual(repr(StoredSet(None, {1})), 'ops.framework.StoredSet({1})')
+        self.assertEqual(repr(ops.StoredSet(None, set())), 'ops.framework.StoredSet()')
+        self.assertEqual(repr(ops.StoredSet(None, {1})), 'ops.framework.StoredSet({1})')
 
     def test_basic_state_storage(self):
-        class SomeObject(Object):
-            _stored = StoredState()
+        class SomeObject(ops.Object):
+            _stored = ops.StoredState()
 
         self._stored_state_tests(SomeObject)
 
     def test_straight_subclass(self):
-        class SomeObject(Object):
-            _stored = StoredState()
+        class SomeObject(ops.Object):
+            _stored = ops.StoredState()
 
         class Sub(SomeObject):
             pass
@@ -917,8 +908,8 @@ class Sub(SomeObject):
         self._stored_state_tests(Sub)
 
     def test_straight_sub_subclass(self):
-        class SomeObject(Object):
-            _stored = StoredState()
+        class SomeObject(ops.Object):
+            _stored = ops.StoredState()
 
         class Sub(SomeObject):
             pass
@@ -929,8 +920,8 @@ class SubSub(SomeObject):
         self._stored_state_tests(SubSub)
 
     def test_two_subclasses(self):
-        class SomeObject(Object):
-            _stored = StoredState()
+        class SomeObject(ops.Object):
+            _stored = ops.StoredState()
 
         class SubA(SomeObject):
             pass
@@ -942,11 +933,11 @@ class SubB(SomeObject):
         self._stored_state_tests(SubB)
 
     def test_the_crazy_thing(self):
-        class NoState(Object):
+        class NoState(ops.Object):
             pass
 
         class StatedObject(NoState):
-            _stored = StoredState()
+            _stored = ops.StoredState()
 
         class Sibling(NoState):
             pass
@@ -1000,8 +991,8 @@ def _stored_state_tests(self, cls):
         framework_copy.close()
 
     def test_two_subclasses_no_conflicts(self):
-        class Base(Object):
-            _stored = StoredState()
+        class Base(ops.Object):
+            _stored = ops.StoredState()
 
         class SubA(Base):
             pass
@@ -1031,8 +1022,8 @@ class SubB(Base):
         self.assertEqual(z2._stored.foo, {1})
 
     def test_two_names_one_state(self):
-        class Mine(Object):
-            _stored = StoredState()
+        class Mine(ops.Object):
+            _stored = ops.StoredState()
             _stored2 = _stored
 
         framework = self.create_framework()
@@ -1051,11 +1042,11 @@ class Mine(Object):
         self.assertNotIn("_stored2", obj.__dict__)
 
     def test_same_name_two_classes(self):
-        class Base(Object):
+        class Base(ops.Object):
             pass
 
         class A(Base):
-            _stored = StoredState()
+            _stored = ops.StoredState()
 
         class B(Base):
             _stored = A._stored
@@ -1079,8 +1070,8 @@ class B(Base):
     def test_mutable_types_invalid(self):
         framework = self.create_framework()
 
-        class SomeObject(Object):
-            _stored = StoredState()
+        class SomeObject(ops.Object):
+            _stored = ops.StoredState()
 
         obj = SomeObject(framework, '1')
         try:
@@ -1161,7 +1152,7 @@ def test_mutable_types(self):
             lambda a, b: a.append(b),
             lambda res, expected_res: (
                 self.assertEqual(res, expected_res),
-                self.assertIsInstance(res[1], StoredList),
+                self.assertIsInstance(res[1], ops.StoredList),
             )
         ), (
             lambda: ['a', ['c']],
@@ -1176,7 +1167,7 @@ def test_mutable_types(self):
             lambda a, b: a.insert(1, b),
             lambda res, expected_res: (
                 self.assertEqual(res, expected_res),
-                self.assertIsInstance(res[1], StoredList)
+                self.assertIsInstance(res[1], ops.StoredList)
             ),
         ), (
             lambda: ['b', 'a', ['c']],
@@ -1186,7 +1177,7 @@ def test_mutable_types(self):
             lambda a, b: a.__setitem__(1, b),
             lambda res, expected_res: (
                 self.assertEqual(res, expected_res),
-                self.assertIsInstance(res[1], StoredList)
+                self.assertIsInstance(res[1], ops.StoredList)
             ),
         ), (
             lambda: ['b', ['d'], 'a', ['c']],
@@ -1251,10 +1242,10 @@ def test_mutable_types(self):
             lambda res, expected_res: self.assertEqual(res, expected_res)
         )]
 
-        class SomeObject(Object):
-            _stored = StoredState()
+        class SomeObject(ops.Object):
+            _stored = ops.StoredState()
 
-        class WrappedFramework(Framework):
+        class WrappedFramework(ops.Framework):
             def __init__(self, store, charm_dir, meta, model, event_name):
                 super().__init__(store, charm_dir, meta, model, event_name)
                 self.snapshots = []
@@ -1271,7 +1262,7 @@ def save_snapshot(self, value):
             obj = SomeObject(framework, '1')
 
             obj._stored.a = get_a()
-            self.assertTrue(isinstance(obj._stored, BoundStoredState))
+            self.assertTrue(isinstance(obj._stored, ops.BoundStoredState))
 
             op(obj._stored.a, b)
             validate_op(obj._stored.a, expected_res)
@@ -1280,7 +1271,7 @@ def save_snapshot(self, value):
             framework.commit()
             # We should see an update for initializing a
             self.assertEqual(framework.snapshots, [
-                (StoredStateData, {'a': get_a()}),
+                (ops.StoredStateData, {'a': get_a()}),
             ])
             del obj
             gc.collect()
@@ -1388,8 +1379,8 @@ def test_comparison_operations(self):
             False
         )]
 
-        class SomeObject(Object):
-            _stored = StoredState()
+        class SomeObject(ops.Object):
+            _stored = ops.StoredState()
 
         framework = self.create_framework()
 
@@ -1427,8 +1418,8 @@ def test_set_operations(self):
             set()
         )]
 
-        class SomeObject(Object):
-            _stored = StoredState()
+        class SomeObject(ops.Object):
+            _stored = ops.StoredState()
 
         framework = self.create_framework()
 
@@ -1459,8 +1450,8 @@ class SomeObject(Object):
     def test_set_default(self):
         framework = self.create_framework()
 
-        class StatefulObject(Object):
-            _stored = StoredState()
+        class StatefulObject(ops.Object):
+            _stored = ops.StoredState()
         parent = StatefulObject(framework, 'key')
         parent._stored.set_default(foo=1)
         self.assertEqual(parent._stored.foo, 1)
@@ -1481,7 +1472,7 @@ class StatefulObject(Object):
         #       parent._stored._data.dirty is False?
 
 
-class GenericObserver(Object):
+class GenericObserver(ops.Object):
     """Generic observer for the tests."""
 
     def __init__(self, parent, key):
@@ -1719,7 +1710,7 @@ def test_basic_interruption_enabled(self):
         framework = self.create_framework()
         framework._juju_debug_at = {'hook'}
 
-        publisher = charm.CharmEvents(framework, "1")
+        publisher = ops.CharmEvents(framework, "1")
         observer = GenericObserver(framework, "1")
         framework.observe(publisher.install, observer.callback_method)
 
@@ -1733,7 +1724,7 @@ def test_basic_interruption_enabled(self):
         self.assertEqual(mock.call_count, 1)
         expected_callback, expected_event = mock.call_args[0]
         self.assertEqual(expected_callback, observer.callback_method)
-        self.assertIsInstance(expected_event, EventBase)
+        self.assertIsInstance(expected_event, ops.EventBase)
         self.assertFalse(observer.called)
 
         # Verify proper message was given to the user.
@@ -1744,8 +1735,8 @@ def test_interruption_enabled_with_all(self):
         framework = self.create_framework(model=test_model)
         framework._juju_debug_at = {'all'}
 
-        class CustomEvents(ObjectEvents):
-            foobar_action = EventSource(charm.ActionEvent)
+        class CustomEvents(ops.ObjectEvents):
+            foobar_action = ops.EventSource(ops.ActionEvent)
 
         publisher = CustomEvents(framework, "1")
         observer = GenericObserver(framework, "1")
@@ -1765,8 +1756,8 @@ def test_actions_are_interrupted(self):
         framework = self.create_framework(model=test_model)
         framework._juju_debug_at = {'hook'}
 
-        class CustomEvents(ObjectEvents):
-            foobar_action = EventSource(charm.ActionEvent)
+        class CustomEvents(ops.ObjectEvents):
+            foobar_action = ops.EventSource(ops.ActionEvent)
 
         publisher = CustomEvents(framework, "1")
         observer = GenericObserver(framework, "1")
@@ -1782,9 +1773,9 @@ class CustomEvents(ObjectEvents):
         self.assertFalse(observer.called)
 
     def test_internal_events_not_interrupted(self):
-        class MyNotifier(Object):
+        class MyNotifier(ops.Object):
             """Generic notifier for the tests."""
-            bar = EventSource(EventBase)
+            bar = ops.EventSource(ops.EventBase)
 
         framework = self.create_framework()
         framework._juju_debug_at = {'hook'}
@@ -1803,7 +1794,7 @@ def test_envvar_mixed(self):
         framework = self.create_framework()
         framework._juju_debug_at = {'foo', 'hook', 'all', 'whatever'}
 
-        publisher = charm.CharmEvents(framework, "1")
+        publisher = ops.CharmEvents(framework, "1")
         observer = GenericObserver(framework, "1")
         framework.observe(publisher.install, observer.callback_method)
 
@@ -1818,7 +1809,7 @@ def test_no_registered_method(self):
         framework = self.create_framework()
         framework._juju_debug_at = {'hook'}
 
-        publisher = charm.CharmEvents(framework, "1")
+        publisher = ops.CharmEvents(framework, "1")
         observer = GenericObserver(framework, "1")
 
         with patch('pdb.runcall') as mock:
@@ -1831,7 +1822,7 @@ def test_envvar_nohook(self):
         framework = self.create_framework()
         framework._juju_debug_at = {'something-else'}
 
-        publisher = charm.CharmEvents(framework, "1")
+        publisher = ops.CharmEvents(framework, "1")
         observer = GenericObserver(framework, "1")
         framework.observe(publisher.install, observer.callback_method)
 
@@ -1846,7 +1837,7 @@ def test_envvar_missing(self):
         framework = self.create_framework()
         framework._juju_debug_at = set()
 
-        publisher = charm.CharmEvents(framework, "1")
+        publisher = ops.CharmEvents(framework, "1")
         observer = GenericObserver(framework, "1")
         framework.observe(publisher.install, observer.callback_method)
 
@@ -1860,7 +1851,7 @@ def test_welcome_message_not_multiple(self):
         framework = self.create_framework()
         framework._juju_debug_at = {'hook'}
 
-        publisher = charm.CharmEvents(framework, "1")
+        publisher = ops.CharmEvents(framework, "1")
         observer = GenericObserver(framework, "1")
         framework.observe(publisher.install, observer.callback_method)
 
diff --git a/test/test_helpers.py b/test/test_helpers.py
index 83ae8cd09..34f951e57 100755
--- a/test/test_helpers.py
+++ b/test/test_helpers.py
@@ -19,9 +19,8 @@
 import tempfile
 import unittest
 
-from ops.charm import CharmMeta
-from ops.framework import Framework
-from ops.model import Model, _ModelBackend
+import ops
+from ops.model import _ModelBackend
 from ops.storage import SQLiteStorage
 
 
@@ -120,7 +119,7 @@ def create_framework(self, *, model=None, tmpdir=None):
             data_fpath = tmpdir / "framework.data"
             charm_dir = tmpdir
 
-        framework = Framework(
+        framework = ops.Framework(
             SQLiteStorage(data_fpath),
             charm_dir,
             meta=None,
@@ -131,6 +130,6 @@ def create_framework(self, *, model=None, tmpdir=None):
     def create_model(self):
         """Create a Model object."""
         backend = _ModelBackend(unit_name='myapp/0')
-        meta = CharmMeta()
-        model = Model(meta, backend)
+        meta = ops.CharmMeta()
+        model = ops.Model(meta, backend)
         return model
diff --git a/test/test_jujuversion.py b/test/test_jujuversion.py
index 542b81ea6..0d180a633 100755
--- a/test/test_jujuversion.py
+++ b/test/test_jujuversion.py
@@ -15,7 +15,7 @@
 import os
 import unittest.mock  # in this file, importing just 'patch' would be confusing
 
-from ops.jujuversion import JujuVersion
+import ops
 
 
 class TestJujuVersion(unittest.TestCase):
@@ -34,7 +34,7 @@ def test_parsing(self):
         ]
 
         for vs, major, minor, tag, patch, build in test_cases:
-            v = JujuVersion(vs)
+            v = ops.JujuVersion(vs)
             self.assertEqual(v.major, major)
             self.assertEqual(v.minor, minor)
             self.assertEqual(v.tag, tag)
@@ -44,34 +44,34 @@ def test_parsing(self):
     @unittest.mock.patch('os.environ', new={})
     def test_from_environ(self):
         # JUJU_VERSION is not set
-        v = JujuVersion.from_environ()
-        self.assertEqual(v, JujuVersion('0.0.0'))
+        v = ops.JujuVersion.from_environ()
+        self.assertEqual(v, ops.JujuVersion('0.0.0'))
 
         os.environ['JUJU_VERSION'] = 'no'
         with self.assertRaisesRegex(RuntimeError, 'not a valid Juju version'):
-            JujuVersion.from_environ()
+            ops.JujuVersion.from_environ()
 
         os.environ['JUJU_VERSION'] = '2.8.0'
-        v = JujuVersion.from_environ()
-        self.assertEqual(v, JujuVersion('2.8.0'))
+        v = ops.JujuVersion.from_environ()
+        self.assertEqual(v, ops.JujuVersion('2.8.0'))
 
     def test_has_app_data(self):
-        self.assertTrue(JujuVersion('2.8.0').has_app_data())
-        self.assertTrue(JujuVersion('2.7.0').has_app_data())
-        self.assertFalse(JujuVersion('2.6.9').has_app_data())
+        self.assertTrue(ops.JujuVersion('2.8.0').has_app_data())
+        self.assertTrue(ops.JujuVersion('2.7.0').has_app_data())
+        self.assertFalse(ops.JujuVersion('2.6.9').has_app_data())
 
     def test_is_dispatch_aware(self):
-        self.assertTrue(JujuVersion('2.8.0').is_dispatch_aware())
-        self.assertFalse(JujuVersion('2.7.9').is_dispatch_aware())
+        self.assertTrue(ops.JujuVersion('2.8.0').is_dispatch_aware())
+        self.assertFalse(ops.JujuVersion('2.7.9').is_dispatch_aware())
 
     def test_has_controller_storage(self):
-        self.assertTrue(JujuVersion('2.8.0').has_controller_storage())
-        self.assertFalse(JujuVersion('2.7.9').has_controller_storage())
+        self.assertTrue(ops.JujuVersion('2.8.0').has_controller_storage())
+        self.assertFalse(ops.JujuVersion('2.7.9').has_controller_storage())
 
     def test_has_secrets(self):
-        self.assertTrue(JujuVersion('3.0.2').has_secrets)
-        self.assertFalse(JujuVersion('3.0.1').has_secrets)
-        self.assertFalse(JujuVersion('2.9.30').has_secrets)
+        self.assertTrue(ops.JujuVersion('3.0.2').has_secrets)
+        self.assertFalse(ops.JujuVersion('3.0.1').has_secrets)
+        self.assertFalse(ops.JujuVersion('2.9.30').has_secrets)
 
     def test_parsing_errors(self):
         invalid_versions = [
@@ -90,7 +90,7 @@ def test_parsing_errors(self):
         ]
         for v in invalid_versions:
             with self.assertRaises(RuntimeError):
-                JujuVersion(v)
+                ops.JujuVersion(v)
 
     def test_equality(self):
         test_cases = [
@@ -118,8 +118,8 @@ def test_equality(self):
         ]
 
         for a, b, expected in test_cases:
-            self.assertEqual(JujuVersion(a) == JujuVersion(b), expected)
-            self.assertEqual(JujuVersion(a) == b, expected)
+            self.assertEqual(ops.JujuVersion(a) == ops.JujuVersion(b), expected)
+            self.assertEqual(ops.JujuVersion(a) == b, expected)
 
     def test_comparison(self):
         test_cases = [
@@ -149,12 +149,12 @@ def test_comparison(self):
 
         for a, b, expected_strict, expected_weak in test_cases:
             with self.subTest(a=a, b=b):
-                self.assertEqual(JujuVersion(a) < JujuVersion(b), expected_strict)
-                self.assertEqual(JujuVersion(a) <= JujuVersion(b), expected_weak)
-                self.assertEqual(JujuVersion(b) > JujuVersion(a), expected_strict)
-                self.assertEqual(JujuVersion(b) >= JujuVersion(a), expected_weak)
+                self.assertEqual(ops.JujuVersion(a) < ops.JujuVersion(b), expected_strict)
+                self.assertEqual(ops.JujuVersion(a) <= ops.JujuVersion(b), expected_weak)
+                self.assertEqual(ops.JujuVersion(b) > ops.JujuVersion(a), expected_strict)
+                self.assertEqual(ops.JujuVersion(b) >= ops.JujuVersion(a), expected_weak)
                 # Implicit conversion.
-                self.assertEqual(JujuVersion(a) < b, expected_strict)
-                self.assertEqual(JujuVersion(a) <= b, expected_weak)
-                self.assertEqual(b > JujuVersion(a), expected_strict)
-                self.assertEqual(b >= JujuVersion(a), expected_weak)
+                self.assertEqual(ops.JujuVersion(a) < b, expected_strict)
+                self.assertEqual(ops.JujuVersion(a) <= b, expected_weak)
+                self.assertEqual(b > ops.JujuVersion(a), expected_strict)
+                self.assertEqual(b >= ops.JujuVersion(a), expected_weak)
diff --git a/test/test_main.py b/test/test_main.py
index 6e49297d9..5c81d827f 100755
--- a/test/test_main.py
+++ b/test/test_main.py
@@ -28,37 +28,9 @@
 
 import logassert
 
-from ops.charm import (
-    ActionEvent,
-    CharmBase,
-    CharmEvents,
-    CharmMeta,
-    CollectMetricsEvent,
-    ConfigChangedEvent,
-    HookEvent,
-    InstallEvent,
-    LeaderSettingsChangedEvent,
-    PebbleReadyEvent,
-    RelationBrokenEvent,
-    RelationChangedEvent,
-    RelationDepartedEvent,
-    RelationEvent,
-    RelationJoinedEvent,
-    SecretChangedEvent,
-    SecretEvent,
-    SecretExpiredEvent,
-    SecretRemoveEvent,
-    SecretRotateEvent,
-    StartEvent,
-    StorageAttachedEvent,
-    UpdateStatusEvent,
-    UpgradeCharmEvent,
-    WorkloadEvent,
-)
-from ops.framework import Framework, LifecycleEvent, StoredStateData
-from ops.main import CHARM_STATE_FILE, _should_use_controller_storage, main
+import ops
+from ops.main import CHARM_STATE_FILE, _should_use_controller_storage
 from ops.storage import SQLiteStorage
-from ops.version import version
 
 from .charms.test_main.src.charm import MyCharmEvents
 from .test_helpers import fake_script, fake_script_calls
@@ -69,7 +41,7 @@
 
 VERSION_LOGLINE = [
     'juju-log', '--log-level', 'DEBUG', '--',
-    f'Operator Framework {version} up and running.',
+    f'Operator Framework {ops.__version__} up and running.',
 ]
 
 logger = logging.getLogger(__name__)
@@ -105,7 +77,7 @@ class CharmInitTestCase(unittest.TestCase):
 
     @patch('sys.stderr', new_callable=io.StringIO)
     def test_breakpoint(self, fake_stderr):
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             pass
         self._check(MyCharm, extra_environ={'JUJU_DEBUG_AT': 'all'})
 
@@ -116,7 +88,7 @@ class MyCharm(CharmBase):
         self.assertIn('Starting pdb to debug charm operator', fake_stderr.getvalue())
 
     def test_no_debug_breakpoint(self):
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             pass
         self._check(MyCharm, extra_environ={'JUJU_DEBUG_AT': ''})
 
@@ -144,10 +116,10 @@ def _check(self, charm_class, *, extra_environ=None, **kwargs):
                             fh.write(b'name: test')
                         mock_charmdir.return_value = tmpdirname
 
-                        main(charm_class, **kwargs)
+                        ops.main(charm_class, **kwargs)
 
     def test_init_signature_passthrough(self):
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
 
             def __init__(self, *args):
                 super().__init__(*args)
@@ -157,7 +129,7 @@ def __init__(self, *args):
         self.assertEqual(warn_cm, [])
 
     def test_init_signature_old_key_argument(self):
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
 
             def __init__(self, framework, somekey):
                 super().__init__(framework, somekey)
@@ -167,7 +139,7 @@ def __init__(self, framework, somekey):
             self._check(MyCharm)
 
     def test_init_signature_only_framework(self):
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
 
             def __init__(self, framework):
                 super().__init__(framework)
@@ -183,21 +155,21 @@ def test_storage_no_storage(self):
             with self.assertRaisesRegex(
                     RuntimeError,
                     'charm set use_juju_for_storage=True, but Juju .* does not support it'):
-                self._check(CharmBase, use_juju_for_storage=True)
+                self._check(ops.CharmBase, use_juju_for_storage=True)
 
     def test_storage_with_storage(self):
         # here we patch juju_backend_available, so it gets set up and falls over when used
         with patch('ops.storage.juju_backend_available') as juju_backend_available:
             juju_backend_available.return_value = True
             with self.assertRaisesRegex(FileNotFoundError, 'state-get'):
-                self._check(CharmBase, use_juju_for_storage=True)
+                self._check(ops.CharmBase, use_juju_for_storage=True)
 
     def test_controller_storage_deprecated(self):
         with patch('ops.storage.juju_backend_available') as juju_backend_available:
             juju_backend_available.return_value = True
             with self.assertWarnsRegex(DeprecationWarning, 'Controller storage'):
                 with self.assertRaisesRegex(FileNotFoundError, 'state-get'):
-                    self._check(CharmBase, use_juju_for_storage=True)
+                    self._check(ops.CharmBase, use_juju_for_storage=True)
 
 
 @patch('sys.argv', new=("hooks/config-changed",))
@@ -205,7 +177,7 @@ def test_controller_storage_deprecated(self):
 class TestDispatch(unittest.TestCase):
     def _check(self, *, with_dispatch=False, dispatch_path=''):
         """Helper for below tests."""
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             def __init__(self, framework):
                 super().__init__(framework)
 
@@ -233,7 +205,7 @@ def __init__(self, framework):
                 with patch('ops.main._emit_charm_event') as mock_charm_event:
                     with patch('ops.main._get_charm_dir') as mock_charmdir:
                         mock_charmdir.return_value = tmpdir
-                        main(MyCharm)
+                        ops.main(MyCharm)
 
         self.assertEqual(mock_charm_event.call_count, 1)
         return mock_charm_event.call_args[0][1]
@@ -285,12 +257,12 @@ def setUp(self):
 
         # Relations events are defined dynamically and modify the class attributes.
         # We use a subclass temporarily to prevent these side effects from leaking.
-        class TestCharmEvents(CharmEvents):
+        class TestCharmEvents(ops.CharmEvents):
             pass
-        CharmBase.on = TestCharmEvents()
+        ops.CharmBase.on = TestCharmEvents()
 
         def cleanup():
-            CharmBase.on = CharmEvents()
+            ops.CharmBase.on = ops.CharmEvents()
         self.addCleanup(cleanup)
 
         fake_script(self, 'juju-log', "exit 0")
@@ -337,10 +309,10 @@ def _read_and_clear_state(self, event_name: str):
                 af = (self.JUJU_CHARM_DIR / 'actions.yaml')
                 if af.exists():
                     with af.open() as a:
-                        meta = CharmMeta.from_yaml(m, a)
+                        meta = ops.CharmMeta.from_yaml(m, a)
                 else:
-                    meta = CharmMeta.from_yaml(m)
-            framework = Framework(storage, self.JUJU_CHARM_DIR, meta, None, event_name)
+                    meta = ops.CharmMeta.from_yaml(m)
+            framework = ops.Framework(storage, self.JUJU_CHARM_DIR, meta, None, event_name)
 
             class ThisCharmEvents(MyCharmEvents):
                 pass
@@ -355,7 +327,7 @@ class Charm(self.charm_module.Charm):
             storage.commit()
             framework.close()
         else:
-            stored = StoredStateData(None, None)
+            stored = ops.StoredStateData(None, None)
         return stored
 
     def _simulate_event(self, event_spec):
@@ -372,16 +344,16 @@ def _simulate_event(self, event_spec):
         })
         if event_spec.set_in_env is not None:
             env.update(event_spec.set_in_env)
-        if issubclass(event_spec.event_type, SecretEvent):
+        if issubclass(event_spec.event_type, ops.SecretEvent):
             env.update({
                 'JUJU_SECRET_ID': event_spec.secret_id,
                 'JUJU_SECRET_LABEL': event_spec.secret_label or '',
             })
-        if issubclass(event_spec.event_type, (SecretRemoveEvent, SecretExpiredEvent)):
+        if issubclass(event_spec.event_type, (ops.SecretRemoveEvent, ops.SecretExpiredEvent)):
             env.update({
                 'JUJU_SECRET_REVISION': str(event_spec.secret_revision or ''),
             })
-        if issubclass(event_spec.event_type, RelationEvent):
+        if issubclass(event_spec.event_type, ops.RelationEvent):
             rel_name = event_spec.event_name.split('_')[0]
             env.update({
                 'JUJU_RELATION': rel_name,
@@ -406,11 +378,11 @@ def _simulate_event(self, event_spec):
                 'JUJU_REMOTE_UNIT': '',
                 'JUJU_REMOTE_APP': '',
             })
-        if issubclass(event_spec.event_type, WorkloadEvent):
+        if issubclass(event_spec.event_type, ops.WorkloadEvent):
             env.update({
                 'JUJU_WORKLOAD_NAME': event_spec.workload_name,
             })
-        if issubclass(event_spec.event_type, ActionEvent):
+        if issubclass(event_spec.event_type, ops.ActionEvent):
             event_filename = event_spec.event_name[:-len('_action')].replace('_', '-')
             env.update({
                 event_spec.env_var: event_filename,
@@ -430,14 +402,14 @@ def _simulate_event(self, event_spec):
 
     def test_event_reemitted(self):
         # First run "install" to make sure all hooks are set up.
-        state = self._simulate_event(EventSpec(InstallEvent, 'install'))
+        state = self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
         self.assertEqual(list(state.observed_event_types), ['InstallEvent'])
 
-        state = self._simulate_event(EventSpec(ConfigChangedEvent, 'config-changed'))
+        state = self._simulate_event(EventSpec(ops.ConfigChangedEvent, 'config-changed'))
         self.assertEqual(list(state.observed_event_types), ['ConfigChangedEvent'])
 
         # Re-emit should pick the deferred config-changed.
-        state = self._simulate_event(EventSpec(UpdateStatusEvent, 'update-status'))
+        state = self._simulate_event(EventSpec(ops.UpdateStatusEvent, 'update-status'))
         self.assertEqual(
             list(state.observed_event_types),
             ['ConfigChangedEvent', 'UpdateStatusEvent'])
@@ -446,15 +418,15 @@ def test_no_reemission_on_collect_metrics(self):
         fake_script(self, 'add-metric', 'exit 0')
 
         # First run "install" to make sure all hooks are set up.
-        state = self._simulate_event(EventSpec(InstallEvent, 'install'))
+        state = self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
         self.assertEqual(list(state.observed_event_types), ['InstallEvent'])
 
-        state = self._simulate_event(EventSpec(ConfigChangedEvent, 'config-changed'))
+        state = self._simulate_event(EventSpec(ops.ConfigChangedEvent, 'config-changed'))
         self.assertEqual(list(state.observed_event_types), ['ConfigChangedEvent'])
 
         # Re-emit should not pick the deferred config-changed because
         # collect-metrics runs in a restricted context.
-        state = self._simulate_event(EventSpec(CollectMetricsEvent, 'collect-metrics'))
+        state = self._simulate_event(EventSpec(ops.CollectMetricsEvent, 'collect-metrics'))
         self.assertEqual(list(state.observed_event_types), ['CollectMetricsEvent'])
 
     def test_multiple_events_handled(self):
@@ -465,19 +437,19 @@ def test_multiple_events_handled(self):
         # Sample events with a different amount of dashes used
         # and with endpoints from different sections of metadata.yaml
         events_under_test = [(
-            EventSpec(InstallEvent, 'install'),
+            EventSpec(ops.InstallEvent, 'install'),
             {},
         ), (
-            EventSpec(StartEvent, 'start'),
+            EventSpec(ops.StartEvent, 'start'),
             {},
         ), (
-            EventSpec(UpdateStatusEvent, 'update_status'),
+            EventSpec(ops.UpdateStatusEvent, 'update_status'),
             {},
         ), (
-            EventSpec(LeaderSettingsChangedEvent, 'leader_settings_changed'),
+            EventSpec(ops.LeaderSettingsChangedEvent, 'leader_settings_changed'),
             {},
         ), (
-            EventSpec(RelationJoinedEvent, 'db_relation_joined',
+            EventSpec(ops.RelationJoinedEvent, 'db_relation_joined',
                       relation_id=1,
                       remote_app='remote',
                       remote_unit='remote/0'),
@@ -486,7 +458,7 @@ def test_multiple_events_handled(self):
              'app_name': 'remote',
              'unit_name': 'remote/0'},
         ), (
-            EventSpec(RelationChangedEvent, 'mon_relation_changed',
+            EventSpec(ops.RelationChangedEvent, 'mon_relation_changed',
                       relation_id=2,
                       remote_app='remote',
                       remote_unit='remote/0'),
@@ -495,7 +467,7 @@ def test_multiple_events_handled(self):
              'app_name': 'remote',
              'unit_name': 'remote/0'},
         ), (
-            EventSpec(RelationChangedEvent, 'mon_relation_changed',
+            EventSpec(ops.RelationChangedEvent, 'mon_relation_changed',
                       relation_id=2,
                       remote_app='remote',
                       remote_unit=None),
@@ -504,7 +476,7 @@ def test_multiple_events_handled(self):
              'app_name': 'remote',
              'unit_name': None},
         ), (
-            EventSpec(RelationDepartedEvent, 'mon_relation_departed',
+            EventSpec(ops.RelationDepartedEvent, 'mon_relation_departed',
                       relation_id=2,
                       remote_app='remote',
                       remote_unit='remote/0',
@@ -515,13 +487,13 @@ def test_multiple_events_handled(self):
              'unit_name': 'remote/0',
              'departing_unit_name': 'remote/42'},
         ), (
-            EventSpec(RelationBrokenEvent, 'ha_relation_broken',
+            EventSpec(ops.RelationBrokenEvent, 'ha_relation_broken',
                       relation_id=3),
             {'relation_name': 'ha',
              'relation_id': 3},
         ), (
             # Events without a remote app specified (for Juju < 2.7).
-            EventSpec(RelationJoinedEvent, 'db_relation_joined',
+            EventSpec(ops.RelationJoinedEvent, 'db_relation_joined',
                       relation_id=1,
                       remote_unit='remote/0'),
             {'relation_name': 'db',
@@ -529,7 +501,7 @@ def test_multiple_events_handled(self):
              'app_name': 'remote',
              'unit_name': 'remote/0'},
         ), (
-            EventSpec(RelationChangedEvent, 'mon_relation_changed',
+            EventSpec(ops.RelationChangedEvent, 'mon_relation_changed',
                       relation_id=2,
                       remote_unit='remote/0'),
             {'relation_name': 'mon',
@@ -537,7 +509,7 @@ def test_multiple_events_handled(self):
              'app_name': 'remote',
              'unit_name': 'remote/0'},
         ), (
-            EventSpec(RelationDepartedEvent, 'mon_relation_departed',
+            EventSpec(ops.RelationDepartedEvent, 'mon_relation_departed',
                       relation_id=2,
                       remote_unit='remote/0',
                       departing_unit_name='remote/42'),
@@ -547,26 +519,26 @@ def test_multiple_events_handled(self):
              'unit_name': 'remote/0',
              'departing_unit_name': 'remote/42'},
         ), (
-            EventSpec(ActionEvent, 'start_action',
+            EventSpec(ops.ActionEvent, 'start_action',
                       env_var='JUJU_ACTION_NAME'),
             {},
         ), (
-            EventSpec(ActionEvent, 'foo_bar_action',
+            EventSpec(ops.ActionEvent, 'foo_bar_action',
                       env_var='JUJU_ACTION_NAME'),
             {},
         ), (
-            EventSpec(PebbleReadyEvent, 'test_pebble_ready',
+            EventSpec(ops.PebbleReadyEvent, 'test_pebble_ready',
                       workload_name='test'),
             {'container_name': 'test'},
         ), (
-            EventSpec(SecretChangedEvent, 'secret_changed',
+            EventSpec(ops.SecretChangedEvent, 'secret_changed',
                       secret_id='secret:12345',
                       secret_label='foo'),
             {'id': 'secret:12345',
              'label': 'foo',
              'revision': 42}
         ), (
-            EventSpec(SecretRotateEvent, 'secret_rotate',
+            EventSpec(ops.SecretRotateEvent, 'secret_rotate',
                       secret_id='secret:12345',
                       secret_label='foo',
                       secret_revision='42'),
@@ -574,7 +546,7 @@ def test_multiple_events_handled(self):
              'label': 'foo',
              'revision': 42}
         ), (
-            EventSpec(SecretRemoveEvent, 'secret_remove',
+            EventSpec(ops.SecretRemoveEvent, 'secret_remove',
                       secret_id='secret:12345',
                       secret_label='foo',
                       secret_revision='42'),
@@ -582,7 +554,7 @@ def test_multiple_events_handled(self):
              'label': 'foo',
              'revision': 42}
         ), (
-            EventSpec(SecretExpiredEvent, 'secret_expired',
+            EventSpec(ops.SecretExpiredEvent, 'secret_expired',
                       secret_id='secret:12345',
                       secret_label='foo',
                       secret_revision='42'),
@@ -594,7 +566,7 @@ def test_multiple_events_handled(self):
         logger.debug('Expected events %s', events_under_test)
 
         # First run "install" to make sure all hooks are set up.
-        self._simulate_event(EventSpec(InstallEvent, 'install'))
+        self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
 
         # Simulate hook executions for every event.
         for event_spec, expected_event_data in events_under_test:
@@ -624,25 +596,25 @@ def test_event_not_implemented(self):
         hook_path.symlink_to('install')
 
         try:
-            self._simulate_event(EventSpec(HookEvent, 'not-implemented-event'))
+            self._simulate_event(EventSpec(ops.HookEvent, 'not-implemented-event'))
         except subprocess.CalledProcessError:
             self.fail('Event simulation for an unsupported event'
                       ' results in a non-zero exit code returned')
 
     def test_no_actions(self):
         (self.JUJU_CHARM_DIR / 'actions.yaml').unlink()
-        self._simulate_event(EventSpec(InstallEvent, 'install'))
+        self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
 
     def test_empty_actions(self):
         (self.JUJU_CHARM_DIR / 'actions.yaml').write_text('')
-        self._simulate_event(EventSpec(InstallEvent, 'install'))
+        self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
 
     def test_collect_metrics(self):
         fake_script(self, 'add-metric', 'exit 0')
-        self._simulate_event(EventSpec(InstallEvent, 'install'))
+        self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
         # Clear the calls during 'install'
         fake_script_calls(self, clear=True)
-        self._simulate_event(EventSpec(CollectMetricsEvent, 'collect_metrics'))
+        self._simulate_event(EventSpec(ops.CollectMetricsEvent, 'collect_metrics'))
 
         expected = [
             VERSION_LOGLINE,
@@ -654,10 +626,10 @@ def test_collect_metrics(self):
         self.assertEqual(calls, expected)
 
     def test_custom_event(self):
-        self._simulate_event(EventSpec(InstallEvent, 'install'))
+        self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
         # Clear the calls during 'install'
         fake_script_calls(self, clear=True)
-        self._simulate_event(EventSpec(UpdateStatusEvent, 'update-status',
+        self._simulate_event(EventSpec(ops.UpdateStatusEvent, 'update-status',
                                        set_in_env={'EMIT_CUSTOM_EVENT': "1"}))
 
         expected = [
@@ -673,24 +645,24 @@ def test_logger(self):
         fake_script(self, 'action-get', "echo '{}'")
 
         test_cases = [(
-            EventSpec(ActionEvent, 'log_critical_action', env_var='JUJU_ACTION_NAME'),
+            EventSpec(ops.ActionEvent, 'log_critical_action', env_var='JUJU_ACTION_NAME'),
             ['juju-log', '--log-level', 'CRITICAL', '--', 'super critical'],
         ), (
-            EventSpec(ActionEvent, 'log_error_action',
+            EventSpec(ops.ActionEvent, 'log_error_action',
                       env_var='JUJU_ACTION_NAME'),
             ['juju-log', '--log-level', 'ERROR', '--', 'grave error'],
         ), (
-            EventSpec(ActionEvent, 'log_warning_action',
+            EventSpec(ops.ActionEvent, 'log_warning_action',
                       env_var='JUJU_ACTION_NAME'),
             ['juju-log', '--log-level', 'WARNING', '--', 'wise warning'],
         ), (
-            EventSpec(ActionEvent, 'log_info_action',
+            EventSpec(ops.ActionEvent, 'log_info_action',
                       env_var='JUJU_ACTION_NAME'),
             ['juju-log', '--log-level', 'INFO', '--', 'useful info'],
         )]
 
         # Set up action symlinks.
-        self._simulate_event(EventSpec(InstallEvent, 'install'))
+        self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
 
         for event_spec, calls in test_cases:
             self._simulate_event(event_spec)
@@ -698,7 +670,7 @@ def test_logger(self):
 
     def test_excepthook(self):
         with self.assertRaises(subprocess.CalledProcessError):
-            self._simulate_event(EventSpec(InstallEvent, 'install',
+            self._simulate_event(EventSpec(ops.InstallEvent, 'install',
                                            set_in_env={'TRY_EXCEPTHOOK': '1'}))
 
         calls = [' '.join(i) for i in fake_script_calls(self)]
@@ -723,7 +695,7 @@ def test_sets_model_name(self):
 
         fake_script(self, 'action-get', "echo '{}'")
         state = self._simulate_event(EventSpec(
-            ActionEvent, 'get_model_name_action',
+            ops.ActionEvent, 'get_model_name_action',
             env_var='JUJU_ACTION_NAME',
             model_name='test-model-name'))
         self.assertIsNotNone(state)
@@ -735,7 +707,7 @@ def test_has_valid_status(self):
         fake_script(self, 'action-get', "echo '{}'")
         fake_script(self, 'status-get', """echo '{"status": "unknown", "message": ""}'""")
         state = self._simulate_event(EventSpec(
-            ActionEvent, 'get_status_action',
+            ops.ActionEvent, 'get_status_action',
             env_var='JUJU_ACTION_NAME'))
         self.assertIsNotNone(state)
         self.assertEqual(state.status_name, 'unknown')
@@ -743,7 +715,7 @@ def test_has_valid_status(self):
         fake_script(
             self, 'status-get', """echo '{"status": "blocked", "message": "help meeee"}'""")
         state = self._simulate_event(EventSpec(
-            ActionEvent, 'get_status_action',
+            ops.ActionEvent, 'get_status_action',
             env_var='JUJU_ACTION_NAME'))
         self.assertIsNotNone(state)
         self.assertEqual(state.status_name, 'blocked')
@@ -811,13 +783,13 @@ def test_setup_event_links(self):
         """Test auto-creation of symlinks caused by initial events."""
         all_event_hooks = [f"hooks/{name.replace('_', '-')}"
                            for name, event_source in self.charm_module.Charm.on.events().items()
-                           if issubclass(event_source.event_type, LifecycleEvent)]
+                           if issubclass(event_source.event_type, ops.LifecycleEvent)]
 
         initial_events = {
-            EventSpec(InstallEvent, 'install'),
-            EventSpec(StorageAttachedEvent, 'disks-storage-attached'),
-            EventSpec(StartEvent, 'start'),
-            EventSpec(UpgradeCharmEvent, 'upgrade-charm'),
+            EventSpec(ops.InstallEvent, 'install'),
+            EventSpec(ops.StorageAttachedEvent, 'disks-storage-attached'),
+            EventSpec(ops.StartEvent, 'start'),
+            EventSpec(ops.UpgradeCharmEvent, 'upgrade-charm'),
         }
         initial_hooks = {f"hooks/{ev.event_name}" for ev in initial_events}
 
@@ -847,7 +819,7 @@ def _assess_event_links(event_spec):
             _assess_event_links(initial_event)
 
     def test_setup_action_links(self):
-        self._simulate_event(EventSpec(InstallEvent, 'install'))
+        self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
         # foo-bar is one of the actions defined in actions.yaml
         action_hook = self.JUJU_CHARM_DIR / 'actions' / 'foo-bar'
         self.assertTrue(action_hook.exists())
@@ -886,10 +858,10 @@ def test_setup_event_links(self):
         all_event_hooks = [f"hooks/{e.replace('_', '-')}"
                            for e in self.charm_module.Charm.on.events().keys()]
         initial_events = {
-            EventSpec(InstallEvent, 'install'),
-            EventSpec(StorageAttachedEvent, 'disks-storage-attached'),
-            EventSpec(StartEvent, 'start'),
-            EventSpec(UpgradeCharmEvent, 'upgrade-charm'),
+            EventSpec(ops.InstallEvent, 'install'),
+            EventSpec(ops.StorageAttachedEvent, 'disks-storage-attached'),
+            EventSpec(ops.StartEvent, 'start'),
+            EventSpec(ops.UpgradeCharmEvent, 'upgrade-charm'),
         }
 
         def _assess_event_links(event_spec):
@@ -908,7 +880,7 @@ def test_hook_and_dispatch(self):
         old_path = self.fake_script_path
         self.fake_script_path = self.hooks_dir
         fake_script(self, 'install', 'exit 0')
-        state = self._simulate_event(EventSpec(InstallEvent, 'install'))
+        state = self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
 
         # the script was called, *and*, the .on. was called
         self.assertEqual(fake_script_calls(self), [['install', '']])
@@ -933,7 +905,7 @@ def test_hook_and_dispatch(self):
 
     def test_non_executable_hook_and_dispatch(self):
         (self.hooks_dir / "install").write_text("")
-        state = self._simulate_event(EventSpec(InstallEvent, 'install'))
+        state = self._simulate_event(EventSpec(ops.InstallEvent, 'install'))
 
         self.assertEqual(list(state.observed_event_types), ['InstallEvent'])
 
@@ -957,7 +929,7 @@ def test_hook_and_dispatch_with_failing_hook(self):
         old_path = self.fake_script_path
         self.fake_script_path = self.hooks_dir
         fake_script(self, 'install', 'exit 42')
-        event = EventSpec(InstallEvent, 'install')
+        event = EventSpec(ops.InstallEvent, 'install')
         with self.assertRaises(subprocess.CalledProcessError):
             self._simulate_event(event)
         self.fake_script_path = old_path
@@ -975,7 +947,7 @@ def test_hook_and_dispatch_with_failing_hook(self):
         self.assertEqual(calls, expected)
 
     def test_hook_and_dispatch_but_hook_is_dispatch(self):
-        event = EventSpec(InstallEvent, 'install')
+        event = EventSpec(ops.InstallEvent, 'install')
         hook_path = self.hooks_dir / 'install'
         for ((rel, ind), path) in {
                 # relative and indirect
@@ -1007,7 +979,7 @@ def test_hook_and_dispatch_but_hook_is_dispatch_copy(self):
         path = (self.hooks_dir / self.charm_exec_path).resolve()
         shutil.copy(str(path), str(hook_path))
 
-        event = EventSpec(InstallEvent, 'install')
+        event = EventSpec(ops.InstallEvent, 'install')
         state = self._simulate_event(event)
 
         # the .on. was only called once
@@ -1156,24 +1128,24 @@ def setUp(self):
         logassert.setup(self, '')
 
     def test_fallback_to_current_juju_version__too_old(self):
-        meta = CharmMeta.from_yaml("series: [kubernetes]")
+        meta = ops.CharmMeta.from_yaml("series: [kubernetes]")
         with patch.dict(os.environ, {"JUJU_VERSION": "1.0"}):
             self.assertFalse(_should_use_controller_storage(Path("/xyzzy"), meta))
             self.assertLogged('Using local storage: JUJU_VERSION=1.0.0')
 
     def test_fallback_to_current_juju_version__new_enough(self):
-        meta = CharmMeta.from_yaml("series: [kubernetes]")
+        meta = ops.CharmMeta.from_yaml("series: [kubernetes]")
         with patch.dict(os.environ, {"JUJU_VERSION": "2.8"}):
             self.assertTrue(_should_use_controller_storage(Path("/xyzzy"), meta))
             self.assertLogged('Using controller storage: JUJU_VERSION=2.8.0')
 
     def test_not_if_not_in_k8s(self):
-        meta = CharmMeta.from_yaml("series: [ecs]")
+        meta = ops.CharmMeta.from_yaml("series: [ecs]")
         with patch.dict(os.environ, {"JUJU_VERSION": "2.8"}):
             self.assertFalse(_should_use_controller_storage(Path("/xyzzy"), meta))
             self.assertLogged('Using local storage: not a Kubernetes podspec charm')
 
     def test_not_if_already_local(self):
-        meta = CharmMeta.from_yaml("series: [kubernetes]")
+        meta = ops.CharmMeta.from_yaml("series: [kubernetes]")
         with patch.dict(os.environ, {"JUJU_VERSION": "2.8"}), tempfile.NamedTemporaryFile() as fd:
             self.assertFalse(_should_use_controller_storage(Path(fd.name), meta))
diff --git a/test/test_model.py b/test/test_model.py
index 703cd7ae3..92a78bd53 100755
--- a/test/test_model.py
+++ b/test/test_model.py
@@ -27,25 +27,16 @@
 
 import pytest
 
-import ops.charm
-import ops.model
+import ops
 import ops.testing
-from ops import model
+from ops import pebble
 from ops._private import yaml
-from ops.charm import RelationMeta, RelationRole
-from ops.pebble import (
-    APIError,
-    ConnectionError,
-    FileInfo,
-    FileType,
-    ServiceInfo,
-    SystemInfo,
-)
+from ops.model import _ModelBackend
 
 
 class TestModel(unittest.TestCase):
     def setUp(self):
-        self.harness = ops.testing.Harness(ops.charm.CharmBase, meta='''
+        self.harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: myapp
             provides:
               db0:
@@ -87,7 +78,7 @@ def test_app_immutable(self):
 
     def test_model_name_from_backend(self):
         self.harness.set_model_name('default')
-        m = ops.model.Model(ops.charm.CharmMeta(), self.harness._backend)
+        m = ops.Model(ops.CharmMeta(), self.harness._backend)
         self.assertEqual(m.name, 'default')
         with self.assertRaises(AttributeError):
             m.name = "changes-disallowed"
@@ -126,17 +117,17 @@ def test_get_relation(self):
         relation_id_db0_b = self.harness.add_relation('db0', 'another')
         self.resetBackendCalls()
 
-        with self.assertRaises(ops.model.ModelError):
+        with self.assertRaises(ops.ModelError):
             # You have to specify it by just the integer ID
             self.model.get_relation('db1', f'db1:{relation_id_db1}')
         rel_db1 = self.model.get_relation('db1', relation_id_db1)
-        self.assertIsInstance(rel_db1, ops.model.Relation)
+        self.assertIsInstance(rel_db1, ops.Relation)
         self.assertBackendCalls([
             ('relation_ids', 'db1'),
             ('relation_list', relation_id_db1),
         ])
         dead_rel = self.model.get_relation('db1', 7)
-        self.assertIsInstance(dead_rel, ops.model.Relation)
+        self.assertIsInstance(dead_rel, ops.Relation)
         self.assertEqual(set(dead_rel.data.keys()), {self.model.unit, self.model.unit.app})
         self.assertEqual(dead_rel.data[self.model.unit], {})
         self.assertBackendCalls([
@@ -150,7 +141,7 @@ def test_get_relation(self):
             ('relation_ids', 'db2'),
         ])
         self.assertIs(self.model.get_relation('db1'), rel_db1)
-        with self.assertRaises(ops.model.TooManyRelatedAppsError):
+        with self.assertRaises(ops.TooManyRelatedAppsError):
             self.model.get_relation('db0')
 
         self.assertBackendCalls([
@@ -189,14 +180,14 @@ def test_invalid_type_relation_data(self):
         relation_id = self.harness.add_relation('db1', 'remoteapp1')
         self.harness.add_relation_unit(relation_id, 'remoteapp1/0')
 
-        with self.assertRaises(model.RelationDataError):
+        with self.assertRaises(ops.RelationDataError):
             with self.harness._event_context('foo_event'):
                 self.harness.update_relation_data(
                     relation_id,
                     'remoteapp1/0',
                     {42: 'remoteapp1-0'})
 
-        with self.assertRaises(model.RelationDataError):
+        with self.assertRaises(ops.RelationDataError):
             with self.harness._event_context('foo_event'):
                 self.harness.update_relation_data(
                     relation_id,
@@ -253,7 +244,7 @@ def test_remote_app_relation_data(self):
 
         rel_db1 = self.model.get_relation('db1')
         # Try to get relation data for an invalid remote application.
-        random_app = self.model._cache.get(ops.model.Application, 'randomapp')
+        random_app = self.model._cache.get(ops.Application, 'randomapp')
         with self.assertRaises(KeyError):
             rel_db1.data[random_app]
 
@@ -287,7 +278,7 @@ def test_relation_data_modify_remote(self):
         self.assertEqual(repr(rel_db1.data[remoteapp1_0]), "{'host': 'remoteapp1/0'}")
 
         with self.harness._event_context('foo_event'):
-            with self.assertRaises(ops.model.RelationDataError):
+            with self.assertRaises(ops.RelationDataError):
                 rel_db1.data[remoteapp1_0]['foo'] = 'bar'
         self.assertNotIn('foo', rel_db1.data[remoteapp1_0])
 
@@ -363,7 +354,7 @@ def test_app_relation_data_modify_local_as_minion(self):
 
         with self.harness._event_context('foo_event'):
             # if we were inside an event context, we'd get:
-            with self.assertRaises(ops.model.RelationDataError):
+            with self.assertRaises(ops.RelationDataError):
                 rel_db1.data[local_app]['password'] = 'foobar'
 
         self.assertBackendCalls([('relation_ids', 'db1'),
@@ -451,7 +442,7 @@ def test_relation_set_fail(self):
 
         def broken_update_relation_data(relation_id, entity, key, value):
             backend._calls.append(('update_relation_data', relation_id, entity, key, value))
-            raise ops.model.ModelError()
+            raise ops.ModelError()
         backend.update_relation_data = broken_update_relation_data
 
         rel_db1 = self.model.get_relation('db1')
@@ -459,10 +450,10 @@ def broken_update_relation_data(relation_id, entity, key, value):
         self.assertIn('host', rel_db1.data[self.model.unit])
 
         with self.harness._event_context('foo_event'):
-            with self.assertRaises(ops.model.ModelError):
+            with self.assertRaises(ops.ModelError):
                 rel_db1.data[self.model.unit]['host'] = 'bar'
             self.assertEqual(rel_db1.data[self.model.unit]['host'], 'myapp-0')
-            with self.assertRaises(ops.model.ModelError):
+            with self.assertRaises(ops.ModelError):
                 del rel_db1.data[self.model.unit]['host']
             self.assertIn('host', rel_db1.data[self.model.unit])
 
@@ -491,7 +482,7 @@ def test_relation_data_type_check(self):
                 (1, 1),
                 (None, None)
         ):
-            with self.assertRaises(ops.model.RelationDataError):
+            with self.assertRaises(ops.RelationDataError):
                 with self.harness.framework._event_context('foo_event'):
                     rel_db1.data[self.model.unit][key] = value
 
@@ -574,7 +565,7 @@ def test_relation_local_app_data_readability_follower(self):
         with self.harness._event_context('foo_event'):
             self.resetBackendCalls()
 
-            with self.assertRaises(ops.model.RelationDataError):
+            with self.assertRaises(ops.RelationDataError):
                 # 'local' is there, but still:
                 rel_db1.data[local_app]['local']
 
@@ -667,7 +658,7 @@ def test_workload_version_invalid(self):
         self.assertBackendCalls([])
 
     def test_resources(self):
-        with self.assertRaises(ops.model.ModelError):
+        with self.assertRaises(ops.ModelError):
             self.harness.model.resources.fetch('foo')
 
         self.harness.add_resource('foo', 'foo contents\n')
@@ -693,7 +684,7 @@ def test_pod_spec(self):
 
         # no leader -> no set pod spec
         self.harness.set_leader(False)
-        with self.assertRaises(ops.model.ModelError):
+        with self.assertRaises(ops.ModelError):
             self.harness.model.pod.set_spec({'foo': 'bar'})
 
     def test_pod_immutable(self):
@@ -702,36 +693,36 @@ def test_pod_immutable(self):
 
     def test_base_status_instance_raises(self):
         with self.assertRaises(TypeError):
-            ops.model.StatusBase('test')
+            ops.StatusBase('test')
 
-        class NoNameStatus(ops.model.StatusBase):
+        class NoNameStatus(ops.StatusBase):
             pass
 
         with self.assertRaises(AttributeError):
-            ops.model.StatusBase.register_status(NoNameStatus)
+            ops.StatusBase.register_status(NoNameStatus)
 
     def test_status_repr(self):
         test_cases = {
-            "ActiveStatus('Seashell')": ops.model.ActiveStatus('Seashell'),
-            "MaintenanceStatus('Red')": ops.model.MaintenanceStatus('Red'),
-            "BlockedStatus('Magenta')": ops.model.BlockedStatus('Magenta'),
-            "WaitingStatus('Thistle')": ops.model.WaitingStatus('Thistle'),
-            'UnknownStatus()': ops.model.UnknownStatus(),
+            "ActiveStatus('Seashell')": ops.ActiveStatus('Seashell'),
+            "MaintenanceStatus('Red')": ops.MaintenanceStatus('Red'),
+            "BlockedStatus('Magenta')": ops.BlockedStatus('Magenta'),
+            "WaitingStatus('Thistle')": ops.WaitingStatus('Thistle'),
+            'UnknownStatus()': ops.UnknownStatus(),
         }
         for expected, status in test_cases.items():
             self.assertEqual(repr(status), expected)
 
     def test_status_eq(self):
         status_types = [
-            ops.model.ActiveStatus,
-            ops.model.MaintenanceStatus,
-            ops.model.BlockedStatus,
-            ops.model.WaitingStatus,
+            ops.ActiveStatus,
+            ops.MaintenanceStatus,
+            ops.BlockedStatus,
+            ops.WaitingStatus,
         ]
 
-        self.assertEqual(ops.model.UnknownStatus(), ops.model.UnknownStatus())
+        self.assertEqual(ops.UnknownStatus(), ops.UnknownStatus())
         for (i, t1) in enumerate(status_types):
-            self.assertNotEqual(t1(''), ops.model.UnknownStatus())
+            self.assertNotEqual(t1(''), ops.UnknownStatus())
             for (j, t2) in enumerate(status_types):
                 self.assertNotEqual(t1('one'), t2('two'))
                 if i == j:
@@ -740,25 +731,25 @@ def test_status_eq(self):
                     self.assertNotEqual(t1('one'), t2('one'))
 
     def test_active_message_default(self):
-        self.assertEqual(ops.model.ActiveStatus().message, '')
+        self.assertEqual(ops.ActiveStatus().message, '')
 
     def test_local_set_valid_unit_status(self):
         self.harness._get_backend_calls(reset=True)
         test_cases = [(
             'active',
-            ops.model.ActiveStatus('Green'),
+            ops.ActiveStatus('Green'),
             ('status_set', 'active', 'Green', {'is_app': False}),
         ), (
             'maintenance',
-            ops.model.MaintenanceStatus('Yellow'),
+            ops.MaintenanceStatus('Yellow'),
             ('status_set', 'maintenance', 'Yellow', {'is_app': False}),
         ), (
             'blocked',
-            ops.model.BlockedStatus('Red'),
+            ops.BlockedStatus('Red'),
             ('status_set', 'blocked', 'Red', {'is_app': False}),
         ), (
             'waiting',
-            ops.model.WaitingStatus('White'),
+            ops.WaitingStatus('White'),
             ('status_set', 'waiting', 'White', {'is_app': False}),
         )]
 
@@ -774,19 +765,19 @@ def test_local_set_valid_app_status(self):
         self.harness.set_leader(True)
         test_cases = [(
             'active',
-            ops.model.ActiveStatus('Green'),
+            ops.ActiveStatus('Green'),
             ('status_set', 'active', 'Green', {'is_app': True}),
         ), (
             'maintenance',
-            ops.model.MaintenanceStatus('Yellow'),
+            ops.MaintenanceStatus('Yellow'),
             ('status_set', 'maintenance', 'Yellow', {'is_app': True}),
         ), (
             'blocked',
-            ops.model.BlockedStatus('Red'),
+            ops.BlockedStatus('Red'),
             ('status_set', 'blocked', 'Red', {'is_app': True}),
         ), (
             'waiting',
-            ops.model.WaitingStatus('White'),
+            ops.WaitingStatus('White'),
             ('status_set', 'waiting', 'White', {'is_app': True}),
         )]
 
@@ -811,14 +802,14 @@ def test_set_app_status_non_leader_raises(self):
             self.model.app.status
 
         with self.assertRaises(RuntimeError):
-            self.model.app.status = ops.model.ActiveStatus()
+            self.model.app.status = ops.ActiveStatus()
 
     def test_set_unit_status_invalid(self):
-        with self.assertRaises(ops.model.InvalidStatusError):
+        with self.assertRaises(ops.InvalidStatusError):
             self.model.unit.status = 'blocked'
 
     def test_set_app_status_invalid(self):
-        with self.assertRaises(ops.model.InvalidStatusError):
+        with self.assertRaises(ops.InvalidStatusError):
             self.model.app.status = 'blocked'
 
     def test_remote_unit_status(self):
@@ -830,14 +821,14 @@ def test_remote_unit_status(self):
         self.resetBackendCalls()
 
         # Remote unit status is always unknown.
-        self.assertEqual(remote_unit.status, ops.model.UnknownStatus())
+        self.assertEqual(remote_unit.status, ops.UnknownStatus())
 
         test_statuses = (
-            ops.model.UnknownStatus(),
-            ops.model.ActiveStatus('Green'),
-            ops.model.MaintenanceStatus('Yellow'),
-            ops.model.BlockedStatus('Red'),
-            ops.model.WaitingStatus('White'),
+            ops.UnknownStatus(),
+            ops.ActiveStatus('Green'),
+            ops.MaintenanceStatus('Yellow'),
+            ops.BlockedStatus('Red'),
+            ops.WaitingStatus('White'),
         )
 
         for target_status in test_statuses:
@@ -854,14 +845,14 @@ def test_remote_app_status(self):
         self.resetBackendCalls()
 
         # Remote application status is always unknown.
-        self.assertIsInstance(remoteapp1.status, ops.model.UnknownStatus)
+        self.assertIsInstance(remoteapp1.status, ops.UnknownStatus)
 
         test_statuses = (
-            ops.model.UnknownStatus(),
-            ops.model.ActiveStatus(),
-            ops.model.MaintenanceStatus('Upgrading software'),
-            ops.model.BlockedStatus('Awaiting manual resolution'),
-            ops.model.WaitingStatus('Awaiting related app updates'),
+            ops.UnknownStatus(),
+            ops.ActiveStatus(),
+            ops.MaintenanceStatus('Upgrading software'),
+            ops.BlockedStatus('Awaiting manual resolution'),
+            ops.WaitingStatus('Awaiting related app updates'),
         )
         for target_status in test_statuses:
             with self.subTest(target_status.name):
@@ -870,9 +861,9 @@ def test_remote_app_status(self):
         self.assertBackendCalls([])
 
     def test_storage(self):
-        meta = ops.charm.CharmMeta()
+        meta = ops.CharmMeta()
         meta.storages = {'disks': None, 'data': None}
-        model = ops.model.Model(meta, ops.model._ModelBackend('myapp/0'))
+        model = ops.Model(meta, _ModelBackend('myapp/0'))
 
         fake_script(self, 'storage-list', '''
             if [ "$1" = disks ]; then
@@ -923,7 +914,7 @@ def test_storage(self):
         ])
 
         # Try to add storage not present in charm metadata.
-        with self.assertRaises(ops.model.ModelError):
+        with self.assertRaises(ops.ModelError):
             model.storages.request('deadbeef')
 
         # Invalid count parameter types.
@@ -942,9 +933,9 @@ def assertBackendCalls(self, expected, *, reset=True):  # noqa: N802
         self.assertEqual(expected, self.harness._get_backend_calls(reset=reset))
 
     def test_run_error(self):
-        model = ops.model.Model(ops.charm.CharmMeta(), ops.model._ModelBackend('myapp/0'))
+        model = ops.Model(ops.CharmMeta(), _ModelBackend('myapp/0'))
         fake_script(self, 'status-get', """echo 'ERROR cannot get status' >&2; exit 1""")
-        with self.assertRaises(ops.model.ModelError) as cm:
+        with self.assertRaises(ops.ModelError) as cm:
             _ = model.unit.status.message
         self.assertEqual(str(cm.exception), 'ERROR cannot get status\n')
         self.assertEqual(cm.exception.args[0], 'ERROR cannot get status\n')
@@ -999,10 +990,10 @@ def list_func_gen(file_list):
         file_infos, dirs = [], set()
         for f in file_list:
             file_infos.append(
-                FileInfo(
+                pebble.FileInfo(
                     path=f,
                     name=os.path.basename(f),
-                    type=FileType.FILE,
+                    type=pebble.FileType.FILE,
                     **args))
 
             # collect all the directories for the test case's files
@@ -1010,10 +1001,10 @@ def list_func_gen(file_list):
             if dirpath != '' and dirpath not in dirs:
                 dirs.update(dirpath)
                 file_infos.append(
-                    FileInfo(
+                    pebble.FileInfo(
                         path=dirpath,
                         name=os.path.basename(dirpath),
-                        type=FileType.DIRECTORY,
+                        type=pebble.FileType.DIRECTORY,
                         **args))
 
         def inner(path):
@@ -1023,7 +1014,7 @@ def inner(path):
                 # exclude file infos for separate trees and also
                 # for the directory we are listing itself - we only want its contents.
                 if not info.path.startswith(path) or (
-                        info.type == FileType.DIRECTORY and path == info.path):
+                        info.type == pebble.FileType.DIRECTORY and path == info.path):
                     continue
                 # exclude file infos for files that are in subdirectories of path.
                 # we only want files that are directly in path.
@@ -1038,14 +1029,14 @@ def inner(path):
     case.path = os.path.normpath(case.path)
     case.files = [os.path.normpath(f) for f in case.files]
     case.want = {os.path.normpath(f) for f in case.want}
-    for f in ops.model.Container._list_recursive(
+    for f in ops.Container._list_recursive(
         list_func_gen(
             case.files), pathlib.Path(
             case.path)):
         path = f.path
         if case.dst is not None:
             # test destination path construction
-            _, path = f.path, ops.model.Container._build_destpath(
+            _, path = f.path, ops.Container._build_destpath(
                 f.path, case.path, case.dst)
         files.add(path)
     assert case.want == files, f'case {case.name!r} has wrong files: want {case.want}, got {files}'
@@ -1121,7 +1112,7 @@ def inner(path):
 @pytest.mark.parametrize('case', recursive_push_pull_cases)
 def test_recursive_push_and_pull(case):
     # full "integration" test of push+pull
-    harness = ops.testing.Harness(ops.charm.CharmBase, meta='''
+    harness = ops.testing.Harness(ops.CharmBase, meta='''
         name: test-app
         containers:
           foo:
@@ -1153,7 +1144,7 @@ def test_recursive_push_and_pull(case):
     errors = []
     try:
         c.push_path(push_path, case.dst)
-    except ops.model.MultiPushPullError as err:
+    except ops.MultiPushPullError as err:
         if not case.errors:
             raise
         errors = {src[len(push_src.name):] for src, _ in err.errors}
@@ -1172,7 +1163,7 @@ def test_recursive_push_and_pull(case):
     errors = []
     try:
         c.pull_path(case.path, os.path.join(pull_dst.name, case.dst[1:]))
-    except ops.model.MultiPushPullError as err:
+    except ops.MultiPushPullError as err:
         if not case.errors:
             raise
         errors = {src for src, _ in err.errors}
@@ -1186,7 +1177,7 @@ def test_recursive_push_and_pull(case):
 class TestApplication(unittest.TestCase):
 
     def setUp(self):
-        self.harness = ops.testing.Harness(ops.charm.CharmBase, meta='''
+        self.harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: myapp
             provides:
               db0:
@@ -1292,7 +1283,7 @@ def test_planned_units_override(self):
 
 class TestContainers(unittest.TestCase):
     def setUp(self):
-        meta = ops.charm.CharmMeta.from_yaml("""
+        meta = ops.CharmMeta.from_yaml("""
 name: k8s-charm
 containers:
   c1:
@@ -1300,8 +1291,8 @@ def setUp(self):
   c2:
     k: v
 """)
-        backend = ops.model._ModelBackend('myapp/0')
-        self.model = ops.model.Model(meta, backend)
+        backend = _ModelBackend('myapp/0')
+        self.model = ops.Model(meta, backend)
 
     def test_unit_containers(self):
         containers = self.model.unit.containers
@@ -1312,9 +1303,9 @@ def test_unit_containers(self):
         self.assertNotIn('c3', containers)
         for name in ['c1', 'c2']:
             container = containers[name]
-            self.assertIsInstance(container, ops.model.Container)
+            self.assertIsInstance(container, ops.Container)
             self.assertEqual(container.name, name)
-            self.assertIsInstance(container.pebble, ops.pebble.Client)
+            self.assertIsInstance(container.pebble, pebble.Client)
         with self.assertRaises(KeyError):
             containers['c3']
 
@@ -1326,10 +1317,10 @@ def test_unit_get_container(self):
         unit = self.model.unit
         for name in ['c1', 'c2']:
             container = unit.get_container(name)
-            self.assertIsInstance(container, ops.model.Container)
+            self.assertIsInstance(container, ops.Container)
             self.assertEqual(container.name, name)
-            self.assertIsInstance(container.pebble, ops.pebble.Client)
-        with self.assertRaises(ops.model.ModelError):
+            self.assertIsInstance(container.pebble, pebble.Client)
+        with self.assertRaises(ops.ModelError):
             unit.get_container('c3')
 
         with self.assertRaises(RuntimeError):
@@ -1339,14 +1330,14 @@ def test_unit_get_container(self):
 
 class TestContainerPebble(unittest.TestCase):
     def setUp(self):
-        meta = ops.charm.CharmMeta.from_yaml("""
+        meta = ops.CharmMeta.from_yaml("""
 name: k8s-charm
 containers:
   c1:
     k: v
 """)
         backend = MockPebbleBackend('myapp/0')
-        self.model = ops.model.Model(meta, backend)
+        self.model = ops.Model(meta, backend)
         self.container = self.model.unit.containers['c1']
         self.pebble = self.container.pebble
 
@@ -1362,7 +1353,7 @@ def test_replan(self):
         self.assertEqual(self.pebble.requests, [('replan',)])
 
     def test_can_connect(self):
-        self.pebble.responses.append(SystemInfo.from_dict({'version': '1.0.0'}))
+        self.pebble.responses.append(pebble.SystemInfo.from_dict({'version': '1.0.0'}))
         self.assertTrue(self.container.can_connect())
         self.assertEqual(self.pebble.requests, [('get_system_info',)])
 
@@ -1401,13 +1392,15 @@ def test_restart(self):
     def test_restart_fallback(self):
         def restart_services(services):
             self.pebble.requests.append(('restart', services))
-            raise APIError({}, 400, "", "")
+            raise pebble.APIError({}, 400, "", "")
 
         self.pebble.restart_services = restart_services
         # Setup the Pebble client to respond to a call to get_services()
         self.pebble.responses.append([
-            ServiceInfo.from_dict({'name': 'foo', 'startup': 'enabled', 'current': 'active'}),
-            ServiceInfo.from_dict({'name': 'bar', 'startup': 'enabled', 'current': 'inactive'}),
+            pebble.ServiceInfo.from_dict(
+                {'name': 'foo', 'startup': 'enabled', 'current': 'active'}),
+            pebble.ServiceInfo.from_dict(
+                {'name': 'bar', 'startup': 'enabled', 'current': 'inactive'}),
         ])
 
         self.container.restart('foo', 'bar')
@@ -1423,10 +1416,10 @@ def restart_services(services):
 
     def test_restart_fallback_non_400_error(self):
         def restart_services(services):
-            raise APIError({}, 500, "", "")
+            raise pebble.APIError({}, 500, "", "")
 
         self.pebble.restart_services = restart_services
-        with self.assertRaises(ops.pebble.APIError) as cm:
+        with self.assertRaises(pebble.APIError) as cm:
             self.container.restart('foo')
         self.assertEqual(cm.exception.code, 500)
 
@@ -1435,15 +1428,15 @@ def test_restart_no_arguments(self):
             self.container.restart()
 
     def test_type_errors(self):
-        meta = ops.charm.CharmMeta.from_yaml("""
+        meta = ops.CharmMeta.from_yaml("""
 name: k8s-charm
 containers:
   c1:
     k: v
 """)
         # Only the real pebble Client checks types, so use actual backend class
-        backend = ops.model._ModelBackend('myapp/0')
-        model = ops.model.Model(meta, backend)
+        backend = _ModelBackend('myapp/0')
+        model = ops.Model(meta, backend)
         container = model.unit.containers['c1']
 
         with self.assertRaises(TypeError):
@@ -1455,7 +1448,7 @@ def test_type_errors(self):
     def test_add_layer(self):
         self.container.add_layer('a', 'summary: str\n')
         self.container.add_layer('b', {'summary': 'dict'})
-        self.container.add_layer('c', ops.pebble.Layer('summary: Layer'))
+        self.container.add_layer('c', pebble.Layer('summary: Layer'))
         self.container.add_layer('d', 'summary: str\n', combine=True)
         self.assertEqual(self.pebble.requests, [
             ('add_layer', 'a', 'summary: str\n', False),
@@ -1470,15 +1463,15 @@ def test_add_layer(self):
 
     def test_get_plan(self):
         plan_yaml = 'services:\n foo:\n  override: replace\n  command: bar'
-        self.pebble.responses.append(ops.pebble.Plan(plan_yaml))
+        self.pebble.responses.append(pebble.Plan(plan_yaml))
         plan = self.container.get_plan()
         self.assertEqual(self.pebble.requests, [('get_plan',)])
-        self.assertIsInstance(plan, ops.pebble.Plan)
+        self.assertIsInstance(plan, pebble.Plan)
         self.assertEqual(plan.to_yaml(), yaml.safe_dump(yaml.safe_load(plan_yaml)))
 
     @staticmethod
     def _make_service(name, startup, current):
-        return ops.pebble.ServiceInfo.from_dict(
+        return pebble.ServiceInfo.from_dict(
             {'name': name, 'startup': startup, 'current': current})
 
     def test_get_services(self):
@@ -1491,22 +1484,22 @@ def test_get_services(self):
         self.assertEqual(len(services), 2)
         self.assertEqual(set(services), {'s1', 's2'})
         self.assertEqual(services['s1'].name, 's1')
-        self.assertEqual(services['s1'].startup, ops.pebble.ServiceStartup.ENABLED)
-        self.assertEqual(services['s1'].current, ops.pebble.ServiceStatus.ACTIVE)
+        self.assertEqual(services['s1'].startup, pebble.ServiceStartup.ENABLED)
+        self.assertEqual(services['s1'].current, pebble.ServiceStatus.ACTIVE)
         self.assertEqual(services['s2'].name, 's2')
-        self.assertEqual(services['s2'].startup, ops.pebble.ServiceStartup.DISABLED)
-        self.assertEqual(services['s2'].current, ops.pebble.ServiceStatus.INACTIVE)
+        self.assertEqual(services['s2'].startup, pebble.ServiceStartup.DISABLED)
+        self.assertEqual(services['s2'].current, pebble.ServiceStatus.INACTIVE)
 
         self.pebble.responses.append(two_services)
         services = self.container.get_services('s1', 's2')
         self.assertEqual(len(services), 2)
         self.assertEqual(set(services), {'s1', 's2'})
         self.assertEqual(services['s1'].name, 's1')
-        self.assertEqual(services['s1'].startup, ops.pebble.ServiceStartup.ENABLED)
-        self.assertEqual(services['s1'].current, ops.pebble.ServiceStatus.ACTIVE)
+        self.assertEqual(services['s1'].startup, pebble.ServiceStartup.ENABLED)
+        self.assertEqual(services['s1'].current, pebble.ServiceStatus.ACTIVE)
         self.assertEqual(services['s2'].name, 's2')
-        self.assertEqual(services['s2'].startup, ops.pebble.ServiceStartup.DISABLED)
-        self.assertEqual(services['s2'].current, ops.pebble.ServiceStatus.INACTIVE)
+        self.assertEqual(services['s2'].startup, pebble.ServiceStartup.DISABLED)
+        self.assertEqual(services['s2'].current, pebble.ServiceStatus.INACTIVE)
 
         self.assertEqual(self.pebble.requests, [
             ('get_services', None),
@@ -1519,12 +1512,12 @@ def test_get_service(self):
         s = self.container.get_service('s1')
         self.assertEqual(self.pebble.requests, [('get_services', ('s1', ))])
         self.assertEqual(s.name, 's1')
-        self.assertEqual(s.startup, ops.pebble.ServiceStartup.ENABLED)
-        self.assertEqual(s.current, ops.pebble.ServiceStatus.ACTIVE)
+        self.assertEqual(s.startup, pebble.ServiceStartup.ENABLED)
+        self.assertEqual(s.current, pebble.ServiceStatus.ACTIVE)
 
-        # If Pebble returns no services, should be a ModelError
+        # If Pebble returns no services, should be a ops.ModelError
         self.pebble.responses.append([])
-        with self.assertRaises(ops.model.ModelError) as cm:
+        with self.assertRaises(ops.ModelError) as cm:
             self.container.get_service('s2')
         self.assertEqual(str(cm.exception), "service 's2' not found")
 
@@ -1538,13 +1531,13 @@ def test_get_service(self):
 
     def test_get_checks(self):
         response_checks = [
-            ops.pebble.CheckInfo.from_dict({
+            pebble.CheckInfo.from_dict({
                 'name': 'c1',
                 'status': 'up',
                 'failures': 0,
                 'threshold': 3,
             }),
-            ops.pebble.CheckInfo.from_dict({
+            pebble.CheckInfo.from_dict({
                 'name': 'c2',
                 'level': 'alive',
                 'status': 'down',
@@ -1557,34 +1550,34 @@ def test_get_checks(self):
         checks = self.container.get_checks()
         self.assertEqual(len(checks), 2)
         self.assertEqual(checks['c1'].name, 'c1')
-        self.assertEqual(checks['c1'].level, ops.pebble.CheckLevel.UNSET)
-        self.assertEqual(checks['c1'].status, ops.pebble.CheckStatus.UP)
+        self.assertEqual(checks['c1'].level, pebble.CheckLevel.UNSET)
+        self.assertEqual(checks['c1'].status, pebble.CheckStatus.UP)
         self.assertEqual(checks['c1'].failures, 0)
         self.assertEqual(checks['c1'].threshold, 3)
         self.assertEqual(checks['c2'].name, 'c2')
-        self.assertEqual(checks['c2'].level, ops.pebble.CheckLevel.ALIVE)
-        self.assertEqual(checks['c2'].status, ops.pebble.CheckStatus.DOWN)
+        self.assertEqual(checks['c2'].level, pebble.CheckLevel.ALIVE)
+        self.assertEqual(checks['c2'].status, pebble.CheckStatus.DOWN)
         self.assertEqual(checks['c2'].failures, 2)
         self.assertEqual(checks['c2'].threshold, 2)
 
         self.pebble.responses.append(response_checks[1:2])
-        checks = self.container.get_checks('c1', 'c2', level=ops.pebble.CheckLevel.ALIVE)
+        checks = self.container.get_checks('c1', 'c2', level=pebble.CheckLevel.ALIVE)
         self.assertEqual(len(checks), 1)
         self.assertEqual(checks['c2'].name, 'c2')
-        self.assertEqual(checks['c2'].level, ops.pebble.CheckLevel.ALIVE)
-        self.assertEqual(checks['c2'].status, ops.pebble.CheckStatus.DOWN)
+        self.assertEqual(checks['c2'].level, pebble.CheckLevel.ALIVE)
+        self.assertEqual(checks['c2'].status, pebble.CheckStatus.DOWN)
         self.assertEqual(checks['c2'].failures, 2)
         self.assertEqual(checks['c2'].threshold, 2)
 
         self.assertEqual(self.pebble.requests, [
             ('get_checks', None, None),
-            ('get_checks', ops.pebble.CheckLevel.ALIVE, ('c1', 'c2')),
+            ('get_checks', pebble.CheckLevel.ALIVE, ('c1', 'c2')),
         ])
 
     def test_get_check(self):
         # Single check returned successfully
         self.pebble.responses.append([
-            ops.pebble.CheckInfo.from_dict({
+            pebble.CheckInfo.from_dict({
                 'name': 'c1',
                 'status': 'up',
                 'failures': 0,
@@ -1594,26 +1587,26 @@ def test_get_check(self):
         c = self.container.get_check('c1')
         self.assertEqual(self.pebble.requests, [('get_checks', None, ('c1', ))])
         self.assertEqual(c.name, 'c1')
-        self.assertEqual(c.level, ops.pebble.CheckLevel.UNSET)
-        self.assertEqual(c.status, ops.pebble.CheckStatus.UP)
+        self.assertEqual(c.level, pebble.CheckLevel.UNSET)
+        self.assertEqual(c.status, pebble.CheckStatus.UP)
         self.assertEqual(c.failures, 0)
         self.assertEqual(c.threshold, 3)
 
-        # If Pebble returns no checks, should be a ModelError
+        # If Pebble returns no checks, should be a ops.ModelError
         self.pebble.responses.append([])
-        with self.assertRaises(ops.model.ModelError) as cm:
+        with self.assertRaises(ops.ModelError) as cm:
             self.container.get_check('c2')
         self.assertEqual(str(cm.exception), "check 'c2' not found")
 
         # If Pebble returns more than one check, RuntimeError is raised
         self.pebble.responses.append([
-            ops.pebble.CheckInfo.from_dict({
+            pebble.CheckInfo.from_dict({
                 'name': 'c1',
                 'status': 'up',
                 'failures': 0,
                 'threshold': 3,
             }),
-            ops.pebble.CheckInfo.from_dict({
+            pebble.CheckInfo.from_dict({
                 'name': 'c2',
                 'level': 'alive',
                 'status': 'down',
@@ -1696,14 +1689,14 @@ def test_remove_path(self):
         ])
 
     def test_can_connect_simple(self):
-        self.pebble.responses.append(SystemInfo.from_dict({'version': '1.0.0'}))
+        self.pebble.responses.append(pebble.SystemInfo.from_dict({'version': '1.0.0'}))
         self.assertTrue(self.container.can_connect())
 
     def test_can_connect_connection_error(self):
         def raise_error():
-            raise ConnectionError('connection error!')
+            raise pebble.ConnectionError('connection error!')
         self.pebble.get_system_info = raise_error
-        with self.assertLogs('ops.model', level='DEBUG') as cm:
+        with self.assertLogs('ops', level='DEBUG') as cm:
             self.assertFalse(self.container.can_connect())
         self.assertEqual(len(cm.output), 1)
         self.assertRegex(cm.output[0], r'DEBUG:ops.model:.*: connection error!')
@@ -1712,16 +1705,16 @@ def test_can_connect_file_not_found_error(self):
         def raise_error():
             raise FileNotFoundError('file not found!')
         self.pebble.get_system_info = raise_error
-        with self.assertLogs('ops.model', level='DEBUG') as cm:
+        with self.assertLogs('ops', level='DEBUG') as cm:
             self.assertFalse(self.container.can_connect())
         self.assertEqual(len(cm.output), 1)
         self.assertRegex(cm.output[0], r'DEBUG:ops.model:.*: file not found!')
 
     def test_can_connect_api_error(self):
         def raise_error():
-            raise APIError('body', 404, 'status', 'api error!')
+            raise pebble.APIError('body', 404, 'status', 'api error!')
         self.pebble.get_system_info = raise_error
-        with self.assertLogs('ops.model') as cm:
+        with self.assertLogs('ops') as cm:
             self.assertFalse(self.container.can_connect())
         self.assertEqual(len(cm.output), 1)
         self.assertRegex(cm.output[0], r'WARNING:ops.model:.*: api error!')
@@ -1778,7 +1771,7 @@ def test_send_signal(self):
         self.pebble.requests = []
 
 
-class MockPebbleBackend(ops.model._ModelBackend):
+class MockPebbleBackend(_ModelBackend):
     def get_pebble(self, socket_path):
         return MockPebbleClient(socket_path)
 
@@ -1810,8 +1803,8 @@ def restart_services(self, service_names):
 
     def add_layer(self, label, layer, combine=False):
         if isinstance(layer, dict):
-            layer = ops.pebble.Layer(layer).to_yaml()
-        elif isinstance(layer, ops.pebble.Layer):
+            layer = pebble.Layer(layer).to_yaml()
+        elif isinstance(layer, pebble.Layer):
             layer = layer.to_yaml()
         self.requests.append(('add_layer', label, layer, combine))
 
@@ -1859,17 +1852,17 @@ def send_signal(self, signal, service_names):
 class TestModelBindings(unittest.TestCase):
 
     def setUp(self):
-        meta = ops.charm.CharmMeta()
+        meta = ops.CharmMeta()
         meta.relations = {
-            'db0': RelationMeta(
-                RelationRole.provides, 'db0', {'interface': 'db0', 'scope': 'global'}),
-            'db1': RelationMeta(
-                RelationRole.requires, 'db1', {'interface': 'db1', 'scope': 'global'}),
-            'db2': RelationMeta(
-                RelationRole.peer, 'db2', {'interface': 'db2', 'scope': 'global'}),
+            'db0': ops.RelationMeta(
+                ops.RelationRole.provides, 'db0', {'interface': 'db0', 'scope': 'global'}),
+            'db1': ops.RelationMeta(
+                ops.RelationRole.requires, 'db1', {'interface': 'db1', 'scope': 'global'}),
+            'db2': ops.RelationMeta(
+                ops.RelationRole.peer, 'db2', {'interface': 'db2', 'scope': 'global'}),
         }
-        self.backend = ops.model._ModelBackend('myapp/0')
-        self.model = ops.model.Model(meta, self.backend)
+        self.backend = _ModelBackend('myapp/0')
+        self.model = ops.Model(meta, self.backend)
 
         fake_script(self, 'relation-ids',
                     """([ "$1" = db0 ] && echo '["db0:4"]') || echo '[]'""")
@@ -1961,7 +1954,7 @@ def _check_binding_data(self, binding_name, binding):
     def test_invalid_keys(self):
         # Basic validation for passing invalid keys.
         for name in (object, 0):
-            with self.assertRaises(ops.model.ModelError):
+            with self.assertRaises(ops.ModelError):
                 self.model.get_binding(name)
 
     def test_dead_relations(self):
@@ -1977,7 +1970,7 @@ def test_dead_relations(self):
                 fi
             ''')
         # Validate the behavior for dead relations.
-        binding = ops.model.Binding('db0', 42, self.model._backend)
+        binding = ops.Binding('db0', 42, self.model._backend)
         self.assertEqual(binding.network.bind_address, ipaddress.ip_address('192.0.2.2'))
         self.assertEqual(fake_script_calls(self, clear=True), [
             ['network-get', 'db0', '-r', '42', '--format=json'],
@@ -2127,7 +2120,7 @@ def setUp(self):
     @property
     def backend(self):
         if self._backend is None:
-            self._backend = ops.model._ModelBackend('myapp/0')
+            self._backend = _ModelBackend('myapp/0')
         return self._backend
 
     def test_relation_get_set_is_app_arg(self):
@@ -2147,10 +2140,10 @@ def test_relation_get_set_is_app_arg(self):
                 self.backend.relation_get(1, 'fooentity', is_app=is_app_v)
 
     def test_is_leader_refresh(self):
-        meta = ops.charm.CharmMeta.from_yaml('''
+        meta = ops.CharmMeta.from_yaml('''
             name: myapp
         ''')
-        model = ops.model.Model(meta, self.backend)
+        model = ops.Model(meta, self.backend)
         fake_script(self, 'is-leader', 'echo false')
         self.assertFalse(model.unit.is_leader())
 
@@ -2175,42 +2168,42 @@ def test_relation_tool_errors(self):
         test_cases = [(
             lambda: fake_script(self, 'relation-list', 'echo fooerror >&2 ; exit 1'),
             lambda: self.backend.relation_list(3),
-            ops.model.ModelError,
+            ops.ModelError,
             [['relation-list', '-r', '3', '--format=json']],
         ), (
             lambda: fake_script(self, 'relation-list', f'echo {err_msg} >&2 ; exit 2'),
             lambda: self.backend.relation_list(3),
-            ops.model.RelationNotFoundError,
+            ops.RelationNotFoundError,
             [['relation-list', '-r', '3', '--format=json']],
         ), (
             lambda: fake_script(self, 'relation-set', 'echo fooerror >&2 ; exit 1'),
             lambda: self.backend.relation_set(3, 'foo', 'bar', is_app=False),
-            ops.model.ModelError,
+            ops.ModelError,
             [['relation-set', '-r', '3', '--file', '-']],
         ), (
             lambda: fake_script(self, 'relation-set', f'echo {err_msg} >&2 ; exit 2'),
             lambda: self.backend.relation_set(3, 'foo', 'bar', is_app=False),
-            ops.model.RelationNotFoundError,
+            ops.RelationNotFoundError,
             [['relation-set', '-r', '3', '--file', '-']],
         ), (
             lambda: None,
             lambda: self.backend.relation_set(3, 'foo', 'bar', is_app=True),
-            ops.model.RelationNotFoundError,
+            ops.RelationNotFoundError,
             [['relation-set', '-r', '3', '--app', '--file', '-']],
         ), (
             lambda: fake_script(self, 'relation-get', 'echo fooerror >&2 ; exit 1'),
             lambda: self.backend.relation_get(3, 'remote/0', is_app=False),
-            ops.model.ModelError,
+            ops.ModelError,
             [['relation-get', '-r', '3', '-', 'remote/0', '--format=json']],
         ), (
             lambda: fake_script(self, 'relation-get', f'echo {err_msg} >&2 ; exit 2'),
             lambda: self.backend.relation_get(3, 'remote/0', is_app=False),
-            ops.model.RelationNotFoundError,
+            ops.RelationNotFoundError,
             [['relation-get', '-r', '3', '-', 'remote/0', '--format=json']],
         ), (
             lambda: None,
             lambda: self.backend.relation_get(3, 'remote/0', is_app=True),
-            ops.model.RelationNotFoundError,
+            ops.RelationNotFoundError,
             [['relation-get', '-r', '3', '-', 'remote/0', '--app', '--format=json']],
         )]
 
@@ -2319,27 +2312,27 @@ def test_status_is_app_forced_kwargs(self):
 
     def test_local_set_invalid_status(self):
         # juju returns exit code 1 if you ask to set status to 'unknown' or 'error'
-        meta = ops.charm.CharmMeta.from_yaml('''
+        meta = ops.CharmMeta.from_yaml('''
             name: myapp
         ''')
-        model = ops.model.Model(meta, self.backend)
+        model = ops.Model(meta, self.backend)
         fake_script(self, 'status-set', 'exit 1')
         fake_script(self, 'is-leader', 'echo true')
 
-        with self.assertRaises(ops.model.ModelError):
-            model.unit.status = ops.model.UnknownStatus()
-        with self.assertRaises(ops.model.ModelError):
-            model.unit.status = ops.model.ErrorStatus()
+        with self.assertRaises(ops.ModelError):
+            model.unit.status = ops.UnknownStatus()
+        with self.assertRaises(ops.ModelError):
+            model.unit.status = ops.ErrorStatus()
 
         self.assertEqual(fake_script_calls(self, True), [
             ['status-set', '--application=False', 'unknown', ''],
             ['status-set', '--application=False', 'error', ''],
         ])
 
-        with self.assertRaises(ops.model.ModelError):
-            model.app.status = ops.model.UnknownStatus()
-        with self.assertRaises(ops.model.ModelError):
-            model.app.status = ops.model.ErrorStatus()
+        with self.assertRaises(ops.ModelError):
+            model.app.status = ops.UnknownStatus()
+        with self.assertRaises(ops.ModelError):
+            model.app.status = ops.ErrorStatus()
 
         # A leadership check is needed for application status.
         self.assertEqual(fake_script_calls(self, True), [
@@ -2350,16 +2343,16 @@ def test_local_set_invalid_status(self):
 
     def test_local_get_status(self):
         for name, expected_cls in (
-            ("active", ops.model.ActiveStatus),
-            ("waiting", ops.model.WaitingStatus),
-            ("blocked", ops.model.BlockedStatus),
-            ("maintenance", ops.model.MaintenanceStatus),
-            ("error", ops.model.ErrorStatus),
+            ("active", ops.ActiveStatus),
+            ("waiting", ops.WaitingStatus),
+            ("blocked", ops.BlockedStatus),
+            ("maintenance", ops.MaintenanceStatus),
+            ("error", ops.ErrorStatus),
         ):
-            meta = ops.charm.CharmMeta.from_yaml('''
+            meta = ops.CharmMeta.from_yaml('''
                 name: myapp
             ''')
-            model = ops.model.Model(meta, self.backend)
+            model = ops.Model(meta, self.backend)
 
             with self.subTest(name):
                 content = json.dumps({
@@ -2390,23 +2383,23 @@ def test_local_get_status(self):
     def test_status_set_is_app_not_bool_raises(self):
         for is_app_v in [None, 1, 2.0, 'a', b'beef', object]:
             with self.assertRaises(TypeError):
-                self.backend.status_set(ops.model.ActiveStatus, is_app=is_app_v)
+                self.backend.status_set(ops.ActiveStatus, is_app=is_app_v)
 
     def test_storage_tool_errors(self):
         test_cases = [(
             lambda: fake_script(self, 'storage-list', 'echo fooerror >&2 ; exit 1'),
             lambda: self.backend.storage_list('foobar'),
-            ops.model.ModelError,
+            ops.ModelError,
             [['storage-list', 'foobar', '--format=json']],
         ), (
             lambda: fake_script(self, 'storage-get', 'echo fooerror >&2 ; exit 1'),
             lambda: self.backend.storage_get('foobar', 'someattr'),
-            ops.model.ModelError,
+            ops.ModelError,
             [['storage-get', '-s', 'foobar', 'someattr', '--format=json']],
         ), (
             lambda: fake_script(self, 'storage-add', 'echo fooerror >&2 ; exit 1'),
             lambda: self.backend.storage_add('foobar', count=2),
-            ops.model.ModelError,
+            ops.ModelError,
             [['storage-add', 'foobar=2']],
         ), (
             lambda: fake_script(self, 'storage-add', 'echo fooerror >&2 ; exit 1'),
@@ -2467,12 +2460,12 @@ def test_network_get_errors(self):
             lambda: fake_script(self, 'network-get',
                                 f'echo {err_no_endpoint} >&2 ; exit 1'),
             lambda: self.backend.network_get("deadbeef"),
-            ops.model.ModelError,
+            ops.ModelError,
             [['network-get', 'deadbeef', '--format=json']],
         ), (
             lambda: fake_script(self, 'network-get', f'echo {err_no_rel} >&2 ; exit 2'),
             lambda: self.backend.network_get("deadbeef", 3),
-            ops.model.RelationNotFoundError,
+            ops.RelationNotFoundError,
             [['network-get', 'deadbeef', '-r', '3', '--format=json']],
         )]
         for do_fake, run, exception, calls in test_cases:
@@ -2484,7 +2477,7 @@ def test_network_get_errors(self):
     def test_action_get_error(self):
         fake_script(self, 'action-get', '')
         fake_script(self, 'action-get', 'echo fooerror >&2 ; exit 1')
-        with self.assertRaises(ops.model.ModelError):
+        with self.assertRaises(ops.ModelError):
             self.backend.action_get()
         calls = [['action-get', '--format=json']]
         self.assertEqual(fake_script_calls(self, clear=True), calls)
@@ -2492,7 +2485,7 @@ def test_action_get_error(self):
     def test_action_set_error(self):
         fake_script(self, 'action-get', '')
         fake_script(self, 'action-set', 'echo fooerror >&2 ; exit 1')
-        with self.assertRaises(ops.model.ModelError):
+        with self.assertRaises(ops.ModelError):
             self.backend.action_set(OrderedDict([('foo', 'bar'), ('dead', 'beef cafe')]))
         self.assertCountEqual(
             ["action-set", "dead=beef cafe", "foo=bar"], fake_script_calls(self, clear=True)[0])
@@ -2500,7 +2493,7 @@ def test_action_set_error(self):
     def test_action_log_error(self):
         fake_script(self, 'action-get', '')
         fake_script(self, 'action-log', 'echo fooerror >&2 ; exit 1')
-        with self.assertRaises(ops.model.ModelError):
+        with self.assertRaises(ops.ModelError):
             self.backend.action_log('log-message')
         calls = [["action-log", "log-message"]]
         self.assertEqual(fake_script_calls(self, clear=True), calls)
@@ -2591,7 +2584,7 @@ def test_juju_log(self):
         self.assertEqual(fake_script_calls(self, clear=True), [])
 
         fake_script(self, 'juju-log', 'exit 1')
-        with self.assertRaises(ops.model.ModelError):
+        with self.assertRaises(ops.ModelError):
             self.backend.juju_log('BAR', 'foo')
         self.assertEqual(fake_script_calls(self, clear=True),
                          [['juju-log', '--log-level', 'BAR', '--', 'foo']])
@@ -2628,7 +2621,7 @@ def test_invalid_metric_names(self):
             ({'BAЯ': 4.2}, {}),
         ]
         for metrics, labels in invalid_inputs:
-            with self.assertRaises(ops.model.ModelError):
+            with self.assertRaises(ops.ModelError):
                 self.backend.add_metrics(metrics, labels)
 
     def test_invalid_metric_values(self):
@@ -2640,7 +2633,7 @@ def test_invalid_metric_values(self):
             ({'foo': '1O'}, {}),
         ]
         for metrics, labels in invalid_inputs:
-            with self.assertRaises(ops.model.ModelError):
+            with self.assertRaises(ops.ModelError):
                 self.backend.add_metrics(metrics, labels)
 
     def test_invalid_metric_labels(self):
@@ -2651,7 +2644,7 @@ def test_invalid_metric_labels(self):
             ({'foo': 4.2}, {'BAЯ': 'baz'}),
         ]
         for metrics, labels in invalid_inputs:
-            with self.assertRaises(ops.model.ModelError):
+            with self.assertRaises(ops.ModelError):
                 self.backend.add_metrics(metrics, labels)
 
     def test_invalid_metric_label_values(self):
@@ -2661,7 +2654,7 @@ def test_invalid_metric_label_values(self):
             ({'foo': 4.2}, {'bar': 'b=az'}),
         ]
         for metrics, labels in invalid_inputs:
-            with self.assertRaises(ops.model.ModelError):
+            with self.assertRaises(ops.ModelError):
                 self.backend.add_metrics(metrics, labels)
 
     def test_relation_remote_app_name_env(self):
@@ -2725,7 +2718,7 @@ class TestLazyMapping(unittest.TestCase):
     def test_invalidate(self):
         loaded = []
 
-        class MyLazyMap(ops.model.LazyMapping):
+        class MyLazyMap(ops.LazyMapping):
             def _load(self):
                 loaded.append(1)
                 return {'foo': 'bar'}
@@ -2742,7 +2735,7 @@ def _load(self):
 
 class TestSecrets(unittest.TestCase):
     def setUp(self):
-        self.model = ops.model.Model(ops.charm.CharmMeta(), ops.model._ModelBackend('myapp/0'))
+        self.model = ops.Model(ops.CharmMeta(), _ModelBackend('myapp/0'))
         self.app = self.model.app
         self.unit = self.model.unit
 
@@ -2750,7 +2743,7 @@ def test_app_add_secret_simple(self):
         fake_script(self, 'secret-add', 'echo secret:123')
 
         secret = self.app.add_secret({'foo': 'x'})
-        self.assertIsInstance(secret, model.Secret)
+        self.assertIsInstance(secret, ops.Secret)
         self.assertEqual(secret.id, 'secret:123')
         self.assertIsNone(secret.label)
 
@@ -2762,7 +2755,7 @@ def test_app_add_secret_args(self):
 
         expire = datetime.datetime(2022, 12, 9, 16, 17, 0)
         secret = self.app.add_secret({'foo': 'x', 'bar': 'y'}, label='lbl', description='desc',
-                                     expire=expire, rotate=model.SecretRotate.HOURLY)
+                                     expire=expire, rotate=ops.SecretRotate.HOURLY)
         self.assertEqual(secret.id, 'secret:234')
         self.assertEqual(secret.label, 'lbl')
         self.assertEqual(secret.get_content(), {'foo': 'x', 'bar': 'y'})
@@ -2776,7 +2769,7 @@ def test_unit_add_secret_simple(self):
         fake_script(self, 'secret-add', 'echo secret:345')
 
         secret = self.unit.add_secret({'foo': 'x'})
-        self.assertIsInstance(secret, model.Secret)
+        self.assertIsInstance(secret, ops.Secret)
         self.assertEqual(secret.id, 'secret:345')
         self.assertIsNone(secret.label)
 
@@ -2788,7 +2781,7 @@ def test_unit_add_secret_args(self):
 
         expire = datetime.datetime(2022, 12, 9, 16, 22, 0)
         secret = self.unit.add_secret({'foo': 'w', 'bar': 'z'}, label='l2', description='xyz',
-                                      expire=expire, rotate=model.SecretRotate.YEARLY)
+                                      expire=expire, rotate=ops.SecretRotate.YEARLY)
         self.assertEqual(secret.id, 'secret:456')
         self.assertEqual(secret.label, 'l2')
         self.assertEqual(secret.get_content(), {'foo': 'w', 'bar': 'z'})
@@ -2874,7 +2867,7 @@ def test_get_secret_not_found(self):
         fake_script(self, 'secret-get', script)
         fake_script(self, 'secret-info-get', script)
 
-        with self.assertRaises(model.SecretNotFoundError):
+        with self.assertRaises(ops.SecretNotFoundError):
             self.model.get_secret(id='123')
 
     def test_get_secret_other_error(self):
@@ -2882,26 +2875,26 @@ def test_get_secret_other_error(self):
         fake_script(self, 'secret-get', script)
         fake_script(self, 'secret-info-get', script)
 
-        with self.assertRaises(model.ModelError) as cm:
+        with self.assertRaises(ops.ModelError) as cm:
             self.model.get_secret(id='123')
-        self.assertNotIsInstance(cm.exception, model.SecretNotFoundError)
+        self.assertNotIsInstance(cm.exception, ops.SecretNotFoundError)
 
 
 class TestSecretInfo(unittest.TestCase):
     def test_init(self):
-        info = model.SecretInfo(
+        info = ops.SecretInfo(
             id='3',
             label='lbl',
             revision=7,
             expires=datetime.datetime(2022, 12, 9, 14, 10, 0),
-            rotation=model.SecretRotate.MONTHLY,
+            rotation=ops.SecretRotate.MONTHLY,
             rotates=datetime.datetime(2023, 1, 9, 14, 10, 0),
         )
         self.assertEqual(info.id, 'secret:3')
         self.assertEqual(info.label, 'lbl')
         self.assertEqual(info.revision, 7)
         self.assertEqual(info.expires, datetime.datetime(2022, 12, 9, 14, 10, 0))
-        self.assertEqual(info.rotation, model.SecretRotate.MONTHLY)
+        self.assertEqual(info.rotation, ops.SecretRotate.MONTHLY)
         self.assertEqual(info.rotates, datetime.datetime(2023, 1, 9, 14, 10, 0))
 
         self.assertTrue(repr(info).startswith('SecretInfo('))
@@ -2909,7 +2902,7 @@ def test_init(self):
 
     def test_from_dict(self):
         utc = datetime.timezone.utc
-        info = model.SecretInfo.from_dict('secret:4', {
+        info = ops.SecretInfo.from_dict('secret:4', {
             'label': 'fromdict',
             'revision': 8,
             'expires': '2022-12-09T14:10:00Z',
@@ -2920,10 +2913,10 @@ def test_from_dict(self):
         self.assertEqual(info.label, 'fromdict')
         self.assertEqual(info.revision, 8)
         self.assertEqual(info.expires, datetime.datetime(2022, 12, 9, 14, 10, 0, tzinfo=utc))
-        self.assertEqual(info.rotation, model.SecretRotate.YEARLY)
+        self.assertEqual(info.rotation, ops.SecretRotate.YEARLY)
         self.assertEqual(info.rotates, datetime.datetime(2023, 1, 9, 14, 10, 0, tzinfo=utc))
 
-        info = model.SecretInfo.from_dict('secret:4', {
+        info = ops.SecretInfo.from_dict('secret:4', {
             'label': 'fromdict',
             'revision': 8,
             'rotation': 'badvalue',
@@ -2935,7 +2928,7 @@ def test_from_dict(self):
         self.assertIsNone(info.rotation)
         self.assertIsNone(info.rotates)
 
-        info = model.SecretInfo.from_dict('5', {'revision': 9})
+        info = ops.SecretInfo.from_dict('5', {'revision': 9})
         self.assertEqual(info.id, 'secret:5')
         self.assertEqual(info.revision, 9)
 
@@ -2944,10 +2937,10 @@ class TestSecretClass(unittest.TestCase):
     maxDiff = 64 * 1024
 
     def setUp(self):
-        self.model = ops.model.Model(ops.charm.CharmMeta(), ops.model._ModelBackend('myapp/0'))
+        self.model = ops.Model(ops.CharmMeta(), _ModelBackend('myapp/0'))
 
     def make_secret(self, id=None, label=None, content=None):
-        return model.Secret(self.model._backend, id=id, label=label, content=content)
+        return ops.Secret(self.model._backend, id=id, label=label, content=content)
 
     def test_id_and_label(self):
         secret = self.make_secret(id=' abc ', label='lbl')
@@ -3065,7 +3058,7 @@ def test_set_info(self):
             label='lab',
             description='desc',
             expire=expire,
-            rotate=model.SecretRotate.MONTHLY,
+            rotate=ops.SecretRotate.MONTHLY,
         )
 
         # If secret doesn't have an ID, we'll run secret-info-get to fetch it
@@ -3186,7 +3179,7 @@ def test_open_port(self):
     def test_open_port_error(self):
         fake_script(self, 'open-port', "echo 'ERROR bad protocol' >&2; exit 1")
 
-        with self.assertRaises(model.ModelError) as cm:
+        with self.assertRaises(ops.ModelError) as cm:
             self.unit.open_port('ftp', 8080)
         self.assertEqual(str(cm.exception), 'ERROR bad protocol\n')
 
@@ -3210,7 +3203,7 @@ def test_close_port(self):
     def test_close_port_error(self):
         fake_script(self, 'close-port', "echo 'ERROR bad protocol' >&2; exit 1")
 
-        with self.assertRaises(model.ModelError) as cm:
+        with self.assertRaises(ops.ModelError) as cm:
             self.unit.close_port('ftp', 8080)
         self.assertEqual(str(cm.exception), 'ERROR bad protocol\n')
 
@@ -3225,10 +3218,10 @@ def test_opened_ports(self):
         self.assertIsInstance(ports_set, set)
         ports = sorted(ports_set, key=lambda p: (p.protocol, p.port))
         self.assertEqual(len(ports), 2)
-        self.assertIsInstance(ports[0], model.OpenedPort)
+        self.assertIsInstance(ports[0], ops.OpenedPort)
         self.assertEqual(ports[0].protocol, 'icmp')
         self.assertIsNone(ports[0].port)
-        self.assertIsInstance(ports[1], model.OpenedPort)
+        self.assertIsInstance(ports[1], ops.OpenedPort)
         self.assertEqual(ports[1].protocol, 'tcp')
         self.assertEqual(ports[1].port, 8080)
 
@@ -3248,10 +3241,10 @@ def test_opened_ports_warnings(self):
         self.assertIsInstance(ports_set, set)
         ports = sorted(ports_set, key=lambda p: (p.protocol, p.port))
         self.assertEqual(len(ports), 2)
-        self.assertIsInstance(ports[0], model.OpenedPort)
+        self.assertIsInstance(ports[0], ops.OpenedPort)
         self.assertEqual(ports[0].protocol, 'tcp')
         self.assertEqual(ports[0].port, 8080)
-        self.assertIsInstance(ports[1], model.OpenedPort)
+        self.assertIsInstance(ports[1], ops.OpenedPort)
         self.assertEqual(ports[1].protocol, 'udp')
         self.assertEqual(ports[1].port, 1000)
 
diff --git a/test/test_pebble.py b/test/test_pebble.py
index 5cfb0929d..2bd206a78 100644
--- a/test/test_pebble.py
+++ b/test/test_pebble.py
@@ -34,7 +34,7 @@
 import pytest
 import websocket
 
-import ops.pebble as pebble
+from ops import pebble
 from ops._private import yaml
 
 # Ensure unittest diffs don't get truncated like "[17 chars]"
diff --git a/test/test_storage.py b/test/test_storage.py
index f1027eb67..fdc32323e 100755
--- a/test/test_storage.py
+++ b/test/test_storage.py
@@ -24,24 +24,24 @@
 
 import yaml
 
-from ops import framework, storage
+import ops
 
 
 class StoragePermutations(abc.ABC):
 
-    def create_framework(self) -> framework.Framework:
+    def create_framework(self) -> ops.Framework:
         """Create a Framework that we can use to test the backend storage."""
-        return framework.Framework(self.create_storage(), None, None, None)
+        return ops.Framework(self.create_storage(), None, None, None)
 
     @abc.abstractmethod
-    def create_storage(self) -> storage.SQLiteStorage:
+    def create_storage(self) -> ops.storage.SQLiteStorage:
         """Create a Storage backend that we can interact with."""
         return NotImplemented
 
     def test_save_and_load_snapshot(self):
         f = self.create_framework()
 
-        class Sample(framework.Object):
+        class Sample(ops.Object):
 
             def __init__(self, parent, key, content):
                 super().__init__(parent, key)
@@ -74,7 +74,7 @@ def restore(self, snapshot):
     def test_emit_event(self):
         f = self.create_framework()
 
-        class Evt(framework.EventBase):
+        class Evt(ops.EventBase):
             def __init__(self, handle, content):
                 super().__init__(handle)
                 self.content = content
@@ -85,10 +85,10 @@ def snapshot(self):
             def restore(self, content):
                 self.content = content
 
-        class Events(framework.ObjectEvents):
-            event = framework.EventSource(Evt)
+        class Events(ops.ObjectEvents):
+            event = ops.EventSource(Evt)
 
-        class Sample(framework.Object):
+        class Sample(ops.Object):
 
             on = Events()
 
@@ -121,37 +121,37 @@ def test_drop_snapshot(self):
         store.save_snapshot('foo', {1: 2})
         self.assertEqual({1: 2}, store.load_snapshot('foo'))
         store.drop_snapshot('foo')
-        with self.assertRaises(storage.NoSnapshotError):
+        with self.assertRaises(ops.storage.NoSnapshotError):
             store.load_snapshot('foo')
 
     def test_save_snapshot_empty_string(self):
         store = self.create_storage()
-        with self.assertRaises(storage.NoSnapshotError):
+        with self.assertRaises(ops.storage.NoSnapshotError):
             store.load_snapshot('foo')
         store.save_snapshot('foo', '')
         self.assertEqual('', store.load_snapshot('foo'))
         store.drop_snapshot('foo')
-        with self.assertRaises(storage.NoSnapshotError):
+        with self.assertRaises(ops.storage.NoSnapshotError):
             store.load_snapshot('foo')
 
     def test_save_snapshot_none(self):
         store = self.create_storage()
-        with self.assertRaises(storage.NoSnapshotError):
+        with self.assertRaises(ops.storage.NoSnapshotError):
             store.load_snapshot('bar')
         store.save_snapshot('bar', None)
         self.assertEqual(None, store.load_snapshot('bar'))
         store.drop_snapshot('bar')
-        with self.assertRaises(storage.NoSnapshotError):
+        with self.assertRaises(ops.storage.NoSnapshotError):
             store.load_snapshot('bar')
 
     def test_save_snapshot_zero(self):
         store = self.create_storage()
-        with self.assertRaises(storage.NoSnapshotError):
+        with self.assertRaises(ops.storage.NoSnapshotError):
             store.load_snapshot('zero')
         store.save_snapshot('zero', 0)
         self.assertEqual(0, store.load_snapshot('zero'))
         store.drop_snapshot('zero')
-        with self.assertRaises(storage.NoSnapshotError):
+        with self.assertRaises(ops.storage.NoSnapshotError):
             store.load_snapshot('zero')
 
     def test_save_notice(self):
@@ -203,7 +203,7 @@ def test_save_load_drop_load_notices(self):
 class TestSQLiteStorage(StoragePermutations, BaseTestCase):
 
     def create_storage(self):
-        return storage.SQLiteStorage(':memory:')
+        return ops.storage.SQLiteStorage(':memory:')
 
 
 def setup_juju_backend(test_case, state_file):
@@ -281,38 +281,38 @@ def create_storage(self):
         state_file = pathlib.Path(fn)
         self.addCleanup(state_file.unlink)
         setup_juju_backend(self, state_file)
-        return storage.JujuStorage()
+        return ops.storage.JujuStorage()
 
 
 class TestSimpleLoader(BaseTestCase):
 
     def test_is_c_loader(self):
-        loader = storage._SimpleLoader(io.StringIO(''))
+        loader = ops.storage._SimpleLoader(io.StringIO(''))
         if getattr(yaml, 'CSafeLoader', None) is not None:
             self.assertIsInstance(loader, yaml.CSafeLoader)
         else:
             self.assertIsInstance(loader, yaml.SafeLoader)
 
     def test_is_c_dumper(self):
-        dumper = storage._SimpleDumper(io.StringIO(''))
+        dumper = ops.storage._SimpleDumper(io.StringIO(''))
         if getattr(yaml, 'CSafeDumper', None) is not None:
             self.assertIsInstance(dumper, yaml.CSafeDumper)
         else:
             self.assertIsInstance(dumper, yaml.SafeDumper)
 
     def test_handles_tuples(self):
-        raw = yaml.dump((1, 'tuple'), Dumper=storage._SimpleDumper)
-        parsed = yaml.load(raw, Loader=storage._SimpleLoader)
+        raw = yaml.dump((1, 'tuple'), Dumper=ops.storage._SimpleDumper)
+        parsed = yaml.load(raw, Loader=ops.storage._SimpleLoader)
         self.assertEqual(parsed, (1, 'tuple'))
 
     def assertRefused(self, obj):  # noqa: N802
         # We shouldn't allow them to be written
         with self.assertRaises(yaml.representer.RepresenterError):
-            yaml.dump(obj, Dumper=storage._SimpleDumper)
+            yaml.dump(obj, Dumper=ops.storage._SimpleDumper)
         # If they did somehow end up written, we shouldn't be able to load them
         raw = yaml.dump(obj, Dumper=yaml.Dumper)
         with self.assertRaises(yaml.constructor.ConstructorError):
-            yaml.load(raw, Loader=storage._SimpleLoader)
+            yaml.load(raw, Loader=ops.storage._SimpleLoader)
 
     def test_forbids_some_types(self):
         self.assertRefused(1 + 2j)
@@ -330,11 +330,11 @@ class Foo:
 class TestJujuStateBackend(BaseTestCase):
 
     def test_is_not_available(self):
-        self.assertFalse(storage.juju_backend_available())
+        self.assertFalse(ops.storage.juju_backend_available())
 
     def test_is_available(self):
         fake_script(self, 'state-get', 'echo ""')
-        self.assertTrue(storage.juju_backend_available())
+        self.assertTrue(ops.storage.juju_backend_available())
         self.assertEqual(fake_script_calls(self, clear=True), [])
 
     def test_set_encodes_args(self):
@@ -343,7 +343,7 @@ def test_set_encodes_args(self):
             fake_script(self, 'state-set', dedent("""
                 cat >> {}
                 """).format(pathlib.Path(t.name).as_posix()))
-            backend = storage._JujuStorageBackend()
+            backend = ops.storage._JujuStorageBackend()
             backend.set('key', {'foo': 2})
             self.assertEqual(fake_script_calls(self, clear=True), [
                 ['state-set', '--file', '-'],
@@ -361,7 +361,7 @@ def test_get(self):
         fake_script(self, 'state-get', dedent("""
             echo 'foo: "bar"'
             """))
-        backend = storage._JujuStorageBackend()
+        backend = ops.storage._JujuStorageBackend()
         value = backend.get('key')
         self.assertEqual(value, {'foo': 'bar'})
         self.assertEqual(fake_script_calls(self, clear=True), [
@@ -374,7 +374,7 @@ def test_set_and_get_complex_value(self):
             fake_script(self, 'state-set', dedent("""
                 cat >> {}
                 """).format(pathlib.Path(t.name).as_posix()))
-            backend = storage._JujuStorageBackend()
+            backend = ops.storage._JujuStorageBackend()
             complex_val = {
                 'foo': 2,
                 3: [1, 2, '3'],
@@ -394,7 +394,7 @@ def test_set_and_get_complex_value(self):
         outer = yaml.safe_load(content)
         key = 'Class[foo]/_stored'
         self.assertEqual(list(outer.keys()), [key])
-        inner = yaml.load(outer[key], Loader=storage._SimpleLoader)
+        inner = yaml.load(outer[key], Loader=ops.storage._SimpleLoader)
         self.assertEqual(complex_val, inner)
         self.assertEqual(content.decode('utf-8'), dedent("""\
             "Class[foo]/_stored": |
diff --git a/test/test_testing.py b/test/test_testing.py
index 581054e79..17d97e1ae 100644
--- a/test/test_testing.py
+++ b/test/test_testing.py
@@ -33,29 +33,11 @@
 import pytest
 import yaml
 
+import ops
 import ops.testing
-from ops import model, pebble
-from ops.charm import (
-    CharmBase,
-    PebbleReadyEvent,
-    RelationDepartedEvent,
-    RelationEvent,
-    StorageAttachedEvent,
-    StorageDetachingEvent,
-)
-from ops.framework import Framework, Object
-from ops.model import (
-    ActiveStatus,
-    Application,
-    MaintenanceStatus,
-    ModelError,
-    RelationNotFoundError,
-    Unit,
-    UnknownStatus,
-    _ModelBackend,
-)
+from ops import pebble
+from ops.model import _ModelBackend
 from ops.testing import (
-    Harness,
     NonAbsolutePathError,
     _Directory,
     _TestingFilesystem,
@@ -66,7 +48,7 @@
 is_linux = platform.system() == 'Linux'
 
 
-class SetLeaderErrorTester(CharmBase):
+class SetLeaderErrorTester(ops.CharmBase):
     """Sets peer relation data inside leader-elected."""
 
     def __init__(self, framework):
@@ -80,7 +62,7 @@ def _on_leader_elected(self, event):
         peers.data[self.app]["foo"] = "bar"
 
 
-class StorageTester(CharmBase):
+class StorageTester(ops.CharmBase):
     """Record the relation-changed events."""
 
     def __init__(self, framework):
@@ -98,7 +80,7 @@ def _on_test_storage_detaching(self, event):
         self.observed_events.append(event)
 
 
-class StorageWithHyphensHelper(Object):
+class StorageWithHyphensHelper(ops.Object):
     def __init__(self, parent, key):
         super().__init__(parent, key)
         self.changes = []
@@ -114,7 +96,7 @@ def on_storage_changed(self, event):
 class TestHarness(unittest.TestCase):
 
     def test_add_relation(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             requires:
                 db:
@@ -131,7 +113,7 @@ def test_add_relation(self):
         self.assertEqual(backend.relation_get(rel_id, 'test-app/0', is_app=False), {})
 
     def test_can_connect_default(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             containers:
               foo:
@@ -159,17 +141,17 @@ def test_can_connect_default(self):
     def test_can_connect_begin_with_initial_hooks(self):
         pebble_ready_calls = collections.defaultdict(int)
 
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             def __init__(self, *args):
                 super().__init__(*args)
                 self.framework.observe(self.on.foo_pebble_ready, self._on_pebble_ready)
                 self.framework.observe(self.on.bar_pebble_ready, self._on_pebble_ready)
 
-            def _on_pebble_ready(self, event: PebbleReadyEvent):
+            def _on_pebble_ready(self, event: ops.PebbleReadyEvent):
                 assert event.workload.can_connect()
                 pebble_ready_calls[event.workload.name] += 1
 
-        harness = Harness(MyCharm, meta='''
+        harness = ops.testing.Harness(MyCharm, meta='''
             name: test-app
             containers:
               foo:
@@ -193,7 +175,7 @@ def _on_pebble_ready(self, event: PebbleReadyEvent):
         container.get_plan()  # shouldn't raise ConnectionError
 
     def test_add_relation_and_unit(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             requires:
                 db:
@@ -213,7 +195,7 @@ def test_add_relation_and_unit(self):
 
     def test_add_relation_with_remote_app_data(self):
         # language=YAML
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             requires:
                 db:
@@ -230,7 +212,7 @@ def test_add_relation_with_remote_app_data(self):
 
     def test_add_relation_with_our_initial_data(self):
 
-        class InitialDataTester(CharmBase):
+        class InitialDataTester(ops.CharmBase):
             """Record the relation-changed events."""
 
             def __init__(self, framework):
@@ -242,7 +224,7 @@ def _on_db_relation_changed(self, event):
                 self.observed_events.append(event)
 
         # language=YAML
-        harness = Harness(InitialDataTester, meta='''
+        harness = ops.testing.Harness(InitialDataTester, meta='''
             name: test-app
             requires:
                 db:
@@ -280,7 +262,7 @@ def _on_db_relation_changed(self, event):
 
     def test_add_peer_relation_with_initial_data_leader(self):
 
-        class InitialDataTester(CharmBase):
+        class InitialDataTester(ops.CharmBase):
             """Record the relation-changed events."""
 
             def __init__(self, framework):
@@ -293,7 +275,7 @@ def _on_cluster_relation_changed(self, event):
                 self.observed_events.append(event)
 
         # language=YAML
-        harness = Harness(InitialDataTester, meta='''
+        harness = ops.testing.Harness(InitialDataTester, meta='''
             name: test-app
             peers:
                 cluster:
@@ -329,10 +311,10 @@ def _on_cluster_relation_changed(self, event):
         harness.update_relation_data(rel_id, 'test-app', {'k': 'v3'})
         self.assertEqual({'k': 'v3'}, backend.relation_get(rel_id, 'test-app', is_app=True))
         self.assertTrue(len(harness.charm.observed_events), 1)
-        self.assertIsInstance(harness.charm.observed_events[0], RelationEvent)
+        self.assertIsInstance(harness.charm.observed_events[0], ops.RelationEvent)
 
     def test_relation_get_when_broken(self):
-        harness = Harness(RelationBrokenTester, meta='''
+        harness = ops.testing.Harness(RelationBrokenTester, meta='''
             name: test-app
             requires:
                 foo:
@@ -350,7 +332,7 @@ def test_relation_get_when_broken(self):
             harness.remove_relation(rel_id)
 
     def test_remove_relation(self):
-        harness = Harness(RelationEventCharm, meta='''
+        harness = ops.testing.Harness(RelationEventCharm, meta='''
             name: test-app
             requires:
                 db:
@@ -372,7 +354,7 @@ def test_remove_relation(self):
         harness.remove_relation(rel_id)
         # Check relation no longer exists
         self.assertEqual(backend.relation_ids('db'), [])
-        self.assertRaises(RelationNotFoundError, backend.relation_list, rel_id)
+        self.assertRaises(ops.RelationNotFoundError, backend.relation_list, rel_id)
         # Check relation broken event is raised with correct data
         changes = harness.charm.get_changes()
         self.assertEqual(changes[0],
@@ -390,7 +372,7 @@ def test_remove_relation(self):
                                    'relation_id': rel_id}})
 
     def test_remove_specific_relation_id(self):
-        harness = Harness(RelationEventCharm, meta='''
+        harness = ops.testing.Harness(RelationEventCharm, meta='''
             name: test-app
             requires:
                 db:
@@ -425,7 +407,7 @@ def test_remove_specific_relation_id(self):
         harness.remove_relation(rel_id_2)
         # Check second relation no longer exists but first does
         self.assertEqual(backend.relation_ids('db'), [rel_id_1])
-        self.assertRaises(RelationNotFoundError, backend.relation_list, rel_id_2)
+        self.assertRaises(ops.RelationNotFoundError, backend.relation_list, rel_id_2)
 
         # Check relation broken event is raised with correct data
         changes = harness.charm.get_changes()
@@ -444,7 +426,7 @@ def test_remove_specific_relation_id(self):
                                    'relation_id': rel_id_2}})
 
     def test_removing_invalid_relation_id_raises_exception(self):
-        harness = Harness(RelationEventCharm, meta='''
+        harness = ops.testing.Harness(RelationEventCharm, meta='''
             name: test-app
             requires:
                 db:
@@ -463,11 +445,11 @@ def test_removing_invalid_relation_id_raises_exception(self):
         self.assertEqual(backend.relation_list(rel_id), ['postgresql/0'])
         harness.charm.get_changes(reset=True)  # created event ignored
         # Check exception is raised if relation id is invalid
-        with self.assertRaises(RelationNotFoundError):
+        with self.assertRaises(ops.RelationNotFoundError):
             harness.remove_relation(rel_id + 1)
 
     def test_remove_relation_unit(self):
-        harness = Harness(RelationEventCharm, meta='''
+        harness = ops.testing.Harness(RelationEventCharm, meta='''
             name: test-app
             requires:
                 db:
@@ -517,7 +499,7 @@ def test_remove_relation_unit(self):
 
     def test_removing_relation_removes_remote_app_data(self):
         # language=YAML
-        harness = Harness(RelationEventCharm, meta='''
+        harness = ops.testing.Harness(RelationEventCharm, meta='''
             name: test-app
             requires:
                 db:
@@ -539,12 +521,12 @@ def test_removing_relation_removes_remote_app_data(self):
         # Check relation and app data are removed
         self.assertEqual(backend.relation_ids('db'), [])
         with harness._event_context('foo'):
-            self.assertRaises(RelationNotFoundError, backend.relation_get,
+            self.assertRaises(ops.RelationNotFoundError, backend.relation_get,
                               rel_id, remote_app, is_app=True)
 
     def test_removing_relation_refreshes_charm_model(self):
         # language=YAML
-        harness = Harness(RelationEventCharm, meta='''
+        harness = ops.testing.Harness(RelationEventCharm, meta='''
             name: test-app
             requires:
                 db:
@@ -575,7 +557,7 @@ def _find_relation_in_model_by_id(self, harness, rel_id):
         return None
 
     def test_removing_relation_unit_removes_data_also(self):
-        harness = Harness(RelationEventCharm, meta='''
+        harness = ops.testing.Harness(RelationEventCharm, meta='''
             name: test-app
             requires:
                 db:
@@ -617,7 +599,7 @@ def test_removing_relation_unit_removes_data_also(self):
                                    'relation_id': rel_id}})
 
     def test_removing_relation_unit_does_not_remove_other_unit_and_data(self):
-        harness = Harness(RelationEventCharm, meta='''
+        harness = ops.testing.Harness(RelationEventCharm, meta='''
             name: test-app
             requires:
                 db:
@@ -663,7 +645,7 @@ def test_removing_relation_unit_does_not_remove_other_unit_and_data(self):
                                    'relation_id': rel_id}})
 
     def test_relation_events(self):
-        harness = Harness(RelationEventCharm, meta='''
+        harness = ops.testing.Harness(RelationEventCharm, meta='''
             name: test-app
             requires:
                 db:
@@ -721,7 +703,7 @@ def test_get_relation_data(self):
                 db:
                     interface: pgsql
         '''
-        harness = Harness(CharmBase, meta=charm_meta)
+        harness = ops.testing.Harness(ops.CharmBase, meta=charm_meta)
         self.addCleanup(harness.cleanup)
         rel_id = harness.add_relation('db', 'postgresql')
         harness.update_relation_data(rel_id, 'postgresql', {'remote': 'data'})
@@ -734,13 +716,13 @@ def test_get_relation_data(self):
             harness.get_relation_data(99, 'postgresql')
 
         meta = yaml.safe_load(charm_meta)
-        t_app = Application('test-app', meta, harness._backend, None)
-        t_unit0 = Unit('test-app/0', meta, harness._backend, {Application: t_app})
-        t_unit1 = Unit('test-app/1', meta, harness._backend, {Application: t_app})
+        t_app = ops.Application('test-app', meta, harness._backend, None)
+        t_unit0 = ops.Unit('test-app/0', meta, harness._backend, {ops.Application: t_app})
+        t_unit1 = ops.Unit('test-app/1', meta, harness._backend, {ops.Application: t_app})
         self.assertEqual(harness.get_relation_data(rel_id, t_app), {})
         self.assertEqual(harness.get_relation_data(rel_id, t_unit0), {})
         self.assertEqual(harness.get_relation_data(rel_id, t_unit1), None)
-        pg_app = Application('postgresql', meta, harness._backend, None)
+        pg_app = ops.Application('postgresql', meta, harness._backend, None)
         self.assertEqual(harness.get_relation_data(rel_id, pg_app), {'remote': 'data'})
 
     def test_create_harness_twice(self):
@@ -750,9 +732,9 @@ def test_create_harness_twice(self):
               db:
                 interface: pgsql
             '''
-        harness1 = Harness(CharmBase, meta=metadata)
+        harness1 = ops.testing.Harness(ops.CharmBase, meta=metadata)
         self.addCleanup(harness1.cleanup)
-        harness2 = Harness(CharmBase, meta=metadata)
+        harness2 = ops.testing.Harness(ops.CharmBase, meta=metadata)
         self.addCleanup(harness2.cleanup)
         harness1.begin()
         harness2.begin()
@@ -766,7 +748,7 @@ def test_create_harness_twice(self):
 
     def test_begin_twice(self):
         # language=YAML
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             requires:
                 db:
@@ -778,7 +760,7 @@ def test_begin_twice(self):
             harness.begin()
 
     def test_update_relation_exposes_new_data(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: my-charm
             requires:
               db:
@@ -797,7 +779,7 @@ def test_update_relation_exposes_new_data(self):
 
     def test_update_relation_no_local_unit_change_event(self):
         # language=YAML
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: my-charm
             requires:
               db:
@@ -822,7 +804,7 @@ def test_update_relation_no_local_unit_change_event(self):
 
     def test_update_peer_relation_no_local_unit_change_event(self):
         # language=YAML
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: postgresql
             peers:
               db:
@@ -859,7 +841,7 @@ def test_update_peer_relation_no_local_unit_change_event(self):
 
     def test_harness_leader_misconfig(self):
         # language=YAML
-        harness = Harness(SetLeaderErrorTester, meta='''
+        harness = ops.testing.Harness(SetLeaderErrorTester, meta='''
             name: postgresql
             peers:
               peer:
@@ -874,7 +856,7 @@ def test_harness_leader_misconfig(self):
 
     def test_update_peer_relation_app_data(self):
         # language=YAML
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: postgresql
             peers:
               db:
@@ -905,7 +887,7 @@ def test_update_peer_relation_app_data(self):
 
     def test_update_relation_no_local_app_change_event(self):
         # language=YAML
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: my-charm
             requires:
               db:
@@ -930,7 +912,7 @@ def test_update_relation_no_local_app_change_event(self):
         self.assertEqual(helper.changes, [])
 
     def test_update_relation_remove_data(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: my-charm
             requires:
               db:
@@ -946,7 +928,7 @@ def test_update_relation_remove_data(self):
         self.assertEqual(viewer.changes, [{'initial': 'data'}, {}])
 
     def test_no_event_on_empty_update_relation_unit_app(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: my-charm
             requires:
               db:
@@ -962,7 +944,7 @@ def test_no_event_on_empty_update_relation_unit_app(self):
         self.assertEqual(viewer.changes, [{'initial': 'data'}])
 
     def test_no_event_on_no_diff_update_relation_unit_app(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: my-charm
             requires:
               db:
@@ -978,7 +960,7 @@ def test_no_event_on_no_diff_update_relation_unit_app(self):
         self.assertEqual(viewer.changes, [{'initial': 'data'}])
 
     def test_no_event_on_empty_update_relation_unit_bag(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: my-charm
             requires:
               db:
@@ -994,7 +976,7 @@ def test_no_event_on_empty_update_relation_unit_bag(self):
         self.assertEqual(viewer.changes, [{'initial': 'data'}])
 
     def test_no_event_on_no_diff_update_relation_unit_bag(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: my-charm
             requires:
               db:
@@ -1011,10 +993,10 @@ def test_no_event_on_no_diff_update_relation_unit_bag(self):
 
     def test_empty_config_raises(self):
         with self.assertRaises(TypeError):
-            Harness(RecordingCharm, config='')
+            ops.testing.Harness(RecordingCharm, config='')
 
     def test_update_config(self):
-        harness = Harness(RecordingCharm, config='''
+        harness = ops.testing.Harness(RecordingCharm, config='''
             options:
                 a:
                     description: a config option
@@ -1044,14 +1026,14 @@ def test_update_config(self):
              ])
 
     def test_update_config_undefined_option(self):
-        harness = Harness(RecordingCharm)
+        harness = ops.testing.Harness(RecordingCharm)
         self.addCleanup(harness.cleanup)
         harness.begin()
         with self.assertRaises(ValueError):
             harness.update_config(key_values={'nonexistent': 'foo'})
 
     def test_update_config_bad_type(self):
-        harness = Harness(RecordingCharm, config='''
+        harness = ops.testing.Harness(RecordingCharm, config='''
             options:
                 a:
                     description: a config option
@@ -1077,7 +1059,7 @@ def test_update_config_bad_type(self):
 
     def test_bad_config_option_type(self):
         with self.assertRaises(RuntimeError):
-            Harness(RecordingCharm, config='''
+            ops.testing.Harness(RecordingCharm, config='''
                 options:
                     a:
                         description: a config option
@@ -1087,7 +1069,7 @@ def test_bad_config_option_type(self):
 
     def test_no_config_option_type(self):
         with self.assertRaises(RuntimeError):
-            Harness(RecordingCharm, config='''
+            ops.testing.Harness(RecordingCharm, config='''
                 options:
                     a:
                         description: a config option
@@ -1096,7 +1078,7 @@ def test_no_config_option_type(self):
 
     def test_uncastable_config_option_type(self):
         with self.assertRaises(RuntimeError):
-            Harness(RecordingCharm, config='''
+            ops.testing.Harness(RecordingCharm, config='''
                 options:
                     a:
                         description: a config option
@@ -1105,7 +1087,7 @@ def test_uncastable_config_option_type(self):
                 ''')
 
     def test_update_config_unset_boolean(self):
-        harness = Harness(RecordingCharm, config='''
+        harness = ops.testing.Harness(RecordingCharm, config='''
             options:
                 a:
                     description: a config option
@@ -1127,7 +1109,7 @@ def test_update_config_unset_boolean(self):
              {'name': 'config-changed', 'data': {'a': False}}])
 
     def test_set_leader(self):
-        harness = Harness(RecordingCharm)
+        harness = ops.testing.Harness(RecordingCharm)
         self.addCleanup(harness.cleanup)
         # No event happens here
         harness.set_leader(False)
@@ -1148,7 +1130,7 @@ def test_set_leader(self):
         self.assertEqual(harness.charm.get_changes(reset=True), [])
 
     def test_relation_set_app_not_leader(self):
-        harness = Harness(RecordingCharm, meta='''
+        harness = ops.testing.Harness(RecordingCharm, meta='''
             name: test-charm
             requires:
                 db:
@@ -1161,7 +1143,7 @@ def test_relation_set_app_not_leader(self):
         harness.add_relation_unit(rel_id, 'postgresql/0')
         rel = harness.charm.model.get_relation('db')
         with harness._event_context('foo'):
-            with self.assertRaises(ModelError):
+            with self.assertRaises(ops.ModelError):
                 rel.data[harness.charm.app]['foo'] = 'bar'
         # The data has not actually been changed
         self.assertEqual(harness.get_relation_data(rel_id, 'test-charm'), {})
@@ -1170,7 +1152,7 @@ def test_relation_set_app_not_leader(self):
         self.assertEqual(harness.get_relation_data(rel_id, 'test-charm'), {'foo': 'bar'})
 
     def test_hooks_enabled_and_disabled(self):
-        harness = Harness(
+        harness = ops.testing.Harness(
             RecordingCharm,
             meta='''
                     name: test-charm
@@ -1202,7 +1184,7 @@ def test_hooks_enabled_and_disabled(self):
             [{'name': 'config-changed', 'data': {'value': 'fourth', 'third': '3'}}])
 
     def test_hooks_disabled_contextmanager(self):
-        harness = Harness(RecordingCharm, meta='''
+        harness = ops.testing.Harness(RecordingCharm, meta='''
                 name: test-charm
                 ''', config='''
                 options:
@@ -1230,7 +1212,7 @@ def test_hooks_disabled_contextmanager(self):
             [{'name': 'config-changed', 'data': {'value': 'fourth', 'third': '3'}}])
 
     def test_hooks_disabled_nested_contextmanager(self):
-        harness = Harness(RecordingCharm, meta='''
+        harness = ops.testing.Harness(RecordingCharm, meta='''
                 name: test-charm
             ''', config='''
                 options:
@@ -1249,7 +1231,7 @@ def test_hooks_disabled_nested_contextmanager(self):
         self.assertEqual(harness.charm.get_changes(reset=True), [])
 
     def test_hooks_disabled_noop(self):
-        harness = Harness(RecordingCharm, meta='''
+        harness = ops.testing.Harness(RecordingCharm, meta='''
                 name: test-charm
             ''', config='''
             options:
@@ -1325,7 +1307,7 @@ def test_config_from_directory(self):
         self.assertIsNone(harness._backend._config._defaults['opt_no_default'])
 
     def test_set_model_name(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
         ''')
         self.addCleanup(harness.cleanup)
@@ -1333,7 +1315,7 @@ def test_set_model_name(self):
         self.assertEqual('foo', harness.model.name)
 
     def test_set_model_name_after_begin(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
         ''')
         self.addCleanup(harness.cleanup)
@@ -1344,7 +1326,7 @@ def test_set_model_name_after_begin(self):
         self.assertEqual(harness.model.name, 'bar')
 
     def test_set_model_uuid_after_begin(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
         ''')
         self.addCleanup(harness.cleanup)
@@ -1356,7 +1338,7 @@ def test_set_model_uuid_after_begin(self):
         self.assertEqual(harness.model.uuid, '96957e90-e006-11eb-ba80-0242ac130004')
 
     def test_set_model_info_after_begin(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
         ''')
         self.addCleanup(harness.cleanup)
@@ -1376,7 +1358,7 @@ def test_set_model_info_after_begin(self):
         self.assertEqual(harness.model.uuid, '96957e90-e006-11eb-ba80-0242ac130004')
 
     def test_add_storage_before_harness_begin(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1394,11 +1376,11 @@ def test_add_storage_before_harness_begin(self):
             # before begin, adding storage does not attach it.
             self.assertNotIn(s, harness._backend.storage_list("test"))
 
-        with self.assertRaises(ops.model.ModelError):
+        with self.assertRaises(ops.ModelError):
             harness._backend.storage_get("test/0", "location")[-6:]
 
     def test_add_storage_then_harness_begin(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1413,19 +1395,19 @@ def test_add_storage_then_harness_begin(self):
 
         harness.add_storage("test", count=3)
 
-        with self.assertRaises(ops.model.ModelError):
+        with self.assertRaises(ops.ModelError):
             harness._backend.storage_get("test/0", "location")[-6:]
 
         harness.begin_with_initial_hooks()
         self.assertEqual(len(harness.charm.observed_events), 3)
         for i in range(3):
-            self.assertTrue(isinstance(harness.charm.observed_events[i], StorageAttachedEvent))
+            self.assertTrue(isinstance(harness.charm.observed_events[i], ops.StorageAttachedEvent))
 
         want = str(pathlib.PurePath('test', '0'))
         self.assertEqual(want, harness._backend.storage_get("test/0", "location")[-6:])
 
     def test_add_storage_not_attached_default(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             requires:
                 db:
@@ -1442,7 +1424,7 @@ def test_add_storage_not_attached_default(self):
             'storage should start in detached state and be excluded from storage listing'
 
     def test_add_storage_without_metadata_key_fails(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             requires:
                 db:
@@ -1457,7 +1439,7 @@ def test_add_storage_without_metadata_key_fails(self):
             "the key 'test' is not specified as a storage key in metadata")
 
     def test_add_storage_after_harness_begin(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1474,7 +1456,7 @@ def test_add_storage_after_harness_begin(self):
         harness.add_storage("test")[0]
         harness.begin_with_initial_hooks()
         self.assertEqual(len(harness.charm.observed_events), 1)
-        self.assertTrue(isinstance(harness.charm.observed_events[0], StorageAttachedEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[0], ops.StorageAttachedEvent))
 
         # Add additional storage
         stor_ids = harness.add_storage("test", count=3, attach=True)
@@ -1491,10 +1473,10 @@ def test_add_storage_after_harness_begin(self):
             self.assertTrue(harness._backend.storage_get(storage_name, "location").endswith(want))
         self.assertEqual(len(harness.charm.observed_events), 4)
         for i in range(1, 4):
-            self.assertTrue(isinstance(harness.charm.observed_events[i], StorageAttachedEvent))
+            self.assertTrue(isinstance(harness.charm.observed_events[i], ops.StorageAttachedEvent))
 
     def test_detach_storage(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1509,17 +1491,17 @@ def test_detach_storage(self):
         stor_id = harness.add_storage("test")[0]
         harness.begin_with_initial_hooks()
         self.assertEqual(len(harness.charm.observed_events), 1)
-        self.assertTrue(isinstance(harness.charm.observed_events[0], StorageAttachedEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[0], ops.StorageAttachedEvent))
 
         # Detach storage
         harness.detach_storage(stor_id)
         self.assertEqual(len(harness.charm.observed_events), 2)
-        self.assertTrue(isinstance(harness.charm.observed_events[1], StorageDetachingEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[1], ops.StorageDetachingEvent))
 
         # Verify backend functions return appropriate values.
         # Real backend would return info only for actively attached storage units.
         self.assertNotIn(stor_id, harness._backend.storage_list("test"))
-        with self.assertRaises(ModelError) as cm:
+        with self.assertRaises(ops.ModelError) as cm:
             harness._backend.storage_get("test/0", "location")
         # Error message modeled after output of
         # "storage-get -s <invalid/inactive id> location" on real deployment
@@ -1531,10 +1513,10 @@ def test_detach_storage(self):
         # Since already detached, no more hooks should fire
         harness.detach_storage(stor_id)
         self.assertEqual(len(harness.charm.observed_events), 2)
-        self.assertTrue(isinstance(harness.charm.observed_events[1], StorageDetachingEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[1], ops.StorageDetachingEvent))
 
     def test_detach_storage_before_harness_begin(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1552,7 +1534,7 @@ def test_detach_storage_before_harness_begin(self):
                          "cannot detach storage before Harness is initialised")
 
     def test_storage_with_hyphens_works(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1573,7 +1555,7 @@ def test_storage_with_hyphens_works(self):
         self.assertEqual(len(helper.changes), 1)
 
     def test_attach_storage(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1588,17 +1570,17 @@ def test_attach_storage(self):
         stor_id = harness.add_storage("test")[0]
         harness.begin_with_initial_hooks()
         self.assertEqual(len(harness.charm.observed_events), 1)
-        self.assertTrue(isinstance(harness.charm.observed_events[0], StorageAttachedEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[0], ops.StorageAttachedEvent))
 
         # Detach storage
         harness.detach_storage(stor_id)
         self.assertEqual(len(harness.charm.observed_events), 2)
-        self.assertTrue(isinstance(harness.charm.observed_events[1], StorageDetachingEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[1], ops.StorageDetachingEvent))
 
         # Re-attach storage
         harness.attach_storage(stor_id)
         self.assertEqual(len(harness.charm.observed_events), 3)
-        self.assertTrue(isinstance(harness.charm.observed_events[2], StorageAttachedEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[2], ops.StorageAttachedEvent))
 
         # Verify backend functions return appropriate values.
         # Real backend would return info only for actively attached storage units.
@@ -1610,10 +1592,10 @@ def test_attach_storage(self):
         # Since already detached, no more hooks should fire
         harness.attach_storage(stor_id)
         self.assertEqual(len(harness.charm.observed_events), 3)
-        self.assertTrue(isinstance(harness.charm.observed_events[2], StorageAttachedEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[2], ops.StorageAttachedEvent))
 
     def test_attach_storage_before_harness_begin(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1630,7 +1612,7 @@ def test_attach_storage_before_harness_begin(self):
         self.assertTrue(stor_id)
 
     def test_remove_storage_before_harness_begin(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1655,10 +1637,10 @@ def test_remove_storage_before_harness_begin(self):
         harness.begin_with_initial_hooks()
         # Only one hook will fire; one won't since it was removed
         self.assertEqual(len(harness.charm.observed_events), 1)
-        self.assertTrue(isinstance(harness.charm.observed_events[0], StorageAttachedEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[0], ops.StorageAttachedEvent))
 
     def test_remove_storage_without_metadata_key_fails(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             requires:
                 db:
@@ -1675,7 +1657,7 @@ def test_remove_storage_without_metadata_key_fails(self):
             "the key 'test' is not specified as a storage key in metadata")
 
     def test_remove_storage_after_harness_begin(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1691,12 +1673,12 @@ def test_remove_storage_after_harness_begin(self):
         stor_ids = harness.add_storage("test", count=2)
         harness.begin_with_initial_hooks()
         self.assertEqual(len(harness.charm.observed_events), 2)
-        self.assertTrue(isinstance(harness.charm.observed_events[0], StorageAttachedEvent))
-        self.assertTrue(isinstance(harness.charm.observed_events[1], StorageAttachedEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[0], ops.StorageAttachedEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[1], ops.StorageAttachedEvent))
 
         harness.remove_storage(stor_ids[1])
         self.assertEqual(len(harness.charm.observed_events), 3)
-        self.assertTrue(isinstance(harness.charm.observed_events[2], StorageDetachingEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[2], ops.StorageDetachingEvent))
 
         attached_storage_ids = harness._backend.storage_list("test")
         self.assertIn(self._extract_storage_index(stor_ids[0]), attached_storage_ids)
@@ -1706,7 +1688,7 @@ def _extract_storage_index(self, stor_id):
         return int(stor_id.split('/')[-1])
 
     def test_remove_detached_storage(self):
-        harness = Harness(StorageTester, meta='''
+        harness = ops.testing.Harness(StorageTester, meta='''
             name: test-app
             requires:
                 db:
@@ -1724,9 +1706,9 @@ def test_remove_detached_storage(self):
         harness.detach_storage(stor_ids[0])
         harness.remove_storage(stor_ids[0])  # Already detached, so won't fire a hook
         self.assertEqual(len(harness.charm.observed_events), 3)
-        self.assertTrue(isinstance(harness.charm.observed_events[0], StorageAttachedEvent))
-        self.assertTrue(isinstance(harness.charm.observed_events[1], StorageAttachedEvent))
-        self.assertTrue(isinstance(harness.charm.observed_events[2], StorageDetachingEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[0], ops.StorageAttachedEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[1], ops.StorageAttachedEvent))
+        self.assertTrue(isinstance(harness.charm.observed_events[2], ops.StorageDetachingEvent))
 
     def test_actions_from_directory(self):
         tmp = pathlib.Path(tempfile.mkdtemp())
@@ -1746,7 +1728,7 @@ def test_actions_from_directory(self):
     def _get_dummy_charm_harness(self, tmp):
         self._write_dummy_charm(tmp)
         charm_mod = importlib.import_module('testcharm')
-        harness = Harness(charm_mod.MyTestingCharm)
+        harness = ops.testing.Harness(charm_mod.MyTestingCharm)
         self.addCleanup(harness.cleanup)
         return harness
 
@@ -1757,7 +1739,7 @@ def _write_dummy_charm(self, tmp):
         with charm_filename.open('wt') as charmpy:
             # language=Python
             charmpy.write(textwrap.dedent('''
-                from ops.charm import CharmBase
+                from ops import CharmBase
                 class MyTestingCharm(CharmBase):
                     pass
                 '''))
@@ -1771,8 +1753,8 @@ def cleanup():
         self.addCleanup(cleanup)
 
     def test_actions_passed_in(self):
-        harness = Harness(
-            CharmBase,
+        harness = ops.testing.Harness(
+            ops.CharmBase,
             meta='''
                 name: test-app
             ''',
@@ -1784,11 +1766,11 @@ def test_actions_passed_in(self):
         self.assertEqual(list(harness.framework.meta.actions), ['test-action'])
 
     def test_event_context(self):
-        class MyCharm(CharmBase):
+        class MyCharm(ops.CharmBase):
             def event_handler(self, evt):
                 evt.relation.data[evt.relation.app]['foo'] = 'bar'
 
-        harness = Harness(MyCharm, meta='''
+        harness = ops.testing.Harness(MyCharm, meta='''
             name: test-charm
             requires:
                 db:
@@ -1802,12 +1784,12 @@ def event_handler(self, evt):
         event.relation = rel
 
         with harness._event_context('my_relation_joined'):
-            with self.assertRaises(ops.model.RelationDataError):
+            with self.assertRaises(ops.RelationDataError):
                 harness.charm.event_handler(event)
 
     def test_event_context_inverse(self):
-        class MyCharm(CharmBase):
-            def __init__(self, framework: Framework):
+        class MyCharm(ops.CharmBase):
+            def __init__(self, framework: ops.Framework):
                 super().__init__(framework)
                 self.framework.observe(self.on.db_relation_joined,
                                        self._join_db)
@@ -1816,7 +1798,7 @@ def _join_db(self, event):
                 # do things with APIs we cannot easily mock
                 raise NotImplementedError
 
-        harness = Harness(MyCharm, meta='''
+        harness = ops.testing.Harness(MyCharm, meta='''
             name: test-charm
             requires:
                 db:
@@ -1845,7 +1827,7 @@ def mock_join_db(event):
         assert rel.data[harness.charm.app]['foo'] == 'bar'
 
     def test_relation_set_deletes(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
             requires:
                 db:
@@ -1862,7 +1844,7 @@ def test_relation_set_deletes(self):
         self.assertEqual({}, harness.get_relation_data(rel_id, 'test-charm/0'))
 
     def test_relation_set_nonstring(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
             requires:
                 db:
@@ -1873,12 +1855,12 @@ def test_relation_set_nonstring(self):
         harness.set_leader(False)
         rel_id = harness.add_relation('db', 'postgresql')
         for invalid_value in (1, 1.2, {}, [], set(), True, object(), type):
-            with self.assertRaises(model.RelationDataError):
+            with self.assertRaises(ops.RelationDataError):
                 harness.update_relation_data(rel_id, 'test-charm/0',
                                              {'foo': invalid_value})
 
     def test_set_workload_version(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: app
             ''')
         self.addCleanup(harness.cleanup)
@@ -1888,7 +1870,7 @@ def test_set_workload_version(self):
         self.assertEqual(harness.get_workload_version(), '1.2.3')
 
     def test_get_backend_calls(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
             requires:
                 db:
@@ -1943,7 +1925,7 @@ def test_get_backend_calls(self):
         self.assertEqual(harness._get_backend_calls(), [])
 
     def test_get_backend_calls_with_kwargs(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
             requires:
                 db:
@@ -1954,42 +1936,42 @@ def test_get_backend_calls_with_kwargs(self):
         unit = harness.charm.model.unit
         # Reset the list, because we don't care what it took to get here
         harness._get_backend_calls(reset=True)
-        unit.status = ActiveStatus()
+        unit.status = ops.ActiveStatus()
         self.assertEqual(
             [('status_set', 'active', '', {'is_app': False})], harness._get_backend_calls())
         harness.set_leader(True)
         app = harness.charm.model.app
         harness._get_backend_calls(reset=True)
-        app.status = ActiveStatus('message')
+        app.status = ops.ActiveStatus('message')
         self.assertEqual(
             [('is_leader',),
              ('status_set', 'active', 'message', {'is_app': True})],
             harness._get_backend_calls())
 
     def test_unit_status(self):
-        harness = Harness(CharmBase, meta='name: test-app')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: test-app')
         self.addCleanup(harness.cleanup)
         harness.set_leader(True)
         harness.begin()
         # default status
-        self.assertEqual(harness.model.unit.status, MaintenanceStatus(''))
-        status = ActiveStatus('message')
+        self.assertEqual(harness.model.unit.status, ops.MaintenanceStatus(''))
+        status = ops.ActiveStatus('message')
         harness.model.unit.status = status
         self.assertEqual(harness.model.unit.status, status)
 
     def test_app_status(self):
-        harness = Harness(CharmBase, meta='name: test-app')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: test-app')
         self.addCleanup(harness.cleanup)
         harness.set_leader(True)
         harness.begin()
         # default status
-        self.assertEqual(harness.model.app.status, UnknownStatus())
-        status = ActiveStatus('message')
+        self.assertEqual(harness.model.app.status, ops.UnknownStatus())
+        status = ops.ActiveStatus('message')
         harness.model.app.status = status
         self.assertEqual(harness.model.app.status, status)
 
     def test_populate_oci_resources(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -2014,7 +1996,7 @@ def test_populate_oci_resources(self):
         self.assertEqual(path.parent.name, 'image2')
 
     def test_resource_folder_cleanup(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -2031,7 +2013,7 @@ def test_resource_folder_cleanup(self):
         self.assertFalse(path.parent.parent.exists())
 
     def test_container_isdir_and_exists(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             containers:
               foo:
@@ -2059,7 +2041,7 @@ def test_container_isdir_and_exists(self):
         self.assertTrue(c.exists(file_path))
 
     def test_add_oci_resource_custom(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -2081,7 +2063,7 @@ def test_add_oci_resource_custom(self):
         self.assertEqual(contents['password'], 'custom_password')
 
     def test_add_oci_resource_no_image(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -2096,7 +2078,7 @@ def test_add_oci_resource_no_image(self):
         self.assertEqual(len(harness._backend._resources_map), 0)
 
     def test_add_resource_unknown(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -2108,7 +2090,7 @@ def test_add_resource_unknown(self):
             harness.add_resource('unknown', 'content')
 
     def test_add_resource_but_oci(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -2120,7 +2102,7 @@ def test_add_resource_but_oci(self):
             harness.add_resource('image', 'content')
 
     def test_add_resource_string(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -2137,7 +2119,7 @@ def test_add_resource_string(self):
             self.assertEqual('foo contents\n', f.read())
 
     def test_add_resource_bytes(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -2155,7 +2137,7 @@ def test_add_resource_bytes(self):
             self.assertEqual(raw_contents, f.read())
 
     def test_add_resource_unknown_filename(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -2169,7 +2151,7 @@ def test_add_resource_unknown_filename(self):
         self.assertEqual(path.parent.name, 'image')
 
     def test_get_pod_spec(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             ''')
         self.addCleanup(harness.cleanup)
@@ -2180,7 +2162,7 @@ def test_get_pod_spec(self):
         self.assertEqual(harness.get_pod_spec(), (container_spec, k8s_resources))
 
     def test_begin_with_initial_hooks_no_relations(self):
-        harness = Harness(RecordingCharm, meta='''
+        harness = ops.testing.Harness(RecordingCharm, meta='''
             name: test-app
             ''', config='''
             options:
@@ -2206,7 +2188,7 @@ def test_begin_with_initial_hooks_no_relations(self):
         )
 
     def test_begin_with_initial_hooks_no_relations_not_leader(self):
-        harness = Harness(RecordingCharm, meta='''
+        harness = ops.testing.Harness(RecordingCharm, meta='''
             name: test-app
             ''', config='''
             options:
@@ -2235,7 +2217,7 @@ class PeerCharm(RelationEventCharm):
             def __init__(self, framework):
                 super().__init__(framework)
                 self.observe_relation_events('peer')
-        harness = Harness(PeerCharm, meta='''
+        harness = ops.testing.Harness(PeerCharm, meta='''
             name: test-app
             peers:
               peer:
@@ -2275,7 +2257,7 @@ class PeerCharm(RelationEventCharm):
             def __init__(self, framework):
                 super().__init__(framework)
                 self.observe_relation_events('peer')
-        harness = Harness(PeerCharm, meta='''
+        harness = ops.testing.Harness(PeerCharm, meta='''
             name: test-app
             peers:
               peer:
@@ -2307,7 +2289,7 @@ class CharmWithDB(RelationEventCharm):
             def __init__(self, framework):
                 super().__init__(framework)
                 self.observe_relation_events('db')
-        harness = Harness(CharmWithDB, meta='''
+        harness = ops.testing.Harness(CharmWithDB, meta='''
             name: test-app
             requires:
               db:
@@ -2330,7 +2312,7 @@ class CharmWithDB(RelationEventCharm):
             def __init__(self, framework):
                 super().__init__(framework)
                 self.observe_relation_events('db')
-        harness = Harness(CharmWithDB, meta='''
+        harness = ops.testing.Harness(CharmWithDB, meta='''
             name: test-app
             requires:
               db:
@@ -2377,7 +2359,7 @@ class CharmWithDB(RelationEventCharm):
             def __init__(self, framework):
                 super().__init__(framework)
                 self.observe_relation_events('db')
-        harness = Harness(CharmWithDB, meta='''
+        harness = ops.testing.Harness(CharmWithDB, meta='''
             name: test-app
             requires:
               db:
@@ -2432,7 +2414,7 @@ class CharmWithDB(RelationEventCharm):
             def __init__(self, framework):
                 super().__init__(framework)
                 self.observe_relation_events('db')
-        harness = Harness(CharmWithDB, meta='''
+        harness = ops.testing.Harness(CharmWithDB, meta='''
             name: test-app
             requires:
               db:
@@ -2495,7 +2477,7 @@ class CharmWithDB(RelationEventCharm):
             def __init__(self, framework):
                 super().__init__(framework)
                 self.observe_relation_events('db')
-        harness = Harness(CharmWithDB, meta='''
+        harness = ops.testing.Harness(CharmWithDB, meta='''
             name: test-app
             requires:
               db:
@@ -2582,7 +2564,7 @@ def __init__(self, framework):
     def test_begin_with_initial_hooks_unknown_status(self):
         # Verify that a charm that does not set a status in the install hook will have an
         # unknown status in the harness.
-        harness = Harness(RecordingCharm, meta='''
+        harness = ops.testing.Harness(RecordingCharm, meta='''
             name: test-app
             ''', config='''
           options:
@@ -2603,7 +2585,7 @@ def test_begin_with_initial_hooks_unknown_status(self):
             {'status': 'unknown', 'message': ''})
 
     def test_begin_with_initial_hooks_install_sets_status(self):
-        harness = Harness(RecordingCharm, meta='''
+        harness = ops.testing.Harness(RecordingCharm, meta='''
             name: test-app
             ''', config='''
             options:
@@ -2622,7 +2604,7 @@ def test_begin_with_initial_hooks_install_sets_status(self):
             {'status': 'maintenance', 'message': 'Status set on install'})
 
     def test_get_pebble_container_plan(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             containers:
               foo:
@@ -2663,7 +2645,7 @@ def test_get_pebble_container_plan(self):
         self.assertEqual(harness_plan.to_yaml(), plan.to_yaml())
 
     def test_get_pebble_container_plan_unknown(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             containers:
               foo:
@@ -2678,7 +2660,7 @@ def test_get_pebble_container_plan_unknown(self):
         self.assertEqual(plan.to_yaml(), "{}\n")
 
     def test_container_pebble_ready(self):
-        harness = Harness(ContainerEventCharm, meta='''
+        harness = ops.testing.Harness(ContainerEventCharm, meta='''
             name: test-app
             containers:
               foo:
@@ -2702,7 +2684,7 @@ def test_container_pebble_ready(self):
 
 class TestNetwork(unittest.TestCase):
     def setUp(self):
-        self.harness = Harness(CharmBase, meta='''
+        self.harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
             requires:
                db:
@@ -2819,11 +2801,11 @@ def test_add_network_ipv6(self):
         self.assertEqual(interface.subnet, ipaddress.IPv6Network('2001:0db8::0:0:0:0/64'))
 
     def test_network_get_relation_not_found(self):
-        with self.assertRaises(model.RelationNotFoundError):
+        with self.assertRaises(ops.RelationNotFoundError):
             self.harness.model.get_binding('db').network
 
     def test_add_network_endpoint_not_in_meta(self):
-        with self.assertRaises(model.ModelError):
+        with self.assertRaises(ops.ModelError):
             self.harness.add_network('35.0.0.1', endpoint='xyz')
 
     def test_add_network_relation_id_set_endpoint_not_set(self):
@@ -2833,16 +2815,16 @@ def test_add_network_relation_id_set_endpoint_not_set(self):
 
     def test_add_network_relation_id_incorrect(self):
         relation_id = self.harness.add_relation('db', 'postgresql')
-        with self.assertRaises(model.ModelError):
+        with self.assertRaises(ops.ModelError):
             self.harness.add_network('35.0.0.1', endpoint='db', relation_id=relation_id + 1)
 
     def test_add_network_endpoint_and_relation_id_do_not_correspond(self):
         relation_id = self.harness.add_relation('db', 'postgresql')
-        with self.assertRaises(model.ModelError):
+        with self.assertRaises(ops.ModelError):
             self.harness.add_network('35.0.0.1', endpoint='foo', relation_id=relation_id)
 
 
-class DBRelationChangedHelper(Object):
+class DBRelationChangedHelper(ops.Object):
     def __init__(self, parent, key):
         super().__init__(parent, key)
         self.changes = []
@@ -2855,7 +2837,7 @@ def on_relation_changed(self, event):
             self.changes.append((event.relation.id, event.app.name))
 
 
-class RelationChangedViewer(Object):
+class RelationChangedViewer(ops.Object):
     """Track relation_changed events and saves the data seen in the relation bucket."""
 
     def __init__(self, charm, relation_name):
@@ -2871,7 +2853,7 @@ def on_relation_changed(self, event):
         self.changes.append(dict(data))
 
 
-class RecordingCharm(CharmBase):
+class RecordingCharm(ops.CharmBase):
     """Record the events that we see, and any associated data."""
 
     def __init__(self, framework):
@@ -2895,7 +2877,7 @@ def get_changes(self, reset=True):
 
     def _on_install(self, _):
         if self.config.get('set_status'):
-            self.unit.status = MaintenanceStatus("Status set on install")
+            self.unit.status = ops.MaintenanceStatus("Status set on install")
         self.changes.append(dict(name='install'))
 
     def _on_start(self, _):
@@ -2966,7 +2948,7 @@ def _observe_relation_event(self, event_name, event):
             app_name = event.app.name
 
         data = dict(app=app_name, unit=unit_name, relation_id=event.relation.id)
-        if isinstance(event, RelationDepartedEvent):
+        if isinstance(event, ops.RelationDepartedEvent):
             data['departing_unit'] = event.departing_unit.name
 
         recording = dict(name=event_name, relation=event.relation.name, data=data)
@@ -3002,7 +2984,7 @@ def observe_container_events(self, container_name):
     def _on_pebble_ready(self, event):
         self._observe_container_event('pebble-ready', event)
 
-    def _observe_container_event(self, event_name, event: PebbleReadyEvent):
+    def _observe_container_event(self, event_name, event: ops.PebbleReadyEvent):
         container_name = None
         if event.workload is not None:
             container_name = event.workload.name
@@ -3025,7 +3007,7 @@ def get_public_methods(obj):
 class TestTestingModelBackend(unittest.TestCase):
 
     def test_conforms_to_model_backend(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: app
             ''')
         self.addCleanup(harness.cleanup)
@@ -3035,7 +3017,7 @@ def test_conforms_to_model_backend(self):
         self.assertEqual(mb_methods, backend_methods)
 
     def test_model_uuid_is_uuid_v4(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
         ''')
         self.addCleanup(harness.cleanup)
@@ -3043,7 +3025,7 @@ def test_model_uuid_is_uuid_v4(self):
         self.assertEqual(uuid.UUID(backend.model_uuid).version, 4)
 
     def test_status_set_get_unit(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: app
             ''')
         self.addCleanup(harness.cleanup)
@@ -3057,7 +3039,7 @@ def test_status_set_get_unit(self):
             {'status': 'unknown', 'message': ''})
 
     def test_status_set_get_app(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: app
             ''')
         self.addCleanup(harness.cleanup)
@@ -3071,7 +3053,7 @@ def test_status_set_get_app(self):
             {'status': 'maintenance', 'message': ''})
 
     def test_relation_ids_unknown_relation(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
             provides:
               db:
@@ -3082,29 +3064,29 @@ def test_relation_ids_unknown_relation(self):
         # With no relations added, we just get an empty list for the interface
         self.assertEqual(backend.relation_ids('db'), [])
         # But an unknown interface raises a ModelError
-        with self.assertRaises(ModelError):
+        with self.assertRaises(ops.ModelError):
             backend.relation_ids('unknown')
 
     def test_relation_get_unknown_relation_id(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
             ''')
         self.addCleanup(harness.cleanup)
         backend = harness._backend
-        with self.assertRaises(RelationNotFoundError):
+        with self.assertRaises(ops.RelationNotFoundError):
             backend.relation_get(1234, 'unit/0', False)
 
     def test_relation_list_unknown_relation_id(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
             ''')
         self.addCleanup(harness.cleanup)
         backend = harness._backend
-        with self.assertRaises(RelationNotFoundError):
+        with self.assertRaises(ops.RelationNotFoundError):
             backend.relation_list(1234)
 
     def test_lazy_resource_directory(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -3122,7 +3104,7 @@ def test_lazy_resource_directory(self):
             msg=f'expected {path} to be a subdirectory of {backend._resource_dir.name}')
 
     def test_resource_get_no_resource(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             resources:
               image:
@@ -3131,14 +3113,14 @@ def test_resource_get_no_resource(self):
             ''')
         self.addCleanup(harness.cleanup)
         backend = harness._backend
-        with self.assertRaises(ModelError) as cm:
+        with self.assertRaises(ops.ModelError) as cm:
             backend.resource_get('foo')
         self.assertIn(
             "units/unit-test-app-0/resources/foo: resource#test-app/foo not found",
             str(cm.exception))
 
     def test_relation_remote_app_name(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-charm
             requires:
                db:
@@ -3158,7 +3140,7 @@ def test_relation_remote_app_name(self):
         self.assertIs(backend.relation_remote_app_name(7), None)
 
     def test_get_pebble_methods(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             ''')
         self.addCleanup(harness.cleanup)
@@ -3170,7 +3152,7 @@ def test_get_pebble_methods(self):
 
 class _TestingPebbleClientMixin:
     def get_testing_client(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             containers:
               mycontainer: {}
@@ -4370,7 +4352,7 @@ def setUp(self):
 
     @unittest.skipUnless(is_linux, 'Pebble runs on Linux')
     def test_container_storage_mounts(self):
-        harness = Harness(CharmBase, meta='''
+        harness = ops.testing.Harness(ops.CharmBase, meta='''
             name: test-app
             containers:
                 c1:
@@ -4483,7 +4465,7 @@ def test_make_dir_with_permission_mask(self):
 
 class TestSecrets(unittest.TestCase):
     def test_add_model_secret_by_app_name_str(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
         relation_id = harness.add_relation('db', 'database')
         harness.add_relation_unit(relation_id, 'database/0')
@@ -4495,7 +4477,7 @@ def test_add_model_secret_by_app_name_str(self):
         self.assertEqual(secret.get_content(), {'password': 'hunter2'})
 
     def test_add_model_secret_by_app_instance(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
         relation_id = harness.add_relation('db', 'database')
         harness.add_relation_unit(relation_id, 'database/0')
@@ -4508,7 +4490,7 @@ def test_add_model_secret_by_app_instance(self):
         self.assertEqual(secret.get_content(), {'password': 'hunter3'})
 
     def test_add_model_secret_by_unit_instance(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
         relation_id = harness.add_relation('db', 'database')
         harness.add_relation_unit(relation_id, 'database/0')
@@ -4521,14 +4503,14 @@ def test_add_model_secret_by_unit_instance(self):
         self.assertEqual(secret.get_content(), {'password': 'hunter4'})
 
     def test_add_model_secret_invalid_content(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
 
         with self.assertRaises(ValueError):
             harness.add_model_secret('database', {'x': 'y'})  # key too short
 
     def test_set_secret_content(self):
-        harness = Harness(EventRecorder, meta='name: webapp')
+        harness = ops.testing.Harness(EventRecorder, meta='name: webapp')
         self.addCleanup(harness.cleanup)
         relation_id = harness.add_relation('db', 'database')
         harness.add_relation_unit(relation_id, 'database/0')
@@ -4541,7 +4523,7 @@ def test_set_secret_content(self):
 
         self.assertEqual(len(harness.charm.events), 1)
         event = harness.charm.events[0]
-        self.assertIsInstance(event, ops.charm.SecretChangedEvent)
+        self.assertIsInstance(event, ops.SecretChangedEvent)
         self.assertEqual(event.secret.get_content(), {'foo': '1'})
         self.assertEqual(event.secret.get_content(refresh=True), {'foo': '2'})
         self.assertEqual(event.secret.get_content(), {'foo': '2'})
@@ -4549,7 +4531,7 @@ def test_set_secret_content(self):
         self.assertEqual(harness.get_secret_revisions(secret_id), [1, 2])
 
     def test_set_secret_content_wrong_owner(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
 
         secret = harness.model.app.add_secret({'foo': 'bar'})
@@ -4557,14 +4539,14 @@ def test_set_secret_content_wrong_owner(self):
             harness.set_secret_content(secret.id, {'bar': 'foo'})
 
     def test_set_secret_content_invalid_secret_id(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
 
         with self.assertRaises(RuntimeError):
             harness.set_secret_content('asdf', {'foo': 'bar'})
 
     def test_set_secret_content_invalid_content(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
 
         secret_id = harness.add_model_secret('database', {'foo': 'bar'})
@@ -4572,7 +4554,7 @@ def test_set_secret_content_invalid_content(self):
             harness.set_secret_content(secret_id, {'x': 'y'})
 
     def test_grant_secret_and_revoke_secret(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
         relation_id = harness.add_relation('db', 'database')
         harness.add_relation_unit(relation_id, 'database/0')
@@ -4584,33 +4566,33 @@ def test_grant_secret_and_revoke_secret(self):
         self.assertEqual(secret.get_content(), {'password': 'hunter2'})
 
         harness.revoke_secret(secret_id, 'webapp')
-        with self.assertRaises(model.SecretNotFoundError):
+        with self.assertRaises(ops.SecretNotFoundError):
             harness.model.get_secret(id=secret_id)
 
     def test_grant_secret_wrong_app(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
         relation_id = harness.add_relation('db', 'database')
         harness.add_relation_unit(relation_id, 'database/0')
 
         secret_id = harness.add_model_secret('database', {'password': 'hunter2'})
         harness.grant_secret(secret_id, 'otherapp')
-        with self.assertRaises(model.SecretNotFoundError):
+        with self.assertRaises(ops.SecretNotFoundError):
             harness.model.get_secret(id=secret_id)
 
     def test_grant_secret_wrong_unit(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
         relation_id = harness.add_relation('db', 'database')
         harness.add_relation_unit(relation_id, 'database/0')
 
         secret_id = harness.add_model_secret('database', {'password': 'hunter2'})
         harness.grant_secret(secret_id, 'webapp/1')  # should be webapp/0
-        with self.assertRaises(model.SecretNotFoundError):
+        with self.assertRaises(ops.SecretNotFoundError):
             harness.model.get_secret(id=secret_id)
 
     def test_grant_secret_no_relation(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
 
         secret_id = harness.add_model_secret('database', {'password': 'hunter2'})
@@ -4618,7 +4600,7 @@ def test_grant_secret_no_relation(self):
             harness.grant_secret(secret_id, 'webapp')
 
     def test_get_secret_grants(self):
-        harness = Harness(CharmBase, meta='name: database')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: database')
         self.addCleanup(harness.cleanup)
 
         relation_id = harness.add_relation('db', 'webapp')
@@ -4635,7 +4617,7 @@ def test_get_secret_grants(self):
         self.assertEqual(harness.get_secret_grants(secret.id, relation_id), {'webapp/0'})
 
     def test_trigger_secret_rotation(self):
-        harness = Harness(EventRecorder, meta='name: database')
+        harness = ops.testing.Harness(EventRecorder, meta='name: database')
         self.addCleanup(harness.cleanup)
 
         secret = harness.model.app.add_secret({'foo': 'x'}, label='lbl')
@@ -4646,11 +4628,11 @@ def test_trigger_secret_rotation(self):
 
         self.assertEqual(len(harness.charm.events), 2)
         event = harness.charm.events[0]
-        self.assertIsInstance(event, ops.charm.SecretRotateEvent)
+        self.assertIsInstance(event, ops.SecretRotateEvent)
         self.assertEqual(event.secret.label, 'lbl')
         self.assertEqual(event.secret.get_content(), {'foo': 'x'})
         event = harness.charm.events[1]
-        self.assertIsInstance(event, ops.charm.SecretRotateEvent)
+        self.assertIsInstance(event, ops.SecretRotateEvent)
         self.assertEqual(event.secret.label, 'override')
         self.assertEqual(event.secret.get_content(), {'foo': 'x'})
 
@@ -4658,7 +4640,7 @@ def test_trigger_secret_rotation(self):
             harness.trigger_secret_rotation('nosecret')
 
     def test_trigger_secret_removal(self):
-        harness = Harness(EventRecorder, meta='name: database')
+        harness = ops.testing.Harness(EventRecorder, meta='name: database')
         self.addCleanup(harness.cleanup)
 
         secret = harness.model.app.add_secret({'foo': 'x'}, label='lbl')
@@ -4669,12 +4651,12 @@ def test_trigger_secret_removal(self):
 
         self.assertEqual(len(harness.charm.events), 2)
         event = harness.charm.events[0]
-        self.assertIsInstance(event, ops.charm.SecretRemoveEvent)
+        self.assertIsInstance(event, ops.SecretRemoveEvent)
         self.assertEqual(event.secret.label, 'lbl')
         self.assertEqual(event.revision, 1)
         self.assertEqual(event.secret.get_content(), {'foo': 'x'})
         event = harness.charm.events[1]
-        self.assertIsInstance(event, ops.charm.SecretRemoveEvent)
+        self.assertIsInstance(event, ops.SecretRemoveEvent)
         self.assertEqual(event.secret.label, 'override')
         self.assertEqual(event.revision, 42)
         self.assertEqual(event.secret.get_content(), {'foo': 'x'})
@@ -4683,7 +4665,7 @@ def test_trigger_secret_removal(self):
             harness.trigger_secret_removal('nosecret', 1)
 
     def test_trigger_secret_expiration(self):
-        harness = Harness(EventRecorder, meta='name: database')
+        harness = ops.testing.Harness(EventRecorder, meta='name: database')
         self.addCleanup(harness.cleanup)
 
         secret = harness.model.app.add_secret({'foo': 'x'}, label='lbl')
@@ -4694,12 +4676,12 @@ def test_trigger_secret_expiration(self):
 
         self.assertEqual(len(harness.charm.events), 2)
         event = harness.charm.events[0]
-        self.assertIsInstance(event, ops.charm.SecretRemoveEvent)
+        self.assertIsInstance(event, ops.SecretRemoveEvent)
         self.assertEqual(event.secret.label, 'lbl')
         self.assertEqual(event.revision, 1)
         self.assertEqual(event.secret.get_content(), {'foo': 'x'})
         event = harness.charm.events[1]
-        self.assertIsInstance(event, ops.charm.SecretRemoveEvent)
+        self.assertIsInstance(event, ops.SecretRemoveEvent)
         self.assertEqual(event.secret.label, 'override')
         self.assertEqual(event.revision, 42)
         self.assertEqual(event.secret.get_content(), {'foo': 'x'})
@@ -4708,7 +4690,7 @@ def test_trigger_secret_expiration(self):
             harness.trigger_secret_removal('nosecret', 1)
 
 
-class EventRecorder(CharmBase):
+class EventRecorder(ops.CharmBase):
     def __init__(self, framework):
         super().__init__(framework)
         self.events = []
@@ -4719,7 +4701,7 @@ def record_event(self, event):
 
 class TestPorts(unittest.TestCase):
     def test_ports(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
         unit = harness.model.unit
 
@@ -4731,13 +4713,13 @@ def test_ports(self):
         self.assertIsInstance(ports_set, set)
         ports = sorted(ports_set, key=lambda p: (p.protocol, p.port))
         self.assertEqual(len(ports), 3)
-        self.assertIsInstance(ports[0], model.OpenedPort)
+        self.assertIsInstance(ports[0], ops.OpenedPort)
         self.assertEqual(ports[0].protocol, 'icmp')
         self.assertIsNone(ports[0].port)
-        self.assertIsInstance(ports[1], model.OpenedPort)
+        self.assertIsInstance(ports[1], ops.OpenedPort)
         self.assertEqual(ports[1].protocol, 'tcp')
         self.assertEqual(ports[1].port, 8080)
-        self.assertIsInstance(ports[2], model.OpenedPort)
+        self.assertIsInstance(ports[2], ops.OpenedPort)
         self.assertEqual(ports[2].protocol, 'udp')
         self.assertEqual(ports[2].port, 4000)
 
@@ -4749,7 +4731,7 @@ def test_ports(self):
         self.assertIsInstance(ports_set, set)
         ports = sorted(ports_set, key=lambda p: (p.protocol, p.port))
         self.assertEqual(len(ports), 1)
-        self.assertIsInstance(ports[0], model.OpenedPort)
+        self.assertIsInstance(ports[0], ops.OpenedPort)
         self.assertEqual(ports[0].protocol, 'icmp')
         self.assertIsNone(ports[0].port)
 
@@ -4759,19 +4741,19 @@ def test_ports(self):
         self.assertEqual(ports_set, set())
 
     def test_errors(self):
-        harness = Harness(CharmBase, meta='name: webapp')
+        harness = ops.testing.Harness(ops.CharmBase, meta='name: webapp')
         self.addCleanup(harness.cleanup)
         unit = harness.model.unit
 
-        with self.assertRaises(model.ModelError):
+        with self.assertRaises(ops.ModelError):
             unit.open_port('icmp', 8080)  # icmp cannot have port
-        with self.assertRaises(model.ModelError):
+        with self.assertRaises(ops.ModelError):
             unit.open_port('ftp', 8080)  # invalid protocol
-        with self.assertRaises(model.ModelError):
+        with self.assertRaises(ops.ModelError):
             unit.open_port('tcp')  # tcp must have port
-        with self.assertRaises(model.ModelError):
+        with self.assertRaises(ops.ModelError):
             unit.open_port('udp')  # udp must have port
-        with self.assertRaises(model.ModelError):
+        with self.assertRaises(ops.ModelError):
             unit.open_port('tcp', 0)  # port out of range
-        with self.assertRaises(model.ModelError):
+        with self.assertRaises(ops.ModelError):
             unit.open_port('tcp', 65536)  # port out of range