Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add the ability to compare two Plan objects with == and create a Plan from a dict #1134

Merged
merged 5 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* `StopEvent`, `RemoveEvent`, and all `LifeCycleEvent`s are no longer deferrable, and will raise a `RuntimeError` if `defer()` is called on the event object.
* 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

Expand Down
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@

_old_compute_navigation_tree = furo._compute_navigation_tree


def _compute_navigation_tree(context):
tree_html = _old_compute_navigation_tree(context)
if not tree_html and context.get("toc"):
tree_html = furo.navigation.get_navigation_tree(context["toc"])
return tree_html


furo._compute_navigation_tree = _compute_navigation_tree


Expand Down
22 changes: 20 additions & 2 deletions ops/pebble.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,17 @@ def __call__(self, data: bytes, done: bool = False) -> None: ...

class _Tempfile(Protocol):
name = ''

def write(self, data: bytes): ...

def close(self): ...


class _FileLikeIO(Protocol[typing.AnyStr]): # That also covers TextIO and BytesIO
def read(self, __n: int = ...) -> typing.AnyStr: ... # for BinaryIO

def write(self, __s: typing.AnyStr) -> int: ...

def __enter__(self) -> typing.IO[typing.AnyStr]: ...


Expand Down Expand Up @@ -276,9 +280,13 @@ def __enter__(self) -> typing.IO[typing.AnyStr]: ...

class _WebSocket(Protocol):
def connect(self, url: str, socket: socket.socket): ...

def shutdown(self): ...

def send(self, payload: str): ...

def send_binary(self, payload: bytes): ...

def recv(self) -> Union[str, bytes]: ...


Expand Down Expand Up @@ -735,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
Expand Down Expand Up @@ -788,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.
Expand Down
143 changes: 139 additions & 4 deletions test/test_pebble.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, {})
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading