-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
ref(rules): Fix typing for rule_snooze & rule #80306
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #80306 +/- ##
===========================================
+ Coverage 59.79% 78.13% +18.33%
===========================================
Files 7200 7191 -9
Lines 318458 318047 -411
Branches 43902 43834 -68
===========================================
+ Hits 190419 248500 +58081
+ Misses 123251 63183 -60068
- Partials 4788 6364 +1576 |
@@ -76,14 +77,15 @@ def serialize(self, obj, attrs, user, **kwargs): | |||
@region_silo_endpoint | |||
class BaseRuleSnoozeEndpoint(ProjectEndpoint): | |||
permission_classes = (ProjectAlertRulePermission,) | |||
rule_model = type[Rule] | type[AlertRule] | |||
rule_model: type[AlertRule | Rule] |
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.
Moved back from Generic to union typehint, since the Model
bound was giving causing mypy to freak out over what T
could be. E.g in lines 87-89/89-91 where we access BaseManager[AlertRule]
to get fetch_for_project
but mypy says BaseManager[T]
doesn't have access to that func.....
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.
might be useful to have an # abstract
comment on this just so it's clear
btw the original code was a type alias so I'm surprised this worked at all!
@@ -64,10 +69,15 @@ def to_internal_value(self, data): | |||
# XXX(epurkhiser): Very hacky, but we really just want validation | |||
# errors that are more specific, not just 'this wasn't filled out', | |||
# give a more generic error for those. | |||
first_error = next(iter(form.errors.values()))[0] |
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.
Interestingly, by using .values()
on ErrorDict the list of ValidationError(<message>)
turns into a list of strings. mypy doesn't pick up on that (django-stubs doesn't say so either) and assumes first_error
is a str | ValidationError
. The below way feels a bit more "stable", since I'm not sure if that transformation of ValidationError -> str
via .values()
is totally intentional.
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.
so the : str
here shouldn't do anything (and shouldn't be there!) -- and actually it looks like the typechecker is correct
form.errors
is a ErrorsDict
which is a dict[str, ErrorsList]
and ErrorsList
is a UserList[ValidationError | _StrOrPromise]
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.
yesssss, though the .values()
transformation makes the result a UserList[_StrOrPromise]
in reality, so I guess we could just type hint the variable to 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.
I would just remove the annotation personally
@@ -168,31 +177,6 @@ def validate_conditions(self, conditions): | |||
def validate(self, attrs): | |||
return super().validate(validate_actions(attrs)) | |||
|
|||
def save(self, rule): |
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.
No code ran/referenced this ._.
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 this gets called from rest_framework maybe?
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.
'o' is .save()
run implicitly when people instantiate the serializer ? I couldn't find any explicit calls for .save()
, and this method doesn't implement any of the create()
or update()
. So, we should be safe ?
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.
DRF won't call save() automatically, it would be a method call we're making. However, I wasn't able to find any callsites for this method either.
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 am not sure -- I thought it was magically invoked by forms somehow -- could probably trace when this function was introduced and see how it was used in that PR and then from there figure out if that code path is still used
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.
:0) good thinking, looks like this (vintage) function used to be used in the rules endpoints, but now we use dataclasses (see: ProjectRuleCreator & ProjectRuleUpdater) that do the same actions instead
@@ -168,31 +177,6 @@ def validate_conditions(self, conditions): | |||
def validate(self, attrs): | |||
return super().validate(validate_actions(attrs)) | |||
|
|||
def save(self, rule): |
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 this gets called from rest_framework maybe?
@@ -64,10 +69,15 @@ def to_internal_value(self, data): | |||
# XXX(epurkhiser): Very hacky, but we really just want validation | |||
# errors that are more specific, not just 'this wasn't filled out', | |||
# give a more generic error for those. | |||
first_error = next(iter(form.errors.values()))[0] |
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.
so the : str
here shouldn't do anything (and shouldn't be there!) -- and actually it looks like the typechecker is correct
form.errors
is a ErrorsDict
which is a dict[str, ErrorsList]
and ErrorsList
is a UserList[ValidationError | _StrOrPromise]
# Due to mypy requiring keyword arguments to not be kwargs, we need to check the type of rule and query accordingly | ||
# In RuleSnooze, rule and alert_rule are mutually exclusive (cannot both be not null) |
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 better fix is to give kwargs
a TypedDict
and leave the code as is
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.
same above too!
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.
nvm 🥲 it turns out a union of Literals in a variable key for a typed dict isn't supported by mypy currently (python/mypy#7752)
class RuleField(TypedDict, total=False):
rule: Rule
alert_rule: AlertRule
kwargs: RuleField = {self.rule_field: rule}
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.
really starting to think this base class is a maintenance problem and trying to be too cute -- and that these are actually two disparate things trying to be shoved into a BaseRuleSnoozeEndpoint shaped box
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.
yeahhhhh like we could delegate the RuleSnooze call to the child classes, and keep the rest of the logic in a helper in the base case, since the main conflict is that the base class doesn't know if it wants rule or alert_rule in the db call.
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 or a few composable functions would probably work too!
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.
Wait, can you elaborate more on what composable functions means in this context ? The google tells me composable functions when we chain functions together/when functions take in other functions but I'm not sure how that would help in this case :thonk
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.
maybe won't need to compose -- but can probably make two separate functions for create_rule_snooze
and delete_rule_snooze
and then call them with the appropriate arguments in the two "subclasses" here -- basically undo the inheritance and instead have free functions which accomplish the same thing (but are less reliant on weird conventions of self
to shoehorn in the right arguments).
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.
Pushing the differences in the subclasses into methods/functions could work. We have similar patterns for the OrganizationUnsubscribe
endpoints and AvatarMixin
.
@@ -104,20 +106,25 @@ def post(self, request: Request, project: Project, rule: Rule | AlertRule) -> Re | |||
|
|||
if not can_edit_alert_rule(project.organization, request): | |||
raise PermissionDenied( | |||
detail="Requesting user cannot mute this rule.", code=status.HTTP_403_FORBIDDEN | |||
detail="Requesting user cannot mute this rule.", code=str(status.HTTP_403_FORBIDDEN) |
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 str(403)
seems suspicious -- what was this fixing? do we even need code=
(surely a PermissionDenied is always going to be a 403 right?)
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, good pt. the 403 comes included in the PermissionDenied
exception class already
@@ -76,14 +77,15 @@ def serialize(self, obj, attrs, user, **kwargs): | |||
@region_silo_endpoint | |||
class BaseRuleSnoozeEndpoint(ProjectEndpoint): | |||
permission_classes = (ProjectAlertRulePermission,) | |||
rule_model = type[Rule] | type[AlertRule] | |||
rule_model: type[AlertRule | Rule] |
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.
might be useful to have an # abstract
comment on this just so it's clear
btw the original code was a type alias so I'm surprised this worked at all!
@@ -169,11 +181,13 @@ def delete(self, request: Request, project: Project, rule: Rule | AlertRule) -> | |||
shared_snooze.delete() | |||
deletion_type = "everyone" | |||
|
|||
# next check if there is a mute for me that I can remove |
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 would be good to retain this comment.
@@ -197,7 +211,8 @@ def delete(self, request: Request, project: Project, rule: Rule | AlertRule) -> | |||
# didn't find a match but there is a shared snooze | |||
if shared_snooze: | |||
raise PermissionDenied( | |||
detail="Requesting user cannot unmute this rule.", code=status.HTTP_403_FORBIDDEN | |||
detail="Requesting user cannot unmute this rule.", | |||
code=str(status.HTTP_403_FORBIDDEN), |
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 code
parameter can likely be removed.
@@ -168,31 +177,6 @@ def validate_conditions(self, conditions): | |||
def validate(self, attrs): | |||
return super().validate(validate_actions(attrs)) | |||
|
|||
def save(self, rule): |
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.
DRF won't call save() automatically, it would be a method call we're making. However, I wasn't able to find any callsites for this method either.
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.
Trying a new thing where we try and do all (read: most) of the typing upfront and then move the modules to their new home.
issue ref #73858