Skip to content

Commit

Permalink
Merge branch 'main' into juju-reboot-929
Browse files Browse the repository at this point in the history
  • Loading branch information
benhoyt authored Oct 20, 2023
2 parents 2cdb33f + 609b05b commit 9b544d0
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 65 deletions.
2 changes: 2 additions & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ Currently we don't publish separate versions of documentation for separate relea

next to the relevant content (e.g. headings, etc.).

Noteworthy changes should also get a new entry in [CHANGES.md](CHANGES.md).


## Dependencies

Expand Down
34 changes: 28 additions & 6 deletions ops/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
List,
Literal,
Mapping,
NoReturn,
Optional,
TextIO,
Tuple,
Expand Down Expand Up @@ -120,11 +121,14 @@ class ActionEvent(EventBase):
params: Dict[str, Any]
"""The parameters passed to the action."""

def defer(self) -> None:
def defer(self) -> NoReturn:
"""Action events are not deferrable like other events.
This is because an action runs synchronously and the administrator
is waiting for the result.
Raises:
RuntimeError: always.
"""
raise RuntimeError('cannot defer action events')

Expand Down Expand Up @@ -180,6 +184,13 @@ def set_results(self, results: Dict[str, Any]):
Args:
results: The result of the action as a Dict
Raises:
ModelError: if a reserved key is used.
ValueError: if ``results`` has a mix of dotted/non-dotted keys that expand out to
result in duplicate keys, for example: :code:`{'a': {'b': 1}, 'a.b': 2}`. Also
raised if a dict is passed with a key that fails to meet the format requirements.
OSError: if extremely large (>100KB) results are provided.
"""
self.framework.model._backend.action_set(results)

Expand Down Expand Up @@ -364,8 +375,11 @@ def add_metrics(self, metrics: Mapping[str, Union[int, float]],
Args:
metrics: Key-value mapping of metrics that have been gathered.
labels: Key-value labels applied to the metrics.
Raises:
ModelError: if invalid keys or values are provided.
"""
self.framework.model._backend.add_metrics(metrics, labels) # type:ignore
self.framework.model._backend.add_metrics(metrics, labels)


class RelationEvent(HookEvent):
Expand Down Expand Up @@ -767,8 +781,12 @@ class SecretRotateEvent(SecretEvent):
revision by calling :meth:`event.secret.set_content() <ops.Secret.set_content>`.
"""

def defer(self) -> None:
"""Secret rotation events are not deferrable (Juju handles re-invocation)."""
def defer(self) -> NoReturn:
"""Secret rotation events are not deferrable (Juju handles re-invocation).
Raises:
RuntimeError: always.
"""
raise RuntimeError(
'Cannot defer secret rotation events. Juju will keep firing this '
'event until you create a new revision.')
Expand Down Expand Up @@ -847,8 +865,12 @@ def restore(self, snapshot: Dict[str, Any]):
super().restore(snapshot)
self._revision = cast(int, snapshot['revision'])

def defer(self) -> None:
"""Secret expiration events are not deferrable (Juju handles re-invocation)."""
def defer(self) -> NoReturn:
"""Secret expiration events are not deferrable (Juju handles re-invocation).
Raises:
RuntimeError: always.
"""
raise RuntimeError(
'Cannot defer secret expiration events. Juju will keep firing '
'this event until you create a new revision.')
Expand Down
21 changes: 14 additions & 7 deletions ops/framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,11 @@ class EventSource:
It is generally used as::
class SomethingHappened(EventBase):
class SomethingHappened(ops.EventBase):
pass
class SomeObject(Object):
something_happened = EventSource(SomethingHappened)
something_happened = ops.EventSource(SomethingHappened)
With that, instances of that type will offer the ``someobj.something_happened``
attribute which is a :class:`BoundEvent`, and may be used to emit and observe
Expand Down Expand Up @@ -445,6 +445,10 @@ def define_event(cls, event_kind: str, event_type: 'Type[EventBase]'):
event. Must be a valid Python identifier, not be a keyword
or an existing attribute.
event_type: A type of the event to define.
Raises:
RuntimeError: if the same event is defined twice, or if ``event_kind``
is an invalid name.
"""
prefix = 'unable to define an event with event_kind that '
if not event_kind.isidentifier():
Expand Down Expand Up @@ -710,7 +714,7 @@ def save_snapshot(self, value: Union["StoredStateData", "EventBase"]):
marshal.dumps(data)
except ValueError:
msg = "unable to save the data for {}, it must contain only simple types: {!r}"
raise ValueError(msg.format(value.__class__.__name__, data))
raise ValueError(msg.format(value.__class__.__name__, data)) from None

