sarj-python-lint 1.2.4__tar.gz → 1.2.6__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 (150) hide show
  1. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. sarj_python_lint-1.2.6/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +227 -0
  5. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +248 -19
  6. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/.gitignore +0 -0
  7. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/LICENSE +0 -0
  8. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/README.md +0 -0
  9. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/json_boundary.py +0 -0
  20. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/py.typed +0 -0
  21. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/ratchet.py +0 -0
  22. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rule_base.py +0 -0
  23. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/__init__.py +0 -0
  24. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  25. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  26. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  27. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_comments.py +0 -0
  28. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  29. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  30. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_first_party.py +0 -0
  31. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  32. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_imports.py +0 -0
  33. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_logging.py +0 -0
  34. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  35. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_paths.py +0 -0
  36. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_project_index.py +0 -0
  37. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  38. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_pytest.py +0 -0
  39. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_sql.py +0 -0
  40. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  41. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  42. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  43. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  44. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  45. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  46. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  47. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  48. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  49. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  50. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  51. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  52. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  53. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  54. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  55. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  56. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  57. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  58. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  59. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  60. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  61. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  62. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  63. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  64. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  65. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  66. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  67. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  68. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  69. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  70. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  71. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  72. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  73. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  74. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  75. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  76. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  77. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  78. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  79. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  80. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  81. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  82. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  83. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  84. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  85. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  86. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  87. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  88. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  89. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  90. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  91. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  92. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  93. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  94. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  95. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  96. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  97. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  98. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  99. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  100. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  101. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  102. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  103. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  104. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  105. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  106. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  107. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  108. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  109. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  110. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  111. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  112. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  113. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  114. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  115. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  116. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  117. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  118. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  119. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  120. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  121. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  122. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  123. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  124. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  125. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  126. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  127. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  128. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  129. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  130. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  131. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  132. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  133. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  134. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  135. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  136. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  137. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  138. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  139. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  140. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  141. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  142. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/stepdown.py +0 -0
  143. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  144. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  145. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  146. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  147. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  148. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  149. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  150. {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.2.4"
3
+ version = "1.2.6"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -75,6 +75,7 @@ from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
75
75
  from sarj_python_lint.rules.no_select_star import NoSelectStar
76
76
  from sarj_python_lint.rules.no_service_behavior_in_settings import NoServiceBehaviorInSettings
77
77
  from sarj_python_lint.rules.no_string_concat_in_loop import NoStringConcatInLoop
78
+ from sarj_python_lint.rules.no_swallowed_asyncio_cancellation import NoSwallowedAsyncioCancellation
78
79
  from sarj_python_lint.rules.no_tautological_expect import NoTautologicalExpect
79
80
  from sarj_python_lint.rules.no_typed_doc_sections import NoTypedDocSections
80
81
  from sarj_python_lint.rules.no_unique_violation_message_match import (
@@ -181,6 +182,7 @@ if TYPE_CHECKING:
181
182
 
182
183
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
183
184
  {
185
+ NoSwallowedAsyncioCancellation.id: NoSwallowedAsyncioCancellation,
184
186
  AsyncMockCallWithoutAwaitAssertion.id: AsyncMockCallWithoutAwaitAssertion,
185
187
  ComplexPostgresQueryRequiresArchitectureReview.id: ComplexPostgresQueryRequiresArchitectureReview,
186
188
  ExcessiveCommentary.id: ExcessiveCommentary,
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from itertools import pairwise
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, ClassVar, final, override
7
+
8
+ from sarj_python_lint.rule_base import (
9
+ AutofixPolicy,
10
+ Diagnostic,
11
+ ExampleFile,
12
+ ExampleOutcome,
13
+ Rule,
14
+ RuleCategory,
15
+ RuleDocumentation,
16
+ RuleExample,
17
+ Severity,
18
+ is_suppressed,
19
+ )
20
+ from sarj_python_lint.rules._ast_index import nodes
21
+ from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Iterator
26
+
27
+ from sarj_python_lint._file_context import PythonFileContext
28
+ from sarj_python_lint.rules._imports import ImportIndex
29
+
30
+
31
+ _ASYNCIO = frozenset({"asyncio", "asyncio.exceptions"})
32
+ _SIMPLE_STATEMENTS = (ast.Expr, ast.Assign, ast.AnnAssign, ast.AugAssign, ast.Delete, ast.Pass)
33
+ _MESSAGE = (
34
+ "CancelledError handler completes normally; re-raise caller cancellation after cleanup, "
35
+ "or guard intentional child cancellation with the current task's cancellation state."
36
+ )
37
+
38
+
39
+ @final
40
+ class NoSwallowedAsyncioCancellation(Rule):
41
+ id = "no-swallowed-asyncio-cancellation"
42
+ code = "SARJ458"
43
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
44
+ default_level=Severity.WARNING,
45
+ summary="Preserve cancellation when an async operation is cancelled.",
46
+ rationale=(
47
+ "Returning or falling through after catching CancelledError can make cancelled work look successful "
48
+ "and interfere with task groups and timeout shutdown. Child cancellation must be distinguished from "
49
+ "cancellation of the caller."
50
+ ),
51
+ remediation=(
52
+ "Re-raise after cleanup. If child cancellation is an expected result, first propagate cancellation "
53
+ "when asyncio.current_task().cancelling() is nonzero. Use an exact suppression for a documented "
54
+ "boundary that deliberately consumes cancellation."
55
+ ),
56
+ category=RuleCategory.CORRECTNESS,
57
+ autofix=AutofixPolicy.NONE,
58
+ limitations=(
59
+ "Only explicit, import-proven asyncio.CancelledError handlers in async functions are inspected; broad handlers, contextlib.suppress, tests, support fixtures, and generated files are excluded.",
60
+ "Only straight-line handlers with a return or normal fallthrough are reported. Compound handlers, re-raising, and translated exceptions are excluded. Any attribute call named uncancel is conservatively excluded without proving its receiver, so unrelated methods can hide a finding.",
61
+ "A single-name task created by asyncio.create_task, immediately cancelled before a try that only awaits it, is intentionally excluded as child cleanup; this does not prove concurrent caller cancellation cannot occur.",
62
+ "Try statements with control-transferring or compound finalizers are excluded, including handlers nested beneath them. Runtime helper effects and interprocedural task ownership are not inferred.",
63
+ ),
64
+ examples=(
65
+ RuleExample(
66
+ example_id="cancelled-operation-returns-success",
67
+ title="Cancellation becomes a successful return",
68
+ outcome=ExampleOutcome.MATCH,
69
+ files=(
70
+ ExampleFile.python(
71
+ "worker.py",
72
+ "import asyncio\n\nasync def worker():\n try:\n await asyncio.sleep(10)\n except asyncio.CancelledError:\n return None\n",
73
+ ),
74
+ ),
75
+ focus_path=PurePosixPath("worker.py"),
76
+ expected_count=1,
77
+ public=True,
78
+ ),
79
+ RuleExample(
80
+ example_id="cancelled-operation-reraises",
81
+ title="Cleanup preserves cancellation",
82
+ outcome=ExampleOutcome.NO_MATCH,
83
+ files=(
84
+ ExampleFile.python(
85
+ "worker.py",
86
+ "import asyncio\n\nasync def worker():\n try:\n await asyncio.sleep(10)\n except asyncio.CancelledError:\n raise\n",
87
+ ),
88
+ ),
89
+ focus_path=PurePosixPath("worker.py"),
90
+ expected_count=0,
91
+ public=True,
92
+ ),
93
+ ),
94
+ )
95
+ description = documentation.summary
96
+
97
+ @override
98
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
99
+ if "CancelledError" not in context.source:
100
+ return []
101
+ if context.generated or is_test_path(context.path) or is_test_support_path(context.path):
102
+ return []
103
+ tree = context.tree
104
+ if tree is None:
105
+ return []
106
+ diagnostics: list[Diagnostic] = []
107
+ for function in nodes(tree, ast.AsyncFunctionDef, index=context.node_index):
108
+ diagnostics.extend(_function_diagnostics(function, context, self.code))
109
+ return sorted(diagnostics, key=lambda item: (item.line, item.col))
110
+
111
+
112
+ def _function_diagnostics(function: ast.AsyncFunctionDef, context: PythonFileContext, code: str) -> list[Diagnostic]:
113
+ local = tuple(_local_nodes(function))
114
+ children = _owned_tasks(local, context.imports)
115
+ previous = _preceding_statements(local)
116
+ diagnostics: list[Diagnostic] = []
117
+ for node in local:
118
+ if not isinstance(node, ast.Try) or _owned_child_cleanup(node, previous.get(id(node)), children):
119
+ continue
120
+ for handler in node.handlers:
121
+ if not _catches_cancellation(handler.type, context.imports) or not _normal_exit(handler.body):
122
+ continue
123
+ if is_suppressed(context.source_lines, handler.lineno, code):
124
+ continue
125
+ diagnostics.append(
126
+ Diagnostic(
127
+ path=context.path,
128
+ line=handler.lineno,
129
+ col=handler.col_offset + 1,
130
+ code=code,
131
+ message=_MESSAGE,
132
+ severity=Severity.WARNING,
133
+ )
134
+ )
135
+ return diagnostics
136
+
137
+
138
+ def _local_nodes(function: ast.AsyncFunctionDef) -> Iterator[ast.AST]:
139
+ stack: list[ast.AST] = list(reversed(function.body))
140
+ yield function
141
+ while stack:
142
+ node = stack.pop()
143
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda):
144
+ continue
145
+ if isinstance(node, ast.Try | ast.TryStar) and any(
146
+ not isinstance(statement, _SIMPLE_STATEMENTS) for statement in node.finalbody
147
+ ):
148
+ continue
149
+ yield node
150
+ stack.extend(reversed(list(ast.iter_child_nodes(node))))
151
+
152
+
153
+ def _catches_cancellation(exception: ast.expr | None, imports: ImportIndex) -> bool:
154
+ if isinstance(exception, ast.Tuple):
155
+ return any(_catches_cancellation(item, imports) for item in exception.elts)
156
+ return exception is not None and imports.resolves(exception, sources=_ASYNCIO, symbol="CancelledError")
157
+
158
+
159
+ def _normal_exit(statements: list[ast.stmt]) -> bool:
160
+ for statement in statements:
161
+ if any(
162
+ isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "uncancel"
163
+ for node in ast.walk(statement)
164
+ ):
165
+ return False
166
+ if isinstance(statement, ast.Return):
167
+ return True
168
+ if not isinstance(statement, _SIMPLE_STATEMENTS):
169
+ return False
170
+ return True
171
+
172
+
173
+ def _owned_tasks(local: tuple[ast.AST, ...], imports: ImportIndex) -> frozenset[str]:
174
+ bindings: dict[str, list[ast.AST]] = {}
175
+ for node in local:
176
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store | ast.Del):
177
+ bindings.setdefault(node.id, []).append(node)
178
+ if (
179
+ isinstance(node, ast.Attribute)
180
+ and isinstance(node.ctx, ast.Store | ast.Del)
181
+ and isinstance(node.value, ast.Name)
182
+ ):
183
+ bindings.setdefault(node.value.id, []).append(node)
184
+ return frozenset(
185
+ node.targets[0].id
186
+ for node in local
187
+ if isinstance(node, ast.Assign)
188
+ and len(node.targets) == 1
189
+ and isinstance(node.targets[0], ast.Name)
190
+ and len(bindings[node.targets[0].id]) == 1
191
+ and isinstance(node.value, ast.Call)
192
+ and imports.resolves(node.value.func, sources=frozenset({"asyncio"}), symbol="create_task")
193
+ )
194
+
195
+
196
+ def _preceding_statements(local: tuple[ast.AST, ...]) -> dict[int, ast.stmt]:
197
+ previous: dict[int, ast.stmt] = {}
198
+ for node in local:
199
+ for suite in _statement_suites(node):
200
+ for first, second in pairwise(suite):
201
+ previous[id(second)] = first
202
+ return previous
203
+
204
+
205
+ def _owned_child_cleanup(node: ast.Try, previous: ast.stmt | None, children: frozenset[str]) -> bool:
206
+ if len(node.body) != 1:
207
+ return False
208
+ match node.body[0], previous:
209
+ case (
210
+ ast.Expr(value=ast.Await(value=ast.Name(id=awaited))),
211
+ ast.Expr(value=ast.Call(func=ast.Attribute(value=ast.Name(id=cancelled), attr="cancel"))),
212
+ ):
213
+ return awaited == cancelled and awaited in children
214
+ case _:
215
+ return False
216
+
217
+
218
+ def _statement_suites(node: ast.AST) -> tuple[list[ast.stmt], ...]:
219
+ match node:
220
+ case ast.Try() | ast.TryStar():
221
+ return node.body, node.orelse, node.finalbody
222
+ case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
223
+ return node.body, node.orelse
224
+ case ast.AsyncFunctionDef() | ast.With() | ast.AsyncWith() | ast.ExceptHandler() | ast.match_case():
225
+ return (node.body,)
226
+ case _:
227
+ return ()
@@ -60,8 +60,16 @@ class _ExternalRecordFinding(NamedTuple):
60
60
  origin: ast.Call
