Skip to content

Commit

Permalink
chore!: adjust privacy (#176)
Browse files Browse the repository at this point in the history
A collection of small changes that generally fall into "remove x from
the public API":

* Remove the `with_can_connect`, `with_leadership`, and
`with_unit_status` convenience methods from `State`.
* Makes `next_relation_id`, `next_action_id`, `next_storage_index`, and
`next_notice_id` private.
* Removes `Context.output_state`.
* Makes all the *_SUFFIX constants private.
* Makes all the *_EVENTS constants private, except `META_EVENTS`, which
is removed.
* Makes `capture_events` private (and consolidates capture_events.py
into runtime.py).
* Makes both `hook_tool_output_fmt` methods private.
* Makes `normalize_name` private.
* Moves all of the Scenario error exception classes (the ones that
no-one should be catching) to a scenario.errors namespace/module.
* Renames the consistency checker module to be private.
* Makes `DEFAULT_JUJU_VERSION` and `DEFAULT_JUJU_DATABAG` private.
* Adds various classes/types to the top-level scenario namespace for use
in type annotations:
* Removes `AnyRelation` in favour of using `RelationBase`
* Removes `PathLike` in favour of `str|Path`.

Fixes #175.
  • Loading branch information
tonyandrewmeyer committed Sep 2, 2024
1 parent abe24c2 commit 33cb2e3
Show file tree
Hide file tree
Showing 20 changed files with 343 additions and 470 deletions.
73 changes: 1 addition & 72 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,51 +263,6 @@ def test_emitted_full():
]
```

### Low-level access: using directly `capture_events`

If you need more control over what events are captured (or you're not into pytest), you can use directly the context
manager that powers the `emitted_events` fixture: `scenario.capture_events`.
This context manager allows you to intercept any events emitted by the framework.

Usage:

```python
import scenario.capture_events

with scenario.capture_events.capture_events() as emitted:
ctx = scenario.Context(SimpleCharm, meta={"name": "capture"})
state_out = ctx.run(
ctx.on.update_status(),
scenario.State(deferred=[ctx.on.start().deferred(SimpleCharm._on_start)])
)

# deferred events get reemitted first
assert isinstance(emitted[0], ops.StartEvent)
# the main Juju event gets emitted next
assert isinstance(emitted[1], ops.UpdateStatusEvent)
# possibly followed by a tail of all custom events that the main Juju event triggered in turn
# assert isinstance(emitted[2], MyFooEvent)
# ...
```

You can filter events by type like so:

```python
import scenario.capture_events

with scenario.capture_events.capture_events(ops.StartEvent, ops.RelationEvent) as emitted:
# capture all `start` and `*-relation-*` events.
pass
```

Configuration:

- Passing no event types, like: `capture_events()`, is equivalent to `capture_events(ops.EventBase)`.
- By default, **framework events** (`PreCommit`, `Commit`) are not considered for inclusion in the output list even if
they match the instance check. You can toggle that by passing: `capture_events(include_framework=True)`.
- By default, **deferred events** are included in the listing if they match the instance check. You can toggle that by
passing: `capture_events(include_deferred=False)`.

## Relations

You can write scenario tests to verify the shape of relation data:
Expand Down Expand Up @@ -439,32 +394,6 @@ joined_event = ctx.on.relation_joined(relation=relation)
The reason for this construction is that the event is associated with some relation-specific metadata, that Scenario
needs to set up the process that will run `ops.main` with the right environment variables.

### Working with relation IDs

Every time you instantiate `Relation` (or peer, or subordinate), the new instance will be given a unique `id`.
To inspect the ID the next relation instance will have, you can call `scenario.state.next_relation_id`.

```python
import scenario.state

next_id = scenario.state.next_relation_id(update=False)
rel = scenario.Relation('foo')
assert rel.id == next_id
```

This can be handy when using `replace` to create new relations, to avoid relation ID conflicts:

```python
import dataclasses
import scenario.state

