diff --git a/integration_tests/test_fix_deprecated_abstractproperty.py b/integration_tests/test_fix_deprecated_abstractproperty.py new file mode 100644 index 00000000..0f2055dc --- /dev/null +++ b/integration_tests/test_fix_deprecated_abstractproperty.py @@ -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 diff --git a/src/codemodder/scripts/generate_docs.py b/src/codemodder/scripts/generate_docs.py index 04194559..8ae4180c 100644 --- a/src/codemodder/scripts/generate_docs.py +++ b/src/codemodder/scripts/generate_docs.py @@ -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.", + ), } diff --git a/src/core_codemods/__init__.py b/src/core_codemods/__init__.py index 1b1f6a13..7b99fa26 100644 --- a/src/core_codemods/__init__.py +++ b/src/core_codemods/__init__.py @@ -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 @@ -41,6 +42,7 @@ DjangoDebugFlagOn, DjangoSessionCookieSecureOff, EnableJinja2Autoescape, + FixDeprecatedAbstractproperty, FixMutableParams, HardenPyyaml, HardenRuamel, diff --git a/src/core_codemods/docs/pixee_python_fix-deprecated-abstractproperty.md b/src/core_codemods/docs/pixee_python_fix-deprecated-abstractproperty.md new file mode 100644 index 00000000..ec4a52f9 --- /dev/null +++ b/src/core_codemods/docs/pixee_python_fix-deprecated-abstractproperty.md @@ -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(): + ... +``` diff --git a/src/core_codemods/fix_deprecated_abstractproperty.py b/src/core_codemods/fix_deprecated_abstractproperty.py new file mode 100644 index 00000000..3ed64a11 --- /dev/null +++ b/src/core_codemods/fix_deprecated_abstractproperty.py @@ -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 diff --git a/tests/codemods/test_fix_deprecated_abstractproperty.py b/tests/codemods/test_fix_deprecated_abstractproperty.py new file mode 100644 index 00000000..71150586 --- /dev/null +++ b/tests/codemods/test_fix_deprecated_abstractproperty.py @@ -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) diff --git a/tests/samples/deprecated_abstractproperty.py b/tests/samples/deprecated_abstractproperty.py new file mode 100644 index 00000000..f4ced273 --- /dev/null +++ b/tests/samples/deprecated_abstractproperty.py @@ -0,0 +1,11 @@ +from abc import abstractproperty as ap, abstractmethod + + +class A: + @ap + def foo(self): + pass + + @abstractmethod + def bar(self): + pass