diff --git a/integration_tests/test_fix_deprecated_logging_warn.py b/integration_tests/test_fix_deprecated_logging_warn.py new file mode 100644 index 000000000..caa08ff75 --- /dev/null +++ b/integration_tests/test_fix_deprecated_logging_warn.py @@ -0,0 +1,16 @@ +from core_codemods.fix_deprecated_logging_warn import FixDeprecatedLoggingWarn +from integration_tests.base_test import ( + BaseIntegrationTest, + original_and_expected_from_code_path, +) + + +class TestFixDeprecatedLoggingWarn(BaseIntegrationTest): + codemod = FixDeprecatedLoggingWarn + code_path = "tests/samples/fix_deprecated_logging_warn.py" + original_code, expected_new_code = original_and_expected_from_code_path( + code_path, [(3, 'log.warning("hello")\n')] + ) + expected_diff = '--- \n+++ \n@@ -1,4 +1,4 @@\n import logging\n \n log = logging.getLogger("my logger")\n-log.warn("hello")\n+log.warning("hello")\n' + expected_line_change = "4" + change_description = FixDeprecatedLoggingWarn.CHANGE_DESCRIPTION diff --git a/src/codemodder/scripts/generate_docs.py b/src/codemodder/scripts/generate_docs.py index 0c50b66dc..815c2e624 100644 --- a/src/codemodder/scripts/generate_docs.py +++ b/src/codemodder/scripts/generate_docs.py @@ -194,6 +194,10 @@ class DocMetadata: importance="Medium", guidance_explained="Breakpoints are generally used only for debugging and can easily be forgotten before deploying code.", ), + "fix-deprecated-logging-warn": 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 6302ed14a..4fc67d3a6 100644 --- a/src/core_codemods/__init__.py +++ b/src/core_codemods/__init__.py @@ -42,7 +42,7 @@ from .subprocess_shell_false import SubprocessShellFalse from .remove_module_global import RemoveModuleGlobal from .remove_debug_breakpoint import RemoveDebugBreakpoint - +from .fix_deprecated_logging_warn import FixDeprecatedLoggingWarn registry = CodemodCollection( origin="pixee", @@ -91,5 +91,6 @@ LiteralOrNewObjectIdentity, RemoveModuleGlobal, RemoveDebugBreakpoint, + FixDeprecatedLoggingWarn, ], ) diff --git a/src/core_codemods/docs/pixee_python_fix-deprecated-logging-warn.md b/src/core_codemods/docs/pixee_python_fix-deprecated-logging-warn.md new file mode 100644 index 000000000..d620f8636 --- /dev/null +++ b/src/core_codemods/docs/pixee_python_fix-deprecated-logging-warn.md @@ -0,0 +1,13 @@ +The `warn` method from `logging` has been [deprecated](https://docs.python.org/3/library/logging.html#logging.Logger.warning) since Python 3.3. + +Our changes look like the following: +```diff + import logging + +- logging.warn("hello") ++ logging.warning("hello") + ... + log = logging.getLogger("my logger") +- log.warn("hello") ++ log.warning("hello") +``` diff --git a/src/core_codemods/fix_deprecated_logging_warn.py b/src/core_codemods/fix_deprecated_logging_warn.py new file mode 100644 index 000000000..6025d2475 --- /dev/null +++ b/src/core_codemods/fix_deprecated_logging_warn.py @@ -0,0 +1,55 @@ +import libcst as cst +from codemodder.codemods.api import SemgrepCodemod, ReviewGuidance +from codemodder.codemods.utils_mixin import NameResolutionMixin + + +class FixDeprecatedLoggingWarn(SemgrepCodemod, NameResolutionMixin): + NAME = "fix-deprecated-logging-warn" + SUMMARY = "Replace Deprecated `logging.warn`" + REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW + DESCRIPTION = "Replace deprecated `logging.warn` with `logging.warning`" + REFERENCES = [ + { + "url": "https://docs.python.org/3/library/logging.html#logging.Logger.warning", + "description": "", + }, + ] + _module_name = "logging" + + @classmethod + def rule(cls): + return """ + rules: + - pattern-either: + - patterns: + - pattern: logging.warn(...) + - pattern-inside: | + import logging + ... + - patterns: + - pattern: logging.getLogger(...).warn(...) + - pattern-inside: | + import logging + ... + - patterns: + - pattern: $VAR.warn(...) + - pattern-inside: | + import logging + ... + $VAR = logging.getLogger(...) + ... + + """ + + def on_result_found(self, original_node, updated_node): + warning = cst.Name(value="warning") + match original_node.func: + case cst.Name(): + self.add_needed_import(self._module_name, "warning") + self.remove_unused_import(original_node.func) + return updated_node.with_changes(func=warning) + case cst.Attribute(): + return updated_node.with_changes( + func=updated_node.func.with_changes(attr=warning) + ) + return original_node diff --git a/tests/codemods/test_fix_deprecated_logging_warn.py b/tests/codemods/test_fix_deprecated_logging_warn.py new file mode 100644 index 000000000..f368c881d --- /dev/null +++ b/tests/codemods/test_fix_deprecated_logging_warn.py @@ -0,0 +1,87 @@ +import pytest +from core_codemods.fix_deprecated_logging_warn import FixDeprecatedLoggingWarn +from tests.codemods.base_codemod_test import BaseSemgrepCodemodTest + + +class TestFixDeprecatedLoggingWarn(BaseSemgrepCodemodTest): + codemod = FixDeprecatedLoggingWarn + + @pytest.mark.parametrize( + "code", + [ + """ + import logging + logging.{}('something') + """, + """ + import logging + log = logging.getLogger('anything') + log.{}('something') + """, + ], + ) + def test_import(self, tmpdir, code): + original_code = code.format("warn") + new_code = code.format("warning") + self.run_and_assert(tmpdir, original_code, new_code) + assert len(self.file_context.codemod_changes) == 1 + + @pytest.mark.parametrize( + "code", + [ + """ + from logging import {0} + {0}('something') + """, + """ + from logging import getLogger + getLogger('anything').{0}('something') + """, + ], + ) + def test_from_import(self, tmpdir, code): + original_code = code.format("warn") + new_code = code.format("warning") + self.run_and_assert(tmpdir, original_code, new_code) + assert len(self.file_context.codemod_changes) == 1 + + @pytest.mark.parametrize( + "input_code,expected_output", + [ + ( + """from logging import warn as warn_func +warn_func('something')""", + """from logging import warning +warning('something')""", + ), + ( + """from logging import getLogger as make_logger +logger = make_logger('anything') +logger.warn('something')""", + """from logging import getLogger as make_logger +logger = make_logger('anything') +logger.warning('something')""", + ), + ], + ) + def test_import_alias(self, tmpdir, input_code, expected_output): + self.run_and_assert(tmpdir, input_code, expected_output) + assert len(self.file_context.codemod_changes) == 1 + + @pytest.mark.parametrize( + "code", + [ + """ + import xyz + xyz.warn('something') + """, + """ + import my_logging + log = my_logging.getLogger('anything') + log.warn('something') + """, + ], + ) + def test_different_warn(self, tmpdir, code): + self.run_and_assert(tmpdir, code, code) + assert len(self.file_context.codemod_changes) == 0 diff --git a/tests/samples/fix_deprecated_logging_warn.py b/tests/samples/fix_deprecated_logging_warn.py new file mode 100644 index 000000000..3a1f8b9d4 --- /dev/null +++ b/tests/samples/fix_deprecated_logging_warn.py @@ -0,0 +1,4 @@ +import logging + +log = logging.getLogger("my logger") +log.warn("hello")