openrewrite-migrate-python 0.5.0.dev20260407172149__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/PKG-INFO +1 -1
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/pyproject.toml +3 -2
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/__init__.py +35 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/__init__.py +23 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/_semantically_equal.py +32 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/all_branches_identical.py +206 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/boolean_checks_not_inverted.py +123 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/collapsible_if_statements.py +195 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/merge_identical_branches.py +242 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/remove_duplicate_conditions.py +190 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/remove_self_assignment.py +152 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/remove_unconditional_value_overwrite.py +207 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/simplify_boolean_literal.py +167 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_code_quality_python/codequality/simplify_redundant_logical_expression.py +93 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/__init__.py +12 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/pkgutil_deprecations.py +34 -46
- openrewrite_migrate_python-0.6.0/src/openrewrite_migrate_python/migrate/typing_callable.py +62 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_migrate_python/migrate/typing_deprecations.py +406 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python.egg-info/PKG-INFO +1 -1
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python.egg-info/SOURCES.txt +12 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python.egg-info/entry_points.txt +1 -0
- openrewrite_migrate_python-0.6.0/src/openrewrite_migrate_python.egg-info/top_level.txt +2 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_pkgutil_deprecations.py +8 -10
- openrewrite_migrate_python-0.6.0/tests/test_typing_callable.py +34 -0
- openrewrite_migrate_python-0.6.0/tests/test_typing_deprecations.py +236 -0
- openrewrite_migrate_python-0.5.0.dev20260407172149/src/openrewrite_migrate_python/migrate/typing_callable.py +0 -142
- openrewrite_migrate_python-0.5.0.dev20260407172149/src/openrewrite_migrate_python/migrate/typing_deprecations.py +0 -326
- openrewrite_migrate_python-0.5.0.dev20260407172149/src/openrewrite_migrate_python.egg-info/top_level.txt +0 -1
- openrewrite_migrate_python-0.5.0.dev20260407172149/tests/test_typing_callable.py +0 -57
- openrewrite_migrate_python-0.5.0.dev20260407172149/tests/test_typing_deprecations.py +0 -36
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/setup.cfg +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/__init__.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/_markers.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/aifc_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/array_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/ast_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/asyncio_coroutine_to_async.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/asyncio_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/calendar_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/cgi_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/cgi_parse_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/collections_abc_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/configparser_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/datetime_utc.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/distutils_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/distutils_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/functools_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/future_imports.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/gettext_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/html_parser_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/imp_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/langchain_classic_imports.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/langchain_community_imports.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/langchain_provider_imports.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/locale_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/locale_getdefaultlocale_deprecation.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/macpath_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/mailcap_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/nntplib_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/os_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/pathlib_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/pep594_system_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/pipes_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/platform_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/re_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/removed_modules_312.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/shutil_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/socket_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/ssl_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/string_formatting.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/sys_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/sys_last_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/tarfile_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/telnetlib_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/tempfile_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/threading_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/threading_is_alive_deprecation.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/typing_union_to_pipe.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/unittest_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/upgrade_to_langchain02.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/upgrade_to_langchain1.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/upgrade_to_python310.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/upgrade_to_python311.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/upgrade_to_python312.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/upgrade_to_python313.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/upgrade_to_python314.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/upgrade_to_python38.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/upgrade_to_python39.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/urllib_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/uu_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/xdrlib_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python/migrate/xml_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python.egg-info/dependency_links.txt +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/src/openrewrite_migrate_python.egg-info/requires.txt +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_aifc_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_array_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_ast_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_asyncio_coroutine_to_async.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_asyncio_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_calendar_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_cgi_parse_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_change_import_recipes.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_collections_abc_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_configparser_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_datetime_utc.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_distutils_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_future_imports.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_gettext_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_html_parser_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_imp_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_langchain_classic_imports.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_langchain_community_imports.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_langchain_provider_imports.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_locale_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_locale_getdefaultlocale_deprecation.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_macpath_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_os_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_pep594_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_pep594_system_migrations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_platform_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_re_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_removed_modules_312.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_shutil_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_ssl_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_string_formatting.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_sys_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_sys_last_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_tarfile_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_threading_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_threading_extended.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_threading_is_alive_deprecation.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_typing_union_to_pipe.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_unittest_deprecations.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_upgrade_recipes.py +0 -0
- {openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/tests/test_xml_deprecations.py +0 -0
{openrewrite_migrate_python-0.5.0.dev20260407172149 → openrewrite_migrate_python-0.6.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openrewrite-migrate-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: OpenRewrite recipes for migrating Python codebases to newer Python versions.
|
|
5
5
|
Author-email: "Moderne Inc." <support@moderne.io>
|
|
6
6
|
License: Moderne Proprietary
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openrewrite-migrate-python"
|
|
7
7
|
description = "OpenRewrite recipes for migrating Python codebases to newer Python versions."
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.6.0" # Updated dynamically during release
|
|
9
9
|
authors = [{ name = "Moderne Inc.", email = "support@moderne.io" }]
|
|
10
10
|
license = { text = "Moderne Proprietary" }
|
|
11
11
|
readme = "README.md"
|
|
@@ -49,10 +49,11 @@ Issues = "https://github.com/openrewrite/rewrite-migrate-python/issues"
|
|
|
49
49
|
|
|
50
50
|
[project.entry-points."openrewrite.recipes"]
|
|
51
51
|
migrate_python = "openrewrite_migrate_python:activate"
|
|
52
|
+
code_quality_python = "openrewrite_code_quality_python:activate"
|
|
52
53
|
|
|
53
54
|
[tool.setuptools.packages.find]
|
|
54
55
|
where = ["src"]
|
|
55
|
-
include = ["openrewrite_migrate_python", "openrewrite_migrate_python.*"]
|
|
56
|
+
include = ["openrewrite_migrate_python", "openrewrite_migrate_python.*", "openrewrite_code_quality_python", "openrewrite_code_quality_python.*"]
|
|
56
57
|
|
|
57
58
|
[tool.setuptools.package-data]
|
|
58
59
|
"*" = ["py.typed", "*.pyi"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""OpenRewrite recipes for Python code quality improvements."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import pkgutil
|
|
6
|
+
|
|
7
|
+
from rewrite import Recipe, RecipeMarketplace
|
|
8
|
+
|
|
9
|
+
__all__ = ["activate"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def activate(marketplace: RecipeMarketplace) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Install all code quality recipes into the marketplace.
|
|
15
|
+
|
|
16
|
+
This function is called by the OpenRewrite discovery mechanism when the
|
|
17
|
+
openrewrite-code-quality-python package is found via entry points.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
marketplace: The RecipeMarketplace to install recipes into
|
|
21
|
+
"""
|
|
22
|
+
from rewrite.decorators import get_recipe_category
|
|
23
|
+
|
|
24
|
+
from . import codequality as codequality_pkg
|
|
25
|
+
|
|
26
|
+
# Auto-discover all Recipe subclasses from the codequality package
|
|
27
|
+
for module_info in pkgutil.walk_packages(
|
|
28
|
+
codequality_pkg.__path__, prefix=codequality_pkg.__name__ + "."
|
|
29
|
+
):
|
|
30
|
+
module = importlib.import_module(module_info.name)
|
|
31
|
+
for _name, obj in inspect.getmembers(module, inspect.isclass):
|
|
32
|
+
if issubclass(obj, Recipe) and obj is not Recipe and obj.__module__ == module.__name__:
|
|
33
|
+
category = get_recipe_category(obj)
|
|
34
|
+
if category is not None:
|
|
35
|
+
marketplace.install(obj, category)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Python code quality recipes."""
|
|
2
|
+
|
|
3
|
+
from .all_branches_identical import AllBranchesIdentical
|
|
4
|
+
from .boolean_checks_not_inverted import BooleanChecksNotInverted
|
|
5
|
+
from .collapsible_if_statements import CollapsibleIfStatements
|
|
6
|
+
from .merge_identical_branches import MergeIdenticalBranches
|
|
7
|
+
from .remove_duplicate_conditions import RemoveDuplicateConditions
|
|
8
|
+
from .remove_self_assignment import RemoveSelfAssignment
|
|
9
|
+
from .remove_unconditional_value_overwrite import RemoveUnconditionalValueOverwrite
|
|
10
|
+
from .simplify_boolean_literal import SimplifyBooleanLiteral
|
|
11
|
+
from .simplify_redundant_logical_expression import SimplifyRedundantLogicalExpression
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AllBranchesIdentical",
|
|
15
|
+
"BooleanChecksNotInverted",
|
|
16
|
+
"CollapsibleIfStatements",
|
|
17
|
+
"MergeIdenticalBranches",
|
|
18
|
+
"RemoveDuplicateConditions",
|
|
19
|
+
"RemoveSelfAssignment",
|
|
20
|
+
"RemoveUnconditionalValueOverwrite",
|
|
21
|
+
"SimplifyBooleanLiteral",
|
|
22
|
+
"SimplifyRedundantLogicalExpression",
|
|
23
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structural equality comparison for LST nodes.
|
|
3
|
+
|
|
4
|
+
Compares two tree nodes by printing them with normalized whitespace
|
|
5
|
+
and checking that the printed representations match. This provides
|
|
6
|
+
the same functionality as ``SemanticallyEqual`` from the Java SDK.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from rewrite.java.support_types import Space
|
|
10
|
+
from rewrite.python.printer import PythonPrinter
|
|
11
|
+
|
|
12
|
+
_printer = PythonPrinter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SemanticallyEqual:
|
|
16
|
+
"""Compare two LST nodes for structural equality."""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def are_equal(a, b) -> bool:
|
|
20
|
+
"""Return True if *a* and *b* are semantically equal.
|
|
21
|
+
|
|
22
|
+
Both nodes are printed after stripping their leading prefix so
|
|
23
|
+
that insignificant whitespace differences are ignored.
|
|
24
|
+
"""
|
|
25
|
+
if a is None and b is None:
|
|
26
|
+
return True
|
|
27
|
+
if a is None or b is None:
|
|
28
|
+
return False
|
|
29
|
+
return (
|
|
30
|
+
_printer.print(a.replace(_prefix=Space.EMPTY))
|
|
31
|
+
== _printer.print(b.replace(_prefix=Space.EMPTY))
|
|
32
|
+
)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recipe to replace if/elif/else chains where every branch has the same body.
|
|
3
|
+
|
|
4
|
+
When all branches of a conditional execute identical code the condition
|
|
5
|
+
is meaningless. The entire ``if``/``elif``/``else`` chain can be
|
|
6
|
+
replaced with a single copy of the body, removing unnecessary
|
|
7
|
+
complexity and making it obvious that the code always runs.
|
|
8
|
+
|
|
9
|
+
See: https://rules.sonarsource.com/python/RSPEC-S3923/
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, List, Optional
|
|
13
|
+
|
|
14
|
+
from rewrite import ExecutionContext, Recipe, TreeVisitor
|
|
15
|
+
from rewrite.category import CategoryDescriptor
|
|
16
|
+
from rewrite.decorators import categorize
|
|
17
|
+
from rewrite.marketplace import Python
|
|
18
|
+
from rewrite.python.visitor import PythonVisitor
|
|
19
|
+
from rewrite.java.support_types import Space
|
|
20
|
+
from rewrite.java.tree import (
|
|
21
|
+
Block,
|
|
22
|
+
If,
|
|
23
|
+
Statement,
|
|
24
|
+
)
|
|
25
|
+
from openrewrite_code_quality_python.codequality._semantically_equal import SemanticallyEqual
|
|
26
|
+
|
|
27
|
+
_CodeQuality = [
|
|
28
|
+
*Python,
|
|
29
|
+
CategoryDescriptor(display_name="Code quality"),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _has_explicit_else(if_: If) -> bool:
|
|
34
|
+
"""Return True if the chain ends with a plain ``else`` (not elif)."""
|
|
35
|
+
current = if_
|
|
36
|
+
while True:
|
|
37
|
+
else_part = current.else_part
|
|
38
|
+
if else_part is None:
|
|
39
|
+
return False
|
|
40
|
+
branch = else_part.body
|
|
41
|
+
if isinstance(branch, If):
|
|
42
|
+
current = branch
|
|
43
|
+
else:
|
|
44
|
+
# Reached a plain else block
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _collect_bodies(if_: If) -> list[Statement]:
|
|
49
|
+
"""Collect the then-part of every branch including the final else."""
|
|
50
|
+
bodies: list[Statement] = [if_.then_part]
|
|
51
|
+
current = if_
|
|
52
|
+
while True:
|
|
53
|
+
else_part = current.else_part
|
|
54
|
+
if else_part is None:
|
|
55
|
+
break
|
|
56
|
+
branch = else_part.body
|
|
57
|
+
if isinstance(branch, If):
|
|
58
|
+
bodies.append(branch.then_part)
|
|
59
|
+
current = branch
|
|
60
|
+
else:
|
|
61
|
+
# Plain else body
|
|
62
|
+
bodies.append(branch)
|
|
63
|
+
break
|
|
64
|
+
return bodies
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _all_bodies_equal(bodies: list[Statement]) -> bool:
|
|
68
|
+
"""Return True when every body is semantically equal to the first."""
|
|
69
|
+
if len(bodies) < 2:
|
|
70
|
+
return False
|
|
71
|
+
first = bodies[0]
|
|
72
|
+
return all(
|
|
73
|
+
SemanticallyEqual.are_equal(first, body)
|
|
74
|
+
for body in bodies[1:]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@categorize(_CodeQuality)
|
|
79
|
+
class AllBranchesIdentical(Recipe):
|
|
80
|
+
"""
|
|
81
|
+
Replace ``if``/``elif``/``else`` chains where every branch has
|
|
82
|
+
the same body with just the body.
|
|
83
|
+
|
|
84
|
+
When every branch of a conditional executes identical code, the
|
|
85
|
+
condition serves no purpose. Removing it makes the code shorter
|
|
86
|
+
and eliminates a source of confusion for future readers.
|
|
87
|
+
|
|
88
|
+
Only applies when the chain has an explicit ``else`` so that the
|
|
89
|
+
branches truly cover all cases.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
Before:
|
|
93
|
+
if cond:
|
|
94
|
+
x()
|
|
95
|
+
else:
|
|
96
|
+
x()
|
|
97
|
+
|
|
98
|
+
After:
|
|
99
|
+
x()
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def name(self) -> str:
|
|
104
|
+
return "org.openrewrite.python.codequality.AllBranchesIdentical"
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def display_name(self) -> str:
|
|
108
|
+
return "Remove conditional with identical branches"
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def description(self) -> str:
|
|
112
|
+
return (
|
|
113
|
+
"Replace `if`/`elif`/`else` chains where every branch has "
|
|
114
|
+
"the same body with just the body, since the condition has "
|
|
115
|
+
"no effect on what code executes."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def tags(self) -> List[str]:
|
|
120
|
+
return ["python", "code-quality", "RSPEC-S3923"]
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def estimated_effort_per_occurrence(self) -> int:
|
|
124
|
+
return 5
|
|
125
|
+
|
|
126
|
+
def editor(self) -> TreeVisitor[Any, ExecutionContext]:
|
|
127
|
+
class Visitor(PythonVisitor[ExecutionContext]):
|
|
128
|
+
def visit_compilation_unit(self, cu, p):
|
|
129
|
+
cu = super().visit_compilation_unit(cu, p)
|
|
130
|
+
new_stmts = _replace_identical_ifs_padded(cu._statements)
|
|
131
|
+
if new_stmts is not None:
|
|
132
|
+
return cu.replace(_statements=new_stmts)
|
|
133
|
+
return cu
|
|
134
|
+
|
|
135
|
+
def visit_block(
|
|
136
|
+
self, block: Block, p: ExecutionContext
|
|
137
|
+
) -> Optional[Block]:
|
|
138
|
+
block = super().visit_block(block, p)
|
|
139
|
+
if not isinstance(block, Block):
|
|
140
|
+
return block
|
|
141
|
+
new_stmts = _replace_identical_ifs_padded(block._statements)
|
|
142
|
+
if new_stmts is not None:
|
|
143
|
+
return block.replace(_statements=new_stmts)
|
|
144
|
+
return block
|
|
145
|
+
|
|
146
|
+
return Visitor()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _replace_identical_ifs_padded(padded_stmts) -> Optional[list]:
|
|
150
|
+
"""Replace if/elif/else with identical branches with just the body.
|
|
151
|
+
|
|
152
|
+
Works on a list of JRightPadded[Statement] and returns a new padded list,
|
|
153
|
+
or None if nothing changed.
|
|
154
|
+
"""
|
|
155
|
+
from rewrite.java.support_types import JRightPadded
|
|
156
|
+
from rewrite.markers import Markers
|
|
157
|
+
|
|
158
|
+
result = []
|
|
159
|
+
changed = False
|
|
160
|
+
for rp in padded_stmts:
|
|
161
|
+
s = rp.element
|
|
162
|
+
if not isinstance(s, If):
|
|
163
|
+
result.append(rp)
|
|
164
|
+
continue
|
|
165
|
+
if not _has_explicit_else(s):
|
|
166
|
+
result.append(rp)
|
|
167
|
+
continue
|
|
168
|
+
bodies = _collect_bodies(s)
|
|
169
|
+
if not _all_bodies_equal(bodies):
|
|
170
|
+
result.append(rp)
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
# Extract the body statements from the then_part Block.
|
|
174
|
+
body = s.then_part
|
|
175
|
+
if isinstance(body, Block) and body._statements:
|
|
176
|
+
first = True
|
|
177
|
+
for body_rp in body._statements:
|
|
178
|
+
bs = body_rp.element
|
|
179
|
+
if first:
|
|
180
|
+
# First body statement gets the if statement's prefix
|
|
181
|
+
new_elem = bs.replace(_prefix=s.prefix)
|
|
182
|
+
result.append(rp.replace(_element=new_elem))
|
|
183
|
+
first = False
|
|
184
|
+
else:
|
|
185
|
+
# Subsequent body statements keep their whitespace
|
|
186
|
+
# but with one indent level removed
|
|
187
|
+
ws = bs.prefix.whitespace
|
|
188
|
+
if_ws = s.prefix.whitespace
|
|
189
|
+
base = if_ws.split("\n")[-1] if "\n" in if_ws else if_ws
|
|
190
|
+
body_indent = ws.split("\n")[-1] if "\n" in ws else ws
|
|
191
|
+
if len(body_indent) > len(base):
|
|
192
|
+
new_ws = ws.replace(body_indent, base, 1)
|
|
193
|
+
else:
|
|
194
|
+
new_ws = ws
|
|
195
|
+
new_elem = bs.replace(_prefix=Space(
|
|
196
|
+
_comments=bs.prefix.comments,
|
|
197
|
+
_whitespace=new_ws,
|
|
198
|
+
))
|
|
199
|
+
result.append(body_rp.replace(_element=new_elem))
|
|
200
|
+
changed = True
|
|
201
|
+
else:
|
|
202
|
+
result.append(rp)
|
|
203
|
+
|
|
204
|
+
if not changed:
|
|
205
|
+
return None
|
|
206
|
+
return result
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recipe to simplify inverted boolean checks.
|
|
3
|
+
|
|
4
|
+
Negating a comparison operator is harder to read than using the
|
|
5
|
+
opposite operator directly. For example, ``not (a == b)`` is clearer
|
|
6
|
+
when written as ``a != b``, and ``not (not x)`` is just ``x``.
|
|
7
|
+
|
|
8
|
+
See: https://rules.sonarsource.com/python/RSPEC-S1940/
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any, List, Optional
|
|
12
|
+
|
|
13
|
+
from rewrite import ExecutionContext, Recipe, TreeVisitor
|
|
14
|
+
from rewrite.category import CategoryDescriptor
|
|
15
|
+
from rewrite.decorators import categorize
|
|
16
|
+
from rewrite.marketplace import Python
|
|
17
|
+
from rewrite.python.visitor import PythonVisitor
|
|
18
|
+
from rewrite.java.support_types import JLeftPadded, Space
|
|
19
|
+
from rewrite.java.tree import (
|
|
20
|
+
Binary as JBinary,
|
|
21
|
+
Expression,
|
|
22
|
+
Parentheses,
|
|
23
|
+
Unary,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_CodeQuality = [
|
|
27
|
+
*Python,
|
|
28
|
+
CategoryDescriptor(display_name="Code quality"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
_OPERATOR_INVERSE = {
|
|
32
|
+
JBinary.Type.Equal: JBinary.Type.NotEqual,
|
|
33
|
+
JBinary.Type.NotEqual: JBinary.Type.Equal,
|
|
34
|
+
JBinary.Type.LessThan: JBinary.Type.GreaterThanOrEqual,
|
|
35
|
+
JBinary.Type.GreaterThan: JBinary.Type.LessThanOrEqual,
|
|
36
|
+
JBinary.Type.LessThanOrEqual: JBinary.Type.GreaterThan,
|
|
37
|
+
JBinary.Type.GreaterThanOrEqual: JBinary.Type.LessThan,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@categorize(_CodeQuality)
|
|
42
|
+
class BooleanChecksNotInverted(Recipe):
|
|
43
|
+
"""
|
|
44
|
+
Replace inverted boolean checks with the equivalent direct comparison.
|
|
45
|
+
|
|
46
|
+
Writing ``not (a == b)`` obscures the intent when ``a != b`` says the
|
|
47
|
+
same thing more directly. Likewise ``not (not x)`` is simply ``x``.
|
|
48
|
+
Flipping the operator or removing the double negation makes the
|
|
49
|
+
condition immediately obvious to the reader.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
Before:
|
|
53
|
+
if not (a == b):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
After:
|
|
57
|
+
if a != b:
|
|
58
|
+
pass
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def name(self) -> str:
|
|
63
|
+
return "org.openrewrite.python.codequality.BooleanChecksNotInverted"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def display_name(self) -> str:
|
|
67
|
+
return "Boolean checks should not be inverted"
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def description(self) -> str:
|
|
71
|
+
return (
|
|
72
|
+
"Replace inverted boolean comparisons like `not (a == b)` with the "
|
|
73
|
+
"equivalent direct operator (`a != b`), and remove double negations "
|
|
74
|
+
"like `not (not x)`."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def tags(self) -> List[str]:
|
|
79
|
+
return ["python", "code-quality", "RSPEC-S1940"]
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def estimated_effort_per_occurrence(self) -> int:
|
|
83
|
+
return 2
|
|
84
|
+
|
|
85
|
+
def editor(self) -> TreeVisitor[Any, ExecutionContext]:
|
|
86
|
+
class Visitor(PythonVisitor[ExecutionContext]):
|
|
87
|
+
def visit_unary(
|
|
88
|
+
self, unary: Unary, p: ExecutionContext
|
|
89
|
+
) -> Optional[Expression]:
|
|
90
|
+
unary = super().visit_unary(unary, p)
|
|
91
|
+
|
|
92
|
+
if not isinstance(unary, Unary):
|
|
93
|
+
return unary
|
|
94
|
+
|
|
95
|
+
if unary.operator != Unary.Type.Not:
|
|
96
|
+
return unary
|
|
97
|
+
|
|
98
|
+
expr = unary.expression
|
|
99
|
+
|
|
100
|
+
# Unwrap parentheses to inspect inner expression
|
|
101
|
+
inner = expr
|
|
102
|
+
if isinstance(inner, Parentheses):
|
|
103
|
+
inner = inner.tree
|
|
104
|
+
|
|
105
|
+
# Double negation: not (not x) -> x
|
|
106
|
+
if isinstance(inner, Unary) and inner.operator == Unary.Type.Not:
|
|
107
|
+
return inner.expression.replace(_prefix=unary.prefix)
|
|
108
|
+
|
|
109
|
+
# Inverted comparison: not (a == b) -> a != b
|
|
110
|
+
if isinstance(inner, JBinary):
|
|
111
|
+
inverse = _OPERATOR_INVERSE.get(inner.operator)
|
|
112
|
+
if inverse is not None:
|
|
113
|
+
return inner.padding.replace(
|
|
114
|
+
_operator=JLeftPadded(
|
|
115
|
+
_before=inner.padding.operator.before,
|
|
116
|
+
_element=inverse,
|
|
117
|
+
_markers=inner.padding.operator.markers,
|
|
118
|
+
)
|
|
119
|
+
).replace(_prefix=unary.prefix)
|
|
120
|
+
|
|
121
|
+
return unary
|
|
122
|
+
|
|
123
|
+
return Visitor()
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recipe to merge collapsible ``if`` statements.
|
|
3
|
+
|
|
4
|
+
When an ``if`` block contains only another ``if`` and neither has an
|
|
5
|
+
``else`` branch, the two conditions can be combined with ``and`` into
|
|
6
|
+
a single ``if``. This reduces nesting and makes the control flow
|
|
7
|
+
easier to follow.
|
|
8
|
+
|
|
9
|
+
See: https://rules.sonarsource.com/python/RSPEC-S1066/
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, List, Optional
|
|
13
|
+
|
|
14
|
+
from rewrite import ExecutionContext, Recipe, TreeVisitor
|
|
15
|
+
from rewrite.category import CategoryDescriptor
|
|
16
|
+
from rewrite.decorators import categorize
|
|
17
|
+
from rewrite.marketplace import Python
|
|
18
|
+
from rewrite.markers import Markers
|
|
19
|
+
from rewrite.python.visitor import PythonVisitor
|
|
20
|
+
from rewrite.java.support_types import JLeftPadded, JRightPadded, Space
|
|
21
|
+
from rewrite.java.tree import (
|
|
22
|
+
Binary as JBinary,
|
|
23
|
+
Block,
|
|
24
|
+
ControlParentheses,
|
|
25
|
+
Expression,
|
|
26
|
+
If,
|
|
27
|
+
Parentheses,
|
|
28
|
+
)
|
|
29
|
+
from rewrite.utils import random_id
|
|
30
|
+
|
|
31
|
+
_CodeQuality = [
|
|
32
|
+
*Python,
|
|
33
|
+
CategoryDescriptor(display_name="Code quality"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _contains_or(expr: Expression) -> bool:
|
|
38
|
+
"""Return True if the top-level expression is an ``or`` binary."""
|
|
39
|
+
return isinstance(expr, JBinary) and expr.operator == JBinary.Type.Or
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _dedent_block(block: Block, indent: str) -> Block:
|
|
43
|
+
"""Remove one level of *indent* from every statement in *block*."""
|
|
44
|
+
if not indent:
|
|
45
|
+
return block
|
|
46
|
+
new_stmts = []
|
|
47
|
+
for rp in block._statements:
|
|
48
|
+
s = rp.element
|
|
49
|
+
ws = s.prefix.whitespace
|
|
50
|
+
if indent in ws:
|
|
51
|
+
ws = ws.replace(indent, "", 1)
|
|
52
|
+
s = s.replace(_prefix=Space(_comments=s.prefix.comments, _whitespace=ws))
|
|
53
|
+
rp = rp.replace(_element=s)
|
|
54
|
+
new_stmts.append(rp)
|
|
55
|
+
return block.replace(_statements=new_stmts)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@categorize(_CodeQuality)
|
|
59
|
+
class CollapsibleIfStatements(Recipe):
|
|
60
|
+
"""
|
|
61
|
+
Merge nested ``if`` statements into a single ``if`` with ``and``.
|
|
62
|
+
|
|
63
|
+
An ``if`` whose body is nothing but another ``if``--and neither has
|
|
64
|
+
an ``else`` clause--can always be rewritten as one ``if`` that
|
|
65
|
+
combines both conditions with ``and``. The merged form is shorter,
|
|
66
|
+
has less indentation, and communicates the same logic.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
Before:
|
|
70
|
+
if a:
|
|
71
|
+
if b:
|
|
72
|
+
do_something()
|
|
73
|
+
|
|
74
|
+
After:
|
|
75
|
+
if a and b:
|
|
76
|
+
do_something()
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def name(self) -> str:
|
|
81
|
+
return "org.openrewrite.python.codequality.CollapsibleIfStatements"
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def display_name(self) -> str:
|
|
85
|
+
return "Merge collapsible if statements"
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def description(self) -> str:
|
|
89
|
+
return (
|
|
90
|
+
"Combine nested `if` statements that have no `else` branch into "
|
|
91
|
+
"a single `if` joined with `and`."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def tags(self) -> List[str]:
|
|
96
|
+
return ["python", "code-quality", "RSPEC-S1066"]
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def estimated_effort_per_occurrence(self) -> int:
|
|
100
|
+
return 5
|
|
101
|
+
|
|
102
|
+
def editor(self) -> TreeVisitor[Any, ExecutionContext]:
|
|
103
|
+
class Visitor(PythonVisitor[ExecutionContext]):
|
|
104
|
+
def visit_if(
|
|
105
|
+
self, if_: If, p: ExecutionContext
|
|
106
|
+
) -> Optional[If]:
|
|
107
|
+
if_ = super().visit_if(if_, p)
|
|
108
|
+
|
|
109
|
+
if not isinstance(if_, If):
|
|
110
|
+
return if_
|
|
111
|
+
|
|
112
|
+
# Outer if must have no else/elif
|
|
113
|
+
if if_.else_part is not None:
|
|
114
|
+
return if_
|
|
115
|
+
|
|
116
|
+
then = if_.then_part
|
|
117
|
+
if not isinstance(then, Block):
|
|
118
|
+
return if_
|
|
119
|
+
|
|
120
|
+
stmts = then.statements
|
|
121
|
+
if len(stmts) != 1:
|
|
122
|
+
return if_
|
|
123
|
+
|
|
124
|
+
inner = stmts[0]
|
|
125
|
+
if not isinstance(inner, If):
|
|
126
|
+
return if_
|
|
127
|
+
|
|
128
|
+
# Inner if must have no else/elif
|
|
129
|
+
if inner.else_part is not None:
|
|
130
|
+
return if_
|
|
131
|
+
|
|
132
|
+
outer_cond = if_.if_condition.tree
|
|
133
|
+
inner_cond = inner.if_condition.tree
|
|
134
|
+
|
|
135
|
+
# Wrap inner condition in parens if it contains `or`
|
|
136
|
+
# to preserve precedence with the new `and`
|
|
137
|
+
if _contains_or(inner_cond):
|
|
138
|
+
inner_cond = Parentheses(
|
|
139
|
+
_id=random_id(),
|
|
140
|
+
_prefix=Space.SINGLE_SPACE,
|
|
141
|
+
_markers=Markers.EMPTY,
|
|
142
|
+
_tree=JRightPadded(
|
|
143
|
+
_element=inner_cond.replace(_prefix=Space.EMPTY),
|
|
144
|
+
_after=Space.EMPTY,
|
|
145
|
+
_markers=Markers.EMPTY,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
inner_cond = inner_cond.replace(_prefix=Space.SINGLE_SPACE)
|
|
150
|
+
|
|
151
|
+
combined = JBinary(
|
|
152
|
+
_id=random_id(),
|
|
153
|
+
_prefix=outer_cond.prefix,
|
|
154
|
+
_markers=Markers.EMPTY,
|
|
155
|
+
_left=outer_cond.replace(_prefix=Space.EMPTY),
|
|
156
|
+
_operator=JLeftPadded(
|
|
157
|
+
_before=Space.SINGLE_SPACE,
|
|
158
|
+
_element=JBinary.Type.And,
|
|
159
|
+
_markers=Markers.EMPTY,
|
|
160
|
+
),
|
|
161
|
+
_right=inner_cond,
|
|
162
|
+
_type=None,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
new_condition = ControlParentheses(
|
|
166
|
+
_id=if_.if_condition.id,
|
|
167
|
+
_prefix=if_.if_condition.prefix,
|
|
168
|
+
_markers=if_.if_condition.markers,
|
|
169
|
+
_tree=JRightPadded(
|
|
170
|
+
_element=combined,
|
|
171
|
+
_after=if_.if_condition.padding.tree.after,
|
|
172
|
+
_markers=if_.if_condition.padding.tree.markers,
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Dedent the inner body by one level since it was
|
|
177
|
+
# nested inside the inner if that we're removing.
|
|
178
|
+
inner_body = inner.then_part
|
|
179
|
+
if isinstance(inner_body, Block):
|
|
180
|
+
# The inner if's prefix tells us the indent unit
|
|
181
|
+
ws = inner.prefix.whitespace
|
|
182
|
+
indent = ws.split("\n")[-1] if "\n" in ws else ws
|
|
183
|
+
inner_body = _dedent_block(inner_body, indent)
|
|
184
|
+
|
|
185
|
+
return if_.replace(
|
|
186
|
+
_if_condition=new_condition,
|
|
187
|
+
).padding.replace(
|
|
188
|
+
_then_part=JRightPadded(
|
|
189
|
+
_element=inner_body,
|
|
190
|
+
_after=if_.padding.then_part.after,
|
|
191
|
+
_markers=if_.padding.then_part.markers,
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return Visitor()
|