Skip to content
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
1 change: 1 addition & 0 deletions testing/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ keywords = ["juju", "test"]
dependencies = [
"ops==3.5.0.dev0",
"PyYAML>=6.0.1",
"typing_extensions>=4.9.0", # for `deprecated` decorator, replace with stdlib `warnings.deprecated` when we're Python 3.13+
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warnings.deprecated is Python 3.13+. Luckily, typing_extensions has backported it, and type checkers respect the backport. This introduces a testing-only runtime dependency on typing_extensions (currently it's type checking only in ops), which I think is OK.

]
readme = "README.md"
requires-python = ">=3.10"
Expand Down
21 changes: 18 additions & 3 deletions testing/src/scenario/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Generic

from typing_extensions import deprecated

import ops
from ops._private.harness import ActionFailed

Expand Down Expand Up @@ -695,15 +697,15 @@ def __init__(
config=config,
)

self.charm_spec = spec
self._charm_spec = spec
self.charm_root = charm_root
self.juju_version = juju_version
if juju_version.split('.')[0] == '2':
logger.warning(
'Juju 2.x is closed and unsupported. You may encounter inconsistencies.',
)

self.app_name = app_name or self.charm_spec.meta.get('name')
self.app_name = app_name or self._charm_spec.meta.get('name')
self.unit_id = unit_id
self._machine_id = machine_id
self._availability_zone = availability_zone
Expand Down Expand Up @@ -736,6 +738,19 @@ def __init__(

self.on = CharmEvents()

@property
@deprecated('Use State.from_context to generate a State from charm metadata.', stacklevel=2)
def charm_spec(self):
"""Deprecated property for accessing the Context's charm specification.

The ``_CharmSpec`` class is private and intended for internal use only.
``Context.charm_spec`` will be removed in a future major version.

Consider accessing the charm class or metadata directly, or using
:meth:`State.from_context` to generate a ``State`` from your charm's metadata.
"""
return self._charm_spec

def _set_output_state(self, output_state: State):
"""Hook for Runtime to set the output state."""
self._output_state = output_state
Expand Down Expand Up @@ -853,7 +868,7 @@ def run(self, event: _Event, state: State) -> State:
def _run(self, event: _Event, state: State):
runtime = Runtime(
app_name=self.app_name,
charm_spec=self.charm_spec,
charm_spec=self._charm_spec,
juju_version=self.juju_version,
charm_root=self.charm_root,
unit_id=self.unit_id,
Expand Down
10 changes: 5 additions & 5 deletions testing/src/scenario/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1780,8 +1780,8 @@ def from_context(
Items that are found in the metadata and are also in the passed
arguments will be merged, with the passed values taking precedence.
"""
meta = ctx.charm_spec.meta
spec_config = ctx.charm_spec.config
meta = ctx._charm_spec.meta
spec_config = ctx._charm_spec.config
config = {} if config is None else config
if spec_config:
options = spec_config.get('options', {})
Expand Down Expand Up @@ -1811,10 +1811,10 @@ def from_context(
continue
storages.add(Storage(name=storage_name))
stored_states = set(stored_states or ())
for attr in dir(ctx.charm_spec.charm_type):
value = getattr(ctx.charm_spec.charm_type, attr)
for attr in dir(ctx._charm_spec.charm_type):
value = getattr(ctx._charm_spec.charm_type, attr)
if isinstance(value, ops.StoredState):
owner_path = ctx.charm_spec.charm_type.handle_kind
owner_path = ctx._charm_spec.charm_type.handle_kind
if any(ss.name == attr and ss.owner_path == owner_path for ss in stored_states):
continue
stored_states.add(StoredState(attr, owner_path=owner_path))
Expand Down
6 changes: 6 additions & 0 deletions testing/tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,9 @@ def _on_start(self, event):
ctx = Context(GoodCharm, meta={'name': 'crashy'})
ctx.run(ctx.on.start(), State())
assert 'TEST_ENV_VAR' not in os.environ


def test_charm_spec_is_deprecated():
ctx = Context(CharmBase, meta={'name': 'some-name'})
with pytest.warns(DeprecationWarning):
ctx.charm_spec # type: ignore
2 changes: 2 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading