openrewrite-migrate-python 0.15.0.dev20260922170519__tar.gz → 0.15.1__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 (160) hide show
  1. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/PKG-INFO +1 -1
  2. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/pyproject.toml +1 -1
  3. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/__init__.py +2 -0
  4. openrewrite_migrate_python-0.15.1/src/openrewrite_code_quality_python/codequality/code_quality.py +73 -0
  5. openrewrite_migrate_python-0.15.1/src/openrewrite_code_quality_python/codequality/remove_unconditional_value_overwrite.py +333 -0
  6. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/string_formatting.py +31 -30
  7. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python.egg-info/PKG-INFO +1 -1
  8. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python.egg-info/SOURCES.txt +1 -0
  9. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_string_formatting.py +48 -13
  10. openrewrite_migrate_python-0.15.0.dev20260922170519/src/openrewrite_code_quality_python/codequality/remove_unconditional_value_overwrite.py +0 -258
  11. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/setup.cfg +0 -0
  12. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/__init__.py +0 -0
  13. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/_reindent.py +0 -0
  14. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/_semantically_equal.py +0 -0
  15. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/_trivia.py +0 -0
  16. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/all_branches_identical.py +0 -0
  17. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/boolean_checks_not_inverted.py +0 -0
  18. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/collapsible_if_statements.py +0 -0
  19. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/find_suspicious_chained_comparison.py +0 -0
  20. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/merge_identical_branches.py +0 -0
  21. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/remove_duplicate_conditions.py +0 -0
  22. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/remove_self_assignment.py +0 -0
  23. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/simplify_boolean_literal.py +0 -0
  24. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_code_quality_python/codequality/simplify_redundant_logical_expression.py +0 -0
  25. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/__init__.py +0 -0
  26. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/__init__.py +0 -0
  27. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/_from_imports.py +0 -0
  28. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/_import_hygiene.py +0 -0
  29. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/_langchain_tables.py +0 -0
  30. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/_markers.py +0 -0
  31. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/_slots.py +0 -0
  32. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/_typing_imports.py +0 -0
  33. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/aifc_migrations.py +0 -0
  34. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/array_deprecations.py +0 -0
  35. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/ast_deprecations.py +0 -0
  36. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/asyncio_coroutine_to_async.py +0 -0
  37. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/asyncio_deprecations.py +0 -0
  38. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/avro_migrations.py +0 -0
  39. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/calendar_deprecations.py +0 -0
  40. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/cgi_migrations.py +0 -0
  41. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/cgi_parse_deprecations.py +0 -0
  42. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/collections_abc_migrations.py +0 -0
  43. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/configparser_deprecations.py +0 -0
  44. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/datetime_utc.py +0 -0
  45. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/distutils_deprecations.py +0 -0
  46. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/distutils_migrations.py +0 -0
  47. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/functools_deprecations.py +0 -0
  48. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/future_imports.py +0 -0
  49. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/gettext_deprecations.py +0 -0
  50. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/html_parser_deprecations.py +0 -0
  51. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/imp_migrations.py +0 -0
  52. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/langchain_classic_imports.py +0 -0
  53. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/langchain_community_imports.py +0 -0
  54. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/langchain_provider_imports.py +0 -0
  55. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/locale_deprecations.py +0 -0
  56. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/locale_getdefaultlocale_deprecation.py +0 -0
  57. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/macpath_deprecations.py +0 -0
  58. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/mailcap_migrations.py +0 -0
  59. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/nntplib_migrations.py +0 -0
  60. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/os_deprecations.py +0 -0
  61. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/pathlib_deprecations.py +0 -0
  62. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/pep594_system_migrations.py +0 -0
  63. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/pipes_migrations.py +0 -0
  64. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/pkgutil_deprecations.py +0 -0
  65. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/platform_deprecations.py +0 -0
  66. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/pydantic_deprecations.py +0 -0
  67. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/pydantic_fields.py +0 -0
  68. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/pydantic_model_fields.py +0 -0
  69. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/re_deprecations.py +0 -0
  70. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/removed_modules_312.py +0 -0
  71. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/shutil_deprecations.py +0 -0
  72. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/socket_deprecations.py +0 -0
  73. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/ssl_deprecations.py +0 -0
  74. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/sys_deprecations.py +0 -0
  75. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/sys_last_deprecations.py +0 -0
  76. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/tarfile_deprecations.py +0 -0
  77. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/telnetlib_migrations.py +0 -0
  78. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/tempfile_deprecations.py +0 -0
  79. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/threading_deprecations.py +0 -0
  80. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/threading_is_alive_deprecation.py +0 -0
  81. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/typing_callable.py +0 -0
  82. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/typing_deprecations.py +0 -0
  83. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/typing_union_to_pipe.py +0 -0
  84. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/unittest_deprecations.py +0 -0
  85. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/upgrade_to_langchain02.py +0 -0
  86. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/upgrade_to_langchain1.py +0 -0
  87. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/upgrade_to_pydantic.py +0 -0
  88. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/upgrade_to_python310.py +0 -0
  89. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/upgrade_to_python311.py +0 -0
  90. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/upgrade_to_python312.py +0 -0
  91. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/upgrade_to_python313.py +0 -0
  92. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/upgrade_to_python314.py +0 -0
  93. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/upgrade_to_python38.py +0 -0
  94. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/upgrade_to_python39.py +0 -0
  95. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/urllib_deprecations.py +0 -0
  96. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/uu_migrations.py +0 -0
  97. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/xdrlib_migrations.py +0 -0
  98. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python/migrate/xml_deprecations.py +0 -0
  99. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python.egg-info/dependency_links.txt +0 -0
  100. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python.egg-info/entry_points.txt +0 -0
  101. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python.egg-info/requires.txt +0 -0
  102. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/src/openrewrite_migrate_python.egg-info/top_level.txt +0 -0
  103. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_aifc_migrations.py +0 -0
  104. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_array_deprecations.py +0 -0
  105. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_ast_deprecations.py +0 -0
  106. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_asyncio_coroutine_to_async.py +0 -0
  107. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_asyncio_deprecations.py +0 -0
  108. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_avro_migrations.py +0 -0
  109. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_calendar_deprecations.py +0 -0
  110. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_cgi_parse_deprecations.py +0 -0
  111. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_change_import_recipes.py +0 -0
  112. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_collections_abc_migrations.py +0 -0
  113. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_configparser_deprecations.py +0 -0
  114. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_datetime_utc.py +0 -0
  115. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_distutils_deprecations.py +0 -0
  116. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_distutils_migrations.py +0 -0
  117. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_functools_deprecations.py +0 -0
  118. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_future_imports.py +0 -0
  119. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_gettext_deprecations.py +0 -0
  120. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_html_parser_deprecations.py +0 -0
  121. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_imp_migrations.py +0 -0
  122. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_langchain_classic_imports.py +0 -0
  123. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_langchain_community_imports.py +0 -0
  124. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_langchain_provider_imports.py +0 -0
  125. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_locale_deprecations.py +0 -0
  126. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_locale_getdefaultlocale_deprecation.py +0 -0
  127. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_macpath_deprecations.py +0 -0
  128. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_mailcap_migrations.py +0 -0
  129. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_markers.py +0 -0
  130. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_os_deprecations.py +0 -0
  131. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_pathlib_deprecations.py +0 -0
  132. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_pep594_migrations.py +0 -0
  133. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_pep594_system_migrations.py +0 -0
  134. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_pkgutil_deprecations.py +0 -0
  135. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_platform_deprecations.py +0 -0
  136. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_pydantic_deprecations.py +0 -0
  137. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_pydantic_fields.py +0 -0
  138. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_pydantic_model_fields.py +0 -0
  139. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_re_deprecations.py +0 -0
  140. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_removed_modules_312.py +0 -0
  141. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_shutil_deprecations.py +0 -0
  142. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_socket_deprecations.py +0 -0
  143. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_ssl_deprecations.py +0 -0
  144. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_sys_deprecations.py +0 -0
  145. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_sys_last_deprecations.py +0 -0
  146. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_tarfile_deprecations.py +0 -0
  147. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_tempfile_deprecations.py +0 -0
  148. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_threading_deprecations.py +0 -0
  149. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_threading_extended.py +0 -0
  150. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_threading_is_alive_deprecation.py +0 -0
  151. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_typing_alias_moves.py +0 -0
  152. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_typing_callable.py +0 -0
  153. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_typing_deprecations.py +0 -0
  154. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_typing_union_to_pipe.py +0 -0
  155. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_unittest_deprecations.py +0 -0
  156. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_upgrade_recipes.py +0 -0
  157. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_upgrade_to_pydantic.py +0 -0
  158. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_urllib_deprecations.py +0 -0
  159. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_xdrlib_migrations.py +0 -0
  160. {openrewrite_migrate_python-0.15.0.dev20260922170519 → openrewrite_migrate_python-0.15.1}/tests/test_xml_deprecations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrewrite-migrate-python
