feat: support charmcraft.yaml format as meta for testing.Context#2296
Conversation
6b900f1 to
488c5fa
Compare
|
|
||
|
|
||
| CONFIG: Mapping[str, Any] = { | ||
| 'options': {'perambulator-abbreviation': {'type': 'string', 'default': 'pram'}} |
| :arg charm_type: the :class:`ops.CharmBase` subclass to handle the event. | ||
| :arg meta: charm metadata to use. Needs to be a valid metadata.yaml format (as a dict). | ||
| If none is provided, we will search for a ``metadata.yaml`` file in the charm root. | ||
| Alternatively, can be a full ``charmcraft.yaml`` dict, in which case ``config`` and |
There was a problem hiding this comment.
The docstring update makes sense.
I wonder if there are some other doc places to update though.
Would you mind going through sections like "how to write unit tests" to see if something can be simplified with the new way?
There was a problem hiding this comment.
Our how to guides assume these arguments are left empty and the data is searched for in the appropriate files in the charm root.
There was a problem hiding this comment.
I was thinking about examples like:
Context(charm_type=MyCharmType, meta={'name': 'my-charm-name'})
That alone seems fine, but if there was meta=..., config={...}, then we'd probably want to update that?
There was a problem hiding this comment.
None of our examples appear to use both meta and config, and it seems that none of them use actions. For the ones that only use config, I think leaving them as config={...} is nicer than changing to. meta={'config': {...}}.
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
| meta = copy.deepcopy(meta) | ||
| config = copy.deepcopy(config) | ||
| actions = copy.deepcopy(actions) |
There was a problem hiding this comment.
Given that we only pop two top-level keys from meta, why deepcopy?
Also, why copy config and actions, these don't seem to be modified.
There was a problem hiding this comment.
My thinking is that since we definitely need a copy if we're going to pop, why not go all the way and only hold onto copies, preventing any future errors or unintended interactions stemming from the same underlying issue.
There was a problem hiding this comment.
So it's a drive-by change.
I think a stronger justification is needed.
There was a problem hiding this comment.
Holding onto an internal reference to the actual dictionary object passed by the caller makes both user code and our internal code error prone. This is highlighted by the fact that it's unsafe for us to pop from it as-is. It's true that we could try to preserve the problem in this PR, but why?
There was a problem hiding this comment.
I agree that it would be cleaner if we broke the link, whether that's by a deep copy or other means (probably it has to be a deep copy at some point, since we use it for things like network-get mocking). However, I also agree that it doesn't really fit with this PR.
My suggestion is that we have a separate PR that does do a deep copy at this point in the code (I think a deep copy is probably simpler and more correct than any of the other options), merge that, and then do this PR.
dimaqq
left a comment
There was a problem hiding this comment.
Let's discuss 3x deepcopy.
| meta = copy.deepcopy(meta) | ||
| config = copy.deepcopy(config) | ||
| actions = copy.deepcopy(actions) | ||
| # Extract config and actions from meta if it contains them (e.g., charmcraft.yaml) | ||
| # and they were not explicitly provided | ||
| if meta is not None: | ||
| meta_config = meta.pop('config', None) | ||
| if config is None: | ||
| config = meta_config | ||
| meta_actions = meta.pop('actions', None) | ||
| if actions is None: | ||
| actions = meta_actions | ||
|
|
There was a problem hiding this comment.
Option 1:
| meta = copy.deepcopy(meta) | |
| config = copy.deepcopy(config) | |
| actions = copy.deepcopy(actions) | |
| # Extract config and actions from meta if it contains them (e.g., charmcraft.yaml) | |
| # and they were not explicitly provided | |
| if meta is not None: | |
| meta_config = meta.pop('config', None) | |
| if config is None: | |
| config = meta_config | |
| meta_actions = meta.pop('actions', None) | |
| if actions is None: | |
| actions = meta_actions | |
| # Extract config and actions from meta if it contains them (e.g., charmcraft.yaml) | |
| # and they were not explicitly provided | |
| if meta is not None: | |
| meta = {**meta} | |
| meta_config = meta.pop('config', None) | |
| if config is None: | |
| config = meta_config | |
| meta_actions = meta.pop('actions', None) | |
| if actions is None: | |
| actions = meta_actions | |
Option 2:
| meta = copy.deepcopy(meta) | |
| config = copy.deepcopy(config) | |
| actions = copy.deepcopy(actions) | |
| # Extract config and actions from meta if it contains them (e.g., charmcraft.yaml) | |
| # and they were not explicitly provided | |
| if meta is not None: | |
| meta_config = meta.pop('config', None) | |
| if config is None: | |
| config = meta_config | |
| meta_actions = meta.pop('actions', None) | |
| if actions is None: | |
| actions = meta_actions | |
| # Extract config and actions from meta if it contains them (e.g., charmcraft.yaml) | |
| # and they were not explicitly provided | |
| if meta is not None: | |
| meta_config = meta.get('config') | |
| meta_actions = meta.get('actions') | |
| # Optional, I think CharmSpec should be doing this instead: | |
| meta = {k: v for k, v in meta.items if k not in ("config", "actions")} | |
| if config is None: | |
| config = meta_config | |
| if actions is None: | |
| actions = meta_actions | |
Option 3, if CharmSpec does the "right thing":
| meta = copy.deepcopy(meta) | |
| config = copy.deepcopy(config) | |
| actions = copy.deepcopy(actions) | |
| # Extract config and actions from meta if it contains them (e.g., charmcraft.yaml) | |
| # and they were not explicitly provided | |
| if meta is not None: | |
| meta_config = meta.pop('config', None) | |
| if config is None: | |
| config = meta_config | |
| meta_actions = meta.pop('actions', None) | |
| if actions is None: | |
| actions = meta_actions | |
| # Extract config and actions from meta if it contains them (e.g., charmcraft.yaml) | |
| # and they were not explicitly provided | |
| if meta is not None: | |
| if config is None: | |
| config = meta.get('config') | |
| if actions is None: | |
| actions = meta.get('actions') | |
Doesn't that look so much simpler?
There was a problem hiding this comment.
_CharmSpec just holds onto references to the three dictionaries.
The dicts are then used via _CharmSpec by the Scenario machinery to check consistency, write the metadata files to the virtual charm root, etc., or by user-facing methods like generating a State from a Context.
This is why I'm proposing that we should work with copies instead of the objects the user passes in.
Re: Options 1 and 2, either of those shallow copy based approaches would do the job for this PR's feature. I just think it would be worth making deep copies instead to remove the underlying problem that requires us to have to think about making copies here once and for all.
tonyandrewmeyer
left a comment
There was a problem hiding this comment.
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)): |
There was a problem hiding this comment.
With #2314 coming, it's probably easier to preemptively use that approach here.
| :arg actions: charm actions to use. Needs to be a valid actions.yaml format (as a dict). | ||
| If none is provided, we will search for a ``actions.yaml`` file in the charm root. | ||
| If none is provided, will be extracted from ``meta`` if present. | ||
| Otherwise, if no meta, actions, or config were provided, will be loaded from the |
There was a problem hiding this comment.
Nice update fixing these to mention charmcraft.yaml in the autospec mode.
| meta = copy.deepcopy(meta) | ||
| config = copy.deepcopy(config) | ||
| actions = copy.deepcopy(actions) |
There was a problem hiding this comment.
I agree that it would be cleaner if we broke the link, whether that's by a deep copy or other means (probably it has to be a deep copy at some point, since we use it for things like network-get mocking). However, I also agree that it doesn't really fit with this PR.
My suggestion is that we have a separate PR that does do a deep copy at this point in the code (I think a deep copy is probably simpler and more correct than any of the other options), merge that, and then do this PR.
This PR adds support for providing
charmcraft.yamlformatted data to themetaargument oftesting.Context.Initially I implemented Dima's suggestion for how to process
testing.Context(..., meta=<my charmcraft.yaml dict>), which is toget'actions' and 'config' from 'meta' if they're not otherwise specified. However, this leads to some unexpected behaviour where the charm name is not automatically derived in the case where we havemeta={'actions': {...}}, breaking symmetry with the handling ofmeta=None, actions={...}.To address this, this PR switches from holding onto the actual
dictobjects provided as arguments, and instead works with deep copies. This allows us topop'actions' and 'config' without unexpected side effects for the caller, restoring symmetry with the behaviour whenactionsandconfigare provided separately.This also eliminates any odd behaviour that might arise from the caller mutating the
metadict after passing it toContext. This is arguably a breaking change, although manipulating testing behaviour in this way would be very much not recommended.Resolves #1424
Open question: Currently
actions=silently takes precedence (though 'actions' is still dropped frommetabefore passing it on to the_CharmSpec, and if this leavesmetaempty, we set a default charm name as we would otherwise). Should it instead be an error to provide (e.g.) 'actions' inmetaand also provide theactions=argument? I can't think of when you'd want the silently override behaviour, but it's more backwards compatible with current behaviour.