sarj-python-lint 1.2.6__tar.gz → 1.2.7__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.6 → sarj_python_lint-1.2.7}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_test_composition.py +15 -4
  4. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +170 -28
  5. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/mock_without_spec.py +1 -1
  6. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/repeated_test_composition.py +1 -1
  7. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/.gitignore +0 -0
  8. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/LICENSE +0 -0
  9. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/README.md +0 -0
  10. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/__init__.py +0 -0
  11. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/__main__.py +0 -0
  12. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/_analysis_session.py +0 -0
  13. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/_file_context.py +0 -0
  14. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/_filesystem.py +0 -0
  15. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/_python_target.py +0 -0
  16. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  17. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/_secret_names.py +0 -0
  18. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/_version.py +0 -0
  19. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/benchmark.py +0 -0
  20. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/json_boundary.py +0 -0
  21. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/py.typed +0 -0
  22. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/ratchet.py +0 -0
  23. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rule_base.py +0 -0
  24. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/__init__.py +0 -0
  25. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  26. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  27. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  28. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_comments.py +0 -0
  29. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  30. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  31. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_first_party.py +0 -0
  32. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  33. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_imports.py +0 -0
  34. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_logging.py +0 -0
  35. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  36. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_paths.py +0 -0
  37. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_project_index.py +0 -0
  38. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  39. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_pytest.py +0 -0
  40. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_registry.py +0 -0
  41. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_sql.py +0 -0
  42. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  43. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  44. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  45. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  46. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  47. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  48. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  49. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  50. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  51. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  52. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  53. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  54. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  55. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  56. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  57. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  58. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  59. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  60. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  61. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  62. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  63. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  64. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  65. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  66. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  67. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  68. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  69. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  70. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  71. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  72. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  73. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  74. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  75. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  76. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  77. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  78. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  79. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  80. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  81. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  82. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  83. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  84. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  85. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  86. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  87. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  88. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  89. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  90. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  91. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  92. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  93. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  94. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  95. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  96. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  97. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  98. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  99. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  100. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  101. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  102. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  103. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  104. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  105. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  106. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  107. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  108. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  109. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  110. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  111. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  112. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  113. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  114. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  115. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  116. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  117. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  118. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  119. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  120. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  121. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  122. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  123. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  124. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  125. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  126. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  127. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  128. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  129. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  130. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  131. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  132. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  133. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  134. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  135. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  136. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  137. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  138. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  139. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  140. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  141. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  142. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/stepdown.py +0 -0
  143. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  144. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  145. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  146. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  147. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  148. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  149. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  150. {sarj_python_lint-1.2.6 → sarj_python_lint-1.2.7}/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.6
3
+ Version: 1.2.7
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.6"
3
+ version = "1.2.7"
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" }]
@@ -189,16 +189,27 @@ def local_names(function: Function) -> set[str]:
189
189
  return names
190
190
 
191
191
 
192
- def changed_before(function: Function, call: ast.Call) -> set[str]:
192
+ def changed_before(function: Function, call: ast.Call, aliases: dict[str, ast.expr]) -> set[str]:
193
193
  changed: set[str] = set()
194
194
  for node in direct_nodes(function):
195
- if not isinstance(node, (ast.Call, ast.Attribute)) or node.lineno >= call.lineno:
195
+ if not isinstance(node, (ast.Call, ast.Attribute, ast.Subscript)) or (node.lineno, node.col_offset) >= (
196
+ call.lineno,
197
+ call.col_offset,
198
+ ):
196
199
  continue
197
200
  target = node.func.value if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) else None
198
- if isinstance(node, ast.Attribute) and isinstance(node.ctx, (ast.Store, ast.Del)):
201
+ if isinstance(node, (ast.Attribute, ast.Subscript)) and isinstance(node.ctx, (ast.Store, ast.Del)):
199
202
  target = node.value
200
- if isinstance(target, ast.Name):
203
+ while isinstance(target, (ast.Attribute, ast.Subscript)):
204
+ target = target.value
205
+ seen: set[str] = set()
206
+ while isinstance(target, ast.Name) and target.id not in seen:
207
+ seen.add(target.id)
201
208
  changed.add(target.id)
209
+ value = aliases.get(target.id)
210
+ if value is None or (value.lineno, value.col_offset) >= (node.lineno, node.col_offset):
211
+ break
212
+ target = value
202
213
  return changed
203
214
 
204
215
 
@@ -41,6 +41,7 @@ _AWAIT_ASSERTIONS: Final = frozenset(
41
41
  }
42
42
  )
43
43
  _AWAIT_STATE: Final = frozenset({"await_count", "await_args", "await_args_list"})
