-
Notifications
You must be signed in to change notification settings - Fork 119
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: remove pebble enum | str unions #1400
base: main
Are you sure you want to change the base?
feat: remove pebble enum | str unions #1400
Conversation
raise ValueError( | ||
'self.current must be a ServiceStatus member (e.g. ServiceStatus.ACTIVE)' | ||
f', not {self.current}' | ||
) | ||
return self.current == ServiceStatus.ACTIVE |
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.
This comparison would be silently False
if self.current
was accidentally a str
.
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.
Would a better solution be to switch to using enum.StrEnum
instead of enum.Enum
? This would seem to match the intent of the original code better. With StrEnum
, ServiceStatus.ACTIVE == 'active'
, so passing around strings instead of enum members would silently succeed instead of silently failing in the absence of a type check.
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.
StrEnum is a py 3.11 feature, isn't it?
We've got to support py 3.8 because Juju supports specific Ubuntu releases as bases and that's where the Python interpreter comes from.
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, agreed. Unfortunately we can't use StrEnum
yet.
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.
Good catch, @dimaqq, thanks.
self.level = CheckLevel.UNKNOWN | ||
# | ||
# these are all Optional in CheckDict and here, why '' instead of None? | ||
# note that all falsey values are filtered out in to_dict | ||
self.period: Optional[str] = dct.get('period', '') | ||
self.timeout: Optional[str] = dct.get('timeout', '') | ||
self.threshold: Optional[int] = dct.get('threshold') |
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 general a lot of values are marked as Optional
when they could perhaps be simply left out of the TypedDict
. I haven't fully checked what the values received from Pebble for these dicts can be -- can these items be the go
version of None
(null
? nil
?). It seems like if so, we should use None
instead of ''
, and if not we shouldn't use Optional
.
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.
Hmm, I'd have to look a bit closer, but I think these will be missing or str in the YAML. So that probably means NotRequired in the TypedDict? In any case, if you want to address this, let's do it in that separate PR.
# so do we want to just use 'unknown' for CheckLevel.UNKNOWN here? | ||
# we still get a warning on making `c1` but not on `c2`, but maybe that's ok | ||
# but is it useful to have 'unknown' in the dict? | ||
# It'll error if that value makes it to pebble, right? | ||
fields = [ |
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'm not really sure what behaviour we want here with CheckLevel.UNKNOWN
.
# while we might want <some-unknown-value-string> to overwrite, | ||
# would we ever want Checklevel.UNKNOWN to overwrite? | ||
if value is CheckLevel.UNKNOWN: | ||
continue |
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 think we should just use other.to_dict()
here, is there a reason we weren't already?
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 see why you want to use other.to_dict()
here, but it doesn't seem the right thing here: we're merging two instances of the class, so it seems weird to convert one to a dict first.
Let's just avoid the special case here and the unknown will be merged (and probably eventually fail at the Pebble level). That seems okay here.
@@ -3046,6 +3105,9 @@ def get_checks( | |||
""" | |||
query: Dict[str, Any] = {} | |||
if level is not None: | |||
# is this bad now that value could be 'unknown'? | |||
# should we explicitly avoid it here? | |||
# if level is not None and level.value is not CheckLevel.UNKNOWN: | |||
query['level'] = level.value |
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.
Another case where it's not clear how to handle CheckLevel.UNKNOWN
on the outgoing side.
@@ -3098,7 +3160,7 @@ def get_notices( | |||
*, | |||
users: Optional[NoticesUsers] = None, | |||
user_id: Optional[int] = None, | |||
types: Optional[Iterable[Union[NoticeType, str]]] = None, | |||
types: Optional[Iterable[NoticeType]] = None, | |||
keys: Optional[Iterable[str]] = None, | |||
) -> List[Notice]: | |||
"""Query for notices that match all of the provided filters. |
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 this will break any users.
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.
There's not a single user of .get_notices() yet.
Source: charm-analysis and github search.
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.
Yep, I'm comfortable with this. Use of notice events themselves are rare; I'd be surprised if anyone actually needed get_notices
even if they were using notices. The charm normally wants the event, not to query for them.
assert info.type == 'foobar' | ||
with pytest.warns(UserWarning): | ||
info = pebble.FileInfo.from_dict(d) | ||
assert info.type == pebble.FileType.UNKNOWN |
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.
Should we be using a different kind of warning 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.
Nah, the default of UserWarning seems fine.
@@ -3863,7 +3863,7 @@ def _notice_matches( | |||
# For example: if user_id filter is set and it doesn't match, return False. | |||
if user_id is not None and not (notice.user_id is None or user_id == notice.user_id): | |||
return False | |||
if types is not None and notice.type not in types: | |||
if types is not None and notice.type.value not in types: | |||
return False |
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.
types
is a collection of str
s. I think this is mimicking the logic of pebble
, so I think it makes sense for us to use notice.type.value
here. Was this always silently broken? Or silently worked because notice.type
was often a str
-- while now it's supposed to always be an enum
member?
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 think you can answer that question, as notices are not really used by charmers yet.
Which means that the question can be rephrased as "do we have a test for this harness feature?" and "does that test actually work?".
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, this was almost certainly silently broken. I should have written a test for this. Can you please add a regression test in test_testing.py
?
Only superficial comments from me, I didn't read the change properly yet. |
@@ -3863,7 +3863,7 @@ def _notice_matches( | |||
# For example: if user_id filter is set and it doesn't match, return False. | |||
if user_id is not None and not (notice.user_id is None or user_id == notice.user_id): | |||
return False | |||
if types is not None and notice.type not in types: | |||
if types is not None and notice.type.value not in types: | |||
return False |
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, this was almost certainly silently broken. I should have written a test for this. Can you please add a regression test in test_testing.py
?
raise ValueError( | ||
'self.current must be a ServiceStatus member (e.g. ServiceStatus.ACTIVE)' | ||
f', not {self.current}' | ||
) |
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 think we should drop this runtime check, as 1) recently we've been leaning more heavily on static typing rather than runtime type checks, and 2) it can basically never happen, especially now, given that these objects are parsed via from_dict
and not created by the user (though I realise we don't prevent that).
raise ValueError( | ||
'self.current must be a ServiceStatus member (e.g. ServiceStatus.ACTIVE)' | ||
f', not {self.current}' | ||
) | ||
return self.current == ServiceStatus.ACTIVE |
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, agreed. Unfortunately we can't use StrEnum
yet.
@@ -1098,11 +1107,16 @@ def __init__(self, name: str, raw: Optional[CheckDict] = None): | |||
self.name = name | |||
dct: CheckDict = raw or {} | |||
self.override: str = dct.get('override', '') | |||
self.level: CheckLevel | |||
dct_level = dct.get('level', '') |
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.
Nit: dct
is confusing here. How about level_str
or raw_level
?
@@ -1098,11 +1107,16 @@ def __init__(self, name: str, raw: Optional[CheckDict] = None): | |||
self.name = name | |||
dct: CheckDict = raw or {} | |||
self.override: str = dct.get('override', '') | |||
self.level: CheckLevel |
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 line needed?
@@ -3046,6 +3105,9 @@ def get_checks( | |||
""" | |||
query: Dict[str, Any] = {} | |||
if level is not None: | |||
# is this bad now that value could be 'unknown'? |
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 think do the same as earlier: put it through as the string "unknown" and let Pebble fail it (in practice it's extremely unlikely to happen, as we haven't added any new levels since they first came out).
@@ -3098,7 +3160,7 @@ def get_notices( | |||
*, | |||
users: Optional[NoticesUsers] = None, | |||
user_id: Optional[int] = None, | |||
types: Optional[Iterable[Union[NoticeType, str]]] = None, | |||
types: Optional[Iterable[NoticeType]] = None, | |||
keys: Optional[Iterable[str]] = None, | |||
) -> List[Notice]: | |||
"""Query for notices that match all of the provided filters. |
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.
Yep, I'm comfortable with this. Use of notice events themselves are rare; I'd be surprised if anyone actually needed get_notices
even if they were using notices. The charm normally wants the event, not to query for them.
assert info.type == 'foobar' | ||
with pytest.warns(UserWarning): | ||
info = pebble.FileInfo.from_dict(d) | ||
assert info.type == pebble.FileType.UNKNOWN |
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.
Nah, the default of UserWarning seems fine.
for current in [pebble.ServiceStatus.INACTIVE, pebble.ServiceStatus.ERROR, 'other']: | ||
for current in ( | ||
status for status in pebble.ServiceStatus if status is not pebble.ServiceStatus.ACTIVE | ||
): |
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.
The nested looping here is hard to read, especially split on multiple lines. I think let's either keep the explicit list, or do it on two lines, something like:
inactive_statuses = [status for status in ServiceStatus if status != ACTIVE]
for current in inactive_statuses: ...
James and I discussed whether we should use "is" or "==" to compare enums, and mildly settled on "let's use ==" as it's what you'd do with regular values (and StrEnum or whatever).
s = pebble.ServiceInfo( | ||
name='s', | ||
startup=pebble.ServiceStartup.ENABLED, | ||
current='active', # pyright: ignore[reportArgumentType] |
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.
Per earlier comment, I think we should drop this runtime check and just let static type checking handle this case.
Resolving #1287, this PR removes the use of
str
andenum
member unions connected to thepebble
module.Generally this just involves typing
dict
s generated by pebble as containingstr
values, and using theenum
types exclusively inops
. The major change is that unknown values from pebble result in anUNKNOWN
member of theenum
, rather than being passed around asstr
s.Reviewers will notice some block comments calling attention to potential issues with the current code, questions about my design choices, and suggestions for further changes. Feedback on these areas would be especially appreciated.