Skip to content

Commit

Permalink
New codemod: fix-deprecated-abstractproperty
Browse files Browse the repository at this point in the history
  • Loading branch information
drdavella committed Dec 8, 2023
1 parent e2a0dbb commit 97cee8b
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 0 deletions.
52 changes: 52 additions & 0 deletions integration_tests/test_fix_deprecated_abstractproperty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from textwrap import dedent

from core_codemods.fix_deprecated_abstractproperty import FixDeprecatedAbstractproperty
from integration_tests.base_test import (
BaseIntegrationTest,
original_and_expected_from_code_path,
)


class TestFixDeprecatedAbstractproperty(BaseIntegrationTest):
codemod = FixDeprecatedAbstractproperty
code_path = "tests/samples/deprecated_abstractproperty.py"

original_code, _ = original_and_expected_from_code_path(code_path, [])
expected_new_code = dedent(
"""\
from abc import abstractmethod
import abc
class A:
@property
@abc.abstractmethod
def foo(self):
pass
@abstractmethod
def bar(self):
pass
"""
)

expected_diff = """\
---
+++
@@ -1,8 +1,10 @@
-from abc import abstractproperty as ap, abstractmethod
+from abc import abstractmethod
+import abc
class A:
- @ap
+ @property
+ @abc.abstractmethod
def foo(self):
pass
"""

expected_line_change = "5"
change_description = FixDeprecatedAbstractproperty.DESCRIPTION
4 changes: 4 additions & 0 deletions src/codemodder/scripts/generate_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ class DocMetadata:
importance="Medium",
guidance_explained="This change will only restrict the response type and will not alter the response data itself. Thus we deem it safe.",
),
"fix-deprecated-abstractproperty": DocMetadata(
importance="Low",
guidance_explained="This change fixes deprecated uses and is safe.",
),
}


Expand Down
2 changes: 2 additions & 0 deletions src/core_codemods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .django_debug_flag_on import DjangoDebugFlagOn
from .django_session_cookie_secure_off import DjangoSessionCookieSecureOff
from .enable_jinja2_autoescape import EnableJinja2Autoescape
from .fix_deprecated_abstractproperty import FixDeprecatedAbstractproperty
from .fix_mutable_params import FixMutableParams
from .harden_pyyaml import HardenPyyaml
from .harden_ruamel import HardenRuamel
Expand Down Expand Up @@ -41,6 +42,7 @@
DjangoDebugFlagOn,
DjangoSessionCookieSecureOff,
EnableJinja2Autoescape,
FixDeprecatedAbstractproperty,
FixMutableParams,
HardenPyyaml,
HardenRuamel,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
The `@abstractproperty` decorator from `abc` has been [deprecated](https://docs.python.org/3/library/abc.html#abc.abstractproperty) since Python 3.3. This is because it's possible to use `@property` in combination with `@abstractmethod`.

Our changes look like the following:
```diff
import abc

class Foo:
- @abc.abstractproperty
+ @property
+ @abc.abstractmethod
def bar():
...
```
43 changes: 43 additions & 0 deletions src/core_codemods/fix_deprecated_abstractproperty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import libcst as cst

from codemodder.codemods.api import BaseCodemod, ReviewGuidance
from codemodder.codemods.utils_mixin import NameResolutionMixin


class FixDeprecatedAbstractproperty(BaseCodemod, NameResolutionMixin):
NAME = "fix-deprecated-abstractproperty"
SUMMARY = "Replace deprecated abstractproperty"
REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW
DESCRIPTION = "Replace deprecated abstractproperty with property and abstractmethod"
REFERENCES = [
{
"url": "https://docs.python.org/3/library/abc.html#abc.abstractproperty",
"description": "",
},
]

def leave_Decorator(
self, original_node: cst.Decorator, updated_node: cst.Decorator
):
if (
base_name := self.find_base_name(original_node.decorator)
) and base_name == "abc.abstractproperty":
self.add_needed_import("abc")
self.remove_unused_import(original_node)
self.add_change(original_node, self.DESCRIPTION)
return cst.FlattenSentinel(
[
cst.Decorator(
decorator=cst.Name(value="property"),
trailing_whitespace=updated_node.trailing_whitespace,
),
cst.Decorator(
decorator=cst.Attribute(
value=cst.Name(value="abc"),
attr=cst.Name(value="abstractmethod"),
)
),
]
)

return original_node
123 changes: 123 additions & 0 deletions tests/codemods/test_fix_deprecated_abstractproperty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from core_codemods.fix_deprecated_abstractproperty import FixDeprecatedAbstractproperty
from tests.codemods.base_codemod_test import BaseCodemodTest


class TestFixDeprecatedAbstractproperty(BaseCodemodTest):
codemod = FixDeprecatedAbstractproperty

def test_import(self, tmpdir):
original_code = """
import abc
class A:
@abc.abstractproperty
def foo(self):
pass
"""
new_code = """
import abc
class A:
@property
@abc.abstractmethod
def foo(self):
pass
"""
self.run_and_assert(tmpdir, original_code, new_code)

def test_import_from(self, tmpdir):
original_code = """
from abc import abstractproperty
class A:
@abstractproperty
def foo(self):
pass
"""
new_code = """
import abc
class A:
@property
@abc.abstractmethod
def foo(self):
pass
"""
self.run_and_assert(tmpdir, original_code, new_code)

def test_import_alias(self, tmpdir):
original_code = """
from abc import abstractproperty as ap
class A:
@ap
def foo(self):
pass
"""
new_code = """
import abc
class A:
@property
@abc.abstractmethod
def foo(self):
pass
"""
self.run_and_assert(tmpdir, original_code, new_code)

def test_different_abstractproperty(self, tmpdir):
new_code = original_code = """
from xyz import abstractproperty
class A:
@abstractproperty
def foo(self):
pass
@property
def bar(self):
pass
"""
self.run_and_assert(tmpdir, original_code, new_code)

def test_preserve_decorator_order(self, tmpdir):
original_code = """
import abc
class A:
@whatever
@abc.abstractproperty
def foo(self):
pass
"""
new_code = """
import abc
class A:
@whatever
@property
@abc.abstractmethod
def foo(self):
pass
"""
self.run_and_assert(tmpdir, original_code, new_code)

def test_preserve_comments(self, tmpdir):
original_code = """
import abc
class A:
@abc.abstractproperty # comment
def foo(self):
pass
"""
new_code = """
import abc
class A:
@property # comment
@abc.abstractmethod
def foo(self):
pass
"""
self.run_and_assert(tmpdir, original_code, new_code)
11 changes: 11 additions & 0 deletions tests/samples/deprecated_abstractproperty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from abc import abstractproperty as ap, abstractmethod


class A:
@ap
def foo(self):
pass

@abstractmethod
def bar(self):
pass

0 comments on commit 97cee8b

Please sign in to comment.