-
Notifications
You must be signed in to change notification settings - Fork 121
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: Make Context and Manager variadic types #1445
Conversation
This change enables passing the Charm type all the way from the Context to the charm attribute in the manager for better autocompletion and type checks.
I like this, but will defer to typing experts for details and py 3.8 compatibility. |
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.
Changes make sense to me. I think CharmEvents.__call__
should have the return type annotated with the new generic type Manager[CharmType]
. I don't have much experience with scenario
, so I'll have to let @tonyandrewmeyer catch whether there are any problems I didn't notice on that front.
@@ -355,7 +355,7 @@ def action( | |||
return _Event(f"{name}_action", action=_Action(name, **kwargs)) | |||
|
|||
|
|||
class Context: | |||
class Context(Generic[CharmType]): |
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.
Confused me a little at first, but this works because CharmType
is already being used as a parameter for (at least) the __init__
method (line 448).
__call__
(line 568) should be annotated as returning a Manager[CharmType]
, no? I guess the types are inferred correctly without the annotation, but still.
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 wonder if we also ought to be pushing this through to the self.ops
object, so ops_main_mock.Ops
has it as well. I think that means that the cast on line 94 could be an "assert not None" line instead - I'm not sure if there are other benefits as well. @james-garner-canonical any thoughts on that?
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.
Doing that would probably be correct as I see that the Ops
class has a charm_spec
argument that's parameterised on charm_spec
(_CharmSpec[CharmType]
). Luckily this will already be correctly parameterised when constructed since Ops
is initialised via the (now generic) Context
. So it should be as simple as changing class Ops
to class Ops(Generic[CharmType])
and changing the type annotation in Manager.__init__
.
Line 90 should probably be if self.ops is None
rather than if not self.ops
, right? Then you should be able to eliminate the cast
without needing an assert
.
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.
Is this what you had in mind? a4a37d8
@@ -43,7 +44,6 @@ | |||
from .ops_main_mock import Ops | |||
from .state import ( | |||
AnyJson, | |||
CharmType, |
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.
Just a comment: this threw me at first, but CharmType
is being imported in the unconditional imports too, so I see that this is just removing a redundant duplicate import
I'm not sure what you mean by this? Do you mean that There will be new Having the If I've misunderstood and you meant something else, could you please elaborate? |
Tested manually with: import ops
class MyCharm(ops.CharmBase):
foo: str
""This is an example attribute, of type string.""" And: from ops import testing
from charm import MyCharm
def test_function():
ctx = testing.Context(MyCharm)
with ctx(ctx.on.config_changed(), testing.State()) as mgr:
mgr.charm.foo (With a And I did indeed get nice autocomplete (including the type and doc, as you'd expect) as well as the improved static checking. Ideally, tests aren't needing to access the charm object in most cases, but this is a nice improvement when they do. I also had no trouble running tests with the code from this branch and with Python 3.8, although I only did some basic checks. |
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.
Generally looks good to me, although I'm not an expert on Python type annotations. Manual testing works as well (as posted separately).
I wonder if we could have a test for this - not necessarily testing the type explicitly (although that would be fine), but one that would exercise it so that the static checks would fail if we regressed on this?
@@ -355,7 +355,7 @@ def action( | |||
return _Event(f"{name}_action", action=_Action(name, **kwargs)) | |||
|
|||
|
|||
class Context: | |||
class Context(Generic[CharmType]): |
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 wonder if we also ought to be pushing this through to the self.ops
object, so ops_main_mock.Ops
has it as well. I think that means that the cast on line 94 could be an "assert not None" line instead - I'm not sure if there are other benefits as well. @james-garner-canonical any thoughts on that?
@tonyandrewmeyer Thank you, I had not realized at the time that the two packages would be built from this single repo, and I was confused that the original repo was being archived. No problem then :)
There is not a lot of content around testing type annotations, I found this: https://typing.readthedocs.io/en/latest/reference/quality.html#testing-using-assert-type-and-warn-unused-ignores. |
I did some type checker tests in the past, IIRC using I reckon this should work: # in tests/resources/example_typing.py
from ops.testing import XYZ
reveal_type(XYZ) # in tests/test_typing_annotations.py
def test_type_annotation():
proc = Popen("mypy ../resources/example_typing.py", stdout=PIPE, text=True)
assert "<what type you expect to be printed.>" in proc.stdout.read()
|
We've had a few discussions on checking the types earlier this year. There were pros and cons, and fancier type tests did not get accepted. What got accepted is here: |
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 to me, thanks.
Note that I have a PR open that refactors a bunch of this code - whichever one lands first, I'll merge across the changes to the other one.
waiting for @james-garner-canonical whose concern appears to have been addressed, ptal. |
Closes canonical#1444. This change enables passing the Charm type all the way from the Context to the charm attribute in the manager for better autocompletion and type checks. You can give this a try with the following code snippet: ```python from ops import CharmBase from testing.src.scenario import Context, State class MyCharm(CharmBase): some_attribute: str def test_function(): ctx = Context(MyCharm) state = State() with ctx(ctx.on.config_changed(), state) as manager: charm = manager.charm charm.some_attribute # behold, autocompletion! ``` A word of caution, though, the testing module is not using the bundled scenario code base. It is still using `ops_scenario`. For users to benefit from this new feature, we would need to change the import machinery.
Closes #1444.
This change enables passing the Charm type all the way from the Context to the charm attribute in the manager for better autocompletion and type checks.
You can give this a try with the following code snippet:
A word of caution, though, the testing module is not using the bundled scenario code base. It is still using
ops_scenario
. For users to benefit from this new feature, we would need to change the import machinery.