3
- Version: 0.15.0.dev20260922170519
3
+ Version: 0.15.1
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.15.0.dev20260922170519" # Updated dynamically during release
8
+ version = "0.15.1" # 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"
@@ -2,6 +2,7 @@
2
2
 
3
3
  from .all_branches_identical import AllBranchesIdentical
4
4
  from .boolean_checks_not_inverted import BooleanChecksNotInverted
5
+ from .code_quality import CodeQuality
5
6
  from .collapsible_if_statements import CollapsibleIfStatements
6
7
  from .merge_identical_branches import MergeIdenticalBranches
7
8
  from .remove_duplicate_conditions import RemoveDuplicateConditions
@@ -13,6 +14,7 @@ from .simplify_redundant_logical_expression import SimplifyRedundantLogicalExpre
13
14
  __all__ = [
14
15
  "AllBranchesIdentical",
15
16
  "BooleanChecksNotInverted",
17
+ "CodeQuality",
16
18
  "CollapsibleIfStatements",
17
19
  "MergeIdenticalBranches",
18
20
  "RemoveDuplicateConditions",
@@ -0,0 +1,73 @@
1
+ """Composite recipe bundling the Python code quality fixes."""
2
+
3
+ from typing import List
4
+
5
+ from rewrite import Recipe
6
+ from rewrite.category import CategoryDescriptor
7
+ from rewrite.decorators import categorize
8
+ from rewrite.marketplace import Python
9
+
10
+ from .all_branches_identical import AllBranchesIdentical
11
+ from .boolean_checks_not_inverted import BooleanChecksNotInverted
12
+ from .collapsible_if_statements import CollapsibleIfStatements
13
+ from .merge_identical_branches import MergeIdenticalBranches
14
+ from .remove_duplicate_conditions import RemoveDuplicateConditions
15
+ from .remove_self_assignment import RemoveSelfAssignment
16
+ from .remove_unconditional_value_overwrite import RemoveUnconditionalValueOverwrite
17
+ from .simplify_boolean_literal import SimplifyBooleanLiteral
18
+ from .simplify_redundant_logical_expression import SimplifyRedundantLogicalExpression
19
+
20
+ _CodeQuality = [
21
+ *Python,
22
+ CategoryDescriptor(display_name="Code quality"),
23
+ ]
24
+
25
+
26
+ @categorize(_CodeQuality)
27
+ class CodeQuality(Recipe):
28
+ """
29
+ Apply every Python code quality fix.
30
+
31
+ Example:
32
+ Before:
33
+ if done == True:
34
+ if ready:
35
+ run()
36
+
37
+ After:
38
+ if done and ready:
39
+ run()
40
+ """
41
+
42
+ @property
43
+ def name(self) -> str:
44
+ return "org.openrewrite.python.codequality.CodeQuality"
45
+
46
+ @property
47
+ def display_name(self) -> str:
48
+ return "Code quality"
49
+
50
+ @property
51
+ def description(self) -> str:
52
+ return (
53
+ "Apply all Python code quality recipes that change code: simplify boolean "
54
+ "expressions, merge and collapse `if` chains, and remove dead conditions and "
55
+ "writes. Search-only recipes, which mark code for review, are not included."
56
+ )
57
+
58
+ @property
59
+ def tags(self) -> List[str]:
60
+ return ["python", "code-quality"]
61
+
62
+ def recipe_list(self) -> List[Recipe]:
63
+ return [
64
+ SimplifyBooleanLiteral(),
65
+ BooleanChecksNotInverted(),
66
+ SimplifyRedundantLogicalExpression(),
67
+ RemoveDuplicateConditions(),
68
+ AllBranchesIdentical(),
69
+ MergeIdenticalBranches(),
70
+ CollapsibleIfStatements(),
71
+ RemoveSelfAssignment(),
72
+ RemoveUnconditionalValueOverwrite(),
73
+ ]
@@ -0,0 +1,333 @@
1
+ """
2
+ Recipe to remove dict entry writes that the next statement overwrites.
3
+
4
+ See: https://rules.sonarsource.com/python/RSPEC-S4143/
5
+ """
6
+
7
+ from itertools import islice
8
+ from typing import Any, List, Optional, Tuple
9
+
10
+ from rewrite import Cursor, ExecutionContext, Recipe, TreeVisitor
11
+ from rewrite.category import CategoryDescriptor
12
+ from rewrite.decorators import categorize
13
+ from rewrite.marketplace import Python
14
+ from rewrite.python.method_matcher import MethodMatcher
15
+ from rewrite.python.visitor import PythonVisitor
16
+ from rewrite.python.tree import (
17
+ CollectionLiteral,
18
+ CompilationUnit,
19
+ ComprehensionExpression,
20
+ DictLiteral,
21
+ ExpressionStatement,
22
+ KeyValue,
23
+ NamedArgument,
24
+ TypeHintedExpression,
25
+ VariableScope,
26
+ )
27
+ from rewrite.java.support_types import Space
28
+ from rewrite.java.tree import (
29
+ ArrayAccess,
30
+ Assignment,
31
+ Block,
32
+ ClassDeclaration,
33
+ Empty,
34
+ Expression,
35
+ Identifier,
36
+ J,
37
+ Lambda,
38
+ Literal,
39
+ MethodDeclaration,
40
+ MethodInvocation,
41
+ Parentheses,
42
+ Statement,
43
+ Try,
44
+ Unary,
45
+ )
46
+ from openrewrite_code_quality_python.codequality._semantically_equal import SemanticallyEqual
47
+ from openrewrite_code_quality_python.codequality._trivia import splice_out, split_line
48
+
49
+ _CodeQuality = [
50
+ *Python,
51
+ CategoryDescriptor(display_name="Code quality"),
52
+ ]
53
+
54
+
55
+ def _get_assignment(stmt: Statement) -> Optional[Assignment]:
56
+ """Extract an Assignment from a statement, if present."""
57
+ if isinstance(stmt, ExpressionStatement):
58
+ expr = stmt.expression
59
+ if isinstance(expr, Assignment):
60
+ return expr
61
+ if isinstance(stmt, Assignment):
62
+ return stmt
63
+ return None
64
+
65
+
66
+ class _Find(PythonVisitor[None]):
67
+ """Records whether any subtree of the visited node satisfies ``predicate``."""
68
+
69
+ def __init__(self, predicate):
70
+ super().__init__()
71
+ self._predicate = predicate
72
+ self.found = False
73
+
74
+ def visit(self, tree, p, parent=None):
75
+ if tree is not None and self._predicate(tree):
76
+ self.found = True
77
+ return super().visit(tree, p, parent)
78
+
79
+
80
+ def _contains(node, predicate) -> bool:
81
+ search = _Find(predicate)
82
+ search.visit(node, None)
83
+ return search.found
84
+
85
+
86
+ def _mentions(node, name: str) -> bool:
87
+ return _contains(node, lambda n: isinstance(n, Identifier) and n.simple_name == name)
88
+
89
+
90
+ def _is_number(expression: J) -> bool:
91
+ return (
92
+ isinstance(expression, Unary)
93
+ and expression.operator in (Unary.Type.Negative, Unary.Type.Positive)
94
+ and isinstance(expression.expression, Literal)
95
+ and isinstance(expression.expression.value, (int, float, complex))
96
+ )
97
+
98
+
99
+ def _is_hashable_constant(expression: J) -> bool:
100
+ """A value whose hashing and comparison run no user code and cannot raise."""
101
+ if isinstance(expression, Parentheses) and isinstance(expression.tree, Expression):
102
+ return _is_hashable_constant(expression.tree)
103
+ if isinstance(expression, Literal) or _is_number(expression):
104
+ return True
105
+ return (
106
+ isinstance(expression, CollectionLiteral)
107
+ and expression.kind == CollectionLiteral.Kind.TUPLE
108
+ and all(_is_hashable_constant(e) for e in expression.elements if not isinstance(e, Empty))
109
+ )
110
+
111
+
112
+ def _is_constant(expression: J) -> bool:
113
+ """A value whose evaluation runs no user code and cannot raise."""
114
+ if isinstance(expression, Parentheses) and isinstance(expression.tree, Expression):
115
+ return _is_constant(expression.tree)
116
+ if isinstance(expression, CollectionLiteral):
117
+ element_ok = _is_hashable_constant if expression.kind == CollectionLiteral.Kind.SET else _is_constant
118
+ return all(element_ok(e) for e in expression.elements if not isinstance(e, Empty))
119
+ if isinstance(expression, DictLiteral):
120
+ return all(
121
+ isinstance(e, KeyValue) and _is_hashable_constant(e.key) and _is_constant(e.value)
122
+ for e in expression.elements
123
+ if not isinstance(e, Empty)
124
+ )
125
+ return _is_hashable_constant(expression)
126
+
127
+
128
+ def _store(stmt: Statement) -> Optional[Tuple[str, Expression, Expression]]:
129
+ """The dict name, key and value of a ``d[key] = value`` statement with a constant key."""
130
+ assign = _get_assignment(stmt)
131
+ if assign is None or not isinstance(assign.variable, ArrayAccess):
132
+ return None
133
+ receiver = assign.variable.indexed
134
+ key = assign.variable.dimension.index
135
+ if not isinstance(receiver, Identifier) or not _is_hashable_constant(key):
136
+ return None
137
+ return receiver.simple_name, key, assign.assignment
138
+
139
+
140
+ _DICT_CONSTRUCTOR = MethodMatcher.create("dict <constructor>(..)")
141
+
142
+
143
+ def _builds_dict(value: Expression) -> bool:
144
+ """True when ``value`` is a new builtin dict holding only constant keys."""
145
+ if isinstance(value, DictLiteral):
146
+ return all(
147
+ isinstance(e, KeyValue) and _is_hashable_constant(e.key)
148
+ for e in value.elements
149
+ if not isinstance(e, Empty)
150
+ )
151
+ return (
152
+ isinstance(value, MethodInvocation)
153
+ and _DICT_CONSTRUCTOR.matches(value)
154
+ and all(isinstance(a, (NamedArgument, Empty)) for a in value.arguments)
155
+ )
156
+
157
+
158
+ def _binds_fresh_dict(stmt: Statement, name: str) -> bool:
159
+ assign = _get_assignment(stmt)
160
+ if assign is None:
161
+ return False
162
+ variable = assign.variable
163
+ if isinstance(variable, TypeHintedExpression):
164
+ variable = variable.expression
165
+ return (
166
+ isinstance(variable, Identifier)
167
+ and variable.simple_name == name
168
+ and _builds_dict(assign.assignment)
169
+ )
170
+
171
+
172
+ def _is_fresh_at(statements: List[Statement], i: int, name: str) -> bool:
173
+ """True when ``name`` holds a dict this block built and has kept to itself up to ``i``.
174
+
175
+ Rebinding the dict on every pass through the block keeps it fresh in a
176
+ loop too. Only constant-key stores may touch it in between, so every key
177
+ it holds hashes and compares without running user code.
178
+ """
179
+ for stmt in reversed(statements[:i]):
180
+ if _binds_fresh_dict(stmt, name):
181
+ return True
182
+ store = _store(stmt)
183
+ if store is not None and store[0] == name and not _mentions(store[2], name):
184
+ continue
185
+ if _mentions(stmt, name):
186
+ return False
187
+ return False
188
+
189
+
190
+ _NESTED_SCOPES = (ClassDeclaration, ComprehensionExpression, Lambda, MethodDeclaration)
191
+
192
+
193
+ def _shared_beyond_frame(function: MethodDeclaration, name: str) -> bool:
194
+ """True when ``name`` in ``function`` is a global, or a cell a nested scope can read."""
195
+ return _contains(
196
+ function.body,
197
+ lambda n: (
198
+ isinstance(n, VariableScope) and any(i.simple_name == name for i in n.names)
199
+ ) or (isinstance(n, _NESTED_SCOPES) and _mentions(n, name)),
200
+ )
201
+
202
+
203
+ def _enclosing_function(cursor: Cursor) -> Optional[MethodDeclaration]:
204
+ """The function whose frame alone can observe a block's locals, if there is one.
205
+
206
+ A ``try`` or ``with`` between them can catch an exception and read the
207
+ dict afterwards, so a block inside one has no such function.
208
+ """
209
+ for value in islice(cursor.get_path(), 1, None):
210
+ if isinstance(value, MethodDeclaration):
211
+ return value
212
+ if isinstance(value, (Try, ClassDeclaration, CompilationUnit)):
213
+ return None
214
+ return None
215
+
216
+
217
+ def _is_dead_write(statements: List[Statement], i: int, function: MethodDeclaration) -> bool:
218
+ """True when the write at ``i`` is overwritten by the next one before anything can see it.
219
+
220
+ The dict has to be one only this frame can reach, which rules out other
221
+ threads, aliases reached through a call, and setter hooks. The dropped
222
+ value has to be a constant, so dropping it drops no code.
223
+ """
224
+ a = _store(statements[i])
225
+ b = _store(statements[i + 1])
226
+ if a is None or b is None:
227
+ return False
228
+ name, key, value = a
229
+ return (
230
+ b[0] == name
231
+ and SemanticallyEqual.are_equal(key, b[1])
232
+ and _is_constant(value)
233
+ and not _mentions(b[2], name)
234
+ and _is_fresh_at(statements, i, name)
235
+ and not _shared_beyond_frame(function, name)
236
+ )
237
+
238
+
239
+ @categorize(_CodeQuality)
240
+ class RemoveUnconditionalValueOverwrite(Recipe):
241
+ """
242
+ Remove a dict entry write that the next statement overwrites.
243
+
244
+ Only a dict the function has just built and kept to itself qualifies.
245
+
246
+ Example:
247
+ Before:
248
+ def f():
249
+ d = {}
250
+ d["key"] = 1
251
+ d["key"] = 2
252
+ return d
253
+
254
+ After:
255
+ def f():
256
+ d = {}
257
+ d["key"] = 2
258
+ return d
259
+ """
260
+
261
+ @property
262
+ def name(self) -> str:
263
+ return "org.openrewrite.python.codequality.RemoveUnconditionalValueOverwrite"
264
+
265
+ @property
266
+ def display_name(self) -> str:
267
+ return "Remove unconditional value overwrites"
268
+
269
+ @property
270
+ def description(self) -> str:
271
+ return (
272
+ "Remove a constant written to a dict key when the next statement "
273
+ "overwrites the same key. Only a dict the enclosing function built "
274
+ "with `{...}` or `dict()` and has not yet passed on qualifies, and "
275
+ "only for constant keys outside `try` and `with` blocks. Writes to "
276
+ "attributes, parameters and globals are left alone, because "
277
+ "another thread, an alias or a setter can observe them."
278
+ )
279
+
280
+ @property
281
+ def tags(self) -> List[str]:
282
+ return ["python", "code-quality", "RSPEC-S4143"]
283
+
284
+ @property
285
+ def estimated_effort_per_occurrence(self) -> int:
286
+ return 5
287
+
288
+ def editor(self) -> TreeVisitor[Any, ExecutionContext]:
289
+ class Visitor(PythonVisitor[ExecutionContext]):
290
+ def visit_block(
291
+ self, block: Block, p: ExecutionContext
292
+ ) -> Optional[Block]:
293
+ block = super().visit_block(block, p)
294
+ if not isinstance(block, Block):
295
+ return block
296
+ function = _enclosing_function(self.cursor)
297
+ if function is None:
298
+ return block
299
+ removed = _remove_overwrites(block._statements, block.end, function)
300
+ if removed is not None:
301
+ return block.replace(_statements=removed[0], _end=removed[1])
302
+ return block
303
+
304
+ return Visitor()
305
+
306
+
307
+ def _removable(padded_statements: list, i: int, function: MethodDeclaration) -> bool:
308
+ """Return True when the write at ``i`` is dead and can be dropped by splicing lines.
309
+
310
+ Both the write and its overwrite have to start a line of their own,
311
+ which a `;`-joined pair does not.
312
+ """
313
+ if i + 1 >= len(padded_statements):
314
+ return False
315
+ statements = [s.element for s in padded_statements]
316
+ if not _is_dead_write(statements, i, function):
317
+ return False
318
+ if split_line(statements[i].prefix) is None:
319
+ return False
320
+ return split_line(statements[i + 1].prefix) is not None
321
+
322
+
323
+ def _remove_overwrites(
324
+ padded_statements: list, end: Space, function: MethodDeclaration
325
+ ) -> Optional[Tuple[list, Space]]:
326
+ """Return the statements and end trivia with dead writes removed, or None if unchanged."""
327
+ to_remove = {
328
+ i for i in range(len(padded_statements))
329
+ if _removable(padded_statements, i, function)
330
+ }
331
+ if not to_remove:
332
+ return None
333
+ return splice_out(padded_statements, end, to_remove)
@@ -21,10 +21,9 @@ from rewrite.markers import Markers
21
21
  from rewrite.python.visitor import PythonVisitor
22
22
  from rewrite.java.support_types import JRightPadded, Space
23
23
  from rewrite.java.tree import Binary, Expression, FieldAccess, Identifier, J, Literal, MethodInvocation, Parentheses
24
+ from rewrite.python.printer import PrintOutputCapture, PythonPrinter
24
25
  from rewrite.python.tree import (
25
26
  CollectionLiteral,
26
- ComprehensionExpression,
27
- DictLiteral,
28
27
  FormattedString,
29
28
  NamedArgument,
30
29
  Star,
@@ -74,17 +73,6 @@ def _get_fstring_delimiter(value_source: str) -> Optional[str]:
74
73
  return 'f' + quote if quote else None
75
74
 
76
75
 
77
- def _opens_with_brace(expr: Any) -> bool:
78
- """Whether ``expr`` prints with a leading ``{`` that the field's own brace makes into ``{{``."""
79
- if isinstance(expr, DictLiteral):
80
- return True
81
- if isinstance(expr, CollectionLiteral):
82
- return expr.kind == CollectionLiteral.Kind.SET
83
- if isinstance(expr, ComprehensionExpression):
84
- return expr.kind in (ComprehensionExpression.Kind.SET, ComprehensionExpression.Kind.DICT)
85
- return False
86
-
87
-
88
76
  def _breaks_replacement_field(expr: Any, quote: str) -> bool:
89
77
  """Whether ``expr`` holds anything a replacement field cannot carry.
90
78
 
@@ -124,11 +112,11 @@ def _unsafe_to_interpolate(expr: Any, delimiter: str) -> bool:
124
112
  """Whether substituting ``expr`` into a replacement field spoils the f-string.
125
113
 
126
114
  A literal argument nests a string in a string, a rewrite worth declining however it
127
- is quoted. Deeper literals are declined only where 3.8 -- the earliest target these
128
- recipes run for -- cannot parse the result.
115
+ is quoted. Other expressions are declined only where 3.8 -- the earliest target
116
+ these recipes run for -- cannot parse the result.
129
117
  """
130
118
  value = expr.value if isinstance(expr, NamedArgument) else expr
131
- if _opens_with_brace(value) or _is_string_literal(value):
119
+ if _is_string_literal(value):
132
120
  return True
133
121
  return _breaks_replacement_field(value, delimiter[1:])
134
122
 
@@ -151,6 +139,23 @@ def _has_backslash(value_source: str) -> bool:
151
139
  return '\\' in content
152
140
 
153
141
 
142
+ def _field_expression(arg_expr: Expression, delimiter: str) -> Optional[JRightPadded]:
143
+ """The argument as a replacement field, or None when it cannot sit inside this f-string."""
144
+ expr = arg_expr.replace(_prefix=Space.EMPTY)
145
+ source = PythonPrinter().print(expr, PrintOutputCapture(None))
146
+ # Only legal in a replacement field from 3.12 (PEP 701)
147
+ if any(c in source for c in (delimiter[1], '\\', '#', '\n')):
148
+ return None
149
+ # A leading or trailing brace would fuse with the field's own and read as an escaped brace
150
+ if source.startswith('{'):
151
+ expr = expr.replace(_prefix=Space.SINGLE_SPACE)
152
+ return JRightPadded(
153
+ _element=expr,
154
+ _after=Space.SINGLE_SPACE if source.endswith('}') else Space.EMPTY,
155
+ _markers=Markers.EMPTY,
156
+ )
157
+
158
+
154
159
  def _convert_format_to_fstring(method: MethodInvocation) -> Optional[FormattedString]:
155
160
  """Convert a ``"...".format(...)`` MethodInvocation to a FormattedString.
156
161
 
@@ -267,11 +272,9 @@ def _convert_format_to_fstring(method: MethodInvocation) -> Optional[FormattedSt
267
272
  return None
268
273
  arg_expr = named_args[field_name]
269
274
 
270
- padded_expr = JRightPadded(
271
- _element=arg_expr.replace(_prefix=Space.EMPTY),
272
- _after=Space.EMPTY,
273
- _markers=Markers.EMPTY,
274
- )
275
+ padded_expr = _field_expression(arg_expr, delimiter)
276
+ if padded_expr is None:
277
+ return None
275
278
 
276
279
  # Map conversion flag
277
280
  conversion_enum = None
@@ -301,9 +304,9 @@ def _convert_format_to_fstring(method: MethodInvocation) -> Optional[FormattedSt
301
304
  _markers=Markers.EMPTY,
302
305
  _expression=padded_expr,
303
306
  _debug=None,
304
- _conversion=conversion_enum,
307
+ _conversion=None,
305
308
  _format=format_node,
306
- ))
309
+ ).with_conversion(conversion_enum))
307
310
 
