Skip to content

Commit

Permalink
Revert "feat!: require keyword arguments for most dataclasses"
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyandrewmeyer authored Jul 8, 2024
1 parent c3b5eba commit c5d6287
Show file tree
Hide file tree
Showing 15 changed files with 120 additions and 475 deletions.
32 changes: 17 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,6 @@ meta = {
}
ctx = scenario.Context(ops.CharmBase, meta=meta, unit_id=1)
ctx.run(ctx.on.start(), state_in) # invalid: this unit's id cannot be the ID of a peer.


```

### SubordinateRelation
Expand Down Expand Up @@ -533,7 +531,7 @@ local_file = pathlib.Path('/path/to/local/real/file.txt')
container = scenario.Container(
name="foo",
can_connect=True,
mounts={'local': scenario.Mount(location='/local/share/config.yaml', source=local_file)}
mounts={'local': scenario.Mount('/local/share/config.yaml', local_file)}
)
state = scenario.State(containers=[container])
```
Expand Down Expand Up @@ -570,7 +568,7 @@ def test_pebble_push():
container = scenario,Container(
name='foo',
can_connect=True,
mounts={'local': Mount(location='/local/share/config.yaml', source=local_file.name)}
mounts={'local': Mount('/local/share/config.yaml', local_file.name)}
)
state_in = State(containers=[container])
ctx = Context(
Expand Down Expand Up @@ -669,29 +667,32 @@ Pebble can generate notices, which Juju will detect, and wake up the charm to
let it know that something has happened in the container. The most common
use-case is Pebble custom notices, which is a mechanism for the workload
application to trigger a charm event.
-

When the charm is notified, there might be a queue of existing notices, or just
the one that has triggered the event:

```python
import ops
import scenario

class MyCharm(ops.CharmBase):
def __init__(self, framework):
super().__init__(framework)
framework.observe(self.on["my-container"].pebble_custom_notice, self._on_notice)
framework.observe(self.on["cont"].pebble_custom_notice, self._on_notice)

def _on_notice(self, event):
event.notice.key # == "example.com/c"
for notice in self.unit.get_container("my-container").get_notices():
for notice in self.unit.get_container("cont").get_notices():
...

ctx = scenario.Context(MyCharm, meta={"name": "foo", "containers": {"my-container": {}}})
notices = [
scenario.Notice(key="example.com/a", occurrences=10),
scenario.Notice(key="example.com/a", occurences=10),
scenario.Notice(key="example.com/b", last_data={"bar": "baz"}),
scenario.Notice(key="example.com/c"),
]
container = scenario.Container("my-container", notices=notices)
ctx.run(container.get_notice("example.com/c").event, scenario.State(containers=[container]))
cont = scenario.Container(notices=notices)
ctx.run(container.get_notice("example.com/c").event, scenario.State(containers=[cont]))
```

## Storage
Expand Down Expand Up @@ -765,14 +766,15 @@ ctx.run(ctx.on.storage_attached(foo_1), scenario.State(storage=[foo_0, foo_1]))
Since `ops 2.6.0`, charms can invoke the `open-port`, `close-port`, and `opened-ports` hook tools to manage the ports opened on the host VM/container. Using the `State.opened_ports` API, you can:

- simulate a charm run with a port opened by some previous execution
```python
ctx = scenario.Context(MyCharm, meta=MyCharm.META)
ctx.run(ctx.on.start(), scenario.State(opened_ports=[scenario.TCPPort(42)]))
ctx.run(ctx.on.start(), scenario.State(opened_ports=[scenario.Port("tcp", 42)]))
```
- assert that a charm has called `open-port` or `close-port`:
```python
ctx = scenario.Context(PortCharm, meta=MyCharm.META)
state1 = ctx.run(ctx.on.start(), scenario.State())
assert state1.opened_ports == [scenario.TCPPort(42)]
assert state1.opened_ports == [scenario.Port("tcp", 42)]

state2 = ctx.run(ctx.on.stop(), state1)
assert state2.opened_ports == []
Expand All @@ -786,8 +788,8 @@ Scenario has secrets. Here's how you use them.
state = scenario.State(
secrets=[
scenario.Secret(
{0: {'key': 'public'}},
id='foo',
contents={0: {'key': 'public'}}
)
]
)
Expand Down Expand Up @@ -815,8 +817,8 @@ To specify a secret owned by this unit (or app):
state = scenario.State(
secrets=[
scenario.Secret(
{0: {'key': 'private'}},
id='foo',
contents={0: {'key': 'private'}},
owner='unit', # or 'app'
remote_grants={0: {"remote"}}
# the secret owner has granted access to the "remote" app over some relation with ID 0
Expand All @@ -831,8 +833,8 @@ To specify a secret owned by some other application and give this unit (or app)
state = scenario.State(
secrets=[
scenario.Secret(
{0: {'key': 'public'}},
id='foo',
contents={0: {'key': 'public'}},
# owner=None, which is the default
revision=0, # the revision that this unit (or app) is currently tracking
)
Expand Down
8 changes: 2 additions & 6 deletions scenario/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,19 @@
Container,
DeferredEvent,
ExecOutput,
ICMPPort,
Model,
Mount,
Network,
Notice,
PeerRelation,
Port,
Relation,
Secret,
State,
StateValidationError,
Storage,
StoredState,
SubordinateRelation,
TCPPort,
UDPPort,
deferred,
)

Expand All @@ -49,9 +47,7 @@
"Address",
"BindAddress",
"Network",
"ICMPPort",
"TCPPort",
"UDPPort",
"Port",
"Storage",
"StoredState",
"State",
Expand Down
18 changes: 8 additions & 10 deletions scenario/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
Storage,
_CharmSpec,
_Event,
_max_posargs,
)

if TYPE_CHECKING: # pragma: no cover
Expand All @@ -35,16 +34,16 @@
DEFAULT_JUJU_VERSION = "3.4"


@dataclasses.dataclass(frozen=True)
class ActionOutput(_max_posargs(0)):
@dataclasses.dataclass
class ActionOutput:
"""Wraps the results of running an action event with `run_action`."""

state: "State"
"""The charm state after the action has been handled.
In most cases, actions are not expected to be affecting it."""
logs: List[str]
"""Any logs associated with the action output, set by the charm."""
results: Optional[Dict[str, Any]] = None
results: Optional[Dict[str, Any]]
"""Key-value mapping assigned by the charm as a result of the action.
Will be None if the charm never calls action-set."""
failure: Optional[str] = None
Expand Down Expand Up @@ -317,7 +316,6 @@ def __init__(
self,
charm_type: Type["CharmType"],
meta: Optional[Dict[str, Any]] = None,
*,
actions: Optional[Dict[str, Any]] = None,
config: Optional[Dict[str, Any]] = None,
charm_root: Optional["PathLike"] = None,
Expand Down Expand Up @@ -384,7 +382,7 @@ def __init__(
defined in metadata.yaml.
:arg unit_id: Unit ID that this charm is deployed as. Defaults to 0.
:arg app_trusted: whether the charm has Juju trust (deployed with ``--trust`` or added with
``juju trust``). Defaults to False.
``juju trust``). Defaults to False
:arg charm_root: virtual charm root the charm will be executed with.
If the charm, say, expects a `./src/foo/bar.yaml` file present relative to the
execution cwd, you need to use this. E.g.:
Expand Down Expand Up @@ -555,10 +553,10 @@ def run_action(self, action: "Action", state: "State") -> ActionOutput:

def _finalize_action(self, state_out: "State"):
ao = ActionOutput(
state=state_out,
logs=self._action_logs,
results=self._action_results,
failure=self._action_failure,
state_out,
self._action_logs,
self._action_results,
self._action_failure,
)

# reset all action-related state
Expand Down
24 changes: 9 additions & 15 deletions scenario/mocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@
cast,
)

from ops import JujuVersion, pebble
from ops.model import CloudSpec as CloudSpec_Ops
from ops.model import ModelError
from ops.model import Port as Port_Ops
from ops.model import RelationNotFoundError
from ops import CloudSpec, JujuVersion, pebble
from ops.model import ModelError, RelationNotFoundError
from ops.model import Secret as Secret_Ops # lol
from ops.model import (
SecretInfo,
Expand All @@ -42,8 +39,8 @@
Mount,
Network,
PeerRelation,
Port,
Storage,
_port_cls_by_protocol,
_RawPortProtocolLiteral,
_RawStatusLiteral,
)
Expand Down Expand Up @@ -115,11 +112,8 @@ def __init__(
self._context = context
self._charm_spec = charm_spec

def opened_ports(self) -> Set[Port_Ops]:
return {
Port_Ops(protocol=port.protocol, port=port.port)
for port in self._state.opened_ports
}
def opened_ports(self) -> Set[Port]:
return set(self._state.opened_ports)

def open_port(
self,
Expand All @@ -128,7 +122,7 @@ def open_port(
):
# fixme: the charm will get hit with a StateValidationError
# here, not the expected ModelError...
port_ = _port_cls_by_protocol[protocol](port=port)
port_ = Port(protocol, port)
ports = self._state.opened_ports
if port_ not in ports:
ports.append(port_)
Expand All @@ -138,7 +132,7 @@ def close_port(
protocol: "_RawPortProtocolLiteral",
port: Optional[int] = None,
):
_port = _port_cls_by_protocol[protocol](port=port)
_port = Port(protocol, port)
ports = self._state.opened_ports
if _port in ports:
ports.remove(_port)
Expand Down Expand Up @@ -635,7 +629,7 @@ def resource_get(self, resource_name: str) -> str:
f"resource {resource_name} not found in State. please pass it.",
)

def credential_get(self) -> CloudSpec_Ops:
def credential_get(self) -> CloudSpec:
if not self._context.app_trusted:
raise ModelError(
"ERROR charm is not trusted, initialise Context with `app_trusted=True`",
Expand Down Expand Up @@ -675,7 +669,7 @@ def __init__(
path = Path(mount.location).parts
mounting_dir = container_root.joinpath(*path[1:])
mounting_dir.parent.mkdir(parents=True, exist_ok=True)
mounting_dir.symlink_to(mount.source)
mounting_dir.symlink_to(mount.src)

self._root = container_root

Expand Down
Loading

0 comments on commit c5d6287

Please sign in to comment.