Skip to content

feat: support charmcraft.yaml format as meta for testing.Context#2296

Merged
james-garner-canonical merged 30 commits intocanonical:mainfrom
james-garner-canonical:26-01+feat+support-charmcraft-yaml-as-meta-for-testing-context
Mar 19, 2026
Merged

feat: support charmcraft.yaml format as meta for testing.Context#2296
james-garner-canonical merged 30 commits intocanonical:mainfrom
james-garner-canonical:26-01+feat+support-charmcraft-yaml-as-meta-for-testing-context

Conversation

@james-garner-canonical
Copy link
Copy Markdown
Contributor

@james-garner-canonical james-garner-canonical commented Jan 30, 2026

This PR adds support for providing charmcraft.yaml formatted data to the meta argument of testing.Context.

It is an error to specify actions or config both as separate arguments and as keys in meta.

Resolves #1424

@james-garner-canonical james-garner-canonical force-pushed the 26-01+feat+support-charmcraft-yaml-as-meta-for-testing-context branch from 6b900f1 to 488c5fa Compare February 5, 2026 23:47
@james-garner-canonical james-garner-canonical marked this pull request as ready for review February 6, 2026 03:02
Copy link
Copy Markdown
Contributor

@dimaqq dimaqq left a comment

Choose a reason for hiding this comment

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

Looks good!