308
311
  return FormattedString(
309
312
  _id=random_id(),
@@ -498,11 +501,9 @@ def _convert_percent_to_fstring(binary: Binary) -> Optional[FormattedString]:
498
501
  arg_expr = args[arg_index]
499
502
  arg_index += 1
500
503
 
501
- padded_expr = JRightPadded(
502
- _element=arg_expr.replace(_prefix=Space.EMPTY),
503
- _after=Space.EMPTY,
504
- _markers=Markers.EMPTY,
505
- )
504
+ padded_expr = _field_expression(arg_expr, delimiter)
505
+ if padded_expr is None:
506
+ return None
506
507
 
507
508
  conversion_enum = None
508
509
  if part_value == 'r':
@@ -514,9 +515,9 @@ def _convert_percent_to_fstring(binary: Binary) -> Optional[FormattedString]:
514
515
  _markers=Markers.EMPTY,
515
516
  _expression=padded_expr,
516
517
  _debug=None,
517
- _conversion=conversion_enum,
518
+ _conversion=None,
518
519
  _format=None,
519
- ))
520
+ ).with_conversion(conversion_enum))
520
521
 
521
522
  return FormattedString(
522
523
  _id=random_id(),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrewrite-migrate-python
3
- Version: 0.15.0.dev20260922170519
3
+ Version: 0.15.1
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
@@ -6,6 +6,7 @@ src/openrewrite_code_quality_python/codequality/_semantically_equal.py
6
6
  src/openrewrite_code_quality_python/codequality/_trivia.py
7
7
  src/openrewrite_code_quality_python/codequality/all_branches_identical.py
8
8
  src/openrewrite_code_quality_python/codequality/boolean_checks_not_inverted.py
9
+ src/openrewrite_code_quality_python/codequality/code_quality.py
9
10
  src/openrewrite_code_quality_python/codequality/collapsible_if_statements.py
10
11
  src/openrewrite_code_quality_python/codequality/find_suspicious_chained_comparison.py
11
12
  src/openrewrite_code_quality_python/codequality/merge_identical_branches.py
@@ -249,6 +249,31 @@ class TestReplaceStrFormatWithFString:
249
249
  )