44
+ _CALL_STATE: Final = frozenset({"called", "call_count"})
44
45
  _CONSTRUCTOR_KEYWORDS: Final = frozenset({"return_value", "side_effect", "name", "wraps", "unsafe", "spec", "spec_set"})
45
46
  _MUTATORS: Final = frozenset({"reset_mock", "configure_mock", "attach_mock", "mock_add_spec"})
46
47
  _IMMEDIATE_CHILD_PARTS: Final = 2
@@ -86,10 +87,25 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
86
87
  "Only direct statements in collected test functions and methods are inspected; fixtures, helpers, nested blocks, and generated files are excluded.",
87
88
  "Requires a fresh local unittest.mock.AsyncMock constructor with unshadowed imports; aliases, reassignment, reset, and ambiguous configuration are excluded.",
88
89
  "Only the mock itself and unchanged immediate children of unspecced, unwrapped mocks are inferred; spec children, return values, and deeper chains are excluded.",
89
- "Any direct positive await assertion or explicit await-state assertion on the same mock is accepted; argument equality and assertion strength are not compared.",
90
+ "Positive call-count and called-state assertions are covered as well as call-assertion methods. Await-state assertions must prove at least one await; argument equality and exact call/await counts are not compared.",
91
+ "Boolean conjunctions combine evidence; disjunctions require evidence in every branch. Dynamic counts and call lists are not inferred.",
90
92
  "Custom assertion helpers and intentional scheduling contracts may require a reasoned local suppression.",
91
93
  ),