self._storage.save_snapshot(value.handle.path, data)

Expand Down Expand Up @@ -954,6 +958,9 @@ def breakpoint(self, name: Optional[str] = None):
stop execution when a hook event is about to be handled.
For those reasons, the "all" and "hook" breakpoint names are reserved.
Raises:
ValueError: if the breakpoint name is invalid.
"""
# If given, validate the name comply with all the rules
if name is not None:
Expand Down Expand Up @@ -1099,15 +1106,15 @@ class StoredState:
Example::
class MyClass(Object):
_stored = StoredState()
class MyClass(ops.Object):
_stored = ops.StoredState()
Instances of ``MyClass`` can transparently save state between invocations by
setting attributes on ``_stored``. Initial state should be set with
``set_default`` on the bound object, that is::
class MyClass(Object):
_stored = StoredState()
class MyClass(ops.Object):
_stored = ops.StoredState()
def __init__(self, parent, key):
super().__init__(parent, key)
Expand Down
59 changes: 51 additions & 8 deletions ops/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ def status(self) -> 'StatusBase':
Example::
self.model.app.status = BlockedStatus('I need a human to come help me')
self.model.app.status = ops.BlockedStatus('I need a human to come help me')
"""
if not self._is_our_app:
return UnknownStatus()
Expand Down Expand Up @@ -402,6 +402,8 @@ def planned_units(self) -> int:
This method only returns data for this charm's application -- the Juju agent isn't
able to see planned unit counts for other applications in the model.
Raises:
RuntimeError: on trying to get the planned units for a remote application.
"""
if not self._is_our_app:
raise RuntimeError(
Expand Down Expand Up @@ -432,6 +434,9 @@ def add_secret(self, content: Dict[str, str], *,
rotate: Rotation policy/time. Every time this elapses, Juju will
notify the charm by sending a SecretRotate event. None (the
default) means to use the Juju default, which is never rotate.
Raises:
ValueError: if the secret is empty, or the secret key is invalid.
"""
Secret._validate_content(content)
id = self._backend.secret_add(
Expand Down Expand Up @@ -507,9 +512,10 @@ def status(self) -> 'StatusBase':
RuntimeError: if setting the status of a unit other than the current unit
InvalidStatusError: if setting the status to something other than
a :class:`StatusBase`
Example::
self.model.unit.status = MaintenanceStatus('reconfiguring the frobnicators')
self.model.unit.status = ops.MaintenanceStatus('reconfiguring the frobnicators')
"""
if not self._is_our_unit:
return UnknownStatus()
Expand Down Expand Up @@ -568,7 +574,11 @@ def set_workload_version(self, version: str) -> None:

@property
def containers(self) -> Mapping[str, 'Container']:
"""Return a mapping of containers indexed by name."""
"""Return a mapping of containers indexed by name.
Raises:
RuntimeError: if called for another unit
"""
if not self._is_our_unit:
raise RuntimeError(f'cannot get container for a remote unit {self}')
return self._containers
Expand All @@ -582,7 +592,7 @@ def get_container(self, container_name: str) -> 'Container':
try:
return self.containers[container_name]
except KeyError:
raise ModelError(f'container {container_name!r} not found')
raise ModelError(f'container {container_name!r} not found') from None

def add_secret(self, content: Dict[str, str], *,
label: Optional[str] = None,
Expand All @@ -592,6 +602,9 @@ def add_secret(self, content: Dict[str, str], *,
"""Create a :class:`Secret` owned by this unit.
See :meth:`Application.add_secret` for parameter details.
Raises:
ValueError: if the secret is empty, or the secret key is invalid.
"""
Secret._validate_content(content)
id = self._backend.secret_add(
Expand Down Expand Up @@ -679,6 +692,11 @@ def set_ports(self, *ports: Union[int, 'Port']) -> None:
Args:
ports: The ports to open. Provide an int to open a TCP port, or
a :class:`Port` to open a port for another protocol.
Raises:
ModelError: if a :class:`Port` is provided where ``protocol`` is 'icmp' but
``port`` is not ``None``, or where ``protocol`` is 'tcp' or 'udp' and ``port``
is ``None``.
"""
# Normalise to get easier comparisons.
existing = {
Expand Down Expand Up @@ -1850,6 +1868,9 @@ def fetch(self, name: str) -> Path:
If successfully fetched, this returns the path where the resource is stored
on disk, otherwise it raises a :class:`NameError`.
Raises:
NameError: if the resource's path cannot be fetched.
"""
if name not in self._paths:
raise NameError(f'invalid resource name: {name}')
Expand Down Expand Up @@ -1919,6 +1940,9 @@ def request(self, storage_name: str, count: int = 1):
Uses storage-add tool to request additional storage. Juju will notify the unit
via ``<storage-name>-storage-attached`` events when it becomes available.
Raises:
ModelError: if the storage is not in the charm's metadata.
"""
if storage_name not in self._storage_map:
raise ModelError(('cannot add storage {!r}:'
Expand Down Expand Up @@ -2012,6 +2036,17 @@ class Container:
This class should not be instantiated directly, instead use :meth:`Unit.get_container`
or :attr:`Unit.containers`.
For methods that make changes to the container, if the change fails or times out, then a
:class:`ops.pebble.ChangeError` or :class:`ops.pebble.TimeoutError` will be raised.
Interactions with the container use Pebble, so all methods may raise exceptions when there are
problems communicating with Pebble. Problems connecting to or transferring data with Pebble
will raise a :class:`ops.pebble.ConnectionError` - generally you can guard against these by
first checking :meth:`can_connect`, but it is possible for problems to occur after
:meth:`can_connect` has succeeded. When an error occurs executing the request, such as trying
to add an invalid layer or execute a command that does not exist, an
:class:`ops.pebble.APIError` is raised.
"""

name: str
Expand Down Expand Up @@ -2139,7 +2174,8 @@ def get_services(self, *service_names: str) -> Mapping[str, 'pebble.ServiceInfo'
def get_service(self, service_name: str) -> pebble.ServiceInfo:
"""Get status information for a single named service.
Raises :class:`ModelError` if service_name is not found.
Raises:
ModelError: if service_name is not found.
"""
services = self.get_services(service_name)
if not services:
Expand All @@ -2166,7 +2202,8 @@ def get_checks(
def get_check(self, check_name: str) -> pebble.CheckInfo:
"""Get check information for a single named check.
Raises :class:`ModelError` if ``check_name`` is not found.
Raises:
ModelError: if ``check_name`` is not found.
"""
checks = self.get_checks(check_name)
if not checks:
Expand Down Expand Up @@ -2557,7 +2594,6 @@ def remove_path(self, path: Union[str, PurePath], *, recursive: bool = False):
Raises:
pebble.PathError: If a relative path is provided, or if `recursive` is False
and the file or directory cannot be removed (it does not exist or is not empty).
"""
self._pebble.remove_path(str(path), recursive=recursive)

Expand Down Expand Up @@ -2627,6 +2663,13 @@ def exec(
See :meth:`ops.pebble.Client.exec` for documentation of the parameters
and return value, as well as examples.
Note that older versions of Juju do not support the ``service_content`` parameter, so if
the Charm is to be used on those versions, then
:meth:`JujuVersion.supports_exec_service_context` should be used as a guard.
Raises:
ExecError: if the command exits with a non-zero exit code.
"""
if service_context is not None:
version = JujuVersion.from_environ()
Expand Down Expand Up @@ -2902,7 +2945,7 @@ def _run(self, *args: str, return_output: bool = False,
try:
result = subprocess.run(args, **kwargs) # type: ignore
except subprocess.CalledProcessError as e:
raise ModelError(e.stderr)
raise ModelError(e.stderr) from e
if return_output:
if result.stdout is None: # type: ignore
return ''
Expand Down
Loading

0 comments on commit 9b544d0

Please sign in to comment.