Skip to content

Commit

Permalink
feat: remove the weak argument from Signal.disconnect()
Browse files Browse the repository at this point in the history
  • Loading branch information
Bruno Alla committed Sep 21, 2020
1 parent fb4bb4c commit ab41491
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 0 deletions.
2 changes: 2 additions & 0 deletions django_codemod/visitors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
FloatRangeModelFieldTransformer,
)
from .shortcuts import RenderToResponseTransformer
from .signals import SignalDisconnectWeakTransformer
from .template_tags import AssignmentTagTransformer
from .timezone import FixedOffsetTransformer
from .translations import (
Expand Down Expand Up @@ -56,6 +57,7 @@
"OnDeleteTransformer",
"QuerySetPaginatorTransformer",
"RenderToResponseTransformer",
"SignalDisconnectWeakTransformer",
"SmartTextTransformer",
"UGetTextLazyTransformer",
"UGetTextNoopTransformer",
Expand Down
98 changes: 98 additions & 0 deletions django_codemod/visitors/signals.py
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)
3 changes: 3 additions & 0 deletions docs/codemods.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Applied by passing the `--removed-in 2.0` or `--deprecated-in 1.9` option:
- Adds the `on_delete=models.CASCADE` to all `ForeignKey` and `OneToOneField`s
that don’t use a different option.
- Replaces template tags decorator `assignment_tag` by `simple_tag`.
- Removes the `weak` argument to `Signal.disconnect()` calls. This will only
apply to built-in signals (`pre_save`, `post_save`, ...) and to `disconnect()`
calls with keyword arguments.

Applied by passing the `--removed-in 2.0` or `--deprecated-in 1.10` option:

Expand Down
2 changes: 2 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ def test_deprecated_in_mapping():
(1, 9): [
"AssignmentTagTransformer",
"OnDeleteTransformer",
"SignalDisconnectWeakTransformer",
],
}

Expand Down Expand Up @@ -268,6 +269,7 @@ def test_removed_in_mapping():
(2, 0): [
"AssignmentTagTransformer",
"OnDeleteTransformer",
"SignalDisconnectWeakTransformer",
"URLResolversTransformer",
],
}
Expand Down
127 changes: 127 additions & 0 deletions tests/visitors/test_signals.py
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)

0 comments on commit ab41491

Please sign in to comment.