Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
4ee7643
Initial implementation.
tonyandrewmeyer Mar 29, 2025
da8d265
Merge origin/main.
tonyandrewmeyer Mar 29, 2025
7f143cd
Tox is green, including docs.
tonyandrewmeyer Mar 30, 2025
739c20c
Add extra tests.
tonyandrewmeyer Mar 30, 2025
2c9bb9f
Tox is green again.
tonyandrewmeyer Mar 30, 2025
a1d4f74
Add a Raises section.
tonyandrewmeyer Mar 30, 2025
fac6c7c
Expand to-do list.
tonyandrewmeyer Mar 31, 2025
f155bf5
Adjustments after initial review.
tonyandrewmeyer Apr 24, 2025
ea10a4d
Provide pydantic for static checks.
tonyandrewmeyer Apr 24, 2025
bd2a2df
Changes from initial review.
tonyandrewmeyer Apr 24, 2025
eb2d6e1
Merge from main.
tonyandrewmeyer Apr 24, 2025
35b643a
Tweaks after merge.
tonyandrewmeyer Apr 24, 2025
441c68a
Add TODO.
tonyandrewmeyer Apr 24, 2025
0966028
Remove ignore used during dev.
tonyandrewmeyer Apr 28, 2025
29875b1
Add TODO note.
tonyandrewmeyer Apr 29, 2025
f13ed9d
Do the status set right at the end, so that it only happens if charms…
tonyandrewmeyer Apr 30, 2025
4320d97
Tweak comment.
tonyandrewmeyer Apr 30, 2025
84ba899
Use cleaner methods to get the set of fields.
tonyandrewmeyer Apr 30, 2025
b13c742
Only pass the config that the class defines. This makes it easier to …
tonyandrewmeyer Apr 30, 2025
30573f9
Provide lazy Secrets in the config - this means you get errors later …
tonyandrewmeyer Apr 30, 2025
172c692
With lazy Secrets, we no longer get errors with missing secrets/inval…
tonyandrewmeyer Apr 30, 2025
3ee1807
Clean up the attr-to-type code.
tonyandrewmeyer Apr 30, 2025
5f8db50
Add a test for the 'only load some config' case.
tonyandrewmeyer Apr 30, 2025
d932a97
Move TODO to a better location.
tonyandrewmeyer Apr 30, 2025
80dfa63
Merge origin/main.
tonyandrewmeyer Apr 30, 2025
197431a
Add tests for uncaught InvalidSchemaError.
tonyandrewmeyer Apr 30, 2025
d0b7e2e
Fix docs.
tonyandrewmeyer Apr 30, 2025
1c09153
Adjustments from spec review.
tonyandrewmeyer May 8, 2025
b716878
Merge origin/main.
tonyandrewmeyer May 8, 2025
97cd70a
Provide Python 3.8 support.
tonyandrewmeyer May 8, 2025
b3c4941
Apply suggestions from code review
tonyandrewmeyer May 20, 2025
3949ca7
Remove methods for customising attribute names, per review.
tonyandrewmeyer May 20, 2025
0b4a84d
Sort the field names for consistent output.
tonyandrewmeyer May 20, 2025
7f203a1
Move private methods from the class to the module, per review.
tonyandrewmeyer May 20, 2025
734b292
Provide a more explicit type, per review.
tonyandrewmeyer May 20, 2025
5b865f2
Get the default value via pydantic and dataclasses if needed.
tonyandrewmeyer May 21, 2025
ea24f94
Improve comment.
tonyandrewmeyer May 21, 2025
dc61cc9
Simplify the BaseModel case.
tonyandrewmeyer May 21, 2025
bf3b0bd
Use fields() per review.
tonyandrewmeyer May 21, 2025
703938c
Update ops/_private/attrdocs.py
tonyandrewmeyer May 21, 2025
9d614ea
Update ops/charm.py
tonyandrewmeyer May 21, 2025
b929496
Add comment so that when 3.8 support is dropped this is found.
tonyandrewmeyer May 21, 2025
832a6d1
Minor tweak per review.
tonyandrewmeyer May 21, 2025
4294b29
Remove the custom exception.
tonyandrewmeyer May 21, 2025
f4ebc54
Reword docstring.
tonyandrewmeyer May 21, 2025
6248c2c
Merge origin/main
tonyandrewmeyer Jul 7, 2025
86a85d9
Tidy up after merge.
tonyandrewmeyer Jul 7, 2025
d1f1113
Small cleanup.
tonyandrewmeyer Jul 7, 2025
ead796e
Merge origin/main.
tonyandrewmeyer Jul 21, 2025
dac0582
Shuffle things around.
tonyandrewmeyer Aug 8, 2025
48352d6
Merge origin/main.
tonyandrewmeyer Aug 8, 2025
b8986d6
More progress.
tonyandrewmeyer Aug 8, 2025
8659adb
Remaining cleanup.
tonyandrewmeyer Aug 11, 2025
8263595
Add a script that updates charmcraft.yaml.
tonyandrewmeyer Aug 11, 2025
d228ac9
Merge origin/main.
tonyandrewmeyer Aug 11, 2025
76dd056
Add support for releasing ops-tools.
tonyandrewmeyer Aug 11, 2025
9c72564
Remove ignored files.
tonyandrewmeyer Aug 11, 2025
36c3b64
Use pydantic for examples.
tonyandrewmeyer Aug 11, 2025
28b7a47
Fix stripping 'action' from the class name.
tonyandrewmeyer Aug 11, 2025
92880d4
No need to have tools installed for the integration tests at this point.
tonyandrewmeyer Aug 11, 2025
951d91c
Make it more convenient to pass in the modules and classes.
tonyandrewmeyer Aug 11, 2025
bb9e760
Improve the readme with an example to run.
tonyandrewmeyer Aug 11, 2025
3a94b7d
Tweaks from the old review.
tonyandrewmeyer Aug 11, 2025
db5e7c8
Fix docs.
tonyandrewmeyer Aug 11, 2025
8db8ee8
Fix the order of moving through the classes, add an explicit test for…
tonyandrewmeyer Aug 14, 2025
eaa7476
Merge origin/main
tonyandrewmeyer Aug 14, 2025
b901a0a
Make the entry point name clearer.
tonyandrewmeyer Aug 14, 2025
335c01c
Allow merging rather than replacing.
tonyandrewmeyer Aug 14, 2025
71d14d6
Add a diff method.
tonyandrewmeyer Aug 14, 2025
a5b5dc5
WiP doctests.
tonyandrewmeyer Aug 15, 2025
b2efed5
Edit the charmcraft.yaml in a way that preserves comments and ordering.
tonyandrewmeyer Aug 18, 2025
b485a50
Update for newer pyright.
tonyandrewmeyer Aug 18, 2025
47e425b
Merge origin/main.
tonyandrewmeyer Aug 18, 2025
f888063
Fix order of output.
tonyandrewmeyer Aug 19, 2025
36aa0e0
Update tools/README.md
tonyandrewmeyer Aug 27, 2025
9044d61
Apply suggestions from code review
tonyandrewmeyer Aug 27, 2025
89cac58
Tweak args, per review.
tonyandrewmeyer Aug 27, 2025
851956c
Merge origin/main.
tonyandrewmeyer Aug 27, 2025
181ce73
Apply suggestions from code review
tonyandrewmeyer Aug 27, 2025
3d26c4b
Apply suggestions from code review
tonyandrewmeyer Sep 3, 2025
223ed26
Remove the --merge and --diff options.
tonyandrewmeyer Sep 4, 2025
7f99709
Remove unnecessary comment.
tonyandrewmeyer Sep 4, 2025
04631c2
Include tools in coverage.
tonyandrewmeyer Sep 4, 2025
d670d04
Tweak comment, per review.
tonyandrewmeyer Sep 4, 2025
b6a8e58
Minor readability improvement, per review.
tonyandrewmeyer Sep 4, 2025
0dfbfa2
Minor readability improvement, per review.
tonyandrewmeyer Sep 4, 2025
10d06ef
Minor refactor, per review.
tonyandrewmeyer Sep 5, 2025
f3ef160
Minor refactor, per review.
tonyandrewmeyer Sep 5, 2025
d462027
Handle lists and tuples in config.
tonyandrewmeyer Sep 5, 2025
b89c2b5
Align the behaviour of juju_names exactly with Relation.save().
tonyandrewmeyer Sep 7, 2025
f7c1747
Merge origin/main.
tonyandrewmeyer Feb 8, 2026
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ coverage.xml
.coverage.data
/.tox
.*.swp
.ruff_cache
.claude/settings.local.json