61
61
 
62
62
 
63
+ @dataclass(frozen=True, slots=True)
64
+ class _WrapperSummary:
65
+ parameters: tuple[str, ...]
66
+ fields: dict[str, str]
67
+ accessors: dict[str, str]
68
+
69
+
63
70
  @dataclass(frozen=True, slots=True)
64
71
  class _ModuleSummaries:
72
+ wrappers: dict[str, _WrapperSummary]
65
73
  decoder_parameters: dict[str, int]
66
74
  record_parameters: dict[str, frozenset[int]]
67
75
  local_parameters: dict[str, frozenset[int]]
@@ -114,6 +122,7 @@ class RequirePydanticForExternalJson(Rule):
114
122
  autofix=AutofixPolicy.NONE,
115
123
  limitations=(
116
124
  "The rule follows import-proven JSON decoders, requests/httpx/aiohttp responses and clients, environment or subprocess results, and simple module-local helpers through single-assignment names.",
125
+ "Simple same-module wrappers preserve constructor arguments through direct stored-field accessor methods, including local identity-or-empty-dict narrowing helpers; inheritance, decorators, mutation, nested captures, unknown member escapes/helpers, and wrapper factory returns are excluded.",
117
126
  "It diagnoses literal-key subscription and get calls; iteration and other dynamic JSON use remain out of scope.",
118
127
  "Unannotated parameters and unknown expressions are not assumed to be external; interprocedural and framework-specific boundaries can remain unreported.",
119
128
  "Names bound by loops, comprehensions, context managers, exception handlers, pattern matching, imports, or destructuring are excluded when their provenance is ambiguous; unrelated binders do not suppress the rest of a function.",
@@ -220,6 +229,7 @@ def _module_summaries(
220
229
  _summarize_function(function, imports, module_validator_names, source_lines, decoders, records=records)
221
230
  response_callables = _response_callables(tree, imports, node_index=node_index)
222
231
  return _ModuleSummaries(
232
+ _wrapper_summaries(tree, imports),
223
233
  decoders,
224
234
  records,
225
235
  _locally_sourced_parameters(functions),
@@ -538,6 +548,7 @@ def _function_findings(
538
548
  response_names,
539
549
  bindings,
540
550
  local_bound_names,
551
+ scope,
541
552
  )
542
553
  validated_names = _validated_names(function, imports, summaries.jsonschema_validator_names)
543
554
  findings: list[_ExternalRecordFinding] = []
@@ -748,6 +759,7 @@ class _OriginResolver:
748
759
  response_names: frozenset[str]
749
760
  bindings: dict[str, ast.expr]
750
761
  local_bound_names: frozenset[str]
762
+ scope: tuple[ast.AST, ...]
751
763
 
752
764
  def origins(self, expression: ast.expr, resolving: frozenset[str] = frozenset()) -> frozenset[ast.Call]:
753
765
  expression = _unwrap_await(expression)
@@ -756,30 +768,80 @@ class _OriginResolver:
756
768
  return frozenset()
757
769
  return self.origins(value, resolving | {expression.id})
758
770
  if isinstance(expression, ast.Call):
759
- if self._is_validation_call(expression):
760
- return frozenset()
761
- if self._is_source(expression):
762
- return frozenset({expression})
763
- found_origins: set[ast.Call] = set()
764
- if (
765
- isinstance(expression.func, ast.Name)
766
- and expression.func.id not in self.local_bound_names
767
- and (position := self.summaries.decoder_parameters.get(expression.func.id)) is not None
768
- and position < len(expression.args)
769
- and self._is_external_input(expression.args[position])
770
- ):
771
- found_origins.add(expression)
772
- for argument in expression.args:
773
- found_origins.update(self.origins(argument, resolving))
774
- for keyword in expression.keywords:
775
- found_origins.update(self.origins(keyword.value, resolving))
776
- return frozenset(found_origins)
777
- found_origins = set()
771
+ return self._call_origins(expression, resolving)
772
+ found_origins: set[ast.Call] = set()
778
773
  for child in ast.iter_child_nodes(expression):
779
774
  if isinstance(child, ast.expr):
780
775
  found_origins.update(self.origins(child, resolving))
781
776
  return frozenset(found_origins)
782
777
 
778
+ def _call_origins(self, call: ast.Call, resolving: frozenset[str]) -> frozenset[ast.Call]:
779
+ if isinstance(call.func, ast.Attribute) and not call.args and not call.keywords:
780
+ wrapped = self._wrapper_value(call.func)
781
+ if wrapped is not None:
782
+ return self.origins(wrapped, resolving)
783
+ if self._is_validation_call(call):
784
+ return frozenset()
785
+ if self._is_source(call):
786
+ return frozenset({call})
787
+ found_origins: set[ast.Call] = set()
788
+ if (
789
+ isinstance(call.func, ast.Name)
790
+ and call.func.id not in self.local_bound_names
791
+ and (position := self.summaries.decoder_parameters.get(call.func.id)) is not None
792
+ and position < len(call.args)
793
+ and self._is_external_input(call.args[position])
794
+ ):
795
+ found_origins.add(call)
796
+ for argument in call.args:
797
+ found_origins.update(self.origins(argument, resolving))
798
+ for keyword in call.keywords:
799
+ found_origins.update(self.origins(keyword.value, resolving))
800
+ return frozenset(found_origins)
801
+
802
+ def _wrapper_value(self, access: ast.Attribute) -> ast.expr | None:
803
+ receiver = self._resolved_binding(access.value) if isinstance(access.value, ast.Name) else access.value
804
+ if not isinstance(receiver, ast.Call) or not isinstance(receiver.func, ast.Name):
805
+ return None
806
+ if receiver.func.id in self.local_bound_names:
807
+ return None
808
+ summary = self.summaries.wrappers.get(receiver.func.id)
809
+ if summary is None or not self._wrapper_is_stable(receiver, summary):
810
+ return None
811
+ field = summary.accessors.get(access.attr)
812
+ parameter = summary.fields.get(field) if field is not None else None
813
+ if parameter is None or any(isinstance(argument, ast.Starred) for argument in receiver.args):
814
+ return None
815
+ if any(keyword.arg is None for keyword in receiver.keywords):
816
+ return None
817
+ position = summary.parameters.index(parameter)
818
+ if position < len(receiver.args):
819
+ return receiver.args[position]
820
+ return next((keyword.value for keyword in receiver.keywords if keyword.arg == parameter), None)
821
+
822
+ def _wrapper_is_stable(self, constructor: ast.Call, summary: _WrapperSummary) -> bool:
823
+ def refers_to_wrapper(value: ast.expr) -> bool:
824
+ return isinstance(value, ast.Name) and self._resolved_binding(value) is constructor
825
+
826
+ for node in self.scope:
827
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda) and any(
828
+ isinstance(child, ast.Name) and refers_to_wrapper(child) for child in ast.walk(node)
829
+ ):
830
+ return False
831
+ if (
832
+ isinstance(node, ast.Attribute)
833
+ and refers_to_wrapper(node.value)
834
+ and (isinstance(node.ctx, ast.Store | ast.Del) or node.attr not in summary.accessors | summary.fields)
835
+ ):
836
+ return False
837
+ if not isinstance(node, ast.Call):
838
+ continue
839
+ if any(refers_to_wrapper(argument) for argument in node.args) or any(
840
+ refers_to_wrapper(keyword.value) for keyword in node.keywords
841
+ ):
842
+ return False
843
+ return True
844
+
783
845
  def _is_validation_call(self, call: ast.Call) -> bool:
784
846
  if _is_validation_call(
785
847
  call,
@@ -1462,3 +1524,170 @@ def _typed_http_clients(
1462
1524
  imports.resolves(argument.annotation, sources=_HTTP_MODULES, symbol=symbol) for symbol in _HTTP_CLIENT_TYPES
1463
1525
  )
1464
1526
  }
1527
+
1528
+
1529
+ def _wrapper_summaries(tree: ast.Module, imports: ImportIndex) -> dict[str, _WrapperSummary]:
1530
+ counts = _suite_binding_counts(tree.body)
1531
+ narrowing_helpers = _narrowing_helpers(tree, imports, counts)
1532
+ mutated_classes = _wrapper_mutated_classes(tree)
1533
+ summaries: dict[str, _WrapperSummary] = {}
1534
+ for node in tree.body:
1535
+ if not isinstance(node, ast.ClassDef) or node.bases or node.keywords or node.decorator_list:
1536
+ continue
1537
+ if counts[node.name] != 1 or node.name in mutated_classes:
1538
+ continue
1539
+ summary = _wrapper_summary(node, narrowing_helpers)
1540
+ if summary is not None:
1541
+ summaries[node.name] = summary
1542
+ return summaries
1543
+
1544
+
1545
+ def _wrapper_summary(node: ast.ClassDef, narrowing_helpers: frozenset[str]) -> _WrapperSummary | None:
1546
+ if any(_wrapper_descriptor(statement) for statement in node.body):
1547
+ return None
1548
+ methods = [statement for statement in node.body if isinstance(statement, ast.FunctionDef)]
1549
+ if any(method.name.startswith("__") and method.name != "__init__" for method in methods):
1550
+ return None
1551
+ constructors = [method for method in methods if method.name == "__init__"]
1552
+ if len(constructors) != 1:
1553
+ return None
1554
+ constructor = constructors[0]
1555
+ fields = _wrapper_fields(constructor)
1556
+ if not fields or fields.keys() & {method.name for method in methods}:
1557
+ return None
1558
+ if any(_wrapper_method_mutates(method) for method in methods if method is not constructor):
1559
+ return None
1560
+ accessors = _wrapper_accessors(node, methods, fields, narrowing_helpers)
1561
+ parameters = tuple(_parameter_positions(constructor))[1:]
1562
+ return _WrapperSummary(parameters, fields, accessors)
1563
+
1564
+
1565
+ def _wrapper_accessors(
1566
+ node: ast.ClassDef,
1567
+ methods: list[ast.FunctionDef],
1568
+ fields: dict[str, str],
1569
+ narrowing_helpers: frozenset[str],
1570
+ ) -> dict[str, str]:
1571
+ method_counts = _suite_binding_counts(node.body)
1572
+ return {
1573
+ method.name: field
1574
+ for method in methods
1575
+ if method_counts[method.name] == 1 and (field := _wrapper_accessor(method, narrowing_helpers)) in fields
1576
+ }
1577
+
1578
+
1579
+ def _wrapper_method_mutates(method: ast.FunctionDef) -> bool:
1580
+ return any(
1581
+ isinstance(child, ast.Attribute)
1582
+ and isinstance(child.ctx, ast.Store | ast.Del)
1583
+ and isinstance(child.value, ast.Name)
1584
+ and child.value.id == "self"
1585
+ for child in _own_scope(method)
1586
+ )
1587
+
1588
+
1589
+ def _wrapper_fields(constructor: ast.FunctionDef) -> dict[str, str]:
1590
+ if constructor.decorator_list or constructor.args.vararg or constructor.args.kwarg or constructor.args.kwonlyargs:
1591
+ return {}
1592
+ parameters = _parameter_positions(constructor)
1593
+ if parameters.get("self") != 0:
1594
+ return {}
1595
+ fields: dict[str, str] = {}
1596
+ for statement in constructor.body:
1597
+ if isinstance(statement, ast.Expr) and isinstance(statement.value, ast.Constant):
1598
+ continue
1599
+ if isinstance(statement, ast.Assign) and len(statement.targets) == 1:
1600
+ target, value = statement.targets[0], statement.value
1601
+ elif isinstance(statement, ast.AnnAssign) and statement.value is not None:
1602
+ target, value = statement.target, statement.value
1603
+ else:
1604
+ return {}
1605
+ if not (
1606
+ isinstance(target, ast.Attribute)
1607
+ and isinstance(target.value, ast.Name)
1608
+ and target.value.id == "self"
1609
+ and target.attr not in fields
1610
+ and isinstance(value, ast.Name)
1611
+ and value.id in parameters
1612
+ and parameters[value.id] > 0
1613
+ ):
1614
+ return {}
1615
+ fields[target.attr] = value.id
1616
+ return fields
1617
+
1618
+
1619
+ def _wrapper_accessor(method: ast.FunctionDef, narrowing_helpers: frozenset[str]) -> str | None:
1620
+ if (
1621
+ method.decorator_list
1622
+ or len(method.body) != 1
1623
+ or tuple(_parameter_positions(method)) != ("self",)
1624
+ or any((method.args.vararg, method.args.kwarg, method.args.kwonlyargs))
1625
+ ):
1626
+ return None
1627
+ statement = method.body[0]
1628
+ if not isinstance(statement, ast.Return):
1629
+ return None
1630
+ value = statement.value
1631
+ if (
1632
+ isinstance(value, ast.Call)
1633
+ and isinstance(value.func, ast.Name)
1634
+ and value.func.id in narrowing_helpers
1635
+ and len(value.args) == 1
1636
+ and not value.keywords
1637
+ ):
1638
+ value = value.args[0]
1639
+ return (
1640
+ value.attr
1641
+ if isinstance(value, ast.Attribute) and isinstance(value.value, ast.Name) and value.value.id == "self"
1642
+ else None
1643
+ )
1644
+
1645
+
1646
+ def _narrowing_helpers(tree: ast.Module, imports: ImportIndex, counts: Counter[str]) -> frozenset[str]:
1647
+ if not imports.builtin_is_unshadowed("isinstance") or not imports.builtin_is_unshadowed("dict"):
1648
+ return frozenset()
1649
+ names: set[str] = set()
1650
+ for function in tree.body:
1651
+ if not isinstance(function, ast.FunctionDef) or function.decorator_list or counts[function.name] != 1:
1652
+ continue
1653
+ parameters = tuple(_parameter_positions(function))
1654
+ if len(parameters) != 1 or len(function.body) != 1:
1655
+ continue
1656
+ statement = function.body[0]
1657
+ if not isinstance(statement, ast.Return) or not isinstance(statement.value, ast.IfExp):
1658
+ continue
1659
+ if any((function.args.kwonlyargs, function.args.vararg, function.args.kwarg)):
1660
+ continue
1661
+ if parameters[0] not in {"dict", "isinstance"} and _is_identity_dict_narrowing(statement.value, parameters[0]):
1662
+ names.add(function.name)
1663
+ return frozenset(names)
1664
+
1665
+
1666
+ def _is_identity_dict_narrowing(value: ast.IfExp, parameter: str) -> bool:
1667
+ match value:
1668
+ case ast.IfExp(
1669
+ body=ast.Name(id=returned),
1670
+ orelse=ast.Dict(keys=[]),
1671
+ test=ast.Call(
1672
+ func=ast.Name(id="isinstance"), args=[ast.Name(id=checked), ast.Name(id="dict")], keywords=[]
1673
+ ),
1674
+ ):
1675
+ return returned == parameter == checked
1676
+ case _:
1677
+ return False
1678
+
1679
+
1680
+ def _wrapper_mutated_classes(tree: ast.Module) -> frozenset[str]:
1681
+ return frozenset(
1682
+ node.value.id
1683
+ for node in walk_ast(tree)
1684
+ if isinstance(node, ast.Attribute)
1685
+ and isinstance(node.ctx, ast.Store | ast.Del)
1686
+ and isinstance(node.value, ast.Name)
1687
+ )
1688
+
1689
+
1690
+ def _wrapper_descriptor(statement: ast.stmt) -> bool:
1691
+ if isinstance(statement, ast.Assign):
1692
+ return not all(isinstance(target, ast.Name) and target.id == "__slots__" for target in statement.targets)
1693
+ return isinstance(statement, ast.AnnAssign | ast.AugAssign)