openrewrite-migrate-python 0.15.1__tar.gz → 0.15.2__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.
Files changed (187) hide show
  1. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/PKG-INFO +2 -2
  2. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/pyproject.toml +2 -2
  3. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/_reindent.py +20 -3
  4. openrewrite_migrate_python-0.15.2/src/openrewrite_code_quality_python/codequality/_semantically_equal.py +38 -0
  5. openrewrite_migrate_python-0.15.2/src/openrewrite_code_quality_python/codequality/_side_effects.py +36 -0
  6. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/_trivia.py +6 -3
  7. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/all_branches_identical.py +34 -14
  8. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/boolean_checks_not_inverted.py +76 -11
  9. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/collapsible_if_statements.py +67 -46
  10. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/find_suspicious_chained_comparison.py +14 -21
  11. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/merge_identical_branches.py +21 -43
  12. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/remove_duplicate_conditions.py +13 -7
  13. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/remove_self_assignment.py +8 -4
  14. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/remove_unconditional_value_overwrite.py +6 -5
  15. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/simplify_boolean_literal.py +28 -34
  16. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/simplify_redundant_logical_expression.py +8 -7
  17. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/__init__.py +8 -0
  18. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/_decorators.py +24 -0
  19. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/_method_renames.py +37 -0
  20. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/_module_imports.py +50 -0
  21. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/aifc_migrations.py +271 -0
  22. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/array_deprecations.py +9 -50
  23. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/ast_deprecations.py +13 -110
  24. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/asyncio_coroutine_to_async.py +56 -54
  25. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/asyncio_deprecations.py +26 -25
  26. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/backoff_to_tenacity.py +707 -0
  27. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/calendar_deprecations.py +8 -38
  28. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/cgi_migrations.py +115 -0
  29. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/cgi_parse_deprecations.py +109 -0
  30. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/collections_abc_migrations.py +6 -11
  31. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/configparser_deprecations.py +115 -0
  32. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/datetime_utc.py +23 -41
  33. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/distutils_deprecations.py +9 -28
  34. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/distutils_migrations.py +7 -3
  35. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/functools_deprecations.py +17 -23
  36. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/future_imports.py +111 -0
  37. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/gettext_deprecations.py +80 -0
  38. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/html_parser_deprecations.py +89 -0
  39. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/imp_migrations.py +76 -0
  40. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/langchain_classic_imports.py +29 -48
  41. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/langchain_community_imports.py +9 -4
  42. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/langchain_provider_imports.py +10 -107
  43. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/locale_deprecations.py +85 -0
  44. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/locale_getdefaultlocale_deprecation.py +8 -7
  45. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/macpath_deprecations.py +66 -0
  46. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/mailcap_migrations.py +70 -0
  47. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/nntplib_migrations.py +68 -0
  48. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/os_deprecations.py +29 -48
  49. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/pathlib_deprecations.py +11 -14
  50. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/pep594_system_migrations.py +230 -0
  51. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/pipes_migrations.py +71 -0
  52. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/pkgutil_deprecations.py +30 -40
  53. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/platform_deprecations.py +91 -0
  54. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/pydantic_deprecations.py +85 -85
  55. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/pydantic_fields.py +9 -21
  56. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/pydantic_model_fields.py +5 -5
  57. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/re_deprecations.py +57 -54
  58. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/removed_modules_312.py +81 -0
  59. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/shutil_deprecations.py +8 -4
  60. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/socket_deprecations.py +8 -4
  61. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/ssl_deprecations.py +13 -42
  62. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/string_formatting.py +39 -20
  63. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/sys_deprecations.py +17 -27
  64. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/sys_last_deprecations.py +41 -21
  65. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/tarfile_deprecations.py +64 -0
  66. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/telnetlib_migrations.py +74 -0
  67. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/tempfile_deprecations.py +12 -20
  68. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/threading_deprecations.py +67 -172
  69. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/threading_is_alive_deprecation.py +6 -28
  70. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/typing_deprecations.py +25 -45
  71. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/typing_union_to_pipe.py +52 -41
  72. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/unittest_deprecations.py +6 -45
  73. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/upgrade_to_python39.py +0 -2
  74. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/urllib_deprecations.py +22 -51
  75. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/uu_migrations.py +70 -0
  76. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/xdrlib_migrations.py +70 -0
  77. openrewrite_migrate_python-0.15.2/src/openrewrite_migrate_python/migrate/xml_deprecations.py +131 -0
  78. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python.egg-info/PKG-INFO +2 -2
  79. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python.egg-info/SOURCES.txt +6 -3
  80. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python.egg-info/requires.txt +1 -1
  81. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_ast_deprecations.py +15 -0
  82. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_asyncio_coroutine_to_async.py +60 -2
  83. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_asyncio_deprecations.py +37 -0
  84. openrewrite_migrate_python-0.15.2/tests/test_backoff_to_tenacity.py +828 -0
  85. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_calendar_deprecations.py +15 -0
  86. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_cgi_parse_deprecations.py +17 -2
  87. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_configparser_deprecations.py +25 -0
  88. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_datetime_utc.py +31 -0
  89. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_distutils_deprecations.py +15 -0
  90. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_functools_deprecations.py +34 -0
  91. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_gettext_deprecations.py +9 -2
  92. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_html_parser_deprecations.py +56 -0
  93. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_langchain_classic_imports.py +11 -0
  94. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_langchain_provider_imports.py +19 -4
  95. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_locale_deprecations.py +21 -0
  96. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_os_deprecations.py +32 -2
  97. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_pathlib_deprecations.py +21 -0
  98. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_pep594_migrations.py +12 -0
  99. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_pkgutil_deprecations.py +44 -0
  100. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_platform_deprecations.py +47 -2
  101. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_pydantic_deprecations.py +146 -0
  102. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_re_deprecations.py +57 -0
  103. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_removed_modules_312.py +13 -0
  104. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_ssl_deprecations.py +22 -0
  105. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_string_formatting.py +101 -2
  106. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_sys_deprecations.py +9 -0
  107. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_sys_last_deprecations.py +44 -0
  108. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_tarfile_deprecations.py +29 -2
  109. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_tempfile_deprecations.py +21 -0
  110. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_threading_deprecations.py +27 -0
  111. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_typing_alias_moves.py +0 -23
  112. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_typing_deprecations.py +32 -0
  113. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_typing_union_to_pipe.py +68 -1
  114. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_unittest_deprecations.py +35 -0
  115. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_urllib_deprecations.py +20 -0
  116. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_xml_deprecations.py +19 -0
  117. openrewrite_migrate_python-0.15.1/src/openrewrite_code_quality_python/codequality/_semantically_equal.py +0 -32
  118. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/_from_imports.py +0 -68
  119. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/_import_hygiene.py +0 -55
  120. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/_typing_imports.py +0 -71
  121. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/aifc_migrations.py +0 -508
  122. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/cgi_migrations.py +0 -204
  123. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/cgi_parse_deprecations.py +0 -205
  124. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/configparser_deprecations.py +0 -254
  125. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/future_imports.py +0 -155
  126. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/gettext_deprecations.py +0 -148
  127. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/html_parser_deprecations.py +0 -106
  128. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/imp_migrations.py +0 -125
  129. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/locale_deprecations.py +0 -190
  130. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/macpath_deprecations.py +0 -116
  131. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/mailcap_migrations.py +0 -120
  132. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/nntplib_migrations.py +0 -118
  133. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/pep594_system_migrations.py +0 -420
  134. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/pipes_migrations.py +0 -121
  135. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/platform_deprecations.py +0 -173
  136. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/removed_modules_312.py +0 -145
  137. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/tarfile_deprecations.py +0 -113
  138. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/telnetlib_migrations.py +0 -123
  139. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/uu_migrations.py +0 -120
  140. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/xdrlib_migrations.py +0 -120
  141. openrewrite_migrate_python-0.15.1/src/openrewrite_migrate_python/migrate/xml_deprecations.py +0 -188
  142. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/setup.cfg +0 -0
  143. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/__init__.py +0 -0
  144. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/__init__.py +0 -0
  145. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_code_quality_python/codequality/code_quality.py +0 -0
  146. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/__init__.py +0 -0
  147. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/_langchain_tables.py +0 -0
  148. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/_markers.py +0 -0
  149. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/_slots.py +0 -0
  150. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/avro_migrations.py +0 -0
  151. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/typing_callable.py +0 -0
  152. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/upgrade_to_langchain02.py +0 -0
  153. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/upgrade_to_langchain1.py +0 -0
  154. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/upgrade_to_pydantic.py +0 -0
  155. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/upgrade_to_python310.py +0 -0
  156. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/upgrade_to_python311.py +0 -0
  157. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/upgrade_to_python312.py +0 -0
  158. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/upgrade_to_python313.py +0 -0
  159. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/upgrade_to_python314.py +0 -0
  160. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python/migrate/upgrade_to_python38.py +0 -0
  161. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python.egg-info/dependency_links.txt +0 -0
  162. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python.egg-info/entry_points.txt +0 -0
  163. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/src/openrewrite_migrate_python.egg-info/top_level.txt +0 -0
  164. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_aifc_migrations.py +0 -0
  165. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_array_deprecations.py +0 -0
  166. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_avro_migrations.py +0 -0
  167. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_change_import_recipes.py +0 -0
  168. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_collections_abc_migrations.py +0 -0
  169. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_distutils_migrations.py +0 -0
  170. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_future_imports.py +0 -0
  171. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_imp_migrations.py +0 -0
  172. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_langchain_community_imports.py +0 -0
  173. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_locale_getdefaultlocale_deprecation.py +0 -0
  174. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_macpath_deprecations.py +0 -0
  175. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_mailcap_migrations.py +0 -0
  176. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_markers.py +0 -0
  177. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_pep594_system_migrations.py +0 -0
  178. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_pydantic_fields.py +0 -0
  179. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_pydantic_model_fields.py +0 -0
  180. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_shutil_deprecations.py +0 -0
  181. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_socket_deprecations.py +0 -0
  182. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_threading_extended.py +0 -0
  183. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_threading_is_alive_deprecation.py +0 -0
  184. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_typing_callable.py +0 -0
  185. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_upgrade_recipes.py +0 -0
  186. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_upgrade_to_pydantic.py +0 -0
  187. {openrewrite_migrate_python-0.15.1 → openrewrite_migrate_python-0.15.2}/tests/test_xdrlib_migrations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrewrite-migrate-python