# Tokens and settings for `act` to run GHA locally
Expand All @@ -28,6 +29,9 @@ coverage.xml
ops_scenario.egg-info
/testing/build/
/testing/dist/
ops_tools.egg-info
/tools/build/
/tools/dist/

# Smoke test artifacts
*.tar.gz
Expand Down
11 changes: 11 additions & 0 deletions .sbomber-manifest-sdist.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,14 @@ artifacts:
name: 'ops-tracing'
version: ''
channel: 'stable'
cycle: '25.10'

- name: 'ops-tools'
type: 'sdist'
version: ''
compression: 'gz'
ssdlc_params:
name: 'ops-tools'
version: ''
channel: 'stable'
cycle: '25.10'
9 changes: 9 additions & 0 deletions .sbomber-manifest-wheel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,12 @@ artifacts:
name: 'ops-tracing'
version: ''
channel: 'stable'
cycle: '25.10'

- name: 'ops-tools'
type: 'wheel'
ssdlc_params:
name: 'ops-tools'
version: ''
channel: 'stable'
cycle: '25.10'
5 changes: 3 additions & 2 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,9 @@ Then, check out the main branch of your forked operator repo and pull upstream t

> Pushing the tags will trigger automatic builds for the Python packages and
> publish them to PyPI ([ops](https://pypi.org/project/ops/)
> ,[ops-scenario](https://pypi.org/project/ops-scenario), and
> [ops-tracing](https://pypi.org/project/ops-tracing/)).
> ,[ops-scenario](https://pypi.org/project/ops-scenario),
> [ops-tracing](https://pypi.org/project/ops-tracing/)), and
> [ops-tools](https://pypi.org/project/ops-tools/)).
> Note that it sometimes take a bit of time for the new releases to show up.
>
> See [.github/workflows/publish.yaml](.github/workflows/publish.yaml) for details.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ An API for tracing charm code and sending data to sources such as the [Canonical
:maxdepth: 1

ops-tracing
ops-tools
```

## Hook commands
Expand Down
7 changes: 7 additions & 0 deletions docs/reference/ops-tools.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.. _ops_tools:

`ops_tools`
===========

.. automodule:: ops_tools
:exclude-members: main
21 changes: 18 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,18 @@ lint = [
"pyright~=1.1",
"typing_extensions~=4.2",
]
docs = [
"ops[testing,tracing]",
"ops-tools",
"canonical-sphinx[full]",
"packaging",
"sphinxcontrib-svg2pdfconverter[CairoSVG]",
"sphinx-last-updated-by-git",
"sphinx-sitemap~=2.8",
]
unit = [
"ops[testing,tracing]",
"ops-tools",
"pytest~=8.4",
"jsonpatch~=1.33",
"pydantic~=2.10",
Expand Down Expand Up @@ -86,12 +96,13 @@ requires = [
build-backend = "setuptools.build_meta"

[tool.uv.workspace]
members = ["tracing", "testing"]
members = ["tracing", "testing", "tools"]

[tool.uv.sources]
ops = { workspace = true }
ops-scenario = { workspace = true }
ops-tracing = { workspace = true }
ops-tools = { workspace = true }

[tool.setuptools.packages.find]
include = ["ops", "ops._private", "ops.lib"]
Expand Down Expand Up @@ -227,6 +238,10 @@ exclude = ["tracing/ops_tracing/vendor/*"]
# All documentation linting.
"D",
]
"tools/tests_tools/*" = [
# All documentation linting.
"D",
]
"ops/_private/timeconv.py" = [
"RUF001", # String contains ambiguous `µ` (MICRO SIGN). Did you mean `μ` (GREEK SMALL LETTER MU)?
"RUF002", # Docstring contains ambiguous `µ` (MICRO SIGN). Did you mean `μ` (GREEK SMALL LETTER MU)?
Expand Down Expand Up @@ -254,9 +269,9 @@ 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"]
include = ["ops/*.py", "ops/_private/*.py", "test/*.py", "test/charms/*/src/*.py", "testing/src/*.py", "tools/src/*.py", "tools/tests_tools/*.py"]
exclude = ["tracing/*"]
extraPaths = ["testing", "tracing"]
extraPaths = ["testing", "tracing", "tools"]
pythonVersion = "3.10" # check no python > 3.10 features are used
pythonPlatform = "All"
typeCheckingMode = "strict"
Expand Down
10 changes: 10 additions & 0 deletions release.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'ops/src': pathlib.Path('ops/version.py'),
'ops/pyproject': pathlib.Path('pyproject.toml'),
'testing': pathlib.Path('testing/pyproject.toml'),
'tools': pathlib.Path('tools/pyproject.toml'),
'tracing': pathlib.Path('tracing/pyproject.toml'),
'uvlock': pathlib.Path('uv.lock'),
'versions_doc': pathlib.Path('docs/explanation/versions.md'),
Expand Down Expand Up @@ -380,6 +381,13 @@ def update_tracing_version(ops_version: str):
update_pyproject_versions(VERSION_FILES['tracing'], ops_version, deps={'ops': ops_version})


def update_tools_version(ops_version: str):
"""Update the tools pyproject version."""
major, rest = ops_version.split('.', 1)
tools_version = f'{int(major) - 2}.{rest}'
update_pyproject_versions(VERSION_FILES['tools'], tools_version, deps={})


def update_versions_doc(version: str):
"""Update the Ops version table in docs/explanation/versions.md.

Expand Down Expand Up @@ -463,6 +471,7 @@ def update_versions_for_release(tag: str):
update_ops_version(tag, scenario_version)
update_testing_version(tag, scenario_version)
update_tracing_version(tag)
update_tools_version(tag)
update_versions_doc(tag)
update_uv_lock()

Expand Down Expand Up @@ -503,6 +512,7 @@ def update_versions_for_post_release(branch_name: str):
update_ops_version(ops_version, scenario_version)
update_testing_version(ops_version, scenario_version)
update_tracing_version(ops_version)
update_tools_version(ops_version)
update_uv_lock()


Expand Down
1 change: 1 addition & 0 deletions test/charms/test_main/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
# during unit tests, and test_main failures that subprocess out are often
# difficult to debug. Uncomment this line to get more informative errors when
# running the tests.
# When uncommented the test_hook_and_dispatch_with_failing_hook test will fail.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit could be perhaps written as "test expected to fail if uncommented".

# logger.addHandler(logging.StreamHandler(sys.stderr))


Expand Down
27 changes: 27 additions & 0 deletions testing/tests/test_e2e/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

from __future__ import annotations

import dataclasses

import ops_tools
import pytest
from scenario import Context
from scenario.state import State, _Action, _next_action_id
Expand Down Expand Up @@ -223,3 +226,27 @@ def test_default_arguments():
assert action.name == name
assert action.params == {}
assert action.id == expected_id


def test_action_using_generated_action():
@dataclasses.dataclass
class Act:
a: int
b: float
c: str

class Charm(CharmBase):
def __init__(self, framework: Framework):
super().__init__(framework)
framework.observe(self.on['act'].action, self._on_action)

def _on_action(self, event: ActionEvent):
self.typed_params = event.load_params(Act, 10, c='foo')

schema = ops_tools.action_to_juju_schema(Act)
ctx = Context(Charm, meta={'name': 'foo'}, actions=schema)
with ctx(ctx.on.action('act', params={'b': 3.14}), State()) as mgr:
mgr.run()
assert mgr.charm.typed_params.a == 10
assert mgr.charm.typed_params.b == 3.14
assert mgr.charm.typed_params.c == 'foo'
47 changes: 37 additions & 10 deletions testing/tests/test_e2e/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@

from __future__ import annotations

import dataclasses

import ops_tools
import pytest
from scenario.context import Context
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):
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)
framework.observe(evt, self._on_event)

def _on_event(self, event):
pass
Expand All @@ -27,7 +30,7 @@ def _on_event(self, event):


def test_config_get(mycharm):
def check_cfg(charm: CharmBase):
def check_cfg(charm: ops.CharmBase):
assert charm.config['foo'] == 'bar'
assert charm.config['baz'] == 1

Expand All @@ -44,7 +47,7 @@ def check_cfg(charm: CharmBase):


def test_config_get_default_from_meta(mycharm):
def check_cfg(charm: CharmBase):
def check_cfg(charm: ops.CharmBase):
assert charm.config['foo'] == 'bar'
assert charm.config['baz'] == 2
assert charm.config['qux'] is False
Expand Down Expand Up @@ -76,11 +79,11 @@ def check_cfg(charm: CharmBase):
),
)
def test_config_in_not_mutated(mycharm, cfg_in):
class MyCharm(CharmBase):
def __init__(self, framework: Framework):
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)
framework.observe(evt, self._on_event)

def _on_event(self, event):
# access the config to trigger a config-get
Expand All @@ -105,3 +108,27 @@ def _on_event(self, event):
)
# check config was not mutated by scenario
assert state_out.config == cfg_in


def test_config_using_generated_config():
@dataclasses.dataclass
class Config:
a: int
b: float
c: str

class Charm(ops.CharmBase):
def __init__(self, framework: ops.Framework):
super().__init__(framework)
framework.observe(self.on.config_changed, self._on_config_changed)

def _on_config_changed(self, event: ops.ConfigChangedEvent):
self.typed_config = self.load_config(Config, 10, c='foo')
Copy link
Contributor

Choose a reason for hiding this comment

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

Question this is unexpected to me.

I would have thought that default are specified in a model, not in the load config call site.

The defaults can be included in both dataclasses and pydantic models, so why add the extra arguments feature in the load_config method?

P.S. maybe I was lazy reading the spec, but somehow I don't recall this API 🙈

P.P.S. I guess it's because Juju action parameters don't have defaults like Juju config does, isn't it?

That also means that we should be testing both required and non-required parameters somewhere... perhaps a negative test in Scenario?


schema = ops_tools.config_to_juju_schema(Config)
ctx = Context(Charm, meta={'name': 'foo'}, config=schema)
with ctx(ctx.on.config_changed(), State(config={'b': 3.14})) as mgr:
mgr.run()
assert mgr.charm.typed_config.a == 10
assert mgr.charm.typed_config.b == 3.14
assert mgr.charm.typed_config.c == 'foo'
Loading
Loading