250
250
  )
251
251
 
252
+ def test_set_literal_argument_is_spaced_from_the_field_braces(self):
253
+ """A brace of the argument's own must not fuse with the field's into an escaped brace."""
254
+ spec = RecipeSpec(recipe=ReplaceStrFormatWithFString())
255
+ spec.rewrite_run(
256
+ python(
257
+ """
258
+ message = "{} outside {} block".format(op, {'for', 'while'})
259
+ """,
260
+ """
261
+ message = f"{op} outside { {'for', 'while'} } block"
262
+ """,
263
+ )
264
+ )
265
+
266
+ def test_no_change_argument_contains_the_delimiter_quote(self):
267
+ """The quote is only legal inside a replacement field from 3.12."""
268
+ spec = RecipeSpec(recipe=ReplaceStrFormatWithFString())
269
+ spec.rewrite_run(
270
+ python(
271
+ """
272
+ message = "{} outside {} block".format(op, {"for", "while"})
273
+ """,
274
+ )
275
+ )
276
+
252
277
  def test_no_change_backslash_in_string(self):
253
278
  """'hello\\n{}'.format(name) — backslash escape, no change."""
254
279
  spec = RecipeSpec(recipe=ReplaceStrFormatWithFString())
@@ -326,19 +351,6 @@ class TestReplaceStrFormatWithFString:
326
351
  )