3
- Version: 0.15.1
3
+ Version: 0.15.2
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
@@ -19,7 +19,7 @@ Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.12
20
20
  Description-Content-Type: text/markdown
21
21
  Provides-Extra: dev
22
- Requires-Dist: openrewrite>=8.92.3; extra == "dev"
22
+ Requires-Dist: openrewrite>=8.92.18; extra == "dev"
23
23
  Requires-Dist: pydantic>=2.11; extra == "dev"
24
24
  Requires-Dist: pytest>=8.0.0; extra == "dev"
25
25
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
@@ -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.15.1" # Updated dynamically during release
8
+ version = "0.15.2" # 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"
@@ -28,7 +28,7 @@ dependencies = []
28
28
 
29
29
  [project.optional-dependencies]
30
30
  dev = [
31
- "openrewrite>=8.92.3",
31
+ "openrewrite>=8.92.18",
32
32
  # Needed so type-attribution tests can resolve `pydantic` types.
33
33
  "pydantic>=2.11",
34
34
  "pytest>=8.0.0",
@@ -4,12 +4,15 @@ Python indentation lives in the ``Space`` before each token, so moving a block
4
4
  to a shallower nesting level means rewriting every line it spans.
5
5
  """
6
6
 
7
- from typing import Optional, TypeVar
7
+ from typing import Optional, TypeVar, cast
8
8
 
9
+ from rewrite.java.markers import TrailingComma
9
10
  from rewrite.java.support_types import Space
11
+ from rewrite.java.tree import J
12
+ from rewrite.markers import Marker
10
13
  from rewrite.python.visitor import PythonVisitor
11
14
 
12
- T = TypeVar("T")
15
+ T = TypeVar("T", bound=J)
13
16
 
14
17
 
15
18
  def inherited_prefix(dropped: Space, body: Space) -> Space:
@@ -65,6 +68,20 @@ class _Reindent(PythonVisitor[None]):
65
68
  return space
66
69
  return Space(_comments=comments, _whitespace=whitespace)
67
70
 
71
+ def visit_marker(self, marker: Marker, p: None) -> Marker:
72
+ # The line break before the bracket closing a trailing-comma list
73
+ # lives on the comma.
74
+ if isinstance(marker, TrailingComma):
75
+ suffix = self.visit_space(marker.suffix, p)
76
+ if suffix is not marker.suffix:
77
+ return marker.replace(_suffix=suffix)
78
+ return marker
79
+
80
+
81
+ def reindent_space(space: Space, old_base: str, new_base: str) -> Space:
82
+ """:func:`reindent` for trivia that has no tree to travel with."""
83
+ return _Reindent(old_base, new_base).visit_space(space, None)
84
+
68
85
 
69
86
  def reindent(tree: T, old_base: str, new_base: str) -> T:
70
87
  """Re-base every line in *tree* that is indented at least to *old_base*.
@@ -74,4 +91,4 @@ def reindent(tree: T, old_base: str, new_base: str) -> T:
74
91
  """
75
92
  if old_base == new_base:
76
93
  return tree
77
- return _Reindent(old_base, new_base).visit(tree, None)
94
+ return cast(T, _Reindent(old_base, new_base).visit(tree, None))
@@ -0,0 +1,38 @@
1
+ """Semantic equality of LST nodes, for recipes that keep one of two equal subtrees."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from rewrite.java.support_types import J, Space
6
+ from rewrite.python.template.comparator import PythonComparatorVisitor
7
+ from rewrite.python.visitor import PythonVisitor
8
+ from rewrite.visitor import Cursor
9
+
10
+
11
+ class _CommentTexts(PythonVisitor[List[str]]):
12
+ def visit_space(self, space: Optional[Space], p: List[str]) -> Space:
13
+ if space is not None:
14
+ p.extend(comment.text for comment in space.comments)
15
+ return super().visit_space(space, p)
16
+
17
+
18
+ def _comment_texts(tree: J) -> List[str]:
19
+ texts: List[str] = []
20
+ _CommentTexts().visit(tree, texts)
21
+ return texts
22
+
23
+
24
+ class SemanticallyEqual:
25
+ """Compare two LST nodes for structural equality."""
26
+
27
+ @staticmethod
28
+ def are_equal(a: Optional[J], b: Optional[J]) -> bool:
29
+ """Whether *a* and *b* are the same code carrying the same comments.
30
+
31
+ Whitespace is ignored. Comments must match, so a recipe dropping either copy loses none.
32
+ """
33
+ if a is None or b is None:
34
+ return a is b
35
+ return (
36
+ PythonComparatorVisitor().compare(a, b, Cursor(None, Cursor.ROOT_VALUE))
37
+ and _comment_texts(a) == _comment_texts(b)
38
+ )
@@ -0,0 +1,36 @@
1
+ from rewrite.java.tree import (
2
+ Binary as JBinary,
3
+ Expression,
4
+ FieldAccess,
5
+ Identifier,
6
+ Literal,
7
+ Parentheses,
8
+ Ternary,
9
+ Unary,
10
+ )
11
+ from rewrite.python.tree import Binary as PyBinary
12
+
13
+
14
+ def is_side_effect_free(expr: Expression) -> bool:
15
+ """Whether evaluating ``expr`` is taken to have no side effects.
16
+
17
+ Names, literals, attribute reads and operators over them are taken as pure.
18
+ A call, subscript, walrus, ``await`` or any other node may run code that matters.
19
+ """
20
+ if isinstance(expr, (Identifier, Literal)):
21
+ return True
22
+ if isinstance(expr, FieldAccess):
23
+ return is_side_effect_free(expr.target)
24
+ if isinstance(expr, Parentheses):
25
+ return isinstance(expr.tree, Expression) and is_side_effect_free(expr.tree)
26
+ if isinstance(expr, Unary):
27
+ return is_side_effect_free(expr.expression)
28
+ if isinstance(expr, (JBinary, PyBinary)):
29
+ return is_side_effect_free(expr.left) and is_side_effect_free(expr.right)
30
+ if isinstance(expr, Ternary):
31
+ return (
32
+ is_side_effect_free(expr.condition)
33
+ and is_side_effect_free(expr.true_part)
34
+ and is_side_effect_free(expr.false_part)
35
+ )
36
+ return False
@@ -12,7 +12,7 @@ from typing import Optional, Tuple
12
12
 
13
13
  from rewrite import Cursor
14
14
  from rewrite.java.support_types import Space
15
- from rewrite.java.tree import Block, ClassDeclaration, If, MethodDeclaration
15
+ from rewrite.java.tree import Block, ClassDeclaration, If, J, MethodDeclaration
16
16
  from rewrite.python.tree import CompilationUnit
17
17
 
18
18
 
@@ -81,9 +81,11 @@ def space_after_block(cursor: Cursor) -> Optional[Space]:
81
81
  know how to look past.
82
82
  """
83
83
  child = cursor.value
84
+ if not isinstance(child, J):
85
+ return None
84
86
  for parent in islice(cursor.get_path(), 1, None):
85
87
  if isinstance(parent, (Block, CompilationUnit)):
86
- statements = parent._statements
88
+ statements = parent.padding.statements
87
89
  index = next(
88
90
  (i for i, s in enumerate(statements) if s.element.id == child.id),
89
91
  None,
@@ -125,7 +127,8 @@ def splice_out(
125
127
  continue
126
128
  if pending is not None:
127
129
  head, line = pending
128
- _, tail = split_line(padded.element.prefix)
130
+ prefix = padded.element.prefix
131
+ _, tail = split_line(prefix) or (Space.EMPTY, prefix)
129
132
  padded = padded.replace(
130
133
  _element=padded.element.replace(
131
134
  _prefix=concat(
@@ -15,7 +15,9 @@ from rewrite import ExecutionContext, Recipe, TreeVisitor
15
15
  from rewrite.category import CategoryDescriptor
16
16
  from rewrite.decorators import categorize
17
17
  from rewrite.marketplace import Python
18
+ from rewrite.python.tree import CompilationUnit
18
19
  from rewrite.python.visitor import PythonVisitor
20
+ from rewrite.java.tree import J
19
21
  from rewrite.java.tree import (
20
22
  Block,
21
23
  If,
@@ -27,6 +29,7 @@ from openrewrite_code_quality_python.codequality._reindent import (
27
29
  reindent,
28
30
  )
29
31
  from openrewrite_code_quality_python.codequality._semantically_equal import SemanticallyEqual
32
+ from openrewrite_code_quality_python.codequality._side_effects import is_side_effect_free
30
33
 
31
34
  _CodeQuality = [
32
35
  *Python,
@@ -68,6 +71,17 @@ def _collect_bodies(if_: If) -> list[Statement]:
68
71
  return bodies
69
72
 
70
73
 
74
+ def _conditions_side_effect_free(if_: If) -> bool:
75
+ current = if_
76
+ while True:
77
+ if not is_side_effect_free(current.if_condition.tree):
78
+ return False
79
+ else_part = current.else_part
80
+ if else_part is None or not isinstance(else_part.body, If):
81
+ return True
82
+ current = else_part.body
83
+
84
+
71
85
  def _all_bodies_equal(bodies: list[Statement]) -> bool:
72
86
  """Return True when every body is semantically equal to the first."""
73
87
  if len(bodies) < 2:
@@ -90,7 +104,8 @@ class AllBranchesIdentical(Recipe):
90
104
  and eliminates a source of confusion for future readers.
91
105
 
92
106
  Only applies when the chain has an explicit ``else`` so that the
93
- branches truly cover all cases.
107
+ branches truly cover all cases, and when no condition may have a
108
+ side effect that removing it would lose.
94
109
 
95
110
  Example:
96
111
  Before:
@@ -116,7 +131,8 @@ class AllBranchesIdentical(Recipe):
116
131
  return (
117
132
  "Replace `if`/`elif`/`else` chains where every branch has "
118
133
  "the same body with just the body, since the condition has "
119
- "no effect on what code executes."
134
+ "no effect on what code executes. A chain whose conditions may "
135
+ "have side effects, such as a call, is left alone."
120
136
  )
121
137
 
122
138
  @property
@@ -130,19 +146,23 @@ class AllBranchesIdentical(Recipe):
130
146
  def editor(self) -> TreeVisitor[Any, ExecutionContext]:
131
147
  class Visitor(PythonVisitor[ExecutionContext]):
132
148
  def visit_compilation_unit(self, cu, p):
133
- cu = super().visit_compilation_unit(cu, p)
134
- new_stmts = _replace_identical_ifs_padded(cu._statements)
149
+ visited = super().visit_compilation_unit(cu, p)
150
+ if not isinstance(visited, CompilationUnit):
151
+ return visited
152
+ cu = visited
153
+ new_stmts = _replace_identical_ifs_padded(cu.padding.statements)
135
154
  if new_stmts is not None:
136
155
  return cu.replace(_statements=new_stmts)
137
156
  return cu
138
157
 
139
158
  def visit_block(
140
159
  self, block: Block, p: ExecutionContext
141
- ) -> Optional[Block]:
142
- block = super().visit_block(block, p)
143
- if not isinstance(block, Block):
144
- return block
145
- new_stmts = _replace_identical_ifs_padded(block._statements)
160
+ ) -> J:
161
+ visited = super().visit_block(block, p)
162
+ if not isinstance(visited, Block):
163
+ return visited
164
+ block = visited
165
+ new_stmts = _replace_identical_ifs_padded(block.padding.statements)
146
166
  if new_stmts is not None:
147
167
  return block.replace(_statements=new_stmts)
148
168
  return block
@@ -163,7 +183,7 @@ def _replace_identical_ifs_padded(padded_stmts) -> Optional[list]:
163
183
  if not isinstance(s, If):
164
184
  result.append(rp)
165
185
  continue
166
- if not _has_explicit_else(s):
186
+ if not _has_explicit_else(s) or not _conditions_side_effect_free(s):
167
187
  result.append(rp)
168
188
  continue
169
189
  bodies = _collect_bodies(s)
@@ -172,12 +192,12 @@ def _replace_identical_ifs_padded(padded_stmts) -> Optional[list]:
172
192
  continue
173
193
 
174
194
  body = s.then_part
175
- if not isinstance(body, Block) or not body._statements:
195
+ if not isinstance(body, Block) or not body.padding.statements:
176
196
  result.append(rp)
177
197
  continue
178
198
 
179
199
  if_indent = line_indent(s.prefix)
180
- body_indent = line_indent(body._statements[0].element.prefix)
200
+ body_indent = line_indent(body.padding.statements[0].element.prefix)
181
201
  if (
182
202
  if_indent is None
183
203
  or body_indent is None
@@ -187,7 +207,7 @@ def _replace_identical_ifs_padded(padded_stmts) -> Optional[list]:
187
207
  continue
188
208
 
189
209
  body = reindent(body, body_indent, if_indent)
190
- first = body._statements[0]
210
+ first = body.padding.statements[0]
191
211
  # Each lifted statement keeps its own padding, which is where a `;`
192
212
  # separating it from the next one is recorded.
193
213
  result.append(
@@ -197,7 +217,7 @@ def _replace_identical_ifs_padded(padded_stmts) -> Optional[list]:
197
217
  )
198
218
  )
199
219
  )
200
- result.extend(body._statements[1:])
220
+ result.extend(body.padding.statements[1:])
201
221
  changed = True
202
222
 
203
223
  if not changed:
@@ -8,18 +8,23 @@ when written as ``a != b``, and ``not (not x)`` is just ``x``.
8
8
  See: https://rules.sonarsource.com/python/RSPEC-S1940/
9
9
  """
10
10
 
11
- from typing import Any, List, Optional
11
+ from typing import Any, List
12
12
 
13
- from rewrite import ExecutionContext, Recipe, TreeVisitor
13
+ from rewrite import Cursor, ExecutionContext, Recipe, TreeVisitor
14
14
  from rewrite.category import CategoryDescriptor
15
15
  from rewrite.decorators import categorize
16
16
  from rewrite.marketplace import Python
17
+ from rewrite.python.tree import Binary as PyBinary
17
18
  from rewrite.python.visitor import PythonVisitor
18
- from rewrite.java.support_types import JLeftPadded, Space
19
+ from rewrite.java.tree import J
20
+ from rewrite.java.support_types import JavaType, JLeftPadded
19
21
  from rewrite.java.tree import (
22
+ Assert,
20
23
  Binary as JBinary,
24
+ ControlParentheses,
21
25
  Expression,
22
26
  Parentheses,
27
+ Ternary,
23
28
  Unary,
24
29
  )
25
30
 
@@ -37,6 +42,59 @@ _OPERATOR_INVERSE = {
37
42
  JBinary.Type.GreaterThanOrEqual: JBinary.Type.LessThan,
38
43
  }
39
44
 
45
+ _EQUALITY = {JBinary.Type.Equal, JBinary.Type.NotEqual}
46
+
47
+ _COMPARISONS = {
48
+ *_OPERATOR_INVERSE,
49
+ PyBinary.Type.In,
50
+ PyBinary.Type.NotIn,
51
+ PyBinary.Type.Is,
52
+ PyBinary.Type.IsNot,
53
+ }
54
+
55
+ _NUMERIC = {
56
+ JavaType.Primitive.Boolean,
57
+ JavaType.Primitive.Int,
58
+ JavaType.Primitive.Long,
59
+ JavaType.Primitive.Double,
60
+ JavaType.Primitive.Float,
61
+ }
62
+
63
+
64
+ def _is_comparison(expr: Expression) -> bool:
65
+ return isinstance(expr, (JBinary, PyBinary)) and expr.operator in _COMPARISONS
66
+
67
+
68
+ def _is_invertible(comparison: JBinary) -> bool:
69
+ # `a < b < c` is one chained comparison whose last link is the outer node.
70
+ if _is_comparison(comparison.left) or _is_comparison(comparison.right):
71
+ return False
72
+ # Only numbers are totally ordered. A set orders by inclusion.
73
+ return comparison.operator in _EQUALITY or (
74
+ comparison.left.type in _NUMERIC and comparison.right.type in _NUMERIC
75
+ )
76
+
77
+
78
+ def _is_read_as_truth_value(cursor: Cursor) -> bool:
79
+ node = cursor.value
80
+ parent = cursor.parent_tree_cursor()
81
+ while isinstance(parent.value, Parentheses):
82
+ node = parent.value
83
+ parent = parent.parent_tree_cursor()
84
+ value = parent.value
85
+ if isinstance(value, ControlParentheses):
86
+ return True
87
+ if isinstance(value, Unary):
88
+ return value.operator == Unary.Type.Not
89
+ if isinstance(value, Ternary):
90
+ return value.condition is node
91
+ if isinstance(value, Assert):
92
+ return value.condition is node
93
+ # `a or b` yields an operand, so its operands are truth values only if it is one.
94
+ if isinstance(value, JBinary) and value.operator in (JBinary.Type.And, JBinary.Type.Or):
95
+ return _is_read_as_truth_value(parent)
96
+ return False
97
+
40
98
 
41
99
  @categorize(_CodeQuality)
42
100
  class BooleanChecksNotInverted(Recipe):
@@ -71,7 +129,10 @@ class BooleanChecksNotInverted(Recipe):
71
129
  return (
72
130
  "Replace inverted boolean comparisons like `not (a == b)` with the "
73
131
  "equivalent direct operator (`a != b`), and remove double negations "
74
- "like `not (not x)`."
132
+ "like `not (not x)`. An ordering such as `not (a < b)` is only "
133
+ "inverted where both operands' types resolve to numbers, a chained "
134
+ "comparison is left alone, and a double negation is only removed "
135
+ "where its value is read as a truth value, as in a condition."
75
136
  )
76
137
 
77
138
  @property
@@ -86,11 +147,11 @@ class BooleanChecksNotInverted(Recipe):
86
147
  class Visitor(PythonVisitor[ExecutionContext]):
87
148
  def visit_unary(
88
149
  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
150
+ ) -> J:
151
+ visited = super().visit_unary(unary, p)
152
+ if not isinstance(visited, Unary):
153
+ return visited
154
+ unary = visited
94
155
 
95
156
  if unary.operator != Unary.Type.Not:
96
157
  return unary
@@ -103,11 +164,15 @@ class BooleanChecksNotInverted(Recipe):
103
164
  inner = inner.tree
104
165
 
105
166
  # Double negation: not (not x) -> x
106
- if isinstance(inner, Unary) and inner.operator == Unary.Type.Not:
167
+ if (
168
+ isinstance(inner, Unary)
169
+ and inner.operator == Unary.Type.Not
170
+ and _is_read_as_truth_value(self.cursor)
171
+ ):
107
172
  return inner.expression.replace(_prefix=unary.prefix)
108
173
 
109
174
  # Inverted comparison: not (a == b) -> a != b
110
- if isinstance(inner, JBinary):
175
+ if isinstance(inner, JBinary) and _is_invertible(inner):
111
176
  inverse = _OPERATOR_INVERSE.get(inner.operator)
112
177
  if inverse is not None:
113
178
  return inner.padding.replace(
@@ -9,34 +9,33 @@ easier to follow.
9
9
  See: https://rules.sonarsource.com/python/RSPEC-S1066/
10
10
  """
11
11
 
12
- from typing import Any, List, Optional
12
+ from typing import Any, List
13
13
 
14
14
  from rewrite import ExecutionContext, Recipe, TreeVisitor
15
15
  from rewrite.category import CategoryDescriptor
16
16
  from rewrite.decorators import categorize
17
17
  from rewrite.marketplace import Python
18
18
  from rewrite.markers import Markers
19
- from rewrite.python.tree import StatementExpression
19
+ from rewrite.python.template.precedence import maybe_parenthesize
20
20
  from rewrite.python.visitor import PythonVisitor
21
- from rewrite.java.support_types import JLeftPadded, JRightPadded, Space
21
+ from rewrite.java.support_types import Comment, JLeftPadded, JRightPadded, Space
22
22
  from rewrite.java.tree import (
23
- Assignment,
24
23
  Binary as JBinary,
25
24
  Block,
26
25
  ControlParentheses,
27
- Expression,
28
26
  If,
29
- Lambda,
30
- Parentheses,
31
- Ternary,
27
+ J,
32
28
  )
33
29
  from rewrite.utils import random_id
34
30
 
35
31
  from openrewrite_code_quality_python.codequality._reindent import (
36
- inherited_prefix,
37
32
  line_indent,
38
33
  reindent,
34
+ reindent_space,
39
35
  )
36
+ from openrewrite_code_quality_python.codequality._trivia import concat, split_line
37
+
38
+ _HOISTED_COMMENTS = "collapsible_if_statements.hoisted_comments"
40
39
 
41
40
  _CodeQuality = [
42
41
  *Python,
@@ -44,33 +43,12 @@ _CodeQuality = [
44
43
  ]
45
44
 
46
45
 
47
- def _binds_looser_than_and(expr: Expression) -> bool:
48
- """Return True for the operand forms that ``and`` would capture.
49
-
50
- Everything below ``and`` in Python's precedence table: ``or``, the
51
- conditional expression, ``lambda``, ``:=`` (a ``J.Assignment`` here) and
52
- ``yield`` (a ``Py.StatementExpression``).
53
- """
54
- if isinstance(expr, JBinary):
55
- return expr.operator == JBinary.Type.Or
56
- return isinstance(expr, (Ternary, Lambda, Assignment, StatementExpression))
57
-
58
-
59
- def _operand(expr: Expression, prefix: Space) -> Expression:
60
- """Prepare *expr* as an operand of the merged ``and``, parenthesizing it
61
- when its own operator would otherwise be captured."""
62
- if _binds_looser_than_and(expr):
63
- return Parentheses(
64
- _id=random_id(),
65
- _prefix=prefix,
66
- _markers=Markers.EMPTY,
67
- _tree=JRightPadded(
68
- _element=expr.replace(_prefix=Space.EMPTY),
69
- _after=Space.EMPTY,
70
- _markers=Markers.EMPTY,
71
- ),
72
- )
73
- return expr.replace(_prefix=prefix)
46
+ def _with_comments_above(prefix: Space, comments: List[Comment]) -> Space:
47
+ """*prefix* with *comments* on their own lines just before the token it
48
+ leads into. Each comment's suffix must already end at that token's indent."""
49
+ if not comments:
50
+ return prefix
51
+ return Space(_comments=[*prefix.comments, *comments], _whitespace=prefix.whitespace)
74
52
 
75
53
 
76
54
  @categorize(_CodeQuality)
@@ -119,13 +97,20 @@ class CollapsibleIfStatements(Recipe):
119
97
 
120
98
  def editor(self) -> TreeVisitor[Any, ExecutionContext]:
121
99
  class Visitor(PythonVisitor[ExecutionContext]):
100
+ def visit_else(self, else_: If.Else, p: ExecutionContext) -> J:
101
+ visited = super().visit_else(else_, p)
102
+ hoisted = self.cursor.get_message(_HOISTED_COMMENTS, [])
103
+ if not isinstance(visited, If.Else) or not hoisted:
104
+ return visited
105
+ return visited.replace(_prefix=_with_comments_above(visited.prefix, hoisted))
106
+
122
107
  def visit_if(
123
108
  self, if_: If, p: ExecutionContext
124
- ) -> Optional[If]:
125
- if_ = super().visit_if(if_, p)
126
-
127
- if not isinstance(if_, If):
128
- return if_
109
+ ) -> J:
110
+ visited = super().visit_if(if_, p)
111
+ if not isinstance(visited, If):
112
+ return visited
113
+ if_ = visited
129
114
 
130
115
  # Outer if must have no else/elif
131
116
  if if_.else_part is not None:
@@ -164,19 +149,50 @@ class CollapsibleIfStatements(Recipe):
164
149
  ):
165
150
  return if_
166
151
 
152
+ # An `elif` starts its line at the `elif` keyword, which the
153
+ # enclosing `Else` holds.
154
+ parent = self.cursor.parent_tree_cursor()
155
+ under_elif = isinstance(parent.value, If.Else)
156
+ outer_indent = line_indent(
157
+ parent.value.prefix if isinstance(parent.value, If.Else) else if_.prefix
158
+ )
159
+ split = split_line(inner.prefix)
160
+ if (
161
+ outer_indent is None
162
+ or not header_indent.startswith(outer_indent)
163
+ or split is None
164
+ ):
165
+ return if_
166
+ # The outer header's trailing comment stays on the merged
167
+ # header. Comments on their own line before the inner `if`
168
+ # describe its condition, so they go above the merged header
169
+ # without the blank lines around them.
170
+ header_trailer, inner_leader = split
171
+ hoisted = reindent_space(
172
+ Space(_comments=inner_leader.comments, _whitespace=""),
173
+ header_indent,
174
+ outer_indent,
175
+ ).comments
176
+
167
177
  combined = JBinary(
168
178
  _id=random_id(),
169
179
  _prefix=if_.if_condition.tree.prefix,
170
180
  _markers=Markers.EMPTY,
171
- _left=_operand(if_.if_condition.tree, Space.EMPTY),
181
+ _left=if_.if_condition.tree.replace(_prefix=Space.EMPTY),
172
182
  _operator=JLeftPadded(
173
183
  _before=Space.SINGLE_SPACE,
174
184
  _element=JBinary.Type.And,
175
185
  _markers=Markers.EMPTY,
176
186
  ),
177
- _right=_operand(inner.if_condition.tree, Space.SINGLE_SPACE),
187
+ _right=reindent(inner.if_condition.tree, header_indent, outer_indent).replace(
188
+ _prefix=Space.SINGLE_SPACE
189
+ ),
178
190
  _type=None,
179
191
  )
192
+ combined = combined.replace(
193
+ _left=maybe_parenthesize(combined, combined.left.id, combined.left),
194
+ _right=maybe_parenthesize(combined, combined.right.id, combined.right),
195
+ )
180
196
 
181
197
  new_condition = ControlParentheses(
182
198
  _id=if_.if_condition.id,
@@ -193,18 +209,23 @@ class CollapsibleIfStatements(Recipe):
193
209
  # level at a time, and each pass rebases the comments the
194
210
  # previous one left at the level it dropped.
195
211
  inner_body = reindent(inner_body, body_indent, header_indent)
196
- first = inner_body._statements[0]
212
+ first = inner_body.padding.statements[0]
197
213
  inner_body = inner_body.replace(
198
214
  _statements=[
199
215
  first.replace(
200
216
  _element=first.element.replace(
201
- _prefix=inherited_prefix(inner.prefix, first.element.prefix)
217
+ _prefix=concat(header_trailer, first.element.prefix)
202
218
  )
203
219
  ),
204
- *inner_body._statements[1:],
220
+ *inner_body.padding.statements[1:],
205
221
  ]
206
222
  )
207
223
 
224
+ if under_elif:
225
+ parent.put_message(_HOISTED_COMMENTS, hoisted)
226
+ else:
227
+ if_ = if_.replace(_prefix=_with_comments_above(if_.prefix, hoisted))
228
+
208
229
  return if_.replace(
209
230
  _if_condition=new_condition,
210
231
  ).padding.replace(