sarj-python-lint 1.8.3__tar.gz → 1.8.4__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 (164) hide show
  1. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/iac_source_coupled_test.py +10 -3
  4. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +191 -45
  5. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/.gitignore +0 -0
  6. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/LICENSE +0 -0
  7. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/README.md +0 -0
  8. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/_analysis_session.py +0 -0
  11. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/_file_context.py +0 -0
  12. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/_filesystem.py +0 -0
  13. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/_python_target.py +0 -0
  14. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  15. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/_secret_names.py +0 -0
  16. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/_version.py +0 -0
  17. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/benchmark.py +0 -0
  18. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/json_boundary.py +0 -0
  19. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/py.typed +0 -0
  20. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/ratchet.py +0 -0
  21. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rule_base.py +0 -0
  22. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/__init__.py +0 -0
  23. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  24. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  25. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  26. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_comments.py +0 -0
  27. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  28. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  29. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  30. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  31. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_first_party.py +0 -0
  32. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  33. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_imports.py +0 -0
  34. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_logging.py +0 -0
  35. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  36. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_paths.py +0 -0
  37. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_project_index.py +0 -0
  38. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  39. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_pytest.py +0 -0
  40. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_registry.py +0 -0
  41. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  42. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  43. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_sql.py +0 -0
  44. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  45. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  46. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  47. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  48. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  49. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  50. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  51. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
  52. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  53. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  54. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  55. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  56. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  57. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  58. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  59. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  60. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  61. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  62. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  63. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  64. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  65. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  66. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  67. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  68. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  69. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  70. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  71. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  72. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  73. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  74. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  75. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  76. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  77. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  78. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  79. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  80. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  81. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  82. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  83. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  84. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  85. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  86. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  87. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  88. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  89. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  90. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  91. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  92. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  93. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  94. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  95. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  96. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  97. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  98. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  99. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  100. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  101. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  102. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  103. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  104. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  105. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  106. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  107. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  108. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  109. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  110. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  111. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  112. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  113. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  114. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  115. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  116. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  117. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  118. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  119. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  120. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  121. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  122. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  123. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  124. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  125. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  126. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  127. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  128. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  129. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  130. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  131. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  132. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  133. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  134. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  135. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  136. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  137. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  138. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  139. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  140. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  141. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  142. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  143. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  144. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  145. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  146. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  147. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  148. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  149. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  150. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  151. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  152. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  153. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  154. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  155. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/stepdown.py +0 -0
  156. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  157. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  158. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  159. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  160. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  161. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  162. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  163. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  164. {sarj_python_lint-1.8.3 → sarj_python_lint-1.8.4}/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.8.3
3
+ Version: 1.8.4
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.8.3"
3
+ version = "1.8.4"
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" }]
@@ -15,14 +15,19 @@ from sarj_python_lint.rule_base import (
15
15
  Severity,
16
16
  )
17
17
  from sarj_python_lint.rules._paths import is_test_path
18
- from sarj_python_lint.rules.no_raw_source_text_test_oracle import FunctionAnalyzer, top_level_test_functions
18
+ from sarj_python_lint.rules.no_raw_source_text_test_oracle import (
19
+ IAC_JSON_SOURCE_SUFFIXES,
20
+ FunctionAnalyzer,
21
+ module_source_path_tuples,
22
+ top_level_test_functions,
23
+ )
19
24
 
20
25
 
21
26
  if TYPE_CHECKING:
22
27
  from sarj_python_lint._file_context import PythonFileContext
23
28
 
24
29
 
25
- IAC_SOURCE_SUFFIXES = (".hcl", ".tf", ".tf.json", ".tfvars", ".tftest.hcl", ".tftest.json")
30
+ IAC_SOURCE_SUFFIXES = (".hcl", ".tf", ".tfvars", ".tftest.hcl", *IAC_JSON_SOURCE_SUFFIXES)
26
31
 
27
32
 
28
33
  @final
@@ -42,7 +47,7 @@ class IacSourceCoupledTest(Rule):
42
47
  ),
43
48
  category=RuleCategory.TESTING,