james-garner-canonical and others added 12 commits February 13, 2026 12:29
Co-authored-by: Dima Tisnek <dimaqq@gmail.com>
This PR drops Scenario's build dependency on
[setuptools_scm](https://pypi.org/project/setuptools-scm/), which we
don't need, as it's for versioning a package based on git
tags. This resolves the current build issue, example
identified by David
[here](https://github.com/canonical/operator/actions/runs/21809969678/job/62920050703#step:5:22).
The build issue can be reproduced locally on `main` with `uv build
--all`. I've verified that the build succeeds with `setuptools_scm`
removed. The root cause of why builds started failing isn't clear
though.
The Juju `credential-get` hook command is available for K8s models as of
Juju 3.6.10, so adjust Scenario's consistency checker to match.

Fixes canonical#2304
This is an automated PR to update the best practices documentation.

Co-authored-by: David Wilding <david.wilding@canonical.com>
This PR prepares the release of version 3.5.2.

---------

Co-authored-by: Tony Meyer <tony.meyer@gmail.com>
This PR updates the version files after the release.
Bumps `cryptography` from 46.0.2 to 46.0.5 to address [this security
issue](https://github.com/canonical/operator/security/dependabot/23).

The package is used by `pyjwt`, which is used by the release script, so
we aren't really exposed by this issue, but it's simple to pull in the
latest fixes and clear the security warning.
The 2.23 maintenance branch does not have the versions.md doc. Handle
that when releasing.
This PR splits [How to run workloads with a Kubernetes
charm](https://documentation.ubuntu.com/ops/latest/howto/run-workloads-with-a-charm-kubernetes/)
into a subcategory of the how-to guides. I've redistributed all the
content without making significant changes. We can do further
improvements in follow-on PRs.

Main parts to focus on reviewing:

- The introductory descriptions on [Manage
containers](https://canonical-ubuntu-documentation-library--2309.com.readthedocs.build/ops/2309/howto/manage-containers/)
and the scope of each page in that subcategory. I moved "Manage Pebble
metrics" into this subcategory because I feel it fits better here,
rather than as a top-level how-to guide (this move isn't in the plan I
prepared internally).

- The same descriptions at [How-to guides > Managing
containers](https://canonical-ubuntu-documentation-library--2309.com.readthedocs.build/ops/2309/howto/#managing-containers).
I'm copying the approach of "Legacy guides" - listing each of the pages
in the subcategory instead of only listing the subcategory itself. I'm
open to changing the approach if we don't like the duplication.

I also fixed `test_add_layer()` in [How-to guides > Manage the workload
container > Write unit
tests](https://canonical-ubuntu-documentation-library--2309.com.readthedocs.build/ops/2309/howto/manage-containers/manage-the-workload-container/#write-unit-tests).
The test previously defined `container` but used `container_in`. I've
switched the test to use `container_in` and `container_out` for clarity.
…nical#2315)

Fixes canonical#2312 

When `ops.hookcmds` has landed, we forgot to add the submodule to the
setuptools config in pyproject.toml and now we get warnings. The PR
addresses that.
As decided at the standup, bumping the default Juju version in Scenario
Context to the version of the current Juju LTS, 3.6.14.
Merge branch 'main' into 26-01+feat+support-charmcraft-yaml-as-meta-for-testing-context
Copy link
Copy Markdown
Contributor

@dimaqq dimaqq left a comment

Choose a reason for hiding this comment

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

Let's discuss 3x deepcopy.

Copy link
Copy Markdown
Collaborator

@tonyandrewmeyer tonyandrewmeyer left a comment

Choose a reason for hiding this comment

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

I feel like it may be better to raise an error if there's an actions/config dict provided both in the meta argument and separately, rather than let one of them 'win'.

I wonder if we should add an example somewhere in the documentation (maybe in one of the library docs that isn't going to be moved to charmlibs?). In most cases, charms are ideally never using this and always auto-spec'ing. However, if they do use it, I would rather we encourage:

ctx = Context(MyCharm, charmcraft_dict)

rather than

ctx = Context(MyCharm, meta=charmcraft_dict)

And reserve the meta= keyword passing for when it's specifically the metadata dict.

I haven't tried running this since it's unclear what the exact implementation of the core piece will be, but I'm happy to approve trusting that it'll work at that point.


def test_init_with_bad_meta():
ctx = Context(MyCharm, meta={'a truth universally acknowledged': 'it'})
with pytest.raises((UncaughtCharmError, KeyError)):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

With #2314 coming, it's probably easier to preemptively use that approach here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TBH I think this is lower weight than adding monkeypatch and using monkeypatch.setenv.

@dimaqq
Copy link
Copy Markdown
Contributor

dimaqq commented Mar 2, 2026

I feel like it may be better to raise an error if there's an actions/config dict provided both in the meta argument and separately, rather than let one of them 'win'.

Let's debate and make a decision on this.

@pytest.fixture
def secrets_context(secrets_charm_meta: dict[str, Any]):
return Context(SecretsCharm, meta=secrets_charm_meta, actions=secrets_charm_meta['actions'])
return Context(SecretsCharm, meta=secrets_charm_meta)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Making it an error to pass actions both as a separate argument and as an entry in meta actually broke this fixture. It loads secrets_charm_meta from a charmcraft.yaml file, and passes ['actions'] explicitly (since it was written before this PR :)).

I think this is a sign this change might be too breaking.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we try it across our inventory of charms?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea, I'm running it now, but I'm not confident in my ability to actually detect new failures from the output. Would you mind running it too?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I get the same number of successes with the branch, so it seems safe to me.

However, I'm open to making it a warning (maybe a deprecation warning?) instead if you think it's better to be safe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since this doesn't break any charm tests we can detect, let's keep it as an error.

@james-garner-canonical
Copy link
Copy Markdown
Contributor Author

Requesting re-review as things have changed significantly:

  • deepcopy change landed separately, so this PR builds on that
  • We explicitly error if actions or config are passed in meta and separately at the same time -- but this actually broke some of our tests, so I wonder if this is too breaking a change.
    • We could let the separate argument take precedence like it does in main, and perhaps warn in this case.
    • Or try a gentler breaking change, like making it an error if they're passed separately and aren't identical.

Copy link
Copy Markdown
Collaborator

@tonyandrewmeyer tonyandrewmeyer left a comment

Choose a reason for hiding this comment

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

I'm fine with this, although I agree that there's some risk and I think we should investigate more what the impact might be. Perhaps we should only be warning when there's a collision.

@pytest.fixture
def secrets_context(secrets_charm_meta: dict[str, Any]):
return Context(SecretsCharm, meta=secrets_charm_meta, actions=secrets_charm_meta['actions'])
return Context(SecretsCharm, meta=secrets_charm_meta)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we try it across our inventory of charms?

Comment on lines +699 to +706
meta = copy.deepcopy(meta) if meta is not None else {'name': str(charm_type.__name__)}
if actions is not None and 'actions' in meta:
raise ValueError('Cannot specify actions in both charmcraft.yaml and separately')
actions = copy.deepcopy(actions) if actions is not None else meta.pop('actions', None)
if config is not None and 'config' in meta:
raise ValueError('Cannot specify config in both charmcraft.yaml and separately')
config = copy.deepcopy(config) if config is not None else meta.pop('config', None)
spec = _CharmSpec(charm_type=charm_type, meta=meta, actions=actions, config=config)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Freel free to push back on this; it's pretty subjective. I don't find this very readable.

Some of it maybe ruff pushes us into, like always using the ternary operator? If so then I would say that the consistency gained through tooling-enforced rules wins out.

Maybe a comment would help? Maybe if it was in a loop? (but it's only two items so ...)

I find my argument weakening by not being able to immediately see how I would write a nicer version of this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe comments to divide into logical sections and explain what's up? Maybe flipping the ternary to use is rather than is not?

            # Metadata: may be in charmcraft.yaml format
            meta = {'name': str(charm_type.__name__)} if meta is None else copy.deepcopy(meta)
            # Actions: it is an error if actions is specified both separately and in charmcraft.yaml
            if actions is not None and 'actions' in meta:
                raise ValueError('Cannot specify actions in both charmcraft.yaml and separately')
            actions = meta.pop('actions', None) if actions is None else copy.deepcopy(actions)
            # Config: it is an error if config is specified both separately and in charmcraft.yaml
            if config is not None and 'config' in meta:
                raise ValueError('Cannot specify config in both charmcraft.yaml and separately')
            config = meta.pop('config', None) if config is None else copy.deepcopy(config)
            # Charm spec from user specified charm type and metadata
            spec = _CharmSpec(charm_type=charm_type, meta=meta, actions=actions, config=config)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe:

            meta = copy.deepcopy(meta) if meta else {'name': str(charm_type.__name__)}
            # actions
            if actions is None:
                actions = meta.pop('actions', None)
            elif 'actions' in meta:
                raise ValueError('Cannot specify actions in both charmcraft.yaml and separately')
            else:
                actions = copy.deepcopy(actions)
            # config
            if config is None:
                config = meta.pop('config', None)
            elif 'config' in meta:
                raise ValueError('Cannot specify config in both charmcraft.yaml and separately')
            else:
                config = copy.deepcopy(config)
            spec = _CharmSpec(charm_type=charm_type, meta=meta, actions=actions, config=config)

Merge branch 'main' into 26-01+feat+support-charmcraft-yaml-as-meta-for-testing-context
@dimaqq
Copy link
Copy Markdown
Contributor

dimaqq commented Mar 18, 2026

Sorry I lost track about what we decided for copying stuff exactly (same pr? separate? specific copies? whack-all deepcopy?).
Will put this on discussion tomorrow.

@james-garner-canonical
Copy link
Copy Markdown
Contributor Author

Sorry I lost track about what we decided for copying stuff exactly (same pr? separate? specific copies? whack-all deepcopy?). Will put this on discussion tomorrow.

We decided on doing the deep copies in:

This PR doesn't introduce any new copying, just pulls 'config' and 'actions' out of meta.

actions=copy.deepcopy(actions),
config=copy.deepcopy(config),
)
meta = copy.deepcopy(meta) if meta is not None else {'name': str(charm_type.__name__)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a subtle change for the case of explicit meta={} and supplying a non-empty actions or config.

I'm not sure why someone would do that, so I think it's OK.

I wonder if the code on line 691 should be changed to:

if not any(x is not None for x in (meta, actions, config)):

Though against, I've no clue why someone may override action or config with an empty dict (no actions or no config), and yet supply a not-None, yet-empty meta.

So maybe current code is fine?
Please double-check my logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right about the subtle behaviour change, and I'm not 100% sure why I made the decision to test meta for None instead of falsiness other than perhaps for symmetry with config and actions. I'll revert to the original behaviour for meta.

@james-garner-canonical james-garner-canonical merged commit 64dde25 into canonical:main Mar 19, 2026
59 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for passing charmcraft.yaml as metadata to testing.Context

5 participants