diff --git a/CHANGES.md b/CHANGES.md index b3b4eb80e..6fd7d6c78 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,8 @@ * `StopEvent`, `RemoveEvent`, and all `LifeCycleEvent`s are no longer deferrable, and will raise a `RuntimeError` if `defer()` is called on the event object. * The remote app name (and its databag) is now consistently available in relation-broken events. * Added `ActionEvent.id`, exposing the JUJU_ACTION_UUID environment variable. +* Added support for creating `pebble.Plan` objects by passing in a `pebble.PlanDict`, the + ability to compare two `Plan` objects with `==`, and the ability to create an empty Plan with `Plan()`. # 2.10.0 diff --git a/ops/pebble.py b/ops/pebble.py index 683bfe718..0b96d596a 100644 --- a/ops/pebble.py +++ b/ops/pebble.py @@ -743,8 +743,11 @@ class Plan: documented at https://github.com/canonical/pebble/#layer-specification. """ - def __init__(self, raw: str): - d = yaml.safe_load(raw) or {} # type: ignore + def __init__(self, raw: Optional[Union[str, 'PlanDict']] = None): + if isinstance(raw, str): # noqa: SIM108 + d = yaml.safe_load(raw) or {} # type: ignore + else: + d = raw or {} d = typing.cast('PlanDict', d) self._raw = raw @@ -796,6 +799,13 @@ def to_yaml(self) -> str: __str__ = to_yaml + def __eq__(self, other: Union['PlanDict', 'Plan']) -> bool: + if isinstance(other, dict): + return self.to_dict() == other + elif isinstance(other, Plan): + return self.to_dict() == other.to_dict() + return NotImplemented + class Layer: """Represents a Pebble configuration layer. diff --git a/test/test_pebble.py b/test/test_pebble.py index 4709e94f0..7c7f42990 100644 --- a/test/test_pebble.py +++ b/test/test_pebble.py @@ -492,10 +492,6 @@ def test_notice_from_dict(self): class TestPlan(unittest.TestCase): - def test_no_args(self): - with self.assertRaises(TypeError): - pebble.Plan() # type: ignore - def test_services(self): plan = pebble.Plan('') self.assertEqual(plan.services, {}) @@ -589,6 +585,37 @@ def test_yaml(self): self.assertEqual(plan.to_yaml(), reformed) self.assertEqual(str(plan), reformed) + def test_plandict(self): + # Starting with nothing, we get the empty result. + plan = pebble.Plan({}) + self.assertEqual(plan.to_dict(), {}) + plan = pebble.Plan() + self.assertEqual(plan.to_dict(), {}) + + # With a service, we return validated yaml content. + raw: pebble.PlanDict = { + "services": { + "foo": { + "override": "replace", + "command": "echo foo", + }, + }, + "checks": { + "bar": { + "http": {"url": "https://example.com/"}, + }, + }, + "log-targets": { + "baz": { + "override": "replace", + "type": "loki", + "location": "https://example.com:3100/loki/api/v1/push", + }, + }, + } + plan = pebble.Plan(raw) + self.assertEqual(plan.to_dict(), raw) + def test_service_equality(self): plan = pebble.Plan(""" services: @@ -610,6 +637,114 @@ def test_service_equality(self): } self.assertEqual(plan.services, services_as_dict) + def test_plan_equality(self): + plan1 = pebble.Plan(''' +services: + foo: + override: replace + command: echo foo +''') + self.assertNotEqual(plan1, "foo") + plan2 = pebble.Plan(''' +services: + foo: + command: echo foo + override: replace +''') + self.assertEqual(plan1, plan2) + plan1_as_dict = { + "services": { + "foo": { + "command": "echo foo", + "override": "replace", + }, + }, + } + self.assertEqual(plan1, plan1_as_dict) + plan3 = pebble.Plan(''' +services: + foo: + override: replace + command: echo bar +''') + # Different command. + self.assertNotEqual(plan1, plan3) + plan4 = pebble.Plan(''' +services: + foo: + override: replace + command: echo foo + +checks: + bar: + http: + https://example.com/ + +log-targets: + baz: + override: replace + type: loki + location: https://example.com:3100/loki/api/v1/push +''') + plan5 = pebble.Plan(''' +services: + foo: + override: replace + command: echo foo + +checks: + bar: + http: + https://different.example.com/ + +log-targets: + baz: + override: replace + type: loki + location: https://example.com:3100/loki/api/v1/push +''') + # Different checks.bar.http + self.assertNotEqual(plan4, plan5) + plan6 = pebble.Plan(''' +services: + foo: + override: replace + command: echo foo + +checks: + bar: + http: + https://example.com/ + +log-targets: + baz: + override: replace + type: loki + location: https://example.com:3200/loki/api/v1/push +''') + # Reordered elements. + self.assertNotEqual(plan4, plan6) + plan7 = pebble.Plan(''' +services: + foo: + command: echo foo + override: replace + +log-targets: + baz: + type: loki + override: replace + location: https://example.com:3100/loki/api/v1/push + +checks: + bar: + http: + https://example.com/ + +''') + # Reordered sections. + self.assertEqual(plan4, plan7) + class TestLayer(unittest.TestCase): def _assert_empty(self, layer: pebble.Layer): diff --git a/tox.ini b/tox.ini index d1ba3fe0f..a5330ae56 100644 --- a/tox.ini +++ b/tox.ini @@ -44,7 +44,7 @@ commands = [testenv:lint] description = Check code against coding style standards deps = - ruff~=0.2.1 + ruff~=0.2.2 commands = ruff check --preview