92
94
  examples=(
95
+ RuleExample(
96
+ example_id="numeric-call-with-vacuous-await-check",
97
+ title="A nonnegative await count does not prove execution",
98
+ outcome=ExampleOutcome.MATCH,
99
+ files=(
100
+ ExampleFile.python(
101
+ "tests/test_delivery.py",
102
+ "from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n assert send.call_count == 2\n assert send.await_count >= 0\n",
103
+ ),
104
+ ),
105
+ focus_path=PurePosixPath("tests/test_delivery.py"),
106
+ expected_count=1,
107
+ public=False,
108
+ ),
93
109
  RuleExample(
94
110
  example_id="call-only-async-mock",
95
111
  title="A call assertion does not prove awaiting",
@@ -129,7 +145,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
129
145
  if (
130
146
  path.suffix != ".py"
131
147
  or "AsyncMock" not in source
132
- or "assert_called" not in source
148
+ or not any(token in source for token in ("assert_called", "call_count", ".called"))
133
149
  or not (path.stem.startswith("test_") or path.stem.endswith("_test"))
134
150
  or context.generated
135
151
  ):
@@ -186,30 +202,46 @@ def _test_imports(module: ImportIndex, test: _TestFunction) -> ImportIndex:
186
202
  return ImportIndex(MappingProxyType(bindings), module.shadowed_names | local.shadowed_names)
187
203
 
188
204
 
189
- def _uncovered_calls(imports: ImportIndex, test: _TestFunction, writes: set[_Reference]) -> Iterator[ast.Call]:
205
+ def _uncovered_calls(
206
+ imports: ImportIndex, test: _TestFunction, writes: set[_Reference]
207
+ ) -> Iterator[ast.Call | ast.Attribute]:
190
208
  nodes = tuple(walk_ast(test))
191
209
  bindings = _mock_bindings(imports, test.body, writes)
192
210
  covered = _await_oracles(test.body)
193
211
  reported: set[_Reference] = set()
194
212
  for statement in test.body:
195
- match statement:
196
- case ast.Expr(value=ast.Call(func=ast.Attribute(value=receiver, attr=method)) as call) if (
197
- method in _CALL_ASSERTIONS
213
+ for reference, oracle in _call_oracles(statement):
214
+ if not reference or reference in covered or reference in reported:
215
+ continue
216
+ binding = bindings.get(reference[0])
217
+ if (
218
+ binding is None
219
+ or not _is_async_reference(reference, binding[1])
220
+ or any(_invalidates_reference(node, reference, binding[0]) for node in nodes)
198
221
  ):
199
- reference = _reference(receiver)
200
- if not reference or reference in covered or reference in reported:
201
- continue
202
- binding = bindings.get(reference[0])
203
- if (
204
- binding is None
205
- or not _is_async_reference(reference, binding[1])
206
- or any(_invalidates_reference(node, reference, binding[0]) for node in nodes)
207
- ):
208
- continue
209
- reported.add(reference)
210
- yield call
211
- case _:
212
- pass
222
+ continue
223
+ reported.add(reference)
224
+ yield oracle
225
+
226
+
227
+ def _call_oracles(statement: ast.stmt) -> Iterator[tuple[_Reference, ast.Call | ast.Attribute]]:
228
+ match statement:
229
+ case ast.Expr(value=ast.Call(func=ast.Attribute(value=receiver, attr=method)) as call) if (
230
+ method in _CALL_ASSERTIONS
231
+ ):
232
+ yield _reference(receiver), call
233
+ case ast.Assert(test=condition):
234
+ for reference in sorted(_positive_states(condition, _CALL_STATE)):
235
+ oracle = next(
236
+ node
237
+ for node in walk_ast(condition)
238
+ if isinstance(node, ast.Attribute)
239
+ and node.attr in _CALL_STATE
240
+ and _reference(node.value) == reference
241
+ )
242
+ yield reference, oracle
243
+ case _:
244
+ pass
213
245
 
214
246
 
215
247
  def _mock_bindings(
@@ -269,21 +301,127 @@ def _await_oracles(statements: list[ast.stmt]) -> set[_Reference]:
269
301
  covered: set[_Reference] = set()
270
302
  for statement in statements:
271
303
  match statement:
272
- case ast.Expr(value=ast.Call(func=ast.Attribute(value=receiver, attr=method))) if (
304
+ case ast.Expr(value=ast.Call(func=ast.Attribute(value=receiver, attr=method)) as call) if (
273
305
  method in _AWAIT_ASSERTIONS
274
306
  ):
275
- covered.add(_reference(receiver))
307
+ if method != "assert_has_awaits" or _has_nonempty_calls_argument(call):
308
+ covered.add(_reference(receiver))
276
309
  case ast.Assert(test=condition):
277
- covered.update(
278
- _reference(node.value)
279
- for node in walk_ast(condition)
280
- if isinstance(node, ast.Attribute) and node.attr in _AWAIT_STATE
281
- )
310
+ covered.update(_positive_states(condition, _AWAIT_STATE))
282
311
  case _:
283
312
  pass
284
313
  return covered
285
314
 
286
315
 
316
+ def _has_nonempty_calls_argument(call: ast.Call) -> bool:
317
+ if call.args:
318
+ return _nonempty_sequence(call.args[0])
319
+ return any(item.arg == "calls" and _nonempty_sequence(item.value) for item in call.keywords)
320
+
321
+
322
+ def _positive_states(condition: ast.expr, states: frozenset[str]) -> set[_Reference]:
323
+ if isinstance(condition, ast.UnaryOp) and isinstance(condition.op, ast.Not):
324
+ operand = condition.operand
325
+ if isinstance(operand, ast.Compare) and len(operand.ops) == 1:
326
+ inverse = _negated_operator(operand.ops[0])
327
+ if inverse is not None:
328
+ return _comparison_states(operand.left, inverse, operand.comparators[0], states) | _comparison_states(
329
+ operand.comparators[0], _reversed_operator(inverse), operand.left, states
330
+ )
331
+ return set()
332
+ if isinstance(condition, ast.BoolOp):
333
+ branches = [_positive_states(value, states) for value in condition.values]
334
+ return (
335
+ branches[0].union(*branches[1:])
336
+ if isinstance(condition.op, ast.And)
337
+ else branches[0].intersection(*branches[1:])
338
+ )
339
+ if isinstance(condition, ast.Attribute) and condition.attr in states:
340
+ return {_reference(condition.value)}
341
+ if isinstance(condition, ast.Compare):
342
+ result: set[_Reference] = set()
343
+ for left, operator, right in zip(
344
+ (condition.left, *condition.comparators[:-1]), condition.ops, condition.comparators, strict=True
345
+ ):
346
+ result.update(_comparison_states(left, operator, right, states))
347
+ result.update(_comparison_states(right, _reversed_operator(operator), left, states))
348
+ return result
349
+ return set()
350
+
351
+
352
+ def _negated_operator(operator: ast.cmpop) -> ast.cmpop | None:
353
+ inverse: dict[type[ast.cmpop], type[ast.cmpop]] = {
354
+ ast.Eq: ast.NotEq,
355
+ ast.NotEq: ast.Eq,
356
+ ast.Is: ast.IsNot,
357
+ ast.IsNot: ast.Is,
358
+ ast.Lt: ast.GtE,
359
+ ast.LtE: ast.Gt,
360
+ ast.Gt: ast.LtE,
361
+ ast.GtE: ast.Lt,
362
+ }
363
+ factory = inverse.get(type(operator))
364
+ return factory() if factory is not None else None
365
+
366
+
367
+ def _reversed_operator(operator: ast.cmpop) -> ast.cmpop:
368
+ match operator:
369
+ case ast.Lt():
370
+ return ast.Gt()
371
+ case ast.LtE():
372
+ return ast.GtE()
373
+ case ast.Gt():
374
+ return ast.Lt()
375
+ case ast.GtE():
376
+ return ast.LtE()
377
+ case _:
378
+ return operator
379
+
380
+
381
+ def _comparison_states(left: ast.expr, operator: ast.cmpop, right: ast.expr, states: frozenset[str]) -> set[_Reference]:
382
+ if not isinstance(left, ast.Attribute):
383
+ return set()
384
+ if (
385
+ states == _AWAIT_STATE
386
+ and isinstance(left.value, ast.Attribute)
387
+ and left.value.attr == "await_args"
388
+ and left.attr in {"args", "kwargs"}
389
+ ):
390
+ return {_reference(left.value.value)}
391
+ if left.attr not in states:
392
+ return set()
393
+ positive = False
394
+ if left.attr in {"call_count", "await_count"} and isinstance(right, ast.Constant) and type(right.value) is int:
395
+ positive = _proves_positive_count(operator, right.value)
396
+ elif left.attr == "called" and isinstance(right, ast.Constant):
397
+ positive = (isinstance(operator, (ast.Eq, ast.Is)) and right.value is True) or (
398
+ isinstance(operator, (ast.NotEq, ast.IsNot)) and right.value is False
399
+ )
400
+ elif left.attr == "await_args" and isinstance(right, ast.Constant) and right.value is None:
401
+ positive = isinstance(operator, (ast.NotEq, ast.IsNot))
402
+ elif left.attr == "await_args_list":
403
+ positive = (isinstance(operator, ast.Eq) and _nonempty_sequence(right)) or (
404
+ isinstance(operator, ast.NotEq) and isinstance(right, ast.List) and not right.elts
405
+ )
406
+ return {_reference(left.value)} if positive else set()
407
+
408
+
409
+ def _proves_positive_count(operator: ast.cmpop, value: int) -> bool:
410
+ match operator:
411
+ case ast.Eq() | ast.GtE():
412
+ return value > 0
413
+ case ast.NotEq():
414
+ return value == 0
415
+ case ast.Gt():
416
+ return value >= 0
417
+ case _:
418
+ return False
419
+
420
+
421
+ def _nonempty_sequence(node: ast.expr) -> bool:
422
+ return isinstance(node, (ast.List, ast.Tuple)) and any(not isinstance(item, ast.Starred) for item in node.elts)
423
+
424
+
287
425
  def _invalidates_reference(node: ast.AST, reference: _Reference, binding: ast.Name) -> bool:
288
426
  match node:
289
427
  case ast.Name(id=name, ctx=ast.Store() | ast.Del()):
@@ -295,7 +433,11 @@ def _invalidates_reference(node: ast.AST, reference: _Reference, binding: ast.Na
295
433
  case ast.Attribute(ctx=ast.Store() | ast.Del()):
296
434
  changed = _reference(node)
297
435
  return bool(changed) and (
298
- reference[: len(changed)] == changed or (changed[:-1] == reference and changed[-1].startswith("assert"))
436
+ reference[: len(changed)] == changed
437
+ or (
438
+ changed[:-1] == reference
439
+ and (changed[-1].startswith("assert") or changed[-1] in _CALL_STATE | _AWAIT_STATE)
440
+ )
299
441
  )
300
442
  case ast.Call(func=ast.Attribute(value=receiver, attr=method)) if method in _MUTATORS:
301
443
  changed = _reference(receiver)
@@ -789,7 +789,7 @@ def _keyword_supplies_contract(keyword: ast.keyword, names: _MockNames, symbol:
789
789
  return False
790
790
  return names.resolve(keyword.value) not in _UNSPECCED_FACTORIES
791
791
  return keyword.arg in _PATCH_CONTRACT_KEYWORDS and not (
792
- isinstance(keyword.value, ast.Constant) and keyword.value.value is None
792
+ isinstance(keyword.value, ast.Constant) and (keyword.value.value is None or keyword.value.value is False)
793
793
  )
794
794
 
795
795
 
@@ -168,7 +168,7 @@ def _candidate(
168
168
  identity = f"{spec.symbol.module}.{spec.symbol.name}"
169
169
  if not _unconditional(context, node, function) or not _subject_called(context, function, node):
170
170
  return identity, None
171
- blocked = (available.blocked - aliases.keys()) | changed_before(function, node)
171
+ blocked = (available.blocked - aliases.keys()) | changed_before(function, node, aliases)
172
172
  scope = WiringScope(available.parameters, aliases, frozenset(blocked))
173
173
  arguments = wiring(context, project, node, spec, scope)
174
174
  return identity, f"{identity}:{arguments!r}" if arguments is not None else None