44
49
  limitations=(
45
- "The rule follows local aliases, path collections, context-managed reads, and common normalization; interprocedural flows remain unreported.",
50
+ "The rule follows local aliases, path collections, context-managed reads, common normalization, and direct comprehension reads unpacked from stable module-level path tuples; interprocedural flows remain unreported.",
46
51
  "Files produced beneath recognized temporary-directory fixtures are generated outputs and remain unreported.",
47
52
  "The Python detector currently owns Terraform and HCL suffixes; YAML remains with the general source-coupled rule.",
48
53
  "Fixture, golden, and snapshot paths are treated as deliberate representation contracts; other packaging, formatter, and compatibility contracts require an exact suppression.",
@@ -93,11 +98,13 @@ class IacSourceCoupledTest(Rule):
93
98
  return []
94
99
  imports = context.module_imports
95
100
  source_lines = context.source_lines
101
+ module_tuples = module_source_path_tuples(tree, imports, IAC_SOURCE_SUFFIXES)
96
102
  assertions = [
97
103
  assertion
98
104
  for function, unittest_style in top_level_test_functions(tree, imports)
99
105
  for assertion in FunctionAnalyzer(
100
106
  IAC_SOURCE_SUFFIXES,
107
+ module_path_tuples=module_tuples,
101
108
  imports=imports,
102
109
  suppression_code=self.code,
103
110
  suppression_lines=source_lines,
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ from collections import Counter
4
5
  from dataclasses import dataclass
5
6
  from pathlib import PurePosixPath
7
+ from types import MappingProxyType
6
8
  from typing import TYPE_CHECKING, ClassVar, NamedTuple, final, override
7
9
 
8
10
  from sarj_python_lint.rule_base import (
@@ -23,6 +25,8 @@ from sarj_python_lint.rules._paths import is_test_path
23
25
 
24
26
 
25
27
  if TYPE_CHECKING:
28
+ from collections.abc import Iterator, Mapping
29
+
26
30
  from sarj_python_lint._file_context import PythonFileContext
27
31
 
28
32
 
@@ -35,6 +39,7 @@ GENERAL_SOURCE_SUFFIXES = (
35
39
  ".mjs",
36
40
  ".py",
37
41
  ".sh",
42
+ ".sh.tftpl",
38
43
  ".sql",
39
44
  ".toml",
40
45
  ".ts",
@@ -43,6 +48,11 @@ GENERAL_SOURCE_SUFFIXES = (
43
48
  ".yml",
44
49
  )
45
50
 
51
+ GENERAL_SOURCE_BASENAMES = frozenset({"Dockerfile"})
52
+ IAC_JSON_SOURCE_SUFFIXES = (".tf.json", ".tftest.json")
53
+
54
+ _EMPTY_PATH_TUPLES: Mapping[str, int] = MappingProxyType({})
55
+
46
56
  _REPRESENTATION_DIRS = frozenset({"fixture", "fixtures", "golden", "goldens", "snapshot", "snapshots"})
47
57
  _PYTEST = frozenset({"pytest"})
48
58
  _UNITTEST = frozenset({"unittest"})
@@ -59,12 +69,25 @@ class _ApiProvenance:
59
69
  shadowed: frozenset[str]
60
70
 
61
71
 
72
+ @dataclass(frozen=True, slots=True)
73
+ class _SourceKinds:
74
+ suffixes: tuple[str, ...]
75
+ basenames: frozenset[str] = frozenset()
76
+
77
+ def matches(self, value: str) -> bool:
78
+ lowered = value.lower()
79
+ for suffix in IAC_JSON_SOURCE_SUFFIXES:
80
+ if lowered.endswith(suffix):
81
+ return suffix in self.suffixes
82
+ return lowered.endswith(self.suffixes) or value.replace("\\", "/").rsplit("/", 1)[-1] in self.basenames
83
+
84
+
62
85
  @dataclass(frozen=True, slots=True)
63
86
  class _TextFlow:
64
87
  raw_names: set[str]
65
88
  path_names: set[str]
66
89
  ephemeral_path_names: set[str]
67
- source_suffixes: tuple[str, ...]
90
+ sources: _SourceKinds
68
91
  api: _ApiProvenance
69
92
 
70
93
 
@@ -123,7 +146,9 @@ class NoRawSourceTextTestOracle(Rule):
123
146
  autofix=AutofixPolicy.NONE,
124
147
  aliases=("source-coupled-test",),
125
148
  limitations=(
126
- "The rule follows local aliases, path aliases, context-managed reads, and common text normalization from source-like code or configuration suffixes; interprocedural flows remain unreported.",
149
+ "The rule recognizes source-like suffixes, shell templates ending in .sh.tftpl, and the exact Dockerfile basename; it follows local aliases, context-managed reads, and common normalization.",
150
+ "Direct generator/list-comprehension reads unpacked from stable module-level path tuples are followed; mutable collections, rebinding, filtered comprehensions, and interprocedural flows remain unreported.",
151
+ "Literal shebang startswith checks are representation contracts and remain unreported; other text compatibility sentinels require an exact suppression.",
127
152
  "Files produced beneath recognized temporary-directory fixtures are generated artifacts, not repository source, and remain unreported.",
128
153
  "Paths beneath fixture, golden, and snapshot directories are treated as deliberate representation contracts and remain unreported.",
129
154
  "When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason.",
@@ -171,11 +196,16 @@ class NoRawSourceTextTestOracle(Rule):
171
196
  return []
172
197
  imports = context.module_imports
173
198
  source_lines = context.source_lines
199
+ module_tuples = module_source_path_tuples(
200
+ tree, imports, GENERAL_SOURCE_SUFFIXES, source_basenames=GENERAL_SOURCE_BASENAMES
201
+ )
174
202
  assertions = [
175
203
  assertion
176
204
  for function, unittest_style in top_level_test_functions(tree, imports)
177
205
  for assertion in FunctionAnalyzer(
178
206
  GENERAL_SOURCE_SUFFIXES,
207
+ source_basenames=GENERAL_SOURCE_BASENAMES,
208
+ module_path_tuples=module_tuples,
179
209
  imports=imports,
180
210
  suppression_code=self.code,
181
211
  suppression_lines=source_lines,
@@ -361,7 +391,8 @@ def _node_is_suppressed(source_lines: list[str], node: ast.AST, code: str) -> bo
361
391
 
362
392
  class FunctionAnalyzer(ast.NodeVisitor):
363
393
  _imports: ImportIndex | None
364
- _source_suffixes: tuple[str, ...]
394
+ _module_path_tuples: Mapping[str, int]
395
+ _sources: _SourceKinds
365
396
  _suppression_code: str | None
366
397
  _suppression_lines: list[str] | None
367
398
  _unittest_style: bool
@@ -370,12 +401,15 @@ class FunctionAnalyzer(ast.NodeVisitor):
370
401
  self,
371
402
  source_suffixes: tuple[str, ...],
372
403
  *,
404
+ source_basenames: frozenset[str] = frozenset(),
405
+ module_path_tuples: Mapping[str, int] = _EMPTY_PATH_TUPLES,
373
406
  imports: ImportIndex | None = None,
374
407
  suppression_code: str | None = None,
375
408
  suppression_lines: list[str] | None = None,
376
409
  unittest_style: bool = False,
377
410
  ) -> None:
378
- self._source_suffixes = source_suffixes
411
+ self._sources = _SourceKinds(source_suffixes, source_basenames)
412
+ self._module_path_tuples = module_path_tuples
379
413
  self._unittest_style = unittest_style
380
414
  self._imports = imports
381
415
  self._suppression_code = suppression_code
@@ -439,7 +473,7 @@ class FunctionAnalyzer(ast.NodeVisitor):
439
473
  if item.optional_vars is not None:
440
474
  self._clear_target(item.optional_vars)
441
475
  if isinstance(item.optional_vars, ast.Name) and _is_source_open(
442
- item.context_expr, self._paths, self._ephemeral_paths, self._source_suffixes, self._provenance
476
+ item.context_expr, self._paths, self._ephemeral_paths, self._sources, self._provenance
443
477
  ):
444
478
  self._raw.add(item.optional_vars.id)
445
479
  self._raw_origins[item.optional_vars.id] = {item.context_expr.lineno}
@@ -569,19 +603,56 @@ class FunctionAnalyzer(ast.NodeVisitor):
569
603
 
570
604
  def _record_target(self, target: ast.expr, value: ast.expr) -> None:
571
605
  self._clear_target(target)
606
+ if isinstance(target, ast.Tuple | ast.List):
607
+ self._record_unpacked_read(target, value)
608
+ return
572
609
  if not isinstance(target, ast.Name):
573
610
  return
574
611
  raw_origins = _expression_origins(value, self._raw_origins, self._flow)
575
612
  if _ephemeral_path_expression(value, self._ephemeral_paths):
576
613
  self._ephemeral_paths.add(target.id)
577
- if _source_path_collection(value, self._paths, self._source_suffixes, self._provenance):
614
+ if _source_path_collection(value, self._paths, self._sources, self._provenance):
578
615
  self._collections.add(target.id)
579
- if _source_path_expression(value, self._paths, self._source_suffixes, self._provenance):
616
+ if _source_path_expression(value, self._paths, self._sources, self._provenance):
580
617
  self._paths.add(target.id)
581
618
  if _raw_text_expression(value, self._flow):
582
619
  self._raw.add(target.id)
583
620
  self._raw_origins[target.id] = raw_origins or {value.lineno}
584
621
 
622
+ def _record_unpacked_read(self, target: ast.Tuple | ast.List, value: ast.expr) -> None:
623
+ if not isinstance(value, ast.GeneratorExp | ast.ListComp) or len(value.generators) != 1:
624
+ return
625
+ generator = value.generators[0]
626
+ if (
627
+ generator.is_async
628
+ or generator.ifs
629
+ or not isinstance(generator.target, ast.Name)
630
+ or not isinstance(generator.iter, ast.Name)
631
+ ):
632
+ return
633
+ if (
634
+ generator.iter.id in self._provenance.shadowed
635
+ or self._module_path_tuples.get(generator.iter.id) != len(target.elts)
636
+ or not all(isinstance(element, ast.Name) for element in target.elts)
637
+ or any(isinstance(child, ast.NamedExpr) for child in walk_ast(value.elt))
638
+ ):
639
+ return
640
+ flow = _TextFlow(
641
+ self._raw - {generator.target.id},
642
+ self._paths | {generator.target.id},
643
+ self._ephemeral_paths - {generator.target.id},
644
+ self._sources,
645
+ _ApiProvenance(self._provenance.imports, self._provenance.shadowed | {generator.target.id}),
646
+ )
647
+ if not _raw_text_expression(value.elt, flow):
648
+ return
649
+ raw_origins = {name: origins for name, origins in self._raw_origins.items() if name != generator.target.id}
650
+ origins = _expression_origins(value.elt, raw_origins, flow) or {value.elt.lineno}
651
+ for element in target.elts:
652
+ if isinstance(element, ast.Name):
653
+ self._raw.add(element.id)
654
+ self._raw_origins[element.id] = set(origins)
655
+
585
656
  def _clear_target(self, target: ast.expr) -> None:
586
657
  for child in walk_ast(target):
587
658
  if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store):
@@ -630,7 +701,7 @@ class FunctionAnalyzer(ast.NodeVisitor):
630
701
  self._raw,
631
702
  self._paths,
632
703
  self._ephemeral_paths,
633
- self._source_suffixes,
704
+ self._sources,
634
705
  self._provenance,
635
706
  )
636
707
 
@@ -646,30 +717,90 @@ def _intersect_flow_states(left: _FlowState, right: _FlowState) -> _FlowState:
646
717
  )
647
718
 
648
719
 
649
- def _function_bound_names(function: ast.FunctionDef | ast.AsyncFunctionDef) -> frozenset[str]:
650
- args = function.args
651
- names = {
652
- argument.arg
653
- for argument in (*args.posonlyargs, *args.args, *args.kwonlyargs, args.vararg, args.kwarg)
654
- if argument is not None
655
- }
656
- pending: list[ast.AST] = list(function.body)
720
+ def module_source_path_tuples(
721
+ tree: ast.Module,
722
+ imports: ImportIndex,
723
+ source_suffixes: tuple[str, ...],
724
+ *,
725
+ source_basenames: frozenset[str] = frozenset(),
726
+ ) -> dict[str, int]:
727
+ sources = _SourceKinds(source_suffixes, source_basenames)
728
+ api = _ApiProvenance(imports, frozenset())
729
+ tuples: dict[str, int] = {}
730
+ for statement in tree.body:
731
+ match statement:
732
+ case ast.Assign(targets=[target], value=value) | ast.AnnAssign(target=target, value=value):
733
+ pass
734
+ case _:
735
+ continue
736
+ if (
737
+ isinstance(target, ast.Name)
738
+ and isinstance(value, ast.Tuple)
739
+ and _source_path_collection(value, set(), sources, api)
740
+ ):
741
+ tuples[target.id] = len(value.elts)
742
+ if not tuples:
743
+ return {}
744
+ bindings = Counter(_enclosing_scope_bindings(tree.body))
745
+ if bindings["*"]:
746
+ return {}
747
+ global_names = {name for node in walk_ast(tree) if isinstance(node, ast.Global) for name in node.names}
748
+ return {name: size for name, size in tuples.items() if bindings[name] == 1 and name not in global_names}
749
+
750
+
751
+ def _enclosing_scope_bindings(nodes: list[ast.stmt]) -> Iterator[str]:
752
+ pending: list[ast.AST] = list(nodes)
657
753
  while pending:
658
754
  current = pending.pop()
659
755
  match current:
660
756
  case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
661
- names.add(current.name)
757
+ yield current.name
758
+ pending.extend(_definition_enclosing_expressions(current))
662
759
  continue
663
- case ast.Lambda() | ast.ListComp() | ast.SetComp() | ast.DictComp() | ast.GeneratorExp():
760
+ case ast.Lambda():
761
+ pending.extend(_definition_enclosing_expressions(current))
762
+ continue
763
+ case ast.ListComp() | ast.SetComp() | ast.DictComp() | ast.GeneratorExp():
764
+ pending.extend(child.target for child in walk_ast(current) if isinstance(child, ast.NamedExpr))
664
765
  continue
665
766
  case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
666
- names.update(alias.asname or alias.name.partition(".")[0] for alias in aliases)
767
+ yield from (alias.asname or alias.name.partition(".")[0] for alias in aliases)
667
768
  continue
668
- case ast.Name(id=name, ctx=(ast.Store() | ast.Del())):
669
- names.add(name)
769
+ case (
770
+ ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
771
+ | ast.ExceptHandler(name=str(name))
772
+ | ast.MatchAs(name=str(name))
773
+ | ast.MatchStar(name=str(name))
774
+ | ast.MatchMapping(rest=str(name))
775
+ ):
776
+ yield name
670
777
  case _:
671
778
  pass
672
779
  pending.extend(ast.iter_child_nodes(current))
780
+
781
+
782
+ def _definition_enclosing_expressions(
783
+ node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda,
784
+ ) -> list[ast.AST]:
785
+ if isinstance(node, ast.ClassDef):
786
+ return [*node.decorator_list, *node.bases, *node.keywords]
787
+ defaults: list[ast.AST] = [
788
+ *node.args.defaults,
789
+ *(default for default in node.args.kw_defaults if default is not None),
790
+ ]
791
+ if isinstance(node, ast.Lambda):
792
+ return defaults
793
+ return [*node.decorator_list, *defaults]
794
+
795
+
796
+ def _function_bound_names(function: ast.FunctionDef | ast.AsyncFunctionDef) -> frozenset[str]:
797
+ args = function.args
798
+ names = {
799
+ argument.arg
800
+ for argument in (*args.posonlyargs, *args.args, *args.kwonlyargs, args.vararg, args.kwarg)
801
+ if argument is not None
802
+ }
803
+ names.update(_enclosing_scope_bindings(function.body))
673
804
  return frozenset(names)
674
805
 
675
806
 
@@ -695,7 +826,7 @@ def _is_source_open(
695
826
  node: ast.expr,
696
827
  path_names: set[str],
697
828
  ephemeral_path_names: set[str],
698
- source_suffixes: tuple[str, ...],
829
+ sources: _SourceKinds,
699
830
  api: _ApiProvenance,
700
831
  ) -> bool:
701
832
  if not isinstance(node, ast.Call):
@@ -715,7 +846,7 @@ def _is_source_open(
715
846
  path = node.func.value
716
847
  else:
717
848
  return False
718
- return _source_path_expression(path, path_names, source_suffixes, api) and not _ephemeral_path_expression(
849
+ return _source_path_expression(path, path_names, sources, api) and not _ephemeral_path_expression(
719
850
  path, ephemeral_path_names
720
851
  )
721
852
 
@@ -723,7 +854,7 @@ def _is_source_open(
723
854
  def _source_path_expression(
724
855
  node: ast.AST,
725
856
  path_names: set[str],
726
- source_suffixes: tuple[str, ...],
857
+ sources: _SourceKinds,
727
858
  api: _ApiProvenance,
728
859
  ) -> bool:
729
860
  if _representation_fixture_path(node):
@@ -732,31 +863,31 @@ def _source_path_expression(
732
863
  case ast.Name(id=name):
733
864
  return name in path_names
734
865
  case ast.Constant(value=str(value)):
735
- return value.lower().endswith(source_suffixes)
866
+ return sources.matches(value)
736
867
  case ast.Attribute(attr="__file__"):
737
- return ".py" in source_suffixes
868
+ return ".py" in sources.suffixes
738
869
  case ast.Call(func=function, args=[path, *_]) if _api_resolves(
739
870
  function, api, sources=frozenset({"pathlib"}), symbol="Path"
740
871
  ):
741
- return _source_path_expression(path, path_names, source_suffixes, api)
872
+ return _source_path_expression(path, path_names, sources, api)
742
873
  case ast.Call(func=function) if _api_resolves(function, api, sources=frozenset({"inspect"}), symbol="getfile"):
743
- return ".py" in source_suffixes
874
+ return ".py" in sources.suffixes
744
875
  case ast.JoinedStr(values=values):
745
- return any(_source_path_expression(value, path_names, source_suffixes, api) for value in values)
876
+ return any(_source_path_expression(value, path_names, sources, api) for value in values)
746
877
  case ast.Call(
747
878
  func=ast.Attribute(value=receiver, attr="with_suffix" | "with_name"),
748
879
  args=[ast.Constant(value=str(value)), *_],
749
880
  ):
750
- return value.lower().endswith(source_suffixes) and (
751
- _source_path_expression(receiver, path_names, source_suffixes, api)
881
+ return sources.matches(value) and (
882
+ _source_path_expression(receiver, path_names, sources, api)
752
883
  or (
753
884
  isinstance(receiver, ast.Call)
754
885
  and _api_resolves(receiver.func, api, sources=frozenset({"pathlib"}), symbol="Path")
755
886
  )
756
887
  )
757
888
  case ast.BinOp(left=left, right=right):
758
- return _source_path_expression(left, path_names, source_suffixes, api) or _source_path_expression(
759
- right, path_names, source_suffixes, api
889
+ return _source_path_expression(left, path_names, sources, api) or _source_path_expression(
890
+ right, path_names, sources, api
760
891
  )
761
892
  case ast.Subscript(value=ast.Name(id=name)):
762
893
  return name in path_names
@@ -781,13 +912,13 @@ def _ephemeral_path_expression(node: ast.AST, ephemeral_path_names: set[str]) ->
781
912
  def _source_path_collection(
782
913
  node: ast.AST,
783
914
  path_names: set[str],
784
- source_suffixes: tuple[str, ...],
915
+ sources: _SourceKinds,
785
916
  api: _ApiProvenance,
786
917
  ) -> bool:
787
918
  return (
788
919
  isinstance(node, (ast.List, ast.Set, ast.Tuple))
789
920
  and bool(node.elts)
790
- and all(_source_path_expression(element, path_names, source_suffixes, api) for element in node.elts)
921
+ and all(_source_path_expression(element, path_names, sources, api) for element in node.elts)
791
922
  )
792
923
 
793
924
 
@@ -795,19 +926,17 @@ def _raw_source_read(
795
926
  node: ast.expr,
796
927
  path_names: set[str],
797
928
  ephemeral_path_names: set[str],
798
- source_suffixes: tuple[str, ...],
929
+ sources: _SourceKinds,
799
930
  api: _ApiProvenance,
800
931
  ) -> bool:
801
932
  if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
802
933
  return False
803
934
  supported_reads = {"read_bytes", "read_text"}
804
935
  if node.func.attr in supported_reads:
805
- return _source_path_expression(
806
- node.func.value, path_names, source_suffixes, api
807
- ) and not _ephemeral_path_expression(node.func.value, ephemeral_path_names)
808
- return node.func.attr == "read" and _is_source_open(
809
- node.func.value, path_names, ephemeral_path_names, source_suffixes, api
810
- )
936
+ return _source_path_expression(node.func.value, path_names, sources, api) and not _ephemeral_path_expression(
937
+ node.func.value, ephemeral_path_names
938
+ )
939
+ return node.func.attr == "read" and _is_source_open(node.func.value, path_names, ephemeral_path_names, sources, api)
811
940
 
812
941
 
813
942
  def _raw_text_expression(
@@ -816,7 +945,7 @@ def _raw_text_expression(
816
945
  ) -> bool:
817
946
  if isinstance(node, ast.Name):
818
947
  return node.id in flow.raw_names
819
- if _raw_source_read(node, flow.path_names, flow.ephemeral_path_names, flow.source_suffixes, flow.api):
948
+ if _raw_source_read(node, flow.path_names, flow.ephemeral_path_names, flow.sources, flow.api):
820
949
  return True
821
950
  if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
822
951
  handle_reads = {"read", "readlines"}
@@ -837,13 +966,30 @@ def _raw_text_oracle(
837
966
  if _is_raw_text_iteration_call(node, flow):
838
967
  return True
839
968
  if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
840
- if node.func.attr in _TEXT_ASSERTIONS and _raw_text_expression(node.func.value, flow):
969
+ if (
970
+ node.func.attr in _TEXT_ASSERTIONS
971
+ and not _is_shebang_contract(node)
972
+ and _raw_text_expression(node.func.value, flow)
973
+ ):
841
974
  return True
842
975
  if node.func.attr in _REGEX_ASSERTIONS and any(_raw_text_expression(argument, flow) for argument in node.args):
843
976
  return True
844
977
  return any(_raw_text_oracle(child, flow) for child in ast.iter_child_nodes(node) if isinstance(child, ast.expr))
845
978
 
846
979
 
980
+ def _is_shebang_contract(node: ast.Call) -> bool:
981
+ return (
982
+ isinstance(node.func, ast.Attribute)
983
+ and node.func.attr == "startswith"
984
+ and not node.keywords
985
+ and len(node.args) == 1
986
+ and isinstance(node.args[0], ast.Constant)
987
+ and isinstance(node.args[0].value, str)
988
+ and node.args[0].value.startswith("#!")
989
+ and len(node.args[0].value.splitlines()) == 1
990
+ )
991
+
992
+
847
993
  def _raw_text_measurement(
848
994
  node: ast.expr,
849
995
  flow: _TextFlow,
@@ -910,7 +1056,7 @@ def _expression_origins(
910
1056
  if isinstance(child, ast.Name):
911
1057
  origins.update(raw_origins.get(child.id, set()))
912
1058
  elif isinstance(child, ast.Call) and _raw_source_read(
913
- child, flow.path_names, flow.ephemeral_path_names, flow.source_suffixes, flow.api
1059
+ child, flow.path_names, flow.ephemeral_path_names, flow.sources, flow.api
914
1060
  ):
915
1061
  origins.add(child.lineno)
916
1062
  return origins