-
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: remove the weak argument from Signal.disconnect()
- Loading branch information
Bruno Alla
committed
Sep 21, 2020
1 parent
fb4bb4c
commit ab41491
Showing
5 changed files
with
232 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
from typing import List, Optional | ||
|
||
from libcst import BaseExpression, Call, ImportFrom, MaybeSentinel, Module | ||
from libcst import matchers as m | ||
|
||
from django_codemod.constants import DJANGO_1_9, DJANGO_2_0 | ||
from django_codemod.visitors.base import BaseDjCodemodTransformer, import_from_matches | ||
|
||
|
||
class SignalDisconnectWeakTransformer(BaseDjCodemodTransformer): | ||
"""Remove the `weak` argument to `Signal.disconnect()`.""" | ||
|
||
deprecated_in = DJANGO_1_9 | ||
removed_in = DJANGO_2_0 | ||
|
||
ctx_key_prefix = "SignalDisconnectWeakTransformer" | ||
ctx_key_call_matchers = f"{ctx_key_prefix}-call_matchers" | ||
builtin_signals = [ | ||
"pre_init", | ||
"post_init", | ||
"pre_save", | ||
"post_save", | ||
"pre_delete", | ||
"post_delete", | ||
"m2m_changed", | ||
"pre_migrate", | ||
"post_migrate", | ||
] | ||
|
||
@property | ||
def disconnect_call_matchers(self) -> List[m.Call]: | ||
return self.context.scratch.get(self.ctx_key_call_matchers, []) | ||
|
||
def add_disconnect_call_matcher(self, call_matcher: m.Call) -> None: | ||
self.context.scratch[ | ||
self.ctx_key_call_matchers | ||
] = self.disconnect_call_matchers + [call_matcher] | ||
|
||
def leave_Module(self, original_node: Module, updated_node: Module) -> Module: | ||
"""Clear context when leaving module.""" | ||
self.context.scratch.pop(self.ctx_key_call_matchers, None) | ||
return super().leave_Module(original_node, updated_node) | ||
|
||
def visit_ImportFrom(self, node: ImportFrom) -> Optional[bool]: | ||
"""Set the `Call` matcher depending on which signals are imported..""" | ||
if import_from_matches(node, ["django", "db", "models", "signals"]): | ||
import_alias_matcher = m.OneOf( | ||
*( | ||
m.ImportAlias(name=m.Name(signal_name)) | ||
for signal_name in self.builtin_signals | ||
) | ||
) | ||
for import_alias in node.names: | ||
if m.matches(import_alias, import_alias_matcher): | ||
# We're visiting an import statement for a built-in signal | ||
# Get the actual name it's imported as (in case of import alias) | ||
imported_name = ( | ||
import_alias.asname | ||
and import_alias.asname.name | ||
or import_alias.name | ||
) | ||
# Add the call matcher for the current signal to the list | ||
self.add_disconnect_call_matcher( | ||
m.Call( | ||
func=m.Attribute( | ||
value=m.Name(imported_name.value), | ||
attr=m.Name("disconnect"), | ||
), | ||
) | ||
) | ||
return super().visit_ImportFrom(node) | ||
|
||
def leave_Call(self, original_node: Call, updated_node: Call) -> BaseExpression: | ||
""" | ||
Remove the `weak` argument if present in the call. | ||
This is only changing calls with keyword arguments. | ||
""" | ||
if self.disconnect_call_matchers and m.matches( | ||
updated_node, m.OneOf(*self.disconnect_call_matchers) | ||
): | ||
updated_args = [] | ||
should_change = False | ||
last_comma = MaybeSentinel.DEFAULT | ||
# Keep all arguments except the one with the keyword `weak` (if present) | ||
for index, arg in enumerate(updated_node.args): | ||
if m.matches(arg, m.Arg(keyword=m.Name("weak"))): | ||
# An argument with the keyword `weak` was found | ||
# -> we need to rewrite the statement | ||
should_change = True | ||
else: | ||
updated_args.append(arg) | ||
last_comma = arg.comma | ||
if should_change: | ||
# Make sure the end of line is formatted as initially | ||
updated_args[-1] = updated_args[-1].with_changes(comma=last_comma) | ||
return updated_node.with_changes(args=updated_args) | ||
return super().leave_Call(original_node, updated_node) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
from parameterized import parameterized | ||
|
||
from django_codemod.visitors import SignalDisconnectWeakTransformer | ||
from tests.visitors.base import BaseVisitorTest | ||
|
||
|
||
class TestSignalDisconnectWeakTransformer(BaseVisitorTest): | ||
|
||
transformer = SignalDisconnectWeakTransformer | ||
DJANGO_SIGNAL_NAMES = [ | ||
"pre_init", | ||
"post_init", | ||
"pre_save", | ||
"post_save", | ||
"pre_delete", | ||
"post_delete", | ||
"m2m_changed", | ||
"pre_migrate", | ||
"post_migrate", | ||
] | ||
|
||
@parameterized.expand(DJANGO_SIGNAL_NAMES) | ||
def test_noop(self, signal_name): | ||
before = after = f""" | ||
from django.db.models.signals import {signal_name} | ||
{signal_name}.disconnect( | ||
receiver=some_handler, | ||
sender=MyModel, | ||
dispatch_uid='something-unique', | ||
) | ||
""" | ||
|
||
self.assertCodemod(before, after) | ||
|
||
@parameterized.expand(DJANGO_SIGNAL_NAMES) | ||
def test_with_kwargs(self, signal_name): | ||
before = f""" | ||
from django.db.models.signals import {signal_name} | ||
{signal_name}.disconnect(receiver=some_handler, sender=MyModel, weak=True) | ||
""" | ||
|
||
after = f""" | ||
from django.db.models.signals import {signal_name} | ||
{signal_name}.disconnect(receiver=some_handler, sender=MyModel) | ||
""" | ||
|
||
self.assertCodemod(before, after) | ||
|
||
@parameterized.expand(DJANGO_SIGNAL_NAMES) | ||
def test_with_kwargs_dispatch_uid(self, signal_name): | ||
before = f""" | ||
from django.db.models.signals import {signal_name} | ||
{signal_name}.disconnect( | ||
receiver=some_handler, | ||
sender=MyModel, | ||
weak=True, | ||
dispatch_uid='my-unique-id', | ||
) | ||
""" | ||
|
||
after = f""" | ||
from django.db.models.signals import {signal_name} | ||
{signal_name}.disconnect( | ||
receiver=some_handler, | ||
sender=MyModel, | ||
dispatch_uid='my-unique-id', | ||
) | ||
""" | ||
|
||
self.assertCodemod(before, after) | ||
|
||
@parameterized.expand(DJANGO_SIGNAL_NAMES) | ||
def test_imported_with_alias(self, signal_name): | ||
before = f""" | ||
from django.db.models.signals import {signal_name} as dj_{signal_name} | ||
dj_{signal_name}.disconnect(receiver=some_handler, weak=True) | ||
""" | ||
|
||
after = f""" | ||
from django.db.models.signals import {signal_name} as dj_{signal_name} | ||
dj_{signal_name}.disconnect(receiver=some_handler) | ||
""" | ||
|
||
self.assertCodemod(before, after) | ||
|
||
def test_multiple_signal_disconnected_single_import(self): | ||
before = """ | ||
from django.db.models.signals import pre_save, post_save | ||
pre_save.disconnect(receiver=some_handler, weak=True) | ||
post_save.disconnect(receiver=some_handler, weak=True) | ||
""" | ||
|
||
after = """ | ||
from django.db.models.signals import pre_save, post_save | ||
pre_save.disconnect(receiver=some_handler) | ||
post_save.disconnect(receiver=some_handler) | ||
""" | ||
|
||
self.assertCodemod(before, after) | ||
|
||
def test_multiple_signal_disconnected_separate_imports(self): | ||
before = """ | ||
from django.db.models.signals import pre_save | ||
from django.db.models.signals import post_save | ||
pre_save.disconnect(receiver=some_handler, weak=True) | ||
post_save.disconnect(receiver=some_handler, weak=True) | ||
""" | ||
|
||
after = """ | ||
from django.db.models.signals import pre_save | ||
from django.db.models.signals import post_save | ||
pre_save.disconnect(receiver=some_handler) | ||
post_save.disconnect(receiver=some_handler) | ||
""" | ||
|
||
self.assertCodemod(before, after) |