rel = scenario.Relation('foo')
rel2 = dataclasses.replace(rel, local_app_data={"foo": "bar"}, id=scenario.state.next_relation_id())
assert rel2.id == rel.id + 1
```

If you don't do this, and pass both relations into a `State`, you will trigger a consistency checker error.

### Additional event parameters

All relation events have some additional metadata that does not belong in the Relation object, such as, for a
Expand Down Expand Up @@ -1231,7 +1160,7 @@ therefore, so far as we're concerned, that can't happen, and therefore we help y
are consistent and raise an exception if that isn't so.

That happens automatically behind the scenes whenever you trigger an event;
`scenario.consistency_checker.check_consistency` is called and verifies that the scenario makes sense.
`scenario._consistency_checker.check_consistency` is called and verifies that the scenario makes sense.

## Caveats:

Expand Down
2 changes: 0 additions & 2 deletions docs/custom_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,8 @@ def _compute_navigation_tree(context):
# ('envvar', 'LD_LIBRARY_PATH').
nitpick_ignore = [
# Please keep this list sorted alphabetically.
('py:class', 'AnyJson'),
('py:class', '_CharmSpec'),
('py:class', '_Event'),
('py:class', 'scenario.state._DCBase'),
('py:class', 'scenario.state._EntityStatus'),
('py:class', 'scenario.state._Event'),
('py:class', 'scenario.state._max_posargs.<locals>._MaxPositionalArgs'),
Expand Down
11 changes: 0 additions & 11 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,6 @@ scenario.Context

.. automodule:: scenario.context

scenario.consistency_checker
============================

.. automodule:: scenario.consistency_checker


scenario.capture_events
=======================

.. automodule:: scenario.capture_events


Indices
=======
Expand Down
5 changes: 0 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,6 @@ skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"

[tool.pyright]
ignore = [
"scenario/sequences.py",
"scenario/capture_events.py"
]
[tool.isort]
profile = "black"

Expand Down
58 changes: 37 additions & 21 deletions scenario/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

from scenario.context import Context, Manager
from scenario.state import (
ActionFailed,
ActiveStatus,
Address,
AnyJson,
BindAddress,
BlockedStatus,
CharmType,
CheckInfo,
CloudCredential,
CloudSpec,
Expand All @@ -16,14 +19,18 @@
ErrorStatus,
Exec,
ICMPPort,
JujuLogLine,
MaintenanceStatus,
Model,
Mount,
Network,
Notice,
PeerRelation,
Port,
RawDataBagContents,
RawSecretRevisionContents,
Relation,
RelationBase,
Resource,
Secret,
State,
Expand All @@ -33,43 +40,52 @@
SubordinateRelation,
TCPPort,
UDPPort,
UnitID,
UnknownStatus,
WaitingStatus,
)

__all__ = [
"ActionFailed",
"ActiveStatus",
"Address",
"AnyJson",
"BindAddress",
"BlockedStatus",
"CharmType",
"CheckInfo",
"CloudCredential",
"CloudSpec",
"Container",
"Context",
"StateValidationError",
"Secret",
"Relation",
"SubordinateRelation",
"PeerRelation",
"Model",
"DeferredEvent",
"ErrorStatus",
"Exec",
"ICMPPort",
"JujuLogLine",
"MaintenanceStatus",
"Manager",
"Model",
"Mount",
"Container",
"Notice",
"Address",
"BindAddress",
"Network",
"Notice",
"PeerRelation",
"Port",
"ICMPPort",
"TCPPort",
"UDPPort",
"RawDataBagContents",
"RawSecretRevisionContents",
"Relation",
"RelationBase",
"Resource",
"Secret",
"State",
"StateValidationError",
"Storage",
"StoredState",
"State",
"DeferredEvent",
"ErrorStatus",
"BlockedStatus",
"WaitingStatus",
"MaintenanceStatus",
"ActiveStatus",
"SubordinateRelation",
"TCPPort",
"UDPPort",
"UnitID",
"UnknownStatus",
"Manager",
"WaitingStatus",
"deferred",
]
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
from numbers import Number
from typing import TYPE_CHECKING, Iterable, List, NamedTuple, Tuple, Union

from scenario.runtime import InconsistentScenarioError
from scenario.errors import InconsistentScenarioError
from scenario.runtime import logger as scenario_logger
from scenario.state import (
PeerRelation,
SubordinateRelation,
_Action,
_CharmSpec,
normalize_name,
_normalise_name,
)

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -170,7 +170,7 @@ def _check_relation_event(
"Please pass one.",
)
else:
if not event.name.startswith(normalize_name(event.relation.endpoint)):
if not event.name.startswith(_normalise_name(event.relation.endpoint)):
errors.append(
f"relation event should start with relation endpoint name. {event.name} does "
f"not start with {event.relation.endpoint}.",
Expand All @@ -194,7 +194,7 @@ def _check_workload_event(
"Please pass one.",
)
else:
if not event.name.startswith(normalize_name(event.container.name)):
if not event.name.startswith(_normalise_name(event.container.name)):
errors.append(
f"workload event should start with container name. {event.name} does "
f"not start with {event.container.name}.",
Expand Down Expand Up @@ -231,7 +231,7 @@ def _check_action_event(
)
return

elif not event.name.startswith(normalize_name(action.name)):
elif not event.name.startswith(_normalise_name(action.name)):
errors.append(
f"action event should start with action name. {event.name} does "
f"not start with {action.name}.",
Expand Down Expand Up @@ -261,7 +261,7 @@ def _check_storage_event(
"cannot construct a storage event without the Storage instance. "
"Please pass one.",
)
elif not event.name.startswith(normalize_name(storage.name)):
elif not event.name.startswith(_normalise_name(storage.name)):
errors.append(
f"storage event should start with storage name. {event.name} does "
f"not start with {storage.name}.",
Expand Down Expand Up @@ -566,8 +566,8 @@ def check_containers_consistency(

# event names will be normalized; need to compare against normalized container names.
meta = charm_spec.meta
meta_containers = list(map(normalize_name, meta.get("containers", {})))
state_containers = [normalize_name(c.name) for c in state.containers]
meta_containers = list(map(_normalise_name, meta.get("containers", {})))
state_containers = [_normalise_name(c.name) for c in state.containers]
all_notices = {notice.id for c in state.containers for notice in c.notices}
all_checks = {
(c.name, check.name) for c in state.containers for check in c.check_infos
Expand Down
Loading

0 comments on commit 33cb2e3

Please sign in to comment.