-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat!: unify run() and run_action() and simplify context managers #162
feat!: unify run() and run_action() and simplify context managers #162
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in principle it looks good, a couple of doubts and thoughts here and there.
scenario/context.py
Outdated
|
||
def run(self) -> "ActionOutput": | ||
return cast("ActionOutput", super().run()) | ||
class ManagedAction(_Manager): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't these types be private?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a good question. @benhoyt's argument was that it's important to have the API (specifically, the .run
/.run_action
and .charm
attributes) documented, e.g. in automatic reference docs not just in the README (or a how-to).
Users definitely shouldn't be instantiating these objects themselves, and they aren't in the top-level scenario
namespace as a reflection of that. I also can't think of any cases where you'd need to use them in a type annotation.
They do 'feel' private to me - they are an implementation detail of how the context manager system works, not a part of the grander Scenario design language. I do think reference docs should include mention of the available attributes, though.
@benhoyt what if they were private, but the Context
docstring had something like:
"""
Objects can also provide a context manager when tests need to interact with the charm object, for example:
ctx = Context(MyCharm)
with ctx(ctx.on.start(), State()) as event:
# `event.charm` is the `MyCharm` object created to process the event
event.run() # When using `ctx.on.action()`, use `event.run_action()`
"""
The wording can be tidied up a bunch, but generally explaining the small API of the context manager object inside of the Context
docstring, rather than having the other objects be public. I think the API is small enough (one method, one object) that people wouldn't be looking them up in reference docs much anyway, and would likely start with the Context
object itself. It also avoids having both Context.run
and ManagedEvent.run
in the reference docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love that the docs would have to explain the run
and run_action
methods just as text inside the docstring. I still think they should be broken out as real types in the docs. They are real types the user interacts with (just not their name), so it seems to me they should have first-class documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah but IMHO 'we need these types to show up in the docs' is not a good argument to have them public.
Is there no way to have them private and still convince sphinx to document them as if they were public?
Also, how's the code completion on the managers? Will the IDE warn against ctx(on.install...) as e: e.run_action()
?
If not, we should try using typing.overload.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Things are a little different now that there's just a single class and one run
that always has the same signature.
IDEs/language servers don't really care about the private-ness, so if Manager
is _Manager
you'll still get .run
and .charm
auto-completion and the docstrings popping up inline.
Sphinx's autodoc functionality can be given a list of private names that should also be included. So we could have _Manager
and it could still be in the reference docs.
I still don't think that anyone is going to be looking up the Manager
class or its attributes/methods in reference documentation, so having it there (in addition to what's in Context
's docstring) would be for completeness rather than adding actual value.
I also do feel like "there is a class called Manager" is an implementation detail that should be open to change in the future without being considered breaking. The public API is that you get an object that has run
and charm
attributes. I think that would most properly be documented like:
class HasRunAndCharm(abc.ABC):
charm: ops.CharmBase
@abc.abstractmethod
def run(): -> State
...
class Context:
def __call__(self, event, state) -> HasRunAndCharm:
...
But that's noisier than just having the class be public.
My preference is to have the name be private (_Manager
or _EventManager
) and if @benhoyt strongly wants it in the reference docs (which I think will be the case even after reading the above 😄) then adjusting the Sphinx autodoc so that it's included.
Co-authored-by: PietroPasotti <[email protected]>
To summarise, with this change the charmer has two options:
or
Did I get that right? Having P.S. @tonyandrewmeyer pls update the PR description, I guess it's P.P.S. perhaps we could clarify this in the docs like this:
or
|
Off-topic charm = scenario.Charm(MyRealCharm, meta=...)
state_out = charm.run(charm.on.startup(), state_in) That would be easier to understand, wouldn't it? |
In my head it's the charm's execution context. The filesystem it is situated in, the envvars the python process runs with (encapsulated further in the Event), the juju version... |
I'm reworking this PR to rename run_action as suggested and as discussed by charm-tech. I'll update the description to match when the code is changed. |
|
Yes. This was the case before as well, it's just the names that have changed (and that you can now use the same API for action events as other events). Note that the "event" that's the first argument would hardly ever be an object named "event", it would almost always be The "event" that is the manager that you get back can obviously be named anything the charmer likes. If people like to stick with "mgr" (or "manager") that would be ok. It only has one attribute ( (One possibility I only thought of while doing the latest changes is: with ctx(ctx.on.start(), State()) as charm:
charm.prepare_function()
ctx.run(charm)
assert charm... But I still prefer what we have to that).
Having a method instead (like we have now in Scenario 6) and either leaving it named "manager" or renaming it was the original option, but I do prefer this version (again there's lots of discussion in the links PRs). If feedback on the beta is really negative, it wouldn't be hard to change it back.
If someone is going to put a name on the first argument, instead of doing an inline |
Let's move that discussion to the ops development channel or a separate issue - these PRs tend to get pretty long as-is, even when staying on topic. |
Ok @benhoyt, @PietroPasotti , @dimaqq this is ready for another round, with the much enlarged scope (I've updated the description to cover it all). I'm pretty happy with the current state, so looking forward to hearing what you all think! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
much nicer! Few nits here and there.
Reading the readme I'm not convinced about the event
name for the context manager. I'd vote for keeping mgr
in the official examples :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like this flattened API, thanks!
The `run_action()` method (both standalone and in the context manager) are removed. This means that all events, including action events, are emitted with the same `run()` call, and both return the output state. To get access to the results of actions, a new `action_output` attribute is added to the `Context`. This is a simplified representation of the Juju `operation` object (and the `task` objects in them), which are displayed with `juju run`, but also available via `juju show-operation`. The class has the same name as the Harness `ActionOutput` and will be consolidated into a single class when Scenario is added to ops. For example: ```python out = ctx.run_action(Action("backup", params={"output": "data.tar.gz"}), State()) assert out.results == {"size": 1234} assert out.logs = [..., ...] assert out.state... state = ctx.run(ctx.on.action("backup", params={"output": "data.tar.gz"}), State()) assert ctx.action_output.results == {"size": 1234} assert ctx.action_output.logs = [..., ...] assert state... ``` When the charm calls `event.fail()`, this raises an exception, in the same way that Harness does. For example: ```python out = ctx.run_action("bad-action", State()) assert out.failure == "couldn't do the thing" with pytest.raises(ActionFailed) as exc_info: ctx.run(ctx.on.action("bad-action"), State()) assert exc_info.value.message == "couldn't do the thing" ``` The advantage of this is that tests that the action is successful do not need to have `assert ctx.action_output.status != "failed"`, which is easy to miss. In addition, the `Context.manager` and `Context.action_manager` methods are replaced by the ability to use the `Context` object itself as a context manager. For example: ```python ctx = Context(MyCharm) with ctx(ctx.on.start(), State()) as event: event.charm.prepare() state = event.run() assert event.charm... ``` The same code is also used (with `ctx.on.action()`) for action events. Advantages: * Slightly shorter code (no ".manager" or ".action_manager") * Avoids naming complications with "manager", "action_manager" and the various alternatives proposed in #115. The `.output` property of the context manager is also removed. The context manager will still handle running the event if it's not done explicitly, but if that's the case then the output is not available. We want to encourage people to explicitly run the event, not rely on the automated behaviour - although I think it does make sense that it does run, rather than raise or end in a weird situation where the event never ran. This replaces #115 and #118, being a combination of ideas/discussion from both, plus incorporating the unification of run/run_action discussed here, and the "action fail raises" discussed elsewhere. Also, as drive-by changes while names are being made consistent: * `_Port` becomes `Port` * `_RelationBase` becomes (again) `RelationBase` --------- Co-authored-by: PietroPasotti <[email protected]>
The
run_action()
method (both standalone and in the context manager) are removed. This means that all events, including action events, are emitted with the samerun()
call, and both return the output state.To get access to the results of actions, new
action_results
andaction_logs
attributes are added to theContext
. For example:When the charm calls
event.fail()
, this raises an exception, in the same way that Harness does. For example:The advantage of this is that tests that the action is successful do not need to have assertions of such, which is easy to miss.
In addition, the
Context.manager
andContext.action_manager
methods are replaced by the ability to use theContext
object itself as a context manager.For example:
The same code is also used (with
ctx.on.action()
) for action events.Advantages:
The
.output
property of the context manager is also removed. The context manager will still handle running the event if it's not done explicitly, but if that's the case then the output is not available. We want to encourage people to explicitly run the event, not rely on the automated behaviour - although I think it does make sense that it does run, rather than raise or end in a weird situation where the event never ran.This replaces #115 and #118, being a combination of ideas/discussion from both, plus incorporating the unification of run/run_action discussed here, and the "action fail raises" discussed elsewhere.
Also, as drive-by changes while names are being made consistent:
_Port
becomesPort
_RelationBase
becomes (again)RelationBase