327
352
  )
328
353
 
329
- def test_no_change_brace_opening_argument(self):
330
- """A brace-opening argument's own brace would read as an escaped `{{`."""
331
- spec = RecipeSpec(recipe=ReplaceStrFormatWithFString())
332
- spec.rewrite_run(
333
- python(
334
- """
335
- keywords = "{}".format({1, 2})
336
- mapping = "{}".format({1: 2})
337
- squares = "{}".format({n: n * n for n in sizes})
338
- """
339
- )
340
- )
341
-
342
354
  def test_idempotent(self):
343
355
  """Running the recipe twice produces the same result."""
344
356
  recipe = ReplaceStrFormatWithFString()
@@ -485,6 +497,29 @@ class TestReplacePercentFormatWithFString:
485
497
  )
486
498
  )
487
499
 
500
+ def test_dict_literal_argument_is_spaced_from_the_field_braces(self):
501
+ spec = RecipeSpec(recipe=ReplacePercentFormatWithFString())
502
+ spec.rewrite_run(
503
+ python(
504
+ """
505
+ message = "config: %s" % ({'a': 1},)
506
+ """,
507
+ """
508
+ message = f"config: { {'a': 1} }"
509
+ """,
510
+ )
511
+ )
512
+
513
+ def test_no_change_argument_contains_the_delimiter_quote(self):
514
+ spec = RecipeSpec(recipe=ReplacePercentFormatWithFString())
515
+ spec.rewrite_run(
516
+ python(
517
+ """
518
+ message = "name: %s" % d["name"]
519
+ """,
520
+ )
521
+ )
522
+
488
523
  def test_no_change_backslash(self):
489
524
  r"""'%s\n' % x — backslash in string, no change."""
490
525
  spec = RecipeSpec(recipe=ReplacePercentFormatWithFString())