diff --git a/pyproject.toml b/pyproject.toml index 4204c4645..84c7974a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -254,8 +254,28 @@ convention = "google" builtins-ignorelist = ["id", "min", "map", "range", "type", "TimeoutError", "ConnectionError", "Warning", "input", "format"] [tool.pyright] -include = ["ops/*.py", "ops/_private/*.py", "test/*.py", "test/charms/*/src/*.py", "testing/src/*.py"] -exclude = ["tracing/*"] +include = ["ops/*.py", "ops/_private/*.py", "test/*.py", "test/charms/*/src/*.py", "testing/src/*.py", "testing/tests/*.py", "testing/tests/test_e2e/*.py"] +exclude = [ + "tracing/*", + "testing/tests/helpers.py", + "testing/tests/test_charm_spec_autoload.py", + "testing/tests/test_consistency_checker.py", + "testing/tests/test_context_on.py", + "testing/tests/test_context.py", + "testing/tests/test_emitted_events_util.py", + "testing/tests/test_plugin.py", + "testing/tests/test_runtime.py", + "testing/tests/test_e2e/test_secrets.py", + "testing/tests/test_e2e/test_relations.py", + "testing/tests/test_e2e/test_trace_data.py", + "testing/tests/test_e2e/test_juju_log.py", + "testing/tests/test_e2e/test_status.py", + "testing/tests/test_e2e/test_storage.py", + "testing/tests/test_e2e/test_manager.py", + "testing/tests/test_e2e/test_ports.py", + "testing/tests/test_e2e/test_rubbish_events.py", + "testing/tests/test_e2e/test_pebble.py", +] extraPaths = ["testing", "tracing"] pythonVersion = "3.10" # check no python > 3.10 features are used pythonPlatform = "All" diff --git a/testing/tests/helpers.py b/testing/tests/helpers.py index ed285ccf3..904157dcf 100644 --- a/testing/tests/helpers.py +++ b/testing/tests/helpers.py @@ -60,7 +60,7 @@ def trigger( return state_out -def jsonpatch_delta(self, other: State): +def jsonpatch_delta(self: State, other: State) -> list[dict[str, Any]]: dict_other = dataclasses.asdict(other) dict_self = dataclasses.asdict(self) for attr in ( @@ -79,5 +79,8 @@ def jsonpatch_delta(self, other: State): return sort_patch(patch) -def sort_patch(patch: list[dict], key=lambda obj: obj['path'] + obj['op']): +def sort_patch( + patch: list[dict[str, Any]], + key: Callable[[dict[str, Any]], str] = lambda obj: obj['path'] + obj['op'], +) -> list[dict[str, Any]]: return sorted(patch, key=key) diff --git a/testing/tests/test_e2e/test_actions.py b/testing/tests/test_e2e/test_actions.py index a13de1f77..b4acc2d11 100644 --- a/testing/tests/test_e2e/test_actions.py +++ b/testing/tests/test_e2e/test_actions.py @@ -3,6 +3,8 @@ from __future__ import annotations +from typing import Any + import pytest from scenario import Context from scenario.state import State, _Action, _next_action_id @@ -13,7 +15,7 @@ @pytest.fixture(scope='function') -def mycharm(): +def mycharm() -> type[CharmBase]: class MyCharm(CharmBase): _evt_handler = None @@ -22,7 +24,7 @@ def __init__(self, framework: Framework): for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: ActionEvent): if handler := self._evt_handler: handler(event) @@ -30,8 +32,8 @@ def _on_event(self, event): @pytest.mark.parametrize('baz_value', (True, False)) -def test_action_event(mycharm, baz_value): - ctx = Context( +def test_action_event(mycharm: type[CharmBase], baz_value: bool): + ctx: Context[CharmBase] = Context( mycharm, meta={'name': 'foo'}, actions={'foo': {'params': {'bar': {'type': 'number'}, 'baz': {'type': 'boolean'}}}}, @@ -47,11 +49,11 @@ def test_action_event(mycharm, baz_value): def test_action_no_results(): class MyCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.act_action, self._on_act_action) - def _on_act_action(self, _): + def _on_act_action(self, _: ActionEvent): pass ctx = Context(MyCharm, meta={'name': 'foo'}, actions={'act': {}}) @@ -61,27 +63,27 @@ def _on_act_action(self, _): @pytest.mark.parametrize('res_value', ('one', 1, [2], ['bar'], (1,), {1, 2})) -def test_action_event_results_invalid(mycharm, res_value): +def test_action_event_results_invalid(mycharm: type[CharmBase], res_value: object): def handle_evt(charm: CharmBase, evt: ActionEvent): with pytest.raises((TypeError, AttributeError)): - evt.set_results(res_value) + evt.set_results(res_value) # type: ignore - mycharm._evt_handler = handle_evt + mycharm._evt_handler = handle_evt # type: ignore ctx = Context(mycharm, meta={'name': 'foo'}, actions={'foo': {}}) ctx.run(ctx.on.action('foo'), State()) @pytest.mark.parametrize('res_value', ({'a': {'b': {'c'}}}, {'d': 'e'})) -def test_action_event_results_valid(mycharm, res_value): - def handle_evt(_: CharmBase, evt): +def test_action_event_results_valid(mycharm: type[CharmBase], res_value: dict[str, Any]): + def handle_evt(_: CharmBase, evt: ActionEvent): if not isinstance(evt, ActionEvent): return evt.set_results(res_value) evt.log('foo') evt.log('bar') - mycharm._evt_handler = handle_evt + mycharm._evt_handler = handle_evt # type: ignore ctx = Context(mycharm, meta={'name': 'foo'}, actions={'foo': {}}) @@ -91,7 +93,7 @@ def handle_evt(_: CharmBase, evt): @pytest.mark.parametrize('res_value', ({'a': {'b': {'c'}}}, {'d': 'e'})) -def test_action_event_outputs(mycharm, res_value): +def test_action_event_outputs(mycharm: type[CharmBase], res_value: dict[str, Any]): def handle_evt(_: CharmBase, evt: ActionEvent): if not isinstance(evt, ActionEvent): return @@ -101,7 +103,7 @@ def handle_evt(_: CharmBase, evt: ActionEvent): evt.log('log2') evt.fail('failed becozz') - mycharm._evt_handler = handle_evt + mycharm._evt_handler = handle_evt # type: ignore ctx = Context(mycharm, meta={'name': 'foo'}, actions={'foo': {}}) with pytest.raises(ActionFailed) as exc_info: @@ -111,13 +113,13 @@ def handle_evt(_: CharmBase, evt: ActionEvent): assert ctx.action_logs == ['log1', 'log2'] -def test_action_event_fail(mycharm): +def test_action_event_fail(mycharm: type[CharmBase]): def handle_evt(_: CharmBase, evt: ActionEvent): if not isinstance(evt, ActionEvent): return evt.fail('action failed!') - mycharm._evt_handler = handle_evt + mycharm._evt_handler = handle_evt # type: ignore ctx = Context(mycharm, meta={'name': 'foo'}, actions={'foo': {}}) with pytest.raises(ActionFailed) as exc_info: @@ -125,13 +127,13 @@ def handle_evt(_: CharmBase, evt: ActionEvent): assert exc_info.value.message == 'action failed!' -def test_action_event_fail_context_manager(mycharm): +def test_action_event_fail_context_manager(mycharm: type[CharmBase]): def handle_evt(_: CharmBase, evt: ActionEvent): if not isinstance(evt, ActionEvent): return evt.fail('action failed!') - mycharm._evt_handler = handle_evt + mycharm._evt_handler = handle_evt # type: ignore ctx = Context(mycharm, meta={'name': 'foo'}, actions={'foo': {}}) with pytest.raises(ActionFailed) as exc_info: @@ -142,11 +144,11 @@ def handle_evt(_: CharmBase, evt: ActionEvent): def test_action_continues_after_fail(): class MyCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_action, self._on_foo_action) - def _on_foo_action(self, event): + def _on_foo_action(self, event: ActionEvent): event.log('starting') event.set_results({'initial': 'result'}) event.fail('oh no!') @@ -160,19 +162,19 @@ def _on_foo_action(self, event): assert ctx.action_results == {'initial': 'result', 'final': 'result'} -def test_action_event_has_id(mycharm): +def test_action_event_has_id(mycharm: type[CharmBase]): def handle_evt(_: CharmBase, evt: ActionEvent): if not isinstance(evt, ActionEvent): return assert isinstance(evt.id, str) and evt.id != '' - mycharm._evt_handler = handle_evt + mycharm._evt_handler = handle_evt # type: ignore ctx = Context(mycharm, meta={'name': 'foo'}, actions={'foo': {}}) ctx.run(ctx.on.action('foo'), State()) -def test_action_event_has_override_id(mycharm): +def test_action_event_has_override_id(mycharm: type[CharmBase]): uuid = '0ddba11-cafe-ba1d-5a1e-dec0debad' def handle_evt(charm: CharmBase, evt: ActionEvent): @@ -180,7 +182,7 @@ def handle_evt(charm: CharmBase, evt: ActionEvent): return assert evt.id == uuid - mycharm._evt_handler = handle_evt + mycharm._evt_handler = handle_evt # type: ignore ctx = Context(mycharm, meta={'name': 'foo'}, actions={'foo': {}}) ctx.run(ctx.on.action('foo', id=uuid), State()) @@ -188,16 +190,16 @@ def handle_evt(charm: CharmBase, evt: ActionEvent): def test_two_actions_same_context(): class MyCharm(CharmBase): - def __init__(self, framework): + def __init__(self, framework: Framework): super().__init__(framework) framework.observe(self.on.foo_action, self._on_foo_action) framework.observe(self.on.bar_action, self._on_bar_action) - def _on_foo_action(self, event): + def _on_foo_action(self, event: ActionEvent): event.log('foo') event.set_results({'foo': 'result'}) - def _on_bar_action(self, event): + def _on_bar_action(self, event: ActionEvent): event.log('bar') event.set_results({'bar': 'result'}) @@ -213,7 +215,7 @@ def _on_bar_action(self, event): def test_positional_arguments(): with pytest.raises(TypeError): - _Action('foo', {}) + _Action('foo', {}) # type: ignore def test_default_arguments(): diff --git a/testing/tests/test_e2e/test_cloud_spec.py b/testing/tests/test_e2e/test_cloud_spec.py index 8a952d134..e50a3d870 100644 --- a/testing/tests/test_e2e/test_cloud_spec.py +++ b/testing/tests/test_e2e/test_cloud_spec.py @@ -15,7 +15,7 @@ def __init__(self, framework: ops.Framework): for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: ops.EventBase): pass diff --git a/testing/tests/test_e2e/test_config.py b/testing/tests/test_e2e/test_config.py index a3dc014ef..e968e5cec 100644 --- a/testing/tests/test_e2e/test_config.py +++ b/testing/tests/test_e2e/test_config.py @@ -3,31 +3,32 @@ from __future__ import annotations +from typing import Any + import pytest from scenario.state import State -from ops.charm import CharmBase -from ops.framework import Framework +import ops from ..helpers import trigger @pytest.fixture(scope='function') -def mycharm(): - class MyCharm(CharmBase): - def __init__(self, framework: Framework): +def mycharm() -> type[ops.CharmBase]: + class MyCharm(ops.CharmBase): + def __init__(self, framework: ops.Framework): super().__init__(framework) for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: ops.EventBase): pass return MyCharm -def test_config_get(mycharm): - def check_cfg(charm: CharmBase): +def test_config_get(mycharm: type[ops.CharmBase]): + def check_cfg(charm: ops.CharmBase): assert charm.config['foo'] == 'bar' assert charm.config['baz'] == 1 @@ -43,8 +44,8 @@ def check_cfg(charm: CharmBase): ) -def test_config_get_default_from_meta(mycharm): - def check_cfg(charm: CharmBase): +def test_config_get_default_from_meta(mycharm: type[ops.CharmBase]): + def check_cfg(charm: ops.CharmBase): assert charm.config['foo'] == 'bar' assert charm.config['baz'] == 2 assert charm.config['qux'] is False @@ -75,18 +76,18 @@ def check_cfg(charm: CharmBase): {'baz': 4, 'foo': 'bar', 'qux': True}, ), ) -def test_config_in_not_mutated(mycharm, cfg_in): - class MyCharm(CharmBase): - def __init__(self, framework: Framework): +def test_config_in_not_mutated(mycharm: type[ops.CharmBase], cfg_in: dict[str, Any]): + class MyCharm(ops.CharmBase): + def __init__(self, framework: ops.Framework): super().__init__(framework) for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: ops.EventBase): # access the config to trigger a config-get - foo_cfg = self.config['foo'] # noqa: F841 - baz_cfg = self.config['baz'] # noqa: F841 - qux_cfg = self.config['qux'] # noqa: F841 + _foo_cfg = self.config['foo'] + _baz_cfg = self.config['baz'] + _qux_cfg = self.config['qux'] state_out = trigger( State( diff --git a/testing/tests/test_e2e/test_deferred.py b/testing/tests/test_e2e/test_deferred.py index 94043cc2a..134262848 100644 --- a/testing/tests/test_e2e/test_deferred.py +++ b/testing/tests/test_e2e/test_deferred.py @@ -11,7 +11,6 @@ from scenario.state import Container, Relation, State, _Event import ops -from ops.framework import LifecycleEvent from ..helpers import trigger @@ -19,7 +18,7 @@ @pytest.fixture(scope='function') -def mycharm(): +def mycharm() -> type[ops.CharmBase]: class MyCharm(ops.CharmBase): META: Mapping[str, typing.Any] = { 'name': 'mycharm', @@ -32,7 +31,7 @@ class MyCharm(ops.CharmBase): def __init__(self, framework: ops.Framework): super().__init__(framework) for evt in self.on.events().values(): - if issubclass(evt.event_type, LifecycleEvent): + if issubclass(evt.event_type, ops.LifecycleEvent): continue self.framework.observe(evt, self._on_event) @@ -46,21 +45,21 @@ def _on_event(self, event: ops.EventBase): return MyCharm -def test_defer(mycharm): - mycharm.defer_next = True - out = trigger(State(), 'start', mycharm, meta=mycharm.META) +def test_defer(mycharm: type[ops.CharmBase]): + mycharm.defer_next = True # type: ignore + out = trigger(State(), 'start', mycharm, meta=mycharm.META) # type: ignore assert len(out.deferred) == 1 assert out.deferred[0].name == 'start' -def test_deferred_evt_emitted(mycharm): - mycharm.defer_next = 2 +def test_deferred_evt_emitted(mycharm: type[ops.CharmBase]): + mycharm.defer_next = 2 # type: ignore out = trigger( - State(deferred=[_Event('update_status').deferred(handler=mycharm._on_event)]), + State(deferred=[_Event('update_status').deferred(handler=mycharm._on_event)]), # type: ignore 'start', mycharm, - meta=mycharm.META, + meta=mycharm.META, # type: ignore ) # we deferred the first 2 events we saw: update-status, start. @@ -69,13 +68,13 @@ def test_deferred_evt_emitted(mycharm): assert update_status.name == 'update_status' # we saw start and update-status. - upstat, start = mycharm.captured + upstat, start = mycharm.captured # type: ignore assert isinstance(upstat, ops.UpdateStatusEvent) assert isinstance(start, ops.StartEvent) -def test_deferred_relation_event(mycharm): - mycharm.defer_next = 2 +def test_deferred_relation_event(mycharm: type[ops.CharmBase]): + mycharm.defer_next = 2 # type: ignore rel = Relation(endpoint='foo', remote_app_name='remote') @@ -84,13 +83,13 @@ def test_deferred_relation_event(mycharm): relations={rel}, deferred=[ _Event('foo_relation_changed', relation=rel).deferred( - handler=mycharm._on_event, + handler=mycharm._on_event, # type: ignore ) ], ), 'start', mycharm, - meta=mycharm.META, + meta=mycharm.META, # type: ignore ) # we deferred the first 2 events we saw: relation-changed, start. @@ -99,25 +98,25 @@ def test_deferred_relation_event(mycharm): assert start.name == 'start' # we saw start and relation-changed. - relation_changed, start = mycharm.captured + relation_changed, start = mycharm.captured # type: ignore assert isinstance(relation_changed, ops.RelationChangedEvent) assert isinstance(start, ops.StartEvent) -def test_deferred_relation_event_from_relation(mycharm): - ctx = Context(mycharm, meta=mycharm.META) - mycharm.defer_next = 2 +def test_deferred_relation_event_from_relation(mycharm: type[ops.CharmBase]): + ctx = Context(mycharm, meta=mycharm.META) # type: ignore + mycharm.defer_next = 2 # type: ignore rel = Relation(endpoint='foo', remote_app_name='remote') out = trigger( State( relations={rel}, deferred=[ - ctx.on.relation_changed(rel, remote_unit=1).deferred(handler=mycharm._on_event) + ctx.on.relation_changed(rel, remote_unit=1).deferred(handler=mycharm._on_event) # type: ignore ], ), 'start', mycharm, - meta=mycharm.META, + meta=mycharm.META, # type: ignore ) # we deferred the first 2 events we saw: foo_relation_changed, start. @@ -132,13 +131,13 @@ def test_deferred_relation_event_from_relation(mycharm): assert start.name == 'start' # we saw start and foo_relation_changed. - relation_changed, start = mycharm.captured + relation_changed, start = mycharm.captured # type: ignore assert isinstance(relation_changed, ops.RelationChangedEvent) assert isinstance(start, ops.StartEvent) -def test_deferred_workload_event(mycharm): - mycharm.defer_next = 2 +def test_deferred_workload_event(mycharm: type[ops.CharmBase]): + mycharm.defer_next = 2 # type: ignore ctr = Container('foo') @@ -146,12 +145,12 @@ def test_deferred_workload_event(mycharm): State( containers={ctr}, deferred=[ - _Event('foo_pebble_ready', container=ctr).deferred(handler=mycharm._on_event) + _Event('foo_pebble_ready', container=ctr).deferred(handler=mycharm._on_event) # type: ignore ], ), 'start', mycharm, - meta=mycharm.META, + meta=mycharm.META, # type: ignore ) # we deferred the first 2 events we saw: foo_pebble_ready, start. @@ -160,18 +159,18 @@ def test_deferred_workload_event(mycharm): assert start.name == 'start' # we saw start and foo_pebble_ready. - ready, start = mycharm.captured + ready, start = mycharm.captured # type: ignore assert isinstance(ready, ops.WorkloadEvent) assert isinstance(start, ops.StartEvent) -def test_defer_reemit_lifecycle_event(mycharm): - ctx = Context(mycharm, meta=mycharm.META, capture_deferred_events=True) +def test_defer_reemit_lifecycle_event(mycharm: type[ops.CharmBase]): + ctx = Context(mycharm, meta=mycharm.META, capture_deferred_events=True) # type: ignore - mycharm.defer_next = 1 + mycharm.defer_next = 1 # type: ignore state_1 = ctx.run(ctx.on.update_status(), State()) - mycharm.defer_next = 0 + mycharm.defer_next = 0 # type: ignore state_2 = ctx.run(ctx.on.start(), state_1) assert [type(e).__name__ for e in ctx.emitted_events] == [ @@ -183,14 +182,14 @@ def test_defer_reemit_lifecycle_event(mycharm): assert not state_2.deferred -def test_defer_reemit_relation_event(mycharm): - ctx = Context(mycharm, meta=mycharm.META, capture_deferred_events=True) +def test_defer_reemit_relation_event(mycharm: type[ops.CharmBase]): + ctx = Context(mycharm, meta=mycharm.META, capture_deferred_events=True) # type: ignore rel = Relation('foo') - mycharm.defer_next = 1 + mycharm.defer_next = 1 # type: ignore state_1 = ctx.run(ctx.on.relation_created(rel), State(relations={rel})) - mycharm.defer_next = 0 + mycharm.defer_next = 0 # type: ignore state_2 = ctx.run(ctx.on.start(), state_1) assert [type(e).__name__ for e in ctx.emitted_events] == [ @@ -233,22 +232,24 @@ def __init__(self, charm: ops.CharmBase): super().__init__(charm, 'my-consumer') -def test_defer_custom_event(mycharm): +def test_defer_custom_event(mycharm: type[ops.CharmBase]): class MyCharm(mycharm): def __init__(self, framework: ops.Framework): super().__init__(framework) self.consumer = MyConsumer(self) - framework.observe(self.consumer.on.foo_changed, self._on_event) + framework.observe(self.consumer.on.foo_changed, self._on_event) # type: ignore - ctx = Context(MyCharm, meta=mycharm.META, capture_deferred_events=True) + ctx = Context(MyCharm, meta=mycharm.META, capture_deferred_events=True) # type: ignore - mycharm.defer_next = 1 - state_1 = ctx.run(ctx.on.custom(MyConsumer.on.foo_changed, 'foo', 28), State()) + mycharm.defer_next = 1 # type: ignore + state_1 = ctx.run( + ctx.on.custom(typing.cast('typing.Any', MyConsumer.on).foo_changed, 'foo', 28), State() + ) assert [type(e).__name__ for e in ctx.emitted_events] == ['CustomEventWithArgs'] assert ctx.emitted_events[0].snapshot() == {'arg0': 'foo', 'arg1': 28} assert len(state_1.deferred) == 1 - mycharm.defer_next = 0 + mycharm.defer_next = 0 # type: ignore state_2 = ctx.run(ctx.on.start(), state_1) assert [type(e).__name__ for e in ctx.emitted_events] == [ 'CustomEventWithArgs', diff --git a/testing/tests/test_e2e/test_event.py b/testing/tests/test_e2e/test_event.py index 979db8158..e57071921 100644 --- a/testing/tests/test_e2e/test_event.py +++ b/testing/tests/test_e2e/test_event.py @@ -3,6 +3,8 @@ from __future__ import annotations +from typing import Any, ClassVar + import pytest from scenario import Context from scenario.state import State, _CharmSpec, _Event, _EventType @@ -32,7 +34,7 @@ ('kaboozle_bar_baz', _EventType.CUSTOM), ), ) -def test_event_type(evt, expected_type): +def test_event_type(evt: str, expected_type: _EventType): event = _Event(evt) assert event._path.type is expected_type @@ -45,7 +47,7 @@ def test_event_type(evt, expected_type): class MyCharm(ops.CharmBase): pass - spec = _CharmSpec( + spec = _CharmSpec[MyCharm]( MyCharm, meta={ 'requires': { @@ -76,6 +78,8 @@ def test_emitted_framework(): def test_emitted_deferred(): class MyCharm(ops.CharmBase): + META: ClassVar[dict[str, Any]] = {'name': 'joop'} + def __init__(self, framework: ops.Framework): super().__init__(framework) framework.observe(self.on.update_status, self._on_update_status) diff --git a/testing/tests/test_e2e/test_network.py b/testing/tests/test_e2e/test_network.py index c5d4eecba..b58295656 100644 --- a/testing/tests/test_e2e/test_network.py +++ b/testing/tests/test_e2e/test_network.py @@ -14,24 +14,22 @@ SubordinateRelation, ) -from ops import RelationNotFoundError -from ops.charm import CharmBase -from ops.framework import Framework +import ops @pytest.fixture(scope='function') -def mycharm(): - class MyCharm(CharmBase): +def mycharm() -> type[ops.CharmBase]: + class MyCharm(ops.CharmBase): _call = None called = False - def __init__(self, framework: Framework): + def __init__(self, framework: ops.Framework): super().__init__(framework) for evt in self.on.events().values(): - self.framework.observe(evt, self._on_event) + framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: ops.EventBase): if MyCharm._call: MyCharm.called = True MyCharm._call(self, event) @@ -39,8 +37,8 @@ def _on_event(self, event): return MyCharm -def test_ip_get(mycharm): - ctx = Context( +def test_ip_get(mycharm: type[ops.CharmBase]): + ctx: Context[ops.CharmBase] = Context( mycharm, meta={ 'name': 'foo', @@ -68,17 +66,30 @@ def test_ip_get(mycharm): ) as mgr: # we have a network for the relation rel = mgr.charm.model.get_relation('metrics-endpoint') - assert str(mgr.charm.model.get_binding(rel).network.bind_address) == '192.0.2.0' + assert rel is not None + binding = mgr.charm.model.get_binding(rel) + assert binding is not None + network = binding.network + assert network is not None + assert str(network.bind_address) == '192.0.2.0' # we have a network for a binding without relations on it - assert str(mgr.charm.model.get_binding('deadnodead').network.bind_address) == '192.0.2.0' + binding = mgr.charm.model.get_binding('deadnodead') + assert binding is not None + network = binding.network + assert network is not None + assert str(network.bind_address) == '192.0.2.0' # and an extra binding - assert str(mgr.charm.model.get_binding('foo').network.bind_address) == '4.4.4.4' + binding = mgr.charm.model.get_binding('foo') + assert binding is not None + network = binding.network + assert network is not None + assert str(network.bind_address) == '4.4.4.4' -def test_no_sub_binding(mycharm): - ctx = Context( +def test_no_sub_binding(mycharm: type[ops.CharmBase]): + ctx: Context[ops.CharmBase] = Context( mycharm, meta={ 'name': 'foo', @@ -94,15 +105,15 @@ def test_no_sub_binding(mycharm): ] ), ) as mgr: - with pytest.raises(RelationNotFoundError): + with pytest.raises(ops.RelationNotFoundError): # sub relations have no network - mgr.charm.model.get_binding('bar').network # noqa: B018 # Used to trigger the error. + mgr.charm.model.get_binding('bar').network # type: ignore # noqa: B018 # Used to trigger the error. -def test_no_relation_error(mycharm): +def test_no_relation_error(mycharm: type[ops.CharmBase]): """Attempting to call get_binding on a non-existing relation -> RelationNotFoundError""" - ctx = Context( + ctx: Context[ops.CharmBase] = Context( mycharm, meta={ 'name': 'foo', @@ -125,12 +136,12 @@ def test_no_relation_error(mycharm): networks={Network('bar')}, ), ) as mgr: - with pytest.raises(RelationNotFoundError): - mgr.charm.model.get_binding('foo').network # noqa: B018 # Used to trigger the error. + with pytest.raises(ops.RelationNotFoundError): + mgr.charm.model.get_binding('foo').network # type: ignore # noqa: B018 # Used to trigger the error. -def test_juju_info_network_default(mycharm): - ctx = Context( +def test_juju_info_network_default(mycharm: type[ops.CharmBase]): + ctx: Context[ops.CharmBase] = Context( mycharm, meta={'name': 'foo'}, ) @@ -140,11 +151,15 @@ def test_juju_info_network_default(mycharm): State(), ) as mgr: # we have a network for the relation - assert str(mgr.charm.model.get_binding('juju-info').network.bind_address) == '192.0.2.0' + binding = mgr.charm.model.get_binding('juju-info') + assert binding is not None + network = binding.network + assert network is not None + assert str(network.bind_address) == '192.0.2.0' -def test_explicit_juju_info_network_override(mycharm): - ctx = Context( +def test_explicit_juju_info_network_override(mycharm: type[ops.CharmBase]): + ctx: Context[ops.CharmBase] = Context( mycharm, meta={ 'name': 'foo', @@ -157,4 +172,8 @@ def test_explicit_juju_info_network_override(mycharm): ctx.on.update_status(), State(), ) as mgr: - assert mgr.charm.model.get_binding('juju-info').network.bind_address + binding = mgr.charm.model.get_binding('juju-info') + assert binding is not None + network = binding.network + assert network is not None + assert network.bind_address diff --git a/testing/tests/test_e2e/test_play_assertions.py b/testing/tests/test_e2e/test_play_assertions.py index 206ff6b83..6fa980c40 100644 --- a/testing/tests/test_e2e/test_play_assertions.py +++ b/testing/tests/test_e2e/test_play_assertions.py @@ -6,29 +6,26 @@ import dataclasses import pytest -from scenario.state import BlockedStatus as ScenarioBlockedStatus -from scenario.state import Relation, State +from scenario.state import BlockedStatus, Relation, State -from ops.charm import CharmBase -from ops.framework import Framework -from ops.model import ActiveStatus, BlockedStatus +import ops from ..helpers import jsonpatch_delta, trigger @pytest.fixture(scope='function') -def mycharm(): - class MyCharm(CharmBase): +def mycharm() -> type[ops.CharmBase]: + class MyCharm(ops.CharmBase): _call = None called = False - def __init__(self, framework: Framework): + def __init__(self, framework: ops.Framework): super().__init__(framework) for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: ops.EventBase): if MyCharm._call: MyCharm.called = True MyCharm._call(self, event) @@ -36,24 +33,22 @@ def _on_event(self, event): return MyCharm -def test_charm_heals_on_start(mycharm): - def pre_event(charm): - assert charm.unit.status == BlockedStatus('foo') - assert not charm.called +def test_charm_heals_on_start(mycharm: type[ops.CharmBase]): + def pre_event(charm: ops.CharmBase): + assert charm.unit.status == ops.BlockedStatus('foo') + assert not charm.called # type: ignore - def call(charm, _): + def call(charm: ops.CharmBase, _: ops.EventBase): if charm.unit.status.message == 'foo': - charm.unit.status = ActiveStatus('yabadoodle') + charm.unit.status = ops.ActiveStatus('yabadoodle') - def post_event(charm): - assert charm.unit.status == ActiveStatus('yabadoodle') - assert charm.called + def post_event(charm: ops.CharmBase): + assert charm.unit.status == ops.ActiveStatus('yabadoodle') + assert charm.called # type: ignore - mycharm._call = call + mycharm._call = call # type: ignore - initial_state = State( - config={'foo': 'bar'}, leader=True, unit_status=ScenarioBlockedStatus('foo') - ) + initial_state = State(config={'foo': 'bar'}, leader=True, unit_status=BlockedStatus('foo')) out = trigger( initial_state, @@ -65,7 +60,7 @@ def post_event(charm): pre_event=pre_event, ) - assert out.unit_status == ActiveStatus('yabadoodle') + assert out.unit_status == ops.ActiveStatus('yabadoodle') out_purged = dataclasses.replace(out, stored_states=initial_state.stored_states) assert jsonpatch_delta(out_purged, initial_state) == [ @@ -82,10 +77,10 @@ def post_event(charm): ] -def test_relation_data_access(mycharm): - mycharm._call = lambda *_: True +def test_relation_data_access(mycharm: type[ops.CharmBase]): + mycharm._call = lambda *_: True # type: ignore - def check_relation_data(charm): + def check_relation_data(charm: ops.CharmBase): foo_relations = charm.model.relations['relation_test'] assert len(foo_relations) == 1 foo_rel = foo_relations[0] diff --git a/testing/tests/test_e2e/test_resource.py b/testing/tests/test_e2e/test_resource.py index 0860d0411..cf1fbb279 100644 --- a/testing/tests/test_e2e/test_resource.py +++ b/testing/tests/test_e2e/test_resource.py @@ -12,7 +12,7 @@ class ResourceCharm(ops.CharmBase): - def __init__(self, framework): + def __init__(self, framework: ops.Framework): super().__init__(framework) diff --git a/testing/tests/test_e2e/test_state.py b/testing/tests/test_e2e/test_state.py index 500098d02..06553fee1 100644 --- a/testing/tests/test_e2e/test_state.py +++ b/testing/tests/test_e2e/test_state.py @@ -33,7 +33,6 @@ StoredState, SubordinateRelation, TCPPort, - _Event, _next_storage_index, layer_from_rockcraft, ) @@ -59,7 +58,7 @@ @pytest.fixture(scope='function') -def mycharm(): +def mycharm() -> type[CharmBase]: class MyCharmEvents(CharmEvents): @classmethod def define_event(cls, event_kind: str, event_type: type[EventBase]): @@ -68,16 +67,16 @@ def define_event(cls, event_kind: str, event_type: type[EventBase]): return super().define_event(event_kind, event_type) class MyCharm(CharmBase): - _call: Callable[[MyCharm, _Event], None] | None = None + _call: Callable[[EventBase], None] | None = None called = False - on = MyCharmEvents() + on = MyCharmEvents() # type: ignore def __init__(self, framework: Framework): super().__init__(framework) for evt in self.on.events().values(): self.framework.observe(evt, self._on_event) - def _on_event(self, event): + def _on_event(self, event: EventBase): if self._call: MyCharm.called = True self._call(event) @@ -86,11 +85,11 @@ def _on_event(self, event): @pytest.fixture -def state(): +def state() -> State: return State(config={'foo': 'bar'}, leader=True) -def test_bare_event(state, mycharm): +def test_bare_event(state: State, mycharm: type[CharmBase]): out = trigger( state, 'start', @@ -102,8 +101,8 @@ def test_bare_event(state, mycharm): assert jsonpatch_delta(state, out_purged) == [] -def test_leader_get(state, mycharm): - def pre_event(charm): +def test_leader_get(state: State, mycharm: type[CharmBase]): + def pre_event(charm: CharmBase): assert charm.unit.is_leader() trigger( @@ -116,8 +115,8 @@ def pre_event(charm): ) -def test_status_setting(state, mycharm): - def call(charm: CharmBase, e): +def test_status_setting(state: State, mycharm: type[CharmBase]): + def call(charm: CharmBase, e: EventBase): if isinstance(e, CollectStatusEvent): return @@ -125,7 +124,7 @@ def call(charm: CharmBase, e): charm.unit.status = ActiveStatus('foo test') charm.app.status = WaitingStatus('foo barz') - mycharm._call = call + mycharm._call = call # type: ignore out = trigger( state, 'start', @@ -148,7 +147,7 @@ def call(charm: CharmBase, e): @pytest.mark.parametrize('connect', (True, False)) -def test_container(connect, mycharm): +def test_container(connect: bool, mycharm: type[CharmBase]): def pre_event(charm: CharmBase): container = charm.unit.get_container('foo') assert container is not None @@ -167,7 +166,7 @@ def pre_event(charm: CharmBase): ) -def test_relation_get(mycharm): +def test_relation_get(mycharm: type[CharmBase]): def pre_event(charm: CharmBase): rel = charm.model.get_relation('foo') assert rel is not None @@ -209,8 +208,8 @@ def pre_event(charm: CharmBase): ) -def test_relation_set(mycharm): - def event_handler(charm: CharmBase, _): +def test_relation_set(mycharm: type[CharmBase]): + def event_handler(charm: CharmBase, _: EventBase): rel = charm.model.get_relation('foo') assert rel is not None rel.data[charm.app]['a'] = 'b' @@ -236,7 +235,7 @@ def pre_event(charm: CharmBase): # with pytest.raises(Exception): # rel.data[charm.model.get_unit("remote/1")]["c"] = "d" - mycharm._call = event_handler + mycharm._call = event_handler # type: ignore relation = Relation( endpoint='foo', interface='bar', @@ -249,7 +248,7 @@ def pre_event(charm: CharmBase): relations={relation}, ) - assert not mycharm.called + assert not mycharm.called # type: ignore out = trigger( state, event='start', @@ -260,7 +259,7 @@ def pre_event(charm: CharmBase): }, pre_event=pre_event, ) - assert mycharm.called + assert mycharm.called # type: ignore assert asdict(out.get_relation(relation.id)) == asdict( replace( @@ -299,7 +298,7 @@ def test_checkinfo_changeid(id: str | None): (Network, (0, 3)), ], ) -def test_positional_arguments(klass, num_args): +def test_positional_arguments(klass: type[Any], num_args: tuple[int, ...]): for num in num_args: args = (None,) * num with pytest.raises(TypeError): @@ -308,12 +307,12 @@ def test_positional_arguments(klass, num_args): def test_model_positional_arguments(): with pytest.raises(TypeError): - Model('', '') + Model('', '') # type: ignore def test_container_positional_arguments(): with pytest.raises(TypeError): - Container('', True) + Container('', True) # type: ignore def test_container_default_values(): @@ -478,8 +477,11 @@ def test_immutable_content_dict_of_dicts( (StoredState(), 'stored_states', 'get_stored_state', 'name'), ], ) -def test_state_immutable(obj_in, attribute: str, get_method: str, key_attr: str, mycharm): - state_in = State(**{attribute: obj_in if isinstance(obj_in, dict) else [obj_in]}) +def test_state_immutable( + obj_in: Any, attribute: str, get_method: str, key_attr: str, mycharm: type[CharmBase] +): + kwargs: dict[str, Any] = {attribute: obj_in if isinstance(obj_in, dict) else [obj_in]} + state_in = State(**kwargs) state_out: State = trigger( state_in, @@ -510,11 +512,13 @@ def test_state_immutable(obj_in, attribute: str, get_method: str, key_attr: str, elif attribute == 'secrets': # State.get_secret only takes keyword arguments, while the others take # only positional arguments. + assert isinstance(obj_in, Secret) obj_out = state_out.get_secret(id=obj_in.id) elif attribute == 'resources': # Charms can't change resources, so there's no State.get_resource. obj_out = next(r for r in state_out.resources if r == obj_in) else: + assert isinstance(obj_in, (RelationBase, Network, Container, Storage, StoredState)) obj_out = getattr(state_out, get_method)(getattr(obj_in, key_attr)) assert obj_in is not obj_out @@ -527,14 +531,16 @@ def test_state_immutable(obj_in, attribute: str, get_method: str, key_attr: str, SubordinateRelation, ], ) -def test_state_immutable_with_changed_data_relation(relation_type: type[RelationBase], mycharm): - def event_handler(charm: CharmBase, _): +def test_state_immutable_with_changed_data_relation( + relation_type: type[RelationBase], mycharm: type[CharmBase] +): + def event_handler(charm: CharmBase, _: EventBase): rel = charm.model.get_relation(relation_type.__name__) assert rel is not None rel.data[charm.app]['a'] = 'b' rel.data[charm.unit]['c'] = 'd' - mycharm._call = event_handler + mycharm._call = event_handler # type: ignore relation_in = relation_type(relation_type.__name__) @@ -560,7 +566,7 @@ def event_handler(charm: CharmBase, _): assert relation_out.local_unit_data == {'c': 'd', **_DEFAULT_JUJU_DATABAG} -def test_state_immutable_with_changed_data_container(mycharm): +def test_state_immutable_with_changed_data_container(mycharm: type[CharmBase]): layer_name = 'my-layer' layer = ops.pebble.Layer({ 'services': { @@ -571,11 +577,11 @@ def test_state_immutable_with_changed_data_container(mycharm): } }) - def event_handler(charm: CharmBase, _): + def event_handler(charm: CharmBase, _: EventBase): container = charm.model.unit.get_container('foo') container.add_layer(layer_name, layer, combine=True) - mycharm._call = event_handler + mycharm._call = event_handler # type: ignore container_in = Container('foo', can_connect=True) state_in = State(containers={container_in}) @@ -595,11 +601,11 @@ def event_handler(charm: CharmBase, _): assert container_out.layers == {layer_name: layer} -def test_state_immutable_with_changed_data_ports(mycharm): - def event_handler(charm: CharmBase, _): +def test_state_immutable_with_changed_data_ports(mycharm: type[CharmBase]): + def event_handler(charm: CharmBase, _: EventBase): charm.model.unit.open_port(protocol='tcp', port=80) - mycharm._call = event_handler + mycharm._call = event_handler # type: ignore state_in = State() state_out = trigger( @@ -613,12 +619,12 @@ def event_handler(charm: CharmBase, _): assert state_out.opened_ports == {TCPPort(80)} -def test_state_immutable_with_changed_data_secret(mycharm): - def event_handler(charm: CharmBase, _): +def test_state_immutable_with_changed_data_secret(mycharm: type[CharmBase]): + def event_handler(charm: CharmBase, _: EventBase): secret = charm.model.get_secret(label='my-secret') secret.set_content({'password': 'bar'}) - mycharm._call = event_handler + mycharm._call = event_handler # type: ignore secret_in = Secret({'password': 'foo'}, label='my-secret', owner='unit') state_in = State(secrets={secret_in}) @@ -782,9 +788,11 @@ def test_layer_from_rockcraft(rockcraft: dict[str, Any]): assert layer_check_level == check.get('level', ops.pebble.CheckLevel.UNSET.value) if 'exec' in check: assert layer_check.exec is not None and 'command' in check['exec'] + assert 'command' in layer_check.exec assert layer_check.exec['command'] == check['exec']['command'] if 'tcp' in check: assert layer_check.tcp is not None and 'port' in check['tcp'] + assert 'port' in layer_check.tcp assert layer_check.tcp['port'] == check['tcp']['port'] @@ -801,7 +809,7 @@ def test_layer_from_rockcraft_safe(): def test_state_from_context(): - meta = { + meta: dict[str, Any] = { 'name': 'sam', 'containers': {'container': {}}, 'peers': {'peer': {'interface': 'friend'}}, @@ -839,7 +847,7 @@ class Charm(ops.CharmBase): def test_state_from_context_extend(): - meta = { + meta: dict[str, Any] = { 'name': 'sam', 'containers': {'container': {}}, 'peers': {'peer': {'interface': 'friend'}}, @@ -880,7 +888,9 @@ class Charm(ops.CharmBase): assert state.get_relations('relreq')[0].interface == 'across' assert state.get_relations('relpro')[0].interface == 'around' assert state.get_relations('sub')[0].interface == 'below' - assert state.get_relation(relation.id).remote_app_data == {'a': 'b'} + retrieved_relation = state.get_relation(relation.id) + assert isinstance(retrieved_relation, Relation) + assert retrieved_relation.remote_app_data == {'a': 'b'} assert isinstance(state.storages, frozenset) assert len(state.storages) == 1 assert next(iter(state.storages)).name == 'storage' @@ -905,7 +915,7 @@ def test_state_from_context_merge_config(): [(Relation, 'relreq'), (PeerRelation, 'peer'), (SubordinateRelation, 'sub')], ) def test_state_from_context_skip_exiting_relation(rel_type: type[RelationBase], endpoint: str): - meta = { + meta: dict[str, Any] = { 'name': 'sam', 'peers': {'peer': {'interface': 'friend'}}, 'requires': { @@ -922,7 +932,7 @@ def test_state_from_context_skip_exiting_relation(rel_type: type[RelationBase], def test_state_from_context_skip_exiting_container(): - meta = { + meta: dict[str, Any] = { 'name': 'sam', 'containers': {'container': {}}, } @@ -937,7 +947,7 @@ def test_state_from_context_skip_exiting_container(): def test_state_from_context_skip_exiting_storage(): - meta = { + meta: dict[str, Any] = { 'name': 'sam', 'storage': {'storage': {}}, } @@ -957,7 +967,7 @@ def test_state_from_context_skip_exiting_stored_state(): class Charm(ops.CharmBase): _stored = ops.StoredState() - meta = { + meta: dict[str, Any] = { 'name': 'sam', } stored_state = StoredState(name='_stored', owner_path='Charm', content={'foo': 'bar'}) @@ -976,7 +986,7 @@ def _make_generator(items: Iterable[Any]) -> Generator[Any]: @pytest.mark.parametrize('iterable', [frozenset, tuple, list, _make_generator]) def test_state_from_non_sets(iterable: Callable[..., Any]): - meta = { + meta: dict[str, Any] = { 'name': 'sam', 'containers': {'container': {}}, 'peers': {'peer': {'interface': 'friend'}}, diff --git a/testing/tests/test_e2e/test_stored_state.py b/testing/tests/test_e2e/test_stored_state.py index 55ae55a9d..5436fd79a 100644 --- a/testing/tests/test_e2e/test_stored_state.py +++ b/testing/tests/test_e2e/test_stored_state.py @@ -10,18 +10,17 @@ from scenario.state import State, StoredState import ops -from ops.framework import StoredState as OpsStoredstate from tests.helpers import trigger @pytest.fixture(scope='function') -def mycharm(): +def mycharm() -> type[ops.CharmBase]: class MyCharm(ops.CharmBase): META: Mapping[str, Any] = {'name': 'mycharm'} _read: ClassVar[dict[str, Any]] = {} - _stored = OpsStoredstate() - _stored2 = OpsStoredstate() + _stored = ops.StoredState() + _stored2 = ops.StoredState() def __init__(self, framework: ops.Framework): super().__init__(framework) @@ -37,8 +36,8 @@ def _on_event(self, _: ops.EventBase): return MyCharm -def test_stored_state_default(mycharm): - out = trigger(State(), 'start', mycharm, meta=mycharm.META) +def test_stored_state_default(mycharm: type[ops.CharmBase]): + out = trigger(State(), 'start', mycharm, meta=mycharm.META) # type: ignore assert out.get_stored_state('_stored', owner_path='MyCharm').content == { 'foo': 'bar', 'baz': {12: 142}, @@ -49,7 +48,7 @@ def test_stored_state_default(mycharm): } -def test_stored_state_initialized(mycharm): +def test_stored_state_initialized(mycharm: type[ops.CharmBase]): out = trigger( State( stored_states={ @@ -58,7 +57,7 @@ def test_stored_state_initialized(mycharm): ), 'start', mycharm, - meta=mycharm.META, + meta=mycharm.META, # type: ignore ) assert out.get_stored_state('_stored', owner_path='MyCharm').content == { 'foo': 'FOOX', @@ -72,7 +71,7 @@ def test_stored_state_initialized(mycharm): def test_positional_arguments(): with pytest.raises(TypeError): - StoredState('_stored', '') + StoredState('_stored', '') # type: ignore def test_default_arguments(): diff --git a/testing/tests/test_e2e/test_vroot.py b/testing/tests/test_e2e/test_vroot.py index e5ee12a96..54eecc86d 100644 --- a/testing/tests/test_e2e/test_vroot.py +++ b/testing/tests/test_e2e/test_vroot.py @@ -4,7 +4,7 @@ from __future__ import annotations import tempfile -from collections.abc import Mapping +from collections.abc import Generator, Mapping from pathlib import Path from typing import Any @@ -31,7 +31,7 @@ def __init__(self, framework: Framework): @pytest.fixture -def charm_virtual_root(): +def charm_virtual_root() -> Generator[Path]: with tempfile.TemporaryDirectory() as mycharm_virtual_root: t = Path(mycharm_virtual_root) src = t / 'src' @@ -47,23 +47,23 @@ def charm_virtual_root(): yield t -def test_charm_virtual_root(charm_virtual_root): +def test_charm_virtual_root(charm_virtual_root: Path): out = trigger( State(), 'start', charm_type=MyCharm, - meta=MyCharm.META, + meta=dict(MyCharm.META), charm_root=charm_virtual_root, ) assert out.unit_status == ActiveStatus('hello world') -def test_charm_virtual_root_cleanup_if_exists(charm_virtual_root): +def test_charm_virtual_root_cleanup_if_exists(charm_virtual_root: Path): meta_file = charm_virtual_root / 'metadata.yaml' raw_ori_meta = yaml.safe_dump({'name': 'karl'}) meta_file.write_text(raw_ori_meta) - ctx = Context(MyCharm, meta=MyCharm.META, charm_root=charm_virtual_root) + ctx = Context(MyCharm, meta=dict(MyCharm.META), charm_root=charm_virtual_root) with ctx( ctx.on.start(), State(), @@ -79,12 +79,12 @@ def test_charm_virtual_root_cleanup_if_exists(charm_virtual_root): assert meta_file.exists() -def test_charm_virtual_root_cleanup_if_not_exists(charm_virtual_root): +def test_charm_virtual_root_cleanup_if_not_exists(charm_virtual_root: Path): meta_file = charm_virtual_root / 'metadata.yaml' assert not meta_file.exists() - ctx = Context(MyCharm, meta=MyCharm.META, charm_root=charm_virtual_root) + ctx = Context(MyCharm, meta=dict(MyCharm.META), charm_root=charm_virtual_root) with ctx( ctx.on.